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:
Bellman
2026-08-13 09:45:24 +09:00
committed by GitHub
parent eec055401e
commit 199fa133d4
18 changed files with 1443 additions and 0 deletions

View 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 3550% 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
```

View 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
}

View 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.

View 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.

View 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.

View 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.

View 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.

View File

@@ -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",

View 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());

View 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());

View 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);
}
});
});

View 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));
}

View 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));
}

View 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';

View 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);
}

View 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;
}

View 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);
}

View 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;
}