@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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +58 -0
- package/CHANGELOG.md +380 -0
- package/LICENSE +21 -0
- package/README.md +358 -0
- package/agents/ping.md +8 -0
- package/agents/worker.md +8 -0
- package/bin/interlock +2655 -0
- package/bin/interlock-graph +6 -0
- package/bin/interlock-run +508 -0
- package/bin/interlock-ship-acp +39 -0
- package/hooks/_shared.mjs +162 -0
- package/hooks/guard-commit.mjs +77 -0
- package/hooks/guard-tasks.mjs +107 -0
- package/hooks/guard-tests.mjs +63 -0
- package/hooks/preflight.mjs +107 -0
- package/lib/artifacts.mjs +316 -0
- package/lib/autonomy.mjs +149 -0
- package/lib/conformance.mjs +132 -0
- package/lib/doctor.mjs +1176 -0
- package/lib/drift.mjs +409 -0
- package/lib/eval-history.mjs +304 -0
- package/lib/evals-calibrate.mjs +317 -0
- package/lib/evals-capture.mjs +437 -0
- package/lib/evals-promote.mjs +257 -0
- package/lib/evals-triage.mjs +216 -0
- package/lib/findings.mjs +340 -0
- package/lib/graph/cli.mjs +266 -0
- package/lib/graph/extract/config-json.mjs +74 -0
- package/lib/graph/extract/create-extractor.mjs +35 -0
- package/lib/graph/extract/memory.mjs +99 -0
- package/lib/graph/extract/openspec.mjs +80 -0
- package/lib/graph/extract/python.mjs +137 -0
- package/lib/graph/extract/regex-fallback.mjs +187 -0
- package/lib/graph/extract/shell.mjs +71 -0
- package/lib/graph/extract/typescript.mjs +217 -0
- package/lib/graph/extract/walk.mjs +272 -0
- package/lib/graph/format.mjs +51 -0
- package/lib/graph/graph/build.mjs +374 -0
- package/lib/graph/graph/query.mjs +249 -0
- package/lib/graph/graph/report.mjs +122 -0
- package/lib/graph/graph/update.mjs +47 -0
- package/lib/graph/paths.mjs +66 -0
- package/lib/graph/retrieve/context.mjs +86 -0
- package/lib/graph/retrieve/docs-index.mjs +116 -0
- package/lib/graph/retrieve/markdown.mjs +246 -0
- package/lib/host/acp.mjs +551 -0
- package/lib/host/claude-cli.mjs +285 -0
- package/lib/host/codex.mjs +231 -0
- package/lib/host/model-map.mjs +129 -0
- package/lib/host/qwen.mjs +176 -0
- package/lib/host/registry.mjs +213 -0
- package/lib/host.mjs +393 -0
- package/lib/ledger.mjs +444 -0
- package/lib/limits.mjs +525 -0
- package/lib/locators.mjs +99 -0
- package/lib/merge-lanes.mjs +148 -0
- package/lib/metrics.mjs +99 -0
- package/lib/notify.mjs +229 -0
- package/lib/outcomes.mjs +560 -0
- package/lib/plan-fingerprint.mjs +570 -0
- package/lib/project-slug.mjs +18 -0
- package/lib/prompts/commit.mjs +26 -0
- package/lib/prompts/dimensions.mjs +70 -0
- package/lib/prompts/handoff.mjs +86 -0
- package/lib/prompts/implementer.mjs +150 -0
- package/lib/prompts/index.mjs +250 -0
- package/lib/prompts/planner.mjs +70 -0
- package/lib/prompts/remediate.mjs +137 -0
- package/lib/prompts/replan.mjs +23 -0
- package/lib/prompts/review.mjs +107 -0
- package/lib/prompts/schemas.mjs +206 -0
- package/lib/prompts/stage.mjs +48 -0
- package/lib/prompts/verify.mjs +88 -0
- package/lib/ready.mjs +812 -0
- package/lib/receipt.mjs +434 -0
- package/lib/remediate.mjs +275 -0
- package/lib/report-html.mjs +623 -0
- package/lib/report.mjs +1124 -0
- package/lib/review-core.mjs +713 -0
- package/lib/risk.mjs +370 -0
- package/lib/run-log.mjs +1004 -0
- package/lib/run-paths.mjs +239 -0
- package/lib/run.mjs +2921 -0
- package/lib/ship-stage.mjs +223 -0
- package/lib/spill.mjs +150 -0
- package/lib/surface.mjs +195 -0
- package/lib/verify.mjs +1074 -0
- package/lib/waves.mjs +2661 -0
- package/package.json +40 -0
- package/shared/CONTEXT-HYGIENE.md +84 -0
- package/shared/DECISION-LEDGER.md +141 -0
- package/shared/EXPLORE-BRIEF.md +96 -0
- package/shared/INVARIANT-SWEEP.md +57 -0
- package/shared/SESSION-SHAPES.md +261 -0
- package/shared/SKILL-CANDIDATE-BRIEF.md +106 -0
- package/shared/TEST-PROFILE.md +204 -0
- package/shared/TOOL-ECONOMY.md +155 -0
- package/skills/bootstrap/SKILL.md +226 -0
- package/skills/commit/SKILL.md +99 -0
- package/skills/dispatch/SKILL.md +78 -0
- package/skills/docs-digest/SKILL.md +109 -0
- package/skills/evals/SKILL.md +160 -0
- package/skills/explain-code/SKILL.md +42 -0
- package/skills/explain-code/commit-teachin.md +136 -0
- package/skills/explain-code/module-walkthrough.md +358 -0
- package/skills/explore/SKILL.md +159 -0
- package/skills/fix-tests/SKILL.md +120 -0
- package/skills/graph/SKILL.md +136 -0
- package/skills/manual-test-plan/SKILL.md +121 -0
- package/skills/mr/SKILL.md +170 -0
- package/skills/report/SKILL.md +121 -0
- package/skills/review-artifacts/SKILL.md +127 -0
- package/skills/review-code/REVIEW.template.md +49 -0
- package/skills/review-code/SKILL.md +156 -0
- package/skills/review-code/dimensions/architecture.md +10 -0
- package/skills/review-code/dimensions/devops.md +12 -0
- package/skills/review-code/dimensions/language.md +12 -0
- package/skills/review-code/dimensions/qa.md +12 -0
- package/skills/review-code/dimensions/security.md +12 -0
- package/skills/review-code/dimensions/technical-lead.md +19 -0
- package/skills/ship/SKILL.md +67 -0
- package/skills/spec/SKILL.md +264 -0
- package/skills/spec/continuity.md +66 -0
- 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
|
+
}
|