@zq-silk/yui 2.1.0 → 2.3.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/README.md +8 -2
- package/dist/cli/commandCatalog.js +32 -6
- package/dist/cli/commandDiscovery.js +4 -1
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli.js +23 -3078
- package/dist/commands/taskCommands.js +84 -28
- package/dist/commands/taskIntegrationCommands.js +8 -17
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +6 -1
- package/dist/controlPlaneCli.js +3099 -0
- package/dist/controller/clientRuntime.js +3 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +58 -82
- package/dist/job/jobCleanupInspection.js +67 -0
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +176 -74
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/demo.js +12 -1
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +13 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +270 -133
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -0
- package/dist/nativeAgent/validation.js +2 -2
- package/dist/repository/gitWorkspace.js +41 -12
- package/dist/repository/taskWorkspaceCoordinator.js +72 -145
- package/dist/repository/taskWorkspacePreparer.js +39 -43
- package/dist/repository/workspaceCleanupInspection.js +88 -7
- package/dist/resources/resourceRegistrar.js +16 -10
- package/dist/runtime/integrationRuntimeIsolation.js +21 -0
- package/dist/runtime/taskRuntimeIsolation.js +91 -21
- package/dist/task/archiveDiagnostics.js +26 -8
- package/dist/task/archivePreflight.js +26 -9
- package/dist/task/leaderArchive.js +9 -15
- package/dist/task/taskAuthority.js +56 -0
- package/dist/web/assets/client/api.js +8 -0
- package/dist/web/assets/client/app.js +74 -9
- package/dist/web/assets/client/components.js +95 -36
- package/dist/web/assets/client/detail.js +110 -64
- package/dist/web/assets/client/dock.js +95 -11
- package/dist/web/assets/client/evidence.js +286 -94
- package/dist/web/assets/client/i18n.js +182 -62
- package/dist/web/assets/client/overview.js +9 -10
- package/dist/web/assets/client/records.js +142 -7
- package/dist/web/assets/client/sections.js +234 -198
- package/dist/web/assets/client/sidebar.js +31 -2
- package/dist/web/assets/icons.js +4 -0
- package/dist/web/assets/shell.js +2 -1
- package/dist/web/assets/styles/components.js +64 -15
- package/dist/web/assets/styles/layout.js +11 -4
- package/dist/web/assets/styles/responsive.js +16 -6
- package/dist/web/assets/styles/tokens.js +1 -1
- package/dist/web/assets/styles/views.js +62 -42
- package/dist/web/webServer.js +12 -1
- package/dist/web/webTaskSurface.js +8 -1
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/observability/README.md +13 -3
- package/docs/observability/README.zh-CN.md +10 -3
- package/docs/release-workflow.md +72 -9
- package/docs/release-workflow.zh-CN.md +55 -6
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/docs/task-delivery.md +36 -8
- package/docs/task-delivery.zh-CN.md +24 -7
- package/i18n/README.zh-CN.md +3 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-leader/references/integration.md +20 -0
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +8 -0
- package/skills/yui-runtime/SKILL.md +11 -0
- package/skills/yui-runtime/references/publication.md +20 -4
- package/skills/yui-worker/SKILL.md +3 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Match the existing model/tool wire budgets; all sizes are UTF-8 JSON bytes.
|
|
2
|
+
const messageBytes = 512 * 1024;
|
|
3
|
+
const argumentBytes = 64 * 1024;
|
|
4
|
+
const callsPerBatch = 8;
|
|
5
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
6
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
7
|
+
const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
8
|
+
const identifier = (value) => nonempty(value) && Buffer.byteLength(value, 'utf8') <= 1024;
|
|
9
|
+
const bytes = (value) => Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
10
|
+
function json(value, depth = 0) {
|
|
11
|
+
if (depth > 32)
|
|
12
|
+
return false;
|
|
13
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
14
|
+
return true;
|
|
15
|
+
if (typeof value === 'number')
|
|
16
|
+
return Number.isFinite(value);
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return Array.from(value).every(v => json(v, depth + 1));
|
|
19
|
+
return object(value) && Object.values(value).every(v => json(v, depth + 1));
|
|
20
|
+
}
|
|
21
|
+
function freeze(value) {
|
|
22
|
+
if (value && typeof value === 'object') {
|
|
23
|
+
for (const child of Object.values(value))
|
|
24
|
+
freeze(child);
|
|
25
|
+
Object.freeze(value);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function snapshot(value, limit) {
|
|
30
|
+
const copy = structuredClone(value);
|
|
31
|
+
if (!json(copy) || bytes(copy) > limit)
|
|
32
|
+
throw new Error('Invalid or oversized JSON');
|
|
33
|
+
return freeze(copy);
|
|
34
|
+
}
|
|
35
|
+
function failure(code, message, effect = 'none') {
|
|
36
|
+
return { ok: false, error: { code, message, effect } };
|
|
37
|
+
}
|
|
38
|
+
function outcome(raw, identity, beforeStart = false) {
|
|
39
|
+
try {
|
|
40
|
+
const value = snapshot(raw, messageBytes);
|
|
41
|
+
if (!object(value))
|
|
42
|
+
throw new Error();
|
|
43
|
+
let normalized;
|
|
44
|
+
if (!beforeStart && value.ok === true && typeof value.content === 'string') {
|
|
45
|
+
normalized = { ok: true, content: value.content };
|
|
46
|
+
}
|
|
47
|
+
else if (value.ok === false && object(value.error) && nonempty(value.error.code)
|
|
48
|
+
&& typeof value.error.message === 'string'
|
|
49
|
+
&& (value.error.effect === 'none' || (!beforeStart && value.error.effect === 'unknown'))) {
|
|
50
|
+
normalized = { ok: false, error: {
|
|
51
|
+
code: value.error.code, message: value.error.message, effect: value.error.effect,
|
|
52
|
+
} };
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
throw new Error();
|
|
56
|
+
// Guarantee the consumer can append the paired message without truncation.
|
|
57
|
+
if (bytes({ role: 'tool', toolCallId: identity.toolCallId, name: identity.name, outcome: normalized }) > messageBytes) {
|
|
58
|
+
throw new Error();
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return failure('tool_protocol', 'Invalid or oversized tool result', beforeStart ? 'none' : 'unknown');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function status(started, value) {
|
|
67
|
+
if (!started)
|
|
68
|
+
return 'not_executed';
|
|
69
|
+
if (value.ok)
|
|
70
|
+
return 'succeeded';
|
|
71
|
+
if (value.error.effect === 'unknown')
|
|
72
|
+
return 'unknown';
|
|
73
|
+
return value.error.code === 'cancelled' ? 'cancelled' : 'failed';
|
|
74
|
+
}
|
|
75
|
+
export function buildExecutor(options) {
|
|
76
|
+
if (!options?.environment || typeof options.environment.acquire !== 'function') {
|
|
77
|
+
throw new Error('Tool environment.acquire capability is required');
|
|
78
|
+
}
|
|
79
|
+
if (!options.permission || typeof options.permission.check !== 'function') {
|
|
80
|
+
throw new Error('Tool permission.check capability is required');
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(options.tools))
|
|
83
|
+
throw new Error('Tool collection is required');
|
|
84
|
+
const acquire = options.environment.acquire.bind(options.environment);
|
|
85
|
+
const check = options.permission.check.bind(options.permission);
|
|
86
|
+
const registry = new Map();
|
|
87
|
+
for (const tool of options.tools) {
|
|
88
|
+
let definition;
|
|
89
|
+
try {
|
|
90
|
+
definition = snapshot(tool.definition, messageBytes);
|
|
91
|
+
if (!identifier(definition.name) || typeof definition.description !== 'string'
|
|
92
|
+
|| !object(definition.inputSchema) || typeof tool.validate !== 'function'
|
|
93
|
+
|| typeof tool.execute !== 'function')
|
|
94
|
+
throw new Error();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new Error('Invalid tool declaration or implementation');
|
|
98
|
+
}
|
|
99
|
+
if (registry.has(definition.name))
|
|
100
|
+
throw new Error('Tool names must be unique');
|
|
101
|
+
registry.set(definition.name, {
|
|
102
|
+
definition, validate: tool.validate.bind(tool), execute: tool.execute.bind(tool),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const definitions = snapshot([...registry.values()].map(t => t.definition), messageBytes);
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
definitions,
|
|
108
|
+
async executeBatch(request) {
|
|
109
|
+
// Admit the whole immutable batch before acquiring any resource.
|
|
110
|
+
let scope, calls;
|
|
111
|
+
try {
|
|
112
|
+
scope = snapshot(request.scope, 4096);
|
|
113
|
+
calls = snapshot(request.calls, messageBytes);
|
|
114
|
+
if (!identifier(scope.sessionId) || !identifier(scope.turnId) || !Number.isSafeInteger(scope.step)
|
|
115
|
+
|| scope.step < 1 || !Array.isArray(calls) || !calls.length || calls.length > callsPerBatch
|
|
116
|
+
|| calls.some(c => !object(c) || !identifier(c.id) || !identifier(c.name)
|
|
117
|
+
|| !json(c.arguments) || bytes(c.arguments) > argumentBytes)
|
|
118
|
+
|| new Set(calls.map(c => c.id)).size !== calls.length)
|
|
119
|
+
throw new Error();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error('Invalid tool batch: bounded JSON, scope and unique call identities required');
|
|
123
|
+
}
|
|
124
|
+
// Missing recording/cancellation capabilities cannot silently become no-ops.
|
|
125
|
+
const signal = request.signal;
|
|
126
|
+
const beforeExecute = request.beforeExecute;
|
|
127
|
+
const afterExecute = request.afterExecute;
|
|
128
|
+
if (!signal || typeof signal.aborted !== 'boolean' || typeof signal.addEventListener !== 'function'
|
|
129
|
+
|| typeof beforeExecute !== 'function' || typeof afterExecute !== 'function') {
|
|
130
|
+
throw new Error('Tool batch signal, beforeExecute and afterExecute capabilities are required');
|
|
131
|
+
}
|
|
132
|
+
const results = [];
|
|
133
|
+
let stopped = null;
|
|
134
|
+
let recordingError;
|
|
135
|
+
for (const call of calls) {
|
|
136
|
+
const identity = freeze({ ...scope, toolCallId: call.id, name: call.name });
|
|
137
|
+
let started = false;
|
|
138
|
+
let cleanup = { status: 'not_acquired' };
|
|
139
|
+
let value = failure('not_started', 'Tool was not started');
|
|
140
|
+
const cancelled = () => {
|
|
141
|
+
if (!signal.aborted)
|
|
142
|
+
return false;
|
|
143
|
+
stopped ??= 'cancelled';
|
|
144
|
+
value = failure('cancelled_before_start', 'Cancelled before tool execution');
|
|
145
|
+
return true;
|
|
146
|
+
};
|
|
147
|
+
const attempt = async () => {
|
|
148
|
+
if (cancelled())
|
|
149
|
+
return;
|
|
150
|
+
const tool = registry.get(call.name);
|
|
151
|
+
if (!tool) {
|
|
152
|
+
value = failure('unknown_tool', 'Unknown tool');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const invalid = tool.validate(call.arguments);
|
|
157
|
+
if (invalid !== null) {
|
|
158
|
+
value = outcome({ ok: false, error: invalid }, identity, true);
|
|
159
|
+
if (!value.ok && value.error.code === 'tool_protocol')
|
|
160
|
+
stopped = 'capability_failed';
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
value = failure('tool_validation', 'Tool validation capability failed');
|
|
166
|
+
stopped = 'capability_failed';
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (cancelled())
|
|
170
|
+
return;
|
|
171
|
+
const invocation = freeze({ identity, call, definition: tool.definition });
|
|
172
|
+
let lease;
|
|
173
|
+
try {
|
|
174
|
+
lease = await acquire(invocation, signal);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
value = failure('environment_failed', 'Environment acquisition failed');
|
|
178
|
+
cleanup = { status: 'acquire_failed', error: {
|
|
179
|
+
code: 'acquire_failed', message: 'Environment provider owns partial-resource cleanup; no lease was returned',
|
|
180
|
+
} };
|
|
181
|
+
stopped = 'capability_failed';
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// A malformed lease cannot prove acquired resources were released.
|
|
185
|
+
let release;
|
|
186
|
+
try {
|
|
187
|
+
if (!lease || typeof lease.release !== 'function')
|
|
188
|
+
throw new Error();
|
|
189
|
+
release = lease.release.bind(lease);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
value = failure('environment_protocol', 'Environment returned an invalid resource lease');
|
|
193
|
+
cleanup = { status: 'failed', error: {
|
|
194
|
+
code: 'invalid_lease', message: 'No release capability; resource effects unknown',
|
|
195
|
+
} };
|
|
196
|
+
stopped = 'cleanup_failed';
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
// Use the same environment for authorization and execution, even if
|
|
201
|
+
// the lease implementation exposes its value through an accessor.
|
|
202
|
+
let environment;
|
|
203
|
+
try {
|
|
204
|
+
environment = lease.value;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
value = failure('environment_failed', 'Environment value is unavailable');
|
|
208
|
+
stopped = 'capability_failed';
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (cancelled())
|
|
212
|
+
return;
|
|
213
|
+
try {
|
|
214
|
+
await beforeExecute(identity);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
value = failure('record_failed', 'Required execution-intent recording failed');
|
|
218
|
+
stopped = 'capability_failed';
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (cancelled())
|
|
222
|
+
return;
|
|
223
|
+
// Permission is the last awaited boundary before tool invocation.
|
|
224
|
+
let decision;
|
|
225
|
+
try {
|
|
226
|
+
decision = snapshot(await check(invocation, environment, signal), 4096);
|
|
227
|
+
if (!object(decision) || !(decision.allowed === true
|
|
228
|
+
|| (decision.allowed === false && typeof decision.reason === 'string')))
|
|
229
|
+
throw new Error();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
value = failure('permission_failed', 'Permission capability failed or returned an invalid decision');
|
|
233
|
+
stopped = 'capability_failed';
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (cancelled())
|
|
237
|
+
return;
|
|
238
|
+
if (!decision.allowed) {
|
|
239
|
+
value = failure('permission_denied', decision.reason);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
started = true;
|
|
243
|
+
try {
|
|
244
|
+
value = outcome(await tool.execute(call.arguments, identity, signal, environment), identity);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
value = failure('tool_exception', 'Tool threw; actual effects are unknown', 'unknown');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
try {
|
|
252
|
+
await release();
|
|
253
|
+
cleanup = { status: 'released' };
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
cleanup = { status: 'failed', error: { code: 'release_failed', message: 'Resource release failed; inspect this invocation' } };
|
|
257
|
+
stopped = 'cleanup_failed';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
if (!stopped)
|
|
262
|
+
await attempt();
|
|
263
|
+
else if (stopped === 'cancelled')
|
|
264
|
+
cancelled();
|
|
265
|
+
if (!value.ok && value.error.effect === 'unknown')
|
|
266
|
+
stopped = 'unknown_effect';
|
|
267
|
+
if (signal.aborted)
|
|
268
|
+
stopped ??= 'cancelled';
|
|
269
|
+
const settlement = freeze({
|
|
270
|
+
identity, started, status: status(started, value), outcome: value,
|
|
271
|
+
cancellationRequested: signal.aborted, cleanup,
|
|
272
|
+
});
|
|
273
|
+
results.push(settlement);
|
|
274
|
+
if (!recordingError) {
|
|
275
|
+
try {
|
|
276
|
+
await afterExecute(settlement);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
recordingError = {
|
|
280
|
+
identity, code: 'result_record_failed',
|
|
281
|
+
message: 'Result recording failed; write effect unknown. Reconcile using in-memory receipts',
|
|
282
|
+
};
|
|
283
|
+
stopped ??= 'capability_failed';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return freeze({ results, stopped, ...(recordingError ? { recordingError } : {}) });
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
@@ -57,10 +57,10 @@ export function response(value, usedIds) {
|
|
|
57
57
|
}
|
|
58
58
|
return snapshot(value);
|
|
59
59
|
}
|
|
60
|
-
export function history(value) {
|
|
60
|
+
export function history(value, maxBytes = limits.historyBytes) {
|
|
61
61
|
const pending = new Map();
|
|
62
62
|
const used = new Set();
|
|
63
|
-
if (!Array.isArray(value) || !json(value) || size(value) >
|
|
63
|
+
if (!Array.isArray(value) || !json(value) || size(value) > maxBytes) {
|
|
64
64
|
throw new AgentFault('invalid_history', 'History must be bounded JSON messages');
|
|
65
65
|
}
|
|
66
66
|
for (const m of value) {
|
|
@@ -821,7 +821,7 @@ export class NodeGitWorkspace {
|
|
|
821
821
|
const state = await this.inspectWorktree(input);
|
|
822
822
|
if (state === "dirty")
|
|
823
823
|
return state;
|
|
824
|
-
const container = await
|
|
824
|
+
const container = await cleanupContainer(input.container);
|
|
825
825
|
const branch = worktreeIdentity(input.taskSegment, input.roleName).branch;
|
|
826
826
|
const path = managedPath(container, input.directory);
|
|
827
827
|
const project = await this.inspect(input.repositoryPath);
|
|
@@ -970,13 +970,13 @@ export class NodeGitWorkspace {
|
|
|
970
970
|
return "removed";
|
|
971
971
|
}
|
|
972
972
|
async inspectWorktree(input) {
|
|
973
|
-
const container = await
|
|
973
|
+
const container = await cleanupContainer(input.container);
|
|
974
974
|
const path = managedPath(container, input.directory);
|
|
975
975
|
const kind = await pathKind(path);
|
|
976
976
|
const branch = input.expectedBranch ?? worktreeIdentity(input.taskSegment, input.roleName).branch;
|
|
977
977
|
if (kind === undefined) {
|
|
978
978
|
const project = await this.inspect(input.repositoryPath);
|
|
979
|
-
await inspectMissingWorktreeRegistration(project.root, path, branch);
|
|
979
|
+
await inspectMissingWorktreeRegistration(project.root, path, branch, input.expectedBranch !== undefined);
|
|
980
980
|
return "missing";
|
|
981
981
|
}
|
|
982
982
|
if (kind === "symlink")
|
|
@@ -984,7 +984,7 @@ export class NodeGitWorkspace {
|
|
|
984
984
|
const canonicalContainerPath = await canonicalContainer(container, false);
|
|
985
985
|
const project = await this.inspect(input.repositoryPath);
|
|
986
986
|
await assertOwnedWorktree(project, canonicalContainerPath, path);
|
|
987
|
-
await assertExpectedBranch(path, branch);
|
|
987
|
+
await assertExpectedBranch(path, branch, input.expectedBranch !== undefined);
|
|
988
988
|
const porcelain = await readWorktreeStatus(path);
|
|
989
989
|
return porcelain.length > 0 ? "dirty" : "clean";
|
|
990
990
|
}
|
|
@@ -1000,19 +1000,19 @@ export class NodeGitWorkspace {
|
|
|
1000
1000
|
});
|
|
1001
1001
|
}
|
|
1002
1002
|
async #removeManagedWorktree(input) {
|
|
1003
|
-
const container = await
|
|
1003
|
+
const container = await cleanupContainer(input.container);
|
|
1004
1004
|
const path = managedPath(container, input.identity.directory);
|
|
1005
1005
|
const kind = await pathKind(path);
|
|
1006
1006
|
if (kind === "symlink")
|
|
1007
1007
|
throw new Error("Managed worktree path must not be a symbolic link.");
|
|
1008
1008
|
const repository = await this.inspect(input.repositoryPath);
|
|
1009
1009
|
if (kind === undefined) {
|
|
1010
|
-
await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch);
|
|
1010
|
+
await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch, true);
|
|
1011
1011
|
}
|
|
1012
1012
|
if (kind === "directory") {
|
|
1013
1013
|
const canonicalContainerPath = await canonicalContainer(container, false);
|
|
1014
1014
|
await assertOwnedWorktree(repository, canonicalContainerPath, path);
|
|
1015
|
-
await assertExpectedBranch(path, input.identity.branch);
|
|
1015
|
+
await assertExpectedBranch(path, input.identity.branch, true);
|
|
1016
1016
|
if (input.discardChanges === true) {
|
|
1017
1017
|
await git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
|
|
1018
1018
|
}
|
|
@@ -1043,19 +1043,22 @@ export class NodeGitWorkspace {
|
|
|
1043
1043
|
/** Missing paths do not imply missing Git metadata. Never prune another
|
|
1044
1044
|
* workspace's registration as a side effect of cleaning this exact owner.
|
|
1045
1045
|
*/
|
|
1046
|
-
async function removeMissingWorktreeRegistration(repositoryRoot, path, branch) {
|
|
1047
|
-
if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch))
|
|
1046
|
+
async function removeMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
|
|
1047
|
+
if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached))
|
|
1048
1048
|
return;
|
|
1049
1049
|
if (await pathKind(path) !== undefined)
|
|
1050
1050
|
throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
|
|
1051
1051
|
await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
|
|
1052
1052
|
}
|
|
1053
|
-
async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch) {
|
|
1053
|
+
async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
|
|
1054
1054
|
const records = (await git(["-C", repositoryRoot, "worktree", "list", "--porcelain", "-z"])).split("\0\0");
|
|
1055
1055
|
const fields = records.map(record => record.split("\0")).find(record => record.includes(`worktree ${path}`));
|
|
1056
1056
|
if (fields === undefined)
|
|
1057
1057
|
return false;
|
|
1058
|
-
|
|
1058
|
+
const pinnedDetached = allowDetached && fields.includes("detached")
|
|
1059
|
+
&& await gitSucceeds(["-C", repositoryRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`])
|
|
1060
|
+
&& fields.includes(`HEAD ${await resolveRefCommit(repositoryRoot, `refs/heads/${branch}`)}`);
|
|
1061
|
+
if ((!fields.includes(`branch refs/heads/${branch}`) && !pinnedDetached) || fields.some(field => field.startsWith("locked"))) {
|
|
1059
1062
|
cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch: fields.find(field => field.startsWith("branch "))?.slice(7) ?? null,
|
|
1060
1063
|
locked: fields.some(field => field.startsWith("locked")) });
|
|
1061
1064
|
}
|
|
@@ -1115,7 +1118,12 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
|
|
|
1115
1118
|
return;
|
|
1116
1119
|
throw new Error(`Managed worktree is rebasing an unexpected branch: ${headName}.`);
|
|
1117
1120
|
}
|
|
1118
|
-
|
|
1121
|
+
// A terminal Integration can legitimately retain a detached checkout.
|
|
1122
|
+
// Its exact owned path/common-dir were checked by the caller; require its
|
|
1123
|
+
// retained branch to pin the same commit before allowing clean removal.
|
|
1124
|
+
if (await resolveRefCommit(path, "HEAD") === await resolveRefCommit(path, `refs/heads/${expected}`))
|
|
1125
|
+
return;
|
|
1126
|
+
cleanupFailure("workspace-identity-mismatch", "Detached worktree is not pinned by its owned branch or rebase.", expected, "unproven detached HEAD");
|
|
1119
1127
|
}
|
|
1120
1128
|
const branch = await gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
|
|
1121
1129
|
if (branch !== expected) {
|
|
@@ -1237,6 +1245,27 @@ async function canonicalContainer(path, create) {
|
|
|
1237
1245
|
// stay rejected by the managed-path lstat checks.
|
|
1238
1246
|
return canonicalDirectory(lexical, "Worktree container");
|
|
1239
1247
|
}
|
|
1248
|
+
/** A previous removal may have deleted the whole owner container before its
|
|
1249
|
+
* receipt committed. Resolve only surviving ancestors, without creating it.
|
|
1250
|
+
* Git registration inspection remains mandatory even when this path is absent.
|
|
1251
|
+
*/
|
|
1252
|
+
async function cleanupContainer(path) {
|
|
1253
|
+
const lexical = resolve(path);
|
|
1254
|
+
if (await pathKind(lexical) === "symlink") {
|
|
1255
|
+
cleanupFailure("workspace-identity-mismatch", "Cleanup container must not be a symbolic link.", "owned directory or missing path", "symbolic link");
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
return await canonicalContainer(lexical, false);
|
|
1259
|
+
}
|
|
1260
|
+
catch (error) {
|
|
1261
|
+
if (!isErrno(error, "ENOENT"))
|
|
1262
|
+
throw error;
|
|
1263
|
+
const parent = dirname(lexical);
|
|
1264
|
+
if (parent === lexical)
|
|
1265
|
+
throw error;
|
|
1266
|
+
return join(await cleanupContainer(parent), relative(parent, lexical));
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1240
1269
|
async function canonicalDirectory(path, label) {
|
|
1241
1270
|
const value = requireText(path, label);
|
|
1242
1271
|
const canonical = await realpath(isAbsolute(value) ? value : resolve(value));
|