@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,246 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { rel } from '../paths.mjs';
|
|
4
|
+
import { walkMarkdown, resolveDocsDirs } from '../extract/walk.mjs';
|
|
5
|
+
import { estimateTokens } from '../format.mjs';
|
|
6
|
+
|
|
7
|
+
// Preferred roots. When none of them hold markdown, resolveDocsDirs() probes
|
|
8
|
+
// for the docs root this repo actually uses (doc/, site/, root README, …), so a
|
|
9
|
+
// project that does not happen to use `docs/` still gets prose retrieval.
|
|
10
|
+
const DEFAULT_DIRS = ['docs', 'openspec/specs', 'openspec/changes'];
|
|
11
|
+
const DEFAULT_PER_SECTION_LINES = 80;
|
|
12
|
+
const DEFAULT_PER_SECTION_TOKENS = 600;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split markdown into heading-delimited sections.
|
|
16
|
+
*/
|
|
17
|
+
export function parseMarkdownSections(filePath, text) {
|
|
18
|
+
const lines = text.split('\n');
|
|
19
|
+
const sections = [];
|
|
20
|
+
let current = {
|
|
21
|
+
file: filePath,
|
|
22
|
+
heading: '(preamble)',
|
|
23
|
+
level: 0,
|
|
24
|
+
startLine: 1,
|
|
25
|
+
lines: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function pushSection(endLine) {
|
|
29
|
+
const content = current.lines.join('\n').trimEnd();
|
|
30
|
+
if (!content && current.heading === '(preamble)') return;
|
|
31
|
+
sections.push({
|
|
32
|
+
file: current.file,
|
|
33
|
+
heading: current.heading,
|
|
34
|
+
level: current.level,
|
|
35
|
+
startLine: current.startLine,
|
|
36
|
+
endLine: endLine,
|
|
37
|
+
content,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < lines.length; i++) {
|
|
42
|
+
const line = lines[i];
|
|
43
|
+
const m = line.match(/^(#{1,6})\s+(.+)$/);
|
|
44
|
+
if (m) {
|
|
45
|
+
pushSection(i);
|
|
46
|
+
current = {
|
|
47
|
+
file: filePath,
|
|
48
|
+
heading: m[2].trim(),
|
|
49
|
+
level: m[1].length,
|
|
50
|
+
startLine: i + 1,
|
|
51
|
+
lines: [line],
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
current.lines.push(line);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
pushSection(lines.length);
|
|
58
|
+
return sections;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Tokenize a query string or term list into lowercase search terms (min length 2).
|
|
63
|
+
*/
|
|
64
|
+
export function tokenizeTerms(input) {
|
|
65
|
+
const raw = Array.isArray(input) ? input.join(' ') : String(input || '');
|
|
66
|
+
return [...new Set(
|
|
67
|
+
raw
|
|
68
|
+
.split(/[\s,|/]+/)
|
|
69
|
+
.map((t) => t.trim().toLowerCase())
|
|
70
|
+
.filter((t) => t.length >= 2)
|
|
71
|
+
.map((t) => t.replace(/^file:/, '').replace(/\.(ts|tsx|js|jsx|md)$/, '')),
|
|
72
|
+
)];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Derive search terms from explicit query, changed paths, and commit subject.
|
|
77
|
+
*/
|
|
78
|
+
export function deriveTerms({ query = '', changedFiles = [], commitSubject = '' } = {}) {
|
|
79
|
+
const fromQuery = tokenizeTerms(query);
|
|
80
|
+
const fromCommit = tokenizeTerms(commitSubject);
|
|
81
|
+
const fromFiles = [];
|
|
82
|
+
for (const f of changedFiles) {
|
|
83
|
+
const parts = String(f).split(/[/\\]/);
|
|
84
|
+
for (const p of parts) {
|
|
85
|
+
const base = p.replace(/\.[^.]+$/, '');
|
|
86
|
+
if (base.length >= 2 && !['src', 'lib', 'app', 'components', 'types'].includes(base)) {
|
|
87
|
+
fromFiles.push(base.toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
fromFiles.push(...tokenizeTerms(f));
|
|
91
|
+
}
|
|
92
|
+
return [...new Set([...fromQuery, ...fromCommit, ...fromFiles])];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Score a section for relevance to terms and changed file paths.
|
|
97
|
+
*/
|
|
98
|
+
export function scoreSection(section, terms, changedPaths = []) {
|
|
99
|
+
if (terms.length === 0 && changedPaths.length === 0) return 0;
|
|
100
|
+
|
|
101
|
+
let score = 0;
|
|
102
|
+
const headingLower = section.heading.toLowerCase();
|
|
103
|
+
const contentLower = section.content.toLowerCase();
|
|
104
|
+
const fileLower = section.file.toLowerCase();
|
|
105
|
+
|
|
106
|
+
for (const term of terms) {
|
|
107
|
+
if (headingLower === term) score += 15;
|
|
108
|
+
else if (headingLower.includes(term)) score += 10;
|
|
109
|
+
if (fileLower.includes(term)) score += 8;
|
|
110
|
+
if (contentLower.includes(term)) score += 3;
|
|
111
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
112
|
+
const re = new RegExp(escaped, 'gi');
|
|
113
|
+
const matches = section.content.match(re);
|
|
114
|
+
if (matches) score += Math.min(matches.length, 5);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const p of changedPaths) {
|
|
118
|
+
const norm = String(p).replace(/\\/g, '/').toLowerCase();
|
|
119
|
+
const base = path.posix.basename(norm, path.posix.extname(norm));
|
|
120
|
+
if (contentLower.includes(norm)) score += 12;
|
|
121
|
+
else if (base.length >= 2 && contentLower.includes(base)) score += 8;
|
|
122
|
+
const topDir = norm.split('/')[0];
|
|
123
|
+
if (topDir && fileLower.includes(topDir)) score += 4;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return score;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function truncateSection(section, { maxLines = DEFAULT_PER_SECTION_LINES, maxTokens = DEFAULT_PER_SECTION_TOKENS } = {}) {
|
|
130
|
+
const lines = section.content.split('\n');
|
|
131
|
+
let out = lines.slice(0, maxLines).join('\n');
|
|
132
|
+
if (estimateTokens(out) > maxTokens) {
|
|
133
|
+
const charLimit = maxTokens * 4;
|
|
134
|
+
out = `${out.slice(0, charLimit)}\n… (section truncated)`;
|
|
135
|
+
} else if (lines.length > maxLines) {
|
|
136
|
+
out = `${out}\n… (${lines.length - maxLines} more lines in section)`;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Retrieve ranked markdown sections within a token budget.
|
|
143
|
+
*/
|
|
144
|
+
export function retrieveMarkdownSections(root, {
|
|
145
|
+
query = '',
|
|
146
|
+
terms: explicitTerms = null,
|
|
147
|
+
changedFiles = [],
|
|
148
|
+
dirs: requestedDirs = null,
|
|
149
|
+
budget = 800,
|
|
150
|
+
perSectionLines = DEFAULT_PER_SECTION_LINES,
|
|
151
|
+
perSectionTokens = DEFAULT_PER_SECTION_TOKENS,
|
|
152
|
+
minScore = 1,
|
|
153
|
+
} = {}) {
|
|
154
|
+
const terms = explicitTerms ?? deriveTerms({ query, changedFiles });
|
|
155
|
+
const changedPaths = changedFiles.map((f) => String(f).replace(/\\/g, '/'));
|
|
156
|
+
|
|
157
|
+
// Fall back to probing only when the caller did not name dirs AND the
|
|
158
|
+
// preferred roots are empty on this repo.
|
|
159
|
+
let dirs = requestedDirs ?? DEFAULT_DIRS;
|
|
160
|
+
if (!requestedDirs && walkMarkdown(root, DEFAULT_DIRS).length === 0) {
|
|
161
|
+
dirs = resolveDocsDirs(root, null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const allSections = [];
|
|
165
|
+
for (const absPath of walkMarkdown(root, dirs)) {
|
|
166
|
+
const filePath = rel(root, absPath);
|
|
167
|
+
let text;
|
|
168
|
+
try {
|
|
169
|
+
text = fs.readFileSync(absPath, 'utf8');
|
|
170
|
+
} catch {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
for (const section of parseMarkdownSections(filePath, text)) {
|
|
174
|
+
const s = scoreSection(section, terms, changedPaths);
|
|
175
|
+
if (s >= minScore) {
|
|
176
|
+
allSections.push({ ...section, score: s });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
allSections.sort((a, b) => {
|
|
182
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
183
|
+
if (a.file !== b.file) return a.file.localeCompare(b.file);
|
|
184
|
+
return a.startLine - b.startLine;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const maxScore = allSections[0]?.score ?? 0;
|
|
188
|
+
const scoreFloor = Math.max(minScore, Math.floor(maxScore * 0.4), maxScore - 20);
|
|
189
|
+
const ranked = allSections.filter((s) => s.score >= scoreFloor);
|
|
190
|
+
|
|
191
|
+
const selected = [];
|
|
192
|
+
let usedTokens = 0;
|
|
193
|
+
const headerTokens = estimateTokens(
|
|
194
|
+
`# Documentation excerpts\nTerms: [${terms.join(', ')}] | budget=${budget}\n\n`,
|
|
195
|
+
);
|
|
196
|
+
usedTokens += headerTokens;
|
|
197
|
+
|
|
198
|
+
for (const section of ranked) {
|
|
199
|
+
const excerpt = truncateSection(section, {
|
|
200
|
+
maxLines: perSectionLines,
|
|
201
|
+
maxTokens: perSectionTokens,
|
|
202
|
+
});
|
|
203
|
+
const block =
|
|
204
|
+
`### ${section.file} — ${section.heading} (L${section.startLine}-L${section.endLine}) [score=${section.score}]\n` +
|
|
205
|
+
`${excerpt}\n`;
|
|
206
|
+
const blockTokens = estimateTokens(block);
|
|
207
|
+
if (usedTokens + blockTokens > budget) {
|
|
208
|
+
if (selected.length === 0 && ranked.length > 0) {
|
|
209
|
+
// Always include at least one hit, truncated to budget
|
|
210
|
+
const trimmed = block.slice(0, Math.max(100, (budget - usedTokens) * 4));
|
|
211
|
+
selected.push(`${trimmed}\n… (truncated to fit budget ${budget})\n`);
|
|
212
|
+
usedTokens = budget;
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
selected.push(block);
|
|
217
|
+
usedTokens += blockTokens;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (selected.length === 0) {
|
|
221
|
+
return {
|
|
222
|
+
text:
|
|
223
|
+
`No matching documentation sections.\n` +
|
|
224
|
+
` Terms searched: ${terms.length ? terms.join(', ') : '(none — pass a query or changed files)'}\n` +
|
|
225
|
+
` Directories: ${dirs.join(', ')}\n` +
|
|
226
|
+
` Hint: broaden query terms or add --changed paths from the task.\n`,
|
|
227
|
+
terms,
|
|
228
|
+
sectionCount: 0,
|
|
229
|
+
tokens: estimateTokens(`No matching documentation sections.\n`),
|
|
230
|
+
empty: true,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const text = `# Documentation excerpts\nTerms: [${terms.join(', ')}] | budget=${budget} | sections=${selected.length}\n\n${selected.join('\n')}`;
|
|
235
|
+
return {
|
|
236
|
+
text,
|
|
237
|
+
terms,
|
|
238
|
+
sectionCount: selected.length,
|
|
239
|
+
tokens: estimateTokens(text),
|
|
240
|
+
empty: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function formatDocsResult(result) {
|
|
245
|
+
return result.text;
|
|
246
|
+
}
|