@gobing-ai/spur 0.3.89 → 0.3.91

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/pipeline-budgets.json +7 -0
  3. package/config/rules/boundary/sp-plugin-standalone.yaml +37 -0
  4. package/config/workflows/decision-routing-example.yaml +134 -0
  5. package/config/workflows/idea-pipeline.yaml +8 -0
  6. package/config/workflows/task-pipeline.yaml +2 -0
  7. package/config/workflows/wayfinder-resolution.yaml +2 -0
  8. package/config/workflows/wrapup-pipeline.yaml +2 -0
  9. package/package.json +9 -9
  10. package/plugins/sp/README.md +1 -0
  11. package/plugins/sp/agents/expert-spur.md +2 -2
  12. package/plugins/sp/lib/idea-handoff.generated.mjs +157 -150
  13. package/plugins/sp/plugin.json +1 -1
  14. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -0
  15. package/plugins/sp/scripts/script-contract-check.ts +58 -0
  16. package/plugins/sp/scripts/surface-drift-inventory.ts +71 -6
  17. package/plugins/sp/scripts/validate-flag-contracts.ts +3 -3
  18. package/plugins/sp/skills/spur-cli/references/agent.md +11 -2
  19. package/plugins/sp/skills/spur-cli/references/self.md +5 -1
  20. package/plugins/sp/skills/spur-cli/references/workflows.md +11 -4
  21. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +21 -2
  22. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +13 -4
  23. package/plugins/sp/skills/spur-dev/references/glossary.md +9 -1
  24. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +9 -1
  25. package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
  26. package/spur.js +3100 -959
  27. package/web/_astro/{BoardApp.CDUcHlTJ.js → BoardApp.BEDWpzsr.js} +82 -82
  28. package/web/_astro/BoardApp.DQG2xfEz.js +1 -0
  29. package/web/_astro/{TaskDetail.DwTmbQp5.js → TaskDetail.CXGltuT_.js} +1 -1
  30. package/web/_astro/{arc.BzF71EFI.js → arc.C0rrflm_.js} +1 -1
  31. package/web/_astro/{architectureDiagram-3BPJPVTR.jvdDahWM.js → architectureDiagram-3BPJPVTR.DJ8DHkWE.js} +1 -1
  32. package/web/_astro/{blockDiagram-GPEHLZMM.zSg4AmFD.js → blockDiagram-GPEHLZMM.D8DHK3Jl.js} +1 -1
  33. package/web/_astro/{c4Diagram-AAUBKEIU.BkUIUQWH.js → c4Diagram-AAUBKEIU.BugQbX9u.js} +1 -1
  34. package/web/_astro/channel.SRrg1P-w.js +1 -0
  35. package/web/_astro/{chunk-2J33WTMH.DvfQ_f50.js → chunk-2J33WTMH.Mv26KlVn.js} +1 -1
  36. package/web/_astro/{chunk-4BX2VUAB.DuI4gQqX.js → chunk-4BX2VUAB.CD51JoT_.js} +1 -1
  37. package/web/_astro/{chunk-55IACEB6.D3BWBOpF.js → chunk-55IACEB6.D_PFaIEe.js} +1 -1
  38. package/web/_astro/{chunk-727SXJPM.3QSi0a9M.js → chunk-727SXJPM.DemMW1ao.js} +1 -1
  39. package/web/_astro/{chunk-AQP2D5EJ.xazCQrAF.js → chunk-AQP2D5EJ.Iu2V5-ex.js} +1 -1
  40. package/web/_astro/{chunk-FMBD7UC4.B2g6u4rA.js → chunk-FMBD7UC4.MsNgSP-E.js} +1 -1
  41. package/web/_astro/{chunk-ND2GUHAM.wWwWs99t.js → chunk-ND2GUHAM.DfbRaAlm.js} +1 -1
  42. package/web/_astro/{chunk-QZHKN3VN.BD5g3qa9.js → chunk-QZHKN3VN.BbJAQ4h-.js} +1 -1
  43. package/web/_astro/{classDiagram-4FO5ZUOK.C7CzCdsX.js → classDiagram-4FO5ZUOK.CPurtiC2.js} +1 -1
  44. package/web/_astro/{classDiagram-v2-Q7XG4LA2.C7CzCdsX.js → classDiagram-v2-Q7XG4LA2.CPurtiC2.js} +1 -1
  45. package/web/_astro/{cose-bilkent-S5V4N54A.Xyiau0gw.js → cose-bilkent-S5V4N54A.CKKdx1bM.js} +1 -1
  46. package/web/_astro/{cynefin-OW5HDTMX.BeC5MWas.js → cynefin-OW5HDTMX.CoKMTg-R.js} +1 -1
  47. package/web/_astro/{dagre-BM42HDAG.yZbMN9vc.js → dagre-BM42HDAG.D4h4_k56.js} +1 -1
  48. package/web/_astro/{diagram-2AECGRRQ.Cmo2zQM-.js → diagram-2AECGRRQ.DJ0h9zgw.js} +1 -1
  49. package/web/_astro/{diagram-5GNKFQAL.D033eSVi.js → diagram-5GNKFQAL.DvPk1jYd.js} +1 -1
  50. package/web/_astro/{diagram-KO2AKTUF.CR6k3Y3G.js → diagram-KO2AKTUF.BFoCkiCr.js} +1 -1
  51. package/web/_astro/{diagram-LMA3HP47.x7mwu8jz.js → diagram-LMA3HP47.exHn9OVx.js} +1 -1
  52. package/web/_astro/{diagram-OG6HWLK6.D8aTTvUr.js → diagram-OG6HWLK6.CeqO34nN.js} +1 -1
  53. package/web/_astro/{erDiagram-TEJ5UH35.BoBqcKXQ.js → erDiagram-TEJ5UH35.D_v7HqxR.js} +1 -1
  54. package/web/_astro/{flowDiagram-I6XJVG4X.D3mTQdrU.js → flowDiagram-I6XJVG4X.EUmrpbwh.js} +1 -1
  55. package/web/_astro/{ganttDiagram-6RSMTGT7.H-cqgIh-.js → ganttDiagram-6RSMTGT7.BOCF5lII.js} +1 -1
  56. package/web/_astro/{gitGraphDiagram-PVQCEYII.B6s9zbfC.js → gitGraphDiagram-PVQCEYII.Di7otYZD.js} +1 -1
  57. package/web/_astro/index.Bx6GY4RH.css +1 -0
  58. package/web/_astro/{infoDiagram-5YYISTIA.BzgCoV6P.js → infoDiagram-5YYISTIA.TcBkCAJk.js} +1 -1
  59. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BZzVhy1-.js → ishikawaDiagram-YF4QCWOH.D-2y4M0c.js} +1 -1
  60. package/web/_astro/{journeyDiagram-JHISSGLW.BV3195Py.js → journeyDiagram-JHISSGLW.DPbJI_n2.js} +1 -1
  61. package/web/_astro/{kanban-definition-UN3LZRKU.BjRd2DWz.js → kanban-definition-UN3LZRKU.EFxhQ9Fj.js} +1 -1
  62. package/web/_astro/{linear.BILTgS5N.js → linear.DSAsQLzs.js} +1 -1
  63. package/web/_astro/{mermaid.core.DBy_WKeW.js → mermaid.core.kAZjgJHG.js} +4 -4
  64. package/web/_astro/{mindmap-definition-RKZ34NQL.BiEjaI4-.js → mindmap-definition-RKZ34NQL.CJY1N_7V.js} +1 -1
  65. package/web/_astro/{pieDiagram-4H26LBE5.i_8V5pIn.js → pieDiagram-4H26LBE5.567ZNoL2.js} +1 -1
  66. package/web/_astro/{quadrantDiagram-W4KKPZXB.BWaW3MHn.js → quadrantDiagram-W4KKPZXB.mqfz9-MY.js} +1 -1
  67. package/web/_astro/{requirementDiagram-4Y6WPE33.CzddBbtg.js → requirementDiagram-4Y6WPE33.Bv1Gv9In.js} +1 -1
  68. package/web/_astro/{sankeyDiagram-5OEKKPKP.X2ww0e-D.js → sankeyDiagram-5OEKKPKP.B6Gs4X4r.js} +1 -1
  69. package/web/_astro/{sequenceDiagram-3UESZ5HK.DSA4kTcc.js → sequenceDiagram-3UESZ5HK.BhYj4v-m.js} +1 -1
  70. package/web/_astro/{stateDiagram-AJRCARHV.D0DtFSpR.js → stateDiagram-AJRCARHV.BPbBnkpw.js} +1 -1
  71. package/web/_astro/{stateDiagram-v2-BHNVJYJU.BfQq0zQv.js → stateDiagram-v2-BHNVJYJU.C4squMNK.js} +1 -1
  72. package/web/_astro/{timeline-definition-PNZ67QCA.Dmlrgi1m.js → timeline-definition-PNZ67QCA.C_SwIHgl.js} +1 -1
  73. package/web/_astro/{vennDiagram-CIIHVFJN.D5mpl00Z.js → vennDiagram-CIIHVFJN.Bz4NZGpQ.js} +1 -1
  74. package/web/_astro/{wardleyDiagram-YWT4CUSO.Df4BdzO4.js → wardleyDiagram-YWT4CUSO.CozMVZ3i.js} +1 -1
  75. package/web/_astro/{xychartDiagram-2RQKCTM6.DiTRreKN.js → xychartDiagram-2RQKCTM6.BwMGBwjB.js} +1 -1
  76. package/web/index.html +2 -2
  77. package/web/_astro/BoardApp.CaCGU_uX.js +0 -1
  78. package/web/_astro/channel.SSVY0JPQ.js +0 -1
  79. package/web/_astro/index.CcU5weKX.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sp",
