@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,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file system-status.mjs - what this machine has left: CPU load, memory,
|
|
4
|
+
* free disk, and what the pipeline itself is using (its sessions, the toolkit
|
|
5
|
+
* server, the log folder).
|
|
6
|
+
*
|
|
7
|
+
* Numbers only. No path, process argument, user name or host name leaves this
|
|
8
|
+
* script: a process is counted by what it is (a Claude Code session, the
|
|
9
|
+
* toolkit server), never listed, and a folder is reported by its size alone.
|
|
10
|
+
*
|
|
11
|
+
* CPU use is measured over a short window from two samples of the per-core
|
|
12
|
+
* counters, so a call takes about SAMPLE_MS. Folder sizes come from `du -sk`
|
|
13
|
+
* under a deadline; a folder too large to measure in time is null, not 0.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* system-status.mjs [--json]
|
|
17
|
+
*
|
|
18
|
+
* LOGS_ROOT overrides ~/.claude/logs/multi-agent.
|
|
19
|
+
*
|
|
20
|
+
* Exit: 0 printed, 2 usage.
|
|
21
|
+
*
|
|
22
|
+
* @module pipeline/scripts/system-status
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { statfsSync } from "node:fs";
|
|
26
|
+
import { cpus, freemem, homedir, loadavg, totalmem } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
|
|
29
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
30
|
+
import { spawnAsync } from "./_spawn-async.mjs";
|
|
31
|
+
|
|
32
|
+
export const CONTRACT_VERSION = "1.0.0";
|
|
33
|
+
export const SAMPLE_MS = 250;
|
|
34
|
+
/** Free disk below this share of the disk turns the panel red. */
|
|
35
|
+
export const DISK_LOW = 0.1;
|
|
36
|
+
const DU_TIMEOUT_MS = 3000;
|
|
37
|
+
const MB = 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
const logsRoot = (env) => env.LOGS_ROOT || join(homedir(), ".claude", "logs", "multi-agent");
|
|
40
|
+
|
|
41
|
+
function cpuTimes() {
|
|
42
|
+
return cpus().map((c) => {
|
|
43
|
+
const t = c.times;
|
|
44
|
+
return {
|
|
45
|
+
busy: t.user + t.nice + t.sys + t.irq,
|
|
46
|
+
total: t.user + t.nice + t.sys + t.irq + t.idle,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Busy share of the CPU between two samples, 0..1, or null without counters. */
|
|
52
|
+
export function cpuShare(a, b) {
|
|
53
|
+
let busy = 0;
|
|
54
|
+
let total = 0;
|
|
55
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
56
|
+
busy += b[i].busy - a[i].busy;
|
|
57
|
+
total += b[i].total - a[i].total;
|
|
58
|
+
}
|
|
59
|
+
return total > 0 ? Math.min(1, Math.max(0, busy / total)) : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function disk(path) {
|
|
63
|
+
try {
|
|
64
|
+
const s = statfsSync(path);
|
|
65
|
+
const total = s.blocks * s.bsize;
|
|
66
|
+
const free = s.bavail * s.bsize;
|
|
67
|
+
return { totalMb: Math.round(total / MB), freeMb: Math.round(free / MB), device: s.type };
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The log folder's size changes slowly and du over a large tree is the
|
|
74
|
+
// slowest part of the answer, so one measurement serves a minute of polls.
|
|
75
|
+
const FOLDER_TTL_MS = 60_000;
|
|
76
|
+
const folderCache = new Map();
|
|
77
|
+
async function folderMb(path) {
|
|
78
|
+
const hit = folderCache.get(path);
|
|
79
|
+
if (hit && Date.now() - hit.at < FOLDER_TTL_MS) return hit.mb;
|
|
80
|
+
const r = await spawnAsync("du", ["-sk", path], { timeoutMs: DU_TIMEOUT_MS });
|
|
81
|
+
const kb = r.status === 0 ? Number(r.stdout.split(/\s+/)[0]) : NaN;
|
|
82
|
+
const mb = Number.isFinite(kb) ? Math.round(kb / 1024) : null;
|
|
83
|
+
folderCache.set(path, { at: Date.now(), mb });
|
|
84
|
+
return mb;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pipeline processes from `ps`, counted and summed, never listed. A Claude Code
|
|
89
|
+
* session is a process whose executable is `claude`; the toolkit server is a
|
|
90
|
+
* node, npx or npm process whose command line names multi-agent-toolkit.
|
|
91
|
+
*/
|
|
92
|
+
export function countProcesses(psText) {
|
|
93
|
+
const out = { sessions: 0, toolkit: 0, rssMb: 0 };
|
|
94
|
+
for (const line of String(psText).split("\n")) {
|
|
95
|
+
const m = /^\s*(\d+)\s+(\S+)(?:\s+(.*))?$/.exec(line);
|
|
96
|
+
if (!m) continue;
|
|
97
|
+
const rssKb = Number(m[1]);
|
|
98
|
+
const exe = m[2].split("/").pop();
|
|
99
|
+
const rest = m[3] || "";
|
|
100
|
+
if (exe === "claude") out.sessions++;
|
|
101
|
+
// The server runs under node or npx; a grep or tail naming it is not it.
|
|
102
|
+
else if (/^(node|npx|npm)$/.test(exe) && /multi-agent-toolkit/.test(rest)) out.toolkit++;
|
|
103
|
+
else continue;
|
|
104
|
+
out.rssMb += rssKb / 1024;
|
|
105
|
+
}
|
|
106
|
+
out.rssMb = Math.round(out.rssMb);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function processes() {
|
|
111
|
+
const r = await spawnAsync("ps", ["-A", "-o", "rss=", "-o", "args="], {
|
|
112
|
+
timeoutMs: 2000,
|
|
113
|
+
maxBytes: 8 * MB,
|
|
114
|
+
});
|
|
115
|
+
return r.status === 0 ? countProcesses(r.stdout) : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Bytes macOS can hand out right away: free pages plus the inactive,
|
|
122
|
+
* speculative and purgeable ones it reclaims on demand. os.freemem() counts
|
|
123
|
+
* free pages only, which reads as nearly full on any Mac that has run a while.
|
|
124
|
+
*/
|
|
125
|
+
export function availableFromVmStat(text) {
|
|
126
|
+
const page = Number((/page size of (\d+) bytes/.exec(text) || [])[1]);
|
|
127
|
+
if (!page) return null;
|
|
128
|
+
const pages = (name) => Number((new RegExp(`Pages ${name}:\\s+(\\d+)`).exec(text) || [])[1] || 0);
|
|
129
|
+
const n = pages("free") + pages("inactive") + pages("speculative") + pages("purgeable");
|
|
130
|
+
return n ? n * page : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function availableMemory() {
|
|
134
|
+
if (process.platform !== "darwin") return freemem();
|
|
135
|
+
const r = await spawnAsync("vm_stat", [], { timeoutMs: 2000, maxBytes: MB });
|
|
136
|
+
return (r.status === 0 && availableFromVmStat(r.stdout)) || freemem();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function systemStatus(env = process.env) {
|
|
140
|
+
const a = cpuTimes();
|
|
141
|
+
await sleep(SAMPLE_MS);
|
|
142
|
+
const b = cpuTimes();
|
|
143
|
+
const cores = b.length;
|
|
144
|
+
const [l1, l5, l15] = loadavg();
|
|
145
|
+
const memTotal = totalmem();
|
|
146
|
+
const memFree = await availableMemory();
|
|
147
|
+
const root = logsRoot(env);
|
|
148
|
+
const home = disk(homedir());
|
|
149
|
+
const logs = disk(root);
|
|
150
|
+
const sameDisk = Boolean(
|
|
151
|
+
home && logs && home.device === logs.device && home.totalMb === logs.totalMb,
|
|
152
|
+
);
|
|
153
|
+
const strip = (d) => (d ? { totalMb: d.totalMb, freeMb: d.freeMb } : null);
|
|
154
|
+
const share = home && home.totalMb ? home.freeMb / home.totalMb : null;
|
|
155
|
+
const cpuUse = cpuShare(a, b);
|
|
156
|
+
const [procs, logsMb] = await Promise.all([processes(), folderMb(root)]);
|
|
157
|
+
return {
|
|
158
|
+
contractVersion: CONTRACT_VERSION,
|
|
159
|
+
generatedAt: new Date().toISOString(),
|
|
160
|
+
cpu: {
|
|
161
|
+
cores,
|
|
162
|
+
usage: cpuUse === null ? null : Math.round(cpuUse * 100) / 100,
|
|
163
|
+
load: [l1, l5, l15].map((x) => Math.round(x * 100) / 100),
|
|
164
|
+
},
|
|
165
|
+
memory: {
|
|
166
|
+
totalMb: Math.round(memTotal / MB),
|
|
167
|
+
usedMb: Math.round((memTotal - memFree) / MB),
|
|
168
|
+
},
|
|
169
|
+
disk: {
|
|
170
|
+
home: strip(home),
|
|
171
|
+
logs: sameDisk ? null : strip(logs),
|
|
172
|
+
},
|
|
173
|
+
pipeline: {
|
|
174
|
+
...(procs ?? { sessions: null, toolkit: null, rssMb: null }),
|
|
175
|
+
logsMb,
|
|
176
|
+
},
|
|
177
|
+
warnings: [
|
|
178
|
+
...(share !== null && share < DISK_LOW ? ["disk-low"] : []),
|
|
179
|
+
...(cores && l5 > cores ? ["cpu-busy"] : []),
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (invokedDirectly(import.meta.url)) {
|
|
185
|
+
const args = process.argv.slice(2);
|
|
186
|
+
if (args.some((a) => a !== "--json")) {
|
|
187
|
+
console.error("usage: system-status.mjs [--json]");
|
|
188
|
+
process.exit(2);
|
|
189
|
+
}
|
|
190
|
+
const doc = await systemStatus();
|
|
191
|
+
process.stdout.write(
|
|
192
|
+
args.includes("--json")
|
|
193
|
+
? JSON.stringify(doc) + "\n"
|
|
194
|
+
: `cpu ${doc.cpu.usage ?? "?"} of ${doc.cpu.cores} cores, memory ${doc.memory.usedMb}/${doc.memory.totalMb} MB, disk free ${doc.disk.home?.freeMb ?? "?"} MB\n`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file task-mirror.mjs - PostToolUse hook: the host's task list, mirrored
|
|
4
|
+
* into the run's tracker without the agent having to remember it.
|
|
5
|
+
*
|
|
6
|
+
* tracker-contract.md asks for one `phase-tracker.sh task` call per native
|
|
7
|
+
* TaskCreate / TaskUpdate so readers that are not the host (runs-index.mjs and
|
|
8
|
+
* the office panel behind the contract server) see the same list under the
|
|
9
|
+
* same phase. That call is an instruction, and an instruction can be skipped;
|
|
10
|
+
* this hook makes the mirror a side effect of the native call itself.
|
|
11
|
+
*
|
|
12
|
+
* Which run: _active-run.mjs findActiveRun, and only a run bound to this
|
|
13
|
+
* session by `sessionId` or `hostSessionId`, the same rule run-stop-guard.mjs
|
|
14
|
+
* uses. No bound run: nothing is written.
|
|
15
|
+
*
|
|
16
|
+
* What is mirrored:
|
|
17
|
+
* - TaskCreate, unless its subject is a phase tile ("Phase 2 Dev"): the phase
|
|
18
|
+
* list already is the state file (tracker-contract.md, "Mirroring").
|
|
19
|
+
* - TaskUpdate of a task already mirrored, with its new status, subject or
|
|
20
|
+
* active form. An update of a phase tile or of a task this hook never saw
|
|
21
|
+
* is left alone.
|
|
22
|
+
* The task goes under the phase in progress when it was created.
|
|
23
|
+
*
|
|
24
|
+
* Exits 0 on every path and prints nothing: a mirror, not a gate. A failure is
|
|
25
|
+
* written to the hook failure log.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { spawnSync } from "node:child_process";
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import { dirname, join } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
|
|
33
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
34
|
+
import { findActiveRun, logHookFailure, runDirOf } from "./_active-run.mjs";
|
|
35
|
+
|
|
36
|
+
const HOOK = "task-mirror";
|
|
37
|
+
const SESSION_BOUND = new Set(["session", "host-session"]);
|
|
38
|
+
const STATUSES = new Set(["pending", "in_progress", "completed"]);
|
|
39
|
+
const TILE = /^Phase \d+\b/;
|
|
40
|
+
const MAX_INPUT = 256 * 1024;
|
|
41
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
|
|
43
|
+
/** The id TaskCreate returned, from its structured result or its text. */
|
|
44
|
+
export function createdId(response) {
|
|
45
|
+
if (!response) return null;
|
|
46
|
+
if (typeof response === "object") {
|
|
47
|
+
const id = response.task?.id ?? response.id ?? response.taskId;
|
|
48
|
+
if (id !== undefined && id !== null && String(id) !== "") return String(id);
|
|
49
|
+
}
|
|
50
|
+
const text = typeof response === "string" ? response : JSON.stringify(response);
|
|
51
|
+
const m = /Task #(\w+)/.exec(text) || /"id"\s*:\s*"?(\w+)/.exec(text);
|
|
52
|
+
return m ? m[1] : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The `phase-tracker.sh task` arguments one native call maps to, or null when
|
|
57
|
+
* the call is not mirrored.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} payload the PostToolUse hook JSON
|
|
60
|
+
* @param {object} tracker the run's tracker-state.json
|
|
61
|
+
*/
|
|
62
|
+
export function mirrorArgs(payload, tracker) {
|
|
63
|
+
const tool = payload?.tool_name;
|
|
64
|
+
const input = payload?.tool_input || {};
|
|
65
|
+
const phases = Array.isArray(tracker?.phases) ? tracker.phases : [];
|
|
66
|
+
if (tool === "TaskCreate") {
|
|
67
|
+
const subject = typeof input.subject === "string" ? input.subject.trim() : "";
|
|
68
|
+
if (!subject || TILE.test(subject)) return null;
|
|
69
|
+
const id = createdId(payload.tool_response);
|
|
70
|
+
// Between two phases the task belongs to the one about to start: the
|
|
71
|
+
// first pending phase after the last finished one, else the last phase.
|
|
72
|
+
const lastDone = phases.findLastIndex((p) => ["completed", "skipped"].includes(p.status));
|
|
73
|
+
const phase =
|
|
74
|
+
phases.find((p) => p.status === "in_progress") ??
|
|
75
|
+
phases.slice(lastDone + 1).find((p) => p.status === "pending") ??
|
|
76
|
+
phases.at(-1);
|
|
77
|
+
if (!id || !phase) return null;
|
|
78
|
+
const args = ["task", String(phase.id), id, "pending", "--subject", subject];
|
|
79
|
+
if (typeof input.activeForm === "string" && input.activeForm)
|
|
80
|
+
args.push("--active-form", input.activeForm);
|
|
81
|
+
return args;
|
|
82
|
+
}
|
|
83
|
+
if (tool === "TaskUpdate") {
|
|
84
|
+
const id = input.taskId === undefined || input.taskId === null ? "" : String(input.taskId);
|
|
85
|
+
if (!id) return null;
|
|
86
|
+
// A resumed run in a new session can number its tasks from 1 again, so
|
|
87
|
+
// the phase in progress is asked first, then the newest phases back.
|
|
88
|
+
const ordered = [...phases.filter((p) => p.status === "in_progress"), ...[...phases].reverse()];
|
|
89
|
+
const phase = ordered.find((p) => (p.tasks || []).some((t) => String(t.id) === id));
|
|
90
|
+
if (!phase) return null;
|
|
91
|
+
const task = phase.tasks.find((t) => String(t.id) === id);
|
|
92
|
+
if (input.status === "deleted") return ["task", String(phase.id), id, "deleted"];
|
|
93
|
+
const status = STATUSES.has(input.status) ? input.status : task.status;
|
|
94
|
+
if (!STATUSES.has(status)) return null;
|
|
95
|
+
const args = ["task", String(phase.id), id, status];
|
|
96
|
+
if (typeof input.subject === "string" && input.subject && !TILE.test(input.subject))
|
|
97
|
+
args.push("--subject", input.subject);
|
|
98
|
+
if (typeof input.activeForm === "string" && input.activeForm)
|
|
99
|
+
args.push("--active-form", input.activeForm);
|
|
100
|
+
return args;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function mirror(payload, env = process.env) {
|
|
106
|
+
if (!payload || !["TaskCreate", "TaskUpdate"].includes(payload.tool_name)) return null;
|
|
107
|
+
const run = findActiveRun({
|
|
108
|
+
sessionId: typeof payload.session_id === "string" ? payload.session_id : null,
|
|
109
|
+
cwd: typeof payload.cwd === "string" ? payload.cwd : null,
|
|
110
|
+
env,
|
|
111
|
+
});
|
|
112
|
+
if (!run || !SESSION_BOUND.has(run.matchedBy) || !run.state?.taskId) return null;
|
|
113
|
+
// The tracker it read is the one it writes: phase-tracker.sh would pick
|
|
114
|
+
// the newest folder for the task id, which another copy could be.
|
|
115
|
+
const trackerPath = join(runDirOf(run.statePath), "tracker-state.json");
|
|
116
|
+
let tracker;
|
|
117
|
+
try {
|
|
118
|
+
tracker = JSON.parse(readFileSync(trackerPath, "utf8"));
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const args = mirrorArgs(payload, tracker);
|
|
123
|
+
if (!args) return null;
|
|
124
|
+
const r = spawnSync("bash", [join(HERE, "phase-tracker.sh"), ...args], {
|
|
125
|
+
env: {
|
|
126
|
+
...env,
|
|
127
|
+
MULTI_AGENT_TASK_ID: run.state.taskId,
|
|
128
|
+
TRACKER_FILE: trackerPath,
|
|
129
|
+
TRACKER_QUIET: "1",
|
|
130
|
+
},
|
|
131
|
+
encoding: "utf8",
|
|
132
|
+
timeout: 8000,
|
|
133
|
+
});
|
|
134
|
+
if (r.status !== 0) logHookFailure(HOOK, `phase-tracker task exited ${r.status}`, env);
|
|
135
|
+
return args;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (invokedDirectly(import.meta.url)) {
|
|
139
|
+
try {
|
|
140
|
+
const raw = readFileSync(0, "utf8").slice(0, MAX_INPUT);
|
|
141
|
+
mirror(JSON.parse(raw));
|
|
142
|
+
} catch (e) {
|
|
143
|
+
try {
|
|
144
|
+
logHookFailure(HOOK, String(e?.message || e).slice(0, 200));
|
|
145
|
+
} catch {
|
|
146
|
+
/* the log is best effort */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# task-mirror.sh - PostToolUse hook (TaskCreate|TaskUpdate) for the pipeline.
|
|
3
|
+
#
|
|
4
|
+
# Mirrors the host's native task list into the run's tracker-state.json, so the
|
|
5
|
+
# office panel and every other reader see what the task widget shows. The
|
|
6
|
+
# logic is in task-mirror.mjs; this wrapper exists because every hook the
|
|
7
|
+
# pipeline registers is a `bash "$HOME/.<host>/scripts/<name>"` command.
|
|
8
|
+
#
|
|
9
|
+
# Exits 0 on every path and prints nothing: a missing node or any internal
|
|
10
|
+
# error means no mirror, never a blocked tool call.
|
|
11
|
+
|
|
12
|
+
set -u
|
|
13
|
+
|
|
14
|
+
HERE="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
|
|
15
|
+
CORE="$HERE/task-mirror.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
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
* MCP_TOOLKIT_URL_POLICY=strict, set after the registration's own environment
|
|
30
30
|
* so neither can be overridden. A registration whose package spec has no exact
|
|
31
31
|
* version (bare, a dist-tag such as `@latest`, or a range) is pinned to
|
|
32
|
-
* MA_TOOLKIT_VERSION
|
|
33
|
-
*
|
|
32
|
+
* MA_TOOLKIT_VERSION, else prefs.global.office.toolkitVersion, else the
|
|
33
|
+
* version the release ships beside the office page (office/toolkit.json, the
|
|
34
|
+
* one it was checked against), and refused when none is set.
|
|
34
35
|
*
|
|
35
36
|
* No tool that is not read-only is allowed today. Opening one up means routing
|
|
36
37
|
* it through the server's two-step confirm, never a direct call.
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
import { spawn } from "node:child_process";
|
|
59
60
|
import { readFileSync } from "node:fs";
|
|
60
61
|
import { homedir } from "node:os";
|
|
61
|
-
import { basename, join } from "node:path";
|
|
62
|
+
import { basename, dirname, join } from "node:path";
|
|
62
63
|
import { clearTimeout, setTimeout } from "node:timers";
|
|
63
64
|
import { createRedactor } from "./transcript-index.mjs";
|
|
64
65
|
import { runMain } from "../lib/fatal.mjs";
|
|
@@ -229,12 +230,15 @@ export function unpinned(entry) {
|
|
|
229
230
|
/**
|
|
230
231
|
* The registration with every unpinned spec set to `<name>@<version>`, or
|
|
231
232
|
* null when it is unpinned and no exact version is known. The version comes
|
|
232
|
-
* from MA_TOOLKIT_VERSION, else prefs.global.office.toolkitVersion
|
|
233
|
+
* from MA_TOOLKIT_VERSION, else prefs.global.office.toolkitVersion, else
|
|
234
|
+
* office/toolkit.json beside the page (MA_OFFICE_PAGE's directory, or the
|
|
235
|
+
* office directory next to this script's).
|
|
233
236
|
*/
|
|
234
237
|
export function pinnedEntry(entry, env = process.env) {
|
|
235
238
|
if (!unpinned(entry)) return entry;
|
|
236
239
|
const prefs = readJson(join(env.HOME || homedir(), ".claude", "multi-agent-preferences.json"));
|
|
237
|
-
const version =
|
|
240
|
+
const version =
|
|
241
|
+
env.MA_TOOLKIT_VERSION || prefs?.global?.office?.toolkitVersion || shippedToolkitVersion(env);
|
|
238
242
|
if (typeof version !== "string" || !SEMVER.test(version)) return null;
|
|
239
243
|
const loose = new Set(packageSpecs(entry).filter((s) => !SEMVER.test(splitSpec(s)[1] ?? "")));
|
|
240
244
|
const pin = (a) => {
|
|
@@ -248,6 +252,24 @@ export function pinnedEntry(entry, env = process.env) {
|
|
|
248
252
|
return { ...entry, args: Array.isArray(entry.args) ? entry.args.map(pin) : entry.args };
|
|
249
253
|
}
|
|
250
254
|
|
|
255
|
+
/** The toolkit version the release ships beside the office page, or undefined. */
|
|
256
|
+
export function shippedToolkitVersion(env = process.env) {
|
|
257
|
+
const dir = env.MA_OFFICE_PAGE
|
|
258
|
+
? dirname(env.MA_OFFICE_PAGE)
|
|
259
|
+
: join(import.meta.dirname, "..", "office");
|
|
260
|
+
const v = readJson(join(dir, "toolkit.json"))?.toolkitVersion;
|
|
261
|
+
return typeof v === "string" && SEMVER.test(v) ? v : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The exact toolkit version the Tools window starts, or null when none is known. */
|
|
265
|
+
export function activeToolkitVersion(env = process.env) {
|
|
266
|
+
const registered = toolkitEntry(env);
|
|
267
|
+
const entry = registered ? pinnedEntry(registered, env) : null;
|
|
268
|
+
const spec = entry ? packageSpecs(entry)[0] : null;
|
|
269
|
+
const v = spec ? splitSpec(spec)[1] : null;
|
|
270
|
+
return typeof v === "string" && SEMVER.test(v) ? v : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
251
273
|
/** read, low or high, from the tool's own annotations. */
|
|
252
274
|
export function riskOf(tool) {
|
|
253
275
|
const a = tool?.annotations ?? {};
|
|
@@ -386,7 +408,7 @@ export function createToolkitClient({ env = process.env } = {}) {
|
|
|
386
408
|
return Promise.reject(
|
|
387
409
|
new ToolError(
|
|
388
410
|
"unpinned",
|
|
389
|
-
"
|
|
411
|
+
"no exact toolkit version is known: the release ships it in office/toolkit.json beside the page, which reinstalling (node install.js) restores; MA_TOOLKIT_VERSION or prefs.global.office.toolkitVersion override it",
|
|
390
412
|
),
|
|
391
413
|
);
|
|
392
414
|
}
|
|
@@ -565,7 +587,11 @@ async function main(argv) {
|
|
|
565
587
|
try {
|
|
566
588
|
let out;
|
|
567
589
|
if (cmd === "list" && !name) {
|
|
568
|
-
out = {
|
|
590
|
+
out = {
|
|
591
|
+
contractVersion: CONTRACT_VERSION,
|
|
592
|
+
version: activeToolkitVersion(),
|
|
593
|
+
tools: await client.listTools(),
|
|
594
|
+
};
|
|
569
595
|
} else if (cmd === "call" && name) {
|
|
570
596
|
let args = {};
|
|
571
597
|
if (raw !== undefined) {
|
|
@@ -139,9 +139,12 @@ const TOKEN_CHAR = /[A-Za-z0-9_\-./+=:@~%]/;
|
|
|
139
139
|
* `text` cut to `limit` characters, backed off to the start of a token that
|
|
140
140
|
* the cut would split, so no half-credential survives on the boundary.
|
|
141
141
|
*/
|
|
142
|
+
// The " ..." ending counts toward the limit, so a cut text is never longer
|
|
143
|
+
// than the field that holds it.
|
|
142
144
|
function cut(text, limit) {
|
|
143
145
|
if (text.length <= limit) return text;
|
|
144
|
-
|
|
146
|
+
if (limit < 5) return text.slice(0, limit);
|
|
147
|
+
let end = limit - 4;
|
|
145
148
|
while (end > 0 && TOKEN_CHAR.test(text[end]) && TOKEN_CHAR.test(text[end - 1])) end--;
|
|
146
149
|
return `${text.slice(0, end).trimEnd()} ...`;
|
|
147
150
|
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* Storage: append-only JSONL, one file per repo:
|
|
11
11
|
* $HOME/.claude/memory/multi-agent/<repo-slug>/triage-corpus.jsonl
|
|
12
12
|
*
|
|
13
|
-
* Each line conforms to pipeline/schemas/triage-corpus.schema.json.
|
|
13
|
+
* Each line conforms to pipeline/schemas/triage-corpus.schema.json. Every row
|
|
14
|
+
* passes the secret scan in _secret-scan.mjs before it is written; one that
|
|
15
|
+
* fails is counted in `droppedBySecretScan` and left out.
|
|
14
16
|
*
|
|
15
17
|
* Why JSONL not SQLite: zero deps, Node 18+ compatible, easy to grep/diff,
|
|
16
18
|
* easy to delete a single row by line. The cost of a full corpus scan is
|
|
@@ -55,6 +57,8 @@ import { readFileSync, existsSync, mkdirSync, appendFileSync, statSync } from "n
|
|
|
55
57
|
import { homedir } from "node:os";
|
|
56
58
|
import { execFileSync } from "node:child_process";
|
|
57
59
|
import { join, basename, dirname, posix } from "node:path";
|
|
60
|
+
import { checkedSlug, memoryExcluded } from "./_memory-policy.mjs";
|
|
61
|
+
import { openScanner, stripPrivate } from "./_secret-scan.mjs";
|
|
58
62
|
import {
|
|
59
63
|
bm25,
|
|
60
64
|
recencyRank,
|
|
@@ -91,6 +95,10 @@ function die(msg, code = 1) {
|
|
|
91
95
|
// prior-art recall always miss. `--git-common-dir` points at the main
|
|
92
96
|
// repository's .git for every linked worktree.
|
|
93
97
|
function repoSlug() {
|
|
98
|
+
return checkedSlug(rawRepoSlug());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rawRepoSlug() {
|
|
94
102
|
if (flags["repo-slug"]) return String(flags["repo-slug"]);
|
|
95
103
|
if (process.env.MULTI_AGENT_REPO_SLUG) return process.env.MULTI_AGENT_REPO_SLUG;
|
|
96
104
|
try {
|
|
@@ -249,9 +257,14 @@ function ingest() {
|
|
|
249
257
|
const fromState = stateDefaults();
|
|
250
258
|
const taskId = flags["task-id"] || fromState.taskId;
|
|
251
259
|
if (!taskId) die("ingest needs --task-id <id> (or --state with a taskId)", 1);
|
|
252
|
-
const taskTitle = flags["task-title"] || fromState.taskTitle || null;
|
|
260
|
+
const taskTitle = stripPrivate(flags["task-title"] || fromState.taskTitle || "").trim() || null;
|
|
253
261
|
const stack = flags.stack || fromState.stack || null;
|
|
254
262
|
const slug = repoSlug();
|
|
263
|
+
if (memoryExcluded(slug)) {
|
|
264
|
+
process.stdout.write(JSON.stringify({ ok: true, slug, ingested: 0, excluded: true }) + "\n");
|
|
265
|
+
process.exitCode = 2;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
255
268
|
|
|
256
269
|
let triage;
|
|
257
270
|
try {
|
|
@@ -274,35 +287,51 @@ function ingest() {
|
|
|
274
287
|
];
|
|
275
288
|
|
|
276
289
|
let written = 0;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
290
|
+
let droppedBySecretScan = 0;
|
|
291
|
+
const scan = openScanner();
|
|
292
|
+
try {
|
|
293
|
+
for (const [classification, rows] of buckets) {
|
|
294
|
+
for (const f of rows) {
|
|
295
|
+
const issue = stripPrivate(f.issue || "").trim();
|
|
296
|
+
if (!issue) continue;
|
|
297
|
+
const file = f.file || "";
|
|
298
|
+
const key = `${taskId}::${classification}::${file}::${issue}`;
|
|
299
|
+
if (seen.has(key)) continue;
|
|
300
|
+
const fix = f.fix ? stripPrivate(f.fix).trim() || null : null;
|
|
301
|
+
const reason = f._reason ? stripPrivate(f._reason).trim() || null : null;
|
|
302
|
+
const reviewer = f.reviewer || null;
|
|
303
|
+
const stored = [issue, fix, reason, file, taskTitle, taskId, stack, reviewer, f.severity];
|
|
304
|
+
if (!scan.isClean(stored.filter(Boolean).join("\n"))) {
|
|
305
|
+
droppedBySecretScan++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const row = {
|
|
309
|
+
v: "1.0.0",
|
|
310
|
+
ts,
|
|
311
|
+
task_id: taskId,
|
|
312
|
+
task_title: taskTitle,
|
|
313
|
+
stack,
|
|
314
|
+
classification,
|
|
315
|
+
severity: f.severity || "suggestion",
|
|
316
|
+
file,
|
|
317
|
+
line: typeof f.line === "number" ? f.line : null,
|
|
318
|
+
issue,
|
|
319
|
+
fix,
|
|
320
|
+
reason,
|
|
321
|
+
reviewer,
|
|
322
|
+
file_sha: fileSha(file, top),
|
|
323
|
+
};
|
|
324
|
+
writeRow(slug, row);
|
|
325
|
+
seen.add(key);
|
|
326
|
+
written++;
|
|
327
|
+
}
|
|
303
328
|
}
|
|
329
|
+
} finally {
|
|
330
|
+
scan.close();
|
|
304
331
|
}
|
|
305
|
-
process.stdout.write(
|
|
332
|
+
process.stdout.write(
|
|
333
|
+
JSON.stringify({ ok: true, slug, ingested: written, droppedBySecretScan }) + "\n",
|
|
334
|
+
);
|
|
306
335
|
// exitCode, not exit(): even this one-line write is on the same rule, and a
|
|
307
336
|
// rule with an exception is the one that gets copied into the next branch.
|
|
308
337
|
// 2 = nothing new, which callers treat as an idempotent success.
|
|
@@ -313,7 +342,7 @@ function ingest() {
|
|
|
313
342
|
function query() {
|
|
314
343
|
const cfg = recallConfig();
|
|
315
344
|
const topN = flags.top ? Math.max(1, Number(flags.top)) : cfg.maxResults;
|
|
316
|
-
const slug =
|
|
345
|
+
const slug = repoSlug();
|
|
317
346
|
const corpus = readCorpus(slug);
|
|
318
347
|
if (corpus.length === 0) {
|
|
319
348
|
process.stdout.write(JSON.stringify({ ok: true, slug, hits: [], total: 0 }) + "\n");
|
|
@@ -347,7 +376,7 @@ function priorArt() {
|
|
|
347
376
|
: [];
|
|
348
377
|
const cfg = recallConfig();
|
|
349
378
|
const topN = flags.top ? Math.max(1, Number(flags.top)) : cfg.maxResults;
|
|
350
|
-
const slug =
|
|
379
|
+
const slug = repoSlug();
|
|
351
380
|
const corpus = readCorpus(slug);
|
|
352
381
|
const out = list.map((f) => {
|
|
353
382
|
if (corpus.length === 0) return [];
|
|
@@ -436,7 +465,7 @@ function rank(corpus, issueText, fileGlob, topN, cfg) {
|
|
|
436
465
|
function showCmd() {
|
|
437
466
|
const id = flags.id && flags.id !== true ? String(flags.id) : null;
|
|
438
467
|
if (!id) die("show needs --id <id>", 1);
|
|
439
|
-
const slug =
|
|
468
|
+
const slug = repoSlug();
|
|
440
469
|
const want = id.startsWith("T:") ? id : `T:${id}`;
|
|
441
470
|
const row = readCorpus(slug).find((r) => rowId(r) === want);
|
|
442
471
|
if (!row) {
|
|
@@ -465,7 +494,7 @@ function showCmd() {
|
|
|
465
494
|
function timelineCmd() {
|
|
466
495
|
const id = flags.anchor && flags.anchor !== true ? String(flags.anchor) : null;
|
|
467
496
|
if (!id) die("timeline needs --anchor <id>", 1);
|
|
468
|
-
const slug =
|
|
497
|
+
const slug = repoSlug();
|
|
469
498
|
const before = flags.before && flags.before !== true ? Math.max(0, Number(flags.before)) : 3;
|
|
470
499
|
const after = flags.after && flags.after !== true ? Math.max(0, Number(flags.after)) : 3;
|
|
471
500
|
const want = id.startsWith("T:") ? id : `T:${id}`;
|
|
@@ -495,15 +524,13 @@ function timelineCmd() {
|
|
|
495
524
|
}
|
|
496
525
|
|
|
497
526
|
function pathCmd() {
|
|
498
|
-
process.stdout.write(
|
|
499
|
-
corpusPath(flags["repo-slug"] ? String(flags["repo-slug"]) : repoSlug()) + "\n",
|
|
500
|
-
);
|
|
527
|
+
process.stdout.write(corpusPath(repoSlug()) + "\n");
|
|
501
528
|
process.exitCode = 0;
|
|
502
529
|
return;
|
|
503
530
|
}
|
|
504
531
|
|
|
505
532
|
function statsCmd() {
|
|
506
|
-
const slug =
|
|
533
|
+
const slug = repoSlug();
|
|
507
534
|
const corpus = readCorpus(slug);
|
|
508
535
|
const counts = { accepted: 0, deferred: 0, rejected: 0 };
|
|
509
536
|
for (const r of corpus) if (counts[r.classification] !== undefined) counts[r.classification]++;
|