chati-dev 4.5.16 → 4.5.27

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 (122) hide show
  1. package/README.md +8 -3
  2. package/bin/chati.js +124 -66
  3. package/framework/agents/build/dev.md +5 -5
  4. package/framework/agents/deploy/devops.md +7 -7
  5. package/framework/agents/discover/brief.md +4 -4
  6. package/framework/agents/discover/brownfield-wu.md +3 -3
  7. package/framework/agents/discover/greenfield-wu.md +3 -3
  8. package/framework/agents/plan/architect.md +2 -2
  9. package/framework/agents/plan/detail.md +4 -4
  10. package/framework/agents/plan/phases.md +2 -2
  11. package/framework/agents/plan/tasks.md +2 -2
  12. package/framework/agents/plan/ux.md +2 -2
  13. package/framework/agents/quality/qa-implementation.md +11 -10
  14. package/framework/agents/quality/qa-planning.md +3 -3
  15. package/framework/agents/quality/qa-visual.md +1 -1
  16. package/framework/config.yaml +3 -3
  17. package/framework/constitution.md +18 -18
  18. package/framework/context/protocols.md +1 -1
  19. package/framework/context/quality.md +1 -1
  20. package/framework/context/root.md +4 -4
  21. package/framework/data/entity-registry.yaml +1 -1
  22. package/framework/domains/agents/orchestrator.yaml +2 -2
  23. package/framework/domains/constitution.yaml +1 -1
  24. package/framework/hooks/advance-trigger.js +4 -6
  25. package/framework/hooks/git-push-authority.js +45 -37
  26. package/framework/hooks/mode-governance.js +149 -40
  27. package/framework/hooks/model-governance.js +13 -20
  28. package/framework/hooks/prism-engine.js +74 -92
  29. package/framework/hooks/reasoning-escalator.js +23 -40
  30. package/framework/hooks/session-digest.js +12 -13
  31. package/framework/hooks/session-reader.js +224 -0
  32. package/framework/hooks/session-writer.js +195 -0
  33. package/framework/hooks/team-quality-gate.js +34 -24
  34. package/framework/i18n/en.yaml +2 -2
  35. package/framework/i18n/es.yaml +2 -2
  36. package/framework/i18n/fr.yaml +2 -2
  37. package/framework/i18n/pt.yaml +2 -2
  38. package/framework/intelligence/context-engine.md +4 -4
  39. package/framework/intelligence/memory-layer.md +1 -1
  40. package/framework/manifest.json +129 -119
  41. package/framework/manifest.sig +1 -1
  42. package/framework/orchestrator/chati-router.js +252 -25
  43. package/framework/orchestrator/chati.md +238 -70
  44. package/framework/schemas/session.schema.json +1 -1
  45. package/framework/tasks/orchestrator-deviation.md +1 -1
  46. package/framework/tasks/orchestrator-escalate.md +1 -1
  47. package/framework/tasks/orchestrator-handoff.md +6 -6
  48. package/framework/tasks/orchestrator-health.md +5 -9
  49. package/framework/tasks/orchestrator-mode-switch.md +3 -7
  50. package/framework/tasks/orchestrator-resume.md +10 -14
  51. package/framework/tasks/orchestrator-route.md +3 -3
  52. package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
  53. package/framework/tasks/orchestrator-status.md +9 -9
  54. package/framework/tasks/orchestrator-suggest-mode.md +1 -1
  55. package/framework/tasks/qa-impl-consolidate.md +2 -2
  56. package/framework/tasks/qa-impl-performance-test.md +4 -4
  57. package/framework/tasks/qa-impl-regression-check.md +4 -4
  58. package/framework/tasks/qa-impl-sast-scan.md +1 -1
  59. package/framework/tasks/qa-impl-test-execute.md +1 -1
  60. package/framework/tasks/qa-impl-verdict.md +2 -2
  61. package/framework/tasks/qa-planning-consolidate.md +3 -3
  62. package/framework/tasks/qa-planning-coverage-plan.md +2 -2
  63. package/framework/tasks/qa-planning-gate-define.md +4 -4
  64. package/framework/tasks/qa-planning-risk-matrix.md +4 -4
  65. package/framework/tasks/qa-planning-test-strategy.md +2 -2
  66. package/node_modules/@chati/browser-capability/src/index.js +12 -2
  67. package/node_modules/@chati/planning/src/index.js +24 -4
  68. package/node_modules/@chati/provider-registry/src/index.js +11 -0
  69. package/node_modules/@chati/rail/src/index.js +1967 -83
  70. package/node_modules/@chati/release-lane/README.md +12 -8
  71. package/node_modules/@chati/release-lane/package.json +1 -1
  72. package/node_modules/@chati/release-lane/src/index.js +1426 -58
  73. package/node_modules/@chati/review-council/src/index.js +63 -0
  74. package/node_modules/@chati/tracking-clickup/README.md +13 -0
  75. package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
  76. package/package-artifact-manifest.json +1 -0
  77. package/package-artifact-manifest.sig +1 -0
  78. package/package.json +16 -7
  79. package/scripts/verify-real-harness-e2e.js +1581 -0
  80. package/src/config/framework-adapter.js +4 -4
  81. package/src/installer/core.js +148 -53
  82. package/src/installer/manifest.js +140 -9
  83. package/src/installer/package-artifact.js +249 -0
  84. package/src/installer/templates.js +65 -20
  85. package/src/installer-v2/catalog-client.js +531 -23
  86. package/src/installer-v2/index.js +91 -34
  87. package/src/installer-v2/installation-authority.js +327 -0
  88. package/src/installer-v2/provider-executable.js +235 -0
  89. package/src/installer-v2/wizard-installation.js +1 -1
  90. package/src/orchestrator/browser-runtime.js +44 -13
  91. package/src/orchestrator/cli.js +1545 -174
  92. package/src/orchestrator/clickup-projection.js +43 -1
  93. package/src/orchestrator/clickup-runtime.js +355 -39
  94. package/src/orchestrator/doctor.js +16 -3
  95. package/src/orchestrator/index.js +16 -0
  96. package/src/orchestrator/planning-runtime.js +20 -2
  97. package/src/orchestrator/rail-adjudication-evidence.js +234 -0
  98. package/src/orchestrator/rail-evidence-authority.js +147 -0
  99. package/src/orchestrator/rail-execution-evidence.js +45 -0
  100. package/src/orchestrator/rail-runtime.js +836 -56
  101. package/src/orchestrator/release-runtime.js +65 -6
  102. package/src/orchestrator/review-runtime.js +186 -41
  103. package/src/orchestrator/runtime-installation-v2.js +236 -20
  104. package/src/orchestrator/session-manager.js +1167 -53
  105. package/src/terminal/adapters/claude-adapter.js +3 -1
  106. package/src/terminal/adapters/codex-adapter.js +2 -0
  107. package/src/terminal/adapters/grok-adapter.js +7 -4
  108. package/src/terminal/handoff-parser.js +19 -1
  109. package/src/terminal/prompt-builder.js +9 -1
  110. package/src/terminal/provider-preflight.js +36 -3
  111. package/src/terminal/rail-execution-worktree.js +213 -0
  112. package/src/terminal/rail-prompts.js +169 -0
  113. package/src/terminal/rail-readonly-workspace.js +324 -0
  114. package/src/terminal/run-agent.js +384 -24
  115. package/src/terminal/run-parallel.js +5 -0
  116. package/src/terminal/run-rail-adjudication.js +323 -0
  117. package/src/terminal/run-rail-review.js +291 -0
  118. package/src/terminal/run-rail-rework.js +325 -0
  119. package/src/terminal/run-rail-task.js +380 -0
  120. package/src/terminal/run-team.js +5 -0
  121. package/src/terminal/spawner.js +1225 -77
  122. package/src/wizard/index.js +3 -2
