@gobing-ai/spur 0.3.83 → 0.3.85

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 (70) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +11 -6
  3. package/config/rules/strict/runtime-boundaries.yaml +1 -0
  4. package/config/workflows/idea-pipeline.yaml +1 -1
  5. package/config/workflows/wrapup-pipeline.yaml +1 -1
  6. package/package.json +1 -1
  7. package/plugins/sp/agents/super-planner.md +2 -1
  8. package/plugins/sp/lib/idea-handoff.generated.mjs +1 -1
  9. package/plugins/sp/plugin.json +1 -1
  10. package/plugins/sp/scripts/surface-drift-inventory.ts +1 -1
  11. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +37 -0
  12. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +55 -1
  13. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +12 -0
  14. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +4 -1
  15. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +60 -4
  16. package/schemas/task-batch.schema.json +5 -0
  17. package/spur.js +1013 -332
  18. package/web/_astro/BoardApp.CHenHFia.js +1 -0
  19. package/web/_astro/{BoardApp.D8bM9pKL.js → BoardApp.yW425dRZ.js} +56 -56
  20. package/web/_astro/{TaskDetail.BPRqgVUE.js → TaskDetail.CGwJAinW.js} +1 -1
  21. package/web/_astro/{arc.BPrPES3z.js → arc.B-qNXzSO.js} +1 -1
  22. package/web/_astro/{architectureDiagram-3BPJPVTR.qX_7q02P.js → architectureDiagram-3BPJPVTR.Bdg-xiji.js} +1 -1
  23. package/web/_astro/{blockDiagram-GPEHLZMM.CUZfj5V7.js → blockDiagram-GPEHLZMM.DFArg3Kp.js} +1 -1
  24. package/web/_astro/{c4Diagram-AAUBKEIU.CTaOr8hH.js → c4Diagram-AAUBKEIU.BkOLcjyx.js} +1 -1
  25. package/web/_astro/channel.CbDHK5UQ.js +1 -0
  26. package/web/_astro/{chunk-2J33WTMH.Dt-9wf3h.js → chunk-2J33WTMH.DN3c20V4.js} +1 -1
  27. package/web/_astro/{chunk-4BX2VUAB.CTC2sdoN.js → chunk-4BX2VUAB.B64K0Ttd.js} +1 -1
  28. package/web/_astro/{chunk-55IACEB6.DQcxt2_g.js → chunk-55IACEB6.C-AJvNwa.js} +1 -1
  29. package/web/_astro/{chunk-727SXJPM.DXFPSn-a.js → chunk-727SXJPM.DMkr46uS.js} +1 -1
  30. package/web/_astro/{chunk-AQP2D5EJ.BCx3U4bT.js → chunk-AQP2D5EJ.4_WGbI64.js} +1 -1
  31. package/web/_astro/{chunk-FMBD7UC4.DL2tJkdO.js → chunk-FMBD7UC4.cbFW_lVE.js} +1 -1
  32. package/web/_astro/{chunk-ND2GUHAM.DZyflMro.js → chunk-ND2GUHAM.CDW_-2O1.js} +1 -1
  33. package/web/_astro/{chunk-QZHKN3VN.CUI2mT09.js → chunk-QZHKN3VN.BoYOt_yg.js} +1 -1
  34. package/web/_astro/{classDiagram-4FO5ZUOK.g4rX4Fr1.js → classDiagram-4FO5ZUOK.BNT6XpHQ.js} +1 -1
  35. package/web/_astro/{classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js → classDiagram-v2-Q7XG4LA2.BNT6XpHQ.js} +1 -1
  36. package/web/_astro/{cose-bilkent-S5V4N54A.CzWJLqp0.js → cose-bilkent-S5V4N54A.CI1DQxR3.js} +1 -1
  37. package/web/_astro/{cynefin-OW5HDTMX.WgsvQeCR.js → cynefin-OW5HDTMX.A8ARH61p.js} +1 -1
  38. package/web/_astro/{dagre-BM42HDAG.Dzv6ngql.js → dagre-BM42HDAG.u28ZmqwR.js} +1 -1
  39. package/web/_astro/{diagram-2AECGRRQ.CpJ4a9rU.js → diagram-2AECGRRQ.Dx8JcrGc.js} +1 -1
  40. package/web/_astro/{diagram-5GNKFQAL.CzPlF_dq.js → diagram-5GNKFQAL.BXJUG9GY.js} +1 -1
  41. package/web/_astro/{diagram-KO2AKTUF.TAkZNTcQ.js → diagram-KO2AKTUF.BtlMuWqW.js} +1 -1
  42. package/web/_astro/{diagram-LMA3HP47.uCjoKSag.js → diagram-LMA3HP47.C4T7qaAy.js} +1 -1
  43. package/web/_astro/{diagram-OG6HWLK6.eMplIjoK.js → diagram-OG6HWLK6.D5HECE10.js} +1 -1
  44. package/web/_astro/{erDiagram-TEJ5UH35.Bf7zoXGz.js → erDiagram-TEJ5UH35.B_hMR3yw.js} +1 -1
  45. package/web/_astro/{flowDiagram-I6XJVG4X.B_bHj3gN.js → flowDiagram-I6XJVG4X.BAmpcYl3.js} +1 -1
  46. package/web/_astro/{ganttDiagram-6RSMTGT7.BasrHRMj.js → ganttDiagram-6RSMTGT7.DV1bSWK-.js} +1 -1
  47. package/web/_astro/{gitGraphDiagram-PVQCEYII.C6iphq1x.js → gitGraphDiagram-PVQCEYII.DxYKkKrJ.js} +1 -1
  48. package/web/_astro/index.CcU5weKX.css +1 -0
  49. package/web/_astro/{infoDiagram-5YYISTIA.HXmDMhW4.js → infoDiagram-5YYISTIA.BAWY2xMb.js} +1 -1
  50. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BSmW8NiU.js → ishikawaDiagram-YF4QCWOH.CZssRn6V.js} +1 -1
  51. package/web/_astro/{journeyDiagram-JHISSGLW.DEQow5fo.js → journeyDiagram-JHISSGLW.C--muARd.js} +1 -1
  52. package/web/_astro/{kanban-definition-UN3LZRKU.IVm9cTdc.js → kanban-definition-UN3LZRKU.D_QCK4et.js} +1 -1
  53. package/web/_astro/{linear.CrsM73_9.js → linear.DzrmTtZ0.js} +1 -1
  54. package/web/_astro/{mermaid.core.CfBeDJls.js → mermaid.core.Da03W3iu.js} +4 -4
  55. package/web/_astro/{mindmap-definition-RKZ34NQL.C3j60Y-0.js → mindmap-definition-RKZ34NQL.6cy-8hR_.js} +1 -1
  56. package/web/_astro/{pieDiagram-4H26LBE5.B-aCMeEA.js → pieDiagram-4H26LBE5.C5rS1pdU.js} +1 -1
  57. package/web/_astro/{quadrantDiagram-W4KKPZXB.Cib965yq.js → quadrantDiagram-W4KKPZXB.w56GZZ6Q.js} +1 -1
  58. package/web/_astro/{requirementDiagram-4Y6WPE33.D61cS4O-.js → requirementDiagram-4Y6WPE33.CipX3Pwu.js} +1 -1
  59. package/web/_astro/{sankeyDiagram-5OEKKPKP.GKF2qVPy.js → sankeyDiagram-5OEKKPKP.C0VVzgJm.js} +1 -1
  60. package/web/_astro/{sequenceDiagram-3UESZ5HK.DZnq8F2h.js → sequenceDiagram-3UESZ5HK.BX2dUUbF.js} +1 -1
  61. package/web/_astro/{stateDiagram-AJRCARHV.DXUFmdgM.js → stateDiagram-AJRCARHV.ypCdgODQ.js} +1 -1
  62. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BtHmhLEz.js → stateDiagram-v2-BHNVJYJU.In0baEtg.js} +1 -1
  63. package/web/_astro/{timeline-definition-PNZ67QCA.Cy-WW2ln.js → timeline-definition-PNZ67QCA.CJN4Vkvl.js} +1 -1
  64. package/web/_astro/{vennDiagram-CIIHVFJN.SLp5b9KI.js → vennDiagram-CIIHVFJN.Cf8KkIPY.js} +1 -1
  65. package/web/_astro/{wardleyDiagram-YWT4CUSO.Bww45mWV.js → wardleyDiagram-YWT4CUSO.DCZBo9xw.js} +1 -1
  66. package/web/_astro/{xychartDiagram-2RQKCTM6.DR4swI6a.js → xychartDiagram-2RQKCTM6.Cm4v_MiJ.js} +1 -1
  67. package/web/index.html +2 -2
  68. package/web/_astro/BoardApp.D-WlxiN2.js +0 -1
  69. package/web/_astro/channel.DGZaFHZx.js +0 -1
  70. package/web/_astro/index.DayyIngm.css +0 -1
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "sp",
10
- "version": "0.3.83",
10
+ "version": "0.3.85",
11
11
  "source": "./plugins/sp"
