@mmerterden/multi-agent-pipeline 10.9.0 → 11.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 (41) hide show
  1. package/CHANGELOG.md +170 -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.md +34 -0
  7. package/pipeline/commands/multi-agent/help.md +10 -0
  8. package/pipeline/commands/multi-agent/manual-test.md +9 -2
  9. package/pipeline/commands/multi-agent/refs/channels/pr.md +1 -1
  10. package/pipeline/commands/multi-agent/refs/cross-cli-contract.md +7 -6
  11. package/pipeline/commands/multi-agent/refs/generate-issue.md +267 -0
  12. package/pipeline/commands/multi-agent/refs/keychain.md +12 -0
  13. package/pipeline/commands/multi-agent/refs/phases/phase-0-init.md +31 -2
  14. package/pipeline/commands/multi-agent/refs/phases/phase-3-dev.md +2 -0
  15. package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +2 -0
  16. package/pipeline/commands/multi-agent/refs/phases/phase-5-test.md +6 -1
  17. package/pipeline/commands/multi-agent/refs/phases/phase-7-report.md +5 -1
  18. package/pipeline/commands/multi-agent/refs/progress-contract.md +1 -0
  19. package/pipeline/commands/multi-agent/refs/tracker-contract.md +56 -13
  20. package/pipeline/commands/multi-agent/resume.md +6 -2
  21. package/pipeline/commands/multi-agent/sync.md +9 -9
  22. package/pipeline/commands/multi-agent.md +2 -0
  23. package/pipeline/lib/figma-mcp-refresh.sh +79 -0
  24. package/pipeline/preferences-template.json +2 -1
  25. package/pipeline/schemas/prefs.schema.json +11 -0
  26. package/pipeline/scripts/fixtures/install-layout.tsv +6 -6
  27. package/pipeline/scripts/phase-tracker.sh +134 -22
  28. package/pipeline/scripts/smoke-channels-approval-gate.sh +60 -0
  29. package/pipeline/scripts/smoke-generate-issue.sh +119 -0
  30. package/pipeline/scripts/smoke-phase-tracker.sh +69 -0
  31. package/pipeline/scripts/smoke-progress-contract.sh +9 -0
  32. package/pipeline/scripts/smoke-tasklist-ordering.sh +1 -0
  33. package/pipeline/scripts/smoke-token-preflight.sh +82 -0
  34. package/pipeline/scripts/smoke-tracker-contract.sh +62 -0
  35. package/pipeline/skills/.skills-index.json +13 -4
  36. package/pipeline/skills/shared/core/multi-agent/SKILL.md +2 -1
  37. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +4 -2
  38. package/pipeline/skills/shared/core/multi-agent-generate/SKILL.md +51 -0
  39. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -0
  40. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +5 -5
  41. package/pipeline/skills/skills-index.md +4 -3
@@ -77,6 +77,7 @@ Lib scripts (`~/.claude/lib/`):
77
77
  | `analysis ["<feature>"]` | Standalone feature-spec analizi: Figma / Swagger / Confluence / Jira / repo girdileri sabit 7-bölümlük şablona dökülür, humanizer'dan geçer, Local/Confluence/Jira hedef(ler)ine post edilir. Worktree veya commit yok. Dev'e zincirleme yok. |
78
78
  | `build-optimize` | iOS-only Xcode build performance wrapper. Vendored `xcode-build-orchestrator`'a dispatch eder; benchmark + compilation / project / SPM analyzer'lar + recommend-first plan `.build-benchmark/optimization-plan.md`. Non-iOS stack'lerde fail-fast. |
79
79
  | `channels [PR-url\|#N\|Jira-url\|Jira-id] [--channels pr,jira,confluence,wiki] [--content normal,test,auto-diff,note] [--message "..."]` | Post task report to multi-select channels (PR description, Jira comment, Confluence page, Wiki pages) with multi-select content sources. Humanizer pass per-channel. Bitbucket PR updates use reviewer-preserving PUT. Phase 7 delegates to this command; also invocable post-hoc for fixes made outside the pipeline. No worktree. |