@@ -1,14 +1,73 @@
1
- import { ReleaseLane } from '@chati/release-lane';
2
- import { resolveRailCompletionReference } from './rail-runtime.js';
1
+ import { join } from 'node:path';
2
+ import {
3
+ ReleaseLane, ReleaseStateAuthority, ReleaseTransport,
4
+ SignedHumanAuthorizationVerifier, SignedSmokeEvidenceVerifier,
5
+ } from '@chati/release-lane';
6
+ import { loadRailHandoff, resolveRailCompletionReference } from './rail-runtime.js';
7
+ import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
8
+ import {
9
+ signHostAuthorityPayload, verifyHostAuthorityPayload,
10
+ } from '../installer-v2/installation-authority.js';
11
+
12
+ const RELEASE_STATE_AUTHORITY_DOMAIN = 'chati-release-state-v1';
13
+
14
+ export {
15
+ signedHumanAuthorizationPayload,
16
+ signedSmokeEvidencePayload,
17
+ } from '@chati/release-lane';
3
18
 
4
19
  /**
5
- * G3 local release authority. The returned ReleaseLane accepts only its own
6
- * FakeReleaseTransport capability and resolves completion evidence from the
7
- * project-local RAIL journal. It has no external deployment adapter.
20
+ * Project release authority. Completion evidence is resolved from the local
21
+ * RAIL journal. External I/O remains behind an explicitly injected minted
22
+ * transport and cannot run before scoped human authorization and smoke proof.
8
23
  */
