@gobing-ai/spur 0.3.67 → 0.3.68
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/corpus-baseline.json +2 -50
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +21 -16
- package/config/workflow-composition-baseline.json +109 -59
- package/config/workflows/docs-pipeline.yaml +98 -22
- package/config/workflows/idea-pipeline.yaml +20 -22
- package/config/workflows/task-pipeline.yaml +207 -108
- package/package.json +9 -9
- package/plugins/sp/README.md +6 -7
- package/plugins/sp/commands/dev-idea.md +5 -3
- package/plugins/sp/commands/dev-plan.md +3 -1
- package/plugins/sp/commands/dev-review-session.md +2 -1
- package/plugins/sp/hooks/context-post-tool.ts +101 -2
- package/plugins/sp/hooks/context-session-start.ts +22 -1
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/stage-registry-adapter.ts +144 -2
- package/plugins/sp/skills/session-review/SKILL.md +16 -0
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +40 -16
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +6 -6
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +80 -11
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +18 -13
- package/spur.js +2069 -597
- package/web/_astro/{BoardApp.BEtcJqde.js → BoardApp.BQFbkeqq.js} +15 -15
- package/web/_astro/BoardApp.CTkqrhWd.js +1 -0
- package/web/_astro/{TaskDetail.ClAbCXom.js → TaskDetail.Dl2Eaj1w.js} +1 -1
- package/web/_astro/{arc.CCvf51_y.js → arc.uG14rp8A.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.C0cb0J5M.js → architectureDiagram-3BPJPVTR.Dye6uD_x.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.CIyjqoCE.js → blockDiagram-GPEHLZMM.B9Pkh7Hb.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.fs14IuFs.js → c4Diagram-AAUBKEIU.C2x7SC_X.js} +1 -1
- package/web/_astro/channel.Dsvulp7W.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CaBKv4ZO.js → chunk-2J33WTMH.D2p4-nWk.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.BOllTPto.js → chunk-4BX2VUAB.S-6jf33o.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.ChEof0O4.js → chunk-55IACEB6.DVXj4Fdh.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.Co2kdjD8.js → chunk-727SXJPM.Dz689FMN.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.SWmfcnog.js → chunk-AQP2D5EJ.KxYj5TnI.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.rDAFifF3.js → chunk-FMBD7UC4.itTQyHQB.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.BCnoXKCw.js → chunk-ND2GUHAM.euSrbJf5.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.RSmy2hDO.js → chunk-QZHKN3VN.OWASJRQy.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.Be7PEfrX.js → classDiagram-4FO5ZUOK.BLvrlpNO.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.Be7PEfrX.js → classDiagram-v2-Q7XG4LA2.BLvrlpNO.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.BkUp2aSK.js → cose-bilkent-S5V4N54A.XBF-rmyD.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.BegGGlUV.js → cynefin-OW5HDTMX.DlCx762Z.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.BkUdjsaC.js → dagre-BM42HDAG.D17Rshxv.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.E9vugt3-.js → diagram-2AECGRRQ.AhBIVJC8.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Dj4yeHXB.js → diagram-5GNKFQAL.C9ximjyC.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.Buaquwli.js → diagram-KO2AKTUF.CZb7Ru_9.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.BV3dgGgm.js → diagram-LMA3HP47.BW7LwqoS.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Cnx3s-tc.js → diagram-OG6HWLK6.XC025W0V.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DKK_abu4.js → erDiagram-TEJ5UH35.CpMXmBDP.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.BNuu9fbm.js → flowDiagram-I6XJVG4X.D2ednJWg.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.b16KUMjy.js → ganttDiagram-6RSMTGT7.BjL9FGKO.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Kh41lbG5.js → gitGraphDiagram-PVQCEYII.B90g1VGk.js} +1 -1
- package/web/_astro/{infoDiagram-5YYISTIA.DEWBXkp-.js → infoDiagram-5YYISTIA.RqgycKtQ.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.DiAdmcL6.js → ishikawaDiagram-YF4QCWOH.Ctn-zt6a.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.D1Ki7IRm.js → journeyDiagram-JHISSGLW.DJhT8Ctp.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.CWUhrQpc.js → kanban-definition-UN3LZRKU.BY1QdejI.js} +1 -1
- package/web/_astro/{linear.BaFsgcCe.js → linear.Di7YObSt.js} +1 -1
- package/web/_astro/{mermaid.core.CHw_AsGy.js → mermaid.core.CbxtJS3Q.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.UIhghgmN.js → mindmap-definition-RKZ34NQL.CxvR4g_J.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.D05l3JUA.js → pieDiagram-4H26LBE5.jNWqnBHH.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.BcWIhIcE.js → quadrantDiagram-W4KKPZXB.BCp12MbA.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.B1rYvKGn.js → requirementDiagram-4Y6WPE33.Dxhm4TyR.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.CKylVRC4.js → sankeyDiagram-5OEKKPKP.BtQXp4J9.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.Dm3uA_s4.js → sequenceDiagram-3UESZ5HK.BWEM1R_Q.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.Bgca_BLe.js → stateDiagram-AJRCARHV.BG3wUkWB.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.C1T7YFrG.js → stateDiagram-v2-BHNVJYJU.BLtMeFVP.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.ZOHJn3Sn.js → timeline-definition-PNZ67QCA.D5fHo0az.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DegZitjD.js → vennDiagram-CIIHVFJN.0DcuMluU.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.BDsC115d.js → wardleyDiagram-YWT4CUSO.BZ-dxgHm.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.D0MO70ea.js → xychartDiagram-2RQKCTM6.Bg-XWF7z.js} +1 -1
- package/web/index.html +1 -1
- package/web/_astro/BoardApp.DBEin4N5.js +0 -1
- package/web/_astro/channel.BGn_DUCD.js +0 -1
|
@@ -16,7 +16,7 @@ Wraps the **sp:spur-dev** skill.
|
|
|
16
16
|
| `"<description>"` | Feature description to plan. | required |
|
|
17
17
|
| `--feature` `<id>` | Attach to an existing feature. | omitted |
|
|
18
18
|
| `--parent` `<feature-id>` | Create under a parent feature. | omitted |
|
|
19
|
-
| `--agent` `<inline\|auto\|name>` | Who runs the model-bearing planning.
|
|
19
|
+
| `--agent` `<inline\|auto\|name>` | Who runs the model-bearing planning. Omission and `inline` drive `idea-pipeline.yaml` in this session with zero external agent/workflow processes; `auto` tier-resolves an executor and a name pins one, both through the async workflow worker. | inline |
|
|
20
20
|
| `--skip-design` | Omit the system-design hop. | off |
|
|
21
21
|
| `--auto` | Skip objective HITL gates. | off |
|
|
22
22
|
| `--approve-taste` | With --auto: skip design-approval pause. | off |
|
|
@@ -43,5 +43,7 @@ for taste gates). Alias: `--design-approved` (prefer `--approve-taste`).
|
|
|
43
43
|
## Implementation
|
|
44
44
|
|
|
45
45
|
- Apply the [inline-default execution-surface contract](../skills/spur-dev/references/cross-cutting.md#inline-default-execution-surface).
|
|
46
|
+
- Omitted/`inline`: drive `idea-pipeline.yaml` through the [inline pipeline driver](../skills/spur-dev/references/inline-pipeline-driver.md). Do not launch `spur workflow run`, `spur agent run`, or a native subagent unless the operator explicitly requests delegation.
|
|
47
|
+
- `auto`/name: launch `spur workflow run idea-pipeline.yaml --async`, observe with one `workflow trace --follow`, and only report cancellation as stopped when `workflow cancel --json` returns `killed: true`.
|
|
46
48
|
- `Skill(skill="sp:spur-dev", args="plan $ARGUMENTS")`
|
|
47
49
|
- Full Design package + batch `design` field contract: `plugins/sp/skills/spur-dev/references/dev-operations.md` § plan and `planning-workflow.md` Step 5.5.
|
|
@@ -12,7 +12,8 @@ default it is report-only: current conversation plus read-only repository eviden
|
|
|
12
12
|
the session context is preserved — no workflow launch, history import, task creation, or
|
|
13
13
|
remediation. With `--triage`, it first triages the findings, then applies direct fixes (pure
|
|
14
14
|
documentation work and one-to-two-line fixes) inline and files everything remaining as exactly one
|
|
15
|
-
new task for further fixing.
|
|
15
|
+
new task for further fixing. The result includes a non-overlapping time breakdown with durations in
|
|
16
|
+
`M:SS` or `H:MM:SS` form and `n/a` for unavailable measurements.
|
|
16
17
|
|
|
17
18
|
## Argument Flags
|
|
18
19
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* context-post-tool — PostToolUse hook for indexed-context
|
|
4
5
|
* (matcher: Bash|Grep|Glob|Read|Write|Edit — task 0248).
|
|
@@ -19,8 +20,9 @@
|
|
|
19
20
|
* `superskill hook run sp context-post-tool`.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
23
|
+
import { execSync } from 'node:child_process';
|
|
24
|
+
import { appendFileSync, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join, resolve, sep } from 'node:path';
|
|
24
26
|
|
|
25
27
|
/** Tools recorded by this hook (must match hooks.json PostToolUse matcher). */
|
|
26
28
|
export const ALLOWED_TOOLS = new Set(['Bash', 'Grep', 'Glob', 'Read', 'Write', 'Edit']);
|
|
@@ -282,6 +284,15 @@ export function recordToolUseEvent(
|
|
|
282
284
|
const session = sessionMeta.session ?? '';
|
|
283
285
|
if (!session) return null;
|
|
284
286
|
|
|
287
|
+
// Freshness stamp (task 0711 R4): the indexed-context producer is the agent
|
|
288
|
+
// following the skill's update guidance; the observable producer moment in
|
|
289
|
+
// code is a Write/Edit landing on a `.spur/context/*.md` file. Each such
|
|
290
|
+
// write refreshes `.freshness.json` so staleness checks can compare the
|
|
291
|
+
// sidecar's source commit against HEAD until the next regeneration.
|
|
292
|
+
if ((toolName === 'Write' || toolName === 'Edit') && isContextIndexFile(contextDir, filePath)) {
|
|
293
|
+
stampContextFreshness(contextDir, currentHeadCommit(), now());
|
|
294
|
+
}
|
|
295
|
+
|
|
285
296
|
const tokens = resolveTokenEstimate(toolName, payload.tool_input, payload.tool_response);
|
|
286
297
|
const ts = now().toISOString();
|
|
287
298
|
const type = mapToolType(toolName);
|
|
@@ -309,6 +320,94 @@ export function recordToolUseEvent(
|
|
|
309
320
|
return event;
|
|
310
321
|
}
|
|
311
322
|
|
|
323
|
+
// ─── Context freshness sidecar (task 0711 R4) ───────────────────────────
|
|
324
|
+
|
|
325
|
+
export interface ContextFreshness {
|
|
326
|
+
schema_version: number;
|
|
327
|
+
source_commit: string;
|
|
328
|
+
generated_at: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export const CONTEXT_FRESHNESS_SCHEMA_VERSION = 1;
|
|
332
|
+
|
|
333
|
+
/** True when `filePath` is a regenerated context index inside `contextDir` (not the sidecars). */
|
|
334
|
+
export function isContextIndexFile(contextDir: string, filePath: string): boolean {
|
|
335
|
+
const resolvedDir = resolve(contextDir);
|
|
336
|
+
const resolvedFile = resolve(filePath);
|
|
337
|
+
if (!resolvedFile.startsWith(resolvedDir + sep)) return false;
|
|
338
|
+
const name = resolvedFile.slice(resolvedDir.length + 1);
|
|
339
|
+
return name.endsWith('.md');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Current HEAD, best-effort — null when git is unavailable or the dir is not a work tree. */
|
|
343
|
+
export function currentHeadCommit(): string | null {
|
|
344
|
+
try {
|
|
345
|
+
const out = execSync('git rev-parse HEAD', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
346
|
+
const commit = out.trim();
|
|
347
|
+
return commit.length > 0 ? commit : null;
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Write/refresh `.spur/context/.freshness.json`. Best-effort: any I/O failure
|
|
355
|
+
* leaves the previous sidecar (or none) in place — the hook contract is
|
|
356
|
+
* fail-open, never blocking.
|
|
357
|
+
*/
|
|
358
|
+
export function stampContextFreshness(contextDir: string, sourceCommit: string | null, at: Date): void {
|
|
359
|
+
if (!sourceCommit) return;
|
|
360
|
+
const body: ContextFreshness = {
|
|
361
|
+
schema_version: CONTEXT_FRESHNESS_SCHEMA_VERSION,
|
|
362
|
+
source_commit: sourceCommit,
|
|
363
|
+
generated_at: at.toISOString(),
|
|
364
|
+
};
|
|
365
|
+
try {
|
|
366
|
+
writeFileSync(join(contextDir, '.freshness.json'), JSON.stringify(body));
|
|
367
|
+
} catch {
|
|
368
|
+
/* fail-open */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface ContextFreshnessCheck {
|
|
373
|
+
stale: boolean;
|
|
374
|
+
reason?: string;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Compare a freshness sidecar against the current HEAD (task 0711 R4). A
|
|
379
|
+
* missing/unparsable sidecar is stale (`never stamped`); a commit mismatch is
|
|
380
|
+
* stale until the producer regenerates the index.
|
|
381
|
+
*/
|
|
382
|
+
export function checkContextFreshness(raw: string | null, headCommit: string | null): ContextFreshnessCheck {
|
|
383
|
+
if (raw === null) return { stale: true, reason: 'never stamped' };
|
|
384
|
+
let body: ContextFreshness;
|
|
385
|
+
try {
|
|
386
|
+
body = JSON.parse(raw) as ContextFreshness;
|
|
387
|
+
} catch {
|
|
388
|
+
return { stale: true, reason: 'malformed sidecar' };
|
|
389
|
+
}
|
|
390
|
+
if (body.schema_version !== CONTEXT_FRESHNESS_SCHEMA_VERSION) {
|
|
391
|
+
return { stale: true, reason: `schema_version ${String(body.schema_version)}` };
|
|
392
|
+
}
|
|
393
|
+
if (typeof body.source_commit !== 'string' || body.source_commit === '') {
|
|
394
|
+
return { stale: true, reason: 'missing source_commit' };
|
|
395
|
+
}
|
|
396
|
+
if (headCommit !== null && body.source_commit !== headCommit) {
|
|
397
|
+
return { stale: true, reason: 'source commit changed since generation' };
|
|
398
|
+
}
|
|
399
|
+
return { stale: false };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Read the freshness sidecar, or null when absent/unreadable. */
|
|
403
|
+
export function readContextFreshness(contextDir: string): string | null {
|
|
404
|
+
try {
|
|
405
|
+
return readFileSync(join(contextDir, '.freshness.json'), 'utf-8');
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
312
411
|
// Entrypoint — thin wrapper; logic lives in {@link recordToolUseEvent} for unit coverage.
|
|
313
412
|
if (import.meta.main) {
|
|
314
413
|
void (async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
4
|
* context-session-start — SessionStart hook for indexed-context.
|
|
4
5
|
*
|
|
@@ -11,10 +12,11 @@
|
|
|
11
12
|
* Self-contained by design (task 0232/0246).
|
|
12
13
|
*/
|
|
13
14
|
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
14
16
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
17
|
import { join } from 'node:path';
|
|
16
|
-
|
|
17
18
|
import { resolveAgentHint, resolveModelHint } from './agent-hint';
|
|
19
|
+
import { checkContextFreshness } from './context-post-tool';
|
|
18
20
|
|
|
19
21
|
export { resolveAgentHint, resolveModelHint } from './agent-hint';
|
|
20
22
|
|
|
@@ -161,6 +163,25 @@ export function recordSessionStart(
|
|
|
161
163
|
if (agent) startEvent.agent = agent;
|
|
162
164
|
if (model) startEvent.model = model;
|
|
163
165
|
|
|
166
|
+
// Freshness check (task 0711 R4): report whether the context indexes were
|
|
167
|
+
// regenerated at the current HEAD. Best-effort, fail-open — git failures
|
|
168
|
+
// yield head=null, which marks stale only via the sidecar's own defects.
|
|
169
|
+
let headCommit: string | null = null;
|
|
170
|
+
try {
|
|
171
|
+
const out = execSync('git rev-parse HEAD', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
172
|
+
const trimmed = out.trim();
|
|
173
|
+
headCommit = trimmed.length > 0 ? trimmed : null;
|
|
174
|
+
} catch {
|
|
175
|
+
headCommit = null;
|
|
176
|
+
}
|
|
177
|
+
let freshnessRaw: string | null = null;
|
|
178
|
+
try {
|
|
179
|
+
freshnessRaw = readFileSync(join(dir, '.freshness.json'), 'utf-8');
|
|
180
|
+
} catch {
|
|
181
|
+
freshnessRaw = null;
|
|
182
|
+
}
|
|
183
|
+
startEvent.contextFreshness = checkContextFreshness(freshnessRaw, headCommit);
|
|
184
|
+
|
|
164
185
|
const ledgerPath = join(dir, 'token-ledger.jsonl');
|
|
165
186
|
try {
|
|
166
187
|
appendFileSync(ledgerPath, `${JSON.stringify(startEvent)}\n`);
|
package/plugins/sp/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.68",
|
|
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"]
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* bun plugins/sp/scripts/stage-registry-adapter.ts --help
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import { execSync } from 'node:child_process';
|
|
28
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
28
29
|
import { join } from 'node:path';
|
|
29
30
|
|
|
30
31
|
// ─── Inline type definitions (mirrors packages/domain/src/stage-registry/) ─
|
|
@@ -203,6 +204,107 @@ export interface TaskSignal {
|
|
|
203
204
|
feature_id?: string | null;
|
|
204
205
|
hasCheckpoint?: boolean;
|
|
205
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Result of routing-checkpoint inspection (task 0711 R3).
|
|
210
|
+
*
|
|
211
|
+
* `usable` feeds `TaskSignal.hasCheckpoint`: only a checkpoint that parses as
|
|
212
|
+
* the canonical frontmatter contract, matches the task (owner identity), is
|
|
213
|
+
* non-terminal, and is not commit-drifted may route A4 (wip + checkpoint →
|
|
214
|
+
* continue). Anything else is reported with its reason and IGNORED — routing
|
|
215
|
+
* falls through to the non-checkpoint row (A5); it is never silently trusted.
|
|
216
|
+
*
|
|
217
|
+
* Lean inline mirror of `packages/app/src/workflow/checkpoint-contract.ts`
|
|
218
|
+
* (the plugin installs into foreign repos and cannot import workspace
|
|
219
|
+
* packages — same discipline as the domain-type mirrors above); the parity
|
|
220
|
+
* test pins the two together.
|
|
221
|
+
*/
|
|
222
|
+
export interface RoutingCheckpointInspection {
|
|
223
|
+
usable: boolean;
|
|
224
|
+
reason?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface RoutingCheckpointOptions {
|
|
228
|
+
/** Current repository HEAD; when provided, a mismatched/absent `source_commit` is drift. */
|
|
229
|
+
headCommit?: string;
|
|
230
|
+
/** Existence probe for referenced artifacts; default `existsSync`. */
|
|
231
|
+
artifactExists?: (path: string) => boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Inspect `.spur/memory/sessions/<wbs>-checkpoint.md` for resume routing
|
|
236
|
+
* (task 0711 R3). A missing file yields `{ usable: false, reason: 'absent' }`
|
|
237
|
+
* — the normal case, indistinguishable for routing from a rejected one.
|
|
238
|
+
*/
|
|
239
|
+
export function inspectRoutingCheckpoint(
|
|
240
|
+
sessionsDir: string,
|
|
241
|
+
wbs: string,
|
|
242
|
+
opts: RoutingCheckpointOptions = {},
|
|
243
|
+
): RoutingCheckpointInspection {
|
|
244
|
+
let raw: string;
|
|
245
|
+
try {
|
|
246
|
+
raw = readFileSync(join(sessionsDir, `${wbs}-checkpoint.md`), 'utf-8');
|
|
247
|
+
} catch {
|
|
248
|
+
return { usable: false, reason: 'absent' };
|
|
249
|
+
}
|
|
250
|
+
if (!raw.startsWith('---')) return { usable: false, reason: 'malformed: missing frontmatter' };
|
|
251
|
+
const end = raw.indexOf('\n---', 3);
|
|
252
|
+
if (end < 0) return { usable: false, reason: 'malformed: unterminated frontmatter' };
|
|
253
|
+
|
|
254
|
+
const scalars = new Map<string, string>();
|
|
255
|
+
const artifacts: string[] = [];
|
|
256
|
+
let inArtifacts = false;
|
|
257
|
+
for (const rawLine of raw.slice(4, end).split('\n')) {
|
|
258
|
+
const line = rawLine.trim();
|
|
259
|
+
if (line.startsWith('- ')) {
|
|
260
|
+
if (inArtifacts) artifacts.push(line.slice(2).trim().replace(/^"|"$/g, ''));
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
inArtifacts = false;
|
|
264
|
+
const sep = line.indexOf(':');
|
|
265
|
+
if (sep <= 0) continue;
|
|
266
|
+
const key = line.slice(0, sep).trim();
|
|
267
|
+
const value = line
|
|
268
|
+
.slice(sep + 1)
|
|
269
|
+
.trim()
|
|
270
|
+
.replace(/^"|"$/g, '');
|
|
271
|
+
if (key === 'artifacts') {
|
|
272
|
+
inArtifacts = true;
|
|
273
|
+
if (value !== '') {
|
|
274
|
+
artifacts.push(
|
|
275
|
+
...value
|
|
276
|
+
.split(',')
|
|
277
|
+
.map((p) => p.trim().replace(/^"|"$/g, ''))
|
|
278
|
+
.filter((p) => p !== ''),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
scalars.set(key, value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (scalars.get('schema_version') !== '1') return { usable: false, reason: 'malformed: schema_version' };
|
|
287
|
+
if (scalars.get('task_wbs') !== wbs) {
|
|
288
|
+
return { usable: false, reason: `owner-mismatch: task_wbs=${scalars.get('task_wbs')} != ${wbs}` };
|
|
289
|
+
}
|
|
290
|
+
const status = scalars.get('status') ?? '';
|
|
291
|
+
if (['done', 'failed', 'cancelled', 'skipped'].includes(status)) {
|
|
292
|
+
return { usable: false, reason: `terminal: status=${status}` };
|
|
293
|
+
}
|
|
294
|
+
const head = opts.headCommit;
|
|
295
|
+
if (head !== undefined) {
|
|
296
|
+
const commit = scalars.get('source_commit') ?? '';
|
|
297
|
+
if (commit === '' || commit !== head) {
|
|
298
|
+
return { usable: false, reason: `commit-drift: checkpoint@${commit.slice(0, 12) || 'none'} != HEAD` };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const probe = opts.artifactExists ?? existsSync;
|
|
302
|
+
for (const artifact of artifacts) {
|
|
303
|
+
const p = artifact.trim().replace(/^"|"$/g, '');
|
|
304
|
+
if (p !== '' && !probe(p)) return { usable: false, reason: `missing-artifact: ${p}` };
|
|
305
|
+
}
|
|
306
|
+
return { usable: true };
|
|
307
|
+
}
|
|
206
308
|
export interface FeatureSignal {
|
|
207
309
|
id: string;
|
|
208
310
|
status: FeatureStatus;
|
|
@@ -894,6 +996,8 @@ const TABLE_C: TableCRow[] = [
|
|
|
894
996
|
},
|
|
895
997
|
{
|
|
896
998
|
// C4 — rule findings: requires spur rule run
|
|
999
|
+
// SAFETY: sentinel, not a string — this row never dispatches; null is the
|
|
1000
|
+
// intentional HITL-stop placeholder that formatStageResult renders as no-command.
|
|
897
1001
|
redirectDispatch: null as unknown as string, // HITL stop
|
|
898
1002
|
rowId: 'C4',
|
|
899
1003
|
probeRows: ['A3', 'A5', 'A6'],
|
|
@@ -1315,6 +1419,17 @@ export function formatStageResult(result: StageResolution): string[] {
|
|
|
1315
1419
|
return lines;
|
|
1316
1420
|
}
|
|
1317
1421
|
|
|
1422
|
+
/** Current repository HEAD, best-effort — null outside a work tree or without git. */
|
|
1423
|
+
function currentHeadCommit(): string | null {
|
|
1424
|
+
try {
|
|
1425
|
+
const out = execSync('git rev-parse HEAD', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
1426
|
+
const commit = out.trim();
|
|
1427
|
+
return commit.length > 0 ? commit : null;
|
|
1428
|
+
} catch {
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1318
1433
|
export function runCli(argv: string[], opts?: { resolve?: (input: ResolutionInput) => StageResolution }): CliResult {
|
|
1319
1434
|
const parsed = parseCliArgs(argv);
|
|
1320
1435
|
const resolve = opts?.resolve ?? resolveStage;
|
|
@@ -1337,6 +1452,30 @@ export function runCli(argv: string[], opts?: { resolve?: (input: ResolutionInpu
|
|
|
1337
1452
|
};
|
|
1338
1453
|
}
|
|
1339
1454
|
|
|
1455
|
+
// Routing checkpoint (0711 R3): a canonical checkpoint at
|
|
1456
|
+
// .spur/memory/sessions/<wbs>-checkpoint.md that survives inspection (owner,
|
|
1457
|
+
// non-terminal, at HEAD, artifacts present) enables the A4 resume row for wip
|
|
1458
|
+
// tasks; anything else is REPORTED and ignored — routing falls through to the
|
|
1459
|
+
// non-checkpoint row. Best-effort and fail-open: git/fs failures leave the
|
|
1460
|
+
// checkpoint unconsidered.
|
|
1461
|
+
let checkpointNote: string | undefined;
|
|
1462
|
+
let hasCheckpoint: boolean | undefined;
|
|
1463
|
+
if (parsed.wbs) {
|
|
1464
|
+
try {
|
|
1465
|
+
const inspection = inspectRoutingCheckpoint(
|
|
1466
|
+
join(process.cwd(), '.spur', 'memory', 'sessions'),
|
|
1467
|
+
parsed.wbs,
|
|
1468
|
+
{ headCommit: currentHeadCommit() ?? undefined },
|
|
1469
|
+
);
|
|
1470
|
+
hasCheckpoint = inspection.usable;
|
|
1471
|
+
if (!inspection.usable && inspection.reason !== 'absent') {
|
|
1472
|
+
checkpointNote = `note: routing checkpoint ignored — ${inspection.reason}`;
|
|
1473
|
+
}
|
|
1474
|
+
} catch {
|
|
1475
|
+
hasCheckpoint = undefined;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1340
1479
|
// Build resolution input from CLI args (note: no live corpus access in CLI mode)
|
|
1341
1480
|
const input: ResolutionInput = {
|
|
1342
1481
|
target: parsed.wbs ?? parsed.feature ?? '',
|
|
@@ -1345,13 +1484,16 @@ export function runCli(argv: string[], opts?: { resolve?: (input: ResolutionInpu
|
|
|
1345
1484
|
once: parsed.once,
|
|
1346
1485
|
auto: parsed.auto,
|
|
1347
1486
|
fullMode: parsed.full,
|
|
1348
|
-
task: parsed.wbs
|
|
1487
|
+
task: parsed.wbs
|
|
1488
|
+
? { wbs: parsed.wbs, status: parsed.taskStatus ?? 'unknown', dependencies: [], hasCheckpoint }
|
|
1489
|
+
: undefined,
|
|
1349
1490
|
feature: parsed.feature ? { id: parsed.feature, status: 'active', tasks: [] } : undefined,
|
|
1350
1491
|
};
|
|
1351
1492
|
|
|
1352
1493
|
try {
|
|
1353
1494
|
const result = resolve(input);
|
|
1354
1495
|
const lines = formatStageResult(result);
|
|
1496
|
+
if (checkpointNote) lines.push(checkpointNote);
|
|
1355
1497
|
return { exitCode: result.reasonKind === 'dispatch' ? 0 : 2, stdout: `${lines.join('\n')}\n`, stderr: '' };
|
|
1356
1498
|
} catch (e) {
|
|
1357
1499
|
return { exitCode: 1, stdout: '', stderr: `error: ${(e as Error).message}` };
|
|
@@ -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
|
|
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-
|
|
126
|
-
|
|
127
|
-
|
|
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)
|
|
@@ -143,7 +145,7 @@ resolved in this order; first match wins:
|
|
|
143
145
|
`--agent auto` tier-resolves an executor (stage `model_policy` → `agent.default` → tier priority)
|
|
144
146
|
**before** merging, so it enters the chain at step 1 already resolved to a concrete name.
|
|
145
147
|
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
|
|
148
|
+
(0687 R3) instead of rejecting — it resolves exactly like an omitted flag. Interactive pipeline wrappers
|
|
147
149
|
consume both inline resolutions identically (0508 eligibility as generalized by 0687 R2) before this
|
|
148
150
|
chain and use the host driver. Omitting the flag on a headless surface forwards nothing, so the
|
|
149
151
|
spawned step resolves to `agent.default` (step 2) or the YAML literal (step 3).
|
|
@@ -193,18 +195,17 @@ explicit process boundary and retain their existing resolution, output, timeout,
|
|
|
193
195
|
contracts. The interactive task wrapper does not change the YAML or engine; it reads the YAML as
|
|
194
196
|
SSOT and interprets the actions in-session before any workflow subprocess exists. It records inline
|
|
195
197
|
provenance without fabricating an `AgentRunTracedResult`.
|
|
196
|
-
`spur agent run`
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
CLI's resolution.
|
|
198
|
+
`spur agent run` resolves omitted or explicit `inline` through tier substitution on its headless
|
|
199
|
+
surface and emits one warning naming the concrete executor; `auto` tier-resolves without the inline
|
|
200
|
+
warning. Interactive dev wrappers consume inline before this boundary, so they never spawn it.
|
|
200
201
|
|
|
201
202
|
### Inline trade-off
|
|
202
203
|
|
|
203
204
|
Inline avoids process startup and preserves the host session's context and tools. Relative to
|
|
204
205
|
subprocess dispatch it provides **no isolated workspace**, **no per-stage subprocess action
|
|
205
206
|
record**, **no independent timeout or abort boundary**, and **no tier-selected executor**: the
|
|
206
|
-
executor is the current coding agent. Interactive
|
|
207
|
-
|
|
207
|
+
executor is the current coding agent. Interactive pipelines retain a run log and session provenance
|
|
208
|
+
through the inline driver; task pipelines additionally record a task run-link. If process isolation or an independently killable
|
|
208
209
|
stage is required, select the subprocess path (`--agent auto` or `--agent <name>`).
|
|
209
210
|
|
|
210
211
|
## Every write is CLI-gated
|
|
@@ -611,22 +612,30 @@ CLI-gated corpus artifact. The `wrapup-pipeline.yaml` `learning-capture` step wr
|
|
|
611
612
|
## Session Checkpoint Convention
|
|
612
613
|
|
|
613
614
|
Long-running pipelines write resumable checkpoints to `.spur/memory/sessions/` so an interrupted
|
|
614
|
-
run can be resumed. The
|
|
615
|
-
|
|
615
|
+
run can be resumed. The canonical frontmatter schema below is parsed by
|
|
616
|
+
`packages/app/src/workflow/checkpoint-contract.ts` (task 0711): a checkpoint that does not match
|
|
617
|
+
it is ignored by routing/cleanup (safe fallthrough) and never reclaimed.
|
|
616
618
|
|
|
617
|
-
**Format:** Markdown file with YAML frontmatter:
|
|
619
|
+
**Format:** Markdown file with YAML frontmatter (canonical field set, task 0711):
|
|
618
620
|
|
|
619
621
|
```yaml
|
|
620
622
|
---
|
|
623
|
+
schema_version: 1
|
|
621
624
|
session_id: "2026-07-01-0167"
|
|
622
625
|
workflow: "task-pipeline"
|
|
623
626
|
run_id: "wf_..."
|
|
624
627
|
task_wbs: "0167"
|
|
625
628
|
feature_id: "I"
|
|
626
629
|
phase: "verify"
|
|
630
|
+
status: "running"
|
|
627
631
|
last_gate: "review-approved"
|
|
628
|
-
|
|
632
|
+
source_commit: "<full 40-hex HEAD at write time>"
|
|
633
|
+
digest: "sha256:..."
|
|
634
|
+
generated_at: "2026-07-01T18:30:00Z"
|
|
635
|
+
updated_at: "2026-07-01T18:30:00Z"
|
|
629
636
|
next_action: "run verification"
|
|
637
|
+
artifacts:
|
|
638
|
+
- .spur/run/0167-verdict.json
|
|
630
639
|
---
|
|
631
640
|
|
|
632
641
|
## Session Notes
|
|
@@ -634,6 +643,17 @@ next_action: "run verification"
|
|
|
634
643
|
<free-form markdown: what was done, what's pending, any blockers>
|
|
635
644
|
```
|
|
636
645
|
|
|
646
|
+
Field semantics (enforced by `parseCheckpointMetadata` / `checkpointStaleness`):
|
|
647
|
+
|
|
648
|
+
- `schema_version` must be `1`; any other value → ignored.
|
|
649
|
+
- `status` is one of `running|pending|approved|done|failed|cancelled|skipped`. A terminal status
|
|
650
|
+
(`done|failed|cancelled|skipped`) marks the checkpoint terminal — never resumed, only cleaned
|
|
651
|
+
up once retention expires.
|
|
652
|
+
- `source_commit` pins repository HEAD at write time; drift staleness is reported, not hidden.
|
|
653
|
+
- `artifacts` lists referenced run files; a missing artifact makes the checkpoint stale.
|
|
654
|
+
- Writers: write after every HITL gate decision, phase transition, and terminal state; overwrite
|
|
655
|
+
the same file on resume (`session_id` = `<date>-<wbs-or-feature>`).
|
|
656
|
+
|
|
637
657
|
**Write checkpoints after:**
|
|
638
658
|
|
|
639
659
|
- Every HITL gate decision (approved/rejected/deferred).
|
|
@@ -652,6 +672,10 @@ next_action: "run verification"
|
|
|
652
672
|
- **Not CLI-gated.** Checkpoint files are written directly by the pipeline's checkpoint action
|
|
653
673
|
(a `shell` step that writes to `.spur/memory/sessions/<session-id>.md`). They do not go through
|
|
654
674
|
`spur task update`.
|
|
675
|
+
- **Canonical schema.** The frontmatter above is the contract (task 0711 R1). Non-canonical or
|
|
676
|
+
malformed checkpoints are ignored by the router and kept (never silently deleted) by cleanup.
|
|
677
|
+
- **Retention.** Terminal checkpoints older than `workflowLogRetentionDays` with no active run are
|
|
678
|
+
reclaimed by `spur workflow clean` (task 0711 R5–R8); non-terminal checkpoints are always kept.
|
|
655
679
|
- **Not a validated corpus.** Checkpoints are working memory. They are overwritten when a session
|
|
656
680
|
resumes and re-checkpoints. They are NOT authoritative task state — the task file is.
|
|
657
681
|
- **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()` |
|
|
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()` |
|
|
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.
|
|
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`
|
|
344
|
-
- **Behavior:** Builds vars from the table above and
|
|
345
|
-
- **Delegation:**
|
|
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
|
|