@repo-os/repoos 0.5.73 → 0.5.74

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 (123) hide show
  1. package/dist/.build-info.json +2 -2
  2. package/dist/.build-stamp.json +1 -1
  3. package/dist/commands/checks.js +5 -6
  4. package/dist/core/attention.d.ts +2 -2
  5. package/dist/core/attention.js +1 -0
  6. package/dist/core/check-wait.d.ts +7 -4
  7. package/dist/core/check-wait.js +11 -8
  8. package/dist/core/context-pack.js +2 -0
  9. package/dist/core/cross-process-slots.d.ts +3 -0
  10. package/dist/core/cross-process-slots.js +26 -5
  11. package/dist/core/git-maintenance.d.ts +16 -1
  12. package/dist/core/git-maintenance.js +82 -1
  13. package/dist/core/git.d.ts +2 -0
  14. package/dist/core/git.js +14 -2
  15. package/dist/core/local-gates.js +5 -2
  16. package/dist/core/local-queue-marker.d.ts +4 -0
  17. package/dist/core/local-queue-marker.js +7 -0
  18. package/dist/core/needs-input.js +2 -2
  19. package/dist/core/shot-fixtures.d.ts +1 -1
  20. package/dist/core/shot-fixtures.js +1 -0
  21. package/dist/server/attention-events.d.ts +1 -0
  22. package/dist/server/attention-events.js +7 -0
  23. package/dist/server/server.js +31 -1
  24. package/dist/server/task-watchdog.d.ts +1 -9
  25. package/dist/server/task-watchdog.js +35 -28
  26. package/dist/ui/assets/{ActivityIndicator-CFysp8fx.js → ActivityIndicator-4ufauHwi.js} +1 -1
  27. package/dist/ui/assets/{AgentModelControl-iHOGbgue.js → AgentModelControl-9-ZIzPrl.js} +1 -1
  28. package/dist/ui/assets/{AgentModelModal-Cifx9MEF.js → AgentModelModal-Dgv9l_07.js} +1 -1
  29. package/dist/ui/assets/{AgentsView-DoAgVcFi.js → AgentsView-Bl9Olksk.js} +1 -1
  30. package/dist/ui/assets/{ChatAgentModelChip-DPHWICS-.js → ChatAgentModelChip-CwDV90K2.js} +1 -1
  31. package/dist/ui/assets/{ChecksView-DaLrQ8xf.js → ChecksView-DTrP7Uuz.js} +0 -0
  32. package/dist/ui/assets/{ContextView-Btiqouai.js → ContextView-CxfgyJ-y.js} +2 -2
  33. package/dist/ui/assets/{DashboardView-BzKRZk7c.js → DashboardView-5rGQ9OEh.js} +1 -1
  34. package/dist/ui/assets/{DependencyChip-BZQN-6jN.js → DependencyChip-CRG4bs5o.js} +1 -1
  35. package/dist/ui/assets/{DeploymentsView-CyiTsye9.js → DeploymentsView-BFOhkvLs.js} +1 -1
  36. package/dist/ui/assets/{DevView-CwWIgB5t.js → DevView-CYfAT-0x.js} +1 -1
  37. package/dist/ui/assets/{DiffView-DPlMpZwl.js → DiffView-BTDGsIts.js} +1 -1
  38. package/dist/ui/assets/{FailedTestsList-DKSLDbG6.js → FailedTestsList-xY10Qzc8.js} +1 -1
  39. package/dist/ui/assets/{InputsView-BZzEXRPF.js → InputsView-7HAgIHQh.js} +1 -1
  40. package/dist/ui/assets/{LoginView-Bww-iSWH.js → LoginView-BwP14kSS.js} +1 -1
  41. package/dist/ui/assets/{ReleasesView-B9Ao5H9s.js → ReleasesView-BTN2vXUI.js} +1 -1
  42. package/dist/ui/assets/{SecretsView-BSEb1LP0.js → SecretsView-CAFjCFQF.js} +1 -1
  43. package/dist/ui/assets/{SettingsView-DE5rdOwQ.js → SettingsView-B2WOxPud.js} +1 -1
  44. package/dist/ui/assets/{StoriesView-CzcU-EES.js → StoriesView-C-euuUSI.js} +1 -1
  45. package/dist/ui/assets/{VoiceDictate-BIXyHCYF.js → VoiceDictate-CES0G8Vq.js} +1 -1
  46. package/dist/ui/assets/WorkView-C9qAO66W.js +2 -0
  47. package/dist/ui/assets/{abnfDiagram-VCTEODGH-D6t2YdDd.js → abnfDiagram-VCTEODGH-DqUj4qFG.js} +1 -1
  48. package/dist/ui/assets/{architectureDiagram-5GKGNRK7-DfvZY3qH.js → architectureDiagram-5GKGNRK7-DAHlI7NO.js} +1 -1
  49. package/dist/ui/assets/{blockDiagram-I7D4REHJ-p9SaIkVm.js → blockDiagram-I7D4REHJ-BZ71-q2F.js} +1 -1
  50. package/dist/ui/assets/{c4Diagram-7LVT6UL2-BL-ftUgy.js → c4Diagram-7LVT6UL2-CXbbODvo.js} +1 -1
  51. package/dist/ui/assets/channel-Cz_Tghq-.js +1 -0
  52. package/dist/ui/assets/{check-setup-DQZqfS14.js → check-setup-Bng0rbuw.js} +1 -1
  53. package/dist/ui/assets/{chunk-2E4U76K2-V3UlY0fF.js → chunk-2E4U76K2-RYa4g59L.js} +1 -1
  54. package/dist/ui/assets/{chunk-4HAMMTFA-CMXUfdBr.js → chunk-4HAMMTFA-CXuZpZ_d.js} +1 -1
  55. package/dist/ui/assets/{chunk-75Z2AOVW-BVgiO27L.js → chunk-75Z2AOVW-BP2GTRXO.js} +1 -1
  56. package/dist/ui/assets/{chunk-CLGD4ZFX-CRaWjb_k.js → chunk-CLGD4ZFX-DLzOmHqh.js} +1 -1
  57. package/dist/ui/assets/{chunk-DU6HZSFF-DoybIPWm.js → chunk-DU6HZSFF-DdjsSi6z.js} +1 -1
  58. package/dist/ui/assets/{chunk-F27PBJKO-N6cLLABx.js → chunk-F27PBJKO-Dnn5wO3i.js} +1 -1
  59. package/dist/ui/assets/{chunk-GMAD6QVW-iqG0Mryo.js → chunk-GMAD6QVW-DdG_nfFi.js} +1 -1
  60. package/dist/ui/assets/{chunk-GVQU2GXP-f8esshKW.js → chunk-GVQU2GXP-DMAbXLSv.js} +1 -1
  61. package/dist/ui/assets/{chunk-IMKFNOWR-DRyUVZ8K.js → chunk-IMKFNOWR-DHz_-TDB.js} +1 -1
  62. package/dist/ui/assets/{chunk-L3NEJ4N5-MIVQTWm1.js → chunk-L3NEJ4N5-BwhWYbi6.js} +1 -1
  63. package/dist/ui/assets/{chunk-OSK3NFVY-BCHyxVoP.js → chunk-OSK3NFVY-CWlbubvG.js} +1 -1
  64. package/dist/ui/assets/{chunk-P2QGCYS3-ChYZkZI_.js → chunk-P2QGCYS3-D9Muf9td.js} +1 -1
  65. package/dist/ui/assets/{chunk-POPQ4Y6H-TgXMUskQ.js → chunk-POPQ4Y6H-D3ODGb45.js} +1 -1
  66. package/dist/ui/assets/{chunk-PWAF6VOD-qsH24WJM.js → chunk-PWAF6VOD-D6LRyAPN.js} +1 -1
  67. package/dist/ui/assets/{chunk-SHT3W25Y-BQu5f8tz.js → chunk-SHT3W25Y-CXu4onhx.js} +1 -1
  68. package/dist/ui/assets/{chunk-SVP7TREG-CvmZPDcQ.js → chunk-SVP7TREG-7kV5LYQU.js} +1 -1
  69. package/dist/ui/assets/{chunk-TICWLB2K-DPOtzVuP.js → chunk-TICWLB2K-ygSlNLtb.js} +1 -1
  70. package/dist/ui/assets/{chunk-TLUHSLCS-D30OEKwA.js → chunk-TLUHSLCS-Bj-PwaDc.js} +2 -2
  71. package/dist/ui/assets/classDiagram-ZZMXUADV-DHAdi9dV.js +1 -0
  72. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-DHAdi9dV.js +1 -0
  73. package/dist/ui/assets/{cynefinDiagram-5FMLGOSQ-BMMxTCVH.js → cynefinDiagram-5FMLGOSQ-DDFWjNjK.js} +1 -1
  74. package/dist/ui/assets/{dagre-GXQ25YYZ-mdcBXgnq.js → dagre-GXQ25YYZ-CClbhBuT.js} +1 -1
  75. package/dist/ui/assets/{diagram-S7CK7UJ4-BYHQN-__.js → diagram-S7CK7UJ4-BXdPXgUF.js} +1 -1
  76. package/dist/ui/assets/{diagram-UQ7AKVKN-du5xkY-t.js → diagram-UQ7AKVKN-Cx3iFpM0.js} +1 -1
  77. package/dist/ui/assets/{diagram-VSXAHHWV-CDljZt2c.js → diagram-VSXAHHWV-szmYWA2M.js} +1 -1
  78. package/dist/ui/assets/{diagram-VX7I27RA-66z56eI2.js → diagram-VX7I27RA-CX2RE3DT.js} +1 -1
  79. package/dist/ui/assets/{diagram-Z3DM3KII-DDSrtck7.js → diagram-Z3DM3KII-CmMfHB9Q.js} +1 -1
  80. package/dist/ui/assets/{docs-8_CFug_V.js → docs-eoM8eVGk.js} +1 -1
  81. package/dist/ui/assets/{ebnfDiagram-PWID7BFC-DQDNc_-9.js → ebnfDiagram-PWID7BFC-BO-2n4ZP.js} +1 -1
  82. package/dist/ui/assets/{erDiagram-RLTQ6QDP-CAWU1WaF.js → erDiagram-RLTQ6QDP-dCEqdV8F.js} +1 -1
  83. package/dist/ui/assets/flowDiagram-HODETNUW-CX9qpoV4.js +1 -0
  84. package/dist/ui/assets/{ganttDiagram-EL5Y4UJY-C6V1kaYI.js → ganttDiagram-EL5Y4UJY-BJb9E6tm.js} +1 -1
  85. package/dist/ui/assets/{gitGraphDiagram-WWUBYQGX-BGygR4v-.js → gitGraphDiagram-WWUBYQGX-B1CtsArh.js} +1 -1
  86. package/dist/ui/assets/index-Pr4f9cgO.js +68 -0
  87. package/dist/ui/assets/{index-BEQoXI01.css → index-m6du4z4N.css} +1 -1
  88. package/dist/ui/assets/{infoDiagram-27XIBGKW-DZILjOjA.js → infoDiagram-27XIBGKW-a6qt1YhM.js} +1 -1
  89. package/dist/ui/assets/{ishikawaDiagram-5VMMS53U-B2cFxmGF.js → ishikawaDiagram-5VMMS53U-DU6MVFDK.js} +1 -1
  90. package/dist/ui/assets/{journeyDiagram-3NMN7TZE-C5Sb8YTl.js → journeyDiagram-3NMN7TZE-oc0zPgCF.js} +1 -1
  91. package/dist/ui/assets/{kanban-definition-UXKFOSKX-CV3Osxtb.js → kanban-definition-UXKFOSKX-BFCluyXI.js} +1 -1
  92. package/dist/ui/assets/{line-BxE-EqfN.js → line-C0dnI-L4.js} +1 -1
  93. package/dist/ui/assets/{mermaid-parser.core-CQ4lbIXW.js → mermaid-parser.core-CfrNQx8w.js} +1 -1
  94. package/dist/ui/assets/{mermaid.core-Do6AR5iD.js → mermaid.core-CKD0p_aW.js} +3 -3
  95. package/dist/ui/assets/{mindmap-definition-YA3MSWOX-Cpi-IHX6.js → mindmap-definition-YA3MSWOX-DEEwX5b3.js} +1 -1
  96. package/dist/ui/assets/{pegDiagram-XKGWAZYB-DMcJVhJW.js → pegDiagram-XKGWAZYB-P0QyFQvY.js} +1 -1
  97. package/dist/ui/assets/{pieDiagram-E7YTZNPT-CdXUU05z.js → pieDiagram-E7YTZNPT-DUingoVK.js} +1 -1
  98. package/dist/ui/assets/{quadrantDiagram-AXDQQJYC-B2_Z5M8E.js → quadrantDiagram-AXDQQJYC-5KZfHHFZ.js} +1 -1
  99. package/dist/ui/assets/{railroadDiagram-O6MQD6OU-u4uj57HH.js → railroadDiagram-O6MQD6OU-D0sx6QJB.js} +1 -1
  100. package/dist/ui/assets/{repo-CPSBfMRE.js → repo-B9GZ5YkR.js} +1 -1
  101. package/dist/ui/assets/{requirementDiagram-BXWQKSXE-B4keHdJD.js → requirementDiagram-BXWQKSXE-B4DIJgDV.js} +1 -1
  102. package/dist/ui/assets/{sankeyDiagram-P5KCCOFB-dZdIPYfI.js → sankeyDiagram-P5KCCOFB-Ds9sRhEp.js} +1 -1
  103. package/dist/ui/assets/{sequenceDiagram-WJ2MYXX4-kBAuNmyS.js → sequenceDiagram-WJ2MYXX4-BsebZj2V.js} +1 -1
  104. package/dist/ui/assets/{stateDiagram-D77RDMKH-D83tShBX.js → stateDiagram-D77RDMKH-D4ndCrsI.js} +1 -1
  105. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-BHwEjBfW.js +1 -0
  106. package/dist/ui/assets/{swimlanes-42K2YHIH-DRDo7aP4.js → swimlanes-42K2YHIH-DUuSZaDY.js} +1 -1
  107. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-BrUdO8ca.js +8 -0
  108. package/dist/ui/assets/{timeline-definition-24CTP7MA-mnm_omtS.js → timeline-definition-24CTP7MA-UcuOAqHi.js} +1 -1
  109. package/dist/ui/assets/{ui-ClI5E21n.js → ui-fJTOEtHH.js} +2 -2
  110. package/dist/ui/assets/{vennDiagram-4TSXK5OY-Djq6ULjF.js → vennDiagram-4TSXK5OY-Doc_z7zd.js} +1 -1
  111. package/dist/ui/assets/{wardleyDiagram-VM6X3IG4-GfB-sti2.js → wardleyDiagram-VM6X3IG4-BbAt19tq.js} +1 -1
  112. package/dist/ui/assets/{xychartDiagram-S5SC5T6Z-DRh9G9Zb.js → xychartDiagram-S5SC5T6Z-CFT0no8b.js} +1 -1
  113. package/dist/ui/index.html +12 -12
  114. package/dist/ui/sw.js +2 -2
  115. package/package.json +1 -1
  116. package/dist/ui/assets/WorkView-DxNhKHZ0.js +0 -2
  117. package/dist/ui/assets/channel-BV21EJtW.js +0 -1
  118. package/dist/ui/assets/classDiagram-ZZMXUADV-DqgLJsDt.js +0 -1
  119. package/dist/ui/assets/classDiagram-v2-VYDZK3BY-DqgLJsDt.js +0 -1
  120. package/dist/ui/assets/flowDiagram-HODETNUW-tMrL1QbJ.js +0 -1
  121. package/dist/ui/assets/index-ib5yOjK6.js +0 -68
  122. package/dist/ui/assets/stateDiagram-v2-MP3YSRHH-CNtNh1Ls.js +0 -1
  123. package/dist/ui/assets/swimlanesDiagram-VR7AAH4N-CGtDYnNu.js +0 -8