9
- export function createProjectReleaseLane({ projectDir, handoff_id, clock = () => new Date() } = {}) {
24
+ export function createProjectReleaseLane({
25
+ projectDir, handoff_id, authorization_issuer_id, authorization_public_key,
26
+ smoke_issuer_id, smoke_public_key,
27
+ clock = () => new Date(),
28
+ } = {}) {
29
+ if (typeof handoff_id !== 'string' || !/^[A-Za-z0-9._-]+$/.test(handoff_id)) {
30
+ throw new Error('handoff_id must be safe for the project-local release journal');
31
+ }
32
+ const artifact = loadRuntimeInstallationV2(projectDir, { clock });
33
+ if (!artifact) throw new Error('release lane requires an authorized v2 installation');
34
+ const { handoff } = loadRailHandoff({ projectDir, handoff_id, clock });
35
+ if (handoff.project_id !== artifact.project_id) {
36
+ throw new Error('release lane handoff does not belong to the authorized installed project');
37
+ }
10
38
  return new ReleaseLane({
11
39
  clock,
12
40
  verify_completion: (immutable_ref) => resolveRailCompletionReference({ projectDir, handoff_id, immutable_ref, clock }),
41
+ authorization_verifier: new SignedHumanAuthorizationVerifier({
42
+ issuer_id: authorization_issuer_id,
43
+ public_key: authorization_public_key,
44
+ }),
45
+ smoke_verifier: new SignedSmokeEvidenceVerifier({
46
+ issuer_id: smoke_issuer_id,
47
+ public_key: smoke_public_key,
48
+ }),
49
+ state_authority: new ReleaseStateAuthority({
50
+ sign: (material) => {
51
+ const signed = signHostAuthorityPayload(material, { domain: RELEASE_STATE_AUTHORITY_DOMAIN });
52
+ return { authority_id: signed.authority_id, authority_signature: signed.signature };
53
+ },
54
+ verify: (material, receipt) => verifyHostAuthorityPayload(material, {
55
+ domain: RELEASE_STATE_AUTHORITY_DOMAIN,
56
+ authorityId: receipt?.authority_id,
57
+ signature: receipt?.authority_signature,
58
+ }),
59
+ }),
60
+ require_authenticated_state: true,
61
+ authority_context: {
62
+ project_id: artifact.project_id,
63
+ handoff_id,
64
+ },
65
+ require_authority_context: true,
66
+ require_production_transport: true,
67
+ state_path: join(projectDir, '.chati', 'release-lane', `${handoff_id}.jsonl`),
13
68
  });
14
69
  }
70
+
71
+ export function createProjectReleaseTransport({ transport_id, authority_id, public_key, release, rollback, lookup } = {}) {
72
+ return new ReleaseTransport({ transport_id, authority_id, public_key, release, rollback, lookup });
73
+ }
@@ -1,24 +1,96 @@
1
- import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import { realpathSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
3
4
  import { acceptReview, decideReviewProgress } from '@chati/review-council';
5
+ import { sha256 } from '@chati/core';
4
6
  import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
5
- import { createProjectRailEngine, loadRailHandoff } from './rail-runtime.js';
7
+ import {
8
+ createProjectRailEngine, loadLatestRailExecutionEvidence, loadRailHandoff, resolveRailAttemptContext,
9
+ } from './rail-runtime.js';
10
+ import { verifyTerminalAuthorityReceipt } from '../terminal/spawner.js';
11
+ import { buildRailReviewPrompt } from '../terminal/rail-prompts.js';
12
+ import { assertRailReadOnlyCommitWorkspaceUnchanged } from '../terminal/rail-readonly-workspace.js';
6
13
 
7
14
  function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
8
- function safeId(value, label) {
9
- if (typeof value !== 'string' || !/^[A-Za-z0-9._-]+$/.test(value)) fail('INVALID_REVIEW_STORAGE_ID', `${label} must contain only letters, digits, dot, underscore or hyphen`);
10
- return value;
15
+
16
+ function trustedGitEnvironment(source = process.env) {
17
+ const environment = Object.fromEntries(Object.entries(source).filter(([key]) => !key.startsWith('GIT_')));
18
+ return {
19
+ ...environment,
20
+ GIT_CONFIG_NOSYSTEM: '1',
21
+ GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
22
+ GIT_TERMINAL_PROMPT: '0',
23
+ };
24
+ }
25
+
26
+ function parseAuthorizedReviewPayload(text) {
27
+ if (typeof text !== 'string') fail('RAIL_REVIEW_OUTPUT_INVALID', 'review output must be a string');
28
+ const startToken = '<CHATI_RAIL_REVIEW>';
29
+ const endToken = '</CHATI_RAIL_REVIEW>';
30
+ const starts = text.split(startToken).length - 1;
31
+ const ends = text.split(endToken).length - 1;
32
+ if (starts !== 1 || ends !== 1) fail('RAIL_REVIEW_OUTPUT_INVALID', 'reviewer must return exactly one CHATI_RAIL_REVIEW envelope');
33
+ const start = text.indexOf(startToken);
34
+ const end = text.indexOf(endToken, start + startToken.length);
35
+ try { return JSON.parse(text.slice(start + startToken.length, end).trim()); }
36
+ catch (error) { return fail('RAIL_REVIEW_OUTPUT_INVALID', `reviewer returned invalid JSON: ${error.message}`); }
11
37
  }
12
- function reviewDir(projectDir, attemptId) { return join(projectDir, '.chati/v2/rail/reviews', safeId(attemptId, 'attempt_id')); }
13
- function reviewPath(projectDir, attemptId, reviewId) { return join(reviewDir(projectDir, attemptId), `${safeId(reviewId, 'review_id')}.json`); }
14
38
 
15
- function readReviewEvidence(projectDir, attemptId) {
16
- const directory = reviewDir(projectDir, attemptId);
17
- if (!existsSync(directory)) return [];
18
- return readdirSync(directory).filter((name) => name.endsWith('.json')).sort().map((name) => {
19
- try { return JSON.parse(readFileSync(join(directory, name), 'utf8')); }
20
- catch { return fail('INVALID_STORED_REVIEW', `review evidence cannot be read: ${name}`); }
39
+ export function recordRailReviewDispatch({
40
+ projectDir, attempt_id, invocation, launch_receipt, prompt, reviewWorkspace, clock,
41
+ } = {}) {
42
+ const lineage = resolveAttemptLineage(projectDir, attempt_id, clock);
43
+ const context = resolveRailAttemptContext({ projectDir, handoff_id: lineage.handoff_id, attempt_id, clock });
44
+ const engine = createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) });
45
+ const reviewSubject = resolveReviewGitSubject({ projectDir, attempt_id, clock });
46
+ if (!reviewWorkspace
47
+ || reviewWorkspace.canonical_project_dir !== realpathSync(resolve(projectDir))
48
+ || reviewWorkspace.base_commit_ref !== reviewSubject.base_commit_ref
49
+ || reviewWorkspace.result_commit_ref !== reviewSubject.result_commit_ref) {
50
+ fail('RAIL_REVIEW_WORKSPACE_MISMATCH', 'review dispatch requires an immutable workspace for the exact result commit');
51
+ }
52
+ const workspaceState = assertRailReadOnlyCommitWorkspaceUnchanged({
53
+ workspace: reviewWorkspace,
54
+ label: 'RAIL review dispatch',
55
+ });
56
+ const canonicalSubject = {
57
+ ...reviewSubject,
58
+ workspace_digest: sha256(workspaceState),
59
+ };
60
+ const round = engine.reviewEvidenceForAttempt(attempt_id).length + 1;
61
+ const canonicalPrompt = buildRailReviewPrompt({
62
+ projectDir: reviewWorkspace.subject_dir,
63
+ context,
64
+ invocation,
65
+ reviewSubject: canonicalSubject,
66
+ round,
21
67
  });
68
+ if (prompt !== canonicalPrompt || invocation?.prompt_digest !== sha256(canonicalPrompt)) {
69
+ fail('NONCANONICAL_REVIEW_PROMPT', 'review dispatch must use the exact canonical prompt for this attempt and round');
70
+ }
71
+ if (!verifyTerminalAuthorityReceipt(launch_receipt, {
72
+ purpose: 'terminal_launch', authority_kind: 'rail_review',
73
+ invocation_id: invocation?.invocation_id, launch_nonce: invocation?.launch_nonce,
74
+ provider_id: invocation?.provider_id, harness_id: invocation?.harness_id,
75
+ model_id: invocation?.model_pin?.model_id,
76
+ reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
77
+ task_id: context.task.task_id,
78
+ working_dir: resolve(reviewWorkspace.subject_dir),
79
+ prompt_digest: invocation.prompt_digest,
80
+ })) fail('UNTRUSTED_REVIEW_LAUNCH', 'review dispatch requires a terminal-authority launch receipt');
81
+ const artifact = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
82
+ if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'RAIL review dispatch requires a v2 installation');
83
+ return engine.recordReviewDispatch({
84
+ attempt_id, invocation, launch_receipt,
85
+ installation: artifact.installation, snapshot: artifact.capability_snapshot,
86
+ project_dir: projectDir,
87
+ working_dir: reviewWorkspace.subject_dir,
88
+ });
89
+ }
90
+
91
+ export function listRailReviewEvidence({ projectDir, attempt_id, clock } = {}) {
92
+ return createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) })
93
+ .reviewEvidenceForAttempt(attempt_id);
22
94
  }
