@mmerterden/multi-agent-pipeline 15.0.0 → 15.2.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 (39) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +9 -9
  3. package/README.tr.md +9 -9
  4. package/SECURITY.md +43 -0
  5. package/docs/adr/0007-multi-tool-adapter-framework.md +1 -1
  6. package/docs/architecture.md +9 -9
  7. package/docs/ecosystem.md +10 -10
  8. package/index.js +4 -1
  9. package/install/_common.mjs +44 -2
  10. package/install/_platform-filter.mjs +6 -131
  11. package/install/_plugin-skills.mjs +17 -28
  12. package/install/claude.mjs +111 -6
  13. package/install/codex.mjs +0 -3
  14. package/install/copilot.mjs +36 -1
  15. package/install/index.mjs +3 -1
  16. package/package.json +2 -2
  17. package/pipeline/commands/multi-agent/refactor/SKILL.md +36 -1
  18. package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
  19. package/pipeline/commands/multi-agent/stack/SKILL.md +18 -8
  20. package/pipeline/commands/multi-agent/update/SKILL.md +31 -4
  21. package/pipeline/lib/parse-complaints.sh +12 -2
  22. package/pipeline/multi-agent-refs/complaint-analysis-template.md +1 -1
  23. package/pipeline/multi-agent-refs/phases/operations.md +7 -1
  24. package/pipeline/multi-agent-refs/phases/phase-7-report.md +6 -0
  25. package/pipeline/multi-agent-refs/tracker-contract.md +2 -1
  26. package/pipeline/preferences-template.json +6 -0
  27. package/pipeline/schemas/prefs.schema.json +27 -0
  28. package/pipeline/scripts/README.md +4 -3
  29. package/pipeline/scripts/check-derived-drift.mjs +5 -2
  30. package/pipeline/scripts/match-skills.mjs +4 -0
  31. package/pipeline/scripts/migrate-prefs.mjs +5 -1
  32. package/pipeline/scripts/phase-tracker.sh +19 -0
  33. package/pipeline/scripts/uninstall.mjs +3 -1
  34. package/pipeline/scripts/usage-report.mjs +457 -0
  35. package/pipeline/scripts/validate-complaint-doc.mjs +32 -11
  36. package/pipeline/skills/.skill-manifest.json +4 -4
  37. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +153 -90
  38. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +18 -8
  39. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
@@ -71,6 +71,33 @@ Update the pipeline in one command. Existing preferences are preserved; only ski
71
71
  fi
72
72
  ```
73
73
 
74
+ 5b. **Auto-enable usage logging when a token is already onboarded.** The private
75
+ usage dashboard is opt-in and off by default. This step flips it ON only when a
76
+ token can be resolved - it NEVER fabricates a secret or ships one, so a machine
77
+ that was never given the token stays silent. Resolution order: env
78
+ `MULTI_AGENT_USAGE_TOKEN`, then `usageLog.token`, then the Keychain item named by
79
+ `keychainMapping.usage_ingest`. Endpoint is left to the emitter's default.
80
+ ```bash
81
+ PREFS="$HOME/.claude/multi-agent-preferences.json"
82
+ if [ -f "$PREFS" ] && command -v jq >/dev/null 2>&1; then
83
+ ENABLED=$(jq -r '.global.usageLog.enabled // false' "$PREFS" 2>/dev/null)
84
+ if [ "$ENABLED" != "true" ]; then
85
+ TOK="${MULTI_AGENT_USAGE_TOKEN:-}"
86
+ [ -z "$TOK" ] && TOK=$(jq -r '.global.usageLog.token // empty' "$PREFS" 2>/dev/null)
87
+ if [ -z "$TOK" ]; then
88
+ KNAME=$(jq -r '.global.keychainMapping.usage_ingest // empty' "$PREFS" 2>/dev/null)
89
+ [ -n "$KNAME" ] && TOK=$(bash "$HOME/.claude/lib/credential-store.sh" get "$KNAME" 2>/dev/null)
90
+ fi
91
+ if [ -n "$TOK" ]; then
92
+ node -e 'const fs=require("fs"),p=process.argv[1];const j=JSON.parse(fs.readFileSync(p,"utf8"));j.global=j.global||{};j.global.usageLog=j.global.usageLog||{};j.global.usageLog.enabled=true;fs.writeFileSync(p,JSON.stringify(j,null,2)+"\n");' "$PREFS"
93
+ echo " -> usage logging activated (ingest token found)"
94
+ else
95
+ echo " -> usage logging left off (no ingest token onboarded)"
96
+ fi
97
+ fi
98
+ fi
99
+ ```
100
+
74
101
  6. **Show the new version**:
75
102
  ```bash