@@ -1,5 +1,5 @@
1
1
  {
2
- "hash": "54182298d418b9bcc12c138af5d99c0470fb3a54a8a573775e600c2e37ab1669",
3
- "version": "0.5.73",
2
+ "hash": "acb4fde7b3c6e297f938786a4607f68999d8c272283c2b7d27f1d821525ef5dd",
3
+ "version": "0.5.74",
4
4
  "devUi": false
5
5
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "generatedAt": "2026-10-29T21:23:55.474Z"
2
+ "generatedAt": "2026-10-30T16:08:37.438Z"
3
3
  }
@@ -2,8 +2,8 @@ import { getCheckStore } from "../core/check-store.js";
2
2
  import { formatSlowestTestFiles } from "../core/test-file-timings.js";
3
3
  import { c } from "../cli/colors.js";
4
4
  import { loadConfig } from "../core/config.js";
5
- import { inspectCrossProcessSlots, localCheckLockRoot, releaseLocalCheckSlot, } from "../core/cross-process-slots.js";
6
- import { readQueueMarkers } from "../core/local-queue-marker.js";
5
+ import { inspectCrossProcessSlots, listSlotWaiterPids, localCheckLockRoot, releaseLocalCheckSlot, } from "../core/cross-process-slots.js";
6
+ import { currentQueueMarkers, readQueueMarkers } from "../core/local-queue-marker.js";
7
7
  import { mainCheckoutRoot } from "../core/git.js";