23
95
 
24
96
  function resolveAttemptLineage(projectDir, attemptId, clock) {
@@ -32,43 +104,116 @@ function resolveAttemptLineage(projectDir, attemptId, clock) {
32
104
  handoff_id: handoff.handoff_id,
33
105
  handoff_manifest_digest: handoff.seal.manifest_digest,
34
106
  task_id: attempt.task_id,
107
+ base_commit_ref: attempt.git_commit_ref,
108
+ });
109
+ }
110
+
111
+ export function resolveReviewGitSubject({ projectDir, attempt_id, clock } = {}) {
112
+ const lineage = resolveAttemptLineage(projectDir, attempt_id, clock);
113
+ const evidence = loadLatestRailExecutionEvidence({ projectDir, attempt_id, clock });
114
+ const engine = createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) });
115
+ const latestControl = engine.records.filter((record) => ['review_decided', 'adjudication_decided'].includes(record.type)
116
+ && record.payload?.attempt_id === attempt_id).at(-1);
117
+ const requiredReworkSource = latestControl?.type === 'review_decided'
118
+ && latestControl.payload.decision?.decision === 'normal-rework'
119
+ ? latestControl.payload.review_ref
120
+ : latestControl?.type === 'adjudication_decided'
121
+ && latestControl.payload.decision?.outcome === 'bounded-correction'
122
+ ? latestControl.payload.adjudication_ref
123
+ : null;
124
+ if (requiredReworkSource && evidence.source_decision_ref !== requiredReworkSource) {
125
+ fail('RAIL_REWORK_RESULT_REQUIRED', 'latest canonical rework decision has no successful execution result');
126
+ }
127
+ if (evidence.attempt_id !== attempt_id || evidence.handoff_id !== lineage.handoff_id || evidence.task_id !== lineage.task_id) {
128
+ fail('REVIEW_EXECUTION_EVIDENCE_MISMATCH', 'review execution evidence is not bound to the claimed handoff, task and attempt');
129
+ }
130
+ const result_commit_ref = evidence.result_commit_ref;
131
+ if (typeof result_commit_ref !== 'string' || !/^[a-f0-9]{40,64}$/.test(result_commit_ref)) {
132
+ fail('REVIEW_RESULT_COMMIT_REQUIRED', 'review requires a result commit captured by immutable execution evidence');
133
+ }
134
+ try {
135
+ execFileSync('git', ['-C', projectDir, 'cat-file', '-e', `${result_commit_ref}^{commit}`], {
136
+ stdio: 'ignore', env: trustedGitEnvironment(),
137
+ });
138
+ } catch {
139
+ return fail('REVIEW_GIT_SUBJECT_UNAVAILABLE', 'the execution evidence result commit is unavailable in local Git');
140
+ }
141
+ if (result_commit_ref === lineage.base_commit_ref) {
142
+ fail('REVIEW_RESULT_COMMIT_REQUIRED', 'review requires a result commit distinct from the sealed base commit');
143
+ }
144
+ const ancestry = spawnSync('git', ['-C', projectDir, 'merge-base', '--is-ancestor', lineage.base_commit_ref, result_commit_ref], {
145
+ env: trustedGitEnvironment(),
35
146
  });
147
+ if (ancestry.status !== 0) fail('REVIEW_RESULT_NOT_DESCENDANT', 'review result commit must descend from the sealed base commit');
148
+ return Object.freeze({ ...lineage, result_commit_ref, execution_evidence_ref: evidence.evidence_id });
36
149
  }
37
150
 
