@telora/daemon 0.20.32 → 0.20.53

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 (202) hide show
  1. package/build-info.json +2 -2
  2. package/dist/agent-state.d.ts +4 -6
  3. package/dist/agent-state.d.ts.map +1 -1
  4. package/dist/agent-state.js +4 -10
  5. package/dist/agent-state.js.map +1 -1
  6. package/dist/branch-status.d.ts +25 -3
  7. package/dist/branch-status.d.ts.map +1 -1
  8. package/dist/branch-status.js +130 -59
  9. package/dist/branch-status.js.map +1 -1
  10. package/dist/completion/completion-decision.d.ts +29 -3
  11. package/dist/completion/completion-decision.d.ts.map +1 -1
  12. package/dist/completion/completion-decision.js +15 -3
  13. package/dist/completion/completion-decision.js.map +1 -1
  14. package/dist/completion/event-entry-phases.d.ts +57 -0
  15. package/dist/completion/event-entry-phases.d.ts.map +1 -0
  16. package/dist/completion/event-entry-phases.js +205 -0
  17. package/dist/completion/event-entry-phases.js.map +1 -0
  18. package/dist/completion/event-phases.d.ts +14 -42
  19. package/dist/completion/event-phases.d.ts.map +1 -1
  20. package/dist/completion/event-phases.js +63 -190
  21. package/dist/completion/event-phases.js.map +1 -1
  22. package/dist/completion/event.d.ts +11 -1
  23. package/dist/completion/event.d.ts.map +1 -1
  24. package/dist/completion/event.js +5 -2
  25. package/dist/completion/event.js.map +1 -1
  26. package/dist/completion/index.d.ts +0 -3
  27. package/dist/completion/index.d.ts.map +1 -1
  28. package/dist/completion/index.js +0 -3
  29. package/dist/completion/index.js.map +1 -1
  30. package/dist/completion/review-exit-phase.d.ts +2 -1
  31. package/dist/completion/review-exit-phase.d.ts.map +1 -1
  32. package/dist/completion/review-exit-phase.js +41 -1
  33. package/dist/completion/review-exit-phase.js.map +1 -1
  34. package/dist/completion/status-advance-phase.d.ts +2 -0
  35. package/dist/completion/status-advance-phase.d.ts.map +1 -1
  36. package/dist/completion/status-advance-phase.js +14 -28
  37. package/dist/completion/status-advance-phase.js.map +1 -1
  38. package/dist/completion/team-completion.js +1 -1
  39. package/dist/completion/team-completion.js.map +1 -1
  40. package/dist/completion/terminated-work-reconcile.d.ts.map +1 -1
  41. package/dist/completion/terminated-work-reconcile.js +10 -0
  42. package/dist/completion/terminated-work-reconcile.js.map +1 -1
  43. package/dist/constants.d.ts +0 -8
  44. package/dist/constants.d.ts.map +1 -1
  45. package/dist/constants.js +0 -8
  46. package/dist/constants.js.map +1 -1
  47. package/dist/delivery-close-actions.d.ts +39 -0
  48. package/dist/delivery-close-actions.d.ts.map +1 -0
  49. package/dist/delivery-close-actions.js +53 -0
  50. package/dist/delivery-close-actions.js.map +1 -0
  51. package/dist/delivery-lifecycle.d.ts +11 -0
  52. package/dist/delivery-lifecycle.d.ts.map +1 -1
  53. package/dist/delivery-lifecycle.js +13 -14
  54. package/dist/delivery-lifecycle.js.map +1 -1
  55. package/dist/delivery-reconciler.d.ts +18 -4
  56. package/dist/delivery-reconciler.d.ts.map +1 -1
  57. package/dist/delivery-reconciler.js +18 -4
  58. package/dist/delivery-reconciler.js.map +1 -1
  59. package/dist/directive-executor.d.ts +1 -1
  60. package/dist/directive-executor.js +1 -1
  61. package/dist/discovery-fetch.d.ts +76 -0
  62. package/dist/discovery-fetch.d.ts.map +1 -0
  63. package/dist/discovery-fetch.js +206 -0
  64. package/dist/discovery-fetch.js.map +1 -0
  65. package/dist/discovery-poll.d.ts +1 -51
  66. package/dist/discovery-poll.d.ts.map +1 -1
  67. package/dist/discovery-poll.js +10 -192
  68. package/dist/discovery-poll.js.map +1 -1
  69. package/dist/drain-teardown.d.ts +11 -6
  70. package/dist/drain-teardown.d.ts.map +1 -1
  71. package/dist/drain-teardown.js +3 -2
  72. package/dist/drain-teardown.js.map +1 -1
  73. package/dist/focus-branch-ahead.d.ts +42 -0
  74. package/dist/focus-branch-ahead.d.ts.map +1 -0
  75. package/dist/focus-branch-ahead.js +45 -0
  76. package/dist/focus-branch-ahead.js.map +1 -0
  77. package/dist/focus-engine.d.ts.map +1 -1
  78. package/dist/focus-engine.js +21 -43
  79. package/dist/focus-engine.js.map +1 -1
  80. package/dist/focus-executor.js +2 -2
  81. package/dist/focus-executor.js.map +1 -1
  82. package/dist/focus-lifecycle.d.ts.map +1 -1
  83. package/dist/focus-lifecycle.js +32 -3
  84. package/dist/focus-lifecycle.js.map +1 -1
  85. package/dist/focus-loop.d.ts +34 -12
  86. package/dist/focus-loop.d.ts.map +1 -1
  87. package/dist/focus-loop.js +78 -15
  88. package/dist/focus-loop.js.map +1 -1
  89. package/dist/focus-merge.d.ts.map +1 -1
  90. package/dist/focus-merge.js +6 -1
  91. package/dist/focus-merge.js.map +1 -1
  92. package/dist/focus-phase.d.ts +18 -3
  93. package/dist/focus-phase.d.ts.map +1 -1
  94. package/dist/focus-phase.js +85 -31
  95. package/dist/focus-phase.js.map +1 -1
  96. package/dist/focus-prompt-builder.d.ts.map +1 -1
  97. package/dist/focus-prompt-builder.js +8 -0
  98. package/dist/focus-prompt-builder.js.map +1 -1
  99. package/dist/focus-provisioning.d.ts.map +1 -1
  100. package/dist/focus-provisioning.js +4 -0
  101. package/dist/focus-provisioning.js.map +1 -1
  102. package/dist/focus-stage-lifecycle/planning-release.d.ts +35 -17
  103. package/dist/focus-stage-lifecycle/planning-release.d.ts.map +1 -1
  104. package/dist/focus-stage-lifecycle/planning-release.js +53 -22
  105. package/dist/focus-stage-lifecycle/planning-release.js.map +1 -1
  106. package/dist/focus-worktree-state.d.ts +17 -6
  107. package/dist/focus-worktree-state.d.ts.map +1 -1
  108. package/dist/focus-worktree-state.js +21 -11
  109. package/dist/focus-worktree-state.js.map +1 -1
  110. package/dist/guard-engine.d.ts +1 -2
  111. package/dist/guard-engine.d.ts.map +1 -1
  112. package/dist/guard-engine.js +1 -2
  113. package/dist/guard-engine.js.map +1 -1
  114. package/dist/health-events.d.ts +10 -0
  115. package/dist/health-events.d.ts.map +1 -0
  116. package/dist/health-events.js +10 -0
  117. package/dist/health-events.js.map +1 -0
  118. package/dist/heartbeat.d.ts.map +1 -1
  119. package/dist/heartbeat.js +98 -21
  120. package/dist/heartbeat.js.map +1 -1
  121. package/dist/host-control/catalog.d.ts.map +1 -1
  122. package/dist/host-control/catalog.js +4 -1
  123. package/dist/host-control/catalog.js.map +1 -1
  124. package/dist/host-control/client.d.ts +3 -4
  125. package/dist/host-control/client.d.ts.map +1 -1
  126. package/dist/host-control/client.js +4 -5
  127. package/dist/host-control/client.js.map +1 -1
  128. package/dist/listener-auto-advance.d.ts +2 -1
  129. package/dist/listener-auto-advance.d.ts.map +1 -1
  130. package/dist/listener-auto-advance.js +36 -40
  131. package/dist/listener-auto-advance.js.map +1 -1
  132. package/dist/listener.d.ts +2 -7
  133. package/dist/listener.d.ts.map +1 -1
  134. package/dist/listener.js +11 -9
  135. package/dist/listener.js.map +1 -1
  136. package/dist/operator-hold.d.ts +66 -0
  137. package/dist/operator-hold.d.ts.map +1 -0
  138. package/dist/operator-hold.js +99 -0
  139. package/dist/operator-hold.js.map +1 -0
  140. package/dist/poll-trigger.d.ts +28 -0
  141. package/dist/poll-trigger.d.ts.map +1 -0
  142. package/dist/poll-trigger.js +31 -0
  143. package/dist/poll-trigger.js.map +1 -0
  144. package/dist/prompt-listing.js +6 -0
  145. package/dist/prompt-listing.js.map +1 -1
  146. package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
  147. package/dist/prompt-sections/execution-sections.js +24 -0
  148. package/dist/prompt-sections/execution-sections.js.map +1 -1
  149. package/dist/queries/context-lock-queries.d.ts.map +1 -1
  150. package/dist/queries/context-lock-queries.js +4 -1
  151. package/dist/queries/context-lock-queries.js.map +1 -1
  152. package/dist/queries/deliveries.d.ts +17 -0
  153. package/dist/queries/deliveries.d.ts.map +1 -1
  154. package/dist/queries/deliveries.js.map +1 -1
  155. package/dist/queries/issues.d.ts +98 -0
  156. package/dist/queries/issues.d.ts.map +1 -1
  157. package/dist/queries/issues.js +135 -0
  158. package/dist/queries/issues.js.map +1 -1
  159. package/dist/queries/schemas.d.ts +3 -3
  160. package/dist/spawn/worktree-session.d.ts.map +1 -1
  161. package/dist/spawn/worktree-session.js +2 -0
  162. package/dist/spawn/worktree-session.js.map +1 -1
  163. package/dist/spawner/index.d.ts +7 -9
  164. package/dist/spawner/index.d.ts.map +1 -1
  165. package/dist/spawner/index.js +13 -11
  166. package/dist/spawner/index.js.map +1 -1
  167. package/dist/spawner/lifecycle.d.ts +1 -18
  168. package/dist/spawner/lifecycle.d.ts.map +1 -1
  169. package/dist/spawner/lifecycle.js +1 -54
  170. package/dist/spawner/lifecycle.js.map +1 -1
  171. package/dist/spawner/liveness.d.ts +5 -14
  172. package/dist/spawner/liveness.d.ts.map +1 -1
  173. package/dist/spawner/liveness.js +5 -67
  174. package/dist/spawner/liveness.js.map +1 -1
  175. package/dist/spawner/state.d.ts +8 -20
  176. package/dist/spawner/state.d.ts.map +1 -1
  177. package/dist/spawner/state.js +8 -34
  178. package/dist/spawner/state.js.map +1 -1
  179. package/dist/types/focus.d.ts +9 -0
  180. package/dist/types/focus.d.ts.map +1 -1
  181. package/dist/types/session.d.ts +0 -44
  182. package/dist/types/session.d.ts.map +1 -1
  183. package/dist/unified-shell.d.ts.map +1 -1
  184. package/dist/unified-shell.js +8 -0
  185. package/dist/unified-shell.js.map +1 -1
  186. package/package.json +1 -2
  187. package/dist/completion/agent-completion.d.ts +0 -48
  188. package/dist/completion/agent-completion.d.ts.map +0 -1
  189. package/dist/completion/agent-completion.js +0 -205
  190. package/dist/completion/agent-completion.js.map +0 -1
  191. package/dist/delivery-guards.d.ts +0 -41
  192. package/dist/delivery-guards.d.ts.map +0 -1
  193. package/dist/delivery-guards.js +0 -200
  194. package/dist/delivery-guards.js.map +0 -1
  195. package/dist/spawner/spawn-team.d.ts +0 -20
  196. package/dist/spawner/spawn-team.d.ts.map +0 -1
  197. package/dist/spawner/spawn-team.js +0 -19
  198. package/dist/spawner/spawn-team.js.map +0 -1
  199. package/dist/spawner/timeout.d.ts +0 -32
  200. package/dist/spawner/timeout.d.ts.map +0 -1
  201. package/dist/spawner/timeout.js +0 -126
  202. package/dist/spawner/timeout.js.map +0 -1
