chati-dev 4.5.28 → 4.5.29

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 (172) hide show
  1. package/bin/chati.js +151 -123
  2. package/framework/agents/build/dev.md +4 -4
  3. package/framework/agents/deploy/devops.md +3 -3
  4. package/framework/agents/discover/brief.md +18 -6
  5. package/framework/agents/discover/brownfield-wu.md +3 -3
  6. package/framework/agents/discover/greenfield-wu.md +4 -4
  7. package/framework/agents/plan/architect.md +2 -2
  8. package/framework/agents/plan/detail.md +4 -4
  9. package/framework/agents/plan/phases.md +3 -3
  10. package/framework/agents/plan/tasks.md +47 -19
  11. package/framework/agents/plan/ux-brand-architect.md +1 -1
  12. package/framework/agents/plan/ux.md +26 -2
  13. package/framework/agents/quality/qa-implementation.md +8 -8
  14. package/framework/agents/quality/qa-planning.md +58 -4
  15. package/framework/agents/quality/qa-visual.md +4 -4
  16. package/framework/config.yaml +2 -2
  17. package/framework/constitution.md +12 -1
  18. package/framework/context/governance.md +2 -1
  19. package/framework/context/root.md +3 -1
  20. package/framework/domains/constitution.yaml +1 -1
  21. package/framework/domains/global.yaml +1 -1
  22. package/framework/domains/workflows/standard-flow.yaml +7 -8
  23. package/framework/hooks/advance-trigger.js +3 -2
  24. package/framework/hooks/brief-validator.js +1 -1
  25. package/framework/hooks/post-dev.js +1 -1
  26. package/framework/hooks/team-quality-gate.js +9 -2
  27. package/framework/i18n/en.yaml +1 -0
  28. package/framework/i18n/es.yaml +1 -0
  29. package/framework/i18n/fr.yaml +1 -0
  30. package/framework/i18n/pt.yaml +1 -0
  31. package/framework/manifest.json +83 -83
  32. package/framework/manifest.sig +1 -1
  33. package/framework/orchestrator/chati-router.js +14 -6
  34. package/framework/orchestrator/chati.md +61 -23
  35. package/framework/rules/human-writing-style.md +7 -0
  36. package/framework/schemas/session.schema.json +25 -2
  37. package/framework/schemas/task.schema.json +26 -0
  38. package/framework/tasks/orchestrator-handoff.md +10 -10
  39. package/framework/tasks/orchestrator-resume.md +3 -3
  40. package/framework/workflows/brownfield-fullstack.yaml +6 -3
  41. package/framework/workflows/brownfield-service.yaml +5 -3
  42. package/framework/workflows/brownfield-ui.yaml +5 -3
  43. package/framework/workflows/greenfield-fullstack.yaml +6 -3
  44. package/framework/workflows/standard-flow.yaml +8 -3
  45. package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
  46. package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
  47. package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
  48. package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
  49. package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
  50. package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
  51. package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
  52. package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
  53. package/node_modules/@chati/core/native/execution-admission.h +59 -0
  54. package/node_modules/@chati/core/native/execution-gate.c +41 -0
  55. package/node_modules/@chati/core/native/linux-process.c +41 -0
  56. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
  57. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
  58. package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
  59. package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
  60. package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
  61. package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
  62. package/node_modules/@chati/core/package.json +2 -1
  63. package/node_modules/@chati/core/scripts/build-native.js +36 -0
  64. package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
  65. package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
  66. package/node_modules/@chati/core/src/darwin-process.js +79 -0
  67. package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
  68. package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
  69. package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
  70. package/node_modules/@chati/core/src/execution-gate.js +8 -0
  71. package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
  72. package/node_modules/@chati/core/src/index.js +18 -0
  73. package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
  74. package/node_modules/@chati/core/src/linux-process.js +133 -0
  75. package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
  76. package/node_modules/@chati/planning/src/index.js +33 -0
  77. package/node_modules/@chati/planning/src/planning-material.js +64 -0
  78. package/node_modules/@chati/planning/src/verification-plan.js +57 -0
  79. package/node_modules/@chati/rail/package.json +1 -0
  80. package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
  81. package/node_modules/@chati/rail/src/index.js +812 -167
  82. package/node_modules/@chati/rail/src/provider-processing.js +118 -0
  83. package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
  84. package/node_modules/@chati/release-lane/src/index.js +40 -43
  85. package/node_modules/@chati/review-council/src/index.js +223 -0
  86. package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
  87. package/package-artifact-manifest.json +1 -1
  88. package/package-artifact-manifest.sig +1 -1
  89. package/package.json +3 -2
  90. package/scripts/verify-real-harness-e2e.js +20 -8
  91. package/src/installer/core.js +147 -107
  92. package/src/installer/package-artifact.js +7 -0
  93. package/src/installer/preflight.js +13 -0
  94. package/src/installer/provider-overlay.js +1 -2
  95. package/src/installer/templates.js +58 -0
  96. package/src/installer-v2/catalog-client.js +31 -1
  97. package/src/installer-v2/clickup-connection-storage.js +92 -0
  98. package/src/installer-v2/clickup-mcp-client.js +155 -0
  99. package/src/installer-v2/clickup-oauth-loopback.js +96 -0
  100. package/src/installer-v2/clickup-oauth.js +233 -0
  101. package/src/installer-v2/index.js +141 -15
  102. package/src/installer-v2/installation-authority.js +98 -4
  103. package/src/installer-v2/provider-executable.js +117 -12
  104. package/src/installer-v2/wizard-installation.js +11 -0
  105. package/src/orchestrator/agent-selector.js +39 -1
  106. package/src/orchestrator/browser-artifact-store.js +131 -0
  107. package/src/orchestrator/cli.js +762 -205
  108. package/src/orchestrator/clickup-mcp-observer.js +226 -0
  109. package/src/orchestrator/clickup-runtime.js +45 -24
  110. package/src/orchestrator/council-gate-runtime.js +903 -0
  111. package/src/orchestrator/evidence-authority-enrollment.js +75 -0
  112. package/src/orchestrator/evidence-authority-request.js +46 -0
  113. package/src/orchestrator/handoff-engine.js +4 -11
  114. package/src/orchestrator/pipeline-manager.js +42 -4
  115. package/src/orchestrator/planning-contract-revision.js +711 -0
  116. package/src/orchestrator/planning-reconciliation.js +302 -0
  117. package/src/orchestrator/planning-runtime.js +476 -29
  118. package/src/orchestrator/rail-adjudication-evidence.js +15 -24
  119. package/src/orchestrator/rail-human-effort-authority.js +46 -0
  120. package/src/orchestrator/rail-human-effort.js +149 -0
  121. package/src/orchestrator/rail-runtime.js +291 -47
  122. package/src/orchestrator/rail-verification.js +278 -0
  123. package/src/orchestrator/review-runtime.js +37 -2
  124. package/src/orchestrator/runtime-installation-v2.js +143 -24
  125. package/src/orchestrator/session-manager.js +1457 -29
  126. package/src/tasks/handoff.js +57 -33
  127. package/src/terminal/adapters/claude-adapter.js +3 -0
  128. package/src/terminal/adapters/codex-adapter.js +13 -0
  129. package/src/terminal/adapters/grok-adapter.js +1 -0
  130. package/src/terminal/agent-attempt-budget.js +35 -0
  131. package/src/terminal/browser-endpoint-proxy.js +174 -0
  132. package/src/terminal/browser-observer-driver.js +94 -0
  133. package/src/terminal/browser-observer-supervisor.js +73 -0
  134. package/src/terminal/browser-observer.js +95 -0
  135. package/src/terminal/browser-process-facts.js +75 -0
  136. package/src/terminal/browser-runtime.js +131 -0
  137. package/src/terminal/darwin-authority-policy.js +213 -0
  138. package/src/terminal/darwin-browser-application.js +116 -0
  139. package/src/terminal/darwin-browser-observer.js +57 -0
  140. package/src/terminal/darwin-execution.js +185 -0
  141. package/src/terminal/executable-snapshot.js +202 -0
  142. package/src/terminal/handoff-parser.js +48 -2
  143. package/src/terminal/isolation.js +14 -13
  144. package/src/terminal/prompt-builder.js +176 -15
  145. package/src/terminal/provider-preflight.js +47 -27
  146. package/src/terminal/rail-execution-worktree.js +22 -6
  147. package/src/terminal/rail-process-observer.js +10 -0
  148. package/src/terminal/rail-prompts.js +21 -3
  149. package/src/terminal/rail-readonly-workspace.js +54 -5
  150. package/src/terminal/rail-worktree-checkpoint.js +315 -0
  151. package/src/terminal/rail-worktree-restoration.js +485 -0
  152. package/src/terminal/run-agent.js +164 -42
  153. package/src/terminal/run-council-gate.js +338 -0
  154. package/src/terminal/run-planning-reconciliation.js +49 -0
  155. package/src/terminal/run-rail-adjudication.js +29 -7
  156. package/src/terminal/run-rail-recovery.js +97 -0
  157. package/src/terminal/run-rail-review.js +36 -15
  158. package/src/terminal/run-rail-rework.js +26 -15
  159. package/src/terminal/run-rail-task.js +22 -9
  160. package/src/terminal/runtime-native-helper.js +85 -0
  161. package/src/terminal/spawner.js +899 -142
  162. package/src/terminal/verification-process-facts.js +49 -0
  163. package/src/upgrade/entrypoints.js +68 -0
  164. package/src/upgrade/generation.js +194 -0
  165. package/src/upgrade/maintenance.js +79 -0
  166. package/src/upgrade/paths.js +81 -0
  167. package/src/upgrade/upgrade.js +267 -0
  168. package/src/utils/execution-role.js +15 -0
  169. package/src/utils/handoff-paths.js +59 -0
  170. package/src/wizard/i18n.js +1 -0
  171. package/src/wizard/index.js +17 -5
  172. package/src/wizard/questions.js +24 -12