38
- /** Stores a selected-only review record and derives the next rework decision. */
39
- export function recordRailReview({ projectDir, review, invocation, clock = () => new Date() } = {}) {
151
+ /** Appends selected-only review evidence to the canonical RAIL journal. */
152
+ export function recordRailReview({
153
+ projectDir, review, review_output, terminal_authority_receipt,
154
+ invocation, result_commit_ref, clock = () => new Date(),
155
+ } = {}) {
156
+ const payload = parseAuthorizedReviewPayload(review_output);
157
+ const outputReview = {
158
+ findings: payload.findings ?? [], verdict: payload.verdict,
159
+ impasse: payload.impasse ?? { kind: 'none' },
160
+ ...(payload.authority_reason ? { authority_reason: payload.authority_reason } : {}),
161
+ ...(payload.authority_reason_ref ? { authority_reason_ref: payload.authority_reason_ref } : {}),
162
+ };
163
+ const suppliedReview = {
164
+ findings: review?.findings ?? [], verdict: review?.verdict,
165
+ impasse: review?.impasse ?? { kind: 'none' },
166
+ ...(review?.authority_reason ? { authority_reason: review.authority_reason } : {}),
167
+ ...(review?.authority_reason_ref ? { authority_reason_ref: review.authority_reason_ref } : {}),
168
+ };
169
+ if (sha256(outputReview) !== sha256(suppliedReview)) {
170
+ fail('RAIL_REVIEW_PAYLOAD_MISMATCH', 'review material is not the payload returned by the authorized reviewer process');
171
+ }
172
+ if (!verifyTerminalAuthorityReceipt(terminal_authority_receipt, {
173
+ purpose: 'terminal_completion', authority_kind: 'rail_review',
174
+ invocation_id: invocation?.invocation_id, launch_nonce: invocation?.launch_nonce,
175
+ provider_id: invocation?.provider_id, harness_id: invocation?.harness_id,
176
+ model_id: invocation?.model_pin?.model_id,
177
+ reasoning_configuration: invocation?.model_pin?.reasoning_configuration,
178
+ exit_status: 0,
179
+ }) || terminal_authority_receipt.stdout_digest !== sha256(review_output)) {
180
+ fail('UNTRUSTED_REVIEW_EVIDENCE', 'review requires output bound to a successful terminal authority receipt');
181
+ }
40
182
  const artifact = loadRuntimeInstallationV2(projectDir, { clock });
41
183
  if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'RAIL review requires a v2 installation');
42
184
  const validated = acceptReview({ review, invocation, installation: artifact.installation, snapshot: artifact.capability_snapshot, clock });
43
- const lineage = resolveAttemptLineage(projectDir, validated.attempt_id, clock);
44
- const path = reviewPath(projectDir, validated.attempt_id, validated.review_id);
45
- const content = `${JSON.stringify({ review: validated, invocation, lineage }, null, 2)}\n`;
46
- const stored = readReviewEvidence(projectDir, validated.attempt_id);
47
- const matching = stored.find((entry) => entry.review?.review_id === validated.review_id);
48
- if (matching && readFileSync(path, 'utf8') !== content) fail('REVIEW_STORAGE_CONFLICT', `review ${validated.review_id} already exists with different material`);
49
- const prior = stored.filter((entry) => entry.review?.review_id !== validated.review_id);
185
+ const baseLineage = resolveAttemptLineage(projectDir, validated.attempt_id, clock);
186
+ const engine = createProjectRailEngine({ projectDir, clock });
187
+ const review_ref = `rail-review://${baseLineage.handoff_id}/${validated.attempt_id}/${validated.review_id}`;
188
+ const existingJournal = engine.records.find((record) => record.type === 'review_decided' && record.payload?.review_ref === review_ref);
189
+ if (existingJournal) {
190
+ if (result_commit_ref !== undefined && result_commit_ref !== existingJournal.payload.result_commit_ref) {
191
+ fail('REVIEW_GIT_SUBJECT_CHANGED', 'Git subject differs from the immutable canonical review');
192
+ }
193
+ const lineage = Object.freeze({ ...baseLineage, result_commit_ref: existingJournal.payload.result_commit_ref });
194
+ const journal = engine.recordReviewDecision({
195
+ attempt_id: validated.attempt_id, review_ref, decision: existingJournal.payload.decision, review: validated, invocation,
196
+ terminal_authority_receipt,
197
+ installation: artifact.installation, snapshot: artifact.capability_snapshot,
198
+ git_dir: projectDir, result_commit_ref: existingJournal.payload.result_commit_ref,
199
+ });
200
+ return Object.freeze({ review_ref, lineage, review: validated, decision: existingJournal.payload.decision, journal, written: false });
201
+ }
202
+ const resolvedSubject = resolveReviewGitSubject({ projectDir, attempt_id: validated.attempt_id, clock });
203
+ if (result_commit_ref !== undefined && result_commit_ref !== resolvedSubject.result_commit_ref) {
204
+ fail('REVIEW_GIT_SUBJECT_CHANGED', 'Git HEAD changed after the review subject was captured');
205
+ }
206
+ const lineage = resolvedSubject;
207
+ const prior = engine.reviewEvidenceForAttempt(validated.attempt_id);
50
208
  const decision = decideReviewProgress({
51
209
  prior_review_evidence: prior, current_review: validated, reviewer_invocation: invocation,
52
210
  installation: artifact.installation, snapshot: artifact.capability_snapshot, clock,
53
211
  });
54
- const review_ref = `rail-review://${lineage.handoff_id}/${validated.attempt_id}/${validated.review_id}`;
55
- let written = false;
56
- if (!matching) {
57
- mkdirSync(reviewDir(projectDir, validated.attempt_id), { recursive: true, mode: 0o700 });
58
- const temporary = `${path}.${process.pid}.tmp`;
59
- writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
60
- try {
61
- linkSync(temporary, path);
62
- written = true;
63
- } catch (error) {
64
- if (error?.code !== 'EEXIST' || readFileSync(path, 'utf8') !== content) fail('REVIEW_STORAGE_CONFLICT', `review ${validated.review_id} raced with different material`);
65
- } finally {
66
- if (existsSync(temporary)) unlinkSync(temporary);
67
- }
68
- }
69
- const journal = createProjectRailEngine({ projectDir, clock }).recordReviewDecision({
212
+ const journal = engine.recordReviewDecision({
70
213
  attempt_id: validated.attempt_id, review_ref, decision, review: validated, invocation,
71
- prior_review_evidence: prior, installation: artifact.installation, snapshot: artifact.capability_snapshot,
214
+ terminal_authority_receipt,
215
+ installation: artifact.installation, snapshot: artifact.capability_snapshot,
216
+ git_dir: projectDir, result_commit_ref: lineage.result_commit_ref,
72
217
  });
73
- return Object.freeze({ path, review_ref, lineage, review: validated, decision, journal, written });
218
+ return Object.freeze({ review_ref, lineage, review: validated, decision, journal, written: !journal.replay });
74
219
  }
@@ -1,13 +1,17 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { INSTALLATION_ARTIFACT_PATH, doctorV2 } from '../installer-v2/index.js';
1
+ import { doctorV2 } from '../installer-v2/index.js';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
4
  import { assertEligibleInvocation } from '@chati/provider-registry';
5
+ import { selectCanonicalReviewer } from '@chati/review-council';
6
+ import {
7
+ readInstallationArtifactFile, verifyInstallationCatalogProvenance,
8
+ } from '../installer-v2/catalog-client.js';
9
+ import { verifyInstallationArtifactAuthority } from '../installer-v2/installation-authority.js';
5
10
 
6
11
  const AGENT_ACTIONS = Object.freeze({
7
12
  'greenfield-wu': 'discovery', 'brownfield-wu': 'discovery', brief: 'discovery',
8
13
  detail: 'planning', architect: 'planning', ux: 'planning', phases: 'planning', tasks: 'planning',
9
- 'qa-planning': 'review', dev: 'build', 'qa-implementation': 'review', 'qa-visual': 'review',
10
- devops: 'build',
14
+ 'qa-planning': 'review',
11
15
  });
12
16
 
13
17
  export const V2_PLANNING_AGENTS = Object.freeze([
@@ -15,7 +19,78 @@ export const V2_PLANNING_AGENTS = Object.freeze([
15
19
  'detail', 'architect', 'ux', 'qa-planning', 'phases', 'tasks',
16
20
  ]);
17
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
+ });
31
+
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;
44
+ throw error;
45
+ }
46
+ return Object.freeze({ valid: true, required: Object.freeze(required) });
47
+ }
48
+
18
49
  const RAIL_ACTIONS = new Set(['build', 'review']);
