@gobing-ai/spur 0.3.91 → 0.3.92
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/pipeline-budgets.json +2 -2
- package/config/plugin-scripts.json +17 -1
- package/config/workflows/feature-lifecycle.yaml +14 -5
- package/config/workflows/feature-verification.yaml +35 -27
- package/config/workflows/history-anatomy.yaml +14 -25
- package/config/workflows/idea-pipeline.yaml +9 -5
- package/config/workflows/task-pipeline.yaml +185 -4
- package/config/workflows/wrapup-pipeline.yaml +47 -5
- package/package.json +9 -9
- package/plugins/sp/agents/super-planner.md +14 -5
- package/plugins/sp/commands/dev-dogfood.md +4 -4
- package/plugins/sp/commands/dev-fixall.md +8 -5
- package/plugins/sp/commands/dev-run.md +6 -0
- package/plugins/sp/commands/dev-runall.md +12 -6
- package/plugins/sp/commands/dev-verify.md +9 -0
- package/plugins/sp/commands/dev-verifyall.md +5 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +301 -300
- package/plugins/sp/lib/inline-run.generated.d.mts +17 -0
- package/plugins/sp/lib/inline-run.generated.mjs +1460 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/environment-lens.md +1 -1
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +136 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +196 -2
- package/plugins/sp/scripts/feature-verification-steps.mjs +174 -0
- package/plugins/sp/scripts/feature-verification-steps.ts +275 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +104 -4
- package/plugins/sp/scripts/history-anatomy-cache.ts +137 -13
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +1 -0
- package/plugins/sp/scripts/inline-run-setup.mjs +349 -0
- package/plugins/sp/scripts/inline-run-setup.ts +192 -75
- package/plugins/sp/scripts/record-feature-sync.mjs +63 -0
- package/plugins/sp/scripts/record-feature-sync.ts +84 -0
- package/plugins/sp/scripts/residual-scan.mjs +476 -0
- package/plugins/sp/scripts/residual-scan.ts +614 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +8 -3
- package/plugins/sp/scripts/task-size-precheck.ts +8 -3
- package/plugins/sp/skills/branch-workflow/SKILL.md +1 -0
- package/plugins/sp/skills/branch-workflow/references/worktree-patterns.md +2 -0
- package/plugins/sp/skills/code-implementation/SKILL.md +17 -0
- package/plugins/sp/skills/code-verification/SKILL.md +21 -0
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +1 -0
- package/plugins/sp/skills/dogfood-testing/SKILL.md +5 -3
- package/plugins/sp/skills/dogfood-testing/references/monitor-ledger.md +63 -26
- package/plugins/sp/skills/dogfood-testing/references/report-template.md +33 -10
- package/plugins/sp/skills/history-anatomy/references/modes.md +5 -3
- package/plugins/sp/skills/next-feature/references/ranking-rubric.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +7 -0
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/session-review/SKILL.md +12 -2
- package/plugins/sp/skills/spur-cli/references/features.md +1 -1
- package/plugins/sp/skills/spur-cli/references/projects.md +3 -1
- package/plugins/sp/skills/spur-cli/references/workflows.md +39 -19
- package/plugins/sp/skills/spur-dev/SKILL.md +11 -4
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +4 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -2
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +285 -57
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +1 -1
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +19 -4
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +43 -16
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +5 -0
- package/spur.js +21827 -19615
- package/web/_astro/BoardApp.CerSBgis.js +192 -0
- package/web/_astro/BoardApp.eoTz0pZs.js +1 -0
- package/web/_astro/{TaskDetail.CXGltuT_.js → TaskDetail.DCqiC-OZ.js} +1 -1
- package/web/_astro/arc.CPwg6Rw0.js +1 -0
- package/web/_astro/{architectureDiagram-3BPJPVTR.DJ8DHkWE.js → architectureDiagram-3BPJPVTR.DM_vp_hO.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.D8DHK3Jl.js → blockDiagram-GPEHLZMM.DXVIiv0p.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.BugQbX9u.js → c4Diagram-AAUBKEIU.BbF_zCxW.js} +1 -1
- package/web/_astro/channel.MYZLKNwy.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Mv26KlVn.js → chunk-2J33WTMH.CAgQHpPC.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.CD51JoT_.js → chunk-4BX2VUAB.BN-5tpw4.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.D_PFaIEe.js → chunk-55IACEB6.CnPkEEr0.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DemMW1ao.js → chunk-727SXJPM.BQzQeMVm.js} +4 -4
- package/web/_astro/{chunk-AQP2D5EJ.Iu2V5-ex.js → chunk-AQP2D5EJ.B6xNyDnL.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.MsNgSP-E.js → chunk-FMBD7UC4.C7f9Ih78.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.DfbRaAlm.js → chunk-ND2GUHAM.CNV1dFXT.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BbJAQ4h-.js → chunk-QZHKN3VN.Cudn2TkJ.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.CPurtiC2.js → classDiagram-4FO5ZUOK.D1NwP50q.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.CPurtiC2.js → classDiagram-v2-Q7XG4LA2.D1NwP50q.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.CKKdx1bM.js → cose-bilkent-S5V4N54A.B1wSL-Xb.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.CoKMTg-R.js → cynefin-OW5HDTMX.BmK52w8G.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.D4h4_k56.js → dagre-BM42HDAG.Bfy5CTDT.js} +2 -2
- package/web/_astro/diagram-2AECGRRQ.DhNnvUvX.js +43 -0
- package/web/_astro/diagram-5GNKFQAL.lTX5KwnS.js +10 -0
- package/web/_astro/{diagram-KO2AKTUF.BFoCkiCr.js → diagram-KO2AKTUF.CW_vMJ4z.js} +3 -3
- package/web/_astro/{diagram-LMA3HP47.exHn9OVx.js → diagram-LMA3HP47.B_8ZGF67.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.CeqO34nN.js → diagram-OG6HWLK6.BppnHsdS.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.D_v7HqxR.js → erDiagram-TEJ5UH35.BEuHXcjJ.js} +5 -5
- package/web/_astro/{flowDiagram-I6XJVG4X.EUmrpbwh.js → flowDiagram-I6XJVG4X.CH-UlnGr.js} +4 -4
- package/web/_astro/{ganttDiagram-6RSMTGT7.BOCF5lII.js → ganttDiagram-6RSMTGT7.BO81S85v.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.Di7otYZD.js → gitGraphDiagram-PVQCEYII.XnPxPPZN.js} +1 -1
- package/web/_astro/index.Hjbr15fG.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.TcBkCAJk.js → infoDiagram-5YYISTIA.JyjYRu_T.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.D-2y4M0c.js → ishikawaDiagram-YF4QCWOH.BBRBF-Fo.js} +5 -5
- package/web/_astro/{journeyDiagram-JHISSGLW.DPbJI_n2.js → journeyDiagram-JHISSGLW.C_iymSyp.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.EFxhQ9Fj.js → kanban-definition-UN3LZRKU.DdfW-Oqt.js} +7 -7
- package/web/_astro/{linear.DSAsQLzs.js → linear.C2_IkbZT.js} +1 -1
- package/web/_astro/mermaid.core.GAOYeSR0.js +303 -0
- package/web/_astro/{mindmap-definition-RKZ34NQL.CJY1N_7V.js → mindmap-definition-RKZ34NQL.DAZIxQSK.js} +2 -2
- package/web/_astro/{pieDiagram-4H26LBE5.567ZNoL2.js → pieDiagram-4H26LBE5.CN8sIhKM.js} +3 -3
- package/web/_astro/{quadrantDiagram-W4KKPZXB.mqfz9-MY.js → quadrantDiagram-W4KKPZXB.3dGcX5GP.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.Bv1Gv9In.js → requirementDiagram-4Y6WPE33.BV2y4dd6.js} +3 -3
- package/web/_astro/{sankeyDiagram-5OEKKPKP.B6Gs4X4r.js → sankeyDiagram-5OEKKPKP.Cqo15Tvo.js} +4 -4
- package/web/_astro/{sequenceDiagram-3UESZ5HK.BhYj4v-m.js → sequenceDiagram-3UESZ5HK.CROCPMJB.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BPbBnkpw.js → stateDiagram-AJRCARHV.RfXZrkFE.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.C4squMNK.js → stateDiagram-v2-BHNVJYJU.CPXmbBs9.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.C_SwIHgl.js → timeline-definition-PNZ67QCA.DdgKTiO8.js} +3 -3
- package/web/_astro/{vennDiagram-CIIHVFJN.Bz4NZGpQ.js → vennDiagram-CIIHVFJN.CPNVSHF1.js} +5 -5
- package/web/_astro/{wardleyDiagram-YWT4CUSO.CozMVZ3i.js → wardleyDiagram-YWT4CUSO.CQhA0Jyr.js} +3 -3
- package/web/_astro/{xychartDiagram-2RQKCTM6.BwMGBwjB.js → xychartDiagram-2RQKCTM6.n61BWyy4.js} +1 -1
- package/web/index.html +2 -2
- package/web/_astro/BoardApp.BEDWpzsr.js +0 -188
- package/web/_astro/BoardApp.DQG2xfEz.js +0 -1
- package/web/_astro/arc.C0rrflm_.js +0 -1
- package/web/_astro/channel.SRrg1P-w.js +0 -1
- package/web/_astro/diagram-2AECGRRQ.DJ0h9zgw.js +0 -43
- package/web/_astro/diagram-5GNKFQAL.DvPk1jYd.js +0 -10
- package/web/_astro/index.Bx6GY4RH.css +0 -1
- package/web/_astro/mermaid.core.kAZjgJHG.js +0 -301
|
@@ -5,27 +5,27 @@
|
|
|
5
5
|
* Unlike the subprocess path (`spur workflow run`), the interactive inline driver
|
|
6
6
|
* allocated a run id but never persisted an authoritative `runs` row, so bound
|
|
7
7
|
* `run.artifact` registration (0785 R3) correctly refused every inline record. This
|
|
8
|
-
* script is the thin delegate the driver now runs at Run setup: it
|
|
9
|
-
*
|
|
8
|
+
* script is the thin delegate the driver now runs at Run setup: it loads the source
|
|
9
|
+
* app service when the SPUR_BIN chain identifies a checkout, otherwise its generated bundle
|
|
10
10
|
* (`createOrAttachInlineRun` / `openInlineRunProjectDb` from packages/app), and lets it
|
|
11
|
-
* resolve the SAME project
|
|
11
|
+
* resolve the SAME project/registered/shared definition the engine would launch, compute the
|
|
12
12
|
* canonical definition digest with the exported hash machinery, and create-or-attach the
|
|
13
13
|
* run row through the existing engine persistence adapter.
|
|
14
14
|
*
|
|
15
15
|
* The script itself contains NO direct SQL, NO second hasher and NO persistence policy —
|
|
16
|
-
* every rule lives in packages/app (0804 D1). On a bundle-only install
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* unbound run (0804 R1 failure policy).
|
|
16
|
+
* every rule lives in packages/app (0804 D1). On a bundle-only install, the existing
|
|
17
|
+
* workflow-show projection selects the CLI's definition; the bundled app revalidates its
|
|
18
|
+
* schema and digest before recording the selected layer. It never creates an unbound run.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* The setup outcome is written into the two-file run record (task 0927 R1) — machine state
|
|
21
|
+
* at `.spur/run/<run-id>.state.json` (atomic replace) plus the `.spur/run/<run-id>.md`
|
|
22
|
+
* run-start header — so the driver can seed the inline var overlay (`__runId`,
|
|
23
|
+
* `__definitionDigest`) that proof capture and bound registration verify against. The
|
|
24
|
+
* pre-0927 `<run-id>-inline-setup.json` sidecar is no longer written. Exit 0 = authoritative
|
|
25
|
+
* identity ready (created or idempotently attached); exit 1 = fail closed, the driver must stop.
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* task-evidence-precheck.ts.
|
|
27
|
+
* Standard script (ADR-065): the generated Node-runnable twin re-enters Bun for the
|
|
28
|
+
* existing SQLite runtime. Bun on PATH is required; a monorepo checkout is not.
|
|
29
29
|
*
|
|
30
30
|
* Usage:
|
|
31
31
|
* bun plugins/sp/scripts/inline-run-setup.ts --run-id <id> --file <definition> [--spur-bin <path>]
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
* 0868): `--action` records one completed action boundary as an `action_runs` row through the
|
|
42
42
|
* shared `WorkflowActionTraceWriter`, `--close` marks the run row terminal through the same
|
|
43
43
|
* writer. `--action` is best-effort: a persistence failure is appended to
|
|
44
|
-
* `.spur/run/<run-id>.
|
|
44
|
+
* `.spur/run/<run-id>.md` and the script still exits 0 with `{"ok":false}` on stdout, so
|
|
45
45
|
* observation never wedges the run. `--close` is NOT best-effort: the run-row closure is
|
|
46
46
|
* bookkeeping, so a missing run row or a persistence failure exits 1 with a named error.
|
|
47
47
|
* Exit 2 is reserved for usage errors.
|
|
@@ -49,12 +49,24 @@
|
|
|
49
49
|
* Env: SPUR_BIN
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
import {
|
|
52
|
+
import { spawnSync } from 'node:child_process';
|
|
53
|
+
import {
|
|
54
|
+
appendFileSync,
|
|
55
|
+
closeSync,
|
|
56
|
+
existsSync,
|
|
57
|
+
mkdirSync,
|
|
58
|
+
openSync,
|
|
59
|
+
readFileSync,
|
|
60
|
+
renameSync,
|
|
61
|
+
unlinkSync,
|
|
62
|
+
writeFileSync,
|
|
63
|
+
writeSync,
|
|
64
|
+
} from 'node:fs';
|
|
53
65
|
import { dirname, join, resolve } from 'node:path';
|
|
54
66
|
import { fileURLToPath } from 'node:url';
|
|
55
67
|
import { getEnvVar } from '../lib/env';
|
|
56
68
|
|
|
57
|
-
/**
|
|
69
|
+
/** Setup outcome projected into the run-record state `.spur/run/<run-id>.state.json` (0927 R1). */
|
|
58
70
|
interface SetupOutcome {
|
|
59
71
|
readonly ok: boolean;
|
|
60
72
|
readonly runId: string;
|
|
@@ -87,8 +99,8 @@ function usage(): never {
|
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
/**
|
|
90
|
-
* The run id becomes
|
|
91
|
-
* single safe filename component before anything is written — the same guard class the
|
|
102
|
+
* The run id becomes filenames under `.spur/run/` (`<run-id>.state.json` + `<run-id>.md`),
|
|
103
|
+
* so it must be a single safe filename component before anything is written — the same guard class the
|
|
92
104
|
* task-pipeline.yaml route-reason action applies to `$__runId` (task 0804 R8). The allowlist
|
|
93
105
|
* refuses path separators, dot traversal (leading `.`), unresolved interpolation (`$`/`{`/`}`) and
|
|
94
106
|
* every other shell/unspecified metachar; valid UUID/timestamp-slug ids pass.
|
|
@@ -97,7 +109,7 @@ const SAFE_RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
|
97
109
|
|
|
98
110
|
function refuseUnsafeRunId(runId: string): never {
|
|
99
111
|
// Refuse BEFORE any outcome write: an unsafe id must never reach
|
|
100
|
-
// `.spur/run/<run-id
|
|
112
|
+
// `.spur/run/<run-id>.state.json` / `.md` (no traversal, no unintended file).
|
|
101
113
|
console.error(`inline-run-setup: refusing unsafe run id: ${runId}`);
|
|
102
114
|
console.error(
|
|
103
115
|
' The run id must be a single safe filename component (alphanumeric/._-, no leading dot, ' +
|
|
@@ -112,12 +124,9 @@ function refuseUnsafeRunId(runId: string): never {
|
|
|
112
124
|
* (--spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`), then derive the repo
|
|
113
125
|
* root from the resolved main module. `bun <repo>/apps/cli/src/index.ts` → repo root is
|
|
114
126
|
* three levels up. A bundle-only install (`spur` on PATH, a bundled `spur.js`, or a spur
|
|
115
|
-
* binary without the app workspace)
|
|
116
|
-
* closed with remediation guidance.
|
|
127
|
+
* binary without the app workspace) uses the adjacent generated application bundle.
|
|
117
128
|
*/
|
|
118
|
-
function resolveAppEntry(
|
|
119
|
-
spurBin: string,
|
|
120
|
-
): { entry: string; repoRoot: string } | { entry: null; repoRoot: null; chain: string } {
|
|
129
|
+
function resolveAppEntry(spurBin: string): { entry: string; portable: boolean } {
|
|
121
130
|
let candidates: string[] = [];
|
|
122
131
|
if (spurBin !== '') {
|
|
123
132
|
candidates = [spurBin];
|
|
@@ -137,16 +146,119 @@ function resolveAppEntry(
|
|
|
137
146
|
const srcDir = dirname(mainModule);
|
|
138
147
|
const repoRoot = resolve(srcDir, '..', '..', '..');
|
|
139
148
|
const appEntry = join(repoRoot, 'packages', 'app', 'src', 'index.ts');
|
|
140
|
-
if (existsSync(appEntry)) return { entry: appEntry,
|
|
141
|
-
return { entry: null, repoRoot: null, chain: `${candidate} (no ${appEntry})` };
|
|
149
|
+
if (existsSync(appEntry)) return { entry: appEntry, portable: false };
|
|
142
150
|
}
|
|
143
|
-
|
|
151
|
+
const entry = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
|
|
152
|
+
if (!existsSync(entry)) {
|
|
153
|
+
throw new Error('inline application bundle is missing — rebuild/install the sp plugin before running inline');
|
|
154
|
+
}
|
|
155
|
+
return { entry, portable: true };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Let the selected CLI own config and layer resolution, then revalidate its snapshot in the app. */
|
|
159
|
+
async function readInstalledInventory(file: string, spurBin: string): Promise<unknown> {
|
|
160
|
+
const localCli = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
161
|
+
const bundle = fileURLToPath(new URL('../lib/inline-run.generated.mjs', import.meta.url));
|
|
162
|
+
const { splitLaunchCommand } = (await import(bundle)) as typeof import('../lib/inline-run.generated.mjs');
|
|
163
|
+
const launch = spurBin
|
|
164
|
+
? splitLaunchCommand(spurBin, 'inline-run-setup "spurBin"')
|
|
165
|
+
: existsSync(localCli)
|
|
166
|
+
? { command: 'bun', leadingArgs: [localCli] }
|
|
167
|
+
: { command: 'spur', leadingArgs: [] };
|
|
168
|
+
if ('error' in launch) throw new Error(launch.error);
|
|
169
|
+
const result = spawnSync(
|
|
170
|
+
launch.command,
|
|
171
|
+
[...launch.leadingArgs, 'workflow', 'show', file, '--format', 'todo', '--json'],
|
|
172
|
+
{ cwd: process.cwd(), encoding: 'utf8', timeout: 30_000, maxBuffer: 4 * 1024 * 1024 },
|
|
173
|
+
);
|
|
174
|
+
if (result.status !== 0) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`could not resolve the workflow definition with the installed CLI: ${result.error?.message ?? result.stderr}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
const value: unknown = JSON.parse(result.stdout);
|
|
180
|
+
// Honor the existing optional JSON envelope without inventing another projection format.
|
|
181
|
+
if (value && typeof value === 'object' && 'ok' in value && 'data' in value && value.ok === true) {
|
|
182
|
+
return value.data;
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
144
185
|
}
|
|
145
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Project the setup outcome into the two-file run record (task 0927 R1): the machine state
|
|
189
|
+
* merges into `.spur/run/<run-id>.state.json` (atomic same-directory temp + rename, the
|
|
190
|
+
* 0925 R1 pattern) and `.spur/run/<run-id>.md` receives its run-start header exactly once.
|
|
191
|
+
* A re-setup of the same run id rewrites the state from the current outcome but keeps the
|
|
192
|
+
* prior `startedAt` instead of resurrecting setup time. Identity/provenance fields only —
|
|
193
|
+
* never prompt bodies — so the record carries no secret-bearing content by construction
|
|
194
|
+
* (0927 AC2). The retired `<run-id>-inline-setup.json` sidecar is not written (0927 R4);
|
|
195
|
+
* its readers were migrated to the pair state in the same change.
|
|
196
|
+
*/
|
|
146
197
|
function writeOutcome(runId: string, outcome: SetupOutcome): void {
|
|
147
198
|
const runDir = join(process.cwd(), '.spur', 'run');
|
|
148
199
|
if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
|
|
149
|
-
|
|
200
|
+
const statePath = join(runDir, `${runId}.state.json`);
|
|
201
|
+
const markdownPath = join(runDir, `${runId}.md`);
|
|
202
|
+
let prior: Record<string, unknown> = {};
|
|
203
|
+
try {
|
|
204
|
+
const parsed: unknown = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
205
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
206
|
+
prior = parsed as Record<string, unknown>;
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
// No prior state (new run) or unreadable file — the outcome alone defines the state.
|
|
210
|
+
}
|
|
211
|
+
const at = new Date().toISOString();
|
|
212
|
+
const state = {
|
|
213
|
+
schemaVersion: 1 as const,
|
|
214
|
+
runId,
|
|
215
|
+
...(outcome.workflowName !== undefined ? { workflowName: outcome.workflowName } : {}),
|
|
216
|
+
...(outcome.status !== undefined ? { status: outcome.status } : {}),
|
|
217
|
+
startedAt: typeof prior.startedAt === 'string' ? prior.startedAt : at,
|
|
218
|
+
updatedAt: at,
|
|
219
|
+
...(outcome.attached !== undefined ? { attached: outcome.attached } : {}),
|
|
220
|
+
...(outcome.definitionDigest !== undefined ? { definitionDigest: outcome.definitionDigest } : {}),
|
|
221
|
+
...(outcome.workflowVersion !== undefined ? { workflowVersion: outcome.workflowVersion } : {}),
|
|
222
|
+
...(outcome.resolvedPath !== undefined ? { resolvedPath: outcome.resolvedPath } : {}),
|
|
223
|
+
...(outcome.layer !== undefined ? { layer: outcome.layer } : {}),
|
|
224
|
+
...(outcome.workdir !== undefined ? { workdir: outcome.workdir } : {}),
|
|
225
|
+
// 0948 R7: project the setup outcome itself, so the retired `-inline-setup.json`
|
|
226
|
+
// sidecar's `ok` is still readable from the pair, and a SUCCESSFUL re-setup
|
|
227
|
+
// explicitly drops any prior `error` — a stale failure message must not outlive
|
|
228
|
+
// the failure it described (the state object is rebuilt, never merged with `prior`).
|
|
229
|
+
ok: outcome.ok,
|
|
230
|
+
...(outcome.ok === false && outcome.error !== undefined ? { error: outcome.error } : {}),
|
|
231
|
+
};
|
|
232
|
+
const temp = `${statePath}.tmp`;
|
|
233
|
+
try {
|
|
234
|
+
writeFileSync(temp, `${JSON.stringify(state, null, 4)}\n`);
|
|
235
|
+
renameSync(temp, statePath);
|
|
236
|
+
} catch {
|
|
237
|
+
// Best-effort: a failing record write must not wedge setup reporting — and never
|
|
238
|
+
// leaves `.tmp` residue behind (0926 R1).
|
|
239
|
+
try {
|
|
240
|
+
unlinkSync(temp);
|
|
241
|
+
} catch {
|
|
242
|
+
// Nothing to clean (temp was never created).
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// 0948 R7: create the header with `wx` (O_EXCL) instead of `existsSync` → `appendFileSync`.
|
|
246
|
+
// The check-then-append pair could append a SECOND header when two setups raced, and it
|
|
247
|
+
// kept "header present" as a separate fact from the identity write. One exclusive create
|
|
248
|
+
// makes exactly-one-header atomic; EEXIST just means it is already there.
|
|
249
|
+
let headerFd: number | undefined;
|
|
250
|
+
try {
|
|
251
|
+
headerFd = openSync(markdownPath, 'wx');
|
|
252
|
+
writeSync(
|
|
253
|
+
headerFd,
|
|
254
|
+
`# spur inline run ${runId} — ${outcome.workflowName ?? 'unknown workflow'} — setup ${at}\n`,
|
|
255
|
+
);
|
|
256
|
+
} catch {
|
|
257
|
+
// EEXIST (already written) or an unwritable path — the human header is not the setup
|
|
258
|
+
// identity, so this stays best-effort exactly like the state write above.
|
|
259
|
+
} finally {
|
|
260
|
+
if (headerFd !== undefined) closeSync(headerFd);
|
|
261
|
+
}
|
|
150
262
|
}
|
|
151
263
|
|
|
152
264
|
/**
|
|
@@ -159,14 +271,7 @@ function writeOutcome(runId: string, outcome: SetupOutcome): void {
|
|
|
159
271
|
* Returns the process exit code: 0 printed, 1 read/resolve failure.
|
|
160
272
|
*/
|
|
161
273
|
async function printFingerprint(taskFile: string, featureFile: string, spurBin: string): Promise<number> {
|
|
162
|
-
const { entry
|
|
163
|
-
if (entry === null || repoRoot === null) {
|
|
164
|
-
console.error(
|
|
165
|
-
`inline-run-setup: FAIL — no monorepo checkout of spur is reachable via ${chain}. ` +
|
|
166
|
-
'The proof-input digest must be computed by the app service; a bundle-only install cannot do it.',
|
|
167
|
-
);
|
|
168
|
-
return 1;
|
|
169
|
-
}
|
|
274
|
+
const { entry } = resolveAppEntry(spurBin);
|
|
170
275
|
const app = (await import(entry)) as {
|
|
171
276
|
computeProofInputFingerprint: (options: Record<string, unknown>) => Promise<string>;
|
|
172
277
|
readProofInputContents: (
|
|
@@ -204,7 +309,7 @@ const CLOSE_STATUSES = new Set(['done', 'failed', 'paused']);
|
|
|
204
309
|
const ACTION_STATUSES = new Set(['done', 'failed']);
|
|
205
310
|
|
|
206
311
|
/** Input for the ADR-117 emission modes (`--action` / `--close`). */
|
|
207
|
-
import type { WorkflowActionTraceWriter } from '@gobing-ai/app';
|
|
312
|
+
import type { WorkflowActionTraceWriter } from '@gobing-ai/spur-app';
|
|
208
313
|
|
|
209
314
|
interface TraceModeInput {
|
|
210
315
|
readonly runId: string;
|
|
@@ -218,9 +323,23 @@ interface TraceModeInput {
|
|
|
218
323
|
}
|
|
219
324
|
|
|
220
325
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
326
|
+
* The run-record file for appended driver lines (task 0927 R1): `.spur/run/<run-id>.md`
|
|
327
|
+
* for pair-based runs. A legacy run that predates the pair keeps appending to its declared
|
|
328
|
+
* `.spur/run/<run-id>.log` — the same precedence as `readWorkflowRunRecord` (the pair wins,
|
|
329
|
+
* legacy stays readable in place).
|
|
330
|
+
*/
|
|
331
|
+
function runRecordLogPath(runDir: string, runId: string): string {
|
|
332
|
+
const markdownPath = join(runDir, `${runId}.md`);
|
|
333
|
+
const legacyLogPath = join(runDir, `${runId}.log`);
|
|
334
|
+
if (existsSync(legacyLogPath) && !existsSync(markdownPath)) return legacyLogPath;
|
|
335
|
+
return markdownPath;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Append one emission-failure line to the run record markdown — `.spur/run/<run-id>.md`,
|
|
340
|
+
* the run log the inline driver already owns. Best-effort and synchronous (the process may
|
|
341
|
+
* exit immediately after), and never throws: an unwritable log must not wedge the run
|
|
342
|
+
* (ADR-117 R3).
|
|
224
343
|
*/
|
|
225
344
|
function appendTraceFailureLine(runId: string, detail: string): void {
|
|
226
345
|
try {
|
|
@@ -228,7 +347,7 @@ function appendTraceFailureLine(runId: string, detail: string): void {
|
|
|
228
347
|
if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
|
|
229
348
|
const safeRunId = runId.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
230
349
|
const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
231
|
-
appendFileSync(
|
|
350
|
+
appendFileSync(runRecordLogPath(runDir, safeRunId), `[${stamp}] ${detail}\n`);
|
|
232
351
|
} catch {
|
|
233
352
|
// Best-effort (R3): the run continues even when the failure cannot be recorded.
|
|
234
353
|
}
|
|
@@ -254,27 +373,20 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
254
373
|
return input.close ? 1 : 0;
|
|
255
374
|
};
|
|
256
375
|
|
|
257
|
-
const { entry, repoRoot, chain } = resolveAppEntry(input.spurBin);
|
|
258
|
-
if (entry === null || repoRoot === null) {
|
|
259
|
-
return fail(
|
|
260
|
-
`no monorepo checkout of spur is reachable via ${chain} — the shared trace writer ` +
|
|
261
|
-
'lives in packages/app (point SPUR_BIN at a repo checkout)',
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
376
|
// Compile-time link (0868 finding #5): the writer half is typed by the real packages/app
|
|
266
377
|
// export (type-only import, erased at runtime) so a signature drift breaks THIS file's
|
|
267
378
|
// typecheck instead of hiding behind the hand-declared cast.
|
|
268
|
-
const app = (await import(entry)) as {
|
|
269
|
-
openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
|
|
270
|
-
createWorkflowActionTraceWriter: (
|
|
271
|
-
db: unknown,
|
|
272
|
-
recordFailure?: (failure: unknown) => void,
|
|
273
|
-
) => WorkflowActionTraceWriter;
|
|
274
|
-
};
|
|
275
|
-
|
|
276
379
|
let projectDb: { adapter: unknown; close: () => void } | undefined;
|
|
277
380
|
try {
|
|
381
|
+
const { entry } = resolveAppEntry(input.spurBin);
|
|
382
|
+
const app = (await import(entry)) as {
|
|
383
|
+
openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
|
|
384
|
+
createWorkflowActionTraceWriter: (
|
|
385
|
+
db: unknown,
|
|
386
|
+
recordFailure?: (failure: unknown) => void,
|
|
387
|
+
) => WorkflowActionTraceWriter;
|
|
388
|
+
};
|
|
389
|
+
|
|
278
390
|
projectDb = await app.openInlineRunProjectDb(process.cwd());
|
|
279
391
|
const writer = app.createWorkflowActionTraceWriter(projectDb.adapter, (failure: unknown) => {
|
|
280
392
|
const detail = failure as { operation?: string; error?: string };
|
|
@@ -321,6 +433,14 @@ async function runTraceMode(input: TraceModeInput): Promise<number> {
|
|
|
321
433
|
}
|
|
322
434
|
|
|
323
435
|
async function main(): Promise<void> {
|
|
436
|
+
// The portable Node twin has no workspace imports; SQLite still uses Spur's existing Bun runtime.
|
|
437
|
+
if (!process.versions.bun) {
|
|
438
|
+
const child = spawnSync('bun', [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
439
|
+
stdio: 'inherit',
|
|
440
|
+
});
|
|
441
|
+
if (child.error) console.error(`inline-run-setup requires Bun on PATH: ${child.error.message}`);
|
|
442
|
+
process.exit(child.status ?? 1);
|
|
443
|
+
}
|
|
324
444
|
let runId = '';
|
|
325
445
|
let file = '';
|
|
326
446
|
let fingerprint = false;
|
|
@@ -396,23 +516,7 @@ async function main(): Promise<void> {
|
|
|
396
516
|
if (runId.trim() === '' || file.trim() === '') usage();
|
|
397
517
|
if (!SAFE_RUN_ID_RE.test(runId)) refuseUnsafeRunId(runId);
|
|
398
518
|
|
|
399
|
-
const { entry,
|
|
400
|
-
if (entry === null || repoRoot === null) {
|
|
401
|
-
const outcome: SetupOutcome = {
|
|
402
|
-
ok: false,
|
|
403
|
-
runId,
|
|
404
|
-
error:
|
|
405
|
-
`inline run setup failed closed: no monorepo checkout of spur is reachable via ${chain}. ` +
|
|
406
|
-
'The authoritative run identity must be persisted by the app service ' +
|
|
407
|
-
'(packages/app/src/services/inline-run-setup.ts); a bundle-only install cannot do this. ' +
|
|
408
|
-
'Remediation: point SPUR_BIN at a repo checkout, e.g. ' +
|
|
409
|
-
'SPUR_BIN="bun /path/to/spur/apps/cli/src/index.ts". The pipeline must not run unbound.',
|
|
410
|
-
};
|
|
411
|
-
writeOutcome(runId, outcome);
|
|
412
|
-
console.error(`inline-run-setup: FAIL for run ${runId}`);
|
|
413
|
-
console.error(` ${outcome.error}`);
|
|
414
|
-
process.exit(1);
|
|
415
|
-
}
|
|
519
|
+
const { entry, portable } = resolveAppEntry(spurBin);
|
|
416
520
|
|
|
417
521
|
// Dynamic import by absolute path: the app source graph resolves its own workspace
|
|
418
522
|
// dependencies from the repo checkout, never from this plugin script's location.
|
|
@@ -422,11 +526,22 @@ async function main(): Promise<void> {
|
|
|
422
526
|
getDb: () => Promise<unknown>;
|
|
423
527
|
file: string;
|
|
424
528
|
runId: string;
|
|
529
|
+
inventory?: unknown;
|
|
530
|
+
embeddedSchemas?: ReadonlyMap<string, string>;
|
|
425
531
|
}) => Promise<SetupOutcome & { ok: boolean }>;
|
|
426
532
|
openInlineRunProjectDb: (workdir: string) => Promise<{ adapter: unknown; close: () => void }>;
|
|
533
|
+
EMBEDDED_SPUR_SCHEMAS?: ReadonlyMap<string, string>;
|
|
427
534
|
};
|
|
428
535
|
|
|
429
536
|
const workdir = process.cwd();
|
|
537
|
+
let inventory: unknown;
|
|
538
|
+
try {
|
|
539
|
+
inventory = await readInstalledInventory(file, spurBin);
|
|
540
|
+
} catch (error) {
|
|
541
|
+
const message = `could not resolve the workflow definition: ${error instanceof Error ? error.message : String(error)}`;
|
|
542
|
+
writeOutcome(runId, { ok: false, runId, error: message });
|
|
543
|
+
throw new Error(message);
|
|
544
|
+
}
|
|
430
545
|
const projectDb = await app.openInlineRunProjectDb(workdir);
|
|
431
546
|
let exitCode = 0;
|
|
432
547
|
try {
|
|
@@ -435,6 +550,8 @@ async function main(): Promise<void> {
|
|
|
435
550
|
getDb: async () => projectDb.adapter,
|
|
436
551
|
file,
|
|
437
552
|
runId,
|
|
553
|
+
inventory,
|
|
554
|
+
...(portable ? { embeddedSchemas: app.EMBEDDED_SPUR_SCHEMAS } : {}),
|
|
438
555
|
});
|
|
439
556
|
writeOutcome(runId, result);
|
|
440
557
|
if (!result.ok) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// plugins/sp/scripts/record-feature-sync.ts
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { appendFileSync, existsSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
// plugins/sp/lib/env.ts
|
|
7
|
+
function getEnvVar(name, fallback) {
|
|
8
|
+
const raw = process.env[name];
|
|
9
|
+
return raw === undefined ? fallback : raw;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// plugins/sp/scripts/record-feature-sync.ts
|
|
13
|
+
function resolveSpurBin(argv) {
|
|
14
|
+
const flag = argv.indexOf("--spur-bin");
|
|
15
|
+
const value = flag !== -1 ? argv[flag + 1] : undefined;
|
|
16
|
+
return value ?? getEnvVar("spurBin") ?? "spur";
|
|
17
|
+
}
|
|
18
|
+
function main() {
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
const spurBin = resolveSpurBin(argv);
|
|
21
|
+
const wbs = getEnvVar("wbs") ?? "";
|
|
22
|
+
if (wbs.length === 0) {
|
|
23
|
+
console.error("record-feature-sync: env `wbs` is required");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
const reportPath = `.spur/run/${wbs}-report.txt`;
|
|
27
|
+
const report = (line) => {
|
|
28
|
+
appendFileSync(reportPath, `${line}
|
|
29
|
+
`);
|
|
30
|
+
};
|
|
31
|
+
const show = spawnSync(spurBin, ["task", "show", wbs, "--json"], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
34
|
+
});
|
|
35
|
+
let featureId = "";
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(show.stdout ?? "");
|
|
38
|
+
featureId = String(parsed.feature_id ?? parsed.frontmatter?.feature_id ?? "");
|
|
39
|
+
} catch {
|
|
40
|
+
featureId = "";
|
|
41
|
+
}
|
|
42
|
+
if (featureId.length === 0) {
|
|
43
|
+
report(`Orphan task ${wbs} — no feature_id linked — proposal: consider linking to a parent feature.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (existsSync("plugins/sp/scripts/feature-sync-bounded.ts")) {
|
|
47
|
+
spawnSync("bun", ["plugins/sp/scripts/feature-sync-bounded.ts", featureId, "--spur-bin", spurBin, "--json"], {
|
|
48
|
+
stdio: "inherit"
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const staged = spawnSync("superskill", ["script", "path", "sp", "feature-sync-bounded.mjs"], {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
55
|
+
});
|
|
56
|
+
const stagedPath = (staged.stdout ?? "").trim();
|
|
57
|
+
if (staged.status === 0 && stagedPath.length > 0 && existsSync(stagedPath)) {
|
|
58
|
+
spawnSync("node", [stagedPath, featureId, "--spur-bin", spurBin, "--json"], { stdio: "inherit" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
spawnSync(spurBin, ["feature", "sync", featureId, "--json"], { stdio: "inherit" });
|
|
62
|
+
}
|
|
63
|
+
main();
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* record-feature-sync — record-step post-record feature sync owner (tasks 0328 / 0411 / 0931).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the record onEnter shell (0931 R5): ADR-115 caps one workflow shell at 10
|
|
5
|
+
* logical commands and 800 chars, and the deferFeatureSync guard plus the sync chain can no
|
|
6
|
+
* longer share it. The pipeline shell resolves this script (in-repo first, superskill-staged
|
|
7
|
+
* .mjs fallback), notes a deferral, and delegates the rest here:
|
|
8
|
+
*
|
|
9
|
+
* - A task with no feature_id gets the orphan proposal note (0328).
|
|
10
|
+
* - The sync routes through feature-sync-bounded.ts (0411 retry suppression), then the
|
|
11
|
+
* superskill-staged module, then bare `spur feature sync` (0328).
|
|
12
|
+
* - Best-effort by contract: a sync failure never fails the record step.
|
|
13
|
+
*
|
|
14
|
+
* Node-builtin imports only (ADR-065): this file is transpiled to a node-runnable .mjs twin
|
|
15
|
+
* by `superskill script convert`. Env: wbs (required), spurBin (default "spur"); the
|
|
16
|
+
* `--spur-bin <bin>` argument wins over the env var.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import { appendFileSync, existsSync } from 'node:fs';
|
|
21
|
+
import { getEnvVar } from '../lib/env';
|
|
22
|
+
|
|
23
|
+
function resolveSpurBin(argv: string[]): string {
|
|
24
|
+
const flag = argv.indexOf('--spur-bin');
|
|
25
|
+
const value = flag !== -1 ? argv[flag + 1] : undefined;
|
|
26
|
+
return value ?? getEnvVar('spurBin') ?? 'spur';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function main(): void {
|
|
30
|
+
const argv = process.argv.slice(2);
|
|
31
|
+
const spurBin = resolveSpurBin(argv);
|
|
32
|
+
const wbs = getEnvVar('wbs') ?? '';
|
|
33
|
+
if (wbs.length === 0) {
|
|
34
|
+
console.error('record-feature-sync: env `wbs` is required');
|
|
35
|
+
process.exit(0); // best-effort contract — never fail the record step
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const reportPath = `.spur/run/${wbs}-report.txt`;
|
|
39
|
+
const report = (line: string): void => {
|
|
40
|
+
appendFileSync(reportPath, `${line}\n`);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const show = spawnSync(spurBin, ['task', 'show', wbs, '--json'], {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
46
|
+
});
|
|
47
|
+
let featureId = '';
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(show.stdout ?? '') as {
|
|
50
|
+
feature_id?: string;
|
|
51
|
+
frontmatter?: { feature_id?: string };
|
|
52
|
+
};
|
|
53
|
+
featureId = String(parsed.feature_id ?? parsed.frontmatter?.feature_id ?? '');
|
|
54
|
+
} catch {
|
|
55
|
+
featureId = '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (featureId.length === 0) {
|
|
59
|
+
report(`Orphan task ${wbs} — no feature_id linked — proposal: consider linking to a parent feature.`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (existsSync('plugins/sp/scripts/feature-sync-bounded.ts')) {
|
|
64
|
+
spawnSync('bun', ['plugins/sp/scripts/feature-sync-bounded.ts', featureId, '--spur-bin', spurBin, '--json'], {
|
|
65
|
+
stdio: 'inherit',
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const staged = spawnSync('superskill', ['script', 'path', 'sp', 'feature-sync-bounded.mjs'], {
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
73
|
+
});
|
|
74
|
+
const stagedPath = (staged.stdout ?? '').trim();
|
|
75
|
+
if (staged.status === 0 && stagedPath.length > 0 && existsSync(stagedPath)) {
|
|
76
|
+
spawnSync('node', [stagedPath, featureId, '--spur-bin', spurBin, '--json'], { stdio: 'inherit' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
spawnSync(spurBin, ['feature', 'sync', featureId, '--json'], { stdio: 'inherit' });
|
|
81
|
+
// Falls through with exit 0: a sync failure never fails the record step (0411).
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main();
|