chati-dev 4.5.6 → 4.5.8
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/agents/build/dev.md +1 -1
- package/framework/agents/deploy/devops.md +1 -1
- package/framework/agents/discover/brief.md +1 -1
- package/framework/agents/discover/brownfield-wu.md +1 -1
- package/framework/agents/discover/greenfield-wu.md +1 -1
- package/framework/agents/plan/architect-data-engineer.md +1 -1
- package/framework/agents/plan/architect-system.md +1 -1
- package/framework/agents/plan/architect.md +1 -1
- package/framework/agents/plan/detail.md +1 -1
- package/framework/agents/plan/phases.md +1 -1
- package/framework/agents/plan/tasks.md +1 -1
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux-component-engineer.md +1 -1
- package/framework/agents/plan/ux-researcher.md +1 -1
- package/framework/agents/plan/ux.md +1 -1
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +1 -1
- package/framework/agents/shared/visualizer.md +1 -1
- package/framework/config.yaml +2 -2
- package/framework/context/root.md +1 -1
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/hooks/license-guard.js +19 -0
- package/framework/manifest.json +55 -50
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +177 -71
- package/framework/orchestrator/chati.md +1 -1
- package/framework/package.json +3 -0
- package/framework/schemas/session.schema.json +26 -9
- package/package.json +2 -1
- package/src/config/claude-settings-generator.js +6 -6
- package/src/installer/core.js +55 -25
- package/src/installer/path-replacement.js +13 -0
- package/src/installer/provider-overlay.js +3 -1
- package/src/installer-v2/index.js +55 -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
|
@@ -35,6 +35,54 @@ function assertString(value, code, label) {
|
|
|
35
35
|
if (typeof value !== 'string' || value.trim() === '') throw new ContractError(code, `${label} must be a non-empty string`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function assertExactKeys(value, allowedKeys, label) {
|
|
39
|
+
assertObject(value, 'INVALID_ARTIFACT', label);
|
|
40
|
+
const unexpected = Object.keys(value).filter((key) => !allowedKeys.has(key));
|
|
41
|
+
if (unexpected.length > 0) {
|
|
42
|
+
throw new ContractError('UNEXPECTED_ARTIFACT_FIELD', `${label} contains unsupported fields: ${unexpected.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ARTIFACT_KEYS = new Set([
|
|
47
|
+
'schema_version', 'artifact_type', 'project_id', 'created_at', 'installation',
|
|
48
|
+
'capability_snapshot', 'brain_read_capability', 'managed_paths',
|
|
49
|
+
'installation_artifact_id', 'digest',
|
|
50
|
+
]);
|
|
51
|
+
const INSTALLATION_KEYS = new Set([
|
|
52
|
+
'installation_id', 'profile', 'enabled_providers', 'capability_snapshot_ref',
|
|
53
|
+
'policy_ref', 'primary_binding', 'primary_harness', 'external_integrations',
|
|
54
|
+
]);
|
|
55
|
+
const BINDING_KEYS = new Set([
|
|
56
|
+
'provider_id', 'harness_id', 'allowed_models', 'allowed_reasoning_configurations',
|
|
57
|
+
]);
|
|
58
|
+
const SNAPSHOT_KEYS = new Set(['snapshot_id', 'expires_at', 'models']);
|
|
59
|
+
const MODEL_KEYS = new Set([
|
|
60
|
+
'provider_id', 'model_id', 'actions', 'adjudication_priority',
|
|
61
|
+
'highest_reasoning_configuration', 'routing_priority', 'tier', 'natural_role',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function assertClosedArtifactSchema(artifact) {
|
|
65
|
+
assertExactKeys(artifact, ARTIFACT_KEYS, 'artifact');
|
|
66
|
+
assertExactKeys(artifact.installation, INSTALLATION_KEYS, 'installation');
|
|
67
|
+
for (const binding of artifact.installation.enabled_providers ?? []) {
|
|
68
|
+
assertExactKeys(binding, BINDING_KEYS, 'enabled provider binding');
|
|
69
|
+
}
|
|
70
|
+
if (artifact.installation.primary_binding !== undefined) {
|
|
71
|
+
assertExactKeys(artifact.installation.primary_binding, new Set(['provider_id', 'harness_id']), 'primary_binding');
|
|
72
|
+
}
|
|
73
|
+
if (artifact.installation.external_integrations !== undefined) {
|
|
74
|
+
assertExactKeys(artifact.installation.external_integrations, new Set(['clickup']), 'external_integrations');
|
|
75
|
+
}
|
|
76
|
+
assertExactKeys(artifact.capability_snapshot, SNAPSHOT_KEYS, 'capability_snapshot');
|
|
77
|
+
for (const model of artifact.capability_snapshot.models ?? []) {
|
|
78
|
+
assertExactKeys(model, MODEL_KEYS, 'capability model');
|
|
79
|
+
}
|
|
80
|
+
const brainKeys = artifact.brain_read_capability?.status === 'configured'
|
|
81
|
+
? new Set(['status', 'mode', 'policy_ref'])
|
|
82
|
+
: new Set(['status']);
|
|
83
|
+
assertExactKeys(artifact.brain_read_capability, brainKeys, 'brain_read_capability');
|
|
84
|
+
}
|
|
85
|
+
|
|
38
86
|
function readClock(clock) {
|
|
39
87
|
if (typeof clock !== 'function') throw new ContractError('MISSING_CLOCK', 'a deterministic clock function is required');
|
|
40
88
|
const result = clock();
|
|
@@ -127,6 +175,7 @@ export function doctorV2(artifact, { clock = () => new Date() } = {}) {
|
|
|
127
175
|
const checks = [];
|
|
128
176
|
try {
|
|
129
177
|
assertObject(artifact, 'INVALID_ARTIFACT', 'artifact');
|
|
178
|
+
assertClosedArtifactSchema(artifact);
|
|
130
179
|
if (artifact.schema_version !== INSTALLATION_SCHEMA_VERSION || artifact.artifact_type !== 'chati-installation') {
|
|
131
180
|
throw new ContractError('UNSUPPORTED_INSTALLATION_ARTIFACT', 'expected a chati installation artifact v2');
|
|
132
181
|
}
|
|
@@ -138,6 +187,12 @@ export function doctorV2(artifact, { clock = () => new Date() } = {}) {
|
|
|
138
187
|
if (!Array.isArray(artifact.managed_paths) || !artifact.managed_paths.every((path) => ensureRelativePath(path))) {
|
|
139
188
|
throw new ContractError('INVALID_MANAGED_PATH', 'managed_paths must be valid project-relative paths');
|
|
140
189
|
}
|
|
190
|
+
if (!artifact.managed_paths.includes(INSTALLATION_ARTIFACT_PATH)) {
|
|
191
|
+
throw new ContractError('INVALID_MANAGED_PATH', `managed_paths must include ${INSTALLATION_ARTIFACT_PATH}`);
|
|
192
|
+
}
|
|
193
|
+
if (new Set(artifact.managed_paths).size !== artifact.managed_paths.length) {
|
|
194
|
+
throw new ContractError('DUPLICATE_MANAGED_PATH', 'managed_paths must be unique');
|
|
195
|
+
}
|
|
141
196
|
checks.push({ name: 'artifact', status: 'pass', message: 'artifact schema and digest are valid' });
|
|
142
197
|
checks.push({ name: 'selected-bindings', status: 'pass', message: 'only explicitly selected provider/harness bindings are present' });
|
|
143
198
|
checks.push({ name: 'brain-capability', status: 'pass', message: 'Brain capability is absent or read-only' });
|
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',
|