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,71 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+ import { ContractError } from '@chati/core';
3
+ import { assertBrowserCapabilityForTask } from '@chati/browser-capability';
4
+
5
+ export const BROWSER_OBSERVER_TOOL_ID = 'chati-playwright-api-v1';
6
+ function fail(message, code = 'BROWSER_OBSERVATION_INVALID') { throw new ContractError(code, message); }
7
+ function exact(value, fields, label) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value)
9
+ || Object.keys(value).length !== fields.length || Object.keys(value).some((key) => !fields.includes(key))) fail(`${label} has an invalid closed schema`);
10
+ }
11
+ function text(value, limit = 8192) { return typeof value === 'string' && value.length > 0 && !value.includes('\0') && Buffer.byteLength(value) <= limit; }
12
+ function localPath(value) { return text(value) && value.startsWith('/') && !value.startsWith('//') && !value.includes('\\'); }
13
+ function executable(value) {
14
+ exact(value, ['executable_path', 'executable_sha256'], 'executable binding');
15
+ if (!text(value.executable_path) || !isAbsolute(value.executable_path) || resolve(value.executable_path) !== value.executable_path
16
+ || !/^[a-f0-9]{64}$/.test(value.executable_sha256)) fail('browser tools require absolute paths and SHA-256 bindings');
17
+ }
18
+
19
+ /** Shared schema for the frozen plan and its resolved, observed execution. */
20
+ export function validateBrowserObservationPlan(input, { frozen = false } = {}) {
21
+ exact(input, ['application', 'browser', 'hostname', 'ready_path', 'timeout_ms', 'steps'], 'browser observation plan');
22
+ exact(input.application, ['executable_binding', 'args', frozen ? 'stdin_ref' : 'stdin', 'port',
23
+ ...(Object.hasOwn(input.application ?? {}, 'listener_transport') ? ['listener_transport'] : [])], 'application');
24
+ executable(input.application.executable_binding);
25
+ const app = input.application;
26
+ if (Object.hasOwn(app, 'listener_transport') && app.listener_transport !== 'darwin_prebound_tcp_v1') fail('unsupported explicit application listener transport');
27
+ const scriptValid = frozen ? /^local-artifact:\/\/sha256\/[a-f0-9]{64}$/.test(app.stdin_ref || '') : text(app.stdin, 1024 * 1024);
28
+ if (!Array.isArray(app.args) || app.args.length > 64 || app.args.some((arg) => typeof arg !== 'string' || arg.includes('\0') || Buffer.byteLength(arg) > 8192)
29
+ || !scriptValid || !Number.isInteger(app.port) || app.port < 1024 || app.port > 65535) fail('application requires bounded literal arguments, frozen startup content and port');
30
+ exact(input.browser, ['executable_binding', 'playwright_version'], 'browser');
31
+ executable(input.browser.executable_binding);
32
+ if (!/^\d+\.\d+\.\d+$/.test(input.browser.playwright_version)) fail('browser driver version must be explicit');
33
+ if (!text(input.hostname, 253) || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(input.hostname)
34
+ || !localPath(input.ready_path) || !Number.isInteger(input.timeout_ms) || input.timeout_ms < 1 || input.timeout_ms > 1800000) fail('browser requires a DNS hostname, local readiness path and bounded timeout');
35
+ if (!Array.isArray(input.steps) || input.steps.length < 2 || input.steps.length > 64 || input.steps[0]?.action !== 'navigate') fail('browser requires navigation and a bounded nonempty scenario');
36
+ let assertions = 0;
37
+ for (const step of input.steps) {
38
+ const fields = { navigate: ['action', 'path'], click: ['action', 'selector'], fill: ['action', 'selector', 'text'],
39
+ assert_text: ['action', 'selector', 'text'], assert_visible: ['action', 'selector'] }[step?.action];
40
+ if (!fields) fail('unsupported browser action');
41
+ exact(step, fields, 'browser step');
42
+ if (step.action === 'navigate' ? !localPath(step.path) : !text(step.selector, 2048)) fail('browser step has an invalid path or selector');
43
+ if ('text' in step && (typeof step.text !== 'string' || step.text.includes('\0') || Buffer.byteLength(step.text) > 8192)) fail('browser input and assertion text must be bounded');
44
+ if (step.action.startsWith('assert_')) assertions += 1;
45
+ }
46
+ if (assertions === 0) fail('browser scenario must contain an actual assertion');
47
+ return input;
48
+ }
49
+
50
+ export function validateTaskBrowserVerificationPlan(task, { required = false } = {}) {
51
+ const plan = task?.browser_verification_plan;
52
+ if (plan === undefined) {
53
+ if (required) fail('browser verification requires a council-approved frozen plan', 'BROWSER_VERIFICATION_PLAN_REQUIRED');
54
+ return null;
55
+ }
56
+ try {
57
+ exact(plan, ['schema_version', 'tool_id', 'evidence_kind', 'criterion_refs', 'observation'], 'frozen browser verification plan');
58
+ const policy = assertBrowserCapabilityForTask(task);
59
+ if (task.requires_browser !== true || !policy || policy.mode !== 'unauthenticated'
60
+ || policy.tool !== 'approved-other' || policy.approved_tool_id !== BROWSER_OBSERVER_TOOL_ID
61
+ || plan.tool_id !== BROWSER_OBSERVER_TOOL_ID || plan.schema_version !== 1 || plan.evidence_kind !== 'dom-assertions') {
62
+ fail('the local observer requires an explicit unauthenticated Playwright API policy and DOM-assertion evidence, not visual-quality approval');
63
+ }
64
+ const known = new Set(task.acceptance_criteria_refs);
65
+ if (known.size === 0 || !Array.isArray(plan.criterion_refs) || plan.criterion_refs.length !== known.size
66
+ || new Set(plan.criterion_refs).size !== known.size || plan.criterion_refs.some((ref) => !known.has(ref))) fail('browser scenario must bind every task acceptance criterion exactly once');
67
+ validateBrowserObservationPlan(plan.observation, { frozen: true });
68
+ if (!policy.allowed_domains.some((domain) => plan.observation.hostname === domain || plan.observation.hostname.endsWith(`.${domain}`))) fail('browser scenario hostname is outside the task policy');
69
+ } catch (error) { fail(error.message, 'BROWSER_VERIFICATION_PLAN_INVALID'); }
70
+ return plan;
71
+ }
@@ -1,6 +1,12 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import { ContractError, canonicalize, sha256 } from '@chati/core';
3
3
  import { assertBrowserCapabilityForTask } from '@chati/browser-capability';
