@mmerterden/multi-agent-pipeline 20.23.2 → 20.25.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 (123) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +8 -6
  3. package/README.tr.md +7 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +7 -6
  7. package/docs/features.md +9 -4
  8. package/install/catalog-history.json +1 -1
  9. package/install/templates/claude-hooks.json +14 -1
  10. package/install/templates/codex-hooks.json +18 -0
  11. package/manifest.json +126 -98
  12. package/package.json +1 -1
  13. package/pipeline/agents/task-clarifier.md +1 -1
  14. package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +4 -2
  15. package/pipeline/commands/multi-agent/deps/SKILL.md +61 -0
  16. package/pipeline/commands/multi-agent/model/SKILL.md +2 -2
  17. package/pipeline/commands/multi-agent/postmortem/SKILL.md +90 -0
  18. package/pipeline/commands/multi-agent/review/SKILL.md +5 -5
  19. package/pipeline/commands/multi-agent/route-on/SKILL.md +1 -1
  20. package/pipeline/commands/multi-agent/setup/SKILL.md +7 -5
  21. package/pipeline/commands/multi-agent/sync/SKILL.md +4 -4
  22. package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +2 -3
  23. package/pipeline/contract/fixtures/runs-failed.json +1 -1
  24. package/pipeline/contract/fixtures/runs-old-schema.json +1 -1
  25. package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
  26. package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
  27. package/pipeline/contract/fixtures/runs-running.json +1 -1
  28. package/pipeline/lib/analysis-jira-write.sh +70 -0
  29. package/pipeline/lib/credential-inventory.sh +5 -5
  30. package/pipeline/lib/decisions.mjs +130 -0
  31. package/pipeline/lib/fetch-crashlytics.sh +6 -8
  32. package/pipeline/lib/model-rung.sh +1 -1
  33. package/pipeline/lib/plan-todos.sh +177 -37
  34. package/pipeline/lib/post-pr-review.sh +4 -6
  35. package/pipeline/lib/route-state.sh +1 -1
  36. package/pipeline/lib/shadow-git.sh +4 -5
  37. package/pipeline/lib/task-graph.mjs +446 -0
  38. package/pipeline/lib/tdd-evidence.mjs +579 -0
  39. package/pipeline/multi-agent-refs/analysis-template-corporate.md +2 -0
  40. package/pipeline/multi-agent-refs/analysis-template.md +4 -2
  41. package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -11
  42. package/pipeline/multi-agent-refs/features/analysis-jira.md +42 -0
  43. package/pipeline/multi-agent-refs/features/autopilot-decisions.md +34 -0
  44. package/pipeline/multi-agent-refs/features/cost-analysis.md +4 -0
  45. package/pipeline/multi-agent-refs/features/dep-bump.md +176 -0
  46. package/pipeline/multi-agent-refs/features/model-fallback.md +21 -14
  47. package/pipeline/multi-agent-refs/features/mutation-probe.md +58 -0
  48. package/pipeline/multi-agent-refs/features/plan-checks.md +67 -0
  49. package/pipeline/multi-agent-refs/features/plan-todos.md +40 -1
  50. package/pipeline/multi-agent-refs/features/rework-escalation.md +49 -0
  51. package/pipeline/multi-agent-refs/features/scope-check.md +1 -0
  52. package/pipeline/multi-agent-refs/help/en.md +2 -0
  53. package/pipeline/multi-agent-refs/help/tr.md +2 -0
  54. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  55. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -3
  56. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +7 -7
  57. package/pipeline/multi-agent-refs/phases/phase-3-review.md +18 -16
  58. package/pipeline/multi-agent-refs/phases/phase-5-report.md +3 -3
  59. package/pipeline/multi-agent-refs/picker-contract.md +3 -2
  60. package/pipeline/multi-agent-refs/response-shape.md +65 -0
  61. package/pipeline/multi-agent-refs/tracker-contract.md +6 -6
  62. package/pipeline/rules/response-shape.md +18 -0
  63. package/pipeline/schemas/agent-state.schema.json +36 -1
  64. package/pipeline/schemas/phases.json +4 -0
  65. package/pipeline/schemas/plan-todos.schema.json +104 -3
  66. package/pipeline/schemas/planning-output.schema.json +46 -0
  67. package/pipeline/schemas/prefs.schema.json +122 -3
  68. package/pipeline/scripts/_code-graph.mjs +2 -2
  69. package/pipeline/scripts/_jsonl.mjs +151 -0
  70. package/pipeline/scripts/_plan-overlap.mjs +231 -0
  71. package/pipeline/scripts/_redact.mjs +124 -10
  72. package/pipeline/scripts/_transcript-hosts.mjs +406 -0
  73. package/pipeline/scripts/analysis-story-tree.mjs +153 -8
  74. package/pipeline/scripts/autopilot-publish.mjs +12 -1
  75. package/pipeline/scripts/build-stack-plugins.mjs +3 -3
  76. package/pipeline/scripts/capture-flush.sh +7 -7
  77. package/pipeline/scripts/code-graph-rules/android.json +1 -1
  78. package/pipeline/scripts/cost-analyze.mjs +32 -43
  79. package/pipeline/scripts/cost-table.json +35 -19
  80. package/pipeline/scripts/decisions-log.mjs +118 -0
  81. package/pipeline/scripts/dep-bump.mjs +1556 -0
  82. package/pipeline/scripts/edit-syntax-gate.mjs +362 -0
  83. package/pipeline/scripts/edit-syntax-gate.sh +32 -0
  84. package/pipeline/scripts/find-polluter.mjs +281 -0
  85. package/pipeline/scripts/gate-ledger.mjs +46 -13
  86. package/pipeline/scripts/gc-abandoned.sh +2 -2
  87. package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
  88. package/pipeline/scripts/git-path.mjs +1 -2
  89. package/pipeline/scripts/host-hooks.mjs +265 -0
  90. package/pipeline/scripts/jira-attach.sh +1 -1
  91. package/pipeline/scripts/keychain.py +3 -3
  92. package/pipeline/scripts/mutation-probe.mjs +484 -0
  93. package/pipeline/scripts/output-quality-check.sh +1 -1
  94. package/pipeline/scripts/phase-tracker.sh +5 -6
  95. package/pipeline/scripts/phase0-exit-gate.mjs +3 -3
  96. package/pipeline/scripts/plan-complexity.mjs +259 -0
  97. package/pipeline/scripts/pre-push-check.sh +3 -6
  98. package/pipeline/scripts/review-decision-gate.mjs +33 -1
  99. package/pipeline/scripts/rework-escalation.mjs +264 -0
  100. package/pipeline/scripts/test-strength.mjs +5 -3
  101. package/pipeline/scripts/transcript-index.mjs +1131 -0
  102. package/pipeline/scripts/uninstall.mjs +90 -4
  103. package/pipeline/scripts/update-issue-progress.sh +4 -6
  104. package/pipeline/scripts/usage-report.mjs +2 -2
  105. package/pipeline/scripts/validate-planning.mjs +48 -6
  106. package/pipeline/skills/.skill-manifest.json +20 -12
  107. package/pipeline/skills/.skills-index.json +23 -1
  108. package/pipeline/skills/shared/README.md +5 -3
  109. package/pipeline/skills/shared/core/multi-agent/SKILL.md +6 -6
  110. package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +4 -2
  111. package/pipeline/skills/shared/core/multi-agent-deps/SKILL.md +60 -0
  112. package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -2
  113. package/pipeline/skills/shared/core/multi-agent-postmortem/SKILL.md +69 -0
  114. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +5 -5
  115. package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +1 -1
  116. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +7 -5
  117. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
  118. package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -3
  119. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +15 -0
  120. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +13 -0
  121. package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +8 -0
  122. package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
  123. package/pipeline/skills/skills-index.md +3 -1
