@mmerterden/multi-agent-pipeline 10.9.0 → 10.12.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 (43) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/pipeline/commands/multi-agent/channels.md +42 -6
  5. package/pipeline/commands/multi-agent/finish.md +15 -6
  6. package/pipeline/commands/multi-agent/generate-bug.md +33 -0
  7. package/pipeline/commands/multi-agent/generate-task.md +33 -0
  8. package/pipeline/commands/multi-agent/help.md +12 -0
  9. package/pipeline/commands/multi-agent/manual-test.md +9 -2
  10. package/pipeline/commands/multi-agent/refs/channels/pr.md +1 -1
  11. package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +7 -6
  12. package/pipeline/commands/multi-agent/refs/generate-issue.md +197 -0
  13. package/pipeline/commands/multi-agent/refs/keychain.md +12 -0
  14. package/pipeline/commands/multi-agent/refs/phases/phase-0-init.md +31 -2
  15. package/pipeline/commands/multi-agent/refs/phases/phase-3-dev.md +2 -0
  16. package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +2 -0
  17. package/pipeline/commands/multi-agent/refs/phases/phase-5-test.md +6 -1
  18. package/pipeline/commands/multi-agent/refs/phases/phase-7-report.md +5 -1
  19. package/pipeline/commands/multi-agent/refs/progress-contract.md +1 -0
  20. package/pipeline/commands/multi-agent/refs/tracker-contract.md +56 -13
  21. package/pipeline/commands/multi-agent/resume.md +6 -2
  22. package/pipeline/commands/multi-agent/sync.md +5 -5
  23. package/pipeline/commands/multi-agent.md +4 -0
  24. package/pipeline/lib/figma-mcp-refresh.sh +79 -0
  25. package/pipeline/preferences-template.json +2 -1
  26. package/pipeline/schemas/prefs.schema.json +11 -0
  27. package/pipeline/scripts/fixtures/install-layout.tsv +6 -6
  28. package/pipeline/scripts/phase-tracker.sh +134 -22
  29. package/pipeline/scripts/smoke-channels-approval-gate.sh +60 -0
  30. package/pipeline/scripts/smoke-generate-issue.sh +114 -0
  31. package/pipeline/scripts/smoke-phase-tracker.sh +69 -0
  32. package/pipeline/scripts/smoke-progress-contract.sh +9 -0
  33. package/pipeline/scripts/smoke-tasklist-ordering.sh +1 -0
  34. package/pipeline/scripts/smoke-token-preflight.sh +82 -0
  35. package/pipeline/scripts/smoke-tracker-contract.sh +62 -0
  36. package/pipeline/skills/.skills-index.json +22 -4
  37. package/pipeline/skills/shared/core/multi-agent/SKILL.md +2 -1
  38. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +4 -2
  39. package/pipeline/skills/shared/core/multi-agent-generate-bug/SKILL.md +39 -0
  40. package/pipeline/skills/shared/core/multi-agent-generate-task/SKILL.md +39 -0
  41. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +4 -0
  42. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +5 -5
  43. package/pipeline/skills/skills-index.md +5 -3
@@ -179,6 +179,17 @@
179
179
  }
180
180
  }
181
181
  },
