@gobing-ai/spur 0.4.1 → 0.4.2

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 (104) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/config/config.global.yaml +4 -2
  3. package/config/templates/AGENTS.md +1 -1
  4. package/config/workflows/wrapup-pipeline.yaml +1 -1
  5. package/package.json +10 -10
  6. package/plugins/sp/README.md +1 -2
  7. package/plugins/sp/agents/super-planner.md +5 -6
  8. package/plugins/sp/commands/dev-review-session.md +2 -1
  9. package/plugins/sp/commands/dev-runall.md +18 -9
  10. package/plugins/sp/hooks/context-owner.ts +56 -0
  11. package/plugins/sp/hooks/context-post-tool.ts +23 -0
  12. package/plugins/sp/hooks/context-session-start.ts +49 -27
  13. package/plugins/sp/hooks/context-session-stop.ts +131 -30
  14. package/plugins/sp/hooks/pi/guard-extension.ts +21 -8
  15. package/plugins/sp/lib/idea-handoff.generated.mjs +4 -2
  16. package/plugins/sp/lib/inline-run.generated.mjs +47 -30
  17. package/plugins/sp/lib/plan-projection.generated.d.mts +19 -0
  18. package/plugins/sp/lib/plan-projection.generated.mjs +113 -0
  19. package/plugins/sp/lib/quality-gate.generated.d.mts +0 -12
  20. package/plugins/sp/lib/quality-gate.generated.mjs +48 -230
  21. package/plugins/sp/lib/transcript.ts +49 -21
  22. package/plugins/sp/plugin.json +1 -1
  23. package/plugins/sp/references/roles.md +1 -1
  24. package/plugins/sp/scripts/batch-plan.mjs +154 -1
  25. package/plugins/sp/scripts/batch-plan.ts +48 -3
  26. package/plugins/sp/scripts/inline-run-setup.mjs +24 -13
  27. package/plugins/sp/scripts/pr-reviewing.mjs +19 -5
  28. package/plugins/sp/scripts/pr-reviewing.ts +34 -9
  29. package/plugins/sp/scripts/quality-gate.mjs +25 -207
  30. package/plugins/sp/scripts/run-summary.mjs +24 -13
  31. package/plugins/sp/scripts/session-timeline.mjs +26 -25
  32. package/plugins/sp/scripts/session-timeline.ts +4 -18
  33. package/plugins/sp/scripts/task-diffstat.mjs +32 -29
  34. package/plugins/sp/scripts/task-diffstat.ts +34 -34
  35. package/plugins/sp/skills/indexed-context/SKILL.md +14 -7
  36. package/plugins/sp/skills/parallel-execution/SKILL.md +2 -2
  37. package/plugins/sp/skills/parallel-execution/references/dispatch-surface.md +1 -1
  38. package/plugins/sp/skills/parallel-execution/references/fan-out-patterns.md +2 -2
  39. package/plugins/sp/skills/pr-reviewing/SKILL.md +7 -3
  40. package/plugins/sp/skills/session-review/SKILL.md +5 -3
  41. package/plugins/sp/skills/spur-cli/references/decision.md +9 -2
  42. package/plugins/sp/skills/spur-cli/references/projects.md +7 -5
  43. package/plugins/sp/skills/spur-dev/SKILL.md +2 -0
  44. package/plugins/sp/skills/spur-dev/references/ac-style-guide.md +27 -0
  45. package/plugins/sp/skills/spur-dev/references/dev-operations.md +2 -3
  46. package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +5 -0
  47. package/plugins/sp/skills/spur-dev/references/execution-batch.md +43 -4
  48. package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +4 -1
  49. package/plugins/sp/skills/spur-dev/references/flag-glossary.md +13 -11
  50. package/plugins/sp/skills/spur-dev/references/inline-pipeline-driver.md +8 -13
  51. package/spur.js +14261 -2846
  52. package/web/_astro/{BoardApp.D66k_s_T.js → BoardApp.B3GQZvj8.js} +1 -1
  53. package/web/_astro/{BoardApp.D6sjAJr_.js → BoardApp.BT7L2kSb.js} +192 -192
  54. package/web/_astro/TaskDetail.Cxov1_N_.js +1 -0
  55. package/web/_astro/{arc._BUadrdH.js → arc.vaT7tPOB.js} +1 -1
  56. package/web/_astro/{architectureDiagram-3BPJPVTR.Dh7xS1Pp.js → architectureDiagram-3BPJPVTR.DRMmTjkN.js} +1 -1
  57. package/web/_astro/{blockDiagram-GPEHLZMM.DFDVpfXm.js → blockDiagram-GPEHLZMM.cla0LO_q.js} +1 -1
  58. package/web/_astro/{c4Diagram-AAUBKEIU.V7_3_glh.js → c4Diagram-AAUBKEIU.D4YAIBKn.js} +1 -1
  59. package/web/_astro/channel.D4nG9YLf.js +1 -0
  60. package/web/_astro/{chunk-2J33WTMH.De1yx7Jz.js → chunk-2J33WTMH.BrSJbMRZ.js} +1 -1
  61. package/web/_astro/{chunk-4BX2VUAB.BDhHFuxx.js → chunk-4BX2VUAB.CuLMgKF5.js} +1 -1
  62. package/web/_astro/{chunk-55IACEB6.DDLkWnmC.js → chunk-55IACEB6.zkvgu54F.js} +1 -1
  63. package/web/_astro/{chunk-727SXJPM.CwfiLnA7.js → chunk-727SXJPM.DxWkACh8.js} +1 -1
  64. package/web/_astro/{chunk-AQP2D5EJ.CtGDmUnM.js → chunk-AQP2D5EJ.42XrmIlH.js} +1 -1
  65. package/web/_astro/{chunk-FMBD7UC4.BOLjpw73.js → chunk-FMBD7UC4.Bao2wMK9.js} +1 -1
  66. package/web/_astro/{chunk-ND2GUHAM.BDhQls6O.js → chunk-ND2GUHAM.BhydojD1.js} +1 -1
  67. package/web/_astro/{chunk-QZHKN3VN.76qBynDF.js → chunk-QZHKN3VN.CmZ82g-H.js} +1 -1
  68. package/web/_astro/{classDiagram-4FO5ZUOK.N0mpPHT0.js → classDiagram-4FO5ZUOK.Blr83Gt_.js} +1 -1
  69. package/web/_astro/{classDiagram-v2-Q7XG4LA2.N0mpPHT0.js → classDiagram-v2-Q7XG4LA2.Blr83Gt_.js} +1 -1
  70. package/web/_astro/{cose-bilkent-S5V4N54A.CKknFfjB.js → cose-bilkent-S5V4N54A.qmfv0SPn.js} +1 -1
  71. package/web/_astro/{cynefin-OW5HDTMX.Cw3ZWBAh.js → cynefin-OW5HDTMX.BtELhXPR.js} +1 -1
  72. package/web/_astro/{dagre-BM42HDAG.D3yIKCRD.js → dagre-BM42HDAG.AWmaAvXo.js} +1 -1
  73. package/web/_astro/{diagram-2AECGRRQ.D51dO83Z.js → diagram-2AECGRRQ.DdH34KDi.js} +1 -1
  74. package/web/_astro/{diagram-5GNKFQAL.Bb-FViDj.js → diagram-5GNKFQAL.BmNPix1k.js} +1 -1
  75. package/web/_astro/{diagram-KO2AKTUF.D3mN8S6x.js → diagram-KO2AKTUF.BNQd_Ppo.js} +1 -1
  76. package/web/_astro/{diagram-LMA3HP47.dMMyPcbJ.js → diagram-LMA3HP47.BAPgDi0e.js} +1 -1
  77. package/web/_astro/{diagram-OG6HWLK6.AzbvzY4C.js → diagram-OG6HWLK6.DZ4zyLdC.js} +1 -1
  78. package/web/_astro/{erDiagram-TEJ5UH35.BR5I4OZb.js → erDiagram-TEJ5UH35.Q1TySMmw.js} +1 -1
  79. package/web/_astro/{flowDiagram-I6XJVG4X.DFqvhGkn.js → flowDiagram-I6XJVG4X.BXhFBicG.js} +1 -1
  80. package/web/_astro/{ganttDiagram-6RSMTGT7.ByJbhH6J.js → ganttDiagram-6RSMTGT7.vHSgcuo2.js} +1 -1
  81. package/web/_astro/{gitGraphDiagram-PVQCEYII.CInHjQR3.js → gitGraphDiagram-PVQCEYII.Bi1f73C7.js} +1 -1
  82. package/web/_astro/{index.COh_kBxN.css → index.BhSwN8yY.css} +1 -1
  83. package/web/_astro/{infoDiagram-5YYISTIA.Cm0dGVjJ.js → infoDiagram-5YYISTIA.DPvXZEWm.js} +1 -1
  84. package/web/_astro/{ishikawaDiagram-YF4QCWOH.C3nOP6Sj.js → ishikawaDiagram-YF4QCWOH.Cz829fsV.js} +1 -1
  85. package/web/_astro/{journeyDiagram-JHISSGLW.4IvLa2MW.js → journeyDiagram-JHISSGLW.enRnB9Me.js} +1 -1
  86. package/web/_astro/{kanban-definition-UN3LZRKU.DS87Cl-k.js → kanban-definition-UN3LZRKU.DnTNk6d2.js} +1 -1
  87. package/web/_astro/{linear.MzmRrNER.js → linear.BzGnBwNL.js} +1 -1
  88. package/web/_astro/{mermaid.core.fK-WNswS.js → mermaid.core.CSbTpIVz.js} +4 -4
  89. package/web/_astro/{mindmap-definition-RKZ34NQL.BqipOHhY.js → mindmap-definition-RKZ34NQL.DGJDy8zT.js} +1 -1
  90. package/web/_astro/{pieDiagram-4H26LBE5.0nBiPqFG.js → pieDiagram-4H26LBE5.HSPdi5_8.js} +1 -1
  91. package/web/_astro/{quadrantDiagram-W4KKPZXB.Szj7Zpx5.js → quadrantDiagram-W4KKPZXB.6Gv8mz2Y.js} +1 -1
  92. package/web/_astro/{requirementDiagram-4Y6WPE33.D4UR6TWo.js → requirementDiagram-4Y6WPE33.CnBCtFwa.js} +1 -1
  93. package/web/_astro/{sankeyDiagram-5OEKKPKP.DAE7BdOs.js → sankeyDiagram-5OEKKPKP.5M8VWAu8.js} +1 -1
  94. package/web/_astro/{sequenceDiagram-3UESZ5HK.BP9ZVq25.js → sequenceDiagram-3UESZ5HK.DN41XN-U.js} +1 -1
  95. package/web/_astro/{stateDiagram-AJRCARHV.BQW7mIxk.js → stateDiagram-AJRCARHV.ErOO3W8j.js} +1 -1
  96. package/web/_astro/{stateDiagram-v2-BHNVJYJU.PbK4nDih.js → stateDiagram-v2-BHNVJYJU.B_ZHICM-.js} +1 -1
  97. package/web/_astro/{timeline-definition-PNZ67QCA.Ci_GvUpo.js → timeline-definition-PNZ67QCA.CWdosr07.js} +1 -1
  98. package/web/_astro/{vennDiagram-CIIHVFJN.DNw1Y5qx.js → vennDiagram-CIIHVFJN.BHzYjNV5.js} +1 -1
  99. package/web/_astro/{wardleyDiagram-YWT4CUSO.CwlkaaNe.js → wardleyDiagram-YWT4CUSO.h8x4IwPX.js} +1 -1
  100. package/web/_astro/{xychartDiagram-2RQKCTM6.MI6ZuiH_.js → xychartDiagram-2RQKCTM6.RTapW2X8.js} +1 -1
  101. package/web/index.html +2 -2
  102. package/plugins/sp/commands/dev-parallel.md +0 -37
  103. package/web/_astro/TaskDetail.BynLnMNc.js +0 -1
  104. package/web/_astro/channel.DZVfto-r.js +0 -1
