@gobing-ai/spur 0.3.80 → 0.3.82
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 +31 -18
- package/config/config.global.yaml +13 -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/templates/docs/99_PROJECT_CONSTITUTION.md +75 -13
- package/config/transition-shims.json +0 -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 +12 -3
- 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/inline-run-setup.ts +69 -1
- package/plugins/sp/scripts/quality-gate.mjs +216 -0
- package/plugins/sp/scripts/quality-gate.ts +287 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -2
- package/plugins/sp/scripts/task-size-precheck.ts +44 -16
- package/plugins/sp/scripts/verify-answer-lint.ts +17 -1
- 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/code-review/references/review-lenses.md +3 -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/SKILL.md +4 -8
- package/plugins/sp/skills/spur-cli/references/agent.md +44 -69
- package/plugins/sp/skills/spur-cli/references/message.md +30 -3
- package/plugins/sp/skills/spur-cli/references/projects.md +25 -1
- package/plugins/sp/skills/spur-cli/references/self.md +6 -5
- package/plugins/sp/skills/spur-cli/references/serve.md +8 -7
- package/plugins/sp/skills/spur-cli/references/tasks.md +1 -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/ac-style-guide.md +10 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +4 -6
- package/plugins/sp/skills/spur-dev/references/glossary.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +11 -1
- 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 +105 -85
- package/spur.js +44616 -43320
- package/web/_astro/BoardApp.D-WlxiN2.js +1 -0
- package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.D8bM9pKL.js} +97 -95
- package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.BPRqgVUE.js} +1 -1
- package/web/_astro/{arc.DWEtA3Tx.js → arc.BPrPES3z.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.qX_7q02P.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.CUZfj5V7.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CTaOr8hH.js} +1 -1
- package/web/_astro/channel.DGZaFHZx.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.Dt-9wf3h.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.CTC2sdoN.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.DQcxt2_g.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DXFPSn-a.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.BCx3U4bT.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.DL2tJkdO.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.DZyflMro.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.CUI2mT09.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.g4rX4Fr1.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.CzWJLqp0.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.WgsvQeCR.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.Dzv6ngql.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.CpJ4a9rU.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.CzPlF_dq.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.TAkZNTcQ.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.uCjoKSag.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.eMplIjoK.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.Bf7zoXGz.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.B_bHj3gN.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BasrHRMj.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.C6iphq1x.js} +1 -1
- package/web/_astro/index.DayyIngm.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.HXmDMhW4.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BSmW8NiU.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.DEQow5fo.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.IVm9cTdc.js} +1 -1
- package/web/_astro/{linear.CHXgcIbN.js → linear.CrsM73_9.js} +1 -1
- package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.CfBeDJls.js} +6 -6
- package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.C3j60Y-0.js} +1 -1
- package/web/_astro/ordinal.BYWQX77i.js +1 -0
- package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.B-aCMeEA.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.Cib965yq.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.D61cS4O-.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.GKF2qVPy.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DZnq8F2h.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DXUFmdgM.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.BtHmhLEz.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.Cy-WW2ln.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.SLp5b9KI.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.Bww45mWV.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DR4swI6a.js} +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/favicon.ico +0 -0
- package/web/favicon.svg +17 -4
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/index.html +2 -2
- package/web/site.webmanifest +31 -0
- package/web/spur_logo.svg +1 -0
- package/plugins/sp/skills/spur-cli/references/team.md +0 -145
- 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
|
+
}
|
|
@@ -18,6 +18,9 @@ Five dimensions for structured code review. Each lens has diagnostic questions;
|
|
|
18
18
|
- Are edge cases handled (empty, null, boundary, error)?
|
|
19
19
|
- Are invariants preserved across the changed functions?
|
|
20
20
|
- Is there a test that fails if the behavior is wrong?
|
|
21
|
+
- A changed route response shape or client parser is covered both ways: a server-side key-shape
|
|
22
|
+
assertion and a test that runs a real payload through the real parser (a dropped key like `teamId`
|
|
23
|
+
must fail something). Missing either → P2.
|
|
21
24
|
|
|
22
25
|
**Severity guide:** Logic errors that produce wrong output → P1. Missing edge case that could cause wrong output → P2. Unclear control flow that might hide bugs → P3.
|
|
23
26
|
|
|
@@ -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 |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: spur-cli
|
|
3
|
-
description: "Reference and operate the complete `spur` CLI surface: task, feature, rule, workflow, builder, agent, message,
|
|
3
|
+
description: "Reference and operate the complete `spur` CLI surface: task, feature, rule, workflow, builder, agent, message, self, history, and projects. Use for verb or flag lookup, machine-readable output and exit contracts, or CLI-gated corpus writes. Triggers: \"spur task\", \"spur history\", \"spur projects\", \"create a task\", \"task check\", and any Spur CLI convention. Not for planning or execution lifecycle orchestration (`sp:spur-dev`)."
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: spur
|
|
@@ -17,7 +17,6 @@ metadata:
|
|
|
17
17
|
- builder
|
|
18
18
|
- agent
|
|
19
19
|
- message
|
|
20
|
-
- team
|
|
21
20
|
- self
|
|
22
21
|
- history
|
|
23
22
|
- projects
|
|
@@ -45,9 +44,8 @@ Pick the noun, read its reference. Each Tier A and Tier B reference owns that no
|
|
|
45
44
|
| **Tier A** | **rule** | Constraint quality gate: run presets, author rules, fine-tune, validate rule files/presets, extend engine | [references/rules.md](references/rules.md) |
|
|
46
45
|
| **Tier A** | **workflow** | Dual-mode workflow runtime: author state-machine / transition-flow workflows, validate, run, read traces | [references/workflows.md](references/workflows.md) |
|
|
47
46
|
| **Tier A** | **builder** | Release plumbing: bump a package (or the `workspace:`-pinned set) with `bump-ver`, delete release tags with `drop-tags`, commit + tag + optional push | [references/builder.md](references/builder.md) |
|
|
48
|
-
| **Tier B** | **agent** | Coding-agent execution surface: run prompts via detected/named agents,
|
|
47
|
+
| **Tier B** | **agent** | Coding-agent execution surface: run prompts via detected/named agents, list agent specs, start/stop supervised processes, readiness check | [references/agent.md](references/agent.md) |
|
|
49
48
|
| **Tier B** | **message** | Durable inter-agent messaging: send, inbox, reply, watch | [references/message.md](references/message.md) |
|
|
50
|
-
| **Tier B** | **team** | Team coordination and supervision: assign, status, up/down rosters, start/stop supervised processes | [references/team.md](references/team.md) |
|
|
51
49
|
| **Tier B** | **self** | Self-management verbs: scaffold (`init`), schema migrations (`migrate`), local web server (`serve`), status overview (`status`); `self init` runs post-scaffold validation probes & layout classification | [references/self.md](references/self.md) |
|
|
52
50
|
| **Tier B** | **history** | Import agent histories, aggregate forensic artifacts, render reports, and run the checkpoint-resumed daily pipeline | [references/history.md](references/history.md) |
|
|
53
51
|
| **Tier B** | **projects** | Manage the local multi-project registry and start/stop project servers | [references/projects.md](references/projects.md) |
|
|
@@ -138,13 +136,11 @@ and spreading it; full contract in `docs/04_DESIGN.md` §1.0.1.
|
|
|
138
136
|
|
|
139
137
|
## See also
|
|
140
138
|
|
|
141
|
-
- **[references/agent.md](references/agent.md)** - coding-agent execution surface (`run`, `
|
|
142
|
-
|
|
139
|
+
- **[references/agent.md](references/agent.md)** - coding-agent execution surface (`run`, `wait`,
|
|
140
|
+
`list --specs`, `start`/`stop`). Cross-references the
|
|
143
141
|
[dispatch-surface rule](../parallel-execution/references/dispatch-surface.md).
|
|
144
142
|
- **[references/message.md](references/message.md)** - durable inter-agent messaging (`send`,
|
|
145
143
|
`inbox`, `reply`, `watch`).
|
|
146
|
-
- **[references/team.md](references/team.md)** - team coordination and supervision (`assign`,
|
|
147
|
-
`status`, `up`/`down`, `start`/`stop`).
|
|
148
144
|
- **[references/self.md](references/self.md)** - `spur self init|migrate|serve|status` CLI verbs
|
|
149
145
|
(the four legacy top-level nouns remain hidden aliases). `self init` runs post-scaffold init
|
|
150
146
|
validation (Phase 1.5/1.6 probes).
|