182
+ "tokenScripts": {
183
+ "type": "object",
184
+ "additionalProperties": {
185
+ "type": [
186
+ "string",
187
+ "null"
188
+ ]
189
+ },
190
+ "default": {},
191
+ "description": "Maps service ids to user-owned token generation scripts (absolute paths, personal - never synced). Used by the Phase 0 token pre-flight: figma_mcp silent renewal reads the .figma-oauth.json next to tokenScripts.figma_mcp; the 'Regenerate now (script)' option runs the script itself."
192
+ },
182
193
  "platformIdentityRouting": {
183
194
  "type": "object",
184
195
  "description": "v2.1.0+. Maps repo URL glob patterns to identity names. Used in Phase 0 to auto-select git identity + PAT based on repo origin. Prevents corporate identity leaking into personal repos and vice versa.",
@@ -1,16 +1,16 @@
1
1
  .claude/CLAUDE.md 1
2
2
  .claude/agents 8
3
- .claude/commands 89
4
- .claude/lib 23
3
+ .claude/commands 92
4
+ .claude/lib 24
5
5
  .claude/multi-agent-preferences.json 1
6
6
  .claude/rules 12
7
7
  .claude/schemas 23
8
- .claude/scripts 171
8
+ .claude/scripts 174
9
9
  .claude/settings.json 1
10
10
  .claude/skills 560
11
11
  .copilot/agents 8
12
12
  .copilot/copilot-instructions.md 1
13
- .copilot/lib 23
13
+ .copilot/lib 24
14
14
  .copilot/schemas 23
15
- .copilot/scripts 171
16
- .copilot/skills 596
15
+ .copilot/scripts 174
16
+ .copilot/skills 598
@@ -14,12 +14,14 @@
14
14
  #
15
15
  # Commands:
16
16
  # init <task_id> Reset tracker for this task
17
- # add <phase_id> "<name>" Register a phase (status=pending)
17
+ # add <phase_id> "<name>" Register a phase (status=pending); idempotent - existing id is a no-op
18
18
  # update <phase_id> <status> Change status + re-render
19
19
  # sub <phase_id> <sub_id> "<name>" [status] Add or update a sub-phase
20
- # tokens <phase_id> <in> <out> Add to phase's token totals (v5.5.0)
20
+ # tokens <phase_id> <in> <out> [cached] Add to phase's token totals (in = fresh input, cache-exclusive)
21
21
  # model <phase_id> <model_name> Tag phase with the LLM that produced its tokens (v8.3.0)
22
22
  # meta <phase_id> <key> <value> Set phase context (account, repos, files, now, ...)
23
+ # now <phase_id> "<text>" Set the phase's live current-action line (quiet, 60-char cap)
24
+ # cost <phase_id>|total Print est. USD for one phase or the whole run ("-" when unpriceable)
23
25
  # render Print current snapshot, no change
24
26
  #
25
27
  # Status enum: pending | in_progress | completed | failed | skipped
@@ -57,8 +59,11 @@ usage:
57
59
  phase-tracker.sh add <phase_id> "<name>"
58
60
  phase-tracker.sh update <phase_id> <pending|in_progress|completed|failed|skipped>
59
61
  phase-tracker.sh sub <phase_id> <sub_id> "<name>" [status]
60
- phase-tracker.sh tokens <phase_id> <in> <out>
62
+ phase-tracker.sh tokens <phase_id> <in> <out> [cached]
63
+ phase-tracker.sh model <phase_id> <model_name>
61
64
  phase-tracker.sh meta <phase_id> <key> <value>
65
+ phase-tracker.sh now <phase_id> "<text>"
66
+ phase-tracker.sh cost <phase_id>|total
62
67
  phase-tracker.sh render
63
68
  USAGE
64
69
  exit 64
@@ -239,6 +244,37 @@ format_elapsed() {
239
244
  fi
240
245
  }
241
246
 
247
+ # Cost table ships next to this script in every install tree (repo,
248
+ # ~/.claude/scripts, ~/.copilot/scripts). Pricing math matches
249
+ # render-agent-log-cost.sh exactly: tokens_in is FRESH input (cache-exclusive,
250
+ # per the disjoint token-count contract), tokens_cached is priced at the
251
+ # discounted cacheReadPerMtok rate, floored to cents. A missing/unparseable
252
+ # table never fails a caller - USD simply renders empty / "-".
253
+ COST_TABLE="$(cd "$(dirname "$0")" && pwd)/cost-table.json"
254
+
255
+ # Print est. USD ("0.21") for one phase index or, with "total", the sum across
256
+ # priceable phases. Prints "-" when nothing is priceable. Never non-zero exit.
257
+ phase_usd() {
258
+ local selector="$1" # integer phase index into .phases[], or "total"
259
+ [ -f "$COST_TABLE" ] || { echo "-"; return 0; }
260
+ local state; state="$(load_state)"
261
+ echo "$state" | jq -r --slurpfile prices "$COST_TABLE" --arg sel "$selector" '
262
+ def usd_of(p): ($prices[0].prices[p.model // ""] // null) as $rate |
263
+ if $rate == null then null
264
+ else ( ((p.tokens_in // 0) / 1000000) * $rate.inPerMtok
265
+ + ((p.tokens_cached // 0) / 1000000) * ($rate.cacheReadPerMtok // $rate.inPerMtok)
266
+ + ((p.tokens_out // 0) / 1000000) * $rate.outPerMtok )
267
+ end;
268
+ def floor_cents(u): if u == null then null else ((u * 100) | floor) / 100 end;
269
+ if $sel == "total" then
270
+ ([.phases[] | usd_of(.) | select(. != null)] | if length == 0 then "-" else (add | floor_cents(.) | tostring) end)
271
+ else
272
+ (.phases[($sel | tonumber)] // null) as $p |
273
+ if $p == null then "-" else (usd_of($p) | floor_cents(.) | if . == null then "-" else tostring end) end
274
+ end
275
+ ' 2>/dev/null || echo "-"
276
+ }
277
+
242
278
  # Read state, atomically write it back. Atomic = write to .tmp, rename.
243
279
  load_state() {
244
280
  if [ ! -f "$TRACKER_FILE" ]; then
@@ -272,10 +308,25 @@ render() {
272
308
  # consecutive tabs (tab is in the default whitespace IFS class), which
273
309
  # silently drops empty middle columns and shifts later fields.
274
310
  # Column order: status, started_at, completed_at, id, name,
275
- # tokens_in, tokens_out, sub_count, meta_keys_csv
311
+ # tokens_in, tokens_out, sub_count, meta_keys_csv, usd
312
+ # The usd column is computed in the same pass with the shared pricing math;
313
+ # a missing/unparseable cost table yields empty usd cells, never a failure.
314
+ local prices_json='{"prices":{}}'
315
+ if [ -f "$COST_TABLE" ]; then
316
+ prices_json=$(cat "$COST_TABLE" 2>/dev/null) || prices_json='{"prices":{}}'
317
+ echo "$prices_json" | jq empty 2>/dev/null || prices_json='{"prices":{}}'
318
+ fi
276
319
  local phase_data
277
- phase_data=$(echo "$state" | jq -r '
278
- .phases // [] | .[] | [
320
+ phase_data=$(echo "$state" | jq -r --argjson prices "$prices_json" '
321
+ def usd_of(p): ($prices.prices[p.model // ""] // null) as $rate |
322
+ if $rate == null then null
323
+ else ( ((p.tokens_in // 0) / 1000000) * $rate.inPerMtok
324
+ + ((p.tokens_cached // 0) / 1000000) * ($rate.cacheReadPerMtok // $rate.inPerMtok)
325
+ + ((p.tokens_out // 0) / 1000000) * $rate.outPerMtok )
326
+ end;
327
+ .phases // []
328
+ | sort_by(.id | (tonumber? // 9999))
329
+ | .[] | [
279
330
  (.status // ""),
280
331
  (.started_at // ""),
281
332
  (.completed_at // ""),
@@ -284,20 +335,21 @@ render() {
284
335
  ((.tokens_in // 0) | tostring),
285
336
  ((.tokens_out // 0) | tostring),
286
337
  ((.subs // []) | length | tostring),
287
- ((.meta // {}) | keys_unsorted | join(","))
338
+ ((.meta // {}) | keys_unsorted | join(",")),
339
+ (usd_of(.) | if . == null then "" else (((. * 100) | floor) / 100 | tostring) end)
288
340
  ] | join("")
289
341
  ')
290
342
 
291
343
  local phase_count=0
292
344
  local -a row_status=() row_started=() row_completed=() row_id=() row_name=()
293
- local -a row_tin=() row_tout=() row_subcount=() row_metakeys=()
345
+ local -a row_tin=() row_tout=() row_subcount=() row_metakeys=() row_usd=()
294
346
  if [ -n "$phase_data" ]; then
295
- local _s _st _ed _id _nm _ti _to _sc _mk
296
- while IFS=$'\x1f' read -r _s _st _ed _id _nm _ti _to _sc _mk; do
347
+ local _s _st _ed _id _nm _ti _to _sc _mk _us
348
+ while IFS=$'\x1f' read -r _s _st _ed _id _nm _ti _to _sc _mk _us; do
297
349
  row_status+=("$_s"); row_started+=("$_st"); row_completed+=("$_ed")
298
350
  row_id+=("$_id"); row_name+=("$_nm")
299
351
  row_tin+=("$_ti"); row_tout+=("$_to")
300
- row_subcount+=("$_sc"); row_metakeys+=("$_mk")
352
+ row_subcount+=("$_sc"); row_metakeys+=("$_mk"); row_usd+=("$_us")
301
353
  phase_count=$((phase_count + 1))
302
354
  done <<< "$phase_data"
303
355
  fi
@@ -367,6 +419,16 @@ render() {
367
419
  local tok_label=""
368
420
  [ "$p_ttotal" -gt 0 ] && tok_label="$(format_tokens $p_ttotal) tok"
369
421
 
422
+ # Est. USD suffix - only when the phase has a priced model AND tokens.
423
+ # "0" (floored below one cent) still renders as ~$0.00 so a priced phase
424
+ # is visibly priced; an unpriceable phase shows no USD at all.
425
+ local p_usd="${row_usd[$i]}"
426
+ local usd_label=""
427
+ if [ -n "$p_usd" ] && [ "$p_ttotal" -gt 0 ]; then
428
+ usd_label=$(printf '~$%.2f' "$p_usd" 2>/dev/null) || usd_label=""
429
+ fi
430
+ [ -n "$usd_label" ] && [ -n "$tok_label" ] && tok_label="${tok_label} · ${usd_label}"
431
+
370
432
  local right_block=""
371
433
  if [ -n "$el_label" ] && [ -n "$tok_label" ]; then
372
434
  right_block=$(printf "%b%s%b ${C_DIM}·${C_RESET} %b%s%b" "$el_color" "$el_label" "$C_RESET" "$C_DIM" "$tok_label" "$C_RESET")
@@ -385,8 +447,8 @@ render() {
385
447
  # Same Unit Separator rationale as the phase batch above.
386
448
  if [ "$pstatus" = "in_progress" ] && [ -n "${row_metakeys[$i]}" ]; then
387
449
  local meta_data key val
388
- meta_data=$(echo "$state" | jq -r --argjson idx "$i" '
389
- .phases[$idx].meta // {} | to_entries[] | [.key, (.value | tostring)] | join("")
450
+ meta_data=$(echo "$state" | jq -r --arg pid "$pid" '
451
+ ([.phases[] | select(.id == $pid)][0].meta) // {} | to_entries[] | [.key, (.value | tostring)] | join("")
390
452
  ')
391
453
  while IFS=$'\x1f' read -r key val; do
392
454
  [ -z "$key" ] && continue
@@ -399,8 +461,8 @@ render() {
399
461
  local sub_count="${row_subcount[$i]}"
400
462
  if [ "$sub_count" != "0" ] && [ "$sub_count" -gt 0 ] 2>/dev/null; then
401
463
  local sub_data
402
- sub_data=$(echo "$state" | jq -r --argjson idx "$i" '
403
- .phases[$idx].subs // [] | .[] | [(.id // ""), (.name // ""), (.status // "")] | join("")
464
+ sub_data=$(echo "$state" | jq -r --arg pid "$pid" '
465
+ ([.phases[] | select(.id == $pid)][0].subs) // [] | .[] | [(.id // ""), (.name // ""), (.status // "")] | join("")
404
466
  ')
405
467
  local j=0
406
468
  local sid sname sstatus sg connector
@@ -417,14 +479,22 @@ render() {
417
479
  done
418
480
  fi
419
481
 
420
- # Total-tokens footer (v5.5.0) - sum phases[].tokens_in + tokens_out.
421
- local total_tokens
482
+ # Total footer - tokens + est. USD (priceable phases) + cached tokens.
483
+ local total_tokens total_cached
422
484
  total_tokens=$(echo "$state" | jq '[.phases[] | ((.tokens_in // 0) + (.tokens_out // 0))] | add // 0')
485
+ total_cached=$(echo "$state" | jq '[.phases[] | (.tokens_cached // 0)] | add // 0')
423
486
  if [ "$total_tokens" -gt 0 ]; then
424
- local total_label
487
+ local total_label total_usd total_suffix=""
425
488
  total_label=$(format_tokens "$total_tokens")
489
+ total_usd=$(phase_usd total)
490
+ if [ -n "$total_usd" ] && [ "$total_usd" != "-" ]; then
491
+ total_suffix=$(printf ' · ~$%.2f' "$total_usd" 2>/dev/null) || total_suffix=""
492
+ fi
493
+ if [ "$total_cached" -gt 0 ] 2>/dev/null; then
494
+ total_suffix="${total_suffix} ($(format_tokens "$total_cached") cached)"
495
+ fi
426
496
  printf "${C_BOLD}${C_BLUE}│${C_RESET} ${C_DIM}%s${C_RESET}\n" "$(printf '%*s' 56 '' | tr ' ' '─')"
427
- printf "${C_BOLD}${C_BLUE}│${C_RESET} ${C_BOLD}Total${C_RESET} ${C_BOLD}${C_CYAN}%s tok${C_RESET}\n" "$total_label"
497
+ printf "${C_BOLD}${C_BLUE}│${C_RESET} ${C_BOLD}Total${C_RESET} ${C_BOLD}${C_CYAN}%s tok%s${C_RESET}\n" "$total_label" "$total_suffix"
428
498
  fi
429
499
 
430
500
  printf "${C_BOLD}${C_BLUE}╰────────────────────────────────────────────────────────────╯${C_RESET}\n"
@@ -459,8 +529,14 @@ case "$ACTION" in
459
529
  [ "$#" -ge 2 ] || { echo "add needs <phase_id> <name>" >&2; exit 64; }
460
530
  PID="$1"; PNAME="$2"
461
531
  state=$(load_state)
462
- new=$(echo "$state" | jq --arg id "$PID" --arg name "$PNAME" \
463
- '.phases += [{"id":$id,"name":$name,"status":"pending","subs":[]}]')
532
+ # Idempotent: a phase id that already exists is a no-op (name, status, and
533
+ # token history are preserved). This is what lets continuation commands
534
+ # (/multi-agent:finish, resume) re-declare their phase set against a
535
+ # pre-existing tracker without duplicating or resetting tiles.
536
+ new=$(echo "$state" | jq --arg id "$PID" --arg name "$PNAME" '
537
+ if any(.phases[]?; .id == $id) then .
538
+ else .phases += [{"id":$id,"name":$name,"status":"pending","subs":[]}]
539
+ end')
464
540
  save_state "$new"
465
541
  render
466
542
  ;;
@@ -581,12 +657,48 @@ case "$ACTION" in
581
657
  fi
582
658
  ;;
583
659
 
660
+ now)
661
+ need_jq
662
+ [ "$#" -ge 2 ] || { echo "now needs <phase_id> <text>" >&2; exit 64; }
663
+ PID="$1"; TEXT="$2"
664
+ # Live current-action line for the active phase. Quiet by construction:
665
+ # progress lines are frequent, so `now` never renders - the card picks the
666
+ # value up at the next boundary render/update. Truncated to 60 chars.
667
+ if [ "${#TEXT}" -gt 60 ]; then
668
+ TEXT="${TEXT:0:59}…"
669
+ fi
670
+ state=$(load_state)
671
+ new=$(echo "$state" | jq --arg id "$PID" --arg v "$TEXT" '
672
+ .phases |= map(
673
+ if .id == $id then .meta = ((.meta // {}) + {"Now": $v}) else . end
674
+ )')
675
+ save_state "$new"
676
+ emit_otel_span "phase.now" "$PID" "$TEXT" "{}"
677
+ ;;
678
+
679
+ cost)
680
+ need_jq
681
+ [ "$#" -ge 1 ] || { echo "cost needs <phase_id>|total" >&2; exit 64; }
682
+ SEL="$1"
683
+ if [ "$SEL" = "total" ]; then
684
+ phase_usd total
685
+ else
686
+ # Resolve phase id -> array index (ids are strings like "3").
687
+ IDX=$(load_state | jq -r --arg id "$SEL" '[.phases[]?.id] | index($id) // "-"')
688
+ if [ "$IDX" = "-" ] || [ -z "$IDX" ]; then
689
+ echo "-"
690
+ else
691
+ phase_usd "$IDX"
692
+ fi
693
+ fi
694
+ ;;
695
+
584
696
  render)
585
697
  render
586
698
  ;;
587
699
 
588
700
  *)
589
- echo "phase-tracker: unknown action '$ACTION' (use init|add|update|sub|tokens|model|meta|render)" >&2
701
+ echo "phase-tracker: unknown action '$ACTION' (use init|add|update|sub|tokens|model|meta|now|cost|render)" >&2
590
702
  exit 64
591
703
  ;;
592
704
  esac
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-channels-approval-gate.sh - contract for the channels Step 6 body preview + approval gate.
3
+ #
4
+ # Verifies:
5
+ # 1. channels.md has the Step 6 gate section with the four decision options.
6
+ # 2. The two (and only two) bypass cases are documented: autopilot + --dry-run.
7
+ # 3. Step 7 dispatch is gated on the Step 6 approval.
8
+ # 4. Byte-identical post rule + re-approval-on-regeneration rule present.
9
+ # 5. Phase 7 Step 1.5 (GitHub issue comment) references the same gate.
10
+ # 6. Required rules include the approval-before-post bullet.
11
+
12
+ set -euo pipefail
13
+
14
+ HERE="$(cd "$(dirname "$0")" && pwd)"
15
+ ROOT="$(cd "$HERE/../.." && pwd)"
16
+ CHANNELS="$ROOT/pipeline/commands/multi-agent/channels.md"
17
+ PHASE7="$ROOT/pipeline/commands/multi-agent/refs/phases/phase-7-report.md"
18
+
19
+ PASS=0
20
+ FAIL=0
21
+ pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
22
+ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
23
+
24
+ for f in "$CHANNELS" "$PHASE7"; do
25
+ [ -f "$f" ] || { echo "error: missing $f" >&2; exit 1; }
26
+ done
27
+
28
+ echo "→ 1. Step 6 gate section + decision options"
29
+ grep -Fq "Step 6 - Body preview + approval gate" "$CHANNELS" && pass "gate section present" || fail "gate section missing"
30
+ for opt in "Approve & post" "Edit" "Skip channels" "Cancel"; do
31
+ grep -Fq "$opt" "$CHANNELS" && pass "option: $opt" || fail "option missing: $opt"
32
+ done
33
+
34
+ echo ""
35
+ echo "→ 2. bypass cases are exactly autopilot + --dry-run"
36
+ grep -Fq "Skipped in exactly two cases" "$CHANNELS" && pass "two-case rule stated" || fail "two-case rule missing"
37
+ grep -Fq "MULTI_AGENT_AUTOPILOT=1" "$CHANNELS" && pass "autopilot bypass documented" || fail "autopilot bypass missing"
38
+ grep -Eq 'dry-run.*(redundant|without posting)' "$CHANNELS" && pass "dry-run bypass documented" || fail "dry-run bypass missing"
39
+
40
+ echo ""
41
+ echo "→ 3. dispatch gated on approval"
42
+ grep -Fq "NEVER starts before the Step 6 gate resolves" "$CHANNELS" && pass "Step 6 gated" || fail "Step 6 gate wiring missing"
43
+
44
+ echo ""
45
+ echo "→ 4. byte-identical + re-approval rules"
46
+ grep -Fq "byte-identical to the approved preview" "$CHANNELS" && pass "byte-identical rule" || fail "byte-identical rule missing"
47
+ grep -Fq "invalidates the approval" "$CHANNELS" && pass "re-approval on regeneration" || fail "re-approval rule missing"
48
+
49
+ echo ""
50
+ echo "→ 5. Phase 7 issue-comment path uses the same gate"
51
+ grep -Fq "same preview + approval gate as channels Step 6" "$PHASE7" && pass "Phase 7 Step 1.5 gated" || fail "Phase 7 Step 1.5 gate reference missing"
52
+ grep -Fq "Autopilot posts directly" "$PHASE7" && pass "Phase 7 autopilot bypass" || fail "Phase 7 autopilot bypass missing"
53
+
54
+ echo ""
55
+ echo "→ 6. required-rules bullet"
56
+ grep -Fq "**Approval before post**" "$CHANNELS" && pass "required-rules bullet present" || fail "required-rules bullet missing"
57
+
58
+ echo ""
59
+ echo "══ channels-approval-gate smoke: $PASS passed, $FAIL failed ══"
60
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-generate-issue.sh - contract for /multi-agent:generate-task + generate-bug.
3
+ #
4
+ # Verifies:
5
+ # 1. All 5 files exist (2 commands, shared ref, 2 Copilot SKILL.md).
6
+ # 2. Frontmatter keys present on both surfaces.
7
+ # 3. Commands delegate to refs/generate-issue.md and declare their ISSUE_TYPE.
8
+ # 4. Approval gate documented and marked unskippable in the ref.
9
+ # 5. Convention mining + createmeta + Agile sprint endpoints present.
10
+ # 6. UTF-8 verbatim POST path (--rawfile + --data-binary) present.
11
+ # 7. Profile section vocab: Task (Goal/Scope/Acceptance), Bug (Steps/Expected/Actual/Environment).
12
+ # 8. Registration: dispatcher rows, inventory lists, count consistency (37).
13
+ # 9. Forbidden strings absent (em-dash, section sign, the M-word, unicode_escape).
14
+
15
+ set -euo pipefail
16
+
17
+ HERE="$(cd "$(dirname "$0")" && pwd)"
18
+ ROOT="$(cd "$HERE/../.." && pwd)"
19
+ CMD_TASK="$ROOT/pipeline/commands/multi-agent/generate-task.md"
20
+ CMD_BUG="$ROOT/pipeline/commands/multi-agent/generate-bug.md"
21
+ REF="$ROOT/pipeline/commands/multi-agent/refs/generate-issue.md"
22
+ SKILL_TASK="$ROOT/pipeline/skills/shared/core/multi-agent-generate-task/SKILL.md"
23
+ SKILL_BUG="$ROOT/pipeline/skills/shared/core/multi-agent-generate-bug/SKILL.md"
24
+ DISPATCHER="$ROOT/pipeline/commands/multi-agent.md"
25
+ CONTRACT="$ROOT/pipeline/commands/multi-agent/refs/cross-cli-contract.md"
26
+ SYNC="$ROOT/pipeline/commands/multi-agent/sync.md"
27
+ HELP="$ROOT/pipeline/commands/multi-agent/help.md"
28
+
29
+ PASS=0
30
+ FAIL=0
31
+ pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
32
+ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
33
+
34
+ echo "→ 1. files exist"
35
+ for f in "$CMD_TASK" "$CMD_BUG" "$REF" "$SKILL_TASK" "$SKILL_BUG"; do
36
+ if [ -f "$f" ]; then pass "exists: ${f#$ROOT/}"; else fail "missing: ${f#$ROOT/}"; fi
37
+ done
38
+ [ "$FAIL" -eq 0 ] || { echo "══ generate-issue smoke: $PASS passed, $FAIL failed ══"; exit 1; }
39
+
40
+ echo ""
41
+ echo "→ 2. frontmatter keys"
42
+ for f in "$CMD_TASK" "$CMD_BUG"; do
43
+ head -5 "$f" | grep -q '^description:' && pass "description: ${f##*/}" || fail "description missing: ${f##*/}"
44
+ head -5 "$f" | grep -q '^argument-hint:' && pass "argument-hint: ${f##*/}" || fail "argument-hint missing: ${f##*/}"
45
+ done
46
+ for f in "$SKILL_TASK" "$SKILL_BUG"; do
47
+ head -8 "$f" | grep -q '^name: multi-agent-generate-' && pass "name: ${f%/*}" || fail "name missing: $f"
48
+ head -8 "$f" | grep -q '^language: en' && pass "language en" || fail "language missing: $f"
49
+ head -8 "$f" | grep -q '^user-invocable: true' && pass "user-invocable" || fail "user-invocable missing: $f"
50
+ done
51
+
52
+ echo ""
53
+ echo "→ 3. commands delegate to the shared ref with a profile"
54
+ for f in "$CMD_TASK" "$CMD_BUG"; do
55
+ grep -Fq "refs/generate-issue.md" "$f" && pass "ref link: ${f##*/}" || fail "ref link missing: ${f##*/}"
56
+ done
57
+ grep -Fq '`ISSUE_TYPE` | `Task`' "$CMD_TASK" && pass "ISSUE_TYPE=Task declared" || fail "ISSUE_TYPE=Task missing"
58
+ grep -Fq '`ISSUE_TYPE` | `Bug`' "$CMD_BUG" && pass "ISSUE_TYPE=Bug declared" || fail "ISSUE_TYPE=Bug missing"
59
+
60
+ echo ""
61
+ echo "→ 4. approval gate documented and unskippable"
62
+ grep -Fq "Full preview + approval gate (never skipped)" "$REF" && pass "gate section present" || fail "gate section missing"
63
+ for marker in "Approve" "Edit" "Cancel" "no bypass" "Nothing was created"; do
64
+ grep -Fqi "$marker" "$REF" && pass "gate marker: $marker" || fail "gate marker missing: $marker"
65
+ done
66
+ grep -Fq "no autopilot variant" "$REF" && pass "no-autopilot rule" || fail "no-autopilot rule missing"
67
+
68
+ echo ""
69
+ echo "→ 5. mining + createmeta + agile endpoints"
70
+ for marker in "rest/api/2/search" "rest/api/2/issue/createmeta" "rest/agile/1.0/board" "sprint?state=active" 'sprint/${SPRINT_ID}/issue' "remotelink" "attachments"; do
71
+ grep -Fq "$marker" "$REF" && pass "endpoint marker: $marker" || fail "endpoint marker missing: $marker"
72
+ done
73
+ grep -Fq "gh-sprint" "$REF" && pass "generic sprint-field detection" || fail "generic sprint-field detection missing"
74
+
75
+ echo ""
76
+ echo "→ 6. UTF-8 verbatim POST path"
77
+ grep -Fq -- "--rawfile" "$REF" && pass "--rawfile present" || fail "--rawfile missing"
78
+ grep -Fq -- "--data-binary" "$REF" && pass "--data-binary present" || fail "--data-binary missing"
79
+ grep -Fqi "humanizer" "$REF" && pass "humanizer pass referenced" || fail "humanizer missing"
80
+
81
+ echo ""
82
+ echo "→ 7. profile section vocabulary"
83
+ for s in "Goal" "Scope" "Acceptance Criteria"; do
84
+ grep -Fq "$s" "$CMD_TASK" && pass "task section: $s" || fail "task section missing: $s"
85
+ done
86
+ for s in "Steps to Reproduce" "Expected Result" "Actual Result" "Environment"; do
87
+ grep -Fq "$s" "$CMD_BUG" && pass "bug section: $s" || fail "bug section missing: $s"
88
+ done
89
+
90
+ echo ""
91
+ echo "→ 8. registration + count consistency (37)"
92
+ grep -Fq '| `generate-task' "$DISPATCHER" && pass "dispatcher routing: generate-task" || fail "dispatcher routing missing: generate-task"
93
+ grep -Fq '| `generate-bug' "$DISPATCHER" && pass "dispatcher routing: generate-bug" || fail "dispatcher routing missing: generate-bug"
94
+ grep -Fq "generate-task.md" "$DISPATCHER" && pass "dispatcher loading row" || fail "dispatcher loading row missing"
95
+ grep -Fq "## 1. Command Inventory (37 commands)" "$CONTRACT" && pass "contract count = 37" || fail "contract count wrong"
96
+ grep -Eq '(^|[ ,])generate-bug, generate-task([ ,]|$)' "$CONTRACT" && pass "contract list has both" || fail "contract list missing entries"
97
+ grep -Fq "**37 commands are synced**" "$SYNC" && pass "sync count = 37" || fail "sync count wrong"
98
+ SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
99
+ grep -Fq "**37 commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = 37" || fail "sync SKILL count wrong (Copilot parity)"
100
+ grep -Fq "generate-task" "$ROOT/pipeline/skills/shared/core/multi-agent-help/SKILL.md" && pass "help SKILL lists generators" || fail "help SKILL missing generators (Copilot parity)"
101
+ grep -c "generate-task" "$HELP" | awk '{exit !($1>=2)}' && pass "help.md has EN + TR entries" || fail "help.md entries incomplete"
102
+
103
+ echo ""
104
+ echo "→ 9. forbidden strings absent"
105
+ MWORD="MAN""DATORY"
106
+ for f in "$CMD_TASK" "$CMD_BUG" "$REF" "$SKILL_TASK" "$SKILL_BUG"; do
107
+ if grep -q $'—\|§\|…' "$f"; then fail "forbidden unicode punctuation in ${f##*/}"; else pass "punctuation clean: ${f##*/}"; fi
108
+ if grep -q "$MWORD" "$f"; then fail "forbidden word in ${f##*/}"; else pass "vocabulary clean: ${f##*/}"; fi
109
+ if grep -q "unicode_escape" "$f" && ! grep -q "Never round-trip" "$f"; then fail "unicode_escape misuse in ${f##*/}"; else pass "encoding rule clean: ${f##*/}"; fi
110
+ done
111
+
112
+ echo ""
113
+ echo "══ generate-issue smoke: $PASS passed, $FAIL failed ══"
114
+ [ "$FAIL" -eq 0 ]
@@ -250,6 +250,75 @@ STATE_LIVE="$HOME/.claude/logs/multi-agent/TOK-LIVE/tracker-state.json"
250
250
  TIN_LIVE=$(jq -r '.phases[0].tokens_in // 0' "$STATE_LIVE")
251
251
  [ "$TIN_LIVE" = "600" ] && pass "TRACKER_QUIET still accumulates state (500+100=600)" || fail "quiet mode broke accumulation (got $TIN_LIVE)"
252
252
 
253
+ echo ""
254
+ echo "→ 20. now action: quiet, truncating current-action line"
255
+ run_plain init NOW-1 >/dev/null
256
+ run_plain add 3 "Dev" >/dev/null
257
+ run_plain update 3 in_progress >/dev/null
258
+ NOW_OUT=$(run_plain now 3 "editing TopBarView.swift")
259
+ if [ -n "$NOW_OUT" ]; then
260
+ fail "now must not render (got: $NOW_OUT)"
261
+ else
262
+ pass "now is quiet (no stdout)"
263
+ fi
264
+ STATE_NOW="$HOME/.claude/logs/multi-agent/NOW-1/tracker-state.json"
265
+ NOW_VAL=$(jq -r '.phases[0].meta.Now' "$STATE_NOW")
266
+ [ "$NOW_VAL" = "editing TopBarView.swift" ] && pass "now stored in meta.Now" || fail "meta.Now wrong: $NOW_VAL"
267
+ run_plain now 3 "this is a deliberately very long current action description that exceeds sixty characters"
268
+ NOW_LEN=$(jq -r '.phases[0].meta.Now | length' "$STATE_NOW")
269
+ [ "$NOW_LEN" = "60" ] && pass "now truncates to 60 chars" || fail "expected 60 chars, got $NOW_LEN"
270
+ RENDER_NOW=$(run_plain render)
271
+ echo "$RENDER_NOW" | grep -q "Now:" && pass "render shows Now: line for active phase" || fail "render missing Now: line"
272
+
273
+ echo ""
274
+ echo "→ 21. add is idempotent (continuation contract)"
275
+ run_plain tokens 3 1000 200 >/dev/null
276
+ run_plain add 3 "DevRenamed" >/dev/null
277
+ CNT=$(jq '.phases | length' "$STATE_NOW")
278
+ [ "$CNT" = "1" ] && pass "duplicate add does not append" || fail "expected 1 phase, got $CNT"
279
+ NAME3=$(jq -r '.phases[0].name' "$STATE_NOW")
280
+ [ "$NAME3" = "Dev" ] && pass "duplicate add preserves original name" || fail "name overwritten: $NAME3"
281
+ TIN3=$(jq -r '.phases[0].tokens_in' "$STATE_NOW")
282
+ [ "$TIN3" = "1000" ] && pass "duplicate add preserves token history" || fail "tokens lost: $TIN3"
283
+
284
+ echo ""
285
+ echo "→ 22. cost action prices via cost-table.json"
286
+ run_plain init COST-1 >/dev/null
287
+ run_plain add 3 "Dev" >/dev/null
288
+ run_plain update 3 in_progress >/dev/null
289
+ TRACKER_QUIET=1 run_plain model 3 opus >/dev/null
290
+ TRACKER_QUIET=1 run_plain tokens 3 100000 20000 >/dev/null
291
+ # opus: 0.1*5 + 0.02*25 = 0.5 + 0.5 = 1.00
292
+ COST3=$(run_plain cost 3)
293
+ [ "$COST3" = "1" ] && pass "cost 3 = 1 (opus 100k in + 20k out)" || fail "expected 1, got $COST3"
294
+ TRACKER_QUIET=1 run_plain tokens 3 0 0 50000 >/dev/null
295
+ # + 50k cached * 0.5/M = 0.025 -> total 1.025 -> floored 1.02
296
+ COST3C=$(run_plain cost 3)
297
+ [ "$COST3C" = "1.02" ] && pass "cached tokens priced at cacheReadPerMtok (1.02)" || fail "expected 1.02, got $COST3C"
298
+ COSTTOT=$(run_plain cost total)
299
+ [ "$COSTTOT" = "1.02" ] && pass "cost total sums priceable phases" || fail "expected 1.02, got $COSTTOT"
300
+ run_plain add 4 "Review" >/dev/null
301
+ TRACKER_QUIET=1 run_plain tokens 4 5000 1000 >/dev/null
302
+ COST4=$(run_plain cost 4)
303
+ [ "$COST4" = "-" ] && pass "cost is '-' for a phase without a model" || fail "expected -, got $COST4"
304
+ COSTBAD=$(run_plain cost 9)
305
+ [ "$COSTBAD" = "-" ] && pass "cost is '-' for unknown phase id" || fail "expected -, got $COSTBAD"
306
+
307
+ echo ""
308
+ echo "→ 23. render shows per-tile USD and cached footer"
309
+ RENDER_COST=$(run_plain render)
310
+ echo "$RENDER_COST" | grep -q '~\$1.02' && pass "tile shows ~\$ estimate for priced phase" || fail "render missing tile USD"
311
+ echo "$RENDER_COST" | grep -q "cached" && pass "footer shows cached tokens" || fail "render missing cached footer"
312
+ echo "$RENDER_COST" | grep -qE "Total.*~\\\$" && pass "footer shows total USD" || fail "render missing total USD"
313
+
314
+ echo ""
315
+ echo "→ 24. render sorts phases by numeric id (continuation ordering)"
316
+ run_plain init ORDER-1 >/dev/null
317
+ for pp in "0:Init" "5:Test"; do run_plain add "${pp%%:*}" "${pp#*:}" >/dev/null; done
318
+ for pp in "4:Review" "6:Commit"; do run_plain add "${pp%%:*}" "${pp#*:}" >/dev/null; done
319
+ ORDER_OUT=$(run_plain render | grep -oE "Phase [0-9]+" | tr -d 'Phase ' | tr '\n' ',')
320
+ [ "$ORDER_OUT" = "0,4,5,6," ] && pass "phases render sorted by id (0,4,5,6)" || fail "order wrong: $ORDER_OUT"
321
+
253
322
  echo ""
254
323
  echo "══ phase-tracker smoke: $PASS passed, $FAIL failed ══"
255
324
  [ "$FAIL" -eq 0 ]
@@ -113,6 +113,15 @@ if [ "$ADOPTED" -eq 0 ]; then
113
113
  warn "no phases have adopted yet - this is expected until WS-4/5/6 land"
114
114
  fi
115
115
 
116
+ echo ""
117
+ echo "→ Live mirror cross-link (tracker coupling)"
118
+ if grep -q "Live mirror to the phase tracker" "$CONTRACT" \
119
+ && grep -q "phase-tracker.sh now" "$CONTRACT"; then
120
+ pass "contract cross-links the tracker mirror rule"
121
+ else
122
+ fail "contract missing the live-mirror cross-link to tracker-contract.md"
123
+ fi
124
+
116
125
  echo ""
117
126
  echo "══ progress-contract smoke: $PASS passed, $WARN warnings, $FAIL failed ══"
118
127
  [ "$FAIL" -eq 0 ]
@@ -57,6 +57,7 @@ MODE_ENTRY_DOCS=(
57
57
  "pipeline/commands/multi-agent/dev-autopilot.md"
58
58
  "pipeline/commands/multi-agent/dev-local.md"
59
59
  "pipeline/commands/multi-agent/dev-local-autopilot.md"
60
+ "pipeline/commands/multi-agent/finish.md"
60
61
  )
61
62
  for rel in "${MODE_ENTRY_DOCS[@]}"; do
62
63
  abs="$ROOT/$rel"