@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.
Files changed (74) hide show
  1. package/ARCHITECTURE.md +2 -0
  2. package/ARCHITECTURE.zh-CN.md +151 -0
  3. package/README.md +211 -14
  4. package/dist/artifacts/artifactCapability.js +74 -0
  5. package/dist/artifacts/artifactCommitLock.js +249 -0
  6. package/dist/artifacts/artifactPaths.js +151 -0
  7. package/dist/artifacts/gitArtifactRef.js +146 -0
  8. package/dist/artifacts/managedGit.js +332 -0
  9. package/dist/artifacts/taskArtifactRepository.js +277 -0
  10. package/dist/cli/commandCatalog.js +14 -10
  11. package/dist/cli.js +67 -0
  12. package/dist/commands/operatorCommands.js +33 -2
  13. package/dist/commands/taskActivationCommands.js +22 -0
  14. package/dist/commands/taskCommands.js +342 -79
  15. package/dist/context/runContextPack.js +28 -16
  16. package/dist/context/taskContext.js +26 -3
  17. package/dist/controller/controller.js +8 -2
  18. package/dist/kernel/builtinCapabilities.js +32 -24
  19. package/dist/message/message.js +56 -0
  20. package/dist/plugins/pluginService.js +11 -3
  21. package/dist/resources/projectResource.js +0 -48
  22. package/dist/resources/projectResourceService.js +3 -81
  23. package/dist/setup/setupCommand.js +3 -8
  24. package/dist/storage/migrations/artifactsToGit.js +338 -0
  25. package/dist/storage/migrations/submitIntent.js +126 -0
  26. package/dist/storage/sqliteSchema.js +37 -3
  27. package/dist/storage/sqliteStore.js +1 -21
  28. package/dist/storage/storageVersions.js +1 -1
  29. package/dist/storage/storeRpc.js +1 -1
  30. package/dist/task/taskActivation.js +26 -0
  31. package/dist/task/taskActivationService.js +85 -69
  32. package/dist/task/taskSubmission.js +236 -0
  33. package/dist/web/assets/client/app.js +3 -2
  34. package/dist/web/assets/client/taskSurface.js +96 -7
  35. package/dist/web/webServer.js +18 -3
  36. package/dist/web/webTaskSurface.js +6 -6
  37. package/dist/workItem/workItem.js +14 -10
  38. package/docs/agent-result-consumption.md +2 -0
  39. package/docs/agent-result-consumption.zh-CN.md +81 -0
  40. package/docs/agent-runtime-drivers.md +2 -0
  41. package/docs/agent-runtime-drivers.zh-CN.md +77 -0
  42. package/docs/architecture/README.md +44 -32
  43. package/docs/architecture/README.zh-CN.md +43 -0
  44. package/docs/architecture/capabilities-and-resources.md +118 -79
  45. package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
  46. package/docs/managed-turn-and-session-runtime.md +2 -0
  47. package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
  48. package/docs/observability/README.md +2 -0
  49. package/docs/observability/README.zh-CN.md +71 -0
  50. package/docs/plugin-sdk.md +320 -217
  51. package/docs/plugin-sdk.zh-CN.md +293 -0
  52. package/docs/provider-runtime.md +2 -0
  53. package/docs/provider-runtime.zh-CN.md +132 -0
  54. package/docs/release-workflow.md +2 -0
  55. package/docs/release-workflow.zh-CN.md +237 -0
  56. package/docs/roles-and-configuration.md +2 -0
  57. package/docs/roles-and-configuration.zh-CN.md +96 -0
  58. package/docs/sqlite-control-plane-design.md +2 -0
  59. package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
  60. package/docs/task-dag-semantics.md +80 -57
  61. package/docs/task-dag-semantics.zh-CN.md +59 -0
  62. package/docs/task-delivery.md +2 -0
  63. package/docs/task-delivery.zh-CN.md +82 -0
  64. package/docs/task-local-identity.md +2 -0
  65. package/docs/task-local-identity.zh-CN.md +58 -0
  66. package/docs/testing/verification-levels.md +2 -0
  67. package/docs/testing/verification-levels.zh-CN.md +69 -0
  68. package/i18n/README.zh-CN.md +199 -10
  69. package/package.json +2 -1
  70. package/skills/yui-leader/SKILL.md +88 -331
  71. package/skills/yui-leader/references/execution.md +303 -0
  72. package/skills/yui-leader/references/planning.md +109 -0
  73. package/skills/yui-leader/references/task-plugins.md +8 -4
  74. 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 { stableArtifactRef } from "../resources/projectResource.js";
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(store, task.id, workItem.candidates));
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(store, task.id, dependency.candidates));
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(store, task.id, item.candidates));
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(store, task.id, dependency.candidates));
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(store, task.id, [candidate]));
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
- for (const artifact of store.listArtifacts(task.id)) {
429
- result.push(materialize("L3", "artifact", artifact.id, artifact));
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. Their immutable Home content survives cleanup of
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(store, taskId, candidates) {
487
+ function candidateArtifacts(taskId, candidates) {
482
488
  return candidates.flatMap((candidate) => (candidate.artifactRefs ?? []).map((ref) => {
483
- const artifact = ref.taskId === taskId ? store.getArtifact(taskId, ref.artifactId) : null;
484
- const actual = artifact === null ? null : stableArtifactRef(artifact);
485
- if (artifact === null || actual?.kind !== ref.kind || actual.digest !== ref.digest) {
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
- return materialize("L3", "artifact", artifact.id, artifact);
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": return store.getArtifact(taskId, refId);
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
- for (const artifact of store.listArtifacts(taskId))
369
- add("artifact", artifact.id, artifact);
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
- if (request.startMode !== "after-planning-turn"
492
- && request.operation.actorId !== `task:${taskId}/role:leader`)
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 { artifactSummary } from "../resources/projectResource.js";
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 immutable text, external version evidence, a Job receipt, or reference material.",
52
- effect: "local-mutation", requiredPermissions: ["task:read"], source: "ProjectResources.saveArtifact",
53
- inputSchema: object({ taskId: text, artifact: { anyOf: [
54
- object({ ...artifactBase, kind: { const: "content" }, content: { type: "string" } }, ["kind", "displayName", "provenance", "content"]),
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 saved results without starting the original Runtime or plugin.",
62
- effect: "query", requiredPermissions: ["task:read"], source: "TaskStore.getArtifact",
63
- inputSchema: object({ taskId: text, artifactId: text }), outputSchema: recordOutput
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 saved results and references.",
67
- effect: "query", requiredPermissions: ["task:read"], source: "TaskStore.listArtifacts",
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
- const result = sendTaskMessageCommand(store, taskId, params.body, params.wakePolicy, { environment: callerEnvironment(caller) }, params.recipient);
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 resources.saveArtifact(taskId, params.artifact);
284
- if (name === "artifact.read") {
285
- const artifact = store.getArtifact(taskId, params.artifactId);
286
- if (!artifact)
287
- throw new Error("Artifact not found in this Task.");
288
- return artifact;
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.listArtifacts(taskId).map(artifactSummary);
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")
@@ -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
- resources.saveArtifact(taskId, { kind: "content", displayName: "Plugin instance cleanup failure",
354
- provenance: `PluginService.dispose ${retired.ref.id}/${retired.ref.generation}`,
355
- content: error instanceof Error ? error.message : String(error) });
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, readFileSync, realpathSync, rmdirSync, statSync } from "node:fs";
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 { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
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, validateArtifact, stableArtifactRef, validateExecutionEnvironmentSnapshot } from "./projectResource.js";
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 Agent: ${agent.id}.`,
57
- `Leader Agent: ${roleStatus.leaderAgentId}.`,
56
+ `Operator: ${agent.id} (${roleStatus.operator}).`,
57
+ `Leader: ${roleStatus.leaderAgentId} (${roleStatus.leader}).`,
58
58
  `Default workspace: ${workspace}.`,
59
- `Operator configuration: ${roleStatus.operator}.`,
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 {