80
+ | `generate ["desc"] [figma-url] [swagger-url]` | Create a standards-compliant Jira issue: asks the type (**Task** / **Bug** / **Story**), mines the project's recent same-type issues for conventions (summary format, labels, priority, test-scenario style), detects the active sprint, drafts from a standard template with auto-sizing sections (Design Reference / API Contract / Screenshots appear only when their source is given), asks about unknown fields, then full draft preview + explicit approval before create. No worktree, no commits. |
80
81
  | `test` or `test [args]` | UI Bug Hunter - screenshot + tap + analyze on booted simulator via MCP (read `$HOME/.claude/commands/sim-test.md`). `/multi-agent:test` also resolves here via the `commands/multi-agent/test.md` delegate. |
81
82
  | `manual-test [#id]` | Phase 5 standalone Manual Test - checks out the task branch, prints Xcode / SourceTree hints, waits for user verdict (`ok` / `fix: ...`). |
82
83
  | `stack [ios\|android\|backend\|mobile\|all]` | Swap skills for next conversation. No arg = show current stack. |
@@ -116,6 +117,7 @@ This command uses lazy loading for token efficiency. Read the relevant sub-file
116
117
  | `local-autopilot` | `$HOME/.claude/commands/multi-agent/local-autopilot.md` |
117
118
  | `dev-local` | `$HOME/.claude/commands/multi-agent/dev-local.md` |
118
119
  | `dev-local-autopilot` | `$HOME/.claude/commands/multi-agent/dev-local-autopilot.md` |
120
+ | `generate` | `$HOME/.claude/commands/multi-agent/generate.md` (loads `refs/generate-issue.md`) |
119
121
  | `stack` | `$HOME/.claude/commands/multi-agent/stack.md` |
120
122
  | `language` | Handled inline - set/show prompt language in preferences |
