chati-dev 4.4.0 → 4.5.1

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 (85) hide show
  1. package/README.md +29 -27
  2. package/bin/chati.js +251 -1
  3. package/framework/agents/plan/tasks.md +46 -1
  4. package/framework/config.yaml +3 -3
  5. package/framework/constitution.md +16 -17
  6. package/framework/context/governance.md +5 -5
  7. package/framework/context/root.md +5 -5
  8. package/framework/data/entity-registry.yaml +1 -1
  9. package/framework/manifest.json +1351 -0
  10. package/framework/manifest.sig +1 -0
  11. package/framework/orchestrator/chati.md +48 -14
  12. package/node_modules/@chati/browser-capability/README.md +10 -0
  13. package/node_modules/@chati/browser-capability/package.json +17 -0
  14. package/node_modules/@chati/browser-capability/src/index.js +165 -0
  15. package/node_modules/@chati/core/package.json +13 -0
  16. package/node_modules/@chati/core/src/index.js +111 -0
  17. package/node_modules/@chati/knowledge-context/package.json +17 -0
  18. package/node_modules/@chati/knowledge-context/src/index.js +202 -0
  19. package/node_modules/@chati/planning/package.json +17 -0
  20. package/node_modules/@chati/planning/src/index.js +367 -0
  21. package/node_modules/@chati/provider-registry/package.json +16 -0
  22. package/node_modules/@chati/provider-registry/src/index.js +132 -0
  23. package/node_modules/@chati/rail/README.md +24 -0
  24. package/node_modules/@chati/rail/package.json +19 -0
  25. package/node_modules/@chati/rail/src/index.js +437 -0
  26. package/node_modules/@chati/release-lane/README.md +24 -0
  27. package/node_modules/@chati/release-lane/package.json +17 -0
  28. package/node_modules/@chati/release-lane/src/index.js +172 -0
  29. package/node_modules/@chati/review-council/package.json +17 -0
  30. package/node_modules/@chati/review-council/src/index.js +264 -0
  31. package/node_modules/@chati/tracking-clickup/README.md +55 -0
  32. package/node_modules/@chati/tracking-clickup/package.json +17 -0
  33. package/node_modules/@chati/tracking-clickup/src/index.js +293 -0
  34. package/package.json +25 -3
  35. package/src/config/claude-settings-generator.js +9 -6
  36. package/src/config/ide-configs.js +11 -0
  37. package/src/context/domain-loader.js +1 -1
  38. package/src/dashboard/data-reader.js +1 -1
  39. package/src/executors/runner.js +1 -1
  40. package/src/installer/core.js +33 -3
  41. package/src/installer/manifest.js +30 -4
  42. package/src/installer/scaffold-applier.js +1 -1
  43. package/src/installer/signing-public-key.pem +1 -1
  44. package/src/installer/templates.js +3 -3
  45. package/src/installer/validator.js +3 -2
  46. package/src/installer-v2/catalog-client.js +141 -0
  47. package/src/installer-v2/index.js +301 -0
  48. package/src/installer-v2/model-catalog-envelope.json +59 -0
  49. package/src/installer-v2/model-catalog.json +33 -0
  50. package/src/installer-v2/model-catalog.sig +1 -0
  51. package/src/installer-v2/wizard-installation.js +115 -0
  52. package/src/intelligence/registry-manager.js +3 -2
  53. package/src/license/client.js +1 -1
  54. package/src/memory/magic-docs.js +6 -1
  55. package/src/memory/session-digest.js +1 -1
  56. package/src/merger/yaml-merger.js +1 -1
  57. package/src/orchestrator/browser-runtime.js +25 -0
  58. package/src/orchestrator/cli.js +112 -15
  59. package/src/orchestrator/clickup-projection.js +84 -0
  60. package/src/orchestrator/clickup-runtime.js +13 -0
  61. package/src/orchestrator/deviation-handler.js +5 -3
  62. package/src/orchestrator/knowledge-runtime.js +64 -0
  63. package/src/orchestrator/planning-runtime.js +127 -0
  64. package/src/orchestrator/rail-runtime.js +421 -0
  65. package/src/orchestrator/release-runtime.js +14 -0
  66. package/src/orchestrator/review-runtime.js +74 -0
  67. package/src/orchestrator/runtime-installation-v2.js +38 -0
  68. package/src/orchestrator/session-manager.js +1 -1
  69. package/src/telemetry/config.js +1 -1
  70. package/src/terminal/adapters/grok-adapter.js +16 -0
  71. package/src/terminal/adapters/index.js +1 -0
  72. package/src/terminal/cli-registry.js +14 -0
  73. package/src/terminal/prompt-builder.js +2 -0
  74. package/src/terminal/run-agent.js +3 -0
  75. package/src/terminal/run-parallel.js +35 -24
  76. package/src/terminal/spawner.js +7 -1
  77. package/src/terminal/team-task-list.js +1 -1
  78. package/src/upgrade/backup.js +22 -44
  79. package/src/upgrade/checker.js +1 -1
  80. package/src/upgrade/migrator.js +52 -13
  81. package/src/utils/constitution-meta.js +12 -0
  82. package/src/utils/feature-flags.js +25 -1
  83. package/src/wizard/i18n.js +8 -2
  84. package/src/wizard/index.js +39 -4
  85. package/src/wizard/questions.js +59 -2
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { fileURLToPath } from 'url';
18
18
  import { buildAgentPrompt } from './prompt-builder.js';
