harnery 0.45.2 → 0.46.1

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 (227) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/core/agents/index.d.ts +1 -0
  21. package/dist/core/agents/index.d.ts.map +1 -1
  22. package/dist/core/agents/index.js +1 -0
  23. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  24. package/dist/core/agents/live-authority-v3.js +11 -4
  25. package/dist/core/agents/render/session-context.d.ts +0 -6
  26. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  27. package/dist/core/agents/render/session-context.js +1 -6
  28. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  29. package/dist/core/agents/rules/claim-conflict.js +36 -16
  30. package/dist/core/agents/rules/stop-hook.d.ts +6 -0
  31. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  32. package/dist/core/agents/rules/stop-hook.js +20 -1
  33. package/dist/core/agents/session-events.d.ts.map +1 -1
  34. package/dist/core/agents/session-events.js +12 -4
  35. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  36. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  38. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  39. package/dist/core/agents/state/live-coordination-view.js +6 -2
  40. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  41. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  42. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  43. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  44. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  45. package/dist/core/agents/state/parent-reference.js +119 -0
  46. package/dist/core/config.d.ts +39 -0
  47. package/dist/core/config.d.ts.map +1 -1
  48. package/dist/core/config.js +63 -0
  49. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  50. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  51. package/dist/core/diagnostics/path-identity.js +51 -6
  52. package/dist/core/events/v3/canonical.js +43 -14
  53. package/dist/core/events/v3/index.d.ts +1 -1
  54. package/dist/core/events/v3/index.d.ts.map +1 -1
  55. package/dist/core/events/v3/index.js +1 -1
  56. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  57. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  58. package/dist/core/events/v3/producers/recorder.js +117 -13
  59. package/dist/core/events/v3/reader.d.ts.map +1 -1
  60. package/dist/core/events/v3/reader.js +53 -13
  61. package/dist/core/events/v3/validate.d.ts.map +1 -1
  62. package/dist/core/events/v3/validate.js +36 -1
  63. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  64. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  65. package/dist/core/hooks/adapter/native-parent.js +91 -0
  66. package/dist/core/hooks/cli.js +103 -5
  67. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  68. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  69. package/dist/core/hooks/resolve/owner.js +0 -23
  70. package/dist/core/hooks/resolve/transcript.d.ts +12 -0
  71. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  72. package/dist/core/hooks/resolve/transcript.js +54 -0
  73. package/dist/core/hooks/shell-admission.d.ts +84 -0
  74. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  75. package/dist/core/hooks/shell-admission.js +622 -0
  76. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  77. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  78. package/dist/core/hooks/shell-commit-tree.js +104 -0
  79. package/dist/core/inbox/contract.d.ts +12 -0
  80. package/dist/core/inbox/contract.d.ts.map +1 -1
  81. package/dist/core/inbox/contract.js +16 -0
  82. package/dist/core/inbox/peer-messages.d.ts +41 -16
  83. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  84. package/dist/core/inbox/peer-messages.js +372 -15
  85. package/dist/core/inbox/service.d.ts +8 -1
  86. package/dist/core/inbox/service.d.ts.map +1 -1
  87. package/dist/core/inbox/service.js +83 -3
  88. package/dist/core/inbox/targets.d.ts +17 -3
  89. package/dist/core/inbox/targets.d.ts.map +1 -1
  90. package/dist/core/inbox/targets.js +77 -2
  91. package/dist/core/presence/git.d.ts +18 -12
  92. package/dist/core/presence/git.d.ts.map +1 -1
  93. package/dist/core/presence/git.js +32 -38
  94. package/dist/core/presence/index.d.ts +2 -2
  95. package/dist/core/presence/index.d.ts.map +1 -1
  96. package/dist/core/presence/index.js +8 -6
  97. package/dist/core/presence/mail.d.ts +133 -0
  98. package/dist/core/presence/mail.d.ts.map +1 -0
  99. package/dist/core/presence/mail.js +464 -0
  100. package/dist/core/semantic/contract.d.ts +5 -5
  101. package/dist/core/storage/modes.d.ts +4 -0
  102. package/dist/core/storage/modes.d.ts.map +1 -1
  103. package/dist/core/storage/modes.js +4 -0
  104. package/dist/core/storage/segments.d.ts.map +1 -1
  105. package/dist/core/storage/segments.js +5 -4
  106. package/dist/lib/admission-history.d.ts +96 -0
  107. package/dist/lib/admission-history.d.ts.map +1 -0
  108. package/dist/lib/admission-history.js +196 -0
  109. package/dist/lib/admission.d.ts +56 -7
  110. package/dist/lib/admission.d.ts.map +1 -1
  111. package/dist/lib/admission.js +161 -19
  112. package/dist/lib/browser/client.d.ts +18 -0
  113. package/dist/lib/browser/client.d.ts.map +1 -1
  114. package/dist/lib/browser/client.js +100 -12
  115. package/dist/lib/browser/content-checks.d.ts +3 -1
  116. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  117. package/dist/lib/browser/content-checks.js +10 -2
  118. package/dist/lib/browser/critique.d.ts +32 -0
  119. package/dist/lib/browser/critique.d.ts.map +1 -1
  120. package/dist/lib/browser/critique.js +72 -3
  121. package/dist/lib/browser/geometry.d.ts +2 -0
  122. package/dist/lib/browser/geometry.d.ts.map +1 -1
  123. package/dist/lib/browser/geometry.js +61 -15
  124. package/dist/lib/browser/index.d.ts +1 -1
  125. package/dist/lib/browser/index.d.ts.map +1 -1
  126. package/dist/lib/browser/index.js +1 -1
  127. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  128. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  129. package/dist/lib/browser/page-review-contracts.js +6 -1
  130. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  131. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  132. package/dist/lib/browser/page-review-judge.js +41 -11
  133. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  134. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  135. package/dist/lib/browser/page-review-pack.js +5 -2
  136. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  137. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  138. package/dist/lib/browser/qa-run-contracts.js +11 -4
  139. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  140. package/dist/lib/browser/qa-run.js +73 -16
  141. package/dist/lib/browser/runts.d.ts +4 -2
  142. package/dist/lib/browser/runts.d.ts.map +1 -1
  143. package/dist/lib/browser/runts.js +84 -33
  144. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  145. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  146. package/dist/lib/browser/windows-chrome.js +42 -0
  147. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  148. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  149. package/dist/lib/browser/windows-login-sync.js +171 -0
  150. package/dist/lib/cookies/client.d.ts +30 -5
  151. package/dist/lib/cookies/client.d.ts.map +1 -1
  152. package/dist/lib/cookies/client.js +55 -10
  153. package/dist/lib/cookies/index.d.ts +1 -1
  154. package/dist/lib/cookies/index.d.ts.map +1 -1
  155. package/dist/lib/cookies/index.js +1 -1
  156. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  157. package/dist/lib/docs-metadata-sync.js +41 -2
  158. package/dist/lib/durable-job.d.ts +8 -0
  159. package/dist/lib/durable-job.d.ts.map +1 -1
  160. package/dist/lib/durable-job.js +10 -0
  161. package/dist/lib/http/client.d.ts +9 -8
  162. package/dist/lib/http/client.d.ts.map +1 -1
  163. package/dist/lib/http/client.js +44 -2
  164. package/dist/lib/instructions/templates.js +4 -4
  165. package/package.json +2 -2
  166. package/src/commander.ts +13 -0
  167. package/src/commands/admission.ts +272 -29
  168. package/src/commands/agents.ts +132 -27
  169. package/src/commands/browse.ts +208 -6
  170. package/src/commands/fetch.ts +1 -0
  171. package/src/commands/ledger-v3.ts +8 -2
  172. package/src/commands/presence.ts +49 -0
  173. package/src/commands/qa-run.ts +4 -3
  174. package/src/commands/review-pack.ts +41 -5
  175. package/src/core/agents/index.ts +6 -0
  176. package/src/core/agents/live-authority-v3.ts +11 -4
  177. package/src/core/agents/render/session-context.ts +1 -7
  178. package/src/core/agents/rules/claim-conflict.ts +50 -18
  179. package/src/core/agents/rules/stop-hook.ts +28 -1
  180. package/src/core/agents/session-events.ts +18 -3
  181. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  182. package/src/core/agents/state/live-coordination-view.ts +13 -6
  183. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  184. package/src/core/agents/state/parent-reference.ts +179 -0
  185. package/src/core/config.ts +85 -0
  186. package/src/core/diagnostics/path-identity.ts +52 -6
  187. package/src/core/events/v3/canonical.ts +46 -13
  188. package/src/core/events/v3/index.ts +3 -0
  189. package/src/core/events/v3/producers/recorder.ts +151 -13
  190. package/src/core/events/v3/reader.ts +55 -11
  191. package/src/core/events/v3/validate.ts +42 -1
  192. package/src/core/hooks/adapter/native-parent.ts +109 -0
  193. package/src/core/hooks/cli.ts +110 -4
  194. package/src/core/hooks/resolve/owner.ts +0 -23
  195. package/src/core/hooks/resolve/transcript.ts +61 -0
  196. package/src/core/hooks/shell-admission.ts +634 -0
  197. package/src/core/hooks/shell-commit-tree.ts +102 -0
  198. package/src/core/inbox/contract.ts +18 -0
  199. package/src/core/inbox/peer-messages.ts +501 -15
  200. package/src/core/inbox/service.ts +108 -3
  201. package/src/core/inbox/targets.ts +94 -3
  202. package/src/core/presence/git.ts +45 -37
  203. package/src/core/presence/index.ts +16 -8
  204. package/src/core/presence/mail.ts +620 -0
  205. package/src/core/storage/modes.ts +6 -0
  206. package/src/core/storage/segments.ts +5 -4
  207. package/src/lib/admission-history.ts +267 -0
  208. package/src/lib/admission.ts +217 -19
  209. package/src/lib/browser/client.ts +147 -35
  210. package/src/lib/browser/content-checks.ts +14 -3
  211. package/src/lib/browser/critique.ts +113 -3
  212. package/src/lib/browser/geometry.ts +63 -14
  213. package/src/lib/browser/index.ts +5 -0
  214. package/src/lib/browser/page-review-contracts.ts +6 -1
  215. package/src/lib/browser/page-review-judge.ts +58 -13
  216. package/src/lib/browser/page-review-pack.ts +13 -2
  217. package/src/lib/browser/qa-run-contracts.ts +30 -4
  218. package/src/lib/browser/qa-run.ts +78 -17
  219. package/src/lib/browser/runts.ts +85 -37
  220. package/src/lib/browser/windows-chrome.ts +50 -0
  221. package/src/lib/browser/windows-login-sync.ts +275 -0
  222. package/src/lib/cookies/client.ts +62 -10
  223. package/src/lib/cookies/index.ts +3 -0
  224. package/src/lib/docs-metadata-sync.ts +44 -2
  225. package/src/lib/durable-job.ts +15 -0
  226. package/src/lib/http/client.ts +63 -1
  227. package/src/lib/instructions/templates.ts +4 -4