8
8
  import { heavySlotLimit } from "../core/resource-limits.js";
9
9
  export function cmdChecks(args) {
@@ -73,10 +73,9 @@ export function cmdChecks(args) {
73
73
  ? `slot ${holder.slot}: ${task}, pid ${holder.pid}, ${holder.dead ? "dead, reclaiming" : holder.wedged ? "wedged" : "alive"}, held ${Math.round(holder.ageMs / 1000)}s, heartbeat ${Math.round(holder.heartbeatAgeMs / 1000)}s ago`
74
74
  : `slot ${holder.slot}: free`);
75
75
  }
76
- for (const marker of readQueueMarkers(boardRoot, cacheDir)
77
- .filter((entry) => entry.kind === "local-check")
78
- .sort((a, b) => a.position - b.position)) {
76
+ const queue = currentQueueMarkers(readQueueMarkers(boardRoot, cacheDir).filter((entry) => entry.kind === "local-check"), listSlotWaiterPids(root));
77
+ queue.forEach((marker) => {
79
78
  const waitingMs = Math.max(0, Date.now() - Date.parse(marker.since));
80
79
  console.log(`queue position ${marker.position}: #${marker.taskId}, waiting ${Math.round(waitingMs / 1000)}s`);
81
- }
80
+ });
82
81
  }
@@ -12,7 +12,7 @@ export interface CloseOutOutcomeFeedEvent {
12
12
  }
13
13
  export type AttentionSeverity = "info" | "warning" | "error";
14
14
  /** Every attention kind the bell and API expose. */