4
+ import { validatePlanningMaterial } from './planning-material.js';
5
+ import { validateTaskVerificationPlan } from './verification-plan.js';
6
+ import { validateTaskBrowserVerificationPlan } from './browser-verification-plan.js';
7
+ export { validatePlanningMaterial, PLANNING_MATERIAL_MAX_BYTES } from './planning-material.js';
8
+ export { validateTaskVerificationPlan } from './verification-plan.js';
9
+ export { validateTaskBrowserVerificationPlan, validateBrowserObservationPlan, BROWSER_OBSERVER_TOOL_ID } from './browser-verification-plan.js';
4
10
 
5
11
  const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
6
12
  const GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i;
@@ -64,6 +70,21 @@ function validateTask(task, knownTaskIds) {
64
70
  for (const field of ['provider_id', 'harness_id', 'model_id', 'reasoning_configuration']) {
65
71
  assertString(task.execution_binding[field], 'TASK_EXECUTION_BINDING_REQUIRED', `task.execution_binding.${field}`);
66
72
  }
73
+ if (task.review_binding !== undefined) {
74
+ assertObject(task.review_binding, 'TASK_REVIEW_BINDING_REQUIRED', 'task.review_binding');
75
+ for (const field of ['provider_id', 'harness_id', 'model_id', 'reasoning_configuration']) {
76
+ assertString(task.review_binding[field], 'TASK_REVIEW_BINDING_REQUIRED', `task.review_binding.${field}`);
77
+ }
78
+ if (task.review_binding.attained_independence_level !== undefined && !['A', 'B', 'C'].includes(task.review_binding.attained_independence_level)) {
79
+ fail('INVALID_HANDOFF_TASK', 'task.review_binding.attained_independence_level must be A, B or C');
80
+ }
81
+ }
82
+ if (task.requirement_refs !== undefined) {
83
+ assertStringArray(task.requirement_refs, 'TASK_REQUIREMENT_TRACEABILITY_REQUIRED', 'task.requirement_refs', { allowEmpty: false });
84
+ if (new Set(task.requirement_refs).size !== task.requirement_refs.length) fail('TASK_REQUIREMENT_TRACEABILITY_REQUIRED', 'task.requirement_refs must be unique');
85
+ }
86
+ if (task.routing_profile !== undefined) assertObject(task.routing_profile, 'INVALID_HANDOFF_TASK', 'task.routing_profile');
87
+ if (task.effort_plan !== undefined) assertObject(task.effort_plan, 'INVALID_HANDOFF_TASK', 'task.effort_plan');
67
88
  if (task.clickup_ref !== undefined) assertImmutableRef(task.clickup_ref, 'INVALID_HANDOFF_TASK', 'task.clickup_ref');
68
89
  if (task.requires_browser !== undefined && typeof task.requires_browser !== 'boolean') fail('INVALID_HANDOFF_TASK', 'task.requires_browser must be boolean');
69
90
  if (task.requires_browser === true && (!task.browser_policy || typeof task.browser_policy !== 'object' || Array.isArray(task.browser_policy))) {
@@ -132,6 +153,11 @@ export function validateSealedHandoff(handoff) {
132
153
  taskIds.add(task.task_id);
133
154
  }
134
155
  for (const task of handoff.tasks) validateTask(task, taskIds);
156
+ for (const task of handoff.tasks) {
157
+ validateTaskVerificationPlan(task);
158
+ validateTaskBrowserVerificationPlan(task);
159
+ }
160
+ validatePlanningMaterial(handoff);
135
161
  validateLineage(handoff.lineage, handoff);
136
162
  assertObject(handoff.seal, 'INVALID_HANDOFF_SEAL', 'seal');
137
163
  assertString(handoff.seal.sealed_by, 'INVALID_HANDOFF_SEAL', 'seal.sealed_by');
@@ -198,6 +224,7 @@ export function assertDispatchable({ handoff, task_id, completed_task_ids = [],
198
224
  git_commit_ref: validated.seal.git_commit_ref,
199
225
  task_id: task.task_id,
200
226
  execution_binding: task.execution_binding,
227
+ ...(task.review_binding === undefined ? {} : { review_binding: task.review_binding }),
201
228
  }));
202
229
  }
