@gobing-ai/spur 0.3.47 → 0.3.49

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 (148) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +94 -15
  3. package/config/transition-shims.json +33 -0
  4. package/config/workflows/basic.yaml +2 -0
  5. package/config/workflows/docs-pipeline.yaml +2 -0
  6. package/config/workflows/feature-dev.yaml +8 -0
  7. package/config/workflows/idea-pipeline.yaml +10 -0
  8. package/config/workflows/planning-pipeline.yaml +4 -0
  9. package/config/workflows/pr-review.yaml +338 -0
  10. package/config/workflows/task-pipeline.yaml +8 -0
  11. package/config/workflows/wayfinder-resolution.yaml +4 -0
  12. package/config/workflows/wrapup-pipeline.yaml +18 -1
  13. package/package.json +8 -8
  14. package/plugins/sp/README.md +9 -6
  15. package/plugins/sp/agents/expert-spur.md +1 -0
  16. package/plugins/sp/commands/dev-arch.md +2 -1
  17. package/plugins/sp/commands/dev-brainstorm.md +2 -1
  18. package/plugins/sp/commands/dev-changelog.md +1 -0
  19. package/plugins/sp/commands/dev-daily.md +1 -0
  20. package/plugins/sp/commands/dev-debug.md +2 -1
  21. package/plugins/sp/commands/dev-dogfood.md +2 -1
  22. package/plugins/sp/commands/{dev-featurechange.md → dev-feature-change.md} +8 -10
  23. package/plugins/sp/commands/dev-find-conflict.md +2 -1
  24. package/plugins/sp/commands/dev-find-issue.md +36 -43
  25. package/plugins/sp/commands/dev-find-next.md +5 -4
  26. package/plugins/sp/commands/dev-fixall.md +1 -0
  27. package/plugins/sp/commands/dev-gitmsg.md +1 -0
  28. package/plugins/sp/commands/dev-gtd.md +12 -12
  29. package/plugins/sp/commands/dev-handover.md +1 -0
  30. package/plugins/sp/commands/dev-history-load.md +63 -0
  31. package/plugins/sp/commands/dev-idea.md +1 -0
  32. package/plugins/sp/commands/dev-next.md +2 -1
  33. package/plugins/sp/commands/dev-parallel.md +2 -1
  34. package/plugins/sp/commands/dev-plan.md +2 -1
  35. package/plugins/sp/commands/dev-pr-review.md +39 -0
  36. package/plugins/sp/commands/dev-refine.md +5 -3
  37. package/plugins/sp/commands/dev-refineall.md +2 -1
  38. package/plugins/sp/commands/dev-refresh.md +2 -1
  39. package/plugins/sp/commands/dev-reverse.md +2 -1
  40. package/plugins/sp/commands/dev-review.md +2 -1
  41. package/plugins/sp/commands/dev-run.md +3 -2
  42. package/plugins/sp/commands/dev-runall.md +3 -2
  43. package/plugins/sp/commands/dev-simplify.md +2 -1
  44. package/plugins/sp/commands/dev-unit.md +2 -1
  45. package/plugins/sp/commands/dev-verify.md +2 -1
  46. package/plugins/sp/commands/dev-verifyall.md +2 -1
  47. package/plugins/sp/commands/dev-wrap.md +7 -5
  48. package/plugins/sp/commands/dev-wrapall.md +7 -5
  49. package/plugins/sp/commands/rule-add.md +1 -0
  50. package/plugins/sp/commands/rule-refine.md +1 -0
  51. package/plugins/sp/commands/rule-scan.md +1 -0
  52. package/plugins/sp/commands/spur-init.md +1 -0
  53. package/plugins/sp/commands/workflow-add.md +1 -0
  54. package/plugins/sp/commands/workflow-refine.md +1 -0
  55. package/plugins/sp/hooks/careful-guard.ts +5 -80
  56. package/plugins/sp/hooks/destructive-policy.ts +146 -0
  57. package/plugins/sp/hooks/pi/guard-extension.ts +33 -46
  58. package/plugins/sp/hooks/task-file-policy.ts +31 -0
  59. package/plugins/sp/hooks/task-write-guard.ts +4 -0
  60. package/plugins/sp/plugin.json +1 -1
  61. package/plugins/sp/references/roles.md +106 -0
  62. package/plugins/sp/scripts/feature-sync-bounded.ts +28 -2
  63. package/plugins/sp/scripts/history-load.ts +400 -0
  64. package/plugins/sp/scripts/pr-reviewing.ts +867 -0
  65. package/plugins/sp/scripts/stage-registry-adapter.ts +66 -31
  66. package/plugins/sp/scripts/surface-drift-inventory.ts +908 -0
  67. package/plugins/sp/scripts/task-size-precheck.ts +30 -4
  68. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  69. package/plugins/sp/scripts/validate-commands.ts +33 -2
  70. package/plugins/sp/scripts/validate-flag-contracts.ts +5 -2
  71. package/plugins/sp/skills/code-implementation/SKILL.md +9 -1
  72. package/plugins/sp/skills/code-verification/SKILL.md +29 -28
  73. package/plugins/sp/skills/issue-finding/SKILL.md +123 -141
  74. package/plugins/sp/skills/issue-finding/examples/expected-findings.json +1 -1
  75. package/plugins/sp/skills/issue-finding/references/session-formats.md +87 -90
  76. package/plugins/sp/skills/next-feature/SKILL.md +6 -6
  77. package/plugins/sp/skills/next-feature/references/handoff-routing.md +5 -5
  78. package/plugins/sp/skills/next-feature/references/signal-derivation.md +7 -2
  79. package/plugins/sp/skills/next-router/SKILL.md +1 -1
  80. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +40 -2
  81. package/plugins/sp/skills/pr-reviewing/SKILL.md +285 -0
  82. package/plugins/sp/skills/spur-cli/SKILL.md +3 -0
  83. package/plugins/sp/skills/spur-cli/references/agent.md +12 -7
  84. package/plugins/sp/skills/spur-cli/references/features/hierarchy-mece.md +5 -5
  85. package/plugins/sp/skills/spur-cli/references/features.md +1 -1
  86. package/plugins/sp/skills/spur-cli/references/team.md +10 -3
  87. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  88. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +17 -0
  89. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +44 -23
  90. package/plugins/sp/skills/spur-dev/references/dev-operations.md +14 -11
  91. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +14 -12
  92. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +28 -7
  93. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +2 -0
  94. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +12 -2
  95. package/schemas/spur-config.schema.json +47 -3
  96. package/spur.js +12223 -7716
  97. package/web/_astro/BoardApp.8hiqShQn.js +1 -0
  98. package/web/_astro/{BoardApp.DKyrGxdo.js → BoardApp.BjQUNhuj.js} +74 -74
  99. package/web/_astro/{TaskDetail.6-27_LMa.js → TaskDetail.CVBuD6dF.js} +1 -1
  100. package/web/_astro/{arc.Df-9AQvS.js → arc.BMMjdODi.js} +1 -1
  101. package/web/_astro/{architectureDiagram-3BPJPVTR.VAI_-paS.js → architectureDiagram-3BPJPVTR.BU5ShzXf.js} +1 -1
  102. package/web/_astro/{blockDiagram-GPEHLZMM.DFpUY1ue.js → blockDiagram-GPEHLZMM.Bj1iEqPD.js} +1 -1
  103. package/web/_astro/{c4Diagram-AAUBKEIU.CF8doOpg.js → c4Diagram-AAUBKEIU.vX8wepCL.js} +1 -1
  104. package/web/_astro/channel.EwdSemIC.js +1 -0
  105. package/web/_astro/{chunk-2J33WTMH.BnjK3fjt.js → chunk-2J33WTMH.BKAipTym.js} +1 -1
  106. package/web/_astro/{chunk-4BX2VUAB.x6ZDnJKq.js → chunk-4BX2VUAB.B68XkPG7.js} +1 -1
  107. package/web/_astro/{chunk-55IACEB6.zY-0uu7w.js → chunk-55IACEB6.BmeDLcrc.js} +1 -1
  108. package/web/_astro/{chunk-727SXJPM.BZxKg_Vi.js → chunk-727SXJPM.PDuBA3Kw.js} +1 -1
  109. package/web/_astro/{chunk-AQP2D5EJ.Cpi9G9Td.js → chunk-AQP2D5EJ.C7A044za.js} +1 -1
  110. package/web/_astro/{chunk-FMBD7UC4.DWTB-Pif.js → chunk-FMBD7UC4.BtzKKFqR.js} +1 -1
  111. package/web/_astro/{chunk-ND2GUHAM.BPDQbiOG.js → chunk-ND2GUHAM.BJuDeeOy.js} +1 -1
  112. package/web/_astro/{chunk-QZHKN3VN.BRWIcuoM.js → chunk-QZHKN3VN.DSeMDgcQ.js} +1 -1
  113. package/web/_astro/{classDiagram-4FO5ZUOK.mGTCZsDO.js → classDiagram-4FO5ZUOK.D53Q4tCw.js} +1 -1
  114. package/web/_astro/{classDiagram-v2-Q7XG4LA2.mGTCZsDO.js → classDiagram-v2-Q7XG4LA2.D53Q4tCw.js} +1 -1
  115. package/web/_astro/{cose-bilkent-S5V4N54A.D1GEut-z.js → cose-bilkent-S5V4N54A.c712AFRH.js} +1 -1
  116. package/web/_astro/{dagre-BM42HDAG.BV0XG9Do.js → dagre-BM42HDAG.D-idisph.js} +1 -1
  117. package/web/_astro/{diagram-2AECGRRQ.DzpYxsjo.js → diagram-2AECGRRQ.DLgnsJCU.js} +1 -1
  118. package/web/_astro/{diagram-5GNKFQAL.Cm9YzJh4.js → diagram-5GNKFQAL.BiaxBVqx.js} +1 -1
  119. package/web/_astro/{diagram-KO2AKTUF.BjhottUj.js → diagram-KO2AKTUF.C8HX1vd8.js} +1 -1
  120. package/web/_astro/{diagram-LMA3HP47.BFsQW5kb.js → diagram-LMA3HP47.CfqDLLes.js} +1 -1
  121. package/web/_astro/{diagram-OG6HWLK6.8pdpzSWO.js → diagram-OG6HWLK6.15SDiEed.js} +1 -1
  122. package/web/_astro/{erDiagram-TEJ5UH35.Bd7KUJmJ.js → erDiagram-TEJ5UH35.DksYtOYM.js} +1 -1
  123. package/web/_astro/{flowDiagram-I6XJVG4X.7LWffkaE.js → flowDiagram-I6XJVG4X.DR_Au-HV.js} +1 -1
  124. package/web/_astro/{ganttDiagram-6RSMTGT7.BeDcO5tI.js → ganttDiagram-6RSMTGT7.CHhHrffI.js} +1 -1
  125. package/web/_astro/{gitGraphDiagram-PVQCEYII.Ca4n730A.js → gitGraphDiagram-PVQCEYII.B2Xehvam.js} +1 -1
  126. package/web/_astro/{index.Dbvuw6d4.css → index.DAxu50UF.css} +1 -1
  127. package/web/_astro/{infoDiagram-5YYISTIA.B0OakQYb.js → infoDiagram-5YYISTIA.C9c3CNNN.js} +1 -1
  128. package/web/_astro/{ishikawaDiagram-YF4QCWOH.DSmNQe-1.js → ishikawaDiagram-YF4QCWOH.BibUHkh8.js} +1 -1
  129. package/web/_astro/{journeyDiagram-JHISSGLW.Cy5ruEUu.js → journeyDiagram-JHISSGLW.BYoVHiyO.js} +1 -1
  130. package/web/_astro/{kanban-definition-UN3LZRKU.CUJXub0p.js → kanban-definition-UN3LZRKU.CM1K5wHE.js} +1 -1
  131. package/web/_astro/{linear.DC1jCCXn.js → linear.SPpjJUb-.js} +1 -1
  132. package/web/_astro/{mermaid.core.DxVP99Ab.js → mermaid.core.BAgx3nnb.js} +4 -4
  133. package/web/_astro/{mindmap-definition-RKZ34NQL.D0MaV6sJ.js → mindmap-definition-RKZ34NQL.D35oPG1R.js} +1 -1
  134. package/web/_astro/{pieDiagram-4H26LBE5.DCC6_q32.js → pieDiagram-4H26LBE5.DiWuRwk7.js} +1 -1
  135. package/web/_astro/{quadrantDiagram-W4KKPZXB.BeUOAM7C.js → quadrantDiagram-W4KKPZXB.B9PBzTWn.js} +1 -1
  136. package/web/_astro/{requirementDiagram-4Y6WPE33.Dbl4MASO.js → requirementDiagram-4Y6WPE33.CYuuamFN.js} +1 -1
  137. package/web/_astro/{sankeyDiagram-5OEKKPKP.HsLg0VS4.js → sankeyDiagram-5OEKKPKP.W24UhhtD.js} +1 -1
  138. package/web/_astro/{sequenceDiagram-3UESZ5HK.DT7DJTnZ.js → sequenceDiagram-3UESZ5HK.BpbNjA51.js} +1 -1
  139. package/web/_astro/{stateDiagram-AJRCARHV.d_ju1Vr1.js → stateDiagram-AJRCARHV.DqVsHudf.js} +1 -1
  140. package/web/_astro/{stateDiagram-v2-BHNVJYJU.DMCAjMJ4.js → stateDiagram-v2-BHNVJYJU.CzwHYX81.js} +1 -1
  141. package/web/_astro/{timeline-definition-PNZ67QCA.DNOHr62_.js → timeline-definition-PNZ67QCA.Bc3B6djw.js} +1 -1
  142. package/web/_astro/{vennDiagram-CIIHVFJN.B7dUy-1W.js → vennDiagram-CIIHVFJN.C-D5rh8O.js} +1 -1
  143. package/web/_astro/{wardley-L42UT6IY.DEqOXvBh.js → wardley-L42UT6IY.D7PdYCqn.js} +1 -1
  144. package/web/_astro/{wardleyDiagram-YWT4CUSO.BCRb2p6x.js → wardleyDiagram-YWT4CUSO.CwmJKXF3.js} +1 -1
  145. package/web/_astro/{xychartDiagram-2RQKCTM6.NxVQLdBh.js → xychartDiagram-2RQKCTM6.avDYnLsb.js} +1 -1
  146. package/web/index.html +2 -2
  147. package/web/_astro/BoardApp.Ce6zJYAH.js +0 -1
  148. package/web/_astro/channel.Uhm9O3UV.js +0 -1
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Run a task — full pipeline (precheck→implement→test→review→approve→verify→record→done) or single-step (implement)
3
+ role: coder
3
4
  argument-hint: "<wbs> [--mode <full|implement>] [--agent <inline|auto|name>] [--auto] [--next] [--wrap] [--continue]"
