@gobing-ai/spur 0.3.98 → 0.4.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 (120) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +17 -0
  3. package/config/decisions/task-pipeline.yaml +55 -0
  4. package/config/plugin-scripts.json +15 -0
  5. package/config/rules/boundary/sp-runtime-path.yaml +2 -0
  6. package/config/rules/quality/coverage-gate.yaml +5 -3
  7. package/config/rules/strict/runtime-boundaries.yaml +11 -0
  8. package/config/rules/structure/protected-files.yaml +3 -0
  9. package/config/templates/task/brainstorm.md +1 -1
  10. package/config/templates/task/feature-impl.md +1 -1
  11. package/config/templates/task/issue.md +1 -1
  12. package/config/templates/task/meta.md +1 -1
  13. package/config/templates/task/review.md +1 -1
  14. package/config/templates/task/standard.md +1 -1
  15. package/config/workflows/task-pipeline.yaml +178 -18
  16. package/package.json +9 -9
  17. package/plugins/sp/README.md +14 -1
  18. package/plugins/sp/agents/super-reviewer.md +25 -11
  19. package/plugins/sp/commands/dev-changelog.md +5 -4
  20. package/plugins/sp/commands/dev-review-session.md +12 -7
  21. package/plugins/sp/commands/dev-run.md +5 -2
  22. package/plugins/sp/commands/dev-runall.md +5 -2
  23. package/plugins/sp/hooks/agent-lifecycle.ts +155 -0
  24. package/plugins/sp/hooks/fleet-guest-stop.ts +180 -0
  25. package/plugins/sp/hooks/hooks.json +45 -1
  26. package/plugins/sp/hooks/pi/guard-extension.ts +21 -0
  27. package/plugins/sp/lib/history-anatomy.generated.mjs +5 -0
  28. package/plugins/sp/lib/idea-handoff.generated.mjs +68 -10
  29. package/plugins/sp/lib/inline-run.generated.d.mts +1 -0
  30. package/plugins/sp/lib/inline-run.generated.mjs +83 -20
  31. package/plugins/sp/lib/residual-scan.generated.d.mts +5 -0
  32. package/plugins/sp/lib/residual-scan.generated.mjs +45 -5
  33. package/plugins/sp/lib/transcript.ts +160 -0
  34. package/plugins/sp/plugin.json +1 -1
  35. package/plugins/sp/scripts/history-anatomy-cache.mjs +5 -0
  36. package/plugins/sp/scripts/inline-run-setup.mjs +21 -20
  37. package/plugins/sp/scripts/inline-run-setup.ts +25 -31
  38. package/plugins/sp/scripts/persist-out-check.mjs +164 -0
  39. package/plugins/sp/scripts/persist-out-check.ts +185 -0
  40. package/plugins/sp/scripts/residual-scan.mjs +52 -15
  41. package/plugins/sp/scripts/residual-scan.ts +11 -11
  42. package/plugins/sp/scripts/run-summary.mjs +286 -0
  43. package/plugins/sp/scripts/run-summary.ts +241 -0
  44. package/plugins/sp/scripts/session-timeline.mjs +234 -0
  45. package/plugins/sp/scripts/session-timeline.ts +152 -0
  46. package/plugins/sp/scripts/task-diffstat.mjs +17 -5
  47. package/plugins/sp/scripts/task-diffstat.ts +25 -4
  48. package/plugins/sp/skills/code-verification/SKILL.md +8 -0
  49. package/plugins/sp/skills/code-verification/references/verdict-schema.md +17 -1
  50. package/plugins/sp/skills/fleet-join/SKILL.md +63 -0
  51. package/plugins/sp/skills/session-review/SKILL.md +46 -24
  52. package/plugins/sp/skills/spur-cli/SKILL.md +1 -0
  53. package/plugins/sp/skills/spur-cli/references/agent.md +108 -6
  54. package/plugins/sp/skills/spur-cli/references/decision.md +97 -0
  55. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +22 -0
  56. package/plugins/sp/skills/spur-composer/SKILL.md +27 -1
  57. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +40 -7
  58. package/plugins/sp/skills/spur-dev/references/dev-operations.md +28 -16
  59. package/plugins/sp/skills/spur-dev/references/execution-batch.md +278 -42
  60. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +17 -1
  61. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +16 -4
  62. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +139 -10
  63. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +6 -0
  64. package/schemas/decision-catalog.schema.json +226 -0
  65. package/schemas/spur-config.schema.json +25 -0
  66. package/spur.js +8856 -2564
  67. package/web/_astro/BoardApp.Bzr4OXrW.js +324 -0
  68. package/web/_astro/{BoardApp.Dflq5a8x.js → BoardApp.VeoxxLBy.js} +1 -1
  69. package/web/_astro/{TaskDetail.CK7oXddP.js → TaskDetail.LAyR_Pvn.js} +1 -1
  70. package/web/_astro/{arc.BIqkDATc.js → arc.f4e0OYLg.js} +1 -1
  71. package/web/_astro/{architectureDiagram-3BPJPVTR.D0B3SvWv.js → architectureDiagram-3BPJPVTR.SSIvxi-Z.js} +1 -1
  72. package/web/_astro/{blockDiagram-GPEHLZMM.C2YyKQmL.js → blockDiagram-GPEHLZMM.DSXIHOFf.js} +1 -1
  73. package/web/_astro/{c4Diagram-AAUBKEIU.DmtTYrt_.js → c4Diagram-AAUBKEIU.DG9Ubu85.js} +1 -1
  74. package/web/_astro/channel.CUFdtWhm.js +1 -0
  75. package/web/_astro/{chunk-2J33WTMH.HsRXbh7l.js → chunk-2J33WTMH.C7QU88Qh.js} +1 -1
  76. package/web/_astro/{chunk-4BX2VUAB.obE_P-R3.js → chunk-4BX2VUAB.26Z94xBj.js} +1 -1
  77. package/web/_astro/{chunk-55IACEB6.CjWTF5mz.js → chunk-55IACEB6.DYRiBkqw.js} +1 -1
  78. package/web/_astro/{chunk-727SXJPM.DMo799Ax.js → chunk-727SXJPM.D5Vfv5pe.js} +1 -1
  79. package/web/_astro/{chunk-AQP2D5EJ.-cE6JOKZ.js → chunk-AQP2D5EJ.DXHV47bN.js} +1 -1
  80. package/web/_astro/{chunk-FMBD7UC4.D6Rw6HIk.js → chunk-FMBD7UC4.D1suAfOs.js} +1 -1
  81. package/web/_astro/{chunk-ND2GUHAM.d5rBD73Z.js → chunk-ND2GUHAM.D6BmdHEY.js} +1 -1
  82. package/web/_astro/{chunk-QZHKN3VN.r3ox40Q3.js → chunk-QZHKN3VN.rnPU-Ak-.js} +1 -1
  83. package/web/_astro/{classDiagram-4FO5ZUOK.CGGbvjjJ.js → classDiagram-4FO5ZUOK.CvEfXIo1.js} +1 -1
  84. package/web/_astro/{classDiagram-v2-Q7XG4LA2.CGGbvjjJ.js → classDiagram-v2-Q7XG4LA2.CvEfXIo1.js} +1 -1
  85. package/web/_astro/{cose-bilkent-S5V4N54A.CmKtPd4l.js → cose-bilkent-S5V4N54A.DkMisLSm.js} +1 -1
  86. package/web/_astro/{cynefin-OW5HDTMX.QNg5GXs8.js → cynefin-OW5HDTMX.C77Q-pF6.js} +1 -1
  87. package/web/_astro/{dagre-BM42HDAG.B1SabsjC.js → dagre-BM42HDAG.YaV8ce27.js} +1 -1
  88. package/web/_astro/{diagram-2AECGRRQ.RtDbNj89.js → diagram-2AECGRRQ.CUyGEKge.js} +1 -1
  89. package/web/_astro/{diagram-5GNKFQAL.IzZpuM-Y.js → diagram-5GNKFQAL.CRgd8dgj.js} +1 -1
  90. package/web/_astro/{diagram-KO2AKTUF.DPTF_fgv.js → diagram-KO2AKTUF.Bmzys9hq.js} +1 -1
  91. package/web/_astro/{diagram-LMA3HP47.CCyza39P.js → diagram-LMA3HP47.BvmJY4Lw.js} +1 -1
  92. package/web/_astro/{diagram-OG6HWLK6.BXAi8zcZ.js → diagram-OG6HWLK6.0hEhD30d.js} +1 -1
  93. package/web/_astro/{erDiagram-TEJ5UH35.DUDX7_MR.js → erDiagram-TEJ5UH35.Bjt8lrnm.js} +1 -1
  94. package/web/_astro/{flowDiagram-I6XJVG4X.C63QTh3N.js → flowDiagram-I6XJVG4X.CJZTjRDV.js} +1 -1
  95. package/web/_astro/{ganttDiagram-6RSMTGT7.CnlZJSwG.js → ganttDiagram-6RSMTGT7._VqoVafc.js} +1 -1
  96. package/web/_astro/{gitGraphDiagram-PVQCEYII.CTqIrOPC.js → gitGraphDiagram-PVQCEYII.CEDb7tVo.js} +1 -1
  97. package/web/_astro/index.Dg_3bOaJ.css +1 -0
  98. package/web/_astro/{infoDiagram-5YYISTIA.BYMiDFh9.js → infoDiagram-5YYISTIA.CgwifPpK.js} +1 -1
  99. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CRiQmI-z.js → ishikawaDiagram-YF4QCWOH.B99wx2J1.js} +1 -1
  100. package/web/_astro/{journeyDiagram-JHISSGLW.YQgWBsEU.js → journeyDiagram-JHISSGLW.2hRwJbEo.js} +1 -1
  101. package/web/_astro/{kanban-definition-UN3LZRKU.1ECnrrK1.js → kanban-definition-UN3LZRKU.BahJkKNC.js} +1 -1
  102. package/web/_astro/{linear.DzVA71yT.js → linear.DguUUe6V.js} +1 -1
  103. package/web/_astro/{mermaid.core.BK7P2Sr_.js → mermaid.core.CvgfmqVr.js} +4 -4
  104. package/web/_astro/{mindmap-definition-RKZ34NQL.BvXyWZK7.js → mindmap-definition-RKZ34NQL.B3qEA3x9.js} +1 -1
  105. package/web/_astro/{pieDiagram-4H26LBE5.BiXk1TW3.js → pieDiagram-4H26LBE5.FlWA-_ru.js} +1 -1
  106. package/web/_astro/{quadrantDiagram-W4KKPZXB.krvBH8lh.js → quadrantDiagram-W4KKPZXB.9bVn35X7.js} +1 -1
  107. package/web/_astro/{requirementDiagram-4Y6WPE33.ih1gL25c.js → requirementDiagram-4Y6WPE33.ChykIhct.js} +1 -1
  108. package/web/_astro/{sankeyDiagram-5OEKKPKP.CpUxkv4u.js → sankeyDiagram-5OEKKPKP.Z2Y3hkYb.js} +1 -1
  109. package/web/_astro/{sequenceDiagram-3UESZ5HK.clAfeN7y.js → sequenceDiagram-3UESZ5HK.J7JsHWDt.js} +1 -1
  110. package/web/_astro/{stateDiagram-AJRCARHV.DpXWb8-m.js → stateDiagram-AJRCARHV.DAPbiCSS.js} +1 -1
  111. package/web/_astro/{stateDiagram-v2-BHNVJYJU.ClZb9GIH.js → stateDiagram-v2-BHNVJYJU.D2jvSi0q.js} +1 -1
  112. package/web/_astro/{timeline-definition-PNZ67QCA.BLcX5dh7.js → timeline-definition-PNZ67QCA.psW2ci29.js} +1 -1
  113. package/web/_astro/{vennDiagram-CIIHVFJN.oGgpKMpx.js → vennDiagram-CIIHVFJN.DlILYVmg.js} +1 -1
  114. package/web/_astro/{wardleyDiagram-YWT4CUSO.BPHtMtt-.js → wardleyDiagram-YWT4CUSO.xNWwLJnn.js} +1 -1
  115. package/web/_astro/{xychartDiagram-2RQKCTM6.BNZOPYSk.js → xychartDiagram-2RQKCTM6.EU_BuH7V.js} +1 -1
  116. package/web/board-runtime.json +1 -1
  117. package/web/index.html +2 -2
  118. package/web/_astro/BoardApp.CYbe-OTb.js +0 -322
  119. package/web/_astro/channel.JuKVyuhZ.js +0 -1
  120. package/web/_astro/index.CbNz17Sx.css +0 -1