19
- import { spawnParallelGroup, spawnTerminal } from './spawner.js';
19
+ import { spawnParallelGroup, spawnTerminal, killTerminal } from './spawner.js';
20
20
  import { TerminalMonitor } from './monitor.js';
21
21
  import { collectResults, mergeHandoffs, buildConsolidatedHandoff } from './collector.js';
22
22
  import { parseAgentOutput } from './handoff-parser.js';
@@ -45,6 +45,19 @@ function parseArgs(argv) {
45
45
  return args;
46
46
  }
47
47
 
48
+ function createSpawnConfig({ agent, taskId, promptResult, projectDir, timeout, strictProvider }) {
49
+ return {
50
+ agent,
51
+ taskId,
52
+ model: promptResult.model,
53
+ provider: promptResult.provider,
54
+ prompt: promptResult.prompt,
55
+ workingDir: projectDir,
56
+ timeout,
57
+ strictProvider,
58
+ };
59
+ }
60
+
48
61
  // ---------------------------------------------------------------------------
49
62
  // Main
50
63
  // ---------------------------------------------------------------------------
@@ -103,17 +116,14 @@ async function main() {
103
116
  previousAgent,
104
117
  sessionState,
105
118
  provider: args.provider || null,
119
+ model: args.model || null,
120
+ strictProvider: args['strict-provider'] === 'true',
106
121
  });
107
122
 