@@ -10,59 +10,9 @@
10
10
  * Runs once per focus-poll cycle, org-scoped, best-effort: a failure here must
11
11
  * never disturb the spawn/merge poll work it rides alongside.
12
12
  */
13
- /** Time-box a single endpoint fetch so an unreachable host fails fast to error. */
14
- export declare const DISCOVERY_FETCH_TIMEOUT_MS = 5000;
15
- /**
16
- * Hard ceiling on a discovery response body. A `/models` listing is tiny; this
17
- * cap defends against a malicious endpoint streaming an unbounded body to OOM
18
- * the daemon (TEL-6). Enforced both via the declared Content-Length and a
19
- * streamed byte ceiling so a lying header can't slip past.
20
- */
21
- export declare const MAX_DISCOVERY_BYTES = 1000000;
13
+ export { isAllowedDiscoveryUrl, normalizeModelList, buildDiscoveryHeaders, fetchModels, DISCOVERY_FETCH_TIMEOUT_MS, MAX_DISCOVERY_BYTES, DISCOVERY_ERROR_STATUS, DISCOVERY_ERROR_UNREACHABLE, DISCOVERY_ERROR_INVALID, DISCOVERY_ERROR_AUTH, } from './discovery-fetch.js';
22
14
  /** Number of claimed discovery requests fetched concurrently per cycle (TEL-7). */
