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,49 @@
1
+ /** Typed process facts, not authentication. Only the private native transport
2
+ * may produce Darwin observations for the trusted canonical verifier. */
3
+ import { createHash } from 'node:crypto';
4
+ import { isAbsolute, resolve } from 'node:path';
5
+ import { canonicalize, darwinSupervisorNonce, parseDarwinReadyMessage, parseDarwinCompletionMessage, sha256 } from '@chati/core';
6
+
7
+ const keys = ['schema_version', 'invocation_context', 'helper_sha256', 'policy_digest', 'working_dir',
8
+ 'arguments_digest', 'stdin_sha256', 'executable_binding_digest', 'ready', 'completion'];
9
+ const digest = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
10
+ const bytesHash = value => createHash('sha256').update(value).digest('hex');
11
+ function invalid() { throw Object.assign(new Error('verification requires a complete native observation of the exact frozen check'), { code: 'VERIFICATION_COMPLETION_INVALID' }); }
12
+
13
+ // Derive facts from the validated/pinned binding, without adding normalizer
14
+ // defaults that were absent from the frozen command's hashed representation.
15
+ export function projectVerificationExecutableBinding(validated, declared) {
16
+ return Object.freeze({ executable_path: validated.executable_path, executable_sha256: validated.executable_sha256,
17
+ ...(Object.hasOwn(declared, 'supporting_executables')
18
+ ? { supporting_executables: structuredClone(validated.supporting_executables) } : {}) });
19
+ }
20
+
21
+ export function createDarwinVerificationBinding({ context, helperSha256, policyDigest, ready, completion, workingDir, executableBinding, args, stdin }) {
22
+ return Object.freeze(canonicalize({ schema_version: 1, invocation_context: context,
23
+ helper_sha256: helperSha256, policy_digest: policyDigest, working_dir: workingDir,
24
+ executable_binding_digest: sha256(executableBinding), arguments_digest: sha256(args), stdin_sha256: bytesHash(stdin), ready, completion }));
25
+ }
26
+
27
+ export function verificationProcessCompleted(facts, expected) {
28
+ if (facts?.containment_kind !== 'darwin_scoped_process_group_v1') {
29
+ return facts?.containment_kind === 'bubblewrap_pid_namespace_v1' && facts.process_tree_stopped === true
30
+ && !Object.hasOwn(facts, 'darwin_verification_binding');
31
+ }
32
+ const binding = facts.darwin_verification_binding;
33
+ if (!binding || Array.isArray(binding) || Object.keys(binding).length !== keys.length || keys.some(key => !Object.hasOwn(binding, key))
34
+ || binding.schema_version !== 1 || facts.process_tree_stopped !== false || facts.process_group_stopped !== true
35
+ || !digest(binding.helper_sha256) || !digest(binding.policy_digest) || !digest(facts.containment_digest)
36
+ || !expected || !isAbsolute(expected.workingDir) || resolve(expected.workingDir) !== binding.working_dir
37
+ || binding.invocation_context?.authority_kind !== 'verification_check'
38
+ || binding.invocation_context.executable_binding_digest !== binding.executable_binding_digest
39
+ || binding.executable_binding_digest !== sha256(expected.executableBinding)
40
+ || sha256(facts.executable_binding) !== binding.executable_binding_digest
41
+ || binding.arguments_digest !== sha256(expected.args) || binding.stdin_sha256 !== bytesHash(expected.stdin)
42
+ || facts.stdin_sha256 !== binding.stdin_sha256) invalid();
43
+ try {
44
+ const ready = parseDarwinReadyMessage(`${JSON.stringify(binding.ready)}\n`, { ...binding.ready,
45
+ nonce: darwinSupervisorNonce(binding.invocation_context) });
46
+ parseDarwinCompletionMessage(`${JSON.stringify(binding.completion)}\n`, ready, facts.exit_code);
47
+ } catch { invalid(); }
48
+ return true;
49
+ }
@@ -0,0 +1,68 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { closeSync, constants, fstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { lstatAtSync, openAtSync } from '@chati/core';
5
+ import { generateClaudeRouter, generateCodexSkill, generateGeminiRouter, generateGrokRouter, generateGrokGovernanceRules } from '../installer/templates.js';
6
+ import { openUpgradeDirectory } from './paths.js';
7
+
8
+ const definitions = Object.freeze([
9
+ ['claude', '.claude/commands/chati.md', generateClaudeRouter],
10
+ ['codex', '.agents/skills/chati/SKILL.md', generateCodexSkill],
11
+ ['grok', '.grok/commands/chati.md', generateGrokRouter],
12
+ ['grok', '.grok/rules/chati-governance.md', generateGrokGovernanceRules],
13
+ ['gemini', '.gemini/commands/chati.toml', generateGeminiRouter],
14
+ ]);
15
+ export const MANAGED_ENTRYPOINT_PATHS = Object.freeze(definitions.map(([, path]) => path));
16
+
17
+ // Exact outputs of production-verified candidate116, both neutral and adapted
18
+ // paths. Signed inventory 65d4466261894dbe167773ee2d80d7be1af55339151864a7dfaace4460f19fd2;
19
+ // templates e8741cbff4b58c391703c6acd83112328055682dbcdaef5cbed3a212abdf32db;
20
+ // core ef9b65f6592f5de4d2367ff54d7a2267c3ad6c182bb70310581a6385016a5825.
21
+ // Never derive legacy ownership from the mutable project's current manifest.
22
+ const legacyHashes = Object.freeze({
23
+ '.claude/commands/chati.md': ['4fddf1519bad9cbd73d1abea2d4090e03faf1aac6330253b2dc0ea0c5c58fc8c', '5894f7307afbc1033fc74eda2ec22020d075db92f3050da126813316785d5d99'],
24
+ '.agents/skills/chati/SKILL.md': ['910c94e5ab8b08a8e357dcce81224a7cb8c898a098105bbcfd7883846a84c076', '43a975bd31e3be126a2ae563071efcb6463a4652ea3e0ab6591f49aab8ed59dd'],
25
+ '.grok/commands/chati.md': ['864ecd0964d28ea4b28a55e4250c3a23035ccee0f9641bc71afe7dadd459928d', '9d69a817220aaaa6f62ad2e33b0d184903dd9d0e83d0066017ebdebeb03be219'],
26
+ '.grok/rules/chati-governance.md': ['dde0f1787ba4ddebdd7471b4f9333435894e99275f7d47df710d1259ac7ee80e'],
27
+ '.gemini/commands/chati.toml': ['440e9625122c58710fc01effaf18e0dc6c32e522bd041b44f3057f71c1940d11', '164bd298566e92021152b9318efcc8ba90bd279f7b7234d675fa72ad791b3535'],
28
+ });
29
+
30
+ export function managedEntrypoints(providers) {
31
+ return definitions.filter(([provider]) => providers.includes(provider)).map(([provider, path, render]) => ({
32
+ path, content: render({ orchestratorPath: `.chati.dev/.adapted/${provider}/orchestrator/chati.md` }),
33
+ neutral: render(),
34
+ }));
35
+ }
36
+
37
+ function readEntrypoint(root, path) {
38
+ const parent = openUpgradeDirectory(root, dirname(path));
39
+ if (parent === undefined) return null;
40
+ let file;
41
+ try {
42
+ try { file = openAtSync(parent, basename(path), constants.O_RDONLY | constants.O_NONBLOCK); }
43
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
44
+ const stat = fstatSync(file);
45
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1024 * 1024) throw Object.assign(new Error(`Unsafe entrypoint: ${path}`), { code: 'UNSAFE_UPGRADE_PATH' });
46
+ const bytes = readFileSync(file);
47
+ const after = lstatAtSync(parent, basename(path));
48
+ if (after.dev !== stat.dev || after.ino !== stat.ino || bytes.length > 1024 * 1024) throw Object.assign(new Error(`Entrypoint changed: ${path}`), { code: 'UPGRADE_STORAGE_CHANGED' });
49
+ return bytes;
50
+ } finally { if (file !== undefined) closeSync(file); closeSync(parent); }
51
+ }
52
+
53
+ /** Recognition precedes all staging; user customizations are never merged. */
54
+ export function stageManagedEntrypoints(projectDir, stagingDir, providers) {
55
+ const entries = managedEntrypoints(providers);
56
+ for (const entry of entries) {
57
+ const bytes = readEntrypoint(projectDir, entry.path);
58
+ if (bytes === null || bytes.equals(Buffer.from(entry.content)) || bytes.equals(Buffer.from(entry.neutral))) continue;
59
+ if (!legacyHashes[entry.path]?.includes(createHash('sha256').update(bytes).digest('hex'))) {
60
+ throw Object.assign(new Error(`Managed entrypoint was edited or is an unrecognized version: ${entry.path}. No active files were replaced. Preserve the customization and resolve this conflict before upgrading.`), { code: 'UPGRADE_ENTRYPOINT_CONFLICT' });
61
+ }
62
+ }
63
+ for (const { path, content } of entries) {
64
+ mkdirSync(dirname(join(stagingDir, path)), { recursive: true });
65
+ writeFileSync(join(stagingDir, path), content, { flag: 'wx' });
66
+ }
67
+ return entries.map(entry => entry.path);
68
+ }
@@ -0,0 +1,194 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { closeSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import * as yaml from 'js-yaml';
7
+ import semver from 'semver';
8
+ import { copyInstalledRuntime, writeClaudeSettingsWithMerge } from '../installer/core.js';
9
+ import { generateManifest, saveManifest, verifyFrameworkSource } from '../installer/manifest.js';
10
+ import { verifyCurrentPackageArtifact } from '../installer/package-artifact.js';
11
+ import { validateInstallation } from '../installer/validator.js';
12
+ import { loadRuntimeInstallationV2 } from '../orchestrator/runtime-installation-v2.js';
13
+ import { openPinnedProviderExecutable, openPinnedProviderSupportingExecutables } from '../installer-v2/provider-executable.js';
14
+ import { findMigrations } from './migrator.js';
15
+ import { requireLicenseEntitlement } from '../license/client.js';
16
+ import { MANAGED_ENTRYPOINT_PATHS, stageManagedEntrypoints } from './entrypoints.js';
17
+
18
+ export const UPGRADE_PACKAGE_ROOT = fileURLToPath(new URL('../../', import.meta.url));
19
+ export const UPGRADE_TARGETS = Object.freeze(['.chati.dev', '.claude/settings.json', ...MANAGED_ENTRYPOINT_PATHS, '.chati/manifest.json']);
20
+ export const upgradeFailure = (code, message) => { throw Object.assign(new Error(message), { code }); };
21
+ const digest = (value) => createHash('sha256').update(value).digest('hex');
22
+ export function upgradeTestHook(phase, detail = {}) {
23
+ globalThis[Symbol.for('chati.upgrade.test-hooks')]?.onPhase?.(phase, detail);
24
+ }
25
+
26
+ /** Bounded to an explicitly selected installation subtree, not the project.
27
+ * Links/special files are refused before staging or promotion can follow them. */
28
+ export function upgradeSnapshot(path) {
29
+ let stat;
30
+ try { stat = lstatSync(path); } catch (error) { if (error.code === 'ENOENT') return null; throw error; }
31
+ const entries = [];
32
+ const walk = (current, relative = '') => {
33
+ const item = lstatSync(current);
34
+ if (item.isDirectory()) {
35
+ entries.push([relative, 'directory', item.mode & 0o777]);
36
+ for (const name of readdirSync(current).sort()) walk(join(current, name), relative ? `${relative}/${name}` : name);
37
+ } else if (item.isFile() && item.nlink === 1) {
38
+ entries.push([relative, 'file', item.mode & 0o777, digest(readFileSync(current))]);
39
+ } else upgradeFailure('UNSAFE_UPGRADE_PATH', `Upgrade target contains a link or special file: ${current}`);
40
+ };
41
+ walk(path);
42
+ return { identity: `${stat.dev}:${stat.ino}`, digest: digest(JSON.stringify(entries)), type: stat.isDirectory() ? 'directory' : 'file' };
43
+ }
44
+
45
+ export function copyUpgradeTree(source, destination, { exclude = [] } = {}) {
46
+ const stat = lstatSync(source);
47
+ if (stat.isDirectory()) {
48
+ mkdirSync(destination, { recursive: true, mode: stat.mode & 0o777 });
49
+ for (const name of readdirSync(source)) {
50
+ // Prior managed backups stay in the retired generation, never recursively
51
+ // duplicate every historical generation into the new runtime.
52
+ if (/^\.backup-v/.test(name) || exclude.includes(name)) continue;
53
+ copyUpgradeTree(join(source, name), join(destination, name));
54
+ }
55
+ } else if (stat.isFile() && stat.nlink === 1) {
56
+ mkdirSync(dirname(destination), { recursive: true });
57
+ copyFileSync(source, destination);
58
+ } else upgradeFailure('UNSAFE_UPGRADE_PATH', `Cannot stage linked or special source: ${source}`);
59
+ }
60
+
61
+ export function verifyUpgradePackage(targetVersion) {
62
+ const version = JSON.parse(readFileSync(join(UPGRADE_PACKAGE_ROOT, 'package.json'), 'utf8')).version;
63
+ if (targetVersion !== undefined && targetVersion !== version) upgradeFailure('UPGRADE_PACKAGE_VERSION_MISMATCH', `This command contains v${version}, not ${targetVersion}`);
64
+ const integrity = verifyCurrentPackageArtifact(UPGRADE_PACKAGE_ROOT);
65
+ if (!integrity.valid) upgradeFailure('UPGRADE_PACKAGE_UNTRUSTED', `Package verification failed: ${integrity.reason}`);
66
+ const framework = verifyFrameworkSource(join(UPGRADE_PACKAGE_ROOT, 'framework'), { expectedVersion: version });
67
+ if (!framework.valid) upgradeFailure('UPGRADE_PACKAGE_UNTRUSTED', `Framework verification failed: ${framework.reason}`);
68
+ return { version, package_sha256: digest(readFileSync(join(UPGRADE_PACKAGE_ROOT, 'package-artifact-manifest.json'))) };
69
+ }
70
+
71
+ export function assertUpgradeBindings(projectDir) {
72
+ const artifact = loadRuntimeInstallationV2(projectDir);
73
+ if (!artifact) return null;
74
+ try {
75
+ for (const binding of artifact.installation.enabled_providers) {
76
+ const descriptors = [];
77
+ try {
78
+ descriptors.push(openPinnedProviderExecutable(binding, { harnessId: binding.harness_id, projectDir }).descriptor);
79
+ for (const pinned of openPinnedProviderSupportingExecutables(binding, { harnessId: binding.harness_id, projectDir })) descriptors.push(pinned.descriptor);
80
+ } finally { for (const fd of descriptors) closeSync(fd); }
81
+ }
82
+ } catch (error) {
83
+ upgradeFailure('UPGRADE_BINDING_RECONFIGURATION_REQUIRED', `Existing executable binding is no longer valid (${error.code}). Explicitly reconfigure the installation before upgrading; no binding was changed.`);
84
+ }
85
+ return artifact;
86
+ }
87
+
88
+ export async function authorizeUpgradeProfile(artifact) {
89
+ if (artifact?.installation?.profile === 'authorized-internal') await requireLicenseEntitlement('internal');
90
+ }
91
+
92
+ export async function readUpgradeInstallation(projectDir, { version, offline = false }) {
93
+ const framework = join(projectDir, '.chati.dev');
94
+ if (!existsSync(framework) || existsSync(join(projectDir, 'chati.dev'))) {
95
+ upgradeFailure('UPGRADE_LAYOUT_REQUIRES_MIGRATION', 'Upgrade requires one canonical .chati.dev installation. Legacy or ambiguous layouts need explicit migration.');
96
+ }
97
+ const config = yaml.load(readFileSync(join(framework, 'config.yaml'), 'utf8'));
98
+ if (!semver.valid(config?.version) || semver.lt(config.version, '4.3.0') || semver.gt(config.version, version)) {
99
+ upgradeFailure('UPGRADE_VERSION_REQUIRES_MIGRATION', 'Automatic upgrade supports versions from 4.3.0 through the current package; older layouts or downgrades require an explicit migration.');
100
+ }
101
+ if (findMigrations(projectDir, config.version, version).length) upgradeFailure('UPGRADE_MIGRATION_REQUIRES_REVIEW', 'This upgrade requires a migration outside the runtime-only write set. No project files were changed.');
102
+ const cli = join(framework, '_cli');
103
+ if (existsSync(cli) && !lstatSync(cli).isDirectory()) upgradeFailure('UNSAFE_UPGRADE_PATH', 'Existing _cli must be a real directory');
104
+ const session = yaml.load(readFileSync(join(projectDir, '.chati/session.yaml'), 'utf8'));
105
+ if (!session || typeof session !== 'object' || Array.isArray(session)) upgradeFailure('UPGRADE_SESSION_INVALID', 'A valid session is required before upgrade');
106
+ if ((session.current_agent && session.session_status !== 'paused')
107
+ || Object.values(session.agents || {}).some(agent => ['running', 'reserved'].includes(agent.execution_claim_state))) {
108
+ upgradeFailure('UPGRADE_RUNTIME_ACTIVE', 'Pause/exit active sessions and resolve execution claims before upgrading. Session state was preserved.');
109
+ }
110
+ let protocol;
111
+ try { protocol = JSON.parse(readFileSync(join(cli, 'version.json'), 'utf8')).maintenance_protocol; }
112
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
113
+ if (protocol !== 1 && !offline) upgradeFailure('UPGRADE_OFFLINE_CONFIRMATION_REQUIRED', 'This installed version cannot exclude legacy runtime processes. Close all Chati sessions, then repeat upgrade --offline. Online upgrade is not supported for this version.');
114
+ const artifact = assertUpgradeBindings(projectDir);
115
+ await authorizeUpgradeProfile(artifact);
116
+ const providers = artifact ? artifact.installation.enabled_providers.map(binding => binding.harness_id)
117
+ : Object.entries(config.providers || {}).filter(([, value]) => value?.enabled === true).map(([name]) => name);
118
+ if (providers.some(name => !['claude', 'codex', 'grok', 'gemini'].includes(name))) upgradeFailure('UPGRADE_PROVIDER_UNSUPPORTED', 'Installed provider configuration requires explicit reconfiguration');
119
+ const protectedPaths = ['.chati/session.yaml', '.chati/v2/installation.json', 'CLAUDE.md'];
120
+ const protectedState = Object.fromEntries(protectedPaths.map(path => [path, upgradeSnapshot(join(projectDir, path))]));
121
+ return { config, artifact, providers: [...new Set(providers)], protectedState };
122
+ }
123
+
124
+ export function assertUpgradeStatePreserved(projectDir, protectedState) {
125
+ for (const [path, before] of Object.entries(protectedState)) {
126
+ if (JSON.stringify(upgradeSnapshot(join(projectDir, path))) !== JSON.stringify(before)) upgradeFailure('UPGRADE_STATE_CHANGED', `Protected project state changed: ${path}`);
127
+ }
128
+ }
129
+
130
+ export async function prepareUpgradeGeneration(projectDir, stagingDir, installation, packageIdentity) {
131
+ const { config, artifact, providers } = installation;
132
+ const entrypointTargets = stageManagedEntrypoints(projectDir, stagingDir, providers);
133
+ const frameworkDir = join(stagingDir, '.chati.dev');
134
+ // These are fully managed namespaces. Overlaying the old CLI could retain
135
+ // removed code or nested dependencies that shadow the new package's graph.
136
+ // The original tree remains untouched and is retired intact at promotion.
137
+ copyUpgradeTree(join(projectDir, '.chati.dev'), frameworkDir, { exclude: ['_cli', 'framework', '.adapted'] });
138
+ upgradeTestHook('before-runtime-copy', { stagingDir });
139
+ copyInstalledRuntime({ frameworkDir, providers, runtimeVersion: artifact ? 2 : undefined, version: packageIdentity.version, strict: true });
140
+ // The router compares installer_version with the bundled CLI stamp: an
141
+ // upgraded generation must advance both, or every later run warns that
142
+ // an upgrade is still required.
143
+ writeFileSync(join(frameworkDir, 'config.yaml'), yaml.dump({ ...config, version: packageIdentity.version, installer_version: packageIdentity.version }, { lineWidth: -1 }));
144
+ copyUpgradeTree(join(projectDir, '.chati/session.yaml'), join(stagingDir, '.chati/session.yaml'));
145
+ if (artifact) copyUpgradeTree(join(projectDir, '.chati/v2/installation.json'), join(stagingDir, '.chati/v2/installation.json'));
146
+ const memories = join(projectDir, '.chati/memories/shared/durable');
147
+ if (existsSync(memories) && lstatSync(memories).isDirectory()) mkdirSync(join(stagingDir, '.chati/memories/shared/durable'), { recursive: true });
148
+ const targets = ['.chati.dev'];
149
+ const claude = join(projectDir, '.claude');
150
+ if (existsSync(claude)) {
151
+ if (!lstatSync(claude).isDirectory() || lstatSync(claude).isSymbolicLink()) upgradeFailure('UNSAFE_UPGRADE_PATH', '.claude must be a real directory');
152
+ const settings = join(claude, 'settings.json');
153
+ if (existsSync(settings)) {
154
+ const parsed = JSON.parse(readFileSync(settings, 'utf8'));
155
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) upgradeFailure('UPGRADE_SETTINGS_INVALID', 'Claude settings must be a JSON object');
156
+ copyUpgradeTree(settings, join(stagingDir, '.claude/settings.json'));
157
+ }
158
+ writeClaudeSettingsWithMerge(join(stagingDir, '.claude/settings.json'), { runtimeVersion: artifact ? 2 : undefined });
159
+ targets.push('.claude/settings.json');
160
+ }
161
+ targets.push(...entrypointTargets);
162
+ // A former manifest covering user instructions or other state cannot be
163
+ // silently narrowed to the runtime write set by this transaction.
164
+ const formerManifest = join(projectDir, '.chati/manifest.json');
165
+ if (existsSync(formerManifest)) {
166
+ const manifest = JSON.parse(readFileSync(formerManifest, 'utf8'));
167
+ if (!manifest.files || Object.keys(manifest.files).some(path => !path.startsWith('.chati.dev/') && path !== '.claude/settings.json' && !MANAGED_ENTRYPOINT_PATHS.includes(path))) {
168
+ upgradeFailure('UPGRADE_MANIFEST_REQUIRES_MIGRATION', 'Existing manifest covers paths outside this upgrade write set');
169
+ }
170
+ }
171
+ const paths = [];
172
+ const collect = (path, relative) => {
173
+ const stat = lstatSync(path);
174
+ if (stat.isDirectory()) for (const name of readdirSync(path)) collect(join(path, name), `${relative}/${name}`);
175
+ else if (stat.isFile() && stat.nlink === 1) paths.push(relative);
176
+ else upgradeFailure('UNSAFE_UPGRADE_PATH', `Invalid staged path: ${relative}`);
177
+ };
178
+ for (const path of targets) collect(join(stagingDir, path), path);
179
+ saveManifest(stagingDir, generateManifest(stagingDir, paths.sort(), packageIdentity.version));
180
+ targets.push('.chati/manifest.json');
181
+ const expected = Object.fromEntries(targets.map(path => [path, upgradeSnapshot(join(stagingDir, path))]));
182
+ upgradeTestHook('before-validation', { stagingDir });
183
+ const validation = await validateInstallation(stagingDir);
184
+ if (validation.passed !== validation.total) upgradeFailure('UPGRADE_VALIDATION_FAILED', `Staged installation passed ${validation.passed}/${validation.total} checks`);
185
+ const modules = ['orchestrator/cli.js', 'terminal/run-agent.js', 'terminal/run-rail-task.js', 'terminal/run-rail-recovery.js'];
186
+ const urls = modules.map(path => pathToFileURL(join(frameworkDir, '_cli', path)).href);
187
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !['NODE_OPTIONS', 'NODE_PATH'].includes(key)));
188
+ execFileSync(process.execPath, ['--input-type=module', '-e', `for (const url of ${JSON.stringify(urls)}) await import(url);`], { cwd: stagingDir, env, stdio: 'pipe', timeout: 20000 });
189
+ for (const path of targets) {
190
+ if (JSON.stringify(upgradeSnapshot(join(stagingDir, path))) !== JSON.stringify(expected[path])) upgradeFailure('UPGRADE_STAGING_CHANGED', `Staged inventory changed during validation: ${path}`);
191
+ }
192
+ assertUpgradeStatePreserved(projectDir, installation.protectedState);
193
+ return { targets, validation: { passed: validation.passed, total: validation.total } };
194
+ }
@@ -0,0 +1,79 @@
1
+ import { closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, realpathSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { lstatAtSync, mkdirAtSync, openAtSync, tryExclusiveFileLockSync, trySharedFileLockSync } from '@chati/core';
4
+
5
+ export const UPGRADE_PENDING_FILE = 'pending.json';
6
+ const LOCK_FILE = 'maintenance.lock';
7
+ const identity = (stat) => `${stat.dev}:${stat.ino}`;
8
+ const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
9
+
10
+ /** Persistent descriptor lock, never unlinked or stolen by age. Readers may
11
+ * coexist; the upgrade owns an exclusive descriptor until its promise settles. */
12
+ export async function withUpgradeMaintenance(projectDir, action, { exclusive = false, createState = false } = {}) {
13
+ const unsafeCode = exclusive ? 'UNSAFE_UPGRADE_PATH' : 'UNSAFE_RUNTIME_PATH';
14
+ const changedCode = exclusive ? 'UPGRADE_STORAGE_CHANGED' : 'UNSAFE_RUNTIME_PATH';
15
+ if (!projectDir && !exclusive) return action();
16
+ const root = realpathSync(projectDir);
17
+ const statePath = join(root, '.chati');
18
+ let stateStat;
19
+ try { stateStat = lstatSync(statePath); }
20
+ catch (error) {
21
+ if (error.code !== 'ENOENT') throw error;
22
+ if (!createState) { if (!exclusive) return action(); throw error; }
23
+ // Only an already authorized installer may bootstrap the shared lock.
24
+ const rootFd = openSync(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
25
+ try {
26
+ try { mkdirAtSync(rootFd, '.chati'); fsyncSync(rootFd); }
27
+ catch (mkdirError) { if (mkdirError.code !== 'EEXIST') throw mkdirError; }
28
+ } finally { closeSync(rootFd); }
29
+ stateStat = lstatSync(statePath);
30
+ }
31
+ if (!stateStat.isDirectory() || stateStat.isSymbolicLink()) fail(unsafeCode, '.chati must be a real directory');
32
+ const rootStat = lstatSync(root);
33
+ let stateFd; let lockFd;
34
+ const directories = [];
35
+ try {
36
+ stateFd = openSync(statePath, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
37
+ directories.push({ fd: stateFd, path: statePath, identity: identity(stateStat) });
38
+ if (identity(fstatSync(stateFd)) !== identity(stateStat)) fail(unsafeCode, '.chati changed while acquiring maintenance');
39
+ let storageDir = statePath;
40
+ for (const name of ['runtime', 'upgrade']) {
41
+ try { mkdirAtSync(stateFd, name); fsyncSync(stateFd); } catch (error) { if (error.code !== 'EEXIST') throw error; }
42
+ const stat = lstatAtSync(stateFd, name);
43
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail(unsafeCode, 'upgrade storage must contain only real directories');
44
+ stateFd = openAtSync(stateFd, name, constants.O_RDONLY | constants.O_DIRECTORY);
45
+ storageDir = join(storageDir, name);
46
+ directories.push({ fd: stateFd, path: storageDir, identity: identity(stat) });
47
+ if (identity(fstatSync(stateFd)) !== identity(stat)) fail(unsafeCode, 'upgrade storage changed while opening');
48
+ }
49
+ lockFd = openAtSync(stateFd, LOCK_FILE, constants.O_RDWR | constants.O_CREAT | constants.O_NONBLOCK, 0o600);
50
+ const lockStat = fstatSync(lockFd);
51
+ if (!lockStat.isFile() || lockStat.nlink !== 1) fail(unsafeCode, 'maintenance lock must be a single-link regular file');
52
+ const assertCurrent = () => {
53
+ if (identity(lstatSync(root)) !== identity(rootStat)
54
+ || directories.some(directory => identity(lstatSync(directory.path)) !== directory.identity)
55
+ || identity(lstatAtSync(stateFd, LOCK_FILE)) !== identity(lockStat)) {
56
+ fail(changedCode, 'maintenance storage identity changed');
57
+ }
58
+ };
59
+ assertCurrent();
60
+ const acquired = exclusive ? tryExclusiveFileLockSync(lockFd) : trySharedFileLockSync(lockFd);
61
+ if (!acquired) fail('UPGRADE_IN_PROGRESS', exclusive
62
+ ? 'A Chati runtime or another upgrade is active. Close active sessions before upgrading.'
63
+ : 'An upgrade is in progress. Resume this command after the upgrade finishes.');
64
+ assertCurrent();
65
+ if (!exclusive) {
66
+ try {
67
+ lstatAtSync(stateFd, UPGRADE_PENDING_FILE);
68
+ fail('UPGRADE_RECOVERY_REQUIRED', 'An interrupted upgrade must be recovered with chati-dev upgrade before starting runtime commands.');
69
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
70
+ }
71
+ globalThis[Symbol.for('chati.upgrade.test-hooks')]?.onMaintenance?.({ exclusive, root });
72
+ const result = await action({ root, stateFd, storageDir, assertCurrent });
73
+ assertCurrent();
74
+ return result;
75
+ } finally {
76
+ if (lockFd !== undefined) closeSync(lockFd);
77
+ for (const directory of directories.reverse()) closeSync(directory.fd);
78
+ }
79
+ }
@@ -0,0 +1,81 @@
1
+ import { closeSync, constants, fstatSync, fsyncSync, openSync } from 'node:fs';
2
+ import { dirname, isAbsolute, sep } from 'node:path';
3
+ import { lstatAtSync, mkdirAtSync, openAtSync } from '@chati/core';
4
+
5
+ const identity = stat => `${stat.dev}:${stat.ino}`;
6
+ const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
7
+
8
+ /** Walk from a pinned root, never following even an intermediate symlink. */
9
+ export function openUpgradeDirectory(root, relativeDirectory, { create = false, expected } = {}) {
10
+ const parts = relativeDirectory === '.' || relativeDirectory === '' ? [] : relativeDirectory.split(sep);
11
+ if (isAbsolute(relativeDirectory) || parts.some(part => !part || part === '.' || part === '..')) {
12
+ fail('UNSAFE_UPGRADE_PATH', 'Upgrade directory must be project-relative');
13
+ }
14
+ let fd = openSync(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
15
+ const check = (path, stat) => {
16
+ if (!stat.isDirectory()) fail('UNSAFE_UPGRADE_PATH', 'Upgrade ancestor must be a real directory');
17
+ if (expected && Object.hasOwn(expected, path) && expected[path] !== identity(stat)) {
18
+ fail('UPGRADE_STORAGE_CHANGED', `Upgrade parent changed: ${path || '.'}`);
19
+ }
20
+ };
21
+ try {
22
+ check('', fstatSync(fd));
23
+ let relative = '';
24
+ for (const part of parts) {
25
+ relative = relative ? `${relative}${sep}${part}` : part;
26
+ let stat;
27
+ try { stat = lstatAtSync(fd, part); }
28
+ catch (error) {
29
+ if (error.code !== 'ENOENT') throw error;
30
+ if (expected?.[relative]) fail('UPGRADE_STORAGE_CHANGED', `Upgrade parent disappeared: ${relative}`);
31
+ if (!create) { closeSync(fd); return undefined; }
32
+ mkdirAtSync(fd, part, 0o700); fsyncSync(fd);
33
+ stat = lstatAtSync(fd, part);
34
+ if (expected) expected[relative] = identity(stat);
35
+ }
36
+ if (!stat.isDirectory() || stat.isSymbolicLink()) fail('UNSAFE_UPGRADE_PATH', `Upgrade ancestor is linked or not a directory: ${relative}`);
37
+ check(relative, stat);
38
+ const next = openAtSync(fd, part, constants.O_RDONLY | constants.O_DIRECTORY);
39
+ if (identity(fstatSync(next)) !== identity(stat)) {
40
+ closeSync(next); fail('UPGRADE_STORAGE_CHANGED', `Upgrade parent changed while opening: ${relative}`);
41
+ }
42
+ closeSync(fd); fd = next;
43
+ }
44
+ return fd;
45
+ } catch (error) { closeSync(fd); throw error; }
46
+ }
47
+
48
+ export function snapshotUpgradeParents(root, targets) {
49
+ const paths = new Set(['']);
50
+ for (const target of targets) {
51
+ let path = dirname(target);
52
+ while (path !== '.') { paths.add(path); path = dirname(path); }
53
+ }
54
+ return Object.fromEntries([...paths].sort().map(path => {
55
+ const fd = openUpgradeDirectory(root, path);
56
+ if (fd === undefined) return [path, null];
57
+ try { return [path, identity(fstatSync(fd))]; } finally { closeSync(fd); }
58
+ }));
59
+ }
60
+
61
+ export function assertUpgradeParentsUnchanged(root, expected) {
62
+ for (const [path, before] of Object.entries(expected)) {
63
+ const fd = openUpgradeDirectory(root, path, { expected });
64
+ try {
65
+ if ((fd === undefined ? null : identity(fstatSync(fd))) !== before) {
66
+ fail('UPGRADE_STORAGE_CHANGED', `Upgrade parent changed: ${path || '.'}`);
67
+ }
68
+ } finally { if (fd !== undefined) closeSync(fd); }
69
+ }
70
+ }
71
+
72
+ export function createUpgradeParents(root, targets, expected) {
73
+ assertUpgradeParentsUnchanged(root, expected);
74
+ const current = { ...expected };
75
+ for (const target of targets) {
76
+ const fd = openUpgradeDirectory(root, dirname(target), { create: true, expected: current });
77
+ closeSync(fd);
78
+ }
79
+ assertUpgradeParentsUnchanged(root, current);
80
+ return current;
81
+ }