@gobing-ai/spur 0.3.72 → 0.3.73
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 +33 -23
- package/config/corpus-baseline.json +168 -0
- package/config/pipeline-budgets.json +7 -3
- package/config/plugin-scripts.json +4 -0
- package/config/proportional-route-table.ts +155 -0
- package/config/rules/structure/protected-files.yaml +3 -0
- package/config/task-pipeline-proportional-migration-plan.md +79 -0
- package/config/workflow-composition-baseline.json +124 -85
- package/config/workflows/docs-pipeline.yaml +11 -1
- package/config/workflows/feature-dev.yaml +27 -20
- package/config/workflows/task-lifecycle.yaml +27 -15
- package/config/workflows/task-pipeline.yaml +72 -11
- package/config/workflows/wrapup-pipeline.yaml +100 -42
- package/package.json +9 -9
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +2 -4
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +2 -4
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +1 -1
- package/plugins/sp/scripts/verify-answer-lint.ts +4 -0
- package/plugins/sp/skills/spur-cli/references/workflows/authoring-workflows.md +23 -0
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +20 -0
- package/schemas/state-machine-workflow.schema.json +3 -1
- package/schemas/transition-flow-workflow.schema.json +3 -1
- package/spur.js +12471 -10279
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* inline-pipeline-parity-check — two-sided gate between the inline pipeline
|
|
4
|
+
* driver's documented action/guard set and the resolved action/guard sets in
|
|
5
|
+
* `.spur/workflows/task-pipeline.yaml` and `.spur/workflows/idea-pipeline.yaml`
|
|
6
|
+
* (task 0755 R2/R3).
|
|
7
|
+
*
|
|
8
|
+
* The driver reference at `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`
|
|
9
|
+
* documents the set of action and guard kinds it implements. The two runtime
|
|
10
|
+
* pipelines are the only consumers the driver needs to keep in step with. The
|
|
11
|
+
* check is a symmetric set diff: an element present in one and absent in the
|
|
12
|
+
* other fails the check and names the element.
|
|
13
|
+
*
|
|
14
|
+
* The set is defined in {@link DOCUMENTED} below; the driver's markdown list is
|
|
15
|
+
* the human mirror. Update both when the driver adds or drops a kind.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* bun plugins/sp/scripts/inline-pipeline-parity-check.ts
|
|
19
|
+
* [--root <path>] default: repo root
|
|
20
|
+
*
|
|
21
|
+
* Exit code: 0 when the sets agree; 1 on any divergence. Violations are printed
|
|
22
|
+
* to stderr; a summary to stdout.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
26
|
+
import { join, resolve } from 'node:path';
|
|
27
|
+
import { parse as parseYaml } from 'yaml';
|
|
28
|
+
|
|
29
|
+
/** Documented action and guard set. Must stay in lockstep with the
|
|
30
|
+
* "Supported action and guard set (0755 R2 parity contract)" section in
|
|
31
|
+
* `plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md`. The
|
|
32
|
+
* driver supports a kind if ANY workflow in `.spur/workflows/*.yaml` uses
|
|
33
|
+
* it (the driver applies to any selected pipeline per its reference doc). */
|
|
34
|
+
const DOCUMENTED = {
|
|
35
|
+
actions: new Set([
|
|
36
|
+
'shell',
|
|
37
|
+
'note',
|
|
38
|
+
'doctor.probe',
|
|
39
|
+
'file.read.into-var',
|
|
40
|
+
'hitl.confirm',
|
|
41
|
+
'agent.run',
|
|
42
|
+
'proof.fingerprint',
|
|
43
|
+
'run.artifact',
|
|
44
|
+
'command.gate',
|
|
45
|
+
]),
|
|
46
|
+
guards: new Set(['always', 'shell']),
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
/** Directory of workflow definitions the driver is responsible for. */
|
|
50
|
+
const WORKFLOW_DIR = join('config', 'workflows');
|
|
51
|
+
|
|
52
|
+
/** Walk a state list and yield every `kind:` value found in `onEnter` action
|
|
53
|
+
* lists. Skips the top-level workflow `kind:` (e.g. `state-machine`). */
|
|
54
|
+
function collectActionKinds(states: unknown): Set<string> {
|
|
55
|
+
const out = new Set<string>();
|
|
56
|
+
if (!Array.isArray(states)) return out;
|
|
57
|
+
for (const state of states) {
|
|
58
|
+
if (typeof state !== 'object' || state === null) continue;
|
|
59
|
+
const onEnter = (state as { onEnter?: unknown }).onEnter;
|
|
60
|
+
if (!Array.isArray(onEnter)) continue;
|
|
61
|
+
for (const action of onEnter) {
|
|
62
|
+
if (typeof action !== 'object' || action === null) continue;
|
|
63
|
+
const kind = (action as { kind?: unknown }).kind;
|
|
64
|
+
if (typeof kind === 'string') out.add(kind);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Walk a transition list and yield every `guard.kind` value. */
|
|
71
|
+
function collectGuardKinds(transitions: unknown): Set<string> {
|
|
72
|
+
const out = new Set<string>();
|
|
73
|
+
if (!Array.isArray(transitions)) return out;
|
|
74
|
+
for (const transition of transitions) {
|
|
75
|
+
if (typeof transition !== 'object' || transition === null) continue;
|
|
76
|
+
const guard = (transition as { guard?: { kind?: unknown } }).guard;
|
|
77
|
+
const kind = guard?.kind;
|
|
78
|
+
if (typeof kind === 'string') out.add(kind);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Symmetric set diff. Returns elements in `a` but not in `b`, and vice versa. */
|
|
84
|
+
function diff<T>(a: Set<T>, b: Set<T>): { onlyInA: T[]; onlyInB: T[] } {
|
|
85
|
+
const onlyInA: T[] = [];
|
|
86
|
+
const onlyInB: T[] = [];
|
|
87
|
+
for (const x of a) if (!b.has(x)) onlyInA.push(x);
|
|
88
|
+
for (const x of b) if (!a.has(x)) onlyInB.push(x);
|
|
89
|
+
return { onlyInA, onlyInB };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseArgs(argv: string[]): { root: string } {
|
|
93
|
+
let root = resolve('.');
|
|
94
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
95
|
+
const arg = argv[i];
|
|
96
|
+
if (arg === '--root' && i + 1 < argv.length) {
|
|
97
|
+
root = resolve(argv[++i] ?? '.');
|
|
98
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
99
|
+
process.stdout.write('Usage: bun inline-pipeline-parity-check.ts [--root <path>]\n');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { root };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listWorkflowFiles(dir: string): string[] {
|
|
107
|
+
let entries: string[];
|
|
108
|
+
try {
|
|
109
|
+
entries = readdirSync(dir);
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
return entries.filter((e) => e.endsWith('.yaml')).map((e) => join(dir, e));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function main(): Promise<number> {
|
|
117
|
+
const { root } = parseArgs(process.argv.slice(2));
|
|
118
|
+
const errors: string[] = [];
|
|
119
|
+
|
|
120
|
+
const workflowDir = join(root, WORKFLOW_DIR);
|
|
121
|
+
if (!statSync(workflowDir, { throwIfNoEntry: false })) {
|
|
122
|
+
process.stderr.write(`inline-pipeline-parity-check: workflow directory not found: ${workflowDir}\n`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const files = listWorkflowFiles(workflowDir);
|
|
127
|
+
if (files.length === 0) {
|
|
128
|
+
process.stderr.write(`inline-pipeline-parity-check: no .yaml workflows found in ${workflowDir}\n`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const unionActions = new Set<string>();
|
|
133
|
+
const unionGuards = new Set<string>();
|
|
134
|
+
const perFileKinds: { path: string; actions: Set<string>; guards: Set<string> }[] = [];
|
|
135
|
+
|
|
136
|
+
for (const path of files) {
|
|
137
|
+
let parsed: unknown;
|
|
138
|
+
try {
|
|
139
|
+
parsed = parseYaml(readFileSync(path, 'utf8'));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
errors.push(`${path}: failed to parse (${err instanceof Error ? err.message : String(err)})`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const def = parsed as { states?: unknown; transitions?: unknown };
|
|
148
|
+
const actions = collectActionKinds(def.states);
|
|
149
|
+
const guards = collectGuardKinds(def.transitions);
|
|
150
|
+
perFileKinds.push({ path, actions, guards });
|
|
151
|
+
for (const a of actions) unionActions.add(a);
|
|
152
|
+
for (const g of guards) unionGuards.add(g);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const actionDiff = diff(unionActions, DOCUMENTED.actions);
|
|
156
|
+
const guardDiff = diff(unionGuards, DOCUMENTED.guards);
|
|
157
|
+
|
|
158
|
+
for (const x of actionDiff.onlyInA) {
|
|
159
|
+
const usedIn = perFileKinds.filter((f) => f.actions.has(x)).map((f) => f.path);
|
|
160
|
+
errors.push(`action kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
|
|
161
|
+
}
|
|
162
|
+
for (const x of actionDiff.onlyInB) {
|
|
163
|
+
errors.push(`action kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
|
|
164
|
+
}
|
|
165
|
+
for (const x of guardDiff.onlyInA) {
|
|
166
|
+
const usedIn = perFileKinds.filter((f) => f.guards.has(x)).map((f) => f.path);
|
|
167
|
+
errors.push(`guard kind "${x}" used in YAML (${usedIn.join(', ')}) but absent from inline-pipeline-driver.md`);
|
|
168
|
+
}
|
|
169
|
+
for (const x of guardDiff.onlyInB) {
|
|
170
|
+
errors.push(`guard kind "${x}" documented in inline-pipeline-driver.md but never used in any workflow`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
process.stderr.write(`inline-pipeline-parity-check: ${errors.length} divergence(s)\n`);
|
|
175
|
+
for (const e of errors) process.stderr.write(` - ${e}\n`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`inline-pipeline-parity-check: ok (${unionActions.size} actions, ${unionGuards.size} guards agree across ${files.length} workflows)\n`,
|
|
181
|
+
);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
process.exit(await main());
|
|
@@ -145,7 +145,7 @@ function main(): void {
|
|
|
145
145
|
|
|
146
146
|
const dbPath = join(process.cwd(), '.spur', 'spur.db');
|
|
147
147
|
if (!existsSync(dbPath)) {
|
|
148
|
-
fail(wbs, [`spur database not found at ${dbPath} — run a real history import first`]);
|
|
148
|
+
fail(wbs, [`local spur database not found at ${dbPath} — run a real history import first`]);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
let count: number;
|
|
@@ -269,6 +269,10 @@ function extractAcIdentities(taskContent: string, featureContent: string | null)
|
|
|
269
269
|
const leading = label.split(/\s+/)[0] ?? '';
|
|
270
270
|
if (leading && leading !== label) identities.add(leading);
|
|
271
271
|
}
|
|
272
|
+
for (const m of section.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
|
|
273
|
+
const title = (m[1] ?? '').trim();
|
|
274
|
+
if (title) identities.add(title);
|
|
275
|
+
}
|
|
272
276
|
if (featureContent !== null) {
|
|
273
277
|
for (const m of featureContent.matchAll(/^[ \t]*Scenario:\s*(.+)\s*$/gm)) {
|
|
274
278
|
const title = (m[1] ?? '').trim();
|
|
@@ -229,3 +229,26 @@ verifying shape, or stand in a `note` action until the path is proven, then swap
|
|
|
229
229
|
`agent.run` input referencing a command rather than a raw prompt, guards a single predicate
|
|
230
230
|
([workflow-fit-and-tuning.md](workflow-fit-and-tuning.md#3-node-simplicity-budget)).
|
|
231
231
|
- [ ] Validates clean AND dry-run reaches the expected terminal state.
|
|
232
|
+
|
|
233
|
+
## Optional version literal (task 0756)
|
|
234
|
+
|
|
235
|
+
Both dialects accept an optional root `version` field. The literal is **behavior-neutral** — it
|
|
236
|
+
exists as an identity tag, not a routing key. The contract:
|
|
237
|
+
|
|
238
|
+
- **Absent** → reported as `unversioned`. The default for all 11 shipped definitions.
|
|
239
|
+
- **Present, non-empty string** → reported as `explicit(<literal>)`. The literal is wrapped in
|
|
240
|
+
parentheses verbatim — no parsing, no ordering, no compatibility check.
|
|
241
|
+
- **Present, empty string (`version: ""`)** → **rejected** with a diagnostic naming the empty
|
|
242
|
+
value. The rejection lives in the resolve/preflight seam
|
|
243
|
+
(`packages/app/src/workflow/workflow-resolver.ts`), not in the dialect JSON schemas: those carry
|
|
244
|
+
`minLength: 1` for editors and Ajv consumers, but the load path validates against the engine's
|
|
245
|
+
Zod schema, which has no minimum. Move the check upstream once
|
|
246
|
+
`@gobing-ai/ts-dual-workflow-engine` ships `z.string().min(1)` on the root version.
|
|
247
|
+
|
|
248
|
+
The literal folds into the definition digest (`packages/app/src/workflow/composition-baseline.ts`),
|
|
249
|
+
so a version-only edit changes the digest with zero behavior change. `show` and `trace` do **not**
|
|
250
|
+
surface the literal by default — the digest stays the rendered run identity (D8 decision D5).
|
|
251
|
+
|
|
252
|
+
**No registry, no semver parser, no compatibility engine.** A future-major requirement needs
|
|
253
|
+
objective evidence: a consumer that branches on version, or a real drift incident the digest
|
|
254
|
+
diagnostic could not disambiguate. Neither exists today.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: inline-pipeline-driver
|
|
3
3
|
description: "Interactive host-session interpreter for Spur state-machine pipelines: execute the existing FSM without a workflow agent subprocess while preserving actions, guards, artifacts, and provenance."
|
|
4
|
+
owner: spur-dev-maintainers
|
|
5
|
+
retirement-criterion: "The per-task interpreter retires once the engine covers per-task execution for /sp:dev-runall with real terminal runs and the parity check (plugins/sp/scripts/inline-pipeline-parity-check.ts) is green (D8 decision D7). Batch orchestration wrapper may remain."
|
|
4
6
|
see_also:
|
|
5
7
|
- spur-dev
|
|
6
8
|
- execution-workflow
|
|
@@ -9,6 +11,24 @@ see_also:
|
|
|
9
11
|
|
|
10
12
|
# Inline Pipeline Driver
|
|
11
13
|
|
|
14
|
+
**Owner:** `spur-dev-maintainers` (per task 0755 R1). Reach the named owner via the frontmatter; no need to read the originating task.
|
|
15
|
+
|
|
16
|
+
**Retirement criterion (0755 R5, D8 decision D7):** the per-task interpreter retires once the engine covers per-task execution for `/sp:dev-runall` with real terminal runs **and** the parity check (this doc's documented action/guard set ≡ the resolved action/guard set of every `.spur/workflows/*.yaml`) is green. Recording the criterion is part of this task; acting on it is not — that is a separate A3-gate decision.
|
|
17
|
+
|
|
18
|
+
## Supported action and guard set (0755 R2 parity contract)
|
|
19
|
+
|
|
20
|
+
The action and guard kinds this driver implements. The parity check
|
|
21
|
+
(`plugins/sp/scripts/inline-pipeline-parity-check.ts`) compares this set against
|
|
22
|
+
the resolved actions and guards of every `.spur/workflows/*.yaml`; any element present
|
|
23
|
+
in one and absent in the other fails the check. Add a new kind here when the driver
|
|
24
|
+
implements it; remove the entry when the corresponding kind is dropped from the YAML.
|
|
25
|
+
|
|
26
|
+
**Actions:** `shell` · `note` · `doctor.probe` · `file.read.into-var` · `hitl.confirm` · `agent.run` · `proof.fingerprint` · `run.artifact` · `command.gate`
|
|
27
|
+
|
|
28
|
+
**Guards (transitions):** `always` · `shell`
|
|
29
|
+
|
|
30
|
+
## What this driver is
|
|
31
|
+
|
|
12
32
|
This driver is the interactive control-inversion path granted by ADR-047. It applies when an
|
|
13
33
|
interactive `/sp:dev-run --mode full`, sequential `/sp:dev-runall`, `/sp:dev-idea`, or
|
|
14
34
|
`/sp:dev-plan` invocation omits `--agent` or passes `--agent inline`. A named executor,
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"minLength": 1
|
|
18
18
|
},
|
|
19
19
|
"version": {
|
|
20
|
-
"type": "string"
|
|
20
|
+
"type": "string",
|
|
21
|
+
"minLength": 1,
|
|
22
|
+
"description": "Optional behavior-neutral identity tag (0756). Absent = unversioned; present = a non-empty opaque literal surfaced as explicit(<literal>). Not parsed, ordered, or compatibility-checked; no registry."
|
|
21
23
|
},
|
|
22
24
|
"description": {
|
|
23
25
|
"type": "string"
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"minLength": 1
|
|
18
18
|
},
|
|
19
19
|
"version": {
|
|
20
|
-
"type": "string"
|
|
20
|
+
"type": "string",
|
|
21
|
+
"minLength": 1,
|
|
22
|
+
"description": "Optional behavior-neutral identity tag (0756). Absent = unversioned; present = a non-empty opaque literal surfaced as explicit(<literal>). Not parsed, ordered, or compatibility-checked; no registry."
|
|
21
23
|
},
|
|
22
24
|
"description": {
|
|
23
25
|
"type": "string"
|