@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
@@ -0,0 +1,239 @@
1
+ // The two path sets a run's receipt records: what the commit touched, and what
2
+ // the executed plan predicted it would touch.
3
+ //
4
+ // Both are read here rather than reported by an agent, and that is the whole
5
+ // point of the module. The commit step in `workflows/ship.js` is a model agent
6
+ // returning `{ok, sha, detail}`; `ship-run` already requires a run's tallies to
7
+ // be its *recorded* outcomes rather than an agent's account of them, and this
8
+ // set carries more weight than a tally because it is the denominator of
9
+ // `interlock report`'s sharpest indicator. An agent that mis-listed its own
10
+ // commit would move that number and nothing downstream could see it.
11
+ //
12
+ // Two properties are load-bearing, and they are the same two the rest of the
13
+ // corpus code holds:
14
+ //
15
+ // 1. NEVER THROWS. A missing git, a detached object, a pruned commit, a
16
+ // directory that is not a repository — each becomes an unobserved result
17
+ // carrying its reason. A reader whose own crash is the output is worse
18
+ // than no reader.
19
+ //
20
+ // 2. UNOBSERVED IS NOT EMPTY. A failed read returns `paths: null`, never
21
+ // `[]`. `[]` asserts that a commit touched nothing or that a plan
22
+ // predicted nothing, and the indicator these feed exists precisely to
23
+ // tell those two apart from "we never found out".
24
+ //
25
+ // Shelling out to `git` from `lib/` is precedented rather than novel:
26
+ // `lib/drift.mjs` already does it, with the same collapse-every-failure-to-null
27
+ // discipline.
28
+ //
29
+ // Exposed to skills and to both ship hosts as `interlock paths touched` and
30
+ // `interlock paths predicted`.
31
+
32
+ import { execFileSync } from 'node:child_process'
33
+ import { existsSync, readFileSync } from 'node:fs'
34
+ import { join } from 'node:path'
35
+ import { PLAN_PATH } from './plan-fingerprint.mjs'
36
+
37
+ /**
38
+ * A commit identifier we are willing to hand to git.
39
+ *
40
+ * Bounded, and rejected outright when it could be read as an option: this value
41
+ * arrives from a receipt, a CLI flag or an agent's report, and none of those is
42
+ * a trusted argument source. A rejected identifier is an unobserved read with a
43
+ * stated reason, never a git invocation we hope fails.
44
+ */
45
+ const SAFE_COMMIT = /^[A-Za-z0-9][A-Za-z0-9._/~^@{}-]{0,199}$/
46
+
47
+ function messageOf(err) {
48
+ return (err && err.message) || String(err)
49
+ }
50
+
51
+ function unobserved(reason, extra = {}) {
52
+ return { observed: false, paths: null, reason, ...extra }
53
+ }
54
+
55
+ /**
56
+ * The repository-relative paths one commit touched, read from version control.
57
+ *
58
+ * @param {string} root repository root the commit lives in
59
+ * @param {string} commit a commit identifier — the sha the receipt already carries
60
+ * @returns {{observed: boolean, paths: string[]|null, reason: string|null, commit: string|null}}
61
+ * `observed: true` carries the set, which MAY be empty when the commit
62
+ * genuinely touched nothing. `observed: false` carries `paths: null` and the
63
+ * reason — never a partial list, and never `[]`.
64
+ */
65
+ export function readTouchedPaths(root, commit) {
66
+ const id = typeof commit === 'string' ? commit.trim() : ''
67
+ try {
68
+ if (typeof root !== 'string' || !root.trim()) {
69
+ return unobserved('no repository root was given', { commit: id || null })
70
+ }
71
+ if (!id) {
72
+ return unobserved('the run recorded no commit identifier', { commit: null })
73
+ }
74
+ if (!SAFE_COMMIT.test(id)) {
75
+ return unobserved(`"${id.slice(0, 60)}" is not a usable git object name`, { commit: id })
76
+ }
77
+
78
+ // `diff-tree -r --name-only` against a single commit lists exactly the paths
79
+ // that commit changed, one per line, with no rename or status columns to
80
+ // parse. `--no-commit-id` drops the header line so every line is a path.
81
+ //
82
+ // `--root` and `-m` are the two commits that would otherwise print nothing
83
+ // and be recorded as a commit that touched no files: a repository's first
84
+ // commit has no parent to diff against, and a merge is skipped entirely
85
+ // unless it is asked for per parent. Both are rare in a ship run and both
86
+ // would be silently wrong, which is the combination worth two flags. `-m`
87
+ // repeats a path once per parent, so the set is deduplicated below.
88
+ const out = execFileSync('git', ['diff-tree', '--no-commit-id', '--name-only', '-r', '-m', '--root', id], {
89
+ cwd: root,
90
+ encoding: 'utf8',
91
+ stdio: ['ignore', 'pipe', 'ignore'],
92
+ timeout: 10000
93
+ })
94
+
95
+ const paths = [
96
+ ...new Set(
97
+ out
98
+ .split('\n')
99
+ .map(line => line.trim())
100
+ .filter(Boolean)
101
+ )
102
+ ]
103
+
104
+ return { observed: true, paths, reason: null, commit: id }
105
+ } catch (err) {
106
+ // Not a repository, git missing, an object git has never seen, a timeout.
107
+ // All four mean the same thing to a reader — the set was not observed — and
108
+ // the reason says which. A partial list is never returned: this command
109
+ // either prints the whole set or fails.
110
+ return unobserved(`the commit's paths could not be read from git: ${messageOf(err)}`, { commit: id })
111
+ }
112
+ }
113
+
114
+ // Every task in a plan, whatever shape the plan was persisted in.
115
+ //
116
+ // `planWaves` emits `waves[].batches[][]` (batch → lane → task) plus an optional
117
+ // `testWave`; a wave state persisted by `wave-state create` keeps the same
118
+ // nesting under one `waves` array. Both are walked here rather than in two
119
+ // readers, because "the executed plan" is whichever of the two the host can hand
120
+ // back, and a reader that understood only one would report the other unreadable.
121
+ function tasksOf(plan) {
122
+ const waves = []
123
+ if (Array.isArray(plan.waves)) waves.push(...plan.waves)
124
+ if (plan.testWave) waves.push(plan.testWave)
125
+
126
+ const tasks = []
127
+ for (const wave of waves) {
128
+ if (!wave || !Array.isArray(wave.batches)) continue
129
+ for (const batch of wave.batches) {
130
+ if (!Array.isArray(batch)) continue
131
+ for (const lane of batch) {
132
+ if (!Array.isArray(lane)) continue
133
+ for (const task of lane) {
134
+ if (task && typeof task === 'object') tasks.push(task)
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return tasks
140
+ }
141
+
142
+ /**
143
+ * The paths the executed plan predicted its tasks would touch: the union of
144
+ * `task.paths` across the plan, plus whether every task declared any.
145
+ *
146
+ * `paths` is optional per task by design — `lib/waves.mjs` states it: "a
147
+ * classifier that cannot predict which files a task will touch must be able to
148
+ * say nothing rather than guess". So the union alone cannot distinguish a plan
149
+ * that predicted nothing extra from a plan that declined to predict, and the
150
+ * completeness marker is not a convenience: without it the report would read a
151
+ * planner's silence as an implementer going off-plan.
152
+ *
153
+ * @param {string} root repository root
154
+ * @param {{planPath?: string, plan?: object}} [opts]
155
+ * `plan` supplies an already-loaded plan (or wave state) and skips the read;
156
+ * `planPath` overrides the default location, repo-relative.
157
+ * @returns {{
158
+ * observed: boolean, paths: string[]|null, complete: boolean|null,
159
+ * reason: string|null, tasks: number|null, predicting: number|null, path: string|null
160
+ * }}
161
+ */
162
+ export function readPredictedPaths(root, opts = {}) {
163
+ const rel =
164
+ opts && typeof opts.planPath === 'string' && opts.planPath.trim() ? opts.planPath.trim() : PLAN_PATH
165
+ const abs = typeof root === 'string' && root.trim() ? join(root, rel) : rel
166
+ const miss = (reason, path) => ({
167
+ observed: false,
168
+ paths: null,
169
+ complete: null,
170
+ reason,
171
+ tasks: null,
172
+ predicting: null,
173
+ path: path || null
174
+ })
175
+
176
+ try {
177
+ let plan = opts && opts.plan
178
+ if (!plan) {
179
+ if (typeof root !== 'string' || !root.trim()) return miss('no repository root was given', null)
180
+ if (!existsSync(abs)) return miss(`no executed plan was found at ${rel}`, abs)
181
+ plan = JSON.parse(readFileSync(abs, 'utf8'))
182
+ }
183
+ if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
184
+ return miss(`the executed plan at ${rel} is not a plan object`, abs)
185
+ }
186
+
187
+ const tasks = tasksOf(plan)
188
+ if (!tasks.length) {
189
+ // Observed, and observed to predict nothing. Reported incomplete rather
190
+ // than vacuously complete: a plan with no tasks predicted no path for any
191
+ // work the run did, and letting it qualify would score the run at zero
192
+ // fidelity for a reason that has nothing to do with the implementer.
193
+ return {
194
+ observed: true,
195
+ paths: [],
196
+ complete: false,
197
+ reason: `the executed plan at ${rel} declares no tasks, so no task declared paths`,
198
+ tasks: 0,
199
+ predicting: 0,
200
+ path: abs
201
+ }
202
+ }
203
+
204
+ const union = new Set()
205
+ let predicting = 0
206
+ for (const task of tasks) {
207
+ const declared = Array.isArray(task.paths) ? task.paths.filter(p => typeof p === 'string' && p.trim()) : []
208
+ if (declared.length) predicting++
209
+ for (const p of declared) union.add(p.trim())
210
+ }
211
+
212
+ const complete = predicting === tasks.length
213
+ return {
214
+ observed: true,
215
+ paths: [...union].sort(),
216
+ complete,
217
+ reason: complete
218
+ ? null
219
+ : `${tasks.length - predicting} of ${tasks.length} executed task(s) declared no paths, so the prediction is incomplete`,
220
+ tasks: tasks.length,
221
+ predicting,
222
+ path: abs
223
+ }
224
+ } catch (err) {
225
+ return miss(`the executed plan at ${rel} could not be read: ${messageOf(err)}`, abs)
226
+ }
227
+ }
228
+
229
+ /** Human-readable rendering of either reader, for the CLI. */
230
+ export function formatPathSet(label, result) {
231
+ if (!result.observed) {
232
+ return `${label} — UNOBSERVED: ${result.reason}\n`
233
+ }
234
+ const lines = [`${label} — ${result.paths.length} path(s) observed`]
235
+ for (const p of result.paths) lines.push(` ${p}`)
236
+ if (result.complete === false) lines.push(` incomplete: ${result.reason}`)
237
+ if (result.complete === true) lines.push(' every executed task declared its paths')
238
+ return lines.join('\n') + '\n'
239
+ }