12
12
  }
13
13
  ]
@@ -95,7 +95,7 @@ agent:
95
95
  # claude-*-5 via claude, grok-4.6 via grok, gemini-3.x via agy) —
96
96
  # consistently outperforms the same model routed through a third-party CLI,
97
97
  # and belongs at the capable rungs. `portable` models (glm-5.x,
98
- # deepseek-v4-*, …) are provider-agnostic and best carried by omp or pi at
98
+ # deepseek-v4-*, …) are provider-agnostic and best carried by pi at
99
99
  # the cheap/standard rungs. The ladder below mixes both classes on purpose;
100
100
  # measure pairings with the history plane before promoting a portable model
101
101
  # into a capable rung. Live tiers: cheap | standard | capable-1 | capable-2
@@ -106,19 +106,24 @@ agent:
106
106
  # capable-1.
107
107
  #
108
108
  # Because this is the global layer, a project that wants one different model
109
- # writes only `- name: omp` + `model: …` — the agent and tier come from
110
- # here.
109
+ # writes only `- name: pi-dsv4-flash` + `model: …` — the agent and tier come
110
+ # from here.
111
111
  executors:
112
112
  # cheap rung is optional: with none declared, scribe-role work starts
113
113
  # one rung up at the cheapest standard executor. Add one when
114
114
  # transcript-heavy mechanical commands (changelog/gitmsg/handover) get
115
115
  # noisy on cost.
116
116
  # - name: minimax
117
- # agent: omp
117
+ # agent: pi
118
118
  # model: minimax/MiniMax-M3
119
119
  # tier: cheap
120
- - name: omp
121
- agent: omp
120
+ # Standard rung. An `agent:` here is a live DISPATCH target, not a label:
121
+ # role routing (`agent.default: coder`, `--agent auto`, every workflow step
122
+ # declaring `agent: auto`) lands on the cheapest eligible standard executor,
123
+ # so listing an agent makes Spur delegate work to it. Keep this rung on an
124
+ # agent the supported install targets actually carry.
125
+ - name: pi-dsv4-flash
126
+ agent: pi
122
127
  model: opencode/deepseek-v4-flash
123
128
  tier: standard
124
129
  - name: pi
@@ -57,6 +57,7 @@ rules:
57
57
  - "packages/app/src/services/token-ledger-service.ts" # FD byte-window log tailing
58
58
  - "packages/app/src/services/token-ledger-watcher.ts" # node:fs watch() live watcher
59
59
  - "packages/app/src/services/project-registry.ts" # atomic projects.json persistence
60
+ - "packages/app/src/services/slash-commands-service.ts" # synchronous ~/.config/spur/slash_commands.json persistence (mirrors project-registry.ts)
60
61
  - "packages/app/src/services/history-service.ts" # versioned analyze artifact + bounded-errors sidecar + latest.json symlink pointer (task 0474); ts-runtime FileSystem seam has no symlink, so the pointer uses node:fs directly (mirrors project-registry.ts persistence exemption)
61
62
  - "packages/app/src/observability/workflow-run-log-sink.ts" # sync FD append for mid-run tail-able all-in-one run log (task 0426 / feature D2); append() is sync from the observability bus
62
63
  - "apps/cli/src/commands/workflow.ts" # FD byte-window tail of the mid-run run log for `workflow trace --follow` streaming (task 0428 / feature D2); readSync at offset over the observability sink's FDs
@@ -33,7 +33,7 @@
33
33
  # lets profile=auto route around the idea-eval taste gate (default "false")
34
34
  # CLI --approve-taste sets both design_approved and idea_approved to true.
35
35
  # spurBin — PATH-independent spur invocation (overridden by CLI at run start)
36
- # agent — agent for agent.run steps (default: omp)
36
+ # agent — agent for agent.run steps (default: auto → `agent.default` in config)
37
37
  #
38
38
  # Reliability (aligned with task-pipeline / ADR-043):
39
39
  # - Soft agent doctor at start → failed via transitions (not raw lifecycle abort)
@@ -31,7 +31,7 @@
31
31
  # merge — set --vars '{"merge":"true"}' to run branch cleanup (irreversible
32
32
  # HITL)
33
33
  # spurBin — PATH-independent spur invocation (overridden by CLI at run start)
34
- # agent — agent for agent.run steps (default: omp)
34
+ # agent — agent for agent.run steps (default: auto → `agent.default` in config)
35
35
  #
36
36
  # Reliability (aligned with task-pipeline / ADR-043):
37
37
  # - Prefer pure slash commands when a command exists; free-form inputs remain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/spur",
3
- "version": "0.3.83",
3
+ "version": "0.3.85",
4
4
  "description": "Spur CLI — local-first harness for mainstream coding agents: constraint checking, workflow orchestration, agent health, and history analytics. Bun-native; exposes the `spur` command.",
