@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.
Files changed (158) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +29 -18
  3. package/config/config.global.yaml +10 -11
  4. package/config/pipeline-budgets.json +34 -2
  5. package/config/plugin-scripts.json +25 -0
  6. package/config/rules/boundary/config-loading-ownership.yaml +0 -3
  7. package/config/rules/boundary/dao-boundary.yaml +4 -17
  8. package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
  9. package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
  10. package/config/rules/boundary/sp-runtime-path.yaml +3 -14
  11. package/config/rules/quality/coverage-gate.yaml +3 -14
  12. package/config/rules/quality/tsdoc-exports.yaml +4 -7
  13. package/config/rules/strict/http-boundaries.yaml +5 -8
  14. package/config/rules/strict/runtime-boundaries.yaml +1 -5
  15. package/config/rules/structure/protected-files.yaml +9 -3
  16. package/config/rules/structure/test-focus-skip.yaml +0 -2
  17. package/config/rules/structure/test-location.yaml +0 -5
  18. package/config/rules/surface/check-cli-surface.yaml +3 -2
  19. package/config/rules/typescript/bun-tooling.yaml +5 -7
  20. package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
  21. package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
  22. package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
  23. package/config/rules/typescript/no-debugger.yaml +0 -2
  24. package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
  25. package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
  26. package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
  27. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
  28. package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
  29. package/config/rules/typescript/output-boundaries.yaml +0 -3
  30. package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
  31. package/config/rules/ui/ui-import-boundary.yaml +1 -5
  32. package/config/transition-shims.json +7 -7
  33. package/config/workflows/basic.yaml +4 -0
  34. package/config/workflows/docs-pipeline.yaml +13 -14
  35. package/config/workflows/feature-dev.yaml +20 -65
  36. package/config/workflows/history-anatomy.yaml +22 -1
  37. package/config/workflows/idea-pipeline.yaml +53 -97
  38. package/config/workflows/pr-review.yaml +21 -33
  39. package/config/workflows/task-pipeline.yaml +87 -330
  40. package/config/workflows/wayfinder-resolution.yaml +12 -26
  41. package/config/workflows/wrapup-pipeline.yaml +48 -189
  42. package/package.json +9 -9
  43. package/plugins/sp/README.md +10 -1
  44. package/plugins/sp/agents/expert-spur.md +41 -19
  45. package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
  46. package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
  47. package/plugins/sp/plugin.json +1 -1
  48. package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
  49. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  50. package/plugins/sp/scripts/idea-handoff.mjs +27 -0
  51. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  52. package/plugins/sp/scripts/quality-gate.mjs +165 -0
  53. package/plugins/sp/scripts/quality-gate.ts +217 -0
  54. package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
  55. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  56. package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
  57. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
  58. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  59. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
  60. package/plugins/sp/skills/spur-cli/references/agent.md +56 -14
  61. package/plugins/sp/skills/spur-cli/references/message.md +30 -3
  62. package/plugins/sp/skills/spur-cli/references/projects.md +45 -1
  63. package/plugins/sp/skills/spur-cli/references/self.md +5 -4
  64. package/plugins/sp/skills/spur-cli/references/serve.md +5 -4
  65. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  66. package/plugins/sp/skills/spur-cli/references/team.md +21 -1
  67. package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
  68. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
  69. package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
  70. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
  71. package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
  72. package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
  73. package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
  74. package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
  75. package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
  76. package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
  77. package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
  78. package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
  79. package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
  80. package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
  81. package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
  82. package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
  83. package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
  84. package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
  85. package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
  86. package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
  87. package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
  88. package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
  89. package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
  90. package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
  91. package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
  92. package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
  93. package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
  94. package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
  95. package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
  96. package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
  97. package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
  98. package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
  99. package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
  100. package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
  101. package/schemas/spur-config.schema.json +49 -0
  102. package/spur.js +46754 -44121
  103. package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.B1U26g3I.js} +97 -95
  104. package/web/_astro/BoardApp.Csgyg-lS.js +1 -0
  105. package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.DwPqpq7v.js} +1 -1
  106. package/web/_astro/{arc.DWEtA3Tx.js → arc.CweZEjN2.js} +1 -1
  107. package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.D89pbDuv.js} +1 -1
  108. package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.BOuTeEpX.js} +1 -1
  109. package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CASbkWZF.js} +1 -1
  110. package/web/_astro/channel.Cx6sXxhq.js +1 -0
  111. package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.BKQYtOvY.js} +1 -1
  112. package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.9sHLdMtG.js} +1 -1
  113. package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.wOLXWlPs.js} +1 -1
  114. package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DovFbwg3.js} +1 -1
  115. package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.B1Weod1X.js} +1 -1
  116. package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.TEMS04st.js} +1 -1
  117. package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.Cp8VT1wQ.js} +1 -1
  118. package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.BzATdEcP.js} +1 -1
  119. package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.C9BOCfAO.js} +1 -1
  120. package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.C9BOCfAO.js} +1 -1
  121. package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.DUnr4UAw.js} +1 -1
  122. package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.rYq5uM3D.js} +1 -1
  123. package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
  124. package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.CWeNKe3I.js} +1 -1
  125. package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.DCkfls10.js} +1 -1
  126. package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.D5U4JCka.js} +1 -1
  127. package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.BZJgqaqG.js} +1 -1
  128. package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.DoMeHvPR.js} +1 -1
  129. package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.B50qwwWX.js} +1 -1
  130. package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.DdGPG6LK.js} +1 -1
  131. package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.QP2MJ12u.js} +1 -1
  132. package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BI6LgKSy.js} +1 -1
  133. package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.npPZiC2G.js} +1 -1
  134. package/web/_astro/index.DayyIngm.css +1 -0
  135. package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.DCJCBVbp.js} +1 -1
  136. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BMLV-3I1.js} +1 -1
  137. package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.LE58crde.js} +1 -1
  138. package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.BPbz8rH9.js} +1 -1
  139. package/web/_astro/{linear.CHXgcIbN.js → linear.DhZaBtYh.js} +1 -1
  140. package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.BD5-jXum.js} +6 -6
  141. package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.MTJyrQ65.js} +1 -1
  142. package/web/_astro/ordinal.BYWQX77i.js +1 -0
  143. package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.BrDhDvIS.js} +1 -1
  144. package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.71d73_5N.js} +1 -1
  145. package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.Bga6UF-z.js} +1 -1
  146. package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.BnHs4K82.js} +1 -1
  147. package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DsfY2gnj.js} +1 -1
  148. package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DvsTSc9a.js} +1 -1
  149. package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.DxzzmHUR.js} +1 -1
  150. package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.4ZuQmOTt.js} +1 -1
  151. package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.Ck5Q86SG.js} +1 -1
  152. package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.BK7k2hXr.js} +1 -1
  153. package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DfCrgauK.js} +1 -1
  154. package/web/index.html +2 -2
  155. package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
  156. package/web/_astro/channel.BAI6xLeV.js +0 -1
  157. package/web/_astro/index.Dcr_8fiK.css +0 -1
  158. 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 team` supervisor → member | spawns `spur agent loop` | Member inherits the supervisor's `SPUR_ROLE` (recursive by construction) |
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 works during the transition with a one-time warning (shim `agent-flag-spec-id`). |
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 polls the
93
- addressed agent's inbox, drains each pending message into an `agent run` invocation, and idles
94
- between drains. It is not typically invoked directly by the operator - `spur team start` launches it
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>` | Idle poll interval in milliseconds (default: `2000`). |
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 sleep for `--poll` ms.
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 `spur team up` can materialize a roster and `spur
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 (start on `team up` without manual `team start`). |
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 team up` / `spur team start` drive the supervisor lifecycle;
220
- `spur agent` provides the execution primitives they compose.
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 lifecycle that launches `agent loop` under
227
- supervision.
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