@@ -15,9 +15,9 @@ import {
15
15
  cleanEventV3Archives,
16
16
  initializeEventLedgerV3,
17
17
  inventoryEventV3Archives,
18
- listHookProducerStateRecordsV3,
19
18
  readEventV3ControlState,
20
19
  recoverInvalidEventLedgerV3,
20
+ scanHookProducerStateRecordsV3,
21
21
  sha256V3,
22
22
  } from "../core/events/v3/index.ts";
23
23
  import type { EventV3SupportClassificationEvidence } from "../core/events/v3/support-storage/index.ts";
@@ -61,7 +61,8 @@ export function registerLedgerV3Command(
61
61
  try {
62
62
  const root = resolve(options.root ?? coordRoot(context));
63
63
  const control = readEventV3ControlState(root);
64
- const producers = listHookProducerStateRecordsV3(root, { includeTerminal: true });
64
+ const scan = scanHookProducerStateRecordsV3(root, { includeTerminal: true });
65
+ const producers = scan.records;
65
66
  const counts = producers.reduce<Record<string, number>>((summary, record) => {
66
67
  summary[record.state.adapter] = (summary[record.state.adapter] ?? 0) + 1;
67
68
  return summary;
@@ -72,6 +73,11 @@ export function registerLedgerV3Command(
72
73
  adapters: Object.keys(counts).sort(),
73
74
  producer_states: producers.length,
74
75
  producer_states_by_adapter: counts,
76
+ invalid_producer_states: scan.invalid.map((file) => ({
77
+ adapter: file.adapter,
78
+ session_hash: file.session_hash,
79
+ reason: file.reason,
80
+ })),
75
81
  },
76
82
  });
77
83
  } catch (error) {
@@ -8,6 +8,7 @@ import {
8
8
  resolveOwner,
9
9
  } from "../core/agents/index.ts";
10
10
  import { readLiveCoordinationRow } from "../core/agents/state/live-coordination-view.ts";
11
+ import { peerInbox } from "../core/inbox/peer-messages.ts";
11
12
  import {
12
13
  fetchPresence,
13
14
  publishPresence,
@@ -15,6 +16,7 @@ import {
15
16
  relayDaemonStatus,
16
17
  runRelayDaemon,
17
18
  } from "../core/presence/index.ts";
19
+ import { importMail, mailStatus, publishMail } from "../core/presence/mail.ts";
18
20
  import {
19
21
  ageSeconds,
20
22
  applyDetection,
@@ -134,6 +136,53 @@ export function registerPresenceCommand(program: Command, emit: EmitContext): vo
134
136
  emit.data(r);
135
137
  });
136
138
 
139
+ cmd
140
+ .command("mail")
141
+ .description(
142
+ "Show cross-machine peer messages (ADR 0222): this machine's unacknowledged outbox, " +
143
+ "acknowledgements it is publishing, and delivered/expired counts. --sync fetches, " +
144
+ "imports, and publishes first (synchronous; errors report).",
145
+ )
146
+ .option(
147
+ "--sync",
148
+ "Fetch peers' refs, import mail addressed here, and publish this machine's ref",
149
+ )
150
+ .option("--json", "JSON output")
151
+ .action((opts: { sync?: boolean; json?: boolean }) => {
152
+ const root = requireRoot(emit);
153
+ let sync: Record<string, unknown> | undefined;
154
+ if (opts.sync) {
155
+ const fetched = fetchPresence(root, { force: true, sync: true });
156
+ if (fetched.status === "error") {
157
+ emit.error({ code: "fetch_failed", message: fetched.error });
158
+ process.exit(1);
159
+ }
160
+ const imported = importMail(root, peerInbox(root), { publish: false });
161
+ const published = publishMail(root, { sync: true });
162
+ if (published.status === "error") {
163
+ emit.error({ code: "publish_failed", message: published.error });
164
+ process.exit(1);
165
+ }
166
+ sync = { imported, published };
167
+ }
168
+ const status = mailStatus(root);
169
+ if (opts.json) emit.config({ format: "json" });
170
+ if (opts.json || !process.stdout.isTTY) {
171
+ emit.data({ ...status, ...(sync ? { sync } : {}) });
172
+ return;
173
+ }
174
+ emit.text(
175
+ `${status.machine}: ${status.outbox.length} waiting for acknowledgement, ` +
176
+ `${status.acks} acknowledgements published, ${status.delivered} delivered, ` +
177
+ `${status.expired} expired\n`,
178
+ );
179
+ for (const m of status.outbox) {
180
+ emit.text(
181
+ ` ${m.message_id} to agent-${m.recipient} on ${m.to_machine} (queued ${m.queued_at})\n`,
182
+ );
183
+ }
184
+ });
185
+
137
186
  cmd
138
187
  .command("relay-daemon")
139
188
  .description(
@@ -17,6 +17,7 @@ import {
17
17
  resolveQaRepoRoot,
18
18
  } from "../core/qa-artifacts.ts";
19
19
  import { acquireAdmission, admissionBaseDir, admissionStatus } from "../lib/admission.ts";
20
+ import { PAGE_REVIEW_MAX_TILE_BUDGET } from "../lib/browser/page-review-contracts.ts";
20
21
  import {
21
22
  deleteExpiredPacks,
22
23
  PAGE_REVIEW_DEFAULT_RETENTION_MINUTES,
@@ -99,7 +100,7 @@ export function registerQaRunCommand(
99
100
  )
100
101
  .option(
101
102
  "--tile-budget <n>",
102
- "Maximum native tiles across the complete review pack (8-400; default 96).",
103
+ `Maximum native tiles across the complete review pack (8-${PAGE_REVIEW_MAX_TILE_BUDGET}; default 96).`,
103
104
  )
104
105
  .option(
105
106
  "--accept-dispositions",
@@ -224,10 +225,10 @@ export function registerQaRunCommand(
224
225
  const policy = { ...validation.job.policy };
225
226
  if (opts.tileBudget !== undefined) {
226
227
  const n = Number(opts.tileBudget);
227
- if (!Number.isInteger(n) || n < 8 || n > 400) {
228
+ if (!Number.isInteger(n) || n < 8 || n > PAGE_REVIEW_MAX_TILE_BUDGET) {
228
229
  emit.error({
229
230
  code: "qa_run_invalid_tile_budget",
230
- message: "--tile-budget must be an integer between 8 and 400",
231
+ message: `--tile-budget must be an integer between 8 and ${PAGE_REVIEW_MAX_TILE_BUDGET}`,
231
232
  });
232
233
  process.exitCode = 1;
233
234
  return;
@@ -14,7 +14,8 @@ import { readLiveCoordinationRow } from "../core/agents/state/live-coordination-
14
14
  import { artifactsRoot } from "../core/artifacts/index.ts";
15
15
  import { resolveBinName, reviewPackAutoCleanEnabled } from "../core/config.ts";
16
16
  import { createManagedQaOutParent, resolveQaRepoRoot } from "../core/qa-artifacts.ts";
17
- import { DEFAULT_CRITIQUE_RUBRIC } from "../lib/browser/critique.ts";
17
+ import { DEFAULT_CRITIQUE_RUBRIC, formatCritiqueUsage } from "../lib/browser/critique.ts";
18
+ import { PAGE_REVIEW_MAX_TILE_BUDGET } from "../lib/browser/page-review-contracts.ts";
18
19
  import { persistPackReviewDecisions } from "../lib/browser/page-review-evidence.ts";
19
20
  import { judgePageReviewPack, toCritiqueRecords } from "../lib/browser/page-review-judge.ts";
20
21
  import {
@@ -382,7 +383,9 @@ export function registerReviewPackCommand(
382
383
  )
383
384
  .option(
384
385
  "--max-tiles <n>",
385
- "Native tile cap per context, including required top and bottom bands.",
386
+ "Native tile cap per context, including required top and bottom bands (default 24). " +
387
+ `The whole pack never exceeds ${PAGE_REVIEW_MAX_TILE_BUDGET} tiles, so the cap is ` +
388
+ "lowered when the contexts would need more.",
386
389
  )
387
390
  .option("--concurrency <n>", "Concurrent capture browsers (1-8; default 2).", "2")
388
391
  .option("--timeout <ms>", "Per-capture timeout in milliseconds (default 120000).", "120000")
@@ -411,6 +414,31 @@ export function registerReviewPackCommand(
411
414
  process.exitCode = 1;
412
415
  return;
413
416
  }
417
+ const requestedMaxTiles = Number(opts.maxTiles ?? "24");
418
+ if (
419
+ !Number.isInteger(requestedMaxTiles) ||
420
+ requestedMaxTiles < 1 ||
421
+ requestedMaxTiles > PAGE_REVIEW_MAX_TILE_BUDGET
422
+ ) {
423
+ emit.error({
424
+ code: "review_pack_invalid_max_tiles",
425
+ message: `--max-tiles must be an integer between 1 and ${PAGE_REVIEW_MAX_TILE_BUDGET}`,
426
+ });
427
+ process.exitCode = 1;
428
+ return;
429
+ }
430
+ // The judge refuses a pack above the ceiling, so share it across contexts here.
431
+ const perContextMaxTiles = Math.max(
432
+ 2,
433
+ Math.min(requestedMaxTiles, Math.floor(PAGE_REVIEW_MAX_TILE_BUDGET / contexts.length)),
434
+ );
435
+ if (perContextMaxTiles < requestedMaxTiles) {
436
+ emit.log(
437
+ `review-pack: ${contexts.length} contexts share the ${PAGE_REVIEW_MAX_TILE_BUDGET}-tile ceiling; ` +
438
+ `capping each at ${perContextMaxTiles} tiles`,
439
+ "warn",
440
+ );
441
+ }
414
442
  const concurrency = Math.max(
415
443
  1,
416
444
  Math.min(8, Number.parseInt(opts.concurrency ?? "2", 10) || 2),
@@ -472,7 +500,8 @@ export function registerReviewPackCommand(
472
500
  "--qa-state",
473
501
  ctx.state,
474
502
  ...(opts.scope ?? []).flatMap((selector) => ["--review-pack-scope", selector]),
475
- ...(opts.maxTiles !== undefined ? ["--check-critique-max-tiles", opts.maxTiles] : []),
503
+ "--check-critique-max-tiles",
504
+ String(perContextMaxTiles),
476
505
  ];
477
506
  emit.log(`capture ${ctx.id}`, "info");
478
507
  const res = await defaultQaRunExec(argv, { timeoutMs, env });
@@ -641,7 +670,12 @@ export function registerReviewPackCommand(
641
670
  else process.env[QA_RUN_HEADLESS_ONLY_ENV] = prior;
642
671
 
643
672
  const merged = new Map((manifest.critique ?? []).map((row) => [row.context_id, row]));
644
- for (const row of toCritiqueRecords(result)) merged.set(row.context_id, row);
673
+ for (const row of toCritiqueRecords(result)) {
674
+ // Re-judging never changes the captured coverage, so a signoff's
675
+ // sampled acceptance (ADR 0224) still applies.
676
+ const prior = merged.get(row.context_id)?.coverage_mode;
677
+ merged.set(row.context_id, prior ? { ...row, coverage_mode: prior } : row);
678
+ }
645
679
  const judgedRecords = new Map(result.contexts.map((row) => [row.context_id, row.record]));
646
680
  finalizePageReviewPack({
647
681
  packDir,
@@ -671,7 +705,9 @@ export function registerReviewPackCommand(
671
705
  );
672
706
  }
673
707
  emit.log(
674
- `judge: ${result.outcome} · pool ${result.pool.concurrency} · ${result.tiles_reviewed} tile(s) in ${result.pool.wall_time_ms}ms · ${packPaths(packDir).review}`,
708
+ `judge: ${result.outcome} · pool ${result.pool.concurrency} · ${result.tiles_reviewed} tile(s) in ${result.pool.wall_time_ms}ms` +
709
+ (result.usage ? ` · ${formatCritiqueUsage(result.usage)}` : "") +
710
+ ` · ${packPaths(packDir).review}`,
675
711
  result.outcome === "pass" ? "info" : "warn",
676
712
  );
677
713
  if (opts.json) emit.data(result as unknown as Record<string, unknown>);
@@ -11,4 +11,10 @@ export * from "./canonical-emit.js";
11
11
  export * from "./coord-client.js";
12
12
  export * from "./live-observation-v3.js";
13
13
  export * from "./session-events.js";
14
+ export {
15
+ type AgentParentReference,
16
+ type AgentParentReferenceOptions,
17
+ type AgentParentSubject,
18
+ resolveAgentParentReference,
19
+ } from "./state/parent-reference.js";
14
20
  export * from "./state/session-state.js";
@@ -25,10 +25,10 @@ import {
25
25
  } from "../events/v3/producers/coordination-recorder.ts";
26
26
  import {
27
27
  type HookProducerStateV3,
28
- listHookProducerStateRecordsV3,
29
28
  readHookProducerStateV3,
30
29
  readTerminalHookProducerStateV3,
31
30
  recordHookSignalV3,
31
+ scanHookProducerStateRecordsV3,
32
32
  } from "../events/v3/producers/recorder.ts";
33
33
  import { canonicalClaimPath } from "./claim-path.ts";
34
34
  import {
@@ -259,13 +259,20 @@ function readInstanceProducerStatesForBootstrap(
259
259
  coordRoot: string,
260
260
  instanceId: `inst_${string}`,
261
261
  ): HookProducerStateV3[] {
262
+ let scan: ReturnType<typeof scanHookProducerStateRecordsV3>;
262
263
  try {
263
- return listHookProducerStateRecordsV3(coordRoot, { includeTerminal: true })
264
- .map((record) => record.state)
265
- .filter((state) => state.instance_id === instanceId);
264
+ scan = scanHookProducerStateRecordsV3(coordRoot, { includeTerminal: true });
266
265
  } catch {
267
266
  throw new LiveCoordinationAuthorityV3Error("producer_state_unsafe");
268
267
  }
268
+ // Another session's invalid file does not block this one; this instance's
269
+ // own invalid file still fails closed.
270
+ if (scan.invalid.some((file) => file.instance_id === instanceId)) {
271
+ throw new LiveCoordinationAuthorityV3Error("producer_state_unsafe");
272
+ }
273
+ return scan.records
274
+ .map((record) => record.state)
275
+ .filter((state) => state.instance_id === instanceId);
269
276
  }
270
277
 
271
278
  function terminalGenerationForSession(
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { spawnSync } from "node:child_process";
11
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
11
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { resolveBinName, resolveHooksSetupHint } from "../../config.ts";
14
14
  import { harneryVersion, loadAdapterWiring } from "../../hooks/adapter/wiring.ts";
@@ -482,9 +482,3 @@ export function formatPendingCouncils(coordRoot: string, agentName: string): str
482
482
  }
483
483
  return `Councils waiting on your input (${pending.length} open): ${list}. Run \`${bin} agents council list --mine\` to see all of them, then \`${bin} agents council show <id>\` for any brief.`;
484
484
  }
485
-
486
- /**
487
- * Suppress unused import warning when statSync isn't used in this file
488
- * (kept exported for future renderers, e.g. heartbeat-freshness coloring).
489
- */
490
- export const _ensureStatSyncImported = statSync;
@@ -18,7 +18,9 @@ import { spawnSync } from "node:child_process";
18
18
  import { existsSync } from "node:fs";
19
19
  import { join } from "node:path";
20
20
  import { adapterFromPlatform as sharedAdapterFromPlatform } from "../../adapter.ts";
21
- import { recordLiveClaimChangeV3 } from "../live-authority-v3.ts";
21
+ import { resolveBinName } from "../../config.ts";
22
+ import { writeProducerDiagnosticV3 } from "../../events/v3/producers/intake.ts";
23
+ import { LiveCoordinationAuthorityV3Error, recordLiveClaimChangeV3 } from "../live-authority-v3.ts";
22
24
  import { readLiveCoordinationRows } from "../state/live-coordination-view.ts";
23
25
 
24
26
  const FRESHNESS_SECS = 600;
@@ -82,8 +84,15 @@ export function evaluateClaim(coordRoot: string, req: ClaimRequest): VerdictResu
82
84
  // prune the claim from their heartbeat and allow the edit. Skips when the
83
85
  // file doesn't exist (intent-to-create claim); that surface IS load-bearing.
84
86
  if (isFileCommittedClean(coordRoot, req.path)) {
85
- if (!releaseClaimThroughLedger(coordRoot, conflict.instance_id, req.path, req)) {
86
- return authorityUnavailable(req.path, "release the stale claim on");
87
+ const refused = releaseClaimThroughLedger(coordRoot, conflict.instance_id, req.path, req);
88
+ if (refused) {
89
+ return authorityUnavailable(
90
+ coordRoot,
91
+ req,
92
+ req.path,
93
+ "release the stale claim on",
94
+ refused,
95
+ );
87
96
  }
88
97
  // Fall through to ordering check + allow.
89
98
  } else {
@@ -146,8 +155,9 @@ export function evaluateClaim(coordRoot: string, req: ClaimRequest): VerdictResu
146
155
  // Every blocker is a finished (committed-clean) edit: prune them so they
147
156
  // stop constraining future acquisitions, then fall through to allow.
148
157
  for (const p of blockers) {
149
- if (!releaseClaimThroughLedger(coordRoot, req.instance_id, p, req)) {
150
- return authorityUnavailable(p, "release the completed claim on");
158
+ const refused = releaseClaimThroughLedger(coordRoot, req.instance_id, p, req);
159
+ if (refused) {
160
+ return authorityUnavailable(coordRoot, req, p, "release the completed claim on", refused);
151
161
  }
152
162
  }
153
163
  }
@@ -156,9 +166,8 @@ export function evaluateClaim(coordRoot: string, req: ClaimRequest): VerdictResu
156
166
  // Acquire the claim: atomic check-and-set. Adds req.path to my
157
167
  // files_touched if not already present.
158
168
  if (req.mode !== "read") {
159
- if (!acquireClaimThroughLedger(coordRoot, req)) {
160
- return authorityUnavailable(req.path, "acquire");
161
- }
169
+ const refused = acquireClaimThroughLedger(coordRoot, req);
170
+ if (refused) return authorityUnavailable(coordRoot, req, req.path, "acquire", refused);
162
171
  }
163
172
 
164
173
  return { allow: true, exit_code: 0, rule: "claim.pass" };
@@ -242,7 +251,8 @@ function isFileCommittedClean(coordRoot: string, relPath: string): boolean {
242
251
  }
243
252
  }
244
253
 
245
- function acquireClaimThroughLedger(coordRoot: string, req: ClaimRequest): boolean {
254
+ /** Returns null when the ledger recorded the claim, else the refusal reason. */
255
+ function acquireClaimThroughLedger(coordRoot: string, req: ClaimRequest): string | null {
246
256
  const owner = readPeers(coordRoot).find((peer) => peer.instance_id === req.instance_id);
247
257
  try {
248
258
  recordLiveClaimChangeV3({
@@ -254,9 +264,9 @@ function acquireClaimThroughLedger(coordRoot: string, req: ClaimRequest): boolea
254
264
  path: req.path,
255
265
  access: "write",
256
266
  });
257
- return true;
258
- } catch {
259
- return false;
267
+ return null;
268
+ } catch (error) {
269
+ return refusalReason(error);
260
270
  }
261
271
  }
262
272
 
@@ -265,7 +275,7 @@ function releaseClaimThroughLedger(
265
275
  subjectInstanceId: string,
266
276
  relPath: string,
267
277
  actor: ClaimRequest,
268
- ): boolean {
278
+ ): string | null {
269
279
  const owner = readPeers(coordRoot).find((peer) => peer.instance_id === actor.instance_id);
270
280
  try {
271
281
  recordLiveClaimChangeV3({
@@ -278,17 +288,39 @@ function releaseClaimThroughLedger(
278
288
  path: relPath,
279
289
  access: "write",
280
290
  });
281
- return true;
282
- } catch {
283
- return false;
291
+ return null;
292
+ } catch (error) {
293
+ return refusalReason(error);
284
294
  }
285
295
  }
286
296
 
287
- function authorityUnavailable(path: string, operation: string): VerdictResult {
297
+ function refusalReason(error: unknown): string {
298
+ if (error instanceof LiveCoordinationAuthorityV3Error) return error.reason;
299
+ return error instanceof Error ? error.message : String(error);
300
+ }
301
+
302
+ function authorityUnavailable(
303
+ coordRoot: string,
304
+ req: ClaimRequest,
305
+ path: string,
306
+ operation: string,
307
+ refusal: string,
308
+ ): VerdictResult {
309
+ // The ledger's own reason is the only evidence of why a healthy-looking
310
+ // session cannot claim; a generic message sent agents to diagnose the wrong
311
+ // layer, so keep the reason in the verdict and in a durable diagnostic.
312
+ writeProducerDiagnosticV3(coordRoot, "claim_authority_refused", {
313
+ instance_id: req.instance_id,
314
+ operation,
315
+ reason: refusal,
316
+ });
288
317
  return {
289
318
  allow: false,
290
319
  exit_code: 2,
291
320
  rule: "claim.authority_unavailable",
292
- reason: `Cannot safely ${operation} ${path}; V3 authority is unavailable.`,
321
+ reason:
322
+ `Cannot safely ${operation} ${path}; the coordination ledger refused the claim ` +
323
+ `(${refusal}). Run \`${resolveBinName(coordRoot)} agents health\` and retry; if health ` +
324
+ "is safe, the refusal reason above names the session state that blocked it.",
293
325
  };
294
326
  }
@@ -36,6 +36,12 @@ export interface StopHookRequest {
36
36
  session_name_observation_unavailable?: boolean;
37
37
  bypass?: boolean;
38
38
  workflow_child?: boolean;
39
+ /**
40
+ * Claude Code moved this session's foreground command into the background
41
+ * and has not yet reported it finished. Finalization cannot pass until it
42
+ * does, so status and reply-box demands wait for the notification turn.
43
+ */
44
+ pending_background_handoff?: boolean;
39
45
  }
40
46
 
41
47
  export const STOP_REMEDIATION_MARKER = "[harnery:stop-remediation";
@@ -170,6 +176,7 @@ export function evaluateStopHookV3Events(
170
176
  // Pure prose turns keep the human-visible acknowledgement but do not require
171
177
  // task or final-status mutations. This preserves the pre-V3 policy exactly.
172
178
  if (!usedTool) {
179
+ if (!ackPresent && req.pending_background_handoff) return backgroundHandoffDeferred();
173
180
  if (!ackPresent) return withCycleAnchor(ackBlock(), cycleAnchor);
174
181
  return {
175
182
  allow: true,
@@ -203,12 +210,32 @@ export function evaluateStopHookV3Events(
203
210
  }
204
211
  }
205
212
 
206
- const blocked = mergeStopBlocks(pending);
213
+ // A status check run now would refuse on the in-flight commit and a box
214
+ // cannot be issued without it, so bouncing only loops until the task ends.
215
+ // Task and naming evidence do not depend on the task and stay enforced.
216
+ const enforced = req.pending_background_handoff
217
+ ? pending.filter(
218
+ (block) => block.rule !== "stop-hook.rule_1_3" && block.rule !== "stop-hook.rule_2_3",
219
+ )
220
+ : pending;
221
+ const blocked = mergeStopBlocks(enforced);
207
222
  if (blocked) return withCycleAnchor(blocked, cycleAnchor);
223
+ if (enforced.length < pending.length) return backgroundHandoffDeferred();
208
224
 
209
225
  return { allow: true, exit_code: 0, rule: "stop-hook.pass" };
210
226
  }
211
227
 
228
+ function backgroundHandoffDeferred(): VerdictResult {
229
+ return {
230
+ allow: true,
231
+ exit_code: 0,
232
+ rule: "stop-hook.background_handoff_deferred",
233
+ reason:
234
+ "a command Claude Code moved to the background is still running; status and reply-box " +
235
+ "evidence are required on the turn its completion notification starts",
236
+ };
237
+ }
238
+
212
239
  function unconditionalVerdict(req: StopHookRequest): VerdictResult | undefined {
213
240
  if (req.bypass) {
214
241
  return {
@@ -89,9 +89,20 @@ function commandKey(fields: Record<string, unknown>): string | undefined {
89
89
  : undefined;
90
90
  }
91
91
 
92
- function flushCommandOutput(command: PendingCommand): void {
92
+ /**
93
+ * `deferDrain` stages each summary durably in the spool without appending it,
94
+ * for a caller that writes the completion next: that write drains every staged
95
+ * row under one lease and one witness advance instead of one per row.
96
+ */
97
+ function flushCommandOutput(command: PendingCommand, deferDrain = false): void {
93
98
  for (const summary of command.streams.values()) {
94
- recordCommandObservation("command.output_observed", command.fields, command.coordRoot, summary);
99
+ recordCommandObservation(
100
+ "command.output_observed",
101
+ command.fields,
102
+ command.coordRoot,
103
+ summary,
104
+ deferDrain,
105
+ );
95
106
  }
96
107
  command.streams.clear();
97
108
  }
@@ -104,6 +115,7 @@ function recordCommandObservation(
104
115
  fields: Record<string, unknown>,
105
116
  coordRoot: string,
106
117
  summary?: OutputSummary,
118
+ deferDrain = false,
107
119
  ): boolean {
108
120
  const instanceId = typeof fields.instance_id === "string" ? fields.instance_id : undefined;
109
121
  if (!instanceId) return false;
@@ -134,6 +146,7 @@ function recordCommandObservation(
134
146
  platform: livePlatformV3(),
135
147
  ...(fields.bridge === "codex-wsl" ? { bridge: "codex-wsl" as const } : {}),
136
148
  monotonic_ns: process.hrtime.bigint().toString(),
149
+ ...(deferDrain ? { writerOptions: { deferDrain: true } } : {}),
137
150
  });
138
151
  if (result.state === "generation_unavailable") {
139
152
  const expectedLifecycleReopenGap =
@@ -276,7 +289,9 @@ export function writeSessionEvent(
276
289
  const coordRoot = command?.coordRoot ?? coordinationRootPath();
277
290
  if (type === "command.completed") {
278
291
  pendingCommands.delete(key);
279
- if (command) flushCommandOutput(command);
292
+ // The completion below drains the staged summaries with itself. If it
293
+ // never writes, the rows stay durable in the spool for the next drain.
294
+ if (command) flushCommandOutput(command, true);
280
295
  recordCommandObservation(type, fields, coordRoot);
281
296
  } else if (recordCommandObservation(type, fields, coordRoot) && !command) {
282
297
  pendingCommands.set(key, {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, resolve, sep } from "node:path";
5
+ import type { AgentParentReference } from "./parent-reference-contract.ts";
5
6
  import type { AgentActivity, TaskState } from "./session-state.ts";
6
7
 
7
8
  export interface Heartbeat {
@@ -50,6 +51,8 @@ export interface Heartbeat {
50
51
  last_status_at?: string;
51
52
  current_turn_id?: string;
52
53
  parent_instance_id?: string;
54
+ /** Read-only display/recipient evidence; never a claim or lifecycle authority input. */
55
+ parent_reference?: AgentParentReference;
53
56
  workflow_run_id?: string;
54
57
  workflow_agent_id?: string;
55
58
  /** Disposable-cache bindings that prove a row belongs to the current V3 generation. */
@@ -27,6 +27,17 @@ export interface V3HeartbeatMaterialization extends Heartbeat {
27
27
  v3_task_state: "set" | "cleared";
28
28
  }
29
29
 
30
+ /** Index exact generations; a current replacement never erases its terminal predecessor. */
31
+ export function indexCoordinationGenerationsV3(
32
+ view: CoordinationViewV3,
33
+ ): ReadonlyMap<string, CoordinationGenerationViewV3> {
34
+ return new Map(
35
+ [...Object.values(view.terminal_generations), ...Object.values(view.instances)].map(
36
+ (generation) => [generation.generation_id, generation],
37
+ ),
38
+ );
39
+ }
40
+
30
41
  /** Read the live coordination rows through the hard-cut ledger route. */
31
42
  export function readLiveCoordinationRows(coordRoot: string): V3HeartbeatMaterialization[] {
32
43
  const route = observeLiveEventLedgerRouteV3(coordRoot);
@@ -38,9 +49,7 @@ export function readLiveCoordinationRows(coordRoot: string): V3HeartbeatMaterial
38
49
  return [];
39
50
  }
40
51
  const caches = readV3Caches(coordRoot);
41
- const byGeneration = new Map(
42
- Object.values(view.instances).map((generation) => [generation.generation_id, generation]),
43
- );
52
+ const byGeneration = indexCoordinationGenerationsV3(view);
44
53
  return Object.values(view.instances)
45
54
  .filter((generation) => generation.authority_eligible)
46
55
  .map((generation) => {
@@ -70,9 +79,7 @@ export function readLiveCoordinationRow(
70
79
  if (!generation?.authority_eligible) return null;
71
80
  const cache = readHeartbeat(coordRoot, nativeInstanceId);
72
81
  const parent = generation.parent_generation_id
73
- ? Object.values(view.instances).find(
74
- (candidate) => candidate.generation_id === generation.parent_generation_id,
75
- )
82
+ ? indexCoordinationGenerationsV3(view).get(generation.parent_generation_id)
76
83
  : undefined;
77
84
  return projectHeartbeatV3(
78
85
  coordRoot,
@@ -0,0 +1,14 @@
1
+ export type AgentParentReference =
2
+ | {
3
+ state: "verified";
4
+ relationship: "delegated-child" | "native-child";
5
+ parent_instance_id: string;
6
+ parent_v3_instance_id: string;
7
+ parent_generation_id: string | null;
8
+ parent_session_id: string | null;
9
+ parent_name: string | null;
10
+ source: "v3-session-link" | "native-session-metadata";
11
+ source_witness?: string;
12
+ addressable: boolean;
13
+ }
14
+ | { state: "none" | "unknown" | "conflict"; reason: string };