203
230
 
@@ -220,6 +247,12 @@ export function validateAttemptBinding({ attempt, handoff }) {
220
247
  const task = validated.tasks.find((candidate) => candidate.task_id === attempt.task_id);
221
248
  if (!task) fail('ATTEMPT_TASK_DRIFT', 'attempt task is not part of bound handoff');
222
249
  if (sha256(attempt.execution_binding) !== sha256(task.execution_binding)) fail('ATTEMPT_EXECUTION_BINDING_DRIFT', 'attempt execution binding differs from the sealed task');
250
+ if (task.review_binding !== undefined && sha256(attempt.review_binding) !== sha256(task.review_binding)) {
251
+ fail('ATTEMPT_REVIEW_BINDING_DRIFT', 'attempt review binding differs from the sealed task');
252
+ }
253
+ if (task.review_binding === undefined && attempt.review_binding !== undefined) {
254
+ fail('ATTEMPT_REVIEW_BINDING_DRIFT', 'attempt has an unexpected review binding');
255
+ }
223
256
  return deepFreeze(canonicalize(attempt));
224
257
  }
225
258
 
@@ -0,0 +1,64 @@
1
+ import { ContractError, canonicalSerialize, sha256 } from '@chati/core';
2
+
3
+ export const PLANNING_MATERIAL_MAX_BYTES = 1_000_000;
4
+
5
+ function invalid(message) {
6
+ throw new ContractError('PLANNING_MATERIAL_INVALID', message);
7
+ }
8
+
9
+ /** Validate the small inline bundle carried by the handoff, never live paths. */
10
+ export function validatePlanningMaterial(handoff, { required = false } = {}) {
11
+ const material = handoff?.planning_material;
12
+ if (material === undefined) {
13
+ if (required) throw new ContractError('PLANNING_MATERIAL_REQUIRED', 'this handoff has no frozen Planning content; regenerate it through Tasks before execution');
14
+ return null;
15
+ }
16
+ if (!material || material.schema_version !== 1 || !Array.isArray(material.artifacts)
17
+ || material.artifacts.length === 0 || !Array.isArray(material.scope_bindings)) {
18
+ invalid('Planning material requires version 1, artifacts and scope bindings');
19
+ }
20
+ if (Buffer.byteLength(canonicalSerialize(material), 'utf8') > PLANNING_MATERIAL_MAX_BYTES) {
21
+ throw new ContractError('PLANNING_MATERIAL_SIZE_LIMIT', `frozen Planning content exceeds the ${PLANNING_MATERIAL_MAX_BYTES} byte budget`);
22
+ }
23
+ const byRef = new Map();
24
+ for (const artifact of material.artifacts) {
25
+ if (!artifact || typeof artifact.content !== 'string' || typeof artifact.ref !== 'string' || !artifact.ref.trim()
26
+ || artifact.sha256 !== sha256(artifact.content) || byRef.has(artifact.ref)
27
+ || (artifact.ref.startsWith('local-artifact://') && artifact.ref !== `local-artifact://sha256/${artifact.sha256}`)) {
28
+ invalid('each Planning artifact must have unique content matching its SHA-256 reference');
29
+ }
30
+ byRef.set(artifact.ref, artifact.content);
31
+ }
32
+ const requiredRefs = [
33
+ ...Object.values(handoff.source_artifacts ?? {}), handoff.task_graph_ref,
34
+ ...(handoff.tasks ?? []).flatMap((task) => task.acceptance_criteria_refs ?? []),
35
+ ...(handoff.tasks ?? []).flatMap((task) => task.verification_plan?.checks?.map((check) => check.stdin_ref) ?? []),
36
+ ...(handoff.tasks ?? []).flatMap((task) => task.browser_verification_plan ? [task.browser_verification_plan.observation.application.stdin_ref] : []),
37
+ ];
38
+ if (!handoff.source_artifacts?.task_details_ref || requiredRefs.some((ref) => !byRef.has(ref))) {
39
+ invalid('Planning material must resolve the task graph, every source, task details and every acceptance criterion');
40
+ }
41
+ if (!byRef.get(handoff.source_artifacts.task_details_ref).trim()) {
42
+ invalid('frozen task details must contain explicit task scopes');
43
+ }
44
+ for (const task of handoff.tasks ?? []) {
45
+ if (task.verification_plan?.checks?.some((check) => !byRef.get(check.stdin_ref)?.trim())) {
46
+ invalid('frozen verification scripts must contain explicit checks');
47
+ }
48
+ if (task.browser_verification_plan && !byRef.get(task.browser_verification_plan.observation.application.stdin_ref)?.trim()) {
49
+ invalid('frozen browser startup content must be present and nonempty');
50
+ }
51
+ }
52
+ const tasks = new Map((handoff.tasks ?? []).map((task) => [task.task_id, task]));
53
+ const boundTasks = new Set();
54
+ for (const binding of material.scope_bindings) {
55
+ const task = tasks.get(binding?.task_id);
56
+ if (!task || boundTasks.has(task.task_id) || binding.ref !== task.execution_scope_ref
57
+ || binding.artifact_ref !== handoff.source_artifacts.task_details_ref) {
58
+ invalid('each task scope must resolve to the frozen task-details document exactly once');
59
+ }
60
+ boundTasks.add(task.task_id);
61
+ }
62
+ if (boundTasks.size !== tasks.size) invalid('Planning material is missing a task scope binding');
63
+ return material;
64
+ }
@@ -0,0 +1,57 @@
1
+ import { isAbsolute, resolve } from 'node:path';
2
+ import { ContractError } from '@chati/core';
3
+
4
+ function invalid(message) { throw new ContractError('VERIFICATION_PLAN_INVALID', message); }
5
+ function exact(value, fields, label) {
6
+ if (!value || typeof value !== 'object' || Array.isArray(value)
7
+ || Object.keys(value).length !== fields.length || Object.keys(value).some((key) => !fields.includes(key))) {
8
+ invalid(`${label} has an invalid closed schema`);
9
+ }
10
+ }
11
+
12
+ /** Frozen verification policy. Approval is supplied by the handoff council. */
13
+ export function validateTaskVerificationPlan(task, { required = false } = {}) {
14
+ const plan = task?.verification_plan;
15
+ if (plan === undefined) {
16
+ if (required) throw new ContractError('VERIFICATION_PLAN_REQUIRED', 'this task requires an approved frozen verification plan before local evidence can be issued');
17
+ return null;
18
+ }
19
+ exact(plan, ['schema_version', 'checks'], 'verification plan');
20
+ if (plan.schema_version !== 1 || !Array.isArray(plan.checks) || plan.checks.length < 2 || plan.checks.length > 32) {
21
+ invalid('verification plan requires version 1 and 2 to 32 mandatory checks');
22
+ }
23
+ const knownCriteria = new Set(task.acceptance_criteria_refs);
24
+ const covered = new Set();
25
+ const ids = new Set();
26
+ let ciCount = 0;
27
+ for (const check of plan.checks) {
28
+ exact(check, ['check_id', 'kind', 'criterion_refs', 'executable_binding', 'args', 'stdin_ref', 'timeout_ms'], 'verification check');
29
+ if (typeof check.check_id !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(check.check_id) || ids.has(check.check_id)) {
30
+ invalid('check IDs must be unique, bounded and path-inert');
31
+ }
32
+ ids.add(check.check_id);
33
+ if (!['ci', 'acceptance'].includes(check.kind) || !Array.isArray(check.criterion_refs)
34
+ || new Set(check.criterion_refs).size !== check.criterion_refs.length
35
+ || check.criterion_refs.some((ref) => !knownCriteria.has(ref))) invalid('check criterion references must belong to this task');
36
+ if (check.kind === 'ci') {
37
+ ciCount += 1;
38
+ if (check.criterion_refs.length !== 0) invalid('CI checks cannot stand in for criterion-specific acceptance checks');
39
+ } else {
40
+ if (check.criterion_refs.length === 0) invalid('acceptance checks must bind one or more criteria');
41
+ check.criterion_refs.forEach((ref) => covered.add(ref));
42
+ }
43
+ exact(check.executable_binding, ['executable_path', 'executable_sha256'], 'check executable binding');
44
+ const binding = check.executable_binding;
45
+ if (typeof binding.executable_path !== 'string' || !isAbsolute(binding.executable_path)
46
+ || resolve(binding.executable_path) !== binding.executable_path
47
+ || !/^[a-f0-9]{64}$/.test(binding.executable_sha256 || '')) invalid('check executable must have a canonical absolute path and SHA-256');
48
+ if (!Array.isArray(check.args) || check.args.length > 64
49
+ || check.args.some((arg) => typeof arg !== 'string' || arg.includes('\0') || Buffer.byteLength(arg) > 8192)) invalid('check arguments must be bounded literal strings');
50
+ if (typeof check.stdin_ref !== 'string' || !/^local-artifact:\/\/sha256\/[a-f0-9]{64}$/.test(check.stdin_ref)) invalid('check script must be frozen by content reference');
51
+ if (!Number.isInteger(check.timeout_ms) || check.timeout_ms < 1 || check.timeout_ms > 1800000) invalid('check timeout must be 1 to 1800000 milliseconds');
52
+ }
53
+ if (ciCount === 0 || knownCriteria.size === 0 || covered.size !== knownCriteria.size) {
54
+ invalid('verification plan must include CI and acceptance checks covering every task criterion');
55
+ }
56
+ return plan;
57
+ }
@@ -8,6 +8,7 @@
8
8
  "dependencies": {
9
9
  "@chati/core": "0.1.0",
10
10
  "@chati/planning": "0.1.0",
11
+ "@chati/provider-registry": "0.1.0",
11
12
  "@chati/review-council": "0.1.0"
12
13
  },
