@zq-silk/yui 0.15.8 → 0.15.9
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/ARCHITECTURE.md +2 -0
- package/ARCHITECTURE.zh-CN.md +151 -0
- package/README.md +211 -14
- package/dist/artifacts/artifactCapability.js +74 -0
- package/dist/artifacts/artifactCommitLock.js +249 -0
- package/dist/artifacts/artifactPaths.js +151 -0
- package/dist/artifacts/gitArtifactRef.js +146 -0
- package/dist/artifacts/managedGit.js +332 -0
- package/dist/artifacts/taskArtifactRepository.js +277 -0
- package/dist/cli/commandCatalog.js +14 -10
- package/dist/cli.js +67 -0
- package/dist/commands/operatorCommands.js +33 -2
- package/dist/commands/taskActivationCommands.js +22 -0
- package/dist/commands/taskCommands.js +342 -79
- package/dist/context/runContextPack.js +28 -16
- package/dist/context/taskContext.js +26 -3
- package/dist/controller/controller.js +8 -2
- package/dist/kernel/builtinCapabilities.js +32 -24
- package/dist/message/message.js +56 -0
- package/dist/plugins/pluginService.js +11 -3
- package/dist/resources/projectResource.js +0 -48
- package/dist/resources/projectResourceService.js +3 -81
- package/dist/setup/setupCommand.js +3 -8
- package/dist/storage/migrations/artifactsToGit.js +338 -0
- package/dist/storage/migrations/submitIntent.js +126 -0
- package/dist/storage/sqliteSchema.js +37 -3
- package/dist/storage/sqliteStore.js +1 -21
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -1
- package/dist/task/taskActivation.js +26 -0
- package/dist/task/taskActivationService.js +85 -69
- package/dist/task/taskSubmission.js +236 -0
- package/dist/web/assets/client/app.js +3 -2
- package/dist/web/assets/client/taskSurface.js +96 -7
- package/dist/web/webServer.js +18 -3
- package/dist/web/webTaskSurface.js +6 -6
- package/dist/workItem/workItem.js +14 -10
- package/docs/agent-result-consumption.md +2 -0
- package/docs/agent-result-consumption.zh-CN.md +81 -0
- package/docs/agent-runtime-drivers.md +2 -0
- package/docs/agent-runtime-drivers.zh-CN.md +77 -0
- package/docs/architecture/README.md +44 -32
- package/docs/architecture/README.zh-CN.md +43 -0
- package/docs/architecture/capabilities-and-resources.md +118 -79
- package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
- package/docs/managed-turn-and-session-runtime.md +2 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
- package/docs/observability/README.md +2 -0
- package/docs/observability/README.zh-CN.md +71 -0
- package/docs/plugin-sdk.md +320 -217
- package/docs/plugin-sdk.zh-CN.md +293 -0
- package/docs/provider-runtime.md +2 -0
- package/docs/provider-runtime.zh-CN.md +132 -0
- package/docs/release-workflow.md +2 -0
- package/docs/release-workflow.zh-CN.md +237 -0
- package/docs/roles-and-configuration.md +2 -0
- package/docs/roles-and-configuration.zh-CN.md +96 -0
- package/docs/sqlite-control-plane-design.md +2 -0
- package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
- package/docs/task-dag-semantics.md +80 -57
- package/docs/task-dag-semantics.zh-CN.md +59 -0
- package/docs/task-delivery.md +2 -0
- package/docs/task-delivery.zh-CN.md +82 -0
- package/docs/task-local-identity.md +2 -0
- package/docs/task-local-identity.zh-CN.md +58 -0
- package/docs/testing/verification-levels.md +2 -0
- package/docs/testing/verification-levels.zh-CN.md +69 -0
- package/i18n/README.zh-CN.md +199 -10
- package/package.json +2 -1
- package/skills/yui-leader/SKILL.md +88 -331
- package/skills/yui-leader/references/execution.md +303 -0
- package/skills/yui-leader/references/planning.md +109 -0
- package/skills/yui-leader/references/task-plugins.md +8 -4
- package/skills/yui-operator/SKILL.md +16 -3
|
@@ -3,7 +3,7 @@ import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publica
|
|
|
3
3
|
import { RUN_INPUT_MAX_DELTAS } from "./runInputContract.js";
|
|
4
4
|
import { assertWorkItemDependenciesCompleted } from "../workItem/dependencyGate.js";
|
|
5
5
|
import { governingWorkItemCandidate, workItemExecutionGroupById } from "../workItem/workItem.js";
|
|
6
|
-
import {
|
|
6
|
+
import { formatGitArtifactRef, validateGitArtifactRef } from "../artifacts/gitArtifactRef.js";
|
|
7
7
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
8
8
|
import { contextContentDigest, contextSnapshotRef, createContextSnapshot, validateContextSnapshot } from "./contextSnapshot.js";
|
|
9
9
|
import { MAX_SYNTHESIS_SOURCE_RUNS, sourceRunContextValue } from "./sourceRunContext.js";
|
|
@@ -99,14 +99,14 @@ export function freezeWorkItemExecutionAssignmentContextSnapshot(store, input, n
|
|
|
99
99
|
materialize("L2", "task", task.id, task),
|
|
100
100
|
materialize("L3", "work-item", workItem.id, workItem)
|
|
101
101
|
];
|
|
102
|
-
materialized.push(...candidateArtifacts(
|
|
102
|
+
materialized.push(...candidateArtifacts(task.id, workItem.candidates));
|
|
103
103
|
assertWorkItemDependenciesCompleted(store, workItem);
|
|
104
104
|
for (const dependencyId of workItem.dependsOn) {
|
|
105
105
|
const dependency = store.getWorkItem(task.id, dependencyId);
|
|
106
106
|
if (dependency === null)
|
|
107
107
|
throw new Error(`WorkItem dependency disappeared: ${dependencyId}.`);
|
|
108
108
|
materialized.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
|
|
109
|
-
materialized.push(...candidateArtifacts(
|
|
109
|
+
materialized.push(...candidateArtifacts(task.id, dependency.candidates));
|
|
110
110
|
}
|
|
111
111
|
for (const binding of task.projectBindings) {
|
|
112
112
|
const project = store.getProject(binding.projectId);
|
|
@@ -371,7 +371,7 @@ function collectAuthorizedContext(store, run) {
|
|
|
371
371
|
if (item === null)
|
|
372
372
|
throw new Error(`AgentRun WorkItem not found: ${run.workItemId}.`);
|
|
373
373
|
result.push(materialize("L3", "work-item", item.id, item));
|
|
374
|
-
result.push(...candidateArtifacts(
|
|
374
|
+
result.push(...candidateArtifacts(task.id, item.candidates));
|
|
375
375
|
if (view === "worker") {
|
|
376
376
|
assertWorkItemDependenciesCompleted(store, item);
|
|
377
377
|
for (const dependencyId of item.dependsOn) {
|
|
@@ -379,7 +379,7 @@ function collectAuthorizedContext(store, run) {
|
|
|
379
379
|
if (dependency === null)
|
|
380
380
|
throw new Error(`WorkItem dependency disappeared: ${dependencyId}.`);
|
|
381
381
|
result.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
|
|
382
|
-
result.push(...candidateArtifacts(
|
|
382
|
+
result.push(...candidateArtifacts(task.id, dependency.candidates));
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
}
|
|
@@ -394,7 +394,7 @@ function collectAuthorizedContext(store, run) {
|
|
|
394
394
|
if (candidate === undefined)
|
|
395
395
|
continue;
|
|
396
396
|
result.push(materialize("L3", "candidate", `${item.id}/${candidate.id}`, candidate));
|
|
397
|
-
result.push(...candidateArtifacts(
|
|
397
|
+
result.push(...candidateArtifacts(task.id, [candidate]));
|
|
398
398
|
}
|
|
399
399
|
}
|
|
400
400
|
if (round.deltaRecheck !== undefined) {
|
|
@@ -425,9 +425,12 @@ function collectAuthorizedContext(store, run) {
|
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
427
|
if (view === "leader") {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
// File artifacts are NOT enumerated into the synchronous context pack: the
|
|
429
|
+
// core cursor does not cover file edits, and an ambient directory listing
|
|
430
|
+
// here would be exactly the forbidden update-time mirror (§3.7). The Leader
|
|
431
|
+
// reads current artifacts on demand via the async `artifact.list` /
|
|
432
|
+
// `artifact.read` capabilities; frozen Candidate artifacts still enter the
|
|
433
|
+
// pack as commit-pinned pointers through candidateArtifacts().
|
|
431
434
|
for (const preparation of store.listEnvironmentPreparations(task.id)) {
|
|
432
435
|
result.push(materialize("L3", "environment-preparation", preparation.id, preparation));
|
|
433
436
|
}
|
|
@@ -476,16 +479,25 @@ function collectAuthorizedContext(store, run) {
|
|
|
476
479
|
return [...unique.values()].sort((left, right) => (contextRefIdentity(left.ref).localeCompare(contextRefIdentity(right.ref))));
|
|
477
480
|
}
|
|
478
481
|
/** Only artifacts attached to already-authorized candidates enter a bounded
|
|
479
|
-
* Worker/Reviewer snapshot.
|
|
482
|
+
* Worker/Reviewer snapshot. Each is a commit-pinned pointer: the commit
|
|
483
|
+
* self-certifies the frozen bytes, so the pointer is carried as pure data with
|
|
484
|
+
* NO build-time Git read. The bytes are resolved lazily on the async expand
|
|
485
|
+
* path (`artifact.read` at the pinned commit); the pointer survives cleanup of
|
|
480
486
|
* the producing workspace and Session. */
|
|
481
|
-
function candidateArtifacts(
|
|
487
|
+
function candidateArtifacts(taskId, candidates) {
|
|
482
488
|
return candidates.flatMap((candidate) => (candidate.artifactRefs ?? []).map((ref) => {
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
throw new Error(`Candidate Artifact is unavailable or drifted: ${ref.artifactId}.`);
|
|
489
|
+
const pinned = validateGitArtifactRef(ref);
|
|
490
|
+
if (pinned.taskId !== taskId) {
|
|
491
|
+
throw new Error(`Candidate Artifact belongs to another Task: ${pinned.taskId}.`);
|
|
487
492
|
}
|
|
488
|
-
|
|
493
|
+
// The refId is the self-certifying string form; the value is the pure
|
|
494
|
+
// pointer, never the bytes. inspectValue reconstructs the same pointer.
|
|
495
|
+
return materialize("L3", "artifact", formatGitArtifactRef(pinned), {
|
|
496
|
+
taskId: pinned.taskId,
|
|
497
|
+
commit: pinned.commit,
|
|
498
|
+
relativePath: pinned.relativePath,
|
|
499
|
+
...(pinned.digest === undefined ? {} : { digest: pinned.digest })
|
|
500
|
+
});
|
|
489
501
|
}));
|
|
490
502
|
}
|
|
491
503
|
/** Lane-specific context that may be layered over an immutable stage base. */
|
|
@@ -6,6 +6,7 @@ import { sourceRunContextValue } from "./sourceRunContext.js";
|
|
|
6
6
|
import { expandTaskMessageResult } from "../message/message.js";
|
|
7
7
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
8
8
|
import { runExecutionObservation } from "../agentRun/agentRun.js";
|
|
9
|
+
import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
|
|
9
10
|
const MAX_RECORDS = 256;
|
|
10
11
|
const MAX_VALUE_BYTES = 4096;
|
|
11
12
|
const MAX_PAGE_BYTES = 128 * 1024;
|
|
@@ -302,7 +303,26 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
302
303
|
return run === null ? null : sourceRunContextValue(run);
|
|
303
304
|
}
|
|
304
305
|
case "review-round": return store.getReviewRound(taskId, refId);
|
|
305
|
-
case "artifact":
|
|
306
|
+
case "artifact": {
|
|
307
|
+
// A file artifact is addressed by its self-certifying commit-pinned refId
|
|
308
|
+
// (git:<commit>:<relativePath>). Resolve it to the pure pointer only; the
|
|
309
|
+
// bytes are read on the async `artifact.read` path, never synchronously in
|
|
310
|
+
// this transaction.
|
|
311
|
+
if (!isGitArtifactRefString(refId))
|
|
312
|
+
return null;
|
|
313
|
+
try {
|
|
314
|
+
const pinned = parseGitArtifactRef(refId, taskId);
|
|
315
|
+
return {
|
|
316
|
+
taskId: pinned.taskId,
|
|
317
|
+
commit: pinned.commit,
|
|
318
|
+
relativePath: pinned.relativePath,
|
|
319
|
+
...(pinned.digest === undefined ? {} : { digest: pinned.digest })
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
306
326
|
case "environment-preparation": return store.getEnvironmentPreparation(taskId, refId);
|
|
307
327
|
case "change-set": return store.getChangeSet(taskId, refId);
|
|
308
328
|
case "managed-workspace": {
|
|
@@ -365,8 +385,11 @@ function authorizedEntries(store, taskId, environment) {
|
|
|
365
385
|
for (const candidate of [...item.candidates].reverse())
|
|
366
386
|
add("candidate", `${item.id}/${candidate.id}`, candidate);
|
|
367
387
|
}
|
|
368
|
-
|
|
369
|
-
|
|
388
|
+
// File artifacts are not enumerated into the synchronous Context listing: the
|
|
389
|
+
// core cursor does not cover file edits and an ambient directory index here
|
|
390
|
+
// would be the forbidden update-time mirror (§3.7). Current artifacts are
|
|
391
|
+
// listed on demand via the async `artifact.list` capability; frozen Candidate
|
|
392
|
+
// artifacts remain reachable as commit-pinned pointers under their Candidate.
|
|
370
393
|
for (const preparation of store.listEnvironmentPreparations(taskId))
|
|
371
394
|
add("environment-preparation", preparation.id, preparation);
|
|
372
395
|
for (const workspace of store.listManagedWorkspaces(taskId))
|
|
@@ -16,6 +16,7 @@ import { nearestDeadlineBatch } from "../coordination/deadlineScheduler.js";
|
|
|
16
16
|
import { mailboxHasWork, nextPendingBatch } from "../coordination/workMailbox.js";
|
|
17
17
|
import { hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
|
|
18
18
|
import { formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
19
|
+
import { activationRequestIsControllerAdoptable } from "../task/taskActivation.js";
|
|
19
20
|
import { parseDurableJobAcknowledgeParams, parseDurableJobCancelParams, parseDurableJobRefParams, parseDurableJobStartParams } from "./jobControl.js";
|
|
20
21
|
const DEFAULT_RECONCILIATION_INTERVAL_MS = reconciliationIntervalMilliseconds();
|
|
21
22
|
const DEFAULT_SIGNAL_WINDOW_MS = 100;
|
|
@@ -488,8 +489,13 @@ async function adoptReleasedTaskActivations(store, workspace, selection, onError
|
|
|
488
489
|
const request = task.activationRequest;
|
|
489
490
|
if (request?.disposition !== "pending")
|
|
490
491
|
continue;
|
|
491
|
-
|
|
492
|
-
|
|
492
|
+
// Continue only requests with a provable source: a released deferral, or an
|
|
493
|
+
// immediate request carrying a recognised origin (explicit action, or a
|
|
494
|
+
// develop submission accepted while unplanned). This admits legal
|
|
495
|
+
// Operator/user immediate requests the old actor filter dropped, while
|
|
496
|
+
// still refusing origin-less historical requests, which stay behind the
|
|
497
|
+
// explicit `yui task activate` boundary.
|
|
498
|
+
if (!activationRequestIsControllerAdoptable(request))
|
|
493
499
|
continue;
|
|
494
500
|
// Later Draft discussion is a Session notification, not another AgentRun.
|
|
495
501
|
// Its durable activation intent is sufficient once the exact native input
|
|
@@ -6,7 +6,7 @@ import { createJobCallAuthority, parseDurableJobStartParams } from "../controlle
|
|
|
6
6
|
import { inspectJobOperation } from "./kernelPorts.js";
|
|
7
7
|
import { CapabilityRegistry, } from "./capabilityRegistry.js";
|
|
8
8
|
import { createProjectResources } from "../resources/projectResourceService.js";
|
|
9
|
-
import {
|
|
9
|
+
import { saveArtifactCapability, readArtifactCapability, listArtifactsCapability } from "../artifacts/artifactCapability.js";
|
|
10
10
|
import { createPluginService } from "../plugins/pluginService.js";
|
|
11
11
|
const text = { type: "string", minLength: 1 };
|
|
12
12
|
const strings = { type: "object", additionalProperties: { type: "string" } };
|
|
@@ -15,7 +15,6 @@ const object = (properties, required = Object.keys(properties)) => ({
|
|
|
15
15
|
});
|
|
16
16
|
const taskInput = object({ taskId: text });
|
|
17
17
|
const recordOutput = { type: "object" };
|
|
18
|
-
const artifactBase = { displayName: text, provenance: text, mediaType: text };
|
|
19
18
|
const taskOutput = { type: "object", required: ["id", "status", "title"], properties: {
|
|
20
19
|
id: text, status: text, title: text
|
|
21
20
|
} };
|
|
@@ -26,6 +25,7 @@ const definitions = [
|
|
|
26
25
|
name: "message.send", summary: "Save collaboration for Leader or continue the same dispatched work owner.",
|
|
27
26
|
effect: "local-mutation", requiredPermissions: ["task:read"], source: "sendTaskMessageCommand",
|
|
28
27
|
inputSchema: object({ taskId: text, body: text,
|
|
28
|
+
intent: { enum: ["record", "discuss", "develop"] },
|
|
29
29
|
wakePolicy: { enum: ["leader", "none"] },
|
|
30
30
|
recipient: object({ roleName: text, workItemId: text, reviewRoundId: text }, ["roleName"])
|
|
31
31
|
}, ["taskId", "body"]), outputSchema: recordOutput
|
|
@@ -48,23 +48,20 @@ const definitions = [
|
|
|
48
48
|
outputSchema: { type: "object", required: ["ref", "value", "coreCursor"] }
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
|
-
name: "artifact.save", summary: "Save
|
|
52
|
-
effect: "local-mutation", requiredPermissions: ["task:read"], source: "
|
|
53
|
-
inputSchema: object({ taskId: text,
|
|
54
|
-
|
|
55
|
-
object({ ...artifactBase, kind: { const: "external-version" }, resourceId: text, version: text, verification: text }, ["kind", "displayName", "provenance", "resourceId", "version", "verification"]),
|
|
56
|
-
object({ ...artifactBase, kind: { const: "receipt" }, jobId: text, receiptRef: text }, ["kind", "displayName", "provenance", "jobId", "receiptRef"]),
|
|
57
|
-
object({ ...artifactBase, kind: { const: "reference" }, locator: text, observedAt: text }, ["kind", "displayName", "provenance", "locator", "observedAt"])
|
|
58
|
-
] } }), outputSchema: recordOutput
|
|
51
|
+
name: "artifact.save", summary: "Save a file artifact by relativePath into the Task's local Git repository; returns a commit-pinned reference.",
|
|
52
|
+
effect: "local-mutation", requiredPermissions: ["task:read"], source: "artifacts.saveArtifactCapability",
|
|
53
|
+
inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" }, message: text, expectedHead: text }, ["taskId", "relativePath", "content"]),
|
|
54
|
+
outputSchema: { type: "object", required: ["taskId", "commit", "relativePath"] }
|
|
59
55
|
},
|
|
60
56
|
{
|
|
61
|
-
name: "artifact.read", summary: "Read
|
|
62
|
-
effect: "query", requiredPermissions: ["task:read"], source: "
|
|
63
|
-
inputSchema: object({ taskId: text,
|
|
57
|
+
name: "artifact.read", summary: "Read a file artifact at HEAD, or at a pinned commit for frozen evidence, without starting a Runtime or plugin.",
|
|
58
|
+
effect: "query", requiredPermissions: ["task:read"], source: "artifacts.readArtifactCapability",
|
|
59
|
+
inputSchema: object({ taskId: text, relativePath: text, commit: text }, ["taskId", "relativePath"]),
|
|
60
|
+
outputSchema: { type: "object", required: ["taskId", "relativePath", "commit", "content"] }
|
|
64
61
|
},
|
|
65
62
|
{
|
|
66
|
-
name: "artifact.list", summary: "List this Task's
|
|
67
|
-
effect: "query", requiredPermissions: ["task:read"], source: "
|
|
63
|
+
name: "artifact.list", summary: "List this Task's tracked file artifacts at HEAD.",
|
|
64
|
+
effect: "query", requiredPermissions: ["task:read"], source: "artifacts.listArtifactsCapability",
|
|
68
65
|
inputSchema: taskInput, outputSchema: { type: "array", items: recordOutput }
|
|
69
66
|
},
|
|
70
67
|
{
|
|
@@ -246,7 +243,11 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
246
243
|
}
|
|
247
244
|
const taskId = invocation.context.targetId;
|
|
248
245
|
if (name === "message.send") {
|
|
249
|
-
|
|
246
|
+
// A managed Task Session resolves to Leader/role authority and never
|
|
247
|
+
// gains develop from an intent argument; only a global Operator Session
|
|
248
|
+
// (user-authority) may carry one. sendTaskMessageCommand enforces this,
|
|
249
|
+
// so the capability just forwards the optional intent unchanged.
|
|
250
|
+
const result = sendTaskMessageCommand(store, taskId, params.body, params.wakePolicy, { environment: callerEnvironment(caller) }, params.recipient, params.intent);
|
|
250
251
|
signal(taskId);
|
|
251
252
|
return result.message;
|
|
252
253
|
}
|
|
@@ -279,16 +280,23 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
279
280
|
return inspectTaskContext(store, taskId, {
|
|
280
281
|
store: params.store, refId: params.refId, digest: params.digest
|
|
281
282
|
}, callerEnvironment(caller));
|
|
283
|
+
// File/directory artifacts live in the Task's local Git repository, not
|
|
284
|
+
// the DB. Save commits exactly one path and returns a self-certifying
|
|
285
|
+
// commit-pinned reference; read/list are ordinary current reads. These
|
|
286
|
+
// are the async Git path (the capability layer awaits invoke()).
|
|
282
287
|
if (name === "artifact.save")
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
return saveArtifactCapability(store.rootDirectory(), taskId, {
|
|
289
|
+
relativePath: params.relativePath, content: params.content,
|
|
290
|
+
...(params.message === undefined ? {} : { message: params.message }),
|
|
291
|
+
...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
|
|
292
|
+
});
|
|
293
|
+
if (name === "artifact.read")
|
|
294
|
+
return readArtifactCapability(store.rootDirectory(), taskId, {
|
|
295
|
+
relativePath: params.relativePath,
|
|
296
|
+
...(params.commit === undefined ? {} : { commit: params.commit })
|
|
297
|
+
});
|
|
290
298
|
if (name === "artifact.list")
|
|
291
|
-
return store.
|
|
299
|
+
return listArtifactsCapability(store.rootDirectory(), taskId);
|
|
292
300
|
if (name === "environment.prepare")
|
|
293
301
|
return resources.prepare(taskId, params.plan);
|
|
294
302
|
if (name === "environment.adopt")
|
package/dist/message/message.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
2
2
|
export const TASK_MESSAGE_KINDS = ["user", "operator", "role-result", "system"];
|
|
3
|
+
/**
|
|
4
|
+
* How a user/operator submission asks its Task to react (task-32 Requirement A).
|
|
5
|
+
*
|
|
6
|
+
* - `record`: save only. No Leader wake, no planning, no activation.
|
|
7
|
+
* - `discuss`: save and route to planning (the default when a submission omits
|
|
8
|
+
* an intent, so an old client keeps its existing Leader-waking behaviour).
|
|
9
|
+
* - `develop`: save the requirement and its activation intent; an unplanned
|
|
10
|
+
* Draft records an activation request and queues only activation processing.
|
|
11
|
+
*
|
|
12
|
+
* The intent is never inferred from body text and is only meaningful on a
|
|
13
|
+
* user/operator submission: a role-result or system Message can never carry one,
|
|
14
|
+
* so an internal Agent report cannot acquire develop authority.
|
|
15
|
+
*/
|
|
16
|
+
export const TASK_SUBMISSION_INTENTS = ["record", "discuss", "develop"];
|
|
3
17
|
export function createTaskMessage(id, taskId, body, kind, author, now, context = {}) {
|
|
4
18
|
validateKindAndAuthor(kind, author);
|
|
5
19
|
const message = {
|
|
@@ -12,6 +26,12 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
|
|
|
12
26
|
...(context.wakePolicy === undefined
|
|
13
27
|
? {}
|
|
14
28
|
: { wakePolicy: context.wakePolicy }),
|
|
29
|
+
...(context.intent === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { intent: context.intent }),
|
|
32
|
+
...(context.submissionKey === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { submissionKey: requireText(context.submissionKey, "Message submission key") }),
|
|
15
35
|
...(context.runId === undefined
|
|
16
36
|
? {}
|
|
17
37
|
: { runId: requireSafeIdentity(context.runId, "Message AgentRun id") }),
|
|
@@ -57,6 +77,24 @@ export function updateDraftTaskMessage(message, update) {
|
|
|
57
77
|
validateTaskMessage(updated);
|
|
58
78
|
return updated;
|
|
59
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Attach the §2.3 receipt a keyed submission earned, in the same transaction that
|
|
82
|
+
* saved and routed the Message. The receipt is a write-once fact: it is only set
|
|
83
|
+
* on a keyed Message that has none yet, so a replay (which never re-routes) can
|
|
84
|
+
* never overwrite the disposition the original submission recorded.
|
|
85
|
+
*/
|
|
86
|
+
export function withSubmissionReceipt(message, receipt) {
|
|
87
|
+
validateTaskMessage(message);
|
|
88
|
+
if (message.submissionKey === undefined) {
|
|
89
|
+
throw new Error(`A submission receipt requires a submission key: ${message.id}.`);
|
|
90
|
+
}
|
|
91
|
+
if (message.submissionReceipt !== undefined) {
|
|
92
|
+
throw new Error(`Submission receipt is already recorded: ${message.id}.`);
|
|
93
|
+
}
|
|
94
|
+
const updated = { ...message, submissionReceipt: receipt };
|
|
95
|
+
validateTaskMessage(updated);
|
|
96
|
+
return updated;
|
|
97
|
+
}
|
|
60
98
|
export function validateTaskMessage(message) {
|
|
61
99
|
if (message.schemaVersion !== 3)
|
|
62
100
|
throw new Error("Task Message must use schemaVersion 3.");
|
|
@@ -74,6 +112,24 @@ export function validateTaskMessage(message) {
|
|
|
74
112
|
&& message.kind !== "operator") {
|
|
75
113
|
throw new Error("Message wakePolicy is only valid for user/operator messages.");
|
|
76
114
|
}
|
|
115
|
+
if (message.intent !== undefined
|
|
116
|
+
&& !TASK_SUBMISSION_INTENTS.includes(message.intent)) {
|
|
117
|
+
throw new Error(`Message intent is invalid: ${String(message.intent)}.`);
|
|
118
|
+
}
|
|
119
|
+
if (message.intent !== undefined
|
|
120
|
+
&& message.kind !== "user"
|
|
121
|
+
&& message.kind !== "operator") {
|
|
122
|
+
throw new Error("Message intent is only valid for user/operator messages.");
|
|
123
|
+
}
|
|
124
|
+
if (message.submissionKey !== undefined) {
|
|
125
|
+
requireText(message.submissionKey, "Message submission key");
|
|
126
|
+
if (message.kind !== "user" && message.kind !== "operator") {
|
|
127
|
+
throw new Error("Message submission key is only valid for user/operator messages.");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (message.submissionReceipt !== undefined && message.submissionKey === undefined) {
|
|
131
|
+
throw new Error("Message submission receipt requires a submission key.");
|
|
132
|
+
}
|
|
77
133
|
if (message.runId !== undefined)
|
|
78
134
|
requireSafeIdentity(message.runId, "Message AgentRun id");
|
|
79
135
|
if (message.recipient !== undefined) {
|
|
@@ -350,9 +350,17 @@ export function createPluginService(store, host, registry) {
|
|
|
350
350
|
if (previous) {
|
|
351
351
|
const retired = previous;
|
|
352
352
|
void host.detach(retired.ref).catch((error) => {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
// A post-publication detach failure is plugin operational telemetry,
|
|
354
|
+
// recorded on the plugin's own failure channel — not a Task product
|
|
355
|
+
// artifact (product artifacts are files in the Task's Git repository).
|
|
356
|
+
try {
|
|
357
|
+
store.recordPluginIntentFailure(taskId, retired.ref.id, intent.revision, {
|
|
358
|
+
message: `Plugin instance cleanup failed for ${retired.ref.id}/${retired.ref.generation}: `
|
|
359
|
+
+ (error instanceof Error ? error.message : String(error)),
|
|
360
|
+
occurredAt: new Date().toISOString()
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
catch { /* best-effort telemetry; never mask the successful activation */ }
|
|
356
364
|
});
|
|
357
365
|
}
|
|
358
366
|
return { provider: ref, validationId, digest: pkg.digest, capabilities: entries.map((entry) => entry.name),
|
|
@@ -25,54 +25,6 @@ export function validateExecutionEnvironmentSnapshot(value) {
|
|
|
25
25
|
export function contentDigest(content) {
|
|
26
26
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
27
27
|
}
|
|
28
|
-
export function validateArtifact(artifact) {
|
|
29
|
-
if (artifact.schemaVersion !== 1)
|
|
30
|
-
throw new Error("Artifact schemaVersion must be 1.");
|
|
31
|
-
requireIdentity(artifact.id, "Artifact id");
|
|
32
|
-
requireIdentity(artifact.taskId, "Artifact Task");
|
|
33
|
-
requireText(artifact.displayName, "Artifact name");
|
|
34
|
-
requireText(artifact.mediaType, "Artifact media type");
|
|
35
|
-
requireText(artifact.provenance, "Artifact provenance");
|
|
36
|
-
requireTimestamp(artifact.createdAt, "Artifact timestamp");
|
|
37
|
-
if (artifact.kind === "content" || artifact.kind === "receipt") {
|
|
38
|
-
if (typeof artifact.content !== "string" || Buffer.byteLength(artifact.content) > 8 * 1024 * 1024) {
|
|
39
|
-
throw new Error("Artifact content must be UTF-8 text of at most 8 MiB.");
|
|
40
|
-
}
|
|
41
|
-
if (artifact.digest !== contentDigest(artifact.content))
|
|
42
|
-
throw new Error("Artifact content digest mismatch.");
|
|
43
|
-
if (artifact.kind === "receipt") {
|
|
44
|
-
requireIdentity(artifact.jobId, "Artifact Job");
|
|
45
|
-
requireText(artifact.receiptRef, "Artifact receipt");
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
else if (artifact.kind === "external-version") {
|
|
49
|
-
requireIdentity(artifact.resourceId, "Artifact resource");
|
|
50
|
-
requireText(artifact.version, "Artifact external version");
|
|
51
|
-
requireText(artifact.verification, "Artifact version verification");
|
|
52
|
-
}
|
|
53
|
-
else if (artifact.kind === "reference") {
|
|
54
|
-
requireText(artifact.locator, "Reference locator");
|
|
55
|
-
requireTimestamp(artifact.observedAt, "Reference observation");
|
|
56
|
-
}
|
|
57
|
-
else
|
|
58
|
-
throw new Error("Unknown Artifact kind.");
|
|
59
|
-
return artifact;
|
|
60
|
-
}
|
|
61
|
-
export function stableArtifactRef(artifact) {
|
|
62
|
-
validateArtifact(artifact);
|
|
63
|
-
if (artifact.kind === "reference")
|
|
64
|
-
throw new Error("Reference material is not a fixed result.");
|
|
65
|
-
return { taskId: artifact.taskId, artifactId: artifact.id, kind: artifact.kind,
|
|
66
|
-
...("digest" in artifact ? { digest: artifact.digest } : {}) };
|
|
67
|
-
}
|
|
68
|
-
/** Listings expose locators and provenance without replaying every body. */
|
|
69
|
-
export function artifactSummary(artifact) {
|
|
70
|
-
if ("content" in artifact) {
|
|
71
|
-
const { content, ...summary } = artifact;
|
|
72
|
-
return { ...summary, contentBytes: Buffer.byteLength(content, "utf8") };
|
|
73
|
-
}
|
|
74
|
-
return artifact;
|
|
75
|
-
}
|
|
76
28
|
export function validateLocalResource(resource) {
|
|
77
29
|
if (resource.schemaVersion !== 1 || resource.kind !== "local-directory" || resource.ownership !== "user") {
|
|
78
30
|
throw new Error("Invalid local Resource record.");
|
|
@@ -1,46 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { lstatSync, mkdirSync, mkdtempSync,
|
|
2
|
+
import { lstatSync, mkdirSync, mkdtempSync, realpathSync, rmdirSync, statSync } from "node:fs";
|
|
3
3
|
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
4
4
|
import { checkGrant, recordGrantUse } from "../grant/capabilityGrant.js";
|
|
5
|
-
import {
|
|
5
|
+
import { requireText } from "../domain/validation.js";
|
|
6
6
|
import { validateProject } from "../repository/project.js";
|
|
7
7
|
import { updateRole } from "../role/role.js";
|
|
8
8
|
import { saveTaskRoleUpdate } from "../role/taskRoleUpdate.js";
|
|
9
9
|
import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
|
|
10
10
|
import { assertProviderConversationReplaceable, currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
11
11
|
import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
12
|
-
import { contentDigest,
|
|
13
|
-
export function validateArtifactInput(value) {
|
|
14
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15
|
-
throw new Error("Artifact input must be an object.");
|
|
16
|
-
const input = value;
|
|
17
|
-
requireText(input.displayName, "Artifact name");
|
|
18
|
-
requireText(input.provenance, "Artifact provenance");
|
|
19
|
-
if (input.mediaType !== undefined)
|
|
20
|
-
requireText(input.mediaType, "Artifact media type");
|
|
21
|
-
switch (input.kind) {
|
|
22
|
-
case "content":
|
|
23
|
-
if (typeof input.content !== "string" || Buffer.byteLength(input.content) > 8 * 1024 * 1024) {
|
|
24
|
-
throw new Error("Artifact content must be UTF-8 text of at most 8 MiB.");
|
|
25
|
-
}
|
|
26
|
-
break;
|
|
27
|
-
case "receipt":
|
|
28
|
-
requireIdentity(input.jobId, "Artifact Job");
|
|
29
|
-
requireText(input.receiptRef, "Artifact receipt");
|
|
30
|
-
break;
|
|
31
|
-
case "external-version":
|
|
32
|
-
requireIdentity(input.resourceId, "Artifact resource");
|
|
33
|
-
requireText(input.version, "Artifact external version");
|
|
34
|
-
requireText(input.verification, "Artifact version verification");
|
|
35
|
-
break;
|
|
36
|
-
case "reference":
|
|
37
|
-
requireText(input.locator, "Reference locator");
|
|
38
|
-
requireTimestamp(input.observedAt, "Reference observation");
|
|
39
|
-
break;
|
|
40
|
-
default: throw new Error("Unknown Artifact kind.");
|
|
41
|
-
}
|
|
42
|
-
return input;
|
|
43
|
-
}
|
|
12
|
+
import { contentDigest, validateExecutionEnvironmentSnapshot } from "./projectResource.js";
|
|
44
13
|
/** Trusted typed owner; the public caller is authenticated by the existing
|
|
45
14
|
* capability boundary. No new Store, worker, scheduling or Git authority. */
|
|
46
15
|
export function createProjectResources(store, now = () => new Date()) {
|
|
@@ -157,47 +126,6 @@ export function createProjectResources(store, now = () => new Date()) {
|
|
|
157
126
|
return updated;
|
|
158
127
|
});
|
|
159
128
|
},
|
|
160
|
-
saveArtifact(taskId, input) {
|
|
161
|
-
task(taskId);
|
|
162
|
-
validateArtifactInput(input);
|
|
163
|
-
const base = { schemaVersion: 1, taskId, id: `artifact-${randomUUID()}`,
|
|
164
|
-
displayName: input.displayName, mediaType: input.mediaType ?? "text/plain",
|
|
165
|
-
provenance: input.provenance, createdAt: now().toISOString() };
|
|
166
|
-
let artifact;
|
|
167
|
-
if (input.kind === "content") {
|
|
168
|
-
artifact = { ...base, kind: input.kind, content: input.content, digest: contentDigest(input.content) };
|
|
169
|
-
}
|
|
170
|
-
else if (input.kind === "receipt") {
|
|
171
|
-
const job = store.getDurableJob(taskId, input.jobId);
|
|
172
|
-
if (!job?.operation.receiptRefs.includes(input.receiptRef))
|
|
173
|
-
throw new Error("Receipt is not recorded by this Task's Job.");
|
|
174
|
-
const path = realpathSync(input.receiptRef);
|
|
175
|
-
if (!within(realpathSync(job.artifactsLocator), path))
|
|
176
|
-
throw new Error("Receipt is outside the Job artifact directory.");
|
|
177
|
-
const content = readText(path);
|
|
178
|
-
artifact = { ...base, kind: input.kind, jobId: job.id, receiptRef: input.receiptRef,
|
|
179
|
-
content, digest: contentDigest(content) };
|
|
180
|
-
}
|
|
181
|
-
else if (input.kind === "external-version") {
|
|
182
|
-
// Recording supplied version evidence is not an external resource read.
|
|
183
|
-
// Actual resource operations must separately pass their owner's grant.
|
|
184
|
-
artifact = { ...base, kind: input.kind, resourceId: input.resourceId,
|
|
185
|
-
version: input.version, verification: input.verification };
|
|
186
|
-
}
|
|
187
|
-
else {
|
|
188
|
-
artifact = { ...base, kind: input.kind, locator: input.locator, observedAt: input.observedAt };
|
|
189
|
-
}
|
|
190
|
-
store.saveArtifact(validateArtifact(artifact));
|
|
191
|
-
return artifact;
|
|
192
|
-
},
|
|
193
|
-
resultRefs(taskId, artifactIds) {
|
|
194
|
-
return artifactIds.map((id) => {
|
|
195
|
-
const artifact = store.getArtifact(taskId, id);
|
|
196
|
-
if (!artifact)
|
|
197
|
-
throw new Error(`Artifact not found: ${taskId}/${id}.`);
|
|
198
|
-
return stableArtifactRef(artifact);
|
|
199
|
-
});
|
|
200
|
-
},
|
|
201
129
|
gitResult(taskId, changeSetId) {
|
|
202
130
|
const changeSet = store.getChangeSet(taskId, changeSetId);
|
|
203
131
|
if (!changeSet)
|
|
@@ -413,9 +341,3 @@ function currentOrAbsentPath(path) {
|
|
|
413
341
|
throw error;
|
|
414
342
|
}
|
|
415
343
|
}
|
|
416
|
-
function readText(path) {
|
|
417
|
-
const stat = statSync(path);
|
|
418
|
-
if (!stat.isFile() || stat.size > 8 * 1024 * 1024)
|
|
419
|
-
throw new Error("Receipt must be a file of at most 8 MiB.");
|
|
420
|
-
return readFileSync(path, "utf8");
|
|
421
|
-
}
|
|
@@ -53,15 +53,10 @@ export async function runSetupCommand(args, env, executor, io = {}) {
|
|
|
53
53
|
return `${[
|
|
54
54
|
"Yui setup complete.",
|
|
55
55
|
`Yui home: ${home}.`,
|
|
56
|
-
`Operator
|
|
57
|
-
`Leader
|
|
56
|
+
`Operator: ${agent.id} (${roleStatus.operator}).`,
|
|
57
|
+
`Leader: ${roleStatus.leaderAgentId} (${roleStatus.leader}).`,
|
|
58
58
|
`Default workspace: ${workspace}.`,
|
|
59
|
-
`Operator configuration
|
|
60
|
-
`Leader configuration: ${roleStatus.leader}.`,
|
|
61
|
-
`Tmux: ${tmuxBin}.`,
|
|
62
|
-
"Setup did not configure Review, Worker, Profiles, or shell completion.",
|
|
63
|
-
"Provider login and native key/initialization confirmations remain with the Provider; setup did not call a model.",
|
|
64
|
-
"Run `yui operator enter` to continue, or `yui config show` to inspect configuration."
|
|
59
|
+
"Run `yui config show` to inspect configuration, or `yui operator enter` to start an Operator session for configuration changes or development tasks."
|
|
65
60
|
].join("\n")}\n`;
|
|
66
61
|
}
|
|
67
62
|
finally {
|