3
- "version": "0.3.89",
3
+ "version": "0.3.91",
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"]
@@ -44,6 +44,7 @@ const DOCUMENTED = {
44
44
  'doctor.probe',
45
45
  'file.read.into-var',
46
46
  'hitl.confirm',
47
+ 'hitl.select',
47
48
  'agent.run',
48
49
  'proof.fingerprint',
49
50
  'run.artifact',
@@ -178,6 +178,47 @@ export function scanShippedSurfaces(pluginDir: string): Array<{ file: string; li
178
178
  return matches;
179
179
  }
180
180
 
181
+ // Bare-specifier value imports in bundled surfaces break `superskill install`, which
182
+ // bundles hooks/scripts on targets with no node_modules (task 0669; releases
183
+ // 0.3.81–0.3.88 failed with "Bundle failed" when scripts imported @gobing-ai/ts-utils).
184
+ // Type-only imports are erased before bundling, so they stay legal. Line-based
185
+ // statement walk (import/export statements are column 0 under biome) — lookahead
186
+ // regexes misbehave in Bun 1.3.x (JSC) when grouped with lazy quantifiers.
187
+ function findGobingAiValueImports(dir: string): { file: string; line: number }[] {
188
+ const hits: { file: string; line: number }[] = [];
189
+ let entries: string[];
190
+ try {
191
+ entries = readdirSync(dir);
192
+ } catch {
193
+ return hits;
194
+ }
195
+ for (const entry of entries) {
196
+ const full = join(dir, entry);
197
+ let st: ReturnType<typeof statSync>;
198
+ try {
199
+ st = statSync(full);
200
+ } catch {
201
+ continue;
202
+ }
203
+ if (st.isDirectory()) {
204
+ hits.push(...findGobingAiValueImports(full));
205
+ } else if (entry.endsWith('.ts')) {
206
+ const lines = readFileSync(full, 'utf8').split('\n');
207
+ let stmtStart = 0;
208
+ for (let i = 0; i < lines.length; i++) {
209
+ if (/^\s*(?:import|export)\b/.test(lines[i])) stmtStart = i;
210
+ if (/(?:from\s*|import\s*|import\s*\(\s*)['"]@gobing-ai\//.test(lines[i])) {
211
+ const opener = lines.slice(stmtStart, i + 1).join(' ');
212
+ if (!/^\s*(?:import|export)\s+type\b/.test(opener)) {
213
+ hits.push({ file: full, line: i + 1 });
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ return hits;
220
+ }
221
+
181
222
  export function validateContract(manifest: ScriptManifest, scriptsDir: string, pluginDir: string): Violation[] {
182
223
  const violations: Violation[] = [];
183
224
  const { tsFiles, mjsFiles } = listDiskScripts(scriptsDir);
@@ -280,6 +321,23 @@ export function validateContract(manifest: ScriptManifest, scriptsDir: string, p
280
321
  });
281
322
  }
282
323
 
324
+ // Rule 5: bundled surfaces (scripts, hooks, lib) must not value-import @gobing-ai/*.
325
+ // superskill install bundles them where no node_modules exists; resolution then
326
+ // depends on the host superskill's private dep tree (0.3.28+ happens to carry
327
+ // ts-utils — an accident we do not depend on). Vendor into plugins/sp/lib/ instead.
328
+ for (const dir of [scriptsDir, join(pluginDir, 'hooks'), join(pluginDir, 'lib')]) {
329
+ for (const hit of findGobingAiValueImports(dir)) {
330
+ violations.push({
331
+ kind: 'gobing_ai_import',
332
+ target: `${hit.file}:${hit.line}`,
333
+ message:
334
+ `bundled surface ${hit.file}:${hit.line} value-imports @gobing-ai/* — ` +
335
+ 'superskill install bundles it with no node_modules ("Bundle failed", task 0669). ' +
336
+ 'Vendor into plugins/sp/lib/ or use a relative import; `import type` is exempt.',
337
+ });
338
+ }
339
+ }
340
+
283
341
  return violations;
284
342
  }
285
343
 
@@ -346,6 +346,67 @@ export function checkNounVerbFlags(
346
346
  }
347
347
  }
348
348
 
349
+ // ─── Semantic operand layer (I7 / task 0906 R2) ─────────────────────────────
350
+
351
+ /**
352
+ * Metadata-only operands the I6 sweep classified. This is the audit boundary, not a
353
+ * CLI section matrix: a key that is ALSO a genuine body heading for the noun (feature
354
+ * `Scope`) is accepted per-noun in checkSectionOperand; anything outside this set is
355
+ * not statically classifiable and stays unverified.
356
+ */
357
+ const SECTION_METADATA_KEYS = new Set([
358
+ 'tags',
359
+ 'priority',
360
+ 'status',
361
+ 'phase',
362
+ 'id',
363
+ 'parent',
364
+ 'name',
365
+ 'owner',
366
+ 'scope',
367
+ ]);
368
+
369
+ /** Body headings that overlap the swept metadata keys, per noun (case-insensitive). */
370
+ const NOUN_BODY_SECTIONS: Record<string, ReadonlySet<string>> = {
371
+ feature: new Set(['scope']),
372
+ };
373
+
374
+ /**
375
+ * Semantic operand check (I7 / task 0906 R2): `--section` is a real flag on
376
+ * task/feature update, so existence parity passes while the operand names a
377
+ * metadata-only key instead of a body section. Classify literal operands only —
378
+ * dynamic operands (placeholders) stay unverified under the existing scanner
379
+ * convention, and quoted/equal forms are handled alongside the spaced form.
380
+ * Comparison is case-normalized; the original operand text stays in the row evidence.
381
+ */
382
+ export function checkSectionOperand(spanRaw: string, occ: { file: string; line: number }): void {
383
+ const parsed = parseInvocation(spanRaw);
384
+ if (!parsed) return;
385
+ const noun = parsed.nouns[0];
386
+ if ((noun !== 'task' && noun !== 'feature') || !parsed.verbs.includes('update')) return;
387
+ for (const m of spanRaw.matchAll(/--section(?:=|\s+)("([^"]*)"|'([^']*)'|[^\s]+)/g)) {
388
+ const operand = (m[2] ?? m[3] ?? m[1] ?? '').replace(/[.,;:]+$/, '');
389
+ if (!operand || PLACEHOLDER.test(operand)) continue; // dynamic — unverified by convention
390
+ const key = operand.toLowerCase();
391
+ if ((NOUN_BODY_SECTIONS[noun] ?? new Set()).has(key)) continue; // genuine body heading
392
+ if (!SECTION_METADATA_KEYS.has(key)) continue; // outside the audit boundary — unverified
393
+ // P3 (0906 review): feature update owns the generic --field/--value pair; task update
394
+ // exposes metadata only via dedicated flags (no --field/--value, no --tags), so the hint
395
+ // must not suggest a pair that would fail with VALIDATION_FAILED on task rows.
396
+ const remediation =
397
+ noun === 'feature'
398
+ ? `use --field ${operand} --value <value>`
399
+ : `task update exposes metadata only via dedicated flags (e.g. --priority <value>), not --section`;
400
+ record(
401
+ `spur ${noun} update --section ${operand}`,
402
+ 'semantic-operand(I6 metadata keys vs noun body sections)',
403
+ 'mismatch',
404
+ `--section writes a body section; "${operand}" is a metadata-only ${noun} operand — ${remediation}`,
405
+ occ,
406
+ );
407
+ }
408
+ }
409
+
349
410
  export function sweepPluginTrees(root: string = PLUGIN_ROOT): void {
350
411
  const files = [
351
412
  ...walk(join(root, 'commands'), ['.md']),
@@ -398,6 +459,7 @@ export function sweepPluginTrees(root: string = PLUGIN_ROOT): void {
398
459
  continue;
399
460
  }
400
461
  checkNounVerbFlags(parsed.nouns, parsed.verbs, parsed.flags, occ);
462
+ checkSectionOperand(span, occ);
401
463
  }
402
464
  if (file.endsWith('.ts')) return;
403
465
  if (inVerbTable && refNoun && /^\|/.test(line)) {
@@ -430,7 +492,10 @@ export function sweepPluginTrees(root: string = PLUGIN_ROOT): void {
430
492
  } else {
431
493
  for (const span of lineInvocationSpans(line)) {
432
494
  const parsed = parseInvocation(span);
433
- if (parsed) checkNounVerbFlags(parsed.nouns, parsed.verbs, parsed.flags, occ);
495
+ if (parsed) {
496
+ checkNounVerbFlags(parsed.nouns, parsed.verbs, parsed.flags, occ);
497
+ checkSectionOperand(span, occ);
498
+ }
434
499
  }
435
500
  }
436
501
  });
@@ -764,11 +829,11 @@ export function sweepWorkflows(opts: { run?: CliRunner; wfDir?: string; link?: s
764
829
  .forEach((line: string, i: number) => {
765
830
  for (const span of lineInvocationSpans(line)) {
766
831
  const parsed = parseInvocation(span);
767
- if (parsed)
768
- checkNounVerbFlags(parsed.nouns, parsed.verbs, parsed.flags, {
769
- file: path,
770
- line: i + 1,
771
- });
832
+ if (parsed) {
833
+ const occ = { file: path, line: i + 1 };
834
+ checkNounVerbFlags(parsed.nouns, parsed.verbs, parsed.flags, occ);
835
+ checkSectionOperand(span, occ);
836
+ }
772
837
  }
773
838
  });
774
839
  }
@@ -406,9 +406,9 @@ export function extractTriggerTable(crossCuttingRaw: string): string[] | null {
406
406
  function adrAgentClaims(adrRaw: string): Map<string, SurfaceBehavior> | null {
407
407
  if (adrRaw.includes('## ADR-047')) {
408
408
  const out = new Map<string, SurfaceBehavior>();
409
- // G5 amendment (feature G5 / task 0565): explicit inline is host-session-only — headless
410
- // surfaces reject it with the stable special error; 0508 native-subagent eligibility
411
- // applies to omitted --agent only, never explicit inline.
409
+ // ADR-087 (task 0687) retired the G5 frozen rejection: explicit `inline` on a headless
410
+ // surface resolves via role/tier substitution with one warning. This claims map mirrors
411
+ // the ADR-047 amendment text as written; ADR-087-aware parsing is a follow-up.
412
412
  out.set('inline', {
413
413
  surfaces: new Set(['inline']),
414
414
  conditional: false,
@@ -231,8 +231,17 @@ to executors via `agent.executors[].agent` (or the model's `<provider>/` prefix)
231
231
  write path. A provider is exhausted when any `primary|secondary|tertiary` window reports
232
232
  `usedPercent >= 100`; the window name and `resetsAt` go into the observation reason. Per-provider
233
233
  `{ "error": … }` entries are skipped (listed, never treated as recovery); healthy entries still
234
- apply. A missing codexbar binary or an unparsable payload exits `1` and changes nothing.
235
- Unmapped providers are listed and never guessed.
234
+ apply. Providers whose windows are all null carry no signal (`no-usage`): they are reported and
235
+ excluded from availability decisions — an absent signal neither disables nor recovers, and an
236
+ exhausted signal wins on shared executors. Operator-owned availability (`disabled: true` or an
237
+ operator ownership object) is never touched. A missing codexbar binary or an unparsable payload
238
+ exits `1` and changes nothing. Unmapped providers are listed and never guessed.
239
+
240
+ Each reported change carries a delivery-semantics `action` (0907): `would-apply` (dry run),
241
+ `applied` — the exact observation created by the invocation was acknowledged without a skip
242
+ (desired state confirmed; not proof of a YAML byte change), `no-op` — already satisfied or
243
+ operator-owned, `skipped` — the observation was superseded or rejected, `pending` — delivery
244
+ unconfirmed or failed. For `skipped`/`pending` the printed target is intent only.
236
245
 
237
246
  **Scheduling is external** (cron/launchd, same pattern as `spur history daily`):
238
247
 
@@ -90,7 +90,11 @@ spur self status --json # machine-readable
90
90
  ```
91
91
 
92
92
  Reports the project's Spur configuration state (init status, feature/task counts, rule preset
93
- health) and git working-tree status. Optional `[path]` argument targets a different project
93
+ health), git working-tree status, and DecisionMaker readiness (0911): the human output adds a
94
+ `DecisionMaker:` line and `--json` exposes `decisionMaker: {enabled, provider,
95
+ credentialPresent, state, connectivity, inlineSupport}` with states `disabled` / `missing-key` /
96
+ `configured-not-probed`. Presence check only — never a live probe, never echoes the key.
97
+ Optional `[path]` argument targets a different project
94
98
  directory. Only flag is `--json`.
95
99
 
96
100
  ## What this skill is NOT
@@ -80,7 +80,9 @@ Use this skill to:
80
80
  - **Author a workflow** — turn a described process into a validated, dry-run-verified YAML definition
81
81
  in the right mode. → authoring-workflows.md
82
82
  - **Validate before trusting** — schema + semantic-check a workflow file (references, terminal
83
- reachability, template vars) before running it.
83
+ reachability, template vars, and since 0911 the HITL decision policy: `decision` only on
84
+ `hitl.confirm`/`hitl.select`, evidence mode banned in `pause: true` states/nodes, one evidence
85
+ action per state/node, existing producer nodes, valid select choices) before running it.
84
86
  - **Run a workflow** — execute a definition and read its run trace (states/nodes entered, transitions
85
87
  taken, terminal status).
86
88
  - **Refine an existing workflow** — fix a stuck guard, add a state/node, retune `iterationBound`,
@@ -108,7 +110,7 @@ The skill's logic divides by **whether the LLM adds value**:
108
110
  | --------- | --------- | ----- | ------------------ |
109
111
  | `validate` | `spur workflow validate` (CLI) | `<file> [--no-schema]` | Schema + semantic verdict |
110
112
  | `run` | `spur workflow run` (CLI) | `<file> [--run-id <id>] [--vars <json>] [--dry-run] [--async] [--no-plan] [--quiet/--silent/--verbose] [--detail <level>] [--trace-file] [--no-log] [--steer]` | Terminal state reached (sync) or run started (async); trace readable |
111
- | `continue` | `spur workflow continue` (CLI) | `[run-id] [--yes] [--answer <yes\|no\|cancel>]` | Resume a paused HITL run (omit id -> most recent paused); `--answer` injects a gate answer before guard re-evaluation |
113
+ | `continue` | `spur workflow continue` (CLI) | `[run-id] [--yes] [--answer <yes\|no\|cancel>] [--async] [--no-log]` | Resume a paused or interrupted run (omit id -> most recent resumable); `--answer` injects a gate answer before guard re-evaluation and is required headless (0901 R3); `--async` detaches the resume (0901 R4) |
112
114
  | `cancel` | `spur workflow cancel` (CLI) | `<run-id>` | Single non-terminal run marked failed (SIGTERM async worker when live) |
113
115
  | `clean` | `spur workflow clean` (CLI) | `[--older-than <min>] [--force] [--logs] [--dry-run]` | Bulk-finalize stale `running`/`pending` runs as failed **and** reclaim retained run logs older than `workflow.logRetentionDays` (30d default) |
114
116
  | `list` | `spur workflow list` (CLI) | — | Available workflow **YAML definition files** (not run records) |
@@ -258,7 +260,7 @@ the operator accepts it, and never hot-edit a running workflow's shell in place.
258
260
  spur workflow validate <file> [--no-schema] [--json]
259
261
  spur workflow show <file> [--format <mermaid|todo>] [--json]
260
262
  spur workflow run <file> [--run-id <id>] [--vars <json>] [--dry-run] [--async] [--no-plan] [--quiet/--silent/--verbose] [--detail <level>] [--trace-file] [--no-log] [--steer] [--json]
261
- spur workflow continue [run-id] [--yes] [--answer <yes|no|cancel>] [--json]
263
+ spur workflow continue [run-id] [--yes] [--answer <yes|no|cancel>] [--async] [--no-log] [--json]
262
264
  spur workflow cancel <run-id> [--json]
263
265
  spur workflow clean [--older-than <minutes>] [--force] [--logs] [--dry-run] [--json]
264
266
  spur workflow list [--json]
@@ -318,7 +320,12 @@ HITL pause/resume: a run that hits a HITL action pauses; resume with `spur workf
318
320
  (`--yes` skips confirmation). A headless `hitl.confirm` persists a default `no` before pausing -
319
321
  use `--answer yes|no|cancel` to inject the operator's gate answer before guard re-evaluation (0433).
320
322
  `--answer` is distinct from `--yes`: `--yes` skips the CLI resume prompt, `--answer` sets the HITL
321
- gate answer. Cancel one live/paused run with `cancel <run-id>`; bulk-finalize orphans stuck in
323
+ gate answer. 0901: resumes accept interrupted runs too (rerun-enter re-executes the interrupted
324
+ node and needs `resumeRerun: true` on the target state); a headless (`--json`/non-TTY) continue
325
+ without `--answer` is refused (exit 2); `--async` detaches the resume and reports started/failed
326
+ after the worker claims the run; resumed runs write the consolidated run log and pass shell
327
+ streams through the secret redactor + 64 KiB tail unless `--no-log`. Cancel one live/paused run
328
+ with `cancel <run-id>`; bulk-finalize orphans stuck in
322
329
  `running`/`pending` with `clean` (`--older-than` default 30 minutes, or `--force`).
323
330
 
324
331
  **Schema resolution parity (0431):** `validate` and `run` both load the workflow through
@@ -61,6 +61,18 @@ The explicit-rejection carve-out above was superseded by ADR-087 (task 0687): a
61
61
  substitutes tier resolution with a warning instead of rejecting — see the substitution blockquote
62
62
  above and `resolveAgent` in `packages/app/src/services/agent-service.ts`.
63
63
 
64
+ ### Run-scoped session policy (B7, summary)
65
+
66
+ When an inline resolution dispatches a native subagent, or a workflow `agent.run` executes, the
67
+ run-scoped session policy applies: coder stages default to `session: reuse`; reviewer, planner,
68
+ and scribe stages default to `fresh`; a reviewer stage may declare `session: reuse` explicitly.
69
+ A runner record without resume capability (`supportsResumeById: false`) forces a fresh dispatch,
70
+ and a disabled pinned executor re-resolves once, then starts fresh. Run traces record the session
71
+ provenance (`reused | fresh`). This is a summary only: the owning contract is
72
+ [session-pinned-dispatch.md §4](../../../../../docs/design/session-pinned-dispatch.md) with role
73
+ semantics in [roles.md](../../../references/roles.md); this section does not restate that
74
+ contract.
75
+
64
76
  ### Objective triggers override the answer
65
77
 
66
78
  The one rule resolves operator *intent*. A trigger is a detected *requirement* the chosen executor
@@ -188,8 +200,15 @@ Do not read provider auth or quota from `spur agent doctor`. The doctor resolves
188
200
  config), so it historically degraded to `status: usable · auth: no · model: unknown` for GLM-style
189
201
  executors and was useless as a preflight gate. Feature B4 removed the auth signal from the surface
190
202
  entirely (no column, no `authenticated` in `--json`) precisely so nothing can read it by mistake;
191
- the precheck probe classifies on usability alone. Exhaustion is detected mid-run by the escalation
192
- classifier, not by any preflight probe.
203
+ the precheck probe classifies on usability alone. Preflight availability does exist — as a
204
+ separate, owned surface (session-pinned-dispatch.md §3.4): `spur agent usage` captures provider
205
+ quota windows into a durable snapshot, the availability drain derives `quota.exhausted` /
206
+ `quota.recovered` observations from it, and `spur agent doctor` renders the provenance (`owner`,
207
+ `since`, `reason`, snapshot `age`). A snapshot older than `agent.usage.maxAgeMs` renders `stale`
208
+ and never enables an executor. Usability (doctor) is not authentication, and neither is a live
209
+ quota guarantee — the signals stay distinct. Mid-run exhaustion is still detected by the
210
+ escalation classifier; the preflight surface informs planning, it does not replace the in-run
211
+ detector.
193
212
 
194
213
  ### Explicit subprocess surfaces are unchanged
195
214
 
@@ -260,7 +260,10 @@ an executor failed — the executor is swappable via config, the pipeline is not
260
260
  **When an `agent.run` step fails (timeout, non-zero exit, empty output):**
261
261
 
262
262
  1. **Diagnose, don't bypass.** Check `spur agent doctor <executor>` — is the agent
263
- installed? Is auth present? Then check `.spur/config.yaml` → which role/executor does the
263
+ installed and usable? Doctor reports usability plus availability provenance (`owner`,
264
+ `since`, `reason`, snapshot `age`); it is read-only and a stale availability snapshot never
265
+ enables an executor — `spur agent usage` is the preflight availability signal. Then check
266
+ `.spur/config.yaml` → which role/executor does the
264
267
  run resolve to (`agent.default` role → stage-registry tier ladder)? Which model does that
265
268
  executor use? Could that model be out of tokens, rate-limited, or deprecated?
266
269
  2. **Switch executors, don't abandon the pipeline.** Override the agent for the run:
@@ -275,12 +278,18 @@ an executor failed — the executor is swappable via config, the pipeline is not
275
278
  Manual section fills outside either driver are indistinguishable from pipeline output and bypass
276
279
  the provenance contract silently.
277
280
 
278
- **Known diagnostic gap:** `spur agent doctor` checks installation, version, and auth — it
279
- cannot detect token quota exhaustion, model deprecation, or rate limits. An executor
280
- configured with `agent: omp` + `model: <provider/model>` passes doctor if `omp` is
281
+ **Known diagnostic gap:** `spur agent doctor` checks installation, version, and usability
282
+ (read-only, with availability provenance from `spur agent usage`; a stale snapshot never
283
+ enables) — it cannot detect mid-run token quota exhaustion, model deprecation, or rate limits.
284
+ An executor configured with `agent: omp` + `model: <provider/model>` passes doctor if `omp` is
281
285
  installed, even if the model is unavailable. If an `agent.run` times out with no useful
282
286
  diagnostic, suspect the model, not the agent binary.
283
287
 
288
+ Executor switching also interacts with run-scoped session policy (coder `reuse`,
289
+ reviewer/planner/scribe `fresh`): see the session-policy summary in
290
+ [cross-cutting.md#inline-default-execution-surface](cross-cutting.md#inline-default-execution-surface);
291
+ this file does not restate that contract.
292
+
284
293
  ## Large tasks and timed-out implement resume (task 0424)
285
294
 
286
295
  Two obligations when driving a task that may not fit one implement pass.
@@ -72,10 +72,18 @@ reserved for this specific planning/execution split).
72
72
  **HITL** (human-in-the-loop) — a workflow state that pauses for explicit operator approval
73
73
  before continuing (`hitl.confirm`). A HITL gate is never auto-dismissed by the engine; `--auto`
74
74
  can only route *around* one whose objective precondition is already met (see the `--auto`
75
- routing contract in `cross-cutting.md`).
75
+ routing contract in `cross-cutting.md`). A `decision` option (0911) pins that action's policy:
76
+ `mode: never` forces the human responder, `mode: evidence` lets the optional DecisionMaker answer
77
+ only from verified prior action evidence (deferring otherwise), and absence keeps the 0910
78
+ implicit behavior when `workflow.hitlDecisionMaker` is enabled.
76
79
  Avoid: *prompt* (reserved for LLM input text), *interrupt* (implies an exception, not a planned
77
80
  pause point).
78
81
 
82
+ **DecisionMaker** — optional provider-backed responder for executed `hitl.confirm`/`hitl.select`
83
+ actions (ADR-123), enabled via `workflow.hitlDecisionMaker: true` and `TYPESAFE_API_KEY`.
84
+ Evidence answers clear the answer variable and record `statusVar: accepted/deferred`; unresolved
85
+ cases defer or delegate to the human. Offline readiness: `spur self status`.
86
+
79
87
  **WBS** (work-breakdown-structure ID) — the four-digit task identifier (e.g. `0187`) that
80
88
  names a task file and its position in the corpus. WBS IDs are assigned once and never reused.
81
89
  Avoid: *task ID* alone (acceptable in prose, but *WBS* is the canonical term when precision
@@ -23,7 +23,7 @@ the resolved actions and guards of every `.spur/workflows/*.yaml`; any element p
23
23
  in one and absent in the other fails the check. Add a new kind here when the driver
24
24
  implements it; remove the entry when the corresponding kind is dropped from the YAML.
25
25
 
26
- **Actions:** `shell` · `note` · `doctor.probe` · `file.read.into-var` · `hitl.confirm` · `agent.run` · `proof.fingerprint` · `run.artifact` · `command.gate`
26
+ **Actions:** `shell` · `note` · `doctor.probe` · `file.read.into-var` · `hitl.confirm` · `hitl.select` · `agent.run` · `proof.fingerprint` · `run.artifact` · `command.gate`
27
27
 
28
28
  **Guards (transitions):** `always` · `shell` · `action-ok` · `contract-violation`
29
29
 
@@ -45,6 +45,14 @@ FSM definition. The driver MUST read that file
45
45
  at invocation time. It must not copy the state list, actions, guards, or transition order into a
46
46
  command, skill, script, or second workflow.
47
47
 
48
+ **Inline HITL defer semantics (0911):** bundled pipeline human gates ship `decision: {mode:
49
+ never}`, so the inline driver always prompts the operator for those gates regardless of
50
+ `workflow.hitlDecisionMaker` — the decision policy never answers inline pipeline gates. A local
51
+ workflow override (`.spur/workflows/*.yaml`) may declare `mode: evidence`, but the driver does
52
+ not claim full inline parity for the DecisionMaker path: evidence-mode auto-answering inside the
53
+ inline driver is an explicit non-goal (subprocess runs own that behavior). No hot reload: config
54
+ and YAML changes require restarting the invoking process.
55
+
48
56
  ## Run setup
49
57
 
50
58
  **Shared startup contract (task 0814 R1/R3/R4/R6/R7).** The order is load-bearing: publish a compact
@@ -120,7 +120,7 @@ Invoked when the operator has a loose idea and the destination itself is foggy.
120
120
  - **## Decisions so far** — empty on creation; populated as questions and tickets resolve (one line each: the decision, or WBS + title + one-line gist)
121
121
  - **### Not yet specified** — the fog of war: in-scope questions you can sense but can't yet phrase sharply enough to ticket. Nest under `## Notes`.
122
122
  - **### Out of scope** — work consciously ruled beyond this destination. Nest under `## Notes`.
123
- - **Tag the feature as a wayfinder map.** `spur feature update <id> --section tags --from-file <(printf '["wayfinder-map"]')`. The `wayfinder-map` tag tells `feature check` to skip BDD AC validation — maps deliberately carry a prose no-AC disclaimer instead of Gherkin scenarios, so without the tag the checker flags a false error.
123
+ - **Tag the feature as a wayfinder map.** `spur feature update <id> --field tags --value wayfinder-map`. The `wayfinder-map` tag tells `feature check` to skip BDD AC validation — maps deliberately carry a prose no-AC disclaimer instead of Gherkin scenarios, so without the tag the checker flags a false error.
124
124
  4. **Create child tasks only for executable investigations.** `spur task create "<title>" --feature <feature-id>` for each sharp question **an implementer can answer without the operator** — research, prototype, inventory, measurement. Anything needing the operator's judgment goes to **## Open questions** instead. Ticket types (see below) determine which skill resolves the tasks.
125
125
  5. **Wire blocking edges.** After all tickets exist (they need IDs before they can reference each other), set dependencies via `spur task update`. Wiring sorts tickets into the frontier (open, unblocked, unclaimed) and the blocked.
126
126
  6. **Populate the fog.** Everything you can't yet specify stays in **### Not yet specified** — sketch it as loosely or as fully as the view allows. Don't pre-slice fog into ticket-sized pieces; one patch may graduate into several tickets, or none.