@@ -69,7 +69,7 @@ host_note() {
69
69
  echo "Claude Code: the fable rung is Fable 5. This switch is live here."
70
70
  ;;
71
71
  copilot)
72
- echo "Copilot CLI: Fable 5 is not offered; its personas never sat on this rung."
72
+ echo "Copilot CLI: Fable is not offered; its personas never sat on this rung."
73
73
  echo "This setting is recorded but changes nothing on this host."
74
74
  ;;
75
75
  codex)
@@ -13,11 +13,25 @@
13
13
  # Subcommands:
14
14
  # init <task-id> [title] Initialize with empty todos[] (Phase 2 usually pipes the JSON instead)
15
15
  # set <task-id> <plan-json> Replace the whole plan from a JSON blob on stdin or arg
16
+ # (exit 2 when a dep names no todo id exactly)
16
17
  # start <task-id> <todo-id> Mark in_progress, set startedAt; refuses if deps unsatisfied
17
- # complete <task-id> <todo-id> [notes] Mark completed, set completedAt
18
+ # or the todo awaits re-plan confirmation
19
+ # complete <task-id> <todo-id> [notes] [--no-tests <reason>]
20
+ # Mark completed, set completedAt; refuses what start refuses
21
+ # (unsatisfied deps on a pending todo, re-plan unconfirmed); checks TDD evidence per
22
+ # prefs.global.tddEvidence.mode (off | warn | enforce, default warn)
23
+ # tdd-red <task-id> <todo-id> <results> [--tests a,b] [--already-passing <reason>]
24
+ # Record the RED run (a named test fails) from JUnit XML / xcresult / log
25
+ # (under enforce, --tests needs results that name failing tests)
26
+ # tdd-green <task-id> <todo-id> <results> Record the GREEN run (no failures, RED's tests among them)
18
27
  # fail <task-id> <todo-id> <reason> Mark failed
