@gobing-ai/spur 0.4.1 → 0.4.3

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 (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +4 -2
  3. package/config/decisions/maker-liveness.yaml +39 -0
  4. package/config/plugin-scripts.json +0 -4
  5. package/config/rules/README.md +1 -0
  6. package/config/rules/docs/sourced-api-claims.yaml +56 -12
  7. package/config/rules/strict/runtime-boundaries.yaml +7 -0
  8. package/config/script-placement-baseline.json +1 -19
  9. package/config/templates/AGENTS.md +1 -1
  10. package/config/workflows/feature-lifecycle.yaml +46 -1
  11. package/config/workflows/feature-verification.yaml +11 -0
  12. package/config/workflows/wrapup-pipeline.yaml +1 -1
  13. package/package.json +10 -10
  14. package/plugins/sp/README.md +23 -18
  15. package/plugins/sp/agents/super-planner.md +5 -6
  16. package/plugins/sp/agents/super-reviewer.md +43 -0
  17. package/plugins/sp/bunfig.toml +8 -5
  18. package/plugins/sp/commands/dev-review-session.md +2 -1
  19. package/plugins/sp/commands/dev-runall.md +18 -9
  20. package/plugins/sp/hooks/careful-guard.ts +1 -1
  21. package/plugins/sp/hooks/context-post-tool.ts +7 -404
  22. package/plugins/sp/hooks/context-session-start.ts +6 -192
  23. package/plugins/sp/hooks/context-session-stop.ts +6 -91
  24. package/plugins/sp/hooks/pi/guard-extension.ts +26 -12
  25. package/plugins/sp/hooks/task-write-guard.ts +1 -1
  26. package/plugins/sp/{hooks → lib}/agent-hint.ts +1 -1
  27. package/plugins/sp/lib/context-owner.ts +56 -0
  28. package/plugins/sp/lib/context-post-tool.ts +432 -0
  29. package/plugins/sp/lib/context-session-start.ts +214 -0
  30. package/plugins/sp/lib/context-session-stop.ts +196 -0
  31. package/plugins/sp/lib/dogfood-pipeline.ts +265 -0
  32. package/plugins/sp/lib/dogfood-report.ts +278 -0
  33. package/plugins/sp/lib/idea-handoff.generated.mjs +6 -4
  34. package/plugins/sp/lib/inline-run-app.ts +40 -0
  35. package/plugins/sp/lib/inline-run.generated.d.mts +3 -3
  36. package/plugins/sp/lib/inline-run.generated.mjs +1361 -87
  37. package/plugins/sp/lib/plan-projection.generated.d.mts +169 -0
  38. package/plugins/sp/lib/plan-projection.generated.mjs +704 -1
  39. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
  40. package/plugins/sp/lib/quality-gate.generated.mjs +62 -231
  41. package/plugins/sp/lib/residual-scan.generated.d.mts +7 -1
  42. package/plugins/sp/lib/residual-scan.generated.mjs +71 -6
  43. package/plugins/sp/lib/run-summary-core.ts +34 -370
  44. package/plugins/sp/lib/transcript.ts +37 -200
  45. package/plugins/sp/plugin.json +1 -1
  46. package/plugins/sp/references/roles.md +1 -1
  47. package/plugins/sp/scripts/batch-plan.mjs +740 -1
  48. package/plugins/sp/scripts/batch-plan.ts +48 -3
  49. package/plugins/sp/scripts/batch-preflight.mjs +23 -1
  50. package/plugins/sp/scripts/batch-preflight.ts +35 -295
  51. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +108 -87
  52. package/plugins/sp/scripts/daily-summary/daily-summary.ts +111 -104
  53. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +2 -1
  54. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +7 -265
  55. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +3 -0
  56. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +6 -272
  57. package/plugins/sp/scripts/feature-verification-steps.mjs +7 -123
  58. package/plugins/sp/scripts/feature-verification-steps.ts +29 -181
  59. package/plugins/sp/scripts/inline-run-dispatch.mjs +27 -26
  60. package/plugins/sp/scripts/inline-run-dispatch.ts +1 -21
  61. package/plugins/sp/scripts/inline-run-setup.mjs +158 -120
  62. package/plugins/sp/scripts/inline-run-setup.ts +1 -21
  63. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  64. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  65. package/plugins/sp/scripts/quality-gate.mjs +38 -208
  66. package/plugins/sp/scripts/residual-scan.mjs +73 -6
  67. package/plugins/sp/scripts/residual-scan.ts +8 -5
  68. package/plugins/sp/scripts/run-summary.mjs +160 -95
  69. package/plugins/sp/scripts/run-summary.ts +7 -5
  70. package/plugins/sp/scripts/session-timeline.mjs +57 -29
  71. package/plugins/sp/scripts/session-timeline.ts +4 -18
  72. package/plugins/sp/scripts/task-diffstat.mjs +43 -30
  73. package/plugins/sp/scripts/task-diffstat.ts +40 -71
  74. package/plugins/sp/skills/code-implementation/SKILL.md +7 -1
  75. package/plugins/sp/skills/code-verification/references/secu-review.md +2 -1
  76. package/plugins/sp/skills/code-verification/references/verdict-schema.md +4 -0
  77. package/plugins/sp/skills/dogfood-testing/SKILL.md +2 -2
  78. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  79. package/plugins/sp/skills/next-feature/SKILL.md +1 -1
  80. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  81. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +2 -2
  82. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  83. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  84. package/plugins/sp/skills/session-review/SKILL.md +5 -3
  85. package/plugins/sp/skills/source-driven-development/SKILL.md +7 -4
  86. package/plugins/sp/skills/spur-cli/references/builder.md +3 -2
  87. package/plugins/sp/skills/spur-cli/references/decision.md +40 -8
  88. package/plugins/sp/skills/spur-cli/references/features.md +46 -5
  89. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  90. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  91. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
  92. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
  93. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +6 -1
  94. package/plugins/sp/skills/spur-dev/references/execution-batch.md +56 -7
  95. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
  96. package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +22 -0
  97. package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +14 -0
  98. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +13 -11
  99. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +37 -25
  100. package/spur.js +89792 -76992
  101. package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.5IIl2O-z.js} +1 -1
  102. package/web/_astro/BoardApp.Drnx-KeM.js +324 -0
  103. package/web/_astro/TaskDetail.Ci4LNn9S.js +1 -0
  104. package/web/_astro/{arc._BUadrdH.js → arc.j2EVkYVd.js} +1 -1
  105. package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DZWSFqEG.js} +1 -1
  106. package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.DE6GpXCN.js} +1 -1
  107. package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.DoVR1G6B.js} +1 -1
  108. package/web/_astro/channel.DmdDxGsq.js +1 -0
  109. package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.Do6-Gbyf.js} +1 -1
  110. package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.BYV1HGHx.js} +1 -1
  111. package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.CvI1zkrc.js} +1 -1
  112. package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DNGSCRlX.js} +1 -1
  113. package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.DXhhgT5z.js} +1 -1
  114. package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.dzkRKP2u.js} +1 -1
  115. package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.DujGLeEf.js} +1 -1
  116. package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.BWtIT3ZU.js} +1 -1
  117. package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.DyApg0Nn.js} +1 -1
  118. package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.DyApg0Nn.js} +1 -1
  119. package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.DJQB-u_l.js} +1 -1
  120. package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.bi3D_siV.js} +1 -1
  121. package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.C-f7TrRg.js} +1 -1
  122. package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DrPfjHQB.js} +1 -1
  123. package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.C9S4WUHO.js} +1 -1
  124. package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.Dzzye8QH.js} +1 -1
  125. package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.DX-LRiof.js} +1 -1
  126. package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.Z-kAKwIz.js} +1 -1
  127. package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.D6dyClSv.js} +1 -1
  128. package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.Cx3q2cxD.js} +1 -1
  129. package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.DU3gH6CT.js} +1 -1
  130. package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.ISmCnJ87.js} +1 -1
  131. package/web/_astro/index.Do2lLdgo.css +1 -0
  132. package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DR6_iNOK.js} +1 -1
  133. package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.D7wY-ZGA.js} +1 -1
  134. package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.DkP9-WK8.js} +1 -1
  135. package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.wtgY98M2.js} +1 -1
  136. package/web/_astro/{linear.MzmRrNER.js → linear.CI6djNi7.js} +1 -1
  137. package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CuQPxS8b.js} +4 -4
  138. package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.5HNIEG7R.js} +1 -1
  139. package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.CchhDFjE.js} +1 -1
  140. package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.PYXXJPSJ.js} +1 -1
  141. package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.cUQuNAKU.js} +1 -1
  142. package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.BO7MnRIA.js} +1 -1
  143. package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.D9bElyD6.js} +1 -1
  144. package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.DhGdRz7I.js} +1 -1
  145. package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.5GvLeAoL.js} +1 -1
  146. package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.LtAT9haM.js} +1 -1
  147. package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.DRth1yLA.js} +1 -1
  148. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h9MlZfcr.js} +1 -1
  149. package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.DHqy9DYA.js} +1 -1
  150. package/web/index.html +2 -2
  151. package/config/proportional-route-table.ts +0 -155
  152. package/plugins/sp/commands/dev-parallel.md +0 -37
  153. package/plugins/sp/scripts/daily-summary/logger.ts +0 -28
  154. package/web/_astro/BoardApp.D6sjAJr_.js +0 -324
  155. package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
  156. package/web/_astro/channel.DZVfto-r.js +0 -1
  157. package/web/_astro/index.COh_kBxN.css +0 -1
  158. /package/plugins/sp/{hooks → lib}/destructive-policy.ts +0 -0
  159. /package/plugins/sp/{hooks → lib}/task-file-policy.ts +0 -0