108
- configs.push({
109
- agent: agents[i],
110
- taskId: taskIds[i],
111
- model: promptResult.model,
112
- provider: promptResult.provider,
113
- prompt: promptResult.prompt,
114
- workingDir: projectDir,
115
- timeout,
116
- });
123
+ configs.push(createSpawnConfig({
124
+ agent: agents[i], taskId: taskIds[i], promptResult, projectDir, timeout,
125
+ strictProvider: args['strict-provider'] === 'true',
126
+ }));
117
127
  } catch (err) {
118
128
  outputError(`Failed to build prompt for ${agents[i]}: ${err.message}`);
119
129
  process.exit(1);
@@ -254,13 +264,13 @@ async function main() {
254
264
  * @param {number} timeout - Per-agent timeout in ms
255
265
  * @returns {Promise<{ groupId: string, terminals: object[] }>}
256
266
  */
257
- async function sequentialFallback(configs, timeout) {
267
+ async function sequentialFallback(configs, timeout, _spawnFn = spawnTerminal) {
258
268
  const groupId = `seq-fallback-${Date.now()}`;
259
269
  const terminals = [];
260
270
 
261
271
  for (let i = 0; i < configs.length; i++) {
262
272
  const cfg = configs[i];
263
- const terminal = spawnTerminal({
273
+ const handle = _spawnFn({
264
274
  agent: cfg.agent,
265
275
  taskId: cfg.taskId,
266
276
  model: cfg.model,
@@ -268,28 +278,29 @@ async function sequentialFallback(configs, timeout) {
268
278
  prompt: cfg.prompt,
269
279
  workingDir: cfg.workingDir,
270
280
  timeout: cfg.timeout || timeout,
281
+ strictProvider: cfg.strictProvider,
271
282
  });
272
283
 
273
- // Wait for this terminal to finish before spawning the next
284
+ // Wait for the REAL process exit before spawning the next agent. The old
285
+ // implementation probed terminal.onExit?.(), a method TerminalHandle never
286
+ // had, so every agent "completed" instantly and the fallback ran the whole
287
+ // group in parallel - defeating its purpose (shedding rate-limit pressure).
274
288
  await new Promise((resolve) => {
275
- const timer = setTimeout(() => {
276
- terminal.kill?.();
289
+ if (!handle?.process || handle.status !== 'running') {
277
290
  resolve();
291
+ return;
292
+ }
293
+ const timer = setTimeout(() => {
294
+ killTerminal(handle).catch(() => {}).then(resolve, resolve);
278
295
  }, (cfg.timeout || timeout) + 5_000);
279
296
 
280
- terminal.onExit?.(() => {
297
+ handle.process.once('exit', () => {
281
298
  clearTimeout(timer);
282
299
  resolve();
283
300
  });
284
-
285
- // If terminal doesn't have onExit, resolve after a short poll
286
- if (!terminal.onExit) {
287
- clearTimeout(timer);
288
- resolve();
289
- }
290
301
  });
291
302
 
292
- terminals.push(terminal);
303
+ terminals.push(handle);
293
304
 
294
305
  // Small delay between spawns to avoid rate limit pressure
295
306
  if (i < configs.length - 1) {
@@ -328,4 +339,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
328
339
  });
329
340
  }
330
341
 
331
- export { parseArgs, determineNextAgent, sequentialFallback };
342
+ export { parseArgs, createSpawnConfig, determineNextAgent, sequentialFallback };
@@ -107,7 +107,8 @@ export function cleanParentEnv(env) {
107
107
  * @property {string} agent - Agent name (e.g. "architect")
108
108
  * @property {string} taskId - Task identifier
109
109
  * @property {string} [model] - LLM model tier name (e.g. opus, pro, codex, claude-sonnet)
110
- * @property {string} [provider] - CLI provider name (claude, gemini, codex)
110
+ * @property {string} [provider] - CLI provider name (claude, gemini, codex, grok)
111
+ * @property {boolean} [strictProvider] - Reject a missing provider rather than using the legacy fallback
111
112
  * @property {string} [prompt] - Full prompt string (from prompt-builder, piped via stdin)
112
113
  * @property {object} [contextPayload] - Context to inject via env var
113
114
  * @property {string[]} [writeScope] - Override write scope
@@ -181,6 +182,11 @@ export function buildSpawnCommand(config) {
181
182
  args = adapterResult.args;
182
183
  prompt = adapterResult.stdinPrompt;
183
184
  } catch (err) {
185
+ if (config.strictProvider) {
186
+ const failure = new Error(`Selected provider "${providerName}" is unavailable: ${err.message}`);
187
+ failure.code = 'SELECTED_PROVIDER_UNAVAILABLE';
188
+ throw failure;
189
+ }
184
190
  // Fallback to claude if provider resolution fails (backwards compatibility)
185
191
  providerFallback = {
186
192
  requested: providerName,
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, realpathSync } from 'fs';
12
12
  import { join, dirname, resolve, relative } from 'path';
13
- import yaml from 'js-yaml';
13
+ import * as yaml from 'js-yaml';
14
14
  import { withLock } from '../utils/file-lock.js';
15
15
 
16
16
  // ---------------------------------------------------------------------------
@@ -1,6 +1,24 @@
1
1
  import { existsSync, mkdirSync, cpSync, rmSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { resolveFrameworkDir } from '../utils/framework-dir.js';
4
+ import { FRAMEWORK_DIRS_TO_COPY } from '../installer/core.js';
5
+
6
+ /**
7
+ * The backup must cover exactly what the upgrade overwrites, or a rollback
8
+ * leaves the install in a chimera state (some dirs at the new version, some
9
+ * restored). The upgrade replaces every dir in FRAMEWORK_DIRS_TO_COPY (via
10
+ * copyFrameworkFiles), so that same list is the backup/restore scope - one
11
+ * source of truth, no asymmetry. Previously the backup saved 12 dirs and the
12
+ * restore returned only 10 (intelligence and data were backed up but never
13
+ * restored; hooks, domains, tasks, scaffold and others were never backed up
14
+ * at all).
15
+ */
16
+ const BACKUP_DIRS = FRAMEWORK_DIRS_TO_COPY;
17
+
18
+ const BACKUP_FILES = [
19
+ 'constitution.md',
20
+ 'config.yaml',
21
+ ];
4
22
 
5
23
  /**
6
24
  * Create backup of chati.dev/ directory before upgrade
@@ -17,28 +35,7 @@ export function createBackup(targetDir, currentVersion) {
17
35
 
18
36
  mkdirSync(backupDir, { recursive: true });
19
37
 
20
- // Backup framework files (NOT artifacts or intelligence)
21
- const dirsToBackup = [
22
- 'orchestrator',
23
- 'agents',
24
- 'templates',
25
- 'workflows',
26
- 'quality-gates',
27
- 'schemas',
28
- 'frameworks',
29
- 'i18n',
30
- 'patterns',
31
- 'migrations',
32
- 'intelligence',
33
- 'data',
34
- ];
35
-
36
- const filesToBackup = [
37
- 'constitution.md',
38
- 'config.yaml',
39
- ];
40
-
41
- for (const dir of dirsToBackup) {
38
+ for (const dir of BACKUP_DIRS) {
42
39
  const src = join(frameworkDir, dir);
43
40
  const dest = join(backupDir, dir);
44
41
  if (existsSync(src)) {
@@ -46,7 +43,7 @@ export function createBackup(targetDir, currentVersion) {
46
43
  }
47
44
  }
48
45
 
49
- for (const file of filesToBackup) {
46
+ for (const file of BACKUP_FILES) {
50
47
  const src = join(frameworkDir, file);
51
48
  const dest = join(backupDir, file);
52
49
  if (existsSync(src)) {
@@ -68,26 +65,7 @@ export function restoreFromBackup(targetDir, version) {
68
65
  throw new Error(`Backup not found: ${backupDir}`);
69
66
  }
70
67
 
71
- // Restore backed-up directories
72
- const dirsToRestore = [
73
- 'orchestrator',
74
- 'agents',
75
- 'templates',
76
- 'workflows',
77
- 'quality-gates',
78
- 'schemas',
79
- 'frameworks',
80
- 'i18n',
81
- 'patterns',
82
- 'migrations',
83
- ];
84
-
85
- const filesToRestore = [
86
- 'constitution.md',
87
- 'config.yaml',
88
- ];
89
-
90
- for (const dir of dirsToRestore) {
68
+ for (const dir of BACKUP_DIRS) {
91
69
  const src = join(backupDir, dir);
92
70
  const dest = join(frameworkDir, dir);
93
71
  if (existsSync(src)) {
@@ -98,7 +76,7 @@ export function restoreFromBackup(targetDir, version) {
98
76
  }
99
77
  }
100
78
 
101
- for (const file of filesToRestore) {
79
+ for (const file of BACKUP_FILES) {
102
80
  const src = join(backupDir, file);
103
81
  const dest = join(frameworkDir, file);
104
82
  if (existsSync(src)) {
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { resolveFrameworkDir } from '../utils/framework-dir.js';
4
- import yaml from 'js-yaml';
4
+ import * as yaml from 'js-yaml';
5
5
  import semver from 'semver';
6
6
 
7
7
  /**
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync
2
2
  import { dirname, join } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { resolveFrameworkDir } from '../utils/framework-dir.js';
5
- import yaml from 'js-yaml';
5
+ import * as yaml from 'js-yaml';
6
6
  import semver from 'semver';
7
7
 
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -42,13 +42,25 @@ export function findMigrations(targetDir, fromVersion, toVersion) {
42
42
  for (const file of files) {
43
43
  try {
44
44
  const content = yaml.load(readFileSync(join(migrationsDir, file), 'utf-8'));
45
- const migration = content?.migration;
45
+ // Two header schemas exist: the nested `migration: { from, to }` form and
46
+ // the older flat `from_version`/`to_version` form (with `operations`
47
+ // instead of `steps`). The flat files were silently skipped here, which
48
+ // hid them from every upgrade.
49
+ const migration = content?.migration
50
+ ?? (content?.from_version && content?.to_version
51
+ ? { from: content.from_version, to: content.to_version, description: content.description, breaking: content.breaking }
52
+ : null);
46
53
 
47
54
  if (!migration?.from || !migration?.to) continue;
48
55
 
49
- // Check if this migration is in the upgrade path
56
+ // A migration applies when its TARGET version lies inside the upgrade
57
+ // interval (fromVersion, toVersion]. The old condition compared the
58
+ // migration's `from` against the user's version with gte, which skipped
59
+ // every pending migration for users sitting ABOVE a migration's starting
60
+ // point (a 4.2.3 user never received v4.2-to-v4.3) and is wrong for
61
+ // users below it too (a 3.x user needs every step on the way up).
50
62
  if (
51
- semver.gte(migration.from, fromVersion) &&
63
+ semver.gt(migration.to, fromVersion) &&
52
64
  semver.lte(migration.to, toVersion)
53
65
  ) {
54
66
  applicable.push({
@@ -57,7 +69,7 @@ export function findMigrations(targetDir, fromVersion, toVersion) {
57
69
  to: migration.to,
58
70
  description: migration.description || '',
59
71
  breaking: migration.breaking || false,
60
- steps: content.steps || [],
72
+ steps: content.steps || content.operations || [],
61
73
  rollback: content.rollback || [],
62
74
  });
63
75
  }
@@ -73,14 +85,20 @@ export function findMigrations(targetDir, fromVersion, toVersion) {
73
85
  }
74
86
 
75
87
  /**
76
- * Execute a single migration step
88
+ * Execute a single migration step. Exported for testing.
77
89
  */
78
- async function executeMigrationStep(targetDir, step) {
90
+ export async function executeMigrationStep(targetDir, step, sourceFile = '') {
79
91
  switch (step.type) {
80
92
  case 'create_directory': {
81
- const dir = join(targetDir, step.path);
82
- if (!existsSync(dir) || !step.idempotent) {
83
- mkdirSync(dir, { recursive: true });
93
+ // Both shapes exist in the migration corpus: `path` (string) in the
94
+ // nested-schema files and `paths` (array) in the older flat-schema ones.
95
+ const dirs = Array.isArray(step.paths) ? step.paths : [step.path];
96
+ for (const rel of dirs) {
97
+ if (!rel) continue;
98
+ const dir = join(targetDir, rel);
99
+ if (!existsSync(dir) || !step.idempotent) {
100
+ mkdirSync(dir, { recursive: true });
101
+ }
84
102
  }
85
103
  break;
86
104
  }
@@ -100,7 +118,19 @@ async function executeMigrationStep(targetDir, step) {
100
118
 
101
119
  const content = yaml.load(readFileSync(filePath, 'utf-8')) || {};
102
120
 
103
- if (step.operation === 'add_field') {
121
+ if (step.changes && typeof step.changes === 'object') {
122
+ // Older flat-schema shape: a `changes:` map of (possibly dotted)
123
+ // field -> value, applied as a plain set.
124
+ for (const [field, value] of Object.entries(step.changes)) {
125
+ const keys = field.split('.');
126
+ let obj = content;
127
+ for (let i = 0; i < keys.length - 1; i++) {
128
+ if (!obj[keys[i]]) obj[keys[i]] = {};
129
+ obj = obj[keys[i]];
130
+ }
131
+ obj[keys[keys.length - 1]] = value;
132
+ }
133
+ } else if (step.operation === 'add_field') {
104
134
  const keys = step.field.split('.');
105
135
  let obj = content;
106
136
  for (let i = 0; i < keys.length - 1; i++) {
@@ -176,7 +206,16 @@ async function executeMigrationStep(targetDir, step) {
176
206
  }
177
207
 
178
208
  default:
179
- // Unknown step type - skip
209
+ // Copy-style steps (copy_file, copy_directory) from the older flat-schema
210
+ // migrations are subsumed by the upgrade's blanket copyFrameworkFiles, so
211
+ // skipping them is correct. Anything else unknown is surfaced instead of
212
+ // silently dropped, so an upgrader from a very old version knows what to
213
+ // review manually.
214
+ if (step.type !== 'copy_file' && step.type !== 'copy_directory') {
215
+ process.stderr.write(
216
+ `[chati] warning: migration step type "${step.type}"${sourceFile ? ` in ${sourceFile}` : ''} is not auto-executable; review the migration file manually\n`
217
+ );
218
+ }
180
219
  break;
181
220
  }
182
221
  }
@@ -196,7 +235,7 @@ export async function runMigrations(targetDir, fromVersion, toVersion) {
196
235
  for (const migration of migrations) {
197
236
  try {
198
237
  for (const step of migration.steps) {
199
- await executeMigrationStep(targetDir, step);
238
+ await executeMigrationStep(targetDir, step, migration.file);
200
239
  }
201
240
  results.push({ file: migration.file, success: true });
202
241
  } catch (err) {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Single source of truth for constitution metadata used by
3
+ * integrity checks.
4
+ *
5
+ * Every check that counts `## Article` headings (doctor, registry health,
6
+ * installer validator, stale-scan check 13) compares against THIS constant,
7
+ * so adding a constitutional article is a one-line bump here. The stale-scan
8
+ * asserts the canonical constitution.md actually contains this many articles,
9
+ * which keeps the constant honest.
10
+ */
11
+
12
+ export const EXPECTED_ARTICLE_COUNT = 25;
@@ -14,9 +14,17 @@ import { resolveFrameworkDir } from './framework-dir.js';
14
14
 
15
15
  /**
16
16
  * All known feature flags with their default values.
17
- * New features start as false (opt-in).
17
+ * New features start as false (opt-in). Defaults describe the state when
18
+ * config.yaml is absent; installed projects get their real values written by
19
+ * generateConfigYaml, and parseFeaturesSection only honors keys listed here,
20
+ * so EVERY boolean flag documented in config.yaml must appear in this map or
21
+ * the user's toggle is silently ignored (which froze 11 flags until 4.4.1).
22
+ * The numeric team_* and build_wave_size keys are intentionally absent: the
23
+ * parser is boolean-only and those values are read directly from config.yaml
24
+ * by their consumers.
18
25
  */
19
26
  const DEFAULTS = {
27
+ // Core framework flags
20
28
  hybrid_budget: false,
21
29
  anti_dash: false,
22
30
  rate_limiter_integration: false,
@@ -28,6 +36,22 @@ const DEFAULTS = {
28
36
  tech_presets: false,
29
37
  doctor_autofix: false,
30
38
  brandbook: false,
39
+ // Intelligence upgrade flags
40
+ undercover_mode: false,
41
+ memory_extraction: false,
42
+ memory_consolidation: false,
43
+ daily_digest: false,
44
+ structured_session_memory: false,
45
+ static_prism_boundary: false,
46
+ token_bracket_estimation: false,
47
+ model_fallback: false,
48
+ frustration_detection: false,
49
+ bash_security_checks: false,
50
+ // Agent Teams (Article XXI). NOTE: isAgentTeamsEnabled in cli.js keeps its
51
+ // own reader with Claude-provider defaulting; this entry exists so a config
52
+ // toggle is at least visible through the generic API.
53
+ agent_teams: false,
54
+ // Execution Kinds (Article XXV)
31
55
  execution_kinds: false,
32
56
  };
33
57
 
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import yaml from 'js-yaml';
4
+ import * as yaml from 'js-yaml';
5
5
  import { resolveFrameworkDir } from '../utils/framework-dir.js';
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -31,12 +31,18 @@ const FALLBACK_EN = {
31
31
  provider_selection_title: 'Which AI providers will you use?',
32
32
  editor_selection_title: 'Which editor IDEs should get rules files? (optional)',
33
33
  primary_provider_title: 'Which is your primary CLI provider?',
34
+ execution_profile_title: 'Select the execution profile',
35
+ execution_profile_standard: 'Standard',
36
+ execution_profile_standard_hint: 'Provider-neutral local project workflow',
37
+ execution_profile_internal: 'Focus AI internal',
38
+ execution_profile_internal_hint: 'Requires ClickUp tracking and completion metrics',
39
+ execution_profile_label: 'Execution profile',
34
40
  primary_provider_label: 'Primary Provider',
35
41
  providers_label: 'Providers',
36
42
  quick_start_switch_hint: 'Switch CLIs anytime — your session continues from where you left off',
37
43
  created_overlays: 'Created provider overlay directories',
38
44
  will_install: 'Will install:',
39
- agents_count: 'Specialized agent definitions across DISCOVER, PLAN, BUILD, DEPLOY phases',
45
+ agents_count: 'Specialized discovery and planning agents with RAIL execution and release lanes',
40
46
  workflows_count: '6 workflow blueprints',
41
47
  templates_count: '6 templates (PRD, Brownfield PRD, Architecture, Task, QA Gate, Quick Brief)',
42
48
  constitution: 'Constitution (25 Articles + Preamble)',
@@ -1,11 +1,14 @@
1
1
  import * as p from '@clack/prompts';
2
- import { readFileSync } from 'fs';
2
+ import { existsSync, readFileSync } from 'fs';
3
3
  import { join, dirname, basename } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { logBanner } from '../utils/logger.js';
6
- import { stepLanguage, stepProjectType, stepProviderSelection, stepEditorSelection, stepPrimaryProvider, stepConfirmation, stepTermsOfUse } from './questions.js';
6
+ import { stepLanguage, stepProjectType, stepProviderSelection, stepEditorSelection, stepPrimaryProvider, stepModelSelection, stepExecutionProfile, stepConfirmation, stepTermsOfUse } from './questions.js';
7
7
  import { createSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
8
8
  import { installFramework } from '../installer/core.js';
9
+ import { INSTALLATION_ARTIFACT_PATH, dryRunV2, installV2, reconfigureV2 } from '../installer-v2/index.js';
10
+ import { buildWizardV2InstallationInput } from '../installer-v2/wizard-installation.js';
11
+ import { resolveCapabilityCatalog, verifySignedCapabilityCatalog } from '../installer-v2/catalog-client.js';
9
12
  import { validateInstallation } from '../installer/validator.js';
10
13
  import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
11
14
  import { sendEvents } from '../telemetry/sender.js';
@@ -69,11 +72,25 @@ export async function runWizard(targetDir, options = {}) {
69
72
  // Step 3b: Primary provider (only if multiple)
70
73
  const primaryProvider = options.llmProvider || await stepPrimaryProvider(selectedProviders);
71
74
 
72
- // Step 3c: Editor Selection (which editor IDEs get rules files)
75
+ // Step 3c: Models allowed for each selected provider. Never add a provider
76
+ // or model that the user did not explicitly bind to this installation.
77
+ if (options.capabilityCatalog) {
78
+ throw Object.assign(new Error('unsigned capabilityCatalog injection is forbidden; provide signedCapabilityCatalogEnvelope'), { code: 'UNSIGNED_CATALOG_FORBIDDEN' });
79
+ }
80
+ const catalogResolution = options.signedCapabilityCatalogEnvelope
81
+ ? { source: 'provided-signed', catalog: verifySignedCapabilityCatalog(options.signedCapabilityCatalogEnvelope, { publicKeyPem: options.catalogPublicKeyPem }) }
82
+ : await resolveCapabilityCatalog({ projectDir: targetDir, catalogUrl: options.catalogUrl, fetchImpl: options.catalogFetch });
83
+ const modelSelections = options.modelSelections || await stepModelSelection(selectedProviders, catalogResolution.catalog);
84
+
85
+ // Step 3d: Profile controls whether ClickUp and internal completion metrics
86
+ // are mandatory. It must never be silently forced on public installations.
87
+ const executionProfile = options.executionProfile || await stepExecutionProfile();
88
+
89
+ // Step 3e: Editor Selection (which editor IDEs get rules files)
73
90
  const selectedEditors = options.editors || await stepEditorSelection();
74
91
 
75
92
  // Combine providers + editors into selectedIDEs for backward compatibility
76
- const providerToIDE = { claude: 'claude-code', gemini: 'gemini-cli', codex: 'codex-cli' };
93
+ const providerToIDE = { claude: 'claude-code', gemini: 'gemini-cli', codex: 'codex-cli', grok: 'grok-cli' };
77
94
  const selectedIDEs = options.ides || [
78
95
  ...selectedProviders.map(p => providerToIDE[p]).filter(Boolean),
79
96
  ...selectedEditors,
@@ -94,9 +111,25 @@ export async function runWizard(targetDir, options = {}) {
94
111
  allProviders: cliProviders,
95
112
  selectedIDEs,
96
113
  selectedMCPs,
114
+ executionProfile,
97
115
  targetDir,
98
116
  version: VERSION,
99
117
  };
118
+ const v2InstallationInput = buildWizardV2InstallationInput({
119
+ projectName,
120
+ selectedProviders: cliProviders,
121
+ primaryProvider,
122
+ modelSelections,
123
+ capabilityCatalog: catalogResolution.catalog,
124
+ profile: executionProfile,
125
+ });
126
+ // Validate the complete v2 material and refuse a repeated init before the
127
+ // legacy installer writes anything. Reconfiguration uses its own backup and
128
+ // atomic replacement path below.
129
+ dryRunV2(v2InstallationInput, { projectDir: targetDir });
130
+ if (existsSync(join(targetDir, INSTALLATION_ARTIFACT_PATH)) && options.reconfigure !== true) {
131
+ throw Object.assign(new Error('a v2 installation already exists; use --reconfigure to replace it safely'), { code: 'INSTALLATION_ALREADY_EXISTS' });
132
+ }
100
133
 
101
134
  await stepConfirmation(config);
102
135
 
@@ -115,6 +148,8 @@ export async function runWizard(targetDir, options = {}) {
115
148
 
116
149
  try {
117
150
  await installFramework(config);
151
+ if (options.reconfigure === true) reconfigureV2(v2InstallationInput, { projectDir: targetDir });
152
+ else installV2(v2InstallationInput, { projectDir: targetDir });
118
153
  installSpinner.stop();
119
154
 
120
155
  showStep(t('installer.created_chati'));
@@ -5,6 +5,7 @@ import { showSummary, showChecklist } from './feedback.js';
5
5
  import { brand, dim, success } from '../utils/colors.js';
6
6
  import { isProviderAvailable } from '../terminal/cli-registry.js';
7
7
  import { IDE_CONFIGS, IDE_TO_PROVIDER } from '../config/ide-configs.js';
8
+ import { supportedV2WizardProviders, wizardDefaultModels, wizardModelSuggestions } from '../installer-v2/wizard-installation.js';
8
9
 
9
10
  /**
10
11
  * Step 1: Language Selection (always in English)
@@ -66,6 +67,7 @@ const PROVIDER_DISPLAY_NAMES = {
66
67
  claude: 'Claude (Anthropic)',
67
68
  gemini: 'Gemini (Google)',
68
69
  codex: 'Codex (OpenAI)',
70
+ grok: 'Grok (xAI)',
69
71
  };
70
72
 
71
73
  /**
@@ -75,7 +77,7 @@ const PROVIDER_DISPLAY_NAMES = {
75
77
  * Returns an array of provider names (e.g. ['claude', 'gemini']).
76
78
  */
77
79
  export async function stepProviderSelection() {
78
- const cliIDEs = Object.entries(IDE_CONFIGS).filter(([, c]) => c.group === 'cli');
80
+ const cliIDEs = Object.entries(IDE_CONFIGS).filter(([key, c]) => c.group === 'cli' && supportedV2WizardProviders.includes(IDE_TO_PROVIDER[key]));
79
81
  const options = [];
80
82
 
81
83
  for (const [key, config] of cliIDEs) {
@@ -110,6 +112,26 @@ export async function stepProviderSelection() {
110
112
  return selected;
111
113
  }
112
114
 
115
+ /**
116
+ * Selects the operational policy profile. Focus AI internal projects require
117
+ * ClickUp tracking; standard projects do not gain that external dependency.
118
+ */
119
+ export async function stepExecutionProfile() {
120
+ const profile = await p.select({
121
+ message: t('installer.execution_profile_title'),
122
+ options: [
123
+ { value: 'other-supported-profile', label: t('installer.execution_profile_standard'), hint: t('installer.execution_profile_standard_hint') },
124
+ { value: 'focus-ai-internal', label: t('installer.execution_profile_internal'), hint: t('installer.execution_profile_internal_hint') },
125
+ ],
126
+ initialValue: 'other-supported-profile',
127
+ });
128
+ if (p.isCancel(profile)) {
129
+ p.cancel('Installation cancelled.');
130
+ process.exit(0);
131
+ }
132
+ return profile;
133
+ }
134
+
113
135
  /**
114
136
  * Step 3b: Editor Selection (multi-select, optional)
115
137
  *
@@ -228,11 +250,43 @@ export async function stepPrimaryProvider(cliProviders) {
228
250
  return primary;
229
251
  }
230
252
 
253
+ /**
254
+ * Records exactly which models the installation is allowed to orchestrate.
255
+ * A catalog refresh can replace the suggestion, but the user remains the
256
+ * authority for the final binding stored in the installation artifact.
257
+ */
258
+ export async function stepModelSelection(providers, capabilityCatalog) {
259
+ const selections = {};
260
+
261
+ for (const provider of providers) {
262
+ const suggestions = wizardModelSuggestions(provider, capabilityCatalog);
263
+ const suggested = suggestions[0] || wizardDefaultModels[provider];
264
+ const answer = await p.text({
265
+ message: `Models for ${PROVIDER_DISPLAY_NAMES[provider] || provider} (comma-separated):`,
266
+ placeholder: suggestions.join(', '),
267
+ initialValue: suggested,
268
+ validate: (value) => {
269
+ const models = String(value || '').split(',').map((item) => item.trim()).filter(Boolean);
270
+ return models.length > 0 ? undefined : 'Select at least one model.';
271
+ },
272
+ });
273
+
274
+ if (p.isCancel(answer)) {
275
+ p.cancel('Installation cancelled.');
276
+ process.exit(0);
277
+ }
278
+
279
+ selections[provider] = String(answer).split(',').map((item) => item.trim()).filter(Boolean);
280
+ }
281
+
282
+ return selections;
283
+ }
284
+
231
285
  /**
232
286
  * Step 4: Confirmation
233
287
  */
234
288
  export async function stepConfirmation(config) {
235
- const { projectName, projectType, language, llmProvider, selectedMCPs, selectedIDEs, allProviders } = config;
289
+ const { projectName, projectType, language, llmProvider, selectedMCPs, selectedIDEs, allProviders, executionProfile } = config;
236
290
 
237
291
  const langName = SUPPORTED_LANGUAGES.find(l => l.value === language)?.label || language;
238
292
 
@@ -256,6 +310,9 @@ export async function stepConfirmation(config) {
256
310
  [t('installer.project_label')]: `${projectName} (${projectType === 'greenfield' ? 'Greenfield' : 'Brownfield'})`,
257
311
  [t('installer.language_label')]: langName,
258
312
  [t('installer.providers_label')]: providersDisplay,
313
+ [t('installer.execution_profile_label')]: executionProfile === 'focus-ai-internal'
314
+ ? `${t('installer.execution_profile_internal')} (ClickUp required)`
315
+ : t('installer.execution_profile_standard'),
259
316
  };
260
317
 
261
318
  // Only show editor IDEs line if any were selected