chati-dev 4.5.15 → 4.5.27
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 +8 -3
- package/bin/chati.js +124 -66
- package/framework/agents/build/dev.md +5 -5
- package/framework/agents/deploy/devops.md +7 -7
- package/framework/agents/discover/brief.md +4 -4
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +3 -3
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +2 -2
- package/framework/agents/plan/tasks.md +2 -2
- package/framework/agents/plan/ux.md +2 -2
- package/framework/agents/quality/qa-implementation.md +11 -10
- package/framework/agents/quality/qa-planning.md +3 -3
- package/framework/agents/quality/qa-visual.md +1 -1
- package/framework/config.yaml +3 -3
- package/framework/constitution.md +18 -18
- package/framework/context/protocols.md +2 -2
- package/framework/context/quality.md +1 -1
- package/framework/context/root.md +4 -4
- package/framework/data/entity-registry.yaml +1 -1
- package/framework/domains/agents/orchestrator.yaml +2 -2
- package/framework/domains/constitution.yaml +1 -1
- package/framework/hooks/advance-trigger.js +4 -6
- package/framework/hooks/git-push-authority.js +45 -37
- package/framework/hooks/mode-governance.js +149 -40
- package/framework/hooks/model-governance.js +13 -20
- package/framework/hooks/prism-engine.js +74 -92
- package/framework/hooks/reasoning-escalator.js +23 -40
- package/framework/hooks/session-digest.js +12 -13
- package/framework/hooks/session-reader.js +224 -0
- package/framework/hooks/session-writer.js +195 -0
- package/framework/hooks/team-quality-gate.js +34 -24
- package/framework/i18n/en.yaml +2 -2
- package/framework/i18n/es.yaml +2 -2
- package/framework/i18n/fr.yaml +2 -2
- package/framework/i18n/pt.yaml +2 -2
- package/framework/intelligence/context-engine.md +4 -4
- package/framework/intelligence/memory-layer.md +1 -1
- package/framework/manifest.json +129 -119
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +278 -24
- package/framework/orchestrator/chati.md +250 -68
- package/framework/schemas/session.schema.json +21 -1
- package/framework/tasks/orchestrator-deviation.md +1 -1
- package/framework/tasks/orchestrator-escalate.md +1 -1
- package/framework/tasks/orchestrator-handoff.md +6 -6
- package/framework/tasks/orchestrator-health.md +5 -9
- package/framework/tasks/orchestrator-mode-switch.md +3 -7
- package/framework/tasks/orchestrator-resume.md +10 -14
- package/framework/tasks/orchestrator-route.md +3 -3
- package/framework/tasks/orchestrator-spawn-terminal.md +1 -1
- package/framework/tasks/orchestrator-status.md +9 -9
- package/framework/tasks/orchestrator-suggest-mode.md +1 -1
- package/framework/tasks/qa-impl-consolidate.md +2 -2
- package/framework/tasks/qa-impl-performance-test.md +4 -4
- package/framework/tasks/qa-impl-regression-check.md +4 -4
- package/framework/tasks/qa-impl-sast-scan.md +1 -1
- package/framework/tasks/qa-impl-test-execute.md +1 -1
- package/framework/tasks/qa-impl-verdict.md +2 -2
- package/framework/tasks/qa-planning-consolidate.md +3 -3
- package/framework/tasks/qa-planning-coverage-plan.md +2 -2
- package/framework/tasks/qa-planning-gate-define.md +4 -4
- package/framework/tasks/qa-planning-risk-matrix.md +4 -4
- package/framework/tasks/qa-planning-test-strategy.md +2 -2
- package/node_modules/@chati/browser-capability/src/index.js +12 -2
- package/node_modules/@chati/planning/src/index.js +24 -4
- package/node_modules/@chati/provider-registry/src/index.js +11 -0
- package/node_modules/@chati/rail/src/index.js +1967 -83
- package/node_modules/@chati/release-lane/README.md +12 -8
- package/node_modules/@chati/release-lane/package.json +1 -1
- package/node_modules/@chati/release-lane/src/index.js +1426 -58
- package/node_modules/@chati/review-council/src/index.js +63 -0
- package/node_modules/@chati/tracking-clickup/README.md +13 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +690 -30
- package/package-artifact-manifest.json +1 -0
- package/package-artifact-manifest.sig +1 -0
- package/package.json +16 -7
- package/scripts/verify-real-harness-e2e.js +1581 -0
- package/src/config/framework-adapter.js +4 -4
- package/src/installer/core.js +157 -56
- package/src/installer/manifest.js +140 -9
- package/src/installer/package-artifact.js +249 -0
- package/src/installer/templates.js +91 -23
- package/src/installer-v2/catalog-client.js +531 -23
- package/src/installer-v2/index.js +91 -34
- package/src/installer-v2/installation-authority.js +327 -0
- package/src/installer-v2/provider-executable.js +235 -0
- package/src/installer-v2/wizard-installation.js +1 -1
- package/src/intelligence/timeline.js +3 -1
- package/src/orchestrator/browser-runtime.js +44 -13
- package/src/orchestrator/cli.js +1616 -151
- package/src/orchestrator/clickup-projection.js +43 -1
- package/src/orchestrator/clickup-runtime.js +355 -39
- package/src/orchestrator/doctor.js +87 -4
- package/src/orchestrator/index.js +16 -0
- package/src/orchestrator/planning-runtime.js +20 -2
- package/src/orchestrator/rail-adjudication-evidence.js +234 -0
- package/src/orchestrator/rail-evidence-authority.js +147 -0
- package/src/orchestrator/rail-execution-evidence.js +45 -0
- package/src/orchestrator/rail-runtime.js +836 -56
- package/src/orchestrator/release-runtime.js +65 -6
- package/src/orchestrator/review-runtime.js +186 -41
- package/src/orchestrator/runtime-installation-v2.js +236 -20
- package/src/orchestrator/session-manager.js +1249 -52
- package/src/terminal/adapters/claude-adapter.js +3 -1
- package/src/terminal/adapters/codex-adapter.js +2 -0
- package/src/terminal/adapters/grok-adapter.js +7 -4
- package/src/terminal/handoff-parser.js +19 -1
- package/src/terminal/prompt-builder.js +10 -1
- package/src/terminal/provider-preflight.js +36 -3
- package/src/terminal/rail-execution-worktree.js +213 -0
- package/src/terminal/rail-prompts.js +169 -0
- package/src/terminal/rail-readonly-workspace.js +324 -0
- package/src/terminal/run-agent.js +434 -16
- package/src/terminal/run-parallel.js +5 -0
- package/src/terminal/run-rail-adjudication.js +323 -0
- package/src/terminal/run-rail-review.js +291 -0
- package/src/terminal/run-rail-rework.js +325 -0
- package/src/terminal/run-rail-task.js +380 -0
- package/src/terminal/run-team.js +5 -0
- package/src/terminal/spawner.js +1225 -77
- package/src/utils/schema-validator.js +5 -0
- package/src/wizard/index.js +3 -2
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, rmSync } from 'fs';
|
|
19
|
+
import { isAbsolute, relative, resolve } from 'path';
|
|
17
20
|
import { buildAgentPrompt } from './prompt-builder.js';
|
|
18
21
|
import { isTransientFailure, spawnTerminalWithRetry } from './spawner.js';
|
|
22
|
+
import { getWriteScope } from './isolation.js';
|
|
19
23
|
import { parseAgentOutput } from './handoff-parser.js';
|
|
20
24
|
import { checkProviderReadiness } from './provider-preflight.js';
|
|
21
25
|
import { createCostTracker } from './cost-tracker.js';
|
|
@@ -23,6 +27,37 @@ import { getRateLimiter } from './rate-limiter.js';
|
|
|
23
27
|
import { initCollector, track as telemetryTrack, flush as telemetryFlush } from '../telemetry/collector.js';
|
|
24
28
|
import { sendEvents } from '../telemetry/sender.js';
|
|
25
29
|
import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetry/config.js';
|
|
30
|
+
import {
|
|
31
|
+
recordPendingInteraction, clearPendingInteraction,
|
|
32
|
+
claimPendingInteraction, releasePendingInteraction,
|
|
33
|
+
loadSession, startAgentExecution, holdAgentExecutionForAdvance, releaseAgentExecution,
|
|
34
|
+
} from '../orchestrator/session-manager.js';
|
|
35
|
+
import { recordEvent, EventType } from '../intelligence/timeline.js';
|
|
36
|
+
import {
|
|
37
|
+
assertRuntimeCommandV2, loadRuntimeInstallationV2, resolveRuntimeExecutableBindingV2,
|
|
38
|
+
} from '../orchestrator/runtime-installation-v2.js';
|
|
39
|
+
|
|
40
|
+
const RUN_AGENT_SECURITY_TEST_HOOKS = Symbol.for('chati.run-agent.security-test-hooks');
|
|
41
|
+
const ADDITIONAL_CONTEXT_MAX_BYTES = 256 * 1024;
|
|
42
|
+
const ADDITIONAL_CONTEXT_MAX_BYTES_BIGINT = BigInt(ADDITIONAL_CONTEXT_MAX_BYTES);
|
|
43
|
+
|
|
44
|
+
function runSecurityTestHook(name, detail) {
|
|
45
|
+
const hook = globalThis[RUN_AGENT_SECURITY_TEST_HOOKS]?.[name];
|
|
46
|
+
if (typeof hook === 'function') hook(detail);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sameAdditionalContextSnapshot(left, right) {
|
|
50
|
+
return Boolean(left && right
|
|
51
|
+
&& left.dev === right.dev
|
|
52
|
+
&& left.ino === right.ino
|
|
53
|
+
&& left.mode === right.mode
|
|
54
|
+
&& left.uid === right.uid
|
|
55
|
+
&& left.gid === right.gid
|
|
56
|
+
&& left.nlink === right.nlink
|
|
57
|
+
&& left.size === right.size
|
|
58
|
+
&& left.mtimeNs === right.mtimeNs
|
|
59
|
+
&& left.ctimeNs === right.ctimeNs);
|
|
60
|
+
}
|
|
26
61
|
|
|
27
62
|
// ---------------------------------------------------------------------------
|
|
28
63
|
// CLI argument parsing (no external deps)
|
|
@@ -35,7 +70,12 @@ function parseArgs(argv) {
|
|
|
35
70
|
if (arg.startsWith('--')) {
|
|
36
71
|
const key = arg.slice(2);
|
|
37
72
|
const next = argv[i + 1];
|
|
38
|
-
if (next && !next.startsWith('--')) {
|
|
73
|
+
if (key === 'additional-context' && next !== undefined && !next.startsWith('--')) {
|
|
74
|
+
args[key] = next;
|
|
75
|
+
i++;
|
|
76
|
+
} else if (key === 'additional-context') {
|
|
77
|
+
args[key] = null;
|
|
78
|
+
} else if (next && !next.startsWith('--')) {
|
|
39
79
|
args[key] = next;
|
|
40
80
|
i++;
|
|
41
81
|
} else {
|
|
@@ -66,21 +106,106 @@ async function main() {
|
|
|
66
106
|
const projectDir = args['project-dir'] || process.cwd();
|
|
67
107
|
const timeout = parseInt(args.timeout, 10) || 600_000; // default 10 minutes
|
|
68
108
|
|
|
109
|
+
const runtimeRoute = validateRuntimeCommand(projectDir, args);
|
|
110
|
+
if (!runtimeRoute.ok) {
|
|
111
|
+
if (args['execution-claim-id']) {
|
|
112
|
+
releaseAgentExecution(projectDir, { agent: args.agent, claimId: args['execution-claim-id'] });
|
|
113
|
+
}
|
|
114
|
+
outputResult({
|
|
115
|
+
status: 'error',
|
|
116
|
+
code: runtimeRoute.code,
|
|
117
|
+
error: runtimeRoute.error,
|
|
118
|
+
agent: args.agent,
|
|
119
|
+
wait_for_user: false,
|
|
120
|
+
});
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A fresh dispatch uses the one-shot execution claim. A resumed interactive
|
|
125
|
+
// relay carries an interaction identity and is serialized separately by
|
|
126
|
+
// claimPendingInteraction after the response file is validated.
|
|
127
|
+
if (runtimeRoute.artifact && !args['interaction-id']) {
|
|
128
|
+
const execution = startAgentExecution(projectDir, {
|
|
129
|
+
agent: args.agent,
|
|
130
|
+
claimId: args['execution-claim-id'] || null,
|
|
131
|
+
});
|
|
132
|
+
if (!execution.saved || !execution.started) {
|
|
133
|
+
outputResult({
|
|
134
|
+
status: 'error',
|
|
135
|
+
code: execution.code || 'SESSION_WRITE_FAILED',
|
|
136
|
+
error: execution.error,
|
|
137
|
+
agent: args.agent,
|
|
138
|
+
wait_for_user: false,
|
|
139
|
+
});
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
process.once('exit', () => {
|
|
143
|
+
releaseAgentExecution(projectDir, {
|
|
144
|
+
agent: args.agent,
|
|
145
|
+
claimId: args['execution-claim-id'],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const responseFile = loadAdditionalContextFile(projectDir, args);
|
|
151
|
+
if (!responseFile.ok) {
|
|
152
|
+
outputResult({
|
|
153
|
+
status: 'error',
|
|
154
|
+
code: responseFile.code,
|
|
155
|
+
error: responseFile.error,
|
|
156
|
+
agent: args.agent,
|
|
157
|
+
wait_for_user: true,
|
|
158
|
+
});
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
if (responseFile.response !== null) args['additional-context'] = responseFile.response;
|
|
162
|
+
|
|
163
|
+
// A session awaiting user input is a local state gate, not a provider
|
|
164
|
+
// preflight concern. Check it before looking for a CLI so a missing or
|
|
165
|
+
// empty relay response cannot be masked by a host-specific CLI absence.
|
|
166
|
+
if (typeof args['additional-context'] !== 'string' || !args['additional-context'].trim()) {
|
|
167
|
+
const pendingGuard = claimPendingInteraction(projectDir, {
|
|
168
|
+
interactionId: args['interaction-id'] || null,
|
|
169
|
+
agent: args.agent,
|
|
170
|
+
taskId: args['task-id'],
|
|
171
|
+
provider: args.provider || null,
|
|
172
|
+
providerId: args['provider-id'] || null,
|
|
173
|
+
model: args.model || null,
|
|
174
|
+
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
175
|
+
catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
|
|
176
|
+
response: args['additional-context'],
|
|
177
|
+
reasoningEscalation: args['reasoning-escalation'] === 'true',
|
|
178
|
+
});
|
|
179
|
+
if (!pendingGuard.saved || !pendingGuard.allowed) {
|
|
180
|
+
outputResult({
|
|
181
|
+
status: 'error',
|
|
182
|
+
code: pendingGuard.code || 'SESSION_WRITE_FAILED',
|
|
183
|
+
error: pendingGuard.error,
|
|
184
|
+
agent: args.agent,
|
|
185
|
+
wait_for_user: true,
|
|
186
|
+
});
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
69
191
|
// Initialize telemetry for this process
|
|
70
192
|
initCollector(isTelemetryEnabled(projectDir));
|
|
71
193
|
|
|
72
194
|
// Load session state if available
|
|
73
195
|
let sessionState = {};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
196
|
+
const loadedSession = loadSession(projectDir);
|
|
197
|
+
if (loadedSession.loaded) {
|
|
198
|
+
sessionState = loadedSession.session || {};
|
|
199
|
+
} else if (existsSync(resolve(projectDir, '.chati', 'session.yaml'))
|
|
200
|
+
|| !['Session file not found', 'Session directory not found'].includes(loadedSession.error)) {
|
|
201
|
+
outputResult({
|
|
202
|
+
status: 'error',
|
|
203
|
+
code: 'INVALID_SESSION_PATH',
|
|
204
|
+
error: loadedSession.error,
|
|
205
|
+
agent: args.agent,
|
|
206
|
+
wait_for_user: false,
|
|
207
|
+
});
|
|
208
|
+
process.exit(1);
|
|
84
209
|
}
|
|
85
210
|
|
|
86
211
|
// Build the agent prompt
|
|
@@ -99,6 +224,11 @@ async function main() {
|
|
|
99
224
|
strictProvider: args['strict-provider'] === 'true',
|
|
100
225
|
});
|
|
101
226
|
} catch (err) {
|
|
227
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
228
|
+
if (!relayState.saved) {
|
|
229
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: err.message });
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
102
232
|
outputError(`Failed to build prompt: ${err.message}`);
|
|
103
233
|
process.exit(1);
|
|
104
234
|
}
|
|
@@ -106,11 +236,23 @@ async function main() {
|
|
|
106
236
|
// Wait for rate limit slot before spawning
|
|
107
237
|
const spawnProvider = promptResult.provider || args.provider;
|
|
108
238
|
if (!spawnProvider) {
|
|
239
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
240
|
+
if (!relayState.saved) {
|
|
241
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: 'PROVIDER_MISSING' });
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
109
244
|
outputError('No routed provider was supplied for agent execution');
|
|
110
245
|
process.exit(1);
|
|
111
246
|
}
|
|
112
|
-
const readiness = checkProviderReadiness(spawnProvider, {
|
|
247
|
+
const readiness = checkProviderReadiness(spawnProvider, {
|
|
248
|
+
workingDir: projectDir, executableBinding: runtimeRoute.executableBinding || undefined,
|
|
249
|
+
});
|
|
113
250
|
if (!readiness.ready) {
|
|
251
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
252
|
+
if (!relayState.saved) {
|
|
253
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: readiness.code });
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
114
256
|
outputResult({ status: 'error', error: readiness.detail, code: readiness.code, provider: spawnProvider });
|
|
115
257
|
process.exit(1);
|
|
116
258
|
}
|
|
@@ -126,6 +268,34 @@ async function main() {
|
|
|
126
268
|
});
|
|
127
269
|
}
|
|
128
270
|
|
|
271
|
+
// Reserve the relay only after local preparation and rate limiting. The
|
|
272
|
+
// lease therefore covers provider execution, not unbounded queue time.
|
|
273
|
+
const pendingClaim = claimPendingInteraction(projectDir, {
|
|
274
|
+
interactionId: args['interaction-id'] || null,
|
|
275
|
+
agent: args.agent,
|
|
276
|
+
taskId: args['task-id'],
|
|
277
|
+
provider: args.provider || null,
|
|
278
|
+
providerId: args['provider-id'] || null,
|
|
279
|
+
model: args.model || null,
|
|
280
|
+
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
281
|
+
catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
|
|
282
|
+
response: args['additional-context'],
|
|
283
|
+
reasoningEscalation: args['reasoning-escalation'] === 'true',
|
|
284
|
+
leaseExpiresAt: new Date(Date.now() + (timeout * 2) + 5 * 60 * 1000).toISOString(),
|
|
285
|
+
});
|
|
286
|
+
if (!pendingClaim.saved || !pendingClaim.allowed) {
|
|
287
|
+
cleanupRejectedResponseFile(responseFile.filePath, pendingClaim);
|
|
288
|
+
outputResult({
|
|
289
|
+
status: 'error',
|
|
290
|
+
code: pendingClaim.code || 'SESSION_WRITE_FAILED',
|
|
291
|
+
error: pendingClaim.error,
|
|
292
|
+
agent: args.agent,
|
|
293
|
+
wait_for_user: true,
|
|
294
|
+
});
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
if (pendingClaim.claimed) args['relay-claim-id'] = pendingClaim.claimId;
|
|
298
|
+
|
|
129
299
|
// Spawn the agent terminal
|
|
130
300
|
const startTime = Date.now();
|
|
131
301
|
let handle;
|
|
@@ -143,17 +313,34 @@ async function main() {
|
|
|
143
313
|
providerId: args['provider-id'] || null,
|
|
144
314
|
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
145
315
|
catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
|
|
316
|
+
providerExecutableBinding: runtimeRoute.executableBinding || undefined,
|
|
317
|
+
...(runtimeRoute.artifact ? {
|
|
318
|
+
writeScope: getWriteScope(args.agent, projectDir),
|
|
319
|
+
authorityContext: {
|
|
320
|
+
authority_kind: 'planning_execution',
|
|
321
|
+
invocation_id: `planning:${args.agent}:${args['task-id']}:${randomUUID()}`,
|
|
322
|
+
launch_nonce: randomUUID(),
|
|
323
|
+
provider_id: runtimeRoute.invocation.provider_id,
|
|
324
|
+
},
|
|
325
|
+
} : {}),
|
|
146
326
|
};
|
|
147
327
|
handle = await spawnTerminalWithRetry(spawnConfig, {
|
|
148
328
|
maxRetries: 1,
|
|
149
329
|
baseDelay: 500,
|
|
150
|
-
|
|
330
|
+
// The orchestrator must execute the routed model exactly. A silent
|
|
331
|
+
// downgrade would make the persisted binding and cost record false.
|
|
332
|
+
enableModelFallback: false,
|
|
151
333
|
shouldRetry(exitCode, output) {
|
|
152
334
|
const combined = Array.isArray(output) ? output.join('') : String(output || '');
|
|
153
335
|
return isTransientFailure(exitCode, combined) || /not logged in|not authenticated/i.test(combined);
|
|
154
336
|
},
|
|
155
337
|
});
|
|
156
338
|
} catch (err) {
|
|
339
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
340
|
+
if (!relayState.saved) {
|
|
341
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: err.message });
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
157
344
|
outputError(`Failed to spawn terminal: ${err.message}`);
|
|
158
345
|
process.exit(1);
|
|
159
346
|
}
|
|
@@ -213,8 +400,14 @@ async function main() {
|
|
|
213
400
|
|
|
214
401
|
// Parse the handoff from stdout
|
|
215
402
|
const parsed = parseAgentOutput(stdout);
|
|
403
|
+
const recovered = recoverInteractiveHandoff(args.agent, stdout, handle.exitCode);
|
|
216
404
|
|
|
217
405
|
if (handle.exitCode !== 0 && /not logged in|not authenticated/i.test(providerOutput)) {
|
|
406
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
407
|
+
if (!relayState.saved) {
|
|
408
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: 'PROVIDER_AUTH_STATE_MISMATCH' });
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
218
411
|
outputResult({
|
|
219
412
|
status: 'error',
|
|
220
413
|
code: 'PROVIDER_AUTH_STATE_MISMATCH',
|
|
@@ -226,7 +419,46 @@ async function main() {
|
|
|
226
419
|
process.exit(1);
|
|
227
420
|
}
|
|
228
421
|
|
|
422
|
+
// A syntactically valid handoff is not a successful execution when the
|
|
423
|
+
// provider process itself failed. In particular, a resumed relay must keep
|
|
424
|
+
// both its pending interaction and response file intact so the exact user
|
|
425
|
+
// answer can be retried. Persisting a trailing `complete` block from a failed
|
|
426
|
+
// process would clear that state and make the next CLI redispatch from zero.
|
|
427
|
+
if (handle.exitCode !== 0) {
|
|
428
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
429
|
+
if (!relayState.saved) {
|
|
430
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: 'PROVIDER_EXECUTION_FAILED' });
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
outputResult({
|
|
434
|
+
status: 'error',
|
|
435
|
+
code: 'PROVIDER_EXECUTION_FAILED',
|
|
436
|
+
error: `${spawnProvider} exited unsuccessfully; its handoff was not committed`,
|
|
437
|
+
provider: spawnProvider,
|
|
438
|
+
exitCode: handle.exitCode,
|
|
439
|
+
retryCount: handle.retryCount || 0,
|
|
440
|
+
agent: args.agent,
|
|
441
|
+
});
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
229
445
|
if (parsed.found && parsed.valid) {
|
|
446
|
+
const persistence = persistInteractiveState(projectDir, args, parsed.handoff, promptResult.model, spawnProvider);
|
|
447
|
+
if (!persistence.saved) {
|
|
448
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
if (parsed.handoff.status !== 'needs_input' && handle.exitCode === 0) {
|
|
452
|
+
const held = holdAgentExecutionForAdvance(projectDir, {
|
|
453
|
+
agent: args.agent,
|
|
454
|
+
claimId: args['interaction-id'] ? null : (args['execution-claim-id'] || null),
|
|
455
|
+
});
|
|
456
|
+
if (!held.saved || !held.held) {
|
|
457
|
+
outputResult({ status: 'error', code: held.code || 'SESSION_WRITE_FAILED', error: held.error, agent: args.agent });
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
cleanupResponseFile(responseFile.filePath);
|
|
230
462
|
outputResult({
|
|
231
463
|
status: parsed.handoff.status,
|
|
232
464
|
agent: args.agent,
|
|
@@ -235,20 +467,30 @@ async function main() {
|
|
|
235
467
|
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
236
468
|
exitCode: handle.exitCode,
|
|
237
469
|
handoff: parsed.handoff,
|
|
470
|
+
...(parsed.handoff.status === 'needs_input' && parsed.handoff.needs_input_question
|
|
471
|
+
? { needs_input_question: parsed.handoff.needs_input_question }
|
|
472
|
+
: {}),
|
|
238
473
|
elapsed,
|
|
239
474
|
costEstimate,
|
|
240
475
|
});
|
|
241
|
-
} else if (
|
|
476
|
+
} else if (recovered) {
|
|
242
477
|
// Interactive discovery models sometimes answer with the user-facing
|
|
243
478
|
// question but omit the machine block. Preserve the exact question while
|
|
244
479
|
// restoring the deterministic relay contract for the parent orchestrator.
|
|
480
|
+
const persistence = persistInteractiveState(projectDir, args, recovered, promptResult.model, spawnProvider);
|
|
481
|
+
if (!persistence.saved) {
|
|
482
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
cleanupResponseFile(responseFile.filePath);
|
|
245
486
|
outputResult({
|
|
246
487
|
status: 'needs_input',
|
|
247
488
|
agent: args.agent,
|
|
248
489
|
model: promptResult.model,
|
|
249
490
|
provider: spawnProvider,
|
|
250
491
|
exitCode: handle.exitCode,
|
|
251
|
-
handoff:
|
|
492
|
+
handoff: recovered,
|
|
493
|
+
needs_input_question: recovered.needs_input_question,
|
|
252
494
|
contractRecovery: 'interactive_output_wrapped',
|
|
253
495
|
elapsed,
|
|
254
496
|
costEstimate,
|
|
@@ -256,6 +498,11 @@ async function main() {
|
|
|
256
498
|
} else {
|
|
257
499
|
// A non-interactive agent that omits the handoff violated the execution
|
|
258
500
|
// contract and cannot be treated as successful or partially complete.
|
|
501
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
502
|
+
if (!relayState.saved) {
|
|
503
|
+
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: parsed.found ? 'INVALID_HANDOFF' : 'MISSING_HANDOFF' });
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
259
506
|
outputResult({
|
|
260
507
|
status: 'error',
|
|
261
508
|
code: parsed.found ? 'INVALID_HANDOFF' : 'MISSING_HANDOFF',
|
|
@@ -275,6 +522,177 @@ async function main() {
|
|
|
275
522
|
process.exit(handle.exitCode === 0 ? 0 : 1);
|
|
276
523
|
}
|
|
277
524
|
|
|
525
|
+
function cleanupResponseFile(filePath) {
|
|
526
|
+
if (!filePath) return { removed: false, error: null };
|
|
527
|
+
try {
|
|
528
|
+
rmSync(filePath);
|
|
529
|
+
return { removed: true, error: null };
|
|
530
|
+
} catch (err) {
|
|
531
|
+
if (err.code === 'ENOENT') return { removed: false, error: null };
|
|
532
|
+
process.stderr.write(`[chati] response-file cleanup failed: ${err.message}\n`);
|
|
533
|
+
return { removed: false, error: err.message };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function cleanupRejectedResponseFile(filePath, claim = {}) {
|
|
538
|
+
if (claim.code !== 'PENDING_INTERACTION_NOT_FOUND') {
|
|
539
|
+
return { removed: false, preserved: Boolean(filePath), error: null };
|
|
540
|
+
}
|
|
541
|
+
return cleanupResponseFile(filePath);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function persistInteractiveState(projectDir, args, handoff, model, provider) {
|
|
545
|
+
if (handoff?.status === 'needs_input' && handoff.needs_input_question) {
|
|
546
|
+
const result = recordPendingInteraction(projectDir, {
|
|
547
|
+
agent: args.agent,
|
|
548
|
+
taskId: args['reasoning-escalation'] === 'true'
|
|
549
|
+
? `${args['task-id']}-post-escalation`
|
|
550
|
+
: args['task-id'],
|
|
551
|
+
question: handoff.needs_input_question,
|
|
552
|
+
provider,
|
|
553
|
+
providerId: args['provider-id'] || null,
|
|
554
|
+
model,
|
|
555
|
+
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
556
|
+
catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
|
|
557
|
+
advanceRelay: Boolean(args['additional-context']?.trim()),
|
|
558
|
+
});
|
|
559
|
+
recordEvent(projectDir, EventType.AGENT_WAITING_FOR_INPUT, args.agent, {
|
|
560
|
+
task_id: args['task-id'],
|
|
561
|
+
});
|
|
562
|
+
return result;
|
|
563
|
+
}
|
|
564
|
+
return clearPendingInteraction(projectDir, {
|
|
565
|
+
agent: args.agent,
|
|
566
|
+
taskId: args['task-id'],
|
|
567
|
+
interactionId: args['interaction-id'] || null,
|
|
568
|
+
claimId: args['relay-claim-id'] || null,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function loadAdditionalContextFile(projectDir, args = {}) {
|
|
573
|
+
const fileArg = args['additional-context-file'];
|
|
574
|
+
if (!fileArg) return { ok: true, response: null, filePath: null };
|
|
575
|
+
if (typeof fileArg !== 'string') {
|
|
576
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: '--additional-context-file requires a path value.' };
|
|
577
|
+
}
|
|
578
|
+
if (args['additional-context'] !== undefined && args['additional-context'] !== null) {
|
|
579
|
+
return { ok: false, code: 'AMBIGUOUS_ADDITIONAL_CONTEXT', error: 'Use either --additional-context-file or --additional-context, not both.' };
|
|
580
|
+
}
|
|
581
|
+
const projectRoot = resolve(projectDir);
|
|
582
|
+
const chatiRoot = resolve(projectRoot, '.chati');
|
|
583
|
+
const runtimeRoot = resolve(chatiRoot, 'runtime');
|
|
584
|
+
const filePath = resolve(fileArg);
|
|
585
|
+
if (!existsSync(chatiRoot) || lstatSync(chatiRoot).isSymbolicLink() || !lstatSync(chatiRoot).isDirectory()) {
|
|
586
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'The .chati directory must be a real directory.' };
|
|
587
|
+
}
|
|
588
|
+
if (!existsSync(runtimeRoot) || lstatSync(runtimeRoot).isSymbolicLink() || !lstatSync(runtimeRoot).isDirectory()) {
|
|
589
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'The .chati/runtime directory must be a real directory.' };
|
|
590
|
+
}
|
|
591
|
+
const realProjectRoot = realpathSync(projectRoot);
|
|
592
|
+
const realChatiRoot = realpathSync(chatiRoot);
|
|
593
|
+
const chatiRel = relative(realProjectRoot, realChatiRoot);
|
|
594
|
+
if (!chatiRel || chatiRel.startsWith('..') || isAbsolute(chatiRel)) {
|
|
595
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'The .chati directory must remain inside the project.' };
|
|
596
|
+
}
|
|
597
|
+
const realRuntimeRoot = realpathSync(runtimeRoot);
|
|
598
|
+
const runtimeRel = relative(realChatiRoot, realRuntimeRoot);
|
|
599
|
+
if (!runtimeRel || runtimeRel.startsWith('..') || isAbsolute(runtimeRel)) {
|
|
600
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'The runtime directory must remain inside .chati.' };
|
|
601
|
+
}
|
|
602
|
+
const realFilePath = existsSync(filePath) ? realpathSync(filePath) : filePath;
|
|
603
|
+
const rel = relative(realRuntimeRoot, realFilePath);
|
|
604
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel)) {
|
|
605
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'Response file must be inside .chati/runtime.' };
|
|
606
|
+
}
|
|
607
|
+
if (!existsSync(filePath)) {
|
|
608
|
+
return { ok: false, code: 'ADDITIONAL_CONTEXT_FILE_MISSING', error: 'Pending response file does not exist.' };
|
|
609
|
+
}
|
|
610
|
+
const pathStat = lstatSync(filePath, { bigint: true });
|
|
611
|
+
if (!pathStat.isFile() || pathStat.isSymbolicLink()) {
|
|
612
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'Pending response path must be a regular file.' };
|
|
613
|
+
}
|
|
614
|
+
if (pathStat.size > ADDITIONAL_CONTEXT_MAX_BYTES_BIGINT) {
|
|
615
|
+
return { ok: false, code: 'ADDITIONAL_CONTEXT_FILE_TOO_LARGE', error: 'Pending response exceeds the 256 KiB limit.', filePath };
|
|
616
|
+
}
|
|
617
|
+
let fd;
|
|
618
|
+
try {
|
|
619
|
+
fd = openSync(
|
|
620
|
+
filePath,
|
|
621
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0),
|
|
622
|
+
);
|
|
623
|
+
const openedStat = fstatSync(fd, { bigint: true });
|
|
624
|
+
if (!openedStat.isFile() || openedStat.nlink !== 1n
|
|
625
|
+
|| !sameAdditionalContextSnapshot(pathStat, openedStat)) {
|
|
626
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'Pending response changed while it was being validated.', filePath };
|
|
627
|
+
}
|
|
628
|
+
if (openedStat.size > ADDITIONAL_CONTEXT_MAX_BYTES_BIGINT) {
|
|
629
|
+
return { ok: false, code: 'ADDITIONAL_CONTEXT_FILE_TOO_LARGE', error: 'Pending response exceeds the 256 KiB limit.', filePath };
|
|
630
|
+
}
|
|
631
|
+
runSecurityTestHook('afterAdditionalContextStat', { path: filePath });
|
|
632
|
+
const bytes = Buffer.allocUnsafe(ADDITIONAL_CONTEXT_MAX_BYTES + 1);
|
|
633
|
+
let total = 0;
|
|
634
|
+
while (total < bytes.length) {
|
|
635
|
+
const bytesRead = readSync(fd, bytes, total, bytes.length - total, null);
|
|
636
|
+
if (bytesRead === 0) break;
|
|
637
|
+
total += bytesRead;
|
|
638
|
+
}
|
|
639
|
+
if (total > ADDITIONAL_CONTEXT_MAX_BYTES) {
|
|
640
|
+
return { ok: false, code: 'ADDITIONAL_CONTEXT_FILE_TOO_LARGE', error: 'Pending response exceeds the 256 KiB limit.', filePath };
|
|
641
|
+
}
|
|
642
|
+
const finalStat = fstatSync(fd, { bigint: true });
|
|
643
|
+
const finalPathStat = lstatSync(filePath, { bigint: true });
|
|
644
|
+
if (!sameAdditionalContextSnapshot(openedStat, finalStat)
|
|
645
|
+
|| !sameAdditionalContextSnapshot(finalStat, finalPathStat)) {
|
|
646
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'Pending response changed while it was being read.', filePath };
|
|
647
|
+
}
|
|
648
|
+
const payload = JSON.parse(bytes.subarray(0, total).toString('utf8'));
|
|
649
|
+
if (typeof payload?.response !== 'string' || !payload.response.trim()) {
|
|
650
|
+
return { ok: false, code: 'EMPTY_ADDITIONAL_CONTEXT', error: 'Pending response must contain a non-empty JSON string field named response.', filePath };
|
|
651
|
+
}
|
|
652
|
+
return { ok: true, response: payload.response, filePath };
|
|
653
|
+
} catch {
|
|
654
|
+
return { ok: false, code: 'INVALID_ADDITIONAL_CONTEXT_FILE', error: 'Could not parse the pending response JSON.', filePath };
|
|
655
|
+
} finally {
|
|
656
|
+
if (fd !== undefined) closeSync(fd);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export function validateRuntimeCommand(projectDir, args = {}) {
|
|
661
|
+
try {
|
|
662
|
+
const artifact = loadRuntimeInstallationV2(projectDir);
|
|
663
|
+
if (!artifact) return { ok: true, artifact: null };
|
|
664
|
+
const invocation = assertRuntimeCommandV2({
|
|
665
|
+
artifact,
|
|
666
|
+
agent: args.agent,
|
|
667
|
+
reasoning_escalation: args['reasoning-escalation'] === 'true',
|
|
668
|
+
command: {
|
|
669
|
+
provider_id: args['provider-id'],
|
|
670
|
+
harness_id: args.provider,
|
|
671
|
+
model_id: args.model,
|
|
672
|
+
reasoning_configuration: args['reasoning-configuration'],
|
|
673
|
+
catalog_snapshot_ref: args['catalog-snapshot-ref'],
|
|
674
|
+
strict_provider: args['strict-provider'] === 'true',
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
const executableBinding = resolveRuntimeExecutableBindingV2({
|
|
678
|
+
artifact,
|
|
679
|
+
provider_id: invocation.provider_id,
|
|
680
|
+
harness_id: invocation.harness_id,
|
|
681
|
+
});
|
|
682
|
+
return { ok: true, artifact, invocation, executableBinding };
|
|
683
|
+
} catch (error) {
|
|
684
|
+
return { ok: false, code: error.code || 'RUNTIME_ROUTE_BINDING_INVALID', error: error.message };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function releasePendingForFailedRelay(projectDir, args) {
|
|
689
|
+
if (!args?.['relay-claim-id']) return { saved: true, released: false };
|
|
690
|
+
return releasePendingInteraction(projectDir, {
|
|
691
|
+
claimId: args['relay-claim-id'],
|
|
692
|
+
interactionId: args['interaction-id'] || null,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
278
696
|
// ---------------------------------------------------------------------------
|
|
279
697
|
// Helpers
|
|
280
698
|
// ---------------------------------------------------------------------------
|
|
@@ -400,4 +818,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
400
818
|
});
|
|
401
819
|
}
|
|
402
820
|
|
|
403
|
-
export { parseArgs, waitForExit, parseSimpleYaml };
|
|
821
|
+
export { cleanupResponseFile, parseArgs, waitForExit, parseSimpleYaml };
|
|
@@ -22,6 +22,7 @@ import { collectResults, mergeHandoffs, buildConsolidatedHandoff } from './colle
|
|
|
22
22
|
import { parseAgentOutput } from './handoff-parser.js';
|
|
23
23
|
import { estimateTokens, COST_PER_1K } from './cost-tracker.js';
|
|
24
24
|
import { getRateLimiter } from './rate-limiter.js';
|
|
25
|
+
import { loadRuntimeInstallationV2 } from '../orchestrator/runtime-installation-v2.js';
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// CLI argument parsing
|
|
@@ -86,6 +87,10 @@ async function main() {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
const projectDir = args['project-dir'] || process.cwd();
|
|
90
|
+
if (loadRuntimeInstallationV2(projectDir)) {
|
|
91
|
+
outputError('Runtime v2 requires individually signed planning commands. The legacy parallel runner is disabled.');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
89
94
|
const previousAgent = args['previous-agent'] || null;
|
|
90
95
|
const timeout = parseInt(args.timeout, 10) || 900_000; // default 15 minutes
|
|
91
96
|
|