23
15
  export declare const DISCOVERY_FETCH_CONCURRENCY = 4;
24
- /** Generic, address-free error strings written back to the DB (TEL-5). The raw
25
- * detail (URL, system error) is logged daemon-side only -- the DB-written value
26
- * is readable by org members and must not leak a probe oracle. */
27
- export declare const DISCOVERY_ERROR_STATUS = "Endpoint returned an error status";
28
- export declare const DISCOVERY_ERROR_UNREACHABLE = "Endpoint unreachable or timed out";
29
- export declare const DISCOVERY_ERROR_INVALID = "Endpoint response was not valid";
30
- /** A 401/403 probe result. Distinct from the generic status message so an
31
- * authenticated org admin operating their own endpoint sees the one actionable
32
- * cause -- it's auth, fix the key -- instead of the address-free catch-all. Stays
33
- * address-free itself (no URL/IP/system detail): the probe-oracle defense (TEL-5)
34
- * is preserved; only the failure *category* is disclosed, never the endpoint. */
35
- export declare const DISCOVERY_ERROR_AUTH = "Authentication failed -- check the API key";
36
- /**
37
- * True only for a base_url that parses as a URL whose protocol is exactly
38
- * `http:` or `https:`. Rejects `file:`, `ftp:`, `gopher:`, non-URL strings, and
39
- * empty -- the SSRF guard predicate (TEL-5). Pure + exported for unit testing.
40
- */
41
- export declare function isAllowedDiscoveryUrl(raw: string): boolean;
42
- /**
43
- * Extract served model id strings from an OpenAI-compatible `/models` payload
44
- * (`{ object: 'list', data: [{ id }, ...] }`). Pure + defensive: tolerates a
45
- * missing/!array `data`, non-object items, and non-string/blank ids, returning
46
- * a de-duplicated, trimmed string[] (possibly empty). Exported for unit testing.
47
- */
48
- export declare function normalizeModelList(payload: unknown): string[];
49
- /**
50
- * Build the request headers for a discovery fetch (D8). When a bearer key VALUE
51
- * is provided (resolved server-side from the Telora-stored discovery secret), an
52
- * `Authorization: Bearer <value>` header is added so an auth-protected endpoint
53
- * (the common case) lists its models instead of 401-ing. A blank/absent value
54
- * yields no auth header (unauthenticated probe). Pure + exported for unit
55
- * testing; the value is never persisted or logged.
56
- */
57
- export declare function buildDiscoveryHeaders(apiKey: string | null | undefined): Record<string, string>;
58
- /**
59
- * Fetch + normalize an endpoint's model list. Throws a `DiscoveryError` whose
60
- * `dbMessage` is a generic, address-free string (TEL-5/TEL-6) -- the caller
61
- * writes that to the DB and logs the rich `.message` daemon-side only. When a
62
- * bearer key VALUE is provided (resolved from the Telora discovery secret), an
63
- * Authorization bearer is sent (D8).
64
- */
65
- export declare function fetchModels(baseUrl: string, apiKey?: string | null): Promise<string[]>;
66
16
  /**
67
17
  * Claim and resolve all of the org's pending discovery requests. Best-effort:
68
18
  * a claim failure logs and returns; per-request fetches run with bounded
@@ -1 +1 @@
1
- {"version":3,"file":"discovery-poll.d.ts","sourceRoot":"","sources":["../src/discovery-poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAaH,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,UAAY,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C;;mEAEmE;AACnE,eAAO,MAAM,sBAAsB,sCAAsC,CAAC;AAC1E,eAAO,MAAM,2BAA2B,sCAAsC,CAAC;AAC/E,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AACzE;;;;kFAIkF;AAClF,eAAO,MAAM,oBAAoB,+CAA+C,CAAC;AAEjF;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ1D;AAkBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAc7D;AA8CD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAChC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKxB;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAqD5F;AAwDD;;;;;GAKG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAoB5D"}
1
+ {"version":3,"file":"discovery-poll.d.ts","sourceRoot":"","sources":["../src/discovery-poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiBH,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,qBAAqB,EACrB,WAAW,EACX,0BAA0B,EAC1B,mBAAmB,EACnB,sBAAsB,EACtB,2BAA2B,EAC3B,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAI9B,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAwD7C;;;;;GAKG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAoB5D"}
@@ -11,202 +11,20 @@
11
11
  * never disturb the spawn/merge poll work it rides alongside.
12
12
  */
13
13
  import { getPendingDiscoveryRequests, resolveDiscoveryRequest, pruneDiscoveryRequests, } from './queries/local-models.js';
14
- import { readLoadableCatalog } from './host-control/index.js';
14
+ // Import readLoadableCatalog from its defining module (host-control/catalog.ts),
15
+ // NOT the host-control/index.js barrel: the barrel re-exports members that
16
+ // import back from this file, so routing through it would close a static cycle.
17
+ import { readLoadableCatalog } from './host-control/catalog.js';
15
18
  import { createLogger } from './log.js';