76
103
  NEW_VERSION=$(node -p "require('$PIPE_DIR/package.json').version" 2>/dev/null || echo "unknown")
@@ -99,12 +126,12 @@ Update the pipeline in one command. Existing preferences are preserved; only ski
99
126
  ## Output
100
127
 
101
128
  ```
102
- Current: v3.6.0
129
+ Current: v14.2.2
103
130
  -> git pull origin main
104
- -> node install.js --all (50 commands, 80 scripts, 203 skills)
105
- -> migrate-prefs.mjs (0 changes - already v2.1.0)
131
+ -> node install.js --all (51 commands, 228 scripts, 205 skills)
132
+ -> migrate-prefs.mjs (0 changes - already v2.6.0)
106
133
 
107
- ✓ Updated: v3.6.0 → v3.7.0
134
+ ✓ Updated: v14.2.2 → v15.0.0
108
135
 
109
136
  Changes:
110
137
  e3f883e fix(dev): Phase 6 must show local test prompt
@@ -64,9 +64,13 @@ REDACT="true"
64
64
 
65
65
  while [ $# -gt 0 ]; do
66
66
  case "$1" in
67
- --file) FILE="$2"; shift 2 ;;
67
+ --file)
68
+ [ $# -ge 2 ] || { echo "ERR: --file needs a value" >&2; exit 4; }
69
+ FILE="$2"; shift 2 ;;
68
70
  --stdin) USE_STDIN="true"; shift ;;
69
- --format) FORMAT="$2"; shift 2 ;;
71
+ --format)
72
+ [ $# -ge 2 ] || { echo "ERR: --format needs a value" >&2; exit 4; }
73
+ FORMAT="$2"; shift 2 ;;
70
74
  --no-redact) REDACT="false"; shift ;;
71
75
  -h|--help)
72
76
  echo "usage: $0 --file <path> [--format csv|xlsx|txt|json|auto] [--no-redact] | --stdin [--format text]" >&2
@@ -150,6 +154,12 @@ REDACTIONS = [
150
154
  (re.compile(r"\b\d{11}\b"), "[REDACTED:national-id]"),
151
155
  (re.compile(r"(?<![\w.-])\+?\d(?:[ ()-]?\d){8,14}\b"), "[REDACTED:phone]"),
152
156
  (re.compile(r"\b(?=[A-Z0-9]{6}\b)(?=[A-Z0-9]*[A-Z])(?=[A-Z0-9]*\d)[A-Z0-9]{6}\b"), "[REDACTED:pnr]"),
157
+ # All-letter PNRs exist too, but a bare 6-letter uppercase word is far too
158
+ # common to redact blind - only take one that follows a PNR-ish keyword.
159
+ (
160
+ re.compile(r"\b(?P<kw>pnr|rezervasyon kodu|booking (?:ref|reference|code))(?P<sep>[^A-Za-z0-9\n]{0,3})[A-Z]{6}\b", re.IGNORECASE),
161
+ lambda m: m.group("kw") + m.group("sep") + "[REDACTED:pnr]",
162
+ ),
153
163
  ]
154
164
 
155
165
  def redact(text):
@@ -44,7 +44,7 @@ Then the coverage table - every selected repo appears, including the ones that c
44
44
 
45
45
  ## 2. Triage Tablosu / Triage Table
46
46
 
47
- One row per complaint. Verdict tokens are the English enum: `client:ios`, `client:android`, `client:web`, `bff:mobile-bff`, `bff:web-bff`, `core`, `insufficient-evidence`. The validator requires every row carrying a `C-NN` id to contain one of these tokens.
47
+ One row per complaint. Verdict tokens are the English enum: `client:ios`, `client:android`, `client:web`, `bff:mobile-bff`, `bff:web-bff`, `core`, `insufficient-evidence`. The validator requires every row in this section whose FIRST cell is a `C-NN` id to carry one of these tokens in a cell of its own (cell-exact; the Section 1 coverage table citing ids in its last column is exempt by scope).
48
48
 
49
49
  ```