13
14
  "scripts": {
@@ -0,0 +1,92 @@
1
+ import { ContractError, sha256, assertDarwinTerminalLaunch, compareDarwinHostBootIdentity } from '@chati/core';
2
+
3
+ const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
4
+ const TERMINATION_ATTESTATION_KEYS = ['operator_id', 'attested_at', 'statement'];
5
+ const TERMINATION_ATTESTATION_STATEMENT = 'no-launch-processes-remain';
6
+
7
+ /** Shape only. Never authenticates the operator identity or the machine; the
8
+ * caller binds those separately (claim operator_id, captured host identity). */
9
+ export function isWellFormedTerminationAttestation(attestation) {
10
+ return Boolean(attestation) && typeof attestation === 'object' && !Array.isArray(attestation)
11
+ && Object.keys(attestation).length === 3
12
+ && TERMINATION_ATTESTATION_KEYS.every((key) => Object.hasOwn(attestation, key))
13
+ && typeof attestation.operator_id === 'string' && attestation.operator_id.trim() !== ''
14
+ && typeof attestation.attested_at === 'string' && RFC3339.test(attestation.attested_at)
15
+ && attestation.statement === TERMINATION_ATTESTATION_STATEMENT;
16
+ }
17
+
18
+ export function assertExecutionInterruptionLifetime(dispatch, interruption) {
19
+ const launch = dispatch.payload.launch_receipt;
20
+ if (launch.process_containment_kind !== 'darwin_scoped_process_group_v1') return true;
21
+ const observed = interruption.payload.process;
22
+ try {
23
+ assertDarwinTerminalLaunch(launch);
24
+ if (!observed || Object.hasOwn(interruption.payload, 'process_namespace_identity_digest')
25
+ || interruption.payload.darwin_host_boot_identity_digest !== sha256(launch.darwin_launch_binding.host_boot_identity)
26
+ || observed.status !== 'exited' || observed.signal_sent !== false) {
27
+ throw new Error('interruption requires a native new-boot observation on the same machine');
28
+ }
29
+ // Same machine either way; a reboot (previous-boot) remains the strongest
30
+ // proof, but a same-boot crash may instead carry a human attestation.
31
+ const relation = compareDarwinHostBootIdentity(launch.darwin_launch_binding.host_boot_identity, observed.host_boot_identity);
32
+ if (observed.reason === 'human-attested') {
33
+ if (Object.keys(observed).length !== 5 || !['same-boot', 'previous-boot'].includes(relation)
34
+ || !isWellFormedTerminationAttestation(observed.attestation)) {
35
+ throw new Error('human-attested interruption requires a well formed attestation on the same machine');
36
+ }
37
+ } else if (Object.keys(observed).length !== 4 || observed.reason !== 'previous-boot' || relation !== 'previous-boot') {
38
+ throw new Error('interruption requires a native new-boot observation on the same machine');
39
+ }
40
+ } catch (cause) {
41
+ throw new ContractError('RECOVERY_RESTORATION_PROOF_INVALID', 'Darwin interruption cannot substitute group absence or another host/boot binding', { cause_code: cause.code });
42
+ }
43
+ return true;
44
+ }
45
+
46
+ export function executionCheckpointBinding(dispatch, interruption) {
47
+ const { attempt_id, invocation, claim_id, launch_receipt: launch } = dispatch.payload;
48
+ return { attempt_id, dispatch_event_id: dispatch.event_id, interruption_event_id: interruption.event_id,
49
+ launch_nonce: invocation.launch_nonce, claim_id, launch_receipt_digest: sha256(launch),
50
+ base_commit_ref: launch.execution_base_commit_ref,
51
+ repository_identity_digest: launch.execution_repository_identity_digest };
52
+ }
53
+
54
+ /** Only journal-linked restoration is evidence, never a caller's status flag. */
55
+ export function restoredExecutionProof(records, dispatch, { verifyAuthority, current = false, requireRecovered = false } = {}) {
56
+ const restored = records.find((record) => record.type === 'execution_worktree_restored'
57
+ && record.payload.dispatch_event_id === dispatch.event_id);
58
+ if (!restored) return null;
59
+ const fail = () => { throw new ContractError('RECOVERY_RESTORATION_PROOF_INVALID', 'restoration must bind the exact ordered canonical interruption, checkpoint and dispatch'); };
60
+ const { attempt_id, invocation, launch_receipt: launch } = dispatch.payload;
61
+ const interruption = records.find((record) => record.type === 'execution_interruption_observed'
62
+ && record.payload.dispatch_event_id === dispatch.event_id);
63
+ const checkpoint = records.find((record) => record.type === 'execution_checkpoint_preserved'
64
+ && record.payload.dispatch_event_id === dispatch.event_id);
65
+ if (dispatch.type !== 'execution_dispatched' || !interruption || !checkpoint
66
+ || ![interruption, checkpoint, restored].every((record) => record.payload.attempt_id === attempt_id
67
+ && record.payload.launch_nonce === invocation.launch_nonce)
68
+ || !(records.indexOf(dispatch) < records.indexOf(interruption)
69
+ && records.indexOf(interruption) < records.indexOf(checkpoint) && records.indexOf(checkpoint) < records.indexOf(restored))
70
+ || interruption.payload.status !== 'interrupted' || interruption.payload.process?.status !== 'exited'
71
+ || interruption.payload.launch_receipt_digest !== sha256(launch)
72
+ || interruption.payload.exit_status !== null || interruption.payload.process_elapsed_ms !== null
73
+ || interruption.payload.result_commit_ref !== null || interruption.payload.output_status !== 'unavailable'
74
+ || restored.payload.checkpoint_event_id !== checkpoint.event_id) fail();
75
+ const binding = executionCheckpointBinding(dispatch, interruption);
76
+ assertExecutionInterruptionLifetime(dispatch, interruption);
77
+ verifyAuthority('execution_checkpoint', checkpoint.payload.checkpoint_receipt, {
78
+ schema_version: 1, purpose: 'rail_execution_checkpoint', project_dir: launch.working_dir, binding });
79
+ verifyAuthority('execution_restoration', restored.payload.restoration_receipt, {
80
+ schema_version: 1, purpose: 'rail_execution_restoration', project_dir: launch.working_dir, binding,
81
+ checkpoint_receipt_digest: sha256(checkpoint.payload.checkpoint_receipt), restored_base_commit_ref: binding.base_commit_ref,
82
+ }, { checkpoint_receipt: checkpoint.payload.checkpoint_receipt, current });
83
+ const proof = { status: 'interrupted_restored', launch_nonce: invocation.launch_nonce,
84
+ dispatch_event_id: dispatch.event_id, interruption_event_id: interruption.event_id,
85
+ checkpoint_event_id: checkpoint.event_id, restoration_event_id: restored.event_id,
86
+ restoration_receipt_digest: sha256(restored.payload.restoration_receipt) };
87
+ if (!requireRecovered) return proof;
88
+ const recovered = records.find((record) => record.type === 'attempt_recovered'
89
+ && record.payload.attempt_id === attempt_id && records.indexOf(record) > records.indexOf(restored)
90
+ && record.payload.reconciliation?.execution_dispatches?.some((entry) => sha256(entry) === sha256(proof)));
91
+ return recovered ? { ...proof, recovery_event_id: recovered.event_id } : null;
92
+ }