@gobing-ai/spur 0.3.95 → 0.3.96
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/plugin-scripts.json +0 -54
- package/config/rules/boundary/sp-script-placement.yaml +19 -0
- package/config/rules/strict/runtime-boundaries.yaml +5 -0
- package/config/rules/structure/test-location.yaml +2 -0
- package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
- package/config/rules/typescript/output-boundaries.yaml +15 -2
- package/config/script-placement-baseline.json +51 -0
- package/config/workflows/feature-verification.yaml +1 -1
- package/config/workflows/history-anatomy.yaml +23 -11
- package/config/workflows/idea-pipeline.yaml +80 -85
- package/config/workflows/pr-review.yaml +18 -9
- package/config/workflows/task-pipeline.yaml +42 -95
- package/config/workflows/wrapup-pipeline.yaml +89 -37
- package/package.json +1 -1
- package/plugins/sp/README.md +8 -10
- package/plugins/sp/agents/super-reviewer.md +43 -18
- package/plugins/sp/commands/dev-fixgha.md +83 -0
- package/plugins/sp/commands/dev-gitmsg.md +8 -6
- package/plugins/sp/commands/dev-gtd.md +2 -2
- package/plugins/sp/commands/dev-idea.md +22 -12
- package/plugins/sp/commands/dev-plan.md +8 -9
- package/plugins/sp/commands/dev-review.md +22 -13
- package/plugins/sp/commands/dev-verifyall.md +1 -1
- package/plugins/sp/commands/spur-init.md +2 -2
- package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +5 -4
- package/plugins/sp/lib/inline-run.generated.d.mts +11 -0
- package/plugins/sp/lib/inline-run.generated.mjs +25 -8
- package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
- package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
- package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
- package/plugins/sp/lib/spur-bin.ts +36 -0
- package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
- package/plugins/sp/lib/step-profile.generated.mjs +174 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/history-anatomy-cache.mjs +20 -19
- package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
- package/plugins/sp/scripts/inline-run-setup.mjs +85 -320
- package/plugins/sp/scripts/inline-run-setup.ts +104 -667
- package/plugins/sp/scripts/quality-gate.mjs +34 -19
- package/plugins/sp/scripts/quality-gate.ts +12 -658
- package/plugins/sp/scripts/residual-scan.mjs +135 -156
- package/plugins/sp/scripts/residual-scan.ts +102 -499
- package/plugins/sp/scripts/script-root.mjs +5 -1
- package/plugins/sp/scripts/script-root.ts +5 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +25 -17
- package/plugins/sp/scripts/workflow-step-profile.ts +21 -315
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +8 -2
- package/plugins/sp/scripts/wrapup-drift-probe.ts +3 -2
- package/plugins/sp/scripts/wrapup-steps.mjs +10 -32
- package/plugins/sp/scripts/wrapup-steps.ts +8 -36
- package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
- package/plugins/sp/skills/code-verification/SKILL.md +34 -9
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +3 -3
- package/plugins/sp/skills/functional-review/SKILL.md +7 -4
- package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
- package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +1 -1
- package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
- package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
- package/plugins/sp/skills/spur-cli/references/features.md +17 -6
- package/plugins/sp/skills/spur-cli/references/init.md +17 -16
- package/plugins/sp/skills/spur-cli/references/self.md +3 -2
- package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +21 -6
- package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
- package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
- package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +77 -34
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
- package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
- package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +29 -3
- package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
- package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
- package/spur.js +1495 -369
- package/web/_astro/{BoardApp.Cpxntzad.js → BoardApp.BIjMatT1.js} +1 -1
- package/web/_astro/{BoardApp.BxJuwD7I.js → BoardApp.GvjIe9Z6.js} +42 -42
- package/web/_astro/{TaskDetail.C5bW4WGV.js → TaskDetail.BZM3EAFx.js} +1 -1
- package/web/_astro/{arc.CyjRvNMY.js → arc.HkRiZnoI.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.B4lWRzJA.js → architectureDiagram-3BPJPVTR.BDdK-tgZ.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.Be38USQ2.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.DT9Fj5Qx.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
- package/web/_astro/channel.DlhX2MEt.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.SeSBWLg5.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DoO4VE14.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DrmTFA53.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.Bi-TUb_V.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.B-xp8Uhi.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.DGWrDnUU.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM._BagvPDy.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.VOboYWQ0.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.D_apfHS7.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.D_apfHS7.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.DNLU_L-x.js → cose-bilkent-S5V4N54A.BraxQ2Nt.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.D8borhV-.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.Uc0lvjcV.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DDIxtDBo.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.DVoDnD3H.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CRubTJ-0.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.m9SeYcXQ.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.C6s2ZjV5.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.nSzr5KTj.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DrZcys16.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.Do4M1b-K.js → ganttDiagram-6RSMTGT7.Dmdxgd-M.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.BWc9oJ4l.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
- package/web/_astro/index.EoZlzLK-.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.BIdOctFk.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CV5SuG2t.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.CxE-rEJ-.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.BPRdKWs_.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
- package/web/_astro/{linear.zRsuuDTE.js → linear.BnzMBgo_.js} +1 -1
- package/web/_astro/{mermaid.core.jAJTcMKc.js → mermaid.core.DSqeFu2Y.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BffJxfCr.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CacwWaE3.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.hXELD06-.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.DdEbPcqT.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DgaDocKv.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.B0KScnAu.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CV9M_WNc.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.BOAo74Et.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.CqqepUMe.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.DDqe4dOG.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.B3R9OPdO.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.B3SvgXpQ.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
- package/web/index.html +2 -2
- package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
- package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
- package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -301
- package/plugins/sp/scripts/feature-sync-bounded.ts +0 -481
- package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
- package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
- package/plugins/sp/scripts/record-feature-sync.ts +0 -84
- package/plugins/sp/scripts/script-contract-check.ts +0 -506
- package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
- package/plugins/sp/scripts/task-evidence-precheck.ts +0 -189
- package/plugins/sp/scripts/task-size-precheck.ts +0 -212
- package/plugins/sp/scripts/transition-shim-check.ts +0 -238
- package/plugins/sp/scripts/validate-commands.ts +0 -689
- package/plugins/sp/scripts/validate-flag-contracts.ts +0 -890
- package/plugins/sp/scripts/verify-answer-lint.ts +0 -549
- package/web/_astro/channel.CI6N_tCg.js +0 -1
- package/web/_astro/index.CENnIEqT.css +0 -1
|
@@ -96,7 +96,7 @@ function runScriptRoot(env, options = {}) {
|
|
|
96
96
|
return write({ mode: "unresolved", error: `installed digest failed: ${String(error)}` });
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
var SCRIPT_ROOT_USAGE = "usage: script-root --run-id <id>";
|
|
99
|
+
var SCRIPT_ROOT_USAGE = "usage: script-root --run-id <id> [--spur-bin <path>]";
|
|
100
100
|
function main(argv, env = getEnvVars(), options = {}) {
|
|
101
101
|
let runId = env.__runId ?? "";
|
|
102
102
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -105,6 +105,10 @@ function main(argv, env = getEnvVars(), options = {}) {
|
|
|
105
105
|
i++;
|
|
106
106
|
continue;
|
|
107
107
|
}
|
|
108
|
+
if (argv[i] === "--spur-bin") {
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
108
112
|
process.stderr.write(`${SCRIPT_ROOT_USAGE}
|
|
109
113
|
`);
|
|
110
114
|
return 2;
|
|
@@ -174,7 +174,7 @@ export function runScriptRoot(env: ScriptRootEnv, options: ScriptRootOptions = {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
export const SCRIPT_ROOT_USAGE = 'usage: script-root --run-id <id>';
|
|
177
|
+
export const SCRIPT_ROOT_USAGE = 'usage: script-root --run-id <id> [--spur-bin <path>]';
|
|
178
178
|
|
|
179
179
|
export function main(argv: string[], env: ScriptRootEnv = getEnvVars(), options: ScriptRootOptions = {}): number {
|
|
180
180
|
let runId = env.__runId ?? '';
|
|
@@ -184,6 +184,10 @@ export function main(argv: string[], env: ScriptRootEnv = getEnvVars(), options:
|
|
|
184
184
|
i++;
|
|
185
185
|
continue;
|
|
186
186
|
}
|
|
187
|
+
if (argv[i] === '--spur-bin') {
|
|
188
|
+
i++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
187
191
|
process.stderr.write(`${SCRIPT_ROOT_USAGE}\n`);
|
|
188
192
|
return 2;
|
|
189
193
|
}
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
// plugins/sp/scripts/workflow-step-profile.ts
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
// plugins/sp/lib/spur-bin.ts
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
|
|
9
11
|
// plugins/sp/lib/env.ts
|
|
10
12
|
function getEnvVar(name, fallback) {
|
|
@@ -12,7 +14,22 @@ function getEnvVar(name, fallback) {
|
|
|
12
14
|
return raw === undefined ? fallback : raw;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
// plugins/sp/
|
|
17
|
+
// plugins/sp/lib/spur-bin.ts
|
|
18
|
+
function spurCommand(spurBin) {
|
|
19
|
+
const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
20
|
+
return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
|
|
21
|
+
}
|
|
22
|
+
function defaultSpurBin() {
|
|
23
|
+
const fromEnv = getEnvVar("SPUR_BIN");
|
|
24
|
+
if (fromEnv)
|
|
25
|
+
return fromEnv;
|
|
26
|
+
const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
27
|
+
if (existsSync(local))
|
|
28
|
+
return `bun ${local}`;
|
|
29
|
+
return "spur";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// plugins/sp/lib/step-profile.generated.mjs
|
|
16
33
|
var DEFAULT_LAST = 20;
|
|
17
34
|
var DEFAULT_WINDOW_SEC = 300;
|
|
18
35
|
function nearestRankP50(values) {
|
|
@@ -156,7 +173,7 @@ function ratio(value) {
|
|
|
156
173
|
}
|
|
157
174
|
function formatStepProfileHuman(profile) {
|
|
158
175
|
const lines = [
|
|
159
|
-
`step profile
|
|
176
|
+
`step profile — ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`
|
|
160
177
|
];
|
|
161
178
|
for (const row of profile.rows) {
|
|
162
179
|
lines.push([
|
|
@@ -175,15 +192,8 @@ function formatStepProfileHuman(profile) {
|
|
|
175
192
|
`)}
|
|
176
193
|
`;
|
|
177
194
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (fromEnv)
|
|
181
|
-
return fromEnv;
|
|
182
|
-
const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
183
|
-
if (existsSync(local))
|
|
184
|
-
return `bun ${local}`;
|
|
185
|
-
return "spur";
|
|
186
|
-
}
|
|
195
|
+
|
|
196
|
+
// plugins/sp/scripts/workflow-step-profile.ts
|
|
187
197
|
function positiveInt(raw, fallback) {
|
|
188
198
|
const parsed = Number.parseInt(raw ?? "", 10);
|
|
189
199
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
@@ -215,9 +225,8 @@ function parseStepProfileCliArgs(argv) {
|
|
|
215
225
|
return { workflow, last, windowSec, spurBin, json, help };
|
|
216
226
|
}
|
|
217
227
|
function runSpurJson(spurBin, args) {
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
const cmdArgs = [...binParts.slice(1), ...args];
|
|
228
|
+
const { cmd, prefix } = spurCommand(spurBin);
|
|
229
|
+
const cmdArgs = [...prefix, ...args];
|
|
221
230
|
const r = spawnSync(cmd, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
222
231
|
const decode = (b) => typeof b === "string" ? b : Buffer.from(b ?? []).toString("utf8");
|
|
223
232
|
return {
|
|
@@ -321,7 +330,6 @@ export {
|
|
|
321
330
|
main,
|
|
322
331
|
formatStepProfileHuman,
|
|
323
332
|
extractExecutions,
|
|
324
|
-
defaultSpurBin,
|
|
325
333
|
buildStepProfile,
|
|
326
334
|
buildRows,
|
|
327
335
|
STEP_PROFILE_USAGE,
|
|
@@ -2,311 +2,31 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* workflow-step-profile — per-node step evidence for `sp:spur-doctor` (task 0827, feature I21).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Statelessness: the script computes the flags arithmetically and holds no thresholds of its own
|
|
13
|
-
* beyond the §10 window `W` (default 300 s). Unknown evidence stays `null` and never becomes zero:
|
|
14
|
-
* a step with no recorded `cost.exact.cacheHit` reports `{p50: null, known: 0}`, not a 0 hit rate.
|
|
15
|
-
* `cost.estimated` (retroactive mappings) never mixes into `cacheHit`.
|
|
5
|
+
* IO glue only (task 1005 R4, ADR-130 lib rule): argv parsing, `spur workflow trace --json`
|
|
6
|
+
* spawning and the two output modes. The aggregation core (nearestRankP50, extractExecutions,
|
|
7
|
+
* rowFlags, buildRows, buildStepProfile, nonDryRuns, formatStepProfileHuman and their types) lives
|
|
8
|
+
* in packages/app/src/workflow/step-profile.ts and reaches this script through the generated
|
|
9
|
+
* standalone bundle `plugins/sp/lib/step-profile.generated.mjs` — node-builtin only, so the
|
|
10
|
+
* committed `.mjs` twin runs under bare `node`. Regenerate with `bun run build:scripts`.
|
|
16
11
|
*
|
|
17
12
|
* Exit: 0 whenever a profile is produced (including `sampledRuns: 0` and flagged rows). Flags are
|
|
18
13
|
* evidence, not a gate. 1, with a stderr message, only when a `spur` call fails, its JSON does not
|
|
19
14
|
* parse, or the workflow argument is missing.
|
|
20
|
-
*
|
|
21
|
-
* Node-builtin imports only; the pure builders are exported for unit testing, `runStepProfileCli`
|
|
22
|
-
* and `main` do the I/O (mirrors the batch-preflight / feature-sync-bounded script shape).
|
|
23
15
|
*/
|
|
24
|
-
|
|
25
16
|
import { spawnSync } from 'node:child_process';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
* carries no import of the domain package (twins must stay self-contained).
|
|
40
|
-
*/
|
|
41
|
-
export interface ActionCostAttributionLike {
|
|
42
|
-
exact: ActionCostLike | null;
|
|
43
|
-
estimated: ActionCostLike | null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** One timeline event. Non-`action` events are ignored; the rest is optional on purpose. */
|
|
47
|
-
export interface TraceActionEvent {
|
|
48
|
-
kind: string;
|
|
49
|
-
node?: string;
|
|
50
|
-
actionKind?: string;
|
|
51
|
-
durationMs?: number | null;
|
|
52
|
-
startedAt?: string | null;
|
|
53
|
-
completedAt?: string | null;
|
|
54
|
-
invocation?: Record<string, string | number | boolean> | null;
|
|
55
|
-
cost?: ActionCostAttributionLike | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** One sampled run: the run id plus its timeline events. */
|
|
59
|
-
export interface StepProfileRun {
|
|
60
|
-
runId: string;
|
|
61
|
-
events: readonly TraceActionEvent[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** A `spur workflow trace --json` list entry (the fields the profile needs). */
|
|
65
|
-
export interface TraceRunEntry {
|
|
66
|
-
runId: string;
|
|
67
|
-
isDryRun?: boolean;
|
|
68
|
-
status?: string;
|
|
69
|
-
workflowName?: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type SessionMode = 'fresh' | 'resumed' | 'mixed';
|
|
73
|
-
|
|
74
|
-
// ── Output shapes ──────────────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
/** One execution of a node's action kind inside one run. */
|
|
77
|
-
export interface StepProfileExecution {
|
|
78
|
-
runId: string;
|
|
79
|
-
node: string;
|
|
80
|
-
actionKind: string;
|
|
81
|
-
durationMs: number | null;
|
|
82
|
-
idleGapMs: number | null;
|
|
83
|
-
/** `invocation.continue` as recorded; null when the event predates the field. */
|
|
84
|
-
session: 'fresh' | 'resumed' | null;
|
|
85
|
-
cacheHit: number | null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** One (node, actionKind) row — the R1 grain. */
|
|
89
|
-
export interface StepProfileRow {
|
|
90
|
-
node: string;
|
|
91
|
-
actionKind: string;
|
|
92
|
-
runs: number;
|
|
93
|
-
executions: number;
|
|
94
|
-
durationMs: { p50: number | null; max: number | null };
|
|
95
|
-
idleGapMs: { p50: number | null };
|
|
96
|
-
session: SessionMode | null;
|
|
97
|
-
cacheHit: { p50: number | null; known: number; of: number };
|
|
98
|
-
flags: string[];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** The `--json` envelope. */
|
|
102
|
-
export interface StepProfile {
|
|
103
|
-
workflow: string;
|
|
104
|
-
windowSec: number;
|
|
105
|
-
sampledRuns: number;
|
|
106
|
-
rows: StepProfileRow[];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Aggregation ────────────────────────────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
export const DEFAULT_LAST = 20;
|
|
112
|
-
/** §10 window `W`: the shortest common provider cache default, in seconds. */
|
|
113
|
-
export const DEFAULT_WINDOW_SEC = 300;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Nearest-rank median: `sorted[Math.ceil(n / 2) - 1]`, so every reported value is one that was
|
|
117
|
-
* actually observed. Empty input is unknown (`null`), never 0.
|
|
118
|
-
*/
|
|
119
|
-
export function nearestRankP50(values: readonly number[]): number | null {
|
|
120
|
-
if (values.length === 0) return null;
|
|
121
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
122
|
-
return sorted[Math.ceil(sorted.length / 2) - 1] ?? null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function numeric(value: number | null | undefined): number | null {
|
|
126
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function msBetween(from: string | null | undefined, to: string | null | undefined): number | null {
|
|
130
|
-
if (typeof from !== 'string' || typeof to !== 'string') return null;
|
|
131
|
-
const start = Date.parse(from);
|
|
132
|
-
const end = Date.parse(to);
|
|
133
|
-
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
134
|
-
return end - start;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function sessionOf(event: TraceActionEvent): 'fresh' | 'resumed' | null {
|
|
138
|
-
const value = event.invocation?.continue;
|
|
139
|
-
if (value === true) return 'resumed';
|
|
140
|
-
if (value === false) return 'fresh';
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function cacheHitOf(event: TraceActionEvent): number | null {
|
|
145
|
-
// `cost.estimated` is deliberately unread: retroactive mappings never mix with exact evidence.
|
|
146
|
-
return numeric(event.cost?.exact?.cacheHit);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Sort key: a parseable `startedAt`, with the unknown (`unavailable` / null) ordering last. */
|
|
150
|
-
function startKey(event: TraceActionEvent): number {
|
|
151
|
-
const parsed = typeof event.startedAt === 'string' ? Date.parse(event.startedAt) : Number.NaN;
|
|
152
|
-
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* The executions of one run: `kind === 'action'` events in `startedAt` order (trace emits them in
|
|
157
|
-
* row-creation order), each carrying its idle gap (from the previous action's `completedAt` in the
|
|
158
|
-
* same run), session mode and cacheHit. The run's first action has no previous action, so its gap
|
|
159
|
-
* is unknown — never 0.
|
|
160
|
-
*/
|
|
161
|
-
export function extractExecutions(runId: string, events: readonly TraceActionEvent[]): StepProfileExecution[] {
|
|
162
|
-
const actions = events
|
|
163
|
-
.filter((e) => e.kind === 'action')
|
|
164
|
-
.sort((a, b) => {
|
|
165
|
-
const left = startKey(a);
|
|
166
|
-
const right = startKey(b);
|
|
167
|
-
return left === right ? 0 : left - right;
|
|
168
|
-
});
|
|
169
|
-
const executions: StepProfileExecution[] = [];
|
|
170
|
-
for (const [index, event] of actions.entries()) {
|
|
171
|
-
const previous = index === 0 ? undefined : actions[index - 1];
|
|
172
|
-
executions.push({
|
|
173
|
-
runId,
|
|
174
|
-
node: event.node ?? '',
|
|
175
|
-
actionKind: event.actionKind ?? '',
|
|
176
|
-
durationMs: numeric(event.durationMs),
|
|
177
|
-
idleGapMs: msBetween(previous?.completedAt, event.startedAt),
|
|
178
|
-
session: sessionOf(event),
|
|
179
|
-
cacheHit: cacheHitOf(event),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
return executions;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function foldSession(executions: readonly StepProfileExecution[]): SessionMode | null {
|
|
186
|
-
const known = new Set(executions.map((e) => e.session).filter((s): s is 'fresh' | 'resumed' => s !== null));
|
|
187
|
-
if (known.size === 0) return null;
|
|
188
|
-
if (known.size === 1) return [...known][0] ?? null;
|
|
189
|
-
return 'mixed';
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* The four §10 cache-window flags for one row. `W` is in seconds and durations compare as
|
|
194
|
-
* `ms > W * 1000`, so a step exactly at the budget is inside it.
|
|
195
|
-
*/
|
|
196
|
-
export function rowFlags(row: StepProfileRow, windowSec: number): string[] {
|
|
197
|
-
const wMs = windowSec * 1000;
|
|
198
|
-
const flags: string[] = [];
|
|
199
|
-
const agentRun = row.actionKind === 'agent.run';
|
|
200
|
-
const resumed = row.session === 'resumed' || row.session === 'mixed';
|
|
201
|
-
const p50 = row.durationMs.p50;
|
|
202
|
-
if (!agentRun && p50 !== null && p50 > wMs) flags.push('step-over-window');
|
|
203
|
-
const idle = row.idleGapMs.p50;
|
|
204
|
-
if (agentRun && resumed && idle !== null && idle > wMs) flags.push('resume-after-idle');
|
|
205
|
-
const hit = row.cacheHit.p50;
|
|
206
|
-
if (agentRun && resumed && row.cacheHit.known > 0 && hit !== null && hit < 0.5) flags.push('resume-cold-cache');
|
|
207
|
-
if (agentRun && p50 !== null && p50 > 2 * wMs) flags.push('agent-run-over-2w');
|
|
208
|
-
return flags;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Fold per-run executions into the sorted (node, actionKind) rows. */
|
|
212
|
-
export function buildRows(executions: readonly StepProfileExecution[], windowSec: number): StepProfileRow[] {
|
|
213
|
-
const grouped = new Map<string, StepProfileExecution[]>();
|
|
214
|
-
for (const execution of executions) {
|
|
215
|
-
const key = `${execution.node}\u0000${execution.actionKind}`;
|
|
216
|
-
const bucket = grouped.get(key);
|
|
217
|
-
if (bucket === undefined) grouped.set(key, [execution]);
|
|
218
|
-
else bucket.push(execution);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const rows: StepProfileRow[] = [];
|
|
222
|
-
for (const bucket of grouped.values()) {
|
|
223
|
-
const first = bucket[0];
|
|
224
|
-
if (first === undefined) continue;
|
|
225
|
-
const durations = bucket.map((e) => e.durationMs).filter((d): d is number => d !== null);
|
|
226
|
-
const gaps = bucket.map((e) => e.idleGapMs).filter((g): g is number => g !== null);
|
|
227
|
-
const hits = bucket.map((e) => e.cacheHit).filter((h): h is number => h !== null);
|
|
228
|
-
const row: StepProfileRow = {
|
|
229
|
-
node: first.node,
|
|
230
|
-
actionKind: first.actionKind,
|
|
231
|
-
runs: new Set(bucket.map((e) => e.runId)).size,
|
|
232
|
-
executions: bucket.length,
|
|
233
|
-
durationMs: {
|
|
234
|
-
p50: nearestRankP50(durations),
|
|
235
|
-
max: durations.length === 0 ? null : Math.max(...durations),
|
|
236
|
-
},
|
|
237
|
-
idleGapMs: { p50: nearestRankP50(gaps) },
|
|
238
|
-
session: first.actionKind === 'agent.run' ? foldSession(bucket) : null,
|
|
239
|
-
cacheHit: {
|
|
240
|
-
p50: nearestRankP50(hits),
|
|
241
|
-
known: hits.length,
|
|
242
|
-
of: bucket.length,
|
|
243
|
-
},
|
|
244
|
-
flags: [],
|
|
245
|
-
};
|
|
246
|
-
row.flags = rowFlags(row, windowSec);
|
|
247
|
-
rows.push(row);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return rows.sort((a, b) =>
|
|
251
|
-
a.node === b.node ? a.actionKind.localeCompare(b.actionKind) : a.node.localeCompare(b.node),
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Build the profile over the sampled (already dry-run-filtered) runs. */
|
|
256
|
-
export function buildStepProfile(input: {
|
|
257
|
-
workflow: string;
|
|
258
|
-
windowSec: number;
|
|
259
|
-
runs: readonly StepProfileRun[];
|
|
260
|
-
}): StepProfile {
|
|
261
|
-
const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
|
|
262
|
-
return {
|
|
263
|
-
workflow: input.workflow,
|
|
264
|
-
windowSec: input.windowSec,
|
|
265
|
-
sampledRuns: input.runs.length,
|
|
266
|
-
rows: buildRows(executions, input.windowSec),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** Drop `isDryRun: true` entries (and anything without a run id) before sampling. */
|
|
271
|
-
export function nonDryRuns(entries: readonly TraceRunEntry[]): TraceRunEntry[] {
|
|
272
|
-
return entries.filter((e) => e.isDryRun !== true && typeof e.runId === 'string' && e.runId.length > 0);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ── Output rendering ───────────────────────────────────────────────────────────────────
|
|
276
|
-
|
|
277
|
-
function seconds(value: number | null): string {
|
|
278
|
-
return value === null ? '?' : `${(value / 1000).toFixed(1)}s`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function ratio(value: number | null): string {
|
|
282
|
-
return value === null ? '?' : value.toFixed(2);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** One line per row; seconds carry one decimal and `?` marks an unknown value. */
|
|
286
|
-
export function formatStepProfileHuman(profile: StepProfile): string {
|
|
287
|
-
const lines = [
|
|
288
|
-
`step profile — ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`,
|
|
289
|
-
];
|
|
290
|
-
for (const row of profile.rows) {
|
|
291
|
-
lines.push(
|
|
292
|
-
[
|
|
293
|
-
row.node.padEnd(20),
|
|
294
|
-
row.actionKind.padEnd(14),
|
|
295
|
-
`${row.runs}/${row.executions}`.padEnd(8),
|
|
296
|
-
`p50=${seconds(row.durationMs.p50)}`.padEnd(12),
|
|
297
|
-
`max=${seconds(row.durationMs.max)}`.padEnd(12),
|
|
298
|
-
`idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
|
|
299
|
-
`session=${row.session ?? '?'}`.padEnd(15),
|
|
300
|
-
`cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
|
|
301
|
-
`flags=${row.flags.length === 0 ? '-' : row.flags.join(',')}`,
|
|
302
|
-
].join(' '),
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
return `${lines.join('\n')}\n`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ── CLI ────────────────────────────────────────────────────────────────────────────────
|
|
309
|
-
|
|
17
|
+
import { defaultSpurBin, spurCommand } from '../lib/spur-bin';
|
|
18
|
+
import {
|
|
19
|
+
buildStepProfile,
|
|
20
|
+
DEFAULT_LAST,
|
|
21
|
+
DEFAULT_WINDOW_SEC,
|
|
22
|
+
formatStepProfileHuman,
|
|
23
|
+
nonDryRuns,
|
|
24
|
+
type StepProfileRun,
|
|
25
|
+
type TraceActionEvent,
|
|
26
|
+
type TraceRunEntry,
|
|
27
|
+
} from '../lib/step-profile.generated.mjs';
|
|
28
|
+
|
|
29
|
+
export * from '../lib/step-profile.generated.mjs';
|
|
310
30
|
export interface StepProfileCliArgs {
|
|
311
31
|
workflow: string;
|
|
312
32
|
last: number;
|
|
@@ -316,19 +36,6 @@ export interface StepProfileCliArgs {
|
|
|
316
36
|
help: boolean;
|
|
317
37
|
}
|
|
318
38
|
|
|
319
|
-
/**
|
|
320
|
-
* Resolve the spur CLI monorepo-safely (copied from feature-sync-bounded: twins stay
|
|
321
|
-
* self-contained and no plugin script imports another):
|
|
322
|
-
* --spur-bin > SPUR_BIN > monorepo-local CLI entry > PATH `spur`.
|
|
323
|
-
*/
|
|
324
|
-
export function defaultSpurBin(): string {
|
|
325
|
-
const fromEnv = getEnvVar('SPUR_BIN');
|
|
326
|
-
if (fromEnv) return fromEnv;
|
|
327
|
-
const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
|
|
328
|
-
if (existsSync(local)) return `bun ${local}`;
|
|
329
|
-
return 'spur';
|
|
330
|
-
}
|
|
331
|
-
|
|
332
39
|
function positiveInt(raw: string | undefined, fallback: number): number {
|
|
333
40
|
const parsed = Number.parseInt(raw ?? '', 10);
|
|
334
41
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
@@ -358,9 +65,8 @@ export function parseStepProfileCliArgs(argv: string[]): StepProfileCliArgs {
|
|
|
358
65
|
type SpawnResult = { stdout: string; stderr: string; exitCode: number; ok: boolean };
|
|
359
66
|
|
|
360
67
|
function runSpurJson(spurBin: string, args: string[]): SpawnResult {
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
const cmdArgs = [...binParts.slice(1), ...args];
|
|
68
|
+
const { cmd, prefix } = spurCommand(spurBin);
|
|
69
|
+
const cmdArgs = [...prefix, ...args];
|
|
364
70
|
const r = spawnSync(cmd, cmdArgs, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
|
|
365
71
|
const decode = (b: unknown): string =>
|
|
366
72
|
typeof b === 'string' ? b : Buffer.from((b as Uint8Array) ?? []).toString('utf8');
|
|
@@ -11,6 +11,12 @@ function getEnvVars() {
|
|
|
11
11
|
return process.env;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// plugins/sp/lib/spur-bin.ts
|
|
15
|
+
function spurCommand(spurBin) {
|
|
16
|
+
const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
17
|
+
return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
// plugins/sp/scripts/wrapup-drift-probe.ts
|
|
15
21
|
var WORKFLOWS_GLOB = `${join("config", "workflows")}/**`;
|
|
16
22
|
var DOC_OWNED_SURFACES = [
|
|
@@ -83,9 +89,9 @@ function driftReasonForPath(path) {
|
|
|
83
89
|
return match ? `matches doc-owned surface ${match.glob}` : null;
|
|
84
90
|
}
|
|
85
91
|
function defaultSpurRunner(env, cwd) {
|
|
86
|
-
const
|
|
92
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
87
93
|
return (args) => {
|
|
88
|
-
const run = spawnSync(
|
|
94
|
+
const run = spawnSync(cmd, [...prefix, ...args], {
|
|
89
95
|
cwd,
|
|
90
96
|
encoding: "utf8",
|
|
91
97
|
env: getEnvVars()
|
|
@@ -28,6 +28,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
28
28
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
29
29
|
import { join } from 'node:path';
|
|
30
30
|
import { getEnvVars } from '../lib/env';
|
|
31
|
+
import { spurCommand } from '../lib/spur-bin';
|
|
31
32
|
|
|
32
33
|
export interface DriftProbeEnv {
|
|
33
34
|
__runId?: string;
|
|
@@ -158,9 +159,9 @@ export function driftReasonForPath(path: string): string | null {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
function defaultSpurRunner(env: DriftProbeEnv, cwd?: string): SpurRunner {
|
|
161
|
-
const
|
|
162
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
162
163
|
return (args: string[]): SpurShowResult => {
|
|
163
|
-
const run = spawnSync(
|
|
164
|
+
const run = spawnSync(cmd, [...prefix, ...args], {
|
|
164
165
|
cwd,
|
|
165
166
|
encoding: 'utf8',
|
|
166
167
|
env: getEnvVars(),
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
// plugins/sp/scripts/wrapup-steps.ts
|
|
5
5
|
import { spawnSync } from "child_process";
|
|
6
|
-
import { appendFileSync,
|
|
6
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
7
|
import { join } from "path";
|
|
8
8
|
|
|
9
9
|
// plugins/sp/lib/env.ts
|
|
@@ -11,6 +11,12 @@ function getEnvVars() {
|
|
|
11
11
|
return process.env;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// plugins/sp/lib/spur-bin.ts
|
|
15
|
+
function spurCommand(spurBin) {
|
|
16
|
+
const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
17
|
+
return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
// plugins/sp/scripts/wrapup-steps.ts
|
|
15
21
|
function jqPick(...values) {
|
|
16
22
|
for (const value of values) {
|
|
@@ -23,10 +29,6 @@ function jqText(value) {
|
|
|
23
29
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
24
30
|
}
|
|
25
31
|
var WBS_PATTERN = /^[0-9]{4}$/;
|
|
26
|
-
function spurCommand(spurBin) {
|
|
27
|
-
const parts = (spurBin ?? "spur").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
28
|
-
return { cmd: parts[0] ?? "spur", prefix: parts.slice(1) };
|
|
29
|
-
}
|
|
30
32
|
function spur(env, args, options = {}) {
|
|
31
33
|
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
32
34
|
const result = spawnSync(cmd, [...prefix, ...args], {
|
|
@@ -323,32 +325,9 @@ function runFeatureTransition(env, options = {}) {
|
|
|
323
325
|
}
|
|
324
326
|
let syncOutput = "";
|
|
325
327
|
let syncRc = 1;
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const result = spawnSync("bun", [boundedTs, ...boundedArgs], { cwd, encoding: "utf8" });
|
|
330
|
-
syncOutput = result.stdout ?? "";
|
|
331
|
-
syncRc = result.status ?? 1;
|
|
332
|
-
if (result.stderr !== null && result.stderr.length > 0)
|
|
333
|
-
process.stderr.write(result.stderr);
|
|
334
|
-
} else {
|
|
335
|
-
const probe = spawnSync("superskill", ["script", "path", "sp", "feature-sync-bounded.mjs"], {
|
|
336
|
-
cwd,
|
|
337
|
-
encoding: "utf8"
|
|
338
|
-
});
|
|
339
|
-
const twin = probe.status === 0 ? (probe.stdout ?? "").trim() : "";
|
|
340
|
-
if (twin.length > 0 && existsSync(twin)) {
|
|
341
|
-
const result = spawnSync("node", [twin, ...boundedArgs], { cwd, encoding: "utf8" });
|
|
342
|
-
syncOutput = result.stdout ?? "";
|
|
343
|
-
syncRc = result.status ?? 1;
|
|
344
|
-
if (result.stderr !== null && result.stderr.length > 0)
|
|
345
|
-
process.stderr.write(result.stderr);
|
|
346
|
-
} else {
|
|
347
|
-
const result = spur(env, ["feature", "sync", feature, "--json"], { cwd, stderr: "inherit" });
|
|
348
|
-
syncOutput = result.stdout;
|
|
349
|
-
syncRc = result.status;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
328
|
+
const sync = spur(env, ["feature", "sync", feature, "--json"], { cwd, stderr: "inherit" });
|
|
329
|
+
syncOutput = sync.stdout;
|
|
330
|
+
syncRc = sync.status;
|
|
352
331
|
process.stdout.write(`${syncOutput}
|
|
353
332
|
`);
|
|
354
333
|
const shown = spur(env, ["feature", "show", feature, "--json"], { cwd });
|
|
@@ -428,7 +407,6 @@ export {
|
|
|
428
407
|
writeRouteReason,
|
|
429
408
|
verdictFromTestingSection,
|
|
430
409
|
taskStatusOf,
|
|
431
|
-
spurCommand,
|
|
432
410
|
runMetrics,
|
|
433
411
|
runFeatureTransition,
|
|
434
412
|
resolveTasks,
|