mirror of
https://github.com/Yeachan-Heo/oh-my-claudecode.git
synced 2026-09-03 06:25:33 +08:00
feat(prompt-ssot): structured prompt SSOT composer and projection digests (#3704)
Additive-only prompt SSOT: structured sections (policy / task-contract / safety / role-delta / workflow-delta / provider-delta / model-tier-delta / output-contract), manifest, deterministic renderer, digest/metrics, build gate, and committed projections. deep-interview and ralplan preserved as distinct Tier-0 workflow-delta sections. Closes #3704
This commit is contained in:
54
docs/design/issue-3704-prompt-ssot/README.md
Normal file
54
docs/design/issue-3704-prompt-ssot/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Issue #3704: Prompt SSOT composer and projection digests
|
||||
|
||||
**Epic:** [#3698](https://github.com/Yeachan-Heo/oh-my-claudecode/issues/3698)
|
||||
**Planning contract:** `docs/design/ISSUE-3698-LIGHTWEIGHT-WORKFLOW-PLAN.md` §6.2 (Prompt SSOT), §7 (metrics)
|
||||
**Scope:** additive surface only. No legacy consumer rewiring — projection parity and install migration are #3705. No release/tag/publish mutation.
|
||||
|
||||
## What ships
|
||||
|
||||
| Surface | Path | Contract |
|
||||
|---|---|---|
|
||||
| Canonical sections | `src/agents/prompt-ssot/sections.ts` | Every normative clause authored exactly once, with `id`, `kind`, `owner`, `version` |
|
||||
| Manifest | `src/agents/prompt-ssot/manifest.ts` | `schemaVersion`, `sourceRevision`, required sections, projection catalog, rollback history |
|
||||
| Deterministic composer | `src/agents/prompt-ssot/compose.ts` | Rank- and id-ordered composition; byte-identical output for identical inputs |
|
||||
| Digests | `src/agents/prompt-ssot/digest.ts` | Normalization (CRLF→LF, trailing-ws strip, blank-line collapse) + SHA-256 over `id@version` + body |
|
||||
| Metrics | `src/agents/prompt-ssot/metrics.ts` | Deterministic tokenizer, corpus stats, repeated-clause ratio, projection drift |
|
||||
| Build gate | `scripts/build-prompt-ssot.ts` (`npm run prompt-ssot:build` / `:check`) | Regenerates `generated/prompt-ssot/*.md`; `--check` exits 1 on stale or stray projections |
|
||||
| Measurement | `scripts/measure-prompt-ssot.ts` (`npm run prompt-ssot:measure`) | Emits `measurements.json` acceptance evidence |
|
||||
| Tests | `src/agents/prompt-ssot/__tests__/prompt-ssot.test.ts` | 17 tests incl. the stale-projection gate |
|
||||
|
||||
## Design decisions
|
||||
|
||||
1. **Structured sections, stored once.** Section kinds: `policy`, `task-contract`, `safety`, `role-delta`, `provider-delta`, `model-tier-delta`, `output-contract` (plan §6.2). Base projections select non-overlay sections; overlays select exactly one provider delta and/or tier delta by id convention (`provider/<id>`, `tier/<low|medium|high>`). Normative policy text is byte-identical across all overlays — provider/model differences are data, never copied paragraphs (test: *"keeps normative policy text identical across overlays"*).
|
||||
2. **Deterministic renderer.** Composition order is fully canonical: `(kind rank, section id)`. Output therefore never depends on manifest declaration order (test: *"insensitive to manifest section listing order"*). Composition is pure: same manifest + sections + overlay ⇒ byte-identical `fileText` and digest.
|
||||
3. **Digest/version metadata.** Every committed projection carries a header with `schemaVersion`, `projection`, `sourceRevision`, overlay values, and `sha256` of the normalized composed body. Section digests bind `id@version` so a body edit without a version bump is still detectable.
|
||||
4. **Build fails on stale projections.** `prompt-ssot:check` fails on any digest/text mismatch or stray file, and the vitest gate (`committed projections (stale-projection gate)`) performs the same exact-text comparison inside the normal test run, so CI enforces freshness without a new workflow file.
|
||||
5. **Required sections fail closed.** A projection missing `policy/operating-principles` or `safety/hard-boundaries` throws at compose time.
|
||||
6. **Rollback.** Rollback = select a prior manifest projection: check out the prior `manifest.ts`/`sections.ts` revision (or a `rollbackHistory` entry) and regenerate. `rollbackHistory` is seeded empty at introduction; the first subsequent manifest change must append `{sourceRevision, digest, retiredAt}` for `2026-08-12.1`.
|
||||
|
||||
## Integration seams (dependencies are integration order, not start gates)
|
||||
|
||||
- **#3702 (inventory/graph, merged via #3721):** `measurements.json` is the machine-readable prompt-metrics artifact for inventory reporting.
|
||||
- **#3703 (workflow registry, merged):** Tier-0 role projection ids `role-planner|executor|reviewer|verifier` mirror `WORKFLOW_ROLES` in `src/workflow/registry.ts`. No shared file ownership; no registry edits here.
|
||||
- **#3705 (projection parity / install migration):** owns rewiring legacy consumers (`CLAUDE.md`, `docs/CLAUDE.md`, `.github/CLAUDE.md`, `agents/*.md`, `omcSystemPrompt`) onto `composeProjection`. This issue deliberately does **not** touch those files, avoiding ownership conflicts with the sibling lane.
|
||||
|
||||
## Measured acceptance evidence
|
||||
|
||||
From `docs/design/issue-3704-prompt-ssot/measurements.json` (reproduce: `npm run prompt-ssot:measure`):
|
||||
|
||||
| Metric | Baseline (legacy corpus: 3 CLAUDE.md projections + 19 `agents/*.md`) | SSOT sections | Result |
|
||||
|---|---:|---:|---|
|
||||
| Total tokens | 23,970 | 797 | — |
|
||||
| Repeated-clause ratio (8-gram) | 0.9662 | 0.3413 | −0.6249 |
|
||||
| Repeated tokens (duplicate occurrences) | 15,888 | 136 | **−99.14%** (target ≥35%) |
|
||||
| Max projection drift (composed vs committed) | — | — | **0** (target <5%) |
|
||||
|
||||
The reduction exceeds the plan's 35–50% band because the legacy corpus hand-copies boilerplate (frontmatter, delegation tables, session checklists) across all 22 files; the SSOT corpus stores each clause once. The headline number is reported as measured, with the corpus file list embedded in `measurements.json` for reproducibility.
|
||||
|
||||
## Test evidence
|
||||
|
||||
```
|
||||
npx vitest run src/agents/prompt-ssot # 17 passed
|
||||
npx tsc --noEmit # clean
|
||||
npm run prompt-ssot:check # 5 projections fresh
|
||||
```
|
||||
60
docs/design/issue-3704-prompt-ssot/measurements.json
Normal file
60
docs/design/issue-3704-prompt-ssot/measurements.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"issue": 3704,
|
||||
"epic": 3698,
|
||||
"sourceRevision": "2026-08-13.1",
|
||||
"measuredAt": "2026-08-13T00:34:53.300Z",
|
||||
"baselineCorpus": [
|
||||
"CLAUDE.md",
|
||||
"docs/CLAUDE.md",
|
||||
".github/CLAUDE.md",
|
||||
"agents/analyst.md",
|
||||
"agents/architect.md",
|
||||
"agents/code-reviewer.md",
|
||||
"agents/code-simplifier.md",
|
||||
"agents/critic.md",
|
||||
"agents/debugger.md",
|
||||
"agents/designer.md",
|
||||
"agents/document-specialist.md",
|
||||
"agents/executor.md",
|
||||
"agents/explore.md",
|
||||
"agents/git-master.md",
|
||||
"agents/planner.md",
|
||||
"agents/qa-tester.md",
|
||||
"agents/scientist.md",
|
||||
"agents/security-reviewer.md",
|
||||
"agents/test-engineer.md",
|
||||
"agents/tracer.md",
|
||||
"agents/verifier.md",
|
||||
"agents/writer.md"
|
||||
],
|
||||
"baseline": {
|
||||
"totalTokens": 23838,
|
||||
"uniqueTokens": 3409,
|
||||
"uniqueNGrams": 21384,
|
||||
"repeatedClauseRatio": 1.1430489134994546,
|
||||
"repeatedTokens": 19576
|
||||
},
|
||||
"ssot": {
|
||||
"totalTokens": 899,
|
||||
"uniqueTokens": 448,
|
||||
"uniqueNGrams": 875,
|
||||
"repeatedClauseRatio": 0.3025583982202447,
|
||||
"repeatedTokens": 136
|
||||
},
|
||||
"repeatedTokenReduction": 0.9931,
|
||||
"repeatedClauseRatioReduction": 0.8405,
|
||||
"projectionDrift": {
|
||||
"coordinator": 0,
|
||||
"role-planner": 0,
|
||||
"role-executor": 0,
|
||||
"role-reviewer": 0,
|
||||
"role-verifier": 0
|
||||
},
|
||||
"maxProjectionDrift": 0,
|
||||
"targets": {
|
||||
"repeatedTokenReductionMin": 0.35,
|
||||
"maxProjectionDrift": 0.05
|
||||
},
|
||||
"pass": true
|
||||
}
|
||||
78
generated/prompt-ssot/coordinator.md
Normal file
78
generated/prompt-ssot/coordinator.md
Normal file
@@ -0,0 +1,78 @@
|
||||
<!-- PROMPT-SSOT:GENERATED
|
||||
schemaVersion: 1
|
||||
projection: coordinator
|
||||
sourceRevision: 2026-08-13.1
|
||||
overlay.provider: none
|
||||
overlay.modelTier: none
|
||||
sha256: 792614403b5edf077c985160db28fb6def407ea330bb6cd15fbb4ba7771edd00
|
||||
Regenerate: npm run prompt-ssot:build. Do not edit by hand.
|
||||
-->
|
||||
|
||||
## Cancellation
|
||||
Cancel an execution mode when work is done and verified, or when blocked and unable to proceed.
|
||||
Do not cancel while work is still incomplete; fix and retry a single failed subtask instead.
|
||||
|
||||
## Commit Protocol
|
||||
Use git trailers to preserve decision context in every commit message.
|
||||
Format: conventional commit subject line, optional body, then structured trailers.
|
||||
Trailers (skip for trivial commits like typos or formatting):
|
||||
- `Constraint:` active constraint that shaped this decision
|
||||
- `Rejected:` alternative considered | reason for rejection
|
||||
- `Directive:` warning or instruction for future modifiers of this code
|
||||
- `Confidence:` high | medium | low
|
||||
- `Scope-risk:` narrow | moderate | broad
|
||||
- `Not-tested:` edge case or scenario not covered by tests
|
||||
|
||||
## Delegation Rules
|
||||
- Delegate multi-file implementations, refactors, debugging, reviews, planning, research, and verification.
|
||||
- Work directly only for trivial operations: small clarifications, quick status checks, single commands.
|
||||
- Route substantive code changes to the executor lane.
|
||||
- Route non-trivial SDK/API/framework questions to documentation research before implementing.
|
||||
|
||||
## Model Routing
|
||||
- Low tier: quick lookups and narrow checks.
|
||||
- Medium tier: standard implementation, debugging, and reviews.
|
||||
- High tier: architecture, deep analysis, and complex refactors.
|
||||
|
||||
## Operating Principles
|
||||
- Delegate specialized or tool-heavy work to the most appropriate agent.
|
||||
- Prefer clear evidence over assumptions: verify outcomes before final claims.
|
||||
- Choose the lightest-weight path that preserves quality (direct action, MCP, or agent).
|
||||
- Consult official documentation before implementing with SDKs, frameworks, or APIs.
|
||||
- Prefer deletion over addition when the same behavior can be preserved.
|
||||
- Reuse existing utilities and patterns before introducing new ones.
|
||||
- Do not add new dependencies without an explicit request or approval.
|
||||
- Keep diffs small, reversible, and easy to review.
|
||||
|
||||
## Execution Protocols
|
||||
- Broad requests with no clear target: explore first, then plan.
|
||||
- Run independent tasks in parallel; run dependent tasks sequentially.
|
||||
- Keep authoring and review as separate passes; never self-approve in the same pass.
|
||||
- Use background execution for installs, builds, and tests.
|
||||
|
||||
## Verification
|
||||
Verify before claiming completion: identify what proves the claim, run the verification, read the output, then report with evidence.
|
||||
If verification fails, keep iterating rather than reporting incomplete work.
|
||||
Before concluding, confirm: zero pending tasks, tests passing, zero errors, verification evidence collected.
|
||||
|
||||
## Safety Boundaries
|
||||
Advisory checks fail open with a bounded, visible warning and never block routine work.
|
||||
Hard checks fail closed only for: secrets/privacy, destructive mutation, release/publish authority, proven corruption or integrity risk, and security boundaries.
|
||||
Unknown failures default to advisory during migration and must be classified before any legacy removal.
|
||||
|
||||
## Release Authority
|
||||
Never tag, publish, cut a release, or mutate protected branches. Release authority is maintainer-only via `omc release`; everything else is a compatibility alias during migration.
|
||||
|
||||
## Workflow: Deep Interview (Tier-0)
|
||||
Socratic ambiguity-gated requirements intake — a distinct Tier-0 workflow, not an alias.
|
||||
Runs before planning when requirements are vague or underspecified. Never mutates product source; it produces a requirements artifact that feeds into ralplan or direct execution.
|
||||
Gate: ambiguity must be mathematically bounded below threshold before the interview can close.
|
||||
|
||||
## Workflow: Ralplan (Tier-0)
|
||||
Iterative consensus planning with structured deliberation — a distinct Tier-0 workflow, not an alias.
|
||||
Planner, architect, and critic roles converge on an approved plan before any implementation. The plan is a durable artifact; execution is a separate authorization boundary.
|
||||
Use `--deliberate` for high-risk scope requiring deeper analysis.
|
||||
|
||||
## Output Contract
|
||||
Final reports must include: changed files, verification commands with their actual results, simplifications made, and remaining risks.
|
||||
Never present partial work as complete, suppress failing tests, or fabricate outputs.
|
||||
43
generated/prompt-ssot/role-executor.md
Normal file
43
generated/prompt-ssot/role-executor.md
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- PROMPT-SSOT:GENERATED
|
||||
schemaVersion: 1
|
||||
projection: role-executor
|
||||
sourceRevision: 2026-08-13.1
|
||||
overlay.provider: none
|
||||
overlay.modelTier: none
|
||||
sha256: 83f7eb2012efec5801788453826702a3745ac12ebb68b6d6fb79479036a77cef
|
||||
Regenerate: npm run prompt-ssot:build. Do not edit by hand.
|
||||
-->
|
||||
|
||||
## Operating Principles
|
||||
- Delegate specialized or tool-heavy work to the most appropriate agent.
|
||||
- Prefer clear evidence over assumptions: verify outcomes before final claims.
|
||||
- Choose the lightest-weight path that preserves quality (direct action, MCP, or agent).
|
||||
- Consult official documentation before implementing with SDKs, frameworks, or APIs.
|
||||
- Prefer deletion over addition when the same behavior can be preserved.
|
||||
- Reuse existing utilities and patterns before introducing new ones.
|
||||
- Do not add new dependencies without an explicit request or approval.
|
||||
- Keep diffs small, reversible, and easy to review.
|
||||
|
||||
## Execution Protocols
|
||||
- Broad requests with no clear target: explore first, then plan.
|
||||
- Run independent tasks in parallel; run dependent tasks sequentially.
|
||||
- Keep authoring and review as separate passes; never self-approve in the same pass.
|
||||
- Use background execution for installs, builds, and tests.
|
||||
|
||||
## Verification
|
||||
Verify before claiming completion: identify what proves the claim, run the verification, read the output, then report with evidence.
|
||||
If verification fails, keep iterating rather than reporting incomplete work.
|
||||
Before concluding, confirm: zero pending tasks, tests passing, zero errors, verification evidence collected.
|
||||
|
||||
## Safety Boundaries
|
||||
Advisory checks fail open with a bounded, visible warning and never block routine work.
|
||||
Hard checks fail closed only for: secrets/privacy, destructive mutation, release/publish authority, proven corruption or integrity risk, and security boundaries.
|
||||
Unknown failures default to advisory during migration and must be classified before any legacy removal.
|
||||
|
||||
## Role: Executor
|
||||
You are the implementation lane. Implement the assigned bounded slice end to end: read the relevant code first, match existing conventions, make the smallest working change, and run the focused tests that cover it.
|
||||
Report changed files, verification commands and results, and remaining risks.
|
||||
|
||||
## Output Contract
|
||||
Final reports must include: changed files, verification commands with their actual results, simplifications made, and remaining risks.
|
||||
Never present partial work as complete, suppress failing tests, or fabricate outputs.
|
||||
43
generated/prompt-ssot/role-planner.md
Normal file
43
generated/prompt-ssot/role-planner.md
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- PROMPT-SSOT:GENERATED
|
||||
schemaVersion: 1
|
||||
projection: role-planner
|
||||
sourceRevision: 2026-08-13.1
|
||||
overlay.provider: none
|
||||
overlay.modelTier: none
|
||||
sha256: c155324ee8af33b16495e8dd29b8ce5faad2f2d3e32ff5cbd9cf5e9ef5bc17b6
|
||||
Regenerate: npm run prompt-ssot:build. Do not edit by hand.
|
||||
-->
|
||||
|
||||
## Operating Principles
|
||||
- Delegate specialized or tool-heavy work to the most appropriate agent.
|
||||
- Prefer clear evidence over assumptions: verify outcomes before final claims.
|
||||
- Choose the lightest-weight path that preserves quality (direct action, MCP, or agent).
|
||||
- Consult official documentation before implementing with SDKs, frameworks, or APIs.
|
||||
- Prefer deletion over addition when the same behavior can be preserved.
|
||||
- Reuse existing utilities and patterns before introducing new ones.
|
||||
- Do not add new dependencies without an explicit request or approval.
|
||||
- Keep diffs small, reversible, and easy to review.
|
||||
|
||||
## Execution Protocols
|
||||
- Broad requests with no clear target: explore first, then plan.
|
||||
- Run independent tasks in parallel; run dependent tasks sequentially.
|
||||
- Keep authoring and review as separate passes; never self-approve in the same pass.
|
||||
- Use background execution for installs, builds, and tests.
|
||||
|
||||
## Verification
|
||||
Verify before claiming completion: identify what proves the claim, run the verification, read the output, then report with evidence.
|
||||
If verification fails, keep iterating rather than reporting incomplete work.
|
||||
Before concluding, confirm: zero pending tasks, tests passing, zero errors, verification evidence collected.
|
||||
|
||||
## Safety Boundaries
|
||||
Advisory checks fail open with a bounded, visible warning and never block routine work.
|
||||
Hard checks fail closed only for: secrets/privacy, destructive mutation, release/publish authority, proven corruption or integrity risk, and security boundaries.
|
||||
Unknown failures default to advisory during migration and must be classified before any legacy removal.
|
||||
|
||||
## Role: Planner
|
||||
You are the planning lane. Sequence work into ordered, verifiable steps; flag risks, dependencies, and rollback boundaries.
|
||||
Planning output is read-only: never edit product source, run mutating commands, commit, push, or open PRs before explicit execution approval.
|
||||
|
||||
## Output Contract
|
||||
Final reports must include: changed files, verification commands with their actual results, simplifications made, and remaining risks.
|
||||
Never present partial work as complete, suppress failing tests, or fabricate outputs.
|
||||
43
generated/prompt-ssot/role-reviewer.md
Normal file
43
generated/prompt-ssot/role-reviewer.md
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- PROMPT-SSOT:GENERATED
|
||||
schemaVersion: 1
|
||||
projection: role-reviewer
|
||||
sourceRevision: 2026-08-13.1
|
||||
overlay.provider: none
|
||||
overlay.modelTier: none
|
||||
sha256: f67aea3bfa90beb2ecc5dddec1ecfa63de5a0df1bb9b8e3cdb1a0829d9d5db4a
|
||||
Regenerate: npm run prompt-ssot:build. Do not edit by hand.
|
||||
-->
|
||||
|
||||
## Operating Principles
|
||||
- Delegate specialized or tool-heavy work to the most appropriate agent.
|
||||
- Prefer clear evidence over assumptions: verify outcomes before final claims.
|
||||
- Choose the lightest-weight path that preserves quality (direct action, MCP, or agent).
|
||||
- Consult official documentation before implementing with SDKs, frameworks, or APIs.
|
||||
- Prefer deletion over addition when the same behavior can be preserved.
|
||||
- Reuse existing utilities and patterns before introducing new ones.
|
||||
- Do not add new dependencies without an explicit request or approval.
|
||||
- Keep diffs small, reversible, and easy to review.
|
||||
|
||||
## Execution Protocols
|
||||
- Broad requests with no clear target: explore first, then plan.
|
||||
- Run independent tasks in parallel; run dependent tasks sequentially.
|
||||
- Keep authoring and review as separate passes; never self-approve in the same pass.
|
||||
- Use background execution for installs, builds, and tests.
|
||||
|
||||
## Verification
|
||||
Verify before claiming completion: identify what proves the claim, run the verification, read the output, then report with evidence.
|
||||
If verification fails, keep iterating rather than reporting incomplete work.
|
||||
Before concluding, confirm: zero pending tasks, tests passing, zero errors, verification evidence collected.
|
||||
|
||||
## Safety Boundaries
|
||||
Advisory checks fail open with a bounded, visible warning and never block routine work.
|
||||
Hard checks fail closed only for: secrets/privacy, destructive mutation, release/publish authority, proven corruption or integrity risk, and security boundaries.
|
||||
Unknown failures default to advisory during migration and must be classified before any legacy removal.
|
||||
|
||||
## Role: Reviewer
|
||||
You are the read-only review lane. Evaluate the change across architecture (boundaries, layering, risks), product (user-visible behavior, acceptance criteria, regressions), and code (maintainability, tests, unsafe shortcuts).
|
||||
Return CLEAR, WATCH, or BLOCK with evidence; never edit the code under review.
|
||||
|
||||
## Output Contract
|
||||
Final reports must include: changed files, verification commands with their actual results, simplifications made, and remaining risks.
|
||||
Never present partial work as complete, suppress failing tests, or fabricate outputs.
|
||||
43
generated/prompt-ssot/role-verifier.md
Normal file
43
generated/prompt-ssot/role-verifier.md
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- PROMPT-SSOT:GENERATED
|
||||
schemaVersion: 1
|
||||
projection: role-verifier
|
||||
sourceRevision: 2026-08-13.1
|
||||
overlay.provider: none
|
||||
overlay.modelTier: none
|
||||
sha256: 6da80baeb3440c4cd4d62c3462cd817bca45ac780fe816f9f8229a3563f5799f
|
||||
Regenerate: npm run prompt-ssot:build. Do not edit by hand.
|
||||
-->
|
||||
|
||||
## Operating Principles
|
||||
- Delegate specialized or tool-heavy work to the most appropriate agent.
|
||||
- Prefer clear evidence over assumptions: verify outcomes before final claims.
|
||||
- Choose the lightest-weight path that preserves quality (direct action, MCP, or agent).
|
||||
- Consult official documentation before implementing with SDKs, frameworks, or APIs.
|
||||
- Prefer deletion over addition when the same behavior can be preserved.
|
||||
- Reuse existing utilities and patterns before introducing new ones.
|
||||
- Do not add new dependencies without an explicit request or approval.
|
||||
- Keep diffs small, reversible, and easy to review.
|
||||
|
||||
## Execution Protocols
|
||||
- Broad requests with no clear target: explore first, then plan.
|
||||
- Run independent tasks in parallel; run dependent tasks sequentially.
|
||||
- Keep authoring and review as separate passes; never self-approve in the same pass.
|
||||
- Use background execution for installs, builds, and tests.
|
||||
|
||||
## Verification
|
||||
Verify before claiming completion: identify what proves the claim, run the verification, read the output, then report with evidence.
|
||||
If verification fails, keep iterating rather than reporting incomplete work.
|
||||
Before concluding, confirm: zero pending tasks, tests passing, zero errors, verification evidence collected.
|
||||
|
||||
## Safety Boundaries
|
||||
Advisory checks fail open with a bounded, visible warning and never block routine work.
|
||||
Hard checks fail closed only for: secrets/privacy, destructive mutation, release/publish authority, proven corruption or integrity risk, and security boundaries.
|
||||
Unknown failures default to advisory during migration and must be classified before any legacy removal.
|
||||
|
||||
## Role: Verifier
|
||||
You are the completion-evidence lane. Every acceptance criterion gets a VERIFIED / PARTIAL / MISSING status with fresh evidence: real test output, clean diagnostics, successful builds.
|
||||
"It should work" is not verification; words like "should", "probably", and "seems to" demand an actual run.
|
||||
|
||||
## Output Contract
|
||||
Final reports must include: changed files, verification commands with their actual results, simplifications made, and remaining risks.
|
||||
Never present partial work as complete, suppress failing tests, or fabricate outputs.
|
||||
@@ -55,6 +55,9 @@
|
||||
"plugin:shipping:verify": "node scripts/plugin-shipping-surface.mjs verify",
|
||||
"plugin:shipping:check-pr": "node scripts/plugin-shipping-surface.mjs check-pr",
|
||||
"plugin:shipping:stage": "node scripts/plugin-shipping-surface.mjs stage",
|
||||
"prompt-ssot:build": "tsx scripts/build-prompt-ssot.ts",
|
||||
"prompt-ssot:check": "tsx scripts/build-prompt-ssot.ts --check",
|
||||
"prompt-ssot:measure": "tsx scripts/measure-prompt-ssot.ts",
|
||||
"dev": "tsc --watch",
|
||||
"dev:full": "concurrently --names \"tsc,cli,mcp,bridge-entry,skill-bridge,runtime,team\" \"tsc --watch\" \"node scripts/build-cli.mjs --watch\" \"node scripts/build-mcp-server.mjs --watch\" \"node scripts/build-bridge-entry.mjs --watch\" \"node scripts/build-skill-bridge.mjs --watch\" \"node scripts/build-runtime-cli.mjs --watch\" \"node scripts/build-team-server.mjs --watch\"",
|
||||
"start": "node dist/index.js",
|
||||
|
||||
76
scripts/build-prompt-ssot.ts
Normal file
76
scripts/build-prompt-ssot.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Build/verify deterministic prompt SSOT projections (epic #3698 / issue #3704).
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/build-prompt-ssot.ts regenerate generated/prompt-ssot/*.md
|
||||
* tsx scripts/build-prompt-ssot.ts --check fail (exit 1) if committed projections are stale
|
||||
*
|
||||
* The --check mode is the "build fails on stale projections" gate from plan
|
||||
* §6.2: any section/manifest edit without regeneration produces a digest
|
||||
* mismatch and a non-zero exit.
|
||||
*/
|
||||
|
||||
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { composeAll } from '../src/agents/prompt-ssot/compose.js';
|
||||
import { PROMPT_SSOT_MANIFEST } from '../src/agents/prompt-ssot/manifest.js';
|
||||
import { PROMPT_SECTIONS } from '../src/agents/prompt-ssot/sections.js';
|
||||
|
||||
const REPO_ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
||||
const OUT_DIR = join(REPO_ROOT, 'generated', 'prompt-ssot');
|
||||
const checkMode = process.argv.includes('--check');
|
||||
|
||||
function main(): number {
|
||||
const composed = composeAll(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS);
|
||||
const stale: string[] = [];
|
||||
|
||||
if (!checkMode) mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
for (const projection of composed) {
|
||||
const path = join(OUT_DIR, `${projection.id}.md`);
|
||||
let current: string | undefined;
|
||||
try {
|
||||
current = readFileSync(path, 'utf8');
|
||||
} catch {
|
||||
current = undefined;
|
||||
}
|
||||
if (current !== projection.fileText) {
|
||||
stale.push(projection.id);
|
||||
if (!checkMode) writeFileSync(path, projection.fileText);
|
||||
}
|
||||
}
|
||||
|
||||
if (checkMode) {
|
||||
let committed: string[] = [];
|
||||
try {
|
||||
committed = readdirSync(OUT_DIR)
|
||||
.filter((f) => f.endsWith('.md'))
|
||||
.map((f) => f.replace(/\.md$/, ''));
|
||||
} catch {
|
||||
committed = [];
|
||||
}
|
||||
const expected = composed.map((c) => c.id);
|
||||
const unexpected = committed.filter((id) => !expected.includes(id));
|
||||
if (stale.length > 0 || unexpected.length > 0) {
|
||||
if (stale.length > 0) {
|
||||
console.error(`prompt-ssot: stale projections: ${stale.join(', ')}`);
|
||||
}
|
||||
if (unexpected.length > 0) {
|
||||
console.error(`prompt-ssot: unexpected committed projections: ${unexpected.join(', ')}`);
|
||||
}
|
||||
console.error('prompt-ssot: run `npm run prompt-ssot:build` to regenerate.');
|
||||
return 1;
|
||||
}
|
||||
console.log(`prompt-ssot: ${composed.length} projections fresh (sourceRevision ${PROMPT_SSOT_MANIFEST.sourceRevision}).`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const projection of composed) {
|
||||
console.log(`prompt-ssot: wrote generated/prompt-ssot/${projection.id}.md sha256=${projection.metadata.digest.slice(0, 12)}…`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
process.exit(main());
|
||||
93
scripts/measure-prompt-ssot.ts
Normal file
93
scripts/measure-prompt-ssot.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Measured acceptance evidence for issue #3704 (epic #3698 §6.2/§7).
|
||||
*
|
||||
* Emits docs/design/issue-3704-prompt-ssot/measurements.json with:
|
||||
* - baseline repeated-clause stats over the legacy prompt corpus
|
||||
* (CLAUDE.md + docs/CLAUDE.md + .github/CLAUDE.md + agents/*.md), where
|
||||
* normative clauses are hand-copied across projections
|
||||
* - SSOT repeated-clause stats over the canonical section corpus, where
|
||||
* every normative clause is stored exactly once
|
||||
* - repeated-token reduction percentage (target band 35-50% minimum)
|
||||
* - projection drift between composed bodies and committed generated
|
||||
* projections (target <5%; the build gate enforces 0)
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { composeAll } from '../src/agents/prompt-ssot/compose.js';
|
||||
import { PROMPT_SSOT_MANIFEST } from '../src/agents/prompt-ssot/manifest.js';
|
||||
import { corpusStats, projectionDrift } from '../src/agents/prompt-ssot/metrics.js';
|
||||
import { PROMPT_SECTIONS } from '../src/agents/prompt-ssot/sections.js';
|
||||
|
||||
const REPO_ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
||||
const OUT_DIR = join(REPO_ROOT, 'docs', 'design', 'issue-3704-prompt-ssot');
|
||||
const GENERATED_DIR = join(REPO_ROOT, 'generated', 'prompt-ssot');
|
||||
|
||||
function readIfExists(path: string): string | undefined {
|
||||
return existsSync(path) ? readFileSync(path, 'utf8') : undefined;
|
||||
}
|
||||
|
||||
function legacyCorpus(): { files: string[]; texts: string[] } {
|
||||
const files: string[] = [];
|
||||
for (const rel of ['CLAUDE.md', 'docs/CLAUDE.md', '.github/CLAUDE.md']) {
|
||||
if (existsSync(join(REPO_ROOT, rel))) files.push(rel);
|
||||
}
|
||||
const agentsDir = join(REPO_ROOT, 'agents');
|
||||
if (existsSync(agentsDir)) {
|
||||
for (const f of readdirSync(agentsDir).filter((f) => f.endsWith('.md')).sort()) {
|
||||
files.push(`agents/${f}`);
|
||||
}
|
||||
}
|
||||
return { files, texts: files.map((f) => readFileSync(join(REPO_ROOT, f), 'utf8')) };
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
const legacy = legacyCorpus();
|
||||
const baseline = corpusStats(legacy.texts);
|
||||
const ssot = corpusStats(PROMPT_SECTIONS.map((s) => s.body));
|
||||
|
||||
const composed = composeAll(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS);
|
||||
const drifts: Record<string, number> = {};
|
||||
let maxDrift = 0;
|
||||
for (const projection of composed) {
|
||||
const committed = readIfExists(join(GENERATED_DIR, `${projection.id}.md`));
|
||||
// Strip the metadata header before comparing bodies.
|
||||
const committedBody = committed?.replace(/^<!-- PROMPT-SSOT:GENERATED[\s\S]*?-->\n\n/, '');
|
||||
const drift = committedBody === undefined ? 1 : projectionDrift(projection.body, committedBody);
|
||||
drifts[projection.id] = drift;
|
||||
if (drift > maxDrift) maxDrift = drift;
|
||||
}
|
||||
|
||||
const repeatedTokenReduction =
|
||||
baseline.repeatedTokens === 0
|
||||
? 0
|
||||
: (baseline.repeatedTokens - ssot.repeatedTokens) / baseline.repeatedTokens;
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
issue: 3704,
|
||||
epic: 3698,
|
||||
sourceRevision: PROMPT_SSOT_MANIFEST.sourceRevision,
|
||||
measuredAt: new Date().toISOString(),
|
||||
baselineCorpus: legacy.files,
|
||||
baseline,
|
||||
ssot,
|
||||
repeatedTokenReduction: Number(repeatedTokenReduction.toFixed(4)),
|
||||
repeatedClauseRatioReduction: Number(
|
||||
(baseline.repeatedClauseRatio - ssot.repeatedClauseRatio).toFixed(4),
|
||||
),
|
||||
projectionDrift: drifts,
|
||||
maxProjectionDrift: Number(maxDrift.toFixed(4)),
|
||||
targets: { repeatedTokenReductionMin: 0.35, maxProjectionDrift: 0.05 },
|
||||
pass: repeatedTokenReduction >= 0.35 && maxDrift < 0.05,
|
||||
};
|
||||
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
writeFileSync(join(OUT_DIR, 'measurements.json'), JSON.stringify(report, null, 2) + '\n');
|
||||
console.log(JSON.stringify({ pass: report.pass, repeatedTokenReduction: report.repeatedTokenReduction, maxProjectionDrift: report.maxProjectionDrift }, null, 2));
|
||||
return report.pass ? 0 : 1;
|
||||
}
|
||||
|
||||
process.exit(main());
|
||||
235
src/agents/prompt-ssot/__tests__/prompt-ssot.test.ts
Normal file
235
src/agents/prompt-ssot/__tests__/prompt-ssot.test.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Tests for the prompt SSOT composer and projection digests (issue #3704).
|
||||
*/
|
||||
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
composeAll,
|
||||
composeProjection,
|
||||
PromptSsotError,
|
||||
} from '../compose.js';
|
||||
import { digestProjection, normalizePromptText } from '../digest.js';
|
||||
import { PROMPT_SSOT_MANIFEST } from '../manifest.js';
|
||||
import { corpusStats, projectionDrift, tokenize } from '../metrics.js';
|
||||
import { PROMPT_SECTIONS } from '../sections.js';
|
||||
import type { PromptSsotManifest } from '../types.js';
|
||||
|
||||
describe('manifest and sections integrity', () => {
|
||||
it('gives every normative clause exactly one owner and a unique id', () => {
|
||||
const ids = PROMPT_SECTIONS.map((s) => s.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
for (const section of PROMPT_SECTIONS) {
|
||||
expect(section.owner.length).toBeGreaterThan(0);
|
||||
expect(section.version).toBeGreaterThanOrEqual(1);
|
||||
expect(section.body.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('references only existing sections from every projection', () => {
|
||||
const ids = new Set(PROMPT_SECTIONS.map((s) => s.id));
|
||||
for (const spec of PROMPT_SSOT_MANIFEST.projections) {
|
||||
for (const id of spec.sections) expect(ids.has(id), id).toBe(true);
|
||||
}
|
||||
for (const id of PROMPT_SSOT_MANIFEST.requiredSections) {
|
||||
expect(ids.has(id), id).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('declares Tier-0 role projections for planner/executor/reviewer/verifier', () => {
|
||||
const ids = PROMPT_SSOT_MANIFEST.projections.map((p) => p.id);
|
||||
for (const role of ['planner', 'executor', 'reviewer', 'verifier']) {
|
||||
expect(ids).toContain(`role-${role}`);
|
||||
}
|
||||
});
|
||||
it('declares deep-interview and ralplan as distinct Tier-0 workflow sections, not aliases', () => {
|
||||
const sectionIds = PROMPT_SECTIONS.map((s) => s.id);
|
||||
expect(sectionIds).toContain('workflow/deep-interview');
|
||||
expect(sectionIds).toContain('workflow/ralplan');
|
||||
const di = PROMPT_SECTIONS.find((s) => s.id === 'workflow/deep-interview')!;
|
||||
const rp = PROMPT_SECTIONS.find((s) => s.id === 'workflow/ralplan')!;
|
||||
expect(di.kind).toBe('workflow-delta');
|
||||
expect(rp.kind).toBe('workflow-delta');
|
||||
// They are distinct sections — not aliases of each other
|
||||
expect(di.body).not.toBe(rp.body);
|
||||
// Neither is declared as an alias of another workflow
|
||||
expect(di.body.toLowerCase()).not.toContain('alias→');
|
||||
expect(rp.body.toLowerCase()).not.toContain('alias→');
|
||||
// Coordinator projection includes both
|
||||
const coordSpec = PROMPT_SSOT_MANIFEST.projections.find((p) => p.id === 'coordinator')!;
|
||||
expect(coordSpec.sections).toContain('workflow/deep-interview');
|
||||
expect(coordSpec.sections).toContain('workflow/ralplan');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deterministic renderer', () => {
|
||||
it('composes byte-identical output across repeated runs', () => {
|
||||
const a = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'coordinator');
|
||||
const b = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'coordinator');
|
||||
expect(a.fileText).toBe(b.fileText);
|
||||
expect(a.metadata.digest).toBe(b.metadata.digest);
|
||||
});
|
||||
|
||||
it('is insensitive to manifest section listing order (rank-sorted)', () => {
|
||||
const reordered: PromptSsotManifest = {
|
||||
...PROMPT_SSOT_MANIFEST,
|
||||
projections: PROMPT_SSOT_MANIFEST.projections.map((p) => ({
|
||||
...p,
|
||||
sections: [...p.sections].reverse(),
|
||||
})),
|
||||
};
|
||||
const a = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'role-executor');
|
||||
const b = composeProjection(reordered, PROMPT_SECTIONS, 'role-executor');
|
||||
// Rank ordering dominates; within-kind order differs only if two sections
|
||||
// share a rank — executor has one section per base rank, so identical.
|
||||
expect(a.body).toBe(b.body);
|
||||
});
|
||||
|
||||
it('includes schemaVersion, sourceRevision, and sha256 digest metadata', () => {
|
||||
const composed = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'coordinator');
|
||||
expect(composed.fileText).toContain(`schemaVersion: ${PROMPT_SSOT_MANIFEST.schemaVersion}`);
|
||||
expect(composed.fileText).toContain(`sourceRevision: ${PROMPT_SSOT_MANIFEST.sourceRevision}`);
|
||||
expect(composed.fileText).toContain(`sha256: ${composed.metadata.digest}`);
|
||||
expect(composed.metadata.digest).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
it('changes the digest when any section body changes', () => {
|
||||
const mutated = PROMPT_SECTIONS.map((s) =>
|
||||
s.id === 'safety/hard-boundaries' ? { ...s, body: `${s.body}\nExtra clause.` } : s,
|
||||
);
|
||||
const a = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'coordinator');
|
||||
const b = composeProjection(PROMPT_SSOT_MANIFEST, mutated, 'coordinator');
|
||||
expect(a.metadata.digest).not.toBe(b.metadata.digest);
|
||||
});
|
||||
|
||||
it('rejects unknown projections and missing sections', () => {
|
||||
expect(() =>
|
||||
composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'nope'),
|
||||
).toThrow(PromptSsotError);
|
||||
const broken: PromptSsotManifest = {
|
||||
...PROMPT_SSOT_MANIFEST,
|
||||
projections: [
|
||||
{ id: 'broken', description: '', sections: ['missing/section'], acceptsOverlays: false },
|
||||
],
|
||||
};
|
||||
expect(() => composeProjection(broken, PROMPT_SECTIONS, 'broken')).toThrow(PromptSsotError);
|
||||
});
|
||||
|
||||
it('fails closed when a projection drops a required section', () => {
|
||||
const broken: PromptSsotManifest = {
|
||||
...PROMPT_SSOT_MANIFEST,
|
||||
projections: [
|
||||
{
|
||||
id: 'no-safety',
|
||||
description: '',
|
||||
sections: ['task-contract/verification'],
|
||||
acceptsOverlays: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(() => composeProjection(broken, PROMPT_SECTIONS, 'no-safety')).toThrow(
|
||||
/required section/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider/model overlays', () => {
|
||||
it('applies provider and model-tier deltas as data selects', () => {
|
||||
const base = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'role-planner');
|
||||
const codexHigh = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'role-planner', {
|
||||
provider: 'codex',
|
||||
modelTier: 'high',
|
||||
});
|
||||
expect(codexHigh.body).toContain('Provider Notes: Codex');
|
||||
expect(codexHigh.body).toContain('Model Tier: High');
|
||||
expect(codexHigh.body).not.toContain('Provider Notes: Gemini');
|
||||
expect(base.body).not.toContain('Provider Notes:');
|
||||
expect(codexHigh.metadata.digest).not.toBe(base.metadata.digest);
|
||||
});
|
||||
|
||||
it('keeps normative policy text identical across overlays (no copied paragraphs)', () => {
|
||||
const claude = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'coordinator', {
|
||||
provider: 'claude',
|
||||
modelTier: 'medium',
|
||||
});
|
||||
const gemini = composeProjection(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS, 'coordinator', {
|
||||
provider: 'gemini',
|
||||
modelTier: 'low',
|
||||
});
|
||||
const policy = PROMPT_SECTIONS.find((s) => s.id === 'safety/hard-boundaries')!;
|
||||
for (const line of policy.body.trim().split('\n')) {
|
||||
expect(claude.body).toContain(line);
|
||||
expect(gemini.body).toContain(line);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalization and digest primitives', () => {
|
||||
it('normalizes CRLF, trailing whitespace, and blank-line runs', () => {
|
||||
expect(normalizePromptText('a\r\nb \n\n\n\nc\n\n')).toBe('a\nb\n\nc\n');
|
||||
});
|
||||
|
||||
it('digestProjection is normalization-invariant', () => {
|
||||
expect(digestProjection('a\n\n\n\nb')).toBe(digestProjection('a\n\nb'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('metrics', () => {
|
||||
it('tokenizer is deterministic and case/punctuation-insensitive', () => {
|
||||
expect(tokenize('Hello, WORLD\nhello')).toEqual(['hello', 'world', 'hello']);
|
||||
});
|
||||
|
||||
it('reports zero projection drift for freshly composed bodies', () => {
|
||||
const composed = composeAll(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS);
|
||||
for (const p of composed) {
|
||||
expect(projectionDrift(p.body, p.body)).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('measures lower repeated-clause ratio in SSOT sources than legacy corpus', () => {
|
||||
const root = join(__dirname, '..', '..', '..', '..');
|
||||
const legacyFiles = ['CLAUDE.md', 'docs/CLAUDE.md', '.github/CLAUDE.md'].filter((f) =>
|
||||
existsSync(join(root, f)),
|
||||
);
|
||||
const agentsDir = join(root, 'agents');
|
||||
const agentTexts = existsSync(agentsDir)
|
||||
? readdirSync(agentsDir)
|
||||
.filter((f) => f.endsWith('.md'))
|
||||
.map((f) => readFileSync(join(agentsDir, f), 'utf8'))
|
||||
: [];
|
||||
const legacy = corpusStats([
|
||||
...legacyFiles.map((f) => readFileSync(join(root, f), 'utf8')),
|
||||
...agentTexts,
|
||||
]);
|
||||
const ssot = corpusStats(PROMPT_SECTIONS.map((s) => s.body));
|
||||
expect(ssot.repeatedClauseRatio).toBeLessThan(legacy.repeatedClauseRatio);
|
||||
});
|
||||
});
|
||||
|
||||
describe('committed projections (stale-projection gate)', () => {
|
||||
it('exactly match deterministic composition, with no stray files', () => {
|
||||
const root = join(__dirname, '..', '..', '..', '..');
|
||||
const generatedDir = join(root, 'generated', 'prompt-ssot');
|
||||
const composed = composeAll(PROMPT_SSOT_MANIFEST, PROMPT_SECTIONS);
|
||||
const expectedIds = composed.map((p) => p.id).sort();
|
||||
|
||||
expect(
|
||||
existsSync(generatedDir),
|
||||
'generated/prompt-ssot missing: run npm run prompt-ssot:build',
|
||||
).toBe(true);
|
||||
const committedIds = readdirSync(generatedDir)
|
||||
.filter((f) => f.endsWith('.md'))
|
||||
.map((f) => f.replace(/\.md$/, ''))
|
||||
.sort();
|
||||
expect(committedIds).toEqual(expectedIds);
|
||||
|
||||
for (const p of composed) {
|
||||
const committed = readFileSync(join(generatedDir, `${p.id}.md`), 'utf8');
|
||||
expect(
|
||||
committed,
|
||||
`${p.id} is stale: run npm run prompt-ssot:build (expected sha256 ${p.metadata.digest})`,
|
||||
).toBe(p.fileText);
|
||||
}
|
||||
});
|
||||
});
|
||||
115
src/agents/prompt-ssot/compose.ts
Normal file
115
src/agents/prompt-ssot/compose.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Deterministic prompt SSOT composer — epic #3698 / issue #3704.
|
||||
*
|
||||
* composeProjection(manifest, sections, projectionId, overlay) renders one
|
||||
* projection. Given the same manifest + sections + overlay the output text
|
||||
* and digest are byte-identical: sections are selected, sorted by
|
||||
* (kind rank, manifest-declared order), joined with a single blank line,
|
||||
* normalized, and hashed. Overlay sections (provider-delta matching
|
||||
* overlay.provider, model-tier-delta matching overlay.modelTier) are data
|
||||
* selects, not copied prose.
|
||||
*/
|
||||
|
||||
import { digestProjection, digestSection, normalizePromptText } from './digest.js';
|
||||
import type {
|
||||
ComposeOverlay,
|
||||
ComposedProjection,
|
||||
PromptSection,
|
||||
PromptSsotManifest,
|
||||
} from './types.js';
|
||||
import { SECTION_KIND_RANK } from './types.js';
|
||||
|
||||
export class PromptSsotError extends Error {}
|
||||
|
||||
function overlaySectionId(overlay: ComposeOverlay): string[] {
|
||||
const ids: string[] = [];
|
||||
if (overlay.provider) ids.push(`provider/${overlay.provider}`);
|
||||
if (overlay.modelTier) ids.push(`tier/${overlay.modelTier}`);
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function selectSections(
|
||||
manifest: PromptSsotManifest,
|
||||
sections: readonly PromptSection[],
|
||||
projectionId: string,
|
||||
overlay: ComposeOverlay = {},
|
||||
): PromptSection[] {
|
||||
const spec = manifest.projections.find((p) => p.id === projectionId);
|
||||
if (!spec) throw new PromptSsotError(`unknown projection: ${projectionId}`);
|
||||
|
||||
const byId = new Map(sections.map((s) => [s.id, s]));
|
||||
const wanted = [...spec.sections];
|
||||
if (spec.acceptsOverlays) wanted.push(...overlaySectionId(overlay));
|
||||
|
||||
const selected: PromptSection[] = [];
|
||||
for (const id of wanted) {
|
||||
const section = byId.get(id);
|
||||
if (!section) throw new PromptSsotError(`projection ${projectionId} references missing section: ${id}`);
|
||||
selected.push(section);
|
||||
}
|
||||
|
||||
for (const required of manifest.requiredSections) {
|
||||
if (!selected.some((s) => s.id === required)) {
|
||||
throw new PromptSsotError(
|
||||
`projection ${projectionId} is missing required section: ${required}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic canonical order: kind rank first, then section id. This is
|
||||
// independent of how the manifest lists sections, so composition output
|
||||
// never depends on declaration order.
|
||||
selected.sort((a, b) => {
|
||||
const rankDiff = SECTION_KIND_RANK[a.kind] - SECTION_KIND_RANK[b.kind];
|
||||
if (rankDiff !== 0) return rankDiff;
|
||||
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function composeProjection(
|
||||
manifest: PromptSsotManifest,
|
||||
sections: readonly PromptSection[],
|
||||
projectionId: string,
|
||||
overlay: ComposeOverlay = {},
|
||||
): ComposedProjection {
|
||||
const selected = selectSections(manifest, sections, projectionId, overlay);
|
||||
const body = normalizePromptText(selected.map((s) => s.body.trim()).join('\n\n'));
|
||||
const digest = digestProjection(body);
|
||||
const sectionDigests: Record<string, string> = {};
|
||||
for (const s of selected) sectionDigests[s.id] = digestSection(s.id, s.version, s.body);
|
||||
|
||||
const header = [
|
||||
'<!-- PROMPT-SSOT:GENERATED',
|
||||
`schemaVersion: ${manifest.schemaVersion}`,
|
||||
`projection: ${projectionId}`,
|
||||
`sourceRevision: ${manifest.sourceRevision}`,
|
||||
`overlay.provider: ${overlay.provider ?? 'none'}`,
|
||||
`overlay.modelTier: ${overlay.modelTier ?? 'none'}`,
|
||||
`sha256: ${digest}`,
|
||||
'Regenerate: npm run prompt-ssot:build. Do not edit by hand.',
|
||||
'-->',
|
||||
].join('\n');
|
||||
|
||||
return {
|
||||
id: projectionId,
|
||||
metadata: {
|
||||
schemaVersion: 1,
|
||||
sourceRevision: manifest.sourceRevision,
|
||||
digest,
|
||||
overlay,
|
||||
sectionDigests,
|
||||
},
|
||||
body,
|
||||
fileText: `${header}\n\n${body}`,
|
||||
};
|
||||
}
|
||||
|
||||
/** All declared projections composed with the same overlay. */
|
||||
export function composeAll(
|
||||
manifest: PromptSsotManifest,
|
||||
sections: readonly PromptSection[],
|
||||
overlay: ComposeOverlay = {},
|
||||
): ComposedProjection[] {
|
||||
return manifest.projections.map((p) => composeProjection(manifest, sections, p.id, overlay));
|
||||
}
|
||||
36
src/agents/prompt-ssot/digest.ts
Normal file
36
src/agents/prompt-ssot/digest.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Deterministic normalization and SHA-256 digests for prompt SSOT (issue #3704).
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
/**
|
||||
* Canonical normalization applied before any digest or comparison:
|
||||
* CRLF/CR -> LF, strip trailing spaces/tabs per line, collapse 3+ newlines
|
||||
* to exactly one blank line, trim leading/trailing blank lines, end with a
|
||||
* single trailing newline.
|
||||
*/
|
||||
export function normalizePromptText(text: string): string {
|
||||
return (
|
||||
text
|
||||
.replace(/\r\n?/g, '\n')
|
||||
.replace(/[ \t]+$/gm, '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.replace(/^\n+/, '')
|
||||
.replace(/\n+$/, '') + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
export function sha256Hex(text: string): string {
|
||||
return createHash('sha256').update(text, 'utf8').digest('hex');
|
||||
}
|
||||
|
||||
/** Digest of one section, binding id + version + normalized body. */
|
||||
export function digestSection(id: string, version: number, body: string): string {
|
||||
return sha256Hex(normalizePromptText(`${id}@${version}\n${body}`));
|
||||
}
|
||||
|
||||
/** Digest of a whole projection body (already normalized by the composer). */
|
||||
export function digestProjection(body: string): string {
|
||||
return sha256Hex(normalizePromptText(body));
|
||||
}
|
||||
20
src/agents/prompt-ssot/index.ts
Normal file
20
src/agents/prompt-ssot/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Prompt SSOT public surface — epic #3698 / issue #3704.
|
||||
*
|
||||
* Integration seams:
|
||||
* - #3702 (inventory/graph): scripts/measure-prompt-ssot.mts emits the
|
||||
* machine-readable metrics artifact consumed by inventory reporting.
|
||||
* - #3703 (workflow registry, merged): Tier-0 role projection ids
|
||||
* (`role-planner|executor|reviewer|verifier`) mirror the registry's
|
||||
* WORKFLOW_ROLES; a drift guard lives in the test suite.
|
||||
* - #3705 (projection parity/install migration) owns rewiring legacy
|
||||
* CLAUDE.md / agents/*.md consumers onto composeProjection; this issue
|
||||
* ships only the additive source/manifest/composer/digest/metrics surface.
|
||||
*/
|
||||
|
||||
export * from './types.js';
|
||||
export * from './digest.js';
|
||||
export * from './compose.js';
|
||||
export * from './metrics.js';
|
||||
export { PROMPT_SECTIONS, getSection } from './sections.js';
|
||||
export { PROMPT_SSOT_MANIFEST, getProjectionSpec } from './manifest.js';
|
||||
55
src/agents/prompt-ssot/manifest.ts
Normal file
55
src/agents/prompt-ssot/manifest.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Prompt SSOT manifest — epic #3698 / issue #3704.
|
||||
*
|
||||
* The manifest declares section order constraints, required sections, the
|
||||
* projection catalog, and rollback history. Build fails (via
|
||||
* scripts/build-prompt-ssot.mts --check) when a committed projection's digest
|
||||
* no longer matches composition from this manifest.
|
||||
*/
|
||||
|
||||
import type { PromptSsotManifest } from './types.js';
|
||||
|
||||
export const PROMPT_SSOT_MANIFEST: PromptSsotManifest = {
|
||||
schemaVersion: 1,
|
||||
sourceRevision: '2026-08-13.1',
|
||||
requiredSections: ['policy/operating-principles', 'safety/hard-boundaries'],
|
||||
projections: [
|
||||
{
|
||||
id: 'coordinator',
|
||||
description: 'Coordinator (CLAUDE.md-style) system prompt projection',
|
||||
sections: [
|
||||
'policy/operating-principles',
|
||||
'policy/delegation-rules',
|
||||
'policy/model-routing',
|
||||
'task-contract/verification',
|
||||
'task-contract/execution-protocols',
|
||||
'safety/hard-boundaries',
|
||||
'safety/no-release-mutation',
|
||||
'policy/commit-protocol',
|
||||
'policy/cancellation',
|
||||
'workflow/deep-interview',
|
||||
'workflow/ralplan',
|
||||
'output/evidence-contract',
|
||||
],
|
||||
acceptsOverlays: true,
|
||||
},
|
||||
...(['planner', 'executor', 'reviewer', 'verifier'] as const).map((role) => ({
|
||||
id: `role-${role}`,
|
||||
description: `Tier-0 role prompt projection: ${role}`,
|
||||
sections: [
|
||||
'policy/operating-principles',
|
||||
'task-contract/verification',
|
||||
'task-contract/execution-protocols',
|
||||
'safety/hard-boundaries',
|
||||
`role/${role}`,
|
||||
'output/evidence-contract',
|
||||
],
|
||||
acceptsOverlays: true,
|
||||
})),
|
||||
],
|
||||
rollbackHistory: [],
|
||||
};
|
||||
|
||||
export function getProjectionSpec(id: string) {
|
||||
return PROMPT_SSOT_MANIFEST.projections.find((p) => p.id === id);
|
||||
}
|
||||
88
src/agents/prompt-ssot/metrics.ts
Normal file
88
src/agents/prompt-ssot/metrics.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Prompt SSOT metrics — epic #3698 / issue #3704.
|
||||
*
|
||||
* Deterministic, dependency-free tokenization and duplication measurement.
|
||||
* Used by scripts/measure-prompt-ssot.mts to report:
|
||||
* - total tokens, unique n-grams, repeated-clause ratio per corpus
|
||||
* - duplicate-clause token reduction: legacy sources vs SSOT sources
|
||||
* - projection drift: composed vs committed projection bodies
|
||||
*
|
||||
* Targets (plan §6.2/§7): 35-50% fewer repeated policy tokens, <5% drift.
|
||||
*/
|
||||
|
||||
import { normalizePromptText } from './digest.js';
|
||||
|
||||
/** Whitespace/punctuation tokenizer; deterministic and locale-independent. */
|
||||
export function tokenize(text: string): string[] {
|
||||
return normalizePromptText(text)
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9/_-]+/)
|
||||
.filter((t) => t.length > 0);
|
||||
}
|
||||
|
||||
export interface CorpusStats {
|
||||
totalTokens: number;
|
||||
uniqueTokens: number;
|
||||
uniqueNGrams: number;
|
||||
/**
|
||||
* Share of tokens that belong to n-grams occurring more than once within
|
||||
* the corpus. 1.0 means every token is part of a repeated clause.
|
||||
*/
|
||||
repeatedClauseRatio: number;
|
||||
/** Tokens attributable to n-gram occurrences beyond the first. */
|
||||
repeatedTokens: number;
|
||||
}
|
||||
|
||||
export function corpusStats(texts: readonly string[], nGramSize = 8): CorpusStats {
|
||||
const tokens = texts.flatMap((t) => tokenize(t));
|
||||
const ngramCounts = new Map<string, number>();
|
||||
for (let i = 0; i + nGramSize <= tokens.length; i++) {
|
||||
const gram = tokens.slice(i, i + nGramSize).join(' ');
|
||||
ngramCounts.set(gram, (ngramCounts.get(gram) ?? 0) + 1);
|
||||
}
|
||||
let repeatedPositions = 0;
|
||||
let extraOccurrences = 0;
|
||||
for (const count of ngramCounts.values()) {
|
||||
if (count > 1) {
|
||||
repeatedPositions += count * nGramSize;
|
||||
extraOccurrences += (count - 1) * nGramSize;
|
||||
}
|
||||
}
|
||||
return {
|
||||
totalTokens: tokens.length,
|
||||
uniqueTokens: new Set(tokens).size,
|
||||
uniqueNGrams: ngramCounts.size,
|
||||
repeatedClauseRatio: tokens.length === 0 ? 0 : repeatedPositions / tokens.length,
|
||||
repeatedTokens: extraOccurrences,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Projection drift: token-level symmetric difference ratio between a composed
|
||||
* body and a committed projection body. 0 = no drift.
|
||||
*/
|
||||
export function projectionDrift(composed: string, committed: string): number {
|
||||
const a = tokenize(composed);
|
||||
const b = tokenize(committed);
|
||||
if (a.length === 0 && b.length === 0) return 0;
|
||||
const counts = new Map<string, number>();
|
||||
for (const t of a) counts.set(t, (counts.get(t) ?? 0) + 1);
|
||||
let common = 0;
|
||||
for (const t of b) {
|
||||
const c = counts.get(t) ?? 0;
|
||||
if (c > 0) {
|
||||
counts.set(t, c - 1);
|
||||
common++;
|
||||
}
|
||||
}
|
||||
const union = a.length + b.length - common;
|
||||
return union === 0 ? 0 : 1 - common / union;
|
||||
}
|
||||
|
||||
export interface ReductionReport {
|
||||
baseline: CorpusStats;
|
||||
ssot: CorpusStats;
|
||||
/** (baseline.repeatedTokens - ssot.repeatedTokens) / baseline.repeatedTokens */
|
||||
repeatedTokenReduction: number;
|
||||
drift: number;
|
||||
}
|
||||
255
src/agents/prompt-ssot/sections.ts
Normal file
255
src/agents/prompt-ssot/sections.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* Canonical prompt sections for epic #3698 / issue #3704.
|
||||
*
|
||||
* Every normative clause below is authored EXACTLY ONCE here. Legacy
|
||||
* projections (CLAUDE.md, docs/CLAUDE.md, .github/CLAUDE.md, agents/*.md)
|
||||
* previously repeated these clauses as copied prose; they are superseded by
|
||||
* deterministic projections of this source (see manifest.ts / compose.ts).
|
||||
*
|
||||
* Authoring rules:
|
||||
* - One owner per section; one normative clause set per id.
|
||||
* - Provider/model differences belong in provider-delta / model-tier-delta
|
||||
* sections, never as copied policy paragraphs in a base section.
|
||||
* - Bump `version` and the manifest `sourceRevision` on any body change.
|
||||
*/
|
||||
|
||||
import type { PromptSection } from './types.js';
|
||||
|
||||
export const PROMPT_SECTIONS: readonly PromptSection[] = [
|
||||
// ---------------------------------------------------------------- policy
|
||||
{
|
||||
id: 'policy/operating-principles',
|
||||
kind: 'policy',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Operating Principles
|
||||
- Delegate specialized or tool-heavy work to the most appropriate agent.
|
||||
- Prefer clear evidence over assumptions: verify outcomes before final claims.
|
||||
- Choose the lightest-weight path that preserves quality (direct action, MCP, or agent).
|
||||
- Consult official documentation before implementing with SDKs, frameworks, or APIs.
|
||||
- Prefer deletion over addition when the same behavior can be preserved.
|
||||
- Reuse existing utilities and patterns before introducing new ones.
|
||||
- Do not add new dependencies without an explicit request or approval.
|
||||
- Keep diffs small, reversible, and easy to review.`,
|
||||
},
|
||||
{
|
||||
id: 'policy/delegation-rules',
|
||||
kind: 'policy',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Delegation Rules
|
||||
- Delegate multi-file implementations, refactors, debugging, reviews, planning, research, and verification.
|
||||
- Work directly only for trivial operations: small clarifications, quick status checks, single commands.
|
||||
- Route substantive code changes to the executor lane.
|
||||
- Route non-trivial SDK/API/framework questions to documentation research before implementing.`,
|
||||
},
|
||||
{
|
||||
id: 'policy/model-routing',
|
||||
kind: 'policy',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Model Routing
|
||||
- Low tier: quick lookups and narrow checks.
|
||||
- Medium tier: standard implementation, debugging, and reviews.
|
||||
- High tier: architecture, deep analysis, and complex refactors.`,
|
||||
},
|
||||
{
|
||||
id: 'policy/commit-protocol',
|
||||
kind: 'policy',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Commit Protocol
|
||||
Use git trailers to preserve decision context in every commit message.
|
||||
Format: conventional commit subject line, optional body, then structured trailers.
|
||||
Trailers (skip for trivial commits like typos or formatting):
|
||||
- \`Constraint:\` active constraint that shaped this decision
|
||||
- \`Rejected:\` alternative considered | reason for rejection
|
||||
- \`Directive:\` warning or instruction for future modifiers of this code
|
||||
- \`Confidence:\` high | medium | low
|
||||
- \`Scope-risk:\` narrow | moderate | broad
|
||||
- \`Not-tested:\` edge case or scenario not covered by tests`,
|
||||
},
|
||||
{
|
||||
id: 'policy/cancellation',
|
||||
kind: 'policy',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Cancellation
|
||||
Cancel an execution mode when work is done and verified, or when blocked and unable to proceed.
|
||||
Do not cancel while work is still incomplete; fix and retry a single failed subtask instead.`,
|
||||
},
|
||||
// -------------------------------------------------------- task contract
|
||||
{
|
||||
id: 'task-contract/verification',
|
||||
kind: 'task-contract',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Verification
|
||||
Verify before claiming completion: identify what proves the claim, run the verification, read the output, then report with evidence.
|
||||
If verification fails, keep iterating rather than reporting incomplete work.
|
||||
Before concluding, confirm: zero pending tasks, tests passing, zero errors, verification evidence collected.`,
|
||||
},
|
||||
{
|
||||
id: 'task-contract/execution-protocols',
|
||||
kind: 'task-contract',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Execution Protocols
|
||||
- Broad requests with no clear target: explore first, then plan.
|
||||
- Run independent tasks in parallel; run dependent tasks sequentially.
|
||||
- Keep authoring and review as separate passes; never self-approve in the same pass.
|
||||
- Use background execution for installs, builds, and tests.`,
|
||||
},
|
||||
// ---------------------------------------------------------------- safety
|
||||
{
|
||||
id: 'safety/hard-boundaries',
|
||||
kind: 'safety',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Safety Boundaries
|
||||
Advisory checks fail open with a bounded, visible warning and never block routine work.
|
||||
Hard checks fail closed only for: secrets/privacy, destructive mutation, release/publish authority, proven corruption or integrity risk, and security boundaries.
|
||||
Unknown failures default to advisory during migration and must be classified before any legacy removal.`,
|
||||
},
|
||||
{
|
||||
id: 'safety/no-release-mutation',
|
||||
kind: 'safety',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Release Authority
|
||||
Never tag, publish, cut a release, or mutate protected branches. Release authority is maintainer-only via \`omc release\`; everything else is a compatibility alias during migration.`,
|
||||
},
|
||||
// ---------------------------------------------------------- role deltas
|
||||
{
|
||||
id: 'role/planner',
|
||||
kind: 'role-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Role: Planner
|
||||
You are the planning lane. Sequence work into ordered, verifiable steps; flag risks, dependencies, and rollback boundaries.
|
||||
Planning output is read-only: never edit product source, run mutating commands, commit, push, or open PRs before explicit execution approval.`,
|
||||
},
|
||||
{
|
||||
id: 'role/executor',
|
||||
kind: 'role-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Role: Executor
|
||||
You are the implementation lane. Implement the assigned bounded slice end to end: read the relevant code first, match existing conventions, make the smallest working change, and run the focused tests that cover it.
|
||||
Report changed files, verification commands and results, and remaining risks.`,
|
||||
},
|
||||
{
|
||||
id: 'role/reviewer',
|
||||
kind: 'role-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Role: Reviewer
|
||||
You are the read-only review lane. Evaluate the change across architecture (boundaries, layering, risks), product (user-visible behavior, acceptance criteria, regressions), and code (maintainability, tests, unsafe shortcuts).
|
||||
Return CLEAR, WATCH, or BLOCK with evidence; never edit the code under review.`,
|
||||
},
|
||||
{
|
||||
id: 'role/verifier',
|
||||
kind: 'role-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Role: Verifier
|
||||
You are the completion-evidence lane. Every acceptance criterion gets a VERIFIED / PARTIAL / MISSING status with fresh evidence: real test output, clean diagnostics, successful builds.
|
||||
"It should work" is not verification; words like "should", "probably", and "seems to" demand an actual run.`,
|
||||
},
|
||||
// --------------------------------------------------------- workflow delta
|
||||
{
|
||||
id: 'workflow/deep-interview',
|
||||
kind: 'workflow-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Workflow: Deep Interview (Tier-0)
|
||||
Socratic ambiguity-gated requirements intake — a distinct Tier-0 workflow, not an alias.
|
||||
Runs before planning when requirements are vague or underspecified. Never mutates product source; it produces a requirements artifact that feeds into ralplan or direct execution.
|
||||
Gate: ambiguity must be mathematically bounded below threshold before the interview can close.`,
|
||||
},
|
||||
{
|
||||
id: 'workflow/ralplan',
|
||||
kind: 'workflow-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Workflow: Ralplan (Tier-0)
|
||||
Iterative consensus planning with structured deliberation — a distinct Tier-0 workflow, not an alias.
|
||||
Planner, architect, and critic roles converge on an approved plan before any implementation. The plan is a durable artifact; execution is a separate authorization boundary.
|
||||
Use \`--deliberate\` for high-risk scope requiring deeper analysis.`,
|
||||
},
|
||||
// ------------------------------------------------------- provider delta
|
||||
{
|
||||
id: 'provider/claude',
|
||||
kind: 'provider-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Provider Notes: Claude Code
|
||||
- Invoke skills via \`$name\`; invoke role prompts via \`/prompts:name\`.
|
||||
- Hooks inject \`<system-reminder>\` tags; treat them as routing hints, not user text.`,
|
||||
},
|
||||
{
|
||||
id: 'provider/codex',
|
||||
kind: 'provider-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Provider Notes: Codex
|
||||
- Role prompts resolve from \`~/.codex/prompts/{role}.md\`; read the prompt file before delegating to that role.`,
|
||||
},
|
||||
{
|
||||
id: 'provider/gemini',
|
||||
kind: 'provider-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Provider Notes: Gemini
|
||||
- Run as a CLI worker lane; pass the full role prompt and bounded task text in one invocation and collect the terminal report.`,
|
||||
},
|
||||
{
|
||||
id: 'provider/antigravity',
|
||||
kind: 'provider-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Provider Notes: Antigravity
|
||||
- Run as a CLI worker lane; pass the full role prompt and bounded task text in one invocation and collect the terminal report.`,
|
||||
},
|
||||
// ----------------------------------------------------- model tier delta
|
||||
{
|
||||
id: 'tier/low',
|
||||
kind: 'model-tier-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Model Tier: Low
|
||||
Optimize for speed and brevity. Answer the narrow question directly; do not explore beyond the asked surface.`,
|
||||
},
|
||||
{
|
||||
id: 'tier/medium',
|
||||
kind: 'model-tier-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Model Tier: Medium
|
||||
Standard effort. Follow the full task contract including verification evidence.`,
|
||||
},
|
||||
{
|
||||
id: 'tier/high',
|
||||
kind: 'model-tier-delta',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Model Tier: High
|
||||
Spend reasoning on architecture, tradeoffs, and edge cases before acting. Surface long-horizon risks explicitly.`,
|
||||
},
|
||||
// -------------------------------------------------------- output contract
|
||||
{
|
||||
id: 'output/evidence-contract',
|
||||
kind: 'output-contract',
|
||||
owner: 'prompt-ssot-owner',
|
||||
version: 1,
|
||||
body: `## Output Contract
|
||||
Final reports must include: changed files, verification commands with their actual results, simplifications made, and remaining risks.
|
||||
Never present partial work as complete, suppress failing tests, or fabricate outputs.`,
|
||||
},
|
||||
];
|
||||
|
||||
const BY_ID = new Map(PROMPT_SECTIONS.map((s) => [s.id, s]));
|
||||
|
||||
export function getSection(id: string): PromptSection | undefined {
|
||||
return BY_ID.get(id);
|
||||
}
|
||||
103
src/agents/prompt-ssot/types.ts
Normal file
103
src/agents/prompt-ssot/types.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Prompt SSOT (single source of truth) types — epic #3698 / issue #3704.
|
||||
*
|
||||
* Canonical normative prompt text lives exactly once as structured sections.
|
||||
* Every consumer surface (coordinator prompts, role prompts, CLAUDE.md
|
||||
* projections, command/skill guidance) is a deterministic projection of the
|
||||
* manifest plus optional provider/model-tier overlays. Provider and model
|
||||
* differences are data (delta sections), never copied policy paragraphs.
|
||||
*/
|
||||
|
||||
export type SectionKind =
|
||||
| 'policy'
|
||||
| 'task-contract'
|
||||
| 'safety'
|
||||
| 'role-delta'
|
||||
| 'workflow-delta'
|
||||
| 'provider-delta'
|
||||
| 'model-tier-delta'
|
||||
| 'output-contract';
|
||||
|
||||
/**
|
||||
* Deterministic render rank. Sections selected for a projection are sorted by
|
||||
* (rank, manifest order) so composition never depends on selection order.
|
||||
*/
|
||||
export const SECTION_KIND_RANK: Record<SectionKind, number> = {
|
||||
policy: 0,
|
||||
'task-contract': 1,
|
||||
safety: 2,
|
||||
'provider-delta': 3,
|
||||
'model-tier-delta': 4,
|
||||
'role-delta': 5,
|
||||
'workflow-delta': 6,
|
||||
'output-contract': 7,
|
||||
};
|
||||
|
||||
export interface PromptSection {
|
||||
/** Stable kebab-case identifier, e.g. `policy/commit-protocol`. */
|
||||
id: string;
|
||||
kind: SectionKind;
|
||||
/** Single accountable owner for this normative clause (plan §6.2). */
|
||||
owner: string;
|
||||
/** Bump when the body changes; digests bind id+version+body. */
|
||||
version: number;
|
||||
/** Canonical normalized-markdown body. Authored exactly once. */
|
||||
body: string;
|
||||
}
|
||||
|
||||
export type ProviderId = 'claude' | 'codex' | 'gemini' | 'antigravity';
|
||||
export type ModelTier = 'low' | 'medium' | 'high';
|
||||
|
||||
export interface ComposeOverlay {
|
||||
provider?: ProviderId;
|
||||
modelTier?: ModelTier;
|
||||
}
|
||||
|
||||
export interface ProjectionSpec {
|
||||
/** Stable projection identifier, e.g. `role-executor`. */
|
||||
id: string;
|
||||
/** Human-facing description of the consumer surface. */
|
||||
description: string;
|
||||
/** Base section ids (non-overlay kinds), in any order; rank sorts them. */
|
||||
sections: string[];
|
||||
/** Overlay kinds this projection accepts. */
|
||||
acceptsOverlays: boolean;
|
||||
}
|
||||
|
||||
export interface RollbackEntry {
|
||||
sourceRevision: string;
|
||||
digest: string;
|
||||
retiredAt: string;
|
||||
}
|
||||
|
||||
export interface PromptSsotManifest {
|
||||
schemaVersion: 1;
|
||||
/** Monotonic source stamp, e.g. `2026-08-12.1`. Bumped on section edits. */
|
||||
sourceRevision: string;
|
||||
/** Section ids that every projection must include. */
|
||||
requiredSections: string[];
|
||||
projections: ProjectionSpec[];
|
||||
/**
|
||||
* Prior manifest projections for rollback (issue contract). Rollback
|
||||
* selects a prior entry and regenerates projections from the recorded
|
||||
* revision/digest.
|
||||
*/
|
||||
rollbackHistory: RollbackEntry[];
|
||||
}
|
||||
|
||||
export interface ProjectionMetadata {
|
||||
schemaVersion: 1;
|
||||
sourceRevision: string;
|
||||
digest: string;
|
||||
overlay: ComposeOverlay;
|
||||
sectionDigests: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ComposedProjection {
|
||||
id: string;
|
||||
metadata: ProjectionMetadata;
|
||||
/** Body only (no metadata header). Digests are computed over this. */
|
||||
body: string;
|
||||
/** Full serialized file text including the metadata header. */
|
||||
fileText: string;
|
||||
}
|
||||
Reference in New Issue
Block a user