@gobing-ai/spur 0.3.67 → 0.3.69

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 (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/corpus-baseline.json +2 -50
  3. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +21 -16
  4. package/config/workflow-composition-baseline.json +110 -60
  5. package/config/workflows/basic.yaml +1 -1
  6. package/config/workflows/docs-pipeline.yaml +99 -23
  7. package/config/workflows/feature-dev.yaml +1 -1
  8. package/config/workflows/idea-pipeline.yaml +21 -23
  9. package/config/workflows/task-pipeline.yaml +233 -121
  10. package/config/workflows/wayfinder-resolution.yaml +1 -1
  11. package/config/workflows/wrapup-pipeline.yaml +1 -1
  12. package/package.json +9 -9
  13. package/plugins/sp/README.md +6 -7
  14. package/plugins/sp/commands/dev-idea.md +5 -3
  15. package/plugins/sp/commands/dev-plan.md +3 -1
  16. package/plugins/sp/commands/dev-review-session.md +2 -1
  17. package/plugins/sp/hooks/context-post-tool.ts +101 -2
  18. package/plugins/sp/hooks/context-session-start.ts +22 -1
  19. package/plugins/sp/plugin.json +1 -1
  20. package/plugins/sp/scripts/stage-registry-adapter.ts +144 -2
  21. package/plugins/sp/scripts/surface-drift-inventory.ts +2 -15
  22. package/plugins/sp/scripts/task-size-precheck.ts +21 -79
  23. package/plugins/sp/skills/session-review/SKILL.md +16 -0
  24. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +42 -17
  25. package/plugins/sp/skills/spur-dev/references/dev-operations.md +6 -6
  26. package/plugins/sp/skills/spur-dev/references/execution-batch.md +80 -11
  27. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +18 -13
  28. package/spur.js +2062 -609
  29. package/web/_astro/{BoardApp.BEtcJqde.js → BoardApp.BQFbkeqq.js} +15 -15
  30. package/web/_astro/BoardApp.CTkqrhWd.js +1 -0
  31. package/web/_astro/{TaskDetail.ClAbCXom.js → TaskDetail.Dl2Eaj1w.js} +1 -1
  32. package/web/_astro/{arc.CCvf51_y.js → arc.uG14rp8A.js} +1 -1
  33. package/web/_astro/{architectureDiagram-3BPJPVTR.C0cb0J5M.js → architectureDiagram-3BPJPVTR.Dye6uD_x.js} +1 -1
  34. package/web/_astro/{blockDiagram-GPEHLZMM.CIyjqoCE.js → blockDiagram-GPEHLZMM.B9Pkh7Hb.js} +1 -1
  35. package/web/_astro/{c4Diagram-AAUBKEIU.fs14IuFs.js → c4Diagram-AAUBKEIU.C2x7SC_X.js} +1 -1
  36. package/web/_astro/channel.Dsvulp7W.js +1 -0
  37. package/web/_astro/{chunk-2J33WTMH.CaBKv4ZO.js → chunk-2J33WTMH.D2p4-nWk.js} +1 -1
  38. package/web/_astro/{chunk-4BX2VUAB.BOllTPto.js → chunk-4BX2VUAB.S-6jf33o.js} +1 -1
  39. package/web/_astro/{chunk-55IACEB6.ChEof0O4.js → chunk-55IACEB6.DVXj4Fdh.js} +1 -1
  40. package/web/_astro/{chunk-727SXJPM.Co2kdjD8.js → chunk-727SXJPM.Dz689FMN.js} +1 -1
  41. package/web/_astro/{chunk-AQP2D5EJ.SWmfcnog.js → chunk-AQP2D5EJ.KxYj5TnI.js} +1 -1
  42. package/web/_astro/{chunk-FMBD7UC4.rDAFifF3.js → chunk-FMBD7UC4.itTQyHQB.js} +1 -1
  43. package/web/_astro/{chunk-ND2GUHAM.BCnoXKCw.js → chunk-ND2GUHAM.euSrbJf5.js} +1 -1
  44. package/web/_astro/{chunk-QZHKN3VN.RSmy2hDO.js → chunk-QZHKN3VN.OWASJRQy.js} +1 -1
  45. package/web/_astro/{classDiagram-4FO5ZUOK.Be7PEfrX.js → classDiagram-4FO5ZUOK.BLvrlpNO.js} +1 -1
  46. package/web/_astro/{classDiagram-v2-Q7XG4LA2.Be7PEfrX.js → classDiagram-v2-Q7XG4LA2.BLvrlpNO.js} +1 -1
  47. package/web/_astro/{cose-bilkent-S5V4N54A.BkUp2aSK.js → cose-bilkent-S5V4N54A.XBF-rmyD.js} +1 -1
  48. package/web/_astro/{cynefin-OW5HDTMX.BegGGlUV.js → cynefin-OW5HDTMX.DlCx762Z.js} +1 -1
  49. package/web/_astro/{dagre-BM42HDAG.BkUdjsaC.js → dagre-BM42HDAG.D17Rshxv.js} +1 -1
  50. package/web/_astro/{diagram-2AECGRRQ.E9vugt3-.js → diagram-2AECGRRQ.AhBIVJC8.js} +1 -1
  51. package/web/_astro/{diagram-5GNKFQAL.Dj4yeHXB.js → diagram-5GNKFQAL.C9ximjyC.js} +1 -1
  52. package/web/_astro/{diagram-KO2AKTUF.Buaquwli.js → diagram-KO2AKTUF.CZb7Ru_9.js} +1 -1
  53. package/web/_astro/{diagram-LMA3HP47.BV3dgGgm.js → diagram-LMA3HP47.BW7LwqoS.js} +1 -1
  54. package/web/_astro/{diagram-OG6HWLK6.Cnx3s-tc.js → diagram-OG6HWLK6.XC025W0V.js} +1 -1
  55. package/web/_astro/{erDiagram-TEJ5UH35.DKK_abu4.js → erDiagram-TEJ5UH35.CpMXmBDP.js} +1 -1
  56. package/web/_astro/{flowDiagram-I6XJVG4X.BNuu9fbm.js → flowDiagram-I6XJVG4X.D2ednJWg.js} +1 -1
  57. package/web/_astro/{ganttDiagram-6RSMTGT7.b16KUMjy.js → ganttDiagram-6RSMTGT7.BjL9FGKO.js} +1 -1
  58. package/web/_astro/{gitGraphDiagram-PVQCEYII.Kh41lbG5.js → gitGraphDiagram-PVQCEYII.B90g1VGk.js} +1 -1
  59. package/web/_astro/{infoDiagram-5YYISTIA.DEWBXkp-.js → infoDiagram-5YYISTIA.RqgycKtQ.js} +1 -1
  60. package/web/_astro/{ishikawaDiagram-YF4QCWOH.DiAdmcL6.js → ishikawaDiagram-YF4QCWOH.Ctn-zt6a.js} +1 -1
  61. package/web/_astro/{journeyDiagram-JHISSGLW.D1Ki7IRm.js → journeyDiagram-JHISSGLW.DJhT8Ctp.js} +1 -1
  62. package/web/_astro/{kanban-definition-UN3LZRKU.CWUhrQpc.js → kanban-definition-UN3LZRKU.BY1QdejI.js} +1 -1
  63. package/web/_astro/{linear.BaFsgcCe.js → linear.Di7YObSt.js} +1 -1
  64. package/web/_astro/{mermaid.core.CHw_AsGy.js → mermaid.core.CbxtJS3Q.js} +4 -4
  65. package/web/_astro/{mindmap-definition-RKZ34NQL.UIhghgmN.js → mindmap-definition-RKZ34NQL.CxvR4g_J.js} +1 -1
  66. package/web/_astro/{pieDiagram-4H26LBE5.D05l3JUA.js → pieDiagram-4H26LBE5.jNWqnBHH.js} +1 -1
  67. package/web/_astro/{quadrantDiagram-W4KKPZXB.BcWIhIcE.js → quadrantDiagram-W4KKPZXB.BCp12MbA.js} +1 -1
  68. package/web/_astro/{requirementDiagram-4Y6WPE33.B1rYvKGn.js → requirementDiagram-4Y6WPE33.Dxhm4TyR.js} +1 -1
  69. package/web/_astro/{sankeyDiagram-5OEKKPKP.CKylVRC4.js → sankeyDiagram-5OEKKPKP.BtQXp4J9.js} +1 -1
  70. package/web/_astro/{sequenceDiagram-3UESZ5HK.Dm3uA_s4.js → sequenceDiagram-3UESZ5HK.BWEM1R_Q.js} +1 -1
  71. package/web/_astro/{stateDiagram-AJRCARHV.Bgca_BLe.js → stateDiagram-AJRCARHV.BG3wUkWB.js} +1 -1
  72. package/web/_astro/{stateDiagram-v2-BHNVJYJU.C1T7YFrG.js → stateDiagram-v2-BHNVJYJU.BLtMeFVP.js} +1 -1
  73. package/web/_astro/{timeline-definition-PNZ67QCA.ZOHJn3Sn.js → timeline-definition-PNZ67QCA.D5fHo0az.js} +1 -1
  74. package/web/_astro/{vennDiagram-CIIHVFJN.DegZitjD.js → vennDiagram-CIIHVFJN.0DcuMluU.js} +1 -1
  75. package/web/_astro/{wardleyDiagram-YWT4CUSO.BDsC115d.js → wardleyDiagram-YWT4CUSO.BZ-dxgHm.js} +1 -1
  76. package/web/_astro/{xychartDiagram-2RQKCTM6.D0MO70ea.js → xychartDiagram-2RQKCTM6.Bg-XWF7z.js} +1 -1
  77. package/web/index.html +1 -1
  78. package/web/_astro/BoardApp.DBEin4N5.js +0 -1
  79. package/web/_astro/channel.BGn_DUCD.js +0 -1
@@ -1,24 +1,23 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * task-size-precheck — pipeline size precheck guard (R2, task 0454) plus the
4
- * size-vs-executor-capability gate (R3, task 0487).
3
+ * task-size-precheck — pipeline size precheck guard (R2, task 0454; count-only
4
+ * since task 0723).
5
5
  *
6
6
  * Shells `spur task show <wbs> --json`, evaluates R-item count and Plan
7
- * checklist count against limits, writes PASS/FAIL to status file. With
8
- * `--executor`, also shells `spur agent doctor <exec> --json` and blocks a large
9
- * task routed to a sub-`capable-1` executor.
7
+ * checklist count against limits, writes PASS/FAIL to status file. No executor
8
+ * or doctor involvement: executor liveness/routing/capabilities are attested
9
+ * fail-closed at the `agent.run` dispatch boundary, not predicted here.
10
10
  *
11
- * Always exits 0 (soft check, like doctor). The precheck→implement guard in
12
- * task-pipeline.yaml reads the status file.
11
+ * Always exits 0 (soft action). The precheck→implement guard in
12
+ * task-pipeline.yaml reads the status file; a missing or failing checker writes
13
+ * FAIL, so readiness fails closed.
13
14
  *
14
15
  * Ships with the plugin to arbitrary projects, so it stays node-builtin-only —
15
- * no workspace imports. The capability tier therefore comes from the CLI rather
16
- * than from `getExecutorTier` directly; `spur agent doctor --json` exposes it as
17
- * `capabilityTier` precisely so the inference regex is not duplicated here.
16
+ * no workspace imports.
18
17
  *
19
18
  * Usage:
20
19
  * bun plugins/sp/scripts/task-size-precheck.ts <wbs> [--spur-bin <path>]
21
- * [--max-reqs <n>] [--max-plan-items <n>] [--executor <name>]
20
+ * [--max-reqs <n>] [--max-plan-items <n>]
22
21
  *
23
22
  * Env: SPUR_BIN, MAX_IMPLEMENT_REQS, MAX_IMPLEMENT_PLAN_ITEMS
24
23
  */
@@ -27,7 +26,6 @@ import { execFileSync } from 'node:child_process';
27
26
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
28
27
  import { join } from 'node:path';
29
28
  import { fileURLToPath } from 'node:url';
30
- import { STAGE_FLOOR_TIER, TIER_ORDER } from './stage-registry-adapter';
31
29
 
32
30
  // ─── Regex (sync with packages/app/src/services/task-size-precheck.ts) ───────
33
31
 
@@ -37,32 +35,11 @@ const R_ITEM_RE = /^\s*-\s*\[[ xX]\]\s*(\*\*)?R\d+\./m;
37
35
  /** Matches checklist items under the Plan section. */
38
36
  const CHECKLIST_ITEM_RE = /^\s*-\s*\[[ xX]\]/m;
39
37
 
40
- /**
41
- * Large-task thresholds for the capability gate (R3, task 0487) — the DEFAULT
42
- * caps, not the overridable `--max-*` limits. Raising the caps says "I accept a
43
- * big task"; it does not make a flash-tier model able to finish one inside
44
- * `implementTimeoutMs`.
45
- */
46
- const LARGE_TASK_REQS = 5;
47
- const LARGE_TASK_PLAN_ITEMS = 8;
48
-
49
- /**
50
- * Capability tiers strong enough for a large task (R3, task 0487). The floor is
51
- * the `review` stage's Layer-1 tier — `reviewer` per `references/roles.md`,
52
- * read via the stage-registry adapter (0538 R4: no tier literal here; roles.md
53
- * is the pointer). Tiers at or above the floor pass. An unreachable roles.md
54
- * degrades to the pre-reconcile band — fail-closed for a safety gate.
55
- */
56
- const CAPABLE_TIERS: ReadonlySet<string> = (() => {
57
- const floor = STAGE_FLOOR_TIER.get('review') ?? 'capable-1';
58
- return new Set(TIER_ORDER.slice(Math.max(0, TIER_ORDER.indexOf(floor))));
59
- })();
60
-
61
38
  // ─── CLI ─────────────────────────────────────────────────────────────────────
62
39
 
63
40
  function usage(): never {
64
41
  console.error(
65
- 'Usage: bun plugins/sp/scripts/task-size-precheck.ts <wbs> [--spur-bin <path>] [--max-reqs <n>] [--max-plan-items <n>] [--executor <name>]',
42
+ 'Usage: bun plugins/sp/scripts/task-size-precheck.ts <wbs> [--spur-bin <path>] [--max-reqs <n>] [--max-plan-items <n>]',
66
43
  );
67
44
  process.exit(1);
68
45
  }
@@ -87,13 +64,14 @@ function parseArgs(argv: string[]): {
87
64
  spurBin: string;
88
65
  maxReqs: number;
89
66
  maxPlanItems: number;
90
- executor: string;
91
67
  } {
92
68
  let spurBin = defaultSpurBin();
93
69
  let wbs = '';
94
- let maxReqs = Number(process.env.MAX_IMPLEMENT_REQS) || 5;
95
- let maxPlanItems = Number(process.env.MAX_IMPLEMENT_PLAN_ITEMS) || 8;
96
- let executor = '';
70
+ // Doubled deterministic ceiling (0723 operator decision): 10 R-items / 16
71
+ // Plan items keep in sync with DEFAULT_TASK_SIZE_LIMITS in
72
+ // packages/app/src/services/task-size-precheck.ts (asserted by test).
73
+ let maxReqs = Number(process.env.MAX_IMPLEMENT_REQS) || 10;
74
+ let maxPlanItems = Number(process.env.MAX_IMPLEMENT_PLAN_ITEMS) || 16;
97
75
 
98
76
  let i = 0;
99
77
  while (i < argv.length) {
@@ -102,13 +80,10 @@ function parseArgs(argv: string[]): {
102
80
  spurBin = argv[i + 1] ?? defaultSpurBin();
103
81
  i += 2;
104
82
  } else if (arg === '--max-reqs') {
105
- maxReqs = Number(argv[i + 1]) || 5;
83
+ maxReqs = Number(argv[i + 1]) || 10;
106
84
  i += 2;
107
85
  } else if (arg === '--max-plan-items') {
108
- maxPlanItems = Number(argv[i + 1]) || 8;
109
- i += 2;
110
- } else if (arg === '--executor') {
111
- executor = argv[i + 1] ?? '';
86
+ maxPlanItems = Number(argv[i + 1]) || 16;
112
87
  i += 2;
113
88
  } else if (!arg.startsWith('--')) {
114
89
  wbs = arg;
@@ -119,7 +94,7 @@ function parseArgs(argv: string[]): {
119
94
  }
120
95
 
121
96
  if (!wbs) usage();
122
- return { wbs, spurBin, maxReqs, maxPlanItems, executor };
97
+ return { wbs, spurBin, maxReqs, maxPlanItems };
123
98
  }
124
99
 
125
100
  /**
@@ -135,29 +110,8 @@ function runSpur(spurBin: string, args: string[]): string {
135
110
  });
136
111
  }
137
112
 
138
- /**
139
- * Capability tier of `executor` per `spur agent doctor <exec> --json`.
140
- * Unknown executor, unreadable doctor output, or an undeclared-and-uninferrable
141
- * tier all read as `standard` — conservative: a false block is one flag away,
142
- * a false pass costs a 30-minute timed-out implement.
143
- */
144
- function resolveCapabilityTier(spurBin: string, executor: string): { tier: string; resolvedName: string } {
145
- try {
146
- const out = runSpur(spurBin, ['agent', 'doctor', executor, '--json']);
147
- const row = JSON.parse(out)?.agents?.[0];
148
- const tier = row?.capabilityTier;
149
- // R1 (0622 F2/F4 residue): `doctor <role>` resolves the role to its cheapest
150
- // eligible executor (`coder` → `omp`); surface the resolved executor name in
151
- // the block message, not the role the caller passed in.
152
- const resolvedName = typeof row?.agent === 'string' && row.agent.length > 0 ? row.agent : executor;
153
- return { tier: typeof tier === 'string' && tier ? tier : 'standard', resolvedName };
154
- } catch {
155
- return { tier: 'standard', resolvedName: executor };
156
- }
157
- }
158
-
159
113
  function main(): void {
160
- const { wbs, spurBin, maxReqs, maxPlanItems, executor } = parseArgs(process.argv.slice(2));
114
+ const { wbs, spurBin, maxReqs, maxPlanItems } = parseArgs(process.argv.slice(2));
161
115
 
162
116
  // Fetch task content via spur
163
117
  let taskContent: string;
@@ -166,7 +120,7 @@ function main(): void {
166
120
  const task = JSON.parse(result);
167
121
  taskContent = task.content ?? task.body ?? '';
168
122
  } catch {
169
- // If spur fails, write FAIL and exit 0 (soft, like doctor)
123
+ // If spur fails, write FAIL and exit 0 the status file carries the verdict
170
124
  const statusDir = join(process.cwd(), '.spur', 'run');
171
125
  if (!existsSync(statusDir)) mkdirSync(statusDir, { recursive: true });
172
126
  writeFileSync(join(statusDir, `${wbs}-precheck-size.status`), 'FAIL\n');
@@ -187,18 +141,6 @@ function main(): void {
187
141
  const planItemCount = planBody.match(new RegExp(CHECKLIST_ITEM_RE.source, 'gm'))?.length ?? 0;
188
142
 
189
143
  const reasons: string[] = [];
190
- // R3 (0487): a large task on a sub-capable executor blocks even when the caller
191
- // raised the caps — the caps are an acceptance of size, not a capability grant.
192
- if (executor && (reqCount > LARGE_TASK_REQS || planItemCount > LARGE_TASK_PLAN_ITEMS)) {
193
- const { tier, resolvedName } = resolveCapabilityTier(spurBin, executor);
194
- if (!CAPABLE_TIERS.has(tier)) {
195
- reasons.push(
196
- `Task size (${reqCount} R-items / ${planItemCount} Plan items) requires a capable executor, ` +
197
- `but ${resolvedName} is tier ${tier}. ` +
198
- `Pass \`--agent <capable>\` or \`--vars '{"implementAgent":"<capable>"}'\`, or split the task.`,
199
- );
200
- }
201
- }
202
144
  if (reqCount > maxReqs) {
203
145
  reasons.push(
204
146
  `Task has ${reqCount} R-items (max ${maxReqs}). ` +
@@ -46,6 +46,9 @@ cross-agent windows, recurrence, trends, or quantitative performance forensics.
46
46
  name the confirmation needed.
47
47
  - State `not available` when compaction or missing output removed evidence. Never reconstruct it from
48
48
  memory or claim a verification that did not run.
49
+ - Derive timing only from timestamps and tool-call records visible in the active session. Use
50
+ non-overlapping stages whose durations sum to elapsed time; render unavailable duration or call
51
+ counts as `n/a` instead of estimating them.
49
52
 
50
53
  ## Triage mode (`--triage`)
51
54
 
@@ -91,6 +94,19 @@ three buckets — never skip triage and start fixing from the raw findings list.
91
94
 
92
95
  State the overall result in one to three sentences, including partial or blocked scope.
93
96
 
97
+ ### Time breakdown
98
+
99
+ Summarize elapsed time and, when supported by evidence, productive work, avoidable setup/recovery,
100
+ and operator wait time. Then render non-overlapping stages:
101
+
102
+ | Stage | Time | Tool calls | Assessment |
103
+ | --- | ---: | ---: | --- |
104
+
105
+ Format durations as `M:SS` below one hour and `H:MM:SS` at one hour or above (`1:44`, not `1m44s`;
106
+ `0:33`, not `33s`). Include a `Total` row when elapsed time is available. Keep operator approval
107
+ waits separate from execution bottlenecks. Use `n/a` for any value not supported by the active
108
+ session evidence.
109
+
94
110
  ### Resolved issues
95
111
 
96
112
  | Issue | Root cause | Resolution | Evidence |
@@ -96,9 +96,9 @@ surface is already resolved and name the trigger / `operator override`. A comman
96
96
  inside that subprocess boundary runs its backing skill in that process; it must not spawn another
97
97
  `spur agent run` for the same trigger. This prevents recursive dispatch.
98
98
 
99
- **Pipeline wrappers (`dev-run`, `dev-runall`)** — the orchestrator is a loop; its *stages* do the
99
+ **Pipeline wrappers (`dev-run`, `dev-runall`, `dev-idea`, `dev-plan`)** — the orchestrator is a loop; its *stages* do the
100
100
  model-bearing work. Interactive omit/`inline` therefore uses the
101
- [inline pipeline driver](inline-pipeline-driver.md): it reads `task-pipeline.yaml`, executes each
101
+ [inline pipeline driver](inline-pipeline-driver.md): it reads the selected pipeline YAML, executes each
102
102
  `agent.run` input through the backing skill in the host session, and preserves every shell action
103
103
  and guard. `auto` or a named executor is merged into per-task `vars.agent` and
104
104
  `vars.implementAgent`, and the workflow's `agent.run` steps run under that subprocess executor (see
@@ -122,9 +122,11 @@ leg for eligible stages. Operator
122
122
  confirmation actions, `pause: true`, and approve/taste/ask decisions stay host-owned. Each inline
123
123
  model stage appends `stage <id> executed inline in session <session-id>` to its run log; a
124
124
  subagent-dispatched stage appends `stage <id> executed via subagent <agent-id> (host session
125
- <session-id>)` instead. `dev-plan` remains a workflow subprocess, as do `dev-run`/`dev-runall` with
126
- `--agent auto` or a name, parallel batches, and every headless `spur workflow run` / `spur agent
127
- run`. `dev-run --mode implement` continues to run its single competency in-session under omitted
125
+ <session-id>)` instead. `dev-idea` and `dev-plan` also drive `idea-pipeline.yaml` in the host session,
126
+ with no native subagent unless the operator explicitly requests delegation. `--agent auto` or a
127
+ name, parallel batches, and every headless `spur workflow run` / `spur agent run` use subprocesses;
128
+ dev-command workflow subprocesses launch `--async` so cancellation owns a process group and only
129
+ `killed: true` means a live run stopped. `dev-run --mode implement` continues to run its single competency in-session under omitted
128
130
  `--agent` or explicit `--agent inline` (identical values, 0687 R1).
129
131
 
130
132
  ### Executor precedence chain (R7)
@@ -137,13 +139,14 @@ resolved in this order; first match wins:
137
139
  2. **`agent.default`** from `.spur/config.yaml` (project layer, then `~/.config/spur/config.yaml`) —
138
140
  `spur workflow run` injects it as the `agent` var when `vars.agent` was not set by the caller.
139
141
  3. **YAML literal `agent:` in the pipeline file** — the last-resort fallback declared in the
140
- workflow YAML (e.g. `agent: "omp"` in `task-pipeline.yaml`). This fires only when no
142
+ workflow YAML. Every shipped pipeline declares `agent: "auto"`, so this rung resolves through
143
+ the role/tier ladder instead of pinning an executor name; it fires only when no
141
144
  `agent.default` is configured anywhere.
142
145
 
143
146
  `--agent auto` tier-resolves an executor (stage `model_policy` → `agent.default` → tier priority)
144
147
  **before** merging, so it enters the chain at step 1 already resolved to a concrete name.
145
148
  On a headless workflow surface, explicit `--agent inline` substitutes tier resolution with a warning
146
- (0687 R3) instead of rejecting — it resolves exactly like an omitted flag. Interactive task wrappers
149
+ (0687 R3) instead of rejecting — it resolves exactly like an omitted flag. Interactive pipeline wrappers
147
150
  consume both inline resolutions identically (0508 eligibility as generalized by 0687 R2) before this
148
151
  chain and use the host driver. Omitting the flag on a headless surface forwards nothing, so the
149
152
  spawned step resolves to `agent.default` (step 2) or the YAML literal (step 3).
@@ -193,18 +196,17 @@ explicit process boundary and retain their existing resolution, output, timeout,
193
196
  contracts. The interactive task wrapper does not change the YAML or engine; it reads the YAML as
194
197
  SSOT and interprets the actions in-session before any workflow subprocess exists. It records inline
195
198
  provenance without fabricating an `AgentRunTracedResult`.
196
- `spur agent run` itself resolves omitted `--agent` to `agent.default`, rejects explicit `--agent
197
- inline` with the stable special error (a headless surface cannot host a session), and `--agent
198
- auto` tier-resolves the unified `--agent` selector on the dev command surface does not change the
199
- CLI's resolution.
199
+ `spur agent run` resolves omitted or explicit `inline` through tier substitution on its headless
200
+ surface and emits one warning naming the concrete executor; `auto` tier-resolves without the inline
201
+ warning. Interactive dev wrappers consume inline before this boundary, so they never spawn it.
200
202
 
201
203
  ### Inline trade-off
202
204
 
203
205
  Inline avoids process startup and preserves the host session's context and tools. Relative to
204
206
  subprocess dispatch it provides **no isolated workspace**, **no per-stage subprocess action
205
207
  record**, **no independent timeout or abort boundary**, and **no tier-selected executor**: the
206
- executor is the current coding agent. Interactive task pipelines retain a run log, run-link, and
207
- session provenance through the inline driver. If process isolation or an independently killable
208
+ executor is the current coding agent. Interactive pipelines retain a run log and session provenance
209
+ through the inline driver; task pipelines additionally record a task run-link. If process isolation or an independently killable
208
210
  stage is required, select the subprocess path (`--agent auto` or `--agent <name>`).
209
211
 
210
212
  ## Every write is CLI-gated
@@ -611,22 +613,30 @@ CLI-gated corpus artifact. The `wrapup-pipeline.yaml` `learning-capture` step wr
611
613
  ## Session Checkpoint Convention
612
614
 
613
615
  Long-running pipelines write resumable checkpoints to `.spur/memory/sessions/` so an interrupted
614
- run can be resumed. The convention is documented here; the actual checkpoint write/read actions
615
- in pipeline YAMLs are added in Phase 4 (task 0171).
616
+ run can be resumed. The canonical frontmatter schema below is parsed by
617
+ `packages/app/src/workflow/checkpoint-contract.ts` (task 0711): a checkpoint that does not match
618
+ it is ignored by routing/cleanup (safe fallthrough) and never reclaimed.
616
619
 
617
- **Format:** Markdown file with YAML frontmatter:
620
+ **Format:** Markdown file with YAML frontmatter (canonical field set, task 0711):
618
621
 
619
622
  ```yaml
620
623
  ---
624
+ schema_version: 1
621
625
  session_id: "2026-07-01-0167"
622
626
  workflow: "task-pipeline"
623
627
  run_id: "wf_..."
624
628
  task_wbs: "0167"
625
629
  feature_id: "I"
626
630
  phase: "verify"
631
+ status: "running"
627
632
  last_gate: "review-approved"
628
- timestamp: "2026-07-01T18:30:00Z"
633
+ source_commit: "<full 40-hex HEAD at write time>"
634
+ digest: "sha256:..."
635
+ generated_at: "2026-07-01T18:30:00Z"
636
+ updated_at: "2026-07-01T18:30:00Z"
629
637
  next_action: "run verification"
638
+ artifacts:
639
+ - .spur/run/0167-verdict.json
630
640
  ---
631
641
 
632
642
  ## Session Notes
@@ -634,6 +644,17 @@ next_action: "run verification"
634
644
  <free-form markdown: what was done, what's pending, any blockers>
635
645
  ```
636
646
 
647
+ Field semantics (enforced by `parseCheckpointMetadata` / `checkpointStaleness`):
648
+
649
+ - `schema_version` must be `1`; any other value → ignored.
650
+ - `status` is one of `running|pending|approved|done|failed|cancelled|skipped`. A terminal status
651
+ (`done|failed|cancelled|skipped`) marks the checkpoint terminal — never resumed, only cleaned
652
+ up once retention expires.
653
+ - `source_commit` pins repository HEAD at write time; drift staleness is reported, not hidden.
654
+ - `artifacts` lists referenced run files; a missing artifact makes the checkpoint stale.
655
+ - Writers: write after every HITL gate decision, phase transition, and terminal state; overwrite
656
+ the same file on resume (`session_id` = `<date>-<wbs-or-feature>`).
657
+
637
658
  **Write checkpoints after:**
638
659
 
639
660
  - Every HITL gate decision (approved/rejected/deferred).
@@ -652,6 +673,10 @@ next_action: "run verification"
652
673
  - **Not CLI-gated.** Checkpoint files are written directly by the pipeline's checkpoint action
653
674
  (a `shell` step that writes to `.spur/memory/sessions/<session-id>.md`). They do not go through
654
675
  `spur task update`.
676
+ - **Canonical schema.** The frontmatter above is the contract (task 0711 R1). Non-canonical or
677
+ malformed checkpoints are ignored by the router and kept (never silently deleted) by cleanup.
678
+ - **Retention.** Terminal checkpoints older than `workflowLogRetentionDays` with no active run are
679
+ reclaimed by `spur workflow clean` (task 0711 R5–R8); non-terminal checkpoints are always kept.
655
680
  - **Not a validated corpus.** Checkpoints are working memory. They are overwritten when a session
656
681
  resumes and re-checkpoints. They are NOT authoritative task state — the task file is.
657
682
  - **One file per session.** The `session_id` is `<date>-<wbs-or-feature>`. A resumed session
@@ -74,7 +74,7 @@ each would be scope creep for one-liner procedures.
74
74
  | 4 | run | `dev-run` | `Skill()` | `sp:spur-dev` (`run` / `implement`) | `<wbs> [--mode <full\|implement>] [--agent <inline\|auto\|name>] [--auto] [--next] [--wrap] [--continue] [--worktree [<name>]]` |
75
75
  | 5 | refine | `dev-refine` | `Skill()` | `sp:spur-dev` (`refine`) | `<wbs> [--focus <mode>] [--description <text>] [--depth <standard\|ready>] [--agent <inline\|auto\|name>] [--auto] [--next]` |
76
76
  | 5a | refineall | `dev-refineall` | `Skill()` | `sp:spur-dev` (`refineall`) | `--feature <id> \| --tasks <selector> [--focus <mode>] [--description <text>] [--depth <standard\|ready>] [--agent <inline\|auto\|name>] [--auto] [--keep-going] [--status <s>] [--json] [--worktree [<name>]]` |
77
- | 6 | plan | `dev-plan` | `Skill()` | `sp:spur-dev` (`plan`) | `"<description>" [--feature <id>] [--parent <feature-id>] [--agent <inline\|auto\|name>] [--skip-design] [--auto] [--approve-taste]` |
77
+ | 6 | plan | `dev-plan` | `Skill()` | inline driver (`idea-pipeline`) or async workflow | `"<description>" [--feature <id>] [--parent <feature-id>] [--agent <inline\|auto\|name>] [--skip-design] [--auto] [--approve-taste]` |
78
78
  | 7 | docs | _(no thin wrapper)_ | `Skill()` | `sp:doc-evolve` | `"<change description>"` |
79
79
  | 8 | changelog | `dev-changelog` | `inline` | git log + conventional-commit grouping | `[--since <ref>] [--until <ref>] [--version <ver>]` |
80
80
  | 9 | gitmsg | `dev-gitmsg` | `inline` | bounded diff capture → concern grouping → conventional commit | `[--commit] [--squash] [--all] [--scope <path>]` |
@@ -85,7 +85,7 @@ each would be scope creep for one-liner procedures.
85
85
  | 13a | parallel | `dev-parallel` | `Skill()` | `sp:parallel-execution` | `--tasks <selector> [--feature <id>] [--mode <fan-out\|review-panel\|investigation>] [--agent <inline\|auto\|name>] [--json]` |
86
86
  | 14 | wrap | `dev-wrap` | `Skill()` | `spur workflow run` (wrapup-pipeline) | `<wbs> [--agent <inline\|auto\|name>] [--auto] [--merge] [--dry-run]` |
87
87
  | 15 | wrapall | `dev-wrapall` | `Skill()` | `spur workflow run` (wrapup-pipeline) | `[--since <iso>] [--feature <id>] [--status <s>] [--agent <inline\|auto\|name>] [--auto] [--merge] [--dry-run]` |
88
- | 16 | idea | `dev-idea` | `Skill()` | `spur workflow run` (idea-pipeline) | `"<idea>" [--auto] [--skip-design] [--approve-taste] [--agent <inline\|auto\|name>]` |
88
+ | 16 | idea | `dev-idea` | `Skill()` | inline driver (`idea-pipeline`) or async workflow | `"<idea>" [--auto] [--skip-design] [--approve-taste] [--agent <inline\|auto\|name>]` |
89
89
 
90
90
  ## Skill-backed operations
91
91
 
@@ -255,7 +255,7 @@ must not be changed without updating the backing skill.
255
255
  ### 6. plan
256
256
 
257
257
  - **Purpose:** Plan a feature from a description — intake → feature create → AC generation → feature check gate → decomposition → batch-create (with **Design by default**).
258
- - **Inputs:** `"<description>"` (required). `--feature <id>` links to an existing feature. `--parent <feature-id>` nests under a parent. The planning pipeline's `agent.run` stages always dispatch a subprocess; `--agent <inline|auto|name>` selector accepted — `inline` substitutes tier resolution with a warning on this headless surface (task 0687) (see [SSOT](cross-cutting.md#inline-default-execution-surface)). **Design package flags (unified with `/sp:dev-idea`):**
258
+ - **Inputs:** `"<description>"` (required). `--feature <id>` links to an existing feature. `--parent <feature-id>` nests under a parent. Omitted/`inline` drives `idea-pipeline.yaml` in the host session; `auto` or a name launches the async workflow worker. **Design package flags (unified with `/sp:dev-idea`):**
259
259
  - **Default:** author task `design` on every batch item + feature satellite when the seam heuristic fires (**ties lean design**). There is **no** `--design` force flag.
260
260
  - `--skip-design` — skip feature satellite **and** omit task `design` fields (scaffold only; refine fills later). Sole design opt-out.
261
261
  - `--approve-taste` — with `--auto`, pre-clear design-approval taste pause when that gate is used (`design_approved=true`). Alias: `--design-approved`.
@@ -340,9 +340,9 @@ must not be changed without updating the backing skill.
340
340
  - `--skip-design` — design package off (system-design + task Design).
341
341
  - `--approve-taste` — with `--auto`, skip **all** remaining taste pauses this run (idea-eval + design-approval). Sets `idea_approved=true` and `design_approved=true`.
342
342
  Aliases (prefer `--approve-taste`): `--idea-approved` → `idea_approved`; `--design-approved` → `design_approved`. There is **no** `--design` force flag.
343
- - **Backing:** `spur workflow run idea-pipeline.yaml` direct workflow invocation.
344
- - **Behavior:** Builds vars from the table above and invokes the idea pipeline. Flow: discovery → **idea-eval** (taste; reject → cancelled) → feature-create → ac-generate → feature-check → system-design (conditional) → design-approval (taste) → decompose → batch-create → handoff. STOPS at handoff — no task execution, no pipeline nesting.
345
- - **Delegation:** Direct `spur workflow run idea-pipeline.yaml` (command maps flags vars, then `Bash`).
343
+ - **Backing:** `idea-pipeline.yaml` through the inline driver for omitted/`inline`, or `spur workflow run idea-pipeline.yaml --async` for `auto`/name.
344
+ - **Behavior:** Builds vars from the table above and drives the idea pipeline. Flow: discovery → **idea-eval** (taste; reject → cancelled) → feature-create → ac-generate → feature-check → system-design (conditional) → design-approval (taste) → decompose → batch-create → handoff. STOPS at handoff — no task execution, no pipeline nesting. Headless runs use one `trace --follow`; cancellation is reported stopped only when `workflow cancel --json` returns `killed: true`.
345
+ - **Delegation:** Host-session inline driver by default; explicit executor selection uses the async workflow worker.
346
346
  - **Idea-evaluation gate:** After discovery, operator reviews `.spur/run/idea-eval-report.md` ([idea-evaluation.md](idea-evaluation.md)). Approve continues; reject/cancel → no feature. Under `--auto`, still pauses unless taste pre-cleared (`--approve-taste` / alias). Enhanced idea is a sidecar — `vars.idea` is not overwritten.
347
347
  - **Design package (`--skip-design` only):**
348
348
 
@@ -421,6 +421,23 @@ written for a batch with nothing to run. The early-exit report carries zero per-
421
421
  terminal action runs. A contract test pins this
422
422
  (`plugins/sp/tests/dogfood-testing/execution-batch-contract.test.ts`).
423
423
 
424
+ **Evidence persistence (worktree batches — task 0720 R3).** A worktree batch's Step 5 report and
425
+ verdict artifacts live in the worktree's own `.spur/run/` while the batch runs — exactly the tree
426
+ create-mode WT-4 deletes. Before any WT-4 removal, persist them into the **invoking** tree, which
427
+ survives removal:
428
+
429
+ - Write the emitted batch report to `.spur/run/worktree-<marker-id>-batch-report.md`.
430
+ - Copy each attempted task's `.spur/run/<wbs>-verdict.json` from the worktree to
431
+ `.spur/run/worktree-<marker-id>-verdicts/<wbs>-verdict.json`.
432
+ - Make the report's per-task verdict references use those persisted invoking-tree paths, not the
433
+ worktree-local paths that removal deletes.
434
+
435
+ Evidence persistence precedes destructive cleanup: a persistence failure (unreadable verdict file,
436
+ disk-full, missing directory) routes to **WT-5** — the worktree and branch are retained so a green
437
+ batch can never destroy its own evidence. Reuse mode retains its operator-owned tree but still
438
+ persists the Step 5 report under the invoking tree; the reused tree's `.spur/run/` remains the live
439
+ copy while that tree lives on.
440
+
424
441
  ## Worktree isolation (`--worktree [<name>]`)
425
442
 
426
443
  When a batch command (`dev-runall`, `dev-refineall`, `dev-verifyall`) is invoked with
@@ -671,14 +688,52 @@ git checkout "$BASE_REF"
671
688
  [ "$(git rev-list --count "$BASE_SHA..$BRANCH")" -gt 0 ] \
672
689
  || { echo "halt: branch carries no commits - nothing to merge" >&2; false; } # -> WT-5
673
690
  git merge --ff-only "$BRANCH" # FF-only: never rebase, merge-commit, or resolve conflicts
674
- # if FF succeeded:
691
+ # if FF succeeded — WT-4a evidence persistence (Step 5, task 0720 R3) runs FIRST:
692
+ # persist the batch report + verdict artifacts into the invoking tree's .spur/run/
693
+ # before anything below touches the worktree. Persistence failure routes to WT-5.
694
+ #
695
+ # WT-4b — bounded CWD-holder cleanup (task 0720 R1). Resolve the EXACT absolute
696
+ # worktree path; a relative path or a stale entry matches the wrong processes.
697
+ WT_PATH="$(cd "../<worktree-dir>" && pwd)"
698
+ # Holders = processes with any open fd under the worktree tree (lsof +D walks the
699
+ # tree; CWD holders are the common case but +D also catches open-file holders —
700
+ # over-match errs toward removal success; a plain -t <dir> matches only the
701
+ # directory itself). Orphaned `serve` proof daemons (PPID 1) are exactly this
702
+ # class: they defeat `git worktree remove` (ENOTEMPTY), defeat rm -rf, while
703
+ # `git worktree prune` still deregisters the tree. Note +D is a full-tree walk,
704
+ # so the wait loop below bounds ITERATIONS (6 × 1s ticks + walk cost), not
705
+ # wall-clock.
706
+ HOLDERS="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
707
+ if [ -n "$HOLDERS" ]; then
708
+ kill -TERM $HOLDERS 2>/dev/null # 1) TERM first, all holders (unquoted — word-split PID list)
709
+ for _ in 1 2 3 4 5 6; do # 2) bounded wait: 6 × 1s ticks
710
+ sleep 1
711
+ [ -z "$(lsof -t +D "$WT_PATH" 2>/dev/null)" ] && break
712
+ done
713
+ SURVIVORS="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
714
+ if [ -n "$SURVIVORS" ]; then
715
+ kill -KILL $SURVIVORS 2>/dev/null # 3) KILL only the survivors (unquoted — one arg per PID)
716
+ sleep 1
717
+ fi
718
+ fi
719
+ # 4) Re-query: only an EMPTY holder set may proceed to remove/prune/branch delete.
720
+ FINAL="$(lsof -t +D "$WT_PATH" 2>/dev/null | sort -u)"
721
+ if [ -n "$FINAL" ]; then
722
+ PORTS="$(lsof -nP -a -p "$(echo "$FINAL" | paste -sd, -)" -iTCP -sTCP:LISTEN 2>/dev/null \
723
+ | awk 'NR>1 {print $9}' | sort -u | paste -sd' ' -)"
724
+ echo "halt: worktree still held by PID(s): $FINAL ${PORTS:+listening: $PORTS}" >&2
725
+ exit 1 # -> WT-5: retain worktree + branch,
726
+ fi # NO prune/remove/branch delete
675
727
  git worktree remove "../<worktree-dir>"
676
728
  git branch -d "$BRANCH"
677
729
  # update marker: status = "merged"
678
730
  ```
679
731
 
680
- On the zero-commit guard firing, fall through to **WT-5** with the halt cause *"branch carries no
681
- commits nothing to merge"*: the worktree and branch are retained, never removed (task 0701 R1).
732
+ On either guard firing — zero-commit branch (task 0701 R1), or surviving CWD holders (task 0720 R1)
733
+ fall through to **WT-5**: the worktree and branch are retained, never removed. While any holder
734
+ remains, do **not** run `git worktree prune`, `git worktree remove`, or branch deletion. The holder
735
+ halt report names every surviving PID; the listening port is best-effort — a CWD holder may own no
736
+ socket, and `lsof` port discovery failing must not hide the PIDs.
682
737
 
683
738
  #### Reuse mode — merge, retain
684
739
 
@@ -711,14 +766,28 @@ risk losing work); WT-5 retains the worktree and branch whenever FF is impossibl
711
766
  Reuse mode is **narrower** than the carve-out (it merges but does not delete the branch), so the
712
767
  carve-out text needs no widening.
713
768
 
714
- **Lifecycle-DB disposition (task 0701 R2d).** The worktree has its own `.spur` lifecycle DB, and
715
- WT-4/WT-5 remove or retain that tree — the DB state does **not** travel with the merge. The
716
- **committed task file is authoritative**: after a green merge the branch's task files read
717
- `done`/`testing` while the invoking tree's DB still reports the pre-batch statuses. Re-sync
718
- explicitly by replaying the recorded terminal transitions in the invoking tree (`spur task update
719
- <wbs> <status>` per task, then `spur task record <wbs>`), or treat the batch report's per-task
720
- table as the source of truth. This is a deliberate choice over auto-migrating DB state: the DB is
721
- per-tree by design and the committed corpus files are the durable record.
769
+ **Lifecycle-DB disposition (task 0701 R2d, amended by 0720).** The worktree has its own `.spur`
770
+ lifecycle DB, and WT-4/WT-5 remove or retain that tree — the DB state does **not** travel with the
771
+ merge. One contract, no alternatives:
772
+
773
+ - **Committed task files own lifecycle state.** The **committed task file is authoritative**: after
774
+ a green merge the branch's task files already read `done`/`testing` in the invoking tree; no
775
+ `spur task update` or `spur task record` replay runs post-merge. Replay is not "one of two
776
+ options" it is removed: it writes `updated_at`-only churn and can never restore worktree-only
777
+ DB rows.
778
+ - **The persisted invoking-tree artifacts own evidence.** The Step 5 batch report at
779
+ `.spur/run/worktree-<marker-id>-batch-report.md` and the copied verdict JSONs under
780
+ `.spur/run/worktree-<marker-id>-verdicts/` (written before WT-4 removal) are the batch/verdict
781
+ record.
782
+ - **Per-worktree lifecycle DB rows intentionally do not travel.** No `task_run_links` import, no
783
+ cross-database provenance synthesis — the DB is per-tree by design.
784
+ - **No timestamp-only corpus churn.** Post-merge the invoking tree's DB statuses may read stale
785
+ relative to the committed files; that divergence is accepted, not repaired. Do not run
786
+ `task update`/`task record` to "catch up" the DB, and do not repair churn with
787
+ `git checkout -- docs/tasks*/`.
788
+
789
+ This is a deliberate choice over auto-migrating DB state: the committed corpus files are the durable
790
+ record and the persisted run artifacts are the evidence record.
722
791
 
723
792
  ### WT-5 — Failure path: retain and report (R5)
724
793
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: inline-pipeline-driver
3
- description: "Interactive host-session interpreter for task-pipeline.yaml: execute the existing FSM without a workflow agent subprocess while preserving actions, guards, artifacts, and provenance."
3
+ description: "Interactive host-session interpreter for Spur state-machine pipelines: execute the existing FSM without a workflow agent subprocess while preserving actions, guards, artifacts, and provenance."
4
4
  see_also:
5
5
  - spur-dev
6
6
  - execution-workflow
@@ -9,12 +9,13 @@ see_also:
9
9
 
10
10
  # Inline Pipeline Driver
11
11
 
12
- This driver is the interactive control-inversion path granted by ADR-047. It applies only when an
13
- interactive `/sp:dev-run --mode full` or sequential `/sp:dev-runall` invocation omits `--agent` (now
14
- the inline default, 0687 R1) or passes `--agent inline`. A named executor, `--agent auto`, parallel
15
- batch mode, `spur workflow run`, and `spur agent run` keep the existing subprocess path.
12
+ This driver is the interactive control-inversion path granted by ADR-047. It applies when an
13
+ interactive `/sp:dev-run --mode full`, sequential `/sp:dev-runall`, `/sp:dev-idea`, or
14
+ `/sp:dev-plan` invocation omits `--agent` or passes `--agent inline`. A named executor,
15
+ `--agent auto`, parallel batch mode, `spur workflow run`, and `spur agent run` keep the existing
16
+ subprocess path.
16
17
 
17
- The project runtime definition — `task-pipeline.yaml`, resolved through the two-tier
18
+ The selected project runtime definition — `task-pipeline.yaml` or `idea-pipeline.yaml`, resolved through the two-tier
18
19
  project→bundled model (task 0648/0650, never an unbundled runtime path) — remains the sole
19
20
  FSM definition. The driver MUST read that file
20
21
  at invocation time. It must not copy the state list, actions, guards, or transition order into a
@@ -22,9 +23,9 @@ command, skill, script, or second workflow.
22
23
 
23
24
  ## Run setup
24
25
 
25
- 1. Resolve `<wbs>`, `--auto`, and any explicit `--vars`; read the YAML and overlay its `vars` defaults
26
- with those invocation values. An explicit `vars.agent` / `vars.implementAgent` is an executor
27
- selection and therefore chooses the subprocess workflow path.
26
+ 1. Resolve the command inputs, `--auto`, and any explicit `--vars`; read the selected YAML and overlay
27
+ its `vars` defaults with those invocation values. An explicit non-inline executor selection
28
+ chooses the subprocess workflow path.
28
29
  2. Allocate a collision-resistant inline run id (`uuidgen`, with a timestamp/pid fallback), create
29
30
  `.spur/run/`, and use `.spur/run/<run-id>.log` as the run log.
30
31
  3. Resolve the host session id from `.spur/context/.session.json`, accepting the normalized hook key
@@ -42,13 +43,14 @@ command, skill, script, or second workflow.
42
43
  never per action.
43
44
  - **Source of truth** = the CLI projection for layer 1; the YAML parsed in step 1 for layer 2.
44
45
  Never hand-copy or hand-derive the state list into the driver, a command, a skill, or a script.
45
- 5. Record lifecycle provenance before entering the FSM:
46
+ 5. For task execution only, record lifecycle provenance before entering the FSM:
46
47
 
47
48
  ```bash
48
49
  spur task run-link <wbs> --source inline-full --run-id <run-id> --json
49
50
  ```
50
51
 
51
- This is required for the normal `testing → done` provenance guard. It is not a guard bypass.
52
+ This is required for the normal `testing → done` provenance guard. Planning pipelines have no
53
+ task lifecycle link and skip this task-specific action.
52
54
 
53
55
  ## YAML interpreter
54
56
 
@@ -61,12 +63,15 @@ Action semantics come from the YAML and the workflow action contract:
61
63
  - `shell` — run the expanded command in the project working tree with resolved vars exported as
62
64
  environment variables. A non-zero result follows the action's existing failure policy.
63
65
  - `note` — append the expanded message to the inline run log.
66
+ - `doctor.probe` — run the declared Spur doctor once, persist its status file, and apply any
67
+ `setVars` result (including a resolved executor) before the next action or state.
64
68
  - `file.read.into-var` — read the declared file into the declared run variable before subsequent
65
69
  actions/guards.
66
70
  - `hitl.confirm` — under `profile=auto`, follow the YAML's auto-skip transition. Otherwise pause,
67
71
  surface the prompt, and resume from the same state with the operator's answer.
68
- - `agent.run` — execute the action's slash command, native-subagent-first (task 0508). Do not call
69
- `spur agent run` and do not re-enter `/sp:dev-run --mode full`. Preserve the YAML options: capture
72
+ - `agent.run` — execute the action's input in the host session. Task execution may use the native
73
+ subagent eligibility below; idea/plan never dispatch a native subagent unless the operator
74
+ explicitly requested delegation. Do not call `spur agent run` or re-enter a full pipeline. Preserve the YAML options: capture
70
75
  `answerFile`; assert `expectFile`; enforce `requireDiff` against a pre-action git snapshot,
71
76
  including the task-scope guard; honor declared error policy. `timeoutMs` is recorded as not
72
77
  applicable because the host session has no independent kill boundary.