@gobing-ai/spur 0.3.94 → 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.
Files changed (198) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +42 -0
  3. package/board/index.d.ts +38 -0
  4. package/config/plugin-scripts.json +3 -52
  5. package/config/rules/boundary/sp-script-placement.yaml +19 -0
  6. package/config/rules/strict/runtime-boundaries.yaml +5 -0
  7. package/config/rules/structure/test-location.yaml +2 -0
  8. package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
  9. package/config/rules/typescript/output-boundaries.yaml +15 -2
  10. package/config/script-placement-baseline.json +51 -0
  11. package/config/workflows/feature-verification.yaml +7 -4
  12. package/config/workflows/history-anatomy.yaml +23 -11
  13. package/config/workflows/idea-pipeline.yaml +87 -86
  14. package/config/workflows/pr-review.yaml +18 -9
  15. package/config/workflows/task-pipeline.yaml +84 -109
  16. package/config/workflows/wrapup-pipeline.yaml +173 -46
  17. package/package.json +15 -11
  18. package/plugins/sp/README.md +8 -10
  19. package/plugins/sp/agents/super-reviewer.md +43 -18
  20. package/plugins/sp/commands/dev-fixgha.md +83 -0
  21. package/plugins/sp/commands/dev-gitmsg.md +8 -6
  22. package/plugins/sp/commands/dev-gtd.md +2 -2
  23. package/plugins/sp/commands/dev-idea.md +22 -12
  24. package/plugins/sp/commands/dev-plan.md +8 -9
  25. package/plugins/sp/commands/dev-review.md +22 -13
  26. package/plugins/sp/commands/dev-verify.md +7 -4
  27. package/plugins/sp/commands/dev-verifyall.md +5 -4
  28. package/plugins/sp/commands/dev-wrap.md +1 -1
  29. package/plugins/sp/commands/spur-init.md +2 -2
  30. package/plugins/sp/hooks/context-post-tool.ts +2 -2
  31. package/plugins/sp/hooks/context-session-start.ts +2 -1
  32. package/plugins/sp/hooks/context-session-stop.ts +22 -14
  33. package/plugins/sp/hooks/pi/guard-extension.ts +109 -150
  34. package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
  35. package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
  36. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -5
  37. package/plugins/sp/lib/inline-run.generated.d.mts +13 -2
  38. package/plugins/sp/lib/inline-run.generated.mjs +33 -16
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
  40. package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
  42. package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
  43. package/plugins/sp/lib/spur-bin.ts +36 -0
  44. package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
  45. package/plugins/sp/lib/step-profile.generated.mjs +174 -0
  46. package/plugins/sp/plugin.json +1 -1
  47. package/plugins/sp/references/roles.md +1 -1
  48. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
  49. package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
  50. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
  51. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
  52. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
  53. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
  54. package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
  55. package/plugins/sp/scripts/history-anatomy-cache.mjs +23 -22
  56. package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
  57. package/plugins/sp/scripts/inline-run-setup.mjs +90 -315
  58. package/plugins/sp/scripts/inline-run-setup.ts +109 -639
  59. package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
  60. package/plugins/sp/scripts/pr-reviewing.ts +1 -0
  61. package/plugins/sp/scripts/quality-gate.mjs +38 -21
  62. package/plugins/sp/scripts/quality-gate.ts +20 -660
  63. package/plugins/sp/scripts/residual-scan.mjs +138 -159
  64. package/plugins/sp/scripts/residual-scan.ts +102 -499
  65. package/plugins/sp/scripts/script-root.mjs +129 -0
  66. package/plugins/sp/scripts/script-root.ts +199 -0
  67. package/plugins/sp/scripts/workflow-step-profile.mjs +27 -16
  68. package/plugins/sp/scripts/workflow-step-profile.ts +22 -314
  69. package/plugins/sp/scripts/wrapup-drift-probe.mjs +12 -3
  70. package/plugins/sp/scripts/wrapup-drift-probe.ts +6 -3
  71. package/plugins/sp/scripts/wrapup-steps.mjs +42 -40
  72. package/plugins/sp/scripts/wrapup-steps.ts +64 -46
  73. package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
  74. package/plugins/sp/skills/code-verification/SKILL.md +46 -16
  75. package/plugins/sp/skills/code-verification/references/verdict-schema.md +13 -2
  76. package/plugins/sp/skills/functional-review/SKILL.md +8 -5
  77. package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
  78. package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
  79. package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
  80. package/plugins/sp/skills/next-router/references/routing-table.md +2 -2
  81. package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
  82. package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
  83. package/plugins/sp/skills/spur-cli/references/features.md +17 -6
  84. package/plugins/sp/skills/spur-cli/references/init.md +17 -16
  85. package/plugins/sp/skills/spur-cli/references/self.md +3 -2
  86. package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
  87. package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +28 -5
  88. package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
  89. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
  90. package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
  91. package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
  93. package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +107 -44
  95. package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
  96. package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
  97. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
  98. package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
  99. package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
  100. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +35 -6
  101. package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
  102. package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
  103. package/plugins/sp/tsconfig.json +8 -0
  104. package/spur.js +6638 -4775
  105. package/web/_astro/BoardApp.BIjMatT1.js +1 -0
  106. package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.GvjIe9Z6.js} +150 -153
  107. package/web/_astro/TaskDetail.BZM3EAFx.js +1 -0
  108. package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
  109. package/web/_astro/{arc.uuAf51IT.js → arc.HkRiZnoI.js} +1 -1
  110. package/web/_astro/architectureDiagram-3BPJPVTR.BDdK-tgZ.js +36 -0
  111. package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
  112. package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
  113. package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
  114. package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
  115. package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
  116. package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
  117. package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
  118. package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
  119. package/web/_astro/{c4Diagram-AAUBKEIU.CMsolcde.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
  120. package/web/_astro/channel.DlhX2MEt.js +1 -0
  121. package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
  122. package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
  123. package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
  124. package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
  125. package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
  126. package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
  127. package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
  128. package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
  129. package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
  130. package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
  131. package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
  132. package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
  133. package/web/_astro/client.BhluLQqe.js +1 -0
  134. package/web/_astro/client.DBpSRLnx.js +9 -0
  135. package/web/_astro/cose-bilkent-S5V4N54A.BraxQ2Nt.js +1 -0
  136. package/web/_astro/{cynefin-OW5HDTMX.C2j1_lKL.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
  137. package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
  138. package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
  139. package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
  140. package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
  141. package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
  142. package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
  143. package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
  144. package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
  145. package/web/_astro/ganttDiagram-6RSMTGT7.Dmdxgd-M.js +292 -0
  146. package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
  147. package/web/_astro/index.5dn-Zm-2.js +1 -0
  148. package/web/_astro/index.C-IqPpW2.js +1 -0
  149. package/web/_astro/index.Cyc_GA4f.js +1 -0
  150. package/web/_astro/index.EoZlzLK-.css +1 -0
  151. package/web/_astro/index.tZfqwSut.js +1 -0
  152. package/web/_astro/{infoDiagram-5YYISTIA.CrjioCTG.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
  153. package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
  154. package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
  155. package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
  156. package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
  157. package/web/_astro/{linear.Bb-3a1d7.js → linear.BnzMBgo_.js} +1 -1
  158. package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.DSqeFu2Y.js} +4 -4
  159. package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
  160. package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
  161. package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
  162. package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
  163. package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
  164. package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
  165. package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
  166. package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
  167. package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
  168. package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
  169. package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
  170. package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
  171. package/web/board-runtime.json +69 -0
  172. package/web/index.html +2 -2
  173. package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
  174. package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
  175. package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -298
  176. package/plugins/sp/scripts/feature-sync-bounded.ts +0 -479
  177. package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
  178. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
  179. package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
  180. package/plugins/sp/scripts/record-feature-sync.ts +0 -84
  181. package/plugins/sp/scripts/script-contract-check.ts +0 -377
  182. package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
  183. package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
  184. package/plugins/sp/scripts/task-evidence-precheck.ts +0 -187
  185. package/plugins/sp/scripts/task-size-precheck.ts +0 -210
  186. package/plugins/sp/scripts/transition-shim-check.ts +0 -238
  187. package/plugins/sp/scripts/validate-commands.ts +0 -689
  188. package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
  189. package/plugins/sp/scripts/verify-answer-lint.ts +0 -547
  190. package/web/_astro/BoardApp.C02hAHPO.js +0 -1
  191. package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
  192. package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
  193. package/web/_astro/channel.fsgl7o5j.js +0 -1
  194. package/web/_astro/client.yhYJvxCU.js +0 -9
  195. package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
  196. package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
  197. package/web/_astro/index.De90oHcH.js +0 -1
  198. package/web/_astro/index.Hjbr15fG.css +0 -1
@@ -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
- * ADR-115 sets step boundaries by the provider cache window, not the clock, and satellite §10 owns
6
- * the four budgets that follow from it. Trace already records each action event's `durationMs`,
7
- * `startedAt`/`completedAt`, `invocation` and `cost`, but nothing aggregates it per node. This
8
- * read-only plugin script (ADR-065) composes the existing `spur workflow trace --json` surface
9
- * (governance §2) into per-node/action-kind rows so doctor maps flag ids to proposals instead of
10
- * re-deriving numbers from prose.
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 { existsSync } from 'node:fs';
27
- import { fileURLToPath } from 'node:url';
28
- import { getEnvVar } from '../lib/env';
29
-
30
- // ── Input shapes (a subset of the live `spur workflow trace --json` payload) ─────────────
31
-
32
- /** One `cost.exact` / `cost.estimated` bucket; only `cacheHit` is read. */
33
- export interface ActionCostLike {
34
- cacheHit: number | null;
35
- }
36
-
37
- /**
38
- * `ActionCostAttribution` as trace emits it. Both classes are read structurally so the script
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,18 +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
- if (getEnvVar('SPUR_BIN')) return getEnvVar('SPUR_BIN');
326
- const local = fileURLToPath(new URL('../../../apps/cli/src/index.ts', import.meta.url));
327
- if (existsSync(local)) return `bun ${local}`;
328
- return 'spur';
329
- }
330
-
331
39
  function positiveInt(raw: string | undefined, fallback: number): number {
332
40
  const parsed = Number.parseInt(raw ?? '', 10);
333
41
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
@@ -343,6 +51,7 @@ export function parseStepProfileCliArgs(argv: string[]): StepProfileCliArgs {
343
51
 
344
52
  for (let i = 0; i < argv.length; i++) {
345
53
  const a = argv[i];
54
+ if (a === undefined) continue;
346
55
  if (a === '--help' || a === '-h') help = true;
347
56
  else if (a === '--json') json = true;
348
57
  else if (a === '--last') last = positiveInt(argv[++i], last);
@@ -356,9 +65,8 @@ export function parseStepProfileCliArgs(argv: string[]): StepProfileCliArgs {
356
65
  type SpawnResult = { stdout: string; stderr: string; exitCode: number; ok: boolean };
357
66
 
358
67
  function runSpurJson(spurBin: string, args: string[]): SpawnResult {
359
- const binParts = spurBin.split(/\s+/).filter(Boolean);
360
- const cmd = binParts[0] ?? 'spur';
361
- const cmdArgs = [...binParts.slice(1), ...args];
68
+ const { cmd, prefix } = spurCommand(spurBin);
69
+ const cmdArgs = [...prefix, ...args];
362
70
  const r = spawnSync(cmd, cmdArgs, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
363
71
  const decode = (b: unknown): string =>
364
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 = [
@@ -57,6 +63,8 @@ function changedPathsOf(section) {
57
63
  const paths = [];
58
64
  for (const match of section.matchAll(CHANGE_ENTRY)) {
59
65
  const path = match[1];
66
+ if (path === undefined)
67
+ continue;
60
68
  if (/\s/.test(path))
61
69
  continue;
62
70
  if (!path.includes("/") && !path.includes("."))
@@ -73,16 +81,17 @@ var SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfaceR
73
81
  function driftReasonForPath(path) {
74
82
  if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix)))
75
83
  return null;
76
- if (path.includes("/") && !KNOWN_TOP_LEVEL.has(path.split("/")[0])) {
84
+ const top = path.split("/")[0];
85
+ if (path.includes("/") && top !== undefined && !KNOWN_TOP_LEVEL.has(top)) {
77
86
  return "new top-level workspace directory";
78
87
  }
79
88
  const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
80
89
  return match ? `matches doc-owned surface ${match.glob}` : null;
81
90
  }
82
91
  function defaultSpurRunner(env, cwd) {
83
- const parts = (env.spurBin ?? "spur").split(/\s+/).filter((part) => part.length > 0);
92
+ const { cmd, prefix } = spurCommand(env.spurBin);
84
93
  return (args) => {
85
- const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
94
+ const run = spawnSync(cmd, [...prefix, ...args], {
86
95
  cwd,
87
96
  encoding: "utf8",
88
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;
@@ -125,6 +126,7 @@ export function changedPathsOf(section: string | null): string[] {
125
126
  const paths: string[] = [];
126
127
  for (const match of section.matchAll(CHANGE_ENTRY)) {
127
128
  const path = match[1];
129
+ if (path === undefined) continue;
128
130
  // Path-like: no whitespace and at least one separator or dotted suffix, so prose
129
131
  // backticks never masquerade as change-map entries.
130
132
  if (/\s/.test(path)) continue;
@@ -148,7 +150,8 @@ const SURFACE_MATCHERS = DOC_OWNED_SURFACES.map((glob) => ({ glob, regex: surfac
148
150
  /** 0944 R2 surface match: doc-owned glob, corpus exclusion, new top-level directory. */
149
151
  export function driftReasonForPath(path: string): string | null {
150
152
  if (CORPUS_PREFIXES.some((prefix) => path.startsWith(prefix))) return null;
151
- if (path.includes('/') && !KNOWN_TOP_LEVEL.has(path.split('/')[0])) {
153
+ const top = path.split('/')[0];
154
+ if (path.includes('/') && top !== undefined && !KNOWN_TOP_LEVEL.has(top)) {
152
155
  return 'new top-level workspace directory';
153
156
  }
154
157
  const match = SURFACE_MATCHERS.find((entry) => entry.regex.test(path));
@@ -156,9 +159,9 @@ export function driftReasonForPath(path: string): string | null {
156
159
  }
157
160
 
158
161
  function defaultSpurRunner(env: DriftProbeEnv, cwd?: string): SpurRunner {
159
- const parts = (env.spurBin ?? 'spur').split(/\s+/).filter((part) => part.length > 0);
162
+ const { cmd, prefix } = spurCommand(env.spurBin);
160
163
  return (args: string[]): SpurShowResult => {
161
- const run = spawnSync(parts[0], [...parts.slice(1), ...args], {
164
+ const run = spawnSync(cmd, [...prefix, ...args], {
162
165
  cwd,
163
166
  encoding: 'utf8',
164
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, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
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], {
@@ -172,6 +174,31 @@ function readFileSyncSafe(path) {
172
174
  return null;
173
175
  }
174
176
  }
177
+ function verdictFromTestingSection(content) {
178
+ const heading = /^#{1,6}\s+Testing\s*$/m.exec(content);
179
+ if (!heading)
180
+ return null;
181
+ const level = heading[0].match(/^#+/)?.[0]?.length ?? 2;
182
+ const rest = content.slice(heading.index + heading[0].length);
183
+ const next = new RegExp(`^#{1,${level}}\\s+\\S`, "m").exec(rest);
184
+ const section = next ? rest.slice(0, next.index) : rest;
185
+ for (const line of section.split(`
186
+ `)) {
187
+ const m = /^(?:-\s*|\*\*)?Verdict:\s*(PASS|PARTIAL|FAIL|UNKNOWN)\b/i.exec(line.trim());
188
+ if (m?.[1] !== undefined)
189
+ return m[1].toUpperCase();
190
+ }
191
+ return null;
192
+ }
193
+ function verdictOfArtifact(verdictPath) {
194
+ try {
195
+ const raw = jqPick(JSON.parse(readFileSync(verdictPath, "utf8")).verdict, "");
196
+ const text = jqText(raw);
197
+ return text.length > 0 ? text : null;
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
175
202
  function runMetrics(env, options = {}) {
176
203
  const cwd = options.cwd;
177
204
  const runId = env.__runId ?? "";
@@ -212,15 +239,13 @@ function runMetrics(env, options = {}) {
212
239
  const frontmatter = parsed.frontmatter !== null && typeof parsed.frontmatter === "object" ? parsed.frontmatter : {};
213
240
  const featureId = String(jqPick(frontmatter.feature_id, parsed.feature_id, ""));
214
241
  const status2 = String(jqPick(frontmatter.status, parsed.status, "unknown"));
215
- let verdict = "UNKNOWN";
216
242
  const verdictPath = join(".spur", "run", `${wbs}-verdict.json`);
217
- if (existsSync(abs(verdictPath))) {
218
- try {
219
- const raw = jqPick(JSON.parse(readFileSync(abs(verdictPath), "utf8")).verdict, "UNKNOWN");
220
- const text = raw === "UNKNOWN" ? "UNKNOWN" : jqText(raw);
221
- if (text.length > 0)
222
- verdict = text;
223
- } catch {}
243
+ const artifactVerdict = verdictOfArtifact(abs(verdictPath));
244
+ const trackedVerdict = verdictFromTestingSection(typeof parsed.content === "string" ? parsed.content : "");
245
+ const verdict = artifactVerdict ?? trackedVerdict ?? "UNKNOWN";
246
+ if (verdict === "UNKNOWN") {
247
+ process.stderr.write(`metrics-record: task ${wbs} has no certifying verdict \u2014 ${verdictPath}: ${artifactVerdict ?? "missing or carries none"}, tracked Testing: ${trackedVerdict ?? "no Verdict: line"} \u2014 recording UNKNOWN telemetry
248
+ `);
224
249
  }
225
250
  const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
226
251
  const row = { wbs, feature_id: featureId, status: status2, verdict, timestamp };
@@ -300,32 +325,9 @@ function runFeatureTransition(env, options = {}) {
300
325
  }
301
326
  let syncOutput = "";
302
327
  let syncRc = 1;
303
- const boundedTs = join("plugins", "sp", "scripts", "feature-sync-bounded.ts");
304
- const boundedArgs = [feature, "--spur-bin", env.spurBin ?? "spur", "--json"];
305
- if (existsSync(cwd ? join(cwd, boundedTs) : boundedTs)) {
306
- const result = spawnSync("bun", [boundedTs, ...boundedArgs], { cwd, encoding: "utf8" });
307
- syncOutput = result.stdout ?? "";
308
- syncRc = result.status ?? 1;
309
- if (result.stderr !== null && result.stderr.length > 0)
310
- process.stderr.write(result.stderr);
311
- } else {
312
- const probe = spawnSync("superskill", ["script", "path", "sp", "feature-sync-bounded.mjs"], {
313
- cwd,
314
- encoding: "utf8"
315
- });
316
- const twin = probe.status === 0 ? (probe.stdout ?? "").trim() : "";
317
- if (twin.length > 0 && existsSync(twin)) {
318
- const result = spawnSync("node", [twin, ...boundedArgs], { cwd, encoding: "utf8" });
319
- syncOutput = result.stdout ?? "";
320
- syncRc = result.status ?? 1;
321
- if (result.stderr !== null && result.stderr.length > 0)
322
- process.stderr.write(result.stderr);
323
- } else {
324
- const result = spur(env, ["feature", "sync", feature, "--json"], { cwd, stderr: "inherit" });
325
- syncOutput = result.stdout;
326
- syncRc = result.status;
327
- }
328
- }
328
+ const sync = spur(env, ["feature", "sync", feature, "--json"], { cwd, stderr: "inherit" });
329
+ syncOutput = sync.stdout;
330
+ syncRc = sync.status;
329
331
  process.stdout.write(`${syncOutput}
330
332
  `);
331
333
  const shown = spur(env, ["feature", "show", feature, "--json"], { cwd });
@@ -403,8 +405,8 @@ function main(argv, env = getEnvVars(), options = {}) {
403
405
  }
404
406
  export {
405
407
  writeRouteReason,
408
+ verdictFromTestingSection,
406
409
  taskStatusOf,
407
- spurCommand,
408
410
  runMetrics,
409
411
  runFeatureTransition,
410
412
  resolveTasks,