5
5
  "keywords": [
6
6
  "spur",
@@ -97,7 +97,8 @@ You own the spaces **between** task runs:
97
97
  You explicitly do **NOT** own step-level execution:
98
98
 
99
99
  - How an `agent.run` step (implement/test/review/verify) runs is `vars.agent`'s concern - default
100
- `omp`, pinned in `task-pipeline.yaml`. `--agent <value>` from the command flows into each
100
+ `auto` (`agent.default` in config resolves the executor), pinned in `task-pipeline.yaml`.
101
+ `--agent <value>` from the command flows into each
101
102
  per-task `vars.agent`; you forward it, you do not interpret it.
102
103
  - You never edit the pipeline YAML, never reach into a step, and never decide how a single
103
104
  `agent.run` stage executes. The per-task pipeline is invoked **verbatim**.
@@ -1325,5 +1325,5 @@ ${G}
1325
1325
  `);this._frontmatter={raw:Z,data:{...this._frontmatter.data,[$]:U}};let W=this._frontmatterBlock.indexOf(X);this._frontmatterBlock=W===-1?`---
1326
1326
  ${Z}
1327
1327
  ---
1328
- `:this._frontmatterBlock.slice(0,W)+Z+this._frontmatterBlock.slice(W+X.length)}serialize(){let $=this._frontmatterBlock;$+=this._preamble;for(let U of this._sections)$+=U.modifiedText??U.originalText;return $}}function Do($){return`"${$.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`}function Ko($){if($.startsWith('"')&&$.endsWith('"'))return $;if(/[:{}[\],&*?|>!%@`#"'\\\n\r]/.test($))return`"${$.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`;if(/^(null|true|false|yes|no|on|off)$/i.test($))return`"${$}"`;if(/^\d/.test($)&&!/^\d{4}-\d{2}-\d{2}/.test($))return`"${$}"`;return $}G6();var LA=["backlog","todo","wip","testing","blocked","done","cancelled"],Fo=["backlog","active","verifying","blocked","done","cancelled"];var QV=["P0","P1","P2","P3"],qo=["simple","standard","complex","research","refine","plan","unit","review","docs"],Io=["task","issue","review","meta","brainstorm"],OA=["standard","feature-impl","issue","review","meta","brainstorm"];var PA=/^[A-Z][1-9]*$/;var Bo={completed:"done",complete:"done","in-progress":"wip","in progress":"wip",in_progress:"wip",dropped:"cancelled",cancel:"cancelled",canceled:"cancelled",new:"backlog",pending:"backlog",blocked:"blocked",wip:"wip",testing:"testing",done:"done",cancelled:"cancelled",backlog:"backlog",todo:"todo"};function Lo($){let U=$.trim().toLowerCase(),J=Bo[U];if(J===void 0)throw Error(`Unknown task status: ${JSON.stringify($)} (allowed: ${LA.join(", ")})`);return J}var w7=F.string().min(1).refine(($)=>!Number.isNaN(Date.parse($)),{message:"must be an ISO 8601 timestamp"}),jA=F.string().regex(PA,{message:"feature id must match ^[A-Z][1-9]*$ (DD-14)"}).nullable().optional(),RA=F.string().regex(/^\d{4}$/,{message:"parent_wbs must be a 4-digit WBS string"}).nullable().optional(),yF$=F.object({schema_version:F.literal(1),name:F.string().min(1),description:F.string().optional(),status:F.preprocess(($)=>{if(typeof $==="string")try{return Lo($)}catch{}return $},F.enum(LA)),type:F.enum(Io).optional().default("task"),template:F.enum(OA).optional(),profile:F.enum(qo).optional(),feature_id:jA,parent_wbs:RA,priority:F.enum(QV).optional(),tags:F.array(F.string()).optional(),dependencies:F.array(F.string()).optional(),ac_numbering:F.literal("task-local").optional(),ac_altitude:F.enum(["graduating","task-local"]).optional(),done_forced:F.preprocess(($)=>typeof $==="string"?$==="true":$,F.boolean().optional()),done_reason:F.string().optional(),feature_link_declined:F.preprocess(($)=>typeof $==="string"?$==="true":$,F.boolean().optional()),created_at:w7,updated_at:w7}),fF$=F.object({schema_version:F.literal(1),id:F.string().regex(PA,{message:"feature id must match ^[A-Z][1-9]*$ (DD-14)"}),name:F.string().min(1),status:F.enum(Fo),priority:F.enum(QV).optional(),tags:F.array(F.string()).optional(),created_at:w7,updated_at:w7});var Oo=F.object({name:F.string().min(1,"name is required"),background:F.string().optional(),requirements:F.string().optional(),design:F.string().optional(),plan:F.string().optional(),acceptance_criteria:F.string().optional(),feature_id:jA,parent_wbs:RA,priority:F.enum(QV).optional(),tags:F.array(F.string()).optional(),template:F.enum(OA).optional()}).strict(),Po=F.array(Oo).min(1,"batch must contain at least one task");u0();G6();var DV=F.enum(Q0),jo=F.object({required:F.array(DV).optional(),optional:F.array(DV).optional(),forbidden:F.array(DV).optional(),gate:F.boolean().optional()}).strict(),Ro=F.enum(["backlog","todo","wip","testing","blocked","done","cancelled"]),cF$=F.object({variants:F.record(F.string(),F.partialRecord(Ro,jo))}).strict();var dF$=new Map(Q0.map(($,U)=>[$,U]));var No=["requirements","design","plan","ac","decisions","dependencies","premises"];function MA($){return`/sp:dev-refine ${$} --auto --depth ready`}var Ao={Solution:!0,Testing:!0,Review:!0,History:!0},vo=Q0.filter(($)=>!($ in Ao));function NA($){let U=H2.parse($,"task"),J=U.frontmatterData??{},G={sections:vo.map((X)=>{let Y=U.getSection(X);return{name:X,body:Y===null?null:Y.trim()}}),featureId:typeof J.feature_id==="string"?J.feature_id:null,template:typeof J.template==="string"?J.template:null,dependencies:Array.isArray(J.dependencies)?[...J.dependencies].map(String).sort():null};return Mo("sha256").update(JSON.stringify(G)).digest("hex")}function AA($){if($===void 0||$.length===0)return{ok:!1,reason:"no ready-checklist evidence"};for(let U of No){let J=$.find((G)=>G.id===U);if(J===void 0)return{ok:!1,reason:`checklist row "${U}" missing`};if(J.pass!==!0)return{ok:!1,reason:`checklist row "${U}" not passing`};if(typeof J.evidence!=="string"||J.evidence.trim()==="")return{ok:!1,reason:`checklist row "${U}" has empty evidence`}}return{ok:!0}}var To=/[;&|<>$`(){}[\]!*?~#\n\r"']/;function vA($,U){if(To.test($))return{error:`${U} must not contain shell metacharacters (got ${$})`};let J=$.trim().split(/\s+/).filter((X)=>X.length>0),G=J[0];if(G===void 0)return{error:`Action option ${U.slice(U.indexOf('"'))} must be a non-empty string`};return{command:G,leadingArgs:J.slice(1)}}function K0($){return`exit=${$.exitCode??"null"}${$.signal?` signal=${$.signal}`:""}: ${$.stderr.trim()||"no stderr"}`}async function TA($){let U=$.projectRoot??process.cwd(),J=$.fileSystem??HZ(),G=$.processExecutor??new Z7,X=$.spurBin??"spur",{runId:Y,featureId:H}=$,_=D0(U,".spur","run"),Z=D0(_,`${Y}-idea-task-batch.json`),W=D0(_,`${Y}-idea-batch-create-result.json`),z=D0(_,`${Y}-idea-task-order.json`),V=D0(_,`${Y}-idea-ready.json`),Q=D0(_,`${Y}-idea-handoff.md`),D=vA(X,'idea-handoff "spurBin"');if("error"in D)return{ok:!1,wbsList:[],nextCommand:"",reportPath:Q,error:D.error};let{command:q,leadingArgs:I}=D;if(!await J.exists(Z)||!await J.exists(W)||!await J.exists(z))return{ok:!1,wbsList:[],nextCommand:"",reportPath:Q,error:"Required batch, result, or order files missing in .spur/run/"};try{let L=JSON.parse(await J.readFile(Z)),P=JSON.parse(await J.readFile(W)),R=JSON.parse(await J.readFile(z)),v=await J.exists(V),M=await(async()=>{if(!v)return[];try{let f=JSON.parse(await J.readFile(V)),V$=typeof f==="object"&&f!==null&&"tasks"in f?f.tasks:void 0;return Array.isArray(V$)?V$:[]}catch{return[]}})(),E=new Map;for(let f of M)if(typeof f?.wbs==="string")E.set(f.wbs,f);if(!Array.isArray(L)||!Array.isArray(P?.wbs)||!Array.isArray(R))return{ok:!1,wbsList:[],nextCommand:"",reportPath:Q,error:"Malformed batch, result, or order JSON structure"};if(L.length!==P.wbs.length)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Batch size mismatch: ${L.length} items declared but ${P.wbs.length} WBS created`};let x=L.map((f)=>f.name);if(new Set(x).size!==x.length)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:"Duplicate task names found in batch declaration"};for(let f of R){let V$=x.indexOf(f.name);if(V$===-1||!P.wbs[V$])return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Task name "${f.name}" from order could not be mapped to created WBS`};let K$=P.wbs[V$],H4=[];for(let G$ of f.depends_on_names??[]){let F$=x.indexOf(G$);if(F$===-1||!P.wbs[F$])return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Dependency task name "${G$}" could not be mapped to created WBS`};H4.push(P.wbs[F$])}if(H4.length>0){let G$=await G.run({command:q,args:[...I,"task","deps",K$,"set",...H4,"--json"],cwd:U,forceBuffered:!0,rejectOnError:!1});if(G$.exitCode!==0)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Failed to set dependencies for task ${K$}: ${K0(G$)}`}}}let p=await G.run({command:q,args:[...I,"feature","refresh","--feature",H,"--json"],cwd:U,forceBuffered:!0,rejectOnError:!1});if(p.exitCode!==0)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Feature refresh for ${H} failed: ${K0(p)}`};let w=[];for(let f of P.wbs){let V$=await G.run({command:q,args:[...I,"task","path",f,"--json"],cwd:U,forceBuffered:!0,rejectOnError:!1});if(V$.exitCode===null)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Task path for ${f} could not be spawned (${q}): ${K0(V$)}`};let K$;if(V$.exitCode!==0)K$=`task path resolution failed: ${K0(V$)}`;else{let G$;try{let F$=JSON.parse(V$.stdout);if(typeof F$==="object"&&F$!==null&&"filePath"in F$){let j$=F$.filePath;if(typeof j$==="string")G$=j$}}catch{G$=void 0}if(G$===void 0)K$="task path output carried no filePath";else{let F$;try{F$=NA(await J.readFile(G$))}catch{F$=void 0}if(F$===void 0)K$=`task file unreadable at ${G$}`;else{let j$=E.get(f);if(j$===void 0)K$="no ready evidence recorded for this run";else if(j$.status!=="ready")K$=`ready evidence status is ${String(j$.status)}`;else if(typeof j$.planningDigest!=="string"||j$.planningDigest!==F$)K$="planning digest stale — task content changed after preparation";else{let F0=AA(Array.isArray(j$.checks)?j$.checks:void 0);if(!F0.ok)K$=F0.reason}}}}if(K$===void 0){let G$=await G.run({command:q,args:[...I,"task","check",f,"--json"],cwd:U,forceBuffered:!0,rejectOnError:!1});if(G$.exitCode===null)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Task check for ${f} could not be spawned (${q}): ${K0(G$)}`};if(G$.exitCode!==0)K$=`deterministic task check failed (${K0(G$)})`}let H4=K$===void 0;w.push({wbs:f,pass:H4,status:H4?"ready":"unready",...H4?{}:{reason:K$,action:MA(f)}})}let r=w.some((f)=>!f.pass),Z$=r?`/sp:dev-refineall --feature ${H} --auto --depth ready`:`/sp:dev-runall --feature ${H} --auto`,z$=["# Idea pipeline handoff report","",`Feature: ${H}`,`Run ID: ${Y}`,"","## Created tasks",...P.wbs.map((f)=>` - ${f}`),"","## Per-task readiness (evidence + digest + task check)","","| WBS | Outcome | Reason |","|-----|---------|--------|",...w.map((f)=>`| ${f.wbs} | ${f.pass?"READY":"UNREADY"} | ${f.reason??"-"} |`),"",...r?["## Preparation actions","",...w.filter((f)=>!f.pass).map((f)=>` - ${f.wbs}: ${f.action}`),""]:[],"## Next command","",Z$,""];return await J.ensureDir(_),await J.writeFile(Q,z$.join(`
1328
+ `:this._frontmatterBlock.slice(0,W)+Z+this._frontmatterBlock.slice(W+X.length)}serialize(){let $=this._frontmatterBlock;$+=this._preamble;for(let U of this._sections)$+=U.modifiedText??U.originalText;return $}}function Do($){return`"${$.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`}function Ko($){if($.startsWith('"')&&$.endsWith('"'))return $;if(/[:{}[\],&*?|>!%@`#"'\\\n\r]/.test($))return`"${$.replace(/\\/g,"\\\\").replace(/"/g,"\\\"")}"`;if(/^(null|true|false|yes|no|on|off)$/i.test($))return`"${$}"`;if(/^\d/.test($)&&!/^\d{4}-\d{2}-\d{2}/.test($))return`"${$}"`;return $}G6();var LA=["backlog","todo","wip","testing","blocked","done","cancelled"],Fo=["backlog","active","verifying","blocked","done","cancelled"];var QV=["P0","P1","P2","P3"],qo=["simple","standard","complex","research","refine","plan","unit","review","docs"],Io=["task","issue","review","meta","brainstorm"],OA=["standard","feature-impl","issue","review","meta","brainstorm"];var PA=/^[A-Z][1-9]*$/;var Bo={completed:"done",complete:"done","in-progress":"wip","in progress":"wip",in_progress:"wip",dropped:"cancelled",cancel:"cancelled",canceled:"cancelled",new:"backlog",pending:"backlog",blocked:"blocked",wip:"wip",testing:"testing",done:"done",cancelled:"cancelled",backlog:"backlog",todo:"todo"};function Lo($){let U=$.trim().toLowerCase(),J=Bo[U];if(J===void 0)throw Error(`Unknown task status: ${JSON.stringify($)} (allowed: ${LA.join(", ")})`);return J}var w7=F.string().min(1).refine(($)=>!Number.isNaN(Date.parse($)),{message:"must be an ISO 8601 timestamp"}),jA=F.string().regex(PA,{message:"feature id must match ^[A-Z][1-9]*$ (DD-14)"}).nullable().optional(),RA=F.string().regex(/^\d{4}$/,{message:"parent_wbs must be a 4-digit WBS string"}).nullable().optional(),yF$=F.object({schema_version:F.literal(1),name:F.string().min(1),description:F.string().optional(),status:F.preprocess(($)=>{if(typeof $==="string")try{return Lo($)}catch{}return $},F.enum(LA)),type:F.enum(Io).optional().default("task"),template:F.enum(OA).optional(),profile:F.enum(qo).optional(),feature_id:jA,parent_wbs:RA,priority:F.enum(QV).optional(),tags:F.array(F.string()).optional(),dependencies:F.array(F.string()).optional(),ac_numbering:F.literal("task-local").optional(),ac_altitude:F.enum(["graduating","task-local"]).optional(),done_forced:F.preprocess(($)=>typeof $==="string"?$==="true":$,F.boolean().optional()),done_reason:F.string().optional(),feature_link_declined:F.preprocess(($)=>typeof $==="string"?$==="true":$,F.boolean().optional()),estimate_hours:F.preprocess(($)=>typeof $==="string"&&$.trim()!==""?Number($):$,F.number().positive().optional()),created_at:w7,updated_at:w7}),fF$=F.object({schema_version:F.literal(1),id:F.string().regex(PA,{message:"feature id must match ^[A-Z][1-9]*$ (DD-14)"}),name:F.string().min(1),status:F.enum(Fo),priority:F.enum(QV).optional(),tags:F.array(F.string()).optional(),created_at:w7,updated_at:w7});var Oo=F.object({name:F.string().min(1,"name is required"),background:F.string().optional(),requirements:F.string().optional(),design:F.string().optional(),plan:F.string().optional(),acceptance_criteria:F.string().optional(),feature_id:jA,parent_wbs:RA,priority:F.enum(QV).optional(),tags:F.array(F.string()).optional(),template:F.enum(OA).optional(),estimate_hours:F.number().positive().optional()}).strict(),Po=F.array(Oo).min(1,"batch must contain at least one task");u0();G6();var DV=F.enum(Q0),jo=F.object({required:F.array(DV).optional(),optional:F.array(DV).optional(),forbidden:F.array(DV).optional(),gate:F.boolean().optional()}).strict(),Ro=F.enum(["backlog","todo","wip","testing","blocked","done","cancelled"]),cF$=F.object({variants:F.record(F.string(),F.partialRecord(Ro,jo))}).strict();var dF$=new Map(Q0.map(($,U)=>[$,U]));var No=["requirements","design","plan","ac","decisions","dependencies","premises"];function MA($){return`/sp:dev-refine ${$} --auto --depth ready`}var Ao={Solution:!0,Testing:!0,Review:!0,History:!0},vo=Q0.filter(($)=>!($ in Ao));function NA($){let U=H2.parse($,"task"),J=U.frontmatterData??{},G={sections:vo.map((X)=>{let Y=U.getSection(X);return{name:X,body:Y===null?null:Y.trim()}}),featureId:typeof J.feature_id==="string"?J.feature_id:null,template:typeof J.template==="string"?J.template:null,dependencies:Array.isArray(J.dependencies)?[...J.dependencies].map(String).sort():null};return Mo("sha256").update(JSON.stringify(G)).digest("hex")}function AA($){if($===void 0||$.length===0)return{ok:!1,reason:"no ready-checklist evidence"};for(let U of No){let J=$.find((G)=>G.id===U);if(J===void 0)return{ok:!1,reason:`checklist row "${U}" missing`};if(J.pass!==!0)return{ok:!1,reason:`checklist row "${U}" not passing`};if(typeof J.evidence!=="string"||J.evidence.trim()==="")return{ok:!1,reason:`checklist row "${U}" has empty evidence`}}return{ok:!0}}var To=/[;&|<>$`(){}[\]!*?~#\n\r"']/;function vA($,U){if(To.test($))return{error:`${U} must not contain shell metacharacters (got ${$})`};let J=$.trim().split(/\s+/).filter((X)=>X.length>0),G=J[0];if(G===void 0)return{error:`Action option ${U.slice(U.indexOf('"'))} must be a non-empty string`};return{command:G,leadingArgs:J.slice(1)}}function K0($){return`exit=${$.exitCode??"null"}${$.signal?` signal=${$.signal}`:""}: ${$.stderr.trim()||"no stderr"}`}async function TA($){let U=$.projectRoot??process.cwd(),J=$.fileSystem??HZ(),G=$.processExecutor??new Z7,X=$.spurBin??"spur",{runId:Y,featureId:H}=$,_=D0(U,".spur","run"),Z=D0(_,`${Y}-idea-task-batch.json`),W=D0(_,`${Y}-idea-batch-create-result.json`),z=D0(_,`${Y}-idea-task-order.json`),V=D0(_,`${Y}-idea-ready.json`),Q=D0(_,`${Y}-idea-handoff.md`),D=vA(X,'idea-handoff "spurBin"');if("error"in D)return{ok:!1,wbsList:[],nextCommand:"",reportPath:Q,error:D.error};let{command:q,leadingArgs:I}=D;if(!await J.exists(Z)||!await J.exists(W)||!await J.exists(z))return{ok:!1,wbsList:[],nextCommand:"",reportPath:Q,error:"Required batch, result, or order files missing in .spur/run/"};try{let L=JSON.parse(await J.readFile(Z)),P=JSON.parse(await J.readFile(W)),R=JSON.parse(await J.readFile(z)),v=await J.exists(V),M=await(async()=>{if(!v)return[];try{let f=JSON.parse(await J.readFile(V)),V$=typeof f==="object"&&f!==null&&"tasks"in f?f.tasks:void 0;return Array.isArray(V$)?V$:[]}catch{return[]}})(),E=new Map;for(let f of M)if(typeof f?.wbs==="string")E.set(f.wbs,f);if(!Array.isArray(L)||!Array.isArray(P?.wbs)||!Array.isArray(R))return{ok:!1,wbsList:[],nextCommand:"",reportPath:Q,error:"Malformed batch, result, or order JSON structure"};if(L.length!==P.wbs.length)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Batch size mismatch: ${L.length} items declared but ${P.wbs.length} WBS created`};let x=L.map((f)=>f.name);if(new Set(x).size!==x.length)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:"Duplicate task names found in batch declaration"};for(let f of R){let V$=x.indexOf(f.name);if(V$===-1||!P.wbs[V$])return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Task name "${f.name}" from order could not be mapped to created WBS`};let K$=P.wbs[V$],H4=[];for(let G$ of f.depends_on_names??[]){let F$=x.indexOf(G$);if(F$===-1||!P.wbs[F$])return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Dependency task name "${G$}" could not be mapped to created WBS`};H4.push(P.wbs[F$])}if(H4.length>0){let G$=await G.run({command:q,args:[...I,"task","deps",K$,"set",...H4,"--json"],cwd:U,forceBuffered:!0,rejectOnError:!1});if(G$.exitCode!==0)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Failed to set dependencies for task ${K$}: ${K0(G$)}`}}}let p=await G.run({command:q,args:[...I,"feature","refresh","--feature",H,"--json"],cwd:U,forceBuffered:!0,rejectOnError:!1});if(p.exitCode!==0)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Feature refresh for ${H} failed: ${K0(p)}`};let w=[];for(let f of P.wbs){let V$=await G.run({command:q,args:[...I,"task","path",f,"--json"],cwd:U,forceBuffered:!0,rejectOnError:!1});if(V$.exitCode===null)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Task path for ${f} could not be spawned (${q}): ${K0(V$)}`};let K$;if(V$.exitCode!==0)K$=`task path resolution failed: ${K0(V$)}`;else{let G$;try{let F$=JSON.parse(V$.stdout);if(typeof F$==="object"&&F$!==null&&"filePath"in F$){let j$=F$.filePath;if(typeof j$==="string")G$=j$}}catch{G$=void 0}if(G$===void 0)K$="task path output carried no filePath";else{let F$;try{F$=NA(await J.readFile(G$))}catch{F$=void 0}if(F$===void 0)K$=`task file unreadable at ${G$}`;else{let j$=E.get(f);if(j$===void 0)K$="no ready evidence recorded for this run";else if(j$.status!=="ready")K$=`ready evidence status is ${String(j$.status)}`;else if(typeof j$.planningDigest!=="string"||j$.planningDigest!==F$)K$="planning digest stale — task content changed after preparation";else{let F0=AA(Array.isArray(j$.checks)?j$.checks:void 0);if(!F0.ok)K$=F0.reason}}}}if(K$===void 0){let G$=await G.run({command:q,args:[...I,"task","check",f,"--json"],cwd:U,forceBuffered:!0,rejectOnError:!1});if(G$.exitCode===null)return{ok:!1,wbsList:P.wbs,nextCommand:"",reportPath:Q,error:`Task check for ${f} could not be spawned (${q}): ${K0(G$)}`};if(G$.exitCode!==0)K$=`deterministic task check failed (${K0(G$)})`}let H4=K$===void 0;w.push({wbs:f,pass:H4,status:H4?"ready":"unready",...H4?{}:{reason:K$,action:MA(f)}})}let r=w.some((f)=>!f.pass),Z$=r?`/sp:dev-refineall --feature ${H} --auto --depth ready`:`/sp:dev-runall --feature ${H} --auto`,z$=["# Idea pipeline handoff report","",`Feature: ${H}`,`Run ID: ${Y}`,"","## Created tasks",...P.wbs.map((f)=>` - ${f}`),"","## Per-task readiness (evidence + digest + task check)","","| WBS | Outcome | Reason |","|-----|---------|--------|",...w.map((f)=>`| ${f.wbs} | ${f.pass?"READY":"UNREADY"} | ${f.reason??"-"} |`),"",...r?["## Preparation actions","",...w.filter((f)=>!f.pass).map((f)=>` - ${f.wbs}: ${f.action}`),""]:[],"## Next command","",Z$,""];return await J.ensureDir(_),await J.writeFile(Q,z$.join(`
1329
1329
  `)),{ok:!0,wbsList:P.wbs,nextCommand:Z$,reportPath:Q}}catch(L){return{ok:!1,wbsList:[],nextCommand:"",reportPath:Q,error:L instanceof Error?L.message:String(L)}}}async function lq$($,U=TA){let J=$.__runId??"",G=$.featureId??"";if(J===""||G==="")return y7("idea-handoff: __runId and featureId env vars are required"),{exitCode:1};let X=await U({runId:J,featureId:G,spurBin:$.spurBin??"spur"});if(!X.ok)return y7(`idea-handoff: ${X.error??"failed"}`),{exitCode:1,result:X};return b7(`idea-handoff: wrote ${X.reportPath} (${X.wbsList.length} task(s))`),b7(`idea-handoff: next -> ${X.nextCommand}`),{exitCode:0,result:X}}export{lq$ as runIdeaHandoffCli};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.83",