19
+ // The `/v1/models` fetch primitives live in discovery-fetch.ts (a leaf module
20
+ // shared with host-control's catalog floor). Import what this orchestration
21
+ // layer uses directly, and re-export the full public surface for back-compat:
22
+ // existing importers and tests read these symbols from './discovery-poll.js'.
23
+ import { DiscoveryError, DISCOVERY_ERROR_UNREACHABLE } from './discovery-fetch.js';
24
+ export { isAllowedDiscoveryUrl, normalizeModelList, buildDiscoveryHeaders, fetchModels, DISCOVERY_FETCH_TIMEOUT_MS, MAX_DISCOVERY_BYTES, DISCOVERY_ERROR_STATUS, DISCOVERY_ERROR_UNREACHABLE, DISCOVERY_ERROR_INVALID, DISCOVERY_ERROR_AUTH, } from './discovery-fetch.js';
16
25
  const log = createLogger({ module: 'discovery' });
17
- /** Time-box a single endpoint fetch so an unreachable host fails fast to error. */
18
- export const DISCOVERY_FETCH_TIMEOUT_MS = 5000;
19
- /**
20
- * Hard ceiling on a discovery response body. A `/models` listing is tiny; this
21
- * cap defends against a malicious endpoint streaming an unbounded body to OOM
22
- * the daemon (TEL-6). Enforced both via the declared Content-Length and a
23
- * streamed byte ceiling so a lying header can't slip past.
24
- */
25
- export const MAX_DISCOVERY_BYTES = 1_000_000;
26
26
  /** Number of claimed discovery requests fetched concurrently per cycle (TEL-7). */
27
27
  export const DISCOVERY_FETCH_CONCURRENCY = 4;
28
- /** Generic, address-free error strings written back to the DB (TEL-5). The raw
29
- * detail (URL, system error) is logged daemon-side only -- the DB-written value
30
- * is readable by org members and must not leak a probe oracle. */
31
- export const DISCOVERY_ERROR_STATUS = 'Endpoint returned an error status';
32
- export const DISCOVERY_ERROR_UNREACHABLE = 'Endpoint unreachable or timed out';
33
- export const DISCOVERY_ERROR_INVALID = 'Endpoint response was not valid';
34
- /** A 401/403 probe result. Distinct from the generic status message so an
35
- * authenticated org admin operating their own endpoint sees the one actionable
36
- * cause -- it's auth, fix the key -- instead of the address-free catch-all. Stays
37
- * address-free itself (no URL/IP/system detail): the probe-oracle defense (TEL-5)
38
- * is preserved; only the failure *category* is disclosed, never the endpoint. */
39
- export const DISCOVERY_ERROR_AUTH = 'Authentication failed -- check the API key';
40
- /**
41
- * True only for a base_url that parses as a URL whose protocol is exactly
42
- * `http:` or `https:`. Rejects `file:`, `ftp:`, `gopher:`, non-URL strings, and
43
- * empty -- the SSRF guard predicate (TEL-5). Pure + exported for unit testing.
44
- */
45
- export function isAllowedDiscoveryUrl(raw) {
46
- let parsed;
47
- try {
48
- parsed = new URL(raw);
49
- }
50
- catch {
51
- return false;
52
- }
53
- return parsed.protocol === 'http:' || parsed.protocol === 'https:';
54
- }
55
- /**
56
- * A daemon-side error carrying a sanitized, address-free message for the DB.
57
- * `fetchModels` throws these so the catch can write `dbMessage` verbatim while
58
- * still logging the rich `message` daemon-side.
59
- */
60
- class DiscoveryError extends Error {
61
- dbMessage;
62
- constructor(message,
63
- /** Generic string safe to write to the org-readable `error` column. */
64
- dbMessage) {
65
- super(message);
66
- this.dbMessage = dbMessage;
67
- this.name = 'DiscoveryError';
68
- }
69
- }
70
- /**
71
- * Extract served model id strings from an OpenAI-compatible `/models` payload
72
- * (`{ object: 'list', data: [{ id }, ...] }`). Pure + defensive: tolerates a
73
- * missing/!array `data`, non-object items, and non-string/blank ids, returning
74
- * a de-duplicated, trimmed string[] (possibly empty). Exported for unit testing.
75
- */
76
- export function normalizeModelList(payload) {
77
- if (!payload || typeof payload !== 'object')
78
- return [];
79
- const data = payload.data;
80
- if (!Array.isArray(data))
81
- return [];
82
- const seen = new Set();
83
- for (const item of data) {
84
- if (!item || typeof item !== 'object')
85
- continue;
86
- const id = item.id;
87
- if (typeof id === 'string') {
88
- const trimmed = id.trim();
89
- if (trimmed)
90
- seen.add(trimmed);
91
- }
92
- }
93
- return [...seen];
94
- }
95
- /**
96
- * Read a fetch response body with a hard byte ceiling, aborting once the cap is
97
- * exceeded so a lying Content-Length (or a chunked body with none) can't OOM the
98
- * daemon (TEL-6). Returns the decoded text. Throws past the ceiling.
99
- */
100
- async function readBodyCapped(res) {
101
- const body = res.body;
102
- if (!body) {
103
- // No stream available (shouldn't happen on Node fetch) -- fall back but still
104
- // cap by post-read byteLength.
105
- const buf = await res.arrayBuffer();
106
- if (buf.byteLength > MAX_DISCOVERY_BYTES) {
107
- throw new DiscoveryError('Response body exceeded byte ceiling', DISCOVERY_ERROR_INVALID);
108
- }
109
- return new TextDecoder().decode(buf);
110
- }
111
- const reader = body.getReader();
112
- const chunks = [];
113
- let total = 0;
114
- try {
115
- for (;;) {
116
- const { done, value } = await reader.read();
117
- if (done)
118
- break;
119
- if (value) {
120
- total += value.byteLength;
121
- if (total > MAX_DISCOVERY_BYTES) {
122
- throw new DiscoveryError('Response body exceeded byte ceiling', DISCOVERY_ERROR_INVALID);
123
- }
124
- chunks.push(value);
125
- }
126
- }
127
- }
128
- finally {
129
- // Stop the underlying transfer; ignore cancel errors.
130
- await reader.cancel().catch((err) => { log.debug('SSE reader cancel failed', { error: String(err?.message ?? err) }); });
131
- }
132
- const merged = new Uint8Array(total);
133
- let offset = 0;
134
- for (const c of chunks) {
135
- merged.set(c, offset);
136
- offset += c.byteLength;
137
- }
138
- return new TextDecoder().decode(merged);
139
- }
140
- /**
141
- * Build the request headers for a discovery fetch (D8). When a bearer key VALUE
142
- * is provided (resolved server-side from the Telora-stored discovery secret), an
143
- * `Authorization: Bearer <value>` header is added so an auth-protected endpoint
144
- * (the common case) lists its models instead of 401-ing. A blank/absent value
145
- * yields no auth header (unauthenticated probe). Pure + exported for unit
146
- * testing; the value is never persisted or logged.
147
- */
148
- export function buildDiscoveryHeaders(apiKey) {
149
- const headers = { accept: 'application/json' };
150
- const value = typeof apiKey === 'string' ? apiKey.trim() : '';
151
- if (value)
152
- headers.Authorization = `Bearer ${value}`;
153
- return headers;
154
- }
155
- /**
156
- * Fetch + normalize an endpoint's model list. Throws a `DiscoveryError` whose
157
- * `dbMessage` is a generic, address-free string (TEL-5/TEL-6) -- the caller
158
- * writes that to the DB and logs the rich `.message` daemon-side only. When a
159
- * bearer key VALUE is provided (resolved from the Telora discovery secret), an
160
- * Authorization bearer is sent (D8).
161
- */
162
- export async function fetchModels(baseUrl, apiKey) {
163
- // TEL-5: validate the operator-supplied base_url before any network call --
164
- // reject anything that isn't a parseable http/https URL (no file:, ftp:, etc).
165
- if (!isAllowedDiscoveryUrl(baseUrl)) {
166
- throw new DiscoveryError(`Rejected non-http(s) discovery URL: ${baseUrl}`, DISCOVERY_ERROR_INVALID);
167
- }
168
- // base_url already includes /v1 (e.g. http://host:8888/v1); append /models.
169
- const url = `${baseUrl.replace(/\/+$/, '')}/models`;
170
- const controller = new AbortController();
171
- const timer = setTimeout(() => controller.abort(), DISCOVERY_FETCH_TIMEOUT_MS);
172
- try {
173
- const res = await fetch(url, { signal: controller.signal, headers: buildDiscoveryHeaders(apiKey) });
174
- if (!res.ok) {
175
- // 401/403: disclose the failure CATEGORY (auth) so an authenticated operator
176
- // can act, while keeping the message address-free (TEL-5). All other non-2xx
177
- // codes stay the generic catch-all so we never leak which path/host exists.
178
- const dbMessage = res.status === 401 || res.status === 403 ? DISCOVERY_ERROR_AUTH : DISCOVERY_ERROR_STATUS;
179
- throw new DiscoveryError(`Endpoint returned HTTP ${res.status}`, dbMessage);
180
- }
181
- // TEL-6: reject a too-large declared body up front...
182
- const declared = Number(res.headers.get('content-length'));
183
- if (Number.isFinite(declared) && declared > MAX_DISCOVERY_BYTES) {
184
- throw new DiscoveryError(`Declared Content-Length ${declared} exceeds cap`, DISCOVERY_ERROR_INVALID);
185
- }
186
- // ...and defensively cap the actual stream in case the header lied / is absent.
187
- const text = await readBodyCapped(res);
188
- let json;
189
- try {
190
- json = JSON.parse(text);
191
- }
192
- catch {
193
- throw new DiscoveryError('Response body was not valid JSON', DISCOVERY_ERROR_INVALID);
194
- }
195
- return normalizeModelList(json);
196
- }
197
- catch (err) {
198
- if (err instanceof DiscoveryError)
199
- throw err;
200
- if (err.name === 'AbortError') {
201
- throw new DiscoveryError(`Timed out after ${DISCOVERY_FETCH_TIMEOUT_MS}ms reaching ${url}`, DISCOVERY_ERROR_UNREACHABLE);
202
- }
203
- // Network-level failure (DNS, connection refused, TLS): generic to the DB.
204
- throw new DiscoveryError(err.message || `Failed to reach ${url}`, DISCOVERY_ERROR_UNREACHABLE);
205
- }
206
- finally {
207
- clearTimeout(timer);
208
- }
209
- }
210
28
  /**
211
29
  * Process one claimed discovery request: fetch + normalize, then write back its
212
30
  * own loaded/error result. Never throws -- one request's failure must not abort
@@ -1 +1 @@
1
- {"version":3,"file":"discovery-poll.js","sourceRoot":"","sources":["../src/discovery-poll.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"discovery-poll.js","sourceRoot":"","sources":["../src/discovery-poll.ts"],"names":[],"mappings":"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"}
@@ -33,13 +33,18 @@ export interface DrainToIdleOptions {
33
33
  /** Poll cadence for the idle gate (ms). Default 1000. */
