@renzrollon/interlock 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +58 -0
  3. package/CHANGELOG.md +380 -0
  4. package/LICENSE +21 -0
  5. package/README.md +358 -0
  6. package/agents/ping.md +8 -0
  7. package/agents/worker.md +8 -0
  8. package/bin/interlock +2655 -0
  9. package/bin/interlock-graph +6 -0
  10. package/bin/interlock-run +508 -0
  11. package/bin/interlock-ship-acp +39 -0
  12. package/hooks/_shared.mjs +162 -0
  13. package/hooks/guard-commit.mjs +77 -0
  14. package/hooks/guard-tasks.mjs +107 -0
  15. package/hooks/guard-tests.mjs +63 -0
  16. package/hooks/preflight.mjs +107 -0
  17. package/lib/artifacts.mjs +316 -0
  18. package/lib/autonomy.mjs +149 -0
  19. package/lib/conformance.mjs +132 -0
  20. package/lib/doctor.mjs +1176 -0
  21. package/lib/drift.mjs +409 -0
  22. package/lib/eval-history.mjs +304 -0
  23. package/lib/evals-calibrate.mjs +317 -0
  24. package/lib/evals-capture.mjs +437 -0
  25. package/lib/evals-promote.mjs +257 -0
  26. package/lib/evals-triage.mjs +216 -0
  27. package/lib/findings.mjs +340 -0
  28. package/lib/graph/cli.mjs +266 -0
  29. package/lib/graph/extract/config-json.mjs +74 -0
  30. package/lib/graph/extract/create-extractor.mjs +35 -0
  31. package/lib/graph/extract/memory.mjs +99 -0
  32. package/lib/graph/extract/openspec.mjs +80 -0
  33. package/lib/graph/extract/python.mjs +137 -0
  34. package/lib/graph/extract/regex-fallback.mjs +187 -0
  35. package/lib/graph/extract/shell.mjs +71 -0
  36. package/lib/graph/extract/typescript.mjs +217 -0
  37. package/lib/graph/extract/walk.mjs +272 -0
  38. package/lib/graph/format.mjs +51 -0
  39. package/lib/graph/graph/build.mjs +374 -0
  40. package/lib/graph/graph/query.mjs +249 -0
  41. package/lib/graph/graph/report.mjs +122 -0
  42. package/lib/graph/graph/update.mjs +47 -0
  43. package/lib/graph/paths.mjs +66 -0
  44. package/lib/graph/retrieve/context.mjs +86 -0
  45. package/lib/graph/retrieve/docs-index.mjs +116 -0
  46. package/lib/graph/retrieve/markdown.mjs +246 -0
  47. package/lib/host/acp.mjs +551 -0
  48. package/lib/host/claude-cli.mjs +285 -0
  49. package/lib/host/codex.mjs +231 -0
  50. package/lib/host/model-map.mjs +129 -0
  51. package/lib/host/qwen.mjs +176 -0
  52. package/lib/host/registry.mjs +213 -0
  53. package/lib/host.mjs +393 -0
  54. package/lib/ledger.mjs +444 -0
  55. package/lib/limits.mjs +525 -0
  56. package/lib/locators.mjs +99 -0
  57. package/lib/merge-lanes.mjs +148 -0
  58. package/lib/metrics.mjs +99 -0
  59. package/lib/notify.mjs +229 -0
  60. package/lib/outcomes.mjs +560 -0
  61. package/lib/plan-fingerprint.mjs +570 -0
  62. package/lib/project-slug.mjs +18 -0
  63. package/lib/prompts/commit.mjs +26 -0
  64. package/lib/prompts/dimensions.mjs +70 -0
  65. package/lib/prompts/handoff.mjs +86 -0
  66. package/lib/prompts/implementer.mjs +150 -0
  67. package/lib/prompts/index.mjs +250 -0
  68. package/lib/prompts/planner.mjs +70 -0
  69. package/lib/prompts/remediate.mjs +137 -0
  70. package/lib/prompts/replan.mjs +23 -0
  71. package/lib/prompts/review.mjs +107 -0
  72. package/lib/prompts/schemas.mjs +206 -0
  73. package/lib/prompts/stage.mjs +48 -0
  74. package/lib/prompts/verify.mjs +88 -0
  75. package/lib/ready.mjs +812 -0
  76. package/lib/receipt.mjs +434 -0
  77. package/lib/remediate.mjs +275 -0
  78. package/lib/report-html.mjs +623 -0
  79. package/lib/report.mjs +1124 -0
  80. package/lib/review-core.mjs +713 -0
  81. package/lib/risk.mjs +370 -0
  82. package/lib/run-log.mjs +1004 -0
  83. package/lib/run-paths.mjs +239 -0
  84. package/lib/run.mjs +2921 -0
  85. package/lib/ship-stage.mjs +223 -0
  86. package/lib/spill.mjs +150 -0
  87. package/lib/surface.mjs +195 -0
  88. package/lib/verify.mjs +1074 -0
  89. package/lib/waves.mjs +2661 -0
  90. package/package.json +40 -0
  91. package/shared/CONTEXT-HYGIENE.md +84 -0
  92. package/shared/DECISION-LEDGER.md +141 -0
  93. package/shared/EXPLORE-BRIEF.md +96 -0
  94. package/shared/INVARIANT-SWEEP.md +57 -0
  95. package/shared/SESSION-SHAPES.md +261 -0
  96. package/shared/SKILL-CANDIDATE-BRIEF.md +106 -0
  97. package/shared/TEST-PROFILE.md +204 -0
  98. package/shared/TOOL-ECONOMY.md +155 -0
  99. package/skills/bootstrap/SKILL.md +226 -0
  100. package/skills/commit/SKILL.md +99 -0
  101. package/skills/dispatch/SKILL.md +78 -0
  102. package/skills/docs-digest/SKILL.md +109 -0
  103. package/skills/evals/SKILL.md +160 -0
  104. package/skills/explain-code/SKILL.md +42 -0
  105. package/skills/explain-code/commit-teachin.md +136 -0
  106. package/skills/explain-code/module-walkthrough.md +358 -0
  107. package/skills/explore/SKILL.md +159 -0
  108. package/skills/fix-tests/SKILL.md +120 -0
  109. package/skills/graph/SKILL.md +136 -0
  110. package/skills/manual-test-plan/SKILL.md +121 -0
  111. package/skills/mr/SKILL.md +170 -0
  112. package/skills/report/SKILL.md +121 -0
  113. package/skills/review-artifacts/SKILL.md +127 -0
  114. package/skills/review-code/REVIEW.template.md +49 -0
  115. package/skills/review-code/SKILL.md +156 -0
  116. package/skills/review-code/dimensions/architecture.md +10 -0
  117. package/skills/review-code/dimensions/devops.md +12 -0
  118. package/skills/review-code/dimensions/language.md +12 -0
  119. package/skills/review-code/dimensions/qa.md +12 -0
  120. package/skills/review-code/dimensions/security.md +12 -0
  121. package/skills/review-code/dimensions/technical-lead.md +19 -0
  122. package/skills/ship/SKILL.md +67 -0
  123. package/skills/spec/SKILL.md +264 -0
  124. package/skills/spec/continuity.md +66 -0
  125. package/workflows/ship.js +565 -0
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@renzrollon/interlock",
3
+ "version": "1.0.0",
4
+ "description": "Autonomous spec-driven development orchestration for Claude Code, layered on OpenSpec",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "bin": {
10
+ "interlock": "./bin/interlock",
11
+ "interlock-graph": "./bin/interlock-graph",
12
+ "interlock-run": "./bin/interlock-run",
13
+ "interlock-ship-acp": "./bin/interlock-ship-acp"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "lib",
18
+ "workflows",
19
+ "skills",
20
+ "hooks",
21
+ "agents",
22
+ "shared",
23
+ ".claude-plugin",
24
+ "README.md",
25
+ "LICENSE",
26
+ "CHANGELOG.md"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "test": "node --test $(find test -name '*.test.mjs' | sort)",
33
+ "validate": "claude plugin validate . --strict"
34
+ },
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/renzrollon/interlock.git"
39
+ }
40
+ }
@@ -0,0 +1,84 @@
1
+ # Context Hygiene — `[[VARIABLE_NAME]]` Placeholder Pattern
2
+
3
+ Shared discipline for all interlock skills that embed user-supplied strings in downstream prompts.
4
+
5
+ ## The problem
6
+
7
+ Agents fill missing context with any string in their active window — leaking PII, credentials, deprecated endpoints, or plausible-but-wrong values. This is **context hallucination**: gap-filling from ambient context rather than verified sources.
8
+
9
+ Hard-coded prompt rules ("don't leak PII") are insufficient. Contexts overflow; prompt injection bypasses inline constraints. The fix is **structural** — wrap sensitive values as opaque placeholders and let a resolver substitute them at call time.
10
+
11
+ ## The syntax
12
+
13
+ Wrap any user-supplied or ambient value that will appear inside a prompt template as `[[VARIABLE_NAME]]`:
14
+
15
+ ```
16
+ [[USER_PROMPT]] Original user request
17
+ [[CHANGE_NAME]] OpenSpec change slug (kebab-case)
18
+ [[REPO_ROOT]] Absolute path to the current repo
19
+ [[ISSUE_REF]] Extracted from branch (e.g., RD-65)
20
+ [[BASE_BRANCH]] Merge target (main / master / trunk)
21
+ [[SANITIZED_EMAIL]] Redacted email from user input
22
+ [[SANITIZED_TOKEN]] Redacted 32+ char alphanumeric token
23
+ [[SANITIZED_AUTH]] Redacted Bearer/Basic auth header
24
+ ```
25
+
26
+ Names are `SCREAMING_SNAKE_CASE`, enclosed in double brackets. No nesting, no expressions.
27
+
28
+ ## The rules
29
+
30
+ 1. **Wrap on emit, resolve on consume.** When a skill composes a downstream prompt, wrap sensitive values. The resolver runs immediately before the tool call.
31
+
32
+ 2. **Resolution order** (highest priority wins):
33
+ - Runtime state overrides (values captured this session)
34
+ - Environment variables of the same name
35
+ - Explicit `AskUserQuestion` prompt if the value is required
36
+
37
+ 3. **Unresolved placeholders stay visible.** Never silently substitute a plausible value. If `[[COMMENTER_EMAIL]]` reaches a tool call unresolved, halt and ask the user. Gap-filling is what this pattern exists to prevent.
38
+
39
+ 4. **Sanitize on ingress.** When a skill receives freeform user text that will be echoed downstream, redact obvious secrets to `[[SANITIZED_*]]` form immediately. Preserve the mapping in session state for later resolution.
40
+
41
+ 5. **Never round-trip through the model.** A resolved value must go directly from the resolver to the tool argument. Do not paste it into a prompt the model reads first.
42
+
43
+ ## Common patterns
44
+
45
+ **Composing a subagent prompt:**
46
+
47
+ ```
48
+ INVESTIGATE: [[USER_PROMPT]]
49
+ CHANGE: [[CHANGE_NAME]]
50
+ FILES TO READ:
51
+ - [[REPO_ROOT]]/openspec/changes/[[CHANGE_NAME]]/proposal.md
52
+ ```
53
+
54
+ **Handing to `gh pr create`:**
55
+
56
+ ```
57
+ --title "feat([[SCOPE]]): [[VERB_PHRASE]]"
58
+ --body "$(...contains [[SUMMARY]], [[RISK]], [[TEST_PLAN]]...)"
59
+ ```
60
+
61
+ **Detecting a leak in output:**
62
+
63
+ If model output contains a literal email, phone number, or long token that was NOT in the user's input, treat it as a hallucination. Do not commit, do not send. Halt and ask.
64
+
65
+ ## What NOT to wrap
66
+
67
+ Placeholders are for **values**, not concepts. Do not wrap:
68
+
69
+ - Verbs or actions (`[[ACTION]]` is meaningless)
70
+ - Boolean state (`[[IS_ACTIVE]]` — just use the boolean)
71
+ - Full sentences (`[[FULL_PROMPT]]` — defeats the purpose)
72
+ - Values already sourced from a schema-validated tool result (they're structured, not freeform)
73
+
74
+ ## Skill integration
75
+
76
+ Skills that emit prompts to subagents or external tools:
77
+
78
+ - **/interlock:spec** — wraps user description, change name, repo root before passing to `openspec new change` and downstream artifact generators.
79
+ - **/interlock:explore** — wraps user question and any file references before fanning out to Explore subagents.
80
+ - **/interlock:ship** — wraps goal and file lists in each subtask prompt.
81
+ - **/interlock:mr** — wraps branch name, issue ref, summary before invoking `glab mr create` / `gh pr create`.
82
+ - **dispatch** — sanitizes freeform user input (emails, tokens, auth headers) at ingress.
83
+
84
+ When adding a new skill that echoes user-supplied strings into a prompt template or tool call, follow the same pattern.
@@ -0,0 +1,141 @@
1
+ # Decision Ledger — Continuity Contract
2
+
3
+ Durable record of every ambiguity a change hit and how it was settled. Lives
4
+ with the change so it survives `/clear`, prompt-cache expiry, and a handoff to
5
+ a different agent — and so a machine can read it.
6
+
7
+ **Path:** `openspec/changes/<change>/decisions.md`
8
+ Versioned with the change; committed alongside `proposal.md` / `design.md` /
9
+ `tasks.md`.
10
+
11
+ **Why a file and not chat:** a decision that exists only in chat does not
12
+ exist. The readiness gate answers one question — *is there anything left here
13
+ that only a human can decide?* — and it answers it by reading this file.
14
+
15
+ ---
16
+
17
+ ## Template (stable — tooling depends on the five columns)
18
+
19
+ ```markdown
20
+ # Decisions — <change>
21
+
22
+ | id | question | class | resolution | evidence |
23
+ |----|----------|-------|------------|----------|
24
+ | D1 | Pin zod version? | needs_human | — | — |
25
+ | D2 | Use existing Session helper vs new | agent_resolved | Reuse lib/session.ts | explore brief §Critical Files |
26
+ ```
27
+
28
+ - `id` — `D1`, `D2`, … unique within the change. Stable: never renumber, and
29
+ never reuse an id for a different question.
30
+ - `question` — one line, answerable. Not a topic ("auth") but a decision
31
+ ("store the session in a cookie or in Redis?").
32
+ - `class` — exactly `needs_human` or `agent_resolved`. Nothing else.
33
+ - `resolution` — what was decided, in words.
34
+ - `evidence` — where the answer came from.
35
+
36
+ Use `—` for a cell with no value. `-` and an empty cell mean the same thing.
37
+ Escape any literal `|` inside a cell as `\|`.
38
+
39
+ ---
40
+
41
+ ## The two classes
42
+
43
+ | Class | Meaning |
44
+ |-------|---------|
45
+ | `needs_human` | Product, policy, pricing, legal, security-posture, pinned-version, or tenancy-boundary judgement the repo cannot answer. **Stops continuity.** |
46
+ | `agent_resolved` | Answered from the repo, the explore brief, or a stated default — and written down. |
47
+
48
+ There is no third class. A row that is neither is a malformed row.
49
+
50
+ **Route to `needs_human` when in doubt.** A wrong `agent_resolved` ships an
51
+ unreviewed product decision; a needless `needs_human` costs one question.
52
+
53
+ ---
54
+
55
+ ## When to write a row
56
+
57
+ **Explore** — before the brief is finished:
58
+
59
+ - Every item under `## Pending Clarifications` gets a `needs_human` row.
60
+ - Every item under `## Assumptions Made` gets an `agent_resolved` row, with the
61
+ evidence pointing at the brief section or the file that justified it.
62
+
63
+ **Spec** — while writing `proposal.md` / `design.md` / `tasks.md`:
64
+
65
+ - Every open question that surfaces mid-flight gets a row before it is acted
66
+ on — never resolve an ambiguity silently in the design prose.
67
+ - Every `agent_resolved` decision must also appear as a written assumption in
68
+ `design.md`, referenced by id (`D2`).
69
+ - Carry forward the rows explore wrote; do not start a fresh ledger.
70
+
71
+ **Answering a `needs_human` row:** edit the row in place — flip the class to
72
+ `agent_resolved`, write the human's answer into `resolution`, and cite the
73
+ human in `evidence` (for example `human decision 2026-08-12`). Do not delete
74
+ the row; the history of what needed a person is the point.
75
+
76
+ ---
77
+
78
+ ## The `agent_resolved` evidence requirement
79
+
80
+ `agent_resolved` is a **claim**, and it is audited.
81
+
82
+ `interlock ledger <change>` checks all three parts of the claim. A row is
83
+ **invalid** when it claims `agent_resolved` and any of these is true:
84
+
85
+ - `resolution` is empty (`—`, `-`, `n/a`, `tbd`, blank), or
86
+ - `evidence` is empty, or
87
+ - the row's `id` appears nowhere in the change's `design.md`.
88
+
89
+ An invalid row is treated as **unresolved** and blocks continuity exactly like
90
+ `needs_human`. Writing the word `agent_resolved` is not a decision; saying what
91
+ was decided and why is.
92
+
93
+ Good evidence is a pointer someone else can follow:
94
+
95
+ - `lib/session.ts:42` — the existing helper
96
+ - `explore brief §Critical Files`
97
+ - `openspec/specs/auth/spec.md §Scenario: expired token`
98
+ - `human decision 2026-08-12`
99
+
100
+ Not evidence: `obvious`, `standard practice`, `see above`, an empty cell. These
101
+ are matched as the **whole cell**, after casing, surrounding whitespace and a
102
+ trailing period are folded — so `Obvious.` is rejected the same way `obvious`
103
+ is, while `lib/session.ts:42 — the obvious existing helper` is real evidence and
104
+ is accepted.
105
+
106
+ **The `design.md` reference.** The id is matched as a token anywhere in
107
+ `design.md`, not as a heading: this contract does not say *where* in the design
108
+ a decision has to be recorded, only that it is. A `design.md` that is missing or
109
+ cannot be read makes every reference **unresolvable** — it is never treated as a
110
+ document that happens to contain every id.
111
+
112
+ ---
113
+
114
+ ## What blocks continuity
115
+
116
+ Continuity stops when **any** of these is true:
117
+
118
+ 1. One or more valid `needs_human` rows remain.
119
+ 2. One or more rows are invalid — bad class, missing id or question, wrong
120
+ column count, or an unsubstantiated `agent_resolved`.
121
+
122
+ 3. The ledger is **missing**, or present and unparseable.
123
+
124
+ A ledger that cannot be parsed is never reported as empty. An empty ledger
125
+ reads as "nothing needs a human", so unreadable rows are surfaced and blocking,
126
+ not dropped.
127
+
128
+ **An absent `decisions.md` is a failure of the audit, not an empty result.**
129
+ `interlock ledger` exits non-zero and says *missing* — distinguishably from
130
+ *present and empty*, and from *unparseable*. The likeliest way this audit fails
131
+ is that nobody wrote the file, so that is the case it refuses to pass. If a
132
+ change genuinely raised no ambiguity, write the ledger with its heading and no
133
+ rows: an empty ledger is a statement, an absent one is a silence.
134
+
135
+ ---
136
+
137
+ ## Presenting blocked rows
138
+
139
+ When decisions block, show **only** the blocking rows — id, question, and where
140
+ the artifacts are. Do not ask a human to re-read the whole spec; they are here
141
+ to answer questions, not to audit markdown.
@@ -0,0 +1,96 @@
1
+ # Explore Brief — Spec Contract
2
+
3
+ Durable handoff from `/interlock:explore` into `/interlock:spec`. Lives on disk
4
+ so `/interlock:spec` can reload discovery after prompt-cache expiry or `/clear`
5
+ without re-deriving it from chat.
6
+
7
+ **Path:** `.claude/handoff/explore-<slug>-<YYYYMMDD-HHMMSS>.md`
8
+ Slug = topic or suggested change name in kebab-case. Same handoff directory
9
+ across a context reset.
10
+
11
+ **When to write**
12
+
13
+ - **Required** in autonomous explore mode (exit criterion).
14
+ - **Optional** in conversational explore when the user asks to “save
15
+ exploration” / “handoff for spec”.
16
+
17
+ **Bound the brief** (~2–6k tokens): make it decision-oriented; cite paths and
18
+ line refs; prefer pointers over pasted code; ASCII diagrams when useful. The
19
+ brief carries conclusions into `/interlock:spec`, not an inventory of every
20
+ file the investigators touched.
21
+
22
+ ---
23
+
24
+ ## Template (stable — `/interlock:spec` depends on these headings)
25
+
26
+ ```markdown
27
+ # Explore Brief: <slug>
28
+
29
+ ## Meta
30
+ - mode: autonomous | conversational-capture
31
+ - topic: <one-line topic>
32
+ - created: <ISO-8601 timestamp>
33
+ - suggested_change_name: <kebab-case or none>
34
+ - related_change: <existing openspec change name or none>
35
+
36
+ ## Problem / Intent
37
+ <!-- Crystallized problem statement and desired outcome -->
38
+
39
+ ## Codebase Findings
40
+ <!-- Decision-relevant conclusions about how it works today, constraints and
41
+ patterns. Cite file:line spans; do not enumerate every file touched. -->
42
+
43
+ ## Critical Files
44
+ <!-- path:line — why it matters (citations over pasted code) -->
45
+
46
+ ## Options Considered
47
+ <!-- Each option: tradeoffs + evidence; mark recommended vs rejected -->
48
+
49
+ ## Recommended Direction
50
+ <!-- Chosen approach and why; pointer back to Options if needed -->
51
+
52
+ ## Assumptions Made
53
+ <!-- Defaults taken when product intent was ambiguous -->
54
+
55
+ ## Pending Clarifications
56
+ <!-- Only true product/policy unknowns that cannot be resolved from the repo.
57
+ In autonomous mode: list for the human later — do not AskUser mid-explore. -->
58
+
59
+ ## Risks / Gaps
60
+ <!-- What could go wrong; layers not swept; contradictions -->
61
+
62
+ ## Spec Ready Checklist
63
+ - [ ] Problem / intent is clear enough to name a change
64
+ - [ ] Recommended direction is stated (or options narrowed to ≤2)
65
+ - [ ] Critical files listed with why
66
+ - [ ] Assumptions recorded
67
+ - [ ] Residual unknowns are product/policy only (not “I didn’t look”)
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Section rules
73
+
74
+ | Section | Rule |
75
+ |---------|------|
76
+ | **Codebase Findings** | Conclusions + cited `file:line` spans only. Omit exhaustive file inventories and raw investigator output. |
77
+ | **Critical Files** | Paths + line refs + why. No large code dumps. |
78
+ | **Options Considered** | Tradeoffs + evidence. Mark recommended vs rejected with rationale. |
79
+ | **Pending Clarifications** | Repo-unresolvable items only. Autonomous mode lists them; does not ask mid-explore. |
80
+ | **Assumptions Made** | Prefer reasonable defaults over blocking; record them here. |
81
+ | **Spec Ready Checklist** | If unchecked items remain that block a correct proposal, say so in chat when pointing at the brief. |
82
+
83
+ ## Chat reply after write
84
+
85
+ Do **not** re-dump the full brief into chat. Reply with:
86
+
87
+ 1. Path to the brief
88
+ 2. A 3–5 line summary (intent + recommended direction + top risk)
89
+ 3. “Ready for `/interlock:spec`” (or the Copilot/VS Code equivalent)
90
+
91
+ ## Spec consumption
92
+
93
+ `/interlock:spec` auto-finds the latest matching brief under
94
+ `.claude/handoff/explore-*.md` (see that skill). Override: user passes an
95
+ explicit brief path. Prefer brief recommendations over re-exploring; reopen
96
+ code only for gaps or contradictions.
@@ -0,0 +1,57 @@
1
+ # Invariant Sweep — The Unit of Work Is the Invariant, Not the File
2
+
3
+ Shared discipline for all interlock skills that plan, review, or apply changes touching **shared or derived state**. Companion to `CONTEXT-HYGIENE.md`.
4
+
5
+ ## The problem
6
+
7
+ Every gate after exploration is scope-leashed by design — `/interlock:spec` sees only artifacts, `/interlock:review-code` / `/interlock:ship` see only the diff. That leash is correct for most changes and wrong for exactly one class: **a change that transforms a value read in more than one place.**
8
+
9
+ When a change normalizes, trims, encodes, or canonicalizes a value stored on shared/derived state, some code paths get updated and others don't. The updated paths are in the diff; the stale readers are not. A diff-scoped or artifact-scoped gate is **structurally blind** to the stale readers — it cannot see the bug, because the bug lives in a file the change never touched.
10
+
11
+ Concrete failure (the one that motivated this file): a dedup change lower-cased an email in the write path. Readers that still compared the **raw** email case-sensitively silently stopped matching. No BDD scenario forced enumeration of mixed-case consumers; no reviewer was licensed to leave the diff. Only an ad-hoc agent sweep — the one thing with no scope leash — caught it.
12
+
13
+ ## The rule
14
+
15
+ **When a change alters an invariant on shared or derived state, the unit of work is the invariant, not the file.** The change is not complete, and the review is not complete, until *every reader of that value* has been enumerated and confirmed to read the canonical form.
16
+
17
+ Two halves, both mandatory:
18
+
19
+ 1. **Normalize once at the boundary, not at each call site.** If a value must be transformed, the design MUST state where the single canonical transform lives, and assert every consumer reads the canonical form. Scattering the transform across call sites guarantees one gets missed.
20
+ 2. **Sweep the consumers.** Enumerate every reader repo-wide — not just the diff. Any reader still consuming the raw/untransformed form, or comparing it case/format-sensitively, is part of the change, not opportunistic creep.
21
+
22
+ ## When it fires
23
+
24
+ Fire the sweep when the change touches a value that is **stored on a shared object or derived and read elsewhere**. Signals:
25
+
26
+ - A session / JWT / token field, request context, or auth principal
27
+ - A cache key, map key, dedup key, or any identity used in a comparison, `Set`/`Map`, or `WHERE`/`===`
28
+ - A normalized form of user input: casing, trimming, encoding, Unicode normalization, ID/slug canonicalization
29
+ - Anything the design describes with "normalize", "canonical", "dedupe", "match", "lookup by"
30
+
31
+ If none of these apply — the change is genuinely local, one reader, no shared identity — the sweep does **not** fire, and the normal diff/artifact leash holds. Do not sweep every change; that defeats the leash's purpose.
32
+
33
+ ## How to sweep
34
+
35
+ **Structural + semantic — two layers**:
36
+
37
+ 1. **Structural (deterministic):** enumerate every reader of the value — the field name, its getter, the map/cache key, the comparison sites. Prefer the graph when present:
38
+ ```bash
39
+ interlock-graph consumers <field-or-symbol>
40
+ ```
41
+ Then `grep`/search the whole repo to catch **dynamic/stringly** readers the graph misses (bracket access, computed keys, SQL, codegen). Enumerate exhaustively. This is a mechanical enumeration, not a judgment call.
42
+ 2. **Semantic (judgment):** for each reader found, decide — does it consume the **canonical** form, or the **raw** form? A reader on the raw form, or a case/format-sensitive comparison, is a defect.
43
+
44
+ **When fanning out** (e.g. in `/interlock:explore` Pattern 4): assign **one agent per layer** — data / API / business-logic / frontend / tests — and make the layers **disjoint** so findings merge without overlap or double-counting. Seed each agent with the relevant `interlock-graph consumers` / module slice from `GRAPH_REPORT.md` when the graph exists. Each agent enumerates readers in its layer only; you merge and flag the raw-form readers in synthesis.
45
+
46
+ ## Severity
47
+
48
+ A reader consuming the raw form of a value the change canonicalized elsewhere is a **BLOCKER** — it is a live correctness bug with a data-dependent trigger (it fails only for the inputs that differ pre/post-transform, which is exactly the set no happy-path test exercises).
49
+
50
+ ## Skill integration
51
+
52
+ - **interlock-graph** — `consumers` is the structural first pass when `.claude/graph/graph.json` exists.
53
+ - **/interlock:explore** — Pattern 4 (Impact Analysis) is the mandatory fan-out for this class. It MUST fire on the signals above regardless of how the user phrased the question.
54
+ - **/interlock:spec** — the BDD edge-case categories include casing/encoding/whitespace/Unicode; the design principle "normalize once at the boundary" is mandatory for identity/shared-value changes.
55
+ - **review-code** — the "breaking API contracts" check is widened from shape to semantic value: graph consumers then grep repo-wide for raw-form readers and flag them BLOCKER. This gate is the licensed exception to the diff leash.
56
+
57
+ When adding a new skill that plans or reviews changes to shared/derived state, wire it to this discipline.