@@ -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();
@@ -24913,7 +24901,7 @@ var POST_WATERMARK_TABLES = [
24913
24901
  "history_daily_stats",
24914
24902
  "history_board_source_daily"
24915
24903
  ];
24916
- var HISTORY_IMPORT_SCHEMA_VERSION = "0.5.21";
24904
+ var HISTORY_IMPORT_SCHEMA_VERSION = "0.5.22";
24917
24905
  var HISTORY_IMPORT_SCHEMA_SQL = `
24918
24906
  CREATE TABLE IF NOT EXISTS history_import_checkpoint (
24919
24907
  source TEXT NOT NULL,
@@ -27867,6 +27855,9 @@ var INBOX_MESSAGES_REQUEST_KEY_SCHEMA_SQL = `
27867
27855
  ALTER TABLE inbox_messages ADD COLUMN request_key TEXT;
27868
27856
  CREATE UNIQUE INDEX IF NOT EXISTS idx_inbox_messages_request_key ON inbox_messages (request_key) WHERE request_key IS NOT NULL;
27869
27857
  `;
27858
+ var INBOX_MESSAGES_READ_AT_SCHEMA_SQL = `
27859
+ ALTER TABLE inbox_messages ADD COLUMN read_at INTEGER;
27860
+ `;
27870
27861
  var QUEUE_JOBS_SCHEMA_SQL = `
