@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.
Files changed (177) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.example.yaml +31 -18
  3. package/config/config.global.yaml +13 -11
  4. package/config/pipeline-budgets.json +34 -2
  5. package/config/plugin-scripts.json +25 -0
  6. package/config/rules/boundary/config-loading-ownership.yaml +0 -3
  7. package/config/rules/boundary/dao-boundary.yaml +4 -17
  8. package/config/rules/boundary/planning-folder-hardcode.yaml +0 -1
  9. package/config/rules/boundary/sp-no-vendor-refs.yaml +3 -2
  10. package/config/rules/boundary/sp-runtime-path.yaml +3 -14
  11. package/config/rules/quality/coverage-gate.yaml +3 -14
  12. package/config/rules/quality/tsdoc-exports.yaml +4 -7
  13. package/config/rules/strict/http-boundaries.yaml +5 -8
  14. package/config/rules/strict/runtime-boundaries.yaml +1 -5
  15. package/config/rules/structure/protected-files.yaml +9 -3
  16. package/config/rules/structure/test-focus-skip.yaml +0 -2
  17. package/config/rules/structure/test-location.yaml +0 -5
  18. package/config/rules/surface/check-cli-surface.yaml +3 -2
  19. package/config/rules/typescript/bun-tooling.yaml +5 -7
  20. package/config/rules/typescript/guarded-happy-dom-register.yaml +0 -2
  21. package/config/rules/typescript/happy-dom-teardown.yaml +0 -2
  22. package/config/rules/typescript/no-biome-suppressions.yaml +0 -2
  23. package/config/rules/typescript/no-debugger.yaml +0 -2
  24. package/config/rules/typescript/no-eslint-suppressions.yaml +0 -4
  25. package/config/rules/typescript/no-leaky-module-mocks.yaml +6 -13
  26. package/config/rules/typescript/no-module-scope-import-calls.yaml +0 -2
  27. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +0 -3
  28. package/config/rules/typescript/no-unmocked-module-eval-side-effects.yaml +0 -3
  29. package/config/rules/typescript/output-boundaries.yaml +0 -3
  30. package/config/rules/typescript/prefer-accessible-role-for-button-queries.yaml +0 -3
  31. package/config/rules/ui/ui-import-boundary.yaml +1 -5
  32. package/config/templates/docs/99_PROJECT_CONSTITUTION.md +75 -13
  33. package/config/transition-shims.json +0 -7
  34. package/config/workflows/basic.yaml +4 -0
  35. package/config/workflows/docs-pipeline.yaml +13 -14
  36. package/config/workflows/feature-dev.yaml +20 -65
  37. package/config/workflows/history-anatomy.yaml +22 -1
  38. package/config/workflows/idea-pipeline.yaml +53 -97
  39. package/config/workflows/pr-review.yaml +21 -33
  40. package/config/workflows/task-pipeline.yaml +87 -330
  41. package/config/workflows/wayfinder-resolution.yaml +12 -26
  42. package/config/workflows/wrapup-pipeline.yaml +48 -189
  43. package/package.json +9 -9
  44. package/plugins/sp/README.md +12 -3
  45. package/plugins/sp/agents/expert-spur.md +41 -19
  46. package/plugins/sp/lib/idea-handoff.generated.d.mts +17 -0
  47. package/plugins/sp/lib/idea-handoff.generated.mjs +1301 -0
  48. package/plugins/sp/plugin.json +1 -1
  49. package/plugins/sp/scripts/feature-dev-precheck.mjs +146 -0
  50. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  51. package/plugins/sp/scripts/idea-handoff.mjs +27 -0
  52. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  53. package/plugins/sp/scripts/inline-run-setup.ts +69 -1
  54. package/plugins/sp/scripts/quality-gate.mjs +216 -0
  55. package/plugins/sp/scripts/quality-gate.ts +287 -0
  56. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -2
  57. package/plugins/sp/scripts/task-size-precheck.ts +44 -16
  58. package/plugins/sp/scripts/verify-answer-lint.ts +17 -1
  59. package/plugins/sp/scripts/workflow-step-profile.mjs +319 -0
  60. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  61. package/plugins/sp/scripts/wrapup-steps.mjs +350 -0
  62. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
  63. package/plugins/sp/skills/code-review/references/review-lenses.md +3 -0
  64. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  65. package/plugins/sp/skills/spec-decomposition/references/decomposition.md +29 -0
  66. package/plugins/sp/skills/spur-cli/SKILL.md +4 -8
  67. package/plugins/sp/skills/spur-cli/references/agent.md +44 -69
  68. package/plugins/sp/skills/spur-cli/references/message.md +30 -3
  69. package/plugins/sp/skills/spur-cli/references/projects.md +25 -1
  70. package/plugins/sp/skills/spur-cli/references/self.md +6 -5
  71. package/plugins/sp/skills/spur-cli/references/serve.md +8 -7
  72. package/plugins/sp/skills/spur-cli/references/tasks.md +1 -1
  73. package/plugins/sp/skills/spur-cli/references/workflows/operations.md +6 -3
  74. package/plugins/sp/skills/spur-cli/references/workflows/workflow-fit-and-tuning.md +57 -18
  75. package/plugins/sp/skills/spur-composer/SKILL.md +145 -0
  76. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +10 -0
  77. package/plugins/sp/skills/spur-dev/references/execution-batch.md +1 -1
  78. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +4 -6
  79. package/plugins/sp/skills/spur-dev/references/glossary.md +1 -1
  80. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +11 -1
  81. package/plugins/sp/skills/spur-dev/references/planning-workflow.md +24 -0
  82. package/plugins/sp/skills/spur-doctor/SKILL.md +138 -0
  83. package/plugins/sp/skills/taste-refactoring-api/README.md +43 -0
  84. package/plugins/sp/skills/taste-refactoring-api/SKILL.md +334 -0
  85. package/plugins/sp/skills/taste-refactoring-api/checklists/daily-api-review.md +71 -0
  86. package/plugins/sp/skills/taste-refactoring-api/examples/refactor-example.md +72 -0
  87. package/plugins/sp/skills/taste-refactoring-api/examples/review-template.md +93 -0
  88. package/plugins/sp/skills/taste-refactoring-api/references/api-refactoring-playbook.md +253 -0
  89. package/plugins/sp/skills/taste-refactoring-api/references/protocol-modes.md +79 -0
  90. package/plugins/sp/skills/taste-refactoring-api/references/research-basis.md +58 -0
  91. package/plugins/sp/skills/taste-refactoring-architect/README.md +26 -0
  92. package/plugins/sp/skills/taste-refactoring-architect/SKILL.md +471 -0
  93. package/plugins/sp/skills/taste-refactoring-architect/checklists/daily-architecture-review.md +48 -0
  94. package/plugins/sp/skills/taste-refactoring-architect/examples/refactor-example.md +55 -0
  95. package/plugins/sp/skills/taste-refactoring-architect/examples/review-template.md +51 -0
  96. package/plugins/sp/skills/taste-refactoring-architect/references/architecture-refactoring-playbook.md +173 -0
  97. package/plugins/sp/skills/taste-refactoring-architect/references/research-basis.md +28 -0
  98. package/plugins/sp/skills/taste-refactoring-tests/README.md +28 -0
  99. package/plugins/sp/skills/taste-refactoring-tests/SKILL.md +482 -0
  100. package/plugins/sp/skills/taste-refactoring-tests/checklists/daily-test-review.md +39 -0
  101. package/plugins/sp/skills/taste-refactoring-tests/examples/refactor-example.md +85 -0
  102. package/plugins/sp/skills/taste-refactoring-tests/examples/review-template.md +59 -0
  103. package/plugins/sp/skills/taste-refactoring-tests/references/research-basis.md +47 -0
  104. package/plugins/sp/skills/taste-refactoring-tests/references/test-refactoring-playbook.md +222 -0
  105. package/plugins/sp/skills/taste-refactoring-ui/README.md +12 -0
  106. package/plugins/sp/skills/taste-refactoring-ui/SKILL.md +290 -0
  107. package/plugins/sp/skills/taste-refactoring-ui/checklists/daily-ui-review.md +72 -0
  108. package/plugins/sp/skills/taste-refactoring-ui/examples/review-template.md +51 -0
  109. package/plugins/sp/skills/taste-refactoring-ui/references/refactoring-ui-playbook.md +170 -0
  110. package/plugins/sp/skills/wayfinder/SKILL.md +2 -2
  111. package/plugins/sp/skills/wayfinder/references/pipeline-resolution.md +30 -0
  112. package/schemas/spur-config.schema.json +105 -85
  113. package/spur.js +44616 -43320
  114. package/web/_astro/BoardApp.D-WlxiN2.js +1 -0
  115. package/web/_astro/{BoardApp.CHQ1lycZ.js → BoardApp.D8bM9pKL.js} +97 -95
  116. package/web/_astro/{TaskDetail.GKfQJ60c.js → TaskDetail.BPRqgVUE.js} +1 -1
  117. package/web/_astro/{arc.DWEtA3Tx.js → arc.BPrPES3z.js} +1 -1
  118. package/web/_astro/{architectureDiagram-3BPJPVTR.DB42oWmP.js → architectureDiagram-3BPJPVTR.qX_7q02P.js} +1 -1
  119. package/web/_astro/{blockDiagram-GPEHLZMM.rhv-zNQV.js → blockDiagram-GPEHLZMM.CUZfj5V7.js} +1 -1
  120. package/web/_astro/{c4Diagram-AAUBKEIU.Ci4-4VvY.js → c4Diagram-AAUBKEIU.CTaOr8hH.js} +1 -1
  121. package/web/_astro/channel.DGZaFHZx.js +1 -0
  122. package/web/_astro/{chunk-2J33WTMH.Cc9veUgf.js → chunk-2J33WTMH.Dt-9wf3h.js} +1 -1
  123. package/web/_astro/{chunk-4BX2VUAB.Bec9c4eI.js → chunk-4BX2VUAB.CTC2sdoN.js} +1 -1
  124. package/web/_astro/{chunk-55IACEB6.DoV8S1iB.js → chunk-55IACEB6.DQcxt2_g.js} +1 -1
  125. package/web/_astro/{chunk-727SXJPM.DwR-Qlyj.js → chunk-727SXJPM.DXFPSn-a.js} +1 -1
  126. package/web/_astro/{chunk-AQP2D5EJ.ND_a81WY.js → chunk-AQP2D5EJ.BCx3U4bT.js} +1 -1
  127. package/web/_astro/{chunk-FMBD7UC4.Wv_jwG48.js → chunk-FMBD7UC4.DL2tJkdO.js} +1 -1
  128. package/web/_astro/{chunk-ND2GUHAM.CXKXCMmp.js → chunk-ND2GUHAM.DZyflMro.js} +1 -1
  129. package/web/_astro/{chunk-QZHKN3VN.nkaoNYQq.js → chunk-QZHKN3VN.CUI2mT09.js} +1 -1
  130. package/web/_astro/{classDiagram-4FO5ZUOK.cMQcVlQu.js → classDiagram-4FO5ZUOK.g4rX4Fr1.js} +1 -1
  131. package/web/_astro/{classDiagram-v2-Q7XG4LA2.cMQcVlQu.js → classDiagram-v2-Q7XG4LA2.g4rX4Fr1.js} +1 -1
  132. package/web/_astro/{cose-bilkent-S5V4N54A.OaDJ7Mr2.js → cose-bilkent-S5V4N54A.CzWJLqp0.js} +1 -1
  133. package/web/_astro/{cynefin-OW5HDTMX.Chi8IphF.js → cynefin-OW5HDTMX.WgsvQeCR.js} +1 -1
  134. package/web/_astro/{cytoscape.esm.DzSz-X2X.js → cytoscape.esm.BB4DxJjf.js} +1 -1
  135. package/web/_astro/{dagre-BM42HDAG.CzK2t_Fp.js → dagre-BM42HDAG.Dzv6ngql.js} +1 -1
  136. package/web/_astro/{diagram-2AECGRRQ.DRvxlVS7.js → diagram-2AECGRRQ.CpJ4a9rU.js} +1 -1
  137. package/web/_astro/{diagram-5GNKFQAL.CnYvNdwA.js → diagram-5GNKFQAL.CzPlF_dq.js} +1 -1
  138. package/web/_astro/{diagram-KO2AKTUF.CpLpMw5R.js → diagram-KO2AKTUF.TAkZNTcQ.js} +1 -1
  139. package/web/_astro/{diagram-LMA3HP47.JTb78qUA.js → diagram-LMA3HP47.uCjoKSag.js} +1 -1
  140. package/web/_astro/{diagram-OG6HWLK6.Bk-1jDIb.js → diagram-OG6HWLK6.eMplIjoK.js} +1 -1
  141. package/web/_astro/{erDiagram-TEJ5UH35.D8hN9GZq.js → erDiagram-TEJ5UH35.Bf7zoXGz.js} +1 -1
  142. package/web/_astro/{flowDiagram-I6XJVG4X.-6zQr6m5.js → flowDiagram-I6XJVG4X.B_bHj3gN.js} +1 -1
  143. package/web/_astro/{ganttDiagram-6RSMTGT7.DboLQ9ca.js → ganttDiagram-6RSMTGT7.BasrHRMj.js} +1 -1
  144. package/web/_astro/{gitGraphDiagram-PVQCEYII.4tYvJKGR.js → gitGraphDiagram-PVQCEYII.C6iphq1x.js} +1 -1
  145. package/web/_astro/index.DayyIngm.css +1 -0
  146. package/web/_astro/{infoDiagram-5YYISTIA.Bd9rXpsB.js → infoDiagram-5YYISTIA.HXmDMhW4.js} +1 -1
  147. package/web/_astro/{ishikawaDiagram-YF4QCWOH.CvMoaf67.js → ishikawaDiagram-YF4QCWOH.BSmW8NiU.js} +1 -1
  148. package/web/_astro/{journeyDiagram-JHISSGLW.Ccy1CA7y.js → journeyDiagram-JHISSGLW.DEQow5fo.js} +1 -1
  149. package/web/_astro/{kanban-definition-UN3LZRKU.0MaMqHNS.js → kanban-definition-UN3LZRKU.IVm9cTdc.js} +1 -1
  150. package/web/_astro/{linear.CHXgcIbN.js → linear.CrsM73_9.js} +1 -1
  151. package/web/_astro/{mermaid.core.Ca-kcelG.js → mermaid.core.CfBeDJls.js} +6 -6
  152. package/web/_astro/{mindmap-definition-RKZ34NQL.BUIDlHa0.js → mindmap-definition-RKZ34NQL.C3j60Y-0.js} +1 -1
  153. package/web/_astro/ordinal.BYWQX77i.js +1 -0
  154. package/web/_astro/{pieDiagram-4H26LBE5.2dX3CU1s.js → pieDiagram-4H26LBE5.B-aCMeEA.js} +1 -1
  155. package/web/_astro/{quadrantDiagram-W4KKPZXB.B3LBlRiv.js → quadrantDiagram-W4KKPZXB.Cib965yq.js} +1 -1
  156. package/web/_astro/{requirementDiagram-4Y6WPE33.X12I2uNx.js → requirementDiagram-4Y6WPE33.D61cS4O-.js} +1 -1
  157. package/web/_astro/{sankeyDiagram-5OEKKPKP.BXohIHqx.js → sankeyDiagram-5OEKKPKP.GKF2qVPy.js} +1 -1
  158. package/web/_astro/{sequenceDiagram-3UESZ5HK.C37ZIUzg.js → sequenceDiagram-3UESZ5HK.DZnq8F2h.js} +1 -1
  159. package/web/_astro/{stateDiagram-AJRCARHV.BRgz317z.js → stateDiagram-AJRCARHV.DXUFmdgM.js} +1 -1
  160. package/web/_astro/{stateDiagram-v2-BHNVJYJU.7VYSXN9-.js → stateDiagram-v2-BHNVJYJU.BtHmhLEz.js} +1 -1
  161. package/web/_astro/{timeline-definition-PNZ67QCA.BVNz_HiN.js → timeline-definition-PNZ67QCA.Cy-WW2ln.js} +1 -1
  162. package/web/_astro/{vennDiagram-CIIHVFJN.CHVDkPX4.js → vennDiagram-CIIHVFJN.SLp5b9KI.js} +1 -1
  163. package/web/_astro/{wardleyDiagram-YWT4CUSO.EQQ_qT9v.js → wardleyDiagram-YWT4CUSO.Bww45mWV.js} +1 -1
  164. package/web/_astro/{xychartDiagram-2RQKCTM6.DrAT9WoP.js → xychartDiagram-2RQKCTM6.DR4swI6a.js} +1 -1
  165. package/web/apple-touch-icon.png +0 -0
  166. package/web/favicon.ico +0 -0
  167. package/web/favicon.svg +17 -4
  168. package/web/icon-192.png +0 -0
  169. package/web/icon-512.png +0 -0
  170. package/web/index.html +2 -2
  171. package/web/site.webmanifest +31 -0
  172. package/web/spur_logo.svg +1 -0
  173. package/plugins/sp/skills/spur-cli/references/team.md +0 -145
  174. package/web/_astro/BoardApp.DV9kx0wo.js +0 -1
  175. package/web/_astro/channel.BAI6xLeV.js +0 -1
  176. package/web/_astro/index.Dcr_8fiK.css +0 -1
  177. 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
+ };