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,278 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { canonicalSerialize, sha256 } from '@chati/core';
5
+ import { validatePlanningMaterial, validateTaskVerificationPlan, validateTaskBrowserVerificationPlan } from '@chati/planning';
6
+ import { validateBrowserLaunchRequest } from '@chati/browser-capability';
7
+ import { browserEvidenceMaterial, completionMetricsReference, effortJournalDigest, providerProcessingMetric } from '@chati/rail';
8
+ import { delegatedAuthorityPublicIdentity, signDelegatedAuthorityStatement } from '../installer-v2/installation-authority.js';
9
+ import { persistImmutableInstallationFile, readSafeInstallationStorageFile } from '../installer-v2/catalog-client.js';
10
+ import { runIsolatedVerificationCheck } from '../terminal/spawner.js';
11
+ import { verificationProcessCompleted } from '../terminal/verification-process-facts.js';
12
+ import { runIsolatedBrowserObservation } from '../terminal/browser-observer.js';
13
+ import { browserObservationCompleted } from '../terminal/browser-process-facts.js';
14
+ import { assertRailReadOnlyCommitWorkspaceUnchanged, cleanupRailReadOnlyCommitWorkspace, createRailReadOnlyCommitWorkspace } from '../terminal/rail-readonly-workspace.js';
15
+ import { loadRuntimeInstallationV2, resolveRuntimeCouncilGatePolicyV2 } from './runtime-installation-v2.js';
16
+ import { assertCouncilGateApproved } from './council-gate-runtime.js';
17
+ import { assertRailHandoffCouncilBinding, collectRailProviderProcessing, resolveRailAttemptContext } from './rail-runtime.js';
18
+ import { collectRailHumanEffort } from './rail-human-effort.js';
19
+ import { resolveReviewGitSubject } from './review-runtime.js';
20
+ import { signedRailEvidencePayload, verifyRailEvidenceAuthorityCertificate, verifyRailEvidenceReceipt } from './rail-evidence-authority.js';
21
+ import { assertLocalBrowserArtifacts, browserRetentionPolicy, persistBrowserArtifact, pruneBrowserArtifacts } from './browser-artifact-store.js';
22
+
23
+ function fail(code, message) { throw Object.assign(new Error(message), { code }); }
24
+ function timestamp(clock) {
25
+ const value = new Date(clock());
26
+ if (!Number.isFinite(value.getTime())) fail('RAIL_VERIFICATION_TIME_INVALID', 'verification clock must be valid');
27
+ return value.toISOString();
28
+ }
29
+
30
+ function loadCertifiedIdentity({ projectDir, artifact, authorityDirectory, clock }) {
31
+ const source = readSafeInstallationStorageFile(join(projectDir, '.chati/v2/rail/evidence-authority.json'), {
32
+ allowMissing: true, maxBytes: 32 * 1024, label: 'RAIL evidence certificate',
33
+ });
34
+ if (source === null) fail('RAIL_EVIDENCE_AUTHORITY_NOT_CONFIGURED', 'enroll an approved CHATI-signed evidence authority before running local verification');
35
+ let certificate;
36
+ try { certificate = JSON.parse(source.content); }
37
+ catch { fail('INVALID_RAIL_EVIDENCE_CERTIFICATE', 'RAIL evidence certificate is not valid JSON'); }
38
+ const verifier = verifyRailEvidenceAuthorityCertificate({ certificate,
39
+ expected_project_id: artifact.project_id,
40
+ root_public_key: readFileSync(new URL('../installer/signing-public-key.pem', import.meta.url), 'utf8'), clock });
41
+ const signingOptions = { projectDir, projectId: artifact.project_id, kind: 'rail-evidence', authorityDirectory };
42
+ const identity = delegatedAuthorityPublicIdentity(signingOptions);
43
+ if (certificate.issuer_id !== identity.issuer_id || certificate.public_key !== identity.public_key) {
44
+ fail('RAIL_VERIFICATION_ISSUER_MISMATCH', 'enrolled certificate does not certify this local delegated identity');
45
+ }
46
+ return { verifier, identity, signingOptions, certificate_digest: sha256(certificate) };
47
+ }
48
+
49
+ function canonicalContext(options) {
50
+ const { projectDir, handoff_id, attempt_id, clock } = options;
51
+ const artifact = loadRuntimeInstallationV2(projectDir, { clock });
52
+ if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'local verification requires a verified v2 installation');
53
+ const context = resolveRailAttemptContext({ projectDir, handoff_id, attempt_id, clock });
54
+ if (context.handoff.project_id !== artifact.project_id) fail('RAIL_VERIFICATION_PROJECT_MISMATCH', 'handoff belongs to another installation');
55
+ if (!['active', 'ready_to_merge'].includes(context.state)) fail('RAIL_VERIFICATION_ATTEMPT_INACTIVE', `attempt is ${context.state}`);
56
+ const material = validatePlanningMaterial(context.handoff, { required: true });
57
+ const plan = validateTaskVerificationPlan(context.task, { required: true });
58
+ // Same council gate as RAIL dispatch: a guided (single-provider) installation
59
+ // must be reconfigured, while an installation whose contract predates the
60
+ // council policy keeps the dispatch semantics instead of a stricter gate.
61
+ const council = resolveRuntimeCouncilGatePolicyV2({ artifact, stage: 'execution-design' });
62
+ if (!council.enabled && council.reason === 'GUIDED_COUNCIL_REQUIRED') {
63
+ fail('GUIDED_COUNCIL_REQUIRED', 'local verification requires installation reconfiguration with two selected review providers');
64
+ }
65
+ const approval = council.enabled
66
+ ? assertCouncilGateApproved({ projectDir, stage: 'execution-design', artifact,
67
+ policy: council.policy, handoff: context.handoff, clock })
68
+ : null;
69
+ if (approval) assertRailHandoffCouncilBinding({ projectDir, handoff: context.handoff, approval });
70
+ const subject = resolveReviewGitSubject({ projectDir, attempt_id, clock });
71
+ const authority = loadCertifiedIdentity({ ...options, artifact });
72
+ const browser = canonicalBrowserContext(context);
73
+ const effort = artifact.installation.external_integrations?.clickup === 'required' ? {
74
+ human: collectRailHumanEffort({ projectDir, attempt_id }),
75
+ processing: collectRailProviderProcessing({ projectDir, attempt_id, clock }),
76
+ source_journal_digest: effortJournalDigest(context.records, attempt_id),
77
+ rework_review_refs: context.records.filter((record) => record.type === 'review_decided'
78
+ && record.payload.attempt_id === attempt_id && record.payload.decision?.decision === 'normal-rework')
79
+ .map((record) => record.payload.review_ref),
80
+ } : null;
81
+ return { context, plan, material, subject, authority, effort, browser,
82
+ binding_digest: sha256({ installation: artifact, subject, plan, browser, effort, certificate_digest: authority.certificate_digest }) };
83
+ }
84
+
85
+ function canonicalBrowserContext(context) {
86
+ if (!context.task.requires_browser) return null;
87
+ const plan = validateTaskBrowserVerificationPlan(context.task, { required: true });
88
+ const policy = context.task.browser_policy;
89
+ browserRetentionPolicy(policy.artifact_policy.retention_class);
90
+ const manifest = [...context.records].reverse().find((record) => record.type === 'execution_dispatched'
91
+ && record.payload.attempt_id === context.attempt.attempt_id && record.payload.run_kind === 'initial'
92
+ && record.payload.browser_manifest)?.payload.browser_manifest;
93
+ const core = { ...manifest }; delete core.manifest_hash;
94
+ if (!manifest || manifest.manifest_type !== 'BrowserDelegationManifest' || manifest.schema_version !== 1
95
+ || manifest.evidence_class !== 'delegation-only' || manifest.manifest_hash !== sha256(core)
96
+ || manifest.delegation?.status !== 'delegated' || manifest.delegation?.evidence_status !== 'required'
97
+ || manifest.task_id !== context.task.task_id || manifest.policy_ref !== policy.policy_id
98
+ || manifest.mode !== policy.mode || manifest.tool !== policy.tool || manifest.profile_isolation !== policy.profile_isolation
99
+ || manifest.allowed_domain !== plan.observation.hostname || manifest.action_confirmation !== policy.action_confirmation
100
+ || sha256(manifest.artifact_policy) !== sha256(policy.artifact_policy)) {
101
+ fail('BROWSER_VERIFICATION_DELEGATION_INVALID', 'real observation requires the exact canonical delegation, approved tool policy and local scenario hostname');
102
+ }
103
+ validateBrowserLaunchRequest({ browser_session_id: manifest.browser_session_id, task_id: manifest.task_id,
104
+ url: `http://${manifest.allowed_domain}:${plan.observation.application.port}${plan.observation.steps[0].path}`,
105
+ classification: manifest.classification, ...(manifest.tenant_ref ? { tenant_ref: manifest.tenant_ref } : {}),
106
+ ...(manifest.action_confirmation_ref ? { action_confirmation_ref: manifest.action_confirmation_ref } : {}) }, policy);
107
+ return { plan, policy, manifest };
108
+ }
109
+
110
+ function persistReport(projectDir, record) {
111
+ const digest = sha256(record);
112
+ const report_path = `.chati/v2/rail/verifications/${digest}.json`;
113
+ persistImmutableInstallationFile(join(projectDir, report_path), `${canonicalSerialize(record)}\n`, {
114
+ label: 'immutable RAIL verification report', conflictCode: 'RAIL_VERIFICATION_REPORT_CONFLICT',
115
+ });
116
+ return { report_path, report_ref: `rail-verification://sha256/${digest}` };
117
+ }
118
+
119
+ /** Canonical observer: no caller-selected command, commit, result or verdict. */
120
+ export async function observeRailVerification(input = {}) {
121
+ const allowed = ['projectDir', 'handoff_id', 'attempt_id', 'authorityDirectory', 'clock'];
122
+ if (!input || typeof input !== 'object' || Array.isArray(input)
123
+ || Object.keys(input).some((key) => !allowed.includes(key))
124
+ || ['projectDir', 'handoff_id', 'attempt_id'].some((key) => typeof input[key] !== 'string' || !input[key].trim())
125
+ || (input.clock !== undefined && typeof input.clock !== 'function')) {
126
+ fail('RAIL_VERIFICATION_INPUT_INVALID', 'verification accepts only the project, canonical handoff and attempt identifiers');
127
+ }
128
+ const options = { ...input, projectDir: resolve(input.projectDir), clock: input.clock ?? (() => new Date()) };
129
+ const initial = canonicalContext(options);
130
+ const scripts = new Map(initial.material.artifacts.map((entry) => [entry.ref, entry.content]));
131
+ const run_id = `verification-${randomUUID()}`;
132
+ const started_at = timestamp(options.clock);
133
+ const checks = [];
134
+ if (initial.browser) {
135
+ const cleanup = pruneBrowserArtifacts({ projectDir: options.projectDir, projectId: initial.context.handoff.project_id, clock: options.clock });
136
+ if (!cleanup.ok) fail('BROWSER_ARTIFACT_PRUNE_FAILED', `browser store has unresolved collection failures: ${cleanup.failures.map((item) => item.code).join(', ')}`);
137
+ }
138
+ for (const check of initial.plan.checks) {
139
+ let workspace;
140
+ let observed;
141
+ let errorCode = null;
142
+ let completionProven = false;
143
+ try {
144
+ workspace = createRailReadOnlyCommitWorkspace({ projectDir: options.projectDir,
145
+ baseCommitRef: initial.subject.base_commit_ref, resultCommitRef: initial.subject.result_commit_ref,
146
+ verificationSubject: true });
147
+ assertRailReadOnlyCommitWorkspaceUnchanged({ workspace, label: 'verification pre-spawn' });
148
+ observed = await runIsolatedVerificationCheck({ workingDir: workspace.subject_dir,
149
+ executableBinding: check.executable_binding, args: check.args,
150
+ stdin: scripts.get(check.stdin_ref), timeoutMs: check.timeout_ms, subjectReadOnly: true });
151
+ completionProven = verificationProcessCompleted(observed, { workingDir: workspace.subject_dir,
152
+ executableBinding: check.executable_binding, args: check.args, stdin: scripts.get(check.stdin_ref) });
153
+ } catch (error) { errorCode = error.code || 'RAIL_VERIFICATION_CHECK_FAILED'; }
154
+ finally {
155
+ if (workspace && process.platform !== 'darwin') {
156
+ try { cleanupRailReadOnlyCommitWorkspace(workspace); }
157
+ catch (error) { errorCode ??= error.code || 'RAIL_VERIFICATION_CLEANUP_FAILED'; }
158
+ }
159
+ }
160
+ const passed = !errorCode && observed?.exit_code === 0 && observed.signal === null
161
+ && observed.timed_out === false && observed.output_limit_exceeded === false
162
+ && observed.input_error === null && observed.subject_read_only === true && completionProven;
163
+ // Base64 preserves exact bounded output bytes without JSON control-character
164
+ // expansion or UTF-8 replacement changing the stored content's digest.
165
+ const facts = Object.fromEntries(Object.entries(observed ?? {}).filter(([key]) => !['stdout', 'stderr'].includes(key)));
166
+ const record = { schema_version: 1, evidence_class: 'local-isolated-verification-check', run_id,
167
+ execution_subject: initial.subject, check, observed_at: timestamp(options.clock), passed, error_code: errorCode, facts };
168
+ const persisted = persistReport(options.projectDir, record);
169
+ checks.push({ check_id: check.check_id, kind: check.kind, criterion_refs: check.criterion_refs,
170
+ passed, exit_code: observed?.exit_code ?? null, error_code: errorCode, ...persisted });
171
+ }
172
+ let browserCheck = null;
173
+ if (initial.browser) {
174
+ let workspace; let observed; let errorCode = null; let completionProven = false;
175
+ const frozen = initial.browser.plan.observation;
176
+ const { stdin_ref, ...application } = frozen.application;
177
+ try {
178
+ workspace = createRailReadOnlyCommitWorkspace({ projectDir: options.projectDir,
179
+ baseCommitRef: initial.subject.base_commit_ref, resultCommitRef: initial.subject.result_commit_ref, verificationSubject: true });
180
+ assertRailReadOnlyCommitWorkspaceUnchanged({ workspace, label: 'browser verification pre-spawn' });
181
+ const observation = { workingDir: realpathSync(workspace.subject_dir), ...frozen,
182
+ application: { ...application, stdin: scripts.get(stdin_ref) } };
183
+ observed = await runIsolatedBrowserObservation(observation);
184
+ completionProven = browserObservationCompleted(observed, observation);
185
+ } catch (error) { errorCode = error.code || 'BROWSER_VERIFICATION_FAILED'; }
186
+ finally {
187
+ if (workspace && process.platform !== 'darwin') {
188
+ try { cleanupRailReadOnlyCommitWorkspace(workspace); }
189
+ catch (error) { errorCode ??= error.code || 'BROWSER_VERIFICATION_CLEANUP_FAILED'; }
190
+ }
191
+ }
192
+ const captured_at = timestamp(options.clock);
193
+ const passed = !errorCode && observed?.passed === true && observed.tool === initial.browser.plan.tool_id
194
+ && completionProven;
195
+ const artifacts = [];
196
+ if (passed) {
197
+ for (const artifact of observed.artifacts) artifacts.push(persistBrowserArtifact({ projectDir: options.projectDir,
198
+ projectId: initial.context.handoff.project_id, run_id, attempt_id: options.attempt_id,
199
+ commit_ref: initial.subject.result_commit_ref, policy_ref: initial.browser.policy.policy_id,
200
+ browser_session_id: initial.browser.manifest.browser_session_id, artifact,
201
+ retention_class: initial.browser.policy.artifact_policy.retention_class, captured_at, clock: options.clock }));
202
+ if (artifacts.length === 0) fail('REAL_BROWSER_ARTIFACT_REQUIRED', 'successful observation did not produce a retained browser artifact');
203
+ }
204
+ // Raw/redacted content has one managed retention owner only. Permanent
205
+ // reports and receipts carry descriptors, hashes and process facts, not copies.
206
+ const facts = Object.fromEntries(Object.entries(observed ?? {}).filter(([key]) => key !== 'artifacts'));
207
+ browserCheck = { passed, error_code: errorCode || observed?.error_code || null, observed_at: captured_at,
208
+ artifacts, ...persistReport(options.projectDir, { schema_version: 1, evidence_class: 'local-isolated-browser-verification',
209
+ run_id, execution_subject: initial.subject, plan_digest: sha256(initial.browser.plan), captured_at, passed,
210
+ error_code: errorCode || observed?.error_code || null, artifacts, facts }) };
211
+ }
212
+ const current = canonicalContext(options);
213
+ if (current.binding_digest !== initial.binding_digest) fail('RAIL_VERIFICATION_SUBJECT_CHANGED', 'canonical result, verification plan or certification changed during observation');
214
+ const ok = checks.length === initial.plan.checks.length && checks.every((check) => check.passed) && (!browserCheck || browserCheck.passed);
215
+ const observed_at = timestamp(options.clock);
216
+ let evidence = null;
217
+ if (ok) {
218
+ const receipt = (kind, material, receiptObservedAt = observed_at) => {
219
+ const unsigned = signedRailEvidencePayload({ authority_kind: kind, issuer_id: current.authority.identity.issuer_id,
220
+ project_id: current.context.handoff.project_id, attempt_id: options.attempt_id,
221
+ commit_ref: current.subject.result_commit_ref, verdict: 'passed', evidence_digest: sha256(material), observed_at: receiptObservedAt });
222
+ const proof = signDelegatedAuthorityStatement(unsigned, current.authority.signingOptions);
223
+ const signed = { ...unsigned, signature: proof.signature };
224
+ if (proof.issuer_id !== unsigned.issuer_id || !verifyRailEvidenceReceipt(current.authority.verifier, signed, unsigned, options.clock)) {
225
+ fail('RAIL_VERIFICATION_SIGNATURE_INVALID', 'observed receipt does not validate against the enrolled authority');
226
+ }
227
+ return signed;
228
+ };
229
+ const ciRefs = checks.filter((check) => check.kind === 'ci').map((check) => check.report_ref);
230
+ const acceptanceRefs = checks.filter((check) => check.kind === 'acceptance').map((check) => check.report_ref);
231
+ let browserEvidence;
232
+ if (browserCheck) {
233
+ const delegation = current.browser.manifest;
234
+ assertLocalBrowserArtifacts({ projectDir: options.projectDir, projectId: current.context.handoff.project_id,
235
+ artifacts: browserCheck.artifacts, clock: options.clock, expected: { attempt_id: options.attempt_id,
236
+ commit_ref: current.subject.result_commit_ref, policy_ref: delegation.policy_ref,
237
+ browser_session_id: delegation.browser_session_id, captured_at: browserCheck.observed_at } });
238
+ const material = browserEvidenceMaterial({ schema_version: 1, manifest_type: 'BrowserRuntimeEvidenceManifest',
239
+ evidence_class: 'real-browser', browser_session_id: delegation.browser_session_id, task_id: current.context.task.task_id,
240
+ policy_ref: delegation.policy_ref, tool: delegation.tool, allowed_domain: delegation.allowed_domain,
241
+ classification: delegation.classification, commit_ref: current.subject.result_commit_ref,
242
+ delegation_manifest_hash: delegation.manifest_hash, observed_at: browserCheck.observed_at, artifacts: browserCheck.artifacts,
243
+ ...(delegation.tenant_ref ? { tenant_ref: delegation.tenant_ref } : {}) });
244
+ browserEvidence = { ...material, evidence_ref: `browser-evidence://sha256/${sha256(material)}`,
245
+ authority_receipt: receipt('rail_browser', material, browserCheck.observed_at) };
246
+ acceptanceRefs.push(browserCheck.report_ref, ...browserCheck.artifacts.map((artifact) => artifact.sanitized_ref), browserEvidence.evidence_ref);
247
+ }
248
+ let metrics;
249
+ if (current.effort) {
250
+ const metric = (source) => {
251
+ const persisted = persistReport(options.projectDir, { ...source, run_id,
252
+ execution_subject: current.subject, observed_at });
253
+ acceptanceRefs.push(persisted.report_ref);
254
+ return source.evidence_class === 'observed-provider-invocation-wall-time'
255
+ ? providerProcessingMetric(source, persisted.report_ref)
256
+ : { unit: 'minutes', value: source.value / 60000, evidence_ref: persisted.report_ref };
257
+ };
258
+ metrics = { actual_human_effort: metric(current.effort.human),
259
+ actual_ai_processing: metric(current.effort.processing),
260
+ source_journal_digest: current.effort.source_journal_digest,
261
+ rework_cycles: current.effort.rework_review_refs.length,
262
+ rework_review_refs: current.effort.rework_review_refs };
263
+ acceptanceRefs.push(completionMetricsReference({ attempt_id: options.attempt_id,
264
+ commit_ref: current.subject.result_commit_ref, metrics }));
265
+ }
266
+ evidence = {
267
+ ci: { verdict: 'passed', commit_ref: current.subject.result_commit_ref, evidence_refs: ciRefs, authority_receipt: receipt('rail_ci', ciRefs) },
268
+ acceptance_evidence_refs: acceptanceRefs, acceptance_authority_receipt: receipt('rail_acceptance', acceptanceRefs),
269
+ ...(metrics ? { metrics } : {}),
270
+ ...(browserEvidence ? { browser_evidence: browserEvidence } : {}),
271
+ };
272
+ }
273
+ const report = { schema_version: 1, evidence_class: 'local-isolated-verification', run_id, started_at, observed_at,
274
+ execution_subject: initial.subject, binding_digest: initial.binding_digest, checks, browser: browserCheck, ok, evidence };
275
+ const persisted = persistReport(options.projectDir, report);
276
+ return Object.freeze({ ok, action: 'rail_verification_observed', run_id,
277
+ execution_subject: initial.subject, checks, browser: browserCheck, evidence, ...persisted });
278
+ }
@@ -3,7 +3,8 @@ import { realpathSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { acceptReview, decideReviewProgress } from '@chati/review-council';
5
5
  import { sha256 } from '@chati/core';
6
- import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
6
+ import { attemptExecutionBase } from '@chati/rail';
7
+ import { assertRuntimeTaskReviewBindingV2, loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
7
8
  import {
8
9
  createProjectRailEngine, loadLatestRailExecutionEvidence, loadRailHandoff, resolveRailAttemptContext,
9
10
  } from './rail-runtime.js';
@@ -13,6 +14,36 @@ import { assertRailReadOnlyCommitWorkspaceUnchanged } from '../terminal/rail-rea
13
14
 
14
15
  function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
15
16
 
17
+ function assertSealedTaskReviewInvocation({ artifact, task, invocation, clock }) {
18
+ if (!task?.review_binding) return;
19
+ const expected = assertRuntimeTaskReviewBindingV2({
20
+ artifact,
21
+ review_binding: task.review_binding,
22
+ builder_identity: {
23
+ provider_id: task.execution_binding.provider_id,
24
+ model_id: task.execution_binding.model_id,
25
+ },
26
+ ...(clock === undefined ? {} : { clock }),
27
+ });
28
+ const actual = {
29
+ provider_id: invocation?.provider_id,
30
+ harness_id: invocation?.harness_id,
31
+ model_id: invocation?.model_pin?.model_id,
32
+ reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
33
+ catalog_snapshot_ref: invocation?.model_pin?.catalog_snapshot_ref,
34
+ };
35
+ const required = {
36
+ provider_id: expected.provider_id,
37
+ harness_id: expected.harness_id,
38
+ model_id: expected.model_pin.model_id,
39
+ reasoning_configuration: expected.model_pin.reasoning_configuration,
40
+ catalog_snapshot_ref: expected.model_pin.catalog_snapshot_ref,
41
+ };
42
+ if (Object.entries(required).some(([field, value]) => actual[field] !== value)) {
43
+ fail('RAIL_REVIEW_ROUTE_MISMATCH', 'review invocation does not match the sealed task review binding');
44
+ }
45
+ }
46
+
16
47
  function trustedGitEnvironment(source = process.env) {
17
48
  const environment = Object.fromEntries(Object.entries(source).filter(([key]) => !key.startsWith('GIT_')));
18
49
  return {
@@ -80,6 +111,7 @@ export function recordRailReviewDispatch({
80
111
  })) fail('UNTRUSTED_REVIEW_LAUNCH', 'review dispatch requires a terminal-authority launch receipt');
81
112
  const artifact = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
82
113
  if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'RAIL review dispatch requires a v2 installation');
114
+ assertSealedTaskReviewInvocation({ artifact, task: context.task, invocation, clock });
83
115
  return engine.recordReviewDispatch({
84
116
  attempt_id, invocation, launch_receipt,
85
117
  installation: artifact.installation, snapshot: artifact.capability_snapshot,
@@ -104,7 +136,7 @@ function resolveAttemptLineage(projectDir, attemptId, clock) {
104
136
  handoff_id: handoff.handoff_id,
105
137
  handoff_manifest_digest: handoff.seal.manifest_digest,
106
138
  task_id: attempt.task_id,
107
- base_commit_ref: attempt.git_commit_ref,
139
+ base_commit_ref: attemptExecutionBase(attempt),
108
140
  });
109
141
  }
110
142
 
@@ -181,6 +213,9 @@ export function recordRailReview({
181
213
  }
182
214
  const artifact = loadRuntimeInstallationV2(projectDir, { clock });
183
215
  if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'RAIL review requires a v2 installation');
216
+ const reviewLineage = resolveAttemptLineage(projectDir, review?.attempt_id, clock);
217
+ const reviewContext = resolveRailAttemptContext({ projectDir, handoff_id: reviewLineage.handoff_id, attempt_id: review?.attempt_id, clock });
218
+ assertSealedTaskReviewInvocation({ artifact, task: reviewContext.task, invocation, clock });
184
219
  const validated = acceptReview({ review, invocation, installation: artifact.installation, snapshot: artifact.capability_snapshot, clock });
185
220
  const baseLineage = resolveAttemptLineage(projectDir, validated.attempt_id, clock);
186
221
  const engine = createProjectRailEngine({ projectDir, clock });
@@ -2,7 +2,7 @@ import { doctorV2 } from '../installer-v2/index.js';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { assertEligibleInvocation } from '@chati/provider-registry';
5
- import { selectCanonicalReviewer } from '@chati/review-council';
5
+ import { selectCanonicalReviewer, selectCouncilReviewers } from '@chati/review-council';
6
6
  import {
7
7
  readInstallationArtifactFile, verifyInstallationCatalogProvenance,
8
8
  } from '../installer-v2/catalog-client.js';
@@ -19,33 +19,36 @@ export const V2_PLANNING_AGENTS = Object.freeze([
19
19
  'detail', 'architect', 'ux', 'qa-planning', 'phases', 'tasks',
20
20
  ]);
21
21
 
22
- const V2_PLANNING_PREDECESSORS = Object.freeze({
23
- brief: Object.freeze(['work-understanding']),
24
- detail: Object.freeze(['work-understanding', 'brief']),
25
- architect: Object.freeze(['work-understanding', 'brief']),
26
- ux: Object.freeze(['work-understanding', 'brief']),
27
- 'qa-planning': Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux']),
28
- phases: Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux', 'qa-planning']),
29
- tasks: Object.freeze(['work-understanding', 'brief', 'detail', 'architect', 'ux', 'qa-planning', 'phases']),
30
- });
22
+ const COUNCIL_GATE_STAGES = new Set(['discovery', 'planning', 'execution-design']);
31
23
 
32
- export function assertV2PlanningPredecessors(session, agent) {
33
- const projectType = session?.project_type || session?.project?.type || 'greenfield';
34
- const required = (V2_PLANNING_PREDECESSORS[agent] || []).map((name) =>
35
- name === 'work-understanding' ? (projectType === 'brownfield' ? 'brownfield-wu' : 'greenfield-wu') : name
36
- );
37
- const incomplete = required.filter((name) =>
38
- session?.agents?.[name]?.status !== 'completed' || !session?.completed_agents?.includes(name)
39
- );
40
- if (incomplete.length > 0) {
41
- const error = new Error(`Agent ${agent} cannot advance before its canonical predecessors complete: ${incomplete.join(', ')}`);
42
- error.code = 'V2_PLANNING_PREDECESSORS_INCOMPLETE';
43
- error.incomplete = incomplete;
24
+ /**
25
+ * Returns the installation-owned council contract for one stage. Artifacts
26
+ * created before this policy existed deliberately remain non-disruptive until
27
+ * their owner explicitly reconfigures the installation.
28
+ */
29
+ export function resolveRuntimeCouncilGatePolicyV2({ artifact, stage } = {}) {
30
+ if (!COUNCIL_GATE_STAGES.has(stage)) {
31
+ const error = new Error(`Unknown council stage: ${stage}`);
32
+ error.code = 'COUNCIL_GATE_STAGE_INVALID';
44
33
  throw error;
45
34
  }
46
- return Object.freeze({ valid: true, required: Object.freeze(required) });
35
+ const policy = artifact?.installation?.quality_council;
36
+ if (!policy || policy.mode !== 'autonomous' || !policy.stages?.includes(stage)) {
37
+ return Object.freeze({ enabled: false, reason: policy?.mode === 'guided' ? 'GUIDED_COUNCIL_REQUIRED' : 'COUNCIL_POLICY_NOT_CONFIGURED' });
38
+ }
39
+ return Object.freeze({
40
+ enabled: true,
41
+ policy: Object.freeze({
42
+ min_members: policy.min_members,
43
+ max_members: policy.max_members,
44
+ require_distinct_providers: policy.require_distinct_providers,
45
+ blocking_severities: [...policy.blocking_severities],
46
+ }),
47
+ });
47
48
  }
48
49
 
50
+ export { assertV2PlanningPredecessors } from './agent-selector.js';
51
+
49
52
  const RAIL_ACTIONS = new Set(['build', 'review']);
50
53
  const REASONING_CONFIGURATIONS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
51
54
 
@@ -169,6 +172,15 @@ export function resolveRuntimeExecutableBindingV2({ artifact, provider_id, harne
169
172
  });
170
173
  }
171
174
 
175
+ function routingPriority(model, routingKey, action) {
176
+ const keys = Array.isArray(routingKey) ? routingKey : [routingKey];
177
+ for (const key of keys) {
178
+ const priority = model.routing_priority?.[key];
179
+ if (priority !== undefined) return priority;
180
+ }
181
+ return model.routing_priority?.[action] ?? 0;
182
+ }
183
+
172
184
  function resolveRuntimeActionV2({ artifact, action, routingKey, binding, model_id, reasoning_configuration, clock = () => new Date() }) {
173
185
  let hasEligibleModel = false;
174
186
  const candidates = artifact.installation.enabled_providers.flatMap((candidateBinding) => {
@@ -193,7 +205,7 @@ function resolveRuntimeActionV2({ artifact, action, routingKey, binding, model_i
193
205
  binding: candidateBinding,
194
206
  model: catalogModel,
195
207
  reasoning,
196
- priority: catalogModel.routing_priority?.[routingKey] ?? catalogModel.routing_priority?.[action] ?? 0,
208
+ priority: routingPriority(catalogModel, routingKey, action),
197
209
  }];
198
210
  });
199
211
  }).sort((left, right) =>
@@ -241,6 +253,93 @@ export function resolveRuntimeRailInvocationV2({ artifact, action = 'build', bin
241
253
  return resolveRuntimeActionV2({ artifact, action, routingKey: `rail:${action}`, binding, model_id, reasoning_configuration, clock });
242
254
  }
243
255
 
256
+ const TASK_RISK_LEVELS = new Set(['low', 'medium', 'high', 'critical']);
257
+
258
+ function normalizeTaskRoutingProfile(profile = {}) {
259
+ if (!profile || typeof profile !== 'object' || Array.isArray(profile) || !TASK_RISK_LEVELS.has(profile.risk_level)) {
260
+ const error = new Error('task routing_profile.risk_level must be low, medium, high or critical');
261
+ error.code = 'TASK_ROUTING_PROFILE_REQUIRED';
262
+ throw error;
263
+ }
264
+ return Object.freeze({ risk_level: profile.risk_level });
265
+ }
266
+
267
+ /**
268
+ * Selects the exact build and independent-review bindings at planning seal
269
+ * time. The model catalog owns the priorities, so an updated signed catalog
270
+ * can change the route without embedding personal ETCAR weights in CHATI.
271
+ */
272
+ export function resolveRuntimeTaskRouteV2({ artifact, routing_profile, required_independence_level = 'A', clock = () => new Date() } = {}) {
273
+ if (!['A', 'B', 'C'].includes(required_independence_level)) {
274
+ const error = new Error('required_independence_level must be A, B or C');
275
+ error.code = 'TASK_ROUTING_PROFILE_INVALID';
276
+ throw error;
277
+ }
278
+ const profile = normalizeTaskRoutingProfile(routing_profile);
279
+ const execution = resolveRuntimeActionV2({
280
+ artifact,
281
+ action: 'build',
282
+ routingKey: [`task:build:${profile.risk_level}`, 'task:build', 'rail:build'],
283
+ clock,
284
+ });
285
+ const builder_identity = { provider_id: execution.provider_id, model_id: execution.model_pin.model_id };
286
+ const selectedReview = resolveRuntimeRailReviewerV2({ artifact, builder_identity, clock });
287
+ const attained = selectedReview.independence_level;
288
+ const strength = { A: 3, B: 2, C: 1 };
289
+ if (strength[attained] < strength[required_independence_level]) {
290
+ const error = new Error(`task requires independence ${required_independence_level}, but the selected catalog route attains ${attained}`);
291
+ error.code = 'TASK_REVIEW_INDEPENDENCE_UNAVAILABLE';
292
+ throw error;
293
+ }
294
+ return Object.freeze({
295
+ routing_profile: profile,
296
+ execution_binding: Object.freeze({
297
+ provider_id: execution.provider_id,
298
+ harness_id: execution.harness_id,
299
+ model_id: execution.model_pin.model_id,
300
+ reasoning_configuration: execution.model_pin.reasoning_configuration,
301
+ }),
302
+ review_binding: Object.freeze({
303
+ provider_id: selectedReview.provider_id,
304
+ harness_id: selectedReview.harness_id,
305
+ model_id: selectedReview.model_pin.model_id,
306
+ reasoning_configuration: selectedReview.model_pin.reasoning_configuration,
307
+ attained_independence_level: attained,
308
+ }),
309
+ });
310
+ }
311
+
312
+ /** Validates a sealed task review binding without allowing a runtime reroute. */
313
+ export function assertRuntimeTaskReviewBindingV2({ artifact, review_binding, builder_identity, clock = () => new Date() } = {}) {
314
+ if (!review_binding || typeof review_binding !== 'object' || Array.isArray(review_binding)) {
315
+ const error = new Error('sealed task review binding is required');
316
+ error.code = 'TASK_REVIEW_BINDING_REQUIRED';
317
+ throw error;
318
+ }
319
+ const invocation = {
320
+ provider_id: review_binding.provider_id,
321
+ harness_id: review_binding.harness_id,
322
+ action: 'review',
323
+ model_pin: {
324
+ model_id: review_binding.model_id,
325
+ catalog_snapshot_ref: artifact.capability_snapshot.snapshot_id,
326
+ reasoning_configuration: review_binding.reasoning_configuration,
327
+ },
328
+ };
329
+ assertEligibleInvocation({ installation: artifact.installation, snapshot: artifact.capability_snapshot, invocation, clock });
330
+ const reviewer_identity = { provider_id: invocation.provider_id, model_id: invocation.model_pin.model_id };
331
+ const independence_level = builder_identity
332
+ ? (builder_identity.provider_id !== reviewer_identity.provider_id ? 'A'
333
+ : builder_identity.model_id !== reviewer_identity.model_id ? 'B' : 'C')
334
+ : null;
335
+ if (review_binding.attained_independence_level !== undefined && review_binding.attained_independence_level !== independence_level) {
336
+ const error = new Error('sealed task review binding independence level drifted');
337
+ error.code = 'TASK_REVIEW_BINDING_DRIFT';
338
+ throw error;
339
+ }
340
+ return Object.freeze({ ...invocation, reviewer_identity, independence_level });
341
+ }
342
+
244
343
  /** Selects the highest-ranked installed reviewer at the strongest available independence level. */
245
344
  export function resolveRuntimeRailReviewerV2({ artifact, builder_identity, clock = () => new Date() }) {
246
345
  return selectCanonicalReviewer({
@@ -251,6 +350,26 @@ export function resolveRuntimeRailReviewerV2({ artifact, builder_identity, clock
251
350
  });
252
351
  }
253
352
 
353
+ /**
354
+ * Resolves the planning-quality council from the providers the user selected
355
+ * during installation. The council is two or three distinct providers, never
356
+ * a hidden provider fallback and never a hard-coded vendor trio.
357
+ */
358
+ export function resolveRuntimeCouncilInvocationsV2({
359
+ artifact,
360
+ builder_identity,
361
+ policy,
362
+ clock = () => new Date(),
363
+ } = {}) {
364
+ return selectCouncilReviewers({
365
+ installation: artifact.installation,
366
+ snapshot: artifact.capability_snapshot,
367
+ builder_identity,
368
+ ...(policy ? { policy } : {}),
369
+ clock,
370
+ });
371
+ }
372
+
254
373
  export function resolveRuntimeAdjudicationV2({ artifact, clock = () => new Date() }) {
255
374
  const candidates = artifact.installation.enabled_providers.flatMap((binding) =>
256
375
  binding.allowed_models.flatMap((modelId) => {