@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
|
@@ -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
|
|
65
|
-
import {
|
|
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
|
-
|
|
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
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
811
|
+
otherTarget = true;
|
|
594
812
|
}
|
|
595
|
-
|
|
596
|
-
|
|
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
|
|
888
|
-
fixedArtifactRefs(
|
|
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
|
|
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.
|
|
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"
|
|
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
|
|
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(
|
|
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
|
-
|
|
2958
|
-
|
|
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
|
-
|
|
5539
|
-
|
|
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
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
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) {
|