@mmerterden/multi-agent-pipeline 20.30.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 +80 -0
- package/README.md +45 -0
- package/README.tr.md +59 -0
- package/docs/facts.json +3 -3
- package/install/_unattended-profile.mjs +61 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +9 -2
- package/install/index.mjs +10 -1
- package/install/templates/claude-hooks.json +12 -1
- package/manifest.json +123 -82
- package/package.json +2 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
- 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/review/SKILL.md +4 -3
- package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
- 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 +22 -8
- 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/features/office-threat-model.md +24 -6
- 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/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 +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 +3996 -884
- package/pipeline/office/toolkit.json +3 -0
- package/pipeline/schemas/activity.schema.json +188 -4
- package/pipeline/schemas/agent-state.schema.json +493 -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 +10 -2
- package/pipeline/schemas/phases.schema.json +86 -19
- 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/runs-index.schema.json +257 -46
- package/pipeline/schemas/system-status.schema.json +111 -0
- package/pipeline/schemas/tools.schema.json +5 -0
- package/pipeline/schemas/unattended-profile.json +7 -2
- package/pipeline/scripts/_active-run.mjs +2 -1
- package/pipeline/scripts/_spawn-async.mjs +9 -5
- package/pipeline/scripts/activity-snapshot.mjs +219 -23
- package/pipeline/scripts/autopilot-runner.mjs +7 -5
- package/pipeline/scripts/contract-server.mjs +551 -25
- 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 +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/prune-logs.sh +53 -2
- package/pipeline/scripts/runs-index.mjs +62 -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/work-items.mjs +191 -22
- package/pipeline/skills/.skill-manifest.json +6 -6
- package/pipeline/skills/.skills-index.json +1 -1
- package/pipeline/skills/shared/README.md +1 -1
- 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 +19 -0
- package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
- package/pipeline/skills/skills-index.md +1 -1
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* strings.json) and this script inlines them into
|
|
9
9
|
* pipeline/office/index.html: the list of sprite names in pipeline/office/assets
|
|
10
10
|
* (served by contract-server.mjs at /office/assets/), the strings as a JSON
|
|
11
|
-
* literal, the two code files in order inside one IIFE.
|
|
11
|
+
* literal, the two code files in order inside one IIFE. It also renders the
|
|
12
|
+
* pixel grids in pipeline/office-art into PNG sprites in pipeline/office/assets
|
|
13
|
+
* and writes pipeline/office/toolkit.json, the toolkit version shipped with
|
|
14
|
+
* the page; --check holds all three to their sources.
|
|
12
15
|
*
|
|
13
16
|
* node pipeline/scripts/office-build.mjs write the page
|
|
14
17
|
* node pipeline/scripts/office-build.mjs --check exit 1 when the page on
|
|
@@ -16,7 +19,9 @@
|
|
|
16
19
|
* sources build to
|
|
17
20
|
*/
|
|
18
21
|
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
19
23
|
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { deflateSync } from "node:zlib";
|
|
20
25
|
import { basename, join } from "node:path";
|
|
21
26
|
import { pathToFileURL } from "node:url";
|
|
22
27
|
|
|
@@ -24,8 +29,94 @@ const ROOT = join(import.meta.dirname, "..");
|
|
|
24
29
|
const SRC = join(ROOT, "office-src");
|
|
25
30
|
const OUT = join(ROOT, "office", "index.html");
|
|
26
31
|
const ASSETS = join(ROOT, "office", "assets");
|
|
32
|
+
const TOOLKIT = join(ROOT, "office", "toolkit.json");
|
|
33
|
+
const ART = join(ROOT, "office-art");
|
|
34
|
+
const FACTS = join(ROOT, "..", "docs", "facts.json");
|
|
27
35
|
const PNG_SIGNATURE = "89504e470d0a1a0a";
|
|
28
36
|
|
|
37
|
+
// --- the project's own sprites -----------------------------------------------
|
|
38
|
+
// Each office-art/<NAME>.txt is a "size WxH" line and H rows of W letters, one
|
|
39
|
+
// letter per pixel from office-art/palette.json, "." transparent. They are
|
|
40
|
+
// encoded here into office/assets/<NAME>.png byte for byte the same each time
|
|
41
|
+
// (no timestamp, fixed compression), so --check can hold the PNGs to their
|
|
42
|
+
// text and a review reads the drawing as a diff.
|
|
43
|
+
|
|
44
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
45
|
+
let c = n;
|
|
46
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
47
|
+
return c >>> 0;
|
|
48
|
+
});
|
|
49
|
+
function crc32(buf) {
|
|
50
|
+
let c = 0xffffffff;
|
|
51
|
+
for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
52
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
53
|
+
}
|
|
54
|
+
function chunk(type, data) {
|
|
55
|
+
const len = Buffer.alloc(4);
|
|
56
|
+
len.writeUInt32BE(data.length);
|
|
57
|
+
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
58
|
+
const crc = Buffer.alloc(4);
|
|
59
|
+
crc.writeUInt32BE(crc32(body));
|
|
60
|
+
return Buffer.concat([len, body, crc]);
|
|
61
|
+
}
|
|
62
|
+
/** An RGBA PNG, unfiltered rows, zlib level 9. */
|
|
63
|
+
export function encodePng(width, height, rgba) {
|
|
64
|
+
const header = Buffer.alloc(13);
|
|
65
|
+
header.writeUInt32BE(width, 0);
|
|
66
|
+
header.writeUInt32BE(height, 4);
|
|
67
|
+
header[8] = 8;
|
|
68
|
+
header[9] = 6;
|
|
69
|
+
const raw = Buffer.alloc((width * 4 + 1) * height);
|
|
70
|
+
for (let y = 0; y < height; y++) {
|
|
71
|
+
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat([
|
|
74
|
+
Buffer.from(PNG_SIGNATURE, "hex"),
|
|
75
|
+
chunk("IHDR", header),
|
|
76
|
+
chunk("IDAT", deflateSync(raw, { level: 9 })),
|
|
77
|
+
chunk("IEND", Buffer.alloc(0)),
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One art file as a PNG; throws on a size line, row length or letter that does not fit. */
|
|
82
|
+
export function renderArt(text, colors, name = "sprite") {
|
|
83
|
+
const lines = text.replace(/\r/g, "").trimEnd().split("\n");
|
|
84
|
+
const m = /^size (\d{1,3})x(\d{1,3})$/.exec(lines[0] ?? "");
|
|
85
|
+
if (!m) throw new Error(`${name}: the first line is "size WxH"`);
|
|
86
|
+
const [w, h] = [Number(m[1]), Number(m[2])];
|
|
87
|
+
const rows = lines.slice(1);
|
|
88
|
+
if (rows.length !== h) throw new Error(`${name}: ${rows.length} rows, expected ${h}`);
|
|
89
|
+
const rgba = Buffer.alloc(w * h * 4);
|
|
90
|
+
rows.forEach((row, y) => {
|
|
91
|
+
if (row.length !== w)
|
|
92
|
+
throw new Error(`${name}: row ${y + 1} is ${row.length} wide, expected ${w}`);
|
|
93
|
+
[...row].forEach((ch, x) => {
|
|
94
|
+
if (ch === ".") return;
|
|
95
|
+
const hex = colors[ch];
|
|
96
|
+
if (!hex) throw new Error(`${name}: "${ch}" is not in the palette`);
|
|
97
|
+
const o = (y * w + x) * 4;
|
|
98
|
+
rgba[o] = parseInt(hex.slice(0, 2), 16);
|
|
99
|
+
rgba[o + 1] = parseInt(hex.slice(2, 4), 16);
|
|
100
|
+
rgba[o + 2] = parseInt(hex.slice(4, 6), 16);
|
|
101
|
+
rgba[o + 3] = 255;
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
return encodePng(w, h, rgba);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every art sprite, name -> PNG bytes. */
|
|
108
|
+
export function artSprites() {
|
|
109
|
+
const { colors } = JSON.parse(readFileSync(join(ART, "palette.json"), "utf8"));
|
|
110
|
+
const out = new Map();
|
|
111
|
+
for (const f of readdirSync(ART).sort()) {
|
|
112
|
+
if (!f.endsWith(".txt")) continue;
|
|
113
|
+
const name = basename(f, ".txt");
|
|
114
|
+
if (!/^[A-Z0-9_]{1,48}$/.test(name)) throw new Error(`${f}: sprite names are A-Z, 0-9 and _`);
|
|
115
|
+
out.set(name, renderArt(readFileSync(join(ART, f), "utf8"), colors, name));
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
29
120
|
// The sprite names the page may load from /office/assets/: every PNG beside
|
|
30
121
|
// the page, checked to be one.
|
|
31
122
|
function atlas() {
|
|
@@ -41,6 +132,20 @@ function atlas() {
|
|
|
41
132
|
return out;
|
|
42
133
|
}
|
|
43
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The toolkit version the office's Tools window starts, shipped beside the
|
|
137
|
+
* page: the version this release was checked against, from docs/facts.json.
|
|
138
|
+
* The host runs the toolkit as @latest; the panel only runs an exact version,
|
|
139
|
+
* and every pipeline update brings the newer one with it.
|
|
140
|
+
*/
|
|
141
|
+
export function toolkitDoc() {
|
|
142
|
+
const v = JSON.parse(readFileSync(FACTS, "utf8")).toolkitVersion;
|
|
143
|
+
if (typeof v !== "string" || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v)) {
|
|
144
|
+
throw new Error("docs/facts.json carries no exact toolkitVersion");
|
|
145
|
+
}
|
|
146
|
+
return JSON.stringify({ toolkitVersion: v }, null, 2) + "\n";
|
|
147
|
+
}
|
|
148
|
+
|
|
44
149
|
export function build() {
|
|
45
150
|
const read = (f) => readFileSync(join(SRC, f), "utf8");
|
|
46
151
|
const strings = JSON.parse(read("strings.json"));
|
|
@@ -56,6 +161,12 @@ export function build() {
|
|
|
56
161
|
"})();",
|
|
57
162
|
].join("\n");
|
|
58
163
|
const style = read("style.css").trim();
|
|
164
|
+
// The build id the server reads back from the installed page: the page
|
|
165
|
+
// compares it with its own to tell that a newer one has been installed.
|
|
166
|
+
const hash = createHash("sha256").update(script).update(style).update(read("shell.html"));
|
|
167
|
+
// Redrawn sprites change the page too, though no source text did.
|
|
168
|
+
for (const [name, png] of artSprites()) hash.update(name).update(png);
|
|
169
|
+
const build = hash.digest("hex").slice(0, 12);
|
|
59
170
|
// Text that would close the inline element early, or open an HTML comment
|
|
60
171
|
// inside it, turns source into markup.
|
|
61
172
|
if (/<\/script|<!--/i.test(script)) throw new Error("the script contains </script or <!--");
|
|
@@ -64,26 +175,52 @@ export function build() {
|
|
|
64
175
|
for (const marker of ["/*STYLE*/", "/*SCRIPT*/"]) {
|
|
65
176
|
if (shell.split(marker).length !== 2) throw new Error(`shell.html needs exactly one ${marker}`);
|
|
66
177
|
}
|
|
67
|
-
|
|
178
|
+
const stamped = script.replace('"use strict";', () => `"use strict";\nconst BUILD = "${build}";`);
|
|
179
|
+
return shell.replace("/*STYLE*/", () => style).replace("/*SCRIPT*/", () => stamped);
|
|
68
180
|
}
|
|
69
181
|
|
|
70
182
|
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
71
183
|
if (isMain) {
|
|
184
|
+
const art = artSprites();
|
|
185
|
+
const check = process.argv.includes("--check");
|
|
186
|
+
if (!check) for (const [name, png] of art) writeFileSync(join(ASSETS, `${name}.png`), png);
|
|
72
187
|
const html = build();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
188
|
+
const toolkit = toolkitDoc();
|
|
189
|
+
if (check) {
|
|
190
|
+
for (const [name, png] of art) {
|
|
191
|
+
let on = null;
|
|
192
|
+
try {
|
|
193
|
+
on = readFileSync(join(ASSETS, `${name}.png`));
|
|
194
|
+
} catch {
|
|
195
|
+
// Missing is stale.
|
|
196
|
+
}
|
|
197
|
+
if (!on || !on.equals(png)) {
|
|
198
|
+
console.error(
|
|
199
|
+
`office/assets/${name}.png is stale: run node pipeline/scripts/office-build.mjs`,
|
|
200
|
+
);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
79
203
|
}
|
|
80
|
-
|
|
204
|
+
const read = (f) => {
|
|
205
|
+
try {
|
|
206
|
+
return readFileSync(f, "utf8");
|
|
207
|
+
} catch {
|
|
208
|
+
// A missing file is stale by definition.
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
if (read(OUT) !== html) {
|
|
81
213
|
console.error("office/index.html is stale: run node pipeline/scripts/office-build.mjs");
|
|
82
214
|
process.exit(1);
|
|
83
215
|
}
|
|
84
|
-
|
|
216
|
+
if (read(TOOLKIT) !== toolkit) {
|
|
217
|
+
console.error("office/toolkit.json is stale: run node pipeline/scripts/office-build.mjs");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
console.log("office/index.html and office/toolkit.json match their sources");
|
|
85
221
|
} else {
|
|
86
222
|
writeFileSync(OUT, html);
|
|
87
|
-
|
|
223
|
+
writeFileSync(TOOLKIT, toolkit);
|
|
224
|
+
console.log(`wrote ${OUT} (${Buffer.byteLength(html)} bytes) and ${TOOLKIT}`);
|
|
88
225
|
}
|
|
89
226
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* on [--no-open] start contract-server.mjs in the background with the
|
|
8
8
|
* installed panel page (../office/index.html, passed as
|
|
9
|
-
* MA_OFFICE_PAGE)
|
|
9
|
+
* MA_OFFICE_PAGE), running until `off`, or reuse the one this script
|
|
10
10
|
* started if it is still alive, then open the browser at
|
|
11
11
|
* /office with a one-time code in the URL fragment,
|
|
12
12
|
* through a 0600 launcher file so the code is never on a
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
* script only reports it; it carries no remote code.
|
|
31
31
|
*
|
|
32
32
|
* State: one 0600 file, ~/.claude/logs/multi-agent/office/office.json, with
|
|
33
|
-
* the pid, port and token file of the server this script started.
|
|
34
|
-
*
|
|
33
|
+
* the pid, port and token file of the server this script started. The server
|
|
34
|
+
* runs until office-off stops it; no daemon, hook or watcher runs beside it.
|
|
35
35
|
*
|
|
36
36
|
* Usage:
|
|
37
37
|
* office-control.mjs on [--no-open] [--json]
|
|
@@ -56,11 +56,16 @@ import {
|
|
|
56
56
|
} from "node:fs";
|
|
57
57
|
import { homedir } from "node:os";
|
|
58
58
|
import { dirname, join } from "node:path";
|
|
59
|
+
|
|
60
|
+
import { codeDigest } from "../lib/code-digest.mjs";
|
|
59
61
|
import { fileURLToPath } from "node:url";
|
|
60
62
|
|
|
61
63
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
// The panel runs until office-off: no idle stop and no lifetime, because a
|
|
65
|
+
// person keeps it open beside their work. The server still binds 127.0.0.1
|
|
66
|
+
// only, and every office-on that starts one mints a new token.
|
|
67
|
+
const IDLE_MINUTES = 0;
|
|
68
|
+
const MAX_HOURS = 0;
|
|
64
69
|
/** How far a process's start may sit before its token file's startedAt. */
|
|
65
70
|
const START_SKEW_MS = 120 * 1000;
|
|
66
71
|
|
|
@@ -248,10 +253,23 @@ function openBrowser(target, env) {
|
|
|
248
253
|
return r.status === 0;
|
|
249
254
|
}
|
|
250
255
|
|
|
251
|
-
|
|
256
|
+
const digestOf = (server) => (server ? codeDigest(dirname(server)) : null);
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Start (or reuse) the panel server and open the browser. A running server
|
|
260
|
+
* whose code on disk has changed since it started (an update installed a new
|
|
261
|
+
* pipeline) is stopped and a new one started, so the panel never runs on a
|
|
262
|
+
* server older than its page.
|
|
263
|
+
*/
|
|
252
264
|
export async function officeOn({ env = process.env, open = true } = {}) {
|
|
253
265
|
let s = current(env);
|
|
254
266
|
let reused = true;
|
|
267
|
+
let restarted = false;
|
|
268
|
+
if (s && s.serverDigest && s.serverDigest !== digestOf(serverScript(env))) {
|
|
269
|
+
officeOff({ env });
|
|
270
|
+
s = null;
|
|
271
|
+
restarted = true;
|
|
272
|
+
}
|
|
255
273
|
if (!s) {
|
|
256
274
|
reused = false;
|
|
257
275
|
const server = serverScript(env);
|
|
@@ -289,13 +307,22 @@ export async function officeOn({ env = process.env, open = true } = {}) {
|
|
|
289
307
|
idleMinutes: info.idleMinutes,
|
|
290
308
|
maxHours: info.maxHours ?? null,
|
|
291
309
|
startedAt: new Date().toISOString(),
|
|
310
|
+
serverDigest: digestOf(server),
|
|
292
311
|
});
|
|
293
312
|
s = current(env);
|
|
294
313
|
if (!s) throw new Error("the server stopped right after it started");
|
|
295
314
|
}
|
|
296
315
|
const page = `${s.url}/office`;
|
|
297
316
|
const opened = open ? openBrowser(`${page}#code=${await panelCode(s)}`, env) : false;
|
|
298
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
running: true,
|
|
319
|
+
reused,
|
|
320
|
+
restarted,
|
|
321
|
+
url: page,
|
|
322
|
+
pid: s.pid,
|
|
323
|
+
idleMinutes: s.idleMinutes,
|
|
324
|
+
opened,
|
|
325
|
+
};
|
|
299
326
|
}
|
|
300
327
|
|
|
301
328
|
/** Stop the server this script started. */
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-credentials.mjs - the key cabinet: which services the pipeline
|
|
4
|
+
* holds a credential for and whether each one works, never a value.
|
|
5
|
+
*
|
|
6
|
+
* The answer comes from lib/credential-inventory.sh, the same inventory a run
|
|
7
|
+
* consults before asking the user for data an external system holds. That
|
|
8
|
+
* script probes a credential through the credential store with stdout
|
|
9
|
+
* discarded, so a secret never reaches this process; this file keeps only the
|
|
10
|
+
* logical name, the state, the probe verdict and what the credential is for,
|
|
11
|
+
* each checked against a fixed vocabulary.
|
|
12
|
+
*
|
|
13
|
+
* Renewing a credential stays in the terminal (`/multi-agent:setup`): the
|
|
14
|
+
* secret never passes through the browser.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* office-credentials.mjs [--json] [--probe]
|
|
18
|
+
*
|
|
19
|
+
* Exit: 0 printed, 2 usage.
|
|
20
|
+
*
|
|
21
|
+
* @module pipeline/scripts/office-credentials
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
29
|
+
import { spawnAsync } from "./_spawn-async.mjs";
|
|
30
|
+
|
|
31
|
+
export const CONTRACT_VERSION = "1.0.0";
|
|
32
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const STATES = new Set(["present", "mapped-but-missing", "unmapped"]);
|
|
34
|
+
const REACH = new Set([
|
|
35
|
+
"not-probed",
|
|
36
|
+
"reachable",
|
|
37
|
+
"auth-rejected",
|
|
38
|
+
"unreachable",
|
|
39
|
+
"no-host-configured",
|
|
40
|
+
"well-formed",
|
|
41
|
+
"not-probeable",
|
|
42
|
+
"malformed",
|
|
43
|
+
"no-credential",
|
|
44
|
+
"probe-error",
|
|
45
|
+
"server-error",
|
|
46
|
+
"tier-1-ready",
|
|
47
|
+
"tier-2-ready",
|
|
48
|
+
"tier-1-no-grant",
|
|
49
|
+
]);
|
|
50
|
+
const NAME = /^[a-z][a-z0-9_]{0,60}$/;
|
|
51
|
+
|
|
52
|
+
function inventoryPath(env) {
|
|
53
|
+
if (env.MA_CREDENTIAL_INVENTORY) return env.MA_CREDENTIAL_INVENTORY;
|
|
54
|
+
for (const p of [join(HERE, "..", "lib", "credential-inventory.sh")]) if (existsSync(p)) return p;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function officeCredentials({ probe = false, env = process.env } = {}) {
|
|
59
|
+
const script = inventoryPath(env);
|
|
60
|
+
const base = {
|
|
61
|
+
contractVersion: CONTRACT_VERSION,
|
|
62
|
+
generatedAt: new Date().toISOString(),
|
|
63
|
+
probed: probe,
|
|
64
|
+
};
|
|
65
|
+
if (!script) return { ...base, available: false, credentials: [] };
|
|
66
|
+
const r = await spawnAsync("bash", [script, "--json", ...(probe ? ["--probe"] : [])], {
|
|
67
|
+
env,
|
|
68
|
+
timeoutMs: probe ? 45000 : 10000,
|
|
69
|
+
});
|
|
70
|
+
const doc = (() => {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(r.stdout || "");
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
const rows = Array.isArray(doc?.credentials) ? doc.credentials : [];
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
available: Boolean(doc),
|
|
81
|
+
credentials: rows
|
|
82
|
+
.filter((c) => typeof c?.logical === "string" && NAME.test(c.logical))
|
|
83
|
+
.map((c) => ({
|
|
84
|
+
logical: c.logical,
|
|
85
|
+
state: STATES.has(c.state) ? c.state : "mapped-but-missing",
|
|
86
|
+
// A verdict this file does not know is reported as unknown, never as
|
|
87
|
+
// not checked: a probe that ran said something.
|
|
88
|
+
reachability: REACH.has(c.reachability) ? c.reachability : probe ? "unknown" : "not-probed",
|
|
89
|
+
capability: typeof c.capability === "string" ? c.capability.slice(0, 160) : null,
|
|
90
|
+
})),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (invokedDirectly(import.meta.url)) {
|
|
95
|
+
const args = process.argv.slice(2);
|
|
96
|
+
if (args.some((a) => a !== "--json" && a !== "--probe")) {
|
|
97
|
+
console.error("usage: office-credentials.mjs [--json] [--probe]");
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
const doc = await officeCredentials({ probe: args.includes("--probe") });
|
|
101
|
+
process.stdout.write(
|
|
102
|
+
args.includes("--json")
|
|
103
|
+
? JSON.stringify(doc) + "\n"
|
|
104
|
+
: doc.credentials.map((c) => `${c.logical}: ${c.state}, ${c.reachability}`).join("\n") + "\n",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-memory.mjs - what the pipeline has learned, per repository.
|
|
4
|
+
*
|
|
5
|
+
* The learning loop writes four stores, and this reads them without changing
|
|
6
|
+
* any:
|
|
7
|
+
*
|
|
8
|
+
* code graph ~/.claude/knowledge/<repo>/code-graph.json (graph-build.mjs)
|
|
9
|
+
* learnings ~/.claude/memory/multi-agent/<repo>/learnings-ledger.jsonl
|
|
10
|
+
* (learnings-ledger.mjs: durable facts, conventions, rejected
|
|
11
|
+
* preferences replayed into later runs)
|
|
12
|
+
* past findings .../triage-corpus.jsonl (triage-memory.mjs: review findings)
|
|
13
|
+
* session notes .../session-notes.jsonl (learn-from-transcripts.mjs)
|
|
14
|
+
*
|
|
15
|
+
* For each repository: when the graph was built and how large it is, how many
|
|
16
|
+
* learnings, findings and session notes there are, the newest learnings'
|
|
17
|
+
* statements (redacted and cut short) and when anything was last written. An
|
|
18
|
+
* empty shelf is reported as empty, so a repository the loop never fed shows.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* office-memory.mjs [--json]
|
|
22
|
+
*
|
|
23
|
+
* MA_MEMORY_ROOT and MA_KNOWLEDGE_ROOT override the two roots. Each store is
|
|
24
|
+
* read asynchronously: lines are counted in chunks and only the last
|
|
25
|
+
* MAX_BYTES are parsed, so a store that keeps growing never blocks the server.
|
|
26
|
+
*
|
|
27
|
+
* Exit: 0 printed, 2 usage.
|
|
28
|
+
*
|
|
29
|
+
* @module pipeline/scripts/office-memory
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { constants, createReadStream, existsSync, readdirSync, statSync } from "node:fs";
|
|
33
|
+
import { open } from "node:fs/promises";
|
|
34
|
+
import { homedir } from "node:os";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
|
|
37
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
38
|
+
import { createRedactor } from "./transcript-index.mjs";
|
|
39
|
+
|
|
40
|
+
export const CONTRACT_VERSION = "1.0.0";
|
|
41
|
+
const MAX_REPOS = 60;
|
|
42
|
+
const RECENT = 5;
|
|
43
|
+
const REPO = /^[A-Za-z0-9._-]{1,100}$/;
|
|
44
|
+
const MAX_BYTES = 1024 * 1024;
|
|
45
|
+
|
|
46
|
+
const roots = (env) => ({
|
|
47
|
+
memory: env.MA_MEMORY_ROOT || join(homedir(), ".claude", "memory", "multi-agent"),
|
|
48
|
+
knowledge: env.MA_KNOWLEDGE_ROOT || join(homedir(), ".claude", "knowledge"),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const dirsOf = (root) => {
|
|
52
|
+
try {
|
|
53
|
+
return readdirSync(root, { withFileTypes: true })
|
|
54
|
+
.filter((d) => d.isDirectory() && REPO.test(d.name))
|
|
55
|
+
.map((d) => d.name);
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// A line that is not a JSON object is skipped like an unreadable one.
|
|
62
|
+
const parse = (line) => {
|
|
63
|
+
try {
|
|
64
|
+
const v = JSON.parse(line);
|
|
65
|
+
return v && typeof v === "object" && !Array.isArray(v) ? [v] : [];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// A store's lines, the same way for a small file read whole and a large one
|
|
72
|
+
// counted in chunks: every newline, and a last line that has none.
|
|
73
|
+
function linesIn(buf) {
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (let i = buf.indexOf(10); i !== -1; i = buf.indexOf(10, i + 1)) n++;
|
|
76
|
+
return n + (buf.length && buf[buf.length - 1] !== 10 ? 1 : 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Counted through the handle already open, so the count is of the same file.
|
|
80
|
+
async function countLines(fh, size) {
|
|
81
|
+
let n = 0;
|
|
82
|
+
let last = 10;
|
|
83
|
+
const stream = createReadStream(null, { fd: fh.fd, start: 0, end: size - 1, autoClose: false });
|
|
84
|
+
for await (const chunk of stream) {
|
|
85
|
+
for (let i = chunk.indexOf(10); i !== -1; i = chunk.indexOf(10, i + 1)) n++;
|
|
86
|
+
if (chunk.length) last = chunk[chunk.length - 1];
|
|
87
|
+
}
|
|
88
|
+
return n + (size > 0 && last !== 10 ? 1 : 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A store's line count and its last lines parsed (unreadable lines skipped);
|
|
93
|
+
* { count: 0, tail: [] } for no file. Only the last `maxBytes` are parsed.
|
|
94
|
+
*/
|
|
95
|
+
async function store(path, maxBytes) {
|
|
96
|
+
let fh;
|
|
97
|
+
try {
|
|
98
|
+
// Non-blocking and not through a link: a FIFO or a symlink planted in a
|
|
99
|
+
// store's place is skipped instead of holding a worker thread.
|
|
100
|
+
fh = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
101
|
+
const st = await fh.stat();
|
|
102
|
+
if (!st.isFile()) return { count: 0, tail: [] };
|
|
103
|
+
const { size } = st;
|
|
104
|
+
const start = Math.max(0, size - maxBytes);
|
|
105
|
+
const buf = Buffer.alloc(size - start);
|
|
106
|
+
await fh.read(buf, 0, buf.length, start);
|
|
107
|
+
let text = buf.toString("utf8");
|
|
108
|
+
if (start > 0) text = text.slice(text.indexOf("\n") + 1);
|
|
109
|
+
const lines = text.split("\n").filter(Boolean);
|
|
110
|
+
const count = start > 0 ? await countLines(fh, size) : linesIn(buf);
|
|
111
|
+
return { count, tail: lines.flatMap(parse) };
|
|
112
|
+
} catch {
|
|
113
|
+
return { count: 0, tail: [] };
|
|
114
|
+
} finally {
|
|
115
|
+
await fh?.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ms = (v) => {
|
|
120
|
+
const t = Date.parse(v ?? "");
|
|
121
|
+
return Number.isFinite(t) ? t : null;
|
|
122
|
+
};
|
|
123
|
+
const iso = (t) => (t === null ? null : new Date(t).toISOString());
|
|
124
|
+
|
|
125
|
+
async function shelf(name, r, now, R, maxBytes) {
|
|
126
|
+
const dir = join(r.memory, name);
|
|
127
|
+
const graphPath = join(r.knowledge, name, "code-graph.json");
|
|
128
|
+
let graph = null;
|
|
129
|
+
if (existsSync(graphPath)) {
|
|
130
|
+
const st = statSync(graphPath);
|
|
131
|
+
graph = {
|
|
132
|
+
builtAt: iso(st.mtimeMs),
|
|
133
|
+
ageDays: Math.max(0, Math.floor((now - st.mtimeMs) / 86_400_000)),
|
|
134
|
+
sizeKb: Math.round(st.size / 1024),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const [learnings, findings, notes] = await Promise.all(
|
|
138
|
+
["learnings-ledger.jsonl", "triage-corpus.jsonl", "session-notes.jsonl"].map((f) =>
|
|
139
|
+
store(join(dir, f), maxBytes),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
const times = [...learnings.tail, ...findings.tail, ...notes.tail]
|
|
143
|
+
.map((x) => ms(x.ts))
|
|
144
|
+
.filter((t) => t !== null);
|
|
145
|
+
if (graph) times.push(ms(graph.builtAt));
|
|
146
|
+
return {
|
|
147
|
+
repo: name,
|
|
148
|
+
graph,
|
|
149
|
+
learnings: learnings.count,
|
|
150
|
+
findings: findings.count,
|
|
151
|
+
sessionNotes: notes.count,
|
|
152
|
+
recent: learnings.tail
|
|
153
|
+
.slice(-RECENT)
|
|
154
|
+
.reverse()
|
|
155
|
+
.map((x) => ({
|
|
156
|
+
kind: typeof x.kind === "string" ? x.kind.slice(0, 30) : null,
|
|
157
|
+
statement: R.excerpt(String(x.statement ?? ""), 120) ?? "",
|
|
158
|
+
at: iso(ms(x.ts)),
|
|
159
|
+
})),
|
|
160
|
+
lastWrite: times.length ? iso(Math.max(...times)) : null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function officeMemory({
|
|
165
|
+
env = process.env,
|
|
166
|
+
now = Date.now(),
|
|
167
|
+
maxBytes = MAX_BYTES,
|
|
168
|
+
} = {}) {
|
|
169
|
+
const r = roots(env);
|
|
170
|
+
// The transcript redactor: credentials by provider rule as well as paths
|
|
171
|
+
// and addresses, the same pass the activity view uses.
|
|
172
|
+
const R = createRedactor();
|
|
173
|
+
const names = [...new Set([...dirsOf(r.memory), ...dirsOf(r.knowledge)])];
|
|
174
|
+
// One shelf that cannot be read is left out; the others still answer.
|
|
175
|
+
const read = await Promise.all(names.map((n) => shelf(n, r, now, R, maxBytes).catch(() => null)));
|
|
176
|
+
const shelves = read
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.sort((a, b) => (b.lastWrite ?? "").localeCompare(a.lastWrite ?? ""))
|
|
179
|
+
.slice(0, MAX_REPOS);
|
|
180
|
+
return { contractVersion: CONTRACT_VERSION, generatedAt: new Date(now).toISOString(), shelves };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (invokedDirectly(import.meta.url)) {
|
|
184
|
+
const args = process.argv.slice(2);
|
|
185
|
+
if (args.some((a) => a !== "--json")) {
|
|
186
|
+
console.error("usage: office-memory.mjs [--json]");
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
const doc = await officeMemory();
|
|
190
|
+
process.stdout.write(
|
|
191
|
+
args.includes("--json")
|
|
192
|
+
? JSON.stringify(doc) + "\n"
|
|
193
|
+
: doc.shelves
|
|
194
|
+
.map(
|
|
195
|
+
(s) =>
|
|
196
|
+
`${s.repo}: graph ${s.graph ? s.graph.ageDays + "d old" : "none"}, ${s.learnings} learnings, ${s.findings} findings, ${s.sessionNotes} notes`,
|
|
197
|
+
)
|
|
198
|
+
.join("\n") + "\n",
|
|
199
|
+
);
|
|
200
|
+
}
|