chati-dev 4.5.14 → 4.5.16

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.
@@ -1,8 +1,8 @@
1
1
  # chati.dev Configuration
2
- version: "4.5.14"
2
+ version: "4.5.16"
3
3
  installed_at: "2026-02-07T10:00:00Z"
4
- updated_at: "2026-08-27T00:00:00Z"
5
- installer_version: "4.5.14"
4
+ updated_at: "2026-08-28T00:00:00Z"
5
+ installer_version: "4.5.16"
6
6
  project_type: greenfield
7
7
  language: en
8
8
  ides: []
@@ -21,7 +21,7 @@ Agent retries on failure. Investigate root cause before escalating.
21
21
  ## 6. Language Protocol
22
22
  - **Interaction**: User's preferred language
23
23
  - **Artifacts**: Always English
24
- - **First step output**: Always English
24
+ - **Every visible interaction**: Use the language selected during installation, starting with the first response. This includes prefaces, progress updates, questions, errors, pauses, and resumptions.
25
25
 
26
26
  ## 7. Deviation Protocol
27
27
  - Deviations handled by orchestrator (not separate agent)
@@ -1,7 +1,7 @@
1
1
  # Chati.dev System Context
2
2
 
3
3
  ## Framework
4
- - **Version**: 4.5.14
4
+ - **Version**: 4.5.16
5
5
  - **Agents**: Specialized discovery and planning agents feeding provider-neutral RAIL execution and a sealed release lane
6
6
  - **Constitution**: 25 Articles + Preamble
7
7
  - **Quality**: 5 pipeline gates + 3-tier verdicts + Fault Vector Protocol (Article XXII)
@@ -100,6 +100,9 @@ installer:
100
100
  system_prerequisites_failed: "System prerequisites not met:"
101
101
  fix_prerequisites: "Fix the issues above and try again."
102
102
  system_check: "System check:"
103
+ system_checking: "Checking system requirements..."
104
+ catalog_resolving: "Loading the signed model catalog..."
105
+ internal_authorizing: "Verifying access to Internal mode..."
103
106
  created_provider_claude: "Created .claude/commands/ (thin router)"
104
107
  created_provider_gemini: "Created .gemini/commands/ (TOML command)"
105
108
  created_provider_codex: "Created .agents/skills/chati/ (Codex skill)"
@@ -114,6 +117,7 @@ installer:
114
117
  license_placeholder: "CHATI-XXXX-XXXX-XXXX"
115
118
  license_skipped: "Skipped. Run: npx chati-dev activate --key=YOUR-KEY when ready."
116
119
  license_activating: "Activating license..."
120
+ license_verifying: "Verifying saved license..."
117
121
  license_activated: "License activated! Plan: {plan}, {days} day(s) remaining."
118
122
  license_activation_failed: "Activation failed: {error}"
119
123
  license_retry: "Run: npx chati-dev activate --key=YOUR-KEY to try again."
@@ -100,6 +100,9 @@ installer:
100
100
  system_prerequisites_failed: "No se cumplen los requisitos previos del sistema:"
101
101
  fix_prerequisites: "Corrija los problemas anteriores e intentelo de nuevo."
102
102
  system_check: "Verificacion del sistema:"
103
+ system_checking: "Verificando los requisitos del sistema..."
104
+ catalog_resolving: "Cargando el catalogo firmado de modelos..."
105
+ internal_authorizing: "Verificando el acceso al modo Internal..."
103
106
  created_provider_claude: "Creado .claude/commands/ (enrutador ligero)"
104
107
  created_provider_gemini: "Creado .gemini/commands/ (comando TOML)"
105
108
  created_provider_codex: "Creado .agents/skills/chati/ (skill de Codex)"
@@ -114,6 +117,7 @@ installer:
114
117
  license_placeholder: "CHATI-XXXX-XXXX-XXXX"
115
118
  license_skipped: "Paso omitido. Cuando quiera, ejecute: npx chati-dev activate --key=SU-CLAVE."
116
119
  license_activating: "Activando la licencia..."
120
+ license_verifying: "Verificando la licencia guardada..."
117
121
  license_activated: "Licencia activada! Plan: {plan}, {days} dia(s) restante(s)."
118
122
  license_activation_failed: "Fallo de activacion: {error}"
119
123
  license_retry: "Ejecute: npx chati-dev activate --key=SU-CLAVE para intentarlo de nuevo."
@@ -100,6 +100,9 @@ installer:
100
100
  system_prerequisites_failed: "Les prerequis systeme ne sont pas satisfaits:"
101
101
  fix_prerequisites: "Corrigez les problemes ci-dessus et reessayez."
102
102
  system_check: "Verification du systeme:"
103
+ system_checking: "Verification des prerequis du systeme..."
104
+ catalog_resolving: "Chargement du catalogue de modeles signe..."
105
+ internal_authorizing: "Verification de l'acces au mode Internal..."
103
106
  created_provider_claude: "Cree .claude/commands/ (routeur leger)"
104
107
  created_provider_gemini: "Cree .gemini/commands/ (commande TOML)"
105
108
  created_provider_codex: "Cree .agents/skills/chati/ (skill Codex)"
@@ -114,6 +117,7 @@ installer:
114
117
  license_placeholder: "CHATI-XXXX-XXXX-XXXX"
115
118
  license_skipped: "Etape ignoree. Lorsque vous le souhaitez, executez: npx chati-dev activate --key=VOTRE-CLE."
116
119
  license_activating: "Activation de la licence..."
120
+ license_verifying: "Verification de la licence enregistree..."
117
121
  license_activated: "Licence activee! Offre: {plan}, {days} jour(s) restant(s)."
118
122
  license_activation_failed: "Echec de l'activation: {error}"
119
123
  license_retry: "Executez: npx chati-dev activate --key=VOTRE-CLE pour reessayer."
@@ -100,6 +100,9 @@ installer:
100
100
  system_prerequisites_failed: "Os pre-requisitos do sistema nao foram atendidos:"
101
101
  fix_prerequisites: "Corrija os problemas acima e tente novamente."
102
102
  system_check: "Verificacao do sistema:"
103
+ system_checking: "Verificando os requisitos do sistema..."
104
+ catalog_resolving: "Carregando o catalogo assinado de modelos..."
105
+ internal_authorizing: "Validando o acesso ao modo Internal..."
103
106
  created_provider_claude: "Criado .claude/commands/ (roteador leve)"
104
107
  created_provider_gemini: "Criado .gemini/commands/ (comando TOML)"
105
108
  created_provider_codex: "Criado .agents/skills/chati/ (skill do Codex)"
@@ -114,6 +117,7 @@ installer:
114
117
  license_placeholder: "CHATI-XXXX-XXXX-XXXX"
115
118
  license_skipped: "Etapa ignorada. Quando quiser, execute: npx chati-dev activate --key=SUA-CHAVE."
116
119
  license_activating: "Ativando a licenca..."
120
+ license_verifying: "Validando a licenca salva..."
117
121
  license_activated: "Licenca ativada! Plano: {plan}, {days} dia(s) restante(s)."
118
122
  license_activation_failed: "Falha na ativacao: {error}"
119
123
  license_retry: "Execute: npx chati-dev activate --key=SUA-CHAVE para tentar novamente."
