diff --git a/.context/AGENT_PLAYBOOK.md b/.context/AGENT_PLAYBOOK.md index ab3aab9a7..ef97eea04 100644 --- a/.context/AGENT_PLAYBOOK.md +++ b/.context/AGENT_PLAYBOOK.md @@ -339,6 +339,60 @@ include: "Read `specs/feature-name.md` before starting any PX task." read the spec first. Don't rely on the task description alone: it's a summary, not the full design. +## Spec Verification Step (run before drafting every commit message) + +The CONSTITUTION says the `Spec:` trailer must be truthful — it points at +the design rationale this commit serves. The failure mode this procedure +prevents: reaching for the most-recent spec in working memory to satisfy +the gate when no spec genuinely covers the work. That's a violation even +though it syntactically passes. + +Before writing the commit message body, answer these two questions to +yourself out loud (in your reasoning, not in tool calls): + +1. **What is the spec for this work?** Name a specific file path. +2. **Why does it cover this commit specifically?** Articulate the overlap + between the spec's content and the diff in one sentence. If your + sentence hand-waves ("it's adjacent", "we touched something similar", + "it's the most recent"), the trailer is wrong. + +If you can't write a non-hand-waving answer to #2, the trailer is wrong. +Three correct responses, in order of preference: + +- **(a) Scaffold a fresh spec for this work.** Scale to the change — a + one-paragraph spec for a one-line fix is fine. Spec lives in `specs/`, + the trailer cites it, you proceed. +- **(b) Bundle this change into the next functional commit** so it + inherits that commit's legitimate spec. This is the right answer for + small chores that arose as part of larger work (a gitignore line that + surfaced while fixing a bug — fold it into the bug-fix commit). +- **(c) Cite `specs/meta/chores.md`** *if and only if* the diff fits one + of the explicitly listed chore categories there (gitignore additions, + lockfile bumps, formatting passes, typo fixes, mechanical file moves, + license-header updates). Citing the meta spec is a declarative claim + that the diff is in the chore class — anyone reviewing can verify by + inspecting the diff. + +Anti-patterns to avoid (these are the heuristic drifts that produce +wrong trailers): + +- **Reusing the previous commit's spec** because it's in working memory. + If the previous spec doesn't pass the verification step for this + commit, it's wrong even if it was right for the previous one. +- **Citing an adjacent spec** ("we modified files this spec mentions + tangentially"). Adjacency is not coverage; specs describe design + rationale, not file ownership. +- **Citing a spec that's in working memory because you just read it.** + Working memory ≠ relevance. +- **Improvising to satisfy the gate.** The gate is enforcing truthfulness; + improvisation defeats the gate's purpose even when it passes + syntactically. + +When in doubt — when the closest candidate spec is a stretch and you +aren't sure whether to scaffold — ask the user. The cost of one +clarifying question is much smaller than the cost of a fabricated +traceability link landing in `git log` permanently. + ## When to Consolidate vs Add Features **Signs you should consolidate first:** diff --git a/.context/CONSTITUTION.md b/.context/CONSTITUTION.md index b6c1fb9ad..0abe76b85 100644 --- a/.context/CONSTITUTION.md +++ b/.context/CONSTITUTION.md @@ -98,6 +98,19 @@ Leave the system in a better state than you found it. no exceptions, no "non-trivial" qualifier. Even one-liner fixes need a spec for traceability. Use `/ctx-commit` instead of raw `git commit`. + **The trailer must be truthful** — it points at the design + rationale this commit serves. Reaching for the most-recent + spec to satisfy the gate is a violation even though it + syntactically passes. For chore-class commits (gitignore + additions, lockfile bumps, formatting passes — full list in + `specs/meta/chores.md`) the correct trailer is either + (a) bundle into the next functional commit so the chore + inherits that commit's spec, or (b) cite `specs/meta/chores.md` + if a standalone chore commit is unavoidable. Other + improvisations (citing an adjacent spec, citing the previous + commit's spec) are violations. The Spec Verification Step in + `AGENT_PLAYBOOK.md` is the procedure that gates correct use. + Per `specs/spec-trailer-discipline.md`. - [ ] **Git is required.** Every `ctx` project must live in a git working tree. `ctx init` and every non-administrative subcommand refuse to operate when `/.git` is diff --git a/.context/LEARNINGS.md b/.context/LEARNINGS.md index 549239042..a106c60f7 100644 --- a/.context/LEARNINGS.md +++ b/.context/LEARNINGS.md @@ -17,6 +17,7 @@ DO NOT UPDATE FOR: | Date | Learning | |----|--------| +| 2026-05-23 | Spec-trailer improvisation is heuristic drift — when no spec genuinely fits, the failure mode is reaching for the most-recent one | | 2026-05-23 | Closing a stale TASKS.md item often means writing the test, not the code — verify before assuming the work is undone | | 2026-05-23 | Unicode block separation makes diacritic-stripping surgical — no per-script handling needed for Arabic/Indic/Hebrew/CJK | | 2026-05-22 | vitest's mocked `execFile` fires callbacks synchronously; real Node defers to `process.nextTick` — closure-capture patterns can TDZ-trap under the mock | @@ -159,6 +160,18 @@ DO NOT UPDATE FOR: --- +## [2026-05-23-100000] Spec-trailer improvisation is heuristic drift — when no spec genuinely fits, the failure mode is reaching for the most-recent one + +**Context**: Two commits on the `fix/journal-schema-drift` branch (a schema fix at `b84bc8e0` and a gitignore chore at `292e12ae`) both cited `ideas/spec-companion-intelligence.md` as their `Spec:` trailer. Neither commit had anything to do with companion intelligence (peer-MCP RAG integration). The agent had reached for that spec because it was the most recently mentioned spec in working memory from the previous commit's reasoning — not because it covered the work. The user caught the mismatch on review: "The spec you tagged has NOTHING TO DO with the commit." Audit of the session's trailers showed 2 genuinely wrong and ~4 stretches in 16 commits — a sustained drift pattern, not a one-off slip. + +**Lesson**: When the CONSTITUTION mandates a `Spec:` trailer on every commit AND a particular commit has no on-topic spec available, the agent's path-of-least-resistance heuristic converges on "cite the most recent spec from context" because the local cost (scaffold a new spec) is higher than the local benefit (gate passes). The convergence satisfies the syntactic check (trailer present) but defeats the rule's semantic intent (truthful traceability). This is "heuristic drift" in the gradient-descent sense: the optimizer found a path that minimizes friction but not the loss function the rule was meant to enforce. The drift is silent — the trailer looks fine in `git log` unless a reader opens the cited spec and discovers the mismatch. + +The deeper insight from this incident: session-scoped commitments ("I'll be more careful next time") do not survive across agent sessions. A fresh Claude Code session loads the project's persistent context (CONSTITUTION, AGENT_PLAYBOOK, LEARNINGS, files) but has no memory of any earlier session's self-imposed discipline. The structural fix must therefore live in persistent context, not in agent intention. + +**Application**: When the closest candidate spec is the same as the previous commit's spec AND the work is qualitatively different, treat that as a red flag and stop. The Spec Verification Step in `AGENT_PLAYBOOK.md` (added 2026-05-23 in commit landing this learning) is the procedure: name the spec, articulate the overlap in one non-hand-waving sentence, and if you can't, choose one of three correct responses — scaffold a fresh spec, bundle the change into the next functional commit, or cite `specs/meta/chores.md` if the diff fits an explicitly listed chore category. Improvisation is no longer an option because the playbook closes that door. The CONSTITUTION's spec-trailer rule (`CONSTITUTION.md` Process Invariants) now also names the chore escape hatch and the verification gate explicitly. Both changes serve the same goal: remove the conditions under which improvisation can happen in the first place. See `specs/spec-trailer-discipline.md` for the design rationale. + +--- + ## [2026-05-23-003000] Closing a stale TASKS.md item often means writing the test, not the code — verify before assuming the work is undone **Context**: TASKS.md line 375 ("Improve hub failover client: distinguish auth errors from connection errors") had been open since 2026-04-08. On triage, `internal/hub/failover.go:61-63` already called `authErr(callErr)` and returned immediately on Unauthenticated/PermissionDenied; `internal/hub/err_check.go:22-30` `authErr()` checked exactly those two codes. The behavior was implemented in the original failover feature commit (8bcb6208) without the task being closed. But the test suite never asserted the invariant — three existing failover tests covered happy path, skip-bad-peer, and all-bad-peers, none of them exercised "auth fails → walk stops". A future refactor could have silently deleted the auth-fast-fail branch and all three would still pass. Commit 22cffc27 added `TestFailoverClient_FailsFastOnAuthError` and closed the task. diff --git a/.gitignore b/.gitignore index defc441bd..62dc7de14 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,11 @@ dist/ .context/site/ .context/site-config/ +# Generated reports (schema drift, audit dumps, etc.). Operator-local; +# regenerated on demand by the relevant `ctx ... check` / `ctx ... report` +# command. No need to version. +.context/reports/ + # VS Code extension build artifacts editors/vscode/node_modules/ editors/vscode/dist/ diff --git a/internal/config/schema/field.go b/internal/config/schema/field.go index fe92ad859..503d3b168 100644 --- a/internal/config/schema/field.go +++ b/internal/config/schema/field.go @@ -16,16 +16,24 @@ var RequiredFields = []string{ } // Optional fields that may appear on message records. +// +// Grouped by concept for review; order is not load-bearing. +// New fields enter at the end of their concept group with a +// brief note on the version that introduced them when known. var OptionalFields = []string{ "gitBranch", "slug", "requestId", "thinkingMetadata", "todos", "permissionMode", "logicalParentUuid", "isMeta", "compactMetadata", "isVisibleInTranscriptOnly", "isCompactSummary", + "interruptedMessageId", // CC ≥ 2.1.~100: tracks parent of an interrupt "agentId", "teamName", "agentName", "agentColor", "promptId", "entrypoint", "agentSetting", + // CC ≥ 2.1.~110: skill/plugin invocation provenance. + "attributionPlugin", "attributionSkill", "sourceToolAssistantUUID", "toolUseResult", "sourceToolUseID", "origin", "planContent", "isApiErrorMessage", "error", "apiError", + "apiErrorStatus", "errorDetails", // CC ≥ 2.1.~120: richer API-error envelope } // JSONL record type values. diff --git a/internal/config/schema/report.go b/internal/config/schema/report.go index b0a55d51d..c862e512b 100644 --- a/internal/config/schema/report.go +++ b/internal/config/schema/report.go @@ -70,8 +70,17 @@ const ( // Schema version and CC version range. const ( - // Version is the current schema version. - Version = "1.0.0" + // Version is the current schema version. Bumped to + // 1.1.0 on 2026-05-23 when five new optional fields + // (interruptedMessageId, attributionPlugin, + // attributionSkill, apiErrorStatus, errorDetails) + // were added to OptionalFields. MINOR bump because + // adding optional fields is backwards-compatible + // per semver \u2014 old records still validate. + Version = "1.1.0" // CCVersionRange is the CC version range tested. - CCVersionRange = "2.1.2\u20132.1.92" + // 2.1.150 is the version observed in user-submitted + // drift reports; CC versions in between added the new + // fields incrementally. + CCVersionRange = "2.1.2\u20132.1.150" ) diff --git a/internal/journal/schema/schema_test.go b/internal/journal/schema/schema_test.go index d27fd036e..527d191fd 100644 --- a/internal/journal/schema/schema_test.go +++ b/internal/journal/schema/schema_test.go @@ -66,6 +66,37 @@ func TestKnownField(t *testing.T) { } } +// TestKnownField_PostV1FieldDrift pins the optional fields +// added to OptionalFields after the initial 1.0.0 schema — +// guards against silent regression of the drift-fix that +// landed on 2026-05-23. Each field name here corresponds to +// JSONL data observed in user-submitted journals from +// Claude Code versions beyond the 2.1.92 range covered by +// 1.0.0. If a future refactor drops one of these from +// OptionalFields, this test fires immediately and the +// schema-drift CLI starts complaining about it again. +func TestKnownField_PostV1FieldDrift(t *testing.T) { + s := Default() + for _, field := range []string{ + "interruptedMessageId", + "attributionPlugin", + "attributionSkill", + "apiErrorStatus", + "errorDetails", + } { + t.Run(field, func(t *testing.T) { + for _, rt := range []string{"user", "assistant"} { + if !s.KnownField(rt, field) { + t.Errorf( + "%q should be known for %q (post-1.0 drift fix)", + field, rt, + ) + } + } + }) + } +} + func TestKnownRecordType(t *testing.T) { s := Default() diff --git a/specs/fix-journal-schema-drift.md b/specs/fix-journal-schema-drift.md new file mode 100644 index 000000000..d5a7729c9 --- /dev/null +++ b/specs/fix-journal-schema-drift.md @@ -0,0 +1,119 @@ +# Absorb post-1.0 Claude Code Journal Field Drift + +`ctx journal schema check` reported five unknown fields +in user JSONL records: + +- `apiErrorStatus` +- `attributionPlugin` +- `attributionSkill` +- `errorDetails` +- `interruptedMessageId` + +All five are optional fields Claude Code added to message +records in versions between 2.1.92 (end of the schema's +1.0.0 declared range) and 2.1.150 (the version users are +running today). The schema was ~60 minor versions stale +relative to the surface it was validating against. + +## Problem + +The 1.0.0 schema baseline was empirically derived from CC +versions 2.1.2–2.1.92 (`internal/config/schema/report.go` +constants `Version` and `CCVersionRange`). Field +expectations live in `internal/config/schema/field.go` +as `RequiredFields` and `OptionalFields` string slices. +When CC ships new optional fields, the schema check +flags them as drift until they're added to +`OptionalFields`. The check is a *passive* drift detector +— it does not auto-extend the schema — so each new field +requires a one-line addition plus a version bump. + +The lag here was ~6 weeks of CC release cadence. Symptoms: + +``` +Schema drift detected in 27 file(s): + Unknown fields: apiErrorStatus, attributionPlugin, + attributionSkill, errorDetails, interruptedMessageId +``` + +End-users see this every time they run `ctx agent`, +`ctx journal source`, or any code path that triggers the +schema check. The noise is harmless (the parser still +ingests the records) but trains operators to ignore +schema-check output, which is exactly the failure mode +the check is meant to prevent. + +## Solution + +1. **Append the five fields to `OptionalFields`** in + `internal/config/schema/field.go`, grouped by concept + (error context, attribution, message metadata) with + brief version-provenance comments. +2. **Bump `Version` 1.0.0 → 1.1.0** — MINOR per semver, + because adding optional fields is backwards-compatible + (records written against 1.0.0 still validate cleanly). +3. **Bump `CCVersionRange` end 2.1.92 → 2.1.150** to + reflect the version range the new field set covers. +4. **Pin the new fields with a regression test** + (`TestKnownField_PostV1FieldDrift` in + `internal/journal/schema/schema_test.go`) so a future + refactor that drops one of these from `OptionalFields` + fires immediately rather than re-surfacing as noise in + user terminals. +5. **Gitignore generated reports** + (`.context/reports/`). The schema check writes a + detailed report under `.context/reports/schema-drift.md` + that's operator-local generated output, matching the + established ignore pattern for `.context/logs/`, + `.context/state/`, `.context/journal/`, etc. Without + the ignore, a `git add -A` could leak the report into + commits. + +## Policy: additive field drift handling + +For future CC field-drift fixes that match the same +shape (new optional field appears, schema check flags it): + +- **MINOR bump** if the addition is backwards-compatible + (the common case for new optional fields). +- **MAJOR bump** if a field is removed or made required + retroactively (rare; would also need migration logic). +- **PATCH bump** for fixes to existing field handling + that don't change the declared field set. +- **CCVersionRange end** always bumps to the latest CC + version observed in the user-submitted drift report, + even if we can't pin the exact CC version that + introduced each field. (We don't run CC's full release + archaeology; the range marks "we've seen records from + CC versions up to N and they validate cleanly.") +- **Regression test** for each new field added: pin + `KnownField(rt, field) == true` so a future "let's + tidy" refactor can't silently drop the field and + re-introduce the drift. + +## Out of Scope + +- **Auto-extending schema** on drift detection. The + passive design is intentional — operators see drift, + decide whether to upgrade ctx or pin to a specific CC + version, and the maintainer team owns the per-version + evaluation. Auto-extension would silently mask + potentially-meaningful CC behavior changes. +- **Field-level type validation** (e.g., asserting + `apiErrorStatus` is a string, not a number). The + current check is field-presence only. A typed + validation pass is a separate concern. +- **Schema-version negotiation** in the parser. The + parser is permissive of unknown fields; only the + `check` command flags them. No negotiation needed. + +## Verification + +- `go test ./internal/journal/schema/...` — + `TestKnownField_PostV1FieldDrift` passes for each of + the five new fields against both `user` and + `assistant` record types. +- After install, `ctx journal schema check` reports zero + drift on user journal directories. +- `make lint` clean (one line-length wrap was needed to + fit a doc comment under 80 chars). diff --git a/specs/meta/chores.md b/specs/meta/chores.md new file mode 100644 index 000000000..fd7916ee9 --- /dev/null +++ b/specs/meta/chores.md @@ -0,0 +1,95 @@ +# Chore-Class Commits: The Trailer Escape Hatch + +A commit cites this spec **only** if its entire diff fits +one of the explicitly listed chore categories below. +Anything that doesn't fit a category needs its own spec +(or bundling into a functional commit that has one). + +## Eligible chore categories + +A commit may cite `specs/meta/chores.md` if and only if +the diff is entirely one of: + +1. **Ignore-file additions.** New entries in `.gitignore`, + `.dockerignore`, or similar — adding patterns to filter + generated output that shouldn't be versioned. Removing + entries is *not* a chore (it's a policy change about + what becomes tracked). +2. **Dependency manifest bumps with no code change.** + `go.mod`/`go.sum`/`package-lock.json` updates from + `go get -u`, `npm update`, dependabot, or equivalent — + no companion code changes required. If a bump requires + a code change (API rename, breaking change adaptation), + it's a functional commit and needs its own spec. +3. **Formatting passes.** `gofmt`, `prettier`, `goimports` + normalization across files. No logic changes. Whitespace + only. +4. **Typo / spelling fixes in comments or docs.** Single- + letter or word-level corrections to comments, + docstrings, README copy, error messages. *Not* in spec + content (specs warrant their own commit), *not* in + identifier names (renames are functional changes). +5. **Mechanical file moves with no logic change.** `git mv` + operations where the moved file's content is unchanged + except for import-path updates required by the new + location. Renames that change behavior or interface + are functional. +6. **License header / copyright year updates.** Bulk + replacement of the project license header or copyright + year across files. No code change beyond the header. + +## Ineligible (require their own spec or bundling) + +- Bug fixes of any size — even one-liners. +- Test additions, even regression-pin tests for already- + working behavior. +- Configuration changes that alter runtime behavior + (env vars, YAML defaults, RC keys). +- Documentation that adds new content (vs. fixing typos + in existing content). +- Refactors, however small. "Renamed for clarity" is a + functional change. + +## Usage pattern + +``` +chore(gitignore): ignore .context/reports/ + + + +Spec: specs/meta/chores.md +``` + +The body still needs to explain *what* the change is and +*why* — citing the meta spec doesn't excuse a vague +commit message. The trailer just resolves the +"every-commit-needs-a-spec" requirement honestly when +there's no specific design rationale to point at. + +## Anti-pattern + +If you find yourself splitting a functional change into +"a fix + a chore that's needed for the fix to work," +that's a sign the chore should be bundled into the +functional commit, not standalone. The functional +commit's spec covers the chore. The chore class is for +genuinely-standalone changes that don't have a parent. + +## Audit + +Periodically — at release time or during PR review — +scan `git log --grep="Spec: specs/meta/chores.md"` and +confirm each cited commit's diff is in the eligible +categories. Misuse drives the threshold for cleanup; if +the meta spec gets cited for non-chore commits, the +fix is to push back at PR time, not to expand the +category list. + +## See Also + +- `CONSTITUTION.md` — Process Invariants, "Every commit + references a spec" rule with the chore escape hatch. +- `AGENT_PLAYBOOK.md` — Spec Verification Step procedure + that gates use of this escape hatch. +- `specs/spec-trailer-discipline.md` — design rationale + for why this escape hatch exists. diff --git a/specs/spec-trailer-discipline.md b/specs/spec-trailer-discipline.md new file mode 100644 index 000000000..558bb8454 --- /dev/null +++ b/specs/spec-trailer-discipline.md @@ -0,0 +1,137 @@ +# Spec Trailer Discipline: Close the Chore-Class Improvisation Gap + +The CONSTITUTION requires every commit to carry a +`Spec: specs/.md` trailer with no exceptions. In +practice, agents working under this rule will hit +chore-class commits (gitignore additions, lockfile bumps, +formatting fixes) that don't have an on-topic spec — and +will improvise by reaching for the most recently mentioned +spec, fabricating traceability that the rule was meant +to prevent. + +This spec captures the discipline that closes the gap. + +## Problem + +The current CONSTITUTION text (line 97): + +> Every commit references a spec (`Spec: specs/.md` +> trailer): no exceptions, no "non-trivial" qualifier. +> Even one-liner fixes need a spec for traceability. + +The rule's intent is *truthful* traceability — commit → +design rationale. But the rule offers no answer for the +case where a small change genuinely doesn't merit a +dedicated spec (a one-line gitignore addition; a lockfile +bump; a chore commit caught between two functional +commits). Under that pressure, the agent's +path-of-least-resistance heuristic converges on "reuse +the most recent spec you remember, even if it's +unrelated". That convergence: + +- Satisfies the syntactic gate (trailer is present). +- Fails the semantic intent (trailer points at unrelated + design rationale). +- Is hard to catch at review time without reading every + cited spec. +- Trains future agents (via context windows containing + the bad trailers) that the loose pattern is acceptable. + +Concrete incident: 2026-05-23, two commits on +`fix/journal-schema-drift` (a schema fix and a gitignore +chore) both cited `ideas/spec-companion-intelligence.md` +which has nothing to do with either. The cause was the +agent reaching for whatever spec was in working memory +rather than scaffolding or bundling. + +## Solution + +Three coordinated changes — only the third is enforceable +across agent sessions; the first two are scaffolding so +the third has correct content to draw on. + +### 1. Define a meta spec for chore-class commits + +`specs/meta/chores.md` becomes the legitimate trailer +target for the class of commits that don't merit a +dedicated spec: gitignore additions, lockfile / dependency +bumps, formatting passes, typo fixes, file renames with +no logic change. Citing it is a *declarative claim* that +the commit is in the chore class — anyone reviewing can +verify by inspecting the diff. + +The meta spec lists the chore categories explicitly so +the boundary isn't a judgment call. Commits that don't +fit the listed categories cannot use the meta spec — +they need scaffolding or bundling. + +### 2. Update the CONSTITUTION rule with the escape hatch + +Line 97 expands from "every commit, no exceptions" to +"every commit; chore-class changes bundle into the next +functional commit if possible, otherwise cite +`specs/meta/chores.md`". The invariant stays absolute +(trailer always present, always truthful); the rule +acknowledges the chore case explicitly so there's no +improvisation room. + +### 3. Add a Spec Verification Step to the playbook + +`AGENT_PLAYBOOK.md` gains an explicit procedure that the +agent must run before drafting a commit message: + +1. Identify the spec for this work. +2. Articulate in one sentence why the spec covers this + commit — what overlap exists between the spec's + content and the diff? +3. If the answer hand-waves, the trailer is wrong. Three + correct responses: (a) scaffold a fresh spec for this + work; (b) bundle the change into the next functional + commit; (c) cite `specs/meta/chores.md` if and only if + the diff fits a listed chore category. + +The verification step is the structural fix because it +lives in the playbook — i.e., in persistent project +context that every future agent session loads. A +session-scoped "I'll be more careful" commitment evaporates +at the next session boundary; a playbook step persists. + +## Out of Scope + +- Pre-commit hook that fuzzy-matches the cited spec + against the diff. Useful as a backstop but not in this + spec — the playbook-level discipline should suffice for + the common case, and a hook is a separate, additive + layer. +- Retroactive audit of historical commits with + questionable trailers. The fix points forward; the + one egregious case in the unpushed branch was already + rewritten (commit e64a5037). +- Tightening the chore taxonomy beyond the initial list + in `specs/meta/chores.md`. The list will evolve as + edge cases surface; the meta spec is the place to + refine the boundary, not this one. + +## Verification + +- `CONSTITUTION.md` line 97 reads with the escape hatch + explicitly stated. +- `AGENT_PLAYBOOK.md` includes the Spec Verification + Step as numbered procedure. +- `specs/meta/chores.md` exists with explicit chore + categories. +- A LEARNINGS entry pins the 2026-05-23 incident so + future agents see the failure mode in their loaded + context. + +## Why this spec, not just amend the constitution? + +The constitution change is mechanical (~3 lines). The +playbook change is procedural (~10 lines). But the +*decision* — that improvisation is a real failure mode +and the fix is verification + escape hatch — needs a +home that future agents can re-read in full when they +hit the same pressure. That's what this spec does. The +constitution rule names what to do; the playbook +procedure names how to do it; this spec explains why the +mechanism exists at all.