ultra-builder-pro-cli 5.0.1 → 6.0.0
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/CHANGELOG.md +103 -3
- package/README.md +67 -478
- package/README.zh-CN.md +49 -422
- package/adapters/_shared/frontmatter.cjs +1 -18
- package/adapters/_shared/plugin-core.cjs +17 -83
- package/adapters/_shared/runtime-assets.cjs +6 -120
- package/adapters/claude.js +8 -24
- package/adapters/codex.js +13 -32
- package/bin/install.js +35 -57
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +13 -101
- package/docs/examples/drive/claude-reviewer.sh +53 -0
- package/docs/examples/drive/claude-writer.sh +43 -0
- package/engine/FORMATS.md +1051 -0
- package/engine/checks.cjs +139 -0
- package/engine/cli.cjs +148 -0
- package/engine/completion.cjs +48 -0
- package/engine/context.cjs +94 -0
- package/engine/drive.cjs +220 -0
- package/engine/expect-worker.cjs +5 -0
- package/engine/expect.cjs +33 -0
- package/engine/formats.cjs +189 -0
- package/engine/git-state.cjs +112 -0
- package/engine/ledger.cjs +205 -0
- package/engine/loading.cjs +28 -0
- package/engine/merge.cjs +273 -0
- package/engine/milestone.cjs +89 -0
- package/engine/plan.cjs +232 -0
- package/engine/project.cjs +5 -0
- package/engine/signals.cjs +3 -0
- package/engine/states.cjs +175 -0
- package/hooks/session-start.cjs +54 -0
- package/package.json +17 -52
- package/skills/ultra/SKILL.md +395 -0
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/changes/active/.gitkeep +0 -1
- package/.ultra-template/changes/archive/.gitkeep +0 -1
- package/.ultra-template/contexts/TEMPLATE.md +0 -135
- package/.ultra-template/decisions/.gitkeep +0 -1
- package/.ultra-template/evidence/.gitkeep +0 -1
- package/.ultra-template/north-star.md +0 -52
- package/.ultra-template/project-brief.md +0 -28
- package/.ultra-template/research/.gitkeep +0 -1
- package/.ultra-template/specs/architecture.md +0 -81
- package/.ultra-template/specs/discovery.md +0 -74
- package/.ultra-template/specs/product.md +0 -80
- package/.ultra-template/specs/research-distillate.md +0 -57
- package/.ultra-template/tasks.json +0 -4
- package/.ultra-template/test-report.json +0 -44
- package/adapters/_shared/host-profile.cjs +0 -5
- package/adapters/grok.js +0 -242
- package/adapters/kimi.js +0 -232
- package/adapters/opencode.js +0 -408
- package/adapters/zcode.js +0 -375
- package/bin/delegate-worker.cjs +0 -5
- package/bin/delegate.cjs +0 -5
- package/docs/ARCHITECTURE.md +0 -274
- package/docs/ARTIFACT-AUTHORITY.md +0 -305
- package/docs/DECISIONS.md +0 -229
- package/docs/PHILOSOPHY.md +0 -261
- package/docs/RUNTIME-COMPAT-MATRIX.md +0 -316
- package/docs/SKILL-AUTHORING.md +0 -113
- package/docs/WORKFLOW-LIFECYCLE.md +0 -350
- package/docs/evals/adversarial-review-2026-08-14.md +0 -84
- package/docs/evals/ultra-5-0-journey-results.md +0 -62
- package/docs/evals/ultra-5-0-live-acp-conformance.md +0 -93
- package/docs/evals/ultra-5-0-route-journeys.md +0 -145
- package/docs/evals/zcode-automation-2026-08-14.md +0 -103
- package/hooks/README.md +0 -52
- package/hooks/_common.py +0 -1779
- package/hooks/adapters/codex.py +0 -295
- package/hooks/adapters/grok.py +0 -134
- package/hooks/adapters/kimi.py +0 -156
- package/hooks/adapters/zcode.py +0 -227
- package/hooks/block_dangerous_commands.py +0 -201
- package/hooks/compact_context.py +0 -224
- package/hooks/mid_workflow_recall.py +0 -47
- package/hooks/post_edit_guard.py +0 -215
- package/hooks/session_context.py +0 -741
- package/skills/ultra-change/SKILL.md +0 -165
- package/skills/ultra-change/references/change-contract.md +0 -199
- package/skills/ultra-change/references/execution-grant.md +0 -109
- package/skills/ultra-change/references/primary-transfer.md +0 -273
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +0 -1928
- package/skills/ultra-delegate/SKILL.md +0 -195
- package/skills/ultra-delegate/patches/zcode-acp-0.1.0-zcode-0.16.5.patch +0 -184
- package/skills/ultra-delegate/references/delegation-contract.md +0 -138
- package/skills/ultra-delegate/scripts/acp-conformance.cjs +0 -287
- package/skills/ultra-delegate/scripts/acp-driver.cjs +0 -762
- package/skills/ultra-delegate/scripts/agent-execution-driver.cjs +0 -320
- package/skills/ultra-delegate/scripts/cli-driver.cjs +0 -61
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-read-only.md +0 -15
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-write.md +0 -17
- package/skills/ultra-delegate/scripts/delegate-worker.cjs +0 -328
- package/skills/ultra-delegate/scripts/delegate.cjs +0 -690
- package/skills/ultra-delegate/scripts/delegate_wait.py +0 -86
- package/skills/ultra-delegate/scripts/host-profile.cjs +0 -267
- package/skills/ultra-deliver/SKILL.md +0 -194
- package/skills/ultra-deliver/references/baseline-reconciliation.md +0 -41
- package/skills/ultra-dev/SKILL.md +0 -265
- package/skills/ultra-dev/references/debugging.md +0 -22
- package/skills/ultra-domain-modeling/SKILL.md +0 -79
- package/skills/ultra-grilling/SKILL.md +0 -121
- package/skills/ultra-grilling/references/reframing.md +0 -76
- package/skills/ultra-init/SKILL.md +0 -143
- package/skills/ultra-init/references/brownfield-adoption.md +0 -32
- package/skills/ultra-init/scripts/init_project.cjs +0 -1031
- package/skills/ultra-plan/SKILL.md +0 -160
- package/skills/ultra-plan/references/task-evidence-v2.md +0 -300
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +0 -859
- package/skills/ultra-research/SKILL.md +0 -167
- package/skills/ultra-research/references/00-problem-validation.md +0 -36
- package/skills/ultra-research/references/01-opportunity-discovery.md +0 -33
- package/skills/ultra-research/references/02-market-assessment.md +0 -31
- package/skills/ultra-research/references/03-alternatives.md +0 -31
- package/skills/ultra-research/references/04-product-strategy.md +0 -41
- package/skills/ultra-research/references/05-assumptions-validation.md +0 -31
- package/skills/ultra-research/references/10-user-personas.md +0 -30
- package/skills/ultra-research/references/11-user-scenarios.md +0 -28
- package/skills/ultra-research/references/20-user-stories.md +0 -31
- package/skills/ultra-research/references/21-features-scope.md +0 -38
- package/skills/ultra-research/references/22-success-metrics.md +0 -33
- package/skills/ultra-research/references/30-architecture-context.md +0 -30
- package/skills/ultra-research/references/31-solution-strategy.md +0 -28
- package/skills/ultra-research/references/32-building-blocks.md +0 -28
- package/skills/ultra-research/references/40-deployment.md +0 -29
- package/skills/ultra-research/references/41-quality-risks.md +0 -28
- package/skills/ultra-research/references/99-synthesis.md +0 -59
- package/skills/ultra-research/references/north-star-v2.md +0 -168
- package/skills/ultra-research/references/wayfinding.md +0 -50
- package/skills/ultra-research/scripts/validate_north_star.cjs +0 -1174
- package/skills/ultra-review/SKILL.md +0 -275
- package/skills/ultra-review/references/adversarial-evaluation.md +0 -31
- package/skills/ultra-review/references/code.md +0 -17
- package/skills/ultra-review/references/comments.md +0 -12
- package/skills/ultra-review/references/design.md +0 -12
- package/skills/ultra-review/references/errors.md +0 -13
- package/skills/ultra-review/references/review-modes.md +0 -29
- package/skills/ultra-review/references/spec.md +0 -18
- package/skills/ultra-review/references/tests.md +0 -12
- package/skills/ultra-review/references/unified-schema.md +0 -284
- package/skills/ultra-review/references/worker-packet.md +0 -183
- package/skills/ultra-review/scripts/review_wait.py +0 -2943
- package/skills/ultra-status/SKILL.md +0 -209
- package/skills/ultra-status/references/route-contract.md +0 -100
- package/skills/ultra-tdd/SKILL.md +0 -91
- package/skills/ultra-tdd/references/templates/README.md +0 -20
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +0 -79
- package/skills/ultra-tdd/references/templates/persistence-real.ts +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +0 -73
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +0 -66
- package/skills/ultra-tdd/references/test-execution.md +0 -23
- package/skills/ultra-test/SKILL.md +0 -244
- package/skills/ultra-test/references/export-syntax.md +0 -18
- package/skills/ultra-test/scripts/validate_review_transport.cjs +0 -549
- package/skills/ultra-test/scripts/worktree_digest.cjs +0 -576
- package/skills/ultra-think/SKILL.md +0 -56
- package/skills/ultra-think/references/autonomy-boundary.md +0 -48
- package/skills/ultra-think/references/interaction-boundary.md +0 -42
|
@@ -1,762 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const { spawn } = require('node:child_process');
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
ACP_PROTOCOL_VERSION,
|
|
9
|
-
AgentExecutionDriver,
|
|
10
|
-
BoundedCapture,
|
|
11
|
-
DEFAULT_CAPTURE_LIMIT_BYTES,
|
|
12
|
-
terminateProcessGroup,
|
|
13
|
-
} = require('./agent-execution-driver.cjs');
|
|
14
|
-
|
|
15
|
-
const MAX_MESSAGE_BYTES = 1024 * 1024;
|
|
16
|
-
const MAX_AGENT_OUTPUT_BYTES = 1024 * 1024;
|
|
17
|
-
const MAX_EVENTS = 512;
|
|
18
|
-
const MAX_EVENT_BYTES = 512 * 1024;
|
|
19
|
-
const MAX_EARLY_SESSION_UPDATES = 32;
|
|
20
|
-
const MAX_EARLY_SESSION_UPDATE_BYTES = 256 * 1024;
|
|
21
|
-
const STOP_REASONS = new Set([
|
|
22
|
-
'end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled',
|
|
23
|
-
]);
|
|
24
|
-
const UPDATE_TYPES = new Set([
|
|
25
|
-
'user_message_chunk', 'agent_message_chunk', 'agent_thought_chunk',
|
|
26
|
-
'tool_call', 'tool_call_update', 'plan', 'available_commands_update',
|
|
27
|
-
'current_mode_update', 'config_option_update', 'session_info_update',
|
|
28
|
-
'usage_update',
|
|
29
|
-
]);
|
|
30
|
-
const EARLY_SESSION_UPDATE_TYPES = new Set([
|
|
31
|
-
'available_commands_update', 'current_mode_update', 'config_option_update',
|
|
32
|
-
'session_info_update',
|
|
33
|
-
]);
|
|
34
|
-
const TOOL_KINDS = new Set([
|
|
35
|
-
'read', 'edit', 'delete', 'move', 'search', 'execute', 'think', 'fetch',
|
|
36
|
-
'switch_mode', 'other',
|
|
37
|
-
]);
|
|
38
|
-
const TOOL_STATUSES = new Set(['pending', 'in_progress', 'completed', 'failed']);
|
|
39
|
-
const EVENT_PHASES = new Set(['initialized', 'created', 'cancel_requested', 'cancel_confirmed']);
|
|
40
|
-
const EVENT_METHODS = new Set(['ignored_extension', 'unsupported']);
|
|
41
|
-
const PERMISSION_DECISIONS = new Set([
|
|
42
|
-
'allow_once', 'reject_once', 'reject_always', 'cancelled', 'denied',
|
|
43
|
-
]);
|
|
44
|
-
const PERMISSION_SCOPES = new Set([
|
|
45
|
-
'inside_envelope', 'outside_envelope', 'operation_denied',
|
|
46
|
-
'allow_once_unavailable', 'reject_option_unavailable',
|
|
47
|
-
]);
|
|
48
|
-
|
|
49
|
-
class AcpFailure extends Error {
|
|
50
|
-
constructor(failureType, message) {
|
|
51
|
-
super(message);
|
|
52
|
-
this.failureType = failureType;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
class AcpRpcError extends Error {
|
|
57
|
-
constructor(error) {
|
|
58
|
-
super(typeof error?.message === 'string' ? error.message : 'ACP request failed');
|
|
59
|
-
this.code = Number.isInteger(error?.code) ? error.code : null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function boundedCount(value) {
|
|
64
|
-
return Number.isInteger(value) && value >= 0 && value <= 1_000_000;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function projectedDerivedEvent(value) {
|
|
68
|
-
if (!isObject(value) || !Number.isInteger(value.sequence)
|
|
69
|
-
|| value.sequence < 1 || value.sequence > MAX_EVENTS
|
|
70
|
-
|| typeof value.observed_at !== 'string'
|
|
71
|
-
|| value.observed_at.length > 32
|
|
72
|
-
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u.test(value.observed_at)
|
|
73
|
-
|| !Number.isFinite(Date.parse(value.observed_at))) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
const event = {
|
|
77
|
-
sequence: value.sequence,
|
|
78
|
-
observed_at: value.observed_at,
|
|
79
|
-
type: value.type,
|
|
80
|
-
};
|
|
81
|
-
if (value.type === 'protocol') {
|
|
82
|
-
if (!EVENT_PHASES.has(value.phase) || value.phase === 'created') return null;
|
|
83
|
-
event.phase = value.phase;
|
|
84
|
-
if (value.phase === 'initialized') {
|
|
85
|
-
if (value.protocol_version !== ACP_PROTOCOL_VERSION
|
|
86
|
-
|| !boundedCount(value.auth_method_count)) return null;
|
|
87
|
-
event.protocol_version = value.protocol_version;
|
|
88
|
-
event.auth_method_count = value.auth_method_count;
|
|
89
|
-
}
|
|
90
|
-
return event;
|
|
91
|
-
}
|
|
92
|
-
if (value.type === 'session') {
|
|
93
|
-
if (value.phase !== 'created') return null;
|
|
94
|
-
event.phase = value.phase;
|
|
95
|
-
return event;
|
|
96
|
-
}
|
|
97
|
-
if (value.type === 'update') {
|
|
98
|
-
if (!UPDATE_TYPES.has(value.update_type) || value.update_type === 'agent_thought_chunk') {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
event.update_type = value.update_type;
|
|
102
|
-
if (['agent_message_chunk', 'user_message_chunk'].includes(value.update_type)) {
|
|
103
|
-
if (value.content_type !== 'text') return null;
|
|
104
|
-
event.content_type = value.content_type;
|
|
105
|
-
} else if (['tool_call', 'tool_call_update'].includes(value.update_type)) {
|
|
106
|
-
if (!(value.tool_kind === null || TOOL_KINDS.has(value.tool_kind))
|
|
107
|
-
|| !(value.tool_status === null || TOOL_STATUSES.has(value.tool_status))
|
|
108
|
-
|| !boundedCount(value.location_count)) return null;
|
|
109
|
-
event.tool_kind = value.tool_kind;
|
|
110
|
-
event.tool_status = value.tool_status;
|
|
111
|
-
event.location_count = value.location_count;
|
|
112
|
-
} else if (value.update_type === 'plan') {
|
|
113
|
-
if (!boundedCount(value.entry_count)) return null;
|
|
114
|
-
event.entry_count = value.entry_count;
|
|
115
|
-
}
|
|
116
|
-
return event;
|
|
117
|
-
}
|
|
118
|
-
if (value.type === 'notification') {
|
|
119
|
-
if (value.method !== 'ignored_extension') return null;
|
|
120
|
-
event.method = value.method;
|
|
121
|
-
return event;
|
|
122
|
-
}
|
|
123
|
-
if (value.type === 'client_request') {
|
|
124
|
-
if (value.method !== 'unsupported' || value.decision !== 'denied') return null;
|
|
125
|
-
event.method = value.method;
|
|
126
|
-
event.decision = value.decision;
|
|
127
|
-
return event;
|
|
128
|
-
}
|
|
129
|
-
if (value.type === 'permission') {
|
|
130
|
-
if (!TOOL_KINDS.has(value.tool_kind) || !PERMISSION_DECISIONS.has(value.decision)
|
|
131
|
-
|| !PERMISSION_SCOPES.has(value.scope)) return null;
|
|
132
|
-
event.tool_kind = value.tool_kind;
|
|
133
|
-
event.decision = value.decision;
|
|
134
|
-
event.scope = value.scope;
|
|
135
|
-
return event;
|
|
136
|
-
}
|
|
137
|
-
if (value.type === 'terminal') {
|
|
138
|
-
if (!STOP_REASONS.has(value.stop_reason)) return null;
|
|
139
|
-
event.stop_reason = value.stop_reason;
|
|
140
|
-
return event;
|
|
141
|
-
}
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
class DerivedEventJournal {
|
|
146
|
-
constructor(file) {
|
|
147
|
-
this.file = file;
|
|
148
|
-
this.count = 0;
|
|
149
|
-
this.bytes = 0;
|
|
150
|
-
fs.writeFileSync(file, '', { mode: 0o600 });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
append(event) {
|
|
154
|
-
if (this.count + 1 > MAX_EVENTS) {
|
|
155
|
-
throw new AcpFailure('acp_event_limit', 'ACP derived event journal exceeded its physical limit.');
|
|
156
|
-
}
|
|
157
|
-
const payload = {
|
|
158
|
-
sequence: this.count + 1,
|
|
159
|
-
observed_at: new Date().toISOString(),
|
|
160
|
-
...event,
|
|
161
|
-
};
|
|
162
|
-
const projected = projectedDerivedEvent(payload);
|
|
163
|
-
if (!projected) {
|
|
164
|
-
throw new AcpFailure('acp_event_shape', 'ACP derived event has an invalid safe shape.');
|
|
165
|
-
}
|
|
166
|
-
const line = `${JSON.stringify(projected)}\n`;
|
|
167
|
-
const bytes = Buffer.byteLength(line);
|
|
168
|
-
if (this.bytes + bytes > MAX_EVENT_BYTES) {
|
|
169
|
-
throw new AcpFailure('acp_event_limit', 'ACP derived event journal exceeded its physical limit.');
|
|
170
|
-
}
|
|
171
|
-
fs.appendFileSync(this.file, line);
|
|
172
|
-
this.count += 1;
|
|
173
|
-
this.bytes += bytes;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function deferred() {
|
|
178
|
-
let resolve;
|
|
179
|
-
let reject;
|
|
180
|
-
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
181
|
-
resolve = resolvePromise;
|
|
182
|
-
reject = rejectPromise;
|
|
183
|
-
});
|
|
184
|
-
return { promise, resolve, reject };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function isObject(value) {
|
|
188
|
-
return !!value && !Array.isArray(value) && typeof value === 'object';
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function delay(milliseconds) {
|
|
192
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function atOrInside(root, candidate) {
|
|
196
|
-
const relative = path.relative(root, candidate);
|
|
197
|
-
return relative === '' || (
|
|
198
|
-
relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function nearestExistingAncestor(candidate) {
|
|
203
|
-
let current = candidate;
|
|
204
|
-
for (;;) {
|
|
205
|
-
if (fs.existsSync(current)) return fs.realpathSync(current);
|
|
206
|
-
const parent = path.dirname(current);
|
|
207
|
-
if (parent === current) return null;
|
|
208
|
-
current = parent;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function authorizedLocation(spec, value) {
|
|
213
|
-
if (typeof value !== 'string' || !path.isAbsolute(value) || path.normalize(value) !== value) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
const protectedRoot = path.join(spec.cwd, '.ultra');
|
|
217
|
-
if (atOrInside(protectedRoot, value)) return false;
|
|
218
|
-
const authorizedRoot = spec.writable_root_paths.find((root) => atOrInside(root, value));
|
|
219
|
-
if (!authorizedRoot) return false;
|
|
220
|
-
const ancestor = nearestExistingAncestor(value);
|
|
221
|
-
let realRoot;
|
|
222
|
-
try { realRoot = fs.realpathSync(authorizedRoot); } catch { return false; }
|
|
223
|
-
return !!ancestor && atOrInside(realRoot, ancestor);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function permissionOption(options, kind) {
|
|
227
|
-
return options.find((option) => (
|
|
228
|
-
isObject(option) && typeof option.optionId === 'string' && option.kind === kind
|
|
229
|
-
)) || null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function permissionDecision(spec, params) {
|
|
233
|
-
if (!isObject(params) || !isObject(params.toolCall) || !Array.isArray(params.options)) {
|
|
234
|
-
throw new AcpFailure('acp_malformed_permission', 'ACP permission request is malformed.');
|
|
235
|
-
}
|
|
236
|
-
const tool = params.toolCall;
|
|
237
|
-
const kind = TOOL_KINDS.has(tool.kind) ? tool.kind : 'other';
|
|
238
|
-
let allowed = false;
|
|
239
|
-
let scope = 'operation_denied';
|
|
240
|
-
if (['read', 'search', 'think'].includes(kind)) {
|
|
241
|
-
allowed = true;
|
|
242
|
-
scope = 'inside_envelope';
|
|
243
|
-
} else if (['edit', 'delete', 'move'].includes(kind)) {
|
|
244
|
-
const locations = Array.isArray(tool.locations) ? tool.locations : [];
|
|
245
|
-
allowed = !spec.read_only && locations.length > 0 && locations.every((location) => (
|
|
246
|
-
isObject(location) && authorizedLocation(spec, location.path)
|
|
247
|
-
));
|
|
248
|
-
scope = allowed ? 'inside_envelope' : 'outside_envelope';
|
|
249
|
-
}
|
|
250
|
-
let selected = allowed ? permissionOption(params.options, 'allow_once') : null;
|
|
251
|
-
if (!selected) {
|
|
252
|
-
allowed = false;
|
|
253
|
-
if (scope === 'inside_envelope') scope = 'allow_once_unavailable';
|
|
254
|
-
selected = permissionOption(params.options, 'reject_once')
|
|
255
|
-
|| permissionOption(params.options, 'reject_always');
|
|
256
|
-
}
|
|
257
|
-
if (!selected) {
|
|
258
|
-
return {
|
|
259
|
-
result: { outcome: { outcome: 'cancelled' } },
|
|
260
|
-
allowed: false,
|
|
261
|
-
decision: 'cancelled',
|
|
262
|
-
scope: scope === 'inside_envelope' ? 'reject_option_unavailable' : scope,
|
|
263
|
-
kind,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
return {
|
|
267
|
-
result: { outcome: { outcome: 'selected', optionId: selected.optionId } },
|
|
268
|
-
allowed,
|
|
269
|
-
decision: selected.kind,
|
|
270
|
-
scope,
|
|
271
|
-
kind,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
class JsonRpcConnection {
|
|
276
|
-
constructor(child, { onNotification, onRequest, onFatal }) {
|
|
277
|
-
this.child = child;
|
|
278
|
-
this.onNotification = onNotification;
|
|
279
|
-
this.onRequest = onRequest;
|
|
280
|
-
this.onFatal = onFatal;
|
|
281
|
-
this.nextId = 1;
|
|
282
|
-
this.pending = new Map();
|
|
283
|
-
this.buffer = '';
|
|
284
|
-
this.failed = false;
|
|
285
|
-
child.stdout.setEncoding('utf8');
|
|
286
|
-
child.stdout.on('data', (chunk) => this.consume(chunk));
|
|
287
|
-
child.stdin.on('error', (error) => {
|
|
288
|
-
if (!['EPIPE', 'ERR_STREAM_DESTROYED'].includes(error.code)) this.fail(error);
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
send(payload) {
|
|
293
|
-
if (this.failed || this.child.stdin.destroyed) return false;
|
|
294
|
-
this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', ...payload })}\n`);
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
request(method, params) {
|
|
299
|
-
const id = this.nextId++;
|
|
300
|
-
const pending = deferred();
|
|
301
|
-
this.pending.set(id, pending);
|
|
302
|
-
if (!this.send({ id, method, params })) {
|
|
303
|
-
this.pending.delete(id);
|
|
304
|
-
pending.reject(new AcpFailure('acp_process_exit', 'ACP process closed before request delivery.'));
|
|
305
|
-
}
|
|
306
|
-
return pending.promise;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
notification(method, params) {
|
|
310
|
-
this.send({ method, params });
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
consume(chunk) {
|
|
314
|
-
if (this.failed) return;
|
|
315
|
-
this.buffer += chunk;
|
|
316
|
-
if (Buffer.byteLength(this.buffer) > MAX_MESSAGE_BYTES && !this.buffer.includes('\n')) {
|
|
317
|
-
this.fail(new AcpFailure('acp_message_too_large', 'ACP message exceeded the physical line limit.'));
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
for (;;) {
|
|
321
|
-
const newline = this.buffer.indexOf('\n');
|
|
322
|
-
if (newline < 0) break;
|
|
323
|
-
const line = this.buffer.slice(0, newline);
|
|
324
|
-
this.buffer = this.buffer.slice(newline + 1);
|
|
325
|
-
if (!line.trim()) continue;
|
|
326
|
-
if (Buffer.byteLength(line) > MAX_MESSAGE_BYTES) {
|
|
327
|
-
this.fail(new AcpFailure('acp_message_too_large', 'ACP message exceeded the physical line limit.'));
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
let message;
|
|
331
|
-
try { message = JSON.parse(line); } catch {
|
|
332
|
-
this.fail(new AcpFailure('acp_malformed_message', 'ACP emitted malformed JSON.'));
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
try { this.handle(message); } catch (error) { this.fail(error); return; }
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
handle(message) {
|
|
340
|
-
if (!isObject(message) || message.jsonrpc !== '2.0') {
|
|
341
|
-
throw new AcpFailure('acp_malformed_message', 'ACP message must be a JSON-RPC 2.0 object.');
|
|
342
|
-
}
|
|
343
|
-
if ('id' in message && !('method' in message) && ('result' in message || 'error' in message)) {
|
|
344
|
-
const hasResult = Object.prototype.hasOwnProperty.call(message, 'result');
|
|
345
|
-
const hasError = Object.prototype.hasOwnProperty.call(message, 'error');
|
|
346
|
-
if (hasResult === hasError) {
|
|
347
|
-
throw new AcpFailure(
|
|
348
|
-
'acp_malformed_message',
|
|
349
|
-
'ACP response must contain exactly one of result or error.',
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
if (hasError && (!isObject(message.error) || !Number.isInteger(message.error.code)
|
|
353
|
-
|| typeof message.error.message !== 'string')) {
|
|
354
|
-
throw new AcpFailure('acp_malformed_message', 'ACP response error is malformed.');
|
|
355
|
-
}
|
|
356
|
-
const pending = this.pending.get(message.id);
|
|
357
|
-
if (!pending) throw new AcpFailure('acp_unknown_response', 'ACP response id was not pending.');
|
|
358
|
-
this.pending.delete(message.id);
|
|
359
|
-
if (hasError) pending.reject(new AcpRpcError(message.error));
|
|
360
|
-
else pending.resolve(message.result);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
if (typeof message.method === 'string' && 'id' in message) {
|
|
364
|
-
this.onRequest(message);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
if (typeof message.method === 'string' && !('id' in message)) {
|
|
368
|
-
this.onNotification(message);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
throw new AcpFailure('acp_malformed_message', 'ACP message has no valid request, response, or notification shape.');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
respond(id, result) {
|
|
375
|
-
this.send({ id, result });
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
respondError(id, code, message) {
|
|
379
|
-
this.send({ id, error: { code, message } });
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
close(error = new AcpFailure('acp_process_exit', 'ACP process exited before the request completed.')) {
|
|
383
|
-
for (const pending of this.pending.values()) pending.reject(error);
|
|
384
|
-
this.pending.clear();
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
fail(error) {
|
|
388
|
-
if (this.failed) return;
|
|
389
|
-
this.failed = true;
|
|
390
|
-
this.close(error);
|
|
391
|
-
this.onFatal(error instanceof Error ? error : new Error(String(error)));
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
class AcpDriver extends AgentExecutionDriver {
|
|
396
|
-
async run() {
|
|
397
|
-
const spec = this.spec;
|
|
398
|
-
if (!path.isAbsolute(spec.cwd)) {
|
|
399
|
-
return this.failure('acp_invalid_cwd', 'ACP cwd must be absolute.');
|
|
400
|
-
}
|
|
401
|
-
const journal = new DerivedEventJournal(spec.events);
|
|
402
|
-
const stderr = new BoundedCapture(spec.capture_limit_bytes || DEFAULT_CAPTURE_LIMIT_BYTES);
|
|
403
|
-
const protocolStdout = new BoundedCapture(spec.capture_limit_bytes || DEFAULT_CAPTURE_LIMIT_BYTES);
|
|
404
|
-
const childClosed = deferred();
|
|
405
|
-
const fatal = deferred();
|
|
406
|
-
const aborted = deferred();
|
|
407
|
-
let spawnError = null;
|
|
408
|
-
let termination = null;
|
|
409
|
-
let protocolVersion = null;
|
|
410
|
-
let sessionId = null;
|
|
411
|
-
let stopReason = null;
|
|
412
|
-
let agentOutput = '';
|
|
413
|
-
let permissionDenied = null;
|
|
414
|
-
let promptPromise = null;
|
|
415
|
-
let expectedClose = false;
|
|
416
|
-
let sessionCreationPending = false;
|
|
417
|
-
let earlySessionUpdateBytes = 0;
|
|
418
|
-
let earlySessionUpdates = [];
|
|
419
|
-
const pendingPermissions = new Map();
|
|
420
|
-
const child = spawn(spec.command, spec.args, {
|
|
421
|
-
cwd: spec.cwd,
|
|
422
|
-
env: { ...process.env, ...spec.env },
|
|
423
|
-
detached: process.platform !== 'win32',
|
|
424
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
425
|
-
});
|
|
426
|
-
child.stderr.on('data', (chunk) => stderr.append(chunk));
|
|
427
|
-
child.on('error', (error) => {
|
|
428
|
-
spawnError = error;
|
|
429
|
-
fatal.resolve(new AcpFailure('spawn_error', 'ACP host process failed to start.'));
|
|
430
|
-
});
|
|
431
|
-
child.on('close', (code, signal) => {
|
|
432
|
-
childClosed.resolve({ code, signal });
|
|
433
|
-
if (!expectedClose) connection.close();
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
const appendEvent = (event) => journal.append(event);
|
|
437
|
-
|
|
438
|
-
const handleUpdate = (message) => {
|
|
439
|
-
const params = message.params;
|
|
440
|
-
const envelopeValid = isObject(params)
|
|
441
|
-
&& typeof params.sessionId === 'string'
|
|
442
|
-
&& params.sessionId.length > 0
|
|
443
|
-
&& isObject(params.update);
|
|
444
|
-
if (!envelopeValid) {
|
|
445
|
-
throw new AcpFailure(
|
|
446
|
-
'acp_malformed_update',
|
|
447
|
-
'ACP session/update envelope is invalid '
|
|
448
|
-
+ `(params_object=${isObject(params)}, session_id_type=${typeof params?.sessionId}, `
|
|
449
|
-
+ `session_established=${typeof sessionId === 'string'}, `
|
|
450
|
-
+ `session_matches=${params?.sessionId === sessionId}, update_object=${isObject(params?.update)}).`,
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
if (sessionId === null && sessionCreationPending) {
|
|
454
|
-
const updateType = params.update.sessionUpdate;
|
|
455
|
-
if (!EARLY_SESSION_UPDATE_TYPES.has(updateType)) {
|
|
456
|
-
throw new AcpFailure(
|
|
457
|
-
'acp_early_update_type',
|
|
458
|
-
'ACP emitted a non-metadata session update before session/new completed.',
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
const messageBytes = Buffer.byteLength(JSON.stringify(message));
|
|
462
|
-
if (earlySessionUpdates.length >= MAX_EARLY_SESSION_UPDATES
|
|
463
|
-
|| earlySessionUpdateBytes + messageBytes > MAX_EARLY_SESSION_UPDATE_BYTES) {
|
|
464
|
-
throw new AcpFailure(
|
|
465
|
-
'acp_early_update_limit',
|
|
466
|
-
'ACP pre-session metadata exceeded its physical buffer limit.',
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
earlySessionUpdateBytes += messageBytes;
|
|
470
|
-
earlySessionUpdates.push(message);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
if (params.sessionId !== sessionId) {
|
|
474
|
-
throw new AcpFailure(
|
|
475
|
-
'acp_malformed_update',
|
|
476
|
-
'ACP session/update envelope is invalid '
|
|
477
|
-
+ `(params_object=true, session_id_type=string, `
|
|
478
|
-
+ `session_established=${typeof sessionId === 'string'}, `
|
|
479
|
-
+ 'session_matches=false, update_object=true).',
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
const update = params.update;
|
|
483
|
-
const updateType = update.sessionUpdate;
|
|
484
|
-
if (!UPDATE_TYPES.has(updateType)) {
|
|
485
|
-
throw new AcpFailure('acp_malformed_update', 'ACP session/update type is not part of stable v1.');
|
|
486
|
-
}
|
|
487
|
-
if (updateType === 'agent_thought_chunk') return;
|
|
488
|
-
if (['agent_message_chunk', 'user_message_chunk'].includes(updateType)) {
|
|
489
|
-
if (!isObject(update.content) || update.content.type !== 'text'
|
|
490
|
-
|| typeof update.content.text !== 'string') {
|
|
491
|
-
throw new AcpFailure('acp_malformed_update', 'ACP message chunk must contain text content.');
|
|
492
|
-
}
|
|
493
|
-
if (updateType === 'agent_message_chunk') {
|
|
494
|
-
agentOutput += update.content.text;
|
|
495
|
-
if (Buffer.byteLength(agentOutput) > MAX_AGENT_OUTPUT_BYTES) {
|
|
496
|
-
throw new AcpFailure('acp_output_limit', 'ACP agent output exceeded its physical limit.');
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
appendEvent({ type: 'update', update_type: updateType, content_type: 'text' });
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
if (['tool_call', 'tool_call_update'].includes(updateType)) {
|
|
503
|
-
if (typeof update.toolCallId !== 'string' || !update.toolCallId) {
|
|
504
|
-
throw new AcpFailure('acp_malformed_update', 'ACP tool update requires a toolCallId.');
|
|
505
|
-
}
|
|
506
|
-
appendEvent({
|
|
507
|
-
type: 'update',
|
|
508
|
-
update_type: updateType,
|
|
509
|
-
tool_kind: TOOL_KINDS.has(update.kind) ? update.kind : null,
|
|
510
|
-
tool_status: typeof update.status === 'string' ? update.status : null,
|
|
511
|
-
location_count: Array.isArray(update.locations) ? update.locations.length : 0,
|
|
512
|
-
});
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
appendEvent({
|
|
516
|
-
type: 'update',
|
|
517
|
-
update_type: updateType,
|
|
518
|
-
...(updateType === 'plan'
|
|
519
|
-
? { entry_count: Array.isArray(update.entries) ? update.entries.length : 0 }
|
|
520
|
-
: {}),
|
|
521
|
-
});
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
const connection = new JsonRpcConnection(child, {
|
|
525
|
-
onFatal: (error) => fatal.resolve(error),
|
|
526
|
-
onNotification: (message) => {
|
|
527
|
-
try {
|
|
528
|
-
if (message.method === 'session/update') handleUpdate(message);
|
|
529
|
-
else appendEvent({ type: 'notification', method: 'ignored_extension' });
|
|
530
|
-
} catch (error) { fatal.resolve(error); }
|
|
531
|
-
},
|
|
532
|
-
onRequest: (message) => {
|
|
533
|
-
if (message.method !== 'session/request_permission') {
|
|
534
|
-
connection.respondError(message.id, -32601, 'Client method is not available.');
|
|
535
|
-
appendEvent({ type: 'client_request', method: 'unsupported', decision: 'denied' });
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
const pending = { responded: false };
|
|
539
|
-
pendingPermissions.set(message.id, pending);
|
|
540
|
-
try {
|
|
541
|
-
if (message.params?.sessionId !== sessionId) {
|
|
542
|
-
throw new AcpFailure('acp_malformed_permission', 'ACP permission session does not match.');
|
|
543
|
-
}
|
|
544
|
-
const decision = permissionDecision(spec, message.params);
|
|
545
|
-
pending.responded = true;
|
|
546
|
-
connection.respond(message.id, decision.result);
|
|
547
|
-
pendingPermissions.delete(message.id);
|
|
548
|
-
appendEvent({
|
|
549
|
-
type: 'permission',
|
|
550
|
-
tool_kind: decision.kind,
|
|
551
|
-
decision: decision.decision,
|
|
552
|
-
scope: decision.scope,
|
|
553
|
-
});
|
|
554
|
-
if (!decision.allowed) {
|
|
555
|
-
permissionDenied = decision;
|
|
556
|
-
fatal.resolve(new AcpFailure(
|
|
557
|
-
'permission_denied',
|
|
558
|
-
`ACP ${decision.kind} request was denied (${decision.scope}).`,
|
|
559
|
-
));
|
|
560
|
-
}
|
|
561
|
-
} catch (error) {
|
|
562
|
-
pendingPermissions.delete(message.id);
|
|
563
|
-
fatal.resolve(error);
|
|
564
|
-
}
|
|
565
|
-
},
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
const cancelPendingPermissions = () => {
|
|
569
|
-
for (const [id, pending] of pendingPermissions.entries()) {
|
|
570
|
-
if (!pending.responded) {
|
|
571
|
-
pending.responded = true;
|
|
572
|
-
connection.respond(id, { outcome: { outcome: 'cancelled' } });
|
|
573
|
-
}
|
|
574
|
-
pendingPermissions.delete(id);
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
const requestTermination = (reason) => {
|
|
578
|
-
if (termination) return;
|
|
579
|
-
termination = reason;
|
|
580
|
-
aborted.resolve(new AcpFailure(reason, `ACP delegation ${reason}.`));
|
|
581
|
-
};
|
|
582
|
-
const cancelPoll = setInterval(() => {
|
|
583
|
-
if (fs.existsSync(spec.cancel)) requestTermination('cancelled');
|
|
584
|
-
}, 50);
|
|
585
|
-
const timeout = setTimeout(() => requestTermination('timeout'), spec.timeout_ms);
|
|
586
|
-
cancelPoll.unref();
|
|
587
|
-
timeout.unref();
|
|
588
|
-
|
|
589
|
-
const waitStep = (promise) => Promise.race([
|
|
590
|
-
promise,
|
|
591
|
-
fatal.promise.then((error) => { throw error; }),
|
|
592
|
-
aborted.promise.then((error) => { throw error; }),
|
|
593
|
-
]);
|
|
594
|
-
const shutdown = async ({ graceful = false, cancel = false } = {}) => {
|
|
595
|
-
if (cancel && sessionId) {
|
|
596
|
-
connection.notification('session/cancel', { sessionId });
|
|
597
|
-
cancelPendingPermissions();
|
|
598
|
-
try { appendEvent({ type: 'protocol', phase: 'cancel_requested' }); } catch {}
|
|
599
|
-
if (promptPromise) {
|
|
600
|
-
try {
|
|
601
|
-
const response = await Promise.race([promptPromise, delay(300).then(() => null)]);
|
|
602
|
-
if (response?.stopReason === 'cancelled') {
|
|
603
|
-
try { appendEvent({ type: 'protocol', phase: 'cancel_confirmed' }); } catch {}
|
|
604
|
-
}
|
|
605
|
-
} catch {}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
expectedClose = true;
|
|
609
|
-
// A Windows process tree cannot be addressed after its root PID exits. For
|
|
610
|
-
// cancellation/timeout, terminate the tree while the adapter is still alive;
|
|
611
|
-
// the earlier ACP cancel handshake has already had its bounded grace period.
|
|
612
|
-
if (!graceful && process.platform === 'win32') {
|
|
613
|
-
terminateProcessGroup(child, 'SIGTERM');
|
|
614
|
-
}
|
|
615
|
-
try { child.stdin.end(); } catch {}
|
|
616
|
-
if (graceful) {
|
|
617
|
-
const closed = await Promise.race([childClosed.promise, delay(750).then(() => null)]);
|
|
618
|
-
if (closed) return closed;
|
|
619
|
-
} else {
|
|
620
|
-
await Promise.race([childClosed.promise, delay(150).then(() => null)]);
|
|
621
|
-
}
|
|
622
|
-
terminateProcessGroup(child, 'SIGTERM');
|
|
623
|
-
const closed = await Promise.race([childClosed.promise, delay(500).then(() => null)]);
|
|
624
|
-
terminateProcessGroup(child, 'SIGKILL');
|
|
625
|
-
return closed || await Promise.race([childClosed.promise, delay(500).then(() => ({ code: null, signal: 'SIGKILL' }))]);
|
|
626
|
-
};
|
|
627
|
-
|
|
628
|
-
let outcome;
|
|
629
|
-
try {
|
|
630
|
-
const initialized = await waitStep(connection.request('initialize', {
|
|
631
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
632
|
-
clientCapabilities: {
|
|
633
|
-
fs: { readTextFile: false, writeTextFile: false },
|
|
634
|
-
terminal: false,
|
|
635
|
-
auth: { terminal: false },
|
|
636
|
-
},
|
|
637
|
-
clientInfo: { name: 'ultra-builder-pro', version: '5.0.0' },
|
|
638
|
-
}));
|
|
639
|
-
if (!isObject(initialized) || initialized.protocolVersion !== ACP_PROTOCOL_VERSION) {
|
|
640
|
-
throw new AcpFailure('acp_protocol_version', 'ACP agent did not negotiate stable protocol v1.');
|
|
641
|
-
}
|
|
642
|
-
protocolVersion = initialized.protocolVersion;
|
|
643
|
-
appendEvent({
|
|
644
|
-
type: 'protocol', phase: 'initialized', protocol_version: protocolVersion,
|
|
645
|
-
auth_method_count: Array.isArray(initialized.authMethods) ? initialized.authMethods.length : 0,
|
|
646
|
-
});
|
|
647
|
-
let session;
|
|
648
|
-
sessionCreationPending = true;
|
|
649
|
-
try {
|
|
650
|
-
session = await waitStep(connection.request('session/new', {
|
|
651
|
-
cwd: spec.cwd,
|
|
652
|
-
mcpServers: [],
|
|
653
|
-
}));
|
|
654
|
-
} catch (error) {
|
|
655
|
-
if (error instanceof AcpRpcError && (error.code === -32000 || /auth/iu.test(error.message))) {
|
|
656
|
-
throw new AcpFailure('acp_auth_required', 'ACP agent requires authentication outside this delegation.');
|
|
657
|
-
}
|
|
658
|
-
throw error;
|
|
659
|
-
} finally {
|
|
660
|
-
sessionCreationPending = false;
|
|
661
|
-
}
|
|
662
|
-
if (!isObject(session) || typeof session.sessionId !== 'string' || !session.sessionId) {
|
|
663
|
-
throw new AcpFailure('acp_malformed_session', 'ACP session/new returned no sessionId.');
|
|
664
|
-
}
|
|
665
|
-
sessionId = session.sessionId;
|
|
666
|
-
appendEvent({ type: 'session', phase: 'created' });
|
|
667
|
-
const bufferedSessionUpdates = earlySessionUpdates;
|
|
668
|
-
earlySessionUpdates = [];
|
|
669
|
-
earlySessionUpdateBytes = 0;
|
|
670
|
-
for (const message of bufferedSessionUpdates) handleUpdate(message);
|
|
671
|
-
promptPromise = connection.request('session/prompt', {
|
|
672
|
-
sessionId,
|
|
673
|
-
prompt: [{ type: 'text', text: spec.prompt }],
|
|
674
|
-
});
|
|
675
|
-
const promptResult = await waitStep(promptPromise);
|
|
676
|
-
if (!isObject(promptResult) || !STOP_REASONS.has(promptResult.stopReason)) {
|
|
677
|
-
throw new AcpFailure('acp_malformed_terminal', 'ACP session/prompt returned an invalid stopReason.');
|
|
678
|
-
}
|
|
679
|
-
stopReason = promptResult.stopReason;
|
|
680
|
-
appendEvent({ type: 'terminal', stop_reason: stopReason });
|
|
681
|
-
if (permissionDenied) {
|
|
682
|
-
outcome = {
|
|
683
|
-
failureType: 'permission_denied',
|
|
684
|
-
failureSummary: `ACP ${permissionDenied.kind} request was denied (${permissionDenied.scope}).`,
|
|
685
|
-
};
|
|
686
|
-
} else if (stopReason === 'cancelled') {
|
|
687
|
-
outcome = { termination: 'cancelled' };
|
|
688
|
-
} else if (agentOutput) {
|
|
689
|
-
fs.writeFileSync(spec.host_output, agentOutput, { mode: 0o600 });
|
|
690
|
-
}
|
|
691
|
-
const closed = await shutdown({ graceful: true });
|
|
692
|
-
outcome = {
|
|
693
|
-
// `session/prompt` is the ACP transport terminal. Closing stdin after
|
|
694
|
-
// that point is client-owned cleanup; adapters differ on the process
|
|
695
|
-
// exit code they use for EOF, so retain it as an observation without
|
|
696
|
-
// converting a completed turn into a task failure.
|
|
697
|
-
code: 0,
|
|
698
|
-
signal: null,
|
|
699
|
-
adapterExitCode: closed?.code ?? null,
|
|
700
|
-
adapterExitSignal: closed?.signal || null,
|
|
701
|
-
termination: outcome?.termination || null,
|
|
702
|
-
spawnError,
|
|
703
|
-
protocolVersion,
|
|
704
|
-
stopReason,
|
|
705
|
-
...outcome,
|
|
706
|
-
};
|
|
707
|
-
} catch (error) {
|
|
708
|
-
const failure = error instanceof AcpFailure
|
|
709
|
-
? error
|
|
710
|
-
: error instanceof AcpRpcError
|
|
711
|
-
? new AcpFailure('acp_rpc_error', 'ACP request returned a JSON-RPC error.')
|
|
712
|
-
: new AcpFailure('acp_protocol_error', 'ACP transport failed.');
|
|
713
|
-
const wasTermination = ['cancelled', 'timeout'].includes(failure.failureType);
|
|
714
|
-
const closed = await shutdown({ cancel: wasTermination || !!sessionId });
|
|
715
|
-
outcome = {
|
|
716
|
-
code: closed?.code ?? null,
|
|
717
|
-
signal: closed?.signal || null,
|
|
718
|
-
adapterExitCode: closed?.code ?? null,
|
|
719
|
-
adapterExitSignal: closed?.signal || null,
|
|
720
|
-
termination: wasTermination ? failure.failureType : null,
|
|
721
|
-
spawnError,
|
|
722
|
-
protocolVersion,
|
|
723
|
-
stopReason,
|
|
724
|
-
...(wasTermination ? {} : {
|
|
725
|
-
failureType: failure.failureType,
|
|
726
|
-
failureSummary: failure.message,
|
|
727
|
-
}),
|
|
728
|
-
};
|
|
729
|
-
stderr.append(`ultra-delegate ACP failure: ${failure.failureType}\n`);
|
|
730
|
-
} finally {
|
|
731
|
-
clearInterval(cancelPoll);
|
|
732
|
-
clearTimeout(timeout);
|
|
733
|
-
connection.close();
|
|
734
|
-
protocolStdout.write(spec.stdout);
|
|
735
|
-
stderr.write(spec.stderr);
|
|
736
|
-
}
|
|
737
|
-
return outcome;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
failure(failureType, failureSummary) {
|
|
741
|
-
return {
|
|
742
|
-
code: null,
|
|
743
|
-
signal: null,
|
|
744
|
-
termination: null,
|
|
745
|
-
spawnError: null,
|
|
746
|
-
protocolVersion: null,
|
|
747
|
-
stopReason: null,
|
|
748
|
-
adapterExitCode: null,
|
|
749
|
-
adapterExitSignal: null,
|
|
750
|
-
failureType,
|
|
751
|
-
failureSummary,
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
module.exports = {
|
|
757
|
-
AcpDriver,
|
|
758
|
-
AcpFailure,
|
|
759
|
-
DerivedEventJournal,
|
|
760
|
-
permissionDecision,
|
|
761
|
-
projectedDerivedEvent,
|
|
762
|
-
};
|