15
- export type AttentionKind = "releaseNotesReady" | "releaseSucceeded" | "releaseFailed" | "closeOutSucceeded" | "closeOutFailed" | "closeOutTimedOut" | "taskReview" | "taskNeedsInput" | "taskNeedsMerge" | "taskAssigned" | "providerFailure" | "providerUsageLimit" | "silentRun" | "slowRun" | "slowRunsRecently" | "spendThreshold" | "awaitingVisualCheck" | "remoteFallback" | "remoteHostDegraded" | "ctoAction" | "ctoSilent";
15
+ export type AttentionKind = "releaseNotesReady" | "releaseSucceeded" | "releaseFailed" | "closeOutSucceeded" | "closeOutFailed" | "closeOutTimedOut" | "taskReview" | "taskNeedsInput" | "taskNeedsMerge" | "taskAssigned" | "providerFailure" | "providerUsageLimit" | "silentRun" | "slowRun" | "slowRunsRecently" | "spendThreshold" | "awaitingVisualCheck" | "remoteFallback" | "remoteHostDegraded" | "ctoAction" | "gitLock" | "ctoSilent";
16
16
  export interface AttentionItem {
17
17
  /** Stable per event — dedupe key for clients and dismiss markers. */
18
18
  id: string;
@@ -33,7 +33,7 @@ export interface AttentionFeed {
33
33
  /** Durable attention events recorded on the server (provider failures, remote fallback). */
34
34
  export interface RecordedAttentionEvent {
35
35
  id: string;
36
- kind: "providerFailure" | "providerUsageLimit" | "remoteFallback" | "ctoAction";
36
+ kind: "providerFailure" | "providerUsageLimit" | "remoteFallback" | "ctoAction" | "gitLock";
37
37
  taskId: string | null;
38
38
  message: string;
39
39
  detail: string;
@@ -26,6 +26,7 @@ const SEVERITY = {
26
26
  remoteFallback: "warning",
27
27
  remoteHostDegraded: "warning",
28
28
  ctoAction: "info",
29
+ gitLock: "warning",
29
30
  ctoSilent: "warning",
30
31
  };
31
32
  /** Attention kinds that imply the CTO (or a driver) should be acting (#0728). */
@@ -9,13 +9,16 @@
9
9
  * `[queue]` status lines and write queue markers; this recorder turns those
10
10
  * lines into (a) a human-readable "waiting: <reason> for Nm" trail on stdout —
11
11
  * which the handoff transcript captures — and (b) a `waited` fact persisted on
12
- * the run's CheckRunRecord. A wait that crosses ten minutes says so once, so
13
- * the transcript names the escalation instead of hanging silently.
12
+ * the run's CheckRunRecord. A wait that crosses ten minutes reminds the
13
+ * engineer to keep waiting.
14
+ * Queueing is normal and does not itself request human input.
14
15
  */
15
16
  /** How often the "still waiting" line repeats while the wait continues. */
16
17
  export declare const CHECK_WAIT_REMINDER_MS = 60000;
17
- /** A wait this long is surfaced as needs_input by the task watchdog. */
18
- export declare const CHECK_WAIT_ESCALATION_MS: number;
18
+ /** A long normal wait gets an explicit keep-waiting reminder. */
19
+ export declare const CHECK_WAIT_REMINDER_THRESHOLD_MS: number;
20
+ /** Missing holder heartbeats for this long indicate an abnormal wait. */
21
+ export declare const CHECK_WAIT_STALLED_MS: number;
19
22
  export interface CheckWait {
20
23
  /** Latest observed reason, verbatim from the gate's status line. */
21
24
  reason: string;
@@ -9,17 +9,20 @@
9
9
  * `[queue]` status lines and write queue markers; this recorder turns those
10
10
  * lines into (a) a human-readable "waiting: <reason> for Nm" trail on stdout —
11
11
  * which the handoff transcript captures — and (b) a `waited` fact persisted on
12
- * the run's CheckRunRecord. A wait that crosses ten minutes says so once, so
13
- * the transcript names the escalation instead of hanging silently.
12
+ * the run's CheckRunRecord. A wait that crosses ten minutes reminds the
13
+ * engineer to keep waiting.
14
+ * Queueing is normal and does not itself request human input.
14
15
  */
15
16
  /** How often the "still waiting" line repeats while the wait continues. */
16
17
  export const CHECK_WAIT_REMINDER_MS = 60_000;
17
- /** A wait this long is surfaced as needs_input by the task watchdog. */
18
- export const CHECK_WAIT_ESCALATION_MS = 10 * 60_000;
18
+ /** A long normal wait gets an explicit keep-waiting reminder. */
19
+ export const CHECK_WAIT_REMINDER_THRESHOLD_MS = 10 * 60_000;
20
+ /** Missing holder heartbeats for this long indicate an abnormal wait. */
21
+ export const CHECK_WAIT_STALLED_MS = 30 * 60_000;
19
22
  export function createCheckWaitRecorder(emit, now = Date.now) {
20
23
  let current = null;
21
24
  let lastAnnouncedAt = 0;
22
- let announcedEscalation = false;
25
+ let announcedLongWait = false;
23
26
  return {
24
27
  onStatusLine(line) {
25
28
  const reason = line.trim();
@@ -37,10 +40,10 @@ export function createCheckWaitRecorder(emit, now = Date.now) {
37
40
  lastAnnouncedAt = now();
38
41
  emit(`waiting: ${reason} for ${formatWaitDuration(waitedMs)}`);
39
42
  }
40
- if (!announcedEscalation && waitedMs >= CHECK_WAIT_ESCALATION_MS) {
41
- announcedEscalation = true;
43
+ if (!announcedLongWait && waitedMs >= CHECK_WAIT_REMINDER_THRESHOLD_MS) {
44
+ announcedLongWait = true;
42
45
  emit(`waiting: ${reason} for ${formatWaitDuration(waitedMs)} — ` +
43
- "this wait is being surfaced as needs input");
46
+ "queued checks are normal — keep waiting; do not end your turn");
44
47
  }
45
48
  },
46
49
  finish() {
@@ -657,6 +657,8 @@ function renderPack(config, task, branch, cwd, cacheHit, bootstrapResult, releva
657
657
  push("bun run build:ui # UI-only build");
658
658
  push("```");
659
659
  push("");
660
+ push("A queued check is normal, including a long wait for a live slot holder or the pressure guard. Keep waiting until it passes, then hand off; do not end your turn because of the waiting badge. Only abnormal waits request input.");
661
+ push("");
660
662
  // ---- Managed Preview ----
661
663
  push("### Managed Preview");
662
664
  push("");
@@ -11,6 +11,7 @@ export interface SlotAcquireOptions {
11
11
  /** Written into the slot dir for debugging and stale recovery. */
12
12
  meta?: Record<string, unknown>;
13
13
  onQueued?: (ahead: number, position: number) => void;
14
+ onWaiterCleared?: () => void;
14
15
  onHeartbeat?: () => void;
15
16
  staleAfterMs?: number;
16
17
  reclaimGraceMs?: number;
@@ -37,6 +38,8 @@ export declare function reapOrphanedLocalCheckGroups(lockRoot: string): number[]
37
38
  export declare function acquireCrossProcessSlot(opts: SlotAcquireOptions): Promise<() => void>;
38
39
  /** Live waiters parked on this slot root (dead pids are ignored). */
39
40
  export declare function countSlotWaiters(lockRoot: string): number;
41
+ /** PIDs that still have a live waiter entry in this slot queue. */
42
+ export declare function listSlotWaiterPids(lockRoot: string): number[];
40
43
  export declare function heavyJobCounts(lockRoot: string): {
41
44
  running: number;
42
45
  queued: number;
@@ -352,6 +352,7 @@ export async function acquireCrossProcessSlot(opts) {
352
352
  /* already gone */
353
353
  }
354
354
  waiterPath = null;
355
+ opts.onWaiterCleared?.();
355
356
  };
356
357
  const markWaiting = () => {
357
358
  if (waiterPath)
@@ -363,6 +364,15 @@ export async function acquireCrossProcessSlot(opts) {
363
364
  };
364
365
  let heartbeat;
365
366
  const signalHandlers = new Map();
367
+ for (const signal of ["SIGTERM", "SIGINT"]) {
368
+ const handler = () => {
369
+ clearWaiter();
370
+ process.off(signal, handler);
371
+ process.kill(process.pid, signal);
372
+ };
373
+ signalHandlers.set(signal, handler);
374
+ process.once(signal, handler);
375
+ }
366
376
  const release = () => {
367
377
  if (heartbeat)
368
378
  clearInterval(heartbeat);
@@ -383,6 +393,9 @@ export async function acquireCrossProcessSlot(opts) {
383
393
  held = tryTakeSlot(opts.lockRoot, slots, opts.meta, opts.staleAfterMs, opts.reclaimGraceMs);
384
394
  if (held) {
385
395
  clearWaiter();
396
+ for (const [signal, handler] of signalHandlers)
397
+ process.off(signal, handler);
398
+ signalHandlers.clear();
386
399
  for (const signal of ["SIGTERM", "SIGINT"]) {
387
400
  const handler = () => {
388
401
  release();
@@ -423,20 +436,24 @@ export async function acquireCrossProcessSlot(opts) {
423
436
  }
424
437
  /** Live waiters parked on this slot root (dead pids are ignored). */
425
438
  export function countSlotWaiters(lockRoot) {
439
+ return listSlotWaiterPids(lockRoot).length;
440
+ }
441
+ /** PIDs that still have a live waiter entry in this slot queue. */
442
+ export function listSlotWaiterPids(lockRoot) {
426
443
  const dir = join(lockRoot, ".queue");
427
444
  let names = [];
428
445
  try {
429
446
  names = readdirSync(dir);
430
447
  }
431
448
  catch {
432
- return 0;
449
+ return [];
433
450
  }
434
- let n = 0;
451
+ const pids = [];
435
452
  for (const name of names) {
436
453
  try {
437
454
  const pid = Number(readFileSync(join(dir, name), "utf8").trim());
438
455
  if (processIsAlive(pid))
439
- n++;
456
+ pids.push(pid);
440
457
  else
441
458
  rmSync(join(dir, name), { force: true });
442
459
  }
@@ -444,7 +461,7 @@ export function countSlotWaiters(lockRoot) {
444
461
  /* raced */
445
462
  }
446
463
  }
447
- return n;
464
+ return pids;
448
465
  }
449
466
  export function heavyJobCounts(lockRoot) {
450
467
  const holders = inspectCrossProcessSlots(lockRoot);
@@ -474,7 +491,11 @@ export async function acquireLocalFallbackSerializeSlot(repoRoot, cacheDir, wait
474
491
  slots: 1,
475
492
  lockRoot: localFallbackSerializeLockRoot(repoRoot, cacheDir),
476
493
  waitTimeoutMs,
477
- meta: { kind: "local-fallback-serialize", id: randomBytes(4).toString("hex") },
494
+ meta: {
495
+ kind: "local-fallback-serialize",
496
+ pid: process.pid,
497
+ id: randomBytes(4).toString("hex"),
498
+ },
478
499
  onQueued: (ahead) => onQueued?.(ahead),
479
500
  });
480
501
  }
@@ -1,16 +1,31 @@
1
1
  import { gitOutputAsync } from "./git.js";
2
+ export interface GitLockMaintenanceOptions {
3
+ now?: () => number;
4
+ processHasLockOpen?: (path: string) => boolean | Promise<boolean>;
5
+ onLock?: (event: {
6
+ path: string;
7
+ state: "live" | "removed" | "cleared";
8
+ ageMs: number;
9
+ }) => void;
10
+ }
11
+ export declare function setGitMaintenanceLockSink(sink: GitLockMaintenanceOptions["onLock"]): void;
2
12
  /** Independent of both worker queues: a history timeout cannot cancel maintenance. */
3
13
  export declare class CommitGraphMaintenance {
4
14
  private readonly root;
5
15
  private readonly git;
6
16
  private readonly warn;
17
+ private readonly lockOptions;
7
18
  private debounce;
8
19
  private tick;
20
+ private lockTick;
9
21
  private running;
10
22
  private closed;
11
- constructor(root: string, git?: typeof gitOutputAsync, warn?: (message: string) => void);
23
+ private liveLocks;
24
+ constructor(root: string, git?: typeof gitOutputAsync, warn?: (message: string) => void, lockOptions?: GitLockMaintenanceOptions);
12
25
  schedule(delayMs?: number): void;
13
26
  check(): Promise<void>;
14
27
  private rebuildIfNeeded;
28
+ private recoverIndexLocks;
29
+ private checkIndexLocks;
15
30
  close(): void;
16
31
  }
@@ -1,25 +1,51 @@
1
1
  import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { execFileSync } from "node:child_process";
2
3
  import { dirname, join, resolve } from "node:path";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import { gitOutputAsync } from "./git.js";
5
6
  const TICK_MS = 5 * 60_000;
7
+ const INDEX_LOCK_TICK_MS = 60_000;
6
8
  const MAX_GRAPH_AGE_MS = 60 * 60_000;
7
9
  const DEBOUNCE_MS = 5_000;
10
+ const STALE_INDEX_LOCK_MS = 2 * 60_000;
11
+ let lockEventSink = undefined;
12
+ export function setGitMaintenanceLockSink(sink) {
13
+ lockEventSink = sink;
14
+ }
15
+ function processHasLockOpen(path) {
16
+ try {
17
+ return (execFileSync("lsof", ["-t", path], {
18
+ encoding: "utf8",
19
+ stdio: ["ignore", "pipe", "ignore"],
20
+ }).trim().length > 0);
21
+ }
22
+ catch (error) {
23
+ // lsof exits 1 when no process has the file open. If the probe itself is
24
+ // unavailable, fail safe and leave the lock for a later maintenance tick.
25
+ return error.status !== 1;
26
+ }
27
+ }
8
28
  /** Independent of both worker queues: a history timeout cannot cancel maintenance. */
9
29
  export class CommitGraphMaintenance {
10
30
  root;
11
31
  git;
12
32
  warn;
33
+ lockOptions;
13
34
  debounce;
14
35
  tick;
36
+ lockTick;
15
37
  running = null;
16
38
  closed = false;
17
- constructor(root, git = gitOutputAsync, warn = console.warn) {
39
+ liveLocks = new Set();
40
+ constructor(root, git = gitOutputAsync, warn = console.warn, lockOptions = {}) {
18
41
  this.root = root;
19
42
  this.git = git;
20
43
  this.warn = warn;
44
+ this.lockOptions = lockOptions;
21
45
  this.tick = setInterval(() => void this.check(), TICK_MS);
22
46
  this.tick.unref?.();
47
+ this.lockTick = setInterval(() => void this.checkIndexLocks(), INDEX_LOCK_TICK_MS);
48
+ this.lockTick.unref?.();
23
49
  this.schedule(0);
24
50
  }
25
51
  schedule(delayMs = DEBOUNCE_MS) {
@@ -48,6 +74,7 @@ export class CommitGraphMaintenance {
48
74
  return this.running;
49
75
  }
50
76
  async rebuildIfNeeded() {
77
+ await this.recoverIndexLocks();
51
78
  const head = await this.git(this.root, ["rev-parse", "HEAD"]);
52
79
  if (!head || this.closed)
53
80
  return;
@@ -89,9 +116,63 @@ export class CommitGraphMaintenance {
89
116
  await rm(temporary, { force: true }).catch(() => { });
90
117
  }
91
118
  }
119
+ async recoverIndexLocks() {
120
+ const now = this.lockOptions.now ?? Date.now;
121
+ const listed = await this.git(this.root, ["worktree", "list", "--porcelain"]);
122
+ const roots = new Set([this.root]);
123
+ for (const line of (listed ?? "").split("\n")) {
124
+ if (line.startsWith("worktree "))
125
+ roots.add(line.slice("worktree ".length).trim());
126
+ }
127
+ for (const root of roots) {
128
+ if (this.closed)
129
+ return;
130
+ const gitDir = await this.git(root, ["rev-parse", "--absolute-git-dir"]);
131
+ if (!gitDir)
132
+ continue;
133
+ const path = join(gitDir, "index.lock");
134
+ const lock = await stat(path).catch(() => null);
135
+ if (!lock) {
136
+ if (this.liveLocks.delete(path))
137
+ (this.lockOptions.onLock ?? lockEventSink)?.({ path, state: "cleared", ageMs: 0 });
138
+ continue;
139
+ }
140
+ const ageMs = Math.max(0, now() - lock.mtimeMs);
141
+ if (ageMs <= STALE_INDEX_LOCK_MS)
142
+ continue;
143
+ let live = true;
144
+ try {
145
+ live = await (this.lockOptions.processHasLockOpen ?? processHasLockOpen)(path);
146
+ }
147
+ catch {
148
+ live = true;
149
+ }
150
+ if (live) {
151
+ if (!this.liveLocks.has(path)) {
152
+ this.liveLocks.add(path);
153
+ (this.lockOptions.onLock ?? lockEventSink)?.({ path, state: "live", ageMs });
154
+ }
155
+ continue;
156
+ }
157
+ await rm(path, { force: true });
158
+ this.liveLocks.delete(path);
159
+ (this.lockOptions.onLock ?? lockEventSink)?.({ path, state: "removed", ageMs });
160
+ }
161
+ }
162
+ async checkIndexLocks() {
163
+ if (this.closed)
164
+ return;
165
+ try {
166
+ await this.recoverIndexLocks();
167
+ }
168
+ catch (error) {
169
+ this.warn(`git index.lock maintenance failed: ${error.message}`);
170
+ }
171
+ }
92
172
  close() {
93
173
  this.closed = true;
94
174
  clearInterval(this.tick);
175
+ clearInterval(this.lockTick);
95
176
  clearTimeout(this.debounce);
96
177
  }
97
178
  }
@@ -35,6 +35,8 @@ export declare function runGit(root: string, args: string[], timeout: number): P
35
35
  * `fsmonitor` daemons running and no known holder).
36
36
  */
37
37
  export declare function isGitLockContention(text: string): boolean;
38
+ /** Install the server's durable logging hook for fail-soft bookkeeping commits. */
39
+ export declare function setGitLockFailureSink(sink: ((root: string, message: string, detail: string) => void) | null): void;
38
40
  /** `retryOnGitLock` predicate for a raw `runGit` result: the lock text, or null. */
39
41
  export declare function gitRunLockContention(r: {
40
42
  status: number | null;
package/dist/core/git.js CHANGED
@@ -135,6 +135,11 @@ export function runGit(root, args, timeout) {
135
135
  export function isGitLockContention(text) {
136
136
  return /Unable to create [^\n]*\.lock|Another git process seems to be running|index\.lock/i.test(text);
137
137
  }
138
+ let gitLockFailureSink = null;
139
+ /** Install the server's durable logging hook for fail-soft bookkeeping commits. */
140
+ export function setGitLockFailureSink(sink) {
141
+ gitLockFailureSink = sink;
142
+ }
138
143
  /** `retryOnGitLock` predicate for a raw `runGit` result: the lock text, or null. */
139
144
  export function gitRunLockContention(r) {
140
145
  return r.status !== 0 && isGitLockContention(r.stderr) ? r.stderr : null;
@@ -1752,12 +1757,19 @@ export function commitFiles(root, absPaths, message) {
1752
1757
  return false;
1753
1758
  if (status.trim() === "")
1754
1759
  return true;
1755
- if (git(root, ["add", "-A", "--", ...rels]) === null)
1760
+ const add = gitCapture(root, ["add", "-A", "--", ...rels]);
1761
+ if (add.status !== 0) {
1762
+ if (isGitLockContention(add.stderr))
1763
+ gitLockFailureSink?.(root, message, add.stderr.trim());
1756
1764
  return false;
1765
+ }
1757
1766
  // `--only` with a pathspec commits just these files and leaves everything
1758
1767
  // else already in the index staged (#0353): a plain `git commit` would sweep
1759
1768
  // in whatever a human or agent had staged in the checkout under our message.
1760
- const committed = git(root, ["commit", "-o", "-m", message, "--", ...rels]) !== null;
1769
+ const commit = gitCapture(root, ["commit", "-o", "-m", message, "--", ...rels]);
1770
+ const committed = commit.status === 0;
1771
+ if (!committed && isGitLockContention(commit.stderr))
1772
+ gitLockFailureSink?.(root, message, commit.stderr.trim());
1761
1773
  // RepoOS's own bookkeeping commits are the main reason the sidebar git-state
1762
1774
  // indicator would otherwise flicker stale — tell it to recompute (#0584).
1763
1775
  if (committed)
@@ -16,7 +16,7 @@ function taskIdFromEnv(env) {
16
16
  const id = env.REPOOS_TASK_ID?.trim() || env.REPOOS_CHECK_TASK_ID?.trim();
17
17
  return id && /^\d+$/.test(id) ? id : null;
18
18
  }
19
- function markQueue(repoRoot, cacheDir, env, kind, ahead, position, message) {
19
+ function markQueue(repoRoot, cacheDir, env, kind, ahead, position, message, pool) {
20
20
  const taskId = taskIdFromEnv(env);
21
21
  if (!taskId)
22
22
  return;
@@ -28,6 +28,8 @@ function markQueue(repoRoot, cacheDir, env, kind, ahead, position, message) {
28
28
  position,
29
29
  message,
30
30
  since: since ?? new Date().toISOString(),
31
+ pool,
32
+ pid: process.pid,
31
33
  });
32
34
  }
33
35
  function unmarkQueue(repoRoot, cacheDir, env, kind) {
@@ -77,7 +79,7 @@ export async function acquireLocalCheckSlot(opts) {
77
79
  ? "waiting for serialized local fallback slot"
78
80
  : `waiting for serialized local fallback (${ahead} ahead, position ${position})`;
79
81
  emit(`[queue] ${msg}`);
80
- markQueue(board, cacheDir, opts.env, "local-check", ahead, position, msg);
82
+ markQueue(board, cacheDir, opts.env, "local-check", ahead, position, msg, "local-fallback-serialize");
81
83
  });
82
84
  }
83
85
  try {
@@ -104,6 +106,7 @@ export async function acquireLocalCheckSlot(opts) {
104
106
  emit(`[queue] ${label}`);
105
107
  markQueue(board, cacheDir, opts.env, "local-check", ahead, position, title);
106
108
  },
109
+ onWaiterCleared: () => unmarkQueue(board, cacheDir, opts.env, "local-check"),
107
110
  });
108
111
  unmarkQueue(board, cacheDir, opts.env, "local-check");
109
112
  return () => {
@@ -1,13 +1,17 @@
1
1
  export type LocalQueueKind = "local-check" | "playwright" | "pressure";
2
2
  export interface LocalQueueMarker {
3
3
  taskId: string;
4
+ pid?: number;
4
5
  kind: LocalQueueKind;
5
6
  position: number;
6
7
  ahead: number;
7
8
  message: string;
8
9
  since: string;
10
+ /** Slot pool being waited on; absent on older markers. */
11
+ pool?: "local-check" | "local-fallback-serialize" | "playwright";
9
12
  }
10
13
  export declare function queueMarkersDir(repoRoot: string, cacheDir: string): string;
11
14
  export declare function writeQueueMarker(repoRoot: string, cacheDir: string, marker: LocalQueueMarker): void;
12
15
  export declare function clearQueueMarker(repoRoot: string, cacheDir: string, taskId: string, kind: LocalQueueKind): void;
13
16
  export declare function readQueueMarkers(repoRoot: string, cacheDir: string): LocalQueueMarker[];
17
+ export declare function currentQueueMarkers(markers: LocalQueueMarker[], liveWaiterPids: Iterable<number>): LocalQueueMarker[];
@@ -39,3 +39,10 @@ export function readQueueMarkers(repoRoot, cacheDir) {
39
39
  return [];
40
40
  }
41
41
  }
42
+ export function currentQueueMarkers(markers, liveWaiterPids) {
43
+ const live = new Set(liveWaiterPids);
44
+ return markers
45
+ .filter((marker) => marker.kind !== "local-check" || (marker.pid && live.has(marker.pid)))
46
+ .sort((a, b) => Date.parse(a.since) - Date.parse(b.since))
47
+ .map((marker, index) => marker.kind === "local-check" ? { ...marker, ahead: index, position: index + 1 } : marker);
48
+ }
@@ -7,8 +7,8 @@ export function needsInputClearsOnNewEngineerRun(reason) {
7
7
  return true;
8
8
  if (reason === "review-human-attention")
9
9
  return true;
10
- // #0974: a check-wait flag was informational — the gate was queued. Once the
11
- // engineer runs again the wait is over, so the flag must not linger.
10
+ // A fresh engineer run retries an abnormal check wait (#0983). If the
11
+ // underlying holder is still dead/stalled, the watchdog will raise it again.
12
12
  if (reason === "check-wait")
13
13
  return true;
14
14
  return reason === "provider-failure" || reason === "degenerate-output";
@@ -3,7 +3,7 @@
3
3
  * path appends `?shotState=<id>` to the route; the UI applies the fixture only
4
4
  * on preview builds (`isPreviewBuild`).
5
5
  */
6
- export declare const SHOT_PREVIEW_FIXTURE_IDS: readonly ["system:worktreeGc", "closeOut:active", "closeOut:checkProgress", "closeOut:checkProgressUnknown", "checks:progress", "checks:timings", "checks:reuse", "pages:scroll", "closeOut:stalled", "closeOut:uploading", "closeOut:remoteTests", "runners:queued", "integration:median15", "card:doneError", "board:withReviewTask", "drawer:reviewWithBranch", "drawer:landable", "drawer:statusReviewFailed", "drawer:statusReviewFailedNarrow", "drawer:statusHandoffFailed", "drawer:statusChecksRunning", "drawer:statusChecksRunningNarrow", "drawer:statusConfirmingHandoff", "drawer:statusConfirmingHandoffNarrow", "drawer:statusChecksFailed", "drawer:statusChecksFailedNarrow", "drawer:statusDetails", "drawer:landableNarrow", "drawer:tabExpandWarnings", "drawer:activeRequirements", "drawer:nativePreviewBuilding", "drawer:nativePreviewRunning", "drawer:nativePreviewFailed", "drawer:shotEvidence", "drawer:debugLogs", "card:humanStep", "card:held", "drawer:held", "card:reviewRoundsExhausted", "card:handoffConflict", "card:checkpointRecovery", "board:cardChips", "card:noChanges", "board:activeCoding", "board:severalWorkingCards", "board:startWorkPending", "drawer:startWorkPending", "settings:birdQuickTrail", "settings:rgbKeystorm", "settings:wheelieCourier", "settings:booSwoop", "settings:pumpkinSpell", "drawer:queuedMessage", "dev:failed", "dev:loading", "dev:running", "secrets:populated", "secrets:removeDialog", "deployments:environmentBinding", "deployments:loading", "editor:unconfigured", "panel:newInputOutstandingUpload", "panel:freeformFailedScreenshot", "panel:defaultScreenshotPicker", "budget:paused", "schedule:newPanel", "reminders:due"];
6
+ export declare const SHOT_PREVIEW_FIXTURE_IDS: readonly ["system:worktreeGc", "closeOut:active", "closeOut:checkProgress", "closeOut:checkProgressUnknown", "checks:progress", "checks:timings", "checks:reuse", "pages:scroll", "closeOut:stalled", "closeOut:uploading", "closeOut:remoteTests", "runners:queued", "integration:median15", "card:doneError", "board:withReviewTask", "drawer:reviewWithBranch", "drawer:landable", "drawer:statusReviewFailed", "drawer:statusReviewFailedNarrow", "drawer:statusHandoffFailed", "drawer:statusChecksRunning", "drawer:statusChecksRunningNarrow", "drawer:statusConfirmingHandoff", "drawer:statusConfirmingHandoffNarrow", "drawer:statusChecksFailed", "drawer:statusChecksFailedNarrow", "drawer:statusDetails", "drawer:landableNarrow", "drawer:tabExpandWarnings", "drawer:activeRequirements", "drawer:nativePreviewBuilding", "drawer:nativePreviewRunning", "drawer:nativePreviewFailed", "drawer:shotEvidence", "drawer:debugLogs", "card:humanStep", "card:held", "drawer:held", "card:reviewRoundsExhausted", "card:handoffConflict", "card:checkpointRecovery", "board:cardChips", "card:noChanges", "board:activeCoding", "checks:waiting", "board:severalWorkingCards", "board:startWorkPending", "drawer:startWorkPending", "settings:birdQuickTrail", "settings:rgbKeystorm", "settings:wheelieCourier", "settings:booSwoop", "settings:pumpkinSpell", "drawer:queuedMessage", "dev:failed", "dev:loading", "dev:running", "secrets:populated", "secrets:removeDialog", "deployments:environmentBinding", "deployments:loading", "editor:unconfigured", "panel:newInputOutstandingUpload", "panel:freeformFailedScreenshot", "panel:defaultScreenshotPicker", "budget:paused", "schedule:newPanel", "reminders:due"];
7
7
  export type ShotPreviewFixtureId = (typeof SHOT_PREVIEW_FIXTURE_IDS)[number];
8
8
  export declare const SHOT_PREVIEW_FIXTURE_QUERY = "shotState";
9
9
  /** Task id used by card/board fixtures so selectors can target one stable card. */
@@ -90,6 +90,7 @@ export const SHOT_PREVIEW_FIXTURE_IDS = [
90
90
  // #0787 — active engineer run on the board so captures can show the compact
91
91
  // coding indicator on a task card.
92
92
  "board:activeCoding",
93
+ "checks:waiting",
93
94
  // #0980 — six active cards for activity-animation captures and profiling.
94
95
  "board:severalWorkingCards",
95
96
  // #0822 — ready task with Start work in flight so the card shows Starting….
@@ -4,5 +4,6 @@ export interface AttentionEventStore {
4
4
  id?: string;
5
5
  }): RecordedAttentionEvent;
6
6
  list(): RecordedAttentionEvent[];
7
+ remove(id: string): void;
7
8
  }
8
9
  export declare function createAttentionEventStore(root: string, cacheDir?: string, onPersistError?: (error: unknown) => void): AttentionEventStore;
@@ -84,6 +84,13 @@ export function createAttentionEventStore(root, cacheDir = ".repoos", onPersistE
84
84
  persist(events);
85
85
  return full;
86
86
  },
87
+ remove(id) {
88
+ const next = load().filter((event) => event.id !== id);
89
+ if (next.length === load().length)
90
+ return;
91
+ events = next;
92
+ persist(events);
93
+ },
87
94
  list() {
88
95
  return [...load()].sort((a, b) => b.at.localeCompare(a.at));
89
96
  },
@@ -99,7 +99,8 @@ import { ensureStoryNumbers } from "../core/story-definition-files.js";
99
99
  import { createLogger } from "../core/logger.js";
100
100
  import { watchHttpRequest } from "./request-timing.js";
101
101
  import { parseFlatToml, loadConfig, resolveServePort, projectDisplayName, } from "../core/config.js";
102
- import { commitTaskFile, commitFiles, syncBranchWithMain, shouldTuneRepoForScaleAtStartup, tuneRepoForScale, runGit, } from "../core/git.js";
102
+ import { commitTaskFile, commitFiles, syncBranchWithMain, shouldTuneRepoForScaleAtStartup, tuneRepoForScale, runGit, setGitLockFailureSink, } from "../core/git.js";
103
+ import { setGitMaintenanceLockSink } from "../core/git-maintenance.js";
103
104
  import { checkServerRegistryPortConflict, logServerRegistryError, refreshRunningServerProjectName, registerRunningServer, removeServerRegistryEntry, shouldRegisterServerRegistry, updateServerRegistryEngineerCount, } from "../core/server-registry.js";
104
105
  import { onGitMutation } from "../core/git-activity.js";
105
106
  import { sweepOrphanedBookkeeping } from "./orphan-bookkeeping.js";
@@ -694,6 +695,13 @@ export function startServer(opts = {}) {
694
695
  bindHost = "127.0.0.1";
695
696
  }
696
697
  const logger = createLogger(config.root);
698
+ setGitLockFailureSink((root, commitMessage, detail) => {
699
+ const context = { root, commitMessage, detail };
700
+ logger.system("warn", `git write failed because index.lock is present: ${detail}`, context);
701
+ const taskId = commitMessage.match(/\b(?:docs|inputs|schedules|stories)\(([^)]+)\)/)?.[1];
702
+ if (taskId)
703
+ logger.task(taskId, "warn", `git bookkeeping write failed because index.lock is present: ${detail}`, context);
704
+ });
697
705
  // Best-effort: enable git's fsmonitor + untracked-cache on the main checkout
698
706
  // only — preview/shot servers and linked task worktrees share `.git/config`
699
707
  // and must not re-enable watchers or spawn another daemon per start (#0958).
@@ -954,6 +962,28 @@ export function startServer(opts = {}) {
954
962
  const bumpAttention = () => {
955
963
  emitEvent({ type: "attention.updated", at: new Date().toISOString() });
956
964
  };
965
+ setGitMaintenanceLockSink(({ path, state, ageMs }) => {
966
+ const id = `gitLock:${path}`;
967
+ if (state === "live") {
968
+ logger.system("warn", "Live git process still holds stale git index.lock", { path, ageMs });
969
+ attentionEvents.record({
970
+ id,
971
+ kind: "gitLock",
972
+ taskId: null,
973
+ message: "Git is still holding a stale index lock",
974
+ detail: `${path} has been present for ${Math.floor(ageMs / 60_000)} minutes. Git writes may be blocked until the process releases it.`,
975
+ at: new Date().toISOString(),
976
+ });
977
+ bumpAttention();
978
+ return;
979
+ }
980
+ if (state === "removed")
981
+ logger.system("info", "Removed stale git index.lock", { path, ageMs });
982
+ if (state === "cleared") {
983
+ attentionEvents.remove(id);
984
+ bumpAttention();
985
+ }
986
+ });
957
987
  wireAttentionNotifications(attentionEvents, bumpAttention);
958
988
  const recordRemoteFallbackAttention = (taskId, detail) => {
959
989
  const at = new Date().toISOString();
@@ -206,15 +206,7 @@ export declare class TaskWatchdog {
206
206
  */
207
207
  private escalateToNeedsInput;
208
208
  private readCurrent;
209
- /**
210
- * #0974: a check that is still waiting — pressure guard, a heavy slot, the
211
- * git queue — after ten minutes is surfaced as needs_input instead of
212
- * hanging silently (the #0970 incident: 26 minutes behind a stale pressure
213
- * guard with nothing on the card). The gates write queue markers while they
214
- * wait; this scan reads them. The flag clears itself once the marker is
215
- * gone (the wait ended and the check is actually running) or on the next
216
- * engineer run — it is informational, not a permanent failure.
217
- */
209
+ /** Queue markers are non-blocking waiting state. Only broken waits need input. */
218
210
  private escalateStaleQueueWaits;
219
211
  private escalateCheckWait;
220
212
  private clearCheckWaitEscalation;