@mmerterden/multi-agent-pipeline 14.2.1 → 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.
Files changed (184) hide show
  1. package/CHANGELOG.md +118 -6
  2. package/README.md +20 -9
  3. package/README.tr.md +150 -0
  4. package/docs/FIGMA_PIPELINE.md +3 -3
  5. package/docs/adr/0006-skills-core-external-split.md +1 -1
  6. package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
  7. package/docs/adr/README.md +1 -0
  8. package/docs/architecture.md +24 -9
  9. package/docs/ecosystem.md +237 -0
  10. package/docs/features.md +5 -5
  11. package/index.js +2 -0
  12. package/install/_codex-agents.mjs +11 -2
  13. package/install/_common.mjs +65 -1
  14. package/install/_dev-only-files.mjs +0 -1
  15. package/install/_platform-filter.mjs +73 -7
  16. package/install/_plugin-skills.mjs +33 -8
  17. package/install/claude.mjs +144 -59
  18. package/install/codex.mjs +37 -7
  19. package/install/copilot.mjs +36 -11
  20. package/install/index.mjs +6 -2
  21. package/install/templates/codex-instructions.md +1 -1
  22. package/install/templates/copilot-instructions.md +15 -12
  23. package/package.json +1 -2
  24. package/pipeline/commands/multi-agent/SKILL.md +4 -2
  25. package/pipeline/commands/multi-agent/analysis/SKILL.md +5 -5
  26. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
  27. package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
  28. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
  29. package/pipeline/commands/multi-agent/channels/SKILL.md +16 -5
  30. package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
  31. package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
  32. package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
  33. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
  34. package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
  35. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
  36. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  37. package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
  38. package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
  39. package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
  40. package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
  41. package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
  42. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
  43. package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
  44. package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
  45. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
  46. package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +13 -9
  47. package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
  48. package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
  49. package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
  50. package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
  51. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
  52. package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
  53. package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
  54. package/pipeline/lib/extract-conventions.sh +44 -15
  55. package/pipeline/lib/fetch-figma-annotations.sh +8 -1
  56. package/pipeline/lib/fetch-fortify.sh +23 -8
  57. package/pipeline/lib/figma-screenshot.sh +11 -1
  58. package/pipeline/lib/issue-fetcher.sh +77 -10
  59. package/pipeline/lib/md2confluence-v3.py +16 -2
  60. package/pipeline/lib/parse-complaints.sh +306 -0
  61. package/pipeline/lib/plan-todos.sh +5 -2
  62. package/pipeline/lib/post-pr-review.sh +8 -6
  63. package/pipeline/lib/shadow-git.sh +50 -9
  64. package/pipeline/lib/submodule-detector.sh +8 -1
  65. package/pipeline/multi-agent-refs/_input-parser.md +1 -1
  66. package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
  67. package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
  68. package/pipeline/multi-agent-refs/channels/jira.md +13 -2
  69. package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
  70. package/pipeline/multi-agent-refs/channels/pr.md +20 -0
  71. package/pipeline/multi-agent-refs/channels/wiki.md +4 -4
  72. package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
  73. package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
  74. package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
  75. package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
  76. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
  77. package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
  78. package/pipeline/multi-agent-refs/generate-issue.md +3 -3
  79. package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
  80. package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
  81. package/pipeline/multi-agent-refs/phases/modes.md +20 -0
  82. package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
  83. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
  84. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
  85. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
  86. package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
  87. package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
  88. package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
  89. package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
  90. package/pipeline/multi-agent-refs/phases.md +6 -0
  91. package/pipeline/multi-agent-refs/rules.md +2 -0
  92. package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
  93. package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
  94. package/pipeline/preferences-template.json +13 -5
  95. package/pipeline/rules/figma-pipeline.md +2 -2
  96. package/pipeline/schemas/agent-state.schema.json +1 -1
  97. package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
  98. package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
  99. package/pipeline/schemas/prefs.schema.json +277 -67
  100. package/pipeline/schemas/token-budget.json +2 -2
  101. package/pipeline/scripts/_stack-routing.mjs +79 -0
  102. package/pipeline/scripts/audit-log-rotate.sh +14 -1
  103. package/pipeline/scripts/build-skills-index.mjs +11 -0
  104. package/pipeline/scripts/build-stack-plugins.mjs +35 -60
  105. package/pipeline/scripts/check-derived-drift.mjs +65 -29
  106. package/pipeline/scripts/diff-explain.mjs +41 -3
  107. package/pipeline/scripts/diff-risk-score.mjs +72 -8
  108. package/pipeline/scripts/gc-worktrees.sh +4 -1
  109. package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
  110. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  111. package/pipeline/scripts/learning-curve.mjs +8 -2
  112. package/pipeline/scripts/match-skills.mjs +8 -2
  113. package/pipeline/scripts/migrate-prefs.mjs +28 -20
  114. package/pipeline/scripts/output-quality-check.sh +15 -4
  115. package/pipeline/scripts/phase-tracker.sh +33 -12
  116. package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
  117. package/pipeline/scripts/pre-commit-check.sh +69 -22
  118. package/pipeline/scripts/render-agent-log-cost.sh +8 -3
  119. package/pipeline/scripts/render-cost-summary.sh +42 -22
  120. package/pipeline/scripts/render-work-summary.sh +47 -13
  121. package/pipeline/scripts/review-scope.mjs +1 -1
  122. package/pipeline/scripts/run-aggregator.mjs +43 -14
  123. package/pipeline/scripts/scan-agent-config.sh +1 -1
  124. package/pipeline/scripts/skill-conformance.mjs +165 -30
  125. package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
  126. package/pipeline/scripts/smoke-schema-validation.sh +5 -1
  127. package/pipeline/scripts/test-gap-rules/android.json +25 -0
  128. package/pipeline/scripts/test-gap-rules/ios.json +34 -0
  129. package/pipeline/scripts/test-gap-rules/node.json +29 -0
  130. package/pipeline/scripts/test-gap-rules/python.json +25 -0
  131. package/pipeline/scripts/test-gap-scan.mjs +45 -6
  132. package/pipeline/scripts/uninstall.mjs +196 -14
  133. package/pipeline/scripts/update-issue-progress.sh +12 -16
  134. package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
  135. package/pipeline/scripts/validate-reviewer.mjs +9 -3
  136. package/pipeline/scripts/worktree-finalize.sh +23 -2
  137. package/pipeline/skills/.skill-manifest.json +156 -108
  138. package/pipeline/skills/.skills-index.json +459 -13
  139. package/pipeline/skills/shared/README.md +15 -11
  140. package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
  141. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
  142. package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
  143. package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
  144. package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
  145. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
  146. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
  147. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
  148. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
  149. package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
  150. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
  151. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
  152. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
  153. package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
  154. package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +10 -6
  155. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
  156. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
  157. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
  158. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
  159. package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
  160. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
  161. package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
  162. package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
  163. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
  164. package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
  165. package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
  166. package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
  167. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
  168. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +187 -0
  169. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
  170. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
  171. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +176 -0
  172. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
  173. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
  174. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
  175. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
  176. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
  177. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
  178. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
  179. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
  180. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
  181. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
  182. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
  183. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
  184. package/pipeline/skills/skills-index.md +9 -5