4
5
  allowed-tools: ["Bash", "Read", "Write", "Edit", "Skill"]
5
6
  ---
@@ -14,7 +15,7 @@ Wraps the **sp:spur-dev** and **sp:code-implementation** skills.
14
15
  | --- | --- | --- |
15
16
  | `<wbs>` | Task WBS to run. | required |
16
17
  | `--mode` `<full\|implement>` | Full pipeline or single implement step. | full |
17
- | `--agent` `<inline\|auto\|name>` | Who runs the model-bearing stages. In an interactive session, omit/`inline` keeps the controller in the host session (non-subprocess); eligible full-mode `agent.run` stages may dispatch once to a native subagent, with host fallback (task 0508). `auto` or a name keeps subprocess dispatch. | inline |
18
+ | `--agent` `<inline\|auto\|name>` | Who runs the model-bearing stages. In an interactive session, omit/`inline` keeps the controller in the host session (non-subprocess) — **omit**'s eligible full-mode `agent.run` stages may dispatch once to a native subagent, with host fallback (task 0508); explicit `--agent inline` is the zero-dispatch carve-out (every stage executes in the invoking session, never a native subagent). `auto` or a name keeps subprocess dispatch. | omit |
18
19
  | `--auto` | Skip objective HITL confirmations. | off |
