@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
@@ -20,8 +20,10 @@ import { agentExecutionComponentLabel } from "../agent/executionComponents.js";
20
20
  import { agentRunConfigurationLabel, renderAgentRunConfiguration } from "../output/agentRunConfigurationPresentation.js";
21
21
  import { formatTimestamp } from "../output/timePresentation.js";
22
22
  import { renderRoleDetails, renderRoleLaunchComparison } from "../output/rolePresentation.js";
23
- import { createTaskMessage, expandTaskMessageResult, taskMessageAuthorLabel, updateDraftTaskMessage } from "../message/message.js";
23
+ import { createTaskMessage, expandTaskMessageResult, taskMessageAuthorLabel, updateDraftTaskMessage, withSubmissionReceipt, TASK_SUBMISSION_INTENTS } from "../message/message.js";
24
24
  import { assertDraftTaskExecutionFree, validateDraftWorkItemEdit } from "../task/draftPlan.js";
25
+ import { TASK_PLANNING_ENTERED_EVENT, decideSubmissionRouting, describeSubmissionFeedback, draftActivationState, draftHasEnteredPlanning, normalizeSubmissionIntent, sameSubmissionTarget } from "../task/taskSubmission.js";
26
+ import { recordTaskActivationRequestInTransaction, taskActivationOperationRef } from "../task/taskActivationService.js";
25
27
  import { cancelInputRequest } from "../input/inputRequest.js";