121
123
  | SwiftUI component task (iOS) | `$HOME/.claude/commands/multi-agent/refs/swiftui-guide.md` |
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env bash
2
+ # figma-mcp-refresh.sh - silent Figma MCP (figu_) token renewal via OAuth refresh grant.
3
+ #
4
+ # Used by the Phase 0 token pre-flight (refs/phases/phase-0-init.md Step 0.7) and the
5
+ # expired-token decision (refs/keychain.md). Never prints token values - status lines only.
6
+ #
7
+ # Flow:
8
+ # 1. Resolve the access-token Keychain name from prefs.global.keychainMapping.figma_mcp.
9
+ # 2. Read the refresh token from "<name>_Refresh" (written by the user's generation script).
10
+ # 3. Read client credentials from .figma-oauth.json next to prefs.global.tokenScripts.figma_mcp.
11
+ # 4. POST grant_type=refresh_token to the Figma token endpoint.
12
+ # 5. On success save the new access token (and rotated refresh token) back to the Keychain.
13
+ #
14
+ # Exit codes:
15
+ # 0 renewed (access token replaced in Keychain)
16
+ # 1 refresh grant rejected by Figma (refresh token dead - run the generation script)
17
+ # 2 prerequisites missing (no mapping / no refresh entry / no oauth config)
18
+
19
+ set -euo pipefail
20
+
21
+ PREFS_FILE="${PREFS_FILE:-$HOME/.claude/multi-agent-preferences.json}"
22
+ TOKEN_ENDPOINT="https://api.figma.com/v1/oauth/token"
23
+ ACCOUNT="${USER:-$(whoami)}"
24
+
25
+ say() { printf '%s\n' "$*"; }
26
+
27
+ [ -f "$PREFS_FILE" ] || { say "figma-mcp-refresh: prefs file missing ($PREFS_FILE)"; exit 2; }
28
+
29
+ KEY_NAME=$(jq -r '.global.keychainMapping.figma_mcp // empty' "$PREFS_FILE")
30
+ [ -n "$KEY_NAME" ] || { say "figma-mcp-refresh: keychainMapping.figma_mcp not set"; exit 2; }
31
+
32
+ REFRESH_KEY="${KEY_NAME}_Refresh"
33
+ REFRESH_TOKEN=$(security find-generic-password -s "$REFRESH_KEY" -w 2>/dev/null || true)
34
+ [ -n "$REFRESH_TOKEN" ] || { say "figma-mcp-refresh: no refresh token at '$REFRESH_KEY'"; exit 2; }
35
+
36
+ SCRIPT_PATH=$(jq -r '.global.tokenScripts.figma_mcp // empty' "$PREFS_FILE")
37
+ OAUTH_CONFIG=""
38
+ if [ -n "$SCRIPT_PATH" ]; then
39
+ OAUTH_CONFIG="$(dirname "$SCRIPT_PATH")/.figma-oauth.json"
40
+ fi
41
+ [ -n "$OAUTH_CONFIG" ] && [ -f "$OAUTH_CONFIG" ] || { say "figma-mcp-refresh: .figma-oauth.json not found (tokenScripts.figma_mcp unset or sibling config missing)"; exit 2; }
42
+
43
+ CLIENT_ID=$(jq -r '.client_id // empty' "$OAUTH_CONFIG")
44
+ CLIENT_SECRET=$(jq -r '.client_secret // empty' "$OAUTH_CONFIG")
45
+ [ -n "$CLIENT_ID" ] || { say "figma-mcp-refresh: client_id missing in .figma-oauth.json"; exit 2; }
46
+
47
+ CURL_ARGS=(
48
+ --data-urlencode "grant_type=refresh_token"
49
+ --data-urlencode "refresh_token=${REFRESH_TOKEN}"
50
+ --data-urlencode "client_id=${CLIENT_ID}"
51
+ )
52
+ [ -n "$CLIENT_SECRET" ] && CURL_ARGS+=( --data-urlencode "client_secret=${CLIENT_SECRET}" )
53
+
54
+ RESPONSE=$(curl -s --max-time 20 -X POST "$TOKEN_ENDPOINT" \
55
+ -H "Content-Type: application/x-www-form-urlencoded" \
56
+ "${CURL_ARGS[@]}")
57
+
58
+ NEW_ACCESS=$(printf '%s' "$RESPONSE" | jq -r '.access_token // empty' 2>/dev/null || true)
59
+ NEW_REFRESH=$(printf '%s' "$RESPONSE" | jq -r '.refresh_token // empty' 2>/dev/null || true)
60
+ EXPIRES_IN=$(printf '%s' "$RESPONSE" | jq -r '.expires_in // "?"' 2>/dev/null || true)
61
+
62
+ if [ -z "$NEW_ACCESS" ]; then
63
+ ERR=$(printf '%s' "$RESPONSE" | jq -r '.error // .message // "unknown"' 2>/dev/null || echo "unknown")
64
+ say "figma-mcp-refresh: refresh grant rejected ($ERR) - regenerate via tokenScripts.figma_mcp"
65
+ exit 1
66
+ fi
67
+
68
+ # Save new access token first, then the rotated refresh token - order matters so a
69
+ # mid-write failure never leaves us with a lost refresh token AND a stale access token.
70
+ security delete-generic-password -s "$KEY_NAME" -a "$ACCOUNT" >/dev/null 2>&1 || true
71
+ security add-generic-password -s "$KEY_NAME" -a "$ACCOUNT" -w "$NEW_ACCESS" -T "" -U
72
+
73
+ if [ -n "$NEW_REFRESH" ]; then
74
+ security delete-generic-password -s "$REFRESH_KEY" -a "$ACCOUNT" >/dev/null 2>&1 || true
75
+ security add-generic-password -s "$REFRESH_KEY" -a "$ACCOUNT" -w "$NEW_REFRESH" -T "" -U
76
+ fi
77
+
78
+ say "figma-mcp-refresh: renewed '$KEY_NAME' (expires_in=${EXPIRES_IN}s, refresh $( [ -n "$NEW_REFRESH" ] && echo rotated || echo unchanged ))"
79
+ exit 0
@@ -14,6 +14,7 @@
14
14
  "firebase": null,
15
15
  "jenkins": null
16
16
  },
17
+ "tokenScripts": {},
17
18
  "platformIdentityRouting": {},
18
19
  "recentProjects": [],
19
20
  "recentBranches": {},
@@ -153,4 +154,4 @@
153
154
  "overrides": {}
154
155
  }
155
156
  }