19
20
  | `--next` | Chain-to-completion via the next-router. | off |
20
21
  | `--wrap` | Run the wrap hop after the main step. The `--agent` selector is preserved into the `/sp:dev-wrap <wbs>` handoff when supplied; omission remains omission. The wrap hop is workflow-backed and reports its trigger-3 subprocess override. | off |
@@ -37,7 +38,7 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
37
38
 
38
39
  **Flags:**
39
40
 
40
- - `--auto` | `--agent <inline|auto|name>` — Skip objective HITL confirmations (taste/irreversible gates still pause). `--agent` names who does the model-bearing work. Interactive omit/`inline` keeps the controller and implement-only stages in this session; full mode reads `task-pipeline.yaml` as the SSOT and interprets its actions/guards through the inline driver, where eligible `agent.run` stages may dispatch once to a native subagent and otherwise run in the host (task 0508). It records `stage <id> executed inline in session <session-id>` or `stage <id> executed via subagent <agent-id> (host session <session-id>)` in the run log. `auto` or a name is merged into `vars.agent` and `vars.implementAgent` and keeps the existing subprocess workflow. Headless `spur workflow run` / `spur agent run` is unchanged. See the [execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
41
+ - `--auto` | `--agent <inline|auto|name>` — Skip objective HITL confirmations (taste/irreversible gates still pause). `--agent` names who does the model-bearing work. Interactive omit/`inline` keeps the controller and implement-only stages in this session; full mode reads `task-pipeline.yaml` as the SSOT and interprets its actions/guards through the inline driver, where **omitted** `--agent`'s eligible `agent.run` stages may dispatch once to a native subagent and otherwise run in the host (task 0508); explicit `--agent inline` is the zero-dispatch carve-out — every stage executes in the invoking session. It records `stage <id> executed inline in session <session-id>` or `stage <id> executed via subagent <agent-id> (host session <session-id>)` in the run log. `auto` or a name is merged into `vars.agent` and `vars.implementAgent` and keeps the existing subprocess workflow. Headless `spur workflow run` / `spur agent run` is unchanged. See the [execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
41
42
 
42
43
  **Mode split (load-bearing — bug-742)**
43
44
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Run a batch of tasks through their pipelines in dependency-correct order — resolve a set, topo-sort, run each via task-pipeline.yaml, emit a batch report
3
+ role: planner
3
4
  argument-hint: "--tasks <selector> [--feature <id>] [--mode <sequential|parallel>] [--keep-going] [--auto] [--agent <inline|auto|name>] [--json] [--wrap] [--next] [--continue] [--worktree [<name>]]"
4
5
  allowed-tools: ["Bash", "Read", "Skill"]
5
6
  ---
@@ -17,7 +18,7 @@ Wraps the **sp:spur-dev** skill.
17
18
  | `--mode` `<sequential\|parallel>` | Batch execution order. | sequential |
18
19
  | `--keep-going` | Continue past per-task failures. | off |
19
20
  | `--auto` | Skip objective HITL gates. | off |
20
- | `--agent` `<inline\|auto\|name>` | Who runs each task's pipeline stages. Interactive sequential omit/`inline` uses the host-session driver — host-controlled, eligible `agent.run` stages may use a native subagent (task 0508); `auto`, a name, parallel mode, and headless invocation use subprocesses. | inline |
21
+ | `--agent` `<inline\|auto\|name>` | Who runs each task's pipeline stages. Interactive sequential omit/`inline` uses the host-session driver — host-controlled; **omit**'s eligible `agent.run` stages may use a native subagent (task 0508); explicit `--agent inline` is the zero-dispatch carve-out (every stage executes in the invoking session). `auto`, a name, parallel mode, and headless invocation use subprocesses. | omit |
21
22
  | `--json` | Emit structured JSON. | off |
22
23
  | `--wrap` | Run the wrap hop per task. The `--agent` selector is preserved into each `/sp:dev-wrap <wbs>` handoff when supplied; omission remains omission. | off |
23
24
  | `--next` | Chain-to-completion via the next-router. | off |
@@ -80,6 +81,6 @@ full distinction.
80
81
 
81
82
  ## Implementation
82
83
 
83
- - Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface). Interactive sequential omit/`inline` keeps the orchestrator in the host session through the [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md); eligible `agent.run` stages may dispatch once to a native subagent with host fallback (task 0508). `--agent auto`, a name, or parallel mode retains the isolated per-task workflow boundary.
84
+ - Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface). Interactive sequential omit/`inline` keeps the orchestrator in the host session through the [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md); **omitted** `--agent`'s eligible `agent.run` stages may dispatch once to a native subagent with host fallback (task 0508); explicit `--agent inline` is the zero-dispatch carve-out. `--agent auto`, a name, or parallel mode retains the isolated per-task workflow boundary.
84
85
  - Interactive sequential omit/inline: `Skill(skill="sp:spur-dev", args="runall-inline $ARGUMENTS")`.
