chati-dev 4.5.6 → 4.5.7
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.
- package/framework/config.yaml +2 -2
- package/framework/context/root.md +1 -1
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/manifest.json +7 -7
- package/framework/manifest.sig +1 -1
- package/framework/schemas/session.schema.json +26 -9
- package/package.json +2 -1
- package/src/installer/core.js +4 -0
- package/src/orchestrator/cli.js +9 -6
- package/src/orchestrator/session-manager.js +134 -96
- package/src/terminal/adapters/claude-adapter.js +9 -2
- package/src/terminal/adapters/codex-adapter.js +9 -2
- package/src/terminal/adapters/gemini-adapter.js +5 -2
- package/src/terminal/adapters/grok-adapter.js +13 -3
- package/src/terminal/cli-registry.js +5 -0
- package/src/terminal/run-agent.js +5 -0
- package/src/terminal/run-parallel.js +7 -0
- package/src/terminal/spawner.js +49 -10
package/framework/config.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Chati.dev System Context
|
|
2
2
|
|
|
3
3
|
## Framework
|
|
4
|
-
- **Version**: 4.5.
|
|
4
|
+
- **Version**: 4.5.7
|
|
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)
|
package/framework/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"createdAt": "2026-08-
|
|
2
|
+
"createdAt": "2026-08-27T10:57:24.520Z",
|
|
3
3
|
"files": {
|
|
4
4
|
"agents/build/dev.md": {
|
|
5
5
|
"hash": "5c8da2eee3357bbe84a6c468c76fe60a0c77357575503c3797aa94128282c565",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"type": "md"
|
|
98
98
|
},
|
|
99
99
|
"config.yaml": {
|
|
100
|
-
"hash": "
|
|
100
|
+
"hash": "394515115a919ac4b838cd58b3cf8f7cb34b535909b9a35f2d472cb51cc107ad",
|
|
101
101
|
"size": 2669,
|
|
102
102
|
"type": "yaml"
|
|
103
103
|
},
|
|
@@ -122,12 +122,12 @@
|
|
|
122
122
|
"type": "md"
|
|
123
123
|
},
|
|
124
124
|
"context/root.md": {
|
|
125
|
-
"hash": "
|
|
125
|
+
"hash": "1058040823bbd394775a017f6b714b5f53ff6030f08113c191d1a477f8635e95",
|
|
126
126
|
"size": 1217,
|
|
127
127
|
"type": "md"
|
|
128
128
|
},
|
|
129
129
|
"data/entity-registry.yaml": {
|
|
130
|
-
"hash": "
|
|
130
|
+
"hash": "8049c68d09ed547d64d1120f10a3f064f1c4f8313eacf8c2d267808e83a07265",
|
|
131
131
|
"size": 34605,
|
|
132
132
|
"type": "yaml"
|
|
133
133
|
},
|
|
@@ -852,8 +852,8 @@
|
|
|
852
852
|
"type": "json"
|
|
853
853
|
},
|
|
854
854
|
"schemas/session.schema.json": {
|
|
855
|
-
"hash": "
|
|
856
|
-
"size":
|
|
855
|
+
"hash": "57b0aa1b097b2253664a548cad42652ed3ec759feab6d0c1098062e88048cf1e",
|
|
856
|
+
"size": 19069,
|
|
857
857
|
"type": "json"
|
|
858
858
|
},
|
|
859
859
|
"schemas/task.schema.json": {
|
|
@@ -1347,5 +1347,5 @@
|
|
|
1347
1347
|
"type": "yaml"
|
|
1348
1348
|
}
|
|
1349
1349
|
},
|
|
1350
|
-
"version": "4.5.
|
|
1350
|
+
"version": "4.5.7"
|
|
1351
1351
|
}
|
package/framework/manifest.sig
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
s4/05J4PlrCMfsXB3OMeVX+lE+7A2MMEycfr10Z+2uwgFh5DlWnSGxkMmCS4dZ3hVAWgSWDk7waAR8kA1JWNAw==
|
|
@@ -196,16 +196,33 @@
|
|
|
196
196
|
},
|
|
197
197
|
"model_selections": {
|
|
198
198
|
"type": "array",
|
|
199
|
-
"description": "
|
|
199
|
+
"description": "Append-only audit log of the exact execution binding used for each task (Article XVI)",
|
|
200
200
|
"items": {
|
|
201
201
|
"type": "object",
|
|
202
|
-
"
|
|
202
|
+
"anyOf": [
|
|
203
|
+
{
|
|
204
|
+
"required": ["agent", "task_id", "provider", "model", "timestamp"],
|
|
205
|
+
"properties": {
|
|
206
|
+
"provider": { "enum": ["claude", "codex", "grok", "gemini"] }
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"required": ["agent", "recommended", "timestamp"],
|
|
211
|
+
"not": { "required": ["task_id", "provider", "model"] }
|
|
212
|
+
}
|
|
213
|
+
],
|
|
203
214
|
"properties": {
|
|
204
215
|
"agent": { "type": "string", "description": "Agent name" },
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
216
|
+
"task_id": { "type": "string", "description": "Task identity supplied to the runner" },
|
|
217
|
+
"provider": { "type": "string", "description": "CLI harness that executed the task" },
|
|
218
|
+
"provider_id": { "type": ["string", "null"], "description": "Vendor provider identifier from the capability catalog" },
|
|
219
|
+
"model": { "type": "string", "minLength": 1, "description": "Exact model identifier used" },
|
|
220
|
+
"reasoning_configuration": { "enum": ["low", "medium", "high", "xhigh", "max", null], "description": "Reasoning effort passed to the CLI when supported" },
|
|
221
|
+
"catalog_snapshot_ref": { "type": ["string", "null"], "description": "Capability catalog snapshot used for routing" },
|
|
222
|
+
"status": { "enum": ["dispatched", "completed", "failed"], "default": "dispatched" },
|
|
223
|
+
"recommended": { "type": "string", "description": "Legacy pre-4.5.7 recommended model field, migrated on session load" },
|
|
224
|
+
"actual": { "type": "string", "description": "Legacy pre-4.5.7 actual model field, migrated on session load" },
|
|
225
|
+
"reason": { "type": "string", "description": "Legacy pre-4.5.7 selection reason" },
|
|
209
226
|
"timestamp": { "type": "string", "format": "date-time" }
|
|
210
227
|
}
|
|
211
228
|
}
|
|
@@ -367,13 +384,13 @@
|
|
|
367
384
|
"active_model": {
|
|
368
385
|
"type": ["string", "null"],
|
|
369
386
|
"default": null,
|
|
370
|
-
"description": "
|
|
387
|
+
"description": "Model identifier detected for the current interactive harness. Written at /chati boot and used for context-window resolution. Per-task routed models are recorded separately in model_selections[]."
|
|
371
388
|
},
|
|
372
389
|
"active_provider": {
|
|
373
390
|
"type": ["string", "null"],
|
|
374
|
-
"enum": ["claude", "gemini", "codex", null],
|
|
391
|
+
"enum": ["claude", "gemini", "codex", "grok", null],
|
|
375
392
|
"default": null,
|
|
376
|
-
"description": "
|
|
393
|
+
"description": "Current interactive CLI harness. This is independent from the per-task provider recorded in model_selections[]."
|
|
377
394
|
},
|
|
378
395
|
"context_tokens_used": {
|
|
379
396
|
"type": "integer",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chati-dev",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.7",
|
|
4
4
|
"description": "AI-Powered Multi-Agent Orchestration System - Structured vibe coding for Full Stack Development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"chalk": "^5.3.0",
|
|
78
78
|
"js-yaml": "^5.4.0",
|
|
79
79
|
"ora": "^8.0.1",
|
|
80
|
+
"proper-lockfile": "4.1.2",
|
|
80
81
|
"semver": "^7.6.0"
|
|
81
82
|
},
|
|
82
83
|
"bundleDependencies": [
|
package/src/installer/core.js
CHANGED
|
@@ -578,6 +578,10 @@ export function updateGitignore(targetDir, selectedIDEs) {
|
|
|
578
578
|
'.chati/memories/*/session/',
|
|
579
579
|
'# NOTE: durable/ and daily/ memories ARE committed — they contain project knowledge',
|
|
580
580
|
'',
|
|
581
|
+
'# Operating system metadata',
|
|
582
|
+
'.DS_Store',
|
|
583
|
+
'._*',
|
|
584
|
+
'',
|
|
581
585
|
'# Claude Code integration (framework portions only)',
|
|
582
586
|
'.claude/commands/chati.md',
|
|
583
587
|
'.claude/rules/chati/',
|
package/src/orchestrator/cli.js
CHANGED
|
@@ -548,7 +548,7 @@ function sessionToPipelineState(session, projectDir) {
|
|
|
548
548
|
* Build the spawn command string for an autonomous agent.
|
|
549
549
|
* Exported for testing (path resolution must hold in every install layout).
|
|
550
550
|
*/
|
|
551
|
-
export function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout, model = null, strictProvider = false) {
|
|
551
|
+
export function buildSpawnCommand(agent, projectDir, previousAgent, provider, timeout, model = null, strictProvider = false, routing = {}) {
|
|
552
552
|
const parts = [
|
|
553
553
|
'node', RUNNER('run-agent.js'),
|
|
554
554
|
'--agent', agent,
|
|
@@ -557,6 +557,9 @@ export function buildSpawnCommand(agent, projectDir, previousAgent, provider, ti
|
|
|
557
557
|
'--previous-agent', previousAgent || 'none',
|
|
558
558
|
'--provider', provider || 'claude',
|
|
559
559
|
...(model ? ['--model', model] : []),
|
|
560
|
+
...(routing.provider_id ? ['--provider-id', routing.provider_id] : []),
|
|
561
|
+
...(routing.reasoning_configuration ? ['--reasoning-configuration', routing.reasoning_configuration] : []),
|
|
562
|
+
...(routing.catalog_snapshot_ref ? ['--catalog-snapshot-ref', routing.catalog_snapshot_ref] : []),
|
|
560
563
|
...(strictProvider ? ['--strict-provider'] : []),
|
|
561
564
|
'--timeout', String(timeout || 600000),
|
|
562
565
|
];
|
|
@@ -591,7 +594,7 @@ export function buildRoutedParallelSpawnCommands(agents, projectDir, previousAge
|
|
|
591
594
|
provider: modelInfo.provider,
|
|
592
595
|
model: modelInfo.model,
|
|
593
596
|
reasoning_configuration: modelInfo.reasoning_configuration,
|
|
594
|
-
command: buildSpawnCommand(agent, projectDir, previousAgent, modelInfo.provider, timeout, modelInfo.model, modelInfo.source === 'installation-v2'),
|
|
597
|
+
command: buildSpawnCommand(agent, projectDir, previousAgent, modelInfo.provider, timeout, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo),
|
|
595
598
|
});
|
|
596
599
|
});
|
|
597
600
|
}
|
|
@@ -849,7 +852,7 @@ async function _handleNextInner(projectDir) {
|
|
|
849
852
|
agent,
|
|
850
853
|
agent_file: agentFile,
|
|
851
854
|
phase: agentDef?.phase || session.mode,
|
|
852
|
-
spawn_command: buildSpawnCommand(agent, projectDir, session.last_handoff || 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2'),
|
|
855
|
+
spawn_command: buildSpawnCommand(agent, projectDir, session.last_handoff || 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo),
|
|
853
856
|
parallel_spawn_command: null,
|
|
854
857
|
parallel_agents: [],
|
|
855
858
|
handoff_status: { valid: true, missing: [], warnings: ['Resuming in-progress agent'] },
|
|
@@ -878,7 +881,7 @@ async function _handleNextInner(projectDir) {
|
|
|
878
881
|
agent: firstAgent,
|
|
879
882
|
agent_file: agentFile,
|
|
880
883
|
phase: 'discover',
|
|
881
|
-
spawn_command: buildSpawnCommand(firstAgent, projectDir, 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2'),
|
|
884
|
+
spawn_command: buildSpawnCommand(firstAgent, projectDir, 'none', modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo),
|
|
882
885
|
parallel_spawn_command: null,
|
|
883
886
|
parallel_agents: [],
|
|
884
887
|
handoff_status: { valid: true, missing: [], warnings: [] },
|
|
@@ -979,10 +982,10 @@ async function _handleNextInner(projectDir) {
|
|
|
979
982
|
teamData = { team_id: teamId, team_type: 'build', members: teamConfig.members };
|
|
980
983
|
} else if (isInteractive) {
|
|
981
984
|
action = 'spawn_routed_interactive';
|
|
982
|
-
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2');
|
|
985
|
+
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo);
|
|
983
986
|
} else {
|
|
984
987
|
action = 'spawn_autonomous';
|
|
985
|
-
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2');
|
|
988
|
+
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000, modelInfo.model, modelInfo.source === 'installation-v2', modelInfo);
|
|
986
989
|
}
|
|
987
990
|
|
|
988
991
|
// Surface criteria (Article XVII): even in autonomous mode, a high-stakes
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import * as yaml from 'js-yaml';
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
|
|
10
|
-
import { join, dirname } from 'path';
|
|
10
|
+
import { join, dirname, resolve } from 'path';
|
|
11
|
+
import lockfile from 'proper-lockfile';
|
|
11
12
|
|
|
12
13
|
const SESSION_FILE = '.chati/session.yaml';
|
|
13
14
|
|
|
@@ -24,6 +25,35 @@ function writeFileAtomic(path, content, encoding = 'utf-8') {
|
|
|
24
25
|
renameSync(tmp, path);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function waitForSessionWriteLock(sessionPath, maxWaitMs = 2000) {
|
|
29
|
+
const startedAt = Date.now();
|
|
30
|
+
while (true) {
|
|
31
|
+
try {
|
|
32
|
+
return lockfile.lockSync(sessionPath, {
|
|
33
|
+
realpath: false,
|
|
34
|
+
stale: 30_000,
|
|
35
|
+
update: 10_000,
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err.code !== 'ELOCKED') throw err;
|
|
39
|
+
if (Date.now() - startedAt >= maxWaitMs) {
|
|
40
|
+
throw new Error(`Timed out waiting for session write lock after ${maxWaitMs}ms`, { cause: err });
|
|
41
|
+
}
|
|
42
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findSessionProjectDir(startDir) {
|
|
48
|
+
let current = resolve(startDir);
|
|
49
|
+
while (true) {
|
|
50
|
+
if (existsSync(join(current, SESSION_FILE))) return current;
|
|
51
|
+
const parent = dirname(current);
|
|
52
|
+
if (parent === current) return null;
|
|
53
|
+
current = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
/**
|
|
28
58
|
* Default session template.
|
|
29
59
|
*/
|
|
@@ -182,8 +212,31 @@ export function migrateSession(session) {
|
|
|
182
212
|
return { migrated: false, fromVersion: null, toVersion: CURRENT_SCHEMA_VERSION };
|
|
183
213
|
}
|
|
184
214
|
|
|
215
|
+
let normalizedLegacySelections = false;
|
|
216
|
+
if (Array.isArray(session.model_selections)) {
|
|
217
|
+
session.model_selections = session.model_selections.map((entry) => {
|
|
218
|
+
if (!entry || typeof entry !== 'object') return entry;
|
|
219
|
+
if (entry.task_id && entry.provider && entry.model) return entry;
|
|
220
|
+
const model = entry.actual || entry.recommended;
|
|
221
|
+
if (!entry.agent || !model || !entry.timestamp) return entry;
|
|
222
|
+
const provider = entry.provider || 'claude';
|
|
223
|
+
if (!['claude', 'codex', 'grok', 'gemini'].includes(provider)) return entry;
|
|
224
|
+
normalizedLegacySelections = true;
|
|
225
|
+
return {
|
|
226
|
+
...entry,
|
|
227
|
+
task_id: entry.task_id || `${entry.agent}-legacy`,
|
|
228
|
+
provider,
|
|
229
|
+
provider_id: entry.provider_id || null,
|
|
230
|
+
model,
|
|
231
|
+
reasoning_configuration: entry.reasoning_configuration || null,
|
|
232
|
+
catalog_snapshot_ref: entry.catalog_snapshot_ref || null,
|
|
233
|
+
status: entry.status || 'dispatched',
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
185
238
|
if (session.schema_version === CURRENT_SCHEMA_VERSION) {
|
|
186
|
-
return { migrated:
|
|
239
|
+
return { migrated: normalizedLegacySelections, fromVersion: session.schema_version, toVersion: CURRENT_SCHEMA_VERSION };
|
|
187
240
|
}
|
|
188
241
|
|
|
189
242
|
// v0 (no schema_version) → v1.0 → v1.1 → v1.2
|
|
@@ -294,22 +347,22 @@ export function loadSession(projectDir) {
|
|
|
294
347
|
|
|
295
348
|
try {
|
|
296
349
|
const content = readFileSync(sessionPath, 'utf-8');
|
|
297
|
-
|
|
350
|
+
let session = yaml.load(content);
|
|
298
351
|
|
|
299
352
|
// Run migration if needed
|
|
300
353
|
const migration = migrateSession(session);
|
|
301
354
|
if (migration.migrated) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
355
|
+
const persisted = mutateSession(projectDir, current => current);
|
|
356
|
+
if (persisted.saved) {
|
|
357
|
+
session = persisted.session;
|
|
358
|
+
} else {
|
|
306
359
|
// Migration write-back failed (read-only filesystem, EACCES). The
|
|
307
360
|
// session continues with the migrated in-memory copy, but disk stays
|
|
308
361
|
// on the old schema and re-migrates on every load. Surface it so the
|
|
309
362
|
// user can fix the cause (make .chati/ writable) instead of it failing
|
|
310
363
|
// silently. The production path (writable FS) never reaches here.
|
|
311
364
|
process.stderr.write(
|
|
312
|
-
`[chati] warning: session migrated in memory but could not be written back to ${sessionPath} (${
|
|
365
|
+
`[chati] warning: session migrated in memory but could not be written back to ${sessionPath} (${persisted.error}); it will re-migrate on every load until the directory is writable\n`
|
|
313
366
|
);
|
|
314
367
|
}
|
|
315
368
|
}
|
|
@@ -336,115 +389,108 @@ export function loadSession(projectDir) {
|
|
|
336
389
|
* @returns {{ saved: boolean }}
|
|
337
390
|
*/
|
|
338
391
|
export function updateSession(projectDir, updates) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// Load existing session
|
|
342
|
-
const loadResult = loadSession(projectDir);
|
|
343
|
-
|
|
344
|
-
if (!loadResult.loaded) {
|
|
345
|
-
return {
|
|
346
|
-
saved: false,
|
|
347
|
-
error: loadResult.error,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
try {
|
|
392
|
+
return mutateSession(projectDir, (session) => {
|
|
352
393
|
// Merge updates
|
|
353
394
|
const merged = {
|
|
354
|
-
...
|
|
395
|
+
...session,
|
|
355
396
|
...updates,
|
|
356
397
|
};
|
|
357
398
|
|
|
358
399
|
// Deep merge agents if provided
|
|
359
400
|
if (updates.agents) {
|
|
360
401
|
merged.agents = {
|
|
361
|
-
...
|
|
402
|
+
...session.agents,
|
|
362
403
|
...updates.agents,
|
|
363
404
|
};
|
|
364
405
|
|
|
365
406
|
// Merge individual agent data
|
|
366
407
|
for (const [agentName, agentData] of Object.entries(updates.agents)) {
|
|
367
408
|
merged.agents[agentName] = {
|
|
368
|
-
...
|
|
409
|
+
...session.agents[agentName],
|
|
369
410
|
...agentData,
|
|
370
411
|
};
|
|
371
412
|
}
|
|
372
413
|
}
|
|
414
|
+
return merged;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
373
417
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
418
|
+
function mutateSession(projectDir, mutator) {
|
|
419
|
+
const sessionPath = join(projectDir, SESSION_FILE);
|
|
420
|
+
if (!existsSync(sessionPath)) return { saved: false, error: 'Session file not found' };
|
|
421
|
+
let releaseLock = null;
|
|
422
|
+
let outcome;
|
|
423
|
+
try {
|
|
424
|
+
releaseLock = waitForSessionWriteLock(sessionPath);
|
|
425
|
+
const session = yaml.load(readFileSync(sessionPath, 'utf-8'));
|
|
426
|
+
if (!session || typeof session !== 'object' || Array.isArray(session)) {
|
|
427
|
+
throw new Error('Invalid session document');
|
|
428
|
+
}
|
|
429
|
+
migrateSession(session);
|
|
430
|
+
const nextSession = mutator(session) || session;
|
|
431
|
+
const yamlContent = yaml.dump(nextSession, { lineWidth: -1, noRefs: true });
|
|
380
432
|
writeFileAtomic(sessionPath, yamlContent);
|
|
381
|
-
|
|
382
|
-
return {
|
|
383
|
-
saved: true,
|
|
384
|
-
session: merged,
|
|
385
|
-
};
|
|
433
|
+
outcome = { saved: true, session: nextSession };
|
|
386
434
|
} catch (err) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
435
|
+
outcome = { saved: false, error: `Failed to update session: ${err.message}` };
|
|
436
|
+
} finally {
|
|
437
|
+
if (releaseLock) {
|
|
438
|
+
try {
|
|
439
|
+
releaseLock();
|
|
440
|
+
} catch (err) {
|
|
441
|
+
outcome = { saved: false, error: `Session write lock was compromised: ${err.message}` };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
391
444
|
}
|
|
445
|
+
return outcome;
|
|
392
446
|
}
|
|
393
447
|
|
|
394
448
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* @returns {{ saved: boolean }}
|
|
449
|
+
* Append the exact provider/model binding used for an execution.
|
|
450
|
+
* A short cross-process lock prevents parallel agents from overwriting each
|
|
451
|
+
* other's append-only audit entries.
|
|
399
452
|
*/
|
|
400
|
-
export function
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
reason: transition.reason || '',
|
|
419
|
-
timestamp: new Date().toISOString(),
|
|
453
|
+
export function recordModelSelection(projectDir, selection) {
|
|
454
|
+
const sessionProjectDir = findSessionProjectDir(projectDir);
|
|
455
|
+
if (!sessionProjectDir) return { saved: false, error: 'Session file not found in working directory or its parents' };
|
|
456
|
+
|
|
457
|
+
return mutateSession(sessionProjectDir, (session) => {
|
|
458
|
+
session.model_selections = Array.isArray(session.model_selections) ? session.model_selections : [];
|
|
459
|
+
session.model_selections.push({
|
|
460
|
+
agent: selection.agent,
|
|
461
|
+
task_id: selection.taskId,
|
|
462
|
+
provider: selection.provider,
|
|
463
|
+
provider_id: selection.providerId || null,
|
|
464
|
+
model: selection.model,
|
|
465
|
+
reasoning_configuration: selection.reasoningConfiguration || null,
|
|
466
|
+
catalog_snapshot_ref: selection.catalogSnapshotRef || null,
|
|
467
|
+
status: selection.status || 'dispatched',
|
|
468
|
+
timestamp: selection.timestamp || new Date().toISOString(),
|
|
469
|
+
});
|
|
470
|
+
return session;
|
|
420
471
|
});
|
|
421
|
-
|
|
422
|
-
// Update current mode (both flat and nested for compatibility)
|
|
423
|
-
session.mode = transition.to;
|
|
424
|
-
if (session.project) {
|
|
425
|
-
session.project.state = transition.to;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return writeSessionToDisk(projectDir, session);
|
|
429
472
|
}
|
|
430
473
|
|
|
431
474
|
/**
|
|
432
|
-
*
|
|
433
|
-
* spread+deep-merge logic (which can corrupt list fields like `completed_agents`
|
|
434
|
-
* when the full session is passed as the updates arg).
|
|
475
|
+
* Record a mode transition in session history.
|
|
435
476
|
* @param {string} projectDir
|
|
436
|
-
* @param {object}
|
|
437
|
-
* @returns {{ saved: boolean
|
|
477
|
+
* @param {object} transition - { from, to, trigger, reason }
|
|
478
|
+
* @returns {{ saved: boolean }}
|
|
438
479
|
*/
|
|
439
|
-
function
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
480
|
+
export function recordModeTransition(projectDir, transition) {
|
|
481
|
+
return mutateSession(projectDir, (session) => {
|
|
482
|
+
session.mode_transitions = session.mode_transitions || [];
|
|
483
|
+
session.mode_transitions.push({
|
|
484
|
+
from: transition.from,
|
|
485
|
+
to: transition.to,
|
|
486
|
+
trigger: transition.trigger || 'manual',
|
|
487
|
+
reason: transition.reason || '',
|
|
488
|
+
timestamp: new Date().toISOString(),
|
|
489
|
+
});
|
|
490
|
+
session.mode = transition.to;
|
|
491
|
+
if (session.project) session.project.state = transition.to;
|
|
492
|
+
return session;
|
|
493
|
+
});
|
|
448
494
|
}
|
|
449
495
|
|
|
450
496
|
/**
|
|
@@ -457,17 +503,8 @@ function writeSessionToDisk(projectDir, session) {
|
|
|
457
503
|
* @returns {{ saved: boolean }}
|
|
458
504
|
*/
|
|
459
505
|
export function recordAgentCompletion(projectDir, completion) {
|
|
460
|
-
const loadResult = loadSession(projectDir);
|
|
461
|
-
|
|
462
|
-
if (!loadResult.loaded) {
|
|
463
|
-
return {
|
|
464
|
-
saved: false,
|
|
465
|
-
error: loadResult.error,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const session = loadResult.session;
|
|
470
506
|
const { agent, status, score, outputs = [], handoffData = {} } = completion;
|
|
507
|
+
return mutateSession(projectDir, (session) => {
|
|
471
508
|
|
|
472
509
|
// Update agent data
|
|
473
510
|
if (!session.agents[agent]) {
|
|
@@ -547,7 +584,8 @@ export function recordAgentCompletion(projectDir, completion) {
|
|
|
547
584
|
}
|
|
548
585
|
}
|
|
549
586
|
|
|
550
|
-
|
|
587
|
+
return session;
|
|
588
|
+
});
|
|
551
589
|
}
|
|
552
590
|
|
|
553
591
|
/**
|
|
@@ -19,15 +19,22 @@
|
|
|
19
19
|
*/
|
|
20
20
|
export function buildCommand(config, provider) {
|
|
21
21
|
const args = [...provider.baseArgs];
|
|
22
|
+
let effectiveModel = null;
|
|
22
23
|
|
|
23
24
|
if (config.model) {
|
|
24
|
-
|
|
25
|
-
args.push(provider.modelFlag,
|
|
25
|
+
effectiveModel = provider.modelMap[config.model] || config.model;
|
|
26
|
+
args.push(provider.modelFlag, effectiveModel);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.reasoningConfiguration) {
|
|
30
|
+
args.push('--effort', config.reasoningConfiguration);
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
return {
|
|
29
34
|
command: provider.command,
|
|
30
35
|
args,
|
|
31
36
|
stdinPrompt: config.prompt || null,
|
|
37
|
+
effectiveModel,
|
|
38
|
+
effectiveReasoningConfiguration: config.reasoningConfiguration || null,
|
|
32
39
|
};
|
|
33
40
|
}
|
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export function buildCommand(config, provider) {
|
|
16
16
|
const args = [...provider.baseArgs];
|
|
17
|
+
let effectiveModel = null;
|
|
17
18
|
|
|
18
19
|
if (config.model) {
|
|
19
|
-
|
|
20
|
-
args.push(provider.modelFlag,
|
|
20
|
+
effectiveModel = provider.modelMap[config.model] || config.model;
|
|
21
|
+
args.push(provider.modelFlag, effectiveModel);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (config.reasoningConfiguration) {
|
|
25
|
+
args.push('-c', `model_reasoning_effort=${JSON.stringify(config.reasoningConfiguration)}`);
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
// Codex exec reads prompt from stdin when `-` is passed
|
|
@@ -27,5 +32,7 @@ export function buildCommand(config, provider) {
|
|
|
27
32
|
command: provider.command,
|
|
28
33
|
args,
|
|
29
34
|
stdinPrompt: config.prompt || null,
|
|
35
|
+
effectiveModel,
|
|
36
|
+
effectiveReasoningConfiguration: config.reasoningConfiguration || null,
|
|
30
37
|
};
|
|
31
38
|
}
|
|
@@ -14,15 +14,18 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export function buildCommand(config, provider) {
|
|
16
16
|
const args = [...provider.baseArgs];
|
|
17
|
+
let effectiveModel = null;
|
|
17
18
|
|
|
18
19
|
if (config.model) {
|
|
19
|
-
|
|
20
|
-
args.push(provider.modelFlag,
|
|
20
|
+
effectiveModel = provider.modelMap[config.model] || config.model;
|
|
21
|
+
args.push(provider.modelFlag, effectiveModel);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
return {
|
|
24
25
|
command: provider.command,
|
|
25
26
|
args,
|
|
26
27
|
stdinPrompt: config.prompt || null,
|
|
28
|
+
effectiveModel,
|
|
29
|
+
effectiveReasoningConfiguration: null,
|
|
27
30
|
};
|
|
28
31
|
}
|
|
@@ -7,10 +7,20 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export function buildCommand(config, provider) {
|
|
9
9
|
const args = [...provider.baseArgs];
|
|
10
|
+
let effectiveModel = null;
|
|
10
11
|
if (config.model) {
|
|
11
|
-
|
|
12
|
-
args.push(provider.modelFlag,
|
|
12
|
+
effectiveModel = provider.modelMap[config.model] || config.model;
|
|
13
|
+
args.push(provider.modelFlag, effectiveModel);
|
|
14
|
+
}
|
|
15
|
+
if (config.reasoningConfiguration) {
|
|
16
|
+
args.push('--reasoning-effort', config.reasoningConfiguration);
|
|
13
17
|
}
|
|
14
18
|
args.push('-p', config.prompt || '');
|
|
15
|
-
return {
|
|
19
|
+
return {
|
|
20
|
+
command: provider.command,
|
|
21
|
+
args,
|
|
22
|
+
stdinPrompt: null,
|
|
23
|
+
effectiveModel,
|
|
24
|
+
effectiveReasoningConfiguration: config.reasoningConfiguration || null,
|
|
25
|
+
};
|
|
16
26
|
}
|
|
@@ -19,6 +19,7 @@ import { parseProviderConfig, parseAgentOverride } from '../utils/config-parser.
|
|
|
19
19
|
* @property {string} command - CLI command name
|
|
20
20
|
* @property {string[]} baseArgs - Default CLI arguments for non-interactive mode
|
|
21
21
|
* @property {string} modelFlag - CLI flag for model selection
|
|
22
|
+
* @property {string} defaultModel - Explicit legacy default model alias
|
|
22
23
|
* @property {boolean} stdinSupport - Whether prompts can be piped via stdin
|
|
23
24
|
* @property {boolean} hooksSupport - Whether the CLI supports hooks (event middleware)
|
|
24
25
|
* @property {boolean} mcpSupport - Whether the CLI supports MCP servers
|
|
@@ -34,6 +35,7 @@ const PROVIDERS = {
|
|
|
34
35
|
command: 'claude',
|
|
35
36
|
baseArgs: ['--print', '--dangerously-skip-permissions'],
|
|
36
37
|
modelFlag: '--model',
|
|
38
|
+
defaultModel: 'sonnet',
|
|
37
39
|
stdinSupport: true,
|
|
38
40
|
hooksSupport: true,
|
|
39
41
|
mcpSupport: true,
|
|
@@ -50,6 +52,7 @@ const PROVIDERS = {
|
|
|
50
52
|
command: 'gemini',
|
|
51
53
|
baseArgs: [],
|
|
52
54
|
modelFlag: '--model',
|
|
55
|
+
defaultModel: 'pro',
|
|
53
56
|
stdinSupport: true,
|
|
54
57
|
hooksSupport: true,
|
|
55
58
|
mcpSupport: true,
|
|
@@ -65,6 +68,7 @@ const PROVIDERS = {
|
|
|
65
68
|
command: 'codex',
|
|
66
69
|
baseArgs: ['exec'],
|
|
67
70
|
modelFlag: '-m',
|
|
71
|
+
defaultModel: 'codex',
|
|
68
72
|
stdinSupport: true,
|
|
69
73
|
hooksSupport: false,
|
|
70
74
|
mcpSupport: true,
|
|
@@ -80,6 +84,7 @@ const PROVIDERS = {
|
|
|
80
84
|
command: 'grok',
|
|
81
85
|
baseArgs: [],
|
|
82
86
|
modelFlag: '-m',
|
|
87
|
+
defaultModel: 'grok',
|
|
83
88
|
stdinSupport: false,
|
|
84
89
|
hooksSupport: true,
|
|
85
90
|
mcpSupport: true,
|
|
@@ -130,6 +130,9 @@ async function main() {
|
|
|
130
130
|
workingDir: projectDir,
|
|
131
131
|
timeout,
|
|
132
132
|
strictProvider: args['strict-provider'] === 'true',
|
|
133
|
+
providerId: args['provider-id'] || null,
|
|
134
|
+
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
135
|
+
catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
|
|
133
136
|
});
|
|
134
137
|
} catch (err) {
|
|
135
138
|
outputError(`Failed to spawn terminal: ${err.message}`);
|
|
@@ -144,6 +147,7 @@ async function main() {
|
|
|
144
147
|
errorType: 'agent_failure',
|
|
145
148
|
agent: args.agent,
|
|
146
149
|
provider: promptResult.provider || args.provider || 'claude',
|
|
150
|
+
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
147
151
|
phase: sessionState?.phase || 'unknown',
|
|
148
152
|
});
|
|
149
153
|
await flushAndSend(projectDir);
|
|
@@ -212,6 +216,7 @@ async function main() {
|
|
|
212
216
|
agent: args.agent,
|
|
213
217
|
model: promptResult.model,
|
|
214
218
|
provider: promptResult.provider || args.provider || 'claude',
|
|
219
|
+
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
215
220
|
exitCode: handle.exitCode,
|
|
216
221
|
handoff: parsed.handoff,
|
|
217
222
|
elapsed,
|
|
@@ -55,6 +55,9 @@ function createSpawnConfig({ agent, taskId, promptResult, projectDir, timeout, s
|
|
|
55
55
|
workingDir: projectDir,
|
|
56
56
|
timeout,
|
|
57
57
|
strictProvider,
|
|
58
|
+
providerId: promptResult.providerId || null,
|
|
59
|
+
reasoningConfiguration: promptResult.reasoningConfiguration || null,
|
|
60
|
+
catalogSnapshotRef: promptResult.catalogSnapshotRef || null,
|
|
58
61
|
};
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -120,6 +123,10 @@ async function main() {
|
|
|
120
123
|
strictProvider: args['strict-provider'] === 'true',
|
|
121
124
|
});
|
|
122
125
|
|
|
126
|
+
promptResult.providerId = args['provider-id'] || null;
|
|
127
|
+
promptResult.reasoningConfiguration = args['reasoning-configuration'] || null;
|
|
128
|
+
promptResult.catalogSnapshotRef = args['catalog-snapshot-ref'] || null;
|
|
129
|
+
|
|
123
130
|
configs.push(createSpawnConfig({
|
|
124
131
|
agent: agents[i], taskId: taskIds[i], promptResult, projectDir, timeout,
|
|
125
132
|
strictProvider: args['strict-provider'] === 'true',
|
package/src/terminal/spawner.js
CHANGED
|
@@ -11,6 +11,7 @@ import { spawn } from 'child_process';
|
|
|
11
11
|
import { validateWriteScopes, buildIsolationEnv } from './isolation.js';
|
|
12
12
|
import { getProvider } from './cli-registry.js';
|
|
13
13
|
import { getRateLimiter } from './rate-limiter.js';
|
|
14
|
+
import { recordModelSelection } from '../orchestrator/session-manager.js';
|
|
14
15
|
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
17
|
// Constants
|
|
@@ -108,6 +109,9 @@ export function cleanParentEnv(env) {
|
|
|
108
109
|
* @property {string} taskId - Task identifier
|
|
109
110
|
* @property {string} [model] - LLM model tier name (e.g. opus, pro, codex, claude-sonnet)
|
|
110
111
|
* @property {string} [provider] - CLI provider name (claude, gemini, codex, grok)
|
|
112
|
+
* @property {string} [providerId] - Vendor provider identifier (anthropic, openai, xai)
|
|
113
|
+
* @property {string} [reasoningConfiguration] - Requested reasoning effort
|
|
114
|
+
* @property {string} [catalogSnapshotRef] - Capability catalog snapshot used for routing
|
|
111
115
|
* @property {boolean} [strictProvider] - Reject a missing provider rather than using the legacy fallback
|
|
112
116
|
* @property {string} [prompt] - Full prompt string (from prompt-builder, piped via stdin)
|
|
113
117
|
* @property {object} [contextPayload] - Context to inject via env var
|
|
@@ -159,6 +163,7 @@ export function buildSpawnCommand(config) {
|
|
|
159
163
|
CHATI_AGENT: config.agent,
|
|
160
164
|
CHATI_TASK_ID: config.taskId,
|
|
161
165
|
CHATI_SPAWNED: 'true',
|
|
166
|
+
...(config.reasoningConfiguration ? { CHATI_REASONING_CONFIGURATION: config.reasoningConfiguration } : {}),
|
|
162
167
|
};
|
|
163
168
|
|
|
164
169
|
if (config.contextPayload) {
|
|
@@ -174,13 +179,20 @@ export function buildSpawnCommand(config) {
|
|
|
174
179
|
const providerName = config.provider || 'claude';
|
|
175
180
|
let command, args, prompt;
|
|
176
181
|
let providerFallback = null;
|
|
182
|
+
let actualProvider = providerName;
|
|
183
|
+
let actualModel;
|
|
184
|
+
let actualReasoningConfiguration = null;
|
|
177
185
|
|
|
178
186
|
try {
|
|
179
187
|
const provider = getProvider(providerName);
|
|
180
|
-
const
|
|
188
|
+
const requestedModel = config.model || provider.defaultModel;
|
|
189
|
+
actualModel = provider.modelMap[requestedModel] || requestedModel;
|
|
190
|
+
const adapterResult = provider.adapter.buildCommand({ ...config, model: actualModel }, provider);
|
|
181
191
|
command = adapterResult.command;
|
|
182
192
|
args = adapterResult.args;
|
|
183
193
|
prompt = adapterResult.stdinPrompt;
|
|
194
|
+
actualModel = adapterResult.effectiveModel;
|
|
195
|
+
actualReasoningConfiguration = adapterResult.effectiveReasoningConfiguration;
|
|
184
196
|
} catch (err) {
|
|
185
197
|
if (config.strictProvider) {
|
|
186
198
|
const failure = new Error(`Selected provider "${providerName}" is unavailable: ${err.message}`);
|
|
@@ -194,18 +206,26 @@ export function buildSpawnCommand(config) {
|
|
|
194
206
|
reason: err.message,
|
|
195
207
|
timestamp: new Date().toISOString(),
|
|
196
208
|
};
|
|
209
|
+
actualProvider = 'claude';
|
|
197
210
|
console.error(`[chati] Provider "${providerName}" resolution failed: ${err.message}. Falling back to claude.`);
|
|
198
211
|
command = 'claude';
|
|
199
212
|
args = ['--print', '--dangerously-skip-permissions'];
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
213
|
+
const claudeProvider = getProvider('claude');
|
|
214
|
+
const requestedClaudeModel = config.model || 'sonnet';
|
|
215
|
+
actualModel = claudeProvider.modelMap[requestedClaudeModel]
|
|
216
|
+
|| (requestedClaudeModel.startsWith('claude-') ? requestedClaudeModel : claudeProvider.modelMap.sonnet);
|
|
217
|
+
args.push('--model', actualModel);
|
|
218
|
+
if (config.reasoningConfiguration) {
|
|
219
|
+
args.push('--effort', config.reasoningConfiguration);
|
|
220
|
+
actualReasoningConfiguration = config.reasoningConfiguration;
|
|
204
221
|
}
|
|
205
222
|
prompt = config.prompt || null;
|
|
206
223
|
}
|
|
207
224
|
|
|
208
|
-
return {
|
|
225
|
+
return {
|
|
226
|
+
command, args, env, terminalId, prompt, providerFallback,
|
|
227
|
+
actualProvider, actualModel, actualReasoningConfiguration,
|
|
228
|
+
};
|
|
209
229
|
}
|
|
210
230
|
|
|
211
231
|
/**
|
|
@@ -215,11 +235,29 @@ export function buildSpawnCommand(config) {
|
|
|
215
235
|
* @returns {TerminalHandle}
|
|
216
236
|
*/
|
|
217
237
|
export function spawnTerminal(config) {
|
|
218
|
-
const {
|
|
238
|
+
const {
|
|
239
|
+
command, args, env, terminalId, prompt, providerFallback,
|
|
240
|
+
actualProvider, actualModel, actualReasoningConfiguration,
|
|
241
|
+
} = buildSpawnCommand(config);
|
|
219
242
|
|
|
220
243
|
const cwd = config.workingDir || process.cwd();
|
|
221
244
|
const timeout = config.timeout || 300_000; // default 5 minutes
|
|
222
245
|
|
|
246
|
+
const selectionRecord = recordModelSelection(cwd, {
|
|
247
|
+
agent: config.agent,
|
|
248
|
+
taskId: config.taskId,
|
|
249
|
+
provider: actualProvider,
|
|
250
|
+
providerId: providerFallback ? null : (config.providerId || null),
|
|
251
|
+
model: actualModel,
|
|
252
|
+
reasoningConfiguration: actualReasoningConfiguration,
|
|
253
|
+
catalogSnapshotRef: providerFallback ? null : (config.catalogSnapshotRef || null),
|
|
254
|
+
});
|
|
255
|
+
if (!selectionRecord.saved) {
|
|
256
|
+
const auditError = new Error(selectionRecord.error || 'Failed to persist model selection audit');
|
|
257
|
+
auditError.code = 'MODEL_SELECTION_AUDIT_FAILED';
|
|
258
|
+
throw auditError;
|
|
259
|
+
}
|
|
260
|
+
|
|
223
261
|
const child = spawn(command, args, {
|
|
224
262
|
cwd,
|
|
225
263
|
env: { ...cleanParentEnv(process.env), ...env },
|
|
@@ -238,8 +276,9 @@ export function spawnTerminal(config) {
|
|
|
238
276
|
process: child,
|
|
239
277
|
agent: config.agent,
|
|
240
278
|
taskId: config.taskId,
|
|
241
|
-
model: config.model || '
|
|
242
|
-
provider:
|
|
279
|
+
model: actualModel || config.model || 'provider-default',
|
|
280
|
+
provider: actualProvider,
|
|
281
|
+
reasoningConfiguration: config.reasoningConfiguration || null,
|
|
243
282
|
providerFallback,
|
|
244
283
|
startedAt: new Date().toISOString(),
|
|
245
284
|
status: 'running',
|
|
@@ -250,7 +289,7 @@ export function spawnTerminal(config) {
|
|
|
250
289
|
};
|
|
251
290
|
|
|
252
291
|
// Record spawn in rate limiter for throttling
|
|
253
|
-
const providerForRate =
|
|
292
|
+
const providerForRate = actualProvider;
|
|
254
293
|
getRateLimiter(providerForRate).recordSpawn();
|
|
255
294
|
|
|
256
295
|
// Capture output (capped at ~10MB to prevent unbounded memory growth)
|