diff --git a/claude/CLAUDE.md b/claude/CLAUDE.md index cfbdc81..cea1339 100644 --- a/claude/CLAUDE.md +++ b/claude/CLAUDE.md @@ -40,9 +40,9 @@ - Treat compaction and principal replacement as lossy handoffs. Preserve the objective in principal-authored goals, plans, catalogs, ledgers, assignments, and checkpoints before context pressure forces recovery. -- Delegates own assigned artifacts and return evidence. The user-facing - principal reviews assignments and remains the logical author of control - state. +- Delegates own assigned artifacts and return evidence through the named return + channel. The user-facing principal reviews assignments and remains the logical + author of control state. - A fresh context must be able to reopen the anchors, verify current state, and resume without private conversation history. diff --git a/codex/AGENTS.md b/codex/AGENTS.md index c8582a4..dea6df8 100644 --- a/codex/AGENTS.md +++ b/codex/AGENTS.md @@ -45,10 +45,10 @@ objective in principal-authored goals, plans, catalogs, ledgers, assignments, and checkpoints before context pressure forces recovery. - Delegates own assigned artifacts and return evidence through the named return - channel. The principal reviews assignments and remains the logical author of - control state. + channel. The user-facing principal reviews assignments and remains the logical + author of control state. - A fresh context must be able to reopen the anchors, verify current state, and - resume without relying on private conversation history. + resume without private conversation history. - A work catalog may record a blocker immediately. Mark the active Codex goal blocked only when the same blocking condition has repeated for at least three consecutive goal turns and no meaningful progress remains without user input diff --git a/local-docs/source-of-truth.md b/local-docs/source-of-truth.md index a5f6f4a..52a06d8 100644 --- a/local-docs/source-of-truth.md +++ b/local-docs/source-of-truth.md @@ -38,7 +38,7 @@ Status values: | 10 | Skill-description length cap | scripts/common.ps1 MaxSkillDescriptionLength | pin | test-compass-architecture.py | consolidated | | 11 | Routing source reference (checklist prose deferred) | workflows/addition-intake.md | pin | policy-contracts.ps1 | consolidated | | 12 | Glossary terms | glossary.md | link | editorial convention | accepted | -| 13 | Shared user-preference prose | manifests/policy-contracts.json | pin | policy-contracts.ps1 | consolidated | +| 13 | Shared runtime-global prose | manifests/policy-contracts.json | pin | policy-contracts.ps1 | consolidated | ## Intentional separations @@ -46,8 +46,10 @@ These duplications are deliberate and stay. They are listed here so future audits do not re-flag them. - `codex/AGENTS.md`, `claude/CLAUDE.md`, and `apps/compass-mcp/profile.md` - serve different runtimes. Policy contracts pin their complete shared Writing, - Git, and PR block while the runtime-specific deltas stay separate. + serve different runtimes. Policy contracts pin the applicable shared blocks: + Writing, Git, and PR prose across all three surfaces, plus continuity, + planning, repository, and Windows prose across the Codex and Claude globals. + Runtime-specific deltas stay separate. - `workflows/templates/*.md` (maintainer, long form), `codex/skills/workspace-steward/references/project-template` copies (starter pack), and the embedded blocks in @@ -78,13 +80,13 @@ audits do not re-flag them. `template-anchors.ps1` pin covers the shared goal, catalog, assignment, and checkpoint identities, plus plan and decision identities where both file families provide those artifacts, so the registered binding is consolidated. -- Cluster 13 (shared user-preference prose): the verbatim Writing and Git - sections across `codex/AGENTS.md`, `claude/CLAUDE.md`, and - `apps/compass-mcp/profile.md` are bound by policy-contract pins so drift fails - doctor. Full generation (a `generate-runtime-globals.py` reassembling each - file from a shared source plus runtime deltas) was deferred because these are - load-bearing runtime instructions; the pin is the safe fallback authorized in - the plan. +- Cluster 13 (shared runtime-global prose): policy contracts pin the verbatim + Writing, Git, and PR blocks across `codex/AGENTS.md`, `claude/CLAUDE.md`, and + `apps/compass-mcp/profile.md`, plus the shared continuity, planning, + repository, and Windows blocks across the Codex and Claude globals. Full + generation (a `generate-runtime-globals.py` reassembling each file from a + shared source plus runtime deltas) was deferred because these are load-bearing + runtime instructions; the pin is the safe fallback authorized in the plan. - Cluster 11 (routing table and update-together checklist): the canonical routing source `workflows/addition-intake.md` is already referenced from the key surfaces, and policy contracts now require the two skill-authoring diff --git a/manifests/policy-contracts.json b/manifests/policy-contracts.json index 060014f..df8bc35 100644 --- a/manifests/policy-contracts.json +++ b/manifests/policy-contracts.json @@ -38,6 +38,26 @@ "## Writing\n\n- Never use em dashes or en dashes in commits, code comments, prose, or chat.\n Use other punctuation or separate sentences.\n\n## Git And Pull Requests\n\n- Commit with one lowercase subject, about 8 words or fewer, with no trailing\n punctuation, body, or `Co-Authored-By` trailer. Describe changes neutrally.\n- Prefer a pull request as the unit for repository changes. Use a descriptive,\n often verb-led title without conventional-commit prefixes. Keep the body to\n 0-2 motivation-first sentences with no headers, checkboxes, emojis, or\n generated footer." ] }, + { + "id": "shared-runtime-prose-codex", + "path": "codex/AGENTS.md", + "exact_sections": { + "Long-Running Continuity": "- Treat compaction and principal replacement as lossy handoffs. Preserve the objective in principal-authored goals, plans, catalogs, ledgers, assignments, and checkpoints before context pressure forces recovery.\n- Delegates own assigned artifacts and return evidence through the named return channel. The user-facing principal reviews assignments and remains the logical author of control state.\n- A fresh context must be able to reopen the anchors, verify current state, and resume without private conversation history.\n- A work catalog may record a blocker immediately. Mark the active Codex goal blocked only when the same blocking condition has repeated for at least three consecutive goal turns and no meaningful progress remains without user input or an external state change. Count the original or user-triggered turn and any automatic continuations. After a blocked goal resumes, start a fresh blocked audit and require three consecutive resumed goal turns before blocking it again. Once those conditions hold, mark the goal blocked instead of leaving it active while repeatedly reporting the blocker.", + "Planning Authority": "- A planning-only request authorizes inspection, research, questions, plans, and explicitly scoped experiments. Production implementation begins when the user or named authority opens that phase.\n- Review material goals, plans, delegation boundaries, and irreversible actions before launch unless the user already granted or waived that review.", + "Repository And State Boundaries": "- In a worktree, read and edit only that worktree. Keep one scope per branch or pull request, and preserve unrelated user work.\n- Prefer the smallest coherent change that fixes the owning boundary. After a repair, review for removable guards, wrappers, duplicate state, and obsolete paths.", + "Windows": "- Container entrypoint `.sh` files use LF line endings. When startup reports `No such file or directory`, inspect line endings before assuming a missing path." + } + }, + { + "id": "shared-runtime-prose-claude", + "path": "claude/CLAUDE.md", + "exact_sections": { + "Long-Running Continuity": "- Treat compaction and principal replacement as lossy handoffs. Preserve the objective in principal-authored goals, plans, catalogs, ledgers, assignments, and checkpoints before context pressure forces recovery.\n- Delegates own assigned artifacts and return evidence through the named return channel. The user-facing principal reviews assignments and remains the logical author of control state.\n- A fresh context must be able to reopen the anchors, verify current state, and resume without private conversation history.", + "Planning Authority": "- A planning-only request authorizes inspection, research, questions, plans, and explicitly scoped experiments. Production implementation begins when the user or named authority opens that phase.\n- Review material goals, plans, delegation boundaries, and irreversible actions before launch unless the user already granted or waived that review.", + "Repository And State Boundaries": "- In a worktree, read and edit only that worktree. Keep one scope per branch or pull request, and preserve unrelated user work.\n- Prefer the smallest coherent change that fixes the owning boundary. After a repair, review for removable guards, wrappers, duplicate state, and obsolete paths.", + "Windows": "- Container entrypoint `.sh` files use LF line endings. When startup reports `No such file or directory`, inspect line endings before assuming a missing path." + } + }, { "id": "routing-source-referenced-by-compass-skill", "path": "codex/skills/compass/SKILL.md", diff --git a/scripts/validate-policy-contracts.py b/scripts/validate-policy-contracts.py index 413373e..fb8220c 100644 --- a/scripts/validate-policy-contracts.py +++ b/scripts/validate-policy-contracts.py @@ -50,17 +50,38 @@ def safe_repo_path(root: Path, value: object) -> Path: return resolved -def string_list(contract_id: str, field: str, value: object, *, required: bool) -> list[str]: - if value is None and not required: +def string_list(contract_id: str, field: str, value: object) -> list[str]: + if value is None: return [] - if not isinstance(value, list) or (required and not value): - qualifier = "non-empty " if required else "" - raise ValueError(f"contract {contract_id}: {field} must be a {qualifier}string array") + if not isinstance(value, list): + raise ValueError(f"contract {contract_id}: {field} must be a string array") if any(not isinstance(item, str) or not item.strip() for item in value): raise ValueError(f"contract {contract_id}: {field} must contain only non-empty strings") return value +def string_mapping(contract_id: str, field: str, value: object) -> dict[str, str]: + if value is None: + return {} + if not isinstance(value, dict) or not value: + raise ValueError(f"contract {contract_id}: {field} must be a non-empty string map") + if any(not isinstance(key, str) or not key.strip() for key in value): + raise ValueError(f"contract {contract_id}: {field} keys must be non-empty strings") + if any(not isinstance(item, str) or not item.strip() for item in value.values()): + raise ValueError(f"contract {contract_id}: {field} values must be non-empty strings") + return value + + +def markdown_section(text: str, heading: str) -> str | None: + pattern = re.compile( + rf"(?ms)^##\s+{re.escape(heading)}\s*$\n(.*?)(?=^##\s+|\Z)" + ) + match = pattern.search(text) + if match is None: + return None + return match.group(1) + + def validate(root: Path, manifest_path: Path) -> list[str]: manifest = load_manifest(manifest_path) problems: list[str] = [] @@ -80,7 +101,9 @@ def validate(root: Path, manifest_path: Path) -> list[str]: continue seen_ids.add(contract_id) - unknown = sorted(set(raw_contract) - {"id", "path", "required", "forbidden"}) + unknown = sorted( + set(raw_contract) - {"id", "path", "required", "forbidden", "exact_sections"} + ) if unknown: problems.append(f"contract {contract_id}: unsupported fields: {', '.join(unknown)}") continue @@ -88,17 +111,25 @@ def validate(root: Path, manifest_path: Path) -> list[str]: try: path = safe_repo_path(root, raw_contract.get("path")) required_phrases = string_list( - contract_id, "required", raw_contract.get("required"), required=True + contract_id, "required", raw_contract.get("required") ) forbidden_phrases = string_list( - contract_id, "forbidden", raw_contract.get("forbidden"), required=False + contract_id, "forbidden", raw_contract.get("forbidden") ) + exact_sections = string_mapping( + contract_id, "exact_sections", raw_contract.get("exact_sections") + ) + if not required_phrases and not exact_sections: + raise ValueError( + f"contract {contract_id}: requires required text or exact_sections" + ) except ValueError as error: problems.append(str(error)) continue try: - content = normalize(path.read_text(encoding="utf-8")) + raw_content = path.read_text(encoding="utf-8") + content = normalize(raw_content) except FileNotFoundError: problems.append(f"contract {contract_id}: missing file: {path.relative_to(root)}") continue @@ -116,6 +147,16 @@ def validate(root: Path, manifest_path: Path) -> list[str]: problems.append( f"contract {contract_id}: forbidden text present in {path.relative_to(root)}: {phrase}" ) + for heading, expected_body in exact_sections.items(): + actual_body = markdown_section(raw_content, heading) + if actual_body is None: + problems.append( + f"contract {contract_id}: missing section in {path.relative_to(root)}: {heading}" + ) + elif normalize(actual_body) != normalize(expected_body): + problems.append( + f"contract {contract_id}: section drift in {path.relative_to(root)}: {heading}" + ) return problems