@mmerterden/multi-agent-pipeline 14.2.2 → 15.1.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 +186 -6
- package/README.md +19 -12
- package/README.tr.md +19 -12
- package/SECURITY.md +43 -0
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0007-multi-tool-adapter-framework.md +1 -1
- package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +13 -13
- package/docs/ecosystem.md +31 -31
- package/docs/features.md +5 -5
- package/index.js +6 -1
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +109 -3
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +54 -113
- package/install/_plugin-skills.mjs +36 -36
- package/install/claude.mjs +251 -61
- package/install/codex.mjs +28 -6
- package/install/copilot.mjs +69 -9
- package/install/index.mjs +9 -3
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +3 -3
- package/package.json +2 -3
- package/pipeline/commands/multi-agent/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
- package/pipeline/commands/multi-agent/refactor/SKILL.md +36 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +62 -40
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +4 -4
- package/pipeline/lib/issue-fetcher.sh +1 -1
- package/pipeline/lib/parse-complaints.sh +316 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +3 -3
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
- package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
- package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
- package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
- package/pipeline/multi-agent-refs/generate-issue.md +1 -1
- package/pipeline/multi-agent-refs/phases/modes.md +1 -1
- package/pipeline/multi-agent-refs/phases/operations.md +7 -1
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +6 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +3 -2
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +18 -5
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +1 -1
- package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
- package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
- package/pipeline/schemas/prefs.schema.json +296 -66
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/README.md +4 -3
- package/pipeline/scripts/_stack-routing.mjs +79 -0
- package/pipeline/scripts/audit-log-rotate.sh +4 -1
- package/pipeline/scripts/build-skills-index.mjs +11 -0
- package/pipeline/scripts/build-stack-plugins.mjs +28 -60
- package/pipeline/scripts/check-derived-drift.mjs +55 -28
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/match-skills.mjs +12 -2
- package/pipeline/scripts/migrate-prefs.mjs +33 -21
- package/pipeline/scripts/phase-tracker.sh +32 -5
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
- package/pipeline/scripts/run-aggregator.mjs +7 -2
- package/pipeline/scripts/scan-agent-config.sh +1 -1
- package/pipeline/scripts/skill-conformance.mjs +165 -30
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
- package/pipeline/scripts/test-gap-rules/android.json +25 -0
- package/pipeline/scripts/test-gap-rules/ios.json +34 -0
- package/pipeline/scripts/test-gap-rules/node.json +29 -0
- package/pipeline/scripts/test-gap-rules/python.json +25 -0
- package/pipeline/scripts/uninstall.mjs +160 -11
- package/pipeline/scripts/usage-report.mjs +426 -0
- package/pipeline/scripts/validate-complaint-doc.mjs +250 -0
- package/pipeline/scripts/validate-reviewer.mjs +9 -3
- package/pipeline/skills/.skill-manifest.json +156 -108
- package/pipeline/skills/.skills-index.json +449 -12
- package/pipeline/skills/shared/README.md +14 -10
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +153 -90
- package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +89 -22
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
- package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
- package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
- package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +53 -10
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +4 -3
- package/pipeline/skills/skills-index.md +7 -4
|
@@ -35,9 +35,20 @@
|
|
|
35
35
|
* @module pipeline/scripts/runtime/uninstall
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
chmodSync,
|
|
40
|
+
existsSync,
|
|
41
|
+
lstatSync,
|
|
42
|
+
readdirSync,
|
|
43
|
+
readFileSync,
|
|
44
|
+
realpathSync,
|
|
45
|
+
renameSync,
|
|
46
|
+
rmSync,
|
|
47
|
+
statSync,
|
|
48
|
+
writeFileSync,
|
|
49
|
+
} from "fs";
|
|
39
50
|
import { execFileSync } from "child_process";
|
|
40
|
-
import { join } from "path";
|
|
51
|
+
import { join, dirname } from "path";
|
|
41
52
|
import { homedir } from "os";
|
|
42
53
|
import { pathToFileURL } from "url";
|
|
43
54
|
import { createInterface } from "readline";
|
|
@@ -100,6 +111,43 @@ const adapterTarget = (() => {
|
|
|
100
111
|
return raw ? raw.slice("--target=".length) : process.cwd();
|
|
101
112
|
})();
|
|
102
113
|
|
|
114
|
+
/**
|
|
115
|
+
* tmp + rename, preserving a symlinked target and the existing file mode.
|
|
116
|
+
*
|
|
117
|
+
* Duplicated from install/_common.mjs rather than imported: this script runs
|
|
118
|
+
* standalone from ~/.claude/scripts, where the install tree does not exist.
|
|
119
|
+
* A plain writeFileSync could leave the host's settings.json truncated on a
|
|
120
|
+
* crash; a naive rename would replace a dotfiles symlink with a regular file
|
|
121
|
+
* and reset a 0600 file to 0644.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} path
|
|
124
|
+
* @param {string} content
|
|
125
|
+
*/
|
|
126
|
+
function atomicWrite(path, content) {
|
|
127
|
+
let target = path;
|
|
128
|
+
try {
|
|
129
|
+
if (lstatSync(path).isSymbolicLink()) target = realpathSync(path);
|
|
130
|
+
} catch {
|
|
131
|
+
// Missing file: nothing to preserve.
|
|
132
|
+
}
|
|
133
|
+
let mode;
|
|
134
|
+
try {
|
|
135
|
+
mode = statSync(target).mode & 0o777;
|
|
136
|
+
} catch {
|
|
137
|
+
mode = undefined;
|
|
138
|
+
}
|
|
139
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
140
|
+
writeFileSync(tmp, content);
|
|
141
|
+
if (mode !== undefined) {
|
|
142
|
+
try {
|
|
143
|
+
chmodSync(tmp, mode);
|
|
144
|
+
} catch {
|
|
145
|
+
// Best effort: a failed chmod must not lose the write.
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
renameSync(tmp, target);
|
|
149
|
+
}
|
|
150
|
+
|
|
103
151
|
/**
|
|
104
152
|
* Report a planned action. Always logged; in --dry-run mode, no side effects
|
|
105
153
|
* are performed.
|
|
@@ -197,10 +245,7 @@ function deregisterCodexMcpServer() {
|
|
|
197
245
|
*/
|
|
198
246
|
function deregisterClaudeMcpServer() {
|
|
199
247
|
const localBin = join(homedir(), ".local", "bin", "claude");
|
|
200
|
-
deregisterMcpServer("claude", existsSync(localBin) ? localBin : undefined, [
|
|
201
|
-
"--scope",
|
|
202
|
-
"user",
|
|
203
|
-
]);
|
|
248
|
+
deregisterMcpServer("claude", existsSync(localBin) ? localBin : undefined, ["--scope", "user"]);
|
|
204
249
|
}
|
|
205
250
|
|
|
206
251
|
/**
|
|
@@ -220,6 +265,106 @@ function deregisterCopilotMcpServer() {
|
|
|
220
265
|
* @param {string} parent
|
|
221
266
|
* @param {(name: string) => boolean} predicate
|
|
222
267
|
*/
|
|
268
|
+
/**
|
|
269
|
+
* Remove the pipeline's command namespace while preserving user-authored
|
|
270
|
+
* local-only alias wrappers (frontmatter `local-only: true`). Those wrappers
|
|
271
|
+
* exist ONLY under the host dir - install/claude.mjs snapshots them across its
|
|
272
|
+
* wipe for the same reason - so removing the namespace wholesale would destroy
|
|
273
|
+
* content a reinstall can never bring back.
|
|
274
|
+
* @param {string} cmdDir - e.g. ~/.claude/commands/multi-agent
|
|
275
|
+
* @returns {number} preserved wrapper dirs
|
|
276
|
+
*/
|
|
277
|
+
function rmCommandsPreservingLocalOnly(cmdDir) {
|
|
278
|
+
// An install interrupted between its wipe and its restore leaves wrappers in
|
|
279
|
+
// a hidden stash beside the namespace. Adopt them back before deciding what
|
|
280
|
+
// to preserve, or the "local-only wrappers preserved" promise below strands
|
|
281
|
+
// them in a dot-directory the user never sees.
|
|
282
|
+
const stashDir = join(dirname(cmdDir), ".multi-agent-wrapper-stash");
|
|
283
|
+
if (existsSync(stashDir) && !dryRun) {
|
|
284
|
+
for (const name of readdirSync(stashDir)) {
|
|
285
|
+
const to = join(cmdDir, name);
|
|
286
|
+
if (existsSync(to)) continue;
|
|
287
|
+
try {
|
|
288
|
+
renameSync(join(stashDir, name), to);
|
|
289
|
+
console.log(` recovered stashed wrapper from an interrupted install: ${name}`);
|
|
290
|
+
} catch {
|
|
291
|
+
/* best effort - a failed recovery must not block the uninstall */
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
rmIfExists(stashDir);
|
|
295
|
+
}
|
|
296
|
+
if (!existsSync(cmdDir)) return 0;
|
|
297
|
+
const isLocalOnly = (dir) => {
|
|
298
|
+
const skill = join(dir, "SKILL.md");
|
|
299
|
+
try {
|
|
300
|
+
return existsSync(skill) && /^local-only:\s*true\s*$/m.test(readFileSync(skill, "utf-8"));
|
|
301
|
+
} catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const preserved = readdirSync(cmdDir, { withFileTypes: true })
|
|
306
|
+
.filter((e) => e.isDirectory() && isLocalOnly(join(cmdDir, e.name)))
|
|
307
|
+
.map((e) => e.name);
|
|
308
|
+
if (preserved.length === 0) {
|
|
309
|
+
rmIfExists(cmdDir);
|
|
310
|
+
return 0;
|
|
311
|
+
}
|
|
312
|
+
for (const entry of readdirSync(cmdDir)) {
|
|
313
|
+
if (preserved.includes(entry)) continue;
|
|
314
|
+
rmIfExists(join(cmdDir, entry));
|
|
315
|
+
}
|
|
316
|
+
console.log(
|
|
317
|
+
` preserved ${preserved.length} local-only alias wrapper(s): ${preserved.join(", ")}`,
|
|
318
|
+
);
|
|
319
|
+
return preserved.length;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Written by install/_platform-filter.mjs beside the skills it delivered. */
|
|
323
|
+
const EXTERNAL_SKILLS_MANIFEST = ".external-skills-manifest.json";
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Remove the external skill catalog THIS install delivered, read from the
|
|
327
|
+
* manifest the installer wrote beside it.
|
|
328
|
+
*
|
|
329
|
+
* Deliberately not derived from `.skills-index.json`: that file is the shipped
|
|
330
|
+
* catalog copied verbatim, so it lists platform-filtered skills that were never
|
|
331
|
+
* installed, and it says nothing about which package version's tree is on disk.
|
|
332
|
+
* Either gap would delete a user-authored skill dir that happens to share a
|
|
333
|
+
* catalog name. Same contract as `.plugin-skills-manifest.json`.
|
|
334
|
+
*
|
|
335
|
+
* No manifest (a pre-manifest install) means ours and theirs are
|
|
336
|
+
* indistinguishable, so nothing is removed and the user is told why.
|
|
337
|
+
*
|
|
338
|
+
* @param {string} skillsDir
|
|
339
|
+
* @returns {number} dirs removed
|
|
340
|
+
*/
|
|
341
|
+
function rmExternalDeliveredSkills(skillsDir) {
|
|
342
|
+
if (!existsSync(skillsDir)) return 0;
|
|
343
|
+
const manifestPath = join(skillsDir, EXTERNAL_SKILLS_MANIFEST);
|
|
344
|
+
let names = null;
|
|
345
|
+
if (existsSync(manifestPath)) {
|
|
346
|
+
try {
|
|
347
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
348
|
+
if (Array.isArray(parsed)) names = new Set(parsed);
|
|
349
|
+
} catch {
|
|
350
|
+
names = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (!names) {
|
|
354
|
+
console.log(
|
|
355
|
+
` note: no ${EXTERNAL_SKILLS_MANIFEST} under ${skillsDir} - external skill dirs left in ` +
|
|
356
|
+
`place (cannot tell pipeline-delivered from user-authored; ` +
|
|
357
|
+
`node install.js --claude prunes copies identical to the shipped catalog, ` +
|
|
358
|
+
`--prune-external removes the rest)`,
|
|
359
|
+
);
|
|
360
|
+
return 0;
|
|
361
|
+
}
|
|
362
|
+
let n = rmMatchingDirs(skillsDir, (name) => names.has(name));
|
|
363
|
+
n += rmMatchingFiles(skillsDir, (name) => /^NOTICE-.*\.md$/.test(name));
|
|
364
|
+
rmIfExists(manifestPath);
|
|
365
|
+
return n;
|
|
366
|
+
}
|
|
367
|
+
|
|
223
368
|
function rmMatchingDirs(parent, predicate) {
|
|
224
369
|
if (!existsSync(parent)) return 0;
|
|
225
370
|
let count = 0;
|
|
@@ -364,7 +509,7 @@ function stripManagedBlock(filePath) {
|
|
|
364
509
|
rmSync(filePath, { force: true });
|
|
365
510
|
console.log(` removed (was pipeline-only): ${filePath}`);
|
|
366
511
|
} else {
|
|
367
|
-
|
|
512
|
+
atomicWrite(filePath, remaining + "\n");
|
|
368
513
|
console.log(` cleaned pipeline section: ${filePath}`);
|
|
369
514
|
}
|
|
370
515
|
return true;
|
|
@@ -381,7 +526,7 @@ function stripManagedBlock(filePath) {
|
|
|
381
526
|
rmSync(filePath, { force: true });
|
|
382
527
|
console.log(` removed (was pipeline-only): ${filePath}`);
|
|
383
528
|
} else {
|
|
384
|
-
|
|
529
|
+
atomicWrite(filePath, cleaned + "\n");
|
|
385
530
|
console.log(` cleaned pipeline block from: ${filePath}`);
|
|
386
531
|
}
|
|
387
532
|
return true;
|
|
@@ -424,7 +569,8 @@ function cleanClaudeSettings(settingsPath) {
|
|
|
424
569
|
(h.matcher === "Bash" || h.matcher === "Bash(git commit:*)") &&
|
|
425
570
|
h.hooks?.some(
|
|
426
571
|
(sub) =>
|
|
427
|
-
sub.command?.includes("pre-commit-check.sh") ||
|
|
572
|
+
sub.command?.includes("pre-commit-check.sh") ||
|
|
573
|
+
sub.command?.includes("agent-guard.sh"),
|
|
428
574
|
)
|
|
429
575
|
),
|
|
430
576
|
);
|
|
@@ -437,7 +583,7 @@ function cleanClaudeSettings(settingsPath) {
|
|
|
437
583
|
if (dryRun) {
|
|
438
584
|
report("would update", settingsPath);
|
|
439
585
|
} else {
|
|
440
|
-
|
|
586
|
+
atomicWrite(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
441
587
|
console.log(` cleaned pipeline hook + env from: ${settingsPath}`);
|
|
442
588
|
}
|
|
443
589
|
}
|
|
@@ -497,6 +643,7 @@ export async function main() {
|
|
|
497
643
|
);
|
|
498
644
|
console.log(" - ~/.claude/CLAUDE.md (your customizations)");
|
|
499
645
|
console.log(" - ~/.claude/rules/ (user-owned; installed write-if-missing, never overwritten)");
|
|
646
|
+
console.log(" - Local-only alias wrappers under commands/multi-agent/ (user-authored)");
|
|
500
647
|
if (!allData) console.log(" - ~/.claude/multi-agent-preferences.json (your settings)");
|
|
501
648
|
console.log(" - Your own content in copilot-instructions.md above the pipeline section");
|
|
502
649
|
console.log("");
|
|
@@ -511,7 +658,7 @@ export async function main() {
|
|
|
511
658
|
console.log(" [Claude Code] Removing...");
|
|
512
659
|
deregisterClaudeMcpServer();
|
|
513
660
|
const CLAUDE = join(HOME, ".claude");
|
|
514
|
-
|
|
661
|
+
rmCommandsPreservingLocalOnly(join(CLAUDE, "commands", "multi-agent"));
|
|
515
662
|
rmIfExists(join(CLAUDE, "scripts"));
|
|
516
663
|
// Pipeline-managed trees the installer lays down alongside scripts/.
|
|
517
664
|
rmIfExists(join(CLAUDE, "multi-agent-refs"));
|
|
@@ -538,6 +685,7 @@ export async function main() {
|
|
|
538
685
|
name === "figma-to-component",
|
|
539
686
|
);
|
|
540
687
|
n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
|
|
688
|
+
n += rmExternalDeliveredSkills(skills);
|
|
541
689
|
if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
|
|
542
690
|
rmIfExists(join(skills, ".skills-index.json"));
|
|
543
691
|
rmIfExists(join(skills, "skills-index.md"));
|
|
@@ -573,6 +721,7 @@ export async function main() {
|
|
|
573
721
|
);
|
|
574
722
|
n += rmMatchingDirs(skills, (name) => PIPELINE_CORE_SKILL_DIRS.includes(name));
|
|
575
723
|
n += rmPluginDeliveredSkills(skills);
|
|
724
|
+
n += rmExternalDeliveredSkills(skills);
|
|
576
725
|
if (n > 0) console.log(` removed ${n} skill dir(s) under ${skills}`);
|
|
577
726
|
rmIfExists(join(skills, ".skills-index.json"));
|
|
578
727
|
rmIfExists(join(skills, "skills-index.md"));
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usage-report.mjs - usage ping (opt-in) for the private mission-control dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Emits one compact activity event keyed by run id (`taskId`). Called at each
|
|
5
|
+
* phase boundary (status=running, via phase-tracker.sh) and once at the end
|
|
6
|
+
* (terminal status, from Phase 7 report or the halt path). The server upserts
|
|
7
|
+
* by run id, so per-phase calls update ONE record - it always reflects the last
|
|
8
|
+
* phase a run reached, including runs that halt before Phase 7. Fire-and-forget,
|
|
9
|
+
* never throws, never delays the run.
|
|
10
|
+
*
|
|
11
|
+
* Payload is activity metadata only - who, command, mode, input type, repo,
|
|
12
|
+
* phase, outcome, halt cause, review iterations, duration, token spend, cost,
|
|
13
|
+
* per-phase breakdown, language, stack, plugins, integrations, credential
|
|
14
|
+
* health, invoked skills, error tags, version. No prompts, code, diffs, token
|
|
15
|
+
* values, or absolute paths.
|
|
16
|
+
*
|
|
17
|
+
* Enable + configure via prefs.global.usageLog:
|
|
18
|
+
* { "enabled": true,
|
|
19
|
+
* "endpoint": "https://mmerterden.vercel.app/api/usage/ingest",
|
|
20
|
+
* "token": "<shared ingest token>" } // token may instead come from
|
|
21
|
+
* // env MULTI_AGENT_USAGE_TOKEN
|
|
22
|
+
* Nothing is sent when enabled is not true or no token resolves.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* node usage-report.mjs --state /path/to/agent-state.json
|
|
26
|
+
* node usage-report.mjs --task-id PROJ-123 # resolves the state path
|
|
27
|
+
*
|
|
28
|
+
* @module pipeline/scripts/usage-report
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { readFileSync, existsSync, readdirSync, appendFileSync } from "fs";
|
|
32
|
+
import { homedir, platform } from "os";
|
|
33
|
+
import { dirname, join } from "path";
|
|
34
|
+
import { fileURLToPath } from "url";
|
|
35
|
+
import { costUsd } from "./_cost.mjs";
|
|
36
|
+
|
|
37
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const ENDPOINT_DEFAULT = "https://mmerterden.vercel.app/api/usage/ingest";
|
|
39
|
+
const TIMEOUT_MS = 2500;
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const out = { state: null, taskId: null, phase: null, status: null, dryRun: false };
|
|
43
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
44
|
+
const a = argv[i];
|
|
45
|
+
if (a === "--state") out.state = argv[++i];
|
|
46
|
+
else if (a === "--task-id") out.taskId = argv[++i];
|
|
47
|
+
else if (a === "--phase") out.phase = argv[++i];
|
|
48
|
+
else if (a === "--status") out.status = argv[++i];
|
|
49
|
+
else if (a === "--dry-run") out.dryRun = true;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readJson(path) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveGlobalPrefs() {
|
|
63
|
+
const path =
|
|
64
|
+
process.env.MULTI_AGENT_PREFS ||
|
|
65
|
+
join(homedir(), ".claude", "multi-agent-preferences.json");
|
|
66
|
+
return readJson(path)?.global ?? {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolvePrefs() {
|
|
70
|
+
return resolveGlobalPrefs().usageLog ?? {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function enabledPlugins(state) {
|
|
74
|
+
const root = state.projectRoot || process.cwd();
|
|
75
|
+
const settings = readJson(join(root, ".claude", "settings.json"));
|
|
76
|
+
const raw = settings?.enabledPlugins ?? settings?.plugins ?? [];
|
|
77
|
+
const list = Array.isArray(raw) ? raw : Object.keys(raw || {});
|
|
78
|
+
return list
|
|
79
|
+
.map((p) => (typeof p === "string" ? p.split("@")[0] : null))
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.slice(0, 20);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function integrationTags(state) {
|
|
85
|
+
const apps = new Set();
|
|
86
|
+
if (state.jiraId) apps.add("jira");
|
|
87
|
+
if (state.remoteType) apps.add(String(state.remoteType).toLowerCase());
|
|
88
|
+
if (state.githubIssue) apps.add("github");
|
|
89
|
+
if (state.confluenceSpace) apps.add("confluence");
|
|
90
|
+
if (state.figmaAccess?.tier) apps.add("figma");
|
|
91
|
+
for (const call of state.telemetry?.mcpCalls ?? []) {
|
|
92
|
+
const tool = String(call?.tool || "");
|
|
93
|
+
const m = tool.match(/^mcp__([a-z0-9_]+?)__/i) || tool.match(/^mcp__([a-z0-9_]+)/i);
|
|
94
|
+
if (m) apps.add(m[1].replace(/_/g, "-").toLowerCase());
|
|
95
|
+
}
|
|
96
|
+
return Array.from(apps).slice(0, 20);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function credHealth() {
|
|
100
|
+
const g = resolveGlobalPrefs();
|
|
101
|
+
const mapping = g.keychainMapping ?? {};
|
|
102
|
+
const on = Object.entries(mapping)
|
|
103
|
+
.filter(([, v]) => typeof v === "string" && v.trim())
|
|
104
|
+
.map(([k]) => k)
|
|
105
|
+
.slice(0, 40);
|
|
106
|
+
|
|
107
|
+
const svc = {};
|
|
108
|
+
for (const [name, st] of Object.entries(g.serviceStatus ?? {})) {
|
|
109
|
+
const err = String(st?.error || "").toLowerCase();
|
|
110
|
+
const expired = /401|403|expired|unauthor|revoked|invalid.token/.test(err);
|
|
111
|
+
svc[name] = {
|
|
112
|
+
s: st?.ok ? "ok" : expired ? "expired" : "fail",
|
|
113
|
+
at: st?.checkedAt || null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { on, svc };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function invokedCalls(state) {
|
|
120
|
+
const names = new Set();
|
|
121
|
+
for (const c of state.telemetry?.skillCalls ?? []) {
|
|
122
|
+
if (c?.skill) names.add(String(c.skill));
|
|
123
|
+
}
|
|
124
|
+
return Array.from(names).slice(0, 30);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function detectOs() {
|
|
128
|
+
const p = platform();
|
|
129
|
+
if (p === "darwin") return "macos";
|
|
130
|
+
if (p === "win32") return "windows";
|
|
131
|
+
if (p === "linux") return "linux";
|
|
132
|
+
return p;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function detectCli() {
|
|
136
|
+
const e = process.env;
|
|
137
|
+
if (e.MULTI_AGENT_CLI) return String(e.MULTI_AGENT_CLI).toLowerCase();
|
|
138
|
+
if (e.CLAUDECODE || e.CLAUDE_CODE_ENTRYPOINT || e.ANTHROPIC_CLI) return "claude";
|
|
139
|
+
if (e.CODEX_HOME || e.CODEX_SANDBOX || e.CODEX_CLI) return "codex";
|
|
140
|
+
if (e.COPILOT_CLI || e.GITHUB_COPILOT_CLI || e.GH_COPILOT || e.COPILOT_AGENT_ID) {
|
|
141
|
+
return "copilot";
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function deriveContext(state) {
|
|
147
|
+
return {
|
|
148
|
+
lang: resolveGlobalPrefs().outputLanguage ?? null,
|
|
149
|
+
os: detectOs(),
|
|
150
|
+
cli: detectCli(),
|
|
151
|
+
stack: Array.isArray(state.stacks)
|
|
152
|
+
? state.stacks.slice(0, 10)
|
|
153
|
+
: state.stack
|
|
154
|
+
? [String(state.stack)]
|
|
155
|
+
: [],
|
|
156
|
+
plug: enabledPlugins(state),
|
|
157
|
+
apps: integrationTags(state),
|
|
158
|
+
creds: credHealth(),
|
|
159
|
+
calls: invokedCalls(state),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveStatePath({ state, taskId }) {
|
|
164
|
+
if (state) return state;
|
|
165
|
+
if (process.env.MULTI_AGENT_STATE_FILE && existsSync(process.env.MULTI_AGENT_STATE_FILE)) {
|
|
166
|
+
return process.env.MULTI_AGENT_STATE_FILE;
|
|
167
|
+
}
|
|
168
|
+
if (!taskId) return null;
|
|
169
|
+
|
|
170
|
+
const logRoot = join(homedir(), ".claude", "logs", "multi-agent");
|
|
171
|
+
const direct = [
|
|
172
|
+
join(logRoot, taskId, "agent-state.json"),
|
|
173
|
+
join(process.cwd(), ".worktrees", taskId, "agent-state.json"),
|
|
174
|
+
join(process.cwd(), "agent-state.json"),
|
|
175
|
+
];
|
|
176
|
+
const hit = direct.find((p) => existsSync(p));
|
|
177
|
+
if (hit) return hit;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
for (const project of readdirSync(logRoot)) {
|
|
181
|
+
const p = join(logRoot, project, taskId, "agent-state.json");
|
|
182
|
+
if (existsSync(p)) return p;
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
/* logs dir missing */
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function packageVersion() {
|
|
191
|
+
const candidates = [
|
|
192
|
+
join(__dirname, "..", "..", "package.json"),
|
|
193
|
+
join(homedir(), ".claude", "multi-agent-pipeline", "package.json"),
|
|
194
|
+
];
|
|
195
|
+
for (const p of candidates) {
|
|
196
|
+
const v = readJson(p)?.version;
|
|
197
|
+
if (v) return v;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function inputType(state) {
|
|
203
|
+
if (state.jiraId) return "jira";
|
|
204
|
+
if (state.githubIssue) return "github-issue";
|
|
205
|
+
return "free-text";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function commandOf(state) {
|
|
209
|
+
if (state.mode) return state.mode;
|
|
210
|
+
if (state.onlyDevelop) return "dev";
|
|
211
|
+
if (state.localMode) return "local";
|
|
212
|
+
return "full";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function resolveTracker(state) {
|
|
216
|
+
const id = state.taskId || state.shortId;
|
|
217
|
+
if (id == null) return null;
|
|
218
|
+
const root = join(homedir(), ".claude", "logs", "multi-agent");
|
|
219
|
+
const candidates = [
|
|
220
|
+
join(root, String(id), "tracker-state.json"),
|
|
221
|
+
state.artifactsPath ? join(state.artifactsPath, "tracker-state.json") : null,
|
|
222
|
+
].filter(Boolean);
|
|
223
|
+
return candidates.find((p) => existsSync(p)) ?? null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function phaseDurationSec(p) {
|
|
227
|
+
if (Number.isFinite(Number(p?.duration_ms))) {
|
|
228
|
+
return Math.round(Number(p.duration_ms) / 1000);
|
|
229
|
+
}
|
|
230
|
+
const s = p?.startedAt || p?.started_at || p?.start;
|
|
231
|
+
const f = p?.finishedAt || p?.finished_at || p?.completed_at || p?.end;
|
|
232
|
+
const sm = s ? Date.parse(s) : NaN;
|
|
233
|
+
const fm = f ? Date.parse(f) : NaN;
|
|
234
|
+
if (Number.isNaN(sm) || Number.isNaN(fm) || fm < sm) return null;
|
|
235
|
+
return Math.round((fm - sm) / 1000);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function trackerSummary(state) {
|
|
239
|
+
const path = resolveTracker(state);
|
|
240
|
+
if (!path) return { tk: null, cost: null, phs: null, failed: [], models: [] };
|
|
241
|
+
const tracker = readJson(path);
|
|
242
|
+
if (!tracker?.phases) return { tk: null, cost: null, phs: null, failed: [], models: [] };
|
|
243
|
+
|
|
244
|
+
let tin = 0;
|
|
245
|
+
let tout = 0;
|
|
246
|
+
let tcache = 0;
|
|
247
|
+
const phs = [];
|
|
248
|
+
const failed = [];
|
|
249
|
+
const models = new Set();
|
|
250
|
+
for (const [id, p] of Object.entries(tracker.phases)) {
|
|
251
|
+
if (p?.status === "failed") failed.push(id);
|
|
252
|
+
if (p?.model) models.add(String(p.model));
|
|
253
|
+
const pin = Number(p?.tokens_in || 0);
|
|
254
|
+
const pout = Number(p?.tokens_out || 0);
|
|
255
|
+
const pcache = Number(p?.tokens_cached || 0);
|
|
256
|
+
tin += pin;
|
|
257
|
+
tout += pout;
|
|
258
|
+
tcache += pcache;
|
|
259
|
+
phs.push({
|
|
260
|
+
p: Number.isNaN(Number(id)) ? id : Number(id),
|
|
261
|
+
st: p?.status || null,
|
|
262
|
+
du: phaseDurationSec(p),
|
|
263
|
+
tk: pin + pout + pcache || null,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
phs.sort((a, b) => Number(a.p) - Number(b.p));
|
|
267
|
+
|
|
268
|
+
const total = tin + tout + tcache;
|
|
269
|
+
const table = readJson(join(__dirname, "cost-table.json"));
|
|
270
|
+
const rate = table?.prices?.opus;
|
|
271
|
+
const cost =
|
|
272
|
+
total > 0 && rate ? Number(costUsd(rate, tin, tout, tcache).toFixed(4)) : null;
|
|
273
|
+
return {
|
|
274
|
+
tk: total || null,
|
|
275
|
+
cost,
|
|
276
|
+
phs: phs.length ? phs : null,
|
|
277
|
+
failed,
|
|
278
|
+
models: Array.from(models).slice(0, 10),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function deriveErrors(state, failedPhases) {
|
|
283
|
+
const errs = new Set();
|
|
284
|
+
if (state.haltReason) errs.add(String(state.haltReason));
|
|
285
|
+
for (const id of failedPhases ?? []) errs.add(`phase-${id}-failed`);
|
|
286
|
+
const s = String(state.status || "").toLowerCase();
|
|
287
|
+
if (s === "failed") errs.add("run-failed");
|
|
288
|
+
return Array.from(errs).slice(0, 20);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function durationSec(state) {
|
|
292
|
+
const s = state.startedAt ? Date.parse(state.startedAt) : NaN;
|
|
293
|
+
const f = state.finishedAt ? Date.parse(state.finishedAt) : NaN;
|
|
294
|
+
if (Number.isNaN(s) || Number.isNaN(f) || f < s) return null;
|
|
295
|
+
return Math.round((f - s) / 1000);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function runId(state) {
|
|
299
|
+
const base = state.taskId ?? state.shortId;
|
|
300
|
+
return base == null ? null : String(base);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function eventTimestamp(state) {
|
|
304
|
+
return state.finishedAt || state.updatedAt || new Date().toISOString();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const TERMINAL_STATUSES = ["complete", "completed", "failed", "paused", "halted", "error"];
|
|
308
|
+
|
|
309
|
+
function isTerminalStatus(state) {
|
|
310
|
+
if (state.haltReason) return true;
|
|
311
|
+
return TERMINAL_STATUSES.includes(String(state.status || "").toLowerCase());
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function appendErrorLedger(event) {
|
|
315
|
+
if (!event.errs || event.errs.length === 0) return;
|
|
316
|
+
const path = join(homedir(), ".claude", "logs", "multi-agent", "errors-ledger.jsonl");
|
|
317
|
+
const row = {
|
|
318
|
+
t: event.t,
|
|
319
|
+
id: event.id,
|
|
320
|
+
u: event.u,
|
|
321
|
+
c: event.c,
|
|
322
|
+
rp: event.rp,
|
|
323
|
+
ph: event.ph,
|
|
324
|
+
v: event.v,
|
|
325
|
+
errs: event.errs,
|
|
326
|
+
};
|
|
327
|
+
try {
|
|
328
|
+
appendFileSync(path, JSON.stringify(row) + "\n");
|
|
329
|
+
} catch {
|
|
330
|
+
/* logs dir missing or unwritable - never break the run */
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildEvent(state) {
|
|
335
|
+
const spend = trackerSummary(state);
|
|
336
|
+
const ctx = deriveContext(state);
|
|
337
|
+
return {
|
|
338
|
+
id: runId(state),
|
|
339
|
+
t: eventTimestamp(state),
|
|
340
|
+
u: state.identity?.username || null,
|
|
341
|
+
c: commandOf(state),
|
|
342
|
+
m: state.mode || null,
|
|
343
|
+
ap: Boolean(state.autopilot),
|
|
344
|
+
lo: Boolean(state.localMode),
|
|
345
|
+
dev: Boolean(state.onlyDevelop),
|
|
346
|
+
in: inputType(state),
|
|
347
|
+
rp: state.project || null,
|
|
348
|
+
ph: typeof state.currentPhase === "number" ? state.currentPhase : null,
|
|
349
|
+
st: state.status || null,
|
|
350
|
+
hr: state.haltReason || null,
|
|
351
|
+
ri: Array.isArray(state.reviewIterations)
|
|
352
|
+
? state.reviewIterations.length
|
|
353
|
+
: 0,
|
|
354
|
+
pr: Boolean(state.pr && (state.pr.url || state.pr.number)),
|
|
355
|
+
du: durationSec(state),
|
|
356
|
+
tk: spend.tk,
|
|
357
|
+
cost: spend.cost,
|
|
358
|
+
phs: spend.phs,
|
|
359
|
+
os: ctx.os,
|
|
360
|
+
cli: ctx.cli,
|
|
361
|
+
models: spend.models,
|
|
362
|
+
lang: ctx.lang,
|
|
363
|
+
stack: ctx.stack,
|
|
364
|
+
plug: ctx.plug,
|
|
365
|
+
apps: ctx.apps,
|
|
366
|
+
creds: ctx.creds,
|
|
367
|
+
calls: ctx.calls,
|
|
368
|
+
errs: deriveErrors(state, spend.failed),
|
|
369
|
+
v: packageVersion(),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function post(endpoint, token, event) {
|
|
374
|
+
const ctrl = new AbortController();
|
|
375
|
+
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
|
|
376
|
+
try {
|
|
377
|
+
await fetch(endpoint, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: {
|
|
380
|
+
"Content-Type": "application/json",
|
|
381
|
+
"X-Usage-Token": token,
|
|
382
|
+
},
|
|
383
|
+
body: JSON.stringify(event),
|
|
384
|
+
signal: ctrl.signal,
|
|
385
|
+
});
|
|
386
|
+
} catch {
|
|
387
|
+
/* never break the run on a telemetry failure */
|
|
388
|
+
} finally {
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function main() {
|
|
394
|
+
const args = parseArgs(process.argv.slice(2));
|
|
395
|
+
const prefs = resolvePrefs();
|
|
396
|
+
const token = process.env.MULTI_AGENT_USAGE_TOKEN || prefs.token || "";
|
|
397
|
+
const endpoint = prefs.endpoint || ENDPOINT_DEFAULT;
|
|
398
|
+
|
|
399
|
+
if (!args.dryRun && (prefs.enabled !== true || !token)) return;
|
|
400
|
+
|
|
401
|
+
const statePath = resolveStatePath(args);
|
|
402
|
+
let state = statePath ? readJson(statePath) : null;
|
|
403
|
+
|
|
404
|
+
if (!state) {
|
|
405
|
+
if (!args.taskId) return;
|
|
406
|
+
state = { taskId: args.taskId };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (args.phase != null) {
|
|
410
|
+
const n = Number(args.phase);
|
|
411
|
+
state.currentPhase = Number.isNaN(n) ? args.phase : n;
|
|
412
|
+
}
|
|
413
|
+
if (args.status) state.status = args.status;
|
|
414
|
+
|
|
415
|
+
const event = buildEvent(state);
|
|
416
|
+
|
|
417
|
+
if (args.dryRun) {
|
|
418
|
+
process.stdout.write(JSON.stringify(event, null, 2) + "\n");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (isTerminalStatus(state)) appendErrorLedger(event);
|
|
423
|
+
await post(endpoint, token, event);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
main().catch(() => process.exit(0));
|