85
86
  - Explicit executor or parallel mode: `Skill(skill="sp:spur-dev", args="runall $ARGUMENTS")` → `sp:super-planner` agent.
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Simplify code for clarity without changing behavior — reduce complexity in recent changes (or a given scope), preserving behavior exactly
3
+ role: coder
3
4
  argument-hint: "[<path-or-scope>] [--scope <recent|all|path>] [--check <cmd>] [--agent <inline|auto|name>] [--auto]"
4
5
  allowed-tools: ["Bash", "Read", "Edit", "Skill"]
5
6
  ---
@@ -15,7 +16,7 @@ Wraps the **sp:code-simplification** skill.
15
16
  | `[<path-or-scope>]` | Path or scope to simplify. | recent |
16
17
  | `--scope` `<recent\|all\|path>` | Scope of the simplification pass. | recent |
17
18
  | `--check` `<cmd>` | Validation command to iterate against. | project gate |
18
- | `--agent` `<inline\|auto\|name>` | Who runs the model-bearing simplification. | inline |
19
+ | `--agent` `<inline\|auto\|name>` | Who runs the model-bearing simplification. | omit |
19
20
  | `--auto` | Skip objective HITL gates (taste gates: over-engineering removal & cross-file utility extraction still require approval). | off |
20
21
 
21
22
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Generate or extend tests until the unit target is met
3
+ role: coder
3
4
  argument-hint: "<target> [--coverage <n>] [--agent <inline|auto|name>] [--auto]"
4
5
  allowed-tools: ["Bash", "Read", "Write", "Edit", "Skill"]
5
6
  ---
@@ -14,7 +15,7 @@ Wraps the **sp:code-testing** skill.
14
15
  | --- | --- | --- |
15
16
  | `<target>` | File / module / path to generate tests for. | required |
16
17
  | `--coverage` `<n>` | Coverage percentage target. | configured |
17
- | `--agent` `<inline\|auto\|name>` | Who runs the model-bearing test work. | inline |
18
+ | `--agent` `<inline\|auto\|name>` | Who runs the model-bearing test work. | omit |
18
19
  | `--auto` | Skip objective HITL gates. | off |
19
20
 
20
21
  For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag-glossary.md).
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Verify a task against its requirements and Acceptance Criteria — traceability check producing a PASS/PARTIAL/FAIL verdict with evidence
3
+ role: reviewer
3
4
  argument-hint: "<wbs> [--agent <inline|auto|name>] [--fix <none|blockers-first|all>] [--focus <lens>] [--bdd] [--auto] [--force] [--next] [--skip-shippable]"
4
5
  allowed-tools: ["Bash", "Read", "Skill"]
5
6
  ---
@@ -13,7 +14,7 @@ Wraps the **sp:code-verification** skill.
13
14
  | Flag | Description | Default |
14
15
  | --- | --- | --- |
15
16
  | `<wbs>` | Task WBS to verify. | required |
16
- | `--agent` `<inline\|auto\|name>` | Who runs the model-bearing verification. | inline |
17
+ | `--agent` `<inline\|auto\|name>` | Who runs the model-bearing verification. | omit |
17
18
  | `--fix` `<none\|blockers-first\|all>` | Auto-fix policy on findings. | none |
18
19
  | `--focus` `<lens>` | Verification lens. | omitted |