34
34
  pollIntervalMs?: number;
35
35
  /**
36
- * Optional safety cap (ms). When set and the fleet has not idled within it,
37
- * the teardown is FORCED with a loud warning -- an operator-set bound that
38
- * opts into a forced stop after waiting. Unset (default) waits indefinitely;
39
- * the fleet is never killed unless `immediate` is passed. The daemon runs
40
- * this in the background, so an indefinite wait never blocks the CLI.
36
+ * REQUIRED deadline for the drain wait -- an unbounded wait must be chosen
37
+ * explicitly, never defaulted into. Either a positive safety cap in ms (when
38
+ * the fleet has not idled within it the teardown is FORCED with a loud
39
+ * warning -- an operator-set bound that opts into a forced stop after
40
+ * waiting), or the literal `'unbounded'` to wait indefinitely for the fleet
41
+ * to finish work naturally. `'unbounded'` is legitimate ONLY where a separate
42
+ * escalation can still force teardown (the stop path upgrades a second signal
43
+ * to an immediate `shutdown()`); the daemon runs this in the background so an
44
+ * indefinite wait never blocks the CLI. Making this non-optional keeps an
45
+ * accidental silent-unbounded wait from being expressible.
41
46
  */
42
- maxWaitMs?: number;
47
+ maxWaitMs: number | 'unbounded';
43
48
  /** Logical clock (ms), injectable for tests. Default Date.now. */
44
49
  now?: () => number;
45
50
  /** Logger seam. Defaults to console.log. */
