chati-dev 4.4.1 → 4.5.2
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/README.md +29 -27
- package/bin/chati.js +235 -1
- package/framework/agents/plan/tasks.md +46 -1
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +13 -16
- package/framework/context/governance.md +5 -5
- package/framework/context/root.md +5 -5
- package/framework/i18n/en.yaml +8 -2
- package/framework/i18n/es.yaml +8 -2
- package/framework/i18n/fr.yaml +8 -2
- package/framework/i18n/pt.yaml +8 -2
- package/framework/manifest.json +21 -21
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati.md +43 -12
- package/node_modules/@chati/browser-capability/README.md +10 -0
- package/node_modules/@chati/browser-capability/package.json +17 -0
- package/node_modules/@chati/browser-capability/src/index.js +165 -0
- package/node_modules/@chati/core/package.json +13 -0
- package/node_modules/@chati/core/src/index.js +111 -0
- package/node_modules/@chati/knowledge-context/package.json +17 -0
- package/node_modules/@chati/knowledge-context/src/index.js +202 -0
- package/node_modules/@chati/planning/package.json +17 -0
- package/node_modules/@chati/planning/src/index.js +367 -0
- package/node_modules/@chati/provider-registry/package.json +16 -0
- package/node_modules/@chati/provider-registry/src/index.js +148 -0
- package/node_modules/@chati/rail/README.md +24 -0
- package/node_modules/@chati/rail/package.json +19 -0
- package/node_modules/@chati/rail/src/index.js +437 -0
- package/node_modules/@chati/release-lane/README.md +24 -0
- package/node_modules/@chati/release-lane/package.json +17 -0
- package/node_modules/@chati/release-lane/src/index.js +172 -0
- package/node_modules/@chati/review-council/package.json +17 -0
- package/node_modules/@chati/review-council/src/index.js +264 -0
- package/node_modules/@chati/tracking-clickup/README.md +55 -0
- package/node_modules/@chati/tracking-clickup/package.json +17 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +293 -0
- package/package.json +25 -3
- package/src/config/ide-configs.js +11 -0
- package/src/context/domain-loader.js +1 -1
- package/src/dashboard/data-reader.js +1 -1
- package/src/executors/runner.js +1 -1
- package/src/installer/core.js +14 -13
- package/src/installer/provider-overlay.js +8 -15
- package/src/installer/scaffold-applier.js +1 -1
- package/src/installer/templates.js +12 -25
- package/src/installer/validator.js +5 -10
- package/src/installer-v2/catalog-client.js +165 -0
- package/src/installer-v2/index.js +304 -0
- package/src/installer-v2/model-catalog-envelope.json +278 -0
- package/src/installer-v2/model-catalog.json +130 -0
- package/src/installer-v2/model-catalog.sig +1 -0
- package/src/installer-v2/wizard-installation.js +116 -0
- package/src/intelligence/registry-manager.js +1 -1
- package/src/license/client.js +27 -1
- package/src/memory/session-digest.js +1 -1
- package/src/merger/yaml-merger.js +1 -1
- package/src/orchestrator/browser-runtime.js +25 -0
- package/src/orchestrator/cli.js +93 -8
- package/src/orchestrator/clickup-projection.js +84 -0
- package/src/orchestrator/clickup-runtime.js +13 -0
- package/src/orchestrator/knowledge-runtime.js +64 -0
- package/src/orchestrator/planning-runtime.js +127 -0
- package/src/orchestrator/rail-runtime.js +421 -0
- package/src/orchestrator/release-runtime.js +14 -0
- package/src/orchestrator/review-runtime.js +74 -0
- package/src/orchestrator/runtime-installation-v2.js +57 -0
- package/src/orchestrator/session-manager.js +1 -1
- package/src/telemetry/config.js +1 -1
- package/src/terminal/adapters/grok-adapter.js +16 -0
- package/src/terminal/adapters/index.js +1 -0
- package/src/terminal/cli-registry.js +20 -5
- package/src/terminal/prompt-builder.js +4 -2
- package/src/terminal/run-agent.js +3 -0
- package/src/terminal/run-parallel.js +21 -10
- package/src/terminal/spawner.js +7 -1
- package/src/terminal/team-task-list.js +1 -1
- package/src/upgrade/checker.js +1 -1
- package/src/upgrade/migrator.js +1 -1
- package/src/utils/config-parser.js +3 -8
- package/src/wizard/i18n.js +10 -4
- package/src/wizard/index.js +49 -17
- package/src/wizard/questions.js +50 -28
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sha256 } from '@chati/core';
|
|
4
|
+
import { RailEngine } from '@chati/rail';
|
|
5
|
+
import { validateSealedHandoff } from '@chati/planning';
|
|
6
|
+
import { loadRuntimeInstallationV2, resolveRuntimeInvocationV2 } from './runtime-installation-v2.js';
|
|
7
|
+
import { createProjectBrowserSession, launchProjectBrowserSession } from './browser-runtime.js';
|
|
8
|
+
import { enqueueClickUpProjectionFromState } from './clickup-projection.js';
|
|
9
|
+
|
|
10
|
+
export const RAIL_V2_DIR = '.chati/v2/rail';
|
|
11
|
+
|
|
12
|
+
function fail(code, message, cause) {
|
|
13
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
14
|
+
error.code = code;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function handoffPath(projectDir, handoffId) {
|
|
19
|
+
if (typeof handoffId !== 'string' || handoffId.trim() === '') fail('INVALID_HANDOFF_ID', 'handoff_id is required');
|
|
20
|
+
if (!/^[A-Za-z0-9._-]+$/.test(handoffId)) fail('INVALID_HANDOFF_ID', 'handoff_id contains unsupported path characters');
|
|
21
|
+
return join(projectDir, RAIL_V2_DIR, 'handoffs', `${handoffId}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function completionReference(handoffId, attemptId, completionId) {
|
|
25
|
+
for (const [label, value] of Object.entries({ handoff_id: handoffId, attempt_id: attemptId, completion_id: completionId })) {
|
|
26
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9._-]+$/.test(value)) fail('INVALID_COMPLETION_REFERENCE', `${label} contains unsupported reference characters`);
|
|
27
|
+
}
|
|
28
|
+
return `rail-completion://${handoffId}/${attemptId}/${completionId}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getRailPaths(projectDir) {
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
root: join(projectDir, RAIL_V2_DIR),
|
|
34
|
+
handoffs: join(projectDir, RAIL_V2_DIR, 'handoffs'),
|
|
35
|
+
journal: join(projectDir, RAIL_V2_DIR, 'journal.jsonl'),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Persist one sealed planning handoff. Conflicting material is never overwritten. */
|
|
40
|
+
export function persistRailHandoff({ projectDir, handoff }) {
|
|
41
|
+
const validated = validateSealedHandoff(handoff);
|
|
42
|
+
const path = handoffPath(projectDir, validated.handoff_id);
|
|
43
|
+
const content = `${JSON.stringify(validated, null, 2)}\n`;
|
|
44
|
+
if (existsSync(path)) {
|
|
45
|
+
const existing = readFileSync(path, 'utf8');
|
|
46
|
+
if (existing === content) return Object.freeze({ path, handoff: validated, written: false });
|
|
47
|
+
fail('HANDOFF_STORAGE_CONFLICT', `sealed handoff ${validated.handoff_id} already exists with different material`);
|
|
48
|
+
}
|
|
49
|
+
mkdirSync(getRailPaths(projectDir).handoffs, { recursive: true, mode: 0o700 });
|
|
50
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
51
|
+
try {
|
|
52
|
+
writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
53
|
+
linkSync(temporary, path);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error?.code === 'EEXIST' && existsSync(path) && readFileSync(path, 'utf8') === content) {
|
|
56
|
+
return Object.freeze({ path, handoff: validated, written: false });
|
|
57
|
+
}
|
|
58
|
+
if (error?.code === 'EEXIST') fail('HANDOFF_STORAGE_CONFLICT', `sealed handoff ${validated.handoff_id} already exists`, error);
|
|
59
|
+
throw error;
|
|
60
|
+
} finally {
|
|
61
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({ path, handoff: validated, written: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function loadRailHandoff({ projectDir, handoff_id }) {
|
|
67
|
+
const path = handoffPath(projectDir, handoff_id);
|
|
68
|
+
if (!existsSync(path)) fail('HANDOFF_NOT_FOUND', `no sealed handoff exists for ${handoff_id}`);
|
|
69
|
+
try {
|
|
70
|
+
return Object.freeze({ path, handoff: validateSealedHandoff(JSON.parse(readFileSync(path, 'utf8'))) });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error?.code) throw error;
|
|
73
|
+
return fail('INVALID_STORED_HANDOFF', `sealed handoff ${handoff_id} cannot be read`, error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createProjectRailEngine({ projectDir, clock = () => new Date() }) {
|
|
78
|
+
return new RailEngine({ journal_path: getRailPaths(projectDir).journal, clock });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function loadProjectRail({ projectDir, handoff_id, clock }) {
|
|
82
|
+
const stored = loadRailHandoff({ projectDir, handoff_id });
|
|
83
|
+
return Object.freeze({ handoff: stored.handoff, engine: createProjectRailEngine({ projectDir, ...(clock === undefined ? {} : { clock }) }) });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function internalTrackingInstallation(projectDir, clock) {
|
|
87
|
+
const artifact = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
|
|
88
|
+
return artifact?.installation?.profile === 'focus-ai-internal'
|
|
89
|
+
&& artifact.installation.external_integrations?.clickup === 'required';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function assertCompletionMetricsEvidence(metrics, reviews, acceptanceEvidenceRefs, records, attemptId) {
|
|
93
|
+
if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) fail('COMPLETION_METRICS_REQUIRED', 'internal merged completion requires human effort, AI processing and rework metrics');
|
|
94
|
+
const evidenceRefs = [metrics.actual_human_effort?.evidence_ref, metrics.actual_ai_processing?.evidence_ref];
|
|
95
|
+
if (!evidenceRefs.every((ref) => typeof ref === 'string' && acceptanceEvidenceRefs?.includes(ref))) {
|
|
96
|
+
fail('COMPLETION_METRICS_EVIDENCE_REQUIRED', 'effort metrics evidence must be included in acceptance_evidence_refs');
|
|
97
|
+
}
|
|
98
|
+
if (!Array.isArray(metrics.rework_review_refs) || !metrics.rework_review_refs.every((ref) => reviews?.includes(ref))) {
|
|
99
|
+
fail('COMPLETION_REWORK_EVIDENCE_REQUIRED', 'rework metric references must be included in completion reviews');
|
|
100
|
+
}
|
|
101
|
+
const journalReworkRefs = records
|
|
102
|
+
.filter((record) => record.type === 'review_decided'
|
|
103
|
+
&& record.payload?.attempt_id === attemptId
|
|
104
|
+
&& record.payload?.decision?.decision === 'normal-rework')
|
|
105
|
+
.map((record) => record.payload.review_ref);
|
|
106
|
+
if (metrics.rework_cycles !== journalReworkRefs.length
|
|
107
|
+
|| new Set(metrics.rework_review_refs).size !== metrics.rework_review_refs.length
|
|
108
|
+
|| !journalReworkRefs.every((ref) => metrics.rework_review_refs.includes(ref))) {
|
|
109
|
+
fail('COMPLETION_REWORK_JOURNAL_MISMATCH', 'rework metrics must exactly match normal-rework decisions in the canonical journal');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function trackingIntentDirectory(projectDir) {
|
|
114
|
+
return join(projectDir, '.chati/v2/tracking/pending-projections');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function trackingIntentPath(projectDir, intent) {
|
|
118
|
+
return join(trackingIntentDirectory(projectDir), `${sha256(intent).slice(0, 32)}.json`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function prepareTrackingIntent(projectDir, intent) {
|
|
122
|
+
const normalized = Object.freeze({ schema_version: 1, ...intent });
|
|
123
|
+
const path = trackingIntentPath(projectDir, normalized);
|
|
124
|
+
const content = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
125
|
+
mkdirSync(trackingIntentDirectory(projectDir), { recursive: true, mode: 0o700 });
|
|
126
|
+
if (existsSync(path)) {
|
|
127
|
+
if (readFileSync(path, 'utf8') !== content) fail('TRACKING_INTENT_CONFLICT', 'tracking intent path contains different material');
|
|
128
|
+
return path;
|
|
129
|
+
}
|
|
130
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
131
|
+
writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
132
|
+
renameSync(temporary, path);
|
|
133
|
+
return path;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function discardTrackingIntentWhenMutationAbsent({ intentPath, engine, matchesMutation }) {
|
|
137
|
+
if (!intentPath || !existsSync(intentPath)) return;
|
|
138
|
+
// If append reached the journal but checkpointing failed, the durable intent
|
|
139
|
+
// must survive so recovery can reconcile the two local stores. Only discard
|
|
140
|
+
// it when the in-memory canonical record set proves no mutation was appended.
|
|
141
|
+
if (!engine.records.some(matchesMutation)) unlinkSync(intentPath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Replays durable local projection intents from canonical RAIL journal state. */
|
|
145
|
+
export function replayPendingClickUpProjections({ projectDir, handoff_id, clock } = {}) {
|
|
146
|
+
const directory = trackingIntentDirectory(projectDir);
|
|
147
|
+
if (!existsSync(directory)) return Object.freeze([]);
|
|
148
|
+
const outcomes = [];
|
|
149
|
+
for (const filename of readdirSync(directory).filter((name) => name.endsWith('.json')).sort()) {
|
|
150
|
+
const path = join(directory, filename);
|
|
151
|
+
let intent;
|
|
152
|
+
try { intent = JSON.parse(readFileSync(path, 'utf8')); } catch (error) {
|
|
153
|
+
outcomes.push(Object.freeze({ path, state: 'blocked', error_code: 'TRACKING_INTENT_CORRUPT', message: error.message }));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (handoff_id && intent.handoff_id !== handoff_id) continue;
|
|
157
|
+
try {
|
|
158
|
+
const { handoff, engine } = loadProjectRail({ projectDir, handoff_id: intent.handoff_id, clock });
|
|
159
|
+
const task = handoff.tasks.find((item) => item.task_id === intent.task_id);
|
|
160
|
+
if (!task?.clickup_ref) fail('MISSING_CLICKUP_REFERENCE', 'internal tracking intent requires task.clickup_ref');
|
|
161
|
+
const claimRecord = engine.records.find((record) => record.type === 'attempt_claimed'
|
|
162
|
+
&& record.payload?.attempt?.attempt_id === intent.attempt_id
|
|
163
|
+
&& record.payload.attempt.task_id === intent.task_id);
|
|
164
|
+
if (!claimRecord) {
|
|
165
|
+
outcomes.push(Object.freeze({ path, state: 'waiting-for-journal' }));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
let operation = 'update';
|
|
169
|
+
let payload = { status: 'in_progress', started_at: claimRecord.payload.claim.lease.acquired_at };
|
|
170
|
+
let completion;
|
|
171
|
+
if (intent.completion_id) {
|
|
172
|
+
const completionRecord = engine.records.find((record) => record.type === 'attempt_completed'
|
|
173
|
+
&& record.payload?.completion?.completion_id === intent.completion_id
|
|
174
|
+
&& record.payload.completion.attempt_id === intent.attempt_id);
|
|
175
|
+
if (!completionRecord) {
|
|
176
|
+
outcomes.push(Object.freeze({ path, state: 'waiting-for-journal' }));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const stored = completionRecord.payload.completion;
|
|
180
|
+
completion = Object.freeze({
|
|
181
|
+
...stored,
|
|
182
|
+
event_id: completionRecord.event_id,
|
|
183
|
+
immutable_ref: completionReference(handoff.handoff_id, stored.attempt_id, stored.completion_id),
|
|
184
|
+
});
|
|
185
|
+
operation = stored.state === 'merged' ? 'close' : 'update';
|
|
186
|
+
payload = stored.state === 'merged'
|
|
187
|
+
? { status: 'done', end_date: stored.completed_at, ...stored.metrics }
|
|
188
|
+
: { status: 'ready_to_merge' };
|
|
189
|
+
}
|
|
190
|
+
const projection = enqueueClickUpProjectionFromState({
|
|
191
|
+
projectDir, handoff, records: engine.records, task_id: intent.task_id, attempt_id: intent.attempt_id,
|
|
192
|
+
operation, payload, ...(completion ? { completion } : {}), ...(clock === undefined ? {} : { clock }),
|
|
193
|
+
});
|
|
194
|
+
// Keep the intent as durable reconstruction material. The projection is
|
|
195
|
+
// idempotent, so an hourly reconciliation can rebuild a deleted outbox
|
|
196
|
+
// from the canonical journal without waiting for another task mutation.
|
|
197
|
+
outcomes.push(Object.freeze({ path, state: 'queued', projection_id: projection.projection_id }));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
outcomes.push(Object.freeze({ path, state: 'pending-local-replay', error_code: error.code || 'TRACKING_PROJECTION_FAILED', message: error.message }));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return Object.freeze(outcomes);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Hourly-safe local reconciliation entry point for the ClickUp outbox. */
|
|
206
|
+
export function reconcileProjectClickUpTracking({ projectDir, handoff_id, clock } = {}) {
|
|
207
|
+
const outcomes = replayPendingClickUpProjections({ projectDir, handoff_id, ...(clock === undefined ? {} : { clock }) });
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
state: outcomes.some((item) => item.state === 'blocked' || item.state === 'pending-local-replay') ? 'pending' : 'synchronized',
|
|
210
|
+
outcomes,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function attemptIdsForHandoff(records, handoffId) {
|
|
215
|
+
return new Set(records.filter((record) => record.type === 'attempt_claimed' && record.payload?.attempt?.handoff_id === handoffId)
|
|
216
|
+
.map((record) => record.payload.attempt.attempt_id));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function taskStates(engine, handoffId) {
|
|
220
|
+
const attemptIds = attemptIdsForHandoff(engine.records, handoffId);
|
|
221
|
+
const states = new Map();
|
|
222
|
+
for (const record of engine.records) {
|
|
223
|
+
if (record.type === 'attempt_claimed' && record.payload?.attempt?.handoff_id === handoffId) {
|
|
224
|
+
states.set(record.payload.attempt.task_id, { task_id: record.payload.attempt.task_id, attempt_id: record.payload.attempt.attempt_id, state: 'active' });
|
|
225
|
+
}
|
|
226
|
+
if (record.type === 'attempt_completed' && attemptIds.has(record.payload?.completion?.attempt_id)) {
|
|
227
|
+
const completion = record.payload.completion;
|
|
228
|
+
states.set(completion.task_id, { task_id: completion.task_id, attempt_id: completion.attempt_id, state: completion.state });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return states;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Returns the dependency-ready tasks from the project-local sealed handoff.
|
|
236
|
+
* A task becomes unblocked only after every predecessor is canonically merged,
|
|
237
|
+
* never merely claimed or marked ready_to_merge.
|
|
238
|
+
*/
|
|
239
|
+
export function selectReadyRailTasks({ projectDir, handoff_id, clock } = {}) {
|
|
240
|
+
const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
|
|
241
|
+
const states = taskStates(engine, handoff.handoff_id);
|
|
242
|
+
const merged = new Set([...states.values()].filter((item) => item.state === 'merged').map((item) => item.task_id));
|
|
243
|
+
const ready = handoff.tasks.filter((task) => !states.has(task.task_id)
|
|
244
|
+
&& task.dependency_ids.every((dependency) => merged.has(dependency)));
|
|
245
|
+
return Object.freeze({
|
|
246
|
+
handoff_id: handoff.handoff_id,
|
|
247
|
+
completed_task_ids: Object.freeze([...merged].sort()),
|
|
248
|
+
ready_task_ids: Object.freeze(ready.map((task) => task.task_id)),
|
|
249
|
+
blocked_task_ids: Object.freeze(handoff.tasks.filter((task) => !states.has(task.task_id) && !ready.includes(task)).map((task) => task.task_id)),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Computes the only local condition that may hand a project from RAIL to the
|
|
255
|
+
* Release Lane. A `ready` result is evidence of merged task completions, not
|
|
256
|
+
* a release authorization and never invokes a deployment adapter.
|
|
257
|
+
*/
|
|
258
|
+
export function selectRailReleaseReadiness({ projectDir, handoff_id, clock } = {}) {
|
|
259
|
+
const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
|
|
260
|
+
const merged = new Map();
|
|
261
|
+
const attemptIds = attemptIdsForHandoff(engine.records, handoff.handoff_id);
|
|
262
|
+
for (const record of engine.records) {
|
|
263
|
+
if (record.type !== 'attempt_completed' || record.payload?.completion?.state !== 'merged' || !attemptIds.has(record.payload.completion.attempt_id)) continue;
|
|
264
|
+
const completion = record.payload.completion;
|
|
265
|
+
merged.set(completion.task_id, completionReference(handoff.handoff_id, completion.attempt_id, completion.completion_id));
|
|
266
|
+
}
|
|
267
|
+
const missing_task_ids = handoff.tasks.filter((task) => !merged.has(task.task_id)).map((task) => task.task_id);
|
|
268
|
+
return Object.freeze({
|
|
269
|
+
handoff_id: handoff.handoff_id,
|
|
270
|
+
state: missing_task_ids.length === 0 ? 'ready' : 'not-ready',
|
|
271
|
+
missing_task_ids: Object.freeze(missing_task_ids),
|
|
272
|
+
source_completion_refs: Object.freeze(handoff.tasks.map((task) => merged.get(task.task_id)).filter(Boolean)),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Claims one dependency-ready RAIL task and invokes the explicitly injected
|
|
278
|
+
* harness adapter with the installation-selected binding. There is no provider
|
|
279
|
+
* fallback and no direct CLI invocation in this boundary.
|
|
280
|
+
*/
|
|
281
|
+
export function dispatchReadyRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, harness_process_id, adapter, browser_request, git_dir = projectDir, lease_ttl_ms, clock } = {}) {
|
|
282
|
+
if (!adapter || typeof adapter.invoke !== 'function') fail('HARNESS_ADAPTER_UNAVAILABLE', 'RAIL dispatch requires an injected harness adapter');
|
|
283
|
+
const installation = loadRuntimeInstallationV2(projectDir, { ...(clock === undefined ? {} : { clock }) });
|
|
284
|
+
if (!installation) fail('V2_INSTALLATION_REQUIRED', 'RAIL dispatch requires a v2 installation');
|
|
285
|
+
const selection = selectReadyRailTasks({ projectDir, handoff_id, clock });
|
|
286
|
+
if (!selection.ready_task_ids.includes(task_id)) fail('TASK_NOT_DISPATCHABLE', 'task is not dependency-ready for dispatch', { task_id, ready_task_ids: selection.ready_task_ids });
|
|
287
|
+
const { handoff } = loadProjectRail({ projectDir, handoff_id, clock });
|
|
288
|
+
const task = handoff.tasks.find((item) => item.task_id === task_id);
|
|
289
|
+
if (!task.execution_binding || typeof task.execution_binding !== 'object'
|
|
290
|
+
|| ['provider_id', 'harness_id', 'model_id', 'reasoning_configuration']
|
|
291
|
+
.some((field) => typeof task.execution_binding[field] !== 'string' || !task.execution_binding[field].trim())) {
|
|
292
|
+
fail('TASK_EXECUTION_BINDING_REQUIRED', 'v2 RAIL dispatch requires a complete explicit task execution binding');
|
|
293
|
+
}
|
|
294
|
+
if (task.requires_browser === true && (!browser_request || typeof browser_request !== 'object')) fail('BROWSER_PREFLIGHT_REQUIRED', 'browser task dispatch requires a complete browser preflight request');
|
|
295
|
+
const browser = task.requires_browser === true
|
|
296
|
+
? launchProjectBrowserSession({ session: createProjectBrowserSession({ task, request: browser_request }), request: browser_request })
|
|
297
|
+
: null;
|
|
298
|
+
const selectedBinding = { provider_id: task.execution_binding.provider_id, harness_id: task.execution_binding.harness_id };
|
|
299
|
+
const invocationBase = resolveRuntimeInvocationV2({
|
|
300
|
+
artifact: installation,
|
|
301
|
+
agent: 'dev',
|
|
302
|
+
binding: selectedBinding,
|
|
303
|
+
model_id: task.execution_binding.model_id,
|
|
304
|
+
reasoning_configuration: task.execution_binding.reasoning_configuration,
|
|
305
|
+
...(clock === undefined ? {} : { clock }),
|
|
306
|
+
});
|
|
307
|
+
if (adapter.harness_id && adapter.harness_id !== invocationBase.harness_id) fail('HARNESS_MISMATCH', 'selected installation binding does not match the injected adapter');
|
|
308
|
+
const claim = claimRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, harness_process_id, completed_task_ids: selection.completed_task_ids, git_dir, ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }), ...(clock === undefined ? {} : { clock }) });
|
|
309
|
+
const invocation = Object.freeze({
|
|
310
|
+
...invocationBase,
|
|
311
|
+
invocation_id: `rail-invocation-${attempt_id}`,
|
|
312
|
+
attempt_id,
|
|
313
|
+
tool_policy_ref: task.routing_constraints_ref,
|
|
314
|
+
input_ref: task.execution_scope_ref,
|
|
315
|
+
expected_output_schema: 'rail-task-execution/v1',
|
|
316
|
+
});
|
|
317
|
+
const receipt = adapter.invoke({ installation: installation.installation, snapshot: installation.capability_snapshot, invocation });
|
|
318
|
+
return Object.freeze({ claim, invocation, receipt, task_id, handoff_id, browser_manifest: browser?.manifest ?? null });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Entry point used by the CHATI runtime after a sealed planning handoff is
|
|
323
|
+
* present. It creates an auditable RAIL claim before any task executor runs.
|
|
324
|
+
*/
|
|
325
|
+
export function claimRailTask({ projectDir, handoff_id, task_id, attempt_id, operator_id, harness_process_id, git_dir = projectDir, lease_ttl_ms, clock } = {}) {
|
|
326
|
+
const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
|
|
327
|
+
const states = taskStates(engine, handoff.handoff_id);
|
|
328
|
+
const completed_task_ids = [...states.values()].filter((item) => item.state === 'merged').map((item) => item.task_id);
|
|
329
|
+
const trackingRequired = internalTrackingInstallation(projectDir, clock);
|
|
330
|
+
const task = handoff.tasks.find((item) => item.task_id === task_id);
|
|
331
|
+
if (trackingRequired && !task?.clickup_ref) fail('MISSING_CLICKUP_REFERENCE', 'internal RAIL task requires clickup_ref before claim');
|
|
332
|
+
const intentPath = trackingRequired ? prepareTrackingIntent(projectDir, { handoff_id, task_id, attempt_id, event: 'claim' }) : null;
|
|
333
|
+
let claim;
|
|
334
|
+
try {
|
|
335
|
+
claim = engine.claimTask({
|
|
336
|
+
handoff, task_id, attempt_id, operator_id, harness_process_id,
|
|
337
|
+
completed_task_ids, git_dir, ...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
|
|
338
|
+
});
|
|
339
|
+
} catch (error) {
|
|
340
|
+
discardTrackingIntentWhenMutationAbsent({
|
|
341
|
+
intentPath,
|
|
342
|
+
engine,
|
|
343
|
+
matchesMutation: (record) => record.type === 'attempt_claimed'
|
|
344
|
+
&& record.payload?.attempt?.attempt_id === attempt_id
|
|
345
|
+
&& record.payload.attempt.task_id === task_id,
|
|
346
|
+
});
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
const tracking_projection = trackingRequired
|
|
350
|
+
? replayPendingClickUpProjections({ projectDir, handoff_id, ...(clock === undefined ? {} : { clock }) }).find((item) => item.path === intentPath)
|
|
351
|
+
: undefined;
|
|
352
|
+
return Object.freeze({ ...claim, ...(tracking_projection ? { tracking_projection } : {}) });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Renew a claim lease against the local project journal and sealed handoff. */
|
|
356
|
+
export function heartbeatRailTask({ projectDir, handoff_id, attempt_id, claim_id, operator_id, lease_ttl_ms, clock } = {}) {
|
|
357
|
+
const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
|
|
358
|
+
return engine.heartbeat({
|
|
359
|
+
attempt_id, claim_id, operator_id,
|
|
360
|
+
...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Recover an expired attempt only after the caller supplies explicit reconciliation evidence. */
|
|
365
|
+
export function recoverRailTask({ projectDir, handoff_id, attempt_id, operator_id, harness_process_id, reconciliation, git_dir = projectDir, lease_ttl_ms, clock } = {}) {
|
|
366
|
+
const { engine } = loadProjectRail({ projectDir, handoff_id, clock });
|
|
367
|
+
return engine.recoverAttempt({
|
|
368
|
+
attempt_id, operator_id, harness_process_id, reconciliation, git_dir,
|
|
369
|
+
...(lease_ttl_ms === undefined ? {} : { lease_ttl_ms }),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Write an immutable C-06 completion record. Git, CI, review and acceptance
|
|
375
|
+
* evidence are validated by RailEngine against the project-local journal.
|
|
376
|
+
*/
|
|
377
|
+
export function completeRailTask({ projectDir, handoff_id, completion_id, attempt_id, claim_id, operator_id, state, git, ci, reviews, acceptance_evidence_refs, completed_at, metrics, clock } = {}) {
|
|
378
|
+
const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
|
|
379
|
+
const trackingRequired = internalTrackingInstallation(projectDir, clock);
|
|
380
|
+
if (trackingRequired && state === 'merged') assertCompletionMetricsEvidence(metrics, reviews, acceptance_evidence_refs, engine.records, attempt_id);
|
|
381
|
+
const taskId = engine.records.find((record) => record.type === 'attempt_claimed' && record.payload?.attempt?.attempt_id === attempt_id)?.payload?.attempt?.task_id;
|
|
382
|
+
const task = handoff.tasks.find((item) => item.task_id === taskId);
|
|
383
|
+
if (trackingRequired && !task?.clickup_ref) fail('MISSING_CLICKUP_REFERENCE', 'internal RAIL task requires clickup_ref before completion');
|
|
384
|
+
const intentPath = trackingRequired ? prepareTrackingIntent(projectDir, { handoff_id, task_id: taskId, attempt_id, completion_id, state, event: 'completion' }) : null;
|
|
385
|
+
let completion;
|
|
386
|
+
try {
|
|
387
|
+
completion = engine.completeAttempt({
|
|
388
|
+
completion_id, attempt_id, claim_id, operator_id, state, git: { ...git, git_dir: projectDir }, ci, reviews, acceptance_evidence_refs,
|
|
389
|
+
...(completed_at === undefined ? {} : { completed_at }), ...(metrics === undefined ? {} : { metrics }), handoff,
|
|
390
|
+
});
|
|
391
|
+
} catch (error) {
|
|
392
|
+
discardTrackingIntentWhenMutationAbsent({
|
|
393
|
+
intentPath,
|
|
394
|
+
engine,
|
|
395
|
+
matchesMutation: (record) => record.type === 'attempt_completed'
|
|
396
|
+
&& record.payload?.completion?.completion_id === completion_id
|
|
397
|
+
&& record.payload.completion.attempt_id === attempt_id,
|
|
398
|
+
});
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
const result = Object.freeze({ ...completion, immutable_ref: completionReference(handoff.handoff_id, completion.attempt_id, completion.completion_id) });
|
|
402
|
+
const tracking_projection = trackingRequired
|
|
403
|
+
? replayPendingClickUpProjections({ projectDir, handoff_id, ...(clock === undefined ? {} : { clock }) }).find((item) => item.path === intentPath)
|
|
404
|
+
: undefined;
|
|
405
|
+
return Object.freeze({ ...result, ...(tracking_projection ? { tracking_projection } : {}) });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Resolves only a completion that exists in the immutable project-local journal. */
|
|
409
|
+
export function resolveRailCompletionReference({ projectDir, handoff_id, immutable_ref, clock } = {}) {
|
|
410
|
+
const { handoff, engine } = loadProjectRail({ projectDir, handoff_id, clock });
|
|
411
|
+
const prefix = `rail-completion://${handoff.handoff_id}/`;
|
|
412
|
+
if (typeof immutable_ref !== 'string' || !immutable_ref.startsWith(prefix)) fail('INVALID_COMPLETION_REFERENCE', 'completion reference is not bound to the sealed handoff');
|
|
413
|
+
const pieces = immutable_ref.slice(prefix.length).split('/');
|
|
414
|
+
if (pieces.length !== 2) fail('INVALID_COMPLETION_REFERENCE', 'completion reference has an invalid shape');
|
|
415
|
+
const [attempt_id, completion_id] = pieces;
|
|
416
|
+
const record = engine.records.find((item) => item.type === 'attempt_completed'
|
|
417
|
+
&& item.payload?.completion?.attempt_id === attempt_id
|
|
418
|
+
&& item.payload.completion.completion_id === completion_id);
|
|
419
|
+
if (!record) fail('COMPLETION_NOT_FOUND', 'completion reference is absent from the project-local journal');
|
|
420
|
+
return Object.freeze({ ...record.payload.completion, immutable_ref });
|
|
421
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReleaseLane } from '@chati/release-lane';
|
|
2
|
+
import { resolveRailCompletionReference } from './rail-runtime.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* G3 local release authority. The returned ReleaseLane accepts only its own
|
|
6
|
+
* FakeReleaseTransport capability and resolves completion evidence from the
|
|
7
|
+
* project-local RAIL journal. It has no external deployment adapter.
|
|
8
|
+
*/
|
|
9
|
+
export function createProjectReleaseLane({ projectDir, handoff_id, clock = () => new Date() } = {}) {
|
|
10
|
+
return new ReleaseLane({
|
|
11
|
+
clock,
|
|
12
|
+
verify_completion: (immutable_ref) => resolveRailCompletionReference({ projectDir, handoff_id, immutable_ref, clock }),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { acceptReview, decideReviewProgress } from '@chati/review-council';
|
|
4
|
+
import { loadRuntimeInstallationV2 } from './runtime-installation-v2.js';
|
|
5
|
+
import { createProjectRailEngine, loadRailHandoff } from './rail-runtime.js';
|
|
6
|
+
|
|
7
|
+
function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
|
|
8
|
+
function safeId(value, label) {
|
|
9
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9._-]+$/.test(value)) fail('INVALID_REVIEW_STORAGE_ID', `${label} must contain only letters, digits, dot, underscore or hyphen`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function reviewDir(projectDir, attemptId) { return join(projectDir, '.chati/v2/rail/reviews', safeId(attemptId, 'attempt_id')); }
|
|
13
|
+
function reviewPath(projectDir, attemptId, reviewId) { return join(reviewDir(projectDir, attemptId), `${safeId(reviewId, 'review_id')}.json`); }
|
|
14
|
+
|
|
15
|
+
function readReviewEvidence(projectDir, attemptId) {
|
|
16
|
+
const directory = reviewDir(projectDir, attemptId);
|
|
17
|
+
if (!existsSync(directory)) return [];
|
|
18
|
+
return readdirSync(directory).filter((name) => name.endsWith('.json')).sort().map((name) => {
|
|
19
|
+
try { return JSON.parse(readFileSync(join(directory, name), 'utf8')); }
|
|
20
|
+
catch { return fail('INVALID_STORED_REVIEW', `review evidence cannot be read: ${name}`); }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveAttemptLineage(projectDir, attemptId, clock) {
|
|
25
|
+
const engine = createProjectRailEngine({ projectDir, clock });
|
|
26
|
+
const claimRecord = engine.records.find((record) => record.type === 'attempt_claimed' && record.payload?.attempt?.attempt_id === attemptId);
|
|
27
|
+
if (!claimRecord) fail('REVIEW_ATTEMPT_NOT_CLAIMED', `review attempt ${attemptId} has no project-local RAIL claim`);
|
|
28
|
+
const attempt = claimRecord.payload.attempt;
|
|
29
|
+
const { handoff } = loadRailHandoff({ projectDir, handoff_id: attempt.handoff_id });
|
|
30
|
+
if (!handoff.tasks.some((task) => task.task_id === attempt.task_id)) fail('REVIEW_ATTEMPT_TASK_NOT_IN_HANDOFF', `claimed task ${attempt.task_id} is absent from its sealed handoff`);
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
handoff_id: handoff.handoff_id,
|
|
33
|
+
handoff_manifest_digest: handoff.seal.manifest_digest,
|
|
34
|
+
task_id: attempt.task_id,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Stores a selected-only review record and derives the next rework decision. */
|
|
39
|
+
export function recordRailReview({ projectDir, review, invocation, clock = () => new Date() } = {}) {
|
|
40
|
+
const artifact = loadRuntimeInstallationV2(projectDir, { clock });
|
|
41
|
+
if (!artifact) fail('V2_INSTALLATION_REQUIRED', 'RAIL review requires a v2 installation');
|
|
42
|
+
const validated = acceptReview({ review, invocation, installation: artifact.installation, snapshot: artifact.capability_snapshot, clock });
|
|
43
|
+
const lineage = resolveAttemptLineage(projectDir, validated.attempt_id, clock);
|
|
44
|
+
const path = reviewPath(projectDir, validated.attempt_id, validated.review_id);
|
|
45
|
+
const content = `${JSON.stringify({ review: validated, invocation, lineage }, null, 2)}\n`;
|
|
46
|
+
const stored = readReviewEvidence(projectDir, validated.attempt_id);
|
|
47
|
+
const matching = stored.find((entry) => entry.review?.review_id === validated.review_id);
|
|
48
|
+
if (matching && readFileSync(path, 'utf8') !== content) fail('REVIEW_STORAGE_CONFLICT', `review ${validated.review_id} already exists with different material`);
|
|
49
|
+
const prior = stored.filter((entry) => entry.review?.review_id !== validated.review_id);
|
|
50
|
+
const decision = decideReviewProgress({
|
|
51
|
+
prior_review_evidence: prior, current_review: validated, reviewer_invocation: invocation,
|
|
52
|
+
installation: artifact.installation, snapshot: artifact.capability_snapshot, clock,
|
|
53
|
+
});
|
|
54
|
+
const review_ref = `rail-review://${lineage.handoff_id}/${validated.attempt_id}/${validated.review_id}`;
|
|
55
|
+
let written = false;
|
|
56
|
+
if (!matching) {
|
|
57
|
+
mkdirSync(reviewDir(projectDir, validated.attempt_id), { recursive: true, mode: 0o700 });
|
|
58
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
59
|
+
writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
60
|
+
try {
|
|
61
|
+
linkSync(temporary, path);
|
|
62
|
+
written = true;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error?.code !== 'EEXIST' || readFileSync(path, 'utf8') !== content) fail('REVIEW_STORAGE_CONFLICT', `review ${validated.review_id} raced with different material`);
|
|
65
|
+
} finally {
|
|
66
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const journal = createProjectRailEngine({ projectDir, clock }).recordReviewDecision({
|
|
70
|
+
attempt_id: validated.attempt_id, review_ref, decision, review: validated, invocation,
|
|
71
|
+
prior_review_evidence: prior, installation: artifact.installation, snapshot: artifact.capability_snapshot,
|
|
72
|
+
});
|
|
73
|
+
return Object.freeze({ path, review_ref, lineage, review: validated, decision, journal, written });
|
|
74
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { INSTALLATION_ARTIFACT_PATH, doctorV2 } from '../installer-v2/index.js';
|
|
4
|
+
import { assertEligibleInvocation } from '@chati/provider-registry';
|
|
5
|
+
|
|
6
|
+
const AGENT_ACTIONS = Object.freeze({
|
|
7
|
+
'greenfield-wu': 'discovery', 'brownfield-wu': 'discovery', brief: 'discovery',
|
|
8
|
+
detail: 'planning', architect: 'planning', ux: 'planning', phases: 'planning', tasks: 'planning',
|
|
9
|
+
'qa-planning': 'review', dev: 'build', 'qa-implementation': 'review', 'qa-visual': 'review',
|
|
10
|
+
devops: 'build',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function loadRuntimeInstallationV2(projectDir, { clock = () => new Date() } = {}) {
|
|
14
|
+
const path = join(projectDir, INSTALLATION_ARTIFACT_PATH);
|
|
15
|
+
if (!existsSync(path)) return null;
|
|
16
|
+
let artifact;
|
|
17
|
+
try { artifact = JSON.parse(readFileSync(path, 'utf8')); } catch (error) {
|
|
18
|
+
throw Object.assign(new Error('Invalid v2 installation artifact'), { code: 'INVALID_INSTALLATION_ARTIFACT', cause: error });
|
|
19
|
+
}
|
|
20
|
+
const doctor = doctorV2(artifact, { clock });
|
|
21
|
+
if (!doctor.passed) throw Object.assign(new Error('V2 installation artifact failed doctor'), { code: doctor.checks[0]?.code || 'INVALID_INSTALLATION_ARTIFACT' });
|
|
22
|
+
return Object.freeze(artifact);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveRuntimeInvocationV2({ artifact, agent, binding, model_id, reasoning_configuration, clock = () => new Date() }) {
|
|
26
|
+
const action = AGENT_ACTIONS[agent];
|
|
27
|
+
if (!action) throw Object.assign(new Error(`No v2 action mapping for agent ${agent}`), { code: 'UNMAPPED_AGENT_ACTION' });
|
|
28
|
+
const candidates = artifact.installation.enabled_providers.flatMap((candidateBinding) => {
|
|
29
|
+
if (binding && (candidateBinding.provider_id !== binding.provider_id || candidateBinding.harness_id !== binding.harness_id)) return [];
|
|
30
|
+
return candidateBinding.allowed_models.flatMap((candidateModelId) => {
|
|
31
|
+
if (model_id && candidateModelId !== model_id) return [];
|
|
32
|
+
const catalogModel = artifact.capability_snapshot.models.find((candidate) =>
|
|
33
|
+
candidate.provider_id === candidateBinding.provider_id && candidate.model_id === candidateModelId
|
|
34
|
+
);
|
|
35
|
+
if (!catalogModel?.actions.includes(action)) return [];
|
|
36
|
+
return [{
|
|
37
|
+
binding: candidateBinding,
|
|
38
|
+
model: catalogModel,
|
|
39
|
+
priority: catalogModel.routing_priority?.[agent] ?? catalogModel.routing_priority?.[action] ?? 0,
|
|
40
|
+
}];
|
|
41
|
+
});
|
|
42
|
+
}).sort((left, right) =>
|
|
43
|
+
right.priority - left.priority
|
|
44
|
+
|| (right.model.adjudication_priority ?? 0) - (left.model.adjudication_priority ?? 0)
|
|
45
|
+
|| `${left.binding.harness_id}:${left.model.model_id}`.localeCompare(`${right.binding.harness_id}:${right.model.model_id}`)
|
|
46
|
+
);
|
|
47
|
+
const selectedCandidate = candidates[0];
|
|
48
|
+
if (!selectedCandidate) throw Object.assign(new Error(`No eligible installed model for ${agent}/${action}`), { code: 'NO_ELIGIBLE_MODEL' });
|
|
49
|
+
const selected = selectedCandidate.binding;
|
|
50
|
+
const model = selectedCandidate.model.model_id;
|
|
51
|
+
const invocation = {
|
|
52
|
+
provider_id: selected.provider_id, harness_id: selected.harness_id, action,
|
|
53
|
+
model_pin: { model_id: model, catalog_snapshot_ref: artifact.capability_snapshot.snapshot_id, reasoning_configuration },
|
|
54
|
+
};
|
|
55
|
+
assertEligibleInvocation({ installation: artifact.installation, snapshot: artifact.capability_snapshot, invocation, clock });
|
|
56
|
+
return Object.freeze(invocation);
|
|
57
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* agent completions, and session validation for the Chati.dev pipeline.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import yaml from 'js-yaml';
|
|
8
|
+
import * as yaml from 'js-yaml';
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'fs';
|
|
10
10
|
import { join, dirname } from 'path';
|
|
11
11
|
|
package/src/telemetry/config.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
11
11
|
import { randomUUID } from 'crypto';
|
|
12
|
-
import yaml from 'js-yaml';
|
|
12
|
+
import * as yaml from 'js-yaml';
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// Config Management
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build CLI adapter.
|
|
3
|
+
*
|
|
4
|
+
* Grok's supported headless form is `grok -p <prompt> -m <model>`. The prompt
|
|
5
|
+
* deliberately remains an argument here because `-p` is the documented
|
|
6
|
+
* non-interactive interface. This adapter does not silently select a model.
|
|
7
|
+
*/
|
|
8
|
+
export function buildCommand(config, provider) {
|
|
9
|
+
const args = [...provider.baseArgs];
|
|
10
|
+
if (config.model) {
|
|
11
|
+
const resolvedModel = provider.modelMap[config.model] || config.model;
|
|
12
|
+
args.push(provider.modelFlag, resolvedModel);
|
|
13
|
+
}
|
|
14
|
+
args.push('-p', config.prompt || '');
|
|
15
|
+
return { command: provider.command, args, stdinPrompt: null };
|
|
16
|
+
}
|