19
20
  | `--bdd` | Run BDD scenarios. | off |
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Verify a batch of tasks against their requirements and Acceptance Criteria — batch traceability check producing per-task verdicts and a summary report
3
+ role: reviewer
3
4
  argument-hint: "--tasks <selector> [--feature <id>] [--agent <inline|auto|name>] [--fix <none|blockers-first|all>] [--focus <lens>] [--bdd] [--auto] [--force] [--next] [--json] [--skip-shippable] [--worktree [<name>]]"
4
5
  allowed-tools: ["Bash", "Read", "Skill"]
5
6
  ---
@@ -14,7 +15,7 @@ Wraps the **sp:spur-dev** and **sp:code-verification** skills.
14
15
  | --- | --- | --- |
15
16
  | `--tasks` `<selector>` | Task selector to verify. | required |
16
17
  | `--feature` `<id>` | Restrict to a feature. | omitted |
17
- | `--agent` `<inline\|auto\|name>` | Who runs each verification. | inline |
18
+ | `--agent` `<inline\|auto\|name>` | Who runs each verification. | omit |
18
19
  | `--fix` `<none\|blockers-first\|all>` | Auto-fix policy on findings. | none |
19
20
  | `--focus` `<lens>` | Verification lens. | omitted |
20
21
  | `--bdd` | Run BDD scenarios. | off |
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Wrap up a single completed task — learnings, metrics, doc-sync, optional feature transition and branch cleanup
3
+ role: coder
3
4
  argument-hint: "<wbs> [--agent <inline|auto|name>] [--auto] [--merge] [--dry-run]"
4
5
  allowed-tools: ["Bash", "Read", "AskUserQuestion"]
5
6
  ---
@@ -13,7 +14,7 @@ Wraps the **wrapup-pipeline.yaml** workflow.
13
14
  | Flag | Description | Default |
14
15
  | --- | --- | --- |
15
16
  | `<wbs>` | Task WBS to wrap. | required |
16
- | `--agent` `<inline\|auto\|name>` | Who runs the wrap's model-bearing steps. Wrap is workflow-backed: omit/`inline` resolves to `agent.default` (objective trigger 3 — durable auditable run record required); `auto` tier-resolves an executor; a name pins that executor into `vars.agent`. | agent.default |
17
+ | `--agent` `<inline\|auto\|name>` | Who runs the wrap's model-bearing steps. Wrap is workflow-backed (headless): `omit` resolves to `agent.default` (objective trigger 3 — durable auditable run record required); explicit `--agent inline` is rejected with the stable special error — a headless workflow surface cannot host a session; `auto` tier-resolves an executor; a name pins that executor into `vars.agent`. | agent.default |
17
18
  | `--auto` | Skip objective HITL gates. | off |
18
19
  | `--merge` | Merge the wrap branch. | off |
19
20
  | `--dry-run` | Render the wrap without writing. | off |
@@ -31,13 +32,13 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
31
32
  - Resolve the executor **before** launching the workflow:
32
33
  - `--agent <name>` → pass the name unchanged into `vars.agent`.
33
34
  - `--agent auto` → tier-resolve a concrete executor first, then merge it into `vars.agent`.
34
- - omit/`--agent inline` → resolve to `agent.default` (the documented headless resolution of `inline`).
35
+ - omit → resolve to `agent.default`; explicit `--agent inline` → rejected with the stable special error (headless surface no dispatch, no `agent.default` fallback).
35
36
  - Emit a pre-dispatch notice naming the override before `spur workflow run`, exactly:
36
- `execution surface: subprocess`, `reason: trigger 3 — durable auditable run record required`, `requested agent: inline|auto|<name>`, `executor: agent.default|<resolved-name>`.
37
+ `execution surface: subprocess`, `reason: trigger 3 — durable auditable run record required`, `requested agent: inline|auto|<name>` (explicit `inline` rejects before dispatch — no executor resolves), `executor: agent.default|<resolved-name>`.
37
38
  - The wrap workflow still creates its durable run record (task_run_links / trace) — the notice reports the override, it does not change the workflow.
38
39
 
39
40
  ```bash
40
- AGENT=… # resolved above: agent.default for inline/omitted, tier-resolved for auto, unchanged for <name>
41
+ AGENT=… # resolved above: agent.default for omitted, tier-resolved for auto, unchanged for <name>; explicit inline errors (headless)
41
42
  echo "execution surface: subprocess; reason: trigger 3 — durable auditable run record required; requested agent: <inline|auto|name>; executor: $AGENT"
42
43
  VARS=$(jq -nc --arg tasks "[\"$WBS\"]" --arg agent "$AGENT" --arg profile "$PROFILE" --arg merge "$MERGE" \
43
44
  '{tasks:$tasks, agent:$agent, profile:$profile, merge:$merge}')
@@ -45,4 +46,5 @@ spur workflow run .spur/workflows/wrapup-pipeline.yaml --vars "$VARS" [--dry-run
45
46
  ```
46
47
 
47
48
  The executor resolution is described in the bullets above; the snippet's `AGENT` variable carries
48
- the resolved name (`agent.default` for omit/`inline`).
49
+ the resolved name (`agent.default` for omitted `--agent`; explicit `--agent inline` is rejected
50
+ before dispatch).
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Wrap up a batch of completed tasks — learnings, metrics, doc-sync, feature transition, optional branch cleanup
3
+ role: coder
3
4
  argument-hint: "[--since <iso-date>] [--feature <id>] [--status <s>] [--agent <inline|auto|name>] [--auto] [--merge] [--dry-run]"
4
5
  allowed-tools: ["Bash", "Read", "AskUserQuestion"]
5
6
  ---
@@ -15,7 +16,7 @@ Wraps the **wrapup-pipeline.yaml** workflow.
15
16
  | `--since` `<iso-date>` | Wrap tasks completed since a date. | configured |
16
17
  | `--feature` `<id>` | Wrap tasks in a feature. | omitted |
17
18
  | `--status` `<s>` | Only wrap tasks in a status. | done |