@@ -1,5 +1,5 @@
1
1
  {
2
- "createdAt": "2026-08-28T08:29:11.442Z",
2
+ "createdAt": "2026-08-28T12:19:47.274Z",
3
3
  "files": {
4
4
  "agents/build/dev.md": {
5
5
  "hash": "f0dd9bcdf44c57fdf64beca65d3617f0f24467b2f6edc9c0577d721b4922977a",
@@ -97,7 +97,7 @@
97
97
  "type": "md"
98
98
  },
99
99
  "config.yaml": {
100
- "hash": "8c95fbfbb1f751468c309007aaafd784835af8fe7d712313733ca82605f13cf1",
100
+ "hash": "66f9a59f21944288544a7aeb5bb0316c3c65be1162faa0db896f5346d8f89877",
101
101
  "size": 2638,
102
102
  "type": "yaml"
103
103
  },
@@ -112,8 +112,8 @@
112
112
  "type": "md"
113
113
  },
114
114
  "context/protocols.md": {
115
- "hash": "c1fe6ae5c0c17ab664394bc3caa3ecddf4cd1c4a17db34ad5f5086bd65ccd399",
116
- "size": 1250,
115
+ "hash": "a80110e083cd4e301173353fa2748e096bdb7a09ffad9e6302ba5075fbf11cee",
116
+ "size": 1410,
117
117
  "type": "md"
118
118
  },
119
119
  "context/quality.md": {
@@ -122,7 +122,7 @@
122
122
  "type": "md"
123
123
  },
124
124
  "context/root.md": {
125
- "hash": "90f8c75eab767daba5178351e02925c99c201215e9f0ea8ee1bf1991eb1136b0",
125
+ "hash": "a226a365f64a28bf952c399dcc97962202bd195fa2a75fd4b894293a1503569d",
126
126
  "size": 1333,
127
127
  "type": "md"
128
128
  },
@@ -377,23 +377,23 @@
377
377
  "type": "js"
378
378
  },
379
379
  "i18n/en.yaml": {
380
- "hash": "220fc363ed6e523509cd50cab92ad17735cc8de37f60030cb8b26dadca1c88f6",
381
- "size": 8082,
380
+ "hash": "dbaf6e3da9820a933bd6b358e5dcce49057c7b34c7855d6f7f443833202f347a",
381
+ "size": 8307,
382
382
  "type": "yaml"
383
383
  },
384
384
  "i18n/es.yaml": {
385
- "hash": "e088fca0e2525482011e290396b7d242751c97af5b23d49e9e84e6c5b2f854f5",
386
- "size": 8685,
385
+ "hash": "047a14e7fa763179002c265f2df3bf0e9a407d5fc8b8fac0ff05ae92562f11eb",
386
+ "size": 8941,
387
387
  "type": "yaml"
388
388
  },
389
389
  "i18n/fr.yaml": {
390
- "hash": "5245519c4acdd577db0052928e0c6ee7b4677212d8b2808cdf0ea4aa89f67b67",
391
- "size": 8826,
390
+ "hash": "8353e42d8124546109833a123143e905dbb976abe658259d3782aa5d1190d0b4",
391
+ "size": 9090,
392
392
  "type": "yaml"
393
393
  },
394
394
  "i18n/pt.yaml": {
395
- "hash": "0a1e9a905db5a776ee790735fbd442d3b0fce93084f1b8fb3ba9021726b3047b",
396
- "size": 8566,
395
+ "hash": "9286764e634c88b1cf8f49f9239d48ff5a2bbef7fe207d61d69d70c501bdca8b",
396
+ "size": 8812,
397
397
  "type": "yaml"
398
398
  },
399
399
  "intelligence/confidence.yaml": {
@@ -462,8 +462,8 @@
462
462
  "type": "yaml"
463
463
  },
464
464
  "orchestrator/chati-router.js": {
465
- "hash": "a7d321ac7454ff762b251d81060da766668b8a1051b92cef6dea17fb092e606a",
466
- "size": 22113,
465
+ "hash": "402033380d5becfb73d77c4c617740e7f32b2abf1c42bf6b9e751b97bd32c876",
466
+ "size": 23194,
467
467
  "type": "js"
468
468
  },
469
469
  "orchestrator/chati-update.md": {
@@ -472,8 +472,8 @@
472
472
  "type": "md"
473
473
  },
474
474
  "orchestrator/chati.md": {
475
- "hash": "b64b1d2e1880f8de6cc80eca9fe42328f1f00a558e8bf03811b4433e883f1af8",
476
- "size": 46098,
475
+ "hash": "b0e7395fa4ae0119e8df721e0f97c04d740c119be5f260673aea14b5672e96ff",
476
+ "size": 46948,
477
477
  "type": "md"
478
478
  },
479
479
  "package.json": {
@@ -857,8 +857,8 @@
857
857
  "type": "json"
858
858
  },
859
859
  "schemas/session.schema.json": {
860
- "hash": "5d43e2cef2722f0bacd31326eab32040c9327469f1b520ecbe0e517940e0f754",
861
- "size": 19183,
860
+ "hash": "553b5bf78761f8dd50f98b1e638d42c1bde473d1c0e562b087b3f26cc8c3585b",
861
+ "size": 19979,
862
862
  "type": "json"
863
863
  },
864
864
  "schemas/task.schema.json": {
@@ -1352,5 +1352,5 @@
1352
1352
  "type": "yaml"
1353
1353
  }
1354
1354
  },
1355
- "version": "4.5.14"
1355
+ "version": "4.5.16"
1356
1356
  }
@@ -1 +1 @@
1
- pPMQq8Hj2U80c5Q0ToefvDYDDokdYPbLnhbB7eXsH4ne15QSk0fHGJHd0QpJbfXKKBEEZlNhBMzMQcoDBbASDw==
1
+ pn6mHfeHH1rfvjDzyoAh8karYWO61dtxogu3tZgcmrb8Q07rgK3jUK0AS4ThiCqIVDiyGOpp0df6GjiZ1meGAg==
@@ -19,7 +19,12 @@ import { existsSync, readFileSync } from 'fs';
19
19
  import { join } from 'path';
20
20
 
21
21
  const PROJECT_DIR = process.cwd();
22
- const ROUTER_ARGS = process.argv.slice(2);
22
+ const RAW_ROUTER_ARGS = process.argv.slice(2);
23
+ const harnessFlagIndex = RAW_ROUTER_ARGS.indexOf('--harness');
24
+ const ACTIVE_HARNESS = harnessFlagIndex >= 0 ? RAW_ROUTER_ARGS[harnessFlagIndex + 1] : null;
25
+ const ROUTER_ARGS = harnessFlagIndex >= 0
26
+ ? RAW_ROUTER_ARGS.filter((_, index) => index !== harnessFlagIndex && index !== harnessFlagIndex + 1)
27
+ : RAW_ROUTER_ARGS;
23
28
 
