@mmerterden/multi-agent-pipeline 14.2.2 → 15.0.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 +76 -6
- package/README.md +15 -8
- package/README.tr.md +15 -8
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.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 +7 -7
- package/docs/ecosystem.md +28 -28
- package/docs/features.md +5 -5
- package/index.js +2 -0
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +65 -1
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +73 -7
- package/install/_plugin-skills.mjs +19 -8
- package/install/claude.mjs +144 -59
- package/install/codex.mjs +28 -3
- package/install/copilot.mjs +36 -11
- package/install/index.mjs +6 -2
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +3 -3
- package/package.json +1 -2
- 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/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
- 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 +1 -1
- package/pipeline/lib/issue-fetcher.sh +1 -1
- package/pipeline/lib/parse-complaints.sh +306 -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/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/tracker-contract.md +1 -1
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +13 -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 +276 -66
- package/pipeline/schemas/token-budget.json +2 -2
- 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 +52 -28
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/match-skills.mjs +8 -2
- package/pipeline/scripts/migrate-prefs.mjs +28 -20
- package/pipeline/scripts/phase-tracker.sh +13 -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 +158 -11
- package/pipeline/scripts/validate-complaint-doc.mjs +229 -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-ship → multi-agent-resume-local}/SKILL.md +6 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -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/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
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"warn_tokens": 5600
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
-
"total_max_tokens":
|
|
40
|
-
"note": "Token estimate = ceil(chars / 4). Per-phase budget rule: warn = current+10% (rounded to nearest 50), max = current+25%. Gives ~6 edit cycles of headroom before warn trips - intentionally quiet under normal maintenance, loud when a phase grows unusually. Only the active phase is loaded (lazy). Recalibrated at v10.0.0 after the validator/consistency/simplifier/lesson gate contracts landed in phases 1-4. Recalibrated again at v10.9.0 after the verify-by-test (Phase 4 Step 3.7), update-check (Phase 0 Step 0.6), immutable-test (Phase 3 GREEN) and redTests re-entry contracts landed - Step 3.7 prose was compressed to a pointer into refs/features/verify-by-test.md before the recalibration. Total bumped 50000 -> 51000 at v12.5.0 after the worktree residue/traversal-prune contract (Phase 0 + Phase 5 heal) and the Reflexion causal-diagnosis contract (Phase 4 lesson memory) landed; the prose was compressed first (161 tokens reclaimed) and every per-phase max still passes - only the aggregate needed room. Recalibrated again at v13.6.0 after the install-relative path correction: an instruction that names `pipeline/scripts/x` resolves only from a repo checkout, and a run happens in the user's worktree, so 157 references across these docs moved to `$HOME/.claude/...` at +5 bytes each - 196 tokens of pure correctness cost. Same discipline as before: prose was compressed FIRST (149 tokens reclaimed, by pointing Phase 1's Figma tier table at the Phase 0 probe that already resolved it and Phase 4's Codex constraints at the always-loaded AGENTS.md block), and only then were the budgets moved. Five warn lines had been permanently amber, which makes the amber tier useless as a signal, so every warn was reset to the documented current+10% and the four maxes that the new warn would have collided with were reset to current+25%. Aggregate 51000 -> 51500. Total bumped 51500 -> 52200 at v14.0.0 after Phase 4 Review entered the four --dev mode phase sets and the criteria-resolution contract (Step 1.78) landed. Same discipline as every prior bump: prose was compressed FIRST, 820 tokens reclaimed, before the number moved. Two of those compressions are structural rather than cosmetic - the hardcoded SwiftUI interaction list in Step 1.5 and the SwiftUI convention paragraph in Step 2.8 were transcriptions of rules that now live in a scoped registry, so keeping them here would have re-created the drift this release exists to remove, and the third moved the Step 1.78 full contract into refs/features/skill-conformance.md leaving a pointer. What remains is contract text that cannot be inferred: the manifest's four consumer-visible parts, the conformance checklist the reviewers must return, and the fail-closed semantics. Every per-phase max still passes (phase-4 12405/14750); only the aggregate needed room. Total bumped 52200 -> 52700 at v14.1.0 after two more contracts landed: stack skill routing (Phase 3 pre-flight step 9) and worktree finalize (Phase 6 step 9). Compression came first, as always, and twice: 224 tokens out of Phase 3 by pointing its criteria-ledger and routing steps at their feature files instead of restating them, and 190 out of Phase 6 by moving the finalize contract into refs/features/worktree-finalize.md and leaving the invocation plus the exit-3 semantics. Both new contracts follow the pattern the earlier ones set: the phase doc carries the call and the decision, the feature file carries the reasoning, and the feature files are outside this budget because it loops only the eight phase-N-* keys. Every per-phase max still passes (phase-3 7677/8950, phase-6 5223/6150 and both under warn); only the aggregate needed room. Total bumped 52700 -> 52750 for the Phase 0 Step 3 branch-persistence correction: the step wrote the legacy `projects[].branches` while the TTL filter two sections below read `global.recentBranches`, and both spots named a `{name, lastUsed}` shape the schema rejects (`branch` required, `additionalProperties: false`), so the recent-branch picker option could never populate and a literal implementation would have failed prefs validation. Naming the right target, the right key and the legacy field to avoid costs 41 tokens over the one line it replaces. Compression came first and was applied three times to the replacement text itself, from 120 tokens down to 66, by moving the rationale out of the phase doc entirely: the reasoning now lives where it is enforced, in the migrate-prefs carry-forward comment and the smoke-pref-migration f7 block, leaving the phase doc with only the instruction. 50 was the smallest step that clears it; phase-0-init sits at 10893/12400, far under its own max, so this is purely an aggregate ceiling."
|
|
39
|
+
"total_max_tokens": 53100,
|
|
40
|
+
"note": "Token estimate = ceil(chars / 4). Per-phase budget rule: warn = current+10% (rounded to nearest 50), max = current+25%. Gives ~6 edit cycles of headroom before warn trips - intentionally quiet under normal maintenance, loud when a phase grows unusually. Only the active phase is loaded (lazy). Recalibrated at v10.0.0 after the validator/consistency/simplifier/lesson gate contracts landed in phases 1-4. Recalibrated again at v10.9.0 after the verify-by-test (Phase 4 Step 3.7), update-check (Phase 0 Step 0.6), immutable-test (Phase 3 GREEN) and redTests re-entry contracts landed - Step 3.7 prose was compressed to a pointer into refs/features/verify-by-test.md before the recalibration. Total bumped 50000 -> 51000 at v12.5.0 after the worktree residue/traversal-prune contract (Phase 0 + Phase 5 heal) and the Reflexion causal-diagnosis contract (Phase 4 lesson memory) landed; the prose was compressed first (161 tokens reclaimed) and every per-phase max still passes - only the aggregate needed room. Recalibrated again at v13.6.0 after the install-relative path correction: an instruction that names `pipeline/scripts/x` resolves only from a repo checkout, and a run happens in the user's worktree, so 157 references across these docs moved to `$HOME/.claude/...` at +5 bytes each - 196 tokens of pure correctness cost. Same discipline as before: prose was compressed FIRST (149 tokens reclaimed, by pointing Phase 1's Figma tier table at the Phase 0 probe that already resolved it and Phase 4's Codex constraints at the always-loaded AGENTS.md block), and only then were the budgets moved. Five warn lines had been permanently amber, which makes the amber tier useless as a signal, so every warn was reset to the documented current+10% and the four maxes that the new warn would have collided with were reset to current+25%. Aggregate 51000 -> 51500. Total bumped 51500 -> 52200 at v14.0.0 after Phase 4 Review entered the four --dev mode phase sets and the criteria-resolution contract (Step 1.78) landed. Same discipline as every prior bump: prose was compressed FIRST, 820 tokens reclaimed, before the number moved. Two of those compressions are structural rather than cosmetic - the hardcoded SwiftUI interaction list in Step 1.5 and the SwiftUI convention paragraph in Step 2.8 were transcriptions of rules that now live in a scoped registry, so keeping them here would have re-created the drift this release exists to remove, and the third moved the Step 1.78 full contract into refs/features/skill-conformance.md leaving a pointer. What remains is contract text that cannot be inferred: the manifest's four consumer-visible parts, the conformance checklist the reviewers must return, and the fail-closed semantics. Every per-phase max still passes (phase-4 12405/14750); only the aggregate needed room. Total bumped 52200 -> 52700 at v14.1.0 after two more contracts landed: stack skill routing (Phase 3 pre-flight step 9) and worktree finalize (Phase 6 step 9). Compression came first, as always, and twice: 224 tokens out of Phase 3 by pointing its criteria-ledger and routing steps at their feature files instead of restating them, and 190 out of Phase 6 by moving the finalize contract into refs/features/worktree-finalize.md and leaving the invocation plus the exit-3 semantics. Both new contracts follow the pattern the earlier ones set: the phase doc carries the call and the decision, the feature file carries the reasoning, and the feature files are outside this budget because it loops only the eight phase-N-* keys. Every per-phase max still passes (phase-3 7677/8950, phase-6 5223/6150 and both under warn); only the aggregate needed room. Total bumped 52700 -> 52750 for the Phase 0 Step 3 branch-persistence correction: the step wrote the legacy `projects[].branches` while the TTL filter two sections below read `global.recentBranches`, and both spots named a `{name, lastUsed}` shape the schema rejects (`branch` required, `additionalProperties: false`), so the recent-branch picker option could never populate and a literal implementation would have failed prefs validation. Naming the right target, the right key and the legacy field to avoid costs 41 tokens over the one line it replaces. Compression came first and was applied three times to the replacement text itself, from 120 tokens down to 66, by moving the rationale out of the phase doc entirely: the reasoning now lives where it is enforced, in the migrate-prefs carry-forward comment and the smoke-pref-migration f7 block, leaving the phase doc with only the instruction. 50 was the smallest step that clears it; phase-0-init sits at 10893/12400, far under its own max, so this is purely an aggregate ceiling. v15.0.0: total 52750 -> 53100, the stack-skill tables in phase-1/2/4 now carry plugin-namespaced names (ai-<stack>-toolkit:<skill>) - functional prefixes, ~170 tokens."
|
|
41
41
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// _stack-routing.mjs
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for "which marketplace plugin carries this external
|
|
4
|
+
// skill". Extracted from build-stack-plugins.mjs so the skills-index builder can
|
|
5
|
+
// stamp a `plugin` field per entry without duplicating the routing table - a
|
|
6
|
+
// second copy would drift the first time a pattern changed, and the index would
|
|
7
|
+
// namespace skills into a plugin that no longer carries them.
|
|
8
|
+
//
|
|
9
|
+
// Routing order is load-bearing and mirrors build-stack-plugins step 2:
|
|
10
|
+
// COMMON_SKILLS first (explicit allowlist, wins over any pattern), then
|
|
11
|
+
// APPLE_ONLY (Apple/Xcode-only names no pattern catches), then STACK_PATTERNS.
|
|
12
|
+
// A skill may match several stack patterns; build-stack-plugins ships it to all
|
|
13
|
+
// of them and --check-routing polices accidental multi-routing. routeSkill()
|
|
14
|
+
// returns the full match list so callers can decide.
|
|
15
|
+
|
|
16
|
+
export const COMMON_PLUGIN = "ai-common-toolkit";
|
|
17
|
+
|
|
18
|
+
export const COMMON_SKILLS = [
|
|
19
|
+
"accessibility-compliance-accessibility-audit",
|
|
20
|
+
"firebase",
|
|
21
|
+
"humanizer",
|
|
22
|
+
"council",
|
|
23
|
+
"search-first",
|
|
24
|
+
"agent-introspection-debugging",
|
|
25
|
+
"skill-creator",
|
|
26
|
+
"backlog",
|
|
27
|
+
"localization-reuse-map",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Apple/Xcode-only skills that match no stack pattern → iOS plugin only.
|
|
31
|
+
export const APPLE_ONLY = [
|
|
32
|
+
"avkit",
|
|
33
|
+
"cryptokit",
|
|
34
|
+
"metrickit-diagnostics",
|
|
35
|
+
"pdfkit",
|
|
36
|
+
"spm-build-analysis",
|
|
37
|
+
"swiftlint",
|
|
38
|
+
"xcode-build-benchmark",
|
|
39
|
+
"xcode-build-fixer",
|
|
40
|
+
"xcode-build-orchestrator",
|
|
41
|
+
"xcode-compilation-analyzer",
|
|
42
|
+
"xcode-project-analyzer",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// stack → matching pattern (per-stack routing patterns (formerly in stack-swap.sh))
|
|
46
|
+
//
|
|
47
|
+
// A skill lands in every plugin whose pattern it matches, so a bare substring
|
|
48
|
+
// alternative that is also a suffix of another stack's skill name silently ships
|
|
49
|
+
// that skill twice. `architecture` did exactly that: it pulled
|
|
50
|
+
// `android-architecture` and `swift-architecture` into ai-backend-toolkit
|
|
51
|
+
// alongside their correct plugins, so a backend project loading the toolkit got
|
|
52
|
+
// Compose and SwiftUI architecture skills. It is anchored to the exact name now.
|
|
53
|
+
// Keep any new single-word alternative anchored unless it genuinely reads as a
|
|
54
|
+
// substring across stacks.
|
|
55
|
+
export const STACK_PATTERNS = {
|
|
56
|
+
"ai-ios-toolkit":
|
|
57
|
+
/(swiftui|swift-|ios-|hig-|apple-|storekit|widgetkit|healthkit|homekit|mapkit|musickit|passkit|pencilkit|realitykit|weatherkit|alarmkit|callkit|cloudkit|coreml|core-|eventkit|energykit|permissionkit|tipkit|shareplay|live-activities|background-processing|app-store|app-clips|app-intents|authentication|contacts-framework|device-integrity|macos-|natural-language|photos-camera|push-notifications|speech-recognition|swiftdata|vision-framework|debugging-instruments|help-skills)/i,
|
|
58
|
+
"ai-android-toolkit":
|
|
59
|
+
/(android|compose-|kotlin-|room-database|retrofit-|gradle-|play-store)/i,
|
|
60
|
+
"ai-backend-toolkit":
|
|
61
|
+
/(fastapi|nodejs|docker|api-pattern|api-security|github-actions|observability|^architecture$|monorepo|clean-code|debugging-strategies|agentflow|closed-loop|context-compression|python-patterns|database-patterns|rest-api-design|testing-backend|ci-cd-pipelines)/i,
|
|
62
|
+
"ai-frontend-toolkit":
|
|
63
|
+
/(react-|nextjs-|typescript-|tailwind-|vue-|web-accessibility|web-performance|css-modern|web-testing|html-semantic)/i,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Which plugins carry this external skill, in routing order.
|
|
68
|
+
* @param {string} name - skill directory name
|
|
69
|
+
* @returns {string[]} plugin names; empty when unrouted
|
|
70
|
+
*/
|
|
71
|
+
export function routeSkill(name) {
|
|
72
|
+
if (COMMON_SKILLS.includes(name)) return [COMMON_PLUGIN];
|
|
73
|
+
if (APPLE_ONLY.includes(name)) return ["ai-ios-toolkit"];
|
|
74
|
+
const hits = [];
|
|
75
|
+
for (const [plugin, re] of Object.entries(STACK_PATTERNS)) {
|
|
76
|
+
if (re.test(name)) hits.push(plugin);
|
|
77
|
+
}
|
|
78
|
+
return hits;
|
|
79
|
+
}
|
|
@@ -57,7 +57,10 @@ if [ -s "$AUDIT_FILE" ]; then
|
|
|
57
57
|
# event appended between the copy and the truncate.
|
|
58
58
|
SPOOL="$AUDIT_DIR/.audit.rotate.$$"
|
|
59
59
|
mv "$AUDIT_FILE" "$SPOOL"
|
|
60
|
-
:
|
|
60
|
+
# touch, not truncate: a writer appending between the mv and this line
|
|
61
|
+
# recreates the live file, and `: >` would destroy that event. touch keeps
|
|
62
|
+
# whatever a racing writer already appended.
|
|
63
|
+
touch "$AUDIT_FILE"
|
|
61
64
|
chmod 600 "$AUDIT_FILE" 2>/dev/null || true
|
|
62
65
|
|
|
63
66
|
# Don't clobber a rotated file from earlier today (multiple invocations).
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { dirname, join, relative, resolve } from "node:path";
|
|
28
28
|
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { routeSkill } from "./_stack-routing.mjs";
|
|
29
30
|
|
|
30
31
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
31
32
|
const repoRoot = resolve(here, "..", "..");
|
|
@@ -92,11 +93,21 @@ for (const full of walkSkills(root)) {
|
|
|
92
93
|
.map((s) => s.trim())
|
|
93
94
|
.filter(Boolean);
|
|
94
95
|
|
|
96
|
+
// External skills reach Claude Code only through the marketplace plugins, so
|
|
97
|
+
// the index carries which plugin owns each one. `invokeAs` is the name a
|
|
98
|
+
// consumer must use: plugin-namespaced on Claude Code, bare elsewhere (the
|
|
99
|
+
// Copilot/Codex copies keep bare directory names). Routing comes from
|
|
100
|
+
// _stack-routing.mjs - the same table build-stack-plugins ships with.
|
|
101
|
+
const plugins = group === "external" ? routeSkill(name) : [];
|
|
102
|
+
const plugin = plugins[0] || null;
|
|
103
|
+
|
|
95
104
|
entries.push({
|
|
96
105
|
name,
|
|
97
106
|
description,
|
|
98
107
|
platform,
|
|
99
108
|
group,
|
|
109
|
+
plugin,
|
|
110
|
+
invokeAs: plugin ? `${plugin}:${name}` : name,
|
|
100
111
|
triggerKeywords: kws,
|
|
101
112
|
triggerPaths: paths,
|
|
102
113
|
relativePath: rel,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* knowledge entries are regenerated.
|
|
15
15
|
*
|
|
16
16
|
* Routing uses the per-stack skill patterns (formerly carried by stack-swap.sh). Genuinely cross-stack
|
|
17
|
-
* skills go to ai-common-
|
|
17
|
+
* skills go to ai-common-toolkit (not into each stack plugin). Apple/
|
|
18
18
|
* Xcode-only vendored skills that match no stack pattern belong to iOS only.
|
|
19
19
|
*
|
|
20
20
|
* Usage:
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "node:fs";
|
|
35
35
|
import { join } from "node:path";
|
|
36
36
|
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { createHash } from "node:crypto";
|
|
37
38
|
|
|
38
39
|
const args = process.argv.slice(2);
|
|
39
40
|
// Accepts both `--key value` and `--key=value`. The `=` form used to fall through to
|
|
@@ -51,54 +52,12 @@ const PIPE_ROOT = getArg("--pipeline", join(HOME, "multi-agent-pipeline"));
|
|
|
51
52
|
const EXTERNAL = join(PIPE_ROOT, "pipeline/skills/shared/external");
|
|
52
53
|
const DRY = args.includes("--dry-run");
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"search-first",
|
|
61
|
-
"agent-introspection-debugging",
|
|
62
|
-
"skill-creator",
|
|
63
|
-
"backlog",
|
|
64
|
-
"localization-reuse-map",
|
|
65
|
-
];
|
|
66
|
-
|
|
67
|
-
// Apple/Xcode-only skills that match no stack pattern → iOS plugin only.
|
|
68
|
-
const APPLE_ONLY = [
|
|
69
|
-
"avkit",
|
|
70
|
-
"cryptokit",
|
|
71
|
-
"metrickit-diagnostics",
|
|
72
|
-
"pdfkit",
|
|
73
|
-
"spm-build-analysis",
|
|
74
|
-
"swiftlint",
|
|
75
|
-
"xcode-build-benchmark",
|
|
76
|
-
"xcode-build-fixer",
|
|
77
|
-
"xcode-build-orchestrator",
|
|
78
|
-
"xcode-compilation-analyzer",
|
|
79
|
-
"xcode-project-analyzer",
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
// stack → matching pattern (per-stack routing patterns (formerly in stack-swap.sh))
|
|
83
|
-
//
|
|
84
|
-
// A skill lands in every plugin whose pattern it matches, so a bare substring
|
|
85
|
-
// alternative that is also a suffix of another stack's skill name silently ships
|
|
86
|
-
// that skill twice. `architecture` did exactly that: it pulled
|
|
87
|
-
// `android-architecture` and `swift-architecture` into ai-backend-toolkit
|
|
88
|
-
// alongside their correct plugins, so a backend project loading the toolkit got
|
|
89
|
-
// Compose and SwiftUI architecture skills. It is anchored to the exact name now.
|
|
90
|
-
// Keep any new single-word alternative anchored unless it genuinely reads as a
|
|
91
|
-
// substring across stacks.
|
|
92
|
-
const STACK_PATTERNS = {
|
|
93
|
-
"ai-ios-engineering-toolkit":
|
|
94
|
-
/(swiftui|swift-|ios-|hig-|apple-|storekit|widgetkit|healthkit|homekit|mapkit|musickit|passkit|pencilkit|realitykit|weatherkit|alarmkit|callkit|cloudkit|coreml|core-|eventkit|energykit|permissionkit|tipkit|shareplay|live-activities|background-processing|app-store|app-clips|app-intents|authentication|contacts-framework|device-integrity|macos-|natural-language|photos-camera|push-notifications|speech-recognition|swiftdata|vision-framework|debugging-instruments|help-skills)/i,
|
|
95
|
-
"ai-android-engineering-toolkit":
|
|
96
|
-
/(android|compose-|kotlin-|room-database|retrofit-|gradle-|play-store)/i,
|
|
97
|
-
"ai-backend-toolkit":
|
|
98
|
-
/(fastapi|nodejs|docker|api-pattern|api-security|github-actions|observability|^architecture$|monorepo|clean-code|debugging-strategies|agentflow|closed-loop|context-compression|python-patterns|database-patterns|rest-api-design|testing-backend|ci-cd-pipelines)/i,
|
|
99
|
-
"ai-frontend-engineering-toolkit":
|
|
100
|
-
/(react-|nextjs-|typescript-|tailwind-|vue-|web-accessibility|web-performance|css-modern|web-testing|html-semantic)/i,
|
|
101
|
-
};
|
|
55
|
+
import {
|
|
56
|
+
COMMON_PLUGIN,
|
|
57
|
+
COMMON_SKILLS,
|
|
58
|
+
APPLE_ONLY,
|
|
59
|
+
STACK_PATTERNS,
|
|
60
|
+
} from "./_stack-routing.mjs";
|
|
102
61
|
|
|
103
62
|
const isDir = (p) => {
|
|
104
63
|
try {
|
|
@@ -110,18 +69,23 @@ const isDir = (p) => {
|
|
|
110
69
|
const listDirs = (p) => (existsSync(p) ? readdirSync(p).filter((n) => isDir(join(p, n))) : []);
|
|
111
70
|
|
|
112
71
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
72
|
+
* Content fingerprint for a skill directory: every file's relative path and a
|
|
73
|
+
* hash of its bytes. A size-only fingerprint missed same-length edits, so a
|
|
74
|
+
* content update could ship without a version bump. The whole authoring tree
|
|
75
|
+
* is a few MB of markdown; hashing it per run costs milliseconds and is only
|
|
76
|
+
* used to decide whether a copy + version bump is warranted.
|
|
116
77
|
*/
|
|
117
78
|
function treeFingerprint(dir) {
|
|
118
79
|
const parts = [];
|
|
119
80
|
const walk = (d, rel) => {
|
|
120
|
-
for (const e of readdirSync(d, { withFileTypes: true }).sort((a, b) =>
|
|
81
|
+
for (const e of readdirSync(d, { withFileTypes: true }).sort((a, b) =>
|
|
82
|
+
a.name.localeCompare(b.name),
|
|
83
|
+
)) {
|
|
121
84
|
const full = join(d, e.name);
|
|
122
85
|
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
123
86
|
if (e.isDirectory()) walk(full, r);
|
|
124
|
-
else if (e.isFile())
|
|
87
|
+
else if (e.isFile())
|
|
88
|
+
parts.push(`${r}:${createHash("sha1").update(readFileSync(full)).digest("hex")}`);
|
|
125
89
|
}
|
|
126
90
|
};
|
|
127
91
|
try {
|
|
@@ -156,7 +120,7 @@ for (const skill of allSkills) {
|
|
|
156
120
|
continue;
|
|
157
121
|
}
|
|
158
122
|
if (APPLE_ONLY.includes(skill)) {
|
|
159
|
-
desired["ai-ios-
|
|
123
|
+
desired["ai-ios-toolkit"].add(skill);
|
|
160
124
|
continue;
|
|
161
125
|
}
|
|
162
126
|
let matched = false;
|
|
@@ -218,7 +182,8 @@ for (const [plugin, want] of Object.entries(desired)) {
|
|
|
218
182
|
const toRemove = [...have].filter((s) => !want.has(s)).sort();
|
|
219
183
|
const setChanged = toAdd.length > 0 || toRemove.length > 0;
|
|
220
184
|
|
|
221
|
-
//
|
|
185
|
+
// Compare EVERY wanted skill against the source, not just the newly added
|
|
186
|
+
// ones, and replace (rm + copy) rather than overlay.
|
|
222
187
|
//
|
|
223
188
|
// This used to copy only on a set change, which quietly broke the single-source
|
|
224
189
|
// claim: editing a skill in shared/external left the plugin serving the old text
|
|
@@ -226,17 +191,20 @@ for (const [plugin, want] of Object.entries(desired)) {
|
|
|
226
191
|
// seeing the plugin copy stay stale while the generator reported "all plugins up
|
|
227
192
|
// to date".
|
|
228
193
|
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
194
|
+
// The rm-before-copy matters too: cpSync overlays and never deletes, so a file
|
|
195
|
+
// REMOVED inside a still-wanted skill survived in every plugin copy forever -
|
|
196
|
+
// and because the overlay kept the orphan on both sides of the fingerprint
|
|
197
|
+
// comparison, the version was not bumped either.
|
|
231
198
|
let contentChanged = false;
|
|
232
199
|
if (!DRY) {
|
|
233
200
|
if (!existsSync(kdir)) mkdirSync(kdir, { recursive: true });
|
|
234
201
|
for (const s of want) {
|
|
235
202
|
const from = join(EXTERNAL, s);
|
|
236
203
|
const to = join(kdir, s);
|
|
237
|
-
|
|
204
|
+
if (treeFingerprint(from) === (existsSync(to) ? treeFingerprint(to) : "")) continue;
|
|
205
|
+
rmSync(to, { recursive: true, force: true });
|
|
238
206
|
cpSync(from, to, { recursive: true });
|
|
239
|
-
|
|
207
|
+
contentChanged = true;
|
|
240
208
|
}
|
|
241
209
|
for (const s of toRemove) rmSync(join(kdir, s), { recursive: true, force: true });
|
|
242
210
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { Buffer } from "node:buffer";
|
|
26
26
|
import { execFileSync } from "node:child_process";
|
|
27
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
27
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
|
|
@@ -72,29 +72,44 @@ function versionFrom(manifest, pluginName) {
|
|
|
72
72
|
function fromLocalClone(entry) {
|
|
73
73
|
if (!entry.upstreamLocalClone) return null;
|
|
74
74
|
const root = expandHome(entry.upstreamLocalClone);
|
|
75
|
-
const
|
|
75
|
+
const relFile =
|
|
76
76
|
entry.upstreamVersionSource === "plugin.json"
|
|
77
|
-
?
|
|
78
|
-
:
|
|
79
|
-
if (!existsSync(file)) return null;
|
|
80
|
-
const version = versionFrom(readJson(file), entry.upstreamPlugin);
|
|
81
|
-
if (!version) return null;
|
|
77
|
+
? `plugins/${entry.upstreamPlugin}/.claude-plugin/plugin.json`
|
|
78
|
+
: ".claude-plugin/marketplace.json";
|
|
82
79
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
80
|
+
// Read the manifest from origin/main, never the checked-out worktree: a
|
|
81
|
+
// clone parked on an old feature branch reported stale versions as current,
|
|
82
|
+
// flagging correctly-bumped derivations as drifted.
|
|
86
83
|
try {
|
|
87
|
-
const
|
|
84
|
+
const raw = execFileSync("git", ["-C", root, "show", `origin/main:${relFile}`], {
|
|
88
85
|
encoding: "utf-8",
|
|
89
86
|
stdio: ["ignore", "pipe", "ignore"],
|
|
90
|
-
})
|
|
91
|
-
|
|
87
|
+
});
|
|
88
|
+
const version = versionFrom(JSON.parse(raw), entry.upstreamPlugin);
|
|
89
|
+
if (version) {
|
|
90
|
+
// origin/main is only as fresh as the last fetch. Surface fetch age so a
|
|
91
|
+
// months-old ref is not silently trusted.
|
|
92
|
+
let fetchAgeDays = null;
|
|
93
|
+
try {
|
|
94
|
+
const fetchHead = join(root, ".git", "FETCH_HEAD");
|
|
95
|
+
if (existsSync(fetchHead)) {
|
|
96
|
+
fetchAgeDays = Math.floor((Date.now() - statSync(fetchHead).mtimeMs) / 86_400_000);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
fetchAgeDays = null;
|
|
100
|
+
}
|
|
101
|
+
return { version, source: "local clone origin/main", authoritative: true, fetchAgeDays };
|
|
102
|
+
}
|
|
92
103
|
} catch {
|
|
93
|
-
// No
|
|
94
|
-
|
|
104
|
+
// No origin/main ref (not a git checkout, or unfetched): fall through to
|
|
105
|
+
// the worktree file, which is better than nothing but never fresher.
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
const file = join(root, relFile);
|
|
109
|
+
if (!existsSync(file)) return null;
|
|
110
|
+
const version = versionFrom(readJson(file), entry.upstreamPlugin);
|
|
111
|
+
if (!version) return null;
|
|
112
|
+
return { version, source: "local clone worktree", authoritative: true, fetchAgeDays: null };
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
function fromRepoApi(entry) {
|
|
@@ -161,46 +176,55 @@ for (const entry of entries) {
|
|
|
161
176
|
continue;
|
|
162
177
|
}
|
|
163
178
|
const drifted = resolved.version !== entry.derivedFromVersion;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
// An acknowledged drift is a conscious "port pending" decision, pinned to the
|
|
180
|
+
// upstream version it was made against: it stays visible but does not fail
|
|
181
|
+
// the gate, and the moment upstream moves past the pin it is plain drift
|
|
182
|
+
// again.
|
|
183
|
+
const acknowledged = drifted && entry.driftAcknowledged === resolved.version;
|
|
184
|
+
const status = acknowledged
|
|
185
|
+
? "drift (acknowledged)"
|
|
186
|
+
: drifted
|
|
187
|
+
? "drift"
|
|
188
|
+
: resolved.authoritative
|
|
189
|
+
? "up to date"
|
|
190
|
+
: "unverified (cache only)";
|
|
169
191
|
rows.push({
|
|
170
192
|
label: entry.label,
|
|
171
193
|
plugin: entry.upstreamPlugin,
|
|
172
194
|
from: entry.derivedFromVersion,
|
|
173
195
|
upstream: resolved.version,
|
|
174
196
|
source: resolved.source,
|
|
175
|
-
|
|
197
|
+
fetchAgeDays: resolved.fetchAgeDays ?? null,
|
|
176
198
|
status,
|
|
177
199
|
});
|
|
178
200
|
}
|
|
179
201
|
|
|
180
202
|
const drifted = rows.filter((r) => r.status === "drift").length;
|
|
203
|
+
const acknowledged = rows.filter((r) => r.status === "drift (acknowledged)").length;
|
|
181
204
|
const unverified = rows.filter((r) => r.status.startsWith("unverified")).length;
|
|
182
205
|
const unreachable = rows.filter((r) => r.status === "unreachable").length;
|
|
183
206
|
|
|
184
207
|
if (asJson) {
|
|
185
208
|
console.log(
|
|
186
209
|
JSON.stringify(
|
|
187
|
-
{ configured: rows.length, drifted, unverified, unreachable, entries: rows },
|
|
210
|
+
{ configured: rows.length, drifted, acknowledged, unverified, unreachable, entries: rows },
|
|
188
211
|
null,
|
|
189
212
|
2,
|
|
190
213
|
),
|
|
191
214
|
);
|
|
192
215
|
} else {
|
|
193
216
|
for (const r of rows) {
|
|
194
|
-
const
|
|
217
|
+
const age =
|
|
218
|
+
r.fetchAgeDays != null && r.fetchAgeDays > 7 ? ` [last fetch ${r.fetchAgeDays}d ago]` : "";
|
|
195
219
|
console.log(
|
|
196
|
-
` ${r.status === "up to date" ? "✓" : "✗"} ${r.plugin}: recorded ${r.from}, ` +
|
|
197
|
-
`upstream ${r.upstream ?? "?"} (${r.source})${
|
|
220
|
+
` ${r.status === "up to date" ? "✓" : r.status === "drift (acknowledged)" ? "!" : "✗"} ${r.plugin}: recorded ${r.from}, ` +
|
|
221
|
+
`upstream ${r.upstream ?? "?"} (${r.source})${age} -> ${r.status}`,
|
|
198
222
|
);
|
|
199
223
|
}
|
|
200
224
|
console.log("");
|
|
201
225
|
console.log(
|
|
202
|
-
`══ check-derived-drift: ${rows.length - drifted - unverified - unreachable} passed
|
|
203
|
-
|
|
226
|
+
`══ check-derived-drift: ${rows.length - drifted - unverified - unreachable} passed ` +
|
|
227
|
+
`(${acknowledged} acknowledged), ${drifted + unverified + unreachable} failed ══`,
|
|
204
228
|
);
|
|
205
229
|
}
|
|
206
230
|
|
|
@@ -125,7 +125,10 @@ done
|
|
|
125
125
|
|
|
126
126
|
# 3. Gitlink residue in the index ("Subproject commit" entries under
|
|
127
127
|
# .worktrees/ from a pre-guard `git add -A`).
|
|
128
|
-
|
|
128
|
+
# Path is the tab-separated second field ("mode sha stage\tpath"); splitting on
|
|
129
|
+
# whitespace truncated any path containing a space and the follow-up
|
|
130
|
+
# `git rm --ignore-unmatch` exited 0 on the phantom, reporting a false success.
|
|
131
|
+
gitlinks=$(git -C "$REPO" ls-files -s -- .worktrees 2>/dev/null | awk -F'\t' '$1 ~ /^160000 / { print $2 }')
|
|
129
132
|
if [ -n "$gitlinks" ]; then
|
|
130
133
|
while IFS= read -r path; do
|
|
131
134
|
[ -z "$path" ] && continue
|
|
@@ -82,7 +82,7 @@ md += `**Total:** ${total} skills (${breakdown}). Auto-generated by \`scripts/ge
|
|
|
82
82
|
md += `## Directory layout\n\n`;
|
|
83
83
|
md += `- **\`core/\`** - ${counts.core} \`multi-agent*\` orchestration skills that are pipeline-critical. Edits here are core-code changes.\n`;
|
|
84
84
|
md += `- **\`external/\`** - ${counts.external} iOS / Android / generic skills imported from the upstream skill library. Mirrors of third-party guidance.\n`;
|
|
85
|
-
md += `- Install
|
|
85
|
+
md += `- Install destinations (ADR-0009): Claude Code gets NO local copy of \`external/\` - it loads those skills from the \`multi-agent-plugins\` marketplace, namespaced (\`ai-<stack>-toolkit:<name>\`); only the two compliance catalogs from \`core/\` land in \`~/.claude/skills/\`. Copilot CLI and Codex CLI receive a flat copy filtered to the enabled stacks. \`external/\` remains the single authoring source that \`build-stack-plugins.mjs\` publishes from.\n\n`;
|
|
86
86
|
md += `Source layout is logical grouping only - skill discovery at runtime is unchanged.\n\n`;
|
|
87
87
|
md += `## Categories\n\n`;
|
|
88
88
|
for (const k of order) {
|
|
@@ -202,13 +202,19 @@ for (const entry of index.entries) {
|
|
|
202
202
|
results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
203
203
|
const top = results.slice(0, opts.limit);
|
|
204
204
|
|
|
205
|
+
// On Claude Code an external skill resolves only through its marketplace plugin,
|
|
206
|
+
// so surface `invokeAs` (plugin-namespaced) as the primary name. Old indexes
|
|
207
|
+
// without the field fall back to the bare name - same behaviour as before.
|
|
205
208
|
if (opts.json) {
|
|
206
|
-
|
|
209
|
+
const matches = top.map((m) => ({ ...m, invokeAs: m.invokeAs || m.name }));
|
|
210
|
+
process.stdout.write(JSON.stringify({ task: taskText, matches }, null, 2) + "\n");
|
|
207
211
|
} else {
|
|
208
212
|
for (const m of top) {
|
|
209
213
|
const tag = `[${m.group}${m.platform ? "/" + m.platform : ""}]`;
|
|
210
214
|
const desc = (m.description || "").slice(0, 80);
|
|
211
|
-
console.log(
|
|
215
|
+
console.log(
|
|
216
|
+
`${String(m.score).padStart(3)} ${(m.invokeAs || m.name).padEnd(46)} ${tag.padEnd(18)} ${desc}`,
|
|
217
|
+
);
|
|
212
218
|
}
|
|
213
219
|
if (top.length === 0) console.log("(no skills matched)");
|
|
214
220
|
}
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import os from "node:os";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
16
17
|
|
|
17
18
|
const DEFAULT_FILE = path.join(os.homedir(), ".claude", "multi-agent-preferences.json");
|
|
18
19
|
|
|
19
20
|
// Single source of truth for the migration target version. Referenced by the
|
|
20
21
|
// migrate() bump chain AND every user-facing message, so they cannot drift.
|
|
21
|
-
const TARGET_VERSION = "2.
|
|
22
|
+
const TARGET_VERSION = "2.6.0";
|
|
22
23
|
|
|
23
24
|
const USAGE = "Usage: migrate-prefs.mjs [--dry-run] [--file <path>]";
|
|
24
25
|
|
|
@@ -75,7 +76,7 @@ function defaultSettings() {
|
|
|
75
76
|
* @returns {Set<string>}
|
|
76
77
|
*/
|
|
77
78
|
function readMigratableVersions(target) {
|
|
78
|
-
const here = path.dirname(
|
|
79
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
79
80
|
const candidates = [
|
|
80
81
|
path.join(here, "..", "schemas", "prefs.schema.json"),
|
|
81
82
|
path.join(here, "..", "..", "pipeline", "schemas", "prefs.schema.json"),
|
|
@@ -297,7 +298,9 @@ function migrate(prefs) {
|
|
|
297
298
|
seeded += 1;
|
|
298
299
|
}
|
|
299
300
|
if (seeded > 0) {
|
|
300
|
-
changes.push(
|
|
301
|
+
changes.push(
|
|
302
|
+
`seeded ${seeded} recentBranches entr${seeded === 1 ? "y" : "ies"} for ${projectKey} from legacy projects[].branches`,
|
|
303
|
+
);
|
|
301
304
|
}
|
|
302
305
|
}
|
|
303
306
|
if (!Array.isArray(out.global.recentGroups)) {
|
|
@@ -332,27 +335,32 @@ function migrate(prefs) {
|
|
|
332
335
|
out.global.skillConformance.blockOnCoverageGap = false;
|
|
333
336
|
changes.push("added skillConformance.blockOnCoverageGap=false (report, do not halt)");
|
|
334
337
|
}
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
if (!out.global.
|
|
339
|
-
out.global.
|
|
340
|
-
changes.push("added
|
|
341
|
-
}
|
|
342
|
-
if (typeof out.global.
|
|
343
|
-
const legacyAutoFix =
|
|
344
|
-
|
|
338
|
+
// Rename chain for the pipeline-tail command: /multi-agent:finish (pre-v2.5.0)
|
|
339
|
+
// -> :ship (v2.5.0) -> :resume-local (current). The autoFix value survives every
|
|
340
|
+
// hop - carry whichever legacy block still holds one rather than reset it.
|
|
341
|
+
if (!out.global.resumeLocal || typeof out.global.resumeLocal !== "object") {
|
|
342
|
+
out.global.resumeLocal = {};
|
|
343
|
+
changes.push("added resumeLocal (/multi-agent:ship renamed to :resume-local)");
|
|
344
|
+
}
|
|
345
|
+
if (typeof out.global.resumeLocal.autoFix !== "boolean") {
|
|
346
|
+
const legacyAutoFix =
|
|
347
|
+
typeof out.global.ship?.autoFix === "boolean"
|
|
348
|
+
? out.global.ship.autoFix
|
|
349
|
+
: out.global.finish?.autoFix;
|
|
350
|
+
out.global.resumeLocal.autoFix = typeof legacyAutoFix === "boolean" ? legacyAutoFix : false;
|
|
345
351
|
changes.push(
|
|
346
352
|
typeof legacyAutoFix === "boolean"
|
|
347
|
-
? "moved finish
|
|
348
|
-
: "added
|
|
353
|
+
? "moved ship/finish autoFix -> resumeLocal.autoFix (value preserved)"
|
|
354
|
+
: "added resumeLocal.autoFix=false",
|
|
349
355
|
);
|
|
350
356
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
357
|
+
for (const legacy of ["ship", "finish"]) {
|
|
358
|
+
if (out.global[legacy] && typeof out.global[legacy] === "object") {
|
|
359
|
+
delete out.global[legacy].autoFix;
|
|
360
|
+
if (Object.keys(out.global[legacy]).length === 0) {
|
|
361
|
+
delete out.global[legacy];
|
|
362
|
+
changes.push(`removed obsolete ${legacy} block (renamed to resumeLocal)`);
|
|
363
|
+
}
|
|
356
364
|
}
|
|
357
365
|
}
|
|
358
366
|
if (!out.global.serviceStatus) {
|
|
@@ -382,6 +382,9 @@ load_state() {
|
|
|
382
382
|
fi
|
|
383
383
|
}
|
|
384
384
|
save_state() {
|
|
385
|
+
# Refuse to persist an empty document: every caller pipes jq output in, and
|
|
386
|
+
# a failed jq yields "" - writing that would destroy the whole tracker state.
|
|
387
|
+
[ -n "$1" ] || { echo "save_state: refusing to write empty state" >&2; return 65; }
|
|
385
388
|
mkdir -p "$TRACKER_DIR" 2>/dev/null
|
|
386
389
|
printf '%s\n' "$1" > "${TRACKER_FILE}.tmp"
|
|
387
390
|
mv "${TRACKER_FILE}.tmp" "$TRACKER_FILE"
|
|
@@ -693,7 +696,7 @@ case "$ACTION" in
|
|
|
693
696
|
state=$(load_state)
|
|
694
697
|
# Idempotent: a phase id that already exists is a no-op (name, status, and
|
|
695
698
|
# token history are preserved). This is what lets continuation commands
|
|
696
|
-
# (/multi-agent:
|
|
699
|
+
# (/multi-agent:resume-local, resume) re-declare their phase set against a
|
|
697
700
|
# pre-existing tracker without duplicating or resetting tiles.
|
|
698
701
|
new=$(echo "$state" | jq --arg id "$PID" --arg name "$PNAME" '
|
|
699
702
|
if any(.phases[]?; .id == $id) then .
|
|
@@ -766,9 +769,13 @@ case "$ACTION" in
|
|
|
766
769
|
[ "$#" -ge 3 ] || { echo "tokens needs <phase_id> <in> <out> [cached]" >&2; exit 64; }
|
|
767
770
|
PID="$1"; T_IN="$2"; T_OUT="$3"; T_CACHED="${4:-0}"
|
|
768
771
|
# Validate integers (cached is optional, defaults to 0 for back-compat).
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
+
# Each field checked separately: concatenation would let an empty field
|
|
773
|
+
# pass and feed jq --argjson an empty string, nuking the state file.
|
|
774
|
+
for v in "$T_IN" "$T_OUT" "$T_CACHED"; do
|
|
775
|
+
case "$v" in
|
|
776
|
+
''|*[!0-9]*) echo "tokens: in/out/cached must be non-negative integers" >&2; exit 64 ;;
|
|
777
|
+
esac
|
|
778
|
+
done
|
|
772
779
|
acquire_state_lock
|
|
773
780
|
state=$(load_state)
|
|
774
781
|
# Additive: add to existing totals so multiple LLM calls accumulate.
|
|
@@ -819,7 +826,8 @@ case "$ACTION" in
|
|
|
819
826
|
)')
|
|
820
827
|
save_state "$new"
|
|
821
828
|
release_state_lock
|
|
822
|
-
|
|
829
|
+
META_EXTRA=$(jq -nc --arg k "$KEY" --arg v "$VALUE" '{meta_key: $k, meta_value: $v}')
|
|
830
|
+
emit_otel_span "phase.meta" "$PID" "$KEY" "$META_EXTRA"
|
|
823
831
|
if [ "${TRACKER_QUIET:-0}" != "1" ]; then
|
|
824
832
|
render
|
|
825
833
|
fi
|
|
@@ -76,7 +76,8 @@ export function inputTextOf(state) {
|
|
|
76
76
|
// evidence[].url / figmaFrames[] are where the analysis phase parks references.
|
|
77
77
|
for (const key of ["figmaFrames", "designRefs"]) {
|
|
78
78
|
const v = state[key];
|
|
79
|
-
if (Array.isArray(v))
|
|
79
|
+
if (Array.isArray(v))
|
|
80
|
+
parts.push(v.map((x) => (typeof x === "string" ? x : JSON.stringify(x))).join(" "));
|
|
80
81
|
}
|
|
81
82
|
return parts.join("\n");
|
|
82
83
|
}
|
|
@@ -198,7 +199,7 @@ function main(argv) {
|
|
|
198
199
|
const args = argv.slice(2);
|
|
199
200
|
const taskId = args.find((a) => !a.startsWith("--"));
|
|
200
201
|
if (!taskId) {
|
|
201
|
-
console.error(
|
|
202
|
+
console.error('usage: phase0-exit-gate.mjs <task_id> [--input "<text>"] [--json]');
|
|
202
203
|
return 2;
|
|
203
204
|
}
|
|
204
205
|
const inputIdx = args.indexOf("--input");
|