@gobing-ai/spur 0.3.80 → 0.3.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/config/config.example.yaml +31 -18
- package/config/config.global.yaml +13 -11
- package/config/pipeline-budgets.json +34 -2
- package/config/plugin-scripts.json +25 -0
- package/config/rules/boundary/config-loading-ownership.yaml +0 -3
- package/config/rules/boundary/dao-boundary.yaml +4 -17
- package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
- package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
- package/config/rules/boundary/sp-runtime-path.yaml +3 -14
- package/config/rules/quality/coverage-gate.yaml +3 -14
- package/config/rules/quality/tsdoc-exports.yaml +4 -7
- package/config/rules/strict/http-boundaries.yaml +5 -8
- package/config/rules/strict/runtime-boundaries.yaml +1 -5
- package/config/rules/structure/protected-files.yaml +9 -3
- package/config/rules/structure/test-focus-skip.yaml +0 -2
- package/config/rules/structure/test-location.yaml +0 -5
- package/config/rules/surface/check-cli-surface.yaml +3 -2
- package/config/rules/typescript/bun-tooling.yaml +5 -7
- package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
- package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
- package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
- package/config/rules/typescript/no-debugger.yaml +0 -2
- package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
- package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
- package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
- package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
- package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
- package/config/rules/typescript/output-boundaries.yaml +0 -3
- package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
- package/config/rules/ui/ui-import-boundary.yaml +1 -5
- package/config/templates/docs/99_PROJECT_CONSTITUTION.md +75 -13
- package/config/transition-shims.json +0 -7
- package/config/workflows/basic.yaml +4 -0
- package/config/workflows/docs-pipeline.yaml +13 -14
- package/config/workflows/feature-dev.yaml +20 -65
- package/config/workflows/history-anatomy.yaml +22 -1
- package/config/workflows/idea-pipeline.yaml +53 -97
- package/config/workflows/pr-review.yaml +21 -33
- package/config/workflows/task-pipeline.yaml +87 -330
- package/config/workflows/wayfinder-resolution.yaml +12 -26
- package/config/workflows/wrapup-pipeline.yaml +48 -189
- package/package.json +9 -9
- package/plugins/sp/README.md +12 -3
- package/plugins/sp/agents/expert-spur.md +41 -19
- package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/idea-handoff.mjs +27 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-run-setup.ts +69 -1
- package/plugins/sp/scripts/quality-gate.mjs +216 -0
- package/plugins/sp/scripts/quality-gate.ts +287 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -2
- package/plugins/sp/scripts/task-size-precheck.ts +44 -16
- package/plugins/sp/scripts/verify-answer-lint.ts +17 -1
- package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
- package/plugins/sp/scripts/wrapup-steps.ts +466 -0
- package/plugins/sp/skills/code-review/references/review-lenses.md +3 -0
- package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
- package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
- package/plugins/sp/skills/spur-cli/SKILL.md +4 -8
- package/plugins/sp/skills/spur-cli/references/agent.md +44 -69
- package/plugins/sp/skills/spur-cli/references/message.md +30 -3
- package/plugins/sp/skills/spur-cli/references/projects.md +25 -1
- package/plugins/sp/skills/spur-cli/references/self.md +6 -5
- package/plugins/sp/skills/spur-cli/references/serve.md +8 -7
- package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
- package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
- package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
- package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +10 -0
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +4 -6
- package/plugins/sp/skills/spur-dev/references/glossary.md +1 -1
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +11 -1
- package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
- package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
- package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
- package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
- package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
- package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
- package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
- package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
- package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
- package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
- package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
- package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
- package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
- package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
- package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
- package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
- package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
- package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
- package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
- package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
- package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
- package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
- package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
- package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
- package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
- package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
- package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
- package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
- package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
- package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
- package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
- package/schemas/spur-config.schema.json +105 -85
- package/spur.js +44616 -43320
- package/web/_astro/BoardApp.D-WlxiN2.js +1 -0
- package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.D8bM9pKL.js} +97 -95
- package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.BPRqgVUE.js} +1 -1
- package/web/_astro/{arc.DWEtA3Tx.js → arc.BPrPES3z.js} +1 -1
- package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.qX_7q02P.js} +1 -1
- package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.CUZfj5V7.js} +1 -1
- package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CTaOr8hH.js} +1 -1
- package/web/_astro/channel.DGZaFHZx.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.Dt-9wf3h.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.CTC2sdoN.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.DQcxt2_g.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DXFPSn-a.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.BCx3U4bT.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.DL2tJkdO.js} +1 -1
- package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.DZyflMro.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.CUI2mT09.js} +1 -1
- package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.g4rX4Fr1.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js} +1 -1
- package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.CzWJLqp0.js} +1 -1
- package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.WgsvQeCR.js} +1 -1
- package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.Dzv6ngql.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.CpJ4a9rU.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.CzPlF_dq.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.TAkZNTcQ.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.uCjoKSag.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.eMplIjoK.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.Bf7zoXGz.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.B_bHj3gN.js} +1 -1
- package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BasrHRMj.js} +1 -1
- package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.C6iphq1x.js} +1 -1
- package/web/_astro/index.DayyIngm.css +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.HXmDMhW4.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BSmW8NiU.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.DEQow5fo.js} +1 -1
- package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.IVm9cTdc.js} +1 -1
- package/web/_astro/{linear.CHXgcIbN.js → linear.CrsM73_9.js} +1 -1
- package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.CfBeDJls.js} +6 -6
- package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.C3j60Y-0.js} +1 -1
- package/web/_astro/ordinal.BYWQX77i.js +1 -0
- package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.B-aCMeEA.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.Cib965yq.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.D61cS4O-.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.GKF2qVPy.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DZnq8F2h.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DXUFmdgM.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.BtHmhLEz.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.Cy-WW2ln.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.SLp5b9KI.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.Bww45mWV.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DR4swI6a.js} +1 -1
- package/web/apple-touch-icon.png +0 -0
- package/web/favicon.ico +0 -0
- package/web/favicon.svg +17 -4
- package/web/icon-192.png +0 -0
- package/web/icon-512.png +0 -0
- package/web/index.html +2 -2
- package/web/site.webmanifest +31 -0
- package/web/spur_logo.svg +1 -0
- package/plugins/sp/skills/spur-cli/references/team.md +0 -145
- package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
- package/web/_astro/channel.BAI6xLeV.js +0 -1
- package/web/_astro/index.Dcr_8fiK.css +0 -1
- package/web/_astro/ordinal.DBvzRdQf.js +0 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// plugins/sp/scripts/workflow-step-profile.ts
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
var DEFAULT_LAST = 20;
|
|
9
|
+
var DEFAULT_WINDOW_SEC = 300;
|
|
10
|
+
function nearestRankP50(values) {
|
|
11
|
+
if (values.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
14
|
+
return sorted[Math.ceil(sorted.length / 2) - 1] ?? null;
|
|
15
|
+
}
|
|
16
|
+
function numeric(value) {
|
|
17
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
18
|
+
}
|
|
19
|
+
function msBetween(from, to) {
|
|
20
|
+
if (typeof from !== "string" || typeof to !== "string")
|
|
21
|
+
return null;
|
|
22
|
+
const start = Date.parse(from);
|
|
23
|
+
const end = Date.parse(to);
|
|
24
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
25
|
+
return null;
|
|
26
|
+
return end - start;
|
|
27
|
+
}
|
|
28
|
+
function sessionOf(event) {
|
|
29
|
+
const value = event.invocation?.continue;
|
|
30
|
+
if (value === true)
|
|
31
|
+
return "resumed";
|
|
32
|
+
if (value === false)
|
|
33
|
+
return "fresh";
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function cacheHitOf(event) {
|
|
37
|
+
return numeric(event.cost?.exact?.cacheHit);
|
|
38
|
+
}
|
|
39
|
+
function startKey(event) {
|
|
40
|
+
const parsed = typeof event.startedAt === "string" ? Date.parse(event.startedAt) : Number.NaN;
|
|
41
|
+
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
42
|
+
}
|
|
43
|
+
function extractExecutions(runId, events) {
|
|
44
|
+
const actions = events.filter((e) => e.kind === "action").sort((a, b) => {
|
|
45
|
+
const left = startKey(a);
|
|
46
|
+
const right = startKey(b);
|
|
47
|
+
return left === right ? 0 : left - right;
|
|
48
|
+
});
|
|
49
|
+
const executions = [];
|
|
50
|
+
for (const [index, event] of actions.entries()) {
|
|
51
|
+
const previous = index === 0 ? undefined : actions[index - 1];
|
|
52
|
+
executions.push({
|
|
53
|
+
runId,
|
|
54
|
+
node: event.node ?? "",
|
|
55
|
+
actionKind: event.actionKind ?? "",
|
|
56
|
+
durationMs: numeric(event.durationMs),
|
|
57
|
+
idleGapMs: msBetween(previous?.completedAt, event.startedAt),
|
|
58
|
+
session: sessionOf(event),
|
|
59
|
+
cacheHit: cacheHitOf(event)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return executions;
|
|
63
|
+
}
|
|
64
|
+
function foldSession(executions) {
|
|
65
|
+
const known = new Set(executions.map((e) => e.session).filter((s) => s !== null));
|
|
66
|
+
if (known.size === 0)
|
|
67
|
+
return null;
|
|
68
|
+
if (known.size === 1)
|
|
69
|
+
return [...known][0] ?? null;
|
|
70
|
+
return "mixed";
|
|
71
|
+
}
|
|
72
|
+
function rowFlags(row, windowSec) {
|
|
73
|
+
const wMs = windowSec * 1000;
|
|
74
|
+
const flags = [];
|
|
75
|
+
const agentRun = row.actionKind === "agent.run";
|
|
76
|
+
const resumed = row.session === "resumed" || row.session === "mixed";
|
|
77
|
+
const p50 = row.durationMs.p50;
|
|
78
|
+
if (!agentRun && p50 !== null && p50 > wMs)
|
|
79
|
+
flags.push("step-over-window");
|
|
80
|
+
const idle = row.idleGapMs.p50;
|
|
81
|
+
if (agentRun && resumed && idle !== null && idle > wMs)
|
|
82
|
+
flags.push("resume-after-idle");
|
|
83
|
+
const hit = row.cacheHit.p50;
|
|
84
|
+
if (agentRun && resumed && row.cacheHit.known > 0 && hit !== null && hit < 0.5)
|
|
85
|
+
flags.push("resume-cold-cache");
|
|
86
|
+
if (agentRun && p50 !== null && p50 > 2 * wMs)
|
|
87
|
+
flags.push("agent-run-over-2w");
|
|
88
|
+
return flags;
|
|
89
|
+
}
|
|
90
|
+
function buildRows(executions, windowSec) {
|
|
91
|
+
const grouped = new Map;
|
|
92
|
+
for (const execution of executions) {
|
|
93
|
+
const key = `${execution.node}\x00${execution.actionKind}`;
|
|
94
|
+
const bucket = grouped.get(key);
|
|
95
|
+
if (bucket === undefined)
|
|
96
|
+
grouped.set(key, [execution]);
|
|
97
|
+
else
|
|
98
|
+
bucket.push(execution);
|
|
99
|
+
}
|
|
100
|
+
const rows = [];
|
|
101
|
+
for (const bucket of grouped.values()) {
|
|
102
|
+
const first = bucket[0];
|
|
103
|
+
if (first === undefined)
|
|
104
|
+
continue;
|
|
105
|
+
const durations = bucket.map((e) => e.durationMs).filter((d) => d !== null);
|
|
106
|
+
const gaps = bucket.map((e) => e.idleGapMs).filter((g) => g !== null);
|
|
107
|
+
const hits = bucket.map((e) => e.cacheHit).filter((h) => h !== null);
|
|
108
|
+
const row = {
|
|
109
|
+
node: first.node,
|
|
110
|
+
actionKind: first.actionKind,
|
|
111
|
+
runs: new Set(bucket.map((e) => e.runId)).size,
|
|
112
|
+
executions: bucket.length,
|
|
113
|
+
durationMs: {
|
|
114
|
+
p50: nearestRankP50(durations),
|
|
115
|
+
max: durations.length === 0 ? null : Math.max(...durations)
|
|
116
|
+
},
|
|
117
|
+
idleGapMs: { p50: nearestRankP50(gaps) },
|
|
118
|
+
session: first.actionKind === "agent.run" ? foldSession(bucket) : null,
|
|
119
|
+
cacheHit: {
|
|
120
|
+
p50: nearestRankP50(hits),
|
|
121
|
+
known: hits.length,
|
|
122
|
+
of: bucket.length
|
|
123
|
+
},
|
|
124
|
+
flags: []
|
|
125
|
+
};
|
|
126
|
+
row.flags = rowFlags(row, windowSec);
|
|
127
|
+
rows.push(row);
|
|
128
|
+
}
|
|
129
|
+
return rows.sort((a, b) => a.node === b.node ? a.actionKind.localeCompare(b.actionKind) : a.node.localeCompare(b.node));
|
|
130
|
+
}
|
|
131
|
+
function buildStepProfile(input) {
|
|
132
|
+
const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
|
|
133
|
+
return {
|
|
134
|
+
workflow: input.workflow,
|
|
135
|
+
windowSec: input.windowSec,
|
|
136
|
+
sampledRuns: input.runs.length,
|
|
137
|
+
rows: buildRows(executions, input.windowSec)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function nonDryRuns(entries) {
|
|
141
|
+
return entries.filter((e) => e.isDryRun !== true && typeof e.runId === "string" && e.runId.length > 0);
|
|
142
|
+
}
|
|
143
|
+
function seconds(value) {
|
|
144
|
+
return value === null ? "?" : `${(value / 1000).toFixed(1)}s`;
|
|
145
|
+
}
|
|
146
|
+
function ratio(value) {
|
|
147
|
+
return value === null ? "?" : value.toFixed(2);
|
|
148
|
+
}
|
|
149
|
+
function formatStepProfileHuman(profile) {
|
|
150
|
+
const lines = [
|
|
151
|
+
`step profile \u2014 ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`
|
|
152
|
+
];
|
|
153
|
+
for (const row of profile.rows) {
|
|
154
|
+
lines.push([
|
|
155
|
+
row.node.padEnd(20),
|
|
156
|
+
row.actionKind.padEnd(14),
|
|
157
|
+
`${row.runs}/${row.executions}`.padEnd(8),
|
|
158
|
+
`p50=${seconds(row.durationMs.p50)}`.padEnd(12),
|
|
159
|
+
`max=${seconds(row.durationMs.max)}`.padEnd(12),
|
|
160
|
+
`idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
|
|
161
|
+
`session=${row.session ?? "?"}`.padEnd(15),
|
|
162
|
+
`cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
|
|
163
|
+
`flags=${row.flags.length === 0 ? "-" : row.flags.join(",")}`
|
|
164
|
+
].join(" "));
|
|
165
|
+
}
|
|
166
|
+
return `${lines.join(`
|
|
167
|
+
`)}
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
function defaultSpurBin() {
|
|
171
|
+
if (process.env.SPUR_BIN)
|
|
172
|
+
return process.env.SPUR_BIN;
|
|
173
|
+
const local = fileURLToPath(new URL("../../../apps/cli/src/index.ts", import.meta.url));
|
|
174
|
+
if (existsSync(local))
|
|
175
|
+
return `bun ${local}`;
|
|
176
|
+
return "spur";
|
|
177
|
+
}
|
|
178
|
+
function positiveInt(raw, fallback) {
|
|
179
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
180
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
181
|
+
}
|
|
182
|
+
function parseStepProfileCliArgs(argv) {
|
|
183
|
+
let workflow = "";
|
|
184
|
+
let spurBin = defaultSpurBin();
|
|
185
|
+
let last = DEFAULT_LAST;
|
|
186
|
+
let windowSec = DEFAULT_WINDOW_SEC;
|
|
187
|
+
let json = false;
|
|
188
|
+
let help = false;
|
|
189
|
+
for (let i = 0;i < argv.length; i++) {
|
|
190
|
+
const a = argv[i];
|
|
191
|
+
if (a === "--help" || a === "-h")
|
|
192
|
+
help = true;
|
|
193
|
+
else if (a === "--json")
|
|
194
|
+
json = true;
|
|
195
|
+
else if (a === "--last")
|
|
196
|
+
last = positiveInt(argv[++i], last);
|
|
197
|
+
else if (a === "--window")
|
|
198
|
+
windowSec = positiveInt(argv[++i], windowSec);
|
|
199
|
+
else if (a === "--spur-bin")
|
|
200
|
+
spurBin = argv[++i] ?? spurBin;
|
|
201
|
+
else if (!a.startsWith("--") && workflow === "")
|
|
202
|
+
workflow = a;
|
|
203
|
+
}
|
|
204
|
+
return { workflow, last, windowSec, spurBin, json, help };
|
|
205
|
+
}
|
|
206
|
+
function runSpurJson(spurBin, args) {
|
|
207
|
+
const binParts = spurBin.split(/\s+/).filter(Boolean);
|
|
208
|
+
const cmd = binParts[0] ?? "spur";
|
|
209
|
+
const cmdArgs = [...binParts.slice(1), ...args];
|
|
210
|
+
const r = spawnSync(cmd, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
211
|
+
const decode = (b) => typeof b === "string" ? b : Buffer.from(b ?? []).toString("utf8");
|
|
212
|
+
return {
|
|
213
|
+
stdout: typeof r.stdout === "string" ? r.stdout : decode(r.stdout),
|
|
214
|
+
stderr: typeof r.stderr === "string" ? r.stderr : decode(r.stderr),
|
|
215
|
+
exitCode: r.status ?? (r.error ? 1 : 0),
|
|
216
|
+
ok: (r.status ?? (r.error ? 1 : 0)) === 0
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
var STEP_PROFILE_USAGE = `usage: workflow-step-profile <workflow> [--last <N>] [--window <sec>] [--json] [--spur-bin <cmd>]
|
|
220
|
+
|
|
221
|
+
Read-only step profile from \`spur workflow trace\`: per node and action kind it reports runs,
|
|
222
|
+
executions, p50/max durationMs, p50 idle gap, session mode and cacheHit p50 with coverage, then
|
|
223
|
+
flags the satellite \xA710 cache-window budgets. --last defaults to ${DEFAULT_LAST} runs and --window
|
|
224
|
+
(W) to ${DEFAULT_WINDOW_SEC} seconds. Unknown evidence is reported as \`?\` / null, never 0.
|
|
225
|
+
|
|
226
|
+
Exit: 0 = profile produced (flags included); 1 = a spur call failed, its JSON did not parse, or the
|
|
227
|
+
workflow argument is missing.`;
|
|
228
|
+
function failure(message) {
|
|
229
|
+
return { exitCode: 1, stdout: "", stderr: `workflow-step-profile: ${message}
|
|
230
|
+
` };
|
|
231
|
+
}
|
|
232
|
+
function runFailure(spurBin, result, what) {
|
|
233
|
+
const detail = result.stderr.trim().split(`
|
|
234
|
+
`).slice(-1)[0] ?? "";
|
|
235
|
+
return failure(`${what} failed (exit ${result.exitCode}${detail === "" ? "" : `: ${detail}`}) [${spurBin}]`);
|
|
236
|
+
}
|
|
237
|
+
function runStepProfileCli(argv) {
|
|
238
|
+
const args = parseStepProfileCliArgs(argv);
|
|
239
|
+
if (args.help)
|
|
240
|
+
return { exitCode: 0, stdout: `${STEP_PROFILE_USAGE}
|
|
241
|
+
`, stderr: "" };
|
|
242
|
+
if (args.workflow === "")
|
|
243
|
+
return failure(`a workflow name is required
|
|
244
|
+
${STEP_PROFILE_USAGE}`);
|
|
245
|
+
const listArgs = [
|
|
246
|
+
"workflow",
|
|
247
|
+
"trace",
|
|
248
|
+
"--workflow",
|
|
249
|
+
args.workflow,
|
|
250
|
+
"--status",
|
|
251
|
+
"done",
|
|
252
|
+
"--last",
|
|
253
|
+
String(args.last),
|
|
254
|
+
"--json"
|
|
255
|
+
];
|
|
256
|
+
const list = runSpurJson(args.spurBin, listArgs);
|
|
257
|
+
if (!list.ok)
|
|
258
|
+
return runFailure(args.spurBin, list, `spur ${listArgs.join(" ")}`);
|
|
259
|
+
let entries;
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(list.stdout);
|
|
262
|
+
if (!Array.isArray(parsed.entries))
|
|
263
|
+
throw new Error('no "entries" array');
|
|
264
|
+
entries = parsed.entries;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
return failure(`spur workflow trace output did not parse as JSON: ${String(err)}`);
|
|
267
|
+
}
|
|
268
|
+
const runs = [];
|
|
269
|
+
for (const entry of nonDryRuns(entries)) {
|
|
270
|
+
const runArgs = ["workflow", "trace", entry.runId, "--json"];
|
|
271
|
+
const timeline = runSpurJson(args.spurBin, runArgs);
|
|
272
|
+
if (!timeline.ok)
|
|
273
|
+
return runFailure(args.spurBin, timeline, `spur ${runArgs.join(" ")}`);
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(timeline.stdout);
|
|
276
|
+
if (!Array.isArray(parsed.events))
|
|
277
|
+
throw new Error('no "events" array');
|
|
278
|
+
runs.push({ runId: entry.runId, events: parsed.events });
|
|
279
|
+
} catch (err) {
|
|
280
|
+
return failure(`spur workflow trace ${entry.runId} output did not parse as JSON: ${String(err)}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const profile = buildStepProfile({ workflow: args.workflow, windowSec: args.windowSec, runs });
|
|
284
|
+
return {
|
|
285
|
+
exitCode: 0,
|
|
286
|
+
stdout: args.json ? `${JSON.stringify(profile, null, 2)}
|
|
287
|
+
` : formatStepProfileHuman(profile),
|
|
288
|
+
stderr: ""
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function main(argv) {
|
|
292
|
+
const { exitCode, stdout, stderr } = runStepProfileCli(argv);
|
|
293
|
+
if (stdout)
|
|
294
|
+
process.stdout.write(stdout);
|
|
295
|
+
if (stderr)
|
|
296
|
+
process.stderr.write(stderr.endsWith(`
|
|
297
|
+
`) ? stderr : `${stderr}
|
|
298
|
+
`);
|
|
299
|
+
return exitCode;
|
|
300
|
+
}
|
|
301
|
+
{
|
|
302
|
+
process.exit(main(process.argv.slice(2)));
|
|
303
|
+
}
|
|
304
|
+
export {
|
|
305
|
+
runStepProfileCli,
|
|
306
|
+
rowFlags,
|
|
307
|
+
parseStepProfileCliArgs,
|
|
308
|
+
nonDryRuns,
|
|
309
|
+
nearestRankP50,
|
|
310
|
+
main,
|
|
311
|
+
formatStepProfileHuman,
|
|
312
|
+
extractExecutions,
|
|
313
|
+
defaultSpurBin,
|
|
314
|
+
buildStepProfile,
|
|
315
|
+
buildRows,
|
|
316
|
+
STEP_PROFILE_USAGE,
|
|
317
|
+
DEFAULT_WINDOW_SEC,
|
|
318
|
+
DEFAULT_LAST
|
|
319
|
+
};
|