@@ -1 +1 @@
1
- {"version":3,"file":"drain-teardown.d.ts","sourceRoot":"","sources":["../src/drain-teardown.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,kEAAkE;AAClE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,eAAe,EAAE,MAAM,OAAO,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,kFAAkF;IAClF,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,0CAA0C;IAC1C,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,4CAA4C;IAC5C,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,gEAAgE;AAChE,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,aAAa,EAAE,OAAO,CAAC;IACvB,qDAAqD;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwCjG"}
1
+ {"version":3,"file":"drain-teardown.d.ts","sourceRoot":"","sources":["../src/drain-teardown.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,kEAAkE;AAClE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,eAAe,EAAE,MAAM,OAAO,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,iFAAiF;IACjF,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,kFAAkF;IAClF,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,0CAA0C;IAC1C,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;OAWG;IACH,SAAS,EAAE,MAAM,GAAG,WAAW,CAAC;IAChC,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,4CAA4C;IAC5C,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,gEAAgE;AAChE,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,aAAa,EAAE,OAAO,CAAC;IACvB,qDAAqD;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAyCjG"}
@@ -23,11 +23,12 @@ export async function runDrainToIdleTeardown(opts) {
23
23
  const startedAt = now();
24
24
  let timedOut = false;
25
25
  // Fast path: already idle.
26
+ const cap = opts.maxWaitMs;
26
27
  while (!opts.isBatchSafeIdle()) {
27
28
  const waited = now() - startedAt;
28
- if (opts.maxWaitMs !== undefined && waited >= opts.maxWaitMs) {
29
+ if (cap !== 'unbounded' && waited >= cap) {
29
30
  timedOut = true;
30
- log(`[drain-teardown] WARNING: fleet did not reach a zero-team boundary within ${Math.round(opts.maxWaitMs / 1000)}s ` +
31
+ log(`[drain-teardown] WARNING: fleet did not reach a zero-team boundary within ${Math.round(cap / 1000)}s ` +
31
32
  '-- forcing teardown now (operator-set cap)');
32
33
  break;
33
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"drain-teardown.js","sourceRoot":"","sources":["../src/drain-teardown.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,MAAM,IAAI,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;AA8DxD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,IAAwB;IACnE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACjC,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC;IAEnD,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,GAAG,CAAC,6DAA6D,CAAC,CAAC;QACnE,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAChF,CAAC;IAED,2EAA2E;IAC3E,qCAAqC;IACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;IAChB,GAAG,CAAC,iFAAiF,CAAC,CAAC;IAEvF,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC;IACxB,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,2BAA2B;IAC3B,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,GAAG,EAAE,GAAG,SAAS,CAAC;QACjC,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,MAAM,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC7D,QAAQ,GAAG,IAAI,CAAC;YAChB,GAAG,CACD,6EAA6E,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI;gBAChH,4CAA4C,CAC/C,CAAC;YACF,MAAM;QACR,CAAC;QACD,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC;IAC9B,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,EAAE,GAAG,SAAS,CAAC;IACnC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,GAAG,CAAC,oCAAoC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,iDAAiD,CAAC,CAAC;IACxH,CAAC;IACD,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AACvE,CAAC"}
1
+ {"version":3,"file":"drain-teardown.js","sourceRoot":"","sources":["../src/drain-teardown.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,MAAM,IAAI,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAmExD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,IAAwB;IACnE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACjC,MAAM,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC;IAEnD,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,GAAG,CAAC,6DAA6D,CAAC,CAAC;QACnE,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAChF,CAAC;IAED,2EAA2E;IAC3E,qCAAqC;IACrC,IAAI,CAAC,QAAQ,EAAE,CAAC;IAChB,GAAG,CAAC,iFAAiF,CAAC,CAAC;IAEvF,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC;IACxB,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,2BAA2B;IAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;IAC3B,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;QAC/B,MAAM,MAAM,GAAG,GAAG,EAAE,GAAG,SAAS,CAAC;QACjC,IAAI,GAAG,KAAK,WAAW,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;YACzC,QAAQ,GAAG,IAAI,CAAC;YAChB,GAAG,CACD,6EAA6E,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI;gBACrG,4CAA4C,CAC/C,CAAC;YACF,MAAM;QACR,CAAC;QACD,MAAM,KAAK,CAAC,cAAc,CAAC,CAAC;IAC9B,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,EAAE,GAAG,SAAS,CAAC;IACnC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,GAAG,CAAC,oCAAoC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,iDAAiD,CAAC,CAAC;IACxH,CAAC;IACD,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACtB,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AACvE,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Landed-work check for focus completion.
3
+ *
4
+ * A focus reads complete only when its agent work has measurably LANDED where a
5
+ * human inspecting the tree will find it. The observed failure: an agent branch
6
+ * carried four substantive commits, was never merged, and the board still read
7
+ * complete because completion was inferred from delivery status, not from where
8
+ * the work actually landed.
9
+ *
10
+ * This reads branch state DETERMINISTICALLY from git (a standing daemon
11
+ * invariant -- never inferred from agent claims): true when the focus's agent
12
+ * branch carries commits not reachable from the product's integration branch.
13
+ * The focus-completion evaluate phase consults it and holds `complete` open while
14
+ * the branch is ahead; the merge-back loop lands the branch (the normal outcome)
15
+ * and files a stuck_focus_branch escalation for a genuinely-unmergeable one.
16
+ *
17
+ * @module focus-branch-ahead
18
+ */
19
+ import { getAllFocusWorktrees } from './focus-worktree-state.js';
20
+ import { branchHasUnmergedCommits } from './worktree/index.js';
21
+ import type { DaemonConfig } from './types.js';
22
+ /** Injected seam so the check is unit-testable without a real worktree/git. */
23
+ export interface FocusBranchAheadDeps {
24
+ getAllFocusWorktrees: typeof getAllFocusWorktrees;
25
+ branchHasUnmergedCommits: typeof branchHasUnmergedCommits;
26
+ }
27
+ /**
28
+ * True when the focus's agent branch carries commits not reachable from the
29
+ * product's integration branch -- i.e. its work has NOT landed.
30
+ *
31
+ * `productConfig` must be product-SCOPED (configForProduct) so integrationBranch
32
+ * and repoPath resolve to the focus's own product in a multi-product daemon.
33
+ *
34
+ * Returns false when the focus has no tracked worktree (nothing to land). The
35
+ * git primitive returns false on any git failure rather than throwing, so a git
36
+ * hiccup fails OPEN (allows complete) rather than wedging completion forever --
37
+ * the merge-back loop's stuck_focus_branch escalation is the durable backstop
38
+ * for work that genuinely cannot land, and the evaluate phase re-checks every
39
+ * tick so a transient false-open self-corrects.
40
+ */
41
+ export declare function focusBranchAhead(focusId: string, productConfig: DaemonConfig, deps?: FocusBranchAheadDeps): boolean;
42
+ //# sourceMappingURL=focus-branch-ahead.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-branch-ahead.d.ts","sourceRoot":"","sources":["../src/focus-branch-ahead.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,+EAA+E;AAC/E,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,OAAO,oBAAoB,CAAC;IAClD,wBAAwB,EAAE,OAAO,wBAAwB,CAAC;CAC3D;AAOD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,YAAY,EAC3B,IAAI,GAAE,oBAAkC,GACvC,OAAO,CAQT"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Landed-work check for focus completion.
3
+ *
4
+ * A focus reads complete only when its agent work has measurably LANDED where a
5
+ * human inspecting the tree will find it. The observed failure: an agent branch
6
+ * carried four substantive commits, was never merged, and the board still read
7
+ * complete because completion was inferred from delivery status, not from where
8
+ * the work actually landed.
9
+ *
10
+ * This reads branch state DETERMINISTICALLY from git (a standing daemon
11
+ * invariant -- never inferred from agent claims): true when the focus's agent
12
+ * branch carries commits not reachable from the product's integration branch.
13
+ * The focus-completion evaluate phase consults it and holds `complete` open while
14
+ * the branch is ahead; the merge-back loop lands the branch (the normal outcome)
15
+ * and files a stuck_focus_branch escalation for a genuinely-unmergeable one.
16
+ *
17
+ * @module focus-branch-ahead
18
+ */
19
+ import { getAllFocusWorktrees } from './focus-worktree-state.js';
20
+ import { branchHasUnmergedCommits } from './worktree/index.js';
21
+ const defaultDeps = {
22
+ getAllFocusWorktrees,
23
+ branchHasUnmergedCommits,
24
+ };
25
+ /**
26
+ * True when the focus's agent branch carries commits not reachable from the
27
+ * product's integration branch -- i.e. its work has NOT landed.
28
+ *
29
+ * `productConfig` must be product-SCOPED (configForProduct) so integrationBranch
30
+ * and repoPath resolve to the focus's own product in a multi-product daemon.
31
+ *
32
+ * Returns false when the focus has no tracked worktree (nothing to land). The
33
+ * git primitive returns false on any git failure rather than throwing, so a git
34
+ * hiccup fails OPEN (allows complete) rather than wedging completion forever --
35
+ * the merge-back loop's stuck_focus_branch escalation is the durable backstop
36
+ * for work that genuinely cannot land, and the evaluate phase re-checks every
37
+ * tick so a transient false-open self-corrects.
38
+ */
39
+ export function focusBranchAhead(focusId, productConfig, deps = defaultDeps) {
40
+ const wt = deps.getAllFocusWorktrees().get(focusId);
41
+ if (!wt)
42
+ return false;
43
+ return deps.branchHasUnmergedCommits(wt.branchName, productConfig.integrationBranch, productConfig.repoPath);
44
+ }
45
+ //# sourceMappingURL=focus-branch-ahead.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-branch-ahead.js","sourceRoot":"","sources":["../src/focus-branch-ahead.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAS/D,MAAM,WAAW,GAAyB;IACxC,oBAAoB;IACpB,wBAAwB;CACzB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAe,EACf,aAA2B,EAC3B,OAA6B,WAAW;IAExC,MAAM,EAAE,GAAG,IAAI,CAAC,oBAAoB,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IACtB,OAAO,IAAI,CAAC,wBAAwB,CAClC,EAAE,CAAC,UAAU,EACb,aAAa,CAAC,iBAAiB,EAC/B,aAAa,CAAC,QAAQ,CACvB,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"focus-engine.d.ts","sourceRoot":"","sources":["../src/focus-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAA2B,gBAAgB,EAAoC,MAAM,qBAAqB,CAAC;AAClH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAwD/C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAOT;AAMD,qBAAa,WAAY,YAAW,eAAe;IACjD,QAAQ,CAAC,IAAI,WAAW;IAExB,wCAAwC;IACxC,OAAO,CAAC,MAAM,CAA6B;IAE3C;;;;OAIG;IACH,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IAEzC;;;;OAIG;IACH,SAAS,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,EAAE,CAAM;IAEjD,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB,CAA6B;IAE1D;;;OAGG;IACH,WAAW,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI;IAUlC,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkEjE,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsEjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkGtB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;YA8C1C,wBAAwB;IA+BtC,IAAI,IAAI,IAAI;IAeN,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA2G/B,WAAW,IAAI,YAAY;IA0D3B,gBAAgB,IAAI,aAAa;IAwBjC;;;OAGG;IACH,OAAO,CAAC,aAAa;CAStB"}
1
+ {"version":3,"file":"focus-engine.d.ts","sourceRoot":"","sources":["../src/focus-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAA2B,gBAAgB,EAAoC,MAAM,qBAAqB,CAAC;AAClH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAwD/C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAOT;AAMD,qBAAa,WAAY,YAAW,eAAe;IACjD,QAAQ,CAAC,IAAI,WAAW;IAExB,wCAAwC;IACxC,OAAO,CAAC,MAAM,CAA6B;IAE3C;;;;OAIG;IACH,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAQ;IAEzC;;;;OAIG;IACH,SAAS,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,EAAE,CAAM;IAEjD,8EAA8E;IAC9E,OAAO,CAAC,qBAAqB,CAA6B;IAE1D;;;OAGG;IACH,WAAW,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI;IAUlC,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkEjE,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAsEjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2FtB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;YAuD1C,wBAAwB;IA+BtC,IAAI,IAAI,IAAI;IAeN,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA+F/B,WAAW,IAAI,YAAY;IAwC3B,gBAAgB,IAAI,aAAa;IAsBjC;;;OAGG;IACH,OAAO,CAAC,aAAa;CAStB"}
@@ -29,14 +29,14 @@ import { hydrateFromDb } from './focus-worktree-state.js';
29
29
  import { rebuildFocusWorktreeState, reconcileGitState } from './focus-lifecycle.js';
30
30
  import { pruneOldLogs } from './log-manager.js';
31
31
  import { getActiveTeams, terminateAllTeams, waitForTeamExit, initGovernor as initExecutorGovernor, } from './focus-executor.js';
32
- import { getRunningAgents } from './agent-state.js';
33
- import { terminateAllAgents, initGovernor as initSpawnerGovernor, checkAgentLiveness } from './spawner/index.js';
32
+ import { initGovernor as initSpawnerGovernor } from './spawner/index.js';
34
33
  import { updateSession } from './queries/sessions.js';
35
34
  import { getAllFocusWorktrees } from './focus-worktree-state.js';
36
35
  import { scanGitActivity } from './git/index.js';
37
36
  import { upsertGitActivity, getLatestGitActivityTimestamp } from './queries/git-activity.js';
38
37
  import { getActiveFocuses } from './queries/deliveries.js';
39
- import { runFocusLoopOnce } from './focus-loop.js';
38
+ import { runFocusLoopOnce, buildFocusLoopDeps } from './focus-loop.js';
39
+ import { focusBranchAhead } from './focus-branch-ahead.js';
40
40
  import { runAuditPhase } from './audit-phase.js';
41
41
  import { registerScanner } from './security-scan-engine.js';
42
42
  import { depsScanner } from './scanners/deps.js';
@@ -179,7 +179,7 @@ export class FocusEngine {
179
179
  // Hydrate worktree state from DB for all products (non-fatal per product)
180
180
  for (const product of config.products) {
181
181
  try {
182
- await hydrateFromDb(product.id);
182
+ await hydrateFromDb(product.id, product.repoPath);
183
183
  }
184
184
  catch (err) {
185
185
  log.warn(`Worktree state hydration failed for product ${product.id} (non-fatal):`, err instanceof Error ? err.message : String(err));
@@ -245,10 +245,6 @@ export class FocusEngine {
245
245
  await pruneOldLogs(pc);
246
246
  }
247
247
  }, 3_600_000, 'log-prune', { skipIfRunning: true }));
248
- // Periodic agent liveness check (60s)
249
- this.intervals.push(safeInterval(async () => {
250
- await checkAgentLiveness(config, getRunningAgents());
251
- }, 60_000, 'agent-liveness', { skipIfRunning: true }));
252
248
  // Periodic tracker-auth liveness (5 min): a token revoked WHILE running
253
249
  // must halt the daemon, not leave the poll loop hitting 401/403 forever.
254
250
  // Startup already validates once in init(); this re-checks at runtime and
@@ -316,7 +312,13 @@ export class FocusEngine {
316
312
  log.warn(`[focus-loop] scope-time audit refresh failed for focus ${focus.focus_id}:`, err.message);
317
313
  }
318
314
  try {
319
- await runFocusLoopOnce(focus.focus_id);
315
+ // Inject the landed-work check, product-scoped so a multi-product
316
+ // daemon resolves each focus's own integration branch/repo. The
317
+ // evaluate phase holds `complete` open while the focus branch is
318
+ // ahead of integration (unlanded work).
319
+ await runFocusLoopOnce(focus.focus_id, buildFocusLoopDeps({
320
+ isFocusBranchAhead: (fid) => focusBranchAhead(fid, configForProduct(config, product)),
321
+ }));
320
322
  }
321
323
  catch (err) {
322
324
  log.warn(`[focus-loop] tick failed for focus ${focus.focus_id}:`, err.message);
@@ -424,21 +426,14 @@ export class FocusEngine {
424
426
  await Promise.allSettled(updates);
425
427
  }
426
428
  }
427
- // Wait for running agents (legacy agents still in flight)
428
- const runningAgents = getRunningAgents();
429
- if (runningAgents.size > 0) {
430
- log.info(`Waiting for ${runningAgents.size} running agents to complete...`);
431
- // Give agents 30 seconds to reach a checkpoint
432
- const timeout = setTimeout(() => {
433
- log.info('Timeout reached, terminating remaining agents...');
434
- terminateAllAgents();
435
- }, 30_000);
436
- // Poll for completion
437
- while (getRunningAgents().size > 0) {
438
- await new Promise((resolve) => setTimeout(resolve, 1000));
439
- }
440
- clearTimeout(timeout);
441
- }
429
+ // NOTE: the former unbounded `while (getRunningAgents().size > 0)` legacy-agent
430
+ // drain loop was removed here. It polled a map that no live spawn path ever
431
+ // populated and had no deadline -- an agent that ignored SIGTERM would wedge
432
+ // shutdown forever (the PRIMARY FINDING of the bounded-teardown review). The
433
+ // only live fleet is `activeTeams`, drained above by the bounded
434
+ // `waitForTeamExit(engineDrainMs)` and SIGKILL-escalated by the single
435
+ // `reapWorkerFleet` backstop in unified-shell Phase 2.4. There is no
436
+ // unbounded wait left in shutdown().
442
437
  // Drain: allow pending async session updates to flush
443
438
  log.info('Draining pending session updates...');
444
439
  await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -449,8 +444,7 @@ export class FocusEngine {
449
444
  // -----------------------------------------------------------------------
450
445
  healthCheck() {
451
446
  const teams = getActiveTeams();
452
- const agents = getRunningAgents();
453
- const activeWorkItems = teams.size + agents.size;
447
+ const activeWorkItems = teams.size;
454
448
  // Determine overall status
455
449
  let status = 'healthy';
456
450
  if (this.config === null) {
@@ -469,26 +463,12 @@ export class FocusEngine {
469
463
  deliveryCount: team.knownDeliveryIds.size,
470
464
  });
471
465
  }
472
- const agentDetails = [];
473
- for (const agent of agents.values()) {
474
- const durationSec = Math.round((Date.now() - agent.startedAt.getTime()) / 1000);
475
- agentDetails.push({
476
- sessionId: agent.sessionId,
477
- deliveryName: agent.deliveryName,
478
- roleName: agent.roleName,
479
- pid: agent.pid,
480
- durationSec,
481
- streamState: agent.streamState,
482
- });
483
- }
484
466
  return {
485
467
  status,
486
468
  activeWorkItems,
487
469
  details: {
488
470
  activeTeams: teams.size,
489
- activeLegacyAgents: agents.size,
490
471
  teams: teamDetails,
491
- agents: agentDetails,
492
472
  },
493
473
  };
494
474
  }
@@ -497,16 +477,14 @@ export class FocusEngine {
497
477
  // -----------------------------------------------------------------------
498
478
  getResourceUsage() {
499
479
  const teams = getActiveTeams();
500
- const agents = getRunningAgents();
501
480
  const worktrees = getAllFocusWorktrees();
502
- // Active Claude processes: team lead processes + legacy agent processes
481
+ // Active Claude processes: team lead processes (the only live spawn fleet).
503
482
  let activeClaudeProcesses = 0;
504
483
  for (const team of teams.values()) {
505
484
  if (team.leadPid) {
506
485
  activeClaudeProcesses++;
507
486
  }
508
487
  }
509
- activeClaudeProcesses += agents.size;
510
488
  return {
511
489
  activeClaudeProcesses,
512
490
  activeWorktrees: worktrees.size,