@@ -0,0 +1,118 @@
1
+ import { canonicalize, ContractError, sha256 } from '@chati/core';
2
+ import { restoredExecutionProof } from './execution-restoration.js';
3
+ import { assertTerminalCompletionContainment } from './terminal-containment.js';
4
+
5
+ const STAGES = Object.freeze({ execution_dispatched: 'execution', review_dispatched: 'review', adjudication_dispatched: 'adjudication' });
6
+ function fail(code, message) { throw new ContractError(code, message); }
7
+
8
+ /** Full observed text, never a summary or caller-declared result. */
9
+ export function assertProviderProcessOutput(output, receipt) {
10
+ if (!output || typeof output !== 'object' || Array.isArray(output)
11
+ || Object.keys(output).length !== 2
12
+ || typeof output.stdout !== 'string' || typeof output.stderr !== 'string') {
13
+ fail('RAIL_PROCESS_OUTPUT_MISMATCH', 'process output must contain exactly stdout and stderr strings');
14
+ }
15
+ if (Buffer.byteLength(output.stdout) > 1_000_000 || Buffer.byteLength(output.stderr) > 100_000) {
16
+ fail('RAIL_PROCESS_OUTPUT_TOO_LARGE', 'observed process output exceeds the journal-bound evidence limit');
17
+ }
18
+ if (sha256(output.stdout) !== receipt.stdout_digest || sha256(output.stderr) !== receipt.stderr_digest) {
19
+ fail('RAIL_PROCESS_OUTPUT_MISMATCH', 'process output must match both digests in its verified completion receipt');
20
+ }
21
+ }
22
+
23
+ export function providerProcessDispatches(records, attemptId) {
24
+ return records.filter((record) => Object.hasOwn(STAGES, record.type) && record.payload?.attempt_id === attemptId);
25
+ }
26
+
27
+ export function assertProviderProcessReceipt(dispatch, receipt, verifyAuthority) {
28
+ const invocation = dispatch.payload.invocation;
29
+ const launch = dispatch.payload.launch_receipt;
30
+ const binding = { authority_kind: `rail_${STAGES[dispatch.type]}`, invocation_id: invocation.invocation_id,
31
+ launch_nonce: invocation.launch_nonce, terminal_id: launch.terminal_id, provider_id: invocation.provider_id,
32
+ harness_id: invocation.harness_id, model_id: invocation.model_pin.model_id,
33
+ reasoning_configuration: invocation.model_pin.reasoning_configuration };
34
+ verifyAuthority('provider_process_launch', launch, { ...binding, purpose: 'terminal_launch' });
35
+ verifyAuthority('provider_process_completion', receipt, { ...binding, purpose: 'terminal_completion',
36
+ launch_receipt_digest: sha256(launch) });
37
+ assertTerminalCompletionContainment(receipt, launch, 'RAIL_PROCESS_CONTAINMENT_MISMATCH');
38
+ if (receipt.process_time_kind !== 'provider-invocation-wall-time'
39
+ || typeof receipt.process_elapsed_ms !== 'number' || !Number.isFinite(receipt.process_elapsed_ms)
40
+ || receipt.process_elapsed_ms < 0 || !Number.isInteger(receipt.exit_status)) {
41
+ fail('RAIL_PROCESS_DURATION_REQUIRED', 'provider processing requires a signed monotonic duration and observed exit, never inferred wall-clock timestamps');
42
+ }
43
+ }
44
+
45
+ function completionReceipt(record) {
46
+ if (['provider_process_observed', 'review_decided'].includes(record.type)) return record.payload.terminal_authority_receipt;
47
+ if (record.type === 'execution_recorded') return record.payload.execution_evidence?.execution?.terminal_authority_receipt;
48
+ if (record.type === 'adjudication_observed') return record.payload.observation?.terminal_authority_receipt;
49
+ return null;
50
+ }
51
+
52
+ /** Called with a verified canonical journal by RailEngine, not caller evidence. */
53
+ export function collectProviderProcessing(records, { attempt_id, verifyAuthority }) {
54
+ const dispatches = providerProcessDispatches(records, attempt_id);
55
+ if (!dispatches.length) fail('RAIL_PROCESS_COVERAGE_INCOMPLETE', 'no dispatched provider process has been observed for this attempt');
56
+ const seenNonces = new Set();
57
+ const processes = dispatches.map((dispatch) => {
58
+ const { invocation } = dispatch.payload;
59
+ // Logical invocation IDs may be reused by a retry/resumed review round.
60
+ // The canonical launch nonce identifies the actual process invocation.
61
+ if (seenNonces.has(invocation.launch_nonce)) {
62
+ fail('RAIL_PROCESS_DISPATCH_CONFLICT', 'canonical provider dispatch identities must be unique');
63
+ }
64
+ seenNonces.add(invocation.launch_nonce);
65
+ const completions = records.filter((record) => record.payload?.attempt_id === attempt_id)
66
+ .map((record) => ({ record, receipt: completionReceipt(record) }))
67
+ .filter(({ receipt }) => receipt?.launch_nonce === invocation.launch_nonce);
68
+ if (!completions.length) {
69
+ const proof = restoredExecutionProof(records, dispatch, { verifyAuthority, requireRecovered: true });
70
+ if (!proof) fail('RAIL_PROCESS_COVERAGE_INCOMPLETE', `provider invocation ${invocation.invocation_id} has no observed completion or canonical restored recovery`);
71
+ verifyAuthority('provider_process_launch', dispatch.payload.launch_receipt, {
72
+ purpose: 'terminal_launch', authority_kind: 'rail_execution', invocation_id: invocation.invocation_id,
73
+ launch_nonce: invocation.launch_nonce, provider_id: invocation.provider_id, harness_id: invocation.harness_id,
74
+ model_id: invocation.model_pin.model_id, reasoning_configuration: invocation.model_pin.reasoning_configuration });
75
+ return { stage: STAGES[dispatch.type], dispatch_event_id: dispatch.event_id,
76
+ source_event_ids: [proof.interruption_event_id, proof.checkpoint_event_id, proof.restoration_event_id, proof.recovery_event_id],
77
+ invocation_id: invocation.invocation_id, launch_nonce: invocation.launch_nonce,
78
+ provider_id: invocation.provider_id, harness_id: invocation.harness_id,
79
+ model_id: invocation.model_pin.model_id, reasoning_configuration: invocation.model_pin.reasoning_configuration,
80
+ measurement_status: 'unavailable_interrupted', process_elapsed_ms: null, exit_status: null,
81
+ receipt_digest: null, reconciliation: proof };
82
+ }
83
+ if (records.some((record) => record.type === 'execution_worktree_restored'
84
+ && record.payload.dispatch_event_id === dispatch.event_id)) {
85
+ fail('RAIL_PROCESS_RECEIPT_CONFLICT', 'restored interruption must not also claim an observed completion');
86
+ }
87
+ const receipt = completions[0].receipt;
88
+ assertProviderProcessReceipt(dispatch, receipt, verifyAuthority);
89
+ if (completions.some((entry) => sha256(entry.receipt) !== sha256(receipt))) {
90
+ fail('RAIL_PROCESS_RECEIPT_CONFLICT', 'one provider invocation has conflicting completion receipts');
91
+ }
92
+ return { stage: STAGES[dispatch.type], dispatch_event_id: dispatch.event_id,
93
+ source_event_ids: [...new Set(completions.map((entry) => entry.record.event_id))],
94
+ invocation_id: invocation.invocation_id, launch_nonce: invocation.launch_nonce,
95
+ provider_id: invocation.provider_id, harness_id: invocation.harness_id,
96
+ model_id: invocation.model_pin.model_id, reasoning_configuration: invocation.model_pin.reasoning_configuration,
97
+ measurement_status: 'observed',
98
+ process_elapsed_ms: receipt.process_elapsed_ms, exit_status: receipt.exit_status,
99
+ receipt_digest: sha256(receipt) };
100
+ });
101
+ const observedValue = processes.filter((entry) => entry.measurement_status === 'observed')
102
+ .reduce((sum, entry) => sum + entry.process_elapsed_ms, 0);
103
+ if (!Number.isFinite(observedValue)) fail('RAIL_PROCESS_DURATION_REQUIRED', 'summed provider processing duration exceeds the finite measurement range');
104
+ const unknown = processes.filter((entry) => entry.measurement_status === 'unavailable_interrupted').map((entry) => entry.launch_nonce);
105
+ return Object.freeze(canonicalize({ schema_version: 2, evidence_class: 'observed-provider-invocation-wall-time',
106
+ attempt_id, counted_stages: [...new Set(processes.map((entry) => entry.stage))],
107
+ unit: 'milliseconds', value: unknown.length ? null : observedValue, observed_value: observedValue,
108
+ coverage: unknown.length ? 'reconciled_incomplete' : 'complete', unknown_launch_nonces: unknown, processes }));
109
+ }
110
+
111
+ /** Preserve unknown duration explicitly when converting units for completion. */
112
+ export function providerProcessingMetric(report, evidence_ref) {
113
+ return canonicalize({ unit: 'minutes', value: report.value === null ? null : report.value / 60000, evidence_ref,
114
+ coverage: { schema_version: 2, status: report.coverage, observed_value: report.observed_value / 60000,
115
+ unknown_launch_nonces: report.unknown_launch_nonces,
116
+ reconciliation_proofs: report.processes.filter((entry) => entry.measurement_status === 'unavailable_interrupted')
117
+ .map((entry) => entry.reconciliation) } });
118
+ }
@@ -0,0 +1,23 @@
1
+ import { ContractError, assertDarwinTerminalLaunch, assertDarwinTerminalCompletion } from '@chati/core';
2
+
3
+ function reject(code, message) { throw new ContractError(code, message); }
4
+ function typed(check, code) {
5
+ try { check(); } catch (error) { reject(code, `Darwin terminal contract rejected: ${error.message}`); }
6
+ }
7
+
8
+ /** Authentication remains with RailEngine's configured authority verifier. */
9
+ export function assertTerminalLaunchContainment(receipt, code) {
10
+ if (receipt.process_containment_kind === 'darwin_scoped_process_group_v1') {
11
+ typed(() => assertDarwinTerminalLaunch(receipt), code);
12
+ } else if (receipt.process_containment_kind !== 'bubblewrap_pid_namespace_v1'
13
+ || Object.hasOwn(receipt, 'darwin_launch_binding')) reject(code, 'unsupported or mixed terminal containment variant');
14
+ }
15
+
16
+ export function assertTerminalCompletionContainment(receipt, launch, code) {
17
+ if (launch.process_containment_kind === 'darwin_scoped_process_group_v1') {
18
+ typed(() => assertDarwinTerminalCompletion(receipt, launch), code);
19
+ } else if (Object.hasOwn(launch, 'darwin_launch_binding')
20
+ || Object.hasOwn(receipt, 'darwin_completion_observation') || Object.hasOwn(receipt, 'darwin_launch_binding_digest')) {
21
+ reject(code, 'Darwin completion cannot be attached to another containment variant');
22
+ }
23
+ }
@@ -1,11 +1,14 @@
1
1
  import { createPublicKey, randomUUID, verify as verifySignature } from 'node:crypto';