27871
27862
  CREATE TABLE IF NOT EXISTS queue_jobs (
27872
27863
  id TEXT PRIMARY KEY,
@@ -28879,6 +28870,11 @@ var CLI_MIGRATIONS = [
28879
28870
  {
28880
28871
  id: "0052_spur_cli_decision_logs",
28881
28872
  sql: DECISION_LOGS_SCHEMA_SQL
28873
+ },
28874
+ {
28875
+ id: "0053_spur_cli_inbox_messages_read_at",
28876
+ sql: INBOX_MESSAGES_READ_AT_SCHEMA_SQL,
28877
+ addColumnIfMissing: { table: "inbox_messages", column: "read_at" }
28882
28878
  }
28883
28879
  ];
28884
28880
  init_zod();
@@ -30367,159 +30363,6 @@ function runShellCommand(cmd, cwd, extraEnv) {
30367
30363
  rmSync(dir, { recursive: true, force: true });
30368
30364
  }
30369
30365
  }
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
30366
  function recomputeGateProofFingerprint(cwd, env2) {
30524
30367
  const taskSpecPath = env2.taskSpecPath ?? env2.taskFile;
30525
30368
  if (!taskSpecPath || taskSpecPath.trim() === "") {
@@ -30643,44 +30486,27 @@ function runQualityGate(mode, env2, options = {}) {
30643
30486
  appendFileSync(abs(logFile), probe.output);
30644
30487
  rmSync(abs(`${logFile}.probe`), { force: true });
30645
30488
  }
30646
- let queueWaitMs = 0;
30647
30489
  let gateRuntimeMs = 0;
30648
30490
  if (gateRc === 0) {
30649
30491
  const delayMs = retryDelayMs(env2);
30650
- const lockLine = (line) => {
30492
+ const gateRuntimeStartMs = Date.now();
30493
+ for (gateAttempt = 1;gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
30494
+ const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
30495
+ const attempt = runShellCommand(env2.qualityGateCmd ?? "", cwd);
30496
+ writeFileSync(abs(attemptLogPath), attempt.output);
30497
+ const locked = isTransientLock(attempt.output);
30498
+ appendFileSync(abs(logFile), attempt.output);
30499
+ rmSync(abs(attemptLogPath), { force: true });
30500
+ gateRc = attempt.code;
30501
+ if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS)
30502
+ break;
30503
+ const line = retryMessage(gateAttempt);
30651
30504
  process.stdout.write(line);
30652
30505
  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();
30506
+ gateSleep(delayMs);
30681
30507
  }
30682
30508
  gateRuntimeMs = Date.now() - gateRuntimeStartMs;
30683
- appendFileSync(abs(logFile), `quality gate lock: queueWaitMs=${queueWaitMs} gateRuntimeMs=${gateRuntimeMs}
30509
+ appendFileSync(abs(logFile), `gate runtime: ${gateRuntimeMs}ms
30684
30510
  `);
30685
30511
  }
30686
30512
  if (gateRc !== 0) {
@@ -30733,7 +30559,6 @@ function runQualityGate(mode, env2, options = {}) {
30733
30559
  }
30734
30560
  ],
30735
30561
  completedAt: new Date().toISOString(),
30736
- queueWaitMs,
30737
30562
  gateRuntimeMs
30738
30563
  });
30739
30564
  writeFileSync(abs(receiptFile), `${JSON.stringify(receipt, null, 2)}
@@ -30811,7 +30636,6 @@ export {
30811
30636
  isTransientLock,
30812
30637
  extractFindings,
30813
30638
  buildReceipt,
30814
- acquireGateLock,
30815
30639
  RETRY_DELAY_MS_ENV,
30816
30640
  RETRY_DELAY_MS_DEFAULT,
30817
30641
  RECEIPT_SCHEMA_VERSION,
@@ -30819,12 +30643,6 @@ export {
30819
30643
  MAX_GATE_ATTEMPTS,
30820
30644
  MAX_FINDINGS,
30821
30645
  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
30646
  FINDINGS_PATTERN,
30829
30647
  COVERAGE_ROW_PATTERN
30830
30648
  };
@@ -40,17 +40,19 @@ function formatTokenSplit(t) {
40
40
  return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
41
41
  }
42
42
  function promptText(row) {
43
+ if (row.type === "custom_message") {
44
+ if (row.display === false)
45
+ return;
46
+ const text = typeof row.content === "string" ? row.content.trim() : "";
47
+ return text === "" ? undefined : text;
48
+ }
43
49
  if (row.type === "message") {
44
50
  if (row.message?.role !== "user")
45
51
  return;
46
- if (isInjectedPrompt(row))
47
- return;
48
- const content2 = row.message.content;
49
- if (typeof content2 === "string")
50
- return content2;
51
- if (!Array.isArray(content2))
52
+ const text = firstTextContent(row);
53
+ if (text === undefined)
52
54
  return;
53
- return content2.find((b) => b.type === "text")?.text;
55
+ return skillInvocationPrompt(text) ?? text;
54
56
  }
55
57
  if (row.type !== "user" || row.isMeta || row.isCompactSummary)
56
58
  return;
@@ -61,12 +63,21 @@ function promptText(row) {
61
63
  return;
62
64
  return content.find((b) => b.type === "text")?.text;
63
65
  }
64
- function isInjectedPrompt(row) {
65
- if (row.type !== "message" || row.message?.role !== "user")
66
- return false;
67
- const content = row.message.content;
68
- const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((b) => b.type === "text")?.text : undefined;
69
- return typeof text === "string" && text.replace(/^\s+/, "").startsWith('<skill name="');
66
+ function firstTextContent(row) {
67
+ const content = row.message?.content;
68
+ if (typeof content === "string")
69
+ return content;
70
+ if (!Array.isArray(content))
71
+ return;
72
+ return content.find((b) => b.type === "text")?.text;
73
+ }
74
+ function skillInvocationPrompt(text) {
75
+ const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
76
+ if (name === undefined)
77
+ return;
78
+ const close = text.lastIndexOf("</skill>");
79
+ const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
80
+ return args === "" ? `/${name}` : `/${name} ${args}`;
70
81
  }
71
82
  function sumTokens(all) {
72
83
  const total = zeroTokens();
@@ -36,17 +36,19 @@ function formatTokenSplit(t) {
36
36
  return `${formatTokens(nonCached + t.cacheRead)} / ${formatTokens(nonCached)}`;
37
37
  }
38
38
  function promptText(row) {
39
+ if (row.type === "custom_message") {
40
+ if (row.display === false)
41
+ return;
42
+ const text = typeof row.content === "string" ? row.content.trim() : "";
43
+ return text === "" ? undefined : text;
44
+ }
39
45
  if (row.type === "message") {
40
46
  if (row.message?.role !== "user")
41
47
  return;
42
- if (isInjectedPrompt(row))
43
- return;
44
- const content2 = row.message.content;
45
- if (typeof content2 === "string")
46
- return content2;
47
- if (!Array.isArray(content2))
48
+ const text = firstTextContent(row);
49
+ if (text === undefined)
48
50
  return;
49
- return content2.find((b) => b.type === "text")?.text;
51
+ return skillInvocationPrompt(text) ?? text;
50
52
  }
51
53
  if (row.type !== "user" || row.isMeta || row.isCompactSummary)
52
54
  return;
@@ -57,12 +59,21 @@ function promptText(row) {
57
59
  return;
58
60
  return content.find((b) => b.type === "text")?.text;
59
61
  }
60
- function isInjectedPrompt(row) {
61
- if (row.type !== "message" || row.message?.role !== "user")
62
- return false;
63
- const content = row.message.content;
64
- const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((b) => b.type === "text")?.text : undefined;
65
- return typeof text === "string" && text.replace(/^\s+/, "").startsWith('<skill name="');
62
+ function firstTextContent(row) {
63
+ const content = row.message?.content;
64
+ if (typeof content === "string")
65
+ return content;
66
+ if (!Array.isArray(content))
67
+ return;
68
+ return content.find((b) => b.type === "text")?.text;
69
+ }
70
+ function skillInvocationPrompt(text) {
71
+ const name = /^\s*<skill name="([^"]+)"/.exec(text)?.[1];
72
+ if (name === undefined)
73
+ return;
74
+ const close = text.lastIndexOf("</skill>");
75
+ const args = close < 0 ? "" : text.slice(close + "</skill>".length).trim();
76
+ return args === "" ? `/${name}` : `/${name} ${args}`;
66
77
  }
67
78
  function sumTokens(all) {
68
79
  const total = zeroTokens();
@@ -212,15 +223,7 @@ function buildTimeline(lines, group) {
212
223
  const open = [];
213
224
  const { rows, skippedLines, format } = parseRows(lines);
214
225
  const isPi = format === "pi";
215
- let injectedPrompts = 0;
216
226
  for (const [row, ts] of rows) {
217
- if (isPi && isInjectedPrompt(row)) {
218
- injectedPrompts++;
219
- const injectedSeg = open.at(-1);
220
- if (injectedSeg)
221
- accumulate(injectedSeg, row, ts);
222
- continue;
223
- }
224
227
  const prompt = promptText(row);
225
228
  if (prompt !== undefined) {
226
229
  open.push({ start: ts, prompt, ...newAcc(ts) });
@@ -253,8 +256,7 @@ function buildTimeline(lines, group) {
253
256
  available: true,
254
257
  segments,
255
258
  totals: render(isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals),
256
- skippedLines,
257
- ...isPi ? { injectedPrompts } : {}
259
+ skippedLines
258
260
  };
259
261
  if (group) {
260
262
  timeline.stages = parseGroups(group, segments.length).map(([a, b]) => render({ ...sumSpans(segments.slice(a - 1, b)), segments: a === b ? `${a}` : `${a}-${b}` }));
@@ -273,8 +275,7 @@ function zeroSegmentReason(rows) {
273
275
  const census = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 5).map(([kind, n]) => `${kind} ${n}`).join(", ");
274
276
  return `unrecognized transcript format (row types: ${census})`;
275
277
  }
276
- const injected = rows.filter(([r]) => isInjectedPrompt(r)).length;
277
- return `${format} transcript with no operator prompts (${rows.length} rows, ${injected} injected)`;
278
+ return `${format} transcript with no operator prompts (${rows.length} rows)`;
278
279
  }
279
280
  function main(argv, env = getEnvVars(), write = (s) => process.stdout.write(s), projectsRoot, writeErr = (s) => process.stderr.write(s)) {
280
281
  let transcript;
@@ -16,7 +16,6 @@ import {
16
16
  accumulate,
17
17
  formatDuration,
18
18
  formatTokenSplit,
19
- isInjectedPrompt,
20
19
  newAcc,
21
20
  parseRows,
22
21
  promptText,
@@ -40,8 +39,6 @@ export interface Timeline {
40
39
  stages?: Rendered<Span & { segments: string }>[];
41
40
  totals: Rendered<Span & { elapsedMs: number; elapsed: string; compactions?: number }>;
42
41
  skippedLines: number;
43
- /** pi only (1138 R2): skill-injected user bodies counted as activity, never as segments. */
44
- injectedPrompts?: number;
45
42
  }
46
43
 
47
44
  const PROMPT_EXCERPT = 80;
@@ -75,17 +72,8 @@ export function buildTimeline(lines: string[], group?: string): Timeline {
75
72
  const open: Open[] = [];
76
73
  const { rows, skippedLines, format } = parseRows(lines);
77
74
  const isPi = format === 'pi';
78
- let injectedPrompts = 0;
79
75
 
80
76
  for (const [row, ts] of rows) {
81
- // 1138 R2: an injected skill body is activity, not a prompt — accumulate it into the open
82
- // segment (its tools and usage are real work) and count it, never open a segment.
83
- if (isPi && isInjectedPrompt(row)) {
84
- injectedPrompts++;
85
- const injectedSeg = open.at(-1);
86
- if (injectedSeg) accumulate(injectedSeg, row, ts);
87
- continue;
88
- }
89
77
  const prompt = promptText(row);
90
78
  if (prompt !== undefined) {
91
79
  open.push({ start: ts, prompt, ...newAcc(ts) });
@@ -124,8 +112,6 @@ export function buildTimeline(lines: string[], group?: string): Timeline {
124
112
  isPi ? { ...totals, compactions: segments.reduce((n, s) => n + (s.compactions ?? 0), 0) } : totals,
125
113
  ),
126
114
  skippedLines,
127
- // Emitted only for pi, as `compactions` is, so Claude output is unchanged (1138 R2).
128
- ...(isPi ? { injectedPrompts } : {}),
129
115
  };
130
116
  if (group) {
131
117
  timeline.stages = parseGroups(group, segments.length).map(([a, b]) =>
@@ -139,8 +125,9 @@ export const SESSION_TIMELINE_USAGE = 'usage: session-timeline [--transcript <pa
139
125
 
140
126
  /**
141
127
  * 1138 R3: a zero-segment result must say WHAT was detected, so an unparseable-but-recognised
142
- * transcript can never read as an authoritative all-zero timeline. A known format names the
143
- * row count and the injected count; an unknown one lists the top-5 row-type census.
128
+ * transcript can never read as an authoritative all-zero timeline. A known format names the row
129
+ * count; an unknown one lists the top-5 row-type census. (1151 R2 removed the pi-only injected
130
+ * count: pi injects no skill bodies, so a wrapper row is an operator prompt and the class is gone.)
144
131
  */
145
132
  export function zeroSegmentReason(rows: [Row, number][]): string {
146
133
  const format = sniffFormat(rows.map(([r]) => r));
@@ -157,8 +144,7 @@ export function zeroSegmentReason(rows: [Row, number][]): string {
157
144
  .join(', ');
158
145
  return `unrecognized transcript format (row types: ${census})`;
159
146
  }
160
- const injected = rows.filter(([r]) => isInjectedPrompt(r)).length;
161
- return `${format} transcript with no operator prompts (${rows.length} rows, ${injected} injected)`;
147
+ return `${format} transcript with no operator prompts (${rows.length} rows)`;
162
148
  }
163
149
 
164
150
  export function main(
@@ -16,7 +16,7 @@ var SENSITIVE_PREFIXES = ["drizzle/", "packages/config/", ".github/", "plugins/s
16
16
  var SENSITIVE_GLOBS = ["apps/server/src/**/auth*", "**/*secret*", "**/*.sql"];
17
17
  function globToRegex(glob) {
18
18
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("**/", "\x00").replaceAll("**", "\x01").replaceAll("*", "[^/]*").replaceAll("\x00", "(?:.*/)?").replaceAll("\x01", ".*");
19
- return new RegExp(`^${escaped}$`);
19
+ return new RegExp(`^${escaped}$`, "s");
20
20
  }
21
21
  var SENSITIVE_MATCHERS = [
22
22
  ...SENSITIVE_PREFIXES.map((prefix) => ({
@@ -52,22 +52,12 @@ function countNewlines(path, cwd) {
52
52
  lines += 1;
53
53
  return lines;
54
54
  }
55
- function expandDiffPath(raw) {
56
- const arrow = raw.indexOf(" => ");
57
- if (arrow === -1)
58
- return [raw];
59
- const open = raw.indexOf("{");
60
- const close = raw.lastIndexOf("}");
61
- if (open !== -1 && close > open && open < arrow && arrow < close) {
62
- const inner = raw.slice(open + 1, close);
63
- const innerArrow = inner.indexOf(" => ");
64
- if (innerArrow !== -1) {
65
- const head = raw.slice(0, open);
66
- const tail = raw.slice(close + 1);
67
- return [head + inner.slice(0, innerArrow) + tail, head + inner.slice(innerArrow + 4) + tail];
68
- }
69
- }
70
- return [raw.slice(0, arrow), raw.slice(arrow + 4)];
55
+ function addCounts(add, del) {
56
+ for (const count of [add, del])
57
+ if (count !== "-" && !/^\d+$/.test(count))
58
+ return null;
59
+ const [a, d] = [add === "-" ? 0 : Number(add), del === "-" ? 0 : Number(del)];
60
+ return Number.isSafeInteger(a) && Number.isSafeInteger(d) ? [a, d] : null;
71
61
  }
72
62
  function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
73
63
  const cwd = options.cwd;
@@ -99,23 +89,37 @@ function runDiffstat(env, options = {}, git = defaultGitRunner(options.cwd)) {
99
89
  } catch {}
100
90
  if (!/^[0-9a-f]{7,40}$/i.test(base))
101
91
  return failSafe("run base .spur/run/<wbs>-base.sha missing or malformed");
102
- const numstat = git(["diff", "--numstat", base]);
92
+ const numstat = git(["diff", "--numstat", "-z", base]);
103
93
  if (numstat.status !== 0)
104
94
  return failSafe(`git diff --numstat failed (status=${numstat.status})`);
95
+ if (numstat.stdout.length > 0 && !numstat.stdout.endsWith("\x00"))
96
+ return failSafe("git diff --numstat -z output is not NUL-terminated (malformed framing)");
105
97
  const paths = new Set;
106
98
  let insertions = 0;
107
99
  let deletions = 0;
108
- for (const line of numstat.stdout.split(`
109
- `)) {
110
- const row2 = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line);
111
- if (!row2)
100
+ const records = numstat.stdout.split("\x00").slice(0, -1);
101
+ for (let i = 0;i < records.length; i++) {
102
+ const header = records[i] ?? "";
103
+ const first = header.indexOf("\t");
104
+ const second = header.indexOf("\t", first + 1);
105
+ if (first < 1 || second < first + 2)
106
+ return failSafe("git diff --numstat -z record is missing counts or path");
107
+ const counts = addCounts(header.slice(0, first), header.slice(first + 1, second));
108
+ if (counts === null)
109
+ return failSafe("git diff --numstat -z record has a malformed count");
110
+ insertions += counts[0];
111
+ deletions += counts[1];
112
+ const path = header.slice(second + 1);
113
+ if (path !== "") {
114
+ paths.add(path);
112
115
  continue;
113
- for (const p of expandDiffPath(row2[3] ?? ""))
114
- paths.add(p);
115
- if (row2[1] !== "-")
116
- insertions += Number(row2[1]);
117
- if (row2[2] !== "-")
118
- deletions += Number(row2[2]);
116
+ }
117
+ const oldPath = records[++i];
118
+ const newPath = records[++i];
119
+ if (!oldPath || !newPath)
120
+ return failSafe("git diff --numstat -z rename record has incomplete paths");
121
+ paths.add(oldPath);
122
+ paths.add(newPath);
119
123
  }
120
124
  const others = git(["ls-files", "--others", "--exclude-standard", "-z"]);
121
125
  if (others.status !== 0)
@@ -161,7 +165,6 @@ export {
161
165
  sensitiveReasonForPath,
162
166
  runDiffstat,
163
167
  main,
164
- expandDiffPath,
165
168
  TASK_DIFFSTAT_USAGE,
166
169
  SENSITIVE_PREFIXES,
167
170
  SENSITIVE_GLOBS