@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/pipeline-budgets.json +7 -0
- package/config/rules/boundary/sp-plugin-standalone.yaml +37 -0
- package/config/workflows/decision-routing-example.yaml +134 -0
- package/config/workflows/idea-pipeline.yaml +8 -0
- package/config/workflows/task-pipeline.yaml +2 -0
- package/config/workflows/wayfinder-resolution.yaml +2 -0
- package/config/workflows/wrapup-pipeline.yaml +2 -0
- package/package.json +9 -9
- package/plugins/sp/README.md +1 -0
- package/plugins/sp/agents/expert-spur.md +2 -2
- package/plugins/sp/lib/idea-handoff.generated.mjs +157 -150
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -0
- package/plugins/sp/scripts/script-contract-check.ts +58 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +71 -6
- package/plugins/sp/scripts/validate-flag-contracts.ts +3 -3
- package/plugins/sp/skills/spur-cli/references/agent.md +11 -2
- package/plugins/sp/skills/spur-cli/references/self.md +5 -1
- package/plugins/sp/skills/spur-cli/references/workflows.md +11 -4
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +21 -2
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +13 -4
- package/plugins/sp/skills/spur-dev/references/glossary.md +9 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +9 -1
- package/plugins/sp/skills/wayfinder/SKILL.md +1 -1
- package/spur.js +3100 -959
- package/web/_astro/{BoardApp.CDUcHlTJ.js → BoardApp.BEDWpzsr.js} +82 -82
- package/web/_astro/BoardApp.DQG2xfEz.js +1 -0
- package/web/_astro/{TaskDetail.DwTmbQp5.js → TaskDetail.CXGltuT_.js} +1 -1
- package/web/_astro/{arc.BzF71EFI.js → arc.C0rrflm_.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.jvdDahWM.js → architectureDiagram-3BPJPVTR.DJ8DHkWE.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.zSg4AmFD.js → blockDiagram-GPEHLZMM.D8DHK3Jl.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.BkUIUQWH.js → c4Diagram-AAUBKEIU.BugQbX9u.js} +1 -1
- package/web/_astro/channel.SRrg1P-w.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.DvfQ_f50.js → chunk-2J33WTMH.Mv26KlVn.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DuI4gQqX.js → chunk-4BX2VUAB.CD51JoT_.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.D3BWBOpF.js → chunk-55IACEB6.D_PFaIEe.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.3QSi0a9M.js → chunk-727SXJPM.DemMW1ao.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.xazCQrAF.js → chunk-AQP2D5EJ.Iu2V5-ex.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.B2g6u4rA.js → chunk-FMBD7UC4.MsNgSP-E.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.wWwWs99t.js → chunk-ND2GUHAM.DfbRaAlm.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BD5g3qa9.js → chunk-QZHKN3VN.BbJAQ4h-.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.C7CzCdsX.js → classDiagram-4FO5ZUOK.CPurtiC2.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.C7CzCdsX.js → classDiagram-v2-Q7XG4LA2.CPurtiC2.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.Xyiau0gw.js → cose-bilkent-S5V4N54A.CKKdx1bM.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BeC5MWas.js → cynefin-OW5HDTMX.CoKMTg-R.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.yZbMN9vc.js → dagre-BM42HDAG.D4h4_k56.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.Cmo2zQM-.js → diagram-2AECGRRQ.DJ0h9zgw.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.D033eSVi.js → diagram-5GNKFQAL.DvPk1jYd.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CR6k3Y3G.js → diagram-KO2AKTUF.BFoCkiCr.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.x7mwu8jz.js → diagram-LMA3HP47.exHn9OVx.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.D8aTTvUr.js → diagram-OG6HWLK6.CeqO34nN.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.BoBqcKXQ.js → erDiagram-TEJ5UH35.D_v7HqxR.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.D3mTQdrU.js → flowDiagram-I6XJVG4X.EUmrpbwh.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.H-cqgIh-.js → ganttDiagram-6RSMTGT7.BOCF5lII.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.B6s9zbfC.js → gitGraphDiagram-PVQCEYII.Di7otYZD.js} +1 -1
- package/web/_astro/index.Bx6GY4RH.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.BzgCoV6P.js → infoDiagram-5YYISTIA.TcBkCAJk.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BZzVhy1-.js → ishikawaDiagram-YF4QCWOH.D-2y4M0c.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.BV3195Py.js → journeyDiagram-JHISSGLW.DPbJI_n2.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BjRd2DWz.js → kanban-definition-UN3LZRKU.EFxhQ9Fj.js} +1 -1
- package/web/_astro/{linear.BILTgS5N.js → linear.DSAsQLzs.js} +1 -1
- package/web/_astro/{mermaid.core.DBy_WKeW.js → mermaid.core.kAZjgJHG.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BiEjaI4-.js → mindmap-definition-RKZ34NQL.CJY1N_7V.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.i_8V5pIn.js → pieDiagram-4H26LBE5.567ZNoL2.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.BWaW3MHn.js → quadrantDiagram-W4KKPZXB.mqfz9-MY.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.CzddBbtg.js → requirementDiagram-4Y6WPE33.Bv1Gv9In.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.X2ww0e-D.js → sankeyDiagram-5OEKKPKP.B6Gs4X4r.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.DSA4kTcc.js → sequenceDiagram-3UESZ5HK.BhYj4v-m.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.D0DtFSpR.js → stateDiagram-AJRCARHV.BPbBnkpw.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.BfQq0zQv.js → stateDiagram-v2-BHNVJYJU.C4squMNK.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.Dmlrgi1m.js → timeline-definition-PNZ67QCA.C_SwIHgl.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.D5mpl00Z.js → vennDiagram-CIIHVFJN.Bz4NZGpQ.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.Df4BdzO4.js → wardleyDiagram-YWT4CUSO.CozMVZ3i.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DiTRreKN.js → xychartDiagram-2RQKCTM6.BwMGBwjB.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.CaCGU_uX.js +0 -1
- package/web/_astro/channel.SSVY0JPQ.js +0 -1
- package/web/_astro/index.CcU5weKX.css +0 -1
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
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"]
|
|
@@ -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)
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
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.
|
|
235
|
-
|
|
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)
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
192
|
-
|
|
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
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
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> --
|
|
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.
|