2
2
  import {
3
- closeSync, constants, existsSync, fstatSync, fsyncSync, ftruncateSync, mkdirSync,
4
- lstatSync, openSync, readSync, renameSync, unlinkSync, writeSync,
3
+ closeSync, constants, fstatSync, fsyncSync, ftruncateSync,
4
+ openSync, readSync, writeSync,
5
5
  } from 'node:fs';
6
6
  import { hostname } from 'node:os';
7
- import { basename, dirname, join, parse, resolve, sep } from 'node:path';
8
- import { ContractError, canonicalize, sha256 } from '@chati/core';
7
+ import { basename, dirname, parse, resolve, sep } from 'node:path';
8
+ import {
9
+ captureHostBootMarker, ContractError, canonicalize, hostBootMarkerChanged, sha256, openAtSync, mkdirAtSync,
10
+ lstatAtSync, renameAtSync, unlinkAtSync,
11
+ } from '@chati/core';
9
12
 
10
13
  const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
11
14
  const RELEASE_STATE_MAX_BYTES = 64 * 1024 * 1024;
@@ -324,16 +327,12 @@ function sameSnapshot(left, right) {
324
327
  && left?.ctimeMs === right?.ctimeMs;
325
328
  }
326
329
 
327
- function descriptorDirectoryPath(descriptor) {
328
- return `/proc/self/fd/${descriptor}`;
329
- }
330
-
331
330
  function openPinnedReleaseDirectory(path, { create = false } = {}) {
332
331
  const absolute = resolve(path);
333
- if (process.platform !== 'linux') {
332
+ if (!['linux', 'darwin'].includes(process.platform)) {
334
333
  fail(
335
334
  'UNSUPPORTED_RELEASE_STATE_PLATFORM',
336
- 'durable release state requires descriptor-relative filesystem access, which is supported only on Linux',
335
+ `durable release state requires descriptor-relative filesystem access; unsupported platform: ${process.platform}`,
337
336
  );
338
337
  }
339
338
  const root = parse(absolute).root;
@@ -341,10 +340,9 @@ function openPinnedReleaseDirectory(path, { create = false } = {}) {
341
340
  let descriptor = openSync(root, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
342
341
  try {
343
342
  for (const segment of segments) {
344
- const childPath = join(descriptorDirectoryPath(descriptor), segment);
345
343
  let childDescriptor;
346
344
  try {
347
- childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
345
+ childDescriptor = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
348
346
  } catch (error) {
349
347
  if (error?.code !== 'ENOENT' || !create) {
350
348
  if (['ELOOP', 'ENOTDIR'].includes(error?.code)) {
@@ -352,9 +350,9 @@ function openPinnedReleaseDirectory(path, { create = false } = {}) {
352
350
  }
353
351
  throw error;
354
352
  }
355
- try { mkdirSync(childPath, { mode: 0o700 }); }
353
+ try { mkdirAtSync(descriptor, segment, { mode: 0o700 }); }
356
354
  catch (mkdirError) { if (mkdirError?.code !== 'EEXIST') throw mkdirError; }
357
- childDescriptor = openSync(childPath, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
355
+ childDescriptor = openAtSync(descriptor, segment, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
358
356
  }
359
357
  const childStat = fstatSync(childDescriptor);
360
358
  if (!childStat.isDirectory()) {
@@ -366,7 +364,6 @@ function openPinnedReleaseDirectory(path, { create = false } = {}) {
366
364
  }
367
365
  return Object.freeze({
368
366
  absolute, descriptor, identity: fstatSync(descriptor),
369
- reference: descriptorDirectoryPath(descriptor),
370
367
  });
371
368
  } catch (error) {
372
369
  closeSync(descriptor);
@@ -388,15 +385,11 @@ function assertPinnedReleaseDirectory(storage) {
388
385
  closeSync(canonical.descriptor);
389
386
  }
390
387
 
391
- function releaseEntryPath(storage, name) {
388
+ function openReleaseEntry(storage, name, flags, mode) {
392
389
  if (basename(name) !== name || name === '.' || name === '..') {
393
390
  fail('UNSAFE_RELEASE_STATE_FILE', 'release state entry must be one path segment');
394
391
  }
395
- return join(storage.reference, name);
396
- }
397
-
398
- function openReleaseEntry(storage, name, flags, mode) {
399
- try { return openSync(releaseEntryPath(storage, name), flags, mode); }
392
+ try { return openAtSync(storage.descriptor, name, flags, mode); }
400
393
  catch (error) {
401
394
  if (['ELOOP', 'ENXIO', 'EISDIR'].includes(error?.code)) {
402
395
  fail('UNSAFE_RELEASE_STATE_FILE', 'release state must not be a symlink, FIFO or directory');
@@ -471,8 +464,6 @@ function writeReleaseCheckpoint(storage, stateName, checkpoint) {
471
464
  if (bytes.length > RELEASE_CHECKPOINT_MAX_BYTES) fail('INVALID_RELEASE_STATE', 'release checkpoint exceeds its safety limit');
472
465
  const targetName = checkpointName(stateName);
473
466
  const temporaryName = `${targetName}.${process.pid}.${randomUUID()}.tmp`;
474
- const temporaryPath = releaseEntryPath(storage, temporaryName);
475
- const targetPath = releaseEntryPath(storage, targetName);
476
467
  let fd;
477
468
  try {
478
469
  assertPinnedReleaseDirectory(storage);
@@ -487,12 +478,12 @@ function writeReleaseCheckpoint(storage, stateName, checkpoint) {
487
478
  closeSync(fd);
488
479
  fd = undefined;
489
480
  assertPinnedReleaseDirectory(storage);
490
- renameSync(temporaryPath, targetPath);
481
+ renameAtSync(storage.descriptor, temporaryName, storage.descriptor, targetName);
491
482
  fsyncSync(storage.descriptor);
492
483
  assertPinnedReleaseDirectory(storage);
493
484
  } finally {
494
485
  if (fd !== undefined) closeSync(fd);
495
- try { unlinkSync(temporaryPath); } catch { /* temporary may already be renamed or absent */ }
486
+ try { unlinkAtSync(storage.descriptor, temporaryName); } catch { /* temporary may already be renamed or absent */ }
496
487
  }
497
488
  }
498
489
 
@@ -543,22 +534,24 @@ function readLockOwner(storage, name, { assertCanonical = true } = {}) {
543
534
 
544
535
  function recoverStaleLock(storage, name) {
545
536
  assertPinnedReleaseDirectory(storage);
546
- const path = releaseEntryPath(storage, name);
547
537
  let stat;
548
- try { stat = lstatSync(path); } catch (error) { return error?.code === 'ENOENT'; }
538
+ try { stat = lstatAtSync(storage.descriptor, name); } catch (error) { return error?.code === 'ENOENT'; }
549
539
  if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) return false;
550
540
  const owner = readLockOwner(storage, name);
551
- if (owner?.hostname === hostname() && processIsAlive(owner.pid)) return false;
541
+ // A rebooted host may recycle the owner's pid onto an unrelated live process.
542
+ // Prove the reboot from the boot marker before ever trusting PID liveness.
543
+ if (owner?.hostname === hostname() && !hostBootMarkerChanged(owner.host_boot_marker, captureHostBootMarker())
544
+ && processIsAlive(owner.pid)) return false;
552
545
  // A PID from another host cannot be proven dead locally. Never steal a
553
546
  // remote lock automatically; explicit operator reconciliation is required.
554
547
  if (owner?.hostname && owner.hostname !== hostname()) return false;
555
548
  if (!owner && Date.now() - stat.mtimeMs < RELEASE_LOCK_STALE_AFTER_MS) return false;
556
549
  const recoveryName = `${name}.recovery`;
557
- const recoveryPath = releaseEntryPath(storage, recoveryName);
558
550
  let recoveryFd;
559
551
  try {
560
- recoveryFd = openSync(recoveryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
561
- const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: hostname(), token: randomUUID() }));
552
+ recoveryFd = openAtSync(storage.descriptor, recoveryName, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
553
+ const recoveryMarker = captureHostBootMarker();
554
+ const bytes = Buffer.from(JSON.stringify({ pid: process.pid, hostname: hostname(), token: randomUUID(), ...(recoveryMarker ? { host_boot_marker: recoveryMarker } : {}) }));
562
555
  writeSync(recoveryFd, bytes, 0, bytes.length);
563
556
  fsyncSync(recoveryFd);
564
557
  } catch {
@@ -566,24 +559,28 @@ function recoverStaleLock(storage, name) {
566
559
  return false;
567
560
  }
568
561
  const quarantineName = `${name}.stale.${process.pid}.${randomUUID()}`;
569
- const quarantine = releaseEntryPath(storage, quarantineName);
570
562
  try {
571
- const current = lstatSync(path);
563
+ const current = lstatAtSync(storage.descriptor, name);
572
564
  const currentOwner = readLockOwner(storage, name);
573
565
  if (!sameIdentity(stat, current) || currentOwner?.token !== owner?.token) return false;
574
- renameSync(path, quarantine);
575
- const quarantined = lstatSync(quarantine);
566
+ renameAtSync(storage.descriptor, name, storage.descriptor, quarantineName);
567
+ const quarantined = lstatAtSync(storage.descriptor, quarantineName);
576
568
  const quarantinedOwner = readLockOwner(storage, quarantineName);
577
569
  if (!sameIdentity(stat, quarantined) || quarantinedOwner?.token !== owner?.token) {
578
- try { if (!existsSync(path)) renameSync(quarantine, path); } catch { /* preserve quarantine fail-closed */ }
570
+ try { lstatAtSync(storage.descriptor, name); }
571
+ catch (error) {
572
+ if (error?.code === 'ENOENT') {
573
+ try { renameAtSync(storage.descriptor, quarantineName, storage.descriptor, name); } catch { /* preserve quarantine fail-closed */ }
574
+ }
575
+ }
579
576
  return false;
580
577
  }
581
- unlinkSync(quarantine);
578
+ unlinkAtSync(storage.descriptor, quarantineName);
582
579
  return true;
583
580
  } catch (error) { return error?.code === 'ENOENT'; }
584
581
  finally {
585
582
  if (recoveryFd !== undefined) closeSync(recoveryFd);
586
- try { unlinkSync(recoveryPath); } catch { /* fail closed if recovery ownership changed */ }
583
+ try { unlinkAtSync(storage.descriptor, recoveryName); } catch { /* fail closed if recovery ownership changed */ }
587
584
  }
588
585
  }
589
586
 
@@ -591,7 +588,6 @@ function acquireReleaseLock(statePath) {
591
588
  const storage = openPinnedReleaseDirectory(dirname(statePath), { create: true });
592
589
  const stateName = basename(statePath);
593
590
  const name = lockName(stateName);
594
- const path = releaseEntryPath(storage, name);
595
591
  let fd;
596
592
  let token;
597
593
  try {
@@ -599,7 +595,7 @@ function acquireReleaseLock(statePath) {
599
595
  assertPinnedReleaseDirectory(storage);
600
596
  let candidate;
601
597
  try {
602
- candidate = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
598
+ candidate = openAtSync(storage.descriptor, name, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW || 0), 0o600);
603
599
  } catch (error) {
604
600
  if (error?.code !== 'EEXIST') throw error;
605
601
  if (recoverStaleLock(storage, name)) continue;
@@ -608,7 +604,8 @@ function acquireReleaseLock(statePath) {
608
604
  }
609
605
  token = `${process.pid}-${Date.now()}-${randomUUID()}`;
610
606
  try {
611
- const bytes = Buffer.from(JSON.stringify({ schema_version: 1, pid: process.pid, hostname: hostname(), token, acquired_at: new Date().toISOString() }));
607
+ const ownerMarker = captureHostBootMarker();
608
+ const bytes = Buffer.from(JSON.stringify({ schema_version: 1, pid: process.pid, hostname: hostname(), token, acquired_at: new Date().toISOString(), ...(ownerMarker ? { host_boot_marker: ownerMarker } : {}) }));
612
609
  writeSync(candidate, bytes, 0, bytes.length);
613
610
  fsyncSync(candidate);
614
611
  fsyncSync(storage.descriptor);
@@ -616,7 +613,7 @@ function acquireReleaseLock(statePath) {
616
613
  break;
617
614
  } catch (error) {
618
615
  closeSync(candidate);
619
- try { unlinkSync(path); } catch { /* the acquisition failed closed */ }
616
+ try { unlinkAtSync(storage.descriptor, name); } catch { /* the acquisition failed closed */ }
620
617
  throw error;
621
618
  }
622
619
  }
@@ -633,7 +630,7 @@ function acquireReleaseLock(statePath) {
633
630
  try { assertPinnedReleaseDirectory(storage); } catch (error) { safetyError = error; }
634
631
  try {
635
632
  if (readLockOwner(storage, name, { assertCanonical: false })?.token === token) {
636
- unlinkSync(path);
633
+ unlinkAtSync(storage.descriptor, name);
637
634
  fsyncSync(storage.descriptor);
638
635
  }
639
636
  } finally { closeSync(storage.descriptor); }
@@ -7,6 +7,21 @@ const DISPOSITIONS = new Set(['accepted', 'rejected', 'unresolved']);
7
7
  const VERDICTS = new Set(['accept', 'rework', 'required-human-decision', 'blocked']);
8
8
  const AUTHORITY_REASONS = new Set(['scope', 'security', 'privacy', 'cost', 'external-action']);
9
9
  const IMPASSE_KINDS = new Set(['none', 'technical']);
10
+ const COUNCIL_REVIEW_VERDICTS = new Set(['accept', 'rework']);
11
+ const COUNCIL_BLOCKING_SEVERITIES = new Set(['critical', 'high']);
12
+
13
+ /**
14
+ * Product-level default for planning-quality gates. It deliberately expresses
15
+ * a range rather than inventing a third provider when an installation has
16
+ * only two selected providers. One-provider installations cannot satisfy the
17
+ * autonomous council quorum.
18
+ */
19
+ export const DEFAULT_COUNCIL_POLICY = Object.freeze({
20
+ min_members: 2,
21
+ max_members: 3,
22
+ require_distinct_providers: true,
23
+ blocking_severities: Object.freeze(['critical', 'high']),
24
+ });
10
25
 
11
26
  function fail(code, message, details = {}) {
12
27
  throw new ContractError(code, message, details);
@@ -126,6 +141,214 @@ function nearestReviewReasoning(binding, model) {
126
141
  })[0] || null;
127
142
  }
128
143
 
144
+ function reviewPriority(model) {
145
+ return model.routing_priority?.['council:review']
146
+ ?? model.routing_priority?.['planning:council']
147
+ ?? model.routing_priority?.review
148
+ ?? 0;
149
+ }
150
+
151
+ function validateCouncilPolicy(policy = DEFAULT_COUNCIL_POLICY) {
152
+ assertObject(policy, 'INVALID_COUNCIL_POLICY', 'council policy');
153
+ const min = policy.min_members;
154
+ const max = policy.max_members;
155
+ if (!Number.isInteger(min) || min < 2) {
156
+ fail('INVALID_COUNCIL_POLICY', 'council policy requires at least two members');
157
+ }
158
+ if (!Number.isInteger(max) || max < min || max > 3) {
159
+ fail('INVALID_COUNCIL_POLICY', 'council policy max_members must be between min_members and 3');
160
+ }
161
+ if (policy.require_distinct_providers !== true) {
162
+ fail('INVALID_COUNCIL_POLICY', 'council policy requires distinct providers');
163
+ }
164
+ const severities = policy.blocking_severities;
165
+ if (!Array.isArray(severities) || severities.length !== COUNCIL_BLOCKING_SEVERITIES.size
166
+ || !severities.every((severity) => COUNCIL_BLOCKING_SEVERITIES.has(severity))
167
+ || new Set(severities).size !== COUNCIL_BLOCKING_SEVERITIES.size) {
168
+ fail('INVALID_COUNCIL_POLICY', 'council blocking severities must contain exactly critical and high');
169
+ }
170
+ return freeze(canonicalize({
171
+ min_members: min,
172
+ max_members: max,
173
+ require_distinct_providers: true,
174
+ blocking_severities: [...new Set(severities)].sort(),
175
+ }));
176
+ }
177
+
178
+ /** Validates the public policy without selecting a provider. */
179
+ export function validateCouncilGatePolicy(policy) {
180
+ return validateCouncilPolicy(policy);
181
+ }
182
+
183
+ function selectBestReviewerPerProvider({ installation, snapshot, builder_identity, clock }) {
184
+ assertObject(installation, 'INVALID_COUNCIL_SELECTION', 'installation');
185
+ assertObject(snapshot, 'INVALID_COUNCIL_SELECTION', 'snapshot');
186
+ if (builder_identity !== undefined) validateIdentity(builder_identity, 'builder_identity');
187
+ const byProvider = new Map();
188
+ for (const binding of installation.enabled_providers || []) {
189
+ for (const modelId of binding.allowed_models || []) {
190
+ const model = snapshot.models?.find((candidate) =>
191
+ candidate.provider_id === binding.provider_id && candidate.model_id === modelId);
192
+ if (!model?.actions?.includes('review')) continue;
193
+ const reasoning = nearestReviewReasoning(binding, model);
194
+ if (!reasoning) continue;
195
+ const reviewer_identity = { provider_id: binding.provider_id, model_id: model.model_id };
196
+ const invocation = {
197
+ provider_id: binding.provider_id,
198
+ harness_id: binding.harness_id,
199
+ action: 'review',
200
+ model_pin: {
201
+ model_id: model.model_id,
202
+ catalog_snapshot_ref: snapshot.snapshot_id,
203
+ reasoning_configuration: reasoning,
204
+ },
205
+ };
206
+ assertEligibleInvocation({ installation, snapshot, invocation, clock });
207
+ const candidate = {
208
+ invocation,
209
+ reviewer_identity,
210
+ priority: reviewPriority(model),
211
+ adjudication_priority: model.adjudication_priority ?? 0,
212
+ independence_level: builder_identity
213
+ ? computeIndependenceLevel({ builder_identity, reviewer_identity })
214
+ : null,
215
+ };
216
+ const current = byProvider.get(binding.provider_id);
217
+ if (!current
218
+ || candidate.priority > current.priority
219
+ || (candidate.priority === current.priority && candidate.adjudication_priority > current.adjudication_priority)
220
+ || (candidate.priority === current.priority && candidate.adjudication_priority === current.adjudication_priority
221
+ && `${candidate.invocation.harness_id}:${candidate.reviewer_identity.model_id}`
222
+ .localeCompare(`${current.invocation.harness_id}:${current.reviewer_identity.model_id}`) < 0)) {
223
+ byProvider.set(binding.provider_id, candidate);
224
+ }
225
+ }
226
+ }
227
+ return [...byProvider.values()].sort((left, right) =>
228
+ right.priority - left.priority
229
+ || right.adjudication_priority - left.adjudication_priority
230
+ || `${left.invocation.provider_id}:${left.invocation.harness_id}:${left.reviewer_identity.model_id}`
231
+ .localeCompare(`${right.invocation.provider_id}:${right.invocation.harness_id}:${right.reviewer_identity.model_id}`)
232
+ );
233
+ }
234
+
235
+ /**
236
+ * Selects the autonomous planning council from the installation allow-list.
237
+ * It chooses at most one model per provider and never pads the council with an
238
+ * unselected vendor. Two selected providers are sufficient; a third improves
239
+ * coverage when available.
240
+ */
241
+ export function selectCouncilReviewers({
242
+ installation,
243
+ snapshot,
244
+ builder_identity,
245
+ policy = DEFAULT_COUNCIL_POLICY,
246
+ clock = () => new Date(),
247
+ } = {}) {
248
+ const normalizedPolicy = validateCouncilPolicy(policy);
249
+ const candidates = selectBestReviewerPerProvider({ installation, snapshot, builder_identity, clock });
250
+ if (candidates.length < normalizedPolicy.min_members) {
251
+ fail(
252
+ 'COUNCIL_QUORUM_UNAVAILABLE',
253
+ `autonomous council requires ${normalizedPolicy.min_members} distinct selected providers with review capability, but only ${candidates.length} are eligible`,
254
+ {
255
+ required_members: normalizedPolicy.min_members,
256
+ eligible_providers: candidates.map(({ invocation }) => invocation.provider_id),
257
+ },
258
+ );
259
+ }
260
+ const members = candidates.slice(0, normalizedPolicy.max_members).map((candidate, index) => ({
261
+ member_id: `council-${index + 1}-${candidate.invocation.provider_id}-${candidate.reviewer_identity.model_id}`,
262
+ ...candidate.invocation,
263
+ reviewer_identity: candidate.reviewer_identity,
264
+ ...(candidate.independence_level ? { independence_level: candidate.independence_level } : {}),
265
+ }));
266
+ return freeze(canonicalize({
267
+ policy: normalizedPolicy,
268
+ member_count: members.length,
269
+ members,
270
+ }));
271
+ }
272
+
273
+ function validateCouncilFinding(finding) {
274
+ validateFinding(finding);
275
+ return finding;
276
+ }
277
+
278
+ /**
279
+ * Validates one member result. Low and medium findings are durable evidence
280
+ * but do not turn an otherwise successful council decision into rework.
281
+ */
282
+ export function validateCouncilMemberReview(result, { blocking_severities = DEFAULT_COUNCIL_POLICY.blocking_severities } = {}) {
283
+ assertObject(result, 'INVALID_COUNCIL_REVIEW', 'council member review');
284
+ assertString(result.member_id, 'INVALID_COUNCIL_REVIEW', 'council member review.member_id');
285
+ validateIdentity(result.reviewer_identity, 'council member review.reviewer_identity');
286
+ if (!COUNCIL_REVIEW_VERDICTS.has(result.verdict)) {
287
+ fail('INVALID_COUNCIL_REVIEW', 'council member review.verdict must be accept or rework');
288
+ }
289
+ if (!Array.isArray(result.findings)) fail('INVALID_COUNCIL_REVIEW', 'council member review.findings must be an array');
290
+ const findingIds = new Set();
291
+ for (const finding of result.findings) {
292
+ validateCouncilFinding(finding);
293
+ if (findingIds.has(finding.finding_id)) {
294
+ fail('DUPLICATE_COUNCIL_FINDING_ID', `council member ${result.member_id} returned finding ${finding.finding_id} more than once`);
295
+ }
296
+ findingIds.add(finding.finding_id);
297
+ }
298
+ const acceptedBlockingFindings = result.findings.filter((finding) =>
299
+ finding.disposition === 'accepted' && blocking_severities.includes(finding.severity));
300
+ if (acceptedBlockingFindings.length > 0 && result.verdict !== 'rework') {
301
+ fail('COUNCIL_BLOCKER_REQUIRES_REWORK', 'an accepted high or critical council finding requires rework');
302
+ }
303
+ return freeze(canonicalize(result));
304
+ }
305
+
306
+ /**
307
+ * Applies unanimity to absence of blockers. This is intentionally not a vote:
308
+ * any accepted high or critical finding blocks, while lower-severity findings
309
+ * stay in the immutable decision trail without blocking the gate.
310
+ */
311
+ export function decideCouncilGate({ selection, reviews } = {}) {
312
+ assertObject(selection, 'INVALID_COUNCIL_SELECTION', 'council selection');
313
+ const policy = validateCouncilPolicy(selection.policy);
314
+ if (!Array.isArray(selection.members) || selection.members.length < policy.min_members || selection.members.length > policy.max_members) {
315
+ fail('INVALID_COUNCIL_SELECTION', 'council selection does not satisfy its policy quorum');
316
+ }
317
+ if (!Array.isArray(reviews)) fail('INVALID_COUNCIL_REVIEW', 'council reviews must be an array');
318
+ const expected = new Map(selection.members.map((member) => [member.member_id, member]));
319
+ if (reviews.length !== expected.size) {
320
+ fail('COUNCIL_QUORUM_INCOMPLETE', 'every selected council member must return one result before the gate can decide');
321
+ }
322
+ const seen = new Set();
323
+ const acceptedBlockingFindings = [];
324
+ const nonBlockingFindings = [];
325
+ const normalizedReviews = reviews.map((result) => {
326
+ const validated = validateCouncilMemberReview(result, { blocking_severities: policy.blocking_severities });
327
+ const member = expected.get(validated.member_id);
328
+ if (!member) fail('COUNCIL_MEMBER_UNEXPECTED', `review returned by unselected council member ${validated.member_id}`);
329
+ if (seen.has(validated.member_id)) fail('DUPLICATE_COUNCIL_MEMBER_REVIEW', `council member ${validated.member_id} returned more than once`);
330
+ seen.add(validated.member_id);
331
+ if (member.reviewer_identity.provider_id !== validated.reviewer_identity.provider_id
332
+ || member.reviewer_identity.model_id !== validated.reviewer_identity.model_id) {
333
+ fail('COUNCIL_MEMBER_IDENTITY_MISMATCH', `council member ${validated.member_id} did not use its selected model`);
334
+ }
335
+ for (const finding of validated.findings) {
336
+ const entry = { member_id: validated.member_id, ...finding };
337
+ if (finding.disposition === 'accepted' && policy.blocking_severities.includes(finding.severity)) acceptedBlockingFindings.push(entry);
338
+ else if (finding.disposition === 'accepted') nonBlockingFindings.push(entry);
339
+ }
340
+ return validated;
341
+ });
342
+ if (seen.size !== expected.size) fail('COUNCIL_QUORUM_INCOMPLETE', 'one or more selected council members did not return a result');
343
+ return freeze(canonicalize({
344
+ verdict: acceptedBlockingFindings.length === 0 ? 'accept' : 'rework',
345
+ member_count: selection.members.length,
346
+ reviews: normalizedReviews,
347
+ blocking_findings: acceptedBlockingFindings,
348
+ non_blocking_findings: nonBlockingFindings,
349
+ }));
350
+ }
351
+
129
352
  /** Selects the one canonical reviewer from the signed selected-only capability projection. */
130
353
  export function selectCanonicalReviewer({ installation, snapshot, builder_identity, clock = () => new Date() } = {}) {
131
354
  assertObject(installation, 'INVALID_REVIEW_SELECTION', 'installation');