@@ -24,23 +24,32 @@ while [ $# -gt 0 ]; do
24
24
  esac
25
25
  done
26
26
 
27
- ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
28
- COST_TABLE="$ROOT/pipeline/scripts/cost-table.json"
29
- COST_LIB="$ROOT/pipeline/scripts/cost-lib.sh"
27
+ # cost-table.json/cost-lib.sh ship as siblings of this script both in the
28
+ # repo (pipeline/scripts/) and once installed (~/.claude/scripts/) - a
29
+ # repo-root-relative path (../../pipeline/scripts/...) only resolves in the
30
+ # repo checkout and silently misses in the installed layout. Match
31
+ # phase-tracker.sh's own resolution instead of re-deriving a repo root.
32
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
33
+ COST_TABLE="$SCRIPT_DIR/cost-table.json"
34
+ COST_LIB="$SCRIPT_DIR/cost-lib.sh"
30
35
  if [ -f "$COST_LIB" ]; then . "$COST_LIB"; else COST_JQ_DEFS=""; fi
31
36
 
32
37
  command -v jq >/dev/null 2>&1 || { echo "render-cost-summary: jq required" >&2; exit 2; }
33
38
 
34
- # Tracker JSON location convention: .worktrees/<TASK>/phase-tracker.json
35
- # Accept both the full key (e.g. "{JIRA_KEY}-1234") and the numeric-only
36
- # variant - the Jira-prefix is project-specific, so strip anything before
37
- # the first dash when trying the fallback.
39
+ # Tracker JSON candidates, worktree convention first, then the location
40
+ # phase-tracker.sh actually writes ($HOME/.claude/logs/multi-agent/<task>/
41
+ # tracker-state.json). The list must stay in lockstep with
42
+ # render-agent-log-cost.sh - before the logs/ candidates were added here,
43
+ # every standard run exited 2 and the channels Cost Summary silently never
44
+ # rendered, because only the worktree paths were tried.
38
45
  tracker_file=""
39
46
  task_id_bare="${TASK_ID##*-}"
40
47
  for candidate in \
41
48
  "$PWD/.worktrees/$TASK_ID/phase-tracker.json" \
42
49
  "$PWD/.worktrees/$task_id_bare/phase-tracker.json" \
43
- "$PWD/.worktrees/task-$task_id_bare/phase-tracker.json"
50
+ "$PWD/.worktrees/task-$task_id_bare/phase-tracker.json" \
51
+ "$HOME/.claude/logs/multi-agent/$TASK_ID/tracker-state.json" \
52
+ "$HOME/.claude/logs/multi-agent/$task_id_bare/tracker-state.json"
44
53
  do
45
54
  [ -f "$candidate" ] && { tracker_file="$candidate"; break; }
46
55
  done
@@ -52,17 +61,27 @@ fi
52
61
  # Build rows - prefer tracker JSON, fall back to spans aggregation
53
62
  rows_json="[]"
54
63
  if [ -n "$tracker_file" ]; then
55
- rows_json=$(jq '[
56
- (.phases // {} | to_entries[]) |
57
- select(.value.tokens_in // 0 > 0 or .value.tokens_out // 0 > 0) |
58
- {
59
- phase: .key,
60
- phase_name: (.value.name // .key),
61
- model: (.value.model // "opus"),
62
- tokens_in: (.value.tokens_in // 0),
63
- tokens_out: (.value.tokens_out // 0)
64
- }
65
- ]' "$tracker_file")
64
+ # Tracker JSON ships in two shapes (same contract as render-agent-log-cost.sh):
65
+ # array shape - phase-tracker.sh writes phases:[{id, name, model, tokens_in, ...}]
66
+ # object shape - older fixture / hand-built variant phases:{ "1": {...}, ... }
67
+ rows_json=$(jq '
68
+ (if (.phases | type) == "array" then
69
+ [.phases[] | {key: (.id | tostring), value: .}]
70
+ else
71
+ (.phases // {} | to_entries)
72
+ end)
73
+ | [ .[] |
74
+ select(.value.tokens_in // 0 > 0 or .value.tokens_out // 0 > 0) |
75
+ {
76
+ phase: .key,
77
+ phase_name: (.value.name // .key),
78
+ model: (.value.model // " - "),
79
+ tokens_in: (.value.tokens_in // 0),
80
+ tokens_out: (.value.tokens_out // 0),
81
+ tokens_cached: (.value.tokens_cached // 0)
82
+ }
83
+ ]
84
+ | sort_by(.phase | tonumber? // 999)' "$tracker_file")
66
85
  fi
67
86
 
68
87
  if [ "$rows_json" = "[]" ] && [ -n "$SPANS_FILE" ] && [ -f "$SPANS_FILE" ]; then
@@ -72,9 +91,10 @@ if [ "$rows_json" = "[]" ] && [ -n "$SPANS_FILE" ] && [ -f "$SPANS_FILE" ]; then
72
91
  map({
73
92
  phase: (.[0].attributes.phase_id // .[0].attributes.phase // "?"),
74
93
  phase_name: (.[0].attributes.phase_name // (.[0].attributes.phase_id // "?")),
75
- model: (.[0].attributes.model // "opus"),
94
+ model: (.[0].attributes.model // " - "),
76
95
  tokens_in: ([.[].attributes.tokens_in_delta // 0] | add),
77
- tokens_out: ([.[].attributes.tokens_out_delta // 0] | add)
96
+ tokens_out: ([.[].attributes.tokens_out_delta // 0] | add),
97
+ tokens_cached: ([.[].attributes.tokens_cached_delta // 0] | add)
78
98
  })
79
99
  ' "$SPANS_FILE")
80
100
  fi
@@ -85,7 +105,7 @@ joined=$(jq -n --argjson rows "$rows_json" --slurpfile prices "$COST_TABLE" "$CO
85
105
  . as $r |
86
106
  ($prices[0].prices[$r.model] // null) as $p |
87
107
  . + {
88
- usd: cost_usd_of($p; $r.tokens_in; $r.tokens_out; 0)
108
+ usd: cost_usd_of($p; $r.tokens_in; $r.tokens_out; ($r.tokens_cached // 0))
89
109
  }
90
110
  )
91
111
  ')
@@ -69,9 +69,11 @@ fi
69
69
 
70
70
  STATE_FILE=""
71
71
  TRACKER_FILE=""
72
+ TRIAGE_FILE=""
72
73
  if [ -n "$WORKTREE" ]; then
73
- [ -f "$WORKTREE/agent-state.json" ] && STATE_FILE="$WORKTREE/agent-state.json"
74
- [ -f "$WORKTREE/phase-tracker.json" ] && TRACKER_FILE="$WORKTREE/phase-tracker.json"
74
+ [ -f "$WORKTREE/agent-state.json" ] && STATE_FILE="$WORKTREE/agent-state.json"
75
+ [ -f "$WORKTREE/phase-tracker.json" ] && TRACKER_FILE="$WORKTREE/phase-tracker.json"
76
+ [ -f "$WORKTREE/triage-output.json" ] && TRIAGE_FILE="$WORKTREE/triage-output.json"
75
77
  fi
76
78
 
77
79
  # Fall back to the salvaged artefacts when the worktree is gone. Phase 6 removes it
@@ -88,8 +90,22 @@ if [ -z "$STATE_FILE" ] || [ -z "$TRACKER_FILE" ]; then
88
90
  "$HOME"/.claude/logs/multi-agent/*/"$task_bare"/artifacts
89
91
  do
90
92
  [ -d "$base" ] || continue
91
- [ -z "$STATE_FILE" ] && [ -f "$base/agent-state.json" ] && STATE_FILE="$base/agent-state.json"
92
- [ -z "$TRACKER_FILE" ] && [ -f "$base/phase-tracker.json" ] && TRACKER_FILE="$base/phase-tracker.json"
93
+ [ -z "$STATE_FILE" ] && [ -f "$base/agent-state.json" ] && STATE_FILE="$base/agent-state.json"
94
+ [ -z "$TRACKER_FILE" ] && [ -f "$base/phase-tracker.json" ] && TRACKER_FILE="$base/phase-tracker.json"
95
+ [ -z "$TRIAGE_FILE" ] && [ -f "$base/triage-output.json" ] && TRIAGE_FILE="$base/triage-output.json"
96
+ done
97
+ fi
98
+
99
+ # Last resort for the tracker: the file phase-tracker.sh actually writes on
100
+ # every run. The worktree/artifacts copies above exist only when something
101
+ # mirrored them there; the logs/ state file always exists for a tracked task.
102
+ if [ -z "$TRACKER_FILE" ]; then
103
+ task_bare="${TASK_ID##*-}"
104
+ for f in \
105
+ "$HOME/.claude/logs/multi-agent/$TASK_ID/tracker-state.json" \
106
+ "$HOME/.claude/logs/multi-agent/$task_bare/tracker-state.json"
107
+ do
108
+ [ -f "$f" ] && { TRACKER_FILE="$f"; break; }
93
109
  done
94
110
  fi
95
111
 
@@ -126,9 +142,15 @@ if [ -n "$STATE_FILE" ]; then
126
142
  state_jira=$(jq -r '.taskId // ""' "$STATE_FILE")
127
143
  state_branch=$(jq -r '.branch // (.projects // [] | .[0].branch // "")' "$STATE_FILE")
128
144
  state_base=$(jq -r '.baseBranch // (.projects // [] | .[0].baseBranch // "")' "$STATE_FILE")
129
- state_pr=$(jq -r '.prNumber // .prUrl // ""' "$STATE_FILE")
130
- state_tasks=$(jq -c '(.planTodos // []) + (.phases["2"].tasks // [])' "$STATE_FILE" 2>/dev/null || echo "[]")
131
- state_triage=$(jq -c '.reviewConsensus // .phases["4"].triage // {}' "$STATE_FILE" 2>/dev/null || echo "{}")
145
+ # agent-state.schema.json has no .prNumber/.prUrl/.planTodos/.reviewConsensus/
146
+ # .phases["2"].tasks/.phases["4"].triage - PR data lives at .pr.{number,url},
147
+ # the plan at .plan.todos[], and review triage in the separate
148
+ # triage-output.json (read via TRIAGE_FILE below, not out of agent-state).
149
+ state_pr=$(jq -r '.pr.number // .pr.url // ""' "$STATE_FILE")
150
+ state_tasks=$(jq -c '.plan.todos // []' "$STATE_FILE" 2>/dev/null || echo "[]")
151
+ fi
152
+ if [ -n "$TRIAGE_FILE" ]; then
153
+ state_triage=$(jq -c '.' "$TRIAGE_FILE" 2>/dev/null || echo "{}")
132
154
  fi
133
155
 
134
156
  # Flag overrides
@@ -141,13 +163,19 @@ fi
141
163
  phase_ticks=""
142
164
  if [ -n "$TRACKER_FILE" ]; then
143
165
  phase_ticks=$(jq -r '
144
- . as $root |
166
+ # Normalize both tracker shapes: phase-tracker.sh writes phases as an
167
+ # array of {id,...}; older fixtures keyed an object by phase id. The
168
+ # string-index below errors on an array and 2>/dev/null would swallow
169
+ # it, silently dropping the whole tick strip.
170
+ (if (.phases | type) == "array"
171
+ then (.phases | map({key: (.id | tostring), value: .}) | from_entries)
172
+ else (.phases // {}) end) as $ph |
145
173
  ["0 Init","1 Analysis","2 Planning","3 Dev","4 Review","5 Test","6 Commit","7 Report"] as $labels |
146
174
  [range(0;8) | tostring] as $ids |
147
175
  $ids
148
176
  | map(. as $i |
149
177
  ($labels[$i | tonumber]) as $label |
150
- (($root.phases // {})[$i] // {}) as $p |
178
+ ($ph[$i] // {}) as $p |
151
179
  (if $p.status == "completed" then "✅"
152
180
  elif $p.status == "in_progress" then "▶"
153
181
  elif $p.status == "failed" then "❌"
@@ -195,16 +223,19 @@ if [ -n "$DIFF_IN" ]; then
195
223
  fi
196
224
  fi
197
225
 
198
- # Task list with ✅ / ⏳ marks - derived from triage if available
226
+ # Task list with ✅ / ⏳ marks. plan-todos.schema.json's todo items carry
227
+ # `.task` (not `.title`/`.subject`) and a status enum of
228
+ # pending|in_progress|completed|skipped|failed (not "done"/"deferred") -
229
+ # the old check here never matched real data, so every todo rendered ⏳.
199
230
  tasks_block=""
200
231
  if [ "$(jq 'length' <<< "$state_tasks")" != "0" ]; then
201
232
  tasks_block=$(jq -r '
202
233
  .[] |
203
234
  "- " +
204
- (if (.status // "done") == "done" then "✅ " else "⏳ " end) +
235
+ (if (.status // "pending") == "completed" then "✅ " else "⏳ " end) +
205
236
  (.id // "") +
206
237
  (if (.id // "") != "" then " " else "" end) +
207
- (.title // .subject // "(untitled)")
238
+ (.task // "(untitled)")
208
239
  ' <<< "$state_tasks")
209
240
  fi
210
241
 
@@ -221,7 +252,10 @@ header="**Task**: \`$state_jira\`"
221
252
  [ -n "$state_branch" ] && header="$header | **Branch**: \`$state_branch\`"
222
253
  [ -n "$state_base" ] && header="$header | **Base**: \`$state_base\`"
223
254
  if [ -n "$state_pr" ] && [ "$state_pr" != "null" ]; then
224
- header="$header | **PR**: $state_pr"
255
+ case "$state_pr" in
256
+ *[!0-9]*) header="$header | **PR**: $state_pr" ;;
257
+ *) header="$header | **PR**: #$state_pr" ;;
258
+ esac
225
259
  fi
226
260
  printf '%s\n\n' "$header"
227
261
 
@@ -81,7 +81,7 @@ function main() {
81
81
  const blockers = [];
82
82
  for (const f of files) {
83
83
  for (const s of f.signals || []) {
84
- if (HIGH_STAKES.has(s.name)) blockers.push({ file: f.file, signal: s.name });
84
+ if (HIGH_STAKES.has(s.name)) blockers.push({ file: f.path, signal: s.name });
85
85
  }
86
86
  }
87
87
 
@@ -82,14 +82,29 @@ function die(msg) {
82
82
  process.exit(1);
83
83
  }
84
84
 
85
- function resolveTaskDir() {
86
- if (flags["task-dir"]) return flags["task-dir"];
87
- if (flags["task-id"] && flags.project) {
85
+ // phase-tracker.sh writes tracker-state.json (never phase-tracker.json) at
86
+ // $HOME/.claude/logs/multi-agent/<task-id>/ (no <project> segment) - the
87
+ // same drift render-cost-summary.sh/render-agent-log-cost.sh already guard
88
+ // against with a candidate list. Mirror that list here instead of assuming
89
+ // one fixed filename/path.
90
+ function resolveTrackerCandidates() {
91
+ const candidates = [];
92
+ if (flags["task-dir"]) {
93
+ candidates.push(join(flags["task-dir"], "tracker-state.json"));
94
+ }
95
+ if (flags["task-id"]) {
88
96
  const HOME = process.env.HOME || process.env.USERPROFILE;
89
97
  if (!HOME) die("neither HOME nor USERPROFILE is set - pass --task-dir");
90
- return join(HOME, ".claude", "logs", "multi-agent", flags.project, String(flags["task-id"]));
98
+ const taskId = String(flags["task-id"]);
99
+ if (flags.project) {
100
+ candidates.push(
101
+ join(HOME, ".claude", "logs", "multi-agent", flags.project, taskId, "tracker-state.json"),
102
+ );
103
+ }
104
+ candidates.push(join(HOME, ".claude", "logs", "multi-agent", taskId, "tracker-state.json"));
91
105
  }
92
- die("either --task-dir or --task-id with --project is required");
106
+ if (!candidates.length) die("either --task-dir or --task-id is required");
107
+ return candidates;
93
108
  }
94
109
 
95
110
  function loadJson(path) {
@@ -161,11 +176,14 @@ function tsCompare(a, b) {
161
176
  }
162
177
 
163
178
  // ─────────────────────────────────────────────────────────────────────────
164
- const taskDir = resolveTaskDir();
165
- if (!existsSync(taskDir)) die(`task dir not found: ${taskDir}`);
166
-
167
- const tracker = loadJson(join(taskDir, "phase-tracker.json"));
168
- if (!tracker) die(`phase-tracker.json not found in ${taskDir}`);
179
+ const trackerCandidates = resolveTrackerCandidates();
180
+ const trackerPath = trackerCandidates.find((p) => existsSync(p));
181
+ if (!trackerPath) {
182
+ die(`tracker state not found, tried:\n${trackerCandidates.map((p) => ` ${p}`).join("\n")}`);
183
+ }
184
+ const taskDir = dirname(trackerPath);
185
+ const tracker = loadJson(trackerPath);
186
+ if (!tracker) die(`failed to parse tracker state at ${trackerPath}`);
169
187
 
170
188
  const otelSpans = loadJsonl(join(taskDir, "otel-spans.jsonl"));
171
189
  const costTable = loadCostTable();
@@ -187,7 +205,18 @@ let totalOut = 0;
187
205
  let totalCost = 0;
188
206
  let unknownModelHits = 0;
189
207
 
190
- for (const phase of tracker.phases || []) {
208
+ // tracker.phases ships in two shapes (same contract render-cost-summary.sh
209
+ // normalizes): an array (phase-tracker.sh's real output) or an object keyed
210
+ // by phase id (older fixtures / hand-built variants). Normalize once so the
211
+ // rest of this file can treat it as a plain array.
212
+ const phaseList = Array.isArray(tracker.phases)
213
+ ? tracker.phases
214
+ : Object.entries(tracker.phases || {}).map(([key, value]) => ({
215
+ ...value,
216
+ id: value?.id ?? key,
217
+ }));
218
+
219
+ for (const phase of phaseList) {
191
220
  const tIn = phase.tokens_in || 0;
192
221
  const tOut = phase.tokens_out || 0;
193
222
  const model = phase.model || inferModelForPhase(phase.id);
@@ -279,9 +308,9 @@ const summary = {
279
308
  kind: "summary",
280
309
  ts: lastTs,
281
310
  task_id: taskId,
282
- phases_total: tracker.phases?.length ?? 0,
283
- phases_completed: (tracker.phases || []).filter((p) => p.status === "completed").length,
284
- phases_failed: (tracker.phases || []).filter((p) => p.status === "failed").length,
311
+ phases_total: phaseList.length,
312
+ phases_completed: phaseList.filter((p) => p.status === "completed").length,
313
+ phases_failed: phaseList.filter((p) => p.status === "failed").length,
285
314
  tokens_in: totalIn,
286
315
  tokens_out: totalOut,
287
316
  total_cost_usd: Number(totalCost.toFixed(6)),
@@ -31,7 +31,7 @@ add "$ROOT/install/templates/claude-hooks.json"
31
31
  add "$ROOT/install/templates/copilot-instructions.md"
32
32
  add "$ROOT/pipeline/preferences-template.json"
33
33
  for f in "$ROOT"/pipeline/agents/*.md; do add "$f"; done
34
- # (figma component skills + their scripts now live in the ai-*-engineering-toolkit
34
+ # (figma component skills + their scripts now live in the ai-*-toolkit
35
35
  # marketplace plugin, not in this repo, so there is nothing figma to scan here.)
36
36
  add "$ROOT/pipeline/scripts/agent-guard.sh"
37
37
  add "$ROOT/pipeline/scripts/pre-commit-check.sh"
@@ -43,6 +43,7 @@
43
43
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
44
44
  import { dirname, join, resolve, relative, basename, extname, sep } from "node:path";
45
45
  import { fileURLToPath } from "node:url";
46
+ import { homedir } from "node:os";
46
47
 
47
48
  const here = dirname(fileURLToPath(import.meta.url));
48
49
 
@@ -64,10 +65,74 @@ function skillsRootCandidates() {
64
65
  return [
65
66
  join(installRoot, "skills"), // ~/.claude/skills, ~/.copilot/skills
66
67
  join(installRoot, "multi-agent-refs", "skills"), // ~/.codex/multi-agent-refs/skills
68
+ ...pluginSkillRoots(), // marketplace stack plugins (Claude Code's only stack-skill source)
67
69
  join(repoRoot, "pipeline", "skills"), // checkout
68
70
  ];
69
71
  }
70
72
 
73
+ // ---------------------------------------------------------------------------
74
+ // Marketplace plugin roots.
75
+ //
76
+ // On Claude Code the stack skills no longer land in ~/.claude/skills - they are
77
+ // loaded from the multi-agent-plugins marketplace. The registries this gate
78
+ // depends on (`ios-coding-standard`, `ios-module-structure`) live in the iOS
79
+ // plugin's knowledge/ tree, so without these roots the registry set silently
80
+ // collapses to the two local compliance catalogs and Phase 4 reads green over
81
+ // criteria that were never loaded. Local checkout is preferred over the plugin
82
+ // cache for the same reason install/_plugin-skills.mjs prefers it: the checkout
83
+ // is what /multi-agent:sync rebuilds, the cache only advances on marketplace
84
+ // refresh. Cache dirs are version-named; newest wins.
85
+ // ---------------------------------------------------------------------------
86
+ const PLUGIN_MARKETPLACE = "multi-agent-plugins";
87
+ const STACK_PLUGIN_NAMES = [
88
+ "ai-ios-toolkit",
89
+ "ai-android-toolkit",
90
+ "ai-frontend-toolkit",
91
+ "ai-backend-toolkit",
92
+ "ai-common-toolkit",
93
+ ];
94
+
95
+ function semverCompare(a, b) {
96
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
97
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
98
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
99
+ const d = (pa[i] || 0) - (pb[i] || 0);
100
+ if (d !== 0) return d;
101
+ }
102
+ return a.localeCompare(b);
103
+ }
104
+
105
+ function pluginSkillRoots() {
106
+ const home = homedir();
107
+ const roots = [];
108
+ for (const name of STACK_PLUGIN_NAMES) {
109
+ const checkout = join(home, PLUGIN_MARKETPLACE, "plugins", name, "skills");
110
+ if (existsSync(checkout)) {
111
+ roots.push(checkout);
112
+ continue;
113
+ }
114
+ const cacheRoot = join(home, ".claude", "plugins", "cache", PLUGIN_MARKETPLACE, name);
115
+ if (!existsSync(cacheRoot)) continue;
116
+ const versions = readdirSafe(cacheRoot)
117
+ .filter((v) => {
118
+ try {
119
+ return statSync(join(cacheRoot, v)).isDirectory();
120
+ } catch {
121
+ return false;
122
+ }
123
+ })
124
+ .sort(semverCompare);
125
+ for (const v of versions.reverse()) {
126
+ const p = join(cacheRoot, v, "skills");
127
+ if (existsSync(p)) {
128
+ roots.push(p);
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ return roots;
134
+ }
135
+
71
136
  /**
72
137
  * Resolve the roots to search, and report every candidate that was PROBED - not
73
138
  * only the ones that existed.
@@ -496,28 +561,64 @@ function auditExceptions(addedByFile, knownIds, today, markerToken) {
496
561
  const reason = rest.replace(/(\d{4}-\d{2}-\d{2})/, "").trim();
497
562
 
498
563
  if (!id) {
499
- findings.push(f(file, line, "blocking", null, "Exception marker names no rule ID.",
500
- "Name the rule the exception applies to: standard:exception(<RULE-ID>) <reason> <expiry>."));
564
+ findings.push(
565
+ f(
566
+ file,
567
+ line,
568
+ "blocking",
569
+ null,
570
+ "Exception marker names no rule ID.",
571
+ "Name the rule the exception applies to: standard:exception(<RULE-ID>) <reason> <expiry>.",
572
+ ),
573
+ );
501
574
  continue;
502
575
  }
503
576
  if (knownIds.size > 0 && !knownIds.has(id)) {
504
- findings.push(f(file, line, "blocking", id,
505
- `Exception cites '${id}', which is not in any registry in scope for this diff.`,
506
- "Use a real rule ID, or drop the marker. An exception against an unknown rule suppresses nothing and hides intent."));
577
+ findings.push(
578
+ f(
579
+ file,
580
+ line,
581
+ "blocking",
582
+ id,
583
+ `Exception cites '${id}', which is not in any registry in scope for this diff.`,
584
+ "Use a real rule ID, or drop the marker. An exception against an unknown rule suppresses nothing and hides intent.",
585
+ ),
586
+ );
507
587
  }
508
588
  if (!dateMatch) {
509
- findings.push(f(file, line, "important", id,
510
- `Exception for '${id}' has no expiry date.`,
511
- "Add an expiry: standard:exception(<RULE-ID>) <reason> YYYY-MM-DD. An exception with no expiry is a permanent silent carve-out."));
589
+ findings.push(
590
+ f(
591
+ file,
592
+ line,
593
+ "important",
594
+ id,
595
+ `Exception for '${id}' has no expiry date.`,
596
+ "Add an expiry: standard:exception(<RULE-ID>) <reason> YYYY-MM-DD. An exception with no expiry is a permanent silent carve-out.",
597
+ ),
598
+ );
512
599
  } else if (dateMatch[1] < today) {
513
- findings.push(f(file, line, "blocking", id,
514
- `Exception for '${id}' expired on ${dateMatch[1]}.`,
515
- "Fix the underlying violation, or renew the exception with a new expiry and a current reason."));
600
+ findings.push(
601
+ f(
602
+ file,
603
+ line,
604
+ "blocking",
605
+ id,
606
+ `Exception for '${id}' expired on ${dateMatch[1]}.`,
607
+ "Fix the underlying violation, or renew the exception with a new expiry and a current reason.",
608
+ ),
609
+ );
516
610
  }
517
611
  if (!reason) {
518
- findings.push(f(file, line, "important", id,
519
- `Exception for '${id}' gives no reason.`,
520
- "State why the rule does not apply here. An unexplained exception cannot be reviewed, only trusted."));
612
+ findings.push(
613
+ f(
614
+ file,
615
+ line,
616
+ "important",
617
+ id,
618
+ `Exception for '${id}' gives no reason.`,
619
+ "State why the rule does not apply here. An unexplained exception cannot be reviewed, only trusted.",
620
+ ),
621
+ );
521
622
  }
522
623
  }
523
624
  }
@@ -614,7 +715,8 @@ function parseArgs(argv) {
614
715
  else if (a === "--repo") o.repo = argv[++i];
615
716
  else if (a === "--out") o.out = argv[++i];
616
717
  else if (a === "--skills-root") o.skillsRoot = argv[++i];
617
- else if (a === "--today") o.today = argv[++i]; // tests pin the clock
718
+ else if (a === "--today")
719
+ o.today = argv[++i]; // tests pin the clock
618
720
  else {
619
721
  console.error(`unknown argument: ${a}`);
620
722
  process.exit(1);
@@ -692,7 +794,11 @@ const unparseable = [...escapedDeclarations];
692
794
 
693
795
  for (const d of discovered) {
694
796
  if (!existsSync(d.rulesPath)) {
695
- unparseable.push({ name: d.name, path: d.rulesPath, reason: "declared standards-registry file does not exist" });
797
+ unparseable.push({
798
+ name: d.name,
799
+ path: d.rulesPath,
800
+ reason: "declared standards-registry file does not exist",
801
+ });
696
802
  continue;
697
803
  }
698
804
  let loaded;
@@ -703,7 +809,11 @@ for (const d of discovered) {
703
809
  continue;
704
810
  }
705
811
  if (loaded.rules.length === 0) {
706
- unparseable.push({ name: d.name, path: d.rulesPath, reason: "no rules extracted - rules: block missing or unexpected indentation" });
812
+ unparseable.push({
813
+ name: d.name,
814
+ path: d.rulesPath,
815
+ reason: "no rules extracted - rules: block missing or unexpected indentation",
816
+ });
707
817
  continue;
708
818
  }
709
819
  registries.push({ ...d, ...loaded });
@@ -716,9 +826,10 @@ const perRegistry = [];
716
826
  for (const reg of registries) {
717
827
  const scope = reg.meta.scope;
718
828
  const regLanguages = scope?.languages ?? [];
719
- const langHit = regLanguages.length === 0
720
- ? Object.keys(languages)
721
- : regLanguages.filter((l) => Object.prototype.hasOwnProperty.call(languages, l));
829
+ const langHit =
830
+ regLanguages.length === 0
831
+ ? Object.keys(languages)
832
+ : regLanguages.filter((l) => Object.prototype.hasOwnProperty.call(languages, l));
722
833
 
723
834
  // Files this registry may speak about at all.
724
835
  const inScopeFiles = changedFiles.filter((file) => {
@@ -747,7 +858,9 @@ for (const reg of registries) {
747
858
  ? inScopeFiles.filter((f2) => matchesAnyGlob(f2, rule.scope.paths))
748
859
  : inScopeFiles;
749
860
  if (files.length === 0) {
750
- const why = rule.scopeReason ? `out-of-scope: ${rule.scopeReason}` : "out-of-scope: no changed file matches the rule's paths";
861
+ const why = rule.scopeReason
862
+ ? `out-of-scope: ${rule.scopeReason}`
863
+ : "out-of-scope: no changed file matches the rule's paths";
751
864
  droppedReasons[why] = (droppedReasons[why] ?? 0) + 1;
752
865
  continue;
753
866
  }
@@ -776,9 +889,16 @@ for (const reg of registries) {
776
889
  languagesMatched: langHit,
777
890
  inScopeFileCount: inScopeFiles.length,
778
891
  notCovered: reg.meta.notCovered,
779
- loadedAs: unscoped ? "reference-only" : inScopeFiles.length ? "rule-checklist" : "not-applicable",
892
+ loadedAs: unscoped
893
+ ? "reference-only"
894
+ : inScopeFiles.length
895
+ ? "rule-checklist"
896
+ : "not-applicable",
780
897
  ...(unscoped
781
- ? { referenceOnlyReason: "registry declares no scope; applying its rules to unknown languages would manufacture findings" }
898
+ ? {
899
+ referenceOnlyReason:
900
+ "registry declares no scope; applying its rules to unknown languages would manufacture findings",
901
+ }
782
902
  : {}),
783
903
  toolchains: detectToolchains(repoRoot, reg.rules),
784
904
  exceptionMarker: reg.meta.exceptionMarker,
@@ -794,7 +914,9 @@ for (const [lang, files] of Object.entries(languages)) {
794
914
  const covered = files.filter((f2) => coveredFiles.has(f2));
795
915
  if (covered.length === 0) {
796
916
  const claims = perRegistry
797
- .flatMap((r) => Object.entries(r.notCovered ?? {}).map(([k, v]) => ({ registry: r.name, key: k, note: v })))
917
+ .flatMap((r) =>
918
+ Object.entries(r.notCovered ?? {}).map(([k, v]) => ({ registry: r.name, key: k, note: v })),
919
+ )
798
920
  .filter((c) => c.key === lang || lang.startsWith(c.key));
799
921
  gaps.push({
800
922
  language: lang,
@@ -803,7 +925,8 @@ for (const [lang, files] of Object.entries(languages)) {
803
925
  reason: claims.length
804
926
  ? `no registry in scope covers ${lang}: ${claims.map((c) => `${c.registry} declares '${c.key}' not covered`).join("; ")}`
805
927
  : `no registry in scope declares ${lang}`,
806
- consequence: "these files are reviewed on general criteria only - no rule IDs apply to them",
928
+ consequence:
929
+ "these files are reviewed on general criteria only - no rule IDs apply to them",
807
930
  });
808
931
  }
809
932
  }
@@ -815,7 +938,7 @@ const { guides, dropped: guidesDropped } = collectModuleGuides(repoRoot, changed
815
938
  // knownIds spans EVERY discovered registry, not just the in-scope ones. Narrowing
816
939
  // it to the selected set would report a valid rule as unknown whenever the
817
940
  // exception sits in a file that rule does not cover, which is a false accusation
818
- // in the one place the author is trying to be explicit. The narrower question -
941
+ // in the one place the author is trying to be explicit. The narrower question -
819
942
  // "is this exception citing a rule that applies here?" - is answered separately
820
943
  // below, so a mis-scoped exception is still reported, just not as a typo.
821
944
  const knownIds = new Set(registries.flatMap((r) => r.rules.map((x) => x.id)));
@@ -872,7 +995,9 @@ for (const reg of perRegistry) {
872
995
  const resolvedNames = new Set(perRegistry.map((r) => r.name));
873
996
  const declaredNames = ledger.entries.map((e) => e?.skill).filter(Boolean);
874
997
  const declaredNotResolved = [...new Set(declaredNames)].filter((n) => !resolvedNames.has(n));
875
- const ledgerMissingTargets = ledger.entries.filter((e) => !Array.isArray(e?.targetFiles) || e.targetFiles.length === 0).length;
998
+ const ledgerMissingTargets = ledger.entries.filter(
999
+ (e) => !Array.isArray(e?.targetFiles) || e.targetFiles.length === 0,
1000
+ ).length;
876
1001
 
877
1002
  // Entries a stack toolkit's own index chose (Phase 3 step 9 stamps `routedBy`).
878
1003
  // Surfaced separately because "the project's toolkit says this skill governs this
@@ -940,10 +1065,20 @@ if (opts.json) {
940
1065
  process.stdout.write(JSON.stringify(manifest, null, 2) + "\n");
941
1066
  } else {
942
1067
  console.log(`criteria-manifest: ${outPath}`);
943
- console.log(` languages: ${Object.entries(manifest.languages).map(([k, v]) => `${k}:${v}`).join(" ") || "(none)"}`);
944
- console.log(` registries: ${perRegistry.length} discovered, ${perRegistry.filter((r) => r.loadedAs === "rule-checklist").length} as rule-checklist`);
1068
+ console.log(
1069
+ ` languages: ${
1070
+ Object.entries(manifest.languages)
1071
+ .map(([k, v]) => `${k}:${v}`)
1072
+ .join(" ") || "(none)"
1073
+ }`,
1074
+ );
1075
+ console.log(
1076
+ ` registries: ${perRegistry.length} discovered, ${perRegistry.filter((r) => r.loadedAs === "rule-checklist").length} as rule-checklist`,
1077
+ );
945
1078
  console.log(` selected rules: ${selected.length}`);
946
- console.log(` coverage: ${coveredFiles.size}/${changedFiles.length} changed files carry rule criteria`);
1079
+ console.log(
1080
+ ` coverage: ${coveredFiles.size}/${changedFiles.length} changed files carry rule criteria`,
1081
+ );
947
1082
  for (const g of gaps) console.log(` GAP ${g.language} (${g.fileCount} file(s)): ${g.reason}`);
948
1083
  console.log(` ledger: ${manifest.ledger.source} (self-report: ${ledger.status})`);
949
1084
  console.log(` findings: ${findings.length}`);
@@ -152,7 +152,7 @@ fi
152
152
  # ──────────────────────────────────────────────────────────────────────────
153
153
  echo ""
154
154
  # Figma/component skill trees no longer ship in the pipeline (v11.2.0+): they
155
- # live in the marketplace component plugins (ai-ios-engineering-toolkit /
155
+ # live in the marketplace component plugins (ai-ios-toolkit /
156
156
  # multi-agent-plugins), which each CLI loads through its own plugin channel.
157
157
  # The pipeline therefore has no figma tree to deploy or parity-check here.
158
158
  echo "→ 7. Agent personas deployed to both CLIs"
@@ -91,10 +91,14 @@ echo "→ 4. prefs.schema.json - keychainMapping covers every resolved service
91
91
  # A floor, not a pin: new credentials get added over time, and pinning the count
92
92
  # made every addition a false failure. What matters is that the keys shipped
93
93
  # flows resolve are all declared.
94
+ # process.stdout.write of a String, not console.log of a Number: console.log
95
+ # inspects non-string arguments, so with FORCE_COLOR set in the environment the
96
+ # count comes back wrapped in ANSI codes and the -ge comparison below silently
97
+ # fails on a value that is actually in range.
94
98
  KM_COUNT=$(node -e "
95
99
  const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
96
100
  const km = s.properties.global.properties.keychainMapping.properties;
97
- console.log(Object.keys(km).length);
101
+ process.stdout.write(String(Object.keys(km).length));
98
102
  " 2>/dev/null)
99
103
  if [ "${KM_COUNT:-0}" -ge 12 ] 2>/dev/null; then
100
104
  pass "keychainMapping declares $KM_COUNT services (≥12 expected)"