@@ -424,7 +424,8 @@ one or more tasks sit on an operator question (escalate, 0933) or an approval ga
424
424
  (cycle / unknown selector) — emit a structured report. The report is the orchestrator's sole
425
425
  output; it does not mutate the corpus (the pipeline's `record` step already wrote per-task
426
426
  results). A `paused` task is non-terminal — the report marks it `paused` (never `done`) and names
427
- the resume command.
427
+ the resume command. Unless `--no-summary`, follow it with the measured
428
+ [execution summary](dev-operations.md#execution-summary), one `--progress <wbs>=<file>` per attempted run.
428
429
 
429
430
  ```
430
431
  ## Batch Report — <selector>
@@ -626,6 +627,20 @@ resolution runs, the loop executes with the worktree as process cwd, and a termi
626
627
  retains after the batch report is emitted. Steps 1–5 themselves are not modified — only their cwd
627
628
  differs.
628
629
 
630
+ **Per-call tree pinning (task 1058).** A host shell call starts in an arbitrary directory and
631
+ inherits no cwd from the previous call, so "the loop runs with the worktree as process cwd" is a
632
+ bootstrap convenience, never a carried guarantee. Every host command boundary in this lifecycle —
633
+ corpus reads/writes, the WT-3b commit, WT-4's merge and evidence persistence, the WT-4a
634
+ persist-out — re-selects its intended tree within the same call via the canonical per-call pin
635
+ protocol ([inline-pipeline-driver.md](inline-pipeline-driver.md) § "Per-call execution-tree
636
+ pin"): `cd --` to the quoted absolute tree, verify `pwd -P` against the recorded physical tree,
637
+ verify Git top-level and expected branch, then execute the already-resolved Spur invocation. The
638
+ identity is consumed, not re-derived: it is the WT-3 marker's confirmed `path` + `branch` (or the
639
+ resolved worktree's physical path + checked-out branch in reuse mode). The one deliberate tree
640
+ change is WT-4: the terminal merge actions select the **invoking** tree, not the execution tree —
641
+ and pin it explicitly the same way. FF ancestry, marker and cleanup sequencing remain owned by
642
+ 1059.
643
+
629
644
  **Portability (R10).** Use portable `git worktree` commands only. Do **not** depend on the Claude
630
645
  Code `EnterWorktree`/`ExitWorktree` tools — the `sp` plugin ships to Codex, Gemini CLI, pi, omp, and
631
646
  OpenCode. The underlying git mechanics (create / list / remove / prune, sibling-directory naming,
@@ -730,6 +745,34 @@ existing batch loop (Steps 1–5) with the worktree as process cwd. `spur workfl
730
745
  from the process (`apps/cli/src/commands/workflow.ts:124`), so no CLI change is needed — `cd` into
731
746
  the worktree directory before launching the loop.
732
747
 
748
+ #### Evidence staging before the first task (create and reuse mode)
749
+
750
+ `.spur/memory/evidence/` and `.spur/run/` are **untracked per-tree** planes, so a fresh worktree
751
+ starts with neither while the invoking tree holds every earlier task's verdict. Both
752
+ `preflightFeature` (`plugins/sp/scripts/wrapup-steps.ts` → `spur feature check --strict --as done`
753
+ with cwd = the worktree) and `runMetrics` resolve verdict artifacts relative to the tree they run
754
+ in, so without this step an already-`done` linked task reads as missing evidence
755
+ (`L4.evidence-not-recoverable`, `L4.scenario-unverified`) and its metrics row degrades to
756
+ `UNKNOWN`.
757
+
758
+ Stage the invoking tree's verdict artifacts into the worktree **before the first task runs** —
759
+ from the invoking tree (cwd), with `$WT` the resolved absolute worktree path. `cp -n` is
760
+ load-bearing: reuse mode may be re-entering a tree that already owns divergence-checked evidence,
761
+ and this step must never clobber it. Only `*-verdict.json` travels in each direction; receipts and
762
+ other scratch artifacts are not staged, so WT-4a's identity classification is unaffected.
763
+
764
+ ```bash
765
+ # Run from the INVOKING tree; $WT is the worktree (create- or reuse-mode) absolute path.
766
+ for d in .spur/memory/evidence .spur/run; do
767
+ mkdir -p "$WT/$d"
768
+ for f in "$d"/*-verdict.json; do [ -f "$f" ] && cp -n "$f" "$WT/$d/"; done
769
+ done
770
+ ```
771
+
772
+ The position relative to the dependency install is immaterial (this is a plain copy into an
773
+ untracked plane); the position relative to the **first task** is not — a preflight or metrics read
774
+ that runs before this step sees an empty evidence plane.
775
+
733
776
  #### Name resolution (`--worktree <name>`)
734
777
 
735
778
  <a id="name-resolution---worktree-name"></a>
@@ -765,6 +808,10 @@ the tree's own source:
765
808
 
766
809
  cd "<worktree>" && bun apps/cli/src/index.ts task check <wbs> --json
767
810
 
811
+ Run that as a per-call pin subshell (task 1058) — `cd --` plus the `pwd -P` / git-top-level /
812
+ branch identity checks of the canonical protocol (inline-pipeline-driver.md § "Per-call
813
+ execution-tree pin") — rather than relying on a persistent `cd` between host tool calls.
814
+
768
815
  Confirm isolation by making a distinctive change in the worktree and checking that
769
816
  the command reflects it.
770
817
 
@@ -832,13 +879,19 @@ Reuse mode resolves the marker by the resolved worktree's `path` (not by `comman
832
879
  ### WT-3b — Commit the batch's writes on `$BRANCH` (task 0701 R1)
833
880
 
834
881
  Before any terminal action, commit the batch's corpus writes **on `$BRANCH`, inside the
835
- worktree** — including the generated task files under `docs/tasks*/` and the kanban index:
882
+ worktree** — including the generated task files under `docs/tasks*/` and the kanban index. The
883
+ commit runs as a per-call pinned subshell (task 1058): it selects the execution tree inside the
884
+ same call, verifies the WT-3 identity, and never relies on a persistent `cd` or a trailing
885
+ `cd -` to restore the caller's directory:
836
886
 
837
887
  ```bash
838
- cd "../<worktree-dir>"
839
- git add <files-the-batch-wrote>
840
- git commit -m "<type>(<scope>): <command> <selector> batch writes"
841
- cd - >/dev/null
888
+ (
889
+ cd -- "../<worktree-dir>" || { echo "tree missing: expected ../<worktree-dir>" >&2; exit 1; }
890
+ [ "$(git branch --show-current)" = "$BRANCH" ] \
891
+ || { echo "branch mismatch: expected $BRANCH, got $(git branch --show-current)" >&2; exit 1; }
892
+ git add <files-the-batch-wrote>
893
+ git commit -m "<type>(<scope>): <command> <selector> batch writes"
894
+ )
842
895
  ```
843
896
 
844
897
  The FF-only git merge carries only commits — uncommitted writes in the worktree would be left
@@ -855,18 +908,66 @@ only what it created*):
855
908
 
856
909
  ```bash
857
910
  # Run these from the main tree (not inside the worktree) - you merge the worktree branch
858
- # back onto the base ref there:
859
- git checkout "$BASE_REF"
911
+ # back onto the base ref there. Per-call tree pinning (task 1058, § above): every command
912
+ # below selects the INVOKING tree (where the WT-3 marker lives), re-pinned with the canonical
913
+ # per-call pin protocol in its own subshell — never the execution tree. The whole sequence is
914
+ # fail-stop: checkout, guard, ancestry check, merge, landed verify, persistence, and cleanup
915
+ # each stop explicitly. Every tip/ancestry read uses local refs — no fetch runs anywhere:
916
+ MARKER=".spur/run/worktree-<marker-id>.json"
917
+ git checkout "$BASE_REF" \
918
+ || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
860
919
  # Guard (task 0701 R1): a zero-commit branch makes the FF-only git merge exit 0
861
- # ("Already up to date") while merging nothing — the two lines below would then delete
920
+ # ("Already up to date") while merging nothing — the lines below would then delete
862
921
  # the worktree holding the only copy of the batch's writes. Refuse instead:
863
- [ "$(git rev-list --count "$BASE_SHA..$BRANCH")" -gt 0 ] \
864
- || { echo "halt: branch carries no commits - nothing to merge" >&2; false; } # -> WT-5
922
+ [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
923
+ || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; } # -> WT-5, marker stays "active"
924
+ # Capture the batch tip BEFORE any cleanup or branch work: every later step (landed verify,
925
+ # mergeCommit, retained report) reads this captured value — a removed branch is never queried.
926
+ BATCH_TIP="$(git rev-parse "$BRANCH")" \
927
+ || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
928
+ # Fresh LOCAL base tip, re-read now; the marker's baseSha may be stale and never governs this
929
+ # decision (an advanced-but-ancestor base still FF-merges, a divergent base halts):
930
+ BASE_TIP="$(git rev-parse "$BASE_REF")" \
931
+ || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
932
+ # Ancestry check immediately before the sole mutation (the FF merge below). Command/read
933
+ # errors are distinguished from ordinary non-ancestry; both fail closed:
934
+ ANCESTRY_RC=0
935
+ git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
936
+ [ "$ANCESTRY_RC" -gt 1 ] \
937
+ && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
938
+ [ "$ANCESTRY_RC" -eq 1 ] && {
939
+ echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
940
+ git log --oneline "$BASE_TIP..$BATCH_TIP" >&2 # batch-side divergent commits
941
+ git log --oneline "$BATCH_TIP..$BASE_TIP" >&2 # base-side divergent commits
942
+ exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
865
943
  git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
866
- # if FF succeeded — WT-4a evidence persistence runs FIRST (Step 5, task 0720 R3):
944
+ [ $? -eq 0 ] \
945
+ || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; } # -> WT-5
946
+ # Landed verification before anything else runs: the captured BATCH_TIP must be an ancestor of
947
+ # the base ref as it now stands (base tips advancing again later stay valid — this binds the
948
+ # merge result, not the end state):
949
+ LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
950
+ || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
951
+ git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
952
+ || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
953
+ # --- Landed from here on. Any halt below is landed-but-incomplete: it records status
954
+ # --- "retained" + mergeCommit "$BATCH_TIP" via write_marker and stops — never a branch query.
955
+ write_marker () { # $1 = merged | retained; read-modify-write: preserve WT-3 fields (id, command, selector, createdAt, adopted, adoptedAt); jq already assumed by WT-3/task-resolution flows
956
+ mkdir -p "$(dirname "$MARKER")"
957
+ if [ -f "$MARKER" ]; then
958
+ TMP_MARKER="$(mktemp)"
959
+ jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
960
+ || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
961
+ mv "$TMP_MARKER" "$MARKER"
962
+ else
963
+ printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
964
+ "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
965
+ fi
966
+ }
967
+ # WT-4a evidence persistence runs FIRST after the landed verify (Step 5, task 0720 R3):
867
968
  # persist the batch report + verdict artifacts AND the per-run provenance (task
868
969
  # 0975 R1) into the invoking tree before anything below touches the worktree.
869
- # Any persistence failure routes to WT-5 — the worktree and branch are retained.
970
+ # Any persistence failure routes to WT-5 — landed-but-incomplete: retained marker + mergeCommit.
870
971
  WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4b below
871
972
  # WT-4a provenance persist-out (task 0975 R1): copy the worktree DB's run rows plus
872
973
  # the .spur/memory/runs/<runId>.md + .state.json records into THIS tree. Run from the main
@@ -880,7 +981,7 @@ WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4
880
981
  # NOT proceed to WT-4b removal:
881
982
  SETUP_SCRIPT=plugins/sp/scripts/inline-run-setup.ts; [ -f config/plugin-scripts.json -a -f "$SETUP_SCRIPT" ] || SETUP_SCRIPT="$(superskill script path sp inline-run-setup.mjs 2>/dev/null)"
882
983
  bun "$SETUP_SCRIPT" --persist-out --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
883
- || { echo "halt: worktree run-record persist-out failed - worktree retained (WT-5)" >&2; exit 1; }
984
+ || { echo "halt: worktree run-record persist-out failed - worktree retained (WT-5)" >&2; write_marker retained; exit 1; }
884
985
  #
885
986
  # WT-4b — bounded CWD-holder cleanup (task 0720 R1). $WT_PATH above is the EXACT
886
987
  # absolute worktree path; a relative path or a stale entry matches the wrong processes.
@@ -911,39 +1012,114 @@ if [ -n "$FINAL" ]; then
911
1012
  PORTS="$(lsof -nP -a -p "$(echo "$FINAL" | paste -sd, -)" -iTCP -sTCP:LISTEN 2>/dev/null \
912
1013
  | awk 'NR>1 {print $9}' | sort -u | paste -sd' ' -)"
913
1014
  echo "halt: worktree still held by PID(s): $FINAL ${PORTS:+listening: $PORTS}" >&2
1015
+ write_marker retained # landed-but-incomplete: retained + mergeCommit
914
1016
  exit 1 # -> WT-5: retain worktree + branch,
915
1017
  fi # NO prune/remove/branch delete
916
- git worktree remove "../<worktree-dir>"
917
- git branch -d "$BRANCH"
1018
+ # Pre-removal evidence assertion (task 1067 R1, E71 enforcement): refuse to remove the
1019
+ # worktree while it still owns evidence absent from (or divergent from) the invoking tree —
1020
+ # the D63 landing skipped persist-out and silently lost verdicts + receipts. WBSes ride the
1021
+ # same TASK_FILE_ARGS resolved for WT-4a; run ids are a separate `--run-id <id>` per run and
1022
+ # belong to THIS check only — `inline-run-setup.ts --persist-out` accepts `--from` and
1023
+ # `--task-file` alone and exits 2 on `--run-id` (1089 driver note).
1024
+ CHECK_SCRIPT=plugins/sp/scripts/persist-out-check.ts; [ -f config/plugin-scripts.json -a -f "$CHECK_SCRIPT" ] || CHECK_SCRIPT="$(superskill script path sp persist-out-check.mjs 2>/dev/null)"
1025
+ bun "$CHECK_SCRIPT" --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
1026
+ || { echo "WT-4 halt: worktree evidence would be abandoned - persist-out missing or stale (WT-5)" >&2; write_marker retained; exit 1; }
1027
+ git worktree remove "../<worktree-dir>" \
1028
+ || { echo "WT-4 halt: worktree remove failed after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
1029
+ # Branch deletion runs only after the merge landed, persisted, and the worktree is gone —
1030
+ # and it is existence-guarded, so a removed branch is never queried. -D over -d: the landed
1031
+ # verify already proved the merge landed, so an unmerged-refusal would only block proven-merged cleanup.
1032
+ if git rev-parse --verify --quiet "$BRANCH" >/dev/null; then
1033
+ git branch -D "$BRANCH" || { echo "WT-4 halt: cannot delete branch $BRANCH after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
1034
+ fi
918
1035
  # WT-4c — clean up registry entry in ~/.config/spur/projects.json (task 0924)
919
1036
  spur projects remove "$WT_PATH" 2>/dev/null || spur projects clean --json 2>/dev/null || true
920
- # update marker: status = "merged"
1037
+ # Success marker, written once and only here: status "merged" + mergeCommit "$BATCH_TIP" —
1038
+ # after landed verification, required persistence, and cleanup:
1039
+ write_marker merged
921
1040
  ```
922
1041
 
923
- On either guard firing — zero-commit branch (task 0701 R1), or surviving CWD holders (task 0720 R1)
924
- — fall through to **WT-5**: the worktree and branch are retained, never removed. While any holder
925
- remains, do **not** run `git worktree prune`, `git worktree remove`, or branch deletion. The holder
1042
+ Every halt above stops explicitly and falls through to **WT-5** — zero-commit branch (task 0701
1043
+ R1), failed checkout, divergent base, failed FF, persist-out failure (task 0975), or surviving CWD
1044
+ holders (task 0720 R1). Pre-merge failures leave the marker at `status: active` and the worktree +
1045
+ branch untouched. Landed-but-incomplete failures record `status: retained` + `mergeCommit`
1046
+ BATCH_TIP: the merge landed, and the retained report reads the captured tip only. While any holder
1047
+ remains, do **not** run `git worktree prune`, `git worktree remove`, or branch deletion — only an
1048
+ EMPTY holder set may proceed to those steps. The holder
926
1049
  halt report names every surviving PID; the listening port is best-effort — a CWD holder may own no
927
1050
  socket, and `lsof` port discovery failing must not hide the PIDs.
928
1051
 
929
1052
  #### Reuse mode — merge, retain
930
1053
 
931
- The FF-merge runs identically (same `git checkout "$BASE_REF" && git merge --ff-only "$BRANCH"`),
932
- but then the worktree and branch are **retained**, not removed:
1054
+ The fail-stop merge sequence runs identically — same guard, captured `BATCH_TIP`/`BASE_TIP`,
1055
+ ancestry check immediately before FF, `git merge --ff-only`, landed verification (per-call pinned
1056
+ to the invoking tree, task 1058) — but no WT-4a/4b/4c and no removal: the worktree and branch are
1057
+ **retained**. Pre-merge halts leave the marker `active`; a persistence failure after the landed
1058
+ merge records `retained` + `mergeCommit`:
933
1059
 
934
1060
  ```bash
935
- git checkout "$BASE_REF"
936
- git merge --ff-only "$BRANCH"
937
- # update marker: status = "merged" (worktree and branch are intentionally NOT removed)
1061
+ # Same fail-stop sequence as create mode (zero-commit guard -> BATCH_TIP/BASE_TIP -> ancestry
1062
+ # rc check -> git merge --ff-only -> landed verify); local refs only — no fetch.
1063
+ MARKER=".spur/run/worktree-<marker-id>.json"
1064
+ git checkout "$BASE_REF" \
1065
+ || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
1066
+ [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
1067
+ || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; }
1068
+ BATCH_TIP="$(git rev-parse "$BRANCH")" || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
1069
+ BASE_TIP="$(git rev-parse "$BASE_REF")" || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
1070
+ ANCESTRY_RC=0
1071
+ git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
1072
+ [ "$ANCESTRY_RC" -gt 1 ] \
1073
+ && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
1074
+ [ "$ANCESTRY_RC" -eq 1 ] && {
1075
+ echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
1076
+ git log --oneline "$BASE_TIP..$BATCH_TIP" >&2
1077
+ git log --oneline "$BATCH_TIP..$BASE_TIP" >&2
1078
+ exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
1079
+ git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
1080
+ [ $? -eq 0 ] \
1081
+ || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; }
1082
+ LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
1083
+ || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
1084
+ git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
1085
+ || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
1086
+ WT_PATH="<resolved-worktree-path>" # reuse mode: the adopted tree's physical path
1087
+ write_marker () { # $1 = merged | retained; read-modify-write: preserve WT-3 fields (id, command, selector, createdAt, adopted, adoptedAt); jq already assumed by WT-3/task-resolution flows
1088
+ mkdir -p "$(dirname "$MARKER")"
1089
+ if [ -f "$MARKER" ]; then
1090
+ TMP_MARKER="$(mktemp)"
1091
+ jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
1092
+ || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
1093
+ mv "$TMP_MARKER" "$MARKER"
1094
+ else
1095
+ printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
1096
+ "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
1097
+ fi
1098
+ }
1099
+ # Step 5 evidence persistence (task 0720 R3) — required BEFORE the success marker:
1100
+ if printf 'batch report %s\n' "$BATCH_TIP" > ".spur/run/worktree-<marker-id>-batch-report.md"; then :; else
1101
+ echo "WT-5 halt: persistence failed after landed merge $BATCH_TIP; recovery reads captured tips only" >&2
1102
+ write_marker retained
1103
+ exit 1
1104
+ fi
1105
+ # Success marker: status "merged" + mergeCommit "$BATCH_TIP" — written once, only after landed verification and required persistence.
1106
+ # The worktree and branch are intentionally NOT removed.
1107
+ write_marker merged
938
1108
  ```
939
1109
 
940
1110
  The operator supplied the tree, so the operator owns its lifetime. After a green reuse batch
941
1111
  `baseRef == $BRANCH`, so the same worktree keeps fast-forwarding on the next invocation instead of
942
1112
  having to be rebuilt — the continue-the-work loop is stable.
943
1113
 
944
- **Fast-forward only.** If the base ref has moved since the worktree was created and FF is
945
- impossible, do **not** rebase, merge-commit, or resolve conflicts — fall through to the retention
946
- path (WT-5) and report the divergence. The corpus files (`docs/tasks*/`, kanban/index) are
1114
+ **Fast-forward only.** `git merge --ff-only` is the sole mutation of the terminal sequence; every
1115
+ other step is a read, a check, a persistence write, or a cleanup, and every tip/ancestry read uses
1116
+ local refs — no fetch runs anywhere in this lifecycle. If the fresh `BASE_TIP` is not an ancestor of
1117
+ `BATCH_TIP`, the ancestry check halts before the merge: the report names both tips and both
1118
+ divergent commit ranges (`$BASE_TIP..$BATCH_TIP` and `$BATCH_TIP..$BASE_TIP`) and falls to the
1119
+ retention path (WT-5) with the tree and branch retained. A concurrent writer that lands between the
1120
+ ancestry check and the merge still trips `--ff-only` — the race survives every precheck, and a
1121
+ failed FF leaves the marker at `status: active`. Do **not** rebase, merge-commit, or resolve
1122
+ conflicts. The corpus files (`docs/tasks*/`, kanban/index) are
947
1123
  auto-generated and conflict-prone; automated conflict resolution over generated files is exactly the
948
1124
  wrong thing to attempt unattended. FF-only means the merge either is trivially correct or does not
949
1125
  happen.
@@ -952,8 +1128,10 @@ happen.
952
1128
  `--worktree --auto` batch — it does **not** pause even though it performs a merge and a branch
953
1129
  deletion. That is the explicit single exception to Auto-Decision Principle #6 (`cross-cutting.md`),
954
1130
  which otherwise pauses any `--merge` / branch-deletion action regardless of `--auto`. The exception
955
- is safe because `git merge --ff-only` and `git branch -d` both fail closed (they refuse rather than
956
- risk losing work); WT-5 retains the worktree and branch whenever FF is impossible or any task fails.
1131
+ is safe because `git merge --ff-only` fails closed (it refuses rather than risk losing work), the
1132
+ branch deletion runs only after the merge has landed, persisted, and cleaned up — and is
1133
+ existence-guarded (`git rev-parse --verify --quiet`), so a removed branch is never queried;
1134
+ WT-5 retains the worktree and branch whenever FF is impossible or any task fails.
957
1135
  Reuse mode is **narrower** than the carve-out (it merges but does not delete the branch), so the
958
1136
  carve-out text needs no widening.
959
1137
 
@@ -982,22 +1160,44 @@ record and the persisted run artifacts are the evidence record.
982
1160
 
983
1161
  ### WT-5 — Failure path: retain and report (R5)
984
1162
 
985
- On any per-task failure, batch halt, HITL pause that ends the run, or non-FF merge from WT-4, the
986
- worktree directory and branch are left **intact**. No destructive automation on this path under any
987
- flag combination (`--auto`, `--force`, `--keep-going` — all leave the worktree in place). Update the
988
- marker: `status = "retained"`. The worktree's own `.spur` lifecycle DB is retained with the tree,
989
- so nothing is lost on this path (see the WT-4 lifecycle-DB disposition for the merged case —
1163
+ On any per-task failure, batch halt, HITL pause that ends the run, or a WT-4 halt, the
1164
+ worktree directory and branch are left **intact**. No destructive automation on this path under
1165
+ any flag combination (`--auto`, `--force`, `--keep-going` — all leave the worktree in place). The
1166
+ marker records which class of halt occurred (frozen vocabulary):
1167
+
1168
+ - **Pre-merge failure** (failed checkout, zero-commit guard, divergent base, failed FF): the merge
1169
+ never landed, so the marker **stays at `status: active`** — the WT-3 marker stands and WT-5 is
1170
+ its disposition. Nothing was merged onto the base ref.
1171
+ - **Landed-but-incomplete failure** (persist-out, holder cleanup, or worktree removal failed after
1172
+ a landed merge): the marker records `status: retained` + `mergeCommit` BATCH_TIP — a truthful
1173
+ report that the merge landed. The retained path reads the captured `BATCH_TIP` only; a removed
1174
+ branch is never queried.
1175
+
1176
+ The worktree's own `.spur` lifecycle DB is retained with the tree,
1177
+ so nothing is lost on either path (see the WT-4 lifecycle-DB disposition for the merged case —
990
1178
  task 0701 R2d). Emit a retention report in the existing halt-report shape:
991
1179
 
1180
+ **Persist-out skip recovery (task 1067 R2, E71).** When evidence was lost to a landing that
1181
+ skipped persist-out (or the pre-removal assertion fired too late), verdicts remain derivable
1182
+ from the task records — `spur task show <wbs> --json` carries the recorded verdict and its
1183
+ proof block, so a lost `<wbs>-verdict.json` is regenerated by re-recording or copying from
1184
+ the task store. Feature run/latest receipts are re-runnable, not recovered: rerun the
1185
+ feature-verification workflow in the invoking tree —
1186
+ `spur workflow run feature-verification.yaml --vars '{"featureId":"<id>"}'` — which rewrites
1187
+ the receipt files it owns. Divergent files are never auto-overwritten; reconcile by hand.
1188
+
992
1189
  ```
993
1190
  ## Worktree retained — <command> <selector>
994
1191
 
995
- **Halt cause:** <one-line cause — batch halted at task <wbs> / non-FF base ref / HITL pause>
1192
+ **Halt cause:** <one-line cause — batch halted at task <wbs> / non-FF base ref / HITL pause / landed-but-incomplete cleanup failure>
996
1193
  **Worktree path:** ../<worktree-dir>
997
1194
  **Branch:** sp/<command>-<selector-slug>-<short-id>
998
1195
  **Base ref:** <base-ref> (<base-sha>)
1196
+ **Merge state:** pre-merge failure — nothing landed | landed-but-incomplete — mergeCommit <batch-tip-sha>
999
1197
 
1000
- The worktree and its branch are intact. Nothing was merged onto the base ref.
1198
+ The worktree and its branch are intact.
1199
+ pre-merge failure: nothing was merged onto the base ref.
1200
+ landed-but-incomplete: the batch merge LANDED as <batch-tip-sha> on <base-ref>; retention covers post-merge persistence/cleanup only.
1001
1201
  Resume, merge, or discard:
1002
1202
 
1003
1203
  resume: cd <worktree-path> && <command> --continue --worktree <worktree-path>
@@ -1010,16 +1210,42 @@ ordered divergence recipe, run by the operator — the driver never merges, reba
1010
1210
  conflicts itself. Other halt causes (task failure, HITL pause) keep the hint as printed:
1011
1211
 
1012
1212
  ```
1013
- # 1. integrate as a merge commit — never a rebase; task evidence cites the branch's commit SHAs
1014
- git checkout <base-ref> && git merge --no-ff --no-commit <branch>
1213
+ # 1. decide whether conflicts need resolving FIRST, because that decides the merge form:
1214
+ # git merge-tree $(git merge-base <base-ref> <branch>) <base-ref> <branch> | grep -q '^<<<<<<<' \
1215
+ # && echo "conflicts — use the two-phase form below" || echo "clean — use the atomic form"
1216
+ #
1217
+ # 1a. CLEAN (the common case): integrate as ONE atomic merge commit. Never a rebase — task
1218
+ # evidence cites the branch's commit SHAs. This is the file's sanctioned merge commit.
1219
+ # Do NOT split this into a two-phase merge + commit when nothing needs resolving: that spans
1220
+ # the whole gate run, and anything in that window that touches the repository's git state
1221
+ # (a test, a hook, a tool invocation) discards MERGE_HEAD silently. The failure is invisible
1222
+ # until the history is read — it produced a single-parent commit in run ada5a36c (task 1090),
1223
+ # which then needed an extra empty ancestry merge to repair. A cheap probe could not
1224
+ # reproduce it (MERGE_HEAD survived `bun run lint`, the rule preset and a test-file subset),
1225
+ # so the cross-command dependency is removed rather than diagnosed.
1226
+ git checkout <base-ref> && git merge --no-ff -m "<prepared message>" <branch>
1227
+
1228
+ # 1b. CONFLICTING: the two-phase form is correct here, because the resolution work must land
1229
+ # between the merge and the commit. Everything in steps 2–4 belongs to this branch only.
1230
+ # A divergent branch cannot fast-forward, so the merge flag from 1a is not needed here.
1231
+ # git checkout <base-ref> && git merge --no-commit <branch>
1015
1232
  # 2. resolve source conflicts by hand; generated files are then regenerated with the project's
1016
1233
  # generator, never hand-merged
1017
1234
  # (this repo: bun run build:plugin-lib && bun run --filter @gobing-ai/spur build:bundle)
1018
1235
  # 3. stage every resolved path and regenerated bundle (git add …) — an unmerged or
1019
1236
  # unstaged path makes step 5 abort
1020
1237
  # 4. run qualityGateCmd once, after ALL conflicts are resolved
1021
- # 5. commit the merge with the prepared message file
1022
- git commit -F <message-file>
1238
+ # 5. commit the merge with the prepared message file — CHECK THE PARENTS FIRST (step 5a)
1239
+ # git commit -F <message-file>
1240
+ #
1241
+ # 5a. ALWAYS verify the merge really is a merge before treating it as landed:
1242
+ # git log -1 --format='%h parents=%p' # must print TWO parents
1243
+ # One parent means MERGE_HEAD was lost somewhere between steps 1 and 5 — the content is on
1244
+ # the base ref but the branch is not an ancestor, so `git branch -d` would refuse and the
1245
+ # retained worktree's provenance would not be readable from the base ref. Recover by
1246
+ # re-running step 1a against the already-landed tree (a content-free merge commit restores
1247
+ # ancestry).
1248
+
1023
1249
  # 6. persist evidence out (WT-4a), then WT-4b/4c cleanup, and set the marker to merged
1024
1250
  inline-run-setup --persist-out --from <worktree> --task-file …
1025
1251
  ```
@@ -1030,6 +1256,16 @@ are long and already resumable via `--continue`; auto-deleting is data loss, aut
1030
1256
  partial result presented as a whole. The answer to "what happens if it fails" is "nothing happens,
1031
1257
  and we tell you where the work is."
1032
1258
 
1259
+ **Partial worktree-removal recovery is inspection-only.** If a create-mode run halted partway
1260
+ through removal (leftover directory, stale registration), the recovery pass gathers facts and
1261
+ nothing else: `git worktree list` (is the tree still registered?), `ls` of the path (what remains
1262
+ on disk?), and the marker / `~/.config/spur/projects.json` registration state. Before removing
1263
+ anything, verify the leftover path is the batch's **owned** path — match it against the marker's
1264
+ `path`/`branch` (or the resolved `--worktree <name>` identity), never a guessed sibling directory.
1265
+ Recursive deletion of leftover directories that survived `git worktree remove` is **never
1266
+ automatic** — it requires explicit operator authorization. No automatic rebase, recovery merge,
1267
+ `git worktree prune`, or holder kill runs on this path.
1268
+
1033
1269
  ### WT-6 — `--continue` re-entry (R7)
1034
1270
 
1035
1271
  A `--continue` resume of a batch started with `--worktree` must re-enter the existing worktree via
@@ -1172,7 +1408,7 @@ integrate(t):
1172
1408
  ├─ rebase fails → git -C "$WT" rebase --abort → conflict path (R4) below
1173
1409
  └─ rebase clean → git checkout "$BASE_REF" → zero-commit guard → git merge --ff-only "$BRANCH"
1174
1410
  → WT-4a verdict persist → WT-4b holders → WT-4c registry
1175
- → git worktree remove → git branch -d → marker merged
1411
+ → git worktree remove → existence-guarded git branch -D → marker merged
1176
1412
  ```
1177
1413
 
1178
1414
  The merge is always `--ff-only`: a base that will not fast-forward after a clean rebase is a
@@ -163,6 +163,20 @@ The pipeline (`kind: state-machine`) runs the work loop:
163
163
  precheck → implement → test [→ test-fix → test-recheck] → review → approve(HITL) → verify → record → done
164
164
  ```
165
165
 
166
+ Two branches reach the bounded repair hop, not one (session finding after 1088):
167
+
168
+ - `test`/`test-recheck` FAIL → `test-fail-triage` (0943's failure-class router) → `test-fix`;
169
+ - `review` without a `Verdict: PASS` in its answer → `review-fail-triage` (the review-side twin) →
170
+ `test-fix`, bounded by the SAME `qualityGateMaxFixAttempts` counter. A FAIL review therefore has
171
+ an on-graph, bounded path to remediation instead of dead-ending the run, and the fix re-enters
172
+ quality → review → verify on a freshly captured proof digest.
173
+
174
+ The completion gate also reads the verifier's confidence (session finding after 1088): the verify
175
+ answer's `Confidence: HIGH|MEDIUM|LOW` line reaches the verdict artifact, and both `verify → record`
176
+ and `record → done` refuse a `LOW` (or absent) level unless the run was launched with
177
+ `--vars '{"ackLowConfidence":"true"}'`. A PASS the verifier would not stand behind is an operator
178
+ risk decision, so it takes an operator acknowledgement — not a silent certification.
179
+
166
180
  Agentic steps use the pure slash inputs declared by each YAML `agent.run` action (ADR-043). The
167
181
  workflow engine dispatches them; the interactive driver invokes their backing skills in-session.
168
182
  The `test` hop is primarily
@@ -203,7 +217,9 @@ When `--next` is set and implementation succeeds:
203
217
  2. **Record provenance** — `spur task run-link <wbs> --source next-auto --json`. Writes a
204
218
  `kind: pipeline` entry into `task_run_links` so the `testing → done` provenance guard
205
219
  (lifecycle-adapter.ts L106-131) accepts the in-session implementation path. Idempotent:
206
- safe to call even when a pipeline link already exists.
220
+ safe to call even when a pipeline link already exists. Run it from the same cwd as the
221
+ `task update` calls in step 1 — per-tree DB isolation makes a link from another tree
222
+ invisible to this tree's guard.
207
223
  3. **On a clean transition:** invoke `/sp:dev-verify <wbs> --auto --next` (`--auto` propagates
208
224
  down the whole chain). The verify step's `--next` transition to `done` now passes the
209
225
  provenance guard because step 2 recorded the link.
@@ -301,10 +301,13 @@ tasks by `updated_at >= date`).
301
301
 
302
302
  **Anchor:** `#flag-fix`.
303
303
 
304
- Remediation policy on verify-family commands (`dev-verify`, `dev-verifyall`) and the refactor
305
- coordinator (`dev-refactor`):
304
+ Remediation policy on verify-family commands (`dev-verify`, `dev-verifyall`), the refactor
305
+ coordinator (`dev-refactor`) and the session review (`dev-review-session`):
306
306
  `none|blockers-first|all`. `none` reports findings without fixing; `blockers-first` fixes only P1/P2;
307
- `all` fixes everything found. Deprecated on `dev-review` (routes to `dev-verify --fix`).
307
+ `all` fixes everything found. On `dev-review-session` the vocabulary is `none|auto|all`: `none`
308
+ report-only, `auto` the `--triage` behavior (inline direct fixes, the rest filed as tasks), `all`
309
+ fixes every actionable finding inline and files no task. Deprecated on `dev-review` (routes to
310
+ `dev-verify --fix`).
308
311
 
309
312
  ### `--until <ref>` — upper bound on a range
310
313
 
@@ -336,6 +339,14 @@ specific status slice).
336
339
  Skip the shippable guard on verify-family commands (`dev-verify`, `dev-verifyall`). Used when
337
340
  verifying a task whose artifact is intentionally not yet shippable (e.g. a doc-only task).
338
341
 
342
+ ### `--no-summary` — skip the execution summary
343
+
344
+ **Anchor:** `#flag-no-summary`.
345
+
346
+ Skip the measured execution summary that run commands (`dev-run`, `dev-runall`) print by default
347
+ once the run settles: per-stage (or per-run) time, operator wait, tool calls and total / non-cached
348
+ tokens from the host transcript. Procedure: [dev-operations.md § Execution summary](dev-operations.md#execution-summary).
349
+
339
350
  ### `--skip-design` — omit the design package
340
351
 
341
352
  **Anchor:** `#flag-skip-design`.
@@ -445,7 +456,8 @@ Review commands (`dev-review`, `dev-review-session`): after the report, bucket e
445
456
  remainder through `spur task` — never fix straight from the raw findings list. Off by default
446
457
  (report-only). Both file one or more implement-ready tasks sized per cohesive unit, under existing
447
458
  features ([dev-operations.md § 2. review](dev-operations.md#2-review)); `dev-review-session` keeps
448
- the stricter direct-fix bar (pure docs / one-to-two-line fixes).
459
+ the stricter direct-fix bar (pure docs / one-to-two-line fixes), and there `--triage` is an alias
460
+ of `--fix auto` ([`--fix`](#flag-fix)).
449
461
 
450
462
  ### `--worktree [<name>]` — run the batch in an isolated git worktree (create or reuse)
451
463