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
@@ -5,14 +5,16 @@ import { fileURLToPath } from 'url';
5
5
  import { createRequire } from 'module';
6
6
  import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
7
7
  import { mergeClaudeMCPConfig, upsertManagedMcpToml } from '../config/mcp-configs.js';
8
- import { generateSessionYaml, generateConfigYaml, generateProjectContext, generateClaudeMd, generateClaudeLocalMd, generateCodexSkill, generateGeminiRouter, generateGrokRouter, generateGrokGovernanceRules, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
8
+ import { generateSessionYaml, generateConfigYaml, generateProjectContext, generateClaudeMd, generateClaudeLocalMd, generateClaudeRouter, generateCodexSkill, generateGeminiRouter, generateGrokRouter, generateGrokGovernanceRules, generateGeminiSessionLock, generateAgentsOverrideMd, generateCodexConstitutionGuardRules, generateCodexReadProtectionRules } from './templates.js';
9
9
  import { generateContextFiles } from '../config/context-file-generator.js';
10
10
  import { adaptFrameworkFile, ADAPTABLE_FILES, stripLegacyBuildSurface } from '../config/framework-adapter.js';
11
11
  import { generateClaudeSettings } from '../config/claude-settings-generator.js';
12
12
  import { generateProviderOverlays } from './provider-overlay.js';
13
13
  import { applyInstalledPathReplacement } from './path-replacement.js';
14
14
  import { verifyFrameworkSource } from './manifest.js';
15
+ import { PACKAGE_ARTIFACT_MANIFEST, PACKAGE_ARTIFACT_SIGNATURE } from './package-artifact.js';
15
16
  import { initializeSessionDocument } from '../orchestrator/session-manager.js';
17
+ import { withUpgradeMaintenance } from '../upgrade/maintenance.js';
16
18
  import * as yaml from 'js-yaml';
17
19
 
18
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -55,6 +57,14 @@ export async function installFramework(config) {
55
57
  );
56
58
  if (initialSession.error) throw new Error(initialSession.error);
57
59
 
60
+ // The first session creates .chati. Hold the lease from this point through
61
+ // all framework/configuration writes, including direct installer API use.
62
+ return withUpgradeMaintenance(targetDir, () => installPreparedFramework(config, enabledProviders, version));
63
+ }
64
+
65
+ async function installPreparedFramework(config, enabledProviders, version) {
66
+ const { targetDir, projectType, language, selectedIDEs, selectedMCPs } = config;
67
+
58
68
  // 2. Create .chati.dev/ framework directory (hidden — dot prefix)
59
69
  // The source is chati.dev/ in the monorepo; installed as .chati.dev/ in user projects.
60
70
  // Text replacement during copy handles all internal path references.
@@ -120,17 +130,61 @@ export async function installFramework(config) {
120
130
  createDir(join(memoriesBase, dir));
121
131
  }
122
132
 
133
+ copyInstalledRuntime({ frameworkDir, providers: enabledProviders, runtimeVersion: config.runtimeVersion, version });
134
+
135
+ // Write config.yaml
136
+ writeFileSync(
137
+ join(frameworkDir, 'config.yaml'),
138
+ generateConfigYaml({
139
+ version, projectType, language, selectedIDEs,
140
+ allProviders: enabledProviders,
141
+ runtimeVersion: config.runtimeVersion,
142
+ installationMode: config.installationMode,
143
+ clickupPreflight: config.clickupPreflight,
144
+ }),
145
+ 'utf-8'
146
+ );
147
+
148
+ // 3. Configure every CLI against its own overlay. The CLI that opens a
149
+ // session becomes that session's orchestrator.
150
+ for (const ideKey of selectedIDEs) {
151
+ const ideProvider = IDE_TO_PROVIDER[ideKey];
152
+ const hasProviderOverlay = ideProvider && enabledProviders.includes(ideProvider);
153
+ const orchestratorPath = hasProviderOverlay
154
+ ? `.chati.dev/.adapted/${ideProvider}/orchestrator/chati.md`
155
+ : '.chati.dev/orchestrator/chati.md';
156
+ await configureIDE(targetDir, ideKey, selectedMCPs, {
157
+ orchestratorPath,
158
+ isSecondary: false,
159
+ providerName: ideProvider,
160
+ runtimeVersion: config.runtimeVersion,
161
+ });
162
+ }
163
+
164
+ await finishProjectInstallation(config);
165
+ }
166
+
167
+ /** Assemble only the managed runtime. Upgrade can reuse this in private staging
168
+ * without regenerating project context, session, bindings or CLAUDE.md. */
169
+ export function copyInstalledRuntime({ frameworkDir, providers = [], runtimeVersion, version, strict = false }) {
170
+ const isRuntimeV2 = runtimeVersion === 2;
171
+ const packageRoot = join(__dirname, '..', '..');
172
+ const packageVersion = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
173
+ if (strict && version !== packageVersion) {
174
+ throw Object.assign(new Error('Runtime version must match the package being copied'), { code: 'UPGRADE_PACKAGE_VERSION_MISMATCH' });
175
+ }
176
+ const options = { strict };
123
177
  // Copy the provider-neutral canonical framework. Every enabled CLI receives
124
178
  // an explicit native overlay below, but no provider owns the source core.
125
- copyFrameworkFiles(frameworkDir, null);
179
+ copyFrameworkFiles(frameworkDir, null, options);
126
180
 
127
181
  // Generate symmetric provider overlays. No CLI is permanently primary.
128
- if (enabledProviders.length > 0) {
129
- generateProviderOverlays(targetDir, FRAMEWORK_SOURCE, enabledProviders);
182
+ if (providers.length > 0) {
183
+ generateProviderOverlays(dirname(frameworkDir), FRAMEWORK_SOURCE, providers, { frameworkDir });
130
184
  }
131
185
 
132
186
  if (isRuntimeV2) {
133
- pruneLegacyExecutionSurface(frameworkDir, enabledProviders);
187
+ pruneLegacyExecutionSurface(frameworkDir, providers);
134
188
  pruneLegacyRegistryEntities(frameworkDir);
135
189
  }
136
190
 
@@ -140,7 +194,8 @@ export async function installFramework(config) {
140
194
  // Result: every install is self-contained; no network/cache dependency at runtime.
141
195
  copyCliSource(
142
196
  join(__dirname, '..'), // packages/chati-dev/src/
143
- join(frameworkDir, '_cli') // .chati.dev/_cli/
197
+ join(frameworkDir, '_cli'), // .chati.dev/_cli/
198
+ options
144
199
  );
145
200
 
146
201
  // The bundled CLI imports ../../framework/hooks/model-governance.js relative
@@ -149,7 +204,7 @@ export async function installFramework(config) {
149
204
  // .chati.dev/framework/hooks/ to satisfy the import path.
150
205
  copyDirVerbatim(
151
206
  join(__dirname, '..', '..', 'framework', 'hooks'),
152
- join(frameworkDir, 'framework', 'hooks')
207
+ join(frameworkDir, 'framework', 'hooks'), options
153
208
  );
154
209
  if (isRuntimeV2) {
155
210
  rmSync(join(frameworkDir, 'framework', 'hooks', 'post-dev.js'), { force: true });
@@ -159,47 +214,34 @@ export async function installFramework(config) {
159
214
  // without relying on a node_modules tree in the user's project.
160
215
  copyCliDependencies(
161
216
  join(__dirname, '..', '..'), // packages/chati-dev/
162
- join(frameworkDir, '_cli', 'node_modules') // .chati.dev/_cli/node_modules/
217
+ join(frameworkDir, '_cli', 'node_modules'), // .chati.dev/_cli/node_modules/
218
+ options
163
219
  );
164
220
 
221
+ // Preserve the publisher's original attestation for native runtime helpers.
222
+ // Their installed bytes must be checked against this signature, never blessed
223
+ // by hashing the installed file itself. The runtime reader verifies it before
224
+ // preparing any executable snapshot. Missing metadata must not stamp success.
225
+ for (const name of [PACKAGE_ARTIFACT_MANIFEST, PACKAGE_ARTIFACT_SIGNATURE]) {
226
+ const source = join(packageRoot, name);
227
+ const stat = lstatSync(source);
228
+ if (!stat.isFile() || stat.nlink !== 1) unsafeManagedPath(source, 'package attestation must be a single-link regular file');
229
+ copySafeManagedFile(source, join(frameworkDir, '_cli', name));
230
+ }
231
+
165
232
  // v4.3.1+ — Write version stamp so chati-router.js can detect bundle/canonical
166
233
  // mismatch on subsequent invocations. Pre-v4.3.1 installs lack this file;
167
234
  // mismatch detection silent-skips when it's missing (graceful degradation).
168
235
  writeFileSync(
169
236
  join(frameworkDir, '_cli', 'version.json'),
170
- JSON.stringify({ version, bundled_at: new Date().toISOString() }, null, 2),
237
+ JSON.stringify({ version, package_version: packageVersion, bundled_at: new Date().toISOString(), maintenance_protocol: 1 }, null, 2),
171
238
  'utf-8'
172
239
  );
173
240
 
174
- // Write config.yaml
175
- writeFileSync(
176
- join(frameworkDir, 'config.yaml'),
177
- generateConfigYaml({
178
- version, projectType, language, selectedIDEs,
179
- allProviders: enabledProviders,
180
- runtimeVersion: config.runtimeVersion,
181
- installationMode: config.installationMode,
182
- clickupPreflight: config.clickupPreflight,
183
- }),
184
- 'utf-8'
185
- );
186
-
187
- // 3. Configure every CLI against its own overlay. The CLI that opens a
188
- // session becomes that session's orchestrator.
189
- for (const ideKey of selectedIDEs) {
190
- const ideProvider = IDE_TO_PROVIDER[ideKey];
191
- const hasProviderOverlay = ideProvider && enabledProviders.includes(ideProvider);
192
- const orchestratorPath = hasProviderOverlay
193
- ? `.chati.dev/.adapted/${ideProvider}/orchestrator/chati.md`
194
- : '.chati.dev/orchestrator/chati.md';
195
- await configureIDE(targetDir, ideKey, selectedMCPs, {
196
- orchestratorPath,
197
- isSecondary: false,
198
- providerName: ideProvider,
199
- runtimeVersion: config.runtimeVersion,
200
- });
201
- }
241
+ }
202
242
 
243
+ async function finishProjectInstallation(config) {
244
+ const { targetDir, projectName, projectType, language, selectedIDEs } = config;
203
245
  const hasClaude = selectedIDEs.includes('claude-code');
204
246
  // 4. Generate the provider-neutral project context. Harness files consume
205
247
  // this artifact and never become the source for one another.
@@ -398,8 +440,8 @@ const TEXT_EXTENSIONS = new Set(['.md', '.js', '.yaml', '.yml', '.json', '.html'
398
440
  * 1. Path replacement (chati.dev/ → .chati.dev/, artifacts/ at root)
399
441
  * 2. Provider adaptation for non-Claude providers
400
442
  */
401
- function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
402
- if (!existsSync(srcDir)) return;
443
+ function copyDirRecursive(srcDir, destDir, provider, relBase = '', options = {}) {
444
+ if (!existsSync(srcDir) && !options.strict) return;
403
445
  createSafeManagedDirectory(destDir);
404
446
 
405
447
  const entries = readdirSync(srcDir, { withFileTypes: true });
@@ -411,7 +453,7 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
411
453
  if (entry.isDirectory()) {
412
454
  // Skip artifacts/ — those live at project root, not inside .chati.dev/
413
455
  if (entry.name === 'artifacts') continue;
414
- copyDirRecursive(srcPath, destPath, provider, relPath);
456
+ copyDirRecursive(srcPath, destPath, provider, relPath, options);
415
457
  } else if (entry.isFile()) {
416
458
  const ext = entry.name.includes('.') ? '.' + entry.name.split('.').pop() : '';
417
459
  const isText = TEXT_EXTENSIONS.has(ext);
@@ -429,7 +471,7 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
429
471
  // Binary file: direct copy
430
472
  copySafeManagedFile(srcPath, destPath);
431
473
  }
432
- }
474
+ } else if (options.strict) unsafeManagedPath(srcPath, 'source must contain only regular files and directories');
433
475
  }
434
476
  }
435
477
 
@@ -442,13 +484,13 @@ function copyDirRecursive(srcDir, destDir, provider, relBase = '') {
442
484
  * @param {string} destDir - Target framework directory (e.g., {project}/chati.dev/)
443
485
  * @param {string} provider - Primary provider for text adaptation ('claude' | 'gemini' | 'codex')
444
486
  */
445
- export function copyFrameworkFiles(destDir, provider = null) {
446
- if (!existsSync(FRAMEWORK_SOURCE)) return;
487
+ export function copyFrameworkFiles(destDir, provider = null, options = {}) {
488
+ if (!existsSync(FRAMEWORK_SOURCE) && !options.strict) return;
447
489
 
448
490
  // Copy loose root files (with path replacement)
449
491
  for (const file of FRAMEWORK_ROOT_FILES) {
450
492
  const src = join(FRAMEWORK_SOURCE, file);
451
- if (!existsSync(src)) continue;
493
+ if (!existsSync(src) && !options.strict) continue;
452
494
  const dest = join(destDir, file);
453
495
  createSafeManagedDirectory(dirname(dest));
454
496
  let content = readFileSync(src, 'utf-8');
@@ -463,7 +505,7 @@ export function copyFrameworkFiles(destDir, provider = null) {
463
505
  for (const dir of FRAMEWORK_DIRS_TO_COPY) {
464
506
  const srcDir = join(FRAMEWORK_SOURCE, dir);
465
507
  const destSubDir = join(destDir, dir);
466
- copyDirRecursive(srcDir, destSubDir, provider, dir);
508
+ copyDirRecursive(srcDir, destSubDir, provider, dir, options);
467
509
  }
468
510
  }
469
511
 
@@ -480,47 +522,7 @@ async function configureIDE(targetDir, ideKey, selectedMCPs, options = {}) {
480
522
 
481
523
  if (ideKey === 'claude-code') {
482
524
  // Thin router
483
- const routerContent = `# /chati — Deterministic Router
484
-
485
- ## STEP 0 — Run the deterministic pipeline router (MUST be first)
486
-
487
- Run via Bash:
488
- \`\`\`
489
- node .chati.dev/orchestrator/chati-router.js --harness claude
490
- \`\`\`
491
-
492
- Parse the JSON output. This single command already handled: license validation, integrity check, session state, pipeline routing, and team registration (if needed). You do NOT need to run any of these separately.
493
-
494
- ### If \`ok\` is false:
495
-
496
- | \`error\` value | What to tell the user |
497
- |---|---|
498
- | \`license_invalid\` | Display \`license.reason\` as a friendly message. STOP. |
499
- | \`license_guard_missing\` | "License enforcement is missing. Reinstall with: npx chati-dev init". STOP. |
500
- | \`integrity_failed\` | "License enforcement was tampered with. Reinstall with: npx chati-dev init". STOP. |
501
- | anything else | Display error. STOP. |
502
-
503
- ### If \`ok\` is true:
504
-
505
- 1. Use \`session.language\` for every visible response, preface, progress message, question and error, starting with the first response (pt = Português, en = English, es = Español, fr = Français).
506
- 2. Read the full orchestrator at \`${orchestratorPath || '.chati.dev/orchestrator/chati.md'}\`.
507
- 3. The \`action\` field tells you what to do. Go to the matching "Action:" section in the orchestrator.
508
- 4. **If \`action\` is \`spawn_team\`**: the team is ALREADY registered (Step 1 already ran as code). The \`team\` object contains \`team_id\`, \`task_list_path\`, \`mailbox_path\`, \`members\`. Go directly to "Action: Spawn Team" Step 2 and use these values in the agent prompts. DO NOT run \`orchestrate spawn-team\` again — it already ran.
509
- 5. **If \`action\` is \`setup\`**: this is a new project. Follow "Action: Setup".
510
- 6. For any other action: follow the matching section in the orchestrator.
511
-
512
- **The \`pipeline\` object contains the full JSON from \`orchestrate next\`.** Use its fields (agent_file, spawn_command, handoff_status, etc.) as documented in the orchestrator.
513
-
514
- **Pending interaction invariant:** If \`action\` is \`resume_pending_interaction\`,
515
- do not execute a provider command. Display exactly \`pipeline.question\`, with
516
- no preface, summary or paraphrase. After the user answers, write
517
- \`{"response":"<exact user response>"}\` to \`pipeline.response_file\` using the
518
- native file-write tool, never a shell command. Then execute
519
- \`pipeline.deferred_spawn_command\` exactly once and unchanged. Never execute a
520
- command merely because a command-shaped field is present.
521
-
522
- **User input:** $ARGUMENTS
523
- `;
525
+ const routerContent = generateClaudeRouter({ orchestratorPath });
524
526
  writeFileSync(join(targetDir, '.claude', 'commands', 'chati.md'), routerContent, 'utf-8');
525
527
 
526
528
  // .claude/settings.json — wires the 10 chati.dev hooks into Claude Code's
@@ -1057,8 +1059,8 @@ function copySafeManagedFile(source, destination) {
1057
1059
  * Used to bundle the CLI source into the user's .chati.dev/_cli/ folder so
1058
1060
  * the framework runs self-contained (no `npx chati-dev` dependency).
1059
1061
  */
1060
- export function copyCliSource(srcDir, destDir) {
1061
- if (!existsSync(srcDir)) return;
1062
+ export function copyCliSource(srcDir, destDir, options = {}) {
1063
+ if (!existsSync(srcDir) && !options.strict) return;
1062
1064
  createSafeManagedDirectory(destDir);
1063
1065
  const entries = readdirSync(srcDir);
1064
1066
  for (const entry of entries) {
@@ -1067,10 +1069,12 @@ export function copyCliSource(srcDir, destDir) {
1067
1069
  const srcPath = join(srcDir, entry);
1068
1070
  const destPath = join(destDir, entry);
1069
1071
  let stat;
1070
- try { stat = statSync(srcPath); } catch { continue; }
1072
+ try { stat = options.strict ? lstatSync(srcPath) : statSync(srcPath); }
1073
+ catch (error) { if (options.strict) throw error; continue; }
1071
1074
  if (stat.isDirectory()) {
1072
- copyCliSource(srcPath, destPath);
1075
+ copyCliSource(srcPath, destPath, options);
1073
1076
  } else {
1077
+ if (options.strict && !stat.isFile()) unsafeManagedPath(srcPath, 'CLI source must be a regular file');
1074
1078
  copySafeManagedFile(srcPath, destPath);
1075
1079
  }
1076
1080
  }
@@ -1079,20 +1083,21 @@ export function copyCliSource(srcDir, destDir) {
1079
1083
  /**
1080
1084
  * Recursively copy a directory verbatim (used for bundling node_modules trees).
1081
1085
  */
1082
- function copyDirVerbatim(srcDir, destDir) {
1083
- if (!existsSync(srcDir)) return;
1086
+ function copyDirVerbatim(srcDir, destDir, options = {}) {
1087
+ if (!existsSync(srcDir) && !options.strict) return;
1084
1088
  createSafeManagedDirectory(destDir);
1085
1089
  for (const entry of readdirSync(srcDir)) {
1086
1090
  const srcPath = join(srcDir, entry);
1087
1091
  const destPath = join(destDir, entry);
1088
1092
  let stat;
1089
- try { stat = statSync(srcPath); } catch { continue; }
1093
+ try { stat = statSync(srcPath); } catch (error) { if (options.strict) throw error; continue; }
1090
1094
  if (stat.isDirectory()) {
1091
- copyDirVerbatim(srcPath, destPath);
1095
+ copyDirVerbatim(srcPath, destPath, options);
1092
1096
  } else {
1097
+ if (options.strict && !stat.isFile()) unsafeManagedPath(srcPath, 'dependency source must be a regular file');
1093
1098
  try { copySafeManagedFile(srcPath, destPath); }
1094
1099
  catch (error) {
1095
- if (error?.code === 'UNSAFE_INSTALLATION_TARGET') throw error;
1100
+ if (options.strict || error?.code === 'UNSAFE_INSTALLATION_TARGET') throw error;
1096
1101
  /* skip unreadable source material */
1097
1102
  }
1098
1103
  }
@@ -1108,11 +1113,11 @@ function copyDirVerbatim(srcDir, destDir) {
1108
1113
  * which works whether the package is hoisted or nested in the user's
1109
1114
  * node_modules (npx cache, npm install, etc.).
1110
1115
  */
1111
- export function copyCliDependencies(pkgDir, destNodeModules) {
1116
+ export function copyCliDependencies(pkgDir, destNodeModules, options = {}) {
1112
1117
  let pkg;
1113
1118
  try {
1114
1119
  pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8'));
1115
- } catch { return; }
1120
+ } catch (error) { if (options.strict) throw error; return; }
1116
1121
  const deps = [
1117
1122
  ...Object.keys(pkg.dependencies || {}),
1118
1123
  ...Object.keys(pkg.optionalDependencies || {}),
@@ -1121,16 +1126,17 @@ export function copyCliDependencies(pkgDir, destNodeModules) {
1121
1126
 
1122
1127
  // Resolve each dep from the package.json location (works for hoisted + nested layouts).
1123
1128
 
1124
- const require = createRequire(join(pkgDir, 'package.json'));
1125
1129
  createSafeManagedDirectory(destNodeModules);
1126
1130
 
1127
1131
  const visited = new Set();
1128
- const queue = [...deps];
1132
+ const resolvedPackages = new Map();
1133
+ const copiedTargets = new Map();
1134
+ const queue = deps.map(name => ({ name, from: pkgDir, destinationParent: dirname(destNodeModules), optional: Object.hasOwn(pkg.optionalDependencies || {}, name) }));
1129
1135
 
1130
1136
  while (queue.length > 0) {
1131
- const depName = queue.shift();
1132
- if (visited.has(depName)) continue;
1133
- visited.add(depName);
1137
+ const { name: depName, optional, from, destinationParent } = queue.shift();
1138
+ if (!options.strict && visited.has(depName)) continue;
1139
+ const require = createRequire(join(options.strict ? from : pkgDir, 'package.json'));
1134
1140
 
1135
1141
  let depDir;
1136
1142
  try {
@@ -1153,19 +1159,53 @@ export function copyCliDependencies(pkgDir, destNodeModules) {
1153
1159
  } catch { /* keep walking */ }
1154
1160
  }
1155
1161
  }
1156
- } catch { continue; }
1157
- if (!depDir) continue;
1162
+ } catch { /* required dependency failures are handled below */ }
1163
+ if (!depDir) {
1164
+ if (options.strict && !optional) {
1165
+ throw Object.assign(new Error(`Required runtime dependency is unavailable: ${depName}`), { code: 'RUNTIME_DEPENDENCY_MISSING' });
1166
+ }
1167
+ // An optional miss must not hide a later required reference.
1168
+ continue;
1169
+ }
1158
1170
  }
1171
+ let destination = join(destNodeModules, depName);
1172
+ if (options.strict && resolvedPackages.has(depName) && resolvedPackages.get(depName) !== depDir) {
1173
+ // Two consumers may require different installed versions of the same
1174
+ // dependency. Preserve Node's nearest-ancestor resolution, never flatten
1175
+ // the second version over the first or silently use the root version.
1176
+ destination = join(destinationParent, 'node_modules', depName);
1177
+ for (let parent = destinationParent; parent !== dirname(destNodeModules); parent = dirname(parent)) {
1178
+ const ancestor = join(parent, 'node_modules', depName);
1179
+ if (copiedTargets.has(ancestor)) {
1180
+ if (copiedTargets.get(ancestor) === depDir) destination = ancestor;
1181
+ break;
1182
+ }
1183
+ if (parent === dirname(parent)) break;
1184
+ }
1185
+ }
1186
+ if (options.strict && copiedTargets.has(destination)) {
1187
+ if (copiedTargets.get(destination) !== depDir) {
1188
+ throw Object.assign(new Error(`Conflicting runtime dependency destination: ${depName}`), { code: 'RUNTIME_DEPENDENCY_LAYOUT_CONFLICT' });
1189
+ }
1190
+ continue;
1191
+ }
1192
+ if (!options.strict && visited.has(depName)) continue;
1193
+ visited.add(depName);
1194
+ if (!resolvedPackages.has(depName)) resolvedPackages.set(depName, depDir);
1195
+ copiedTargets.set(destination, depDir);
1159
1196
  const depPkgJsonPath = join(depDir, 'package.json');
1160
- copyDirVerbatim(depDir, join(destNodeModules, depName));
1197
+ copyDirVerbatim(depDir, destination, options);
1161
1198
 
1162
1199
  // Include transitive deps
1163
1200
  try {
1164
1201
  const depPkg = JSON.parse(readFileSync(depPkgJsonPath, 'utf-8'));
1165
1202
  for (const t of Object.keys(depPkg.dependencies || {})) {
1166
- if (!visited.has(t)) queue.push(t);
1203
+ if (options.strict || !visited.has(t)) queue.push({ name: t, from: depDir, destinationParent: destination, optional: Object.hasOwn(depPkg.optionalDependencies || {}, t) });
1204
+ }
1205
+ for (const t of Object.keys(depPkg.optionalDependencies || {})) {
1206
+ if ((options.strict || !visited.has(t)) && !Object.hasOwn(depPkg.dependencies || {}, t)) queue.push({ name: t, from: depDir, destinationParent: destination, optional: true });
1167
1207
  }
1168
- } catch { /* continue */ }
1208
+ } catch (error) { if (options.strict) throw error; }
1169
1209
  }
1170
1210
  }
1171
1211
 
@@ -165,6 +165,13 @@ export function buildPackageArtifactManifest(packageRoot, { createdAt = new Date
165
165
  if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
166
166
  throw new Error(`publish artifact entry must be a single-link regular file: ${relativePath}`);
167
167
  }
168
+ // A valid signature over an unstable npm archive is not sufficient: npm
169
+ // renames .gitignore and adds read bits to owner-only files on install.
170
+ // Refuse such inputs at the producer. Never transform the signed identity
171
+ // in the installed reader or silently chmod the publisher's source tree.
172
+ if (relativePath.split('/').includes('.gitignore') || ![0o644, 0o755].includes(mode)) {
173
+ throw new Error(`publish artifact is not install-stable: ${relativePath}; prepare a private source copy with 0644/0755 modes and no bundled .gitignore`);
174
+ }
168
175
  files[relativePath] = Object.freeze({
169
176
  // npm normalizes file modes in the tarball. Signing the on-disk source
170
177
  // mode would reject a clean install when its umask differs, even though
@@ -13,6 +13,15 @@
13
13
 
14
14
  import { execSync } from 'child_process';
15
15
 
16
+ /** Platform eligibility only, not proof of execution containment or full E2E. */
17
+ export function assertInstallerPlatformSupported() {
18
+ if (['linux', 'darwin'].includes(process.platform)) return;
19
+ const message = process.platform === 'win32'
20
+ ? 'Native Windows installation is not supported. WSL 2 is the proposed Linux execution route, but it has not been validated for this release. Setup stopped before license activation or dependency installation.'
21
+ : `Installation is not supported on ${process.platform}. Setup stopped before license activation or dependency installation.`;
22
+ throw Object.assign(new Error(message), { code: 'UNSUPPORTED_INSTALLATION_PLATFORM' });
23
+ }
24
+
16
25
  /**
17
26
  * @typedef {{ name: string, status: 'pass'|'fail'|'warn', version?: string, message: string }} PreflightCheck
18
27
  * @typedef {{ passed: boolean, checks: PreflightCheck[] }} PreflightResult
@@ -25,6 +34,10 @@ import { execSync } from 'child_process';
25
34
  * @returns {Promise<PreflightResult>}
26
35
  */
27
36
  export async function runPreflightCheck(options = {}) {
37
+ try { assertInstallerPlatformSupported(); }
38
+ catch (error) {
39
+ return { passed: false, checks: [{ name: 'platform', status: 'fail', message: error.message }] };
40
+ }
28
41
  const checks = [];
29
42
  let mandatoryFailed = false;
30
43
 
@@ -29,9 +29,8 @@ import { applyInstalledPathReplacement } from './path-replacement.js';
29
29
  * @param {string[]} allProviders - All CLI provider names
30
30
  * @returns {{ generated: string[], skipped: string[] }}
31
31
  */
32
- export function generateProviderOverlays(targetDir, frameworkSource, allProviders) {
32
+ export function generateProviderOverlays(targetDir, frameworkSource, allProviders, { frameworkDir = join(targetDir, '.chati.dev') } = {}) {
33
33
  const result = { generated: [], skipped: [] };
34
- const frameworkDir = join(targetDir, '.chati.dev');
35
34
 
36
35
  for (const provider of allProviders) {
37
36
  const overlayDir = join(frameworkDir, '.adapted', provider);
@@ -1,4 +1,5 @@
1
1
  import * as yaml from 'js-yaml';
2
+ import { DELEGATED_EXECUTION_BOUNDARY } from '../utils/execution-role.js';
2
3
 
3
4
  const IDE_PROVIDER_MAP = Object.freeze({
4
5
  'claude-code': 'claude',
@@ -221,12 +222,61 @@ Type \`/chati\` to start or resume Chati.dev.
221
222
  * The user invokes with $chati or Codex auto-detects based on description.
222
223
  * Also reads AGENTS.md for project context (auto-generated by context-file-generator).
223
224
  */
225
+ /** Render only the managed Claude entry command; no project configuration writes. */
226
+ export function generateClaudeRouter(options = {}) {
227
+ const { orchestratorPath } = options;
228
+ return `# /chati — Deterministic Router
229
+
230
+ ${DELEGATED_EXECUTION_BOUNDARY}
231
+
232
+ ## STEP 0 — Run the deterministic pipeline router (MUST be first)
233
+
234
+ Run via Bash:
235
+ \`\`\`
236
+ node .chati.dev/orchestrator/chati-router.js --harness claude
237
+ \`\`\`
238
+
239
+ Parse the JSON output. This single command already handled: license validation, integrity check, session state, pipeline routing, and team registration (if needed). You do NOT need to run any of these separately.
240
+
241
+ ### If \`ok\` is false:
242
+
243
+ | \`error\` value | What to tell the user |
244
+ |---|---|
245
+ | \`license_invalid\` | Display \`license.reason\` as a friendly message. STOP. |
246
+ | \`license_guard_missing\` | "License enforcement is missing. Reinstall with: npx chati-dev init". STOP. |
247
+ | \`integrity_failed\` | "License enforcement was tampered with. Reinstall with: npx chati-dev init". STOP. |
248
+ | anything else | Display error. STOP. |
249
+
250
+ ### If \`ok\` is true:
251
+
252
+ 1. Use \`session.language\` for every visible response, preface, progress message, question and error, starting with the first response (pt = Português, en = English, es = Español, fr = Français).
253
+ 2. Read the full orchestrator at \`${orchestratorPath || '.chati.dev/orchestrator/chati.md'}\`.
254
+ 3. The \`action\` field tells you what to do. Go to the matching "Action:" section in the orchestrator.
255
+ 4. **If \`action\` is \`spawn_team\`**: the team is ALREADY registered (Step 1 already ran as code). The \`team\` object contains \`team_id\`, \`task_list_path\`, \`mailbox_path\`, \`members\`. Go directly to "Action: Spawn Team" Step 2 and use these values in the agent prompts. DO NOT run \`orchestrate spawn-team\` again — it already ran.
256
+ 5. **If \`action\` is \`setup\`**: this is a new project. Follow "Action: Setup".
257
+ 6. For any other action: follow the matching section in the orchestrator.
258
+
259
+ **The \`pipeline\` object contains the full JSON from \`orchestrate next\`.** Use its fields (agent_file, spawn_command, handoff_status, etc.) as documented in the orchestrator.
260
+
261
+ **Pending interaction invariant:** If \`action\` is \`resume_pending_interaction\`,
262
+ do not execute a provider command. Display exactly \`pipeline.question\`, with
263
+ no preface, summary or paraphrase. After the user answers, write
264
+ \`{"response":"<exact user response>"}\` to \`pipeline.response_file\` using the
265
+ native file-write tool, never a shell command. Then execute
266
+ \`pipeline.deferred_spawn_command\` exactly once and unchanged. Never execute a
267
+ command merely because a command-shaped field is present.
268
+
269
+ **User input:** $ARGUMENTS
270
+ `;
271
+ }
272
+
224
273
  export function generateCodexSkill(options = {}) {
225
274
  const orchPath = options.orchestratorPath || '.chati.dev/orchestrator/chati.md';
226
275
  return `---
227
276
  name: chati
228
277
  description: >
229
278
  Use this skill when the user wants to start or continue a Chati.dev session.
279
+ It is not for subprocesses already delegated an agent/task by the parent runtime.
230
280
  This activates the orchestrator which routes to the correct agent in the pipeline.
231
281
  Invoke when the user mentions chati, /chati, or wants to discover, plan, execute, or release a project.
232
282
  Do NOT use for general coding questions unrelated to the Chati.dev pipeline.
@@ -234,6 +284,8 @@ description: >
234
284
 
235
285
  # Chati.dev Orchestrator
236
286
 
287
+ ${DELEGATED_EXECUTION_BOUNDARY}
288
+
237
289
  ## STEP 0 — Run the deterministic router (MUST be the first action)
238
290
 
239
291
  Use the shell in the current project directory and run:
@@ -315,6 +367,8 @@ export function generateGeminiRouter(options = {}) {
315
367
  const orchPath = options.orchestratorPath || '.chati.dev/orchestrator/chati.md';
316
368
  return `description = "Activate Chati.dev orchestrator"
317
369
  prompt = """
370
+ ${DELEGATED_EXECUTION_BOUNDARY}
371
+
318
372
  STEP 0: Run the deterministic router before reading mutable state or responding:
319
373
 
320
374
  node .chati.dev/orchestrator/chati-router.js --harness gemini
@@ -348,6 +402,8 @@ export function generateGrokRouter(options = {}) {
348
402
  const orchPath = options.orchestratorPath || '.chati.dev/orchestrator/chati.md';
349
403
  return `# Chati.dev Orchestrator
350
404
 
405
+ ${DELEGATED_EXECUTION_BOUNDARY}
406
+
351
407
  ## STEP 0: Run the deterministic router first
352
408
 
353
409
  Before reading mutable session files or responding, run:
@@ -397,6 +453,8 @@ User input: $ARGUMENTS
397
453
  export function generateGrokGovernanceRules() {
398
454
  return `# Chati.dev Grok Governance
399
455
 
456
+ ${DELEGATED_EXECUTION_BOUNDARY}
457
+
400
458
  For every CHATI.DEV entry or continuation, run this boundary before reading
401
459
  mutable CHATI state or taking an action:
402
460
 
@@ -1,7 +1,7 @@
1
1
  import { createPublicKey, randomUUID, verify as cryptoVerify } from 'node:crypto';
2
2
  import {
3
3
  closeSync, constants, fstatSync, fsyncSync, linkSync, lstatSync,
4
- mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync,
4
+ mkdirSync, openSync, readFileSync, readdirSync, readSync, realpathSync, renameSync, unlinkSync, writeSync,
5
5
  } from 'node:fs';
6
6
  import { basename, dirname, join, parse, relative, resolve, sep } from 'node:path';
7
7
  import { isIP } from 'node:net';
@@ -343,6 +343,36 @@ export function replaceInstallationStorageFile(path, content, source) {
343
343
  return replaceInstallationArtifact(path, content, source);
344
344
  }
345
345
 
346
+ /** Enumerate only the pinned directory; callers still authenticate each entry. */
347
+ export function listSafeInstallationStorageFiles(path) {
348
+ let directory;
349
+ try { directory = openPinnedInstallationDirectory(path); }
350
+ catch (error) { if (error?.code === 'ENOENT') return []; throw error; }
351
+ try {
352
+ assertPinnedInstallationDirectory(directory);
353
+ const names = readdirSync(directory.reference);
354
+ if (names.length > 10000) throw catalogError('INSTALLATION_STORAGE_SIZE_LIMIT', 'managed storage exceeds its bounded entry budget');
355
+ return names.sort();
356
+ } finally { closeSync(directory.descriptor); }
357
+ }
358
+
359
+ /** Delete only the exact, already authenticated snapshot selected by the caller. */
360
+ export function removeSafeInstallationStorageFile(path, source) {
361
+ const directory = openPinnedInstallationDirectory(dirname(path));
362
+ try {
363
+ runInstallationStorageSecurityTestHook('beforeRemovalCommit', { path, directory: directory.absolute });
364
+ assertPinnedInstallationDirectory(directory);
365
+ const current = readSafeInstallationEntry(directory, basename(path), { allowMissing: true });
366
+ if (current === null) return false;
367
+ if (!sameSnapshot(current.stat, source.stat) || current.content !== source.content) {
368
+ throw catalogError('INSTALLATION_STORAGE_CONFLICT', 'managed storage changed before removal');
369
+ }
370
+ unlinkSync(installationEntryPath(directory, basename(path)));
371
+ fsyncSync(directory.descriptor);
372
+ return true;
373
+ } finally { closeSync(directory.descriptor); }
374
+ }
375
+
346
376
  export function readInstallationArtifactFile(projectDir, { allowMissing = true } = {}) {
347
377
  const directory = resolve(projectDir, '.chati/v2');
348
378
  return readSafeInstallationFile(join(directory, 'installation.json'), { allowMissing });