@gobing-ai/spur 0.3.80 → 0.3.81
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/config.example.yaml +29 -18
- package/config/config.global.yaml +10 -11
- package/config/pipeline-budgets.json +34 -2
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/config-loading-ownership.yaml +0 -3
- package/config/rules/boundary/dao-boundary.yaml +4 -17
- package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
- package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
- package/config/rules/boundary/sp-runtime-path.yaml +3 -14
- package/config/rules/quality/coverage-gate.yaml +3 -14
- package/config/rules/quality/tsdoc-exports.yaml +4 -7
- package/config/rules/strict/http-boundaries.yaml +5 -8
- package/config/rules/strict/runtime-boundaries.yaml +1 -5
- package/config/rules/structure/protected-files.yaml +9 -3
- package/config/rules/structure/test-focus-skip.yaml +0 -2
- package/config/rules/structure/test-location.yaml +0 -5
- package/config/rules/surface/check-cli-surface.yaml +3 -2
- package/config/rules/typescript/bun-tooling.yaml +5 -7
- package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
- package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
- package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
- package/config/rules/typescript/no-debugger.yaml +0 -2
- package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
- package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
- package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
- package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
- package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
- package/config/rules/typescript/output-boundaries.yaml +0 -3
- package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
- package/config/rules/ui/ui-import-boundary.yaml +1 -5
- package/config/transition-shims.json +7 -7
- package/config/workflows/basic.yaml +4 -0
- package/config/workflows/docs-pipeline.yaml +13 -14
- package/config/workflows/feature-dev.yaml +20 -65
- package/config/workflows/history-anatomy.yaml +22 -1
- package/config/workflows/idea-pipeline.yaml +53 -97
- package/config/workflows/pr-review.yaml +21 -33
- package/config/workflows/task-pipeline.yaml +87 -330
- package/config/workflows/wayfinder-resolution.yaml +12 -26
- package/config/workflows/wrapup-pipeline.yaml +48 -189
- package/package.json +9 -9
- package/plugins/sp/README.md +10 -1
- package/plugins/sp/agents/expert-spur.md +41 -19
- package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/idea-handoff.mjs +27 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/quality-gate.mjs +165 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
- package/plugins/sp/skills/spur-cli/references/agent.md +56 -14
- package/plugins/sp/skills/spur-cli/references/message.md +30 -3
- package/plugins/sp/skills/spur-cli/references/projects.md +45 -1
- package/plugins/sp/skills/spur-cli/references/self.md +5 -4
- package/plugins/sp/skills/spur-cli/references/serve.md +5 -4
- package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
- package/plugins/sp/skills/spur-cli/references/team.md +21 -1
- package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
- package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
- package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
- package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
- package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
- package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
- package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
- package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
- package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
- package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
- package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
- package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
- package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
- package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
- package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
- package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
- package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
- package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
- package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
- package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
- package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
- package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
- package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
- package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
- package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
- package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
- package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
- package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
- package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
- package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
- package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
- package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
- package/schemas/spur-config.schema.json +49 -0
- package/spur.js +46754 -44121
- package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.B1U26g3I.js} +97 -95
- package/web/_astro/BoardApp.Csgyg-lS.js +1 -0
- package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.DwPqpq7v.js} +1 -1
- package/web/_astro/{arc.DWEtA3Tx.js → arc.CweZEjN2.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.D89pbDuv.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.BOuTeEpX.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CASbkWZF.js} +1 -1
- package/web/_astro/channel.Cx6sXxhq.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.BKQYtOvY.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.9sHLdMtG.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.wOLXWlPs.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DovFbwg3.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.B1Weod1X.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.TEMS04st.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.Cp8VT1wQ.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.BzATdEcP.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.C9BOCfAO.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.C9BOCfAO.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.DUnr4UAw.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.rYq5uM3D.js} +1 -1
- package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.CWeNKe3I.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.DCkfls10.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.D5U4JCka.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.BZJgqaqG.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.DoMeHvPR.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.B50qwwWX.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.DdGPG6LK.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.QP2MJ12u.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BI6LgKSy.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.npPZiC2G.js} +1 -1
- package/web/_astro/index.DayyIngm.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.DCJCBVbp.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BMLV-3I1.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.LE58crde.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.BPbz8rH9.js} +1 -1
- package/web/_astro/{linear.CHXgcIbN.js → linear.DhZaBtYh.js} +1 -1
- package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.BD5-jXum.js} +6 -6
- package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.MTJyrQ65.js} +1 -1
- package/web/_astro/ordinal.BYWQX77i.js +1 -0
- package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.BrDhDvIS.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.71d73_5N.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.Bga6UF-z.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.BnHs4K82.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DsfY2gnj.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DvsTSc9a.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.DxzzmHUR.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.4ZuQmOTt.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.Ck5Q86SG.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.BK7k2hXr.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DfCrgauK.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
- package/web/_astro/channel.BAI6xLeV.js +0 -1
- package/web/_astro/index.Dcr_8fiK.css +0 -1
- package/web/_astro/ordinal.DBvzRdQf.js +0 -1
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* wrapup-steps — deterministic wrap-up capture, metrics and feature sync behind the
|
|
4
|
+
* wrapup-pipeline wrappers (task 0824, feature I21, governance §1.2 composition budgets).
|
|
5
|
+
*
|
|
6
|
+
* Reproduces the former wrapup-pipeline `task-resolve:onEnter:0`, `metrics-record:onEnter:0`
|
|
7
|
+
* and `feature-transition:onEnter:0` shell programs one-for-one so the workflow stays inside
|
|
8
|
+
* the shell-program caps while writing the same `.spur/run` artifacts:
|
|
9
|
+
* - `<runId>-wrapup-tasks.json` normalized, deduplicated WBS capture (resolve)
|
|
10
|
+
* - `<runId>-wrapup-resolve.status` `PASS`/`FAIL` (resolve)
|
|
11
|
+
* - `<runId>-route-reason.txt` route reason (written by the workflow route writer)
|
|
12
|
+
* - `.spur/memory/wrapup-metrics.jsonl` one row per task (metrics)
|
|
13
|
+
* - `<runId>-wrapup-metrics.status` `PASS`/`FAIL` (metrics)
|
|
14
|
+
* - `<runId>-wrapup-sync.status` `PASS`/`FAIL` (feature-transition)
|
|
15
|
+
*
|
|
16
|
+
* Environment comes from the workflow vars: `__runId`, `tasks`, `feature`, `featureGateCmd`
|
|
17
|
+
* and `spurBin` (split on whitespace into a command plus prefix args, so
|
|
18
|
+
* `bun apps/cli/src/index.ts` works).
|
|
19
|
+
*
|
|
20
|
+
* Truthfulness contract (0770 + 0783): wrap-up never mutates task status; a lookup failure is
|
|
21
|
+
* recorded as FAIL, never silently omitted as success; metrics rows are serialized as JSON
|
|
22
|
+
* (never interpolated printf); and the required sync succeeds only for a valid matching
|
|
23
|
+
* unblocked proposal whose target status is freshly observed — a gate PASS can never convert
|
|
24
|
+
* a failed sync into success. The process always exits 0 after resolve/metrics/
|
|
25
|
+
* feature-transition; the verdict lives in the status file (an empty `feature` is a
|
|
26
|
+
* mis-invocation and exits 1).
|
|
27
|
+
*
|
|
28
|
+
* Node-builtin imports only; pure helpers are exported for unit testing (ADR-065).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
|
|
35
|
+
export interface WrapupStepsEnv {
|
|
36
|
+
__runId?: string;
|
|
37
|
+
tasks?: string;
|
|
38
|
+
feature?: string;
|
|
39
|
+
featureGateCmd?: string;
|
|
40
|
+
spurBin?: string;
|
|
41
|
+
[key: string]: string | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WrapupStepsOptions {
|
|
45
|
+
/** Base directory for `.spur/run` / `.spur/memory`; defaults to the process cwd. */
|
|
46
|
+
cwd?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** jq `//` chain: first value that is neither null, undefined nor false; otherwise the fallback. */
|
|
50
|
+
export function jqPick(...values: unknown[]): unknown {
|
|
51
|
+
for (const value of values) {
|
|
52
|
+
if (value !== null && value !== undefined && value !== false) return value;
|
|
53
|
+
}
|
|
54
|
+
return values[values.length - 1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `jq -r` text rendering of a JSON value (strings raw, everything else compact JSON). */
|
|
58
|
+
function jqText(value: unknown): string {
|
|
59
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Canonical four-digit WBS string: whitespace is rejected, not trimmed (0783 R1). */
|
|
63
|
+
export const WBS_PATTERN = /^[0-9]{4}$/;
|
|
64
|
+
|
|
65
|
+
/** `spurBin` splits on whitespace into a command plus prefix args (so `bun x.ts` works). */
|
|
66
|
+
export function spurCommand(spurBin: string | undefined): { cmd: string; prefix: string[] } {
|
|
67
|
+
const parts = (spurBin ?? 'spur')
|
|
68
|
+
.trim()
|
|
69
|
+
.split(/\s+/)
|
|
70
|
+
.filter((p) => p.length > 0);
|
|
71
|
+
return { cmd: parts[0] ?? 'spur', prefix: parts.slice(1) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spur(
|
|
75
|
+
env: WrapupStepsEnv,
|
|
76
|
+
args: string[],
|
|
77
|
+
options: { cwd?: string; stderr?: 'inherit' } = {},
|
|
78
|
+
): { status: number; stdout: string } {
|
|
79
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
80
|
+
const result = spawnSync(cmd, [...prefix, ...args], {
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
...(options.stderr === 'inherit' ? { stdio: ['ignore', 'pipe', 'inherit'] as const } : {}),
|
|
84
|
+
});
|
|
85
|
+
if (result.error !== undefined) return { status: result.status ?? 1, stdout: '' };
|
|
86
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? '' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** jq status-chain semantics for `task show` output; null/missing status means the lookup failed. */
|
|
90
|
+
export function taskStatusOf(taskJson: string): { resolved: unknown; present: boolean } {
|
|
91
|
+
let parsed: unknown;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(taskJson);
|
|
94
|
+
} catch {
|
|
95
|
+
return { resolved: null, present: false };
|
|
96
|
+
}
|
|
97
|
+
if (parsed === null || typeof parsed !== 'object') return { resolved: null, present: false };
|
|
98
|
+
const frontmatter = (parsed as Record<string, unknown>).frontmatter;
|
|
99
|
+
const fmStatus =
|
|
100
|
+
frontmatter !== null && typeof frontmatter === 'object'
|
|
101
|
+
? (frontmatter as Record<string, unknown>).status
|
|
102
|
+
: undefined;
|
|
103
|
+
const status = (parsed as Record<string, unknown>).status;
|
|
104
|
+
// jq: `.frontmatter.status // .status` is null only when frontmatter.status is null/absent
|
|
105
|
+
// AND .status is null; a trailing false is kept (false != null in jq).
|
|
106
|
+
if (fmStatus === null || fmStatus === undefined) {
|
|
107
|
+
return { resolved: status ?? null, present: status !== undefined && status !== null };
|
|
108
|
+
}
|
|
109
|
+
return { resolved: fmStatus, present: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** One truthy status display: `unresolved` when the lookup left nothing to show. */
|
|
113
|
+
function statusDisplay(taskJson: string): string {
|
|
114
|
+
const { resolved } = taskStatusOf(taskJson);
|
|
115
|
+
if (resolved === null || resolved === undefined || resolved === false) return 'unresolved';
|
|
116
|
+
return jqText(resolved);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ResolveResult {
|
|
120
|
+
status: 'PASS' | 'FAIL';
|
|
121
|
+
statusFile: string;
|
|
122
|
+
tasksFile: string;
|
|
123
|
+
/** Exit code the workflow wrapper observes (only an empty __runId is a hard failure). */
|
|
124
|
+
exitCode: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `resolve` — parse and validate vars.tasks exactly once, then resolve every member. */
|
|
128
|
+
export function resolveTasks(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): ResolveResult {
|
|
129
|
+
const cwd = options.cwd;
|
|
130
|
+
const runId = env.__runId ?? '';
|
|
131
|
+
if (runId.length === 0) {
|
|
132
|
+
process.stderr.write('task-resolve: __runId is empty — refusing the legacy fixed-path fallback\n');
|
|
133
|
+
return { status: 'FAIL', statusFile: '', tasksFile: '', exitCode: 1 };
|
|
134
|
+
}
|
|
135
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
136
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
137
|
+
const relReasonFile = join('.spur', 'run', `${runId}-route-reason.txt`);
|
|
138
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-resolve.status`);
|
|
139
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
140
|
+
|
|
141
|
+
const writeFail = (reason: string): ResolveResult => {
|
|
142
|
+
writeFileSync(abs(relReasonFile), reason);
|
|
143
|
+
writeFileSync(abs(relStatusFile), 'FAIL\n');
|
|
144
|
+
return { status: 'FAIL', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
let parsedTasks: unknown;
|
|
148
|
+
try {
|
|
149
|
+
parsedTasks = JSON.parse(env.tasks ?? '');
|
|
150
|
+
} catch {
|
|
151
|
+
parsedTasks = undefined;
|
|
152
|
+
}
|
|
153
|
+
const validArray =
|
|
154
|
+
Array.isArray(parsedTasks) && parsedTasks.every((w) => typeof w === 'string' && WBS_PATTERN.test(w));
|
|
155
|
+
if (!validArray) {
|
|
156
|
+
process.stderr.write(
|
|
157
|
+
'task-resolve: tasks must be a JSON array of canonical four-digit WBS strings (whitespace is rejected, not trimmed)\n',
|
|
158
|
+
);
|
|
159
|
+
return writeFail('failed:tasks is not a JSON array of canonical four-digit WBS strings');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Dedupe in first-seen order (never sorted).
|
|
163
|
+
const deduped: string[] = [];
|
|
164
|
+
for (const wbs of parsedTasks as string[]) {
|
|
165
|
+
if (!deduped.includes(wbs)) deduped.push(wbs);
|
|
166
|
+
}
|
|
167
|
+
writeFileSync(abs(relTasksFile), `${JSON.stringify(deduped)}\n`);
|
|
168
|
+
|
|
169
|
+
let unresolved = false;
|
|
170
|
+
for (const wbs of deduped) {
|
|
171
|
+
const shown = spur(env, ['task', 'show', wbs, '--json'], { cwd });
|
|
172
|
+
const status = shown.status === 0 ? statusDisplay(shown.stdout) : 'unresolved';
|
|
173
|
+
if (status !== 'done' && status !== 'cancelled') {
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
`task-resolve: task ${wbs} did not resolve to a completed status (status=${status})\n`,
|
|
176
|
+
);
|
|
177
|
+
unresolved = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (unresolved) {
|
|
181
|
+
return writeFail(`failed:unresolved or non-completed task (see ${relTasksFile})`);
|
|
182
|
+
}
|
|
183
|
+
writeFileSync(abs(relStatusFile), 'PASS\n');
|
|
184
|
+
return { status: 'PASS', statusFile: relStatusFile, tasksFile: relTasksFile, exitCode: 0 };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface MetricsResult {
|
|
188
|
+
status: 'PASS' | 'FAIL';
|
|
189
|
+
statusFile: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `metrics` — append one JSONL row per captured task; a missing row is never silently absorbed. */
|
|
193
|
+
export function runMetrics(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): MetricsResult {
|
|
194
|
+
const cwd = options.cwd;
|
|
195
|
+
const runId = env.__runId ?? '';
|
|
196
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
197
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'memory') : join('.spur', 'memory'), { recursive: true });
|
|
198
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-metrics.status`);
|
|
199
|
+
const relTasksFile = join('.spur', 'run', `${runId}-wrapup-tasks.json`);
|
|
200
|
+
const relMetricsFile = join('.spur', 'memory', 'wrapup-metrics.jsonl');
|
|
201
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
202
|
+
|
|
203
|
+
const fail = (): MetricsResult => {
|
|
204
|
+
writeFileSync(abs(relStatusFile), 'FAIL\n');
|
|
205
|
+
return { status: 'FAIL', statusFile: relStatusFile };
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let captured: unknown;
|
|
209
|
+
try {
|
|
210
|
+
captured = JSON.parse(readFileSync(abs(relTasksFile), 'utf8'));
|
|
211
|
+
} catch {
|
|
212
|
+
captured = undefined;
|
|
213
|
+
}
|
|
214
|
+
const validCapture = Array.isArray(captured) && captured.every((w) => typeof w === 'string' && WBS_PATTERN.test(w));
|
|
215
|
+
if (!validCapture) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
'metrics-record: run-scoped task capture missing, corrupted or non-canonical — refusing to record metrics\n',
|
|
218
|
+
);
|
|
219
|
+
return fail();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let metricsRc = 0;
|
|
223
|
+
for (const wbs of captured as string[]) {
|
|
224
|
+
const shown = spur(env, ['task', 'show', wbs, '--json'], { cwd });
|
|
225
|
+
const lookup = shown.status === 0 ? taskStatusOf(shown.stdout) : { resolved: null, present: false };
|
|
226
|
+
if (!lookup.present) {
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`metrics-record: task ${wbs} lookup failed or was malformed — recording FAIL instead of silently omitting its metrics row\n`,
|
|
229
|
+
);
|
|
230
|
+
metricsRc = 1;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const parsed = JSON.parse(shown.stdout) as Record<string, unknown>;
|
|
234
|
+
const frontmatter =
|
|
235
|
+
parsed.frontmatter !== null && typeof parsed.frontmatter === 'object'
|
|
236
|
+
? (parsed.frontmatter as Record<string, unknown>)
|
|
237
|
+
: {};
|
|
238
|
+
const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ''));
|
|
239
|
+
const status = String(jqPick(frontmatter.status, parsed.status, 'unknown'));
|
|
240
|
+
|
|
241
|
+
// jq `//` semantics: null and false count as missing; an empty string result stays UNKNOWN.
|
|
242
|
+
let verdict = 'UNKNOWN';
|
|
243
|
+
const verdictPath = join('.spur', 'run', `${wbs}-verdict.json`);
|
|
244
|
+
if (existsSync(abs(verdictPath))) {
|
|
245
|
+
try {
|
|
246
|
+
const raw = jqPick(JSON.parse(readFileSync(abs(verdictPath), 'utf8')).verdict, 'UNKNOWN');
|
|
247
|
+
const text = raw === 'UNKNOWN' ? 'UNKNOWN' : jqText(raw);
|
|
248
|
+
if (text.length > 0) verdict = text;
|
|
249
|
+
} catch {
|
|
250
|
+
// unreadable verdict file keeps UNKNOWN telemetry
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
255
|
+
const row = { wbs, feature_id: featureId, status, verdict, timestamp };
|
|
256
|
+
try {
|
|
257
|
+
appendFileSync(abs(relMetricsFile), `${JSON.stringify(row)}\n`);
|
|
258
|
+
} catch {
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
`metrics-record: metrics append failed for task ${wbs} — recording FAIL instead of claiming the row landed\n`,
|
|
261
|
+
);
|
|
262
|
+
metricsRc = 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const status: 'PASS' | 'FAIL' = metricsRc === 0 ? 'PASS' : 'FAIL';
|
|
266
|
+
writeFileSync(abs(relStatusFile), `${status}\n`);
|
|
267
|
+
return { status, statusFile: relStatusFile };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface FeatureTransitionResult {
|
|
271
|
+
status: 'PASS' | 'FAIL';
|
|
272
|
+
statusFile: string;
|
|
273
|
+
/** Only an empty vars.feature is a hard failure (mis-invocation, not a blocked sync). */
|
|
274
|
+
exitCode: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Classify one sync result per 0783 R4; returns the blocking reason or '' for a verified sync. */
|
|
278
|
+
export function classifySync(
|
|
279
|
+
syncOutput: string,
|
|
280
|
+
syncRc: number,
|
|
281
|
+
feature: string,
|
|
282
|
+
observed: string,
|
|
283
|
+
): { reason: string; applied: string; syncOk: boolean } {
|
|
284
|
+
if (syncRc !== 0) {
|
|
285
|
+
return { reason: `sync exited nonzero (rc=${syncRc})`, applied: 'unreadable', syncOk: false };
|
|
286
|
+
}
|
|
287
|
+
let parsed: unknown;
|
|
288
|
+
try {
|
|
289
|
+
parsed = JSON.parse(syncOutput);
|
|
290
|
+
} catch {
|
|
291
|
+
parsed = undefined;
|
|
292
|
+
}
|
|
293
|
+
const obj = parsed !== null && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : undefined;
|
|
294
|
+
const proposal =
|
|
295
|
+
obj?.proposal !== null && typeof obj?.proposal === 'object'
|
|
296
|
+
? (obj.proposal as Record<string, unknown>)
|
|
297
|
+
: undefined;
|
|
298
|
+
const shapeOk =
|
|
299
|
+
obj !== undefined &&
|
|
300
|
+
proposal !== undefined &&
|
|
301
|
+
typeof proposal.featureId === 'string' &&
|
|
302
|
+
typeof proposal.from === 'string' &&
|
|
303
|
+
typeof proposal.to === 'string' &&
|
|
304
|
+
typeof obj.applied === 'boolean';
|
|
305
|
+
if (!shapeOk) {
|
|
306
|
+
return { reason: 'malformed or unreadable sync result', applied: 'unreadable', syncOk: false };
|
|
307
|
+
}
|
|
308
|
+
const applied = obj.applied === true ? 'true' : 'false';
|
|
309
|
+
const pFrom = String(jqPick(proposal.from, ''));
|
|
310
|
+
const pTo = String(jqPick(proposal.to, ''));
|
|
311
|
+
if (String(jqPick(proposal.featureId, '')) !== feature) {
|
|
312
|
+
return { reason: `sync proposal does not match feature ${feature}`, applied, syncOk: false };
|
|
313
|
+
}
|
|
314
|
+
if (proposal.gateBlocked === true) {
|
|
315
|
+
return {
|
|
316
|
+
reason: 'sync proposal is gate-blocked — a blocked sync is not a no-change success',
|
|
317
|
+
applied,
|
|
318
|
+
syncOk: false,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (proposal.requiresConfirm === true) {
|
|
322
|
+
return { reason: 'sync proposal requires operator confirmation', applied, syncOk: false };
|
|
323
|
+
}
|
|
324
|
+
if (applied === 'true' && observed !== pTo) {
|
|
325
|
+
return {
|
|
326
|
+
reason: `applied sync did not land on the proposal target (observed=${observed}, to=${pTo})`,
|
|
327
|
+
applied,
|
|
328
|
+
syncOk: false,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (applied === 'false' && (pFrom !== pTo || observed !== pTo)) {
|
|
332
|
+
return {
|
|
333
|
+
reason: `sync applied nothing without a from==to observed no-op (from=${pFrom}, to=${pTo}, observed=${observed})`,
|
|
334
|
+
applied,
|
|
335
|
+
syncOk: false,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return { reason: '', applied, syncOk: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** `feature-transition` — required bounded sync, observation and the affected-feature gate. */
|
|
342
|
+
export function runFeatureTransition(env: WrapupStepsEnv, options: WrapupStepsOptions = {}): FeatureTransitionResult {
|
|
343
|
+
const cwd = options.cwd;
|
|
344
|
+
const runId = env.__runId ?? '';
|
|
345
|
+
const feature = env.feature ?? '';
|
|
346
|
+
mkdirSync(cwd ? join(cwd, '.spur', 'run') : join('.spur', 'run'), { recursive: true });
|
|
347
|
+
const relStatusFile = join('.spur', 'run', `${runId}-wrapup-sync.status`);
|
|
348
|
+
const abs = (p: string): string => (cwd ? join(cwd, p) : p);
|
|
349
|
+
if (feature.length === 0) {
|
|
350
|
+
process.stderr.write(
|
|
351
|
+
'feature-transition: vars.feature is empty — refusing no-op feature sync (mis-invocation, not a blocked sync)\n',
|
|
352
|
+
);
|
|
353
|
+
return { status: 'FAIL', statusFile: relStatusFile, exitCode: 1 };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Three branches, first that exists relative to the cwd; current arguments including --spur-bin.
|
|
357
|
+
let syncOutput = '';
|
|
358
|
+
let syncRc = 1;
|
|
359
|
+
const boundedTs = join('plugins', 'sp', 'scripts', 'feature-sync-bounded.ts');
|
|
360
|
+
const boundedArgs = [feature, '--spur-bin', env.spurBin ?? 'spur', '--json'];
|
|
361
|
+
if (existsSync(cwd ? join(cwd, boundedTs) : boundedTs)) {
|
|
362
|
+
const result = spawnSync('bun', [boundedTs, ...boundedArgs], { cwd, encoding: 'utf8' });
|
|
363
|
+
syncOutput = result.stdout ?? '';
|
|
364
|
+
syncRc = result.status ?? 1;
|
|
365
|
+
if (result.stderr !== null && result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
366
|
+
} else {
|
|
367
|
+
const probe = spawnSync('superskill', ['script', 'path', 'sp', 'feature-sync-bounded.mjs'], {
|
|
368
|
+
cwd,
|
|
369
|
+
encoding: 'utf8',
|
|
370
|
+
});
|
|
371
|
+
const twin = probe.status === 0 ? (probe.stdout ?? '').trim() : '';
|
|
372
|
+
if (twin.length > 0 && existsSync(twin)) {
|
|
373
|
+
const result = spawnSync('node', [twin, ...boundedArgs], { cwd, encoding: 'utf8' });
|
|
374
|
+
syncOutput = result.stdout ?? '';
|
|
375
|
+
syncRc = result.status ?? 1;
|
|
376
|
+
if (result.stderr !== null && result.stderr.length > 0) process.stderr.write(result.stderr);
|
|
377
|
+
} else {
|
|
378
|
+
// Last branch: stderr streams through (visible), stdout is the JSON payload.
|
|
379
|
+
const result = spur(env, ['feature', 'sync', feature, '--json'], { cwd, stderr: 'inherit' });
|
|
380
|
+
syncOutput = result.stdout;
|
|
381
|
+
syncRc = result.status;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
process.stdout.write(`${syncOutput}\n`);
|
|
385
|
+
|
|
386
|
+
const shown = spur(env, ['feature', 'show', feature, '--json'], { cwd });
|
|
387
|
+
let observed = '';
|
|
388
|
+
if (shown.status === 0) {
|
|
389
|
+
try {
|
|
390
|
+
const parsed = JSON.parse(shown.stdout) as Record<string, unknown>;
|
|
391
|
+
const frontmatter =
|
|
392
|
+
parsed.frontmatter !== null && typeof parsed.frontmatter === 'object'
|
|
393
|
+
? (parsed.frontmatter as Record<string, unknown>)
|
|
394
|
+
: {};
|
|
395
|
+
const picked = jqPick(parsed.status, frontmatter.status, '');
|
|
396
|
+
observed = picked === '' ? '' : jqText(picked);
|
|
397
|
+
} catch {
|
|
398
|
+
observed = '';
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (observed.length === 0) observed = 'unreadable';
|
|
402
|
+
|
|
403
|
+
const classified = classifySync(syncOutput, syncRc, feature, observed);
|
|
404
|
+
const { reason, applied } = classified;
|
|
405
|
+
const syncOk = classified.syncOk;
|
|
406
|
+
|
|
407
|
+
let gate = 'skipped';
|
|
408
|
+
if (applied === 'true' || syncRc !== 0) {
|
|
409
|
+
process.stdout.write(
|
|
410
|
+
`feature-transition: sync applied or failed after a possible partial transition for ${feature} — running feature gate: ${env.featureGateCmd ?? ''}\n`,
|
|
411
|
+
);
|
|
412
|
+
const gateResult = spawnSync('sh', ['-c', env.featureGateCmd ?? ''], { cwd, stdio: 'inherit' });
|
|
413
|
+
if ((gateResult.status ?? 1) === 0) {
|
|
414
|
+
gate = 'PASS';
|
|
415
|
+
process.stdout.write(`feature-transition: feature gate PASS for feature ${feature}\n`);
|
|
416
|
+
} else {
|
|
417
|
+
gate = 'FAIL';
|
|
418
|
+
process.stderr.write(
|
|
419
|
+
`feature-transition: feature gate FAIL for feature ${feature} — inspect findings before reporting the transition complete\n`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
process.stdout.write(
|
|
424
|
+
`feature-transition: sync did not apply a transition (rc=${syncRc}, applied=${applied}) — feature gate skipped\n`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let syncStatus: 'PASS' | 'FAIL';
|
|
429
|
+
if (!syncOk || gate === 'FAIL') {
|
|
430
|
+
syncStatus = 'FAIL';
|
|
431
|
+
process.stderr.write(
|
|
432
|
+
`feature-transition: required synchronization failed for ${feature} — ${reason}; gate=${gate}\n`,
|
|
433
|
+
);
|
|
434
|
+
} else if (applied === 'false') {
|
|
435
|
+
syncStatus = 'PASS';
|
|
436
|
+
process.stdout.write(
|
|
437
|
+
`feature-transition: feature sync verified for ${feature} (from==to observed at ${observed}, gate=${gate}) — explicit no-change\n`,
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
syncStatus = 'PASS';
|
|
441
|
+
process.stdout.write(
|
|
442
|
+
`feature-transition: feature sync verified for ${feature} (applied, observed=${observed}, gate=${gate})\n`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
writeFileSync(abs(relStatusFile), `${syncStatus}\n`);
|
|
446
|
+
return { status: syncStatus, statusFile: relStatusFile, exitCode: 0 };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export const WRAPUP_STEPS_USAGE =
|
|
450
|
+
'usage: wrapup-steps.ts <resolve|metrics|feature-transition> (env: __runId, tasks, feature, featureGateCmd, spurBin)';
|
|
451
|
+
|
|
452
|
+
export function main(argv: string[], env: WrapupStepsEnv = process.env, options: WrapupStepsOptions = {}): number {
|
|
453
|
+
const sub = argv[0];
|
|
454
|
+
if (sub === 'resolve') return resolveTasks(env, options).exitCode;
|
|
455
|
+
if (sub === 'metrics') {
|
|
456
|
+
runMetrics(env, options);
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
if (sub === 'feature-transition') return runFeatureTransition(env, options).exitCode;
|
|
460
|
+
process.stderr.write(`${WRAPUP_STEPS_USAGE}\n`);
|
|
461
|
+
return 2;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (import.meta.main) {
|
|
465
|
+
process.exit(main(process.argv.slice(2)));
|
|
466
|
+
}
|
|
@@ -128,7 +128,7 @@ rule at the source, so no per-path shim is needed:
|
|
|
128
128
|
| `spur agent run` (CLI) | `AgentService.run` → resolution → child process | Declared wins; absent inherits via `SPUR_ROLE`; envelope carries `roleOrigin` |
|
|
129
129
|
| Workflow `agent.run` step | `AgentRunActionRunner` → `AgentService.runTraced` | Step `role:` is **mandatory** (0538 R2, `agent-run.ts` fails a role-less step before dispatch) — always a declaration (`roleOrigin: 'declared'`); inheritance applies at the next fan-out boundary the step's subagent itself dispatches |
|
|
130
130
|
| `spur agent loop` | `AgentService.run` per drained iteration | Same resolution path as `spur agent run`; inherits its own `SPUR_ROLE` |
|
|
131
|
-
| `spur
|
|
131
|
+
| `spur serve` team supervisor → member | spawns `spur agent loop` | Member inherits the supervisor's `SPUR_ROLE` (recursive by construction) |
|
|
132
132
|
| Native subagent fan-out (this skill's default) | in-session `Task()`/`Skill()` | In-session subagents share the host session; when they themselves dispatch, the host's role is already in the session env — the rule holds at the next `spur agent run` boundary |
|
|
133
133
|
| `plugins/sp/evals/run-eval.ts` | `spawnSync('spur agent run', …)` per scenario | Out of scope: a top-level eval harness, not a fan-out — no dispatcher role exists to inherit; each scenario is an independent top-level run (documented, no shim) |
|
|
134
134
|
|
|
@@ -525,6 +525,35 @@ The payload is a top-level JSON **array** (no `tasks` wrapper):
|
|
|
525
525
|
]
|
|
526
526
|
```
|
|
527
527
|
|
|
528
|
+
## Idea-pipeline emission
|
|
529
|
+
|
|
530
|
+
When the idea-pipeline workflow dispatches you for a feature, read the brainstorm artifact, the
|
|
531
|
+
feature AC, and the design doc, then emit two run-scoped artifacts.
|
|
532
|
+
|
|
533
|
+
**Sizing first, before any JSON.** Apply the `Default to NOT decomposing` rubric to the whole
|
|
534
|
+
unit of work — if it scores 0–2 the correct output is a ONE-entry batch, not many.
|
|
535
|
+
|
|
536
|
+
**Scenario count is not task count.** Merge scenarios that one task delivers (same file surface,
|
|
537
|
+
same subsystem, or unreadable apart in review), and list every scenario a task covers in its
|
|
538
|
+
background. Merging never costs AC coverage — one task may carry several scenarios. Do not emit
|
|
539
|
+
one entry per scenario or per requirement by reflex.
|
|
540
|
+
|
|
541
|
+
**The batch.** Produce a task-batch JSON array at the workflow-provided batch path
|
|
542
|
+
(`.spur/run/<runId>-idea-task-batch.json`), validated against `task-batch.schema.json`.
|
|
543
|
+
Schema-permitted fields per entry: `name`, `background`, `requirements`, `design`, `plan`,
|
|
544
|
+
`acceptance_criteria`, `feature_id`, `parent_wbs`, `priority`, `tags`, `template` — schema
|
|
545
|
+
validation rejects anything else. `design`, `plan`, and `acceptance_criteria` are supported batch
|
|
546
|
+
fields and normal default planning fills them from your analysis; the per-task refine step after
|
|
547
|
+
batch-create still deepens them when a task needs more detail. Validate locally against the
|
|
548
|
+
schema before emitting.
|
|
549
|
+
|
|
550
|
+
**The order sidecar.** Also emit the private task-order sidecar at
|
|
551
|
+
`.spur/run/<runId>-idea-task-order.json`: a JSON array (one entry per batch item) of
|
|
552
|
+
`{ name: <exact batch item name>, depends_on_names: [<batch item names>] }` declaring
|
|
553
|
+
ordering/dependencies between the batch items; state `depends_on_names: []` per item when no
|
|
554
|
+
ordering exists. Every `name` and every dependency must match exactly one batch item `name` —
|
|
555
|
+
it is private workflow data, not part of task-batch.schema.json.
|
|
556
|
+
|
|
528
557
|
## Common schema violations
|
|
529
558
|
|
|
530
559
|
| Violation | Fix |
|
|
@@ -24,11 +24,13 @@ that before using `run` for fan-out dispatch.
|
|
|
24
24
|
| `run <prompt>` | Execute a prompt or slash command via a coding agent | `--agent <name>` `--spec <id>` `--model <name>` `--mode <mode>` `--continue` `--cwd <path>` `--drain` `--json` |
|
|
25
25
|
| `loop` | Persistent self-draining inbox loop for a team member (supervisor-managed) | `--spec <id>` `--agent <id>` `--poll <ms>` |
|
|
26
26
|
| `wait [<specId>]` | Identity-pinned wait for an occupant run to reach a lifecycle state (G4 wave 2; `--role` selector per 0685) | `--role <name>` `--run <runId>` `--until <state>...` `--timeout <ms>` `--json` |
|
|
27
|
-
| `list` | List detected coding agents, or team agent specs with `--specs` | `--specs` `--json` |
|
|
27
|
+
| `list` | List detected coding agents, or team agent specs with `--specs` (live run status merged from `spur serve`) | `--specs` `--server <url>` `--json` |
|
|
28
28
|
| `doctor [agent]` | Check agent readiness | `--json` `--probe-health` `--force-refresh` |
|
|
29
29
|
| `create <id>` | Write a team agent spec to `.spur/agents/<id>.yaml` | `--type` `--tags` `--model` `--autonomy` `--system-prompt` `--name` `--workspace` `--purpose` `--auto-start` `--no-identity-preamble` `--json` |
|
|
30
30
|
| `edit <id>` | Open an agent spec in `$EDITOR`, or print its path | - |
|
|
31
31
|
| `delete <id>` | Remove an agent spec | `--force` |
|
|
32
|
+
| `start <spec-id>` | Start a supervised agent process (requires `spur serve`; 0848 moved home of `spur team start`) | `--server <url>` `--json` |
|
|
33
|
+
| `stop <spec-id>` | Stop a supervised agent process (requires `spur serve`; 0848 moved home of `spur team stop`) | `--server <url>` `--json` |
|
|
32
34
|
|
|
33
35
|
`list`, `doctor`, `run`, `wait`, and `create` accept `--json` plus `--json-envelope`. `loop`, `edit`,
|
|
34
36
|
and `delete` are human/process-control surfaces. **Exit codes:** `0` success, `1` failure, and `2`
|
|
@@ -54,7 +56,7 @@ through a coding agent as an external process, producing a persisted run record
|
|
|
54
56
|
| `--mode <mode>` | Agent output mode: `text` or `json`. |
|
|
55
57
|
| `--continue` | Resume the previous agent session instead of starting fresh. |
|
|
56
58
|
| `--cwd <path>` | Working directory for agent execution (default: current directory). |
|
|
57
|
-
| `--spec <id>` | Team agent spec id (occupant addressing, 0542 R1). Pairs with `--drain`; with `--spec` alone the run is addressed to the occupant without touching the inbox. A legacy `--agent <spec-id>` still
|
|
59
|
+
| `--spec <id>` | Team agent spec id (occupant addressing, 0542 R1). Pairs with `--drain`; with `--spec` alone the run is addressed to the occupant without touching the inbox. A legacy `--agent <spec-id>` is still accepted as fallback addressing (task 0849 retired the `agent-flag-spec-id` deprecation warning). |
|
|
58
60
|
| `--drain` | Prepend pending inbox messages addressed to `--spec <id>` before the prompt. |
|
|
59
61
|
| `--json` | Output machine-readable JSON where supported. |
|
|
60
62
|
| `--json-envelope` | Wrap JSON using the facade's standard output contract. |
|
|
@@ -89,9 +91,13 @@ justify it - but ensure the run executes in a context that can write the target
|
|
|
89
91
|
spur agent loop --agent worker-1 --poll 2000
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
`loop` is the **persistent self-draining wrapper** used by the team supervisor. It
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
`loop` is the **persistent self-draining wrapper** used by the team supervisor. It waits for a
|
|
95
|
+
wake on the `system_events` ledger — a human request (`message.sent`), a strategy change
|
|
96
|
+
(`strategy.changed`), a capacity change (`fleet.capacity.changed`), or a completion receipt
|
|
97
|
+
(`agent.invoke.exit`) — then drains the inbox into an `agent run` invocation. An idle wake
|
|
98
|
+
records the hold reason instead of dispatching; with no wake event at all it still drains every
|
|
99
|
+
`--poll` ms (backstop). It
|
|
100
|
+
between drains. It is not typically invoked directly by the operator - `spur agent start` launches it
|
|
95
101
|
under supervision.
|
|
96
102
|
|
|
97
103
|
### Flags
|
|
@@ -99,10 +105,10 @@ under supervision.
|
|
|
99
105
|
| Flag | Purpose |
|
|
100
106
|
|------|---------|
|
|
101
107
|
| `--spec <id>` | **Required.** Team agent spec id / message recipient (0542 R1; legacy `--agent <spec-id>` still read with a one-time warning). |
|
|
102
|
-
| `--poll <ms>` |
|
|
108
|
+
| `--poll <ms>` | Wakeup backstop timeout in milliseconds — drains at least this often (default: `2000`). |
|
|
103
109
|
|
|
104
110
|
The loop runs until `SIGINT` / `SIGTERM`. Each iteration: check inbox -> if messages, drain each
|
|
105
|
-
into `run` with `--drain` -> else
|
|
111
|
+
into `run` with `--drain` -> else record the idle hold (an empty drain dispatches nothing).
|
|
106
112
|
|
|
107
113
|
## `wait` - identity-pinned occupant wait (G4 wave 2)
|
|
108
114
|
|
|
@@ -152,7 +158,17 @@ spur agent list --json # machine-readable
|
|
|
152
158
|
```
|
|
153
159
|
|
|
154
160
|
Without `--specs`, lists coding agents detected on the host (by binary on `PATH`). With `--specs`,
|
|
155
|
-
lists team agent specs (`.spur/agents/*.yaml`)
|
|
161
|
+
lists team agent specs (`.spur/agents/*.yaml`) **with live run status merged from the server's
|
|
162
|
+
supervisor** (0848, the moved home of `spur team status`): each row carries a trailing status column
|
|
163
|
+
(`running` / `stopped` / `errored` / `unknown`) and `pid=<n>` where a process exists. When `spur serve`
|
|
164
|
+
is unreachable, the listing falls back to all `stopped` with a stderr warning. `--server <url>`
|
|
165
|
+
(default `http://localhost:3000/api`) targets the supervisor API.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
spur agent list --specs
|
|
169
|
+
# planner claude reviewer claude plans the work running pid=4132
|
|
170
|
+
# worker-1 pi worker pi implements stopped
|
|
171
|
+
```
|
|
156
172
|
|
|
157
173
|
## `doctor` - readiness check
|
|
158
174
|
|
|
@@ -176,7 +192,8 @@ spur agent create reviewer --type codex --autonomy review --auto-start
|
|
|
176
192
|
```
|
|
177
193
|
|
|
178
194
|
Writes a team agent spec to `.spur/agents/<id>.yaml`. The spec captures the agent's identity
|
|
179
|
-
(type, model, autonomy, system prompt, tags) so
|
|
195
|
+
(type, model, autonomy, system prompt, tags) so the fleet declaration (`.spur/fleet.json`, converted
|
|
196
|
+
by `spur projects migrate`) can materialize a roster and `spur
|
|
180
197
|
agent loop` can self-drain its inbox.
|
|
181
198
|
|
|
182
199
|
### Flags
|
|
@@ -191,7 +208,7 @@ agent loop` can self-drain its inbox.
|
|
|
191
208
|
| `--name <name>` | Agent display name. |
|
|
192
209
|
| `--workspace <path>` | Workspace path for this agent. |
|
|
193
210
|
| `--purpose <text>` | Team identity purpose. |
|
|
194
|
-
| `--auto-start` | Auto-start flag (
|
|
211
|
+
| `--auto-start` | Auto-start flag (started by the supervisor when serve materializes the fleet; without it, start manually with `spur agent start`). |
|
|
195
212
|
| `--no-identity-preamble` | Disable the identity preamble prepended to prompts. |
|
|
196
213
|
| `--json` | Output machine-readable JSON. |
|
|
197
214
|
|
|
@@ -211,20 +228,45 @@ spur agent delete worker-1 --force
|
|
|
211
228
|
|
|
212
229
|
`--force` is required (guards against accidental deletion). Removes `.spur/agents/<id>.yaml`.
|
|
213
230
|
|
|
231
|
+
## `start` - start a supervised process (0848)
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
spur agent start worker-1
|
|
235
|
+
spur agent start worker-1 --json
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The moved home of `spur team start`. Posts to the `spur serve` supervisor API
|
|
239
|
+
(`POST /api/team/agents/:id/start`) and prints `started <id> (pid=<n>, status=<s>)`. Requires a
|
|
240
|
+
reachable `spur serve`; `--server <url>` (default `http://localhost:3000/api`) targets it. Exit `1`
|
|
241
|
+
when the server is unreachable or the start fails.
|
|
242
|
+
|
|
243
|
+
## `stop` - stop a supervised process (0848)
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
spur agent stop worker-1
|
|
247
|
+
spur agent stop worker-1 --json
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The moved home of `spur team stop`. Posts to the supervisor API
|
|
251
|
+
(`POST /api/team/agents/:id/stop`) and prints `stopped <id>`. Same server requirement and flags as
|
|
252
|
+
`start`. `spur agent delete` (with `--force`) remains the spec-removal counterpart of the old
|
|
253
|
+
`team down --purge`.
|
|
254
|
+
|
|
214
255
|
## What this skill is NOT
|
|
215
256
|
|
|
216
257
|
- **Not the dispatch decision.** *When* to use `spur agent run` vs a native subagent is the
|
|
217
258
|
**[dispatch-surface rule](../../parallel-execution/references/dispatch-surface.md)**, not this
|
|
218
259
|
reference. This reference documents the verbs; that rule decides which surface carries a dispatch.
|
|
219
|
-
- **Not the team orchestrator.** `spur
|
|
220
|
-
`
|
|
260
|
+
- **Not the team orchestrator.** The `spur serve` supervisor drives the lifecycle: `spur agent
|
|
261
|
+
start` / `stop` manage supervised processes and `agent list --specs` reports live state (0848
|
|
262
|
+
moved these homes off the deprecated `spur team` noun).
|
|
221
263
|
|
|
222
264
|
## See also
|
|
223
265
|
|
|
224
266
|
- **[dispatch-surface.md](../../parallel-execution/references/dispatch-surface.md)** - native
|
|
225
267
|
subagent vs `spur agent run` decision rule. `--model` and `--agent` are its escalation levers.
|
|
226
|
-
- **`spur team` (see [team.md](team.md))** - team
|
|
227
|
-
|
|
268
|
+
- **`spur team` (see [team.md](team.md))** - deprecated team noun (0848); its verbs moved to this
|
|
269
|
+
noun (`start`/`stop`/`list --specs`) and to `spur task update --assignee`.
|
|
228
270
|
- **`spur message` (see [message.md](message.md))** - the inbox `--drain` reads from.
|
|
229
271
|
- **`sp:spur-cli`** SKILL.md - the facade that routes to this reference.
|
|
230
272
|
|