50
50
  | Id | Özet (redakte) | trx/conv | Verdict | Confidence | Routing |
@@ -95,7 +95,13 @@ halt per the halt-visibility rule), `3` I/O error.
95
95
  Reads need no wrapper; the rename makes any read see either the old or the new
96
96
  document, never a truncated one.
97
97
 
98
- **Halt visibility (required, autopilot included).** A halt is never silent. Whenever a phase halts on a hard error (validator failed twice, no subagent returned, dispatch error past fallback, lock irrecoverable), in addition to the `agent-log.md` line: (a) write `state.status = "paused"` and `state.haltReason = "<phase>:<cause>"`; (b) record the cause on the tracker via `phase-tracker.sh meta <phase> halt "<cause>"` and `phase-tracker.sh update <phase> failed`; (c) emit one `>&2` alert line `HALT phase <N>: <cause> - resume with /multi-agent:resume #<id>`. Autopilot suppresses *confirmations*, not *halts* - the user must always be able to see why an unattended run stopped without reading the log.
98
+ **Halt visibility (required, autopilot included).** A halt is never silent. Whenever a phase halts on a hard error (validator failed twice, no subagent returned, dispatch error past fallback, lock irrecoverable), in addition to the `agent-log.md` line: (a) write `state.status = "paused"` and `state.haltReason = "<phase>:<cause>"`; (b) record the cause on the tracker via `phase-tracker.sh meta <phase> halt "<cause>"` and `phase-tracker.sh update <phase> failed`; (c) emit one `>&2` alert line `HALT phase <N>: <cause> - resume with /multi-agent:resume #<id>`; (d) if `prefs.global.usageLog.enabled` is true, emit the end-of-run usage ping so a run that never reaches Phase 7 is still recorded with the phase it stopped at (`state.currentPhase` + `haltReason`) - the emitter no-ops when logging is off or unconfigured:
99
+
100
+ ```bash
101
+ node $HOME/.claude/scripts/usage-report.mjs --state "$STATE_FILE" >/dev/null 2>&1 || true
102
+ ```
103
+
104
+ Autopilot suppresses *confirmations*, not *halts* - the user must always be able to see why an unattended run stopped without reading the log. The dashboard upserts by run id, so this halt event and a later Phase 7 event (after resume) collapse into one record.
99
105
 
100
106
  ### Pipeline Best Practices
101
107
 
@@ -196,6 +196,12 @@ $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 7 task.completed \
196
196
  duration_ms=$TOTAL_DURATION