24
29
  async function readEnabledHarnesses(projectDir) {
25
30
  const installationPath = join(projectDir, '.chati', 'v2', 'installation.json');
@@ -312,6 +317,7 @@ const VALID_ORCHESTRATE_SUBS = new Set([
312
317
  'spawn-team', 'team-status', 'team-dissolve',
313
318
  'wait-for-capture', 'wave-status', 'assess-coverage',
314
319
  'wait-for-license', 'doctor', 'run-script-task',
320
+ 'activate-harness',
315
321
  ]);
316
322
 
317
323
  const VALID_RAIL_SUBS = new Set(['rail-next']);
@@ -486,13 +492,34 @@ async function main() {
486
492
  pipeline: null,
487
493
  team: null,
488
494
  error: null,
495
+ active_harness: ACTIVE_HARNESS,
489
496
  };
490
497
 
498
+ const canonicalSessionPath = join(PROJECT_DIR, '.chati', 'session.yaml');
499
+ if (ACTIVE_HARNESS && existsSync(canonicalSessionPath)) {
500
+ const cliMod = await findCliModule(PROJECT_DIR);
501
+ if (!cliMod) {
502
+ result.ok = false;
503
+ result.action = 'error';
504
+ result.error = 'cli_module_not_found';
505
+ console.log(JSON.stringify(result));
506
+ return;
507
+ }
508
+ const activation = await captureOrchestrate(cliMod, 'activate-harness', ['--harness', ACTIVE_HARNESS], PROJECT_DIR);
509
+ if (!activation.activated) {
510
+ result.ok = false;
511
+ result.action = 'error';
512
+ result.error = activation.error || 'harness_activation_failed';
513
+ console.log(JSON.stringify(result));
514
+ return;
515
+ }
516
+ }
517
+
491
518
  // -----------------------------------------------------------------------
492
519
  // 1. Session state
493
520
  // -----------------------------------------------------------------------
494
521
  try {
495
- const sessionPath = join(PROJECT_DIR, '.chati', 'session.yaml');
522
+ const sessionPath = canonicalSessionPath;
496
523
  if (existsSync(sessionPath)) {
497
524
  const raw = readFileSync(sessionPath, 'utf-8');
498
525
  const extract = (key) => {
@@ -103,6 +103,7 @@ Parse the JSON output. The `action` field tells you what to do:
103
103
  | `setup` | Action: Setup |
104
104
  | `activate_interactive` | Action: Interactive Agent |
105
105
  | `spawn_routed_interactive` | Action: Routed Interactive Agent |
106
+ | `resume_pending_interaction` | Action: Resume Pending Interaction |
106
107
  | `spawn_autonomous` | Action: Autonomous Agent |
107
108
  | `spawn_parallel` | Action: Parallel Spawn |
108
109
  | `spawn_team` | Action: Spawn Team (Article XXI) |
@@ -204,6 +205,19 @@ internal router.
204
205
  4. The opened CLI remains the conversation orchestrator. The routed process
205
206
  does not become a framework-wide primary CLI.
206
207
 
208
+ ## Action: Resume Pending Interaction
209
+
210
+ The routed process already asked a question and the canonical session stored
211
+ it. Do not execute `spawn_command` yet and do not dispatch the agent again.
212
+
213
+ 1. Present only the exact `question` returned by the router, using
214
+ `session.language`.
215
+ 2. When the user answers, execute the returned `spawn_command` once with
216
+ `--additional-context "{user_response}"` appended.
217
+ 3. Parse the new result using Action: Routed Interactive Agent.
218
+ 4. Read `relay_cycles` from the router response. After three unresolved relay cycles, use the configured reasoning
219
+ escalation. Do not invent an answer or silently switch providers.
220
+
207
221
  ---
208
222
 
209
223
  ## Completion Options (Protocol 3 - Guided Options)
@@ -977,7 +991,7 @@ When the independent RAIL reviewer classifies a fault:
977
991
  - Read language from JSON `session.language` field
978
992
  - ALL interactions, guidance, questions, options in that language
979
993
  - ALL artifacts, handoffs, templates in English (enforced by Constitution Article VII)
980
- - First step output of any agent: always English
994
+ - Every visible interaction from the first response onward follows `session.language`, including prefaces, progress messages, questions, errors, pauses, and resumptions.
981
995
 
982
996
  ---
983
997
 
@@ -85,6 +85,26 @@
85
85
  "type": "string",
86
86
  "description": "Currently active agent name"
87
87
  },
88
+ "active_harness": {
89
+ "type": ["string", "null"],
90
+ "enum": ["claude", "codex", "grok", null],
91
+ "default": null,
92
+ "description": "CLI or UI currently orchestrating the conversation. Independent from task provider routing."
93
+ },
94
+ "session_status": {
95
+ "enum": ["inactive", "active", "paused", "completed"],
96
+ "default": "inactive"
97
+ },
98
+ "current_execution_binding": {
99
+ "type": ["object", "null"],
100
+ "default": null,
101
+ "description": "Exact provider, model and reasoning binding for the active routed task."
102
+ },
103
+ "pending_interaction": {
104
+ "type": ["object", "null"],
105
+ "default": null,
106
+ "description": "Durable interactive relay, including the exact unanswered question and its execution binding."
107
+ },
88
108
  "language": {
89
109
  "enum": ["en", "pt", "es", "fr"],
90
110
  "default": "en",
@@ -122,7 +142,7 @@
122
142
  "type": "object",
123
143
  "required": ["status", "score", "criteria_count"],
124
144
  "properties": {
125
- "status": { "enum": ["pending", "in_progress", "completed", "skipped", "needs_revalidation"] },
145
+ "status": { "enum": ["pending", "in_progress", "waiting_for_user", "completed", "skipped", "needs_revalidation"] },
126
146
  "score": { "type": "number", "minimum": 0, "maximum": 100 },
127
147
  "criteria_count": { "type": "integer", "minimum": 0 },
128
148
  "completed_at": { "type": ["string", "null"], "format": "date-time" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.5.14",
3
+ "version": "4.5.16",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System - Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -182,7 +182,13 @@ export async function installFramework(config) {
182
182
  // Write config.yaml
183
183
  writeFileSync(
184
184
  join(frameworkDir, 'config.yaml'),
185
- generateConfigYaml({ version, projectType, language, selectedIDEs, allProviders: enabledProviders, runtimeVersion: config.runtimeVersion }),
185
+ generateConfigYaml({
186
+ version, projectType, language, selectedIDEs,
187
+ allProviders: enabledProviders,
188
+ runtimeVersion: config.runtimeVersion,
189
+ installationMode: config.installationMode,
190
+ clickupPreflight: config.clickupPreflight,
191
+ }),
186
192
  'utf-8'
187
193
  );
188
194
 
@@ -481,7 +487,7 @@ async function configureIDE(targetDir, ideKey, selectedMCPs, options = {}) {
481
487
 
482
488
  Run via Bash:
483
489
  \`\`\`
484
- node .chati.dev/orchestrator/chati-router.js
490
+ node .chati.dev/orchestrator/chati-router.js --harness claude
485
491
  \`\`\`
486
492
 
487
493
  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.
@@ -497,7 +503,7 @@ Parse the JSON output. This single command already handled: license validation,
497
503
 
498
504
  ### If \`ok\` is true:
499
505
 
500
- 1. Use \`session.language\` for ALL responses (pt = Português, en = English, es = Español, fr = Français).
506
+ 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).
501
507
  2. Read the full orchestrator at \`${orchestratorPath || '.chati.dev/orchestrator/chati.md'}\`.
502
508
  3. The \`action\` field tells you what to do. Go to the matching "Action:" section in the orchestrator.
503
509
  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.
@@ -55,6 +55,10 @@ export function generateSessionYaml(config) {
55
55
  scaffold_applied: [],
56
56
  active_model: null,
57
57
  active_provider: null,
58
+ active_harness: null,
59
+ session_status: 'inactive',
60
+ current_execution_binding: null,
61
+ pending_interaction: null,
58
62
  context_tokens_used: 0,
59
63
  context_window_tokens: null,
60
64
  context_last_bracket: null,
@@ -119,6 +123,13 @@ export function generateConfigYaml(config) {
119
123
  codex: { enabled: providers.includes('codex') || selectedIDEs.includes('codex-cli') },
120
124
  grok: { enabled: providers.includes('grok') || selectedIDEs.includes('grok-cli') },
121
125
  },
126
+ integrations: {
127
+ clickup: {
128
+ required: config.installationMode === 'internal',
129
+ connected_harnesses: config.clickupPreflight?.connected_providers || [],
130
+ durable_projection_queue: '.chati/v2/tracking/pending-projections',
131
+ },
132
+ },
122
133
  };
123
134
 
124
135
  // Feature flags (Intelligence Upgrade + Article XXI Agent Teams)
@@ -228,7 +239,7 @@ description: >
228
239
  Use the shell in the current project directory and run:
229
240
 
230
241
  \`\`\`bash
231
- node .chati.dev/orchestrator/chati-router.js
242
+ node .chati.dev/orchestrator/chati-router.js --harness codex
232
243
  \`\`\`
233
244
 
234
245
  Do not infer provider authentication, session state, model availability, or the
@@ -239,7 +250,8 @@ routed spawn command returns a provider authentication error.
239
250
  Parse the JSON output before responding:
240
251
 
241
252
  - If \`ok\` is \`false\`, report the exact \`error\` and relevant detail, then stop.
242
- - If \`ok\` is \`true\`, use \`session.language\` for every response.
253
+ - If \`ok\` is \`true\`, use \`session.language\` for every visible response,
254
+ preface, progress message, question and error, starting with the first response.
243
255
  - Follow the returned \`action\` in the adapted orchestrator.
244
256
  - When the action includes \`pipeline.spawn_command\`, execute that exact command.
245
257
  Never replace it with an inferred provider check or a manually constructed command.
@@ -256,8 +268,9 @@ Parse the JSON output before responding:
256
268
 
257
269
  ## CRITICAL: Language Override
258
270
 
259
- Read \`.chati/session.yaml\` field \`language\` BEFORE anything else.
260
- ALL responses MUST be in this language. This overrides any global setting.
271
+ Use the canonical \`session.language\` returned by the router. ALL visible
272
+ responses MUST use this language from the first response onward. This
273
+ overrides any global setting.
261
274
 
262
275
  | Value | Language |
263
276
  |-------|----------|
@@ -296,7 +309,7 @@ export function generateGeminiRouter(options = {}) {
296
309
  prompt = """
297
310
  CRITICAL — Language Override:
298
311
  Read .chati/session.yaml field "language" BEFORE anything else.
299
- ALL responses MUST be in this language (en, pt, es, fr).
312
+ Every visible response, including the first response, MUST be in this language (en, pt, es, fr).
300
313
  If session.yaml does not exist or has no language field, default to English.
301
314
 
302
315
  The orchestrator and all agent files are pre-configured for Gemini CLI.
@@ -323,13 +336,23 @@ export function generateGrokRouter(options = {}) {
323
336
  const orchPath = options.orchestratorPath || '.chati.dev/orchestrator/chati.md';
324
337
  return `# Chati.dev Orchestrator
325
338
 
326
- Read \`.chati/session.yaml\` and use its \`language\` for every response.
327
- If the field is absent, default to English.
339
+ ## STEP 0: Run the deterministic router first
340
+
341
+ Before reading mutable session files or responding, run:
342
+
343
+ \`\`\`bash
344
+ node .chati.dev/orchestrator/chati-router.js --harness grok
345
+ \`\`\`
346
+
347
+ Parse the JSON output. It is the source of truth for session state, task
348
+ routing and provider authentication. Use the returned \`session.language\` for
349
+ every visible response, preface, progress message, question and error,
350
+ starting with the first response. If absent, default to English.
328
351
 
329
352
  The orchestrator and agent files are pre-configured for Grok CLI. Read and
330
353
  execute the full orchestrator at \`${orchPath}\`.
331
354
 
332
- Load this context:
355
+ Only after the router succeeds, load this context:
333
356
  - \`.chati/session.yaml\`
334
357
  - \`AGENTS.md\`
335
358
  - \`.grok/session-lock.md\`
@@ -33,6 +33,8 @@ export const EventType = {
33
33
  PROVIDER_SELECTED: 'provider_selected',
34
34
  SESSION_STARTED: 'session_started',
35
35
  SESSION_ENDED: 'session_ended',
36
+ HARNESS_ACTIVATED: 'harness_activated',
37
+ AGENT_WAITING_FOR_INPUT: 'agent_waiting_for_input',
36
38
  CORRECTION_TRIGGERED: 'correction_triggered',
37
39
  CORRECTION_ESCALATED: 'correction_escalated',
38
40
  };
@@ -84,7 +86,7 @@ export function recordEvent(projectDir, type, agent, data = {}) {
84
86
  // Prevents duplicate AGENT_ACTIVATED entries when handleAdvance chains into
85
87
  // handleNext while the caller also invokes next separately.
86
88
  const last = timeline[timeline.length - 1];
87
- if (last && last.type === type && last.agent === agent) {
89
+ if (type === EventType.AGENT_ACTIVATED && last && last.type === type && last.agent === agent) {
88
90
  return last;
89
91
  }
90
92
 
@@ -799,6 +799,35 @@ function removeSessionLock(projectDir, resumeMsg) {
799
799
  async function handleNext(projectDir) {
800
800
  const result = await _handleNextInner(projectDir);
801
801
 
802
+ // The canonical session owns the active task. Native harness lock files are
803
+ // projections only, so a CLI switch cannot create a second execution.
804
+ // Persist only actions that actually dispatch an agent. Surface holds and
805
+ // RAIL handoffs are control-plane responses, not running pipeline agents.
806
+ const dispatchActions = new Set([
807
+ 'spawn_routed_interactive',
808
+ 'spawn_autonomous',
809
+ 'spawn_parallel',
810
+ 'spawn_team',
811
+ ]);
812
+ if (result?.agent && dispatchActions.has(result.action)) {
813
+ const binding = result.model_info ? {
814
+ provider: result.model_info.provider || null,
815
+ provider_id: result.model_info.provider_id || null,
816
+ model: result.model_info.model || null,
817
+ reasoning_configuration: result.model_info.reasoning_configuration || null,
818
+ catalog_snapshot_ref: result.model_info.catalog_snapshot_ref || null,
819
+ } : null;
820
+ const persisted = updateSession(projectDir, {
821
+ current_agent: result.agent,
822
+ ...(binding ? { current_execution_binding: binding } : {}),
823
+ agents: { [result.agent]: { status: 'in_progress' } },
824
+ });
825
+ if (!persisted.saved) {
826
+ return errorResult(`Could not persist canonical agent state: ${persisted.error}`, 'SESSION_WRITE_FAILED');
827
+ }
828
+ recordEvent(projectDir, EventType.AGENT_ACTIVATED, result.agent, { action: result.action, phase: result.phase });
829
+ }
830
+
802
831
  // Fase 8: attach state-at-a-glance card to every non-error response.
803
832
  // The card renders as a JSON string field; the terminal / orchestrator
804
833
  // prompt can console.log it verbatim. Silent on setup/error responses
@@ -815,7 +844,7 @@ async function handleNext(projectDir) {
815
844
  // Centralized session lock update: applies to ALL return paths.
816
845
  // Activates lock when an agent is returned, deactivates on completion.
817
846
  if (result && result.agent && result.action !== 'error' && result.action !== 'setup') {
818
- const activating = ['activate_interactive', 'spawn_routed_interactive', 'spawn_autonomous', 'spawn_team', 'spawn_parallel'];
847
+ const activating = ['activate_interactive', 'spawn_routed_interactive', 'spawn_autonomous', 'spawn_team', 'spawn_parallel', 'resume_pending_interaction'];
819
848
  if (activating.includes(result.action)) {
820
849
  try {
821
850
  const runtimePipeline = getRuntimeAgentPipeline(projectDir, result.session);
@@ -852,6 +881,40 @@ async function _handleNextInner(projectDir) {
852
881
 
853
882
  const installationV2 = loadRuntimeInstallationV2(projectDir);
854
883
  const runtimePipeline = getRuntimeAgentPipeline(projectDir, session);
884
+
885
+ const pending = session.pending_interaction;
886
+ if (pending?.status === 'waiting_for_user' && pending.agent && pending.question) {
887
+ const binding = pending.execution_binding || session.current_execution_binding || resolveAgentModel(pending.agent, projectDir);
888
+ const agentDef = getAgentDefinition(pending.agent);
889
+ return {
890
+ action: 'resume_pending_interaction',
891
+ agent: pending.agent,
892
+ agent_file: getAgentFile(pending.agent, projectDir) || null,
893
+ phase: agentDef?.phase || session.mode,
894
+ question: pending.question,
895
+ relay_cycles: pending.relay_cycles || 1,
896
+ spawn_command: buildSpawnCommand(
897
+ pending.agent,
898
+ projectDir,
899
+ session.last_handoff || 'none',
900
+ binding.provider,
901
+ 600000,
902
+ binding.model,
903
+ Boolean(installationV2),
904
+ binding,
905
+ ),
906
+ model_info: binding,
907
+ redispatched: false,
908
+ pipeline_progress: getPipelineProgress(sessionToPipelineState(session, projectDir)),
909
+ session: {
910
+ language: session.language,
911
+ project_type: session.project_type || session.project?.type,
912
+ execution_mode: session.execution_mode,
913
+ active_harness: session.active_harness,
914
+ },
915
+ status_summary: `Waiting for the user's answer to ${pending.agent}.`,
916
+ };
917
+ }
855
918
  if (installationV2 && session.rail_handoff_id) {
856
919
  const completedPlanning = runtimePipeline.length;
857
920
  return {
@@ -2405,8 +2468,8 @@ async function handleStatus(projectDir) {
2405
2468
  // bracket prism-engine last computed. If the session predates v1.2
2406
2469
  // (no context_* fields yet), the block falls back to estimateContextBracket
2407
2470
  // so the existing response shape stays stable for consumers.
2408
- const activeModel = session.active_model || null;
2409
- const activeProvider = session.active_provider || (session.providers_enabled || [])[0] || null;
2471
+ const activeModel = session.current_execution_binding?.model || session.active_model || null;
2472
+ const activeProvider = session.current_execution_binding?.provider || session.active_provider || null;
2410
2473
  const windowTokens = session.context_window_tokens != null
2411
2474
  ? session.context_window_tokens
2412
2475
  : resolveContextLimit(activeModel, activeProvider);
@@ -2440,9 +2503,32 @@ async function handleStatus(projectDir) {
2440
2503
  },
2441
2504
  user_level: session.user_level || 'auto',
2442
2505
  execution_mode: session.execution_mode || 'interactive',
2506
+ active_harness: session.active_harness || null,
2507
+ current_execution_binding: session.current_execution_binding || null,
2508
+ pending_interaction: session.pending_interaction || null,
2443
2509
  };
2444
2510
  }
2445
2511
 
2512
+ async function handleActivateHarness(projectDir, args) {
2513
+ const harness = args.harness;
2514
+ if (!['claude', 'codex', 'grok'].includes(harness)) {
2515
+ return errorResult('Harness must be claude, codex, or grok.', 'INVALID_HARNESS');
2516
+ }
2517
+ const { loaded, session } = loadSession(projectDir);
2518
+ if (!loaded || !session) return errorResult('No session found', 'NO_SESSION');
2519
+ const previous = session.active_harness || null;
2520
+ const wasActive = session.session_status === 'active';
2521
+ const saved = updateSession(projectDir, { active_harness: harness, session_status: 'active' });
2522
+ if (!saved.saved) return errorResult(saved.error, 'SESSION_WRITE_FAILED');
2523
+ if (previous !== harness) {
2524
+ recordEvent(projectDir, EventType.HARNESS_ACTIVATED, 'orchestrator', { harness, previous_harness: previous });
2525
+ }
2526
+ if (previous !== harness || !wasActive) {
2527
+ recordEvent(projectDir, EventType.SESSION_STARTED, session.current_agent || 'orchestrator', { harness, resumed: Boolean(previous) });
2528
+ }
2529
+ return { activated: true, harness, previous_harness: previous, language: session.language || 'en' };
2530
+ }
2531
+
2446
2532
  async function handleDeviation(projectDir, args) {
2447
2533
  const type = args.type;
2448
2534
  const target = args.target || null;
@@ -2525,8 +2611,8 @@ async function handleExit(projectDir, args) {
2525
2611
  }
2526
2612
 
2527
2613
  // Save current state
2528
- recordEvent(projectDir, EventType.SESSION_ENDED, session.current_agent || 'orchestrator', { mode: session.mode });
2529
- await updateSession(projectDir, {});
2614
+ recordEvent(projectDir, EventType.SESSION_ENDED, session.current_agent || 'orchestrator', { mode: session.mode, harness: session.active_harness || null });
2615
+ await updateSession(projectDir, { session_status: 'paused' });
2530
2616
 
2531
2617
  // Release session ownership
2532
2618
  const userId = args['user-id'] || process.env.USER || 'default';
@@ -2536,9 +2622,14 @@ async function handleExit(projectDir, args) {
2536
2622
 
2537
2623
  // Remove lock from CLAUDE.local.md
2538
2624
  const lang = session.language || 'en';
2539
- const resumeMsg = RESUME_MESSAGES[lang] || RESUME_MESSAGES.en;
2625
+ const genericResumeMsg = RESUME_MESSAGES[lang] || RESUME_MESSAGES.en;
2626
+ const resumeMsg = session.active_harness === 'codex'
2627
+ ? genericResumeMsg.replaceAll('/chati', '$chati')
2628
+ : genericResumeMsg;
2540
2629
  try {
2541
- removeSessionLock(projectDir, resumeMsg);
2630
+ // Each lock target translates the generic command to its native syntax.
2631
+ // The returned message below remains native to the harness that exited.
2632
+ removeSessionLock(projectDir, genericResumeMsg);
2542
2633
  } catch { /* expected: lock file may not exist */ }
2543
2634
 
2544
2635
  return {
@@ -3561,6 +3652,9 @@ export async function runOrchestrate(subCommand, argv, projectDir) {
3561
3652
  case 'exit':
3562
3653
  output(await handleExit(projectDir, args));
3563
3654
  break;
3655
+ case 'activate-harness':
3656
+ output(await handleActivateHarness(projectDir, args));
3657
+ break;
3564
3658
  case 'providers':
3565
3659
  output(await handleProviders(projectDir));
3566
3660
  break;
@@ -29,6 +29,7 @@
29
29
  import { existsSync, readFileSync, readdirSync } from 'fs';
30
30
  import { join } from 'path';
31
31
  import { readLicenseStatus } from '../license/wait.js';
32
+ import * as yaml from 'js-yaml';
32
33
 
33
34
  export const SEVERITY = Object.freeze({
34
35
  BLOCK: 'BLOCK',
@@ -202,10 +203,13 @@ export function checkModel({ projectDir }) {
202
203
  };
203
204
  }
204
205
  try {
205
- const raw = readFileSync(sessionPath, 'utf-8');
206
- const match = raw.match(/^\s*active_model:\s*(.+)$/m);
207
- const value = match ? match[1].trim().replace(/^["']|["']$/g, '') : '';
208
- if (!value || value === 'null' || value === '~' || value === '') {
206
+ const session = yaml.load(readFileSync(sessionPath, 'utf-8')) || {};
207
+ const value = session.active_model;
208
+ const routedModel = session.current_execution_binding?.model;
209
+ if (!value && routedModel) {
210
+ return { id: 'model', severity: SEVERITY.WARN, status: 'pass', message: `Routed execution model: ${routedModel}` };
211
+ }
212
+ if (!value) {
209
213
  return {
210
214
  id: 'model',
211
215
  severity: SEVERITY.WARN,
@@ -222,6 +226,70 @@ export function checkModel({ projectDir }) {
222
226
  }
223
227
  }
224
228
 
229
+ /** Compare canonical session state with native harness projections. */
230
+ export function checkRuntimeState({ projectDir }) {
231
+ const sessionPath = join(projectDir, '.chati', 'session.yaml');
232
+ if (!existsSync(sessionPath)) {
233
+ return { id: 'runtime-state', severity: SEVERITY.WARN, status: 'pass', message: 'Session not initialized.' };
234
+ }
235
+ try {
236
+ const session = yaml.load(readFileSync(sessionPath, 'utf-8')) || {};
237
+ const lockPaths = [
238
+ join(projectDir, 'CLAUDE.local.md'),
239
+ join(projectDir, 'AGENTS.override.md'),
240
+ join(projectDir, '.grok', 'session-lock.md'),
241
+ join(projectDir, '.gemini', 'session-lock.md'),
242
+ ];
243
+ const projectedAgents = [];
244
+ for (const lockPath of lockPaths) {
245
+ if (!existsSync(lockPath)) continue;
246
+ const raw = readFileSync(lockPath, 'utf-8');
247
+ if (!/(?:Session Lock:\s*ACTIVE|\*\*Status:\s*ACTIVE\*\*)/i.test(raw)) continue;
248
+ const match = raw.match(/\*\*Agent\*\*:\s*([^\n]+)/i);
249
+ if (match) projectedAgents.push(match[1].trim());
250
+ }
251
+ const mismatches = [...new Set(projectedAgents)].filter(agent => agent !== session.current_agent);
252
+ if (mismatches.length > 0) {
253
+ return {
254
+ id: 'runtime-state', severity: SEVERITY.WARN, status: 'fail',
255
+ message: `Canonical current_agent (${session.current_agent || 'none'}) disagrees with native lock projection(s): ${mismatches.join(', ')}`,
256
+ fix: 'Run the deterministic router once from the active CLI to refresh native projections.',
257
+ };
258
+ }
259
+ return { id: 'runtime-state', severity: SEVERITY.WARN, status: 'pass', message: 'Canonical session and native lock projections agree.' };
260
+ } catch (err) {
261
+ return { id: 'runtime-state', severity: SEVERITY.WARN, status: 'fail', message: `Cannot inspect runtime state: ${err.message}` };
262
+ }
263
+ }
264
+
265
+ /** Surface durable ClickUp projection intents awaiting synchronization. */
266
+ export function checkClickUpTracking({ projectDir }) {
267
+ const pendingDir = join(projectDir, '.chati', 'v2', 'tracking', 'pending-projections');
268
+ try {
269
+ const durableIntents = existsSync(pendingDir)
270
+ ? readdirSync(pendingDir).filter(name => name.endsWith('.json')).length
271
+ : 0;
272
+ const outboxPath = join(projectDir, '.chati', 'v2', 'tracking', 'clickup-outbox.json');
273
+ let outboxPending = 0;
274
+ if (existsSync(outboxPath)) {
275
+ const outbox = JSON.parse(readFileSync(outboxPath, 'utf8'));
276
+ outboxPending = (outbox.projections || []).filter(projection =>
277
+ ['pending', 'retryable_failure', 'sent_unconfirmed'].includes(projection.delivery_state)
278
+ ).length;
279
+ }
280
+ if (durableIntents === 0 && outboxPending === 0) {
281
+ return { id: 'clickup-tracking', severity: SEVERITY.WARN, status: 'pass', message: 'No pending ClickUp projections.' };
282
+ }
283
+ return {
284
+ id: 'clickup-tracking', severity: SEVERITY.WARN, status: 'fail',
285
+ message: `${durableIntents} pending reconstruction intent(s) and ${outboxPending} pending ClickUp delivery projection(s) await synchronization.`,
286
+ fix: 'Restore ClickUp connectivity. The durable queue can then be replayed without losing local progress.',
287
+ };
288
+ } catch (err) {
289
+ return { id: 'clickup-tracking', severity: SEVERITY.WARN, status: 'fail', message: `Cannot inspect ClickUp projection queue: ${err.message}` };
290
+ }
291
+ }
292
+
225
293
  /**
226
294
  * Node version gate. Framework assumes Node 22+ for modern ESM, fetch,
227
295
  * globbing. Earlier versions break test runs and hook spawns.
@@ -342,6 +410,8 @@ export function runDoctor(opts) {
342
410
  checkSession({ projectDir: opts.projectDir }),
343
411
  checkHooks({ projectDir: opts.projectDir }),
344
412
  checkModel({ projectDir: opts.projectDir }),
413
+ checkRuntimeState({ projectDir: opts.projectDir }),
414
+ checkClickUpTracking({ projectDir: opts.projectDir }),
345
415
  checkNode(),
346
416
  checkConstitution({ projectDir: opts.projectDir }),
347
417
  checkFailures({ projectDir: opts.projectDir }),
@@ -106,6 +106,12 @@ const DEFAULT_SESSION = {
106
106
  // context_last_bracket: last computed bracket — telemetry + /chati status.
107
107
  active_model: null,
108
108
  active_provider: null,
109
+ // The harness is the CLI/UI currently orchestrating the session. It is not
110
+ // the provider selected for the routed task.
111
+ active_harness: null,
112
+ session_status: 'inactive',
113
+ current_execution_binding: null,
114
+ pending_interaction: null,
109
115
  context_tokens_used: 0,
110
116
  context_window_tokens: null,
111
117
  context_last_bracket: null,
@@ -237,8 +243,24 @@ export function migrateSession(session) {
237
243
  });
238
244
  }
239
245
 
246
+ // Seed additive runtime fields even when the schema version is current.
247
+ // Patch releases can therefore repair installed v1.2 sessions safely.
248
+ let seededRuntimeFields = false;
249
+ const runtimeDefaults = {
250
+ active_harness: null,
251
+ session_status: 'inactive',
252
+ current_execution_binding: null,
253
+ pending_interaction: null,
254
+ };
255
+ for (const [key, value] of Object.entries(runtimeDefaults)) {
256
+ if (!(key in session)) {
257
+ session[key] = value;
258
+ seededRuntimeFields = true;
259
+ }
260
+ }
261
+
240
262
  if (session.schema_version === CURRENT_SCHEMA_VERSION) {
241
- return { migrated: normalizedLegacySelections, fromVersion: session.schema_version, toVersion: CURRENT_SCHEMA_VERSION };
263
+ return { migrated: normalizedLegacySelections || seededRuntimeFields, fromVersion: session.schema_version, toVersion: CURRENT_SCHEMA_VERSION };
242
264
  }
243
265
 
244
266
  // v0 (no schema_version) → v1.0 → v1.1 → v1.2
@@ -492,6 +514,67 @@ export function recordModelSelection(projectDir, selection) {
492
514
  });
493
515
  }
494
516
 
517
+ /** Persist an interactive question so another harness resumes it verbatim. */
518
+ export function recordPendingInteraction(projectDir, interaction) {
519
+ const sessionProjectDir = findSessionProjectDir(projectDir);
520
+ if (!sessionProjectDir) return { saved: false, error: 'Session file not found in working directory or its parents' };
521
+ if (!interaction?.agent || !interaction?.question) {
522
+ return { saved: false, error: 'Pending interaction requires agent and question' };
523
+ }
524
+ return mutateSession(sessionProjectDir, (session) => {
525
+ const previous = session.pending_interaction;
526
+ const taskId = interaction.taskId || `${interaction.agent}-task`;
527
+ const binding = {
528
+ provider: interaction.provider || null,
529
+ provider_id: interaction.providerId || null,
530
+ model: interaction.model || null,
531
+ reasoning_configuration: interaction.reasoningConfiguration || null,
532
+ catalog_snapshot_ref: interaction.catalogSnapshotRef || null,
533
+ };
534
+ const sameRelay = previous?.agent === interaction.agent && previous?.task_id === taskId;
535
+ const now = interaction.timestamp || new Date().toISOString();
536
+ session.current_agent = interaction.agent;
537
+ session.current_execution_binding = binding;
538
+ session.agents = session.agents || {};
539
+ session.agents[interaction.agent] = {
540
+ ...(session.agents[interaction.agent] || {}),
541
+ status: 'waiting_for_user',
542
+ };
543
+ session.pending_interaction = {
544
+ status: 'waiting_for_user',
545
+ agent: interaction.agent,
546
+ task_id: taskId,
547
+ language: session.language || 'en',
548
+ question: interaction.question,
549
+ relay_cycles: sameRelay ? (previous.relay_cycles || 1) + 1 : 1,
550
+ execution_binding: binding,
551
+ created_at: sameRelay ? previous.created_at : now,
552
+ updated_at: now,
553
+ };
554
+ return session;
555
+ });
556
+ }
557
+
558
+ /** Clear a pending interaction only when the caller owns that relay. */
559
+ export function clearPendingInteraction(projectDir, match = {}) {
560
+ const sessionProjectDir = findSessionProjectDir(projectDir);
561
+ if (!sessionProjectDir) return { saved: false, cleared: false, error: 'Session file not found in working directory or its parents' };
562
+ let cleared = false;
563
+ const result = mutateSession(sessionProjectDir, (session) => {
564
+ const pending = session.pending_interaction;
565
+ if (!pending) return session;
566
+ if (match.agent && pending.agent !== match.agent) return session;
567
+ if (match.taskId && pending.task_id !== match.taskId) return session;
568
+ session.pending_interaction = null;
569
+ if (session.agents?.[pending.agent]?.status === 'waiting_for_user') {
570
+ session.agents[pending.agent].status = 'in_progress';
571
+ }
572
+ cleared = true;
573
+ return session;
574
+ });
575
+ return { ...result, cleared };
576
+ }
577
+
495
578
  /**
496
579
  * Record a mode transition in session history.
497
580
  * @param {string} projectDir
@@ -392,6 +392,7 @@ function buildSessionSection(config, resolvedModelInfo = {}) {
392
392
  `- **Type**: ${project.type || 'greenfield'}`,
393
393
  `- **Mode**: ${project.state || 'discover'}`,
394
394
  `- **Language**: ${state.language || 'en'} (interaction) / English (artifacts)`,
395
+ `- **Language Contract**: Every visible interaction, including your first response, preface, progress update, question, error, pause and resumption, MUST use ${state.language || 'en'}.`,
395
396
  `- **User Level**: ${state.user_level || 'auto'}`,
396
397
  `- **Execution Mode**: ${state.execution_mode || 'autonomous'}`,
397
398
  `- **Your Agent**: ${config.agent}`,
@@ -23,6 +23,8 @@ import { getRateLimiter } from './rate-limiter.js';
23
23
  import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
24
24
  import { sendEvents } from '../telemetry/sender.js';
25
25
  import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetry/config.js';
26
+ import { recordPendingInteraction, clearPendingInteraction } from '../orchestrator/session-manager.js';
27
+ import { recordEvent, EventType } from '../intelligence/timeline.js';
26
28
 
27
29
  // ---------------------------------------------------------------------------
28
30
  // CLI argument parsing (no external deps)
@@ -111,6 +113,11 @@ async function main() {
111
113
  }
112
114
  const readiness = checkProviderReadiness(spawnProvider, { workingDir: projectDir });
113
115
  if (!readiness.ready) {
116
+ const relayState = clearPendingForFailedRelay(projectDir, args);
117
+ if (!relayState.saved) {
118
+ outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: readiness.code });
119
+ process.exit(1);
120
+ }
114
121
  outputResult({ status: 'error', error: readiness.detail, code: readiness.code, provider: spawnProvider });
115
122
  process.exit(1);
116
123
  }
@@ -154,6 +161,11 @@ async function main() {
154
161
  },
155
162
  });
156
163
  } catch (err) {
164
+ const relayState = clearPendingForFailedRelay(projectDir, args);
165
+ if (!relayState.saved) {
166
+ outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: err.message });
167
+ process.exit(1);
168
+ }
157
169
  outputError(`Failed to spawn terminal: ${err.message}`);
158
170
  process.exit(1);
159
171
  }
@@ -213,8 +225,14 @@ async function main() {
213
225
 
214
226
  // Parse the handoff from stdout
215
227
  const parsed = parseAgentOutput(stdout);
228
+ const recovered = recoverInteractiveHandoff(args.agent, stdout, handle.exitCode);
216
229
 
217
230
  if (handle.exitCode !== 0 && /not logged in|not authenticated/i.test(providerOutput)) {
231
+ const relayState = clearPendingForFailedRelay(projectDir, args);
232
+ if (!relayState.saved) {
233
+ outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: 'PROVIDER_AUTH_STATE_MISMATCH' });
234
+ process.exit(1);
235
+ }
218
236
  outputResult({
219
237
  status: 'error',
220
238
  code: 'PROVIDER_AUTH_STATE_MISMATCH',
@@ -227,6 +245,11 @@ async function main() {
227
245
  }
228
246
 
229
247
  if (parsed.found && parsed.valid) {
248
+ const persistence = persistInteractiveState(projectDir, args, parsed.handoff, promptResult.model, spawnProvider);
249
+ if (!persistence.saved) {
250
+ outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
251
+ process.exit(1);
252
+ }
230
253
  outputResult({
231
254
  status: parsed.handoff.status,
232
255
  agent: args.agent,
@@ -238,17 +261,22 @@ async function main() {
238
261
  elapsed,
239
262
  costEstimate,
240
263
  });
241
- } else if (recoverInteractiveHandoff(args.agent, stdout, handle.exitCode)) {
264
+ } else if (recovered) {
242
265
  // Interactive discovery models sometimes answer with the user-facing
243
266
  // question but omit the machine block. Preserve the exact question while
244
267
  // restoring the deterministic relay contract for the parent orchestrator.
268
+ const persistence = persistInteractiveState(projectDir, args, recovered, promptResult.model, spawnProvider);
269
+ if (!persistence.saved) {
270
+ outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
271
+ process.exit(1);
272
+ }
245
273
  outputResult({
246
274
  status: 'needs_input',
247
275
  agent: args.agent,
248
276
  model: promptResult.model,
249
277
  provider: spawnProvider,
250
278
  exitCode: handle.exitCode,
251
- handoff: recoverInteractiveHandoff(args.agent, stdout, handle.exitCode),
279
+ handoff: recovered,
252
280
  contractRecovery: 'interactive_output_wrapped',
253
281
  elapsed,
254
282
  costEstimate,
@@ -256,6 +284,11 @@ async function main() {
256
284
  } else {
257
285
  // A non-interactive agent that omits the handoff violated the execution
258
286
  // contract and cannot be treated as successful or partially complete.
287
+ const relayState = clearPendingForFailedRelay(projectDir, args);
288
+ if (!relayState.saved) {
289
+ outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: parsed.found ? 'INVALID_HANDOFF' : 'MISSING_HANDOFF' });
290
+ process.exit(1);
291
+ }
259
292
  outputResult({
260
293
  status: 'error',
261
294
  code: parsed.found ? 'INVALID_HANDOFF' : 'MISSING_HANDOFF',
@@ -275,6 +308,31 @@ async function main() {
275
308
  process.exit(handle.exitCode === 0 ? 0 : 1);
276
309
  }
277
310
 
311
+ function persistInteractiveState(projectDir, args, handoff, model, provider) {
312
+ if (handoff?.status === 'needs_input' && handoff.needs_input_question) {
313
+ const result = recordPendingInteraction(projectDir, {
314
+ agent: args.agent,
315
+ taskId: args['task-id'],
316
+ question: handoff.needs_input_question,
317
+ provider,
318
+ providerId: args['provider-id'] || null,
319
+ model,
320
+ reasoningConfiguration: args['reasoning-configuration'] || null,
321
+ catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
322
+ });
323
+ recordEvent(projectDir, EventType.AGENT_WAITING_FOR_INPUT, args.agent, {
324
+ task_id: args['task-id'],
325
+ });
326
+ return result;
327
+ }
328
+ return clearPendingInteraction(projectDir, { agent: args.agent, taskId: args['task-id'] });
329
+ }
330
+
331
+ export function clearPendingForFailedRelay(projectDir, args) {
332
+ if (!args?.['additional-context']) return { saved: true, cleared: false };
333
+ return clearPendingInteraction(projectDir, { agent: args.agent, taskId: args['task-id'] });
334
+ }
335
+
278
336
  // ---------------------------------------------------------------------------
279
337
  // Helpers
280
338
  // ---------------------------------------------------------------------------
@@ -171,6 +171,10 @@ export const SESSION_SCHEMA = {
171
171
  language: { type: 'string', required: true, default: 'en' },
172
172
  pipeline_phase: { type: 'string', enum: ['discover', 'plan', 'rail', 'release', 'build', 'validate', 'deploy', 'completed'] },
173
173
  current_agent: { type: 'string' },
174
+ active_harness: { type: 'string', enum: ['claude', 'codex', 'grok'] },
175
+ session_status: { type: 'string', enum: ['inactive', 'active', 'paused', 'completed'] },
176
+ current_execution_binding: { type: 'object' },
177
+ pending_interaction: { type: 'object' },
174
178
  governance_mode: { type: 'string', enum: ['planning', 'build', 'deploy'] },
175
179
  execution_mode: { type: 'string', enum: ['interactive', 'autonomous'] },
176
180
  context_bracket: { type: 'string', enum: ['FRESH', 'MODERATE', 'DEPLETED', 'CRITICAL'] },
@@ -191,6 +195,7 @@ export const CONFIG_SCHEMA = {
191
195
  language: { type: 'string', default: 'en' },
192
196
  ides: { type: 'array' },
193
197
  providers: { type: 'object' },
198
+ integrations: { type: 'object' },
194
199
  agent_overrides: { type: 'object' },
195
200
  },
196
201
  };
@@ -13,6 +13,17 @@ export function createSpinner(text) {
13
13
  });
14
14
  }
15
15
 
16
+ /** Run one automatic operation with a spinner that always stops. */
17
+ export async function withSpinner(text, operation, { spinnerFactory = createSpinner } = {}) {
18
+ const spinner = spinnerFactory(text);
19
+ spinner.start();
20
+ try {
21
+ return await operation();
22
+ } finally {
23
+ spinner.stop();
24
+ }
25
+ }
26
+
16
27
  /**
17
28
  * Show installation progress step
18
29
  */
@@ -106,6 +106,9 @@ const FALLBACK_EN = {
106
106
  system_prerequisites_failed: 'System prerequisites not met:',
107
107
  fix_prerequisites: 'Fix the issues above and try again.',
108
108
  system_check: 'System check:',
109
+ system_checking: 'Checking system requirements...',
110
+ catalog_resolving: 'Loading the signed model catalog...',
111
+ internal_authorizing: 'Verifying access to Internal mode...',
109
112
  created_provider_claude: 'Created .claude/commands/ (thin router)',
110
113
  created_provider_gemini: 'Created .gemini/commands/ (TOML command)',
111
114
  created_provider_codex: 'Created .agents/skills/chati/ (Codex skill)',
@@ -120,6 +123,7 @@ const FALLBACK_EN = {
120
123
  license_placeholder: 'CHATI-XXXX-XXXX-XXXX',
121
124
  license_skipped: 'Skipped. Run: npx chati-dev activate --key=YOUR-KEY when ready.',
122
125
  license_activating: 'Activating license...',
126
+ license_verifying: 'Verifying saved license...',
123
127
  license_activated: 'License activated! Plan: {plan}, {days} day(s) remaining.',
124
128
  license_activation_failed: 'Activation failed: {error}',
125
129
  license_retry: 'Run: npx chati-dev activate --key=YOUR-KEY to try again.',
@@ -4,7 +4,7 @@ import { join, dirname, basename } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { logBanner } from '../utils/logger.js';
6
6
  import { WIZARD_BACK, stepLanguage, stepProjectType, stepProviderSelection, stepInstallationMode, stepConfirmation, stepTermsOfUse } from './questions.js';
7
- import { createSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
7
+ import { createSpinner, withSpinner, showStep, showValidation, showQuickStart } from './feedback.js';
8
8
  import { installFramework } from '../installer/core.js';
9
9
  import { INSTALLATION_ARTIFACT_PATH, dryRunV2, installV2, reconfigureV2 } from '../installer-v2/index.js';
10
10
  import { buildWizardV2InstallationInput } from '../installer-v2/wizard-installation.js';
@@ -48,7 +48,7 @@ export async function runWizard(targetDir, options = {}) {
48
48
 
49
49
  // Preflight: validate system prerequisites (Node >= 20, npm, Git, Playwright)
50
50
  const { runPreflightCheck } = await import('../installer/preflight.js');
51
- const preflight = await runPreflightCheck();
51
+ const preflight = await withSpinner(t('installer.system_checking'), () => runPreflightCheck());
52
52
  const preflightFmt = preflight.checks.map(c => {
53
53
  const icon = c.status === 'pass' ? '✓' : c.status === 'warn' ? '⚠' : '✗';
54
54
  return ` ${icon} ${c.message}`;
@@ -128,9 +128,13 @@ export async function runWizard(targetDir, options = {}) {
128
128
  continue;
129
129
  }
130
130
  selectedProviders = value;
131
- catalogResolution ??= options.signedCapabilityCatalogEnvelope
132
- ? { source: 'provided-signed', catalog: verifySignedCapabilityCatalog(options.signedCapabilityCatalogEnvelope, { publicKeyPem: options.catalogPublicKeyPem }) }
133
- : await resolveCapabilityCatalog({ projectDir: targetDir, catalogUrl: options.catalogUrl, fetchImpl: options.catalogFetch });
131
+ if (!catalogResolution) {
132
+ catalogResolution = await withSpinner(t('installer.catalog_resolving'), async () => (
133
+ options.signedCapabilityCatalogEnvelope
134
+ ? { source: 'provided-signed', catalog: verifySignedCapabilityCatalog(options.signedCapabilityCatalogEnvelope, { publicKeyPem: options.catalogPublicKeyPem }) }
135
+ : resolveCapabilityCatalog({ projectDir: targetDir, catalogUrl: options.catalogUrl, fetchImpl: options.catalogFetch })
136
+ ));
137
+ }
134
138
  // Provider selection defines the permitted routing pool. Model discovery
135
139
  // and task-specific choice stay inside the control plane.
136
140
  modelSelections = options.modelSelections;
@@ -149,7 +153,10 @@ export async function runWizard(targetDir, options = {}) {
149
153
  installationMode = value;
150
154
  if (installationMode === 'internal') {
151
155
  try {
152
- await requireLicenseEntitlement('internal', { validate: options.licenseValidator });
156
+ await withSpinner(
157
+ t('installer.internal_authorizing'),
158
+ () => requireLicenseEntitlement('internal', { validate: options.licenseValidator }),
159
+ );
153
160
  } catch (error) {
154
161
  if (options.installationMode !== undefined) throw error;
155
162
  p.log.error(error.message);
@@ -328,7 +335,10 @@ async function runLicenseActivationStep() {
328
335
  const existingKey = getLicenseKey();
329
336
  if (existingKey) {
330
337
  try {
331
- const status = await ensureCurrentMachineLicense({ key: existingKey });
338
+ const status = await withSpinner(
339
+ t('installer.license_verifying'),
340
+ () => ensureCurrentMachineLicense({ key: existingKey }),
341
+ );
332
342
  if (status.status === 'VALID') return;
333
343
  p.log.warn(t('installer.license_saved_invalid', { reason: status.reason || status.status }));
334
344
  } catch (error) {