@zq-silk/yui 2.1.0 → 2.2.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 +4 -0
- package/dist/cli/commandCatalog.js +24 -3
- package/dist/cli/commandDiscovery.js +4 -1
- package/dist/cli.js +23 -3078
- package/dist/commands/taskCommands.js +79 -21
- package/dist/commands/taskIntegrationCommands.js +3 -1
- 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 +3092 -0
- 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 +50 -15
- 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/task/taskAuthority.js +56 -0
- package/dist/web/assets/client/app.js +64 -3
- package/dist/web/assets/client/detail.js +6 -6
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/overview.js +12 -9
- package/dist/web/assets/client/sidebar.js +31 -2
- package/dist/web/assets/shell.js +2 -1
- package/dist/web/assets/styles/components.js +2 -0
- package/dist/web/assets/styles/layout.js +11 -4
- package/dist/web/assets/styles/responsive.js +12 -4
- package/dist/web/assets/styles/views.js +16 -13
- 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/release-workflow.md +47 -9
- package/docs/release-workflow.zh-CN.md +36 -6
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +5 -0
- package/skills/yui-runtime/SKILL.md +7 -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) {
|
|
@@ -51,6 +51,62 @@ export function assertTaskDeliveryAuthority(store, environment, taskId) {
|
|
|
51
51
|
}
|
|
52
52
|
return actor;
|
|
53
53
|
}
|
|
54
|
+
/** Project effects consume the current Session's captured scope, never the
|
|
55
|
+
* Role's desired configuration or the mere presence of a writable directory.
|
|
56
|
+
* Task management (including abort/inspection) remains a separate authority. */
|
|
57
|
+
export function assertTaskProjectWriteAuthority(store, environment, taskId, projectId) {
|
|
58
|
+
const actor = assertTaskDeliveryAuthority(store, environment, taskId);
|
|
59
|
+
if (actor !== "leader")
|
|
60
|
+
return actor;
|
|
61
|
+
const caller = currentManagedRuntime(store, environment, taskId, LEADER_ROLE);
|
|
62
|
+
const session = store.getTaskRoleSessionSet(taskId, LEADER_ROLE)?.sessions[caller.agentId];
|
|
63
|
+
const effective = session?.effective;
|
|
64
|
+
const task = store.getTask(taskId);
|
|
65
|
+
const main = store.getTaskWorkspace(taskId);
|
|
66
|
+
const entry = main?.entries.find(entry => entry.projectId === projectId);
|
|
67
|
+
if (task?.status !== "active" || task.executionGate.state !== "enabled"
|
|
68
|
+
|| !task.projectBindings.some(binding => binding.projectId === projectId)
|
|
69
|
+
|| effective?.profileAccess !== "write" || !effective.writeProjectIds.includes(projectId)
|
|
70
|
+
|| main?.owner.type !== "task" || main.owner.taskId !== taskId
|
|
71
|
+
|| effective.workspace.root !== main.root || entry?.access !== "write"
|
|
72
|
+
|| !effective.workspace.entries.some(captured => captured.projectId === projectId
|
|
73
|
+
&& captured.access === "write" && captured.path === entry.path)) {
|
|
74
|
+
throw usageError("Project write requires this Task's current delivery Session with a captured writable Project in its Task-main workspace. Read the Session scope; use formal Session replacement to adopt a new boundary.");
|
|
75
|
+
}
|
|
76
|
+
return actor;
|
|
77
|
+
}
|
|
78
|
+
/** A managed Leader integrates into its captured Task-main, not any branch
|
|
79
|
+
* reachable through the same Git repository. The executor also verifies that
|
|
80
|
+
* this ref is physically checked out here immediately before target effects. */
|
|
81
|
+
export function taskIntegrationTargetCheckout(store, environment, taskId, projectId, targetRef) {
|
|
82
|
+
if (assertTaskProjectWriteAuthority(store, environment, taskId, projectId) !== "leader") {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return taskIntegrationSettlementCheckout(store, environment, taskId, projectId, targetRef);
|
|
86
|
+
}
|
|
87
|
+
/** Confirming an already-applied CAS is Task management, not a Project write
|
|
88
|
+
* grant. Keep the current delivery identity and captured target ownership, but
|
|
89
|
+
* do not require or change its declared write scope. Never use this for CAS. */
|
|
90
|
+
export function taskIntegrationSettlementCheckout(store, environment, taskId, projectId, targetRef) {
|
|
91
|
+
if (assertTaskDeliveryAuthority(store, environment, taskId) !== "leader")
|
|
92
|
+
return undefined;
|
|
93
|
+
const task = store.getTask(taskId);
|
|
94
|
+
const main = store.getTaskWorkspace(taskId);
|
|
95
|
+
const entry = main?.entries.find(entry => entry.projectId === projectId);
|
|
96
|
+
const caller = currentManagedRuntime(store, environment, taskId, LEADER_ROLE);
|
|
97
|
+
const effective = store.getTaskRoleSessionSet(taskId, LEADER_ROLE)?.sessions[caller.agentId]?.effective;
|
|
98
|
+
const captured = effective?.workspace.entries.find(entry => entry.projectId === projectId);
|
|
99
|
+
const branchRef = (ref) => ref.startsWith("refs/heads/") ? ref : `refs/heads/${ref}`;
|
|
100
|
+
if (task?.status !== "active" || !task.projectBindings.some(binding => binding.projectId === projectId)
|
|
101
|
+
|| main?.owner.type !== "task" || main.owner.taskId !== taskId
|
|
102
|
+
|| effective?.workspace.root !== main.root || entry?.access !== "write"
|
|
103
|
+
|| captured?.path !== entry.path || entry.branch === undefined || captured.branch === undefined
|
|
104
|
+
|| branchRef(entry.branch) !== branchRef(captured.branch)
|
|
105
|
+
|| branchRef(targetRef) !== branchRef(captured.branch)) {
|
|
106
|
+
throw usageError("Managed Leader Integration target must be its captured Task-main branch and checkout.");
|
|
107
|
+
}
|
|
108
|
+
return entry.path;
|
|
109
|
+
}
|
|
54
110
|
/**
|
|
55
111
|
* Authority for a live steer/interrupt of an exact current native Turn
|
|
56
112
|
* (decision-3 §1/§9, message-5 gap B). The legal evidence is the native
|
|
@@ -15,7 +15,7 @@ import { entriesOf } from "/assets/js/records.js";
|
|
|
15
15
|
|
|
16
16
|
const $ = function (selector) { return document.querySelector(selector); };
|
|
17
17
|
const el = {
|
|
18
|
-
sidebar: $(".sidebar"), center: $("#center"), detail: $("#detail"),
|
|
18
|
+
sidebar: $(".sidebar"), sidebarDivider: $("#sidebar-divider"), center: $("#center"), detail: $("#detail"),
|
|
19
19
|
search: $("#search"), filters: $("#status-filters"), attention: $("#attention-bar"), tasks: $("#task-list"),
|
|
20
20
|
catalogNext: $("#catalog-next"), catalogReset: $("#catalog-reset"), catalogCount: $("#catalog-count"),
|
|
21
21
|
catalogAttentionReset: $("#catalog-attention-reset"),
|
|
@@ -43,6 +43,9 @@ function readPreference(key, fallback) {
|
|
|
43
43
|
function writePreference(key, value) {
|
|
44
44
|
try { localStorage.setItem(key, value); } catch {}
|
|
45
45
|
}
|
|
46
|
+
function clearPreference(key) {
|
|
47
|
+
try { localStorage.removeItem(key); } catch {}
|
|
48
|
+
}
|
|
46
49
|
const dock = {
|
|
47
50
|
open: readPreference("yui.dock.open", "true") !== "false",
|
|
48
51
|
mode: "discussion",
|
|
@@ -52,6 +55,10 @@ const dock = {
|
|
|
52
55
|
// Task, never restored from the desktop preference.
|
|
53
56
|
sheet: false
|
|
54
57
|
};
|
|
58
|
+
// Task list width: null keeps the CSS default (--sidebar-w); otherwise the width
|
|
59
|
+
// the user dragged to. It is a preference; the width actually applied is clamped
|
|
60
|
+
// to the current window.
|
|
61
|
+
const sidebar = { width: Number(readPreference("yui.sidebar.width", "")) || null, resizing: false };
|
|
55
62
|
const narrow = window.matchMedia("(max-width: 900px)");
|
|
56
63
|
|
|
57
64
|
const i18n = createI18n(el.locale);
|
|
@@ -510,7 +517,7 @@ function updateLayout() {
|
|
|
510
517
|
el.dockTabSession.setAttribute("aria-selected", String(dock.mode === "session"));
|
|
511
518
|
el.dockTabDiscussion.disabled = !state.selected;
|
|
512
519
|
el.dockSwap.setAttribute("aria-pressed", String(dock.center));
|
|
513
|
-
|
|
520
|
+
applySidebarWidth();
|
|
514
521
|
syncDockButtons();
|
|
515
522
|
}
|
|
516
523
|
function syncDockButtons() {
|
|
@@ -598,9 +605,63 @@ el.divider.addEventListener("keydown", function (event) {
|
|
|
598
605
|
event.preventDefault();
|
|
599
606
|
setDockWidth(event.key === "Home" ? 320 : event.key === "End" ? maxDockWidth() : dock.width + delta, true);
|
|
600
607
|
});
|
|
601
|
-
window.addEventListener("resize", function () { setDockWidth(dock.width, false); });
|
|
602
608
|
narrow.addEventListener("change", updateLayout);
|
|
603
609
|
|
|
610
|
+
// --- Sidebar width ----------------------------------------------------------------
|
|
611
|
+
const SIDEBAR_MIN = 240;
|
|
612
|
+
const SIDEBAR_MAX = 560;
|
|
613
|
+
function maxSidebarWidth() {
|
|
614
|
+
// An open dock can shrink to its minimum, so it reserves only that much.
|
|
615
|
+
const reserve = dockVisible() ? 320 + 9 : 0;
|
|
616
|
+
return Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, window.innerWidth - reserve - 420));
|
|
617
|
+
}
|
|
618
|
+
function applySidebarWidth() {
|
|
619
|
+
const root = document.documentElement.style;
|
|
620
|
+
if (sidebar.width === null) root.removeProperty("--sidebar-w");
|
|
621
|
+
else root.setProperty("--sidebar-w", Math.max(SIDEBAR_MIN, Math.min(maxSidebarWidth(), sidebar.width)) + "px");
|
|
622
|
+
// A wider task list leaves the dock less room; the dock keeps its stored width.
|
|
623
|
+
setDockWidth(dock.width, false);
|
|
624
|
+
el.sidebarDivider.setAttribute("aria-valuemax", String(maxSidebarWidth()));
|
|
625
|
+
el.sidebarDivider.setAttribute("aria-valuenow", String(el.sidebar.offsetWidth));
|
|
626
|
+
}
|
|
627
|
+
function setSidebarWidth(width, persist) {
|
|
628
|
+
sidebar.width = width === null ? null : Math.max(SIDEBAR_MIN, Math.min(maxSidebarWidth(), Math.round(width)));
|
|
629
|
+
if (persist) {
|
|
630
|
+
if (sidebar.width === null) clearPreference("yui.sidebar.width");
|
|
631
|
+
else writePreference("yui.sidebar.width", String(sidebar.width));
|
|
632
|
+
}
|
|
633
|
+
applySidebarWidth();
|
|
634
|
+
}
|
|
635
|
+
el.sidebarDivider.addEventListener("pointerdown", function (event) {
|
|
636
|
+
if (event.button !== 0 || window.innerWidth <= 900) return;
|
|
637
|
+
sidebar.resizing = true;
|
|
638
|
+
el.sidebarDivider.setPointerCapture(event.pointerId);
|
|
639
|
+
document.body.classList.add("sidebar-resizing");
|
|
640
|
+
event.preventDefault();
|
|
641
|
+
});
|
|
642
|
+
el.sidebarDivider.addEventListener("pointermove", function (event) {
|
|
643
|
+
if (!sidebar.resizing) return;
|
|
644
|
+
setSidebarWidth(event.clientX - el.sidebar.getBoundingClientRect().left, false);
|
|
645
|
+
});
|
|
646
|
+
function finishSidebarResize() {
|
|
647
|
+
if (!sidebar.resizing) return;
|
|
648
|
+
sidebar.resizing = false;
|
|
649
|
+
document.body.classList.remove("sidebar-resizing");
|
|
650
|
+
if (sidebar.width !== null) writePreference("yui.sidebar.width", String(sidebar.width));
|
|
651
|
+
}
|
|
652
|
+
el.sidebarDivider.addEventListener("pointerup", finishSidebarResize);
|
|
653
|
+
el.sidebarDivider.addEventListener("pointercancel", finishSidebarResize);
|
|
654
|
+
el.sidebarDivider.addEventListener("dblclick", function () { setSidebarWidth(null, true); });
|
|
655
|
+
el.sidebarDivider.addEventListener("keydown", function (event) {
|
|
656
|
+
const current = el.sidebar.offsetWidth;
|
|
657
|
+
const next = event.key === "ArrowRight" ? current + 16 : event.key === "ArrowLeft" ? current - 16
|
|
658
|
+
: event.key === "Home" ? SIDEBAR_MIN : event.key === "End" ? maxSidebarWidth() : null;
|
|
659
|
+
if (next === null) return;
|
|
660
|
+
event.preventDefault();
|
|
661
|
+
setSidebarWidth(next, true);
|
|
662
|
+
});
|
|
663
|
+
window.addEventListener("resize", applySidebarWidth);
|
|
664
|
+
|
|
604
665
|
// --- Sidebar controls -------------------------------------------------------------
|
|
605
666
|
let searchTimer = null;
|
|
606
667
|
el.search.addEventListener("input", function () {
|
|
@@ -129,14 +129,14 @@ function renderOverviewPanel(panel, data, t, locale, ctx) {
|
|
|
129
129
|
panel.append(needs);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
// Independent column stacks, not paired rows: a long progress card must not
|
|
133
|
+
// push the next row down and leave a gap under the shorter Session card.
|
|
134
|
+
// Main column: what the Task is and where it stands. Rail: live and decided facts.
|
|
134
135
|
const sessions = card({ title: t("sessions.title"), icon: "terminal", hint: t("sessions.hint") });
|
|
135
136
|
sessions.body.append(h("div", { dataset: { slot: "sessions" } }));
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
panel.append(grid);
|
|
137
|
+
panel.append(h("div.card-columns", null,
|
|
138
|
+
h("div.card-column", null, progressCard(data, t, locale, ctx), goalCard(data, t, ctx)),
|
|
139
|
+
h("div.card-column", null, sessions, decisionsCard(data, t, locale, ctx))));
|
|
140
140
|
if (task.status === "draft") panel.append(planningCard(data, t, locale));
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -7,6 +7,8 @@ const EN = {
|
|
|
7
7
|
"sidebar.noMatch": "No tasks match this filter.",
|
|
8
8
|
"sidebar.activeRuns": "Open execution records",
|
|
9
9
|
"sidebar.workItems": "Work items",
|
|
10
|
+
"sidebar.resize": "Resize task list",
|
|
11
|
+
"sidebar.resizeHint": "Drag to resize · double-click to reset",
|
|
10
12
|
"search.label": "Search tasks",
|
|
11
13
|
"search.placeholder": "Search title, ID, tag…",
|
|
12
14
|
"filters.label": "Filter by status",
|
|
@@ -560,6 +562,8 @@ const ZH = {
|
|
|
560
562
|
"sidebar.noMatch": "没有符合筛选的任务。",
|
|
561
563
|
"sidebar.activeRuns": "未结清的执行记录",
|
|
562
564
|
"sidebar.workItems": "工作项",
|
|
565
|
+
"sidebar.resize": "调整任务列表宽度",
|
|
566
|
+
"sidebar.resizeHint": "拖拽调整宽度 · 双击恢复默认",
|
|
563
567
|
"search.label": "搜索任务",
|
|
564
568
|
"search.placeholder": "搜索标题、ID、标签…",
|
|
565
569
|
"filters.label": "按状态筛选",
|
|
@@ -28,7 +28,12 @@ export function renderOverview(container, state, t, locale, actions) {
|
|
|
28
28
|
metricTile(t("metrics.completed"), counts ? counts.completed : "—", { tone: "ok" }),
|
|
29
29
|
metricTile(t("metrics.total"), counts ? counts.total : "—")));
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
// Two independent column stacks instead of paired rows: each card keeps its
|
|
32
|
+
// natural height, so a short card never leaves a gap beside a long one.
|
|
33
|
+
// Main column: the work path (answer, in progress, recent). Rail: catalog-wide
|
|
34
|
+
// signals and the explicit Session read.
|
|
35
|
+
const main = h("div.overview-col.overview-main");
|
|
36
|
+
const rail = h("div.overview-col.overview-rail");
|
|
32
37
|
// Open questions: the catalog names the affected Tasks, never the questions.
|
|
33
38
|
const inbox = card({ title: t("overview.inbox"), icon: "inbox", count: counts ? counts.openInputs : null });
|
|
34
39
|
const items = state.attention || [];
|
|
@@ -41,7 +46,7 @@ export function renderOverview(container, state, t, locale, actions) {
|
|
|
41
46
|
h("span.list-row-go", null, t("overview.answer"), icon("chevron", "icon-sm")));
|
|
42
47
|
})));
|
|
43
48
|
}
|
|
44
|
-
|
|
49
|
+
main.append(inbox);
|
|
45
50
|
|
|
46
51
|
const catalog = card({ title: t("overview.catalog"), icon: "pulse", hint: t("overview.catalogHint") });
|
|
47
52
|
if (state.catalogAttention) {
|
|
@@ -59,16 +64,14 @@ export function renderOverview(container, state, t, locale, actions) {
|
|
|
59
64
|
})));
|
|
60
65
|
catalog.body.append(note(t("overview.signalsHelp")));
|
|
61
66
|
} else catalog.body.append(emptyState(t("loading.dashboard")));
|
|
62
|
-
|
|
63
|
-
page.append(grid);
|
|
67
|
+
rail.append(catalog);
|
|
64
68
|
|
|
65
69
|
const active = (state.tasks || []).filter(function (task) { return task.status === "active"; });
|
|
66
70
|
const recent = (state.tasks || []).filter(function (task) { return task.status !== "archived"; }).slice().sort(byNewest).slice(0, 8);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
page.append(
|
|
71
|
-
page.append(sessionCard(state, t, locale, actions));
|
|
71
|
+
main.append(taskListCard(t("overview.activeNow"), "pulse", active, t("overview.activeEmpty"), t, locale, actions));
|
|
72
|
+
main.append(taskListCard(t("overview.recent"), "history", recent, t("sidebar.empty"), t, locale, actions));
|
|
73
|
+
rail.append(sessionCard(state, t, locale, actions));
|
|
74
|
+
page.append(h("div.overview-layout", null, main, rail));
|
|
72
75
|
page.append(h("p.page-foot", null, t("overview.pageScope")));
|
|
73
76
|
container.append(page);
|
|
74
77
|
}
|
|
@@ -30,7 +30,24 @@ export function renderAttentionBar(container, state, t, onPick) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// The tab row is created once and updated in place so its horizontal scroll
|
|
33
|
-
// position survives every refresh.
|
|
33
|
+
// position survives every refresh. When the sidebar is too narrow for every
|
|
34
|
+
// tab, the clipped edge fades out, the wheel scrolls the row sideways and a
|
|
35
|
+
// newly selected tab is scrolled into view.
|
|
36
|
+
function syncTabsOverflow(row) {
|
|
37
|
+
const max = row.scrollWidth - row.clientWidth;
|
|
38
|
+
const before = row.scrollLeft > 1;
|
|
39
|
+
const after = row.scrollLeft < max - 1;
|
|
40
|
+
row.dataset.fade = before && after ? "both" : after ? "end" : before ? "start" : "none";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function revealTab(row, tab) {
|
|
44
|
+
const margin = 28;
|
|
45
|
+
const left = tab.offsetLeft - margin;
|
|
46
|
+
const right = tab.offsetLeft + tab.offsetWidth + margin;
|
|
47
|
+
if (left < row.scrollLeft) row.scrollLeft = Math.max(0, left);
|
|
48
|
+
else if (right > row.scrollLeft + row.clientWidth) row.scrollLeft = right - row.clientWidth;
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
export function renderFilters(container, state, t, onFilter) {
|
|
35
52
|
const counts = state.counts || {};
|
|
36
53
|
let row = container.querySelector(":scope > .tabs-row");
|
|
@@ -41,6 +58,13 @@ export function renderFilters(container, state, t, onFilter) {
|
|
|
41
58
|
h("span.status-tab-label"), h("span.status-tab-count")));
|
|
42
59
|
});
|
|
43
60
|
container.append(row);
|
|
61
|
+
row.addEventListener("scroll", function () { syncTabsOverflow(row); }, { passive: true });
|
|
62
|
+
row.addEventListener("wheel", function (event) {
|
|
63
|
+
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX) || row.scrollWidth <= row.clientWidth) return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
row.scrollLeft += event.deltaY;
|
|
66
|
+
}, { passive: false });
|
|
67
|
+
new ResizeObserver(function () { syncTabsOverflow(row); }).observe(row);
|
|
44
68
|
}
|
|
45
69
|
STATUS_FILTERS.forEach(function (status) {
|
|
46
70
|
const tab = row.querySelector('[data-status="' + status + '"]');
|
|
@@ -56,6 +80,11 @@ export function renderFilters(container, state, t, onFilter) {
|
|
|
56
80
|
badge.hidden = hide;
|
|
57
81
|
if (!hide) badge.textContent = String(count);
|
|
58
82
|
});
|
|
83
|
+
if (row.dataset.selected !== state.filter) {
|
|
84
|
+
row.dataset.selected = state.filter;
|
|
85
|
+
revealTab(row, row.querySelector('[data-status="' + state.filter + '"]'));
|
|
86
|
+
}
|
|
87
|
+
syncTabsOverflow(row);
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
function groupOf(task) {
|
|
@@ -106,7 +135,7 @@ function taskRow(task, state, t, locale, onSelect) {
|
|
|
106
135
|
h("span.task-body", null,
|
|
107
136
|
h("span.task-line", null, h("span.task-name", null, task.title), h("time.task-time", { dateTime: task.updatedAt }, relativeTime(task.updatedAt, locale, t))),
|
|
108
137
|
h("span.task-line.task-sub", null, h("code.task-id", null, task.id),
|
|
109
|
-
counts.workItems > 0 ? h("span.task-
|
|
138
|
+
counts.workItems > 0 ? h("span.task-count", { title: t("sidebar.workItems") }, icon("layers", "icon-sm"), String(counts.workItems)) : null,
|
|
110
139
|
signals)));
|
|
111
140
|
return row;
|
|
112
141
|
}
|
package/dist/web/assets/shell.js
CHANGED
|
@@ -56,12 +56,13 @@ export const DASHBOARD_HTML = `<!doctype html>
|
|
|
56
56
|
<button id="operator-terminal" class="foot-btn" type="button" title="Operator session · O">
|
|
57
57
|
${iconSvg("terminal")}<span data-i18n="actions.operator">Operator</span><kbd>O</kbd>
|
|
58
58
|
</button>
|
|
59
|
-
<button id="global-input-open" class="foot-btn" type="button">
|
|
59
|
+
<button id="global-input-open" class="foot-btn" type="button" title="Global input" data-i18n-title="global.open">
|
|
60
60
|
${iconSvg("broadcast")}<span data-i18n="global.open">Global input</span>
|
|
61
61
|
</button>
|
|
62
62
|
<button id="settings-open" class="icon-btn" type="button" aria-label="Settings" data-i18n-aria-label="settings.title" title="Settings">${iconSvg("settings")}</button>
|
|
63
63
|
</footer>
|
|
64
64
|
</aside>
|
|
65
|
+
<div id="sidebar-divider" class="sidebar-divider" role="separator" tabindex="0" aria-orientation="vertical" aria-valuemin="240" aria-valuemax="560" aria-valuenow="264" aria-label="Resize task list" data-i18n-aria-label="sidebar.resize" title="Drag to resize · double-click to reset" data-i18n-title="sidebar.resizeHint"><span aria-hidden="true"></span></div>
|
|
65
66
|
<main id="center" class="center" tabindex="-1">
|
|
66
67
|
<div id="detail" class="detail"></div>
|
|
67
68
|
</main>
|
|
@@ -73,6 +73,8 @@ code.id{padding:1px 6px;border-radius:var(--r-xs);background:var(--surface-3);co
|
|
|
73
73
|
.card-body{display:grid;gap:12px;padding:14px 16px 16px;min-width:0}
|
|
74
74
|
.card-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:start}
|
|
75
75
|
.card-grid-1{grid-template-columns:minmax(0,1fr)}
|
|
76
|
+
.card-columns{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:start}
|
|
77
|
+
.card-column{display:flex;flex-direction:column;gap:14px;min-width:0}
|
|
76
78
|
.stack{display:grid;gap:10px;min-width:0}
|
|
77
79
|
|
|
78
80
|
/* Disclosure */
|