19
28
  # skip <task-id> <todo-id> <reason> Mark skipped (counts as satisfied for dependents)
20
- # next <task-id> Print next pending todo (deps-respecting); empty if none
29
+ # next <task-id> Print the next ready todo (deps-respecting, ranked by what it unblocks); empty if none, blocked todos named on stderr
30
+ # drift <task-id> <todo-id> [--files <path|->] [--not-done N] [--reason <text>]
31
+ # The todo ended off-plan (files outside its files[],
32
+ # planned items not done, or a stated reason): flag its
33
+ # pending transitive dependents needsReplan; prints JSON
34
+ # confirm-replan <task-id> <todo-id> [note] Confirm a flagged todo's plan still holds, so it can start
21
35
  # list <task-id> Render Markdown checklist
22
36
  # status <task-id> Print one-line summary: "2/5 done, 1 in progress"
23
37
  # show <task-id> Print the full plan JSON
@@ -59,7 +73,7 @@ CMD="${1:-}"
59
73
  shift || true
60
74
 
61
75
  usage() {
62
- sed -n '2,30p' "$0" >&2
76
+ sed -n '2,37p' "$0" >&2
63
77
  exit 64
64
78
  }
65
79
 
@@ -71,10 +85,9 @@ resolve_state() {
71
85
  local task_id="$1"
72
86
  local found=""
73
87
  # multi-repo-pipeline.sh writes agent-state.json under a per-project
74
- # directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/) - not the
75
- # ~/.claude/projects/*/state/ path this used to glob, which no writer in
76
- # the pipeline ever populates.
77
- # This globbed ONLY the nested layout; flat-layout runs resolved to nothing.
88
+ # directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/), other
89
+ # writers write it flat; no writer uses ~/.claude/projects/*/state/.
90
+ # ma_resolve_run_file checks both layouts.
78
91
  found="$(ma_resolve_run_file "$task_id" agent-state.json 2>/dev/null || true)"
79
92
  if [ -z "$found" ]; then
80
93
  echo "plan-todos: agent-state.json not found for task=$task_id" >&2
@@ -115,12 +128,8 @@ do_set() {
115
128
  if [ -z "$plan_blob" ] || [ "$plan_blob" = "-" ]; then
116
129
  plan_blob=$(cat)
117
130
  fi
118
- # A planning-output document is accepted directly and converted here.
119
- #
120
- # The conversion used to live as a jq blob inside phase-1-plan.md, which
121
- # made the mapping from `tasks[]` to `todos[]` a thing two files defined - and
122
- # the phase doc was the copy nothing tested. Accepting both shapes costs four
123
- # lines and removes the second definition.
131
+ # A planning-output document is accepted directly and converted here, so the
132
+ # mapping from `tasks[]` to `todos[]` has one tested definition.
124
133
  if jq -e '.tasks and (.todos | not)' <<<"$plan_blob" >/dev/null 2>&1; then
125
134
  plan_blob=$(jq '{
126
135
  title: (.summary // .title // "plan"),
@@ -129,7 +138,9 @@ do_set() {
129
138
  task: (.title // .subject // ""),
130
139
  status: "pending",
131
140
  deps: (.dependsOn // .blockedBy // [])
132
- } ]
141
+ } + (if (.files | type) == "array" then {files: .files} else {} end)
142
+ + (if (.creates | type) == "array" then {creates: .creates} else {} end)
143
+ + (if .type then {type: .type} else {} end) ]
133
144
  }' <<<"$plan_blob")
134
145
  fi
135
146
 
@@ -144,6 +155,23 @@ do_set() {
144
155
  echo "plan-todos: every todo must be an object with string .id and .task fields" >&2
145
156
  exit 2
146
157
  fi
158
+ # Every dependency must name a todo exactly. Ids are compared as written, so
159
+ # `t1` against a todo `T1` would leave the dependent blocked for good; the
160
+ # error names the id that differs only by case.
161
+ local bad_deps
162
+ bad_deps=$(jq -r '
163
+ [.todos[].id] as $ids
164
+ | [.todos[] | .id as $owner | (.deps // [])[]
165
+ | select((type != "string") or (. as $d | $ids | index($d) | not))
166
+ | . as $d
167
+ | ($ids | map(select(($d | type) == "string" and ascii_downcase == ($d | ascii_downcase)))) as $near
168
+ | "\($owner) depends on \($d | tojson), which is no todo id"
169
+ + (if ($near | length) > 0 then " (did you mean \($near | map(tojson) | join(", "))?)" else "" end)]
170
+ | .[]' <<<"$plan_blob" 2>/dev/null)
171
+ if [ -n "$bad_deps" ]; then
172
+ printf 'plan-todos: %s\n' "$bad_deps" >&2
173
+ exit 2
174
+ fi
147
175
  local state_path; state_path=$(resolve_state "$task_id")
148
176
  local now; now=$(iso_now)
149
177
  local new
@@ -169,6 +197,22 @@ deps_satisfied() {
169
197
  ' <<<"$state" >/dev/null 2>&1
170
198
  }
171
199
 
200
+ refuse_unconfirmed_replan() {
201
+ # $1 state JSON, $2 task_id, $3 todo_id. A todo planned on top of one that
202
+ # drifted waits for a person to confirm its plan: exits 2 until confirm-replan.
203
+ local replan
204
+ replan=$(jq -r --arg id "$3" '
205
+ (.plan.todos // []) | map(select(.id == $id))[0].needsReplan
206
+ | select(. != null and (.confirmedAt // "") == "")
207
+ | "\(.because)\t\(.reason // "")"
208
+ ' <<<"$1")
209
+ if [ -n "$replan" ]; then
210
+ printf "plan-todos: '%s' awaits re-plan confirmation: %s drifted (%s). Check its plan, then: plan-todos.sh confirm-replan %s %s\n" \
211
+ "$3" "${replan%%$'\t'*}" "${replan#*$'\t'}" "$2" "$3" >&2
212
+ exit 2
213
+ fi
214
+ }
215
+
172
216
  do_start() {
173
217
  local task_id="${1:-}"; local todo_id="${2:-}"
174
218
  [ -z "$task_id" ] || [ -z "$todo_id" ] && { echo "usage: start <task-id> <todo-id>" >&2; exit 64; }
@@ -184,6 +228,7 @@ do_start() {
184
228
  echo "plan-todos: deps for '$todo_id' not satisfied (still pending/in_progress/failed)" >&2
185
229
  exit 2
186
230
  fi
231
+ refuse_unconfirmed_replan "$state" "$task_id" "$todo_id"
187
232
  local now; now=$(iso_now)
188
233
  local new
189
234
  new=$(jq --arg id "$todo_id" --arg t "$now" '
@@ -202,18 +247,85 @@ require_todo() {
202
247
  fi
203
248
  }
204
249
 
250
+ TDD_EVIDENCE="$_MA_RP_HERE/tdd-evidence.mjs"
251
+
252
+ tdd_mode() {
253
+ local prefs="${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}" mode
254
+ mode=$(jq -r '.global.tddEvidence.mode // "warn"' "$prefs" 2>/dev/null) || mode=warn
255
+ case "$mode" in off|warn|enforce) printf '%s' "$mode" ;; *) printf 'warn' ;; esac
256
+ }
257
+
258
+ write_tdd() {
259
+ # $1 state path, $2 todo_id, $3 tdd record JSON
260
+ local new
261
+ new=$(jq --arg id "$2" --argjson r "$3" '
262
+ .plan.todos |= map(if .id == $id then .tdd = $r else . end)
263
+ ' "$1")
264
+ write_state "$1" "$new"
265
+ }
266
+
267
+ do_tdd_run() {
268
+ # $1 red|green, then <task-id> <todo-id> <results> [flags passed to tdd-evidence.mjs]
269
+ local kind="$1"; shift
270
+ local task_id="${1:-}"; local todo_id="${2:-}"; local results="${3:-}"
271
+ [ -z "$task_id" ] || [ -z "$todo_id" ] || [ -z "$results" ] && { echo "usage: tdd-$kind <task-id> <todo-id> <results-file> [flags]" >&2; exit 64; }
272
+ shift 3
273
+ # Only the caller-owned flags pass through; --state, --todo, --results and
274
+ # --mode come from the pipeline, so a trailing copy cannot override them.
275
+ local extra=()
276
+ while [ $# -gt 0 ]; do
277
+ case "$1" in
278
+ --tests|--already-passing)
279
+ [ $# -ge 2 ] || { echo "tdd-$kind: $1 needs a value" >&2; exit 64; }
280
+ extra+=("$1" "$2"); shift 2 ;;
281
+ *) echo "tdd-$kind: unsupported flag '$1' (only --tests and --already-passing)" >&2; exit 64 ;;
282
+ esac
283
+ done
284
+ local state_path; state_path=$(resolve_state "$task_id")
285
+ require_todo "$state_path" "$todo_id"
286
+ local record mode_flag=()
287
+ [ "$kind" = "red" ] && mode_flag=(--mode "$(tdd_mode)")
288
+ record=$(node "$TDD_EVIDENCE" "$kind" --state "$state_path" --todo "$todo_id" --results "$results" ${mode_flag[@]+"${mode_flag[@]}"} ${extra[@]+"${extra[@]}"}) || exit 2
289
+ write_tdd "$state_path" "$todo_id" "$record"
290
+ }
291
+
205
292
  do_complete() {
206
- local task_id="${1:-}"; local todo_id="${2:-}"; local notes="${3:-}"
207
- [ -z "$task_id" ] || [ -z "$todo_id" ] && { echo "usage: complete <task-id> <todo-id> [notes]" >&2; exit 64; }
293
+ local task_id="${1:-}"; local todo_id="${2:-}"; local notes="" no_tests=""
294
+ [ -z "$task_id" ] || [ -z "$todo_id" ] && { echo "usage: complete <task-id> <todo-id> [notes] [--no-tests <reason>]" >&2; exit 64; }
295
+ shift 2
296
+ while [ $# -gt 0 ]; do
297
+ case "$1" in
298
+ --no-tests) no_tests="${2:-}"; [ -z "$no_tests" ] && { echo "complete: --no-tests needs a reason" >&2; exit 64; }; shift 2 ;;
299
+ *) notes="$1"; shift ;;
300
+ esac
301
+ done
208
302
  local state_path; state_path=$(resolve_state "$task_id")
209
303
  require_todo "$state_path" "$todo_id"
304
+ local state; state=$(cat "$state_path")
305
+ refuse_unconfirmed_replan "$state" "$task_id" "$todo_id"
306
+ # A todo that never started skipped the deps check start makes.
307
+ if [ "$(jq -r --arg id "$todo_id" '(.plan.todos // []) | map(select(.id == $id))[0].status // "pending"' <<<"$state")" = "pending" ] \
308
+ && ! deps_satisfied "$state" "$todo_id"; then
309
+ echo "plan-todos: deps for '$todo_id' not satisfied (still pending/in_progress/failed); it cannot complete before they do" >&2
310
+ exit 2
311
+ fi
312
+ local verdict action
313
+ verdict=$(node "$TDD_EVIDENCE" check --state "$state_path" --todo "$todo_id" --mode "$(tdd_mode)" \
314
+ ${no_tests:+--no-tests "$no_tests"}) || exit 2
315
+ action=$(jq -r '.action' <<<"$verdict")
316
+ if [ "$action" = "refuse" ]; then
317
+ echo "plan-todos: $(jq -r '.message' <<<"$verdict") - tddEvidence.mode is enforce" >&2
318
+ exit 2
319
+ fi
320
+ [ "$action" = "warn" ] && echo "plan-todos: warning: $(jq -r '.message' <<<"$verdict"); recorded as unverified" >&2
210
321
  local now; now=$(iso_now)
211
322
  local new
212
- new=$(jq --arg id "$todo_id" --arg t "$now" --arg n "$notes" '
323
+ new=$(jq --arg id "$todo_id" --arg t "$now" --arg n "$notes" --argjson v "$verdict" '
213
324
  .plan.todos |= map(
214
325
  if .id == $id then
215
326
  .status = "completed" | .completedAt = $t
216
327
  | if ($n != "") then .notes = $n else . end
328
+ | if ($v.tdd != null) then .tdd = $v.tdd else . end
217
329
  else . end
218
330
  )
219
331
  ' "$state_path")
@@ -254,25 +366,47 @@ do_next() {
254
366
  local task_id="${1:-}"
255
367
  [ -z "$task_id" ] && { echo "usage: next <task-id>" >&2; exit 64; }
256
368
  local state_path; state_path=$(resolve_state "$task_id")
257
- # Pick the first pending todo whose deps are all completed|skipped.
258
- jq -r '
259
- (.plan.todos // []) as $all
260
- | $all
261
- | map(select(.status == "pending"))
262
- | map(
263
- . as $t
264
- | select(
265
- (.deps // [])
266
- | all(
267
- . as $d
268
- | $all | map(select(.id == $d))[0] // null
269
- | . != null and (.status == "completed" or .status == "skipped")
270
- )
271
- )
272
- )
273
- | (.[0] // empty)
274
- | if . == null or . == "" then "" else (.id // "") + "\t" + (.task // "") end
275
- ' "$state_path"
369
+ # Ready set ranked by the work each todo unblocks; blocked todos are named on stderr.
370
+ node "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/task-graph.mjs" next "$state_path" --task-id "$task_id"
371
+ }
372
+
373
+ do_drift() {
374
+ local task_id="${1:-}"; local todo_id="${2:-}"
375
+ [ -z "$task_id" ] || [ -z "$todo_id" ] && { echo "usage: drift <task-id> <todo-id> [--files <path|->] [--not-done N] [--reason <text>]" >&2; exit 64; }
376
+ shift 2
377
+ local state_path; state_path=$(resolve_state "$task_id")
378
+ require_todo "$state_path" "$todo_id"
379
+ local result rc=0
380
+ result=$(node "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/task-graph.mjs" drift "$state_path" "$todo_id" "$@") || rc=$?
381
+ [ "$rc" -eq 0 ] || exit "$rc"
382
+ if [ "$(jq -r '.drifted' <<<"$result")" = "true" ]; then
383
+ local new
384
+ new=$(jq --argjson r "$result" '.plan.todos = $r.todos' "$state_path")
385
+ write_state "$state_path" "$new"
386
+ fi
387
+ jq -c 'del(.todos)' <<<"$result"
388
+ }
389
+
390
+ do_confirm_replan() {
391
+ local task_id="${1:-}"; local todo_id="${2:-}"; local note="${3:-}"
392
+ [ -z "$task_id" ] || [ -z "$todo_id" ] && { echo "usage: confirm-replan <task-id> <todo-id> [note]" >&2; exit 64; }
393
+ local state_path; state_path=$(resolve_state "$task_id")
394
+ require_todo "$state_path" "$todo_id"
395
+ if ! jq -e --arg id "$todo_id" '(.plan.todos // []) | map(select(.id == $id))[0].needsReplan | type == "object"' "$state_path" >/dev/null 2>&1; then
396
+ echo "plan-todos: '$todo_id' has no re-plan flag to confirm" >&2
397
+ exit 2
398
+ fi
399
+ local now; now=$(iso_now)
400
+ local new
401
+ new=$(jq --arg id "$todo_id" --arg t "$now" --arg n "$note" '
402
+ .plan.todos |= map(
403
+ if .id == $id then
404
+ .needsReplan.confirmedAt = $t
405
+ | if ($n != "") then .needsReplan.note = $n else . end
406
+ else . end
407
+ )
408
+ ' "$state_path")
409
+ write_state "$state_path" "$new"
276
410
  }
277
411
 
278
412
  do_list() {
@@ -294,7 +428,9 @@ do_list() {
294
428
  | ($marks[.status] // "- [ ]") + " **" + (.id // "") + "** " + (.task // "")
295
429
  + (if (.notes // "") != "" then " _(" + .notes + ")_" else "" end)
296
430
  + (if (.failureReason // "") != "" then " _**failed:** " + .failureReason + "_" else "" end)
297
- + (if (.skipReason // "") != "" then " _**skipped:** " + .skipReason + "_" else "" end))
431
+ + (if (.skipReason // "") != "" then " _**skipped:** " + .skipReason + "_" else "" end)
432
+ + (if (.needsReplan // null) != null and (.needsReplan.confirmedAt // "") == ""
433
+ then " _**needs re-plan:** " + (.needsReplan.because // "?") + " drifted_" else "" end))
298
434
  )
299
435
  | .[]
300
436
  ' "$state_path"
@@ -332,9 +468,13 @@ case "$CMD" in
332
468
  set) do_set "$@" ;;
333
469
  start) do_start "$@" ;;
334
470
  complete) do_complete "$@" ;;
471
+ tdd-red) do_tdd_run red "$@" ;;
472
+ tdd-green) do_tdd_run green "$@" ;;
335
473
  fail) do_fail "$@" ;;
336
474
  skip) do_skip "$@" ;;
337
475
  next) do_next "$@" ;;
476
+ drift) do_drift "$@" ;;
477
+ confirm-replan) do_confirm_replan "$@" ;;
338
478
  list) do_list "$@" ;;
339
479
  status) do_status "$@" ;;
340
480
  show) do_show "$@" ;;
@@ -107,12 +107,10 @@ if [ -z "$TASK_ID" ]; then
107
107
  fi
108
108
 
109
109
  # Resolve agent-state.json. multi-repo-pipeline.sh writes it under a
110
- # per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/)
111
- # - not the ~/.claude/projects/*/state/ path this used to glob, which no
112
- # writer in the pipeline ever populates.
113
- # This globbed ONLY the nested layout. phase-tracker.sh and most Phase 0 paths
114
- # write flat, so on a real machine the majority of runs resolved to nothing and
115
- # this script reported "no agent-state.json" for a run whose state existed.
110
+ # per-project directory ($HOME/.claude/logs/multi-agent/{project}/{taskId}/),
111
+ # while phase-tracker.sh and most Phase 0 paths write it flat; no writer uses
112
+ # ~/.claude/projects/*/state/. ma_resolve_run_file checks both layouts, so a
113
+ # run whose state exists is always found.
116
114
  AGENT_STATE="$(ma_resolve_run_file "$TASK_ID" agent-state.json 2>/dev/null || true)"
117
115
 
118
116
  if [ -z "$AGENT_STATE" ]; then
@@ -77,7 +77,7 @@ free_savings() {
77
77
  since=$(( $(date +%s) - 7 * 86400 ))
78
78
  jq -rs --argjson since "$since" --slurpfile ct "$COST_TABLE" '
79
79
  map(select((.ts | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601? // 0) >= $since))
80
- | ($ct[0].prices.haiku // {inPerMtok: 1, outPerMtok: 5}) as $h
80
+ | ($ct[0].prices.haiku // {inPerMtok: 0.5, outPerMtok: 2.5}) as $h
81
81
  | {
82
82
  n: length,
83
83
  ok: (map(select(.ok and (.cached | not))) | length),
@@ -227,11 +227,10 @@ do_prune() {
227
227
  printf 'shadow-git: pruned shadow repo for %s\n' "$task_id" >&2
228
228
  else
229
229
  [ ! -d "$sd/.git" ] && { err "no shadow for $task_id"; exit 1; }
230
- # Retention never touches a real work-tree: this used to run
231
- # `reset --hard` with --work-tree=$(pwd), silently rewriting whatever
232
- # directory prune happened to be invoked from, and it kept the OLDEST
233
- # commit inside the window while discarding every newer one - backwards
234
- # for a "keep the last N days" policy. Every git call below is
230
+ # Retention never touches a real work-tree: a `reset --hard` with
231
+ # --work-tree=$(pwd) would silently rewrite whatever directory prune is
232
+ # invoked from. It keeps every commit inside the window and drops the
233
+ # older ones, as a "keep the last N days" policy requires. Every git call below is
235
234
  # git-dir-only plumbing (log/rev-parse/commit-tree/update-ref/gc - no
236
235
  # checkout, no reset, no clean); the one --work-tree `sg` still wants
237
236
  # points at $sd itself, which prune already owns.