@gobing-ai/spur 0.3.97 → 0.3.99

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 (90) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/plugin-scripts.json +5 -0
  3. package/config/templates/task/brainstorm.md +1 -1
  4. package/config/templates/task/feature-impl.md +1 -1
  5. package/config/templates/task/issue.md +1 -1
  6. package/config/templates/task/meta.md +1 -1
  7. package/config/templates/task/review.md +1 -1
  8. package/config/templates/task/standard.md +1 -1
  9. package/package.json +9 -9
  10. package/plugins/sp/README.md +9 -9
  11. package/plugins/sp/lib/idea-handoff.generated.mjs +37 -4
  12. package/plugins/sp/lib/inline-run.generated.mjs +54 -14
  13. package/plugins/sp/lib/residual-scan.generated.d.mts +5 -0
  14. package/plugins/sp/lib/residual-scan.generated.mjs +45 -5
  15. package/plugins/sp/plugin.json +1 -1
  16. package/plugins/sp/references/roles.md +3 -3
  17. package/plugins/sp/scripts/inline-run-setup.mjs +4 -2
  18. package/plugins/sp/scripts/inline-run-setup.ts +12 -12
  19. package/plugins/sp/scripts/persist-out-check.mjs +164 -0
  20. package/plugins/sp/scripts/persist-out-check.ts +185 -0
  21. package/plugins/sp/scripts/residual-scan.mjs +50 -8
  22. package/plugins/sp/scripts/residual-scan.ts +4 -3
  23. package/plugins/sp/scripts/task-diffstat.mjs +17 -5
  24. package/plugins/sp/scripts/task-diffstat.ts +25 -4
  25. package/plugins/sp/skills/code-implementation/SKILL.md +1 -1
  26. package/plugins/sp/skills/code-verification/SKILL.md +3 -0
  27. package/plugins/sp/skills/code-verification/references/verdict-schema.md +17 -1
  28. package/plugins/sp/skills/conflict-finding/SKILL.md +1 -1
  29. package/plugins/sp/skills/conflict-finding/references/authority-resolution.md +5 -3
  30. package/plugins/sp/skills/conflict-finding/references/remediation-routing.md +9 -8
  31. package/plugins/sp/skills/doc-evolve/SKILL.md +4 -3
  32. package/plugins/sp/skills/spur-cli/references/tasks/section-editing.md +7 -4
  33. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +7 -4
  34. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +40 -7
  35. package/plugins/sp/skills/spur-dev/references/dev-operations.md +16 -13
  36. package/plugins/sp/skills/spur-dev/references/execution-batch.md +217 -38
  37. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +3 -1
  38. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +7 -8
  39. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +110 -3
  40. package/spur.js +2142 -637
  41. package/web/_astro/{BoardApp.vveLOdDq.js → BoardApp.BVp-LYi6.js} +101 -101
  42. package/web/_astro/{BoardApp.Bmv5WkJ9.js → BoardApp.i4eh_fHA.js} +1 -1
  43. package/web/_astro/{TaskDetail.IKc3uDYV.js → TaskDetail.C5Cu8LdB.js} +1 -1
  44. package/web/_astro/{arc.C63Ufg35.js → arc.Bu5XpBEz.js} +1 -1
  45. package/web/_astro/{architectureDiagram-3BPJPVTR.CP8Hyldk.js → architectureDiagram-3BPJPVTR.9vHQTvhF.js} +1 -1
  46. package/web/_astro/{blockDiagram-GPEHLZMM.Bo_48MRA.js → blockDiagram-GPEHLZMM.Cn2Xl7D6.js} +1 -1
  47. package/web/_astro/{c4Diagram-AAUBKEIU.Ch61sw-O.js → c4Diagram-AAUBKEIU.7o0P_QDo.js} +1 -1
  48. package/web/_astro/channel.6IFIKBhu.js +1 -0
  49. package/web/_astro/{chunk-2J33WTMH.CHpzS7xf.js → chunk-2J33WTMH.C3zZFsNt.js} +1 -1
  50. package/web/_astro/{chunk-4BX2VUAB.DzMNNWIw.js → chunk-4BX2VUAB.CNy_M7fO.js} +1 -1
  51. package/web/_astro/{chunk-55IACEB6.BqPrDw_Q.js → chunk-55IACEB6.2TiPnLBL.js} +1 -1
  52. package/web/_astro/{chunk-727SXJPM.ZzBHhNw-.js → chunk-727SXJPM.5CGSyKAp.js} +1 -1
  53. package/web/_astro/{chunk-AQP2D5EJ.1_O_LLkx.js → chunk-AQP2D5EJ.w-ZIgfsK.js} +1 -1
  54. package/web/_astro/{chunk-FMBD7UC4.GS4UHg7_.js → chunk-FMBD7UC4.BkcEyhKa.js} +1 -1
  55. package/web/_astro/{chunk-ND2GUHAM.BnqotN4g.js → chunk-ND2GUHAM.mxKKbBG4.js} +1 -1
  56. package/web/_astro/{chunk-QZHKN3VN.Ghuh8zoJ.js → chunk-QZHKN3VN.cyA0z9aT.js} +1 -1
  57. package/web/_astro/{classDiagram-4FO5ZUOK.BW9vC3zg.js → classDiagram-4FO5ZUOK.CqtUzwhX.js} +1 -1
  58. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BW9vC3zg.js → classDiagram-v2-Q7XG4LA2.CqtUzwhX.js} +1 -1
  59. package/web/_astro/{cose-bilkent-S5V4N54A.D6jNC0ti.js → cose-bilkent-S5V4N54A.BExPTopI.js} +1 -1
  60. package/web/_astro/{cynefin-OW5HDTMX.BOGoCs4L.js → cynefin-OW5HDTMX.Bq2i10Cn.js} +1 -1
  61. package/web/_astro/{dagre-BM42HDAG.8iU5VcWi.js → dagre-BM42HDAG.By_HsOuv.js} +1 -1
  62. package/web/_astro/{diagram-2AECGRRQ.CVt88HwH.js → diagram-2AECGRRQ.BtU0b7hz.js} +1 -1
  63. package/web/_astro/{diagram-5GNKFQAL.C9HxUUMl.js → diagram-5GNKFQAL.D00ztMdT.js} +1 -1
  64. package/web/_astro/{diagram-KO2AKTUF.4ywkQN3t.js → diagram-KO2AKTUF.DMVJWaUN.js} +1 -1
  65. package/web/_astro/{diagram-LMA3HP47.C48A6gpz.js → diagram-LMA3HP47.Cxqrjg5-.js} +1 -1
  66. package/web/_astro/{diagram-OG6HWLK6.DxFieQW4.js → diagram-OG6HWLK6.2lz_1B5h.js} +1 -1
  67. package/web/_astro/{erDiagram-TEJ5UH35.CtjtQqYs.js → erDiagram-TEJ5UH35.BVQEuJ3T.js} +1 -1
  68. package/web/_astro/{flowDiagram-I6XJVG4X.C5zAiahU.js → flowDiagram-I6XJVG4X.Qlyo90ky.js} +1 -1
  69. package/web/_astro/{ganttDiagram-6RSMTGT7.D_L6zsny.js → ganttDiagram-6RSMTGT7.DRwb8HHZ.js} +1 -1
  70. package/web/_astro/{gitGraphDiagram-PVQCEYII.Ignnrk3V.js → gitGraphDiagram-PVQCEYII.YfoPthfS.js} +1 -1
  71. package/web/_astro/{infoDiagram-5YYISTIA.DLv8-P5u.js → infoDiagram-5YYISTIA.DvFREck1.js} +1 -1
  72. package/web/_astro/{ishikawaDiagram-YF4QCWOH.p0OTpwFO.js → ishikawaDiagram-YF4QCWOH.BzCj9Q0-.js} +1 -1
  73. package/web/_astro/{journeyDiagram-JHISSGLW.CIaBt-DD.js → journeyDiagram-JHISSGLW.BDMT_AFb.js} +1 -1
  74. package/web/_astro/{kanban-definition-UN3LZRKU.DGySIntq.js → kanban-definition-UN3LZRKU.D6Fh3gj9.js} +1 -1
  75. package/web/_astro/{linear.BATV4RXu.js → linear.Dxw00fGQ.js} +1 -1
  76. package/web/_astro/{mermaid.core.DzkwM3VX.js → mermaid.core.CsoxUWa0.js} +4 -4
  77. package/web/_astro/{mindmap-definition-RKZ34NQL.3piBRRzA.js → mindmap-definition-RKZ34NQL.CN8oeHLu.js} +1 -1
  78. package/web/_astro/{pieDiagram-4H26LBE5.DUNueLub.js → pieDiagram-4H26LBE5.C7ADqQ_7.js} +1 -1
  79. package/web/_astro/{quadrantDiagram-W4KKPZXB.9bihVbrt.js → quadrantDiagram-W4KKPZXB.Cw9lKyHP.js} +1 -1
  80. package/web/_astro/{requirementDiagram-4Y6WPE33.CkjsC-YT.js → requirementDiagram-4Y6WPE33.B1ni-z4q.js} +1 -1
  81. package/web/_astro/{sankeyDiagram-5OEKKPKP.Dbq4vaoQ.js → sankeyDiagram-5OEKKPKP.BhexdjVr.js} +1 -1
  82. package/web/_astro/{sequenceDiagram-3UESZ5HK.BivrH99P.js → sequenceDiagram-3UESZ5HK.C3Rm7dpQ.js} +1 -1
  83. package/web/_astro/{stateDiagram-AJRCARHV.CqLQ1Vzz.js → stateDiagram-AJRCARHV.CkuMt27u.js} +1 -1
  84. package/web/_astro/{stateDiagram-v2-BHNVJYJU.DvvoLD3b.js → stateDiagram-v2-BHNVJYJU.Dc_aazUA.js} +1 -1
  85. package/web/_astro/{timeline-definition-PNZ67QCA.B-KR78gv.js → timeline-definition-PNZ67QCA.ja7sUFhH.js} +1 -1
  86. package/web/_astro/{vennDiagram-CIIHVFJN.Ck0Eieb5.js → vennDiagram-CIIHVFJN.BxIshX_K.js} +1 -1
  87. package/web/_astro/{wardleyDiagram-YWT4CUSO.CRfdd_do.js → wardleyDiagram-YWT4CUSO.DfHlC0sh.js} +1 -1
  88. package/web/_astro/{xychartDiagram-2RQKCTM6.Ck_Jxk9C.js → xychartDiagram-2RQKCTM6.DSnhHwCh.js} +1 -1
  89. package/web/index.html +1 -1
  90. package/web/_astro/channel.BKCmJpWM.js +0 -1
