@mmerterden/multi-agent-pipeline 20.29.0 → 20.33.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 +86 -0
- package/README.md +50 -5
- package/README.tr.md +64 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +9 -9
- package/install/_unattended-profile.mjs +61 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +19 -3
- package/install/codex.mjs +2 -0
- package/install/copilot.mjs +13 -0
- package/install/index.mjs +10 -1
- package/install/templates/claude-hooks.json +12 -1
- package/manifest.json +230 -88
- package/package.json +3 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +13 -15
- package/pipeline/commands/multi-agent/office-off/SKILL.md +27 -0
- package/pipeline/commands/multi-agent/office-on/SKILL.md +51 -0
- package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +4 -3
- package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/serve/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/stack/SKILL.md +15 -8
- package/pipeline/commands/multi-agent/steer/SKILL.md +10 -9
- package/pipeline/commands/multi-agent/sync/SKILL.md +15 -3
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
- package/pipeline/contract/CHANGELOG.md +108 -0
- package/pipeline/contract/README.md +72 -12
- 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 +506 -27
- package/pipeline/contract/types/index.d.ts +829 -4
- 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/cross-cli-contract.md +5 -4
- package/pipeline/multi-agent-refs/features/office-threat-model.md +109 -0
- package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +3 -0
- package/pipeline/multi-agent-refs/help/tr.md +3 -0
- package/pipeline/multi-agent-refs/phases/modes.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +16 -10
- package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
- package/pipeline/office/NOTICE.md +45 -0
- package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
- package/pipeline/office/assets/BIN.png +0 -0
- package/pipeline/office/assets/BOOKSHELF.png +0 -0
- package/pipeline/office/assets/CACTUS.png +0 -0
- package/pipeline/office/assets/CLOCK.png +0 -0
- package/pipeline/office/assets/COFFEE.png +0 -0
- package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
- package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
- package/pipeline/office/assets/COUNTER_SINK.png +0 -0
- package/pipeline/office/assets/CUSHIONED_BENCH.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_BACK.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_FRONT.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_SIDE.png +0 -0
- package/pipeline/office/assets/DESK_FRONT.png +0 -0
- package/pipeline/office/assets/DESK_SIDE.png +0 -0
- package/pipeline/office/assets/DEVICE_STAND.png +0 -0
- package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
- package/pipeline/office/assets/FILING_CABINET.png +0 -0
- package/pipeline/office/assets/FRIDGE.png +0 -0
- package/pipeline/office/assets/HANGING_PLANT.png +0 -0
- package/pipeline/office/assets/LARGE_PAINTING.png +0 -0
- package/pipeline/office/assets/LARGE_PLANT.png +0 -0
- package/pipeline/office/assets/MAILBOX.png +0 -0
- package/pipeline/office/assets/MOP_BUCKET.png +0 -0
- package/pipeline/office/assets/PC_BACK.png +0 -0
- package/pipeline/office/assets/PC_FRONT_OFF.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_1.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_2.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_3.png +0 -0
- package/pipeline/office/assets/PC_SIDE.png +0 -0
- package/pipeline/office/assets/PLANT.png +0 -0
- package/pipeline/office/assets/PLANT_2.png +0 -0
- package/pipeline/office/assets/POT.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/SMALL_PAINTING.png +0 -0
- package/pipeline/office/assets/SMALL_PAINTING_2.png +0 -0
- package/pipeline/office/assets/SMALL_TABLE_FRONT.png +0 -0
- package/pipeline/office/assets/SMALL_TABLE_SIDE.png +0 -0
- package/pipeline/office/assets/SOFA_BACK.png +0 -0
- package/pipeline/office/assets/SOFA_FRONT.png +0 -0
- package/pipeline/office/assets/SOFA_SIDE.png +0 -0
- package/pipeline/office/assets/TABLE_FRONT.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/assets/WHITEBOARD.png +0 -0
- package/pipeline/office/assets/WOODEN_BENCH.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_BACK.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_FRONT.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_SIDE.png +0 -0
- package/pipeline/office/assets/char_0.png +0 -0
- package/pipeline/office/assets/char_1.png +0 -0
- package/pipeline/office/assets/char_2.png +0 -0
- package/pipeline/office/assets/char_3.png +0 -0
- package/pipeline/office/assets/char_4.png +0 -0
- package/pipeline/office/assets/char_5.png +0 -0
- package/pipeline/office/assets/floor_0.png +0 -0
- package/pipeline/office/assets/floor_1.png +0 -0
- package/pipeline/office/assets/floor_2.png +0 -0
- package/pipeline/office/assets/floor_3.png +0 -0
- package/pipeline/office/assets/floor_4.png +0 -0
- package/pipeline/office/assets/floor_5.png +0 -0
- package/pipeline/office/assets/floor_6.png +0 -0
- package/pipeline/office/assets/floor_7.png +0 -0
- package/pipeline/office/assets/floor_8.png +0 -0
- package/pipeline/office/assets/wall_0.png +0 -0
- package/pipeline/office/index.html +8431 -0
- package/pipeline/office/toolkit.json +3 -0
- package/pipeline/schemas/activity-chat.schema.json +78 -0
- package/pipeline/schemas/activity-roles.json +48 -0
- package/pipeline/schemas/activity-roles.schema.json +49 -0
- package/pipeline/schemas/activity.schema.json +613 -0
- package/pipeline/schemas/agent-state.schema.json +493 -115
- package/pipeline/schemas/autopilot-on-request.schema.json +33 -0
- package/pipeline/schemas/autopilot-on.schema.json +111 -0
- package/pipeline/schemas/contract-error.schema.json +45 -4
- package/pipeline/schemas/issue-maturity.schema.json +68 -0
- 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/ledger.schema.json +202 -0
- 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/permissions.schema.json +47 -0
- package/pipeline/schemas/phases.json +10 -2
- package/pipeline/schemas/phases.schema.json +86 -19
- package/pipeline/schemas/prefs.schema.json +19 -0
- package/pipeline/schemas/prs.schema.json +78 -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/review-verdict-request.schema.json +20 -0
- package/pipeline/schemas/review-verdict.schema.json +62 -0
- package/pipeline/schemas/routine-delete.schema.json +59 -0
- package/pipeline/schemas/routines.schema.json +38 -0
- package/pipeline/schemas/run-evidence.schema.json +29 -0
- package/pipeline/schemas/run-results.schema.json +218 -0
- package/pipeline/schemas/runs-index.schema.json +257 -46
- package/pipeline/schemas/service-read.schema.json +58 -0
- package/pipeline/schemas/stack-change.schema.json +82 -0
- package/pipeline/schemas/stack-request.schema.json +33 -0
- package/pipeline/schemas/stack.schema.json +59 -0
- package/pipeline/schemas/steer-request.schema.json +25 -0
- package/pipeline/schemas/steer.schema.json +59 -0
- package/pipeline/schemas/system-status.schema.json +111 -0
- package/pipeline/schemas/tool-call-request.schema.json +19 -0
- package/pipeline/schemas/tool-call.schema.json +47 -0
- package/pipeline/schemas/tools.schema.json +65 -0
- package/pipeline/schemas/unattended-profile.json +7 -2
- package/pipeline/scripts/_active-run.mjs +2 -1
- package/pipeline/scripts/_cost.mjs +66 -1
- package/pipeline/scripts/_office-smoke-lib.sh +115 -0
- package/pipeline/scripts/_spawn-async.mjs +77 -0
- package/pipeline/scripts/_transcript-tail.mjs +274 -0
- package/pipeline/scripts/activity-snapshot.mjs +1223 -0
- package/pipeline/scripts/autopilot-control.mjs +214 -9
- package/pipeline/scripts/autopilot-runner.mjs +7 -5
- package/pipeline/scripts/contract-server.mjs +1853 -197
- package/pipeline/scripts/cost-analyze.mjs +2 -53
- package/pipeline/scripts/cost-table.json +1 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
- package/pipeline/scripts/job-launch.mjs +477 -0
- package/pipeline/scripts/launch-request.mjs +94 -9
- package/pipeline/scripts/office-build.mjs +226 -0
- package/pipeline/scripts/office-control.mjs +410 -0
- package/pipeline/scripts/office-credentials.mjs +106 -0
- package/pipeline/scripts/office-ledger.mjs +409 -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/prune-logs.sh +53 -2
- package/pipeline/scripts/routine-registry.mjs +42 -6
- package/pipeline/scripts/run-results.mjs +373 -0
- package/pipeline/scripts/runs-index.mjs +62 -2
- package/pipeline/scripts/service-read.mjs +244 -0
- package/pipeline/scripts/stack-toggle.mjs +285 -0
- package/pipeline/scripts/steer-run.mjs +97 -0
- 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 +625 -0
- package/pipeline/scripts/transcript-index.mjs +4 -1
- package/pipeline/scripts/uninstall.mjs +3 -0
- package/pipeline/scripts/work-items.mjs +473 -0
- package/pipeline/skills/.skill-manifest.json +23 -11
- package/pipeline/skills/.skills-index.json +34 -1
- package/pipeline/skills/shared/README.md +6 -3
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +13 -15
- package/pipeline/skills/shared/core/multi-agent-office-off/SKILL.md +16 -0
- package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +21 -0
- package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -0
- package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +14 -6
- package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +10 -9
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
- package/pipeline/skills/skills-index.md +4 -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
|