3
+ "version": "0.3.85",
4
4
  "description": "Spur — a local-first harness engineering toolkit that wraps mainstream coding agents with constraint checking, workflow orchestration, and history analytics.",
5
5
  "extensions": {
6
6
  "pi": ["./hooks/pi/guard-extension.ts"]
@@ -636,7 +636,7 @@ const JSON_PROBES: string[][] = [
636
636
  ['feature', 'show', 'I3', '--json'],
637
637
  ['rule', 'list', '--json'],
638
638
  ['agent', 'list', '--json'],
639
- ['agent', 'doctor', 'omp', '--json'],
639
+ ['agent', 'doctor', 'pi', '--json'],
640
640
  ['workflow', 'list', '--json'],
641
641
  ['workflow', 'validate', 'basic.yaml', '--json'],
642
642
  ['status', '--json'],
@@ -65,6 +65,19 @@ and put prompt-layer routing policy into a domain-layer registry. This reference
65
65
  surface carries the work*; ADR-033 decides *which tier runs on it*. Read both; apply each to its
66
66
  own axis.
67
67
 
68
+ ## Cheap-model hint for read-only fan-out (2026-09-15 subagent-dispatch evaluation)
69
+
70
+ Surface choice and model choice stay separate axes, but the invocation surface is where the
71
+ cheap-model hint is applied. On hosts whose native subagent invocation accepts a per-call model
72
+ override (Claude Code: the Agent tool's `model` parameter), dispatch read-only fan-out workers —
73
+ `dev-parallel --mode investigation` and competency-lens review's read-only lenses — with the host's
74
+ cheapest capable model. The rule is owned by
75
+ [fan-out-patterns.md § Cheap-model hint for read-only fan-out](fan-out-patterns.md#cheap-model-hint-for-read-only-fan-out)
76
+ and carries two prohibitions and one exemption: invocation-side only, **never** a subagent
77
+ definition-file edit (superskill owns those), **never** a hard pin (the host may ignore it), and
78
+ `sp:spur-dev` pipeline stage dispatch is exempt — ADR-033's `model_policy` and ADR-078's role
79
+ config own that axis.
80
+
68
81
  ## The sandbox reliability tax on `spur agent run`
69
82
 
70
83
  `spur agent run` runs the target agent as an external process. Under a sandboxed Bash session it
@@ -98,6 +111,30 @@ native subagent with shared-worktree capability); the inline driver remains the
98
111
  provenance, artifact validation, and no-replay guarantees. This reference stays the authority for
99
112
  the native-subagent versus `spur agent run` choice everywhere else.
100
113
 
114
+ ## Fork dispatch on hosts that support it (2026-09-15 subagent-dispatch evaluation)
115
+
116
+ A fresh native subagent starts with an empty context: it re-loads every skill file, task document,
117
+ and convention the host already holds — the dominant cost of dispatching a context-heavy stage.
118
+ Claude Code offers a **fork** subagent that inherits the parent's full conversation and shares its
119
+ prompt cache, so the worker starts warm instead of re-ingesting. When a dispatch's value depends on
120
+ the host's accumulated context — an implement stage following a long precheck/design discussion, or
121
+ an investigation fan-out whose question only makes sense against what the session already learned —
122
+ prefer the fork surface over a cold native subagent.
123
+
124
+ Three constraints keep fork honest:
125
+
126
+ - **Host-specific.** Fork is a Claude Code capability. Hosts without it use the standard native
127
+ subagent; this paragraph changes nothing there.
128
+ - **Fork always runs the parent's model.** It is never a cost-routing move — cheap-tier work
129
+ (scribe-role fan-out, read-only exploration) still goes to a cold cheap worker.
130
+ - **Fork inherits the host's framing.** Stages whose value is independent eyes — review, verify,
131
+ adversarial panels — must NOT fork; a worker that inherits the author's reasoning grades its own
132
+ homework. Fork is for continuity, cold dispatch is for independence.
133
+
134
+ The inline driver's resume-over-re-dispatch rule
135
+ ([inline-pipeline-driver.md](../../spur-dev/references/inline-pipeline-driver.md)) is the same
136
+ instinct one level down: reuse a warm worker for continuation, dispatch fresh for judgment.
137
+
101
138
  ## Role propagation across fan-out (task 0551, feature I4)
102
139
 
103
140
  When a run dispatches subagents, the **effective role** each subagent resolves through follows one
@@ -42,7 +42,7 @@ Four proven patterns for parallel subagent execution. Each pattern has a distinc
42
42
 
43
43
  **Example:** Code review of a PR → correctness lens, security lens, efficiency lens, maintainability lens. Each subagent produces per-lens findings; synthesis merges into a unified P1–P4 table.
44
44
 
45
- **Anti-pattern:** Using competency-lens review when a single reviewer would catch everything. For a 20-line change, one thorough review beats 3 shallow ones.
45
+ **Anti-pattern:** Using competency-lens review when a single reviewer would catch everything. A sub-floor scope (see [Size floor](#size-floor)) is cheaper as one thorough pass than as N shallow ones — do not fan it out at all.
46
46
 
47
47
  ### 3. Independent-Task Batch
48
48
 
@@ -89,6 +89,7 @@ Four proven patterns for parallel subagent execution. Each pattern has a distinc
89
89
  | Sequential dependency chain | **Do not fan out** | — |
90
90
  | Single file touched by multiple tasks | **Do not fan out** | — |
91
91
  | Token budget < 20k remaining | **Do not fan out** | — |
92
+ | Scope below the [Size floor](#size-floor) | **Do not fan out — execute inline** | — |
92
93
 
93
94
  ## Token-budget guard
94
95
 
@@ -99,3 +100,56 @@ remaining_budget >= (N × per_subagent_estimate) + synthesis_estimate
99
100
  ```
100
101
 
101
102
  If not: reduce N, or serialize. A fan-out that exhausts the budget mid-run leaves partial results that are worse than sequential. The driver is responsible for this check — the skill provides the estimates; the orchestrator applies them.
103
+
104
+ ## Size floor
105
+
106
+ Fanning out below a minimum work size wastes the dispatch: a ~20-line change is cheaper as one
107
+ thorough inline pass than as 3 shallow parallel ones. The floor is met — and the driver MUST
108
+ **not** fan out, executing inline instead — when **any** of these constants holds:
109
+
110
+ - the scope is a **single file**; or
111
+ - the estimated diff is **under ~50 lines**; or
112
+ - the work is **one focused question**.
113
+
114
+ The thresholds are constants: they are measured from observable scope (files touched, diff lines,
115
+ a single question), never from a model estimate, and never exposed as a config knob — a value that
116
+ never changes is a constant. The floor decides *whether to fan out at all*; once it is met, the
117
+ [When-to-use decision table](#when-to-use-decision-table) and the
118
+ [Token-budget guard](#token-budget-guard) decide *which pattern* and *how many*. This is the
119
+ pre-dispatch gate, not an anti-pattern remark.
120
+
121
+ ## Pre-dispatch permission check
122
+
123
+ Claude Code exposes no dry-run permission API, so the contract is **name-capabilities + fail-fast
124
+ blocker** — never a permission-probing subsystem:
125
+
126
+ 1. Before dispatching a worker, the orchestrator names the required capabilities in the dispatch
127
+ prompt: the read/search tools it needs, every shell action it will run, and any write surface it
128
+ holds.
129
+ 2. The worker is instructed to **return a blocker immediately on the first missing capability**
130
+ instead of waiting at a permission prompt the host cannot see — a worker parked at that prompt is
131
+ invisible until join.
132
+ 3. Record one run-log line per dispatch: `stage <id> permission precheck: ok | <missing capability>`
133
+ (for ad-hoc fan-out, `<id>` is the dispatched worker's ledger id from
134
+ [Keep a durable progress ledger](../SKILL.md#keep-a-durable-progress-ledger)).
135
+
136
+ Read-only investigation fan-out avoids the write-permission class by construction:
137
+ `dev-parallel --mode investigation` and competency-lens review's read-only lenses dispatch
138
+ **read-only worker shapes** — search/read tools only, with no write surface declared.
139
+
140
+ ## Cheap-model hint for read-only fan-out
141
+
142
+ On hosts whose native subagent invocation accepts a per-call model override (Claude Code: the Agent
143
+ tool's `model` parameter), dispatch read-only fan-out workers — `dev-parallel --mode investigation`
144
+ and competency-lens review's read-only lenses — with the host's cheapest capable model. The rule is
145
+ invocation-side, with two prohibitions and one exemption:
146
+
147
+ - **Never a definition-file edit.** Subagent definition files (`plugins/sp/agents/*.md`) belong to
148
+ superskill's lifecycle; this plugin never edits them to route cost.
149
+ - **Never a hard pin.** The host may ignore the override; the hint names a preference, not a
150
+ contract.
151
+ - **`sp:spur-dev` pipeline stage dispatch is exempt.** Tier routing there is ADR-033/ADR-078's job
152
+ via [`plugins/sp/references/roles.md`](../../../references/roles.md); this hint does not touch it.
153
+
154
+ Surface choice is [dispatch-surface.md](dispatch-surface.md)'s axis; this hint applies on the
155
+ surface it selects, not instead of it.
@@ -46,6 +46,18 @@ Apply the two in order:
46
46
  Without the second clause, cohesion reads as "never split" — the opposite failure. The knobs are
47
47
  the escape hatch; cohesion is the default.
48
48
 
49
+ ### Persist the estimate: `estimate_hours`
50
+
51
+ Every batch item's hour estimate MUST also travel into the batch JSON as the item's
52
+ `estimate_hours` field (a positive number) — `spur task batch-create` persists it to the task's
53
+ frontmatter, and it can be corrected later with `spur task update <wbs> --estimate-hours <n>`. The
54
+ estimate already exists at this point (the hour knobs were just applied above); writing it down
55
+ costs nothing and makes the size decision visible to later stages instead of re-derived. Its
56
+ primary consumer is the inline pipeline driver's dispatch floor
57
+ ([inline-pipeline-driver.md](../../spur-dev/references/inline-pipeline-driver.md)): a task at or
58
+ below 1 hour executes its stages host-inline rather than paying a subagent dispatch. An estimate
59
+ left unwritten forgoes that gate — the task dispatches like any large one.
60
+
49
61
  ### Worked example: H8's own first decomposition
50
62
 
51
63
  Feature H8 ("sp command surface coherence") decomposed into five tasks, each 3–8h — fully inside
@@ -114,7 +114,10 @@ inline` interpret the existing `task-pipeline.yaml` in the host session; they do
114
114
  workflow run` and never redirect silently to `agent.default`. Interactive **omit** is
115
115
  **host-controlled and non-subprocess**, but no longer guarantees host-context execution for every
116
116
  model stage (task 0508): an eligible `agent.run` stage — pure-slash input, non-interactive state,
117
- native subagent with shared-worktree read/write/shell capability — dispatches **once** to that
117
+ native subagent with shared-worktree read/write/shell capability, and a task above the
118
+ `estimate_hours` dispatch floor (2026-09-15 subagent-dispatch evaluation; the floor and the
119
+ resume-over-re-dispatch rule for worker-role continuation stages are owned by
120
+ [inline-pipeline-driver.md](inline-pipeline-driver.md)) — dispatches **once** to that
118
121
  native subagent and joins before the driver continues; any pre-dispatch eligibility failure falls
119
122
  back to one host execution, and a failure after dispatch follows the stage's error policy with no
120
123
  automatic host replay. Inline resolution (omitted or explicit, 0687 R1/R2) keeps the native-subagent
@@ -236,13 +236,38 @@ Action semantics come from the YAML and the workflow action contract:
236
236
  decision, or other operator prompt.
237
237
  4. The platform exposes a native subagent that shares the working tree and has read, write, shell,
238
238
  and Spur task/run-artifact access.
239
-
240
- All four pass → dispatch. Any pre-dispatch failure → execute the stage **once** in the host session.
239
+ 5. **Size floor (2026-09-15 subagent-dispatch evaluation).** The task file's frontmatter
240
+ `estimate_hours` — when present — is **above** the dispatch floor of **1 hour**. A task at or
241
+ below the floor is cheaper to execute than to delegate: the stage runs host-inline and the run
242
+ log carries `stage <id> executed inline in session <session-id> (below dispatch floor:
243
+ estimate_hours <n> <= 1)`. The field is authored at decomposition time (batch item
244
+ `estimate_hours` → frontmatter) or set via `spur task update <wbs> --estimate-hours <n>`; a task
245
+ with no `estimate_hours` passes this condition unchanged. The driver reads the frontmatter value
246
+ directly — never estimates size itself.
247
+
248
+ All five pass → dispatch. Any pre-dispatch failure → execute the stage **once** in the host session.
241
249
  An `agent.run` whose `input` is free-form prose rather than a pure slash command fails condition 2
242
250
  and is never dispatch-eligible: the driver executes it in the host session and logs it with the
243
251
  existing host-fallback line `stage <id> executed inline in session <session-id>` — it does not
244
252
  reformulate the prose into a command, spawn a subagent for it, or silently promote it to dispatch.
245
- No token estimate, stage-size threshold, model heuristic, or configuration switch is added.
253
+ Beyond the deterministic `estimate_hours` floor in condition 5, no token estimate, model heuristic,
254
+ or configuration switch is added.
255
+
256
+ **Pre-dispatch permission check (2026-09-15 subagent-dispatch evaluation).** Claude Code exposes no
257
+ dry-run permission API, so the contract is **name-capabilities + fail-fast blocker** — never a
258
+ permission-probing subsystem. Before dispatching a stage, the driver names the stage's required
259
+ capabilities in the dispatch payload: the slash command (field 2), the resolved Spur invocation
260
+ (field 4), and any shell actions the YAML action declares. The delegate is instructed to **return a
261
+ blocker immediately on the first missing permission** rather than stalling — a worker parked at a
262
+ prompt the host cannot see is invisible until join. Record one run-log line per dispatch:
263
+
264
+ ```text
265
+ stage <id> permission precheck: ok | <missing capability>
266
+ ```
267
+
268
+ Read-only investigation fan-out (`/sp:dev-parallel --mode investigation`) dispatches read-only
269
+ worker shapes by construction — see the [pre-dispatch permission
270
+ rule](../../parallel-execution/references/fan-out-patterns.md).
246
271
 
247
272
  **Dispatch and join:** before dispatch, capture the same pre-action git snapshot used by
248
273
  `requireDiff` enforcement, and resolve `answerFile`/`expectFile` against the worktree root — the
@@ -251,7 +276,7 @@ to write and what post-join validation reads. Resolving once at the dispatch bou
251
276
  surface at once; a relative path would resolve against whatever cwd the writer process happens to
252
277
  have.
253
278
 
254
- **Dispatch payload (task 0818 R2).** Send exactly these five fields. The earlier "send only the
279
+ **Dispatch payload (task 0818 R2).** Send exactly these six fields. The earlier "send only the
255
280
  stage id, the slash command, and the no-recursion notice" restriction is **deliberately replaced**:
256
281
  the execution-tree cwd, the Spur invocation, and the output path are all already resolved at this
257
282
  boundary, and a delegate left to re-derive them re-derives them against its own cwd and PATH.
@@ -268,6 +293,17 @@ boundary, and a delegate left to re-derive them re-derives them against its own
268
293
  `--spur-bin` flag rather than a new mechanism.
269
294
  5. The **resolved absolute output path** (`answerFile`/`expectFile`, resolved as above) and the
270
295
  **owning stage's artifact contract** — for a verify stage, the compact contract below.
296
+ 6. **The implement-stage acceptance-evidence requirement** — for the implement stage and any stage
297
+ whose YAML action declares `requireDiff`. A `requireDiff` stage is the one whose delegate writes
298
+ the deliverable, so its handoff carries three components:
299
+ (a) **the task's AC identities verbatim** — the exact `Scenario:` titles / checklist text read by
300
+ the driver from the task file, never paraphrased;
301
+ (b) **required evidence** — the pasted output of the narrow targeted tests the delegate ran, and
302
+ a `file:line` change map written into the task's `## Solution` section;
303
+ (c) the reminder that **a delegate success message is not evidence** — post-join validation reads
304
+ the artifacts and the diff, not the delegate's claims.
305
+ Component (a) is read from the task by the driver; this extends the payload contract and is NOT a
306
+ new YAML key.
271
307
 
272
308
  Nothing else: no task/session transcripts, no machine-specific session paths. The WBS/path already
273
309
  carried by the slash command remains the task handoff.
@@ -313,6 +349,26 @@ before the subagent starts, log the reason and use host fallback. If a started s
313
349
  leaves invalid artifacts, do **not** replay the stage in the host — follow the YAML error policy so
314
350
  partial mutations are not duplicated.
315
351
 
352
+ **Resume over re-dispatch for worker-role continuation (2026-09-15 subagent-dispatch evaluation).**
353
+ Some stages continue the *same* task's work product after a finding: `test-fix` after a failing
354
+ `test`, an implement rework after review findings. A cold re-dispatch makes the next worker
355
+ re-ingest the task, the diff, and every skill file it already loaded. When the host platform
356
+ supports addressing a completed subagent again (Claude Code: send a follow-up message to the same
357
+ agent — its context survives completion), the driver SHOULD resume the prior same-task worker
358
+ subagent for that continuation stage instead of dispatching a fresh one, carrying the same
359
+ six-field payload plus the finding that triggered the continuation. Provenance uses the resumed
360
+ form:
361
+
362
+ ```text
363
+ stage <id> executed via subagent <agent-id> (resumed; host session <session-id>)
364
+ ```
365
+
366
+ The resume rule applies to **worker-role** stages only (implement, test-fix). Reviewer and verify
367
+ stages always dispatch **fresh**: their value is independent eyes on the diff, and a resumed worker
368
+ grading its own work defeats the stage. A continuation stage also dispatches fresh when no prior
369
+ same-task subagent exists (the earlier stage ran host-inline or below the dispatch floor), when a
370
+ host-owned gate sat between the stages, or when the platform cannot address completed subagents.
371
+
316
372
  **Timeout boundary (task 0727):** a dispatched subagent is governed by
317
373
  **the host platform's subagent limit, not the YAML timeoutMs** — `timeoutMs` stays not-applicable
318
374
  for host execution only — and before dispatch the driver must
@@ -58,6 +58,11 @@
58
58
  "type": "string",
59
59
  "enum": ["feature-impl", "issue", "review", "meta"],
60
60
  "description": "Template variant for section structure (default: the standard template)."
61
+ },
62
+ "estimate_hours": {
63
+ "type": "number",
64
+ "exclusiveMinimum": 0,
65
+ "description": "Size estimate in hours — persisted to frontmatter; the inline pipeline driver's dispatch floor reads it (at/below floor → host-inline, no subagent dispatch)."
61
66
  }
62
67
  },
63
68
  "additionalProperties": false