chati-dev 4.5.16 → 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 +1 -1
- 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 +252 -25
- package/framework/orchestrator/chati.md +238 -70
- package/framework/schemas/session.schema.json +1 -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 +148 -53
- package/src/installer/manifest.js +140 -9
- package/src/installer/package-artifact.js +249 -0
- package/src/installer/templates.js +65 -20
- 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/orchestrator/browser-runtime.js +44 -13
- package/src/orchestrator/cli.js +1545 -174
- package/src/orchestrator/clickup-projection.js +43 -1
- package/src/orchestrator/clickup-runtime.js +355 -39
- package/src/orchestrator/doctor.js +16 -3
- 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 +1167 -53
- 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 +9 -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 +384 -24
- 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/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,8 +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';
|
|
26
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
recordPendingInteraction, clearPendingInteraction,
|
|
32
|
+
claimPendingInteraction, releasePendingInteraction,
|
|
33
|
+
loadSession, startAgentExecution, holdAgentExecutionForAdvance, releaseAgentExecution,
|
|
34
|
+
} from '../orchestrator/session-manager.js';
|
|
27
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
|
+
}
|
|
28
61
|
|
|
29
62
|
// ---------------------------------------------------------------------------
|
|
30
63
|
// CLI argument parsing (no external deps)
|
|
@@ -37,7 +70,12 @@ function parseArgs(argv) {
|
|
|
37
70
|
if (arg.startsWith('--')) {
|
|
38
71
|
const key = arg.slice(2);
|
|
39
72
|
const next = argv[i + 1];
|
|
40
|
-
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('--')) {
|
|
41
79
|
args[key] = next;
|
|
42
80
|
i++;
|
|
43
81
|
} else {
|
|
@@ -68,21 +106,106 @@ async function main() {
|
|
|
68
106
|
const projectDir = args['project-dir'] || process.cwd();
|
|
69
107
|
const timeout = parseInt(args.timeout, 10) || 600_000; // default 10 minutes
|
|
70
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
|
+
|
|
71
191
|
// Initialize telemetry for this process
|
|
72
192
|
initCollector(isTelemetryEnabled(projectDir));
|
|
73
193
|
|
|
74
194
|
// Load session state if available
|
|
75
195
|
let sessionState = {};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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);
|
|
86
209
|
}
|
|
87
210
|
|
|
88
211
|
// Build the agent prompt
|
|
@@ -101,6 +224,11 @@ async function main() {
|
|
|
101
224
|
strictProvider: args['strict-provider'] === 'true',
|
|
102
225
|
});
|
|
103
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
|
+
}
|
|
104
232
|
outputError(`Failed to build prompt: ${err.message}`);
|
|
105
233
|
process.exit(1);
|
|
106
234
|
}
|
|
@@ -108,12 +236,19 @@ async function main() {
|
|
|
108
236
|
// Wait for rate limit slot before spawning
|
|
109
237
|
const spawnProvider = promptResult.provider || args.provider;
|
|
110
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
|
+
}
|
|
111
244
|
outputError('No routed provider was supplied for agent execution');
|
|
112
245
|
process.exit(1);
|
|
113
246
|
}
|
|
114
|
-
const readiness = checkProviderReadiness(spawnProvider, {
|
|
247
|
+
const readiness = checkProviderReadiness(spawnProvider, {
|
|
248
|
+
workingDir: projectDir, executableBinding: runtimeRoute.executableBinding || undefined,
|
|
249
|
+
});
|
|
115
250
|
if (!readiness.ready) {
|
|
116
|
-
const relayState =
|
|
251
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
117
252
|
if (!relayState.saved) {
|
|
118
253
|
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: readiness.code });
|
|
119
254
|
process.exit(1);
|
|
@@ -133,6 +268,34 @@ async function main() {
|
|
|
133
268
|
});
|
|
134
269
|
}
|
|
135
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
|
+
|
|
136
299
|
// Spawn the agent terminal
|
|
137
300
|
const startTime = Date.now();
|
|
138
301
|
let handle;
|
|
@@ -150,18 +313,30 @@ async function main() {
|
|
|
150
313
|
providerId: args['provider-id'] || null,
|
|
151
314
|
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
152
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
|
+
} : {}),
|
|
153
326
|
};
|
|
154
327
|
handle = await spawnTerminalWithRetry(spawnConfig, {
|
|
155
328
|
maxRetries: 1,
|
|
156
329
|
baseDelay: 500,
|
|
157
|
-
|
|
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,
|
|
158
333
|
shouldRetry(exitCode, output) {
|
|
159
334
|
const combined = Array.isArray(output) ? output.join('') : String(output || '');
|
|
160
335
|
return isTransientFailure(exitCode, combined) || /not logged in|not authenticated/i.test(combined);
|
|
161
336
|
},
|
|
162
337
|
});
|
|
163
338
|
} catch (err) {
|
|
164
|
-
const relayState =
|
|
339
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
165
340
|
if (!relayState.saved) {
|
|
166
341
|
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: err.message });
|
|
167
342
|
process.exit(1);
|
|
@@ -228,7 +403,7 @@ async function main() {
|
|
|
228
403
|
const recovered = recoverInteractiveHandoff(args.agent, stdout, handle.exitCode);
|
|
229
404
|
|
|
230
405
|
if (handle.exitCode !== 0 && /not logged in|not authenticated/i.test(providerOutput)) {
|
|
231
|
-
const relayState =
|
|
406
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
232
407
|
if (!relayState.saved) {
|
|
233
408
|
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: 'PROVIDER_AUTH_STATE_MISMATCH' });
|
|
234
409
|
process.exit(1);
|
|
@@ -244,12 +419,46 @@ async function main() {
|
|
|
244
419
|
process.exit(1);
|
|
245
420
|
}
|
|
246
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
|
+
|
|
247
445
|
if (parsed.found && parsed.valid) {
|
|
248
446
|
const persistence = persistInteractiveState(projectDir, args, parsed.handoff, promptResult.model, spawnProvider);
|
|
249
447
|
if (!persistence.saved) {
|
|
250
448
|
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
|
|
251
449
|
process.exit(1);
|
|
252
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);
|
|
253
462
|
outputResult({
|
|
254
463
|
status: parsed.handoff.status,
|
|
255
464
|
agent: args.agent,
|
|
@@ -258,6 +467,9 @@ async function main() {
|
|
|
258
467
|
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
259
468
|
exitCode: handle.exitCode,
|
|
260
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
|
+
: {}),
|
|
261
473
|
elapsed,
|
|
262
474
|
costEstimate,
|
|
263
475
|
});
|
|
@@ -270,6 +482,7 @@ async function main() {
|
|
|
270
482
|
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: persistence.error, agent: args.agent });
|
|
271
483
|
process.exit(1);
|
|
272
484
|
}
|
|
485
|
+
cleanupResponseFile(responseFile.filePath);
|
|
273
486
|
outputResult({
|
|
274
487
|
status: 'needs_input',
|
|
275
488
|
agent: args.agent,
|
|
@@ -277,6 +490,7 @@ async function main() {
|
|
|
277
490
|
provider: spawnProvider,
|
|
278
491
|
exitCode: handle.exitCode,
|
|
279
492
|
handoff: recovered,
|
|
493
|
+
needs_input_question: recovered.needs_input_question,
|
|
280
494
|
contractRecovery: 'interactive_output_wrapped',
|
|
281
495
|
elapsed,
|
|
282
496
|
costEstimate,
|
|
@@ -284,7 +498,7 @@ async function main() {
|
|
|
284
498
|
} else {
|
|
285
499
|
// A non-interactive agent that omits the handoff violated the execution
|
|
286
500
|
// contract and cannot be treated as successful or partially complete.
|
|
287
|
-
const relayState =
|
|
501
|
+
const relayState = releasePendingForFailedRelay(projectDir, args);
|
|
288
502
|
if (!relayState.saved) {
|
|
289
503
|
outputResult({ status: 'error', code: 'SESSION_WRITE_FAILED', error: relayState.error, cause: parsed.found ? 'INVALID_HANDOFF' : 'MISSING_HANDOFF' });
|
|
290
504
|
process.exit(1);
|
|
@@ -308,29 +522,175 @@ async function main() {
|
|
|
308
522
|
process.exit(handle.exitCode === 0 ? 0 : 1);
|
|
309
523
|
}
|
|
310
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
|
+
|
|
311
544
|
function persistInteractiveState(projectDir, args, handoff, model, provider) {
|
|
312
545
|
if (handoff?.status === 'needs_input' && handoff.needs_input_question) {
|
|
313
546
|
const result = recordPendingInteraction(projectDir, {
|
|
314
547
|
agent: args.agent,
|
|
315
|
-
taskId: args['
|
|
548
|
+
taskId: args['reasoning-escalation'] === 'true'
|
|
549
|
+
? `${args['task-id']}-post-escalation`
|
|
550
|
+
: args['task-id'],
|
|
316
551
|
question: handoff.needs_input_question,
|
|
317
552
|
provider,
|
|
318
553
|
providerId: args['provider-id'] || null,
|
|
319
554
|
model,
|
|
320
555
|
reasoningConfiguration: args['reasoning-configuration'] || null,
|
|
321
556
|
catalogSnapshotRef: args['catalog-snapshot-ref'] || null,
|
|
557
|
+
advanceRelay: Boolean(args['additional-context']?.trim()),
|
|
322
558
|
});
|
|
323
559
|
recordEvent(projectDir, EventType.AGENT_WAITING_FOR_INPUT, args.agent, {
|
|
324
560
|
task_id: args['task-id'],
|
|
325
561
|
});
|
|
326
562
|
return result;
|
|
327
563
|
}
|
|
328
|
-
return clearPendingInteraction(projectDir, {
|
|
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
|
+
}
|
|
329
658
|
}
|
|
330
659
|
|
|
331
|
-
export function
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
});
|
|
334
694
|
}
|
|
335
695
|
|
|
336
696
|
// ---------------------------------------------------------------------------
|
|
@@ -458,4 +818,4 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
|
458
818
|
});
|
|
459
819
|
}
|
|
460
820
|
|
|
461
|
-
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
|
|