@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +42 -0
- package/board/index.d.ts +38 -0
- package/config/plugin-scripts.json +3 -52
- package/config/rules/boundary/sp-script-placement.yaml +19 -0
- package/config/rules/strict/runtime-boundaries.yaml +5 -0
- package/config/rules/structure/test-location.yaml +2 -0
- package/config/rules/typescript/no-syscall-emulation-in-boundary-mock.yaml +1 -1
- package/config/rules/typescript/output-boundaries.yaml +15 -2
- package/config/script-placement-baseline.json +51 -0
- package/config/workflows/feature-verification.yaml +7 -4
- package/config/workflows/history-anatomy.yaml +23 -11
- package/config/workflows/idea-pipeline.yaml +87 -86
- package/config/workflows/pr-review.yaml +18 -9
- package/config/workflows/task-pipeline.yaml +84 -109
- package/config/workflows/wrapup-pipeline.yaml +173 -46
- package/package.json +15 -11
- package/plugins/sp/README.md +8 -10
- package/plugins/sp/agents/super-reviewer.md +43 -18
- package/plugins/sp/commands/dev-fixgha.md +83 -0
- package/plugins/sp/commands/dev-gitmsg.md +8 -6
- package/plugins/sp/commands/dev-gtd.md +2 -2
- package/plugins/sp/commands/dev-idea.md +22 -12
- package/plugins/sp/commands/dev-plan.md +8 -9
- package/plugins/sp/commands/dev-review.md +22 -13
- package/plugins/sp/commands/dev-verify.md +7 -4
- package/plugins/sp/commands/dev-verifyall.md +5 -4
- package/plugins/sp/commands/dev-wrap.md +1 -1
- package/plugins/sp/commands/spur-init.md +2 -2
- package/plugins/sp/hooks/context-post-tool.ts +2 -2
- package/plugins/sp/hooks/context-session-start.ts +2 -1
- package/plugins/sp/hooks/context-session-stop.ts +22 -14
- package/plugins/sp/hooks/pi/guard-extension.ts +109 -150
- package/plugins/sp/lib/history-anatomy.generated.d.mts +112 -0
- package/plugins/sp/lib/history-anatomy.generated.mjs +686 -0
- package/plugins/sp/lib/idea-handoff.generated.mjs +6 -5
- package/plugins/sp/lib/inline-run.generated.d.mts +13 -2
- package/plugins/sp/lib/inline-run.generated.mjs +33 -16
- package/plugins/sp/lib/quality-gate.generated.d.mts +104 -0
- package/plugins/sp/lib/quality-gate.generated.mjs +438 -0
- package/plugins/sp/lib/residual-scan.generated.d.mts +62 -0
- package/plugins/sp/lib/residual-scan.generated.mjs +210 -0
- package/plugins/sp/lib/spur-bin.ts +36 -0
- package/plugins/sp/lib/step-profile.generated.d.mts +71 -0
- package/plugins/sp/lib/step-profile.generated.mjs +174 -0
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/references/roles.md +1 -1
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +3 -3
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +3 -3
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +1 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +9 -7
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +8 -7
- package/plugins/sp/scripts/feature-verification-steps.ts +6 -6
- package/plugins/sp/scripts/history-anatomy-cache.mjs +23 -22
- package/plugins/sp/scripts/history-anatomy-cache.ts +23 -928
- package/plugins/sp/scripts/inline-run-setup.mjs +90 -315
- package/plugins/sp/scripts/inline-run-setup.ts +109 -639
- package/plugins/sp/scripts/pr-reviewing.mjs +2 -0
- package/plugins/sp/scripts/pr-reviewing.ts +1 -0
- package/plugins/sp/scripts/quality-gate.mjs +38 -21
- package/plugins/sp/scripts/quality-gate.ts +20 -660
- package/plugins/sp/scripts/residual-scan.mjs +138 -159
- package/plugins/sp/scripts/residual-scan.ts +102 -499
- package/plugins/sp/scripts/script-root.mjs +129 -0
- package/plugins/sp/scripts/script-root.ts +199 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +27 -16
- package/plugins/sp/scripts/workflow-step-profile.ts +22 -314
- package/plugins/sp/scripts/wrapup-drift-probe.mjs +12 -3
- package/plugins/sp/scripts/wrapup-drift-probe.ts +6 -3
- package/plugins/sp/scripts/wrapup-steps.mjs +42 -40
- package/plugins/sp/scripts/wrapup-steps.ts +64 -46
- package/plugins/sp/skills/code-improvement/SKILL.md +5 -4
- package/plugins/sp/skills/code-verification/SKILL.md +46 -16
- package/plugins/sp/skills/code-verification/references/verdict-schema.md +13 -2
- package/plugins/sp/skills/functional-review/SKILL.md +8 -5
- package/plugins/sp/skills/functional-review/references/verdict-schema.md +1 -1
- package/plugins/sp/skills/history-anatomy/references/modes.md +2 -1
- package/plugins/sp/skills/next-feature/references/handoff-routing.md +1 -1
- package/plugins/sp/skills/next-router/references/routing-table.md +2 -2
- package/plugins/sp/skills/spur-cli/SKILL.md +3 -3
- package/plugins/sp/skills/spur-cli/references/agent.md +10 -10
- package/plugins/sp/skills/spur-cli/references/features.md +17 -6
- package/plugins/sp/skills/spur-cli/references/init.md +17 -16
- package/plugins/sp/skills/spur-cli/references/self.md +3 -2
- package/plugins/sp/skills/spur-cli/references/serve.md +10 -10
- package/plugins/sp/skills/spur-cli/references/tasks/verbs.md +28 -5
- package/plugins/sp/skills/spur-cli/references/tasks.md +14 -8
- package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +4 -3
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +11 -9
- package/plugins/sp/skills/spur-dev/references/decision-brief.md +1 -1
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +86 -70
- package/plugins/sp/skills/spur-dev/references/done-housekeeping.md +3 -3
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +107 -44
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +5 -9
- package/plugins/sp/skills/spur-dev/references/feature-link-helper.md +3 -3
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +31 -21
- package/plugins/sp/skills/spur-dev/references/gate-checklists.md +19 -19
- package/plugins/sp/skills/spur-dev/references/idea-evaluation.md +4 -3
- package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +35 -6
- package/plugins/sp/skills/spur-doctor/SKILL.md +1 -1
- package/plugins/sp/skills/sys-architecture/SKILL.md +3 -2
- package/plugins/sp/tsconfig.json +8 -0
- package/spur.js +6638 -4775
- package/web/_astro/BoardApp.BIjMatT1.js +1 -0
- package/web/_astro/{BoardApp.FTEs3-N8.js → BoardApp.GvjIe9Z6.js} +150 -153
- package/web/_astro/TaskDetail.BZM3EAFx.js +1 -0
- package/web/_astro/_commonjsHelpers.CqkleIqs.js +1 -0
- package/web/_astro/{arc.uuAf51IT.js → arc.HkRiZnoI.js} +1 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.BDdK-tgZ.js +36 -0
- package/web/_astro/{blockDiagram-GPEHLZMM.D1mGCq3p.js → blockDiagram-GPEHLZMM.CZ9kOBxx.js} +1 -1
- package/web/_astro/board-facade-react-dom-client.B2U5wMvk.js +1 -0
- package/web/_astro/board-facade-react-dom.Cd2MuMhF.js +1 -0
- package/web/_astro/board-facade-react-jsx-dev-runtime.B_L3JzfC.js +1 -0
- package/web/_astro/board-facade-react-jsx-runtime.BY2yfHRi.js +1 -0
- package/web/_astro/board-facade-react-router-dom.Dy5pjzOw.js +1 -0
- package/web/_astro/board-facade-react-router.DjjqU27_.js +1 -0
- package/web/_astro/board-facade-react.Dgf4GRyj.js +1 -0
- package/web/_astro/{c4Diagram-AAUBKEIU.CMsolcde.js → c4Diagram-AAUBKEIU.DXK9qeRD.js} +1 -1
- package/web/_astro/channel.DlhX2MEt.js +1 -0
- package/web/_astro/{chunk-2J33WTMH.CrGA3fik.js → chunk-2J33WTMH.BTD2WeX5.js} +1 -1
- package/web/_astro/{chunk-4BX2VUAB.DsLVVla0.js → chunk-4BX2VUAB.Ci6Qbcvk.js} +1 -1
- package/web/_astro/{chunk-55IACEB6.Dxc59Tfi.js → chunk-55IACEB6.yl3zsj7p.js} +1 -1
- package/web/_astro/{chunk-727SXJPM.CaEVE2Wy.js → chunk-727SXJPM.ClXZsfyR.js} +1 -1
- package/web/_astro/{chunk-AQP2D5EJ.BiJ4HXeI.js → chunk-AQP2D5EJ.BmKWZBcP.js} +1 -1
- package/web/_astro/{chunk-FMBD7UC4.Mxf1fru5.js → chunk-FMBD7UC4.By30fcb7.js} +1 -1
- package/web/_astro/chunk-JMJ3UQ3L.BQJEJHu4.js +34 -0
- package/web/_astro/{chunk-ND2GUHAM.pyOWQixH.js → chunk-ND2GUHAM.24NmrD-k.js} +1 -1
- package/web/_astro/{chunk-QZHKN3VN.BmEsg4vr.js → chunk-QZHKN3VN.BN4sKdcS.js} +1 -1
- package/web/_astro/chunk-YNUBSHFH.CxeAygfi.js +7 -0
- package/web/_astro/{classDiagram-4FO5ZUOK.BHhhMFTO.js → classDiagram-4FO5ZUOK.CJpoMPb5.js} +1 -1
- package/web/_astro/{classDiagram-v2-Q7XG4LA2.BHhhMFTO.js → classDiagram-v2-Q7XG4LA2.CJpoMPb5.js} +1 -1
- package/web/_astro/client.BhluLQqe.js +1 -0
- package/web/_astro/client.DBpSRLnx.js +9 -0
- package/web/_astro/cose-bilkent-S5V4N54A.BraxQ2Nt.js +1 -0
- package/web/_astro/{cynefin-OW5HDTMX.C2j1_lKL.js → cynefin-OW5HDTMX.gzoU73oL.js} +1 -1
- package/web/_astro/{dagre-BM42HDAG.hZ2NCTdT.js → dagre-BM42HDAG.wyDfWySU.js} +1 -1
- package/web/_astro/{diagram-2AECGRRQ.Bfw5_EzK.js → diagram-2AECGRRQ.h2_EbMxV.js} +1 -1
- package/web/_astro/{diagram-5GNKFQAL.Bt1V_Tmk.js → diagram-5GNKFQAL.Db6dcs89.js} +1 -1
- package/web/_astro/{diagram-KO2AKTUF.DEk-YFwp.js → diagram-KO2AKTUF.NVwzxu_U.js} +1 -1
- package/web/_astro/{diagram-LMA3HP47.CDjndtQm.js → diagram-LMA3HP47.B-eNgDQk.js} +1 -1
- package/web/_astro/{diagram-OG6HWLK6.cFnUHScG.js → diagram-OG6HWLK6.Bsa8ZRbJ.js} +1 -1
- package/web/_astro/{erDiagram-TEJ5UH35.DlhYp7NV.js → erDiagram-TEJ5UH35.BospRE5Q.js} +1 -1
- package/web/_astro/{flowDiagram-I6XJVG4X.DNTpsxfx.js → flowDiagram-I6XJVG4X.DMzvHwAA.js} +1 -1
- package/web/_astro/ganttDiagram-6RSMTGT7.Dmdxgd-M.js +292 -0
- package/web/_astro/{gitGraphDiagram-PVQCEYII.DkEqNI0P.js → gitGraphDiagram-PVQCEYII.CaqdO0rL.js} +1 -1
- package/web/_astro/index.5dn-Zm-2.js +1 -0
- package/web/_astro/index.C-IqPpW2.js +1 -0
- package/web/_astro/index.Cyc_GA4f.js +1 -0
- package/web/_astro/index.EoZlzLK-.css +1 -0
- package/web/_astro/index.tZfqwSut.js +1 -0
- package/web/_astro/{infoDiagram-5YYISTIA.CrjioCTG.js → infoDiagram-5YYISTIA.DDVD9Wn7.js} +1 -1
- package/web/_astro/{ishikawaDiagram-YF4QCWOH.BcK0CN8n.js → ishikawaDiagram-YF4QCWOH.EKyCUFsL.js} +1 -1
- package/web/_astro/{journeyDiagram-JHISSGLW.p0CbBDX1.js → journeyDiagram-JHISSGLW.By2-RkYB.js} +1 -1
- package/web/_astro/jsx-runtime.DWOiEOII.js +1 -0
- package/web/_astro/{kanban-definition-UN3LZRKU.puHIFt6J.js → kanban-definition-UN3LZRKU.ar-WmlyR.js} +1 -1
- package/web/_astro/{linear.Bb-3a1d7.js → linear.BnzMBgo_.js} +1 -1
- package/web/_astro/{mermaid.core.CJDgXOJs.js → mermaid.core.DSqeFu2Y.js} +4 -4
- package/web/_astro/{mindmap-definition-RKZ34NQL.BoAZEI9t.js → mindmap-definition-RKZ34NQL.1XcF8Wh2.js} +1 -1
- package/web/_astro/{pieDiagram-4H26LBE5.CJNSdqY7.js → pieDiagram-4H26LBE5.DnItHOdK.js} +1 -1
- package/web/_astro/{quadrantDiagram-W4KKPZXB.DtTy7_0Z.js → quadrantDiagram-W4KKPZXB.BQQyrnBt.js} +1 -1
- package/web/_astro/{requirementDiagram-4Y6WPE33.BNYV98Xg.js → requirementDiagram-4Y6WPE33.Dmzxun82.js} +1 -1
- package/web/_astro/{sankeyDiagram-5OEKKPKP.DRq9DGHp.js → sankeyDiagram-5OEKKPKP.BBIeRKYB.js} +1 -1
- package/web/_astro/{sequenceDiagram-3UESZ5HK.B7KdBFCm.js → sequenceDiagram-3UESZ5HK.CUNVdBBf.js} +1 -1
- package/web/_astro/{stateDiagram-AJRCARHV.CD62ZJ2H.js → stateDiagram-AJRCARHV.DlFf2sJm.js} +1 -1
- package/web/_astro/{stateDiagram-v2-BHNVJYJU.CeVk1TAj.js → stateDiagram-v2-BHNVJYJU.DXR7I1H5.js} +1 -1
- package/web/_astro/{timeline-definition-PNZ67QCA.C_ZPA5tT.js → timeline-definition-PNZ67QCA.BcO-GyX2.js} +1 -1
- package/web/_astro/{vennDiagram-CIIHVFJN.CrThO_FQ.js → vennDiagram-CIIHVFJN.ChCECmmW.js} +1 -1
- package/web/_astro/{wardleyDiagram-YWT4CUSO.DSZCA5nl.js → wardleyDiagram-YWT4CUSO.CRaT12mD.js} +1 -1
- package/web/_astro/{xychartDiagram-2RQKCTM6.KI7baTuk.js → xychartDiagram-2RQKCTM6.B3lsHVpj.js} +1 -1
- package/web/board-runtime.json +69 -0
- package/web/index.html +2 -2
- package/plugins/sp/lib/artifact-digest.generated.d.mts +0 -7
- package/plugins/sp/lib/artifact-digest.generated.mjs +0 -48
- package/plugins/sp/scripts/feature-sync-bounded.mjs +0 -298
- package/plugins/sp/scripts/feature-sync-bounded.ts +0 -479
- package/plugins/sp/scripts/idea-coverage-check.ts +0 -168
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +0 -298
- package/plugins/sp/scripts/record-feature-sync.mjs +0 -63
- package/plugins/sp/scripts/record-feature-sync.ts +0 -84
- package/plugins/sp/scripts/script-contract-check.ts +0 -377
- package/plugins/sp/scripts/stage-registry-adapter.ts +0 -1533
- package/plugins/sp/scripts/surface-drift-inventory.ts +0 -989
- package/plugins/sp/scripts/task-evidence-precheck.ts +0 -187
- package/plugins/sp/scripts/task-size-precheck.ts +0 -210
- package/plugins/sp/scripts/transition-shim-check.ts +0 -238
- package/plugins/sp/scripts/validate-commands.ts +0 -689
- package/plugins/sp/scripts/validate-flag-contracts.ts +0 -878
- package/plugins/sp/scripts/verify-answer-lint.ts +0 -547
- package/web/_astro/BoardApp.C02hAHPO.js +0 -1
- package/web/_astro/TaskDetail.C-GdsS-t.js +0 -1
- package/web/_astro/architectureDiagram-3BPJPVTR.CGe629A1.js +0 -36
- package/web/_astro/channel.fsgl7o5j.js +0 -1
- package/web/_astro/client.yhYJvxCU.js +0 -9
- package/web/_astro/cose-bilkent-S5V4N54A.2fH4YOlp.js +0 -1
- package/web/_astro/ganttDiagram-6RSMTGT7.DC_p36PI.js +0 -292
- package/web/_astro/index.De90oHcH.js +0 -1
- 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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Statelessness: the script computes the flags arithmetically and holds no thresholds of its own
|
|
13
|
-
* beyond the §10 window `W` (default 300 s). Unknown evidence stays `null` and never becomes zero:
|
|
14
|
-
* a step with no recorded `cost.exact.cacheHit` reports `{p50: null, known: 0}`, not a 0 hit rate.
|
|
15
|
-
* `cost.estimated` (retroactive mappings) never mixes into `cacheHit`.
|
|
5
|
+
* IO glue only (task 1005 R4, ADR-130 lib rule): argv parsing, `spur workflow trace --json`
|
|
6
|
+
* spawning and the two output modes. The aggregation core (nearestRankP50, extractExecutions,
|
|
7
|
+
* rowFlags, buildRows, buildStepProfile, nonDryRuns, formatStepProfileHuman and their types) lives
|
|
8
|
+
* in packages/app/src/workflow/step-profile.ts and reaches this script through the generated
|
|
9
|
+
* standalone bundle `plugins/sp/lib/step-profile.generated.mjs` — node-builtin only, so the
|
|
10
|
+
* committed `.mjs` twin runs under bare `node`. Regenerate with `bun run build:scripts`.
|
|
16
11
|
*
|
|
17
12
|
* Exit: 0 whenever a profile is produced (including `sampledRuns: 0` and flagged rows). Flags are
|
|
18
13
|
* evidence, not a gate. 1, with a stderr message, only when a `spur` call fails, its JSON does not
|
|
19
14
|
* parse, or the workflow argument is missing.
|
|
20
|
-
*
|
|
21
|
-
* Node-builtin imports only; the pure builders are exported for unit testing, `runStepProfileCli`
|
|
22
|
-
* and `main` do the I/O (mirrors the batch-preflight / feature-sync-bounded script shape).
|
|
23
15
|
*/
|
|
24
|
-
|
|
25
16
|
import { spawnSync } from 'node:child_process';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
* carries no import of the domain package (twins must stay self-contained).
|
|
40
|
-
*/
|
|
41
|
-
export interface ActionCostAttributionLike {
|
|
42
|
-
exact: ActionCostLike | null;
|
|
43
|
-
estimated: ActionCostLike | null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** One timeline event. Non-`action` events are ignored; the rest is optional on purpose. */
|
|
47
|
-
export interface TraceActionEvent {
|
|
48
|
-
kind: string;
|
|
49
|
-
node?: string;
|
|
50
|
-
actionKind?: string;
|
|
51
|
-
durationMs?: number | null;
|
|
52
|
-
startedAt?: string | null;
|
|
53
|
-
completedAt?: string | null;
|
|
54
|
-
invocation?: Record<string, string | number | boolean> | null;
|
|
55
|
-
cost?: ActionCostAttributionLike | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** One sampled run: the run id plus its timeline events. */
|
|
59
|
-
export interface StepProfileRun {
|
|
60
|
-
runId: string;
|
|
61
|
-
events: readonly TraceActionEvent[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** A `spur workflow trace --json` list entry (the fields the profile needs). */
|
|
65
|
-
export interface TraceRunEntry {
|
|
66
|
-
runId: string;
|
|
67
|
-
isDryRun?: boolean;
|
|
68
|
-
status?: string;
|
|
69
|
-
workflowName?: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type SessionMode = 'fresh' | 'resumed' | 'mixed';
|
|
73
|
-
|
|
74
|
-
// ── Output shapes ──────────────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
/** One execution of a node's action kind inside one run. */
|
|
77
|
-
export interface StepProfileExecution {
|
|
78
|
-
runId: string;
|
|
79
|
-
node: string;
|
|
80
|
-
actionKind: string;
|
|
81
|
-
durationMs: number | null;
|
|
82
|
-
idleGapMs: number | null;
|
|
83
|
-
/** `invocation.continue` as recorded; null when the event predates the field. */
|
|
84
|
-
session: 'fresh' | 'resumed' | null;
|
|
85
|
-
cacheHit: number | null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** One (node, actionKind) row — the R1 grain. */
|
|
89
|
-
export interface StepProfileRow {
|
|
90
|
-
node: string;
|
|
91
|
-
actionKind: string;
|
|
92
|
-
runs: number;
|
|
93
|
-
executions: number;
|
|
94
|
-
durationMs: { p50: number | null; max: number | null };
|
|
95
|
-
idleGapMs: { p50: number | null };
|
|
96
|
-
session: SessionMode | null;
|
|
97
|
-
cacheHit: { p50: number | null; known: number; of: number };
|
|
98
|
-
flags: string[];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** The `--json` envelope. */
|
|
102
|
-
export interface StepProfile {
|
|
103
|
-
workflow: string;
|
|
104
|
-
windowSec: number;
|
|
105
|
-
sampledRuns: number;
|
|
106
|
-
rows: StepProfileRow[];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Aggregation ────────────────────────────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
export const DEFAULT_LAST = 20;
|
|
112
|
-
/** §10 window `W`: the shortest common provider cache default, in seconds. */
|
|
113
|
-
export const DEFAULT_WINDOW_SEC = 300;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Nearest-rank median: `sorted[Math.ceil(n / 2) - 1]`, so every reported value is one that was
|
|
117
|
-
* actually observed. Empty input is unknown (`null`), never 0.
|
|
118
|
-
*/
|
|
119
|
-
export function nearestRankP50(values: readonly number[]): number | null {
|
|
120
|
-
if (values.length === 0) return null;
|
|
121
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
122
|
-
return sorted[Math.ceil(sorted.length / 2) - 1] ?? null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function numeric(value: number | null | undefined): number | null {
|
|
126
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function msBetween(from: string | null | undefined, to: string | null | undefined): number | null {
|
|
130
|
-
if (typeof from !== 'string' || typeof to !== 'string') return null;
|
|
131
|
-
const start = Date.parse(from);
|
|
132
|
-
const end = Date.parse(to);
|
|
133
|
-
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
134
|
-
return end - start;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function sessionOf(event: TraceActionEvent): 'fresh' | 'resumed' | null {
|
|
138
|
-
const value = event.invocation?.continue;
|
|
139
|
-
if (value === true) return 'resumed';
|
|
140
|
-
if (value === false) return 'fresh';
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function cacheHitOf(event: TraceActionEvent): number | null {
|
|
145
|
-
// `cost.estimated` is deliberately unread: retroactive mappings never mix with exact evidence.
|
|
146
|
-
return numeric(event.cost?.exact?.cacheHit);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Sort key: a parseable `startedAt`, with the unknown (`unavailable` / null) ordering last. */
|
|
150
|
-
function startKey(event: TraceActionEvent): number {
|
|
151
|
-
const parsed = typeof event.startedAt === 'string' ? Date.parse(event.startedAt) : Number.NaN;
|
|
152
|
-
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* The executions of one run: `kind === 'action'` events in `startedAt` order (trace emits them in
|
|
157
|
-
* row-creation order), each carrying its idle gap (from the previous action's `completedAt` in the
|
|
158
|
-
* same run), session mode and cacheHit. The run's first action has no previous action, so its gap
|
|
159
|
-
* is unknown — never 0.
|
|
160
|
-
*/
|
|
161
|
-
export function extractExecutions(runId: string, events: readonly TraceActionEvent[]): StepProfileExecution[] {
|
|
162
|
-
const actions = events
|
|
163
|
-
.filter((e) => e.kind === 'action')
|
|
164
|
-
.sort((a, b) => {
|
|
165
|
-
const left = startKey(a);
|
|
166
|
-
const right = startKey(b);
|
|
167
|
-
return left === right ? 0 : left - right;
|
|
168
|
-
});
|
|
169
|
-
const executions: StepProfileExecution[] = [];
|
|
170
|
-
for (const [index, event] of actions.entries()) {
|
|
171
|
-
const previous = index === 0 ? undefined : actions[index - 1];
|
|
172
|
-
executions.push({
|
|
173
|
-
runId,
|
|
174
|
-
node: event.node ?? '',
|
|
175
|
-
actionKind: event.actionKind ?? '',
|
|
176
|
-
durationMs: numeric(event.durationMs),
|
|
177
|
-
idleGapMs: msBetween(previous?.completedAt, event.startedAt),
|
|
178
|
-
session: sessionOf(event),
|
|
179
|
-
cacheHit: cacheHitOf(event),
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
return executions;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function foldSession(executions: readonly StepProfileExecution[]): SessionMode | null {
|
|
186
|
-
const known = new Set(executions.map((e) => e.session).filter((s): s is 'fresh' | 'resumed' => s !== null));
|
|
187
|
-
if (known.size === 0) return null;
|
|
188
|
-
if (known.size === 1) return [...known][0] ?? null;
|
|
189
|
-
return 'mixed';
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* The four §10 cache-window flags for one row. `W` is in seconds and durations compare as
|
|
194
|
-
* `ms > W * 1000`, so a step exactly at the budget is inside it.
|
|
195
|
-
*/
|
|
196
|
-
export function rowFlags(row: StepProfileRow, windowSec: number): string[] {
|
|
197
|
-
const wMs = windowSec * 1000;
|
|
198
|
-
const flags: string[] = [];
|
|
199
|
-
const agentRun = row.actionKind === 'agent.run';
|
|
200
|
-
const resumed = row.session === 'resumed' || row.session === 'mixed';
|
|
201
|
-
const p50 = row.durationMs.p50;
|
|
202
|
-
if (!agentRun && p50 !== null && p50 > wMs) flags.push('step-over-window');
|
|
203
|
-
const idle = row.idleGapMs.p50;
|
|
204
|
-
if (agentRun && resumed && idle !== null && idle > wMs) flags.push('resume-after-idle');
|
|
205
|
-
const hit = row.cacheHit.p50;
|
|
206
|
-
if (agentRun && resumed && row.cacheHit.known > 0 && hit !== null && hit < 0.5) flags.push('resume-cold-cache');
|
|
207
|
-
if (agentRun && p50 !== null && p50 > 2 * wMs) flags.push('agent-run-over-2w');
|
|
208
|
-
return flags;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Fold per-run executions into the sorted (node, actionKind) rows. */
|
|
212
|
-
export function buildRows(executions: readonly StepProfileExecution[], windowSec: number): StepProfileRow[] {
|
|
213
|
-
const grouped = new Map<string, StepProfileExecution[]>();
|
|
214
|
-
for (const execution of executions) {
|
|
215
|
-
const key = `${execution.node}\u0000${execution.actionKind}`;
|
|
216
|
-
const bucket = grouped.get(key);
|
|
217
|
-
if (bucket === undefined) grouped.set(key, [execution]);
|
|
218
|
-
else bucket.push(execution);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const rows: StepProfileRow[] = [];
|
|
222
|
-
for (const bucket of grouped.values()) {
|
|
223
|
-
const first = bucket[0];
|
|
224
|
-
if (first === undefined) continue;
|
|
225
|
-
const durations = bucket.map((e) => e.durationMs).filter((d): d is number => d !== null);
|
|
226
|
-
const gaps = bucket.map((e) => e.idleGapMs).filter((g): g is number => g !== null);
|
|
227
|
-
const hits = bucket.map((e) => e.cacheHit).filter((h): h is number => h !== null);
|
|
228
|
-
const row: StepProfileRow = {
|
|
229
|
-
node: first.node,
|
|
230
|
-
actionKind: first.actionKind,
|
|
231
|
-
runs: new Set(bucket.map((e) => e.runId)).size,
|
|
232
|
-
executions: bucket.length,
|
|
233
|
-
durationMs: {
|
|
234
|
-
p50: nearestRankP50(durations),
|
|
235
|
-
max: durations.length === 0 ? null : Math.max(...durations),
|
|
236
|
-
},
|
|
237
|
-
idleGapMs: { p50: nearestRankP50(gaps) },
|
|
238
|
-
session: first.actionKind === 'agent.run' ? foldSession(bucket) : null,
|
|
239
|
-
cacheHit: {
|
|
240
|
-
p50: nearestRankP50(hits),
|
|
241
|
-
known: hits.length,
|
|
242
|
-
of: bucket.length,
|
|
243
|
-
},
|
|
244
|
-
flags: [],
|
|
245
|
-
};
|
|
246
|
-
row.flags = rowFlags(row, windowSec);
|
|
247
|
-
rows.push(row);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return rows.sort((a, b) =>
|
|
251
|
-
a.node === b.node ? a.actionKind.localeCompare(b.actionKind) : a.node.localeCompare(b.node),
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Build the profile over the sampled (already dry-run-filtered) runs. */
|
|
256
|
-
export function buildStepProfile(input: {
|
|
257
|
-
workflow: string;
|
|
258
|
-
windowSec: number;
|
|
259
|
-
runs: readonly StepProfileRun[];
|
|
260
|
-
}): StepProfile {
|
|
261
|
-
const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
|
|
262
|
-
return {
|
|
263
|
-
workflow: input.workflow,
|
|
264
|
-
windowSec: input.windowSec,
|
|
265
|
-
sampledRuns: input.runs.length,
|
|
266
|
-
rows: buildRows(executions, input.windowSec),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** Drop `isDryRun: true` entries (and anything without a run id) before sampling. */
|
|
271
|
-
export function nonDryRuns(entries: readonly TraceRunEntry[]): TraceRunEntry[] {
|
|
272
|
-
return entries.filter((e) => e.isDryRun !== true && typeof e.runId === 'string' && e.runId.length > 0);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ── Output rendering ───────────────────────────────────────────────────────────────────
|
|
276
|
-
|
|
277
|
-
function seconds(value: number | null): string {
|
|
278
|
-
return value === null ? '?' : `${(value / 1000).toFixed(1)}s`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function ratio(value: number | null): string {
|
|
282
|
-
return value === null ? '?' : value.toFixed(2);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/** One line per row; seconds carry one decimal and `?` marks an unknown value. */
|
|
286
|
-
export function formatStepProfileHuman(profile: StepProfile): string {
|
|
287
|
-
const lines = [
|
|
288
|
-
`step profile — ${profile.workflow} (window ${profile.windowSec}s, ${profile.sampledRuns} sampled runs)`,
|
|
289
|
-
];
|
|
290
|
-
for (const row of profile.rows) {
|
|
291
|
-
lines.push(
|
|
292
|
-
[
|
|
293
|
-
row.node.padEnd(20),
|
|
294
|
-
row.actionKind.padEnd(14),
|
|
295
|
-
`${row.runs}/${row.executions}`.padEnd(8),
|
|
296
|
-
`p50=${seconds(row.durationMs.p50)}`.padEnd(12),
|
|
297
|
-
`max=${seconds(row.durationMs.max)}`.padEnd(12),
|
|
298
|
-
`idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
|
|
299
|
-
`session=${row.session ?? '?'}`.padEnd(15),
|
|
300
|
-
`cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
|
|
301
|
-
`flags=${row.flags.length === 0 ? '-' : row.flags.join(',')}`,
|
|
302
|
-
].join(' '),
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
return `${lines.join('\n')}\n`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// ── CLI ────────────────────────────────────────────────────────────────────────────────
|
|
309
|
-
|
|
17
|
+
import { defaultSpurBin, spurCommand } from '../lib/spur-bin';
|
|
18
|
+
import {
|
|
19
|
+
buildStepProfile,
|
|
20
|
+
DEFAULT_LAST,
|
|
21
|
+
DEFAULT_WINDOW_SEC,
|
|
22
|
+
formatStepProfileHuman,
|
|
23
|
+
nonDryRuns,
|
|
24
|
+
type StepProfileRun,
|
|
25
|
+
type TraceActionEvent,
|
|
26
|
+
type TraceRunEntry,
|
|
27
|
+
} from '../lib/step-profile.generated.mjs';
|
|
28
|
+
|
|
29
|
+
export * from '../lib/step-profile.generated.mjs';
|
|
310
30
|
export interface StepProfileCliArgs {
|
|
311
31
|
workflow: string;
|
|
312
32
|
last: number;
|
|
@@ -316,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
|
|
360
|
-
const
|
|
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
|
-
|
|
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
|
|
92
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
84
93
|
return (args) => {
|
|
85
|
-
const run = spawnSync(
|
|
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
|
-
|
|
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
|
|
162
|
+
const { cmd, prefix } = spurCommand(env.spurBin);
|
|
160
163
|
return (args: string[]): SpurShowResult => {
|
|
161
|
-
const run = spawnSync(
|
|
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,
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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,
|