197
197
  ```
198
198
 
199
+ **Usage ping (optional, private dashboard).** When `prefs.global.usageLog.enabled` is true, emit one end-of-run activity event to the configured private dashboard. One POST per run (never per phase), fire-and-forget, activity metadata only - who, command, mode, input type, repo, phase reached, outcome, halt cause, review-iteration count, duration, token spend, cost, version. No prompts, code, diffs, or absolute paths. The script no-ops when `usageLog.enabled` is not true or no ingest token resolves, so the call is unconditional and never blocks the run.
200
+
201
+ ```bash
202
+ node $HOME/.claude/scripts/usage-report.mjs --state "$STATE_FILE" >/dev/null 2>&1 || true
203
+ ```
204
+
199
205
  **Aggregate before reporting**: run `aggregate-metrics.mjs --since=$(date -u -v-30d +%Y-%m-%d)` and embed output in agent-log.md. Use `--json` for Jira. Aggregator handles missing files gracefully.
200
206
 
201
207
  **Per-run outcome metrics (evidence corpus):** also emit `run-metrics.mjs --state <agent-state.json>` and append/persist its JSON. It records the numbers that actually answer "did this run go well" - review iterations (rework loops), first-pass-clean, reviewer signal-to-noise (accepted / raw findings), consensus verdict, build outcome. Accumulating these across real runs is the real-world validation that golden tasks + benchmarks only approximate; keep the corpus so the pipeline's quality can be measured, not asserted.
@@ -135,7 +135,8 @@ Mode-specific phase sets:
135
135
 
136
136
  | Mode | TaskCreate set (in order) |
137
137
  |---|---|
138
- | Full pipeline (`/multi-agent`, `:autopilot`, `:local`, `:local-autopilot`) | 0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 (all 8) |
138
+ | Full interactive (`/multi-agent`) | 0 → 1 → 2 → 3 → 4 → 5 → 6 → 7 (all 8) |
139
+ | `:autopilot`, `:local`, `:local-autopilot` | 0 → 1 → 2 → 3 → 4 → 6 → 7 (7 phases - the interactive Phase 5 test gate is dropped in every autopilot/local variant) |
139
140
  | `:dev` | 0 → 3 → 4 → 5 → 6 → 7 (6 phases - 1/2 omitted entirely) |
140
141
  | `:dev-autopilot`, `:dev-local`, `:dev-local-autopilot` | 0 → 3 → 4 → 6 → 7 (5 phases - 1/2/5 omitted entirely; the autopilot/local variants drop the interactive Phase 5 test gate) |
141
142
 
@@ -12,6 +12,7 @@
12
12
  "figma_mcp": null,
13
13
  "fortify": null,
14
14
  "graylog": null,
15
+ "usage_ingest": null,
15
16
  "firebase": null,
16
17
  "jenkins": null
17
18
  },
@@ -74,6 +75,11 @@
74
75
  "onExceed": "warn",
75
76
  "pricingModel": "opus"
76
77
  },
78
+ "usageLog": {
79
+ "enabled": false,
80
+ "endpoint": "https://mmerterden.vercel.app/api/usage/ingest",
81
+ "token": ""
82
+ },
77
83
  "derivedSkillSources": [],
78
84
  "devToolkit": {
79
85
  "enabled": true,
@@ -188,6 +188,13 @@
188
188
  "null"
189
189
  ]
190
190
  },
191
+ "usage_ingest": {
192
+ "type": [
193
+ "string",
194
+ "null"
195
+ ],
196
+ "description": "Keychain item holding the shared ingest token for the private usage dashboard. When set, usage-report.mjs reads the token from here (never from a synced file), and /multi-agent:update auto-enables usageLog once the token is onboarded. Absent = usage logging stays off."
197
+ },
191
198
  "figma_pat": {
192
199
  "type": [
193
200
  "string",
@@ -1077,6 +1084,26 @@
1077
1084
  }
1078
1085
  }
1079
1086
  },
1087
+ "usageLog": {
1088
+ "type": "object",
1089
+ "additionalProperties": false,
1090
+ "description": "Optional end-of-run usage ping to a private dashboard. When enabled, Phase 7 emits ONE activity event per run (never per phase) via pipeline/scripts/usage-report.mjs: who, command, mode, input type, repo, phase reached, outcome, halt cause, review-iteration count, duration, token spend, cost, version. Never prompts, code, diffs, or absolute paths. Off by default; the emitter no-ops unless enabled is true AND a token resolves (from token below or env MULTI_AGENT_USAGE_TOKEN), so it is inert for anyone who has not configured it.",
1091
+ "properties": {
1092
+ "enabled": {
1093
+ "type": "boolean",
1094
+ "default": false,
1095
+ "description": "Master switch. When false the emitter exits silently and nothing leaves the machine."
1096
+ },
1097
+ "endpoint": {
1098
+ "type": "string",
1099
+ "description": "Ingest URL that receives the activity event. The token is sent in the X-Usage-Token header."
1100
+ },
1101
+ "token": {
1102
+ "type": "string",
1103
+ "description": "Shared ingest token the receiving endpoint validates. May be left empty here and supplied via env MULTI_AGENT_USAGE_TOKEN instead. Local prefs only, never synced."
1104
+ }
1105
+ }
1106
+ },
1080
1107
  "reviewWatch": {
1081
1108
  "type": "object",
1082
1109
  "additionalProperties": false,
@@ -82,13 +82,14 @@ Shell scripts invoked during pipeline execution.
82
82
  - `github-ssh-setup.sh` - first-run SSH key setup helper
83
83
  - `sync-parity-check.sh` - manual parity verification helper
84
84
 
85
- ## Node.js helpers (45 `.mjs` files)
85
+ ## Node.js helpers (54 `.mjs` files)
86
86
 
87
87
  - `aggregate-metrics.mjs` - collects telemetry aggregates
88
88
  - `learning-curve.mjs` - time-bucketed trend over `metrics.jsonl` (first-pass clean rate, review cycles, rework/task, tokens/task, cache ratio); `--bucket`, `--since`, `--json`, `--markdown`
89
- - `eval-triage.mjs` - runs the 10 triage eval fixtures
89
+ - `eval-triage.mjs` - runs the 11 triage eval fixtures
90
+ - `eval-mine-corpus.mjs` - manual maintainer CLI: mines `~/.claude/memory/multi-agent/<repo-slug>/triage-corpus.jsonl` from real runs into new triage eval fixtures (dev-only, excluded from the npm package; not part of `npm test`)
90
91
  - `gen-skills-index.mjs` - regenerates `pipeline/skills/shared/README.md` skill catalog
91
- - `migrate-prefs.mjs` - preference migration driver (v2.0.0 through v2.4.0)
92
+ - `migrate-prefs.mjs` - preference migration driver (v2.0.0 through v2.6.0)
92
93
  - `migrate-state.mjs` - generic migration runner (reads `schemas/migrations/`)
93
94
  - `token-budget-report.mjs` - per-phase token usage report
94
95
  - `validate-analysis.mjs`, `validate-planning.mjs`, `validate-reviewer.mjs`, `validate-triage.mjs` - per-phase output validators
@@ -179,8 +179,11 @@ for (const entry of entries) {
179
179
  // An acknowledged drift is a conscious "port pending" decision, pinned to the
180
180
  // upstream version it was made against: it stays visible but does not fail
181
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;
182
+ // again. The pin only counts against an AUTHORITATIVE resolution - a cache
183
+ // that happens to hold the pinned version proves nothing about upstream and
184
+ // must not silence the unverified state.
185
+ const acknowledged =
186
+ drifted && resolved.authoritative && entry.driftAcknowledged === resolved.version;
184
187
  const status = acknowledged
185
188
  ? "drift (acknowledged)"
186
189
  : drifted
@@ -52,6 +52,10 @@ function defaultIndexCandidates() {
52
52
  const repoRoot = resolve(here, "..", ".."); // <repo> when here is <repo>/pipeline/scripts
53
53
  return [
54
54
  join(installRoot, "skills", ".skills-index.json"),
55
+ // Codex CLI keeps skills under multi-agent-refs/skills (install/codex.mjs
56
+ // writes the index there); without this candidate every dynamic-skill
57
+ // dispatch on Codex exited 1 with "cannot read index".
58
+ join(installRoot, "multi-agent-refs", "skills", ".skills-index.json"),
55
59
  join(repoRoot, "pipeline", "skills", ".skills-index.json"),
56
60
  ];
57
61
  }
@@ -94,7 +94,11 @@ function readMigratableVersions(target) {
94
94
  }
95
95
  // Schema unreadable: refusing to migrate would be worse than migrating from a
96
96
  // version this build cannot enumerate, so fall back to the known history.
97
- return new Set(["2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0"]);
97
+ // Every released schemaVersion below TARGET_VERSION belongs here - the
98
+ // 2.4.0->2.5.0 and 2.5.0->2.6.0 bumps both shipped without extending it,
99
+ // which made a schema-less install throw "unknown schemaVersion" on a prefs
100
+ // file one version behind.
101
+ return new Set(["2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0"]);
98
102
  }
99
103
 
100
104
  function defaultKeychainMapping() {
@@ -115,6 +115,23 @@ need_jq() {
115
115
  }
116
116
  }
117
117
 
118
+ # Live usage ping (best-effort). Emits one per-phase update to the private
119
+ # dashboard via usage-report.mjs, always status=running so per-phase updates
120
+ # never fold the run into rollup counters - the terminal fold comes only from
121
+ # the Phase 7 / halt emit that reads the real run status. The emitter no-ops
122
+ # unless prefs.global.usageLog.enabled; detached so it never blocks a boundary.
123
+ usage_live_ping() {
124
+ local task="$1" phase="$2"
125
+ local script="$HOME/.claude/scripts/usage-report.mjs"
126
+ local prefs="$HOME/.claude/multi-agent-preferences.json"
127
+ [ -n "$task" ] && [ -f "$script" ] || return 0
128
+ # Cheap gate: only spawn the emitter when usage logging is actually on, so a
129
+ # user who never enabled it pays nothing per phase boundary. jq is already a
130
+ # hard dependency of this script.
131
+ jq -e '.global.usageLog.enabled == true' "$prefs" >/dev/null 2>&1 || return 0
132
+ ( node "$script" --task-id "$task" --phase "$phase" --status running >/dev/null 2>&1 & ) 2>/dev/null || true
133
+ }
134
+
118
135
  # Decode HTML entities in a tile title.
119
136
  #
120
137
  # `rules.md` forbids entities in "titles, commit messages, task subjects, or body
@@ -681,6 +698,7 @@ case "$ACTION" in
681
698
  # subsequent tracker call (the pointer flips on the most recent init).
682
699
  echo "$TRACKER_FILE" > "$HOME/.claude/logs/multi-agent/.tracker-current"
683
700
  render
701
+ usage_live_ping "$TASK_ID" 0
684
702
  if [ "${MULTI_AGENT_QUIET:-0}" != "1" ]; then
685
703
  echo "phase-tracker: tracker initialized for task=$TASK_ID" >&2
686
704
  echo "phase-tracker: TIP - for concurrent-safe resolution, set:" >&2
@@ -733,6 +751,7 @@ case "$ACTION" in
733
751
  release_state_lock
734
752
  emit_otel_span "phase.update" "$PID" "$STATUS" "{\"status\": \"$STATUS\"}"
735
753
  render
754
+ usage_live_ping "$(basename "$TRACKER_DIR")" "$PID"
736
755
  ;;
737
756
 
738
757
  sub)
@@ -353,7 +353,9 @@ function rmExternalDeliveredSkills(skillsDir) {
353
353
  if (!names) {
354
354
  console.log(
355
355
  ` note: no ${EXTERNAL_SKILLS_MANIFEST} under ${skillsDir} - external skill dirs left in ` +
356
- `place (cannot tell pipeline-delivered from user-authored; re-run install once, then uninstall)`,
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)`,
357
359
  );
358
360
  return 0;
359
361
  }