156
- }
157
+ }
@@ -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,119 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-generate-issue.sh - contract for /multi-agent:generate.
3
+ #
4
+ # Verifies:
5
+ # 1. All 3 files exist (1 command, shared ref, 1 Copilot/plugin SKILL.md).
6
+ # 2. Frontmatter keys present on both surfaces.
7
+ # 3. Command delegates to refs/generate-issue.md and asks the 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. Standard template vocab: Task/Story (Scope/Acceptance/Test Scenarios), Bug (Steps/Expected/Actual/Environment), Swagger.
12
+ # 8. Registration: dispatcher rows, inventory lists, count consistency (36).
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="$ROOT/pipeline/commands/multi-agent/generate.md"
20
+ REF="$ROOT/pipeline/commands/multi-agent/refs/generate-issue.md"
21
+ SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-generate/SKILL.md"
22
+ DISPATCHER="$ROOT/pipeline/commands/multi-agent.md"
23
+ CONTRACT="$ROOT/pipeline/commands/multi-agent/refs/cross-cli-contract.md"
24
+ SYNC="$ROOT/pipeline/commands/multi-agent/sync.md"
25
+ HELP="$ROOT/pipeline/commands/multi-agent/help.md"
26
+
27
+ PASS=0
28
+ FAIL=0
29
+ pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
30
+ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
31
+
32
+ echo "→ 1. files exist"
33
+ for f in "$CMD" "$REF" "$SKILL"; do
34
+ if [ -f "$f" ]; then pass "exists: ${f#$ROOT/}"; else fail "missing: ${f#$ROOT/}"; fi
35
+ done
36
+ # old split commands must be gone
37
+ for f in "$ROOT/pipeline/commands/multi-agent/generate-task.md" "$ROOT/pipeline/commands/multi-agent/generate-bug.md"; do
38
+ if [ -f "$f" ]; then fail "stale file present: ${f#$ROOT/}"; else pass "removed: ${f##*/}"; fi
39
+ done
40
+ [ "$FAIL" -eq 0 ] || { echo "══ generate-issue smoke: $PASS passed, $FAIL failed ══"; exit 1; }
41
+
42
+ echo ""
43
+ echo "→ 2. frontmatter keys"
44
+ head -5 "$CMD" | grep -q '^description:' && pass "description: ${CMD##*/}" || fail "description missing: ${CMD##*/}"
45
+ head -5 "$CMD" | grep -q '^argument-hint:' && pass "argument-hint: ${CMD##*/}" || fail "argument-hint missing: ${CMD##*/}"
46
+ head -8 "$SKILL" | grep -q '^name: multi-agent-generate$' && pass "name: multi-agent-generate" || fail "name wrong: $SKILL"
47
+ head -8 "$SKILL" | grep -q '^language: en' && pass "language en" || fail "language missing: $SKILL"
48
+ head -8 "$SKILL" | grep -q '^user-invocable: true' && pass "user-invocable" || fail "user-invocable missing: $SKILL"
49
+
50
+ echo ""
51
+ echo "→ 3. command delegates to the shared ref and asks the type"
52
+ grep -Fq "refs/generate-issue.md" "$CMD" && pass "ref link present" || fail "ref link missing"
53
+ for t in "Task" "Bug" "Story"; do
54
+ grep -Fq "$t" "$CMD" && pass "type offered: $t" || fail "type missing: $t"
55
+ done
56
+ grep -Fqi "Pick issue type" "$REF" && pass "type-selection step present" || fail "type-selection step missing"
57
+
58
+ echo ""
59
+ echo "→ 4. approval gate documented and unskippable"
60
+ grep -Fq "Full preview + approval gate (never skipped)" "$REF" && pass "gate section present" || fail "gate section missing"
61
+ for marker in "Approve" "Edit" "Cancel" "no bypass" "Nothing was created"; do
62
+ grep -Fqi "$marker" "$REF" && pass "gate marker: $marker" || fail "gate marker missing: $marker"
63
+ done
64
+ grep -Fq "no autopilot variant" "$REF" && pass "no-autopilot rule" || fail "no-autopilot rule missing"
65
+
66
+ echo ""
67
+ echo "→ 5. mining + createmeta + agile endpoints"
68
+ 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
69
+ grep -Fq "$marker" "$REF" && pass "endpoint marker: $marker" || fail "endpoint marker missing: $marker"
70
+ done
71
+ grep -Fq "gh-sprint" "$REF" && pass "generic sprint-field detection" || fail "generic sprint-field detection missing"
72
+
73
+ echo ""
74
+ echo "→ 6. UTF-8 verbatim POST path"
75
+ grep -Fq -- "--rawfile" "$REF" && pass "--rawfile present" || fail "--rawfile missing"
76
+ grep -Fq -- "--data-binary" "$REF" && pass "--data-binary present" || fail "--data-binary missing"
77
+ grep -Fqi "humanizer" "$REF" && pass "humanizer pass referenced" || fail "humanizer missing"
78
+
79
+ echo ""
80
+ echo "→ 7. standard template vocabulary + auto-sizing + swagger"
81
+ for s in "Scope" "Acceptance Criteria" "Test Scenarios"; do
82
+ grep -Fq "$s" "$REF" && pass "task/story section: $s" || fail "task/story section missing: $s"
83
+ done
84
+ for s in "Steps to Reproduce" "Expected Result" "Actual Result" "Environment"; do
85
+ grep -Fq "$s" "$REF" && pass "bug section: $s" || fail "bug section missing: $s"
86
+ done
87
+ grep -Fqi "auto-sizing" "$REF" && pass "auto-sizing rule present" || fail "auto-sizing rule missing"
88
+ grep -Fqi "swagger" "$REF" && pass "swagger context present" || fail "swagger context missing"
89
+
90
+ echo ""
91
+ echo "→ 8. registration + count consistency (36)"
92
+ grep -Fq '| `generate ' "$DISPATCHER" && pass "dispatcher routing: generate" || fail "dispatcher routing missing: generate"
93
+ grep -Fq "generate.md" "$DISPATCHER" && pass "dispatcher loading row" || fail "dispatcher loading row missing"
94
+ grep -Fq "## 1. Command Inventory (36 commands)" "$CONTRACT" && pass "contract count = 36" || fail "contract count wrong"
95
+ grep -Eq '(^|[ ,])generate, help([ ,]|$)' "$CONTRACT" && pass "contract list has generate" || fail "contract list missing generate"
96
+ grep -Fq "**36 commands are synced**" "$SYNC" && pass "sync count = 36" || fail "sync count wrong"
97
+ SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
98
+ grep -Fq "**36 commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = 36" || fail "sync SKILL count wrong (Copilot parity)"
99
+ grep -Fq "generate" "$ROOT/pipeline/skills/shared/core/multi-agent-help/SKILL.md" && pass "help SKILL lists generate" || fail "help SKILL missing generate (Copilot parity)"
100
+ grep -c "multi-agent:generate" "$HELP" | awk '{exit !($1>=2)}' && pass "help.md has EN + TR entries" || fail "help.md entries incomplete"
101
+ # no stale split-command references anywhere in the synced surfaces
102
+ if grep -rqE "generate-task|generate-bug" "$DISPATCHER" "$CONTRACT" "$SYNC" "$HELP" "$CMD" "$REF" "$SKILL"; then
103
+ fail "stale generate-task/generate-bug reference found"
104
+ else
105
+ pass "no stale split-command references"
106
+ fi
107
+
108
+ echo ""
109
+ echo "→ 9. forbidden strings absent"
110
+ MWORD="MAN""DATORY"
111
+ for f in "$CMD" "$REF" "$SKILL"; do
112
+ if grep -q $'—\|§\|…' "$f"; then fail "forbidden unicode punctuation in ${f##*/}"; else pass "punctuation clean: ${f##*/}"; fi
113
+ if grep -q "$MWORD" "$f"; then fail "forbidden word in ${f##*/}"; else pass "vocabulary clean: ${f##*/}"; fi
114
+ 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
115
+ done
116
+
117
+ echo ""
118
+ echo "══ generate-issue smoke: $PASS passed, $FAIL failed ══"
119
+ [ "$FAIL" -eq 0 ]