@@ -530,7 +530,7 @@ pass before the invoking tree is written → WT-5. Without `--task-file` nothing
530
530
  The shapes are pinned (task 0975 R1; `record-missing` and citation behavior per 0984): idempotent on re-persist;
531
531
  success exits 0 printing
532
532
  `{"ok":true,"persisted":<n>,"skipped":[{"id":<run-id>,"reason":"id-exists"|"external-key-conflict"|"record-conflict:<file>"|"record-missing:<file>"|"cited-directory:<name>"|"cited-symlink:<name>"|"cited-non-file:<name>"}]}`
533
- — an `external-key-conflict` skip leaves the target run unchanged; an `id-exists` replay repairs missing owned artifacts and task links without duplicating them. A
533
+ — an `external-key-conflict` skip leaves the target run unchanged **and fails the pass (1049)**: the delegate exits 1 printing `{"ok":false,"error":…}` naming the skipped source run ids, because that (workflow, external key) identity already belongs to a different receiving run — the batch was not persisted, so reconcile the source worktree by hand (it stays the provenance owner of record) before any teardown: auditable reconciliation names the original archived DB snapshot and the skipped source run identities, verifies the archive by file hash and by merged-commit ancestry of the source branch, and treats any residual deletion as explicit operator-authorized cleanup under the strict canonical evidence rules above. An `id-exists` replay repairs missing owned artifacts and task links without duplicating them. A
534
534
  `record-conflict:<file>` never overwrites a divergent invoking-tree record and causes the delegate to exit 1, retaining the worktree. A
535
535
  `record-missing:<file>` skip is a known `task-lifecycle`/`feature-lifecycle` row with no record file
536
536
  at all (its inserted DB row still counts in `persisted` — 0984 R5). Any failure
@@ -626,6 +626,20 @@ resolution runs, the loop executes with the worktree as process cwd, and a termi
626
626
  retains after the batch report is emitted. Steps 1–5 themselves are not modified — only their cwd
627
627
  differs.
628
628
 