@@ -21,16 +21,14 @@ import {
21
21
  mkdirSync,
22
22
  mkdtempSync,
23
23
  openSync,
24
- readdirSync,
25
24
  readFileSync,
26
- rmdirSync,
27
25
  rmSync,
28
26
  statSync,
29
27
  unlinkSync,
30
28
  writeFileSync
31
29
  } from "node:fs";
32
- import { homedir, tmpdir } from "node:os";
33
- import { dirname, join, resolve } from "node:path";
30
+ import { tmpdir } from "node:os";
31
+ import { join, resolve } from "node:path";
34
32
  import { createHash } from "node:crypto";
35
33
  var __defProp = Object.defineProperty;
36
34
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -82,19 +80,9 @@ var init_api_response = __esm(() => {
82
80
  CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
83
81
  });
84
82
  var init_cursor = () => {};
85
- function getEnvVar(name, fallback) {
86
- const raw = process.env[name];
87
- return raw === undefined ? fallback : raw;
88
- }
89
83
  function getEnvVars2() {
90
84
  return process.env;
91
85
  }
92
- function setEnvVar(name, value) {
93
- if (value === undefined)
94
- delete process.env[name];
95
- else
96
- process.env[name] = value;
97
- }
98
86
  var init_event_severity = () => {};
99
87
  var init_dist = __esm(() => {
100
88
  init_api_response();
@@ -24449,6 +24437,7 @@ function isRfc3339Timestamp(value) {
24449
24437
  var ALL_FINDING_CODES = [
24450
24438
  "L1.markdown-parse",
24451
24439
  "L1.schema-validation",
24440
+ "L1.wbs-collision",
24452
24441
  "L2.missing-required-section",
24453
24442
  "L2.forbidden-section",
24454
24443
  "L2.disallowed-section",
@@ -24476,6 +24465,7 @@ var ALL_FINDING_CODES = [
24476
24465
  "L4.design-placeholder",
24477
24466
  "L4.feature-not-found",
24478
24467
  "L4.feature-terminal",
24468
+ "L4.feature-deprecated",
24479
24469
  "L4.missing-feature-id",
24480
24470
  "L4.parent-not-found",
24481
24471
  "L4.dependency-not-found",
@@ -24913,7 +24903,7 @@ var POST_WATERMARK_TABLES = [
24913
24903
  "history_daily_stats",
24914
24904
  "history_board_source_daily"
24915
24905
  ];
24916
- var HISTORY_IMPORT_SCHEMA_VERSION = "0.5.21";
24906
+ var HISTORY_IMPORT_SCHEMA_VERSION = "0.5.24";
24917
24907
  var HISTORY_IMPORT_SCHEMA_SQL = `
24918
24908
  CREATE TABLE IF NOT EXISTS history_import_checkpoint (
24919
24909
  source TEXT NOT NULL,
@@ -27632,6 +27622,8 @@ var artifactsTable = defineTable("artifacts", {
27632
27622
  ...standardColumns
27633
27623
  });
27634
27624
  var artifacts = artifactsTable.table;
27625
+ var encoder = new TextEncoder;
27626
+ var decoder = new TextDecoder;
27635
27627
  init_schema();
27636
27628
  var phaseRunsTable = defineTable("phase_runs", {
27637
27629
  id: text("id").primaryKey(),
@@ -27867,6 +27859,9 @@ var INBOX_MESSAGES_REQUEST_KEY_SCHEMA_SQL = `
27867
27859
  ALTER TABLE inbox_messages ADD COLUMN request_key TEXT;
27868
27860
  CREATE UNIQUE INDEX IF NOT EXISTS idx_inbox_messages_request_key ON inbox_messages (request_key) WHERE request_key IS NOT NULL;
27869
27861
  `;
27862
+ var INBOX_MESSAGES_READ_AT_SCHEMA_SQL = `
27863
+ ALTER TABLE inbox_messages ADD COLUMN read_at INTEGER;
27864
+ `;
27870
27865
  var QUEUE_JOBS_SCHEMA_SQL = `
27871
27866
  CREATE TABLE IF NOT EXISTS queue_jobs (
27872
27867
  id TEXT PRIMARY KEY,
@@ -28879,6 +28874,11 @@ var CLI_MIGRATIONS = [
28879
28874
  {
28880
28875
  id: "0052_spur_cli_decision_logs",
28881
28876
  sql: DECISION_LOGS_SCHEMA_SQL
28877
+ },
28878
+ {
28879
+ id: "0053_spur_cli_inbox_messages_read_at",
28880
+ sql: INBOX_MESSAGES_READ_AT_SCHEMA_SQL,
28881
+ addColumnIfMissing: { table: "inbox_messages", column: "read_at" }
28882
28882
  }
28883
28883
  ];
28884
28884
  init_zod();
@@ -29824,7 +29824,15 @@ function escapeYamlValue(value) {
29824
29824
  }
29825
29825
  init_zod();
29826
29826
  var TASK_STATUSES = ["backlog", "todo", "wip", "testing", "blocked", "done", "cancelled"];
29827
- var FEATURE_STATUSES = ["backlog", "active", "verifying", "blocked", "done", "cancelled"];
29827
+ var FEATURE_STATUSES = [
29828
+ "backlog",
29829
+ "active",
29830
+ "verifying",
29831
+ "blocked",
29832
+ "done",
29833
+ "cancelled",
29834
+ "deprecated"
29835
+ ];
29828
29836
  var PRIORITIES = ["P0", "P1", "P2", "P3"];
29829
29837
  var PROFILES = [
29830
29838
  "simple",
@@ -30367,159 +30375,6 @@ function runShellCommand(cmd, cwd, extraEnv) {
30367
30375
  rmSync(dir, { recursive: true, force: true });
30368
30376
  }
30369
30377
  }
30370
- var GATE_LOCK_DIR_ENV = "SPUR_GATE_LOCK_DIR";
30371
- var GATE_LOCK_POLL_MS_ENV = "SPUR_GATE_LOCK_POLL_MS";
30372
- var GATE_LOCK_OFF_ENV = "SPUR_GATE_LOCK";
30373
- var GATE_LOCK_TOKEN_ENV = "SPUR_GATE_LOCK_TOKEN";
30374
- var GATE_LOCK_POLL_MS_DEFAULT = 5000;
30375
- var GATE_LOCK_MKDIR_HOLD_MS_ENV = "SPUR_GATE_LOCK_TEST_MKDIR_HOLD_MS";
30376
- var GATE_LOCK_LOST_RACE_SLEEP_MS = 20;
30377
- var GATE_LOCK_LOST_RACE_LOG_AT = 250;
30378
- function gateLockDir() {
30379
- const raw = getEnvVar(GATE_LOCK_DIR_ENV);
30380
- return raw !== undefined && raw.length > 0 ? raw : join(homedir(), ".config", "spur", "run", "full-gate.lock");
30381
- }
30382
- function gateLockPollMs() {
30383
- const raw = Number.parseInt(getEnvVar(GATE_LOCK_POLL_MS_ENV) ?? "", 10);
30384
- return Number.isFinite(raw) && raw > 0 ? raw : GATE_LOCK_POLL_MS_DEFAULT;
30385
- }
30386
- function isProcessAlive(pid) {
30387
- try {
30388
- process.kill(pid, 0);
30389
- return true;
30390
- } catch (error51) {
30391
- return error51.code !== "ESRCH";
30392
- }
30393
- }
30394
- function readGateLockClaim(dir) {
30395
- let markers;
30396
- try {
30397
- markers = readdirSync(dir).sort();
30398
- } catch {
30399
- return null;
30400
- }
30401
- for (const marker of markers) {
30402
- try {
30403
- const claim = JSON.parse(readFileSync(join(dir, marker), "utf8"));
30404
- if (typeof claim.pid === "number")
30405
- return { claim, marker };
30406
- } catch {
30407
- const pid = Number.parseInt(/^(\d+)-/.exec(marker)?.[1] ?? "", 10);
30408
- if (pid > 0) {
30409
- return { claim: { pid, startedAt: Date.now(), wbs: "?", runId: "?", cwd: "?" }, marker };
30410
- }
30411
- }
30412
- }
30413
- return null;
30414
- }
30415
- function acquireGateLock(env2, logLine = () => {}) {
30416
- if ((getEnvVar(GATE_LOCK_OFF_ENV) ?? "") === "off") {
30417
- return { release() {}, queueWaitMs: 0, token: "" };
30418
- }
30419
- const dir = gateLockDir();
30420
- mkdirSync(dirname(dir), { recursive: true });
30421
- const pollMs = gateLockPollMs();
30422
- const claim = {
30423
- pid: process.pid,
30424
- startedAt: Date.now(),
30425
- wbs: env2.wbs,
30426
- runId: receiptRunId(env2),
30427
- cwd: process.cwd()
30428
- };
30429
- const marker = `${process.pid}-${randomUUID()}`;
30430
- const startedWaitingAt = Date.now();
30431
- let holderLogged = false;
30432
- let graceLogged = false;
30433
- let lostRaces = 0;
30434
- let lostRaceLogged = false;
30435
- const mkdirHoldMs = Number.parseInt(getEnvVar(GATE_LOCK_MKDIR_HOLD_MS_ENV) ?? "", 10);
30436
- for (;; ) {
30437
- let dirIno = null;
30438
- try {
30439
- mkdirSync(dir);
30440
- dirIno = statSync(dir).ino;
30441
- if (Number.isFinite(mkdirHoldMs) && mkdirHoldMs > 0)
30442
- gateSleep(mkdirHoldMs);
30443
- writeFileSync(join(dir, marker), JSON.stringify(claim), { flag: "wx" });
30444
- } catch (error51) {
30445
- const code = error51.code;
30446
- if (code === "ENOENT" || code === "EINVAL") {
30447
- lostRaces += 1;
30448
- if (lostRaces === GATE_LOCK_LOST_RACE_LOG_AT && !lostRaceLogged) {
30449
- lostRaceLogged = true;
30450
- logLine(`quality gate lock: still losing the claim race after ${lostRaces} attempts (dir ${dir})
30451
- `);
30452
- }
30453
- gateSleep(GATE_LOCK_LOST_RACE_SLEEP_MS);
30454
- continue;
30455
- }
30456
- if (code !== "EEXIST") {
30457
- throw error51;
30458
- }
30459
- }
30460
- if (dirIno !== null) {
30461
- let currentIno = null;
30462
- let entries = [];
30463
- try {
30464
- currentIno = statSync(dir).ino;
30465
- entries = readdirSync(dir);
30466
- } catch {}
30467
- if (currentIno !== dirIno || entries.length !== 1 || entries[0] !== marker) {
30468
- try {
30469
- unlinkSync(join(dir, marker));
30470
- } catch {}
30471
- lostRaces += 1;
30472
- gateSleep(GATE_LOCK_LOST_RACE_SLEEP_MS);
30473
- continue;
30474
- }
30475
- return {
30476
- release() {
30477
- try {
30478
- rmSync(join(dir, marker), { force: true });
30479
- rmdirSync(dir);
30480
- } catch {}
30481
- },
30482
- queueWaitMs: Date.now() - startedWaitingAt,
30483
- token: marker
30484
- };
30485
- }
30486
- const held = readGateLockClaim(dir);
30487
- if (held !== null && isProcessAlive(held.claim.pid)) {
30488
- if (getEnvVar(GATE_LOCK_TOKEN_ENV) === held.marker) {
30489
- return { release() {}, queueWaitMs: Date.now() - startedWaitingAt, token: held.marker };
30490
- }
30491
- if (!holderLogged) {
30492
- holderLogged = true;
30493
- logLine(`quality gate lock: waiting for holder (wbs ${held.claim.wbs}, run ${held.claim.runId}, pid ${held.claim.pid}, cwd ${held.claim.cwd}, held ${Date.now() - held.claim.startedAt}ms)
30494
- `);
30495
- }
30496
- } else if (held !== null) {
30497
- logLine(`quality gate lock: reclaimed stale claim (pid ${held.claim.pid})
30498
- `);
30499
- try {
30500
- unlinkSync(join(dir, held.marker));
30501
- rmdirSync(dir);
30502
- } catch {}
30503
- } else {
30504
- try {
30505
- rmdirSync(dir);
30506
- graceLogged = false;
30507
- } catch {
30508
- gateSleep(pollMs);
30509
- try {
30510
- rmdirSync(dir);
30511
- graceLogged = false;
30512
- } catch {
30513
- if (!graceLogged) {
30514
- graceLogged = true;
30515
- logLine(`quality gate lock: dir has no readable claim and is not empty — waiting
30516
- `);
30517
- }
30518
- }
30519
- }
30520
- }
30521
- }
30522
- }
30523
30378
  function recomputeGateProofFingerprint(cwd, env2) {
30524
30379
  const taskSpecPath = env2.taskSpecPath ?? env2.taskFile;
30525
30380
  if (!taskSpecPath || taskSpecPath.trim() === "") {
@@ -30643,44 +30498,27 @@ function runQualityGate(mode, env2, options = {}) {
30643
30498
  appendFileSync(abs(logFile), probe.output);
30644
30499
  rmSync(abs(`${logFile}.probe`), { force: true });
30645
30500
  }
30646
- let queueWaitMs = 0;
30647
30501
  let gateRuntimeMs = 0;
30648
30502
  if (gateRc === 0) {
30649
30503
  const delayMs = retryDelayMs(env2);
30650
- const lockLine = (line) => {
30504
+ const gateRuntimeStartMs = Date.now();
30505
+ for (gateAttempt = 1;gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
30506
+ const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
30507
+ const attempt = runShellCommand(env2.qualityGateCmd ?? "", cwd);
30508
+ writeFileSync(abs(attemptLogPath), attempt.output);
30509
+ const locked = isTransientLock(attempt.output);
30510
+ appendFileSync(abs(logFile), attempt.output);
30511
+ rmSync(abs(attemptLogPath), { force: true });
30512
+ gateRc = attempt.code;
30513
+ if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS)
30514
+ break;
30515
+ const line = retryMessage(gateAttempt);
30651
30516
  process.stdout.write(line);
30652
30517
  appendFileSync(abs(logFile), line);
30653
- };
30654
- const lock = acquireGateLock(env2, lockLine);
30655
- queueWaitMs = lock.queueWaitMs;
30656
- lockLine(`quality gate lock: queueWaitMs=${queueWaitMs}
30657
- `);
30658
- const gateRuntimeStartMs = Date.now();
30659
- const previousToken = getEnvVar(GATE_LOCK_TOKEN_ENV);
30660
- if (lock.token.length > 0)
30661
- setEnvVar(GATE_LOCK_TOKEN_ENV, lock.token);
30662
- try {
30663
- for (gateAttempt = 1;gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
30664
- const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
30665
- const attempt = runShellCommand(env2.qualityGateCmd ?? "", cwd, lock.token.length > 0 ? { [GATE_LOCK_TOKEN_ENV]: lock.token } : undefined);
30666
- writeFileSync(abs(attemptLogPath), attempt.output);
30667
- const locked = isTransientLock(attempt.output);
30668
- appendFileSync(abs(logFile), attempt.output);
30669
- rmSync(abs(attemptLogPath), { force: true });
30670
- gateRc = attempt.code;
30671
- if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS)
30672
- break;
30673
- const line = retryMessage(gateAttempt);
30674
- process.stdout.write(line);
30675
- appendFileSync(abs(logFile), line);
30676
- gateSleep(delayMs);
30677
- }
30678
- } finally {
30679
- setEnvVar(GATE_LOCK_TOKEN_ENV, previousToken);
30680
- lock.release();
30518
+ gateSleep(delayMs);
30681
30519
  }
30682
30520
  gateRuntimeMs = Date.now() - gateRuntimeStartMs;
30683
- appendFileSync(abs(logFile), `quality gate lock: queueWaitMs=${queueWaitMs} gateRuntimeMs=${gateRuntimeMs}
30521
+ appendFileSync(abs(logFile), `gate runtime: ${gateRuntimeMs}ms
30684
30522
  `);
30685
30523
  }
30686
30524
  if (gateRc !== 0) {
@@ -30733,7 +30571,6 @@ function runQualityGate(mode, env2, options = {}) {
30733
30571
  }
30734
30572
  ],
30735
30573
  completedAt: new Date().toISOString(),
30736
- queueWaitMs,
30737
30574
  gateRuntimeMs
30738
30575
  });
30739
30576
  writeFileSync(abs(receiptFile), `${JSON.stringify(receipt, null, 2)}
@@ -30811,7 +30648,6 @@ export {
30811
30648
  isTransientLock,
30812
30649
  extractFindings,
30813
30650
  buildReceipt,
30814
- acquireGateLock,
30815
30651
  RETRY_DELAY_MS_ENV,
30816
30652
  RETRY_DELAY_MS_DEFAULT,
30817
30653
  RECEIPT_SCHEMA_VERSION,
@@ -30819,12 +30655,6 @@ export {
30819
30655
  MAX_GATE_ATTEMPTS,
30820
30656
  MAX_FINDINGS,
30821
30657
  LOCKED_PATTERN,
30822
- GATE_LOCK_TOKEN_ENV,
30823
- GATE_LOCK_POLL_MS_ENV,
30824
- GATE_LOCK_POLL_MS_DEFAULT,
30825
- GATE_LOCK_OFF_ENV,
30826
- GATE_LOCK_MKDIR_HOLD_MS_ENV,
30827
- GATE_LOCK_DIR_ENV,
30828
30658
  FINDINGS_PATTERN,
30829
30659
  COVERAGE_ROW_PATTERN
30830
30660
  };
@@ -15,6 +15,32 @@ function getEnvVars() {
15
15
  // plugins/sp/lib/residual-scan.generated.mjs
16
16
  import { join } from "node:path";
17
17
  import { createHash } from "node:crypto";
18
+ var CLAIM_MARKERS = [
19
+ /@[a-z0-9._-]+\/[a-z0-9._-]+/i,
20
+ /\bnode_modules\//,
21
+ /\b\d+\.\d+\.\d+\b/,
22
+ /`[A-Za-z_$][\w.$]*\s*\([^`]*\)`/
23
+ ];
24
+ var CITATION_FORMS = [
25
+ /`[^`\s]+:\d+(?:-\d+)?`/,
26
+ /`[^`]+`\s+lines?\s+\d+/i,
27
+ /\b[\w./-]+\.[A-Za-z0-9]+:\d+(?:-\d+)?\b/,
28
+ /https?:\/\/\S+/i
29
+ ];
30
+ function hasClaimMarker(evidence) {
31
+ return CLAIM_MARKERS.some((re) => re.test(evidence));
32
+ }
33
+ function hasCitationForm(evidence) {
34
+ return CITATION_FORMS.some((re) => re.test(evidence));
35
+ }
36
+ var CONFIDENCE_LEVELS = ["HIGH", "MEDIUM", "LOW"];
37
+ function extractAnswerConfidence(text) {
38
+ const matches = [...text.matchAll(/^\s*Confidence:\s*(\S+)\s*$/gim)];
39
+ if (matches.length !== 1)
40
+ return;
41
+ const value = (matches[0]?.[1] ?? "").toUpperCase();
42
+ return CONFIDENCE_LEVELS.includes(value) ? value : undefined;
43
+ }
18
44
  var MARKER_PATTERN = /TODO|FIXME|XXX|HACK/;
19
45
  var PRIORITY_PATTERN = /^P[1-4]/;
20
46
  var RANGE_PRIORITY = /^P[1-4]\s*[\u2013\u2014-]\s*P?[1-4]/;
@@ -41,11 +67,15 @@ function locationOf(locationCell, finding) {
41
67
  const backtick = finding.match(/`([^`]+)`/)?.[1];
42
68
  return backtick === undefined ? "" : normalizeAnchor(backtick);
43
69
  }
44
- function parseReviewFindings(taskContent) {
70
+ function reviewBody(taskContent) {
45
71
  const section = taskContent.split(/^### Review\b/m)[1];
46
- if (section === undefined)
72
+ return section === undefined ? null : section.split(/^### /m)[0] ?? "";
73
+ }
74
+ function parseReviewFindings(taskContent) {
75
+ const section = reviewBody(taskContent);
76
+ if (section === null)
47
77
  return [];
48
- const body = section.split(/^### /m)[0] ?? "";
78
+ const body = section;
49
79
  const out = [];
50
80
  const lines = body.split(`
51
81
  `);
@@ -158,7 +188,8 @@ function scanResiduals(inputs) {
158
188
  "review-finding": true,
159
189
  "diff-marker": base !== null,
160
190
  "unchecked-box": true,
161
- "staging-residue": true
191
+ "staging-residue": true,
192
+ "review-contract": false
162
193
  },
163
194
  items,
164
195
  counts
@@ -179,8 +210,36 @@ function blockingReviewFindings(taskContent, deferrals) {
179
210
  }));
180
211
  return classify(items, [...tableDeferrals, ...deferrals]);
181
212
  }
213
+ function reviewContractViolations(taskContent) {
214
+ const violations = [];
215
+ const body = reviewBody(taskContent);
216
+ if (body !== null && extractAnswerConfidence(body.replace(/\*\*/g, "")) === undefined) {
217
+ violations.push({
218
+ location: "task-file:Review",
219
+ text: "Review report carries no single valid `**Confidence:** HIGH | MEDIUM | LOW` line — " + "state the level the report can defend (HIGH: every requirement row MET, every finding " + "dispositioned, evidence cited; MEDIUM/LOW: name the gap)."
220
+ });
221
+ }
222
+ for (const row of parseReviewFindings(taskContent)) {
223
+ if (hasClaimMarker(row.text) && !hasCitationForm(row.text)) {
224
+ violations.push({
225
+ location: row.location.length > 0 ? row.location : "task-file:Review",
226
+ text: `${row.priority} finding asserts an external API/library/version claim without a ` + "same-cell citation (add a repo `path:line`, an external named-origin form, or a URL " + `to the Finding cell): ${row.text.slice(0, 80)}`
227
+ });
228
+ }
229
+ }
230
+ return violations;
231
+ }
182
232
  function buildReviewGateArtifact(wbs, taskContent, deferrals) {
183
- const items = blockingReviewFindings(taskContent, deferrals);
233
+ const items = [
234
+ ...blockingReviewFindings(taskContent, deferrals),
235
+ ...reviewContractViolations(taskContent).map((v) => ({
236
+ id: makeItemId("review-contract", v.location, v.text),
237
+ category: "review-contract",
238
+ class: "blocking",
239
+ location: v.location,
240
+ text: v.text
241
+ }))
242
+ ];
184
243
  const blocking = items.filter((i) => i.class === "blocking");
185
244
  const counts = { blocking: 0, deferrable: 0, advisory: 0, housekeeping: 0 };
186
245
  for (const item of items)
@@ -192,7 +251,8 @@ function buildReviewGateArtifact(wbs, taskContent, deferrals) {
192
251
  "review-finding": true,
193
252
  "diff-marker": false,
194
253
  "unchecked-box": false,
195
- "staging-residue": false
254
+ "staging-residue": false,
255
+ "review-contract": true
196
256
  },
197
257
  items,
198
258
  counts
@@ -518,6 +578,12 @@ function settleMode(opts, env, io) {
518
578
  }
519
579
  function reportMode(opts, env, io) {
520
580
  const runDir = join2(opts.root, ".spur", "run");
581
+ const verdictPath = recordedVerdictPath(runDir, opts.wbs, fs);
582
+ if (!fs.existsSync(verdictPath)) {
583
+ io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}
584
+ `);
585
+ return 0;
586
+ }
521
587
  const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === "residual-sweep");
522
588
  if (sweep === undefined || sweep.status !== "fail")
523
589
  return 0;
@@ -558,6 +624,7 @@ process.exit(main(process.argv.slice(2)));
558
624
  export {
559
625
  verdictDisagreementNote,
560
626
  scanResiduals2 as scanResiduals,
627
+ reviewContractViolations,
561
628
  renderReport,
562
629
  recordedVerdictPath,
563
630
  parseScanArgs,
@@ -162,11 +162,8 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
162
162
  if (fs.existsSync(residualsPath)) prior = JSON.parse(fs.readFileSync(residualsPath, 'utf8'));
163
163
  const deferred = scan.items.filter((i) => i.class === 'deferrable');
164
164
  if (deferred.length > 0 && prior.followUp === undefined) {
165
- // The follow-up is filed UNLINKED: carrying a feature edge to the feature whose run produced
166
- // it made that feature's done-gate fail with `L4.verifying-incomplete-tasks` the moment a
167
- // residual was deferred (2026-10-04 session, E72/1086→1087), so a deferral must not be able
168
- // to block the completing feature. The source task and its feature stay named in the
169
- // Background, which keeps the traceability without the lifecycle coupling.
165
+ // Filed UNLINKED: a feature edge failed that feature's done-gate (`L4.verifying-incomplete-tasks`,
166
+ // E72/1086→1087); the Background still names the source task and feature for traceability.
170
167
  const args = ['task', 'create', `Residuals from ${wbs}`, '--skip-ready', '--json'];
171
168
  const created = spur(env, opts.spurBin, args, opts.root);
172
169
  let wbsNew = '';
@@ -206,6 +203,12 @@ function settleMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
206
203
  }
207
204
  function reportMode(opts: ParsedArgs, env: ScanEnv, io: ScanIo): number {
208
205
  const runDir = join(opts.root, '.spur', 'run');
206
+ // A fix-budget-exhausted run never reaches verify: no verdict is a no-op, not ENOENT (1178 run 1).
207
+ const verdictPath = core.recordedVerdictPath(runDir, opts.wbs, fs);
208
+ if (!fs.existsSync(verdictPath)) {
209
+ io.err(`residual-report: no verdict at ${verdictPath}; nothing to report for ${opts.wbs}\n`);
210
+ return 0;
211
+ }
209
212
  const sweep = loadVerdict(runDir, opts.wbs).checks.find((c) => c.name === 'residual-sweep');
210
213
  if (sweep === undefined || sweep.status !== 'fail') return 0;
211
214
  const { content } = loadTask(env, opts.spurBin, opts.wbs, opts.root);