@mmerterden/multi-agent-pipeline 20.30.0 → 20.34.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/CHANGELOG.md +123 -0
- package/README.md +47 -1
- package/README.tr.md +61 -1
- package/docs/adr/0018-cross-repo-links-not-a-global-graph.md +43 -0
- package/docs/adr/README.md +2 -1
- package/docs/facts.json +6 -5
- package/docs/features.md +1 -1
- package/install/_dev-only-files.mjs +1 -0
- package/install/_unattended-profile.mjs +61 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +16 -2
- package/install/index.mjs +10 -1
- package/install/templates/claude-hooks.json +23 -1
- package/manifest.json +198 -134
- package/package.json +4 -1
- package/pipeline/agents/code-reviewer.md +9 -0
- package/pipeline/agents/explorer.md +4 -0
- package/pipeline/agents/plan-critic.md +4 -2
- package/pipeline/commands/multi-agent/SKILL.md +17 -136
- package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -297
- package/pipeline/commands/multi-agent/graph/SKILL.md +5 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/office-on/SKILL.md +8 -7
- package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +8 -22
- package/pipeline/commands/multi-agent/review/SKILL.md +11 -134
- package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +19 -224
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -0
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
- package/pipeline/contract/CHANGELOG.md +46 -0
- package/pipeline/contract/README.md +27 -10
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/fixtures/launch-plan.json +1 -1
- package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
- package/pipeline/contract/fixtures/runs-empty.json +1 -1
- package/pipeline/contract/fixtures/runs-failed.json +6 -1
- package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
- package/pipeline/contract/fixtures/runs-running.json +6 -1
- package/pipeline/contract/manifest.json +153 -19
- package/pipeline/contract/types/index.d.ts +304 -6
- package/pipeline/lib/bg-launch.mjs +106 -0
- package/pipeline/lib/code-digest.mjs +49 -0
- package/pipeline/lib/credential-resolve.mjs +42 -2
- package/pipeline/multi-agent-refs/channels/generator.md +146 -0
- package/pipeline/multi-agent-refs/channels/menu.md +55 -0
- package/pipeline/multi-agent-refs/channels/multi-repo.md +6 -0
- package/pipeline/multi-agent-refs/channels/preview.md +25 -0
- package/pipeline/multi-agent-refs/channels/reference.md +98 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +1 -1
- package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
- package/pipeline/multi-agent-refs/features/office-threat-model.md +24 -6
- package/pipeline/multi-agent-refs/features/plan-critic.md +5 -2
- package/pipeline/multi-agent-refs/features/review-delta.md +4 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +10 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +2 -2
- package/pipeline/multi-agent-refs/help/tr.md +2 -2
- package/pipeline/multi-agent-refs/knowledge.md +9 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +163 -0
- package/pipeline/multi-agent-refs/phases/modes.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +9 -8
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +12 -7
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -3
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +19 -10
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/resume/details.md +35 -0
- package/pipeline/multi-agent-refs/review/inputs.md +87 -0
- package/pipeline/multi-agent-refs/review/module-guides.md +25 -0
- package/pipeline/multi-agent-refs/review/valven.md +42 -0
- package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
- package/pipeline/multi-agent-refs/setup/accounts.md +27 -0
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +5 -0
- package/pipeline/multi-agent-refs/setup/firebase.md +5 -0
- package/pipeline/multi-agent-refs/setup/google-play.md +4 -0
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +21 -0
- package/pipeline/multi-agent-refs/setup/language.md +46 -0
- package/pipeline/multi-agent-refs/setup/multi-repo-group.md +51 -0
- package/pipeline/multi-agent-refs/setup/summaries.md +63 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +51 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
- package/pipeline/office/NOTICE.md +4 -1
- package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
- package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
- package/pipeline/office/assets/COUNTER_SINK.png +0 -0
- package/pipeline/office/assets/DEVICE_STAND.png +0 -0
- package/pipeline/office/assets/FILING_CABINET.png +0 -0
- package/pipeline/office/assets/FRIDGE.png +0 -0
- package/pipeline/office/assets/MAILBOX.png +0 -0
- package/pipeline/office/assets/MOP_BUCKET.png +0 -0
- package/pipeline/office/assets/PRINTER.png +0 -0
- package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
- package/pipeline/office/assets/TOILET_STALL.png +0 -0
- package/pipeline/office/assets/VENDING.png +0 -0
- package/pipeline/office/assets/WASHBASIN.png +0 -0
- package/pipeline/office/assets/WATER_COOLER.png +0 -0
- package/pipeline/office/index.html +3936 -819
- package/pipeline/office/toolkit.json +3 -0
- package/pipeline/rules/response-shape.md +6 -7
- package/pipeline/schemas/activity.schema.json +188 -4
- package/pipeline/schemas/agent-state.schema.json +511 -115
- package/pipeline/schemas/contract-error.schema.json +12 -1
- package/pipeline/schemas/job-change.schema.json +72 -0
- package/pipeline/schemas/job-request.schema.json +38 -0
- package/pipeline/schemas/launch.json +4 -4
- package/pipeline/schemas/office-credentials.schema.json +71 -0
- package/pipeline/schemas/office-jobs.json +253 -0
- package/pipeline/schemas/office-jobs.schema.json +62 -0
- package/pipeline/schemas/office-memory.schema.json +105 -0
- package/pipeline/schemas/office-settings-request.schema.json +30 -0
- package/pipeline/schemas/office-settings.schema.json +67 -0
- package/pipeline/schemas/phases.json +14 -6
- package/pipeline/schemas/phases.schema.json +86 -19
- package/pipeline/schemas/plan-critique.schema.json +9 -2
- package/pipeline/schemas/planning-output.schema.json +25 -2
- package/pipeline/schemas/prefs.schema.json +24 -1
- package/pipeline/schemas/prs.schema.json +5 -0
- package/pipeline/schemas/prune-logs-request.schema.json +26 -0
- package/pipeline/schemas/prune-logs.schema.json +52 -0
- package/pipeline/schemas/repo-change.schema.json +54 -0
- package/pipeline/schemas/repo-request.schema.json +29 -0
- package/pipeline/schemas/repos.schema.json +49 -1
- package/pipeline/schemas/reviewer-output.schema.json +20 -2
- package/pipeline/schemas/runs-index.schema.json +257 -46
- package/pipeline/schemas/system-status.schema.json +111 -0
- package/pipeline/schemas/token-budget.json +4 -4
- package/pipeline/schemas/tools.schema.json +5 -0
- package/pipeline/schemas/triage-output.schema.json +20 -2
- package/pipeline/schemas/unattended-profile.json +7 -2
- package/pipeline/scripts/_active-run.mjs +2 -1
- package/pipeline/scripts/_graph-analysis.mjs +270 -0
- package/pipeline/scripts/_memory-policy.mjs +165 -0
- package/pipeline/scripts/_secret-scan.mjs +105 -0
- package/pipeline/scripts/_spawn-async.mjs +9 -5
- package/pipeline/scripts/_unified-diff.mjs +2 -2
- package/pipeline/scripts/activity-snapshot.mjs +219 -23
- package/pipeline/scripts/autopilot-runner.mjs +7 -5
- package/pipeline/scripts/capture-flush.sh +12 -2
- package/pipeline/scripts/check-read-size.sh +9 -0
- package/pipeline/scripts/code-graph-rules/android.json +8 -3
- package/pipeline/scripts/contract-server.mjs +575 -28
- package/pipeline/scripts/debt-ledger.mjs +323 -0
- package/pipeline/scripts/file-context.mjs +261 -0
- package/pipeline/scripts/file-context.sh +20 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
- package/pipeline/scripts/graph-links.mjs +230 -0
- package/pipeline/scripts/graph-query.mjs +133 -1
- package/pipeline/scripts/graph-report.mjs +65 -0
- package/pipeline/scripts/job-launch.mjs +477 -0
- package/pipeline/scripts/launch-request.mjs +94 -9
- package/pipeline/scripts/learn-from-transcripts.mjs +12 -48
- package/pipeline/scripts/learnings-ledger.mjs +143 -24
- package/pipeline/scripts/office-build.mjs +148 -11
- package/pipeline/scripts/office-control.mjs +34 -7
- package/pipeline/scripts/office-credentials.mjs +106 -0
- package/pipeline/scripts/office-memory.mjs +200 -0
- package/pipeline/scripts/office-prune.mjs +125 -0
- package/pipeline/scripts/office-settings.mjs +145 -0
- package/pipeline/scripts/phase-tracker.sh +12 -5
- package/pipeline/scripts/plan-critique-gate.mjs +11 -2
- package/pipeline/scripts/prune-logs.sh +53 -2
- package/pipeline/scripts/rework-rulings.mjs +106 -0
- package/pipeline/scripts/runs-index.mjs +62 -2
- package/pipeline/scripts/scope-check-gate.mjs +149 -2
- package/pipeline/scripts/system-status.mjs +196 -0
- package/pipeline/scripts/task-mirror.mjs +150 -0
- package/pipeline/scripts/task-mirror.sh +20 -0
- package/pipeline/scripts/toolkit-tools.mjs +33 -7
- package/pipeline/scripts/transcript-index.mjs +4 -1
- package/pipeline/scripts/triage-memory.mjs +64 -37
- package/pipeline/scripts/validate-planning.mjs +68 -4
- package/pipeline/scripts/validate-reviewer.mjs +20 -0
- package/pipeline/scripts/validate-triage.mjs +107 -2
- package/pipeline/scripts/work-items.mjs +191 -22
- package/pipeline/skills/.skill-manifest.json +9 -9
- package/pipeline/skills/.skills-index.json +1 -1
- package/pipeline/skills/shared/README.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +5 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +3 -4
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +21 -0
- package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
- package/pipeline/skills/skills-index.md +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file debt-ledger.mjs - the known limits a repository carries, in one list.
|
|
4
|
+
*
|
|
5
|
+
* Three sources feed it, each a limit somebody accepted on purpose:
|
|
6
|
+
*
|
|
7
|
+
* shortcut a `shortcut: <limit>, <when to replace>` marker a diff adds
|
|
8
|
+
* (features/scope-check.md, "Known limits in the code")
|
|
9
|
+
* deferred a triage finding deferred as out of scope
|
|
10
|
+
* parked a finding parked by rework-rulings.mjs at the rework cap
|
|
11
|
+
*
|
|
12
|
+
* Storage: $HOME/.claude/memory/multi-agent/<repo-slug>/debt.jsonl, append-only,
|
|
13
|
+
* one row per item; the same item from the same task is written once. Every
|
|
14
|
+
* row passes the secret scan before it is written (_secret-scan.mjs).
|
|
15
|
+
*
|
|
16
|
+
* Subcommands:
|
|
17
|
+
* markers --patch <unified diff|-> print the markers the diff adds (no write)
|
|
18
|
+
* add --patch <diff> --task <id> record those markers
|
|
19
|
+
* from-run --triage <path> [--state <agent-state.json>] --task <id>
|
|
20
|
+
* record deferred findings and parked rulings
|
|
21
|
+
* list [--open] [--json] rows, newest first
|
|
22
|
+
* resolve --id <id> mark one row resolved (appends a resolution row)
|
|
23
|
+
* [--repo-slug <slug>] on every subcommand; else $MULTI_AGENT_REPO_SLUG, then the git repo name.
|
|
24
|
+
*
|
|
25
|
+
* Exit: 0 done · 1 IO error · 2 nothing to record or unknown id · 64 usage
|
|
26
|
+
*
|
|
27
|
+
* @module pipeline/scripts/debt-ledger
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
31
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
32
|
+
import { homedir } from "node:os";
|
|
33
|
+
import { basename, dirname, join } from "node:path";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
35
|
+
|
|
36
|
+
import { shortId } from "./_retrieval.mjs";
|
|
37
|
+
import { parseUnifiedDiff } from "./_unified-diff.mjs";
|
|
38
|
+
import { checkedSlug, memoryExcluded } from "./_memory-policy.mjs";
|
|
39
|
+
import { openScanner, stripPrivate } from "./_secret-scan.mjs";
|
|
40
|
+
|
|
41
|
+
const COMMENT_OPENERS = ["//", "#", "/*", "<!--", "--", ";", "*"];
|
|
42
|
+
const MAX_LINE = 2000;
|
|
43
|
+
const MAX_TEXT = 300;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* True when position `at` of the line sits inside a string literal. With
|
|
47
|
+
* `apostrophes: false` a single quote opens nothing: before a `//` or `#`
|
|
48
|
+
* comment it is as often a lifetime or a word's apostrophe as a string.
|
|
49
|
+
*/
|
|
50
|
+
function insideString(line, at, apostrophes = true) {
|
|
51
|
+
let quote = null;
|
|
52
|
+
for (let i = 0; i < at; i++) {
|
|
53
|
+
const c = line[i];
|
|
54
|
+
if (quote) {
|
|
55
|
+
if (c === "\\") i++;
|
|
56
|
+
else if (c === quote) quote = null;
|
|
57
|
+
} else if (c === '"' || c === "`" || (apostrophes && c === "'")) {
|
|
58
|
+
quote = c;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return quote !== null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The text after `shortcut:` when it opens a comment (only spaces between the
|
|
66
|
+
* comment opener and the marker, the opener outside any string), else null.
|
|
67
|
+
* `--` counts only at the start of the line, so a CLI flag is not a comment.
|
|
68
|
+
* Lowercase only, as documented.
|
|
69
|
+
*/
|
|
70
|
+
function markerIn(line) {
|
|
71
|
+
const at = line.indexOf("shortcut:");
|
|
72
|
+
if (at === -1) return null;
|
|
73
|
+
const before = line.slice(0, at).trimEnd();
|
|
74
|
+
const opener = COMMENT_OPENERS.find((o) => before.endsWith(o));
|
|
75
|
+
if (!opener) return null;
|
|
76
|
+
const openAt = before.length - opener.length;
|
|
77
|
+
if (opener === "--" && before.trim() !== "--") return null;
|
|
78
|
+
if (insideString(line, openAt, opener !== "//" && opener !== "#")) return null;
|
|
79
|
+
return line.slice(at + "shortcut:".length).trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Markers added by a unified diff: [{file, line, text}]. */
|
|
83
|
+
export function markersIn(patch) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const [file, diff] of parseUnifiedDiff(patch)) {
|
|
86
|
+
for (const { line, text: raw } of diff.addedAt) {
|
|
87
|
+
const marker = markerIn(raw.slice(0, MAX_LINE));
|
|
88
|
+
if (marker === null) continue;
|
|
89
|
+
let text = marker;
|
|
90
|
+
for (const close of ["*/", "-->"])
|
|
91
|
+
if (text.endsWith(close)) text = text.slice(0, -close.length);
|
|
92
|
+
text = text.trimEnd();
|
|
93
|
+
if (text) out.push({ file, line, text });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Deferred findings and parked rulings of one run: [{kind, file, line, text}]. */
|
|
100
|
+
export function runItems(triage, state) {
|
|
101
|
+
const items = [];
|
|
102
|
+
for (const d of triage?.deferred ?? []) {
|
|
103
|
+
const f = d?.finding ?? {};
|
|
104
|
+
if (!f.issue) continue;
|
|
105
|
+
items.push({
|
|
106
|
+
kind: "deferred",
|
|
107
|
+
file: f.file ?? null,
|
|
108
|
+
line: Number.isInteger(f.line) ? f.line : null,
|
|
109
|
+
text: `${f.issue}${d.reason ? ` (deferred: ${d.reason})` : ""}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const iterations = Array.isArray(state?.reviewIterations) ? state.reviewIterations : [];
|
|
113
|
+
for (const r of iterations.at(-1)?.rulings ?? []) {
|
|
114
|
+
if (r?.ruling !== "parked" || !r.issue) continue;
|
|
115
|
+
items.push({
|
|
116
|
+
kind: "parked",
|
|
117
|
+
file: r.file ?? null,
|
|
118
|
+
line: Number.isInteger(r.line) ? r.line : null,
|
|
119
|
+
text: `${r.issue} (${r.reason})`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return items;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const rowId = (r) => shortId("D", r.kind, r.file ?? "", r.text);
|
|
126
|
+
|
|
127
|
+
function parseFlags(argv) {
|
|
128
|
+
const flags = {};
|
|
129
|
+
for (let i = 0; i < argv.length; i++) {
|
|
130
|
+
const a = argv[i];
|
|
131
|
+
if (!a.startsWith("--")) continue;
|
|
132
|
+
const next = argv[i + 1];
|
|
133
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
134
|
+
flags[a.slice(2)] = next;
|
|
135
|
+
i++;
|
|
136
|
+
} else flags[a.slice(2)] = true;
|
|
137
|
+
}
|
|
138
|
+
return flags;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function repoSlug(flags) {
|
|
142
|
+
return checkedSlug(rawRepoSlug(flags));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function rawRepoSlug(flags) {
|
|
146
|
+
if (typeof flags["repo-slug"] === "string") return flags["repo-slug"];
|
|
147
|
+
if (process.env.MULTI_AGENT_REPO_SLUG) return process.env.MULTI_AGENT_REPO_SLUG;
|
|
148
|
+
try {
|
|
149
|
+
const common = execFileSync(
|
|
150
|
+
"git",
|
|
151
|
+
["rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
152
|
+
{
|
|
153
|
+
encoding: "utf8",
|
|
154
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
155
|
+
},
|
|
156
|
+
).trim();
|
|
157
|
+
const root = basename(common) === ".git" ? dirname(common) : common.replace(/\.git$/, "");
|
|
158
|
+
if (basename(root)) return basename(root);
|
|
159
|
+
} catch {
|
|
160
|
+
/* not a git repo */
|
|
161
|
+
}
|
|
162
|
+
return "default";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const ledgerPath = (slug) =>
|
|
166
|
+
join(homedir(), ".claude", "memory", "multi-agent", slug, "debt.jsonl");
|
|
167
|
+
|
|
168
|
+
function readRows(slug) {
|
|
169
|
+
const p = ledgerPath(slug);
|
|
170
|
+
if (!existsSync(p)) return [];
|
|
171
|
+
return readFileSync(p, "utf8")
|
|
172
|
+
.split("\n")
|
|
173
|
+
.filter((l) => l.trim())
|
|
174
|
+
.flatMap((l) => {
|
|
175
|
+
try {
|
|
176
|
+
return [JSON.parse(l)];
|
|
177
|
+
} catch {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Current state per id: the last row wins, so a resolution closes its item. */
|
|
184
|
+
function current(rows) {
|
|
185
|
+
const byId = new Map();
|
|
186
|
+
for (const r of rows) byId.set(r.id, { ...byId.get(r.id), ...r });
|
|
187
|
+
return [...byId.values()];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function record(slug, items, task) {
|
|
191
|
+
if (memoryExcluded(slug)) return { added: 0, droppedBySecretScan: 0, excluded: true };
|
|
192
|
+
const p = ledgerPath(slug);
|
|
193
|
+
const seen = new Set(readRows(slug).map((r) => `${r.id}::${r.task}`));
|
|
194
|
+
const scan = openScanner();
|
|
195
|
+
let added = 0;
|
|
196
|
+
let droppedBySecretScan = 0;
|
|
197
|
+
try {
|
|
198
|
+
for (const it of items) {
|
|
199
|
+
const whole = stripPrivate(it.text).replace(/\s+/g, " ").trim();
|
|
200
|
+
if (!whole) continue;
|
|
201
|
+
// Scanned whole and cut afterwards: a token across the cut would
|
|
202
|
+
// otherwise be shortened below the scanner's minimum and stored.
|
|
203
|
+
const text = whole.slice(0, MAX_TEXT);
|
|
204
|
+
const row = { kind: it.kind, file: it.file, line: it.line, text };
|
|
205
|
+
const id = rowId(row);
|
|
206
|
+
if (seen.has(`${id}::${task}`)) continue;
|
|
207
|
+
if (!scan.isClean(`${whole}\n${it.file ?? ""}`)) {
|
|
208
|
+
droppedBySecretScan++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
212
|
+
appendFileSync(
|
|
213
|
+
p,
|
|
214
|
+
JSON.stringify({ v: 1, id, ts: new Date().toISOString(), task, status: "open", ...row }) +
|
|
215
|
+
"\n",
|
|
216
|
+
);
|
|
217
|
+
seen.add(`${id}::${task}`);
|
|
218
|
+
added++;
|
|
219
|
+
}
|
|
220
|
+
} finally {
|
|
221
|
+
scan.close();
|
|
222
|
+
}
|
|
223
|
+
return { added, droppedBySecretScan };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readJson(path) {
|
|
227
|
+
try {
|
|
228
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function readPatch(path) {
|
|
235
|
+
return path === "-" ? readFileSync(0, "utf8") : readFileSync(path, "utf8");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function main(argv) {
|
|
239
|
+
const sub = argv[0];
|
|
240
|
+
const flags = parseFlags(argv.slice(1));
|
|
241
|
+
const out = (doc) => process.stdout.write(JSON.stringify(doc) + "\n");
|
|
242
|
+
const slug = repoSlug(flags);
|
|
243
|
+
const task = typeof flags.task === "string" ? flags.task : null;
|
|
244
|
+
|
|
245
|
+
if (sub === "markers" || sub === "add") {
|
|
246
|
+
if (typeof flags.patch !== "string") return usage();
|
|
247
|
+
const markers = markersIn(readPatch(flags.patch));
|
|
248
|
+
if (sub === "markers") {
|
|
249
|
+
out({ markers });
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
if (!task) return usage();
|
|
253
|
+
const r = record(
|
|
254
|
+
slug,
|
|
255
|
+
markers.map((m) => ({ kind: "shortcut", ...m })),
|
|
256
|
+
task,
|
|
257
|
+
);
|
|
258
|
+
out({ ok: true, slug, ...r });
|
|
259
|
+
return r.added ? 0 : 2;
|
|
260
|
+
}
|
|
261
|
+
if (sub === "from-run") {
|
|
262
|
+
if (typeof flags.triage !== "string" || !task) return usage();
|
|
263
|
+
const triage = readJson(flags.triage);
|
|
264
|
+
if (!triage) {
|
|
265
|
+
console.error(`debt-ledger: triage unreadable: ${flags.triage}`);
|
|
266
|
+
return 1;
|
|
267
|
+
}
|
|
268
|
+
const state = typeof flags.state === "string" ? readJson(flags.state) : null;
|
|
269
|
+
const r = record(slug, runItems(triage, state), task);
|
|
270
|
+
out({ ok: true, slug, ...r });
|
|
271
|
+
return r.added ? 0 : 2;
|
|
272
|
+
}
|
|
273
|
+
if (sub === "list") {
|
|
274
|
+
let rows = current(readRows(slug)).reverse();
|
|
275
|
+
if (flags.open) rows = rows.filter((r) => r.status === "open");
|
|
276
|
+
if (flags.json) out({ slug, rows });
|
|
277
|
+
else
|
|
278
|
+
for (const r of rows)
|
|
279
|
+
process.stdout.write(
|
|
280
|
+
`${r.id} ${r.status} ${r.kind} ${r.file ?? "-"}${r.line ? `:${r.line}` : ""} ${r.text}\n`,
|
|
281
|
+
);
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
if (sub === "resolve") {
|
|
285
|
+
if (typeof flags.id !== "string") return usage();
|
|
286
|
+
if (!current(readRows(slug)).some((r) => r.id === flags.id)) {
|
|
287
|
+
console.error(`debt-ledger: no item ${flags.id}`);
|
|
288
|
+
return 2;
|
|
289
|
+
}
|
|
290
|
+
appendFileSync(
|
|
291
|
+
ledgerPath(slug),
|
|
292
|
+
JSON.stringify({ id: flags.id, status: "resolved", resolvedAt: new Date().toISOString() }) +
|
|
293
|
+
"\n",
|
|
294
|
+
);
|
|
295
|
+
out({ ok: true, slug, resolved: flags.id });
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
return usage();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function usage() {
|
|
302
|
+
console.error(
|
|
303
|
+
"usage: debt-ledger.mjs markers --patch <diff|-> | add --patch <diff> --task <id> | from-run --triage <path> [--state <path>] --task <id> | list [--open] [--json] | resolve --id <id>",
|
|
304
|
+
);
|
|
305
|
+
return 64;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const invoked = () => {
|
|
309
|
+
try {
|
|
310
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
311
|
+
} catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (invoked()) {
|
|
317
|
+
try {
|
|
318
|
+
process.exitCode = main(process.argv.slice(2));
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.error(`debt-ledger: ${e.message}`);
|
|
321
|
+
process.exitCode = 1;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file file-context.mjs - what the repository's memory says about a file,
|
|
4
|
+
* handed to the model right after it reads that file.
|
|
5
|
+
*
|
|
6
|
+
* PostToolUse on Read. For the file just read it looks up, in the per-repo
|
|
7
|
+
* stores under ~/.claude/memory/multi-agent/<repo-slug>/:
|
|
8
|
+
*
|
|
9
|
+
* learnings-ledger.jsonl durable facts and conventions whose scope covers the file,
|
|
10
|
+
* minus those the feedback suppressed (_memory-policy.mjs)
|
|
11
|
+
* triage-corpus.jsonl the latest review findings recorded against it
|
|
12
|
+
* debt.jsonl open known limits in it (debt-ledger.mjs)
|
|
13
|
+
*
|
|
14
|
+
* and returns at most MAX_LINES of them as PostToolUse additionalContext,
|
|
15
|
+
* inside an <untrusted-data> block: the rows hold model-written text, so they
|
|
16
|
+
* are evidence about the file, never an instruction.
|
|
17
|
+
*
|
|
18
|
+
* When it runs: inside a pipeline run bound to the session (_active-run.mjs),
|
|
19
|
+
* or anywhere when prefs.global.fileContext.enabled is true; never when that
|
|
20
|
+
* flag is false. Each file is answered once per session. The text passes the
|
|
21
|
+
* secret scan before it is handed over; a hit means nothing is said.
|
|
22
|
+
*
|
|
23
|
+
* `file-context.mjs --lines <path>` prints the same lines, secret-scanned and
|
|
24
|
+
* wrapped the same way, for a caller that already decided to show them
|
|
25
|
+
* (check-read-size.sh, when it turns a whole-file read away). It applies no
|
|
26
|
+
* session or run condition, but stays silent when the flag is false.
|
|
27
|
+
*
|
|
28
|
+
* Blocks nothing and prints nothing on any failure: a missing store, a slow
|
|
29
|
+
* scan or a malformed payload all end in exit 0 with no output.
|
|
30
|
+
*
|
|
31
|
+
* @module pipeline/scripts/file-context
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
35
|
+
import {
|
|
36
|
+
existsSync,
|
|
37
|
+
lstatSync,
|
|
38
|
+
mkdirSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
realpathSync,
|
|
41
|
+
statSync,
|
|
42
|
+
writeFileSync,
|
|
43
|
+
} from "node:fs";
|
|
44
|
+
import { homedir, tmpdir } from "node:os";
|
|
45
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
46
|
+
|
|
47
|
+
import { wrapUntrusted } from "../lib/untrusted.mjs";
|
|
48
|
+
import { findActiveRun } from "./_active-run.mjs";
|
|
49
|
+
import { ledgerRowId, standing } from "./_memory-policy.mjs";
|
|
50
|
+
import { openScanner } from "./_secret-scan.mjs";
|
|
51
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
52
|
+
|
|
53
|
+
const MAX_INPUT = 256 * 1024;
|
|
54
|
+
const MAX_LINES = 6;
|
|
55
|
+
const MAX_LINE_CHARS = 220;
|
|
56
|
+
const STORE_MAX_BYTES = 4 * 1024 * 1024;
|
|
57
|
+
|
|
58
|
+
const prefsPath = (env) =>
|
|
59
|
+
env.MULTI_AGENT_PREFS || join(env.HOME || homedir(), ".claude", "multi-agent-preferences.json");
|
|
60
|
+
|
|
61
|
+
/** off | on | run-only, from prefs.global.fileContext.enabled. */
|
|
62
|
+
export function mode(env = process.env) {
|
|
63
|
+
try {
|
|
64
|
+
const flag = JSON.parse(readFileSync(prefsPath(env), "utf8"))?.global?.fileContext?.enabled;
|
|
65
|
+
if (flag === false) return "off";
|
|
66
|
+
if (flag === true) return "on";
|
|
67
|
+
} catch {
|
|
68
|
+
/* no prefs: the default */
|
|
69
|
+
}
|
|
70
|
+
return "run-only";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Repo slug and the file's path inside the repo, as the stores key them. */
|
|
74
|
+
export function locate(filePath) {
|
|
75
|
+
const dir = dirname(filePath);
|
|
76
|
+
const git = (args) =>
|
|
77
|
+
spawnSync("git", ["-C", dir, ...args], { encoding: "utf8", timeout: 2000 }).stdout?.trim();
|
|
78
|
+
const top = git(["rev-parse", "--show-toplevel"]);
|
|
79
|
+
if (!top) return null;
|
|
80
|
+
const common = git(["rev-parse", "--path-format=absolute", "--git-common-dir"]) || "";
|
|
81
|
+
const root = basename(common) === ".git" ? dirname(common) : top;
|
|
82
|
+
// git answers with the resolved path (/private/var on macOS), so the file is
|
|
83
|
+
// resolved the same way before the two are compared.
|
|
84
|
+
return { slug: basename(root), rel: relative(realpathSync(top), realpathSync(filePath)) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rows(path) {
|
|
88
|
+
try {
|
|
89
|
+
if (statSync(path).size > STORE_MAX_BYTES) return [];
|
|
90
|
+
return readFileSync(path, "utf8")
|
|
91
|
+
.split("\n")
|
|
92
|
+
.filter((l) => l.trim())
|
|
93
|
+
.flatMap((l) => {
|
|
94
|
+
try {
|
|
95
|
+
return [JSON.parse(l)];
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function scopeCovers(scope, rel) {
|
|
106
|
+
if (!scope || scope === "global") return false;
|
|
107
|
+
const dirOf = (g) =>
|
|
108
|
+
String(g)
|
|
109
|
+
.replace(/\/+$/, "")
|
|
110
|
+
.replace(/\/\*+$/, "");
|
|
111
|
+
const s = dirOf(scope);
|
|
112
|
+
return s === rel || rel.startsWith(s + "/");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const clip = (s) => {
|
|
116
|
+
const one = String(s ?? "")
|
|
117
|
+
.replace(/\s+/g, " ")
|
|
118
|
+
.trim();
|
|
119
|
+
return one.length > MAX_LINE_CHARS ? one.slice(0, MAX_LINE_CHARS - 3) + "..." : one;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** The lines worth saying about one file, most specific first. */
|
|
123
|
+
export function linesFor(storeDir, rel) {
|
|
124
|
+
const out = [];
|
|
125
|
+
const feedback = new Map();
|
|
126
|
+
for (const f of rows(join(storeDir, "learnings-feedback.jsonl"))) {
|
|
127
|
+
if (!feedback.has(f.id)) feedback.set(f.id, []);
|
|
128
|
+
feedback.get(f.id).push(f);
|
|
129
|
+
}
|
|
130
|
+
for (const r of rows(join(storeDir, "learnings-ledger.jsonl"))) {
|
|
131
|
+
if (!scopeCovers(r.scope, rel)) continue;
|
|
132
|
+
if (standing(feedback.get(ledgerRowId(r)) ?? []).suppressed) continue;
|
|
133
|
+
out.push(`${r.kind}: ${clip(r.statement)}`);
|
|
134
|
+
}
|
|
135
|
+
const findings = rows(join(storeDir, "triage-corpus.jsonl"))
|
|
136
|
+
.filter((r) => r.file === rel)
|
|
137
|
+
.slice(-3)
|
|
138
|
+
.reverse();
|
|
139
|
+
for (const f of findings) {
|
|
140
|
+
out.push(`past review (${f.classification}, ${f.severity}): ${clip(f.issue)}`);
|
|
141
|
+
}
|
|
142
|
+
const debt = new Map();
|
|
143
|
+
for (const r of rows(join(storeDir, "debt.jsonl"))) debt.set(r.id, { ...debt.get(r.id), ...r });
|
|
144
|
+
for (const d of debt.values()) {
|
|
145
|
+
if (d.status === "open" && d.file === rel) out.push(`known limit (${d.kind}): ${clip(d.text)}`);
|
|
146
|
+
}
|
|
147
|
+
return out.slice(0, MAX_LINES);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The cache directory exists, is a real directory and belongs to this user. */
|
|
151
|
+
function ownDir(dir) {
|
|
152
|
+
try {
|
|
153
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
154
|
+
const st = lstatSync(dir);
|
|
155
|
+
return st.isDirectory() && (process.getuid === undefined || st.uid === process.getuid());
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function seenBefore(sessionId, rel, slug) {
|
|
162
|
+
if (!sessionId) return false;
|
|
163
|
+
const uid = process.getuid?.() ?? "user";
|
|
164
|
+
const dir = join(tmpdir(), `multi-agent-file-context-${uid}`);
|
|
165
|
+
const f = join(dir, `${String(sessionId).replace(/[^A-Za-z0-9_-]/g, "")}.json`);
|
|
166
|
+
if (!ownDir(dir)) return false;
|
|
167
|
+
let seen = [];
|
|
168
|
+
try {
|
|
169
|
+
seen = JSON.parse(readFileSync(f, "utf8"));
|
|
170
|
+
} catch {
|
|
171
|
+
/* first file this session */
|
|
172
|
+
}
|
|
173
|
+
const key = `${slug}:${rel}`;
|
|
174
|
+
if (seen.includes(key)) return true;
|
|
175
|
+
try {
|
|
176
|
+
writeFileSync(f, JSON.stringify([...seen, key].slice(-500)), { mode: 0o600 });
|
|
177
|
+
} catch {
|
|
178
|
+
/* a lost cache only means saying it twice */
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The hook's answer for one payload, or null for silence. */
|
|
184
|
+
export function answer(payload, env = process.env) {
|
|
185
|
+
if (payload?.tool_name !== "Read") return null;
|
|
186
|
+
const filePath = payload?.tool_input?.file_path;
|
|
187
|
+
if (typeof filePath !== "string" || !filePath || !existsSync(filePath)) return null;
|
|
188
|
+
const m = mode(env);
|
|
189
|
+
if (m === "off") return null;
|
|
190
|
+
if (m === "run-only" && !findActiveRun({ sessionId: payload.session_id, cwd: payload.cwd, env }))
|
|
191
|
+
return null;
|
|
192
|
+
const where = locate(filePath);
|
|
193
|
+
if (!where || where.rel.startsWith("..")) return null;
|
|
194
|
+
const storeDir = join(env.HOME || homedir(), ".claude", "memory", "multi-agent", where.slug);
|
|
195
|
+
const lines = linesFor(storeDir, where.rel);
|
|
196
|
+
if (!lines.length || seenBefore(payload.session_id, where.rel, where.slug)) return null;
|
|
197
|
+
const text = lines.map((l) => `- ${l}`).join("\n");
|
|
198
|
+
const scan = openScanner();
|
|
199
|
+
try {
|
|
200
|
+
if (!scan.isClean(text)) return null;
|
|
201
|
+
} finally {
|
|
202
|
+
scan.close();
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
hookSpecificOutput: {
|
|
206
|
+
hookEventName: "PostToolUse",
|
|
207
|
+
additionalContext: wrapUntrusted(`memory:${where.slug}/${where.rel}`, text),
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The scanned lines for one file inside an untrusted-data block, or "" when
|
|
214
|
+
* file context is off, there is nothing, or the scan refused it.
|
|
215
|
+
*/
|
|
216
|
+
export function blockForPath(filePath, env = process.env) {
|
|
217
|
+
if (mode(env) === "off" || !existsSync(filePath)) return "";
|
|
218
|
+
const where = locate(filePath);
|
|
219
|
+
if (!where || where.rel.startsWith("..")) return "";
|
|
220
|
+
const storeDir = join(env.HOME || homedir(), ".claude", "memory", "multi-agent", where.slug);
|
|
221
|
+
const text = linesFor(storeDir, where.rel)
|
|
222
|
+
.map((l) => `- ${l}`)
|
|
223
|
+
.join("\n");
|
|
224
|
+
if (!text) return "";
|
|
225
|
+
const scan = openScanner();
|
|
226
|
+
try {
|
|
227
|
+
return scan.isClean(text) ? wrapUntrusted(`memory:${where.slug}/${where.rel}`, text) : "";
|
|
228
|
+
} finally {
|
|
229
|
+
scan.close();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function main() {
|
|
234
|
+
const i = process.argv.indexOf("--lines");
|
|
235
|
+
if (i > 0) {
|
|
236
|
+
const block = process.argv[i + 1] ? blockForPath(process.argv[i + 1]) : "";
|
|
237
|
+
if (block) process.stdout.write(block);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
let raw;
|
|
241
|
+
try {
|
|
242
|
+
raw = readFileSync(0, "utf8");
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!raw || raw.length > MAX_INPUT) return;
|
|
247
|
+
let payload;
|
|
248
|
+
try {
|
|
249
|
+
payload = JSON.parse(raw);
|
|
250
|
+
} catch {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const out = answer(payload);
|
|
254
|
+
if (out) process.stdout.write(JSON.stringify(out) + "\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
if (invokedDirectly(import.meta.url)) main();
|
|
259
|
+
} catch {
|
|
260
|
+
/* silent by contract */
|
|
261
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# file-context.sh - PostToolUse hook (Read) for the pipeline.
|
|
3
|
+
#
|
|
4
|
+
# Hands the model what the repository's memory says about the file it just
|
|
5
|
+
# read: ledger facts, past review findings and open known limits. The logic is
|
|
6
|
+
# in file-context.mjs; this wrapper exists because every hook the pipeline
|
|
7
|
+
# registers is a `bash "$HOME/.<host>/scripts/<name>"` command.
|
|
8
|
+
#
|
|
9
|
+
# Exits 0 on every path: a missing node, a slow scan or any internal error
|
|
10
|
+
# means no context, never a blocked tool call.
|
|
11
|
+
|
|
12
|
+
set -u
|
|
13
|
+
|
|
14
|
+
HERE="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
|
|
15
|
+
CORE="$HERE/file-context.mjs"
|
|
16
|
+
[ -f "$CORE" ] || exit 0
|
|
17
|
+
command -v node >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
19
|
+
node "$CORE" 2>/dev/null || true
|
|
20
|
+
exit 0
|
|
@@ -40,10 +40,12 @@ const CONTRACT = JSON.parse(
|
|
|
40
40
|
readFileSync(new URL("../schemas/phases.json", import.meta.url), "utf8"),
|
|
41
41
|
);
|
|
42
42
|
const PHASES = CONTRACT.phases;
|
|
43
|
+
// A mode may name its steps itself: the analysis command runs the six slots as
|
|
44
|
+
// Intake, Evidence, Synthesis, Review, Publish and Report.
|
|
43
45
|
const modePhases = (name) =>
|
|
44
46
|
CONTRACT.modes[name].phases.map((id) => {
|
|
45
47
|
const p = PHASES.find((x) => x.id === id);
|
|
46
|
-
return `${p.id}:${p.name}`;
|
|
48
|
+
return `${p.id}:${CONTRACT.modes[name].names?.[id] ?? p.name}`;
|
|
47
49
|
});
|
|
48
50
|
|
|
49
51
|
const PHASE_NAMES = PHASES.map((p) => `${p.id}:${p.name}`);
|