629
+ **Per-call tree pinning (task 1058).** A host shell call starts in an arbitrary directory and
630
+ inherits no cwd from the previous call, so "the loop runs with the worktree as process cwd" is a
631
+ bootstrap convenience, never a carried guarantee. Every host command boundary in this lifecycle —
632
+ corpus reads/writes, the WT-3b commit, WT-4's merge and evidence persistence, the WT-4a
633
+ persist-out — re-selects its intended tree within the same call via the canonical per-call pin
634
+ protocol ([inline-pipeline-driver.md](inline-pipeline-driver.md) § "Per-call execution-tree
635
+ pin"): `cd --` to the quoted absolute tree, verify `pwd -P` against the recorded physical tree,
636
+ verify Git top-level and expected branch, then execute the already-resolved Spur invocation. The
637
+ identity is consumed, not re-derived: it is the WT-3 marker's confirmed `path` + `branch` (or the
638
+ resolved worktree's physical path + checked-out branch in reuse mode). The one deliberate tree
639
+ change is WT-4: the terminal merge actions select the **invoking** tree, not the execution tree —
640
+ and pin it explicitly the same way. FF ancestry, marker and cleanup sequencing remain owned by
641
+ 1059.
642
+
629
643
  **Portability (R10).** Use portable `git worktree` commands only. Do **not** depend on the Claude
630
644
  Code `EnterWorktree`/`ExitWorktree` tools — the `sp` plugin ships to Codex, Gemini CLI, pi, omp, and
631
645
  OpenCode. The underlying git mechanics (create / list / remove / prune, sibling-directory naming,
@@ -765,6 +779,10 @@ the tree's own source:
765
779
 
766
780
  cd "<worktree>" && bun apps/cli/src/index.ts task check <wbs> --json
767
781
 
782
+ Run that as a per-call pin subshell (task 1058) — `cd --` plus the `pwd -P` / git-top-level /
783
+ branch identity checks of the canonical protocol (inline-pipeline-driver.md § "Per-call
784
+ execution-tree pin") — rather than relying on a persistent `cd` between host tool calls.
785
+
768
786
  Confirm isolation by making a distinctive change in the worktree and checking that
769
787
  the command reflects it.
770
788
 
@@ -832,13 +850,19 @@ Reuse mode resolves the marker by the resolved worktree's `path` (not by `comman
832
850
  ### WT-3b — Commit the batch's writes on `$BRANCH` (task 0701 R1)
833
851
 
834
852
  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:
853
+ worktree** — including the generated task files under `docs/tasks*/` and the kanban index. The
854
+ commit runs as a per-call pinned subshell (task 1058): it selects the execution tree inside the
855
+ same call, verifies the WT-3 identity, and never relies on a persistent `cd` or a trailing
856
+ `cd -` to restore the caller's directory:
836
857
 
837
858
  ```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
859
+ (
860
+ cd -- "../<worktree-dir>" || { echo "tree missing: expected ../<worktree-dir>" >&2; exit 1; }
861
+ [ "$(git branch --show-current)" = "$BRANCH" ] \
862
+ || { echo "branch mismatch: expected $BRANCH, got $(git branch --show-current)" >&2; exit 1; }
863
+ git add <files-the-batch-wrote>
864
+ git commit -m "<type>(<scope>): <command> <selector> batch writes"
865
+ )
842
866
  ```
843
867
 
844
868
  The FF-only git merge carries only commits — uncommitted writes in the worktree would be left
@@ -855,18 +879,66 @@ only what it created*):
855
879
 
856
880
  ```bash
857
881
  # 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"
882
+ # back onto the base ref there. Per-call tree pinning (task 1058, § above): every command
883
+ # below selects the INVOKING tree (where the WT-3 marker lives), re-pinned with the canonical
884
+ # per-call pin protocol in its own subshell — never the execution tree. The whole sequence is
885
+ # fail-stop: checkout, guard, ancestry check, merge, landed verify, persistence, and cleanup
886
+ # each stop explicitly. Every tip/ancestry read uses local refs — no fetch runs anywhere:
887
+ MARKER=".spur/run/worktree-<marker-id>.json"
888
+ git checkout "$BASE_REF" \
889
+ || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
860
890
  # 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
891
+ # ("Already up to date") while merging nothing — the lines below would then delete
862
892
  # 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
893
+ [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
894
+ || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; } # -> WT-5, marker stays "active"
895
+ # Capture the batch tip BEFORE any cleanup or branch work: every later step (landed verify,
896
+ # mergeCommit, retained report) reads this captured value — a removed branch is never queried.
897
+ BATCH_TIP="$(git rev-parse "$BRANCH")" \
898
+ || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
899
+ # Fresh LOCAL base tip, re-read now; the marker's baseSha may be stale and never governs this
900
+ # decision (an advanced-but-ancestor base still FF-merges, a divergent base halts):
901
+ BASE_TIP="$(git rev-parse "$BASE_REF")" \
902
+ || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
903
+ # Ancestry check immediately before the sole mutation (the FF merge below). Command/read
904
+ # errors are distinguished from ordinary non-ancestry; both fail closed:
905
+ ANCESTRY_RC=0
906
+ git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
907
+ [ "$ANCESTRY_RC" -gt 1 ] \
908
+ && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
909
+ [ "$ANCESTRY_RC" -eq 1 ] && {
910
+ echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
911
+ git log --oneline "$BASE_TIP..$BATCH_TIP" >&2 # batch-side divergent commits
912
+ git log --oneline "$BATCH_TIP..$BASE_TIP" >&2 # base-side divergent commits
913
+ exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
865
914
  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):
915
+ [ $? -eq 0 ] \
916
+ || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; } # -> WT-5
917
+ # Landed verification before anything else runs: the captured BATCH_TIP must be an ancestor of
918
+ # the base ref as it now stands (base tips advancing again later stay valid — this binds the
919
+ # merge result, not the end state):
920
+ LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
921
+ || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
922
+ git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
923
+ || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
924
+ # --- Landed from here on. Any halt below is landed-but-incomplete: it records status
925
+ # --- "retained" + mergeCommit "$BATCH_TIP" via write_marker and stops — never a branch query.
926
+ 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
927
+ mkdir -p "$(dirname "$MARKER")"
928
+ if [ -f "$MARKER" ]; then
929
+ TMP_MARKER="$(mktemp)"
930
+ jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
931
+ || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
932
+ mv "$TMP_MARKER" "$MARKER"
933
+ else
934
+ printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
935
+ "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
936
+ fi
937
+ }
938
+ # WT-4a evidence persistence runs FIRST after the landed verify (Step 5, task 0720 R3):
867
939
  # persist the batch report + verdict artifacts AND the per-run provenance (task
868
940
  # 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.
941
+ # Any persistence failure routes to WT-5 — landed-but-incomplete: retained marker + mergeCommit.
870
942
  WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4b below
871
943
  # WT-4a provenance persist-out (task 0975 R1): copy the worktree DB's run rows plus
872
944
  # the .spur/memory/runs/<runId>.md + .state.json records into THIS tree. Run from the main
@@ -880,7 +952,7 @@ WT_PATH="$(cd "../<worktree-dir>" && pwd)" # hoisted: needed by WT-4a AND WT-4
880
952
  # NOT proceed to WT-4b removal:
881
953
  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
954
  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; }
955
+ || { echo "halt: worktree run-record persist-out failed - worktree retained (WT-5)" >&2; write_marker retained; exit 1; }
884
956
  #
885
957
  # WT-4b — bounded CWD-holder cleanup (task 0720 R1). $WT_PATH above is the EXACT
886
958
  # absolute worktree path; a relative path or a stale entry matches the wrong processes.
@@ -911,39 +983,112 @@ if [ -n "$FINAL" ]; then
911
983
  PORTS="$(lsof -nP -a -p "$(echo "$FINAL" | paste -sd, -)" -iTCP -sTCP:LISTEN 2>/dev/null \
912
984
  | awk 'NR>1 {print $9}' | sort -u | paste -sd' ' -)"
913
985
  echo "halt: worktree still held by PID(s): $FINAL ${PORTS:+listening: $PORTS}" >&2
986
+ write_marker retained # landed-but-incomplete: retained + mergeCommit
914
987
  exit 1 # -> WT-5: retain worktree + branch,
915
988
  fi # NO prune/remove/branch delete
916
- git worktree remove "../<worktree-dir>"
917
- git branch -d "$BRANCH"
989
+ # Pre-removal evidence assertion (task 1067 R1, E71 enforcement): refuse to remove the
990
+ # worktree while it still owns evidence absent from (or divergent from) the invoking tree —
991
+ # the D63 landing skipped persist-out and silently lost verdicts + receipts. Task-file
992
+ # WBSes / run ids ride the same TASK_FILE_ARGS resolved for WT-4a (run ids: --run-id <id>).
993
+ 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)"
994
+ bun "$CHECK_SCRIPT" --from "$WT_PATH" "${TASK_FILE_ARGS[@]}" \
995
+ || { echo "WT-4 halt: worktree evidence would be abandoned - persist-out missing or stale (WT-5)" >&2; write_marker retained; exit 1; }
996
+ git worktree remove "../<worktree-dir>" \
997
+ || { echo "WT-4 halt: worktree remove failed after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
998
+ # Branch deletion runs only after the merge landed, persisted, and the worktree is gone —
999
+ # and it is existence-guarded, so a removed branch is never queried. -D over -d: the landed
1000
+ # verify already proved the merge landed, so an unmerged-refusal would only block proven-merged cleanup.
1001
+ if git rev-parse --verify --quiet "$BRANCH" >/dev/null; then
1002
+ git branch -D "$BRANCH" || { echo "WT-4 halt: cannot delete branch $BRANCH after landed merge $BATCH_TIP" >&2; write_marker retained; exit 1; }
1003
+ fi
918
1004
  # WT-4c — clean up registry entry in ~/.config/spur/projects.json (task 0924)
919
1005
  spur projects remove "$WT_PATH" 2>/dev/null || spur projects clean --json 2>/dev/null || true
920
- # update marker: status = "merged"
1006
+ # Success marker, written once and only here: status "merged" + mergeCommit "$BATCH_TIP" —
1007
+ # after landed verification, required persistence, and cleanup:
1008
+ write_marker merged
921
1009
  ```
922
1010
 
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
1011
+ Every halt above stops explicitly and falls through to **WT-5** — zero-commit branch (task 0701
1012
+ R1), failed checkout, divergent base, failed FF, persist-out failure (task 0975), or surviving CWD
1013
+ holders (task 0720 R1). Pre-merge failures leave the marker at `status: active` and the worktree +
1014
+ branch untouched. Landed-but-incomplete failures record `status: retained` + `mergeCommit`
1015
+ BATCH_TIP: the merge landed, and the retained report reads the captured tip only. While any holder
1016
+ remains, do **not** run `git worktree prune`, `git worktree remove`, or branch deletion — only an
1017
+ EMPTY holder set may proceed to those steps. The holder
926
1018
  halt report names every surviving PID; the listening port is best-effort — a CWD holder may own no
927
1019
  socket, and `lsof` port discovery failing must not hide the PIDs.
928
1020
 
929
1021
  #### Reuse mode — merge, retain
930
1022
 
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:
1023
+ The fail-stop merge sequence runs identically — same guard, captured `BATCH_TIP`/`BASE_TIP`,
1024
+ ancestry check immediately before FF, `git merge --ff-only`, landed verification (per-call pinned
1025
+ to the invoking tree, task 1058) — but no WT-4a/4b/4c and no removal: the worktree and branch are
1026
+ **retained**. Pre-merge halts leave the marker `active`; a persistence failure after the landed
1027
+ merge records `retained` + `mergeCommit`:
933
1028
 
934
1029
  ```bash
935
- git checkout "$BASE_REF"
936
- git merge --ff-only "$BRANCH"
937
- # update marker: status = "merged" (worktree and branch are intentionally NOT removed)
1030
+ # Same fail-stop sequence as create mode (zero-commit guard -> BATCH_TIP/BASE_TIP -> ancestry
1031
+ # rc check -> git merge --ff-only -> landed verify); local refs only — no fetch.
1032
+ MARKER=".spur/run/worktree-<marker-id>.json"
1033
+ git checkout "$BASE_REF" \
1034
+ || { echo "WT-4 halt: checkout $BASE_REF failed - nothing merged, marker stays \"active\"" >&2; exit 1; } # -> WT-5
1035
+ [ "$(git rev-list --count "$BASE_REF..$BRANCH")" -gt 0 ] \
1036
+ || { echo "WT-4 halt: branch carries no commits - nothing to merge" >&2; exit 1; }
1037
+ BATCH_TIP="$(git rev-parse "$BRANCH")" || { echo "WT-4 halt: cannot read $BRANCH" >&2; exit 1; }
1038
+ BASE_TIP="$(git rev-parse "$BASE_REF")" || { echo "WT-4 halt: cannot read $BASE_REF" >&2; exit 1; }
1039
+ ANCESTRY_RC=0
1040
+ git merge-base --is-ancestor "$BASE_TIP" "$BATCH_TIP" || ANCESTRY_RC=$?
1041
+ [ "$ANCESTRY_RC" -gt 1 ] \
1042
+ && { echo "WT-4 halt: ancestry check could not run (git exit $ANCESTRY_RC) - failing closed" >&2; exit 1; }
1043
+ [ "$ANCESTRY_RC" -eq 1 ] && {
1044
+ echo "WT-4 halt: divergent - batch tip $BATCH_TIP vs base tip $BASE_TIP" >&2
1045
+ git log --oneline "$BASE_TIP..$BATCH_TIP" >&2
1046
+ git log --oneline "$BATCH_TIP..$BASE_TIP" >&2
1047
+ exit 1; } # -> WT-5: tree + branch retained, marker stays "active"
1048
+ git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
1049
+ [ $? -eq 0 ] \
1050
+ || { echo "WT-4 halt: FF-merge failed - the concurrent-writer race survives every precheck; marker stays \"active\"" >&2; exit 1; }
1051
+ LANDED_BASE_TIP="$(git rev-parse "$BASE_REF")" \
1052
+ || { echo "WT-4 halt: cannot read landed base tip" >&2; exit 1; }
1053
+ git merge-base --is-ancestor "$BATCH_TIP" "$LANDED_BASE_TIP" \
1054
+ || { echo "WT-4 halt: landed verify failed - $BATCH_TIP is not an ancestor of $LANDED_BASE_TIP" >&2; exit 1; }
1055
+ WT_PATH="<resolved-worktree-path>" # reuse mode: the adopted tree's physical path
1056
+ 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
1057
+ mkdir -p "$(dirname "$MARKER")"
1058
+ if [ -f "$MARKER" ]; then
1059
+ TMP_MARKER="$(mktemp)"
1060
+ jq --arg s "$1" --arg m "$BATCH_TIP" '.status = $s | .mergeCommit = $m' "$MARKER" > "$TMP_MARKER" \
1061
+ || { echo "WT-4 halt: cannot rewrite marker $MARKER" >&2; rm -f "$TMP_MARKER"; exit 1; }
1062
+ mv "$TMP_MARKER" "$MARKER"
1063
+ else
1064
+ printf '{ "baseRef": "%s", "baseSha": "%s", "branch": "%s", "path": "%s", "status": "%s", "mergeCommit": "%s" }\n' \
1065
+ "$BASE_REF" "$BASE_SHA" "$BRANCH" "$WT_PATH" "$1" "$BATCH_TIP" > "$MARKER"
1066
+ fi
1067
+ }
1068
+ # Step 5 evidence persistence (task 0720 R3) — required BEFORE the success marker:
1069
+ if printf 'batch report %s\n' "$BATCH_TIP" > ".spur/run/worktree-<marker-id>-batch-report.md"; then :; else
1070
+ echo "WT-5 halt: persistence failed after landed merge $BATCH_TIP; recovery reads captured tips only" >&2
1071
+ write_marker retained
1072
+ exit 1
1073
+ fi
1074
+ # Success marker: status "merged" + mergeCommit "$BATCH_TIP" — written once, only after landed verification and required persistence.
1075
+ # The worktree and branch are intentionally NOT removed.
1076
+ write_marker merged
938
1077
  ```
939
1078
 
940
1079
  The operator supplied the tree, so the operator owns its lifetime. After a green reuse batch
941
1080
  `baseRef == $BRANCH`, so the same worktree keeps fast-forwarding on the next invocation instead of
942
1081
  having to be rebuilt — the continue-the-work loop is stable.
943
1082
 
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
1083
+ **Fast-forward only.** `git merge --ff-only` is the sole mutation of the terminal sequence; every
1084
+ other step is a read, a check, a persistence write, or a cleanup, and every tip/ancestry read uses
1085
+ local refs — no fetch runs anywhere in this lifecycle. If the fresh `BASE_TIP` is not an ancestor of
1086
+ `BATCH_TIP`, the ancestry check halts before the merge: the report names both tips and both
1087
+ divergent commit ranges (`$BASE_TIP..$BATCH_TIP` and `$BATCH_TIP..$BASE_TIP`) and falls to the
1088
+ retention path (WT-5) with the tree and branch retained. A concurrent writer that lands between the
1089
+ ancestry check and the merge still trips `--ff-only` — the race survives every precheck, and a
1090
+ failed FF leaves the marker at `status: active`. Do **not** rebase, merge-commit, or resolve
1091
+ conflicts. The corpus files (`docs/tasks*/`, kanban/index) are
947
1092
  auto-generated and conflict-prone; automated conflict resolution over generated files is exactly the
948
1093
  wrong thing to attempt unattended. FF-only means the merge either is trivially correct or does not
949
1094
  happen.
@@ -952,8 +1097,10 @@ happen.
952
1097
  `--worktree --auto` batch — it does **not** pause even though it performs a merge and a branch
953
1098
  deletion. That is the explicit single exception to Auto-Decision Principle #6 (`cross-cutting.md`),
954
1099
  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.
1100
+ is safe because `git merge --ff-only` fails closed (it refuses rather than risk losing work), the
1101
+ branch deletion runs only after the merge has landed, persisted, and cleaned up — and is
1102
+ existence-guarded (`git rev-parse --verify --quiet`), so a removed branch is never queried;
1103
+ WT-5 retains the worktree and branch whenever FF is impossible or any task fails.
957
1104
  Reuse mode is **narrower** than the carve-out (it merges but does not delete the branch), so the
958
1105
  carve-out text needs no widening.
959
1106
 
@@ -982,22 +1129,44 @@ record and the persisted run artifacts are the evidence record.
982
1129
 
983
1130
  ### WT-5 — Failure path: retain and report (R5)
984
1131
 
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 —
1132
+ On any per-task failure, batch halt, HITL pause that ends the run, or a WT-4 halt, the
1133
+ worktree directory and branch are left **intact**. No destructive automation on this path under
1134
+ any flag combination (`--auto`, `--force`, `--keep-going` — all leave the worktree in place). The
1135
+ marker records which class of halt occurred (frozen vocabulary):
1136
+
1137
+ - **Pre-merge failure** (failed checkout, zero-commit guard, divergent base, failed FF): the merge
1138
+ never landed, so the marker **stays at `status: active`** — the WT-3 marker stands and WT-5 is
1139
+ its disposition. Nothing was merged onto the base ref.
1140
+ - **Landed-but-incomplete failure** (persist-out, holder cleanup, or worktree removal failed after
1141
+ a landed merge): the marker records `status: retained` + `mergeCommit` BATCH_TIP — a truthful
1142
+ report that the merge landed. The retained path reads the captured `BATCH_TIP` only; a removed
1143
+ branch is never queried.
1144
+
1145
+ The worktree's own `.spur` lifecycle DB is retained with the tree,
1146
+ so nothing is lost on either path (see the WT-4 lifecycle-DB disposition for the merged case —
990
1147
  task 0701 R2d). Emit a retention report in the existing halt-report shape:
991
1148
 
1149
+ **Persist-out skip recovery (task 1067 R2, E71).** When evidence was lost to a landing that
1150
+ skipped persist-out (or the pre-removal assertion fired too late), verdicts remain derivable
1151
+ from the task records — `spur task show <wbs> --json` carries the recorded verdict and its
1152
+ proof block, so a lost `<wbs>-verdict.json` is regenerated by re-recording or copying from
1153
+ the task store. Feature run/latest receipts are re-runnable, not recovered: rerun the
1154
+ feature-verification workflow in the invoking tree —
1155
+ `spur workflow run feature-verification.yaml --vars '{"featureId":"<id>"}'` — which rewrites
1156
+ the receipt files it owns. Divergent files are never auto-overwritten; reconcile by hand.
1157
+
992
1158
  ```
993
1159
  ## Worktree retained — <command> <selector>
994
1160
 
995
- **Halt cause:** <one-line cause — batch halted at task <wbs> / non-FF base ref / HITL pause>
1161
+ **Halt cause:** <one-line cause — batch halted at task <wbs> / non-FF base ref / HITL pause / landed-but-incomplete cleanup failure>
996
1162
  **Worktree path:** ../<worktree-dir>
997
1163
  **Branch:** sp/<command>-<selector-slug>-<short-id>
998
1164
  **Base ref:** <base-ref> (<base-sha>)
1165
+ **Merge state:** pre-merge failure — nothing landed | landed-but-incomplete — mergeCommit <batch-tip-sha>
999
1166
 
1000
- The worktree and its branch are intact. Nothing was merged onto the base ref.
1167
+ The worktree and its branch are intact.
1168
+ pre-merge failure: nothing was merged onto the base ref.
1169
+ landed-but-incomplete: the batch merge LANDED as <batch-tip-sha> on <base-ref>; retention covers post-merge persistence/cleanup only.
1001
1170
  Resume, merge, or discard:
1002
1171
 
1003
1172
  resume: cd <worktree-path> && <command> --continue --worktree <worktree-path>
@@ -1030,6 +1199,16 @@ are long and already resumable via `--continue`; auto-deleting is data loss, aut
1030
1199
  partial result presented as a whole. The answer to "what happens if it fails" is "nothing happens,
1031
1200
  and we tell you where the work is."
1032
1201
 
1202
+ **Partial worktree-removal recovery is inspection-only.** If a create-mode run halted partway
1203
+ through removal (leftover directory, stale registration), the recovery pass gathers facts and
1204
+ nothing else: `git worktree list` (is the tree still registered?), `ls` of the path (what remains
1205
+ on disk?), and the marker / `~/.config/spur/projects.json` registration state. Before removing
1206
+ anything, verify the leftover path is the batch's **owned** path — match it against the marker's
1207
+ `path`/`branch` (or the resolved `--worktree <name>` identity), never a guessed sibling directory.
1208
+ Recursive deletion of leftover directories that survived `git worktree remove` is **never
1209
+ automatic** — it requires explicit operator authorization. No automatic rebase, recovery merge,
1210
+ `git worktree prune`, or holder kill runs on this path.
1211
+
1033
1212
  ### WT-6 — `--continue` re-entry (R7)
1034
1213
 
1035
1214
  A `--continue` resume of a batch started with `--worktree` must re-enter the existing worktree via
@@ -1172,7 +1351,7 @@ integrate(t):
1172
1351
  ├─ rebase fails → git -C "$WT" rebase --abort → conflict path (R4) below
1173
1352
  └─ rebase clean → git checkout "$BASE_REF" → zero-commit guard → git merge --ff-only "$BRANCH"
1174
1353
  → WT-4a verdict persist → WT-4b holders → WT-4c registry
1175
- → git worktree remove → git branch -d → marker merged
1354
+ → git worktree remove → existence-guarded git branch -D → marker merged
1176
1355
  ```
1177
1356
 
1178
1357
  The merge is always `--ff-only`: a base that will not fast-forward after a clean rebase is a
@@ -203,7 +203,9 @@ When `--next` is set and implementation succeeds:
203
203
  2. **Record provenance** — `spur task run-link <wbs> --source next-auto --json`. Writes a
204
204
  `kind: pipeline` entry into `task_run_links` so the `testing → done` provenance guard
205
205
  (lifecycle-adapter.ts L106-131) accepts the in-session implementation path. Idempotent:
206
- safe to call even when a pipeline link already exists.
206
+ safe to call even when a pipeline link already exists. Run it from the same cwd as the
207
+ `task update` calls in step 1 — per-tree DB isolation makes a link from another tree
208
+ invisible to this tree's guard.
207
209
  3. **On a clean transition:** invoke `/sp:dev-verify <wbs> --auto --next` (`--auto` propagates
208
210
  down the whole chain). The verify step's `--next` transition to `done` now passes the
209
211
  provenance guard because step 2 recorded the link.
@@ -17,17 +17,16 @@ patterns, skill-backed, inline) remains in `dev-operations.md`.
17
17
 
18
18
  Every flag shared by two or more commands has exactly one entry here. This is the single place a
19
19
  shared flag's meaning is stated; command argument-hints and table rows cite it and must not
20
- re-define it. The shared-flag set is derived mechanically from the 28 command argument-hints (a flag
20
+ re-define it. The shared-flag set is derived mechanically from all live `dev-*` command argument-hints (a flag
21
21
  counts as shared when it appears in two or more); flags appearing on a single command stay documented
22
22
  in that command's body and are not listed here.
23
23
 
24
- **Reference form (mechanically detectable).** A command file points at a glossary entry with a
25
- markdown link whose link target is the entry's anchor — `[`--next`](#flag-next)`. The anchor is
26
- always `#flag-<name>` where `<name>` is the flag minus its leading `--` (so `--keep-going` →
27
- `#flag-keep-going`). The test gate (`command-flag-parity.test.ts`, task 0403) finds these references
28
- with the regex ``\[`--<flag>`\]\(#flag-`` over each command file — **a shared flag declared without
29
- this reference fails the build.** Prose-only citations (the flag name mentioned in a sentence but not
30
- in the link form) do not count; the reference must be the link.
24
+ **Reference form (mechanically detectable).** Each `dev-*` command carries exactly one canonical
25
+ footer link to `../skills/spur-dev/references/flag-glossary.md`, enforced by
26
+ `scripts/commands/validate-commands.ts` (task 0412). Per-flag deep links are not required.
27
+ `command-flag-parity.test.ts` derives shared flags from all live hints and requires exactly one
28
+ canonical glossary entry per shared flag. Each entry retains its `#flag-<name>` anchor, where
29
+ `<name>` is the flag minus its leading `--` (so `--keep-going` → `#flag-keep-going`), for navigation.
31
30
 
32
31
  **Availability rule.** Two flags are only declared where the underlying capability already exists:
33
32
  `--json` where the command already produces a structured result a script could consume; `--auto`
@@ -75,6 +75,9 @@ the human/native presentation layer — labels are display addresses only, never
75
75
  2. Allocate a collision-resistant inline run id (`uuidgen`, with a timestamp/pid fallback), create
76
76
  `.spur/run/` for attempt staging and `.spur/memory/runs/` for retained records, and use the two-file run record (task 0927) — append lines to
77
77
  `.spur/memory/runs/<run-id>.md` and read machine state from `.spur/memory/runs/<run-id>.state.json`.
78
+ The run id lives in those two files and the persisted run row — never stage it in an ad-hoc
79
+ `*.env` file: the `no-env-files` gate rule flags any `.env` name, so run-scoped staging files
80
+ use `<run-id>-*` names with `.md`/`.json`/`.txt`/`.status` extensions only.
78
81
  3. **Authoritative run identity (task 0804 R1, fail-closed).** Persist the run row through the
79
82
  internal delegate before any stage executes — this is what makes bound `run.artifact` record
80
83
  accept the inline run (0785 R3):
@@ -139,7 +142,10 @@ the human/native presentation layer — labels are display addresses only, never
139
142
  safety checks succeed, create/adopt and `cd` into the execution tree; confirm absolute cwd,
140
143
  branch, base SHA, and ownership. An invalid/empty target, unsupported mode, ambiguous ownership,
141
144
  or stale target stops without creating a tree or discarding work. All subsequent tools, agents,
142
- corpus writes, and run artifacts use the confirmed execution tree.
145
+ corpus writes, and run artifacts use the confirmed execution tree — **re-selected per call, not
146
+ inherited**: a host shell call starts in an arbitrary directory and carries no cwd from the
147
+ previous call, so every later call runs inside the **Per-call execution-tree pin** protocol
148
+ below (task 1058), which consumes the identity confirmed here.
143
149
  8. **Publish the workflow inventory (R4), BEFORE reading the YAML.** Resolve the selected workflow
144
150
  through the same project/bundled resolver as execution and run
145
151
  `spur workflow show <resolved-file> --no-logo --format todo --json`. Validate the projection with
@@ -183,6 +189,12 @@ the human/native presentation layer — labels are display addresses only, never
183
189
  This is required for the normal `testing → done` provenance guard. Planning pipelines have no
184
190
  task lifecycle link and skip this task-specific action.
185
191
 
192
+ **Run it from the task's execution tree.** Every tree has an isolated `.spur` DB, so a link
193
+ recorded from the invoking tree never satisfies a worktree's guard — the exact failure that
194
+ forced audited `--provenance-bypass` on tasks 1029/1030 (E93). Under `--worktree`, enter the
195
+ worktree first and run this command (and every lifecycle-touching command: `task record`,
196
+ `task update <wbs> testing|done`) from the worktree root.
197
+
186
198
  ### Idea-pipeline quick start (0887 R1/R2)
187
199
 
188
200
  Minimum files to read for `/sp:dev-idea` inline runs — then drive `idea-pipeline.yaml`:
@@ -212,6 +224,91 @@ Expected artifacts per stage (all run-scoped under `.spur/run/<run-id>-*`):
212
224
  | ready-prepare | `-idea-ready.json` |
213
225
  | handoff-finalize | `-idea-handoff.md` |
214
226
 
227
+ ## Per-call execution-tree pin (task 1058)
228
+
229
+ A host shell tool call may start in an arbitrary directory; no earlier call's `cd` is input to the
230
+ next call. During inline run `95522d21` that drift silently landed corpus writes in the invoking
231
+ tree instead of the run worktree. Every host-session corpus command — reads, writes, absolute
232
+ section-input files and output artifacts — therefore selects the confirmed execution tree **within
233
+ the same tool call**, in a self-contained subshell. The identity is never re-derived: it is the
234
+ tree path, branch and resolved Spur invocation confirmed at bootstrap isolation (step 7) and, for
235
+ batch worktrees, recorded in the WT-3 marker (execution-batch.md). The resolved invocation is
236
+ field 4 of the dispatch payload — never a bare `spur`.
237
+
238
+ Template — every host shell call follows this shape; paths are quoted because trees may contain
239
+ spaces:
240
+
241
+ ```bash
242
+ (
243
+ cd -- "$SPUR_TREE" || { echo "tree missing: expected $SPUR_TREE" >&2; exit 1; }
244
+ ACTUAL_TREE="$(pwd -P)"
245
+ [ "$ACTUAL_TREE" = "$SPUR_TREE" ] || { echo "tree mismatch: expected $SPUR_TREE, got $ACTUAL_TREE" >&2; exit 1; }
246
+ GIT_TOP="$(git rev-parse --show-toplevel)" || exit 1
247
+ [ "$GIT_TOP" = "$SPUR_TREE" ] || { echo "toplevel mismatch: expected $SPUR_TREE, got $GIT_TOP" >&2; exit 1; }
248
+ ACTUAL_BRANCH="$(git branch --show-current)"
249
+ [ "$ACTUAL_BRANCH" = "$SPUR_BRANCH" ] || { echo "branch mismatch: expected $SPUR_BRANCH, got $ACTUAL_BRANCH" >&2; exit 1; }
250
+ exec <resolved-spur-invocation> task show <wbs> --json # the already-resolved CLI command
251
+ )
252
+ ```
253
+
254
+ Rules (R1):
255
+
256
+ - **Pin reads as well as writes.** `task show`, `feature show`, `workflow show` and `task check`
257
+ use the same subshell — a read from the wrong tree is a wrong answer, not a harmless one.
258
+ - **Validate the returned `filePath`.** A pinned `task show --json` must return a `filePath`
259
+ inside the expected configured corpus/tree. Configured task folders may intentionally live
260
+ outside the default `docs/tasks` — check membership in the configured folder set, not one
261
+ literal path.
262
+ - **Prove sections with a fresh pinned read.** After a section write, re-run a pinned
263
+ `task show <wbs> --json` and assert the new body there; a heading-only grep is not proof of
264
+ section contents.
265
+ - **Resolve section input/output paths before changing tree.** `--section <name> --from-file
266
+ <path>`, `answerFile`/`expectFile` and run artifacts are resolved against the confirmed tree
267
+ (dispatch field 5) before the subshell `cd`s.
268
+ - **Fail closed before writing.** A failed `cd` or any identity mismatch exits nonzero naming
269
+ expected vs actual, before the CLI runs — there is no designated exit code; consumers read
270
+ nonzero and the named expected/actual trees. A PWD assertion alone cannot select a tree: `cd`
271
+ first, then verify the physical path and repository identity.
272
+ - **Native tool cwd options are additive.** A tool-level cwd/`cd` pin may additionally set the
273
+ call's directory, but never replaces the identity checks above.
274
+ - **Host-only scope.** Engine `shell` actions already bind `context.workdir`
275
+ (`packages/app/src/workflow/actions/shell.ts:98`) — leave them alone. No service-wide
276
+ `process.chdir`, no new public flag, no helper framework.
277
+
278
+ The `spur …` command examples in this driver (run setup, workflow inventory, `task run-link`,
279
+ fingerprint capture, record/done sequencing) abbreviate the pinned subshell form for readability;
280
+ each executes inside the template above with the confirmed tree, branch and resolved invocation
281
+ substituted. A dispatched subagent inherits the same duty: its own tool calls may each start in
282
+ an arbitrary directory, so every shell call the delegate makes re-pins the execution tree with
283
+ the identity supplied in the dispatch payload — never re-derived.
284
+
285
+ ## Chunked implement dispatch contract (cap-limited worker models, task 1066)
286
+
287
+ Cap-limited worker executors die on monolithic implement briefs: the output cap is consumed by
288
+ one giant thinking block before any file action lands. Recognized failure signature —
289
+ consecutive implement dispatches end with `stopReason=length` and **zero output** (no tool
290
+ call, no file write), optionally with a `contact_supervisor` call mid-death. The trigger is
291
+ the observed signature, never a model name: any executor that exhibits it is treated as
292
+ cap-limited for the rest of the run.
293
+
294
+ Before dispatching an `implement` stage to a worker executor:
295
+
296
+ 1. **Chunk the brief.** Split the implement scope into small, independent dispatches ordered
297
+ by dependency (e.g. test cases → doc rewrite → task sections), one bounded objective per
298
+ dispatch. Never pack the whole task plan into one dispatch.
299
+ 2. **Cap thinking.** Instruct the worker to keep every thinking block under ~150 words and act
300
+ between thoughts; never plan the whole task in one block.
301
+ 3. **Fallback ladder.** A worker still dying with the signature: re-chunk the remaining scope
302
+ smaller once; still failing → execute the stage host-inline (the driver's inline stage
303
+ fallback) and record the fallback in the run log.
304
+
305
+ Evidence (2026-10-02, task 1059 implement stage, runall-D63-2ebbd97c): 4/4 single-shot
306
+ implement dispatches died `stopReason=length` on a 16k-output-cap worker; the same scope in
307
+ three chunked dispatches with the thinking cap succeeded 3/3. See the pitfalls 2026-10-02
308
+ entry and `.spur/memory/runs/runall-D63-2ebbd97c.md`.
309
+
310
+ ## Comprehensive-check retention and evidence (R7/R8)
311
+
215
312
  ## Comprehensive-check retention and evidence (R7/R8)
216
313
 
217
314
  **R7 — comprehensive checks stay at their owning boundaries.** Quick readiness and plan projection are
@@ -293,7 +390,8 @@ Action semantics come from the YAML and the workflow action contract:
293
390
  `verify-verdict`: verdict `PASS`), `proofBinding: current` is honored against a freshly captured
294
391
  proof digest — capture it with `bun "$SETUP_SCRIPT" --fingerprint --task-file <task path>
295
392
  [--feature-file <feature path>]`, the same entry point used at Run setup, which prints the engine
296
- `sha256:<hex>` digest. Run it from the worktree root (cwd feeds the git-tree half of the digest)
393
+ `sha256:<hex>` digest. Run it inside a per-call pin subshell for the confirmed execution tree
394
+ (the cwd feeds the git-tree half of the digest)
297
395
  and pass the same `--feature-file` the run folded in — omitting it, or running from elsewhere,
298
396
  yields a different digest and the mismatch surfaces later as a refused `run.artifact`
299
397
  registration — and the run-scoped review-completion marker exists — then appends one provenance
@@ -311,6 +409,10 @@ Action semantics come from the YAML and the workflow action contract:
311
409
 
312
410
  **Native-subagent dispatch (R2 eligibility, evaluated before each action):**
313
411
 
412
+ Before dispatching an `implement` stage to a worker executor, apply the chunked implement
413
+ dispatch contract (§ Chunked implement dispatch contract): chunk the brief, cap thinking
414
+ blocks, and know the fallback ladder before spending a dispatch.
415
+
314
416
  1. The invocation is one of the two interactive inline full-pipeline surfaces (`dev-run --mode full`
315
417
  or sequential `dev-runall`) and the resolved selector is inline — i.e. `--agent` **omitted**
316
418
  (0687 R1 default) or `--agent inline` passed explicitly (0687 R2 generalized from omit-only).
@@ -388,7 +490,10 @@ boundary, and a delegate left to re-derive them re-derives them against its own
388
490
  that invocation and MUST NOT rely on a bare `spur`: a competing `spur` earlier on the delegate's
389
491
  PATH otherwise wins. Setting `SPUR_BIN` alone does **not** change bare-command resolution — only
390
492
  using the supplied invocation does. Spur-owned scripted calls take it through the existing
391
- `--spur-bin` flag rather than a new mechanism.
493
+ `--spur-bin` flag rather than a new mechanism. A delegate's tool calls may each start in an
494
+ arbitrary directory, so every shell call the delegate makes re-pins the execution tree per the
495
+ **Per-call execution-tree pin** protocol using this supplied identity — never an inherited cwd
496
+ and never a re-derivation.
392
497
  5. The **resolved absolute output path** (`answerFile`/`expectFile`, resolved as above) and the
393
498
  **owning stage's artifact contract** — for a verify stage, the compact contract below.
394
499
  6. **The implement-stage acceptance-evidence requirement** — for the implement stage and any stage
@@ -633,6 +738,8 @@ tasks (0617, 0619) because the sections were hand-written **before** the verdict
633
738
  ```bash
634
739
  # verdict artifact first (shape: {wbs, verdict, requirements:[{id,status,evidence}], checks:[], source})
635
740
  # then the record hop; then re-write Testing/Solution if record's backfill is thinner than intended.
741
+ # Both calls are per-call pinned (task 1058): the record hop selects the execution tree inside
742
+ # the same tool call — never through a cwd a previous call happened to leave behind.
636
743
  spur task update <wbs> wip --no-lifecycle
637
744
  spur task record <wbs> --solution-from-diff --transition testing
638
745
  ```