@gobing-ai/spur 0.3.83 → 0.3.84
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/rules/strict/runtime-boundaries.yaml +1 -0
- package/package.json +1 -1
- package/plugins/sp/lib/idea-handoff.generated.mjs +1 -1
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +37 -0
- package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +55 -1
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +12 -0
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +4 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +60 -4
- package/schemas/task-batch.schema.json +5 -0
- package/spur.js +780 -325
- package/web/_astro/{BoardApp.D8bM9pKL.js → BoardApp.D4mBUGLT.js} +64 -64
- package/web/_astro/BoardApp.DFPYpI5O.js +1 -0
- package/web/_astro/{TaskDetail.BPRqgVUE.js → TaskDetail.DhCWaOPR.js} +1 -1
- package/web/_astro/{arc.BPrPES3z.js → arc.D3X0nfq-.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.qX_7q02P.js → architectureDiagram-3BPJPVTR.BQDoiGme.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.CUZfj5V7.js → blockDiagram-GPEHLZMM.BjHekBbM.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.CTaOr8hH.js → c4Diagram-AAUBKEIU.BvHt171l.js} +1 -1
- package/web/_astro/channel.BlQwxyoI.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Dt-9wf3h.js → chunk-2J33WTMH.CwxGXlDR.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CTC2sdoN.js → chunk-4BX2VUAB.Dia1YtwU.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DQcxt2_g.js → chunk-55IACEB6.Oz9epCXa.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DXFPSn-a.js → chunk-727SXJPM.DKFYJjYJ.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.BCx3U4bT.js → chunk-AQP2D5EJ.AD7w5oVz.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.DL2tJkdO.js → chunk-FMBD7UC4.D0OqTj52.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.DZyflMro.js → chunk-ND2GUHAM.CU5ppR7j.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.CUI2mT09.js → chunk-QZHKN3VN.Bu1rFztw.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.g4rX4Fr1.js → classDiagram-4FO5ZUOK.C0Pgm5yE.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js → classDiagram-v2-Q7XG4LA2.C0Pgm5yE.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CzWJLqp0.js → cose-bilkent-S5V4N54A.C0ecLb1e.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.WgsvQeCR.js → cynefin-OW5HDTMX.CpStVmhO.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Dzv6ngql.js → dagre-BM42HDAG.BPWN3W9f.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.CpJ4a9rU.js → diagram-2AECGRRQ.DnXbEot4.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CzPlF_dq.js → diagram-5GNKFQAL.BYJsgUiI.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.TAkZNTcQ.js → diagram-KO2AKTUF.CU4Bqprx.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.uCjoKSag.js → diagram-LMA3HP47.BQLOANIu.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.eMplIjoK.js → diagram-OG6HWLK6.Cn1UVhqj.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.Bf7zoXGz.js → erDiagram-TEJ5UH35.C8FdZyvA.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.B_bHj3gN.js → flowDiagram-I6XJVG4X.QSZCR8Uo.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.BasrHRMj.js → ganttDiagram-6RSMTGT7.O1Py31MY.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.C6iphq1x.js → gitGraphDiagram-PVQCEYII.DoeJRBfY.js} +1 -1
- package/web/_astro/index.TPF1FerP.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.HXmDMhW4.js → infoDiagram-5YYISTIA.9npLJecW.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BSmW8NiU.js → ishikawaDiagram-YF4QCWOH.DXanvOr3.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.DEQow5fo.js → journeyDiagram-JHISSGLW.DxbVjxAv.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.IVm9cTdc.js → kanban-definition-UN3LZRKU.BDk6_iq0.js} +1 -1
- package/web/_astro/{linear.CrsM73_9.js → linear.C9cCDjT3.js} +1 -1
- package/web/_astro/{mermaid.core.CfBeDJls.js → mermaid.core.CpHRe5nH.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.C3j60Y-0.js → mindmap-definition-RKZ34NQL.D-6C85bn.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.B-aCMeEA.js → pieDiagram-4H26LBE5.B3Oxodty.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.Cib965yq.js → quadrantDiagram-W4KKPZXB.QZ_9Hshu.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.D61cS4O-.js → requirementDiagram-4Y6WPE33.BimvuK53.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.GKF2qVPy.js → sankeyDiagram-5OEKKPKP.D5Kkpx2C.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DZnq8F2h.js → sequenceDiagram-3UESZ5HK.B4YR6fAE.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.DXUFmdgM.js → stateDiagram-AJRCARHV.Bt2wkZok.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.BtHmhLEz.js → stateDiagram-v2-BHNVJYJU.YZ4X7dfQ.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.Cy-WW2ln.js → timeline-definition-PNZ67QCA.DmiTBL8w.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.SLp5b9KI.js → vennDiagram-CIIHVFJN.KNJV40mf.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.Bww45mWV.js → wardleyDiagram-YWT4CUSO.BVAaKgWH.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DR4swI6a.js → xychartDiagram-2RQKCTM6.D5Jx7zB2.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.D-WlxiN2.js +0 -1
- package/web/_astro/channel.DGZaFHZx.js +0 -1
- package/web/_astro/index.DayyIngm.css +0 -1
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobing-ai/spur",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.84",
|
|
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",
|
|
@@ -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};
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.84",
|
|
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"]
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|