26
28
  import { retireExactActiveRun, terminalizeExactTaskRun, validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
27
29
  import { copyGlobalRoleToTaskRole, createRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateRole } from "../role/role.js";
@@ -34,7 +36,7 @@ import { createTaskBrief, updateTaskBrief } from "../brief/taskBrief.js";
34
36
  import { createDecision, supersedeDecision } from "../decision/decision.js";
35
37
  import { createMilestone } from "../milestone/milestone.js";
36
38
  import { runPublicationCommand } from "./taskPublicationCommands.js";
37
- import { runTaskActivationCommand } from "./taskActivationCommands.js";
39
+ import { runTaskActivationCommand, nonLeaderActivationIdentity } from "./taskActivationCommands.js";
38
40
  import { assertTaskRemoteDeliveryProof, projectTaskRemoteDeliveryFromStore, renderTaskRemoteDelivery, runTaskRemoteDeliveryCommand } from "./taskRemoteDeliveryCommand.js";
39
41
  import { enqueueRoleRunDispatch, enqueueWork, settleExactWorkExecution } from "../coordination/workMailboxQueue.js";
40
42
  import { completeProcessing } from "../coordination/workMailbox.js";
@@ -44,6 +46,7 @@ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.j
44
46
  import { addTaskProjectBinding, archiveTask, completeTask, createTask, retireTask, reopenTask, taskOwnsManagedWorkspace, updateTaskMetadata } from "../task/task.js";
45
47
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
46
48
  import { projectCompletionReadiness } from "../task/completionReadiness.js";
49
+ import { StorageConflictError } from "../storage/taskStore.js";
47
50
  import { requireResolvedAgentProfileRuntime } from "../profile/agentProfileRuntime.js";
48
51
  import { assertProjectActive, resolveProject } from "../repository/project.js";
49
52
  import { currentWorkItemCandidate, currentWorkItemExecutionGroup, workItemExecutionGroupById, createWorkItem, editWorkItemDefinition, attachWorkItemExecutionGroup, updateWorkItemExecutionGroup, retireWorkItem, prepareWorkItemDispatch, submitWorkItemCandidate, updateWorkItemWriteProjects, updateWorkItemStatus } from "../workItem/workItem.js";
@@ -61,8 +64,8 @@ import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./rol
61
64
  import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
62
65
  import { runTaskContextCommand } from "./taskContextCommand.js";
63
66
  import { listContextMessages } from "../context/taskContext.js";
64
- import { createProjectResources, validateArtifactInput } from "../resources/projectResourceService.js";
65
- import { artifactSummary } from "../resources/projectResource.js";
67
+ import { createProjectResources } from "../resources/projectResourceService.js";
68
+ import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
66
69
  import { runTaskNextActionCommand } from "./taskNextActionCommand.js";
67
70
  import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
68
71
  import { inspectTaskRoleRuntimeStatuses, renderTaskRoleRuntimeStatus, taskRoleActiveWorkLabel, taskRoleLastRunLabel, taskRoleNativeSessionLabel, taskRoleOpenInputLabel, taskRoleTmuxLabel } from "./taskRoleRuntimeStatus.js";
@@ -303,44 +306,6 @@ export function runTaskCommand(args, store, options = {}) {
303
306
  assertTaskDeliveryAuthority(store, options.environment, options.environment.YUI_TASK_ID);
304
307
  }
305
308
  switch (command) {
306
- case "artifact": {
307
- const [action, taskId, value] = rest;
308
- if (!taskId || !["list", "show", "save"].includes(action)
309
- || rest.length !== (action === "list" ? 2 : 3)) {
310
- throw usageError("Usage: yui task artifact list <task> | show <task> <artifact-id> | save <task> <artifact-json>");
311
- }
312
- if (options.environment?.YUI_SESSION_SCOPE === "task" && options.environment.YUI_TASK_ID !== taskId) {
313
- throw usageError("Artifact is outside the managed Task scope.");
314
- }
315
- requireTask(store, taskId);
316
- let data;
317
- if (action === "list")
318
- data = store.listArtifacts(taskId).map(artifactSummary);
319
- else if (action === "show") {
320
- data = store.getArtifact(taskId, value);
321
- if (data === null)
322
- throw usageError("Artifact not found in this Task.");
323
- }
324
- else {
325
- taskActor(store, options, taskId);
326
- let parsed;
327
- try {
328
- parsed = JSON.parse(value);
329
- }
330
- catch {
331
- throw usageError("Artifact input must be JSON.");
332
- }
333
- let input;
334
- try {
335
- input = validateArtifactInput(parsed);
336
- }
337
- catch (error) {
338
- throw usageError(`Artifact input is invalid: ${error instanceof Error ? error.message : String(error)}`);
339
- }
340
- data = createProjectResources(store).saveArtifact(taskId, input);
341
- }
342
- return output(JSON.stringify(data, null, 2), data);
343
- }
344
309
  case "create": return createTaskCommand(rest, store, options);
345
310
  case "update": return output(updateTaskCommand(rest, store, options));
346
311
  case "list": return listTaskCommand(rest, store);
@@ -580,27 +545,273 @@ export function updateTaskMetadataCommand(store, taskId, patch, options = {}) {
580
545
  notifyMailbox(options.runtime, taskMailbox(result.id), result.id);
581
546
  return result;
582
547
  }
583
- export function submitOperatorMessage(body, taskId, store, options = {}) {
548
+ function routeUserSubmission(tx, task, actor, body, intent, now, submissionKey, target) {
549
+ const kind = actor;
550
+ const author = actor === "operator"
551
+ ? { type: "operator" }
552
+ : { type: "user" };
553
+ // §2.3 keyed idempotency: a retry is detected by reading an existing keyed
554
+ // Message, and its original disposition is reproduced from the receipt that
555
+ // Message carries — never recomputed from current state. A matching key with
556
+ // matching input and target replays; any mismatch conflicts.
557
+ if (submissionKey !== undefined) {
558
+ const prior = tx.listMessages(task.id).find((message) => message.submissionKey === submissionKey);
559
+ if (prior !== undefined) {
560
+ return replayKeyedSubmission(task, prior, kind, body, intent, target);
561
+ }
562
+ }
563
+ // Save first, recording intent and key, so the "saved" facet holds regardless of
564
+ // how routing then resolves (§2.5). Intent is stored, never re-read from the body.
565
+ const message = appendMessage(tx, task.id, body, kind, author, now, {
566
+ intent,
567
+ ...(submissionKey === undefined ? {} : { submissionKey })
568
+ });
569
+ // Re-read phase and activation inside this same transaction: this is the point
570
+ // the race is decided at (§2.3).
571
+ const enteredPlanning = draftHasEnteredPlanning(tx, task);
572
+ const activation = draftActivationState(task.activationRequest);
573
+ const routing = decideSubmissionRouting({
574
+ intent,
575
+ status: task.status,
576
+ enteredPlanning,
577
+ activation,
578
+ executionEnabled: task.executionGate.state === "enabled",
579
+ // develop adopts no extra resource by guessing: the workspace is still built
580
+ // from the Task's own Project bindings when it activates (§2.3).
581
+ developEnvironmentPlan: { kind: "empty" }
582
+ });
583
+ let queuedForLeader = false;
584
+ let activationRef;
585
+ let activationFailure;
586
+ switch (routing.kind) {
587
+ case "record":
588
+ case "planned-needs-manual-activation":
589
+ case "activation-blocked-execution-stopped":
590
+ // Save only. No Leader wake, no planning, no activation.
591
+ break;
592
+ case "await-activation":
593
+ // The submission is a post-activation input or a report against an existing
594
+ // request; it acts on nothing itself but surfaces the exact reference.
595
+ activationRef = task.activationRequest === undefined
596
+ ? undefined
597
+ : taskActivationOperationRef(task.id, task.activationRequest.operation.requestId);
598
+ if (routing.state === "failed") {
599
+ activationFailure = task.activationRequest?.outcome;
600
+ }
601
+ break;
602
+ case "enter-planning":
603
+ // The one place the shared service itself writes the planning-entered fact,
604
+ // in the same transaction as the message (§2.2 derivation source (a)).
605
+ recordTaskEvent(tx, task.id, TASK_PLANNING_ENTERED_EVENT, {
606
+ messageId: message.id,
607
+ intent
608
+ }, now);
609
+ enqueueWork(tx, leaderMailbox(task.id), submissionEnqueueReason(actor), now, [messageRef(task.id, message.id)], { source: actor, dedupeKey: `message:${task.id}:${message.id}` });
610
+ queuedForLeader = true;
611
+ break;
612
+ case "continue-planning":
613
+ case "active-context":
614
+ enqueueWork(tx, leaderMailbox(task.id), submissionEnqueueReason(actor), now, [messageRef(task.id, message.id)], { source: actor, dedupeKey: `message:${task.id}:${message.id}` });
615
+ queuedForLeader = true;
616
+ break;
617
+ case "activate": {
618
+ // Record the activation in-transaction, then queue only activation
619
+ // processing. The requestId is derived from the message so a retried
620
+ // submission cannot mint a second request.
621
+ const identity = nonLeaderActivationIdentity(tx, actor);
622
+ recordTaskActivationRequestInTransaction(tx, {
623
+ taskId: task.id,
624
+ requestId: `submit-${message.id}`,
625
+ actorId: identity.actorId,
626
+ authorityRef: identity.authorityRef,
627
+ environmentPlan: routing.environmentPlan,
628
+ origin: "submit-develop"
629
+ }, now);
630
+ activationRef = taskActivationOperationRef(task.id, `submit-${message.id}`);
631
+ enqueueWork(tx, taskMailbox(task.id), "activation-requested", now, [taskRef(task.id)]);
632
+ break;
633
+ }
634
+ default: {
635
+ const exhaustive = routing;
636
+ throw new Error(`Unhandled submission routing: ${JSON.stringify(exhaustive)}`);
637
+ }
638
+ }
639
+ const feedback = describeSubmissionFeedback({
640
+ taskId: task.id,
641
+ messageId: message.id,
642
+ status: task.status,
643
+ enteredPlanning,
644
+ activationState: activation,
645
+ routing,
646
+ ...(activationRef === undefined ? {} : { activationRef }),
647
+ ...(activationFailure === undefined ? {} : { activationFailure })
648
+ });
649
+ // §2.3 receipt: a keyed submission records the disposition it actually received
650
+ // (routing + feedback) and the target its key bound to, in this same
651
+ // transaction, so a later retry reproduces exactly this outcome rather than
652
+ // re-deriving one from a phase or activation that has since changed.
653
+ if (submissionKey !== undefined && target !== undefined) {
654
+ const receipted = withSubmissionReceipt(message, { target, routing, feedback });
655
+ tx.updateMessage(task.id, receipted);
656
+ return { task, message: receipted, routing, feedback, queuedForLeader };
657
+ }
658
+ return { task, message, routing, feedback, queuedForLeader };
659
+ }
660
+ /**
661
+ * Reproduce a keyed submission's original outcome from the receipt it recorded
662
+ * (task-32 §2.3), never by recomputing from the Task's current state. The prior
663
+ * Message's frozen {@link SubmissionReceipt} names the routing it received and the
664
+ * §2.5 feedback it returned, so a gate enabled or an activation cancelled after
665
+ * the fact can no longer fabricate a different disposition. The replay writes
666
+ * nothing — any Leader wake or activation request the original made already
667
+ * exists — and preserves the original Task/Message/routing/request references
668
+ * exactly.
669
+ *
670
+ * Same key with a different normalized input (kind, body or intent) or a
671
+ * different target is a conflict, never a silent overwrite or a cross-target
672
+ * replay. A prior keyed Message that carries no receipt (an old client, before
673
+ * receipts existed) also conflicts rather than being replayed from a rebuilt
674
+ * disposition.
675
+ */
676
+ function replayKeyedSubmission(task, prior, kind, body, intent, target) {
677
+ const receipt = prior.submissionReceipt;
678
+ if (prior.kind !== kind
679
+ || prior.body !== body
680
+ || normalizeSubmissionIntent(prior.intent) !== intent
681
+ || receipt === undefined
682
+ || (target !== undefined && !sameSubmissionTarget(receipt.target, target))) {
683
+ throw new StorageConflictError(`Submission key ${prior.submissionKey} was already used for a different submission on ${task.id}/${prior.id}.`);
684
+ }
685
+ // Reproduce the recorded disposition verbatim; a retry acts on nothing itself.
686
+ return {
687
+ task,
688
+ message: prior,
689
+ routing: receipt.routing,
690
+ feedback: receipt.feedback,
691
+ queuedForLeader: false
692
+ };
693
+ }
694
+ /** The mailbox reason a submission wake carries, preserving the existing
695
+ * operator-input / user-message vocabulary the Controller already understands. */
696
+ function submissionEnqueueReason(actor) {
697
+ return actor === "operator" ? "operator-input" : "user-message";
698
+ }
699
+ /**
700
+ * Render the §2.5 feedback as CLI text, one facet per line, so the user always
701
+ * sees the save, the phase, planning, activation, delivery and next step as
702
+ * separate statements — never a single "started". Web and capability callers
703
+ * return {@link SubmissionFeedback} structurally instead of this text.
704
+ */
705
+ function renderSubmissionFeedback(feedback) {
706
+ const lines = [
707
+ `Saved message ${feedback.saved.messageId} to ${feedback.saved.taskId}.`,
708
+ `Phase: ${SUBMISSION_PHASE_LABEL[feedback.phase]}.`
709
+ ];
710
+ if (feedback.planning !== "none") {
711
+ lines.push(`Planning: ${feedback.planning === "entered"
712
+ ? "started; the Leader is queued to plan"
713
+ : "continued; the Leader is queued"}.`);
714
+ }
715
+ if (feedback.activation !== "none") {
716
+ lines.push(`Activation: ${SUBMISSION_ACTIVATION_LABEL[feedback.activation]}.`);
717
+ }
718
+ if (feedback.delivery === "queued" && feedback.planning === "none") {
719
+ lines.push("Delivery: queued to the Leader.");
720
+ }
721
+ const nextStep = feedback.nextStep;
722
+ if (nextStep !== undefined)
723
+ lines.push(`Next: ${renderSubmissionNextStep(nextStep)}`);
724
+ return `${lines.join("\n")}\n`;
725
+ }
726
+ const SUBMISSION_PHASE_LABEL = {
727
+ active: "active",
728
+ "draft-planning": "Draft, in planning",
729
+ "draft-unplanned": "Draft, not yet in planning"
730
+ };
731
+ const SUBMISSION_ACTIVATION_LABEL = {
732
+ none: "none",
733
+ requested: "requested; activation is queued",
734
+ pending: "already pending; this input waits for it",
735
+ failed: "the previous request failed",
736
+ "manual-required": "already planned; activate explicitly to develop",
737
+ "execution-stopped": "not requested; execution is stopped"
738
+ };
739
+ function renderSubmissionNextStep(step) {
740
+ switch (step.kind) {
741
+ case "activate-manually":
742
+ return `activate it explicitly with "yui task activate ${step.taskId}".`;
743
+ case "start-execution":
744
+ return `start execution with "yui task execution start ${step.taskId}", then submit develop again.`;
745
+ case "await-pending-activation":
746
+ return `wait for the pending activation ${step.activationRef}.`;
747
+ case "resolve-failed-activation":
748
+ return `retry with a new activation request or cancel ${step.activationRef} (failure: ${step.failure}).`;
749
+ default: {
750
+ const exhaustive = step;
751
+ throw new Error(`Unhandled submission next step: ${JSON.stringify(exhaustive)}`);
752
+ }
753
+ }
754
+ }
755
+ /** Parse and validate the CLI `--intent` option. Absent leaves it undefined so
756
+ * the shared service applies the discuss default (task-32 §2.5). */
757
+ function parseSubmissionIntentOption(raw, usage) {
758
+ if (raw === undefined)
759
+ return undefined;
760
+ if (TASK_SUBMISSION_INTENTS.includes(raw)) {
761
+ return raw;
762
+ }
763
+ throw usageError(`--intent must be one of ${TASK_SUBMISSION_INTENTS.join(", ")}: ${raw}.`, usage);
764
+ }
765
+ export function submitOperatorMessage(body, taskId, store, options = {}, intent, submissionKey) {
584
766
  const now = clock(options);
767
+ const effectiveIntent = normalizeSubmissionIntent(intent);
585
768
  const result = store.transaction((tx) => {
586
769
  if (taskId !== undefined) {
770
+ // Addressed submit: the key is scoped to this Task and dedups within it, so
771
+ // no other Task is read (§2.3). Its target is this Task.
587
772
  const task = requireTask(tx, taskId);
588
773
  assertTaskOpen(task);
589
- const message = appendMessage(tx, task.id, body, "operator", { type: "operator" }, now);
590
- if (leaderWakingTaskStatus(task.status)) {
591
- enqueueWork(tx, leaderMailbox(task.id), "operator-input", now, [messageRef(task.id, message.id)]);
774
+ const routed = routeUserSubmission(tx, task, "operator", body, effectiveIntent, now, submissionKey, { kind: "task", taskId: task.id });
775
+ return { ...routed, created: false };
776
+ }
777
+ // Task-less submit: the key is scoped to "create a new Task". A retry must
778
+ // locate the Draft the original create produced — not any same-key Message on
779
+ // an addressed Task — and a key already bound to a specific Task cannot be
780
+ // reused to create (§2.3 "同 key 不同目标冲突").
781
+ if (submissionKey !== undefined) {
782
+ const lookup = findSubmissionKeyCreate(tx, submissionKey);
783
+ if (lookup.kind === "create") {
784
+ const task = requireTask(tx, lookup.taskId);
785
+ const routed = routeUserSubmission(tx, task, "operator", body, effectiveIntent, now, submissionKey, { kind: "create" });
786
+ return { ...routed, created: false };
787
+ }
788
+ if (lookup.kind === "other-target") {
789
+ throw new StorageConflictError(`Submission key ${submissionKey} is already bound to a specific Task; a task-less create cannot reuse it.`);
790
+ }
791
+ }
792
+ const createdAgg = createTaskAggregate(tx, titleFrom(body), {}, now);
793
+ const routed = routeUserSubmission(tx, createdAgg.task, "operator", body, effectiveIntent, now, submissionKey, { kind: "create" });
794
+ return { ...routed, task: createdAgg.task, created: true };
795
+ });
796
+ notifyMailbox(options.runtime, result.queuedForLeader ? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
797
+ const header = result.created
798
+ ? `Created Draft task ${result.task.id}: ${result.task.title}\n`
799
+ : "";
800
+ return `${header}${renderSubmissionFeedback(result.feedback)}`;
801
+ }
802
+ function findSubmissionKeyCreate(store, submissionKey) {
803
+ let otherTarget = false;
804
+ for (const task of store.listTasks()) {
805
+ for (const message of store.listMessages(task.id)) {
806
+ if (message.submissionKey !== submissionKey)
807
+ continue;
808
+ if (message.submissionReceipt?.target.kind === "create") {
809
+ return { kind: "create", taskId: task.id };
592
810
  }
593
- return { task, message, created: false };
811
+ otherTarget = true;
594
812
  }
595
- const created = createTaskAggregate(tx, titleFrom(body), {}, now);
596
- const message = appendMessage(tx, created.task.id, body, "operator", { type: "operator" }, now);
597
- enqueueWork(tx, leaderMailbox(created.task.id), "operator-input", now, [messageRef(created.task.id, message.id)]);
598
- return { ...created, message, created: true };
599
- });
600
- notifyMailbox(options.runtime, leaderWakingTaskStatus(result.task.status) ? leaderMailbox(result.task.id) : taskMailbox(result.task.id), result.task.id);
601
- return result.created
602
- ? `Created Draft task ${result.task.id}: ${result.task.title}\nSubmitted message ${result.message.id}\n`
603
- : `Submitted message ${result.message.id} to ${result.task.id}\n`;
813
+ }
814
+ return otherTarget ? { kind: "other-target" } : { kind: "unused" };
604
815
  }
605
816
  function createTaskCommand(args, store, options) {
606
817
  const parsed = parseTaskCreation(args, store);
@@ -884,8 +1095,8 @@ function completeTaskCommand(args, store, options) {
884
1095
  throw usageError(formatCompletionBlockers(task.id, readiness.blockers));
885
1096
  }
886
1097
  for (const ref of request.artifactRefs) {
887
- if (ref.startsWith("artifact-")) {
888
- fixedArtifactRefs(tx, task.id, [ref]);
1098
+ if (isGitArtifactRefString(ref)) {
1099
+ fixedArtifactRefs(task.id, [ref]);
889
1100
  }
890
1101
  else if (ref.startsWith("turn:")) {
891
1102
  const run = tx.getRun(task.id, ref.slice("turn:".length));
@@ -894,7 +1105,7 @@ function completeTaskCommand(args, store, options) {
894
1105
  }
895
1106
  }
896
1107
  else if (!/^https?:\/\/[^\s]+$/u.test(ref)) {
897
- throw usageError("Completion --artifact-ref must be a saved artifact id, turn:<local-turn-id>, or an explicit HTTP(S) reference URL.");
1108
+ throw usageError("Completion --artifact-ref must be a commit-pinned git:<commit>:<relativePath> reference, turn:<local-turn-id>, or an explicit HTTP(S) reference URL.");
898
1109
  }
899
1110
  }
900
1111
  const completed = completeTask(task, now, { by: actor, summary, artifactRefs: request.artifactRefs });
@@ -1388,8 +1599,8 @@ function taskMessageCommand(args, store, options) {
1388
1599
  return { kind: "output", output: `${JSON.stringify(expanded, null, 2)}\n`, data: expanded };
1389
1600
  }
1390
1601
  if (command === "send") {
1391
- const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--wake-policy leader|none] [--to <role> --work-item <id>|--review-round <id>].";
1392
- const parsed = parseTail(rest, new Set(["--body-file", "--wake-policy", "--to", "--work-item", "--review-round"]), usage);
1602
+ const usage = "Task message send usage: yui task message send <id> (<body>|--body-file <path|->) [--intent record|discuss|develop] [--request-id <key>] [--wake-policy leader|none] [--to <role> --work-item <id>|--review-round <id>].";
1603
+ const parsed = parseTail(rest, new Set(["--body-file", "--intent", "--request-id", "--wake-policy", "--to", "--work-item", "--review-round"]), usage);
1393
1604
  if (parsed.positionals.length < 1 || parsed.positionals.length > 2)
1394
1605
  throw usageError(usage);
1395
1606
  const body = readCommandText(parsed.positionals[1], parsed.options.get("--body-file"), "--body", usage);
@@ -1404,15 +1615,25 @@ function taskMessageCommand(args, store, options) {
1404
1615
  else {
1405
1616
  throw usageError(`--wake-policy must be 'leader' or 'none': ${wakePolicyRaw}.`);
1406
1617
  }
1618
+ const intent = parseSubmissionIntentOption(parsed.options.get("--intent"), usage);
1619
+ const submissionKey = parsed.options.get("--request-id");
1620
+ if (submissionKey !== undefined && submissionKey.trim().length === 0) {
1621
+ throw usageError("--request-id is required.", usage);
1622
+ }
1407
1623
  const recipientRole = parsed.options.get("--to");
1408
1624
  const workItemId = parsed.options.get("--work-item");
1409
1625
  const reviewRoundId = parsed.options.get("--review-round");
1410
1626
  if (recipientRole === undefined && (workItemId !== undefined || reviewRoundId !== undefined))
1411
1627
  throw usageError("--to is required for scoped Message delivery.");
1412
- const result = sendTaskMessageCommand(store, parsed.positionals[0], body, wakePolicy, options, recipientRole === undefined ? undefined : { roleName: recipientRole, workItemId, reviewRoundId });
1628
+ const result = sendTaskMessageCommand(store, parsed.positionals[0], body, wakePolicy, options, recipientRole === undefined ? undefined : { roleName: recipientRole, workItemId, reviewRoundId }, intent, submissionKey);
1629
+ // A user/operator submission returns the unified §2.5 feedback; render each
1630
+ // facet on its own line and expose the structure to non-text callers.
1631
+ if (result.feedback !== undefined) {
1632
+ return output(renderSubmissionFeedback(result.feedback), { taskId: result.task.id, message: result.message, submission: result.feedback });
1633
+ }
1413
1634
  const reason = result.message.continuation?.notDeliveredReason;
1414
1635
  const delivery = reason !== undefined ? { state: "not-delivered", reason }
1415
- : recipientRole !== undefined || result.actor !== "leader" && wakePolicy !== "none"
1636
+ : recipientRole !== undefined || result.queuedForLeader
1416
1637
  ? { state: "queued" } : { state: "saved" };
1417
1638
  return output(`Saved message ${result.message.id} to ${result.task.id} (${delivery.state}${reason === undefined ? "" : `: ${reason}`}).\n`, { taskId: result.task.id, message: result.message, delivery });
1418
1639
  }
@@ -1487,12 +1708,18 @@ function taskMessageCommand(args, store, options) {
1487
1708
  * execution is decided by the Controller from the Task's own status, so this
1488
1709
  * function never names a purpose and no second planning path exists.
1489
1710
  */
1490
- export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options = {}, recipient) {
1711
+ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options = {}, recipient, intent, submissionKey) {
1491
1712
  if (!body.trim())
1492
1713
  throw usageError("Message body is required.");
1493
1714
  if (recipient !== undefined && wakePolicy !== undefined) {
1494
1715
  throw usageError("--wake-policy applies only to unaddressed Leader Messages; an owner-directed Message uses its exact continuation boundary.");
1495
1716
  }
1717
+ if (recipient !== undefined && intent !== undefined) {
1718
+ throw usageError("A submission intent applies only to unaddressed Leader Messages; an owner-directed Message uses its exact continuation boundary.");
1719
+ }
1720
+ if (recipient !== undefined && submissionKey !== undefined) {
1721
+ throw usageError("A submission key applies only to unaddressed Leader Messages; an owner-directed Message uses its exact continuation boundary.");
1722
+ }
1496
1723
  const now = clock(options);
1497
1724
  const result = store.transaction((tx) => {
1498
1725
  const task = requireTask(tx, taskId);
@@ -1508,6 +1735,23 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
1508
1735
  }
1509
1736
  }
1510
1737
  const actor = roleCaller === undefined ? taskActor(tx, options, task.id) : "role";
1738
+ // A user/operator Message with no explicit recipient is the single path that
1739
+ // carries submission intent (record | discuss | develop). It routes through
1740
+ // the one shared submission service so intent means exactly the same thing
1741
+ // here as on every other surface. Internal role/leader result messages and
1742
+ // owner-directed continuations keep their exact existing boundary and never
1743
+ // gain develop authority (task-32 §2.5).
1744
+ if (recipient === undefined && (actor === "user" || actor === "operator")) {
1745
+ const effectiveIntent = normalizeSubmissionIntent(intent, wakePolicy);
1746
+ const routed = routeUserSubmission(tx, task, actor, body, effectiveIntent, now, submissionKey, { kind: "task", taskId: task.id });
1747
+ return {
1748
+ task: routed.task, message: routed.message, actor,
1749
+ queuedForLeader: routed.queuedForLeader, feedback: routed.feedback
1750
+ };
1751
+ }
1752
+ if (intent !== undefined) {
1753
+ throw usageError("Submission intent applies only to an unaddressed user or Operator Message.");
1754
+ }
1511
1755
  if (recipient !== undefined && actor === "leader")
1512
1756
  assertTaskDeliveryAuthority(tx, options.environment, task.id);
1513
1757
  const target = recipient === undefined ? undefined
@@ -1519,7 +1763,6 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
1519
1763
  ...(recipient.reviewRoundId === undefined ? {} : { reviewRoundId: recipient.reviewRoundId })
1520
1764
  });
1521
1765
  const context = {
1522
- ...(wakePolicy === undefined || actor === "leader" || actor === "role" ? {} : { wakePolicy }),
1523
1766
  ...(target === undefined ? {} : { recipient: target }),
1524
1767
  ...(recipient?.workItemId === undefined ? {} : { workItemId: recipient.workItemId })
1525
1768
  };
@@ -1530,8 +1773,13 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
1530
1773
  : actor === "operator"
1531
1774
  ? appendMessage(tx, task.id, body, "operator", { type: "operator" }, now, context)
1532
1775
  : appendMessage(tx, task.id, body, "user", { type: "user" }, now, context);
1776
+ // Reached only by addressed Messages and by an unaddressed Leader result now:
1777
+ // unaddressed user/operator submissions are handled above by the shared
1778
+ // service. A Worker/Reviewer message addressed to its Leader still wakes the
1779
+ // Leader exactly as before; an owner-directed continuation posts to the Task
1780
+ // mailbox for its exact owner Run.
1533
1781
  const queuedForLeader = target?.ownerRunId === undefined
1534
- && leaderWakingTaskStatus(task.status) && actor !== "leader" && wakePolicy !== "none";
1782
+ && leaderWakingTaskStatus(task.status) && actor !== "leader";
1535
1783
  if (target?.ownerRunId !== undefined) {
1536
1784
  const reason = messageContinuationBlocker(tx, message);
1537
1785
  if (reason !== undefined) {
@@ -1543,7 +1791,7 @@ export function sendTaskMessageCommand(store, taskId, body, wakePolicy, options
1543
1791
  else if (queuedForLeader) {
1544
1792
  enqueueWork(tx, leaderMailbox(task.id), actor === "operator" ? "operator-input" : "user-message", now, [messageRef(task.id, message.id)], { source: actor, dedupeKey: `message:${task.id}:${message.id}` });
1545
1793
  }
1546
- return { task, message, actor, queuedForLeader };
1794
+ return { task, message, actor, queuedForLeader, feedback: undefined };
1547
1795
  });
1548
1796
  if (recipient !== undefined) {
1549
1797
  notifyMailbox(options.runtime, taskMailbox(result.task.id), result.task.id);
@@ -2527,7 +2775,7 @@ function updateWorkScope(args, store, options) {
2527
2775
  return `${updated.changed ? "Updated" : "Unchanged"} Work Item Project scope ${updated.item.id}: ${updated.item.writeProjectIds.join(", ") || "read-only"}\n`;
2528
2776
  }
2529
2777
  function updateWork(args, store, options) {
2530
- const usage = "Task work update usage: yui task work update <task>/<work> <todo|running|done|failed> [--summary <text>] [--artifact-ref <artifact-id> ...].";
2778
+ const usage = "Task work update usage: yui task work update <task>/<work> <todo|running|done|failed> [--summary <text>] [--artifact-ref git:<commit>:<relative-path> ...].";
2531
2779
  const parsed = parseMultiValueTail(args, new Set(["--summary"]), new Set(["--artifact-ref"]), usage);
2532
2780
  exactPositionals(parsed.positionals, 2, usage);
2533
2781
  const requested = parsed.positionals[1];
@@ -2546,7 +2794,7 @@ function updateWork(args, store, options) {
2546
2794
  const current = requireWorkItem(tx, parsed.positionals[0], options);
2547
2795
  const task = requireTask(tx, current.taskId);
2548
2796
  assertTaskOpen(task);
2549
- const artifactRefs = artifactIds.length === 0 ? undefined : fixedArtifactRefs(tx, task.id, artifactIds);
2797
+ const artifactRefs = artifactIds.length === 0 ? undefined : fixedArtifactRefs(task.id, artifactIds);
2550
2798
  if (current.assignee === undefined) {
2551
2799
  taskActor(tx, options, task.id);
2552
2800
  if (status === "running") {
@@ -2954,8 +3202,11 @@ function acceptWork(args, store, options) {
2954
3202
  : item.candidates.find(({ id }) => id === candidateId);
2955
3203
  if (candidate === undefined)
2956
3204
  throw usageError(`Work Item Candidate not found: ${candidateId}.`);
2957
- if (candidate.artifactRefs !== undefined && !isDeepStrictEqual(fixedArtifactRefs(tx, task.id, candidate.artifactRefs.map((ref) => ref.artifactId)), candidate.artifactRefs))
2958
- throw usageError("Candidate Artifact references no longer match their saved immutable results.");
3205
+ // A Candidate's artifact references are commit-pinned (git:<commit>:<path>):
3206
+ // the commit self-certifies the frozen bytes, so they cannot drift and are
3207
+ // re-validated for shape whenever the Candidate is loaded. Their bytes are
3208
+ // resolved lazily on the async read/context path, never re-derived from a DB
3209
+ // mirror here.
2959
3210
  const taskFinalContract = taskFinalReviewContractForMutation(tx, task.id, options);
2960
3211
  const latestReview = reviewRoundsByIdentity(tx.listReviewRounds(item.taskId)
2961
3212
  .filter((round) => round.workItemId === item.id
@@ -5535,15 +5786,27 @@ function requireWorkItemCandidate(item) {
5535
5786
  }
5536
5787
  return candidate;
5537
5788
  }
5538
- function fixedArtifactRefs(store, taskId, ids) {
5539
- if (new Set(ids).size !== ids.length)
5789
+ /**
5790
+ * Parse `--artifact-ref git:<commit>:<relativePath>` selectors into canonical
5791
+ * commit-pinned references for the given Task. This is a PURE shape check with
5792
+ * no Git or DB I/O: the commit self-certifies the frozen bytes, so a valid
5793
+ * pinned reference is complete evidence. The bytes are proven to exist lazily
5794
+ * when they are resolved on the async read/context path.
5795
+ */
5796
+ function fixedArtifactRefs(taskId, refs) {
5797
+ if (new Set(refs).size !== refs.length)
5540
5798
  throw usageError("Artifact references must be unique.");
5541
- try {
5542
- return createProjectResources(store).resultRefs(taskId, ids);
5543
- }
5544
- catch (error) {
5545
- throw usageError(`Result Artifact is unavailable: ${messageOf(error)}`);
5546
- }
5799
+ return refs.map((ref) => {
5800
+ if (!isGitArtifactRefString(ref)) {
5801
+ throw usageError("Artifact reference must be a commit-pinned git:<commit>:<relativePath> reference.");
5802
+ }
5803
+ try {
5804
+ return parseGitArtifactRef(ref, taskId);
5805
+ }
5806
+ catch (error) {
5807
+ throw usageError(`Artifact reference is invalid: ${messageOf(error)}`);
5808
+ }
5809
+ });
5547
5810
  }
5548
5811
  /** ReviewRound ids are the durable Task-local creation order; wall time is not causal. */
5549
5812
  function reviewRoundsByIdentity(rounds) {