50
+ const REASONING_CONFIGURATIONS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
51
+
52
+ function highestCompatibleReasoning(binding, model) {
53
+ const maximum = model.highest_reasoning_configuration || 'xhigh';
54
+ const maximumRank = REASONING_CONFIGURATIONS.indexOf(maximum);
55
+ if (maximumRank < 0) return null;
56
+ const allowed = new Set(binding.allowed_reasoning_configurations || []);
57
+ for (let rank = maximumRank; rank >= 0; rank -= 1) {
58
+ const candidate = REASONING_CONFIGURATIONS[rank];
59
+ if (allowed.has(candidate)) return candidate;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function nearestCompatibleReasoning(binding, model, preferred) {
65
+ const maximum = model.highest_reasoning_configuration || 'xhigh';
66
+ const maximumRank = REASONING_CONFIGURATIONS.indexOf(maximum);
67
+ const preferredRank = REASONING_CONFIGURATIONS.indexOf(preferred);
68
+ if (maximumRank < 0 || preferredRank < 0) return null;
69
+ const allowed = (binding.allowed_reasoning_configurations || [])
70
+ .filter((candidate) => {
71
+ const rank = REASONING_CONFIGURATIONS.indexOf(candidate);
72
+ return rank >= 0 && rank <= maximumRank;
73
+ });
74
+ return allowed.sort((left, right) => {
75
+ const leftRank = REASONING_CONFIGURATIONS.indexOf(left);
76
+ const rightRank = REASONING_CONFIGURATIONS.indexOf(right);
77
+ return Math.abs(leftRank - preferredRank) - Math.abs(rightRank - preferredRank)
78
+ || leftRank - rightRank;
79
+ })[0] || null;
80
+ }
81
+
82
+ function compatibleReasoning(binding, model, requested, preferred) {
83
+ if (requested === undefined || requested === null) {
84
+ return nearestCompatibleReasoning(binding, model, preferred);
85
+ }
86
+ const requestedRank = REASONING_CONFIGURATIONS.indexOf(requested);
87
+ const maximumRank = REASONING_CONFIGURATIONS.indexOf(model.highest_reasoning_configuration || 'xhigh');
88
+ if (requestedRank < 0 || maximumRank < 0 || requestedRank > maximumRank
89
+ || !binding.allowed_reasoning_configurations?.includes(requested)) {
90
+ return null;
91
+ }
92
+ return requested;
93
+ }
19
94
 
20
95
  export function describeRuntimeInstallationV2(artifact) {
21
96
  const railExecutionPool = artifact.installation.enabled_providers.map((binding) => {
@@ -48,21 +123,54 @@ export function describeRuntimeInstallationV2(artifact) {
48
123
  });
49
124
  }
50
125
 
51
- export function loadRuntimeInstallationV2(projectDir, { clock = () => new Date() } = {}) {
52
- const path = join(projectDir, INSTALLATION_ARTIFACT_PATH);
53
- if (!existsSync(path)) return null;
126
+ export function loadRuntimeInstallationV2(projectDir, {
127
+ clock = () => new Date(), authorityDirectory, source = null,
128
+ } = {}) {
129
+ const installationSource = source ?? readInstallationArtifactFile(projectDir);
130
+ if (!installationSource) {
131
+ if (existsSync(join(resolve(projectDir), '.chati', 'v2'))) {
132
+ throw Object.assign(new Error('V2 installation artifact is required for a V2 project'), {
133
+ code: 'INSTALLATION_ARTIFACT_REQUIRED',
134
+ });
135
+ }
136
+ return null;
137
+ }
54
138
  let artifact;
55
- try { artifact = JSON.parse(readFileSync(path, 'utf8')); } catch (error) {
139
+ try { artifact = JSON.parse(installationSource.content); } catch (error) {
56
140
  throw Object.assign(new Error('Invalid v2 installation artifact'), { code: 'INVALID_INSTALLATION_ARTIFACT', cause: error });
57
141
  }
58
142
  const doctor = doctorV2(artifact, { clock });
59
143
  if (!doctor.passed) throw Object.assign(new Error('V2 installation artifact failed doctor'), { code: doctor.checks[0]?.code || 'INVALID_INSTALLATION_ARTIFACT' });
144
+ verifyInstallationArtifactAuthority(projectDir, artifact, { authorityDirectory });
145
+ verifyInstallationCatalogProvenance(projectDir, artifact.capability_snapshot, { clock });
60
146
  return Object.freeze(artifact);
61
147
  }
62
148
 
63
- export function resolveRuntimeInvocationV2({ artifact, agent, binding, model_id, reasoning_configuration, clock = () => new Date() }) {
64
- const action = AGENT_ACTIONS[agent];
65
- if (!action) throw Object.assign(new Error(`No v2 action mapping for agent ${agent}`), { code: 'UNMAPPED_AGENT_ACTION' });
149
+ export function resolveRuntimeExecutableBindingV2({ artifact, provider_id, harness_id } = {}) {
150
+ const binding = artifact?.installation?.enabled_providers?.find((candidate) =>
151
+ candidate.provider_id === provider_id && candidate.harness_id === harness_id);
152
+ if (!binding) {
153
+ throw Object.assign(new Error('provider executable binding is not part of the selected installation'), { code: 'BINDING_NOT_SELECTED' });
154
+ }
155
+ if (typeof binding.executable_path !== 'string' || !binding.executable_path.startsWith('/')
156
+ || !/^[a-f0-9]{64}$/.test(binding.executable_sha256 || '')) {
157
+ throw Object.assign(
158
+ new Error(`installed ${provider_id}/${harness_id} binding does not seal a provider executable`),
159
+ { code: 'PROVIDER_EXECUTABLE_BINDING_REQUIRED' },
160
+ );
161
+ }
162
+ return Object.freeze({
163
+ executable_path: binding.executable_path,
164
+ executable_sha256: binding.executable_sha256,
165
+ supporting_executables: Object.freeze((binding.supporting_executables || []).map((entry) => Object.freeze({
166
+ executable_path: entry.executable_path,
167
+ executable_sha256: entry.executable_sha256,
168
+ }))),
169
+ });
170
+ }
171
+
172
+ function resolveRuntimeActionV2({ artifact, action, routingKey, binding, model_id, reasoning_configuration, clock = () => new Date() }) {
173
+ let hasEligibleModel = false;
66
174
  const candidates = artifact.installation.enabled_providers.flatMap((candidateBinding) => {
67
175
  if (binding && (candidateBinding.provider_id !== binding.provider_id || candidateBinding.harness_id !== binding.harness_id)) return [];
68
176
  return candidateBinding.allowed_models.flatMap((candidateModelId) => {
@@ -71,10 +179,21 @@ export function resolveRuntimeInvocationV2({ artifact, agent, binding, model_id,
71
179
  candidate.provider_id === candidateBinding.provider_id && candidate.model_id === candidateModelId
72
180
  );
73
181
  if (!catalogModel?.actions.includes(action)) return [];
182
+ hasEligibleModel = true;
183
+ const defaultReasoning = catalogModel.tier === 'worker'
184
+ ? 'low'
185
+ : catalogModel.tier === 'workhorse'
186
+ ? 'medium'
187
+ : 'high';
188
+ const reasoning = compatibleReasoning(
189
+ candidateBinding, catalogModel, reasoning_configuration, defaultReasoning,
190
+ );
191
+ if (!reasoning) return [];
74
192
  return [{
75
193
  binding: candidateBinding,
76
194
  model: catalogModel,
77
- priority: catalogModel.routing_priority?.[agent] ?? catalogModel.routing_priority?.[action] ?? 0,
195
+ reasoning,
196
+ priority: catalogModel.routing_priority?.[routingKey] ?? catalogModel.routing_priority?.[action] ?? 0,
78
197
  }];
79
198
  });
80
199
  }).sort((left, right) =>
@@ -83,22 +202,119 @@ export function resolveRuntimeInvocationV2({ artifact, agent, binding, model_id,
83
202
  || `${left.binding.harness_id}:${left.model.model_id}`.localeCompare(`${right.binding.harness_id}:${right.model.model_id}`)
84
203
  );
85
204
  const selectedCandidate = candidates[0];
86
- if (!selectedCandidate) throw Object.assign(new Error(`No eligible installed model for ${agent}/${action}`), { code: 'NO_ELIGIBLE_MODEL' });
205
+ if (!selectedCandidate) {
206
+ throw Object.assign(
207
+ new Error(`No eligible installed model and reasoning configuration for ${routingKey}/${action}`),
208
+ { code: hasEligibleModel ? 'NO_ELIGIBLE_REASONING' : 'NO_ELIGIBLE_MODEL' },
209
+ );
210
+ }
87
211
  const selected = selectedCandidate.binding;
88
212
  const model = selectedCandidate.model.model_id;
89
- const defaultReasoning = selectedCandidate.model.tier === 'worker'
90
- ? 'low'
91
- : selectedCandidate.model.tier === 'workhorse'
92
- ? 'medium'
93
- : 'high';
94
213
  const invocation = {
95
214
  provider_id: selected.provider_id, harness_id: selected.harness_id, action,
96
215
  model_pin: {
97
216
  model_id: model,
98
217
  catalog_snapshot_ref: artifact.capability_snapshot.snapshot_id,
99
- reasoning_configuration: reasoning_configuration ?? defaultReasoning,
218
+ reasoning_configuration: selectedCandidate.reasoning,
100
219
  },
101
220
  };
102
221
  assertEligibleInvocation({ installation: artifact.installation, snapshot: artifact.capability_snapshot, invocation, clock });
103
222
  return Object.freeze(invocation);
104
223
  }
224
+
225
+ export function resolveRuntimeInvocationV2({ artifact, agent, binding, model_id, reasoning_configuration, clock = () => new Date() }) {
226
+ if (!V2_PLANNING_AGENTS.includes(agent)) {
227
+ throw Object.assign(
228
+ new Error(`Agent ${agent} is not executable through the v2 planning runtime; implementation and release must use RAIL`),
229
+ { code: 'V2_RAIL_EXECUTION_REQUIRED' },
230
+ );
231
+ }
232
+ const action = AGENT_ACTIONS[agent];
233
+ if (!action) throw Object.assign(new Error(`No v2 action mapping for agent ${agent}`), { code: 'UNMAPPED_AGENT_ACTION' });
234
+ return resolveRuntimeActionV2({ artifact, action, routingKey: agent, binding, model_id, reasoning_configuration, clock });
235
+ }
236
+
237
+ export function resolveRuntimeRailInvocationV2({ artifact, action = 'build', binding, model_id, reasoning_configuration, clock = () => new Date() }) {
238
+ if (!RAIL_ACTIONS.has(action)) {
239
+ throw Object.assign(new Error(`Action ${action} is not executable through RAIL`), { code: 'INVALID_RAIL_ACTION' });
240
+ }
241
+ return resolveRuntimeActionV2({ artifact, action, routingKey: `rail:${action}`, binding, model_id, reasoning_configuration, clock });
242
+ }
243
+
244
+ /** Selects the highest-ranked installed reviewer at the strongest available independence level. */
245
+ export function resolveRuntimeRailReviewerV2({ artifact, builder_identity, clock = () => new Date() }) {
246
+ return selectCanonicalReviewer({
247
+ installation: artifact.installation,
248
+ snapshot: artifact.capability_snapshot,
249
+ builder_identity,
250
+ clock,
251
+ });
252
+ }
253
+
254
+ export function resolveRuntimeAdjudicationV2({ artifact, clock = () => new Date() }) {
255
+ const candidates = artifact.installation.enabled_providers.flatMap((binding) =>
256
+ binding.allowed_models.flatMap((modelId) => {
257
+ const model = artifact.capability_snapshot.models.find((candidate) =>
258
+ candidate.provider_id === binding.provider_id && candidate.model_id === modelId
259
+ );
260
+ if (!model?.actions.includes('adjudication')) return [];
261
+ const reasoning = highestCompatibleReasoning(binding, model);
262
+ if (!reasoning) return [];
263
+ return [{ binding, model, reasoning }];
264
+ })
265
+ ).sort((left, right) =>
266
+ (right.model.adjudication_priority ?? 0) - (left.model.adjudication_priority ?? 0)
267
+ || (right.model.routing_priority?.adjudication ?? 0) - (left.model.routing_priority?.adjudication ?? 0)
268
+ || `${left.binding.harness_id}:${left.model.model_id}`.localeCompare(`${right.binding.harness_id}:${right.model.model_id}`)
269
+ );
270
+ const selected = candidates[0];
271
+ if (!selected) throw Object.assign(new Error('No eligible installed model for adjudication'), { code: 'NO_ELIGIBLE_ADJUDICATOR' });
272
+ const invocation = {
273
+ provider_id: selected.binding.provider_id,
274
+ harness_id: selected.binding.harness_id,
275
+ action: 'adjudication',
276
+ model_pin: {
277
+ model_id: selected.model.model_id,
278
+ catalog_snapshot_ref: artifact.capability_snapshot.snapshot_id,
279
+ reasoning_configuration: selected.reasoning,
280
+ },
281
+ };
282
+ assertEligibleInvocation({ installation: artifact.installation, snapshot: artifact.capability_snapshot, invocation, clock });
283
+ return Object.freeze(invocation);
284
+ }
285
+
286
+ /**
287
+ * Revalidate a terminal command against the exact route selected from the
288
+ * integrity-checked installation artifact derived from the signed capability
289
+ * catalog. Eligibility alone is insufficient because an
290
+ * installation can permit several models while the router selected one.
291
+ */
292
+ export function assertRuntimeCommandV2({ artifact, agent, command = {}, reasoning_escalation = false, clock = () => new Date() }) {
293
+ const expected = reasoning_escalation
294
+ ? resolveRuntimeAdjudicationV2({ artifact, clock })
295
+ : resolveRuntimeInvocationV2({ artifact, agent, clock });
296
+ const actual = {
297
+ provider_id: command.provider_id || null,
298
+ harness_id: command.harness_id || null,
299
+ model_id: command.model_id || null,
300
+ reasoning_configuration: command.reasoning_configuration || null,
301
+ catalog_snapshot_ref: command.catalog_snapshot_ref || null,
302
+ strict_provider: command.strict_provider === true,
303
+ };
304
+ const comparisons = [
305
+ ['provider_id', expected.provider_id, actual.provider_id],
306
+ ['harness_id', expected.harness_id, actual.harness_id],
307
+ ['model_id', expected.model_pin.model_id, actual.model_id],
308
+ ['reasoning_configuration', expected.model_pin.reasoning_configuration, actual.reasoning_configuration],
309
+ ['catalog_snapshot_ref', expected.model_pin.catalog_snapshot_ref, actual.catalog_snapshot_ref],
310
+ ['strict_provider', true, actual.strict_provider],
311
+ ];
312
+ const mismatches = comparisons.filter(([, expectedValue, actualValue]) => expectedValue !== actualValue);
313
+ if (mismatches.length > 0) {
314
+ throw Object.assign(
315
+ new Error(`Runtime command does not match the integrity-checked v2 route: ${mismatches.map(([field]) => field).join(', ')}`),
316
+ { code: 'RUNTIME_ROUTE_BINDING_MISMATCH', mismatches: mismatches.map(([field]) => field) },
317
+ );
318
+ }
319
+ return expected;
320
+ }