chati-dev 4.5.5 → 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/bin/chati.js +35 -1
- package/framework/agents/plan/tasks.md +1 -1
- package/framework/config.yaml +8 -17
- package/framework/constitution.md +30 -20
- package/framework/context/governance.md +3 -1
- package/framework/context/root.md +1 -1
- package/framework/data/entity-registry.yaml +2 -2
- package/framework/domains/agents/orchestrator.yaml +1 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +2 -2
- package/framework/hooks/model-governance.js +9 -0
- package/framework/manifest.json +38 -38
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati.md +27 -3
- package/framework/schemas/session.schema.json +27 -10
- package/framework/tasks/brownfield-wu-architecture-map.md +1 -1
- package/framework/tasks/brownfield-wu-deep-discovery.md +1 -1
- package/framework/tasks/brownfield-wu-dependency-scan.md +1 -1
- package/framework/tasks/brownfield-wu-migration-plan.md +1 -1
- package/framework/tasks/brownfield-wu-report.md +1 -1
- package/framework/tasks/brownfield-wu-risk-assess.md +1 -1
- package/framework/tasks/greenfield-wu-report.md +1 -1
- package/node_modules/@chati/provider-registry/src/index.js +3 -2
- package/node_modules/@chati/tracking-clickup/src/index.js +22 -0
- package/package.json +2 -1
- package/src/config/gemini-hooks-generator.js +10 -4
- package/src/dashboard/layout.js +6 -4
- package/src/installer/core.js +20 -0
- package/src/installer/templates.js +21 -1
- package/src/installer-v2/clickup-preflight.js +32 -0
- package/src/installer-v2/model-catalog-envelope.json +15 -13
- package/src/installer-v2/model-catalog.json +5 -5
- package/src/installer-v2/model-catalog.sig +1 -1
- package/src/installer-v2/wizard-installation.js +1 -1
- package/src/intelligence/registry-manager.js +9 -3
- package/src/orchestrator/cli.js +41 -23
- package/src/orchestrator/clickup-projection.js +25 -8
- package/src/orchestrator/clickup-runtime.js +89 -1
- package/src/orchestrator/planning-runtime.js +1 -2
- package/src/orchestrator/rail-runtime.js +1 -2
- package/src/orchestrator/runtime-installation-v2.js +10 -1
- 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/src/wizard/index.js +8 -0
- package/src/wizard/questions.js +7 -3
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { sha256 } from '@chati/core';
|
|
1
4
|
import { createProjectRailEngine, loadRailHandoff } from './rail-runtime.js';
|
|
2
|
-
import { enqueueClickUpProjectionFromState } from './clickup-projection.js';
|
|
5
|
+
import { createClickUpTrackingFromState, enqueueClickUpProjectionFromState } from './clickup-projection.js';
|
|
3
6
|
|
|
4
7
|
/** Enqueues a durable local ClickUp projection. It never sends to ClickUp. */
|
|
5
8
|
export function enqueueClickUpProjection({ projectDir, handoff_id, task_id, attempt_id, operation = 'update', payload, completion, decisions, clock = () => new Date() } = {}) {
|
|
@@ -11,3 +14,88 @@ export function enqueueClickUpProjection({ projectDir, handoff_id, task_id, atte
|
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export { enqueueClickUpProjectionFromState } from './clickup-projection.js';
|
|
17
|
+
|
|
18
|
+
export function listPendingClickUpProjections({ projectDir } = {}) {
|
|
19
|
+
const path = join(projectDir, '.chati/v2/tracking/clickup-outbox.json');
|
|
20
|
+
if (!existsSync(path)) return Object.freeze([]);
|
|
21
|
+
const outbox = JSON.parse(readFileSync(path, 'utf8'));
|
|
22
|
+
return Object.freeze((outbox.projections || [])
|
|
23
|
+
.filter((projection) => ['pending', 'retryable_failure', 'sent_unconfirmed'].includes(projection.delivery_state))
|
|
24
|
+
.map((projection) => Object.freeze({
|
|
25
|
+
projection_id: projection.projection_id,
|
|
26
|
+
clickup_ref: projection.clickup_ref,
|
|
27
|
+
operation: projection.operation,
|
|
28
|
+
payload: projection.payload,
|
|
29
|
+
idempotency_key: projection.idempotency_key,
|
|
30
|
+
})));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SENSITIVE_RECEIPT_KEY = /(authorization|cookie|credential|password|secret|token)/i;
|
|
34
|
+
|
|
35
|
+
export function sanitizeClickUpReceipt(value, depth = 0) {
|
|
36
|
+
if (depth > 8) return '[truncated]';
|
|
37
|
+
if (Array.isArray(value)) return value.slice(0, 100).map((item) => sanitizeClickUpReceipt(item, depth + 1));
|
|
38
|
+
if (!value || typeof value !== 'object') return value;
|
|
39
|
+
return Object.fromEntries(Object.entries(value)
|
|
40
|
+
.filter(([key]) => !SENSITIVE_RECEIPT_KEY.test(key))
|
|
41
|
+
.map(([key, item]) => [key, sanitizeClickUpReceipt(item, depth + 1)]));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasRemoteReceiptIdentifier(value, depth = 0) {
|
|
45
|
+
if (depth > 8 || !value || typeof value !== 'object') return false;
|
|
46
|
+
if (Array.isArray(value)) return value.some((item) => hasRemoteReceiptIdentifier(item, depth + 1));
|
|
47
|
+
return Object.entries(value).some(([key, item]) =>
|
|
48
|
+
(/^(id|task_id|comment_id|task_url|url)$/i.test(key) && typeof item === 'string' && item.trim() !== '')
|
|
49
|
+
|| hasRemoteReceiptIdentifier(item, depth + 1));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function clickUpReceiptConfirmsSuccess(receipt) {
|
|
53
|
+
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) return false;
|
|
54
|
+
if (receipt.isError === true || receipt.success === false) return false;
|
|
55
|
+
if (typeof receipt.status === 'string' && /^(failed|error|unauthorized)$/i.test(receipt.status)) return false;
|
|
56
|
+
return receipt.success === true
|
|
57
|
+
|| (typeof receipt.status === 'string' && /^(success|ok|completed)$/i.test(receipt.status))
|
|
58
|
+
|| hasRemoteReceiptIdentifier(receipt);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function acknowledgeClickUpProjection({ projectDir, handoff_id, projection_id, receipt, clock = () => new Date() } = {}) {
|
|
62
|
+
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) throw Object.assign(new Error('receipt must be an object'), { code: 'INVALID_CLICKUP_RECEIPT' });
|
|
63
|
+
const { handoff } = loadRailHandoff({ projectDir, handoff_id });
|
|
64
|
+
const outboxPath = join(projectDir, '.chati/v2/tracking/clickup-outbox.json');
|
|
65
|
+
const outbox = JSON.parse(readFileSync(outboxPath, 'utf8'));
|
|
66
|
+
const projection = outbox.projections?.find((item) => item.projection_id === projection_id);
|
|
67
|
+
if (!projection) throw Object.assign(new Error(`projection ${projection_id} does not exist`), { code: 'PROJECTION_NOT_FOUND' });
|
|
68
|
+
const records = createProjectRailEngine({ projectDir, clock }).records;
|
|
69
|
+
if (projection.delivery_state === 'confirmed' && projection.receipt_ref) {
|
|
70
|
+
const tracking = createClickUpTrackingFromState({
|
|
71
|
+
projectDir, handoff, records, task_id: projection.task_id, clock, extra_verified_refs: [projection.receipt_ref],
|
|
72
|
+
});
|
|
73
|
+
return tracking.confirmExternalDelivery({ projection_id, receipt_ref: projection.receipt_ref });
|
|
74
|
+
}
|
|
75
|
+
if (!clickUpReceiptConfirmsSuccess(receipt)) {
|
|
76
|
+
throw Object.assign(new Error('receipt does not prove a successful ClickUp operation'), { code: 'CLICKUP_RECEIPT_UNCONFIRMED' });
|
|
77
|
+
}
|
|
78
|
+
const observed = clock();
|
|
79
|
+
const receivedAt = (observed instanceof Date ? observed : new Date(observed)).toISOString();
|
|
80
|
+
const safeReceipt = sanitizeClickUpReceipt(receipt);
|
|
81
|
+
const receiptMaterial = { schema_version: 1, projection_id, received_at: receivedAt, response: safeReceipt };
|
|
82
|
+
if (JSON.stringify(receiptMaterial).length > 64 * 1024) {
|
|
83
|
+
throw Object.assign(new Error('receipt exceeds 64 KiB after sanitization'), { code: 'CLICKUP_RECEIPT_TOO_LARGE' });
|
|
84
|
+
}
|
|
85
|
+
const digest = sha256(receiptMaterial);
|
|
86
|
+
const receiptPath = join(projectDir, '.chati/v2/tracking/receipts', `${digest}.json`);
|
|
87
|
+
mkdirSync(dirname(receiptPath), { recursive: true, mode: 0o700 });
|
|
88
|
+
const serialized = `${JSON.stringify(receiptMaterial, null, 2)}\n`;
|
|
89
|
+
if (!existsSync(receiptPath)) {
|
|
90
|
+
const temporary = `${receiptPath}.${process.pid}.tmp`;
|
|
91
|
+
writeFileSync(temporary, serialized, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
92
|
+
renameSync(temporary, receiptPath);
|
|
93
|
+
} else if (readFileSync(receiptPath, 'utf8') !== serialized) {
|
|
94
|
+
throw Object.assign(new Error('receipt digest collision'), { code: 'CLICKUP_RECEIPT_CONFLICT' });
|
|
95
|
+
}
|
|
96
|
+
const receiptRef = `clickup-receipt://sha256/${digest}`;
|
|
97
|
+
const tracking = createClickUpTrackingFromState({
|
|
98
|
+
projectDir, handoff, records, task_id: projection.task_id, clock, extra_verified_refs: [receiptRef],
|
|
99
|
+
});
|
|
100
|
+
return tracking.confirmExternalDelivery({ projection_id, receipt_ref: receiptRef });
|
|
101
|
+
}
|
|
@@ -87,8 +87,7 @@ export function compilePlanningHandoff({ projectDir, clock = () => new Date() }
|
|
|
87
87
|
};
|
|
88
88
|
if (sources.ux_path) source_artifacts.ux_ref = immutableFileRef(projectDir, sources.ux_path, 'PLANNING_SOURCE_ARTIFACTS_REQUIRED');
|
|
89
89
|
const ids = new Set();
|
|
90
|
-
const clickupRequired = installation.installation.
|
|
91
|
-
&& installation.installation.external_integrations?.clickup === 'required';
|
|
90
|
+
const clickupRequired = installation.installation.external_integrations?.clickup === 'required';
|
|
92
91
|
const tasks = planning.tasks.map((task) => {
|
|
93
92
|
if (!task || typeof task !== 'object' || typeof task.id !== 'string' || !task.id.trim() || ids.has(task.id)) fail('INVALID_PLANNING_TASK', 'each planning task needs a unique id');
|
|
94
93
|
ids.add(task.id);
|
|
@@ -85,8 +85,7 @@ function loadProjectRail({ projectDir, handoff_id, clock }) {
|
|
|
85
85
|
|
|
86
86
|
function internalTrackingInstallation(projectDir, clock) {
|
|
87
87
|
const artifact = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
|
|
88
|
-
return artifact?.installation?.
|
|
89
|
-
&& artifact.installation.external_integrations?.clickup === 'required';
|
|
88
|
+
return artifact?.installation?.external_integrations?.clickup === 'required';
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
function assertCompletionMetricsEvidence(metrics, reviews, acceptanceEvidenceRefs, records, attemptId) {
|
|
@@ -48,9 +48,18 @@ export function resolveRuntimeInvocationV2({ artifact, agent, binding, model_id,
|
|
|
48
48
|
if (!selectedCandidate) throw Object.assign(new Error(`No eligible installed model for ${agent}/${action}`), { code: 'NO_ELIGIBLE_MODEL' });
|
|
49
49
|
const selected = selectedCandidate.binding;
|
|
50
50
|
const model = selectedCandidate.model.model_id;
|
|
51
|
+
const defaultReasoning = selectedCandidate.model.tier === 'worker'
|
|
52
|
+
? 'low'
|
|
53
|
+
: selectedCandidate.model.tier === 'workhorse'
|
|
54
|
+
? 'medium'
|
|
55
|
+
: 'high';
|
|
51
56
|
const invocation = {
|
|
52
57
|
provider_id: selected.provider_id, harness_id: selected.harness_id, action,
|
|
53
|
-
model_pin: {
|
|
58
|
+
model_pin: {
|
|
59
|
+
model_id: model,
|
|
60
|
+
catalog_snapshot_ref: artifact.capability_snapshot.snapshot_id,
|
|
61
|
+
reasoning_configuration: reasoning_configuration ?? defaultReasoning,
|
|
62
|
+
},
|
|
54
63
|
};
|
|
55
64
|
assertEligibleInvocation({ installation: artifact.installation, snapshot: artifact.capability_snapshot, invocation, clock });
|
|
56
65
|
return Object.freeze(invocation);
|
|
@@ -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',
|