18
- | `--agent` `<inline\|auto\|name>` | Who runs the wrap's model-bearing steps. Wrap is workflow-backed: omit/`inline` resolves to `agent.default` (objective trigger 3 — durable auditable run record required); `auto` tier-resolves an executor; a name pins that executor into `vars.agent`. | agent.default |
19
+ | `--agent` `<inline\|auto\|name>` | Who runs the wrap's model-bearing steps. Wrap is workflow-backed (headless): `omit` resolves to `agent.default` (objective trigger 3 — durable auditable run record required); explicit `--agent inline` is rejected with the stable special error — a headless workflow surface cannot host a session; `auto` tier-resolves an executor; a name pins that executor into `vars.agent`. | agent.default |
19
20
  | `--auto` | Skip objective HITL gates. | off |
20
21
  | `--merge` | Merge wrap branches. | off |
21
22
  | `--dry-run` | Render wraps without writing. | off |
@@ -33,13 +34,13 @@ For shared semantics, see the [flag glossary](../skills/spur-dev/references/flag
33
34
  - Resolve the executor **before** launching the workflow:
34
35
  - `--agent <name>` → pass the name unchanged into `vars.agent`.
35
36
  - `--agent auto` → tier-resolve a concrete executor first, then merge it into `vars.agent`.
36
- - omit/`--agent inline` → resolve to `agent.default` (the documented headless resolution of `inline`).
37
+ - omit → resolve to `agent.default`; explicit `--agent inline` → rejected with the stable special error (headless surface no dispatch, no `agent.default` fallback).
37
38
  - Emit a pre-dispatch notice naming the override before `spur workflow run`, exactly:
38
- `execution surface: subprocess`, `reason: trigger 3 — durable auditable run record required`, `requested agent: inline|auto|<name>`, `executor: agent.default|<resolved-name>`.
39
+ `execution surface: subprocess`, `reason: trigger 3 — durable auditable run record required`, `requested agent: inline|auto|<name>` (explicit `inline` rejects before dispatch — no executor resolves), `executor: agent.default|<resolved-name>`.
39
40
  - The wrap workflow still creates its durable run record — the notice reports the override, it does not change the workflow.
40
41
 
41
42
  ```bash
42
- AGENT=… # resolved above: agent.default for inline/omitted, tier-resolved for auto, unchanged for <name>
43
+ AGENT=… # resolved above: agent.default for omitted, tier-resolved for auto, unchanged for <name>; explicit inline errors (headless)
43
44
  echo "execution surface: subprocess; reason: trigger 3 — durable auditable run record required; requested agent: <inline|auto|name>; executor: $AGENT"
44
45
  VARS=$(jq -nc --arg tasks "$TASKS" --arg feature "$FEATURE" --arg agent "$AGENT" --arg profile "$PROFILE" --arg merge "$MERGE" \
45
46
  '{tasks:$tasks, feature:$feature, agent:$agent, profile:$profile, merge:$merge}')
@@ -47,5 +48,6 @@ spur workflow run .spur/workflows/wrapup-pipeline.yaml --vars "$VARS" [--dry-run
47
48
  ```
48
49
 
49
50
  The executor resolution is described in the bullets above; the snippet's `AGENT` variable carries
50
- the resolved name (`agent.default` for omit/`inline`).
51
+ the resolved name (`agent.default` for omitted `--agent`; explicit `--agent inline` is rejected
52
+ before dispatch).
51
53
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Author a validated, smoke-tested constraint rule
3
+ role: scribe
3
4
  argument-hint: "\"<description>\" [--file <path>] [--preset <target>]"
4
5
  allowed-tools: ["Bash", "Read", "Write", "Skill"]
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Refine a constraint rule or preset, then re-verify it
3
+ role: scribe
3
4
  argument-hint: "<rule-file-or-preset> [--intent \"<goal>\"] [--severity <sev>] [--scope <glob>] [--exempt <path>] [--disable <id>] [--override <id>] [--dry-run]"
4
5
  allowed-tools: ["Bash", "Read", "Edit", "Skill"]
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Discover recurring anti-patterns worth codifying as rules
3
+ role: reviewer
3
4
  argument-hint: "[<path-or-glob>]"
4
5
  allowed-tools: ["Bash", "Read", "Grep", "Glob", "Skill"]
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Initialize a new Spur project — scaffold config + docs, then customize for this project's stack and scope
3
+ role: scribe
3
4
  argument-hint: "[--name <name>] [--minimal] [--force]"
4
5
  allowed-tools: ["Bash", "Read", "Write", "Skill"]
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Author a validated, dry-run-verified workflow in the right execution mode
3
+ role: scribe
3
4
  argument-hint: "\"<description>\" [--kind <state-machine|transition-flow>] [--file <path>]"
4
5
  allowed-tools: ["Bash", "Read", "Write", "Skill"]
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Refine an existing workflow, then re-validate and re-dry-run it
3
+ role: scribe
3
4
  argument-hint: "<workflow-file> [--intent \"<goal>\"] [--dry-run]"
4
5
  allowed-tools: ["Bash", "Read", "Edit", "Skill"]
5
6
  ---
@@ -16,8 +16,13 @@
16
16
  * emits an `allow` decision. A broken guard must never wedge an agent tool call.
17
17
  *
18
18
  * **Escape hatch:** `SPUR_CAREFUL=off` short-circuits to allow.
19
+ *
20
+ * Classification itself lives in `destructive-policy.ts` — one policy, every platform
21
+ * adapter. This file owns the Claude Code payload/decision shape only.
19
22
  */
20
23
 
24
+ import { classifyCommand } from './destructive-policy';
25
+
21
26
  interface ToolPayload {
22
27
  tool_name?: string;
23
28
  tool_input?: { command?: string };
@@ -37,86 +42,6 @@ function preToolUseDecision(decision: Decision, reason?: string): never {
37
42
  process.exit(0);
38
43
  }
39
44
 
40
- /** Well-known rebuildable caches a `rm -rf` may target without a warning. */
41
- const SAFE_RM_TARGET =
42
- /^(?:\.?\/)?(?:[\w.@-]+\/)*(?:node_modules|dist|\.next|coverage|build|\.turbo|\.cache|\.parcel-cache|out)\/?\*?$/;
43
-
44
- /**
45
- * Expand an argument string into the set of flags it sets, splitting short-flag
46
- * clusters into their individual letters: `-Rf --force` → `{R, f, --force}`.
47
- *
48
- * Matching flags with ad-hoc regexes per call site is what let `rm -R` through —
49
- * `-\w*r` only ever matched the lowercase spelling, even though `man rm` defines
50
- * `-r` as "Equivalent to -R". Parsing once, case-preserved, makes that class of
51
- * miss unrepresentable: a caller names every spelling it cares about explicitly.
52
- */
53
- function parseFlags(args: string): Set<string> {
54
- const flags = new Set<string>();
55
- for (const token of args.trim().split(/\s+/)) {
56
- if (token.length < 2 || !token.startsWith('-') || token === '--') continue;
57
- if (token.startsWith('--')) {
58
- flags.add(token.split('=')[0] as string); // `--force=x` → `--force`
59
- continue;
60
- }
61
- for (const ch of token.slice(1)) flags.add(ch);
62
- }
63
- return flags;
64
- }
65
-
66
- /** True when a `rm` invocation is both recursive and forced (any flag spelling). */
67
- function isRecursiveForceRm(args: string): boolean {
68
- const flags = parseFlags(args);
69
- // POSIX rm accepts -r and -R interchangeably; --recursive is the GNU long form.
70
- const recursive = flags.has('r') || flags.has('R') || flags.has('--recursive');
71
- const force = flags.has('f') || flags.has('--force');
72
- return recursive && force;
73
- }
74
-
75
- /** True when every non-flag target of a `rm` invocation is a known-safe cache path. */
76
- function rmTargetsAllSafe(args: string): boolean {
77
- const targets = args
78
- .trim()
79
- .split(/\s+/)
80
- .filter((t) => t.length > 0 && !t.startsWith('-'));
81
- if (targets.length === 0) return false;
82
- return targets.every((t) => SAFE_RM_TARGET.test(t));
83
- }
84
-
85
- /** Always-warn destructive patterns (no safe exception). */
86
- const DESTRUCTIVE: Array<{ label: string; re: RegExp }> = [
87
- {
88
- label: 'a SQL DROP/TRUNCATE (DROP TABLE/DATABASE, TRUNCATE)',
89
- re: /\b(?:DROP\s+(?:TABLE|DATABASE)|TRUNCATE(?:\s+TABLE)?)\b/i,
90
- },
91
- { label: 'a force push (git push --force / -f)', re: /\bgit\s+push\b[^\n]*(?:--force(?!-with-lease)|\s-f\b)/i },
92
- {
93
- // `git push origin +main` forces that ref without any --force flag.
94
- label: 'a force push via a + refspec (git push … +ref)',
95
- re: /\bgit\s+push\b[^\n]*\s\+[\w./-]+/i,
96
- },
97
- { label: 'a hard reset (git reset --hard)', re: /\bgit\s+reset\b[^\n]*--hard\b/i },
98
- {
99
- label: 'a working-tree discard (git checkout . / git restore .)',
100
- re: /\bgit\s+(?:checkout|restore)\s+(?:--\s+)?\.(?:\s|$)/i,
101
- },
102
- { label: 'a cluster delete (kubectl delete)', re: /\bkubectl\s+delete\b/i },
103
- { label: 'a docker prune (docker system prune)', re: /\bdocker\s+system\s+prune\b/i },
104
- ];
105
-
106
- /** Return a human label for the destructive command, or null when the command is safe. */
107
- function classifyCommand(command: string): string | null {
108
- for (const rmMatch of command.matchAll(/\brm\b([^\n&|;]*)/g)) {
109
- const args = rmMatch[1] ?? '';
110
- if (isRecursiveForceRm(args) && !rmTargetsAllSafe(args)) {
111
- return 'a recursive force remove (rm -rf)';
112
- }
113
- }
114
- for (const { label, re } of DESTRUCTIVE) {
115
- if (re.test(command)) return label;
116
- }
117
- return null;
118
- }
119
-
120
45
  async function main(): Promise<void> {
121
46
  if (process.env.SPUR_CAREFUL === 'off') preToolUseDecision('allow');
122
47
 
@@ -0,0 +1,146 @@
1
+ /**
2
+ * destructive-policy — the single destructive-command classifier shared by every
3
+ * platform guard (task 0215 R3; extracted from `careful-guard.ts`).
4
+ *
5
+ * **Why this module exists.** The policy used to live inline in `careful-guard.ts`
6
+ * (Claude Code) and was re-implemented with hand-rolled regexes in
7
+ * `pi/guard-extension.ts` (Pi). The two drifted: the Pi copy allowed
8
+ * `rm -rf node_modules /etc/nginx`, `rm -R --force /var/data`, `git push -f`, and
9
+ * `git push origin +main` — all of which the Claude matrix pins as `ask` — while
10
+ * warning on `git push --force-with-lease` and `rm -f config.json`, which it pins as
11
+ * `allow`. Seven of ten pinned cases diverged. A safety control with one
12
+ * implementation per platform is a safety control that is wrong on all but one of
13
+ * them, so the policy is defined once, here, and every adapter imports it.
14
+ *
15
+ * Adapters own **I/O only** (payload shape, how a prompt is raised). They must not
16
+ * re-derive classification.
17
+ */
18
+
19
+ /**
20
+ * Well-known rebuildable caches a `rm -rf` may target without a warning.
21
+ *
22
+ * Project-relative only — the leading `/` this used to accept made the exception
23
+ * match by *basename anywhere on the filesystem*, so `rm -rf /Users/me/dist` was
24
+ * treated as routine. Escaping targets are rejected by {@link escapesProject}
25
+ * before this is consulted; keeping the anchor tight is belt-and-braces.
26
+ */
27
+ const SAFE_RM_TARGET =
28
+ /^(?:\.\/)?(?:[\w.@-]+\/)*(?:node_modules|dist|\.next|coverage|build|\.turbo|\.cache|\.parcel-cache|out)\/?\*?$/;
29
+
30
+ /**
31
+ * True when a target points outside the project tree: an absolute path, a `~`
32
+ * home path, or one that walks out via `..`.
33
+ *
34
+ * Recursive deletion inside the project is routine (build caches, scratch dirs) and
35
+ * recoverable from git; recursive deletion *outside* it is neither, whatever the
36
+ * directory happens to be named. This is the axis that decides a bare `rm -r`,
37
+ * which is otherwise unguarded — `rm -r ./tmpdir` stays routine while
38
+ * `rm -r /Users/me/photos` prompts.
39
+ */
40
+ function escapesProject(target: string): boolean {
41
+ const t = target.replace(/^['"]|['"]$/g, '');
42
+ if (t.startsWith('/') || t.startsWith('~')) return true;
43
+ if (t.startsWith('$')) return true; // `$HOME/...`, `"$HOME"/...` — unknown expansion
44
+ return t.split('/').includes('..');
45
+ }
46
+
47
+ /**
48
+ * Expand an argument string into the set of flags it sets, splitting short-flag
49
+ * clusters into their individual letters: `-Rf --force` → `{R, f, --force}`.
50
+ *
51
+ * Matching flags with ad-hoc regexes per call site is what let `rm -R` through —
52
+ * `-\w*r` only ever matched the lowercase spelling, even though `man rm` defines
53
+ * `-r` as "Equivalent to -R". Parsing once, case-preserved, makes that class of
54
+ * miss unrepresentable: a caller names every spelling it cares about explicitly.
55
+ */
56
+ export function parseFlags(args: string): Set<string> {
57
+ const flags = new Set<string>();
58
+ for (const token of args.trim().split(/\s+/)) {
59
+ if (token.length < 2 || !token.startsWith('-') || token === '--') continue;
60
+ if (token.startsWith('--')) {
61
+ flags.add(token.split('=')[0] as string); // `--force=x` → `--force`
62
+ continue;
63
+ }
64
+ for (const ch of token.slice(1)) flags.add(ch);
65
+ }
66
+ return flags;
67
+ }
68
+
69
+ /** True when a `rm` invocation is both recursive and forced (any flag spelling). */
70
+ export function isRecursiveForceRm(args: string): boolean {
71
+ const flags = parseFlags(args);
72
+ // POSIX rm accepts -r and -R interchangeably; --recursive is the GNU long form.
73
+ const recursive = flags.has('r') || flags.has('R') || flags.has('--recursive');
74
+ const force = flags.has('f') || flags.has('--force');
75
+ return recursive && force;
76
+ }
77
+
78
+ /**
79
+ * True when **every** non-flag target of a `rm` invocation is a known-safe cache
80
+ * path. Every, not some: `rm -rf node_modules /etc/nginx` must still warn, and a
81
+ * substring test over the whole argument string (the Pi copy's approach) let one
82
+ * cache path whitelist every other target in the same command.
83
+ */
84
+ export function rmTargetsAllSafe(args: string): boolean {
85
+ const targets = rmTargets(args);
86
+ if (targets.length === 0) return false;
87
+ return targets.every((t) => SAFE_RM_TARGET.test(t));
88
+ }
89
+
90
+ /** Non-flag targets of a `rm` invocation. */
91
+ function rmTargets(args: string): string[] {
92
+ return args
93
+ .trim()
94
+ .split(/\s+/)
95
+ .filter((t) => t.length > 0 && !t.startsWith('-'));
96
+ }
97
+
98
+ /** Always-warn destructive patterns (no safe exception). */
99
+ const DESTRUCTIVE: Array<{ label: string; re: RegExp }> = [
100
+ {
101
+ label: 'a SQL DROP/TRUNCATE (DROP TABLE/DATABASE, TRUNCATE)',
102
+ re: /\b(?:DROP\s+(?:TABLE|DATABASE)|TRUNCATE(?:\s+TABLE)?)\b/i,
103
+ },
104
+ { label: 'a force push (git push --force / -f)', re: /\bgit\s+push\b[^\n]*(?:--force(?!-with-lease)|\s-f\b)/i },
105
+ {
106
+ // `git push origin +main` forces that ref without any --force flag.
107
+ label: 'a force push via a + refspec (git push … +ref)',
108
+ re: /\bgit\s+push\b[^\n]*\s\+[\w./-]+/i,
109
+ },
110
+ { label: 'a hard reset (git reset --hard)', re: /\bgit\s+reset\b[^\n]*--hard\b/i },
111
+ {
112
+ label: 'a working-tree discard (git checkout . / git restore .)',
113
+ re: /\bgit\s+(?:checkout|restore)\s+(?:--\s+)?\.(?:\s|$)/i,
114
+ },
115
+ {
116
+ // `git clean` destroys UNTRACKED files — nothing in git to recover them from,
117
+ // which makes it the least recoverable command in this family. `-n`/`--dry-run`
118
+ // only lists, so the prompt is gated on the force flag git itself requires.
119
+ label: 'an untracked-file delete (git clean -f)',
120
+ re: /\bgit\s+clean\b[^\n]*(?:\s-[a-zA-Z]*f|\s--force\b)/i,
121
+ },
122
+ { label: 'a cluster delete (kubectl delete)', re: /\bkubectl\s+delete\b/i },
123
+ { label: 'a docker prune (docker system prune)', re: /\bdocker\s+system\s+prune\b/i },
124
+ ];
125
+
126
+ /** Return a human label for the destructive command, or null when the command is safe. */
127
+ export function classifyCommand(command: string): string | null {
128
+ for (const rmMatch of command.matchAll(/\brm\b([^\n&|;]*)/g)) {
129
+ const args = rmMatch[1] ?? '';
130
+ const flags = parseFlags(args);
131
+ const recursive = flags.has('r') || flags.has('R') || flags.has('--recursive');
132
+ // A recursive delete reaching outside the project prompts whether or not
133
+ // `--force` was passed: `rm -r` deletes a whole tree without prompting for
134
+ // any writable file, and nothing outside the project is recoverable from git.
135
+ if (recursive && rmTargets(args).some(escapesProject)) {
136
+ return 'a recursive remove outside the project (rm -r on an absolute, ~, or ../ path)';
137
+ }
138
+ if (isRecursiveForceRm(args) && !rmTargetsAllSafe(args)) {
139
+ return 'a recursive force remove (rm -rf)';
140
+ }
141
+ }
142
+ for (const { label, re } of DESTRUCTIVE) {
143
+ if (re.test(command)) return label;
144
+ }
145
+ return null;
146
+ }