@zq-silk/yui 0.2.0 → 0.4.2
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 +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { normalizedUniqueIdentities, normalizedUniqueText, requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
|
+
import { normalizeCheckResult } from "./checkResult.js";
|
|
4
|
+
export function createIntegrationAttempt(input, now) {
|
|
5
|
+
const timestamp = now.toISOString();
|
|
6
|
+
return validateIntegrationAttempt({
|
|
7
|
+
schemaVersion: 2,
|
|
8
|
+
id: input.id,
|
|
9
|
+
taskId: input.taskId,
|
|
10
|
+
projectId: input.projectId,
|
|
11
|
+
targetRef: input.targetRef,
|
|
12
|
+
expectedHead: input.expectedHead,
|
|
13
|
+
changeSetIds: [...input.changeSetIds],
|
|
14
|
+
checkCommands: normalizedUniqueText(input.checkCommands ?? [], "Integration check command"),
|
|
15
|
+
status: "running",
|
|
16
|
+
createdAt: timestamp,
|
|
17
|
+
updatedAt: timestamp
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function requireLeaderDecision(attempt, report, now) {
|
|
21
|
+
validateIntegrationAttempt(attempt);
|
|
22
|
+
const continuingAfterResolution = attempt.status === "blocked"
|
|
23
|
+
&& attempt.resolution?.action === "manual-resolution";
|
|
24
|
+
if (attempt.status !== "running" && !continuingAfterResolution) {
|
|
25
|
+
throw new Error(`Integration cannot request a decision from ${attempt.status}.`);
|
|
26
|
+
}
|
|
27
|
+
const { resolution: _previousResolution, endedAt: _endedAt, ...unresolved } = attempt;
|
|
28
|
+
return validateIntegrationAttempt({
|
|
29
|
+
...unresolved,
|
|
30
|
+
status: "blocked",
|
|
31
|
+
conflict: normalizeConflictReport(report),
|
|
32
|
+
updatedAt: now.toISOString()
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function recordResolutionDecision(attempt, decision, now) {
|
|
36
|
+
validateIntegrationAttempt(attempt);
|
|
37
|
+
if (attempt.status !== "blocked" || attempt.conflict === undefined) {
|
|
38
|
+
throw new Error("Integration has no pending semantic decision.");
|
|
39
|
+
}
|
|
40
|
+
if (decision.action !== "manual-resolution" && decision.action !== "reject") {
|
|
41
|
+
throw new Error(`Resolution action is invalid: ${String(decision.action)}.`);
|
|
42
|
+
}
|
|
43
|
+
const timestamp = now.toISOString();
|
|
44
|
+
return validateIntegrationAttempt({
|
|
45
|
+
...attempt,
|
|
46
|
+
status: decision.action === "reject" ? "failed" : "blocked",
|
|
47
|
+
resolution: {
|
|
48
|
+
action: decision.action,
|
|
49
|
+
rationale: requireText(decision.rationale, "Resolution rationale"),
|
|
50
|
+
decidedBy: "leader",
|
|
51
|
+
decidedAt: timestamp
|
|
52
|
+
},
|
|
53
|
+
updatedAt: timestamp,
|
|
54
|
+
...(decision.action === "reject" ? { endedAt: timestamp } : {})
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function updateIntegrationAttempt(attempt, patch, now) {
|
|
58
|
+
validateIntegrationAttempt(attempt);
|
|
59
|
+
const status = patch.status ?? attempt.status;
|
|
60
|
+
const terminal = ["committed", "failed"].includes(status);
|
|
61
|
+
const updated = {
|
|
62
|
+
...attempt,
|
|
63
|
+
...patch,
|
|
64
|
+
status,
|
|
65
|
+
updatedAt: now.toISOString(),
|
|
66
|
+
...(terminal ? { endedAt: now.toISOString() } : {})
|
|
67
|
+
};
|
|
68
|
+
if (!terminal && updated.endedAt !== undefined) {
|
|
69
|
+
const { endedAt: _endedAt, ...active } = updated;
|
|
70
|
+
return validateIntegrationAttempt(active);
|
|
71
|
+
}
|
|
72
|
+
return validateIntegrationAttempt(updated);
|
|
73
|
+
}
|
|
74
|
+
export function validateIntegrationAttempt(attempt) {
|
|
75
|
+
if (attempt.schemaVersion !== 2) {
|
|
76
|
+
throw new Error("IntegrationAttempt must use schemaVersion 2.");
|
|
77
|
+
}
|
|
78
|
+
validateTaskRecordReference({
|
|
79
|
+
taskId: attempt.taskId,
|
|
80
|
+
localId: attempt.id
|
|
81
|
+
}, "integrationAttempt");
|
|
82
|
+
requireIdentity(attempt.projectId, "Project id");
|
|
83
|
+
requireText(attempt.targetRef, "Integration target ref");
|
|
84
|
+
requireCommit(attempt.expectedHead, "Integration expected head");
|
|
85
|
+
normalizedUniqueIdentities(attempt.changeSetIds, "ChangeSet id");
|
|
86
|
+
for (const changeSetId of attempt.changeSetIds) {
|
|
87
|
+
validateTaskRecordReference({ taskId: attempt.taskId, localId: changeSetId }, "changeSet");
|
|
88
|
+
}
|
|
89
|
+
normalizedUniqueText(attempt.checkCommands, "Integration check command");
|
|
90
|
+
if (attempt.changeSetIds.length === 0) {
|
|
91
|
+
throw new Error("IntegrationAttempt requires at least one ChangeSet.");
|
|
92
|
+
}
|
|
93
|
+
if (attempt.candidateCommit !== undefined) {
|
|
94
|
+
requireCommit(attempt.candidateCommit, "Integration candidate commit");
|
|
95
|
+
}
|
|
96
|
+
if (![
|
|
97
|
+
"running",
|
|
98
|
+
"blocked",
|
|
99
|
+
"validating",
|
|
100
|
+
"committed",
|
|
101
|
+
"failed"
|
|
102
|
+
].includes(attempt.status)) {
|
|
103
|
+
throw new Error(`Integration status is invalid: ${String(attempt.status)}.`);
|
|
104
|
+
}
|
|
105
|
+
if (attempt.conflict !== undefined)
|
|
106
|
+
normalizeConflictReport(attempt.conflict);
|
|
107
|
+
if (attempt.status === "blocked" && attempt.conflict === undefined) {
|
|
108
|
+
throw new Error("A blocked Integration needs a ConflictReport.");
|
|
109
|
+
}
|
|
110
|
+
if (attempt.resolution !== undefined) {
|
|
111
|
+
if (attempt.resolution.action !== "manual-resolution"
|
|
112
|
+
&& attempt.resolution.action !== "reject") {
|
|
113
|
+
throw new Error(`Resolution action is invalid: ${String(attempt.resolution.action)}.`);
|
|
114
|
+
}
|
|
115
|
+
requireText(attempt.resolution.rationale, "Resolution rationale");
|
|
116
|
+
if (attempt.resolution.decidedBy !== "leader") {
|
|
117
|
+
throw new Error("Only the Leader may record a ResolutionDecision.");
|
|
118
|
+
}
|
|
119
|
+
requireTimestamp(attempt.resolution.decidedAt, "Resolution decidedAt");
|
|
120
|
+
}
|
|
121
|
+
attempt.checks?.forEach(normalizeCheckResult);
|
|
122
|
+
requireTimestamp(attempt.createdAt, "Integration Attempt createdAt");
|
|
123
|
+
requireTimestamp(attempt.updatedAt, "Integration Attempt updatedAt");
|
|
124
|
+
if (["committed", "failed"].includes(attempt.status)) {
|
|
125
|
+
requireTimestamp(attempt.endedAt ?? "", "Integration Attempt endedAt");
|
|
126
|
+
}
|
|
127
|
+
return attempt;
|
|
128
|
+
}
|
|
129
|
+
function normalizeConflictReport(report) {
|
|
130
|
+
const affectedPaths = normalizedUniqueText(report.affectedPaths, "Conflict path");
|
|
131
|
+
return {
|
|
132
|
+
affectedPaths,
|
|
133
|
+
summary: requireText(report.summary, "Conflict summary")
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function requireCommit(value, label) {
|
|
137
|
+
const normalized = requireText(value, label).toLowerCase();
|
|
138
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(normalized)) {
|
|
139
|
+
throw new Error(`${label} is invalid.`);
|
|
140
|
+
}
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { formatTimestamp } from "../output/timePresentation.js";
|
|
2
|
+
import { formatInputRequestReceiptId, formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
|
+
export function createInputRequestOperatorPresentation(request, context) {
|
|
4
|
+
const policy = request.policy;
|
|
5
|
+
const recommendation = policy.kind === "recommended"
|
|
6
|
+
? request.choices.find(({ key }) => key === policy.recommendedChoiceKey)
|
|
7
|
+
: undefined;
|
|
8
|
+
if (policy.kind === "recommended" && recommendation === undefined) {
|
|
9
|
+
throw new Error(`Input request ${request.id} recommendation ${policy.recommendedChoiceKey} is missing.`);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
category: "attention",
|
|
13
|
+
taskId: request.taskId,
|
|
14
|
+
receiptId: formatInputRequestReceiptId(request.taskId, request.id),
|
|
15
|
+
source: { kind: "input-request", taskId: request.taskId, localId: request.id },
|
|
16
|
+
text: [
|
|
17
|
+
"A Task Leader is waiting for a boundary decision. Inspect it before involving the user.",
|
|
18
|
+
"Present it only when it is a user-owned choice, authorization, credential, unavailable external fact, or irreversible operation.",
|
|
19
|
+
`If it is an implementation, scheduling, review, or recoverable runtime choice, do not present it to the user; return it to the Leader with: yui task input cancel ${request.taskId} ${request.id} --reason \"Operator returned: not a user-owned boundary\"`,
|
|
20
|
+
"For a user-owned boundary, do not answer it yourself; do not answer or choose on the user's behalf; relay the user's exact response only.",
|
|
21
|
+
`Task: ${request.taskId}`,
|
|
22
|
+
`Input: ${request.id}`,
|
|
23
|
+
`Question: ${request.question}`,
|
|
24
|
+
...(request.choices.length === 0
|
|
25
|
+
? ["Answer type: free text"]
|
|
26
|
+
: ["Choices:", ...request.choices.map(({ key, label }) => ` ${key}: ${label}`)]),
|
|
27
|
+
...(policy.kind === "required"
|
|
28
|
+
? ["Decision policy: this requires the user's response; there is no automatic fallback."]
|
|
29
|
+
: [
|
|
30
|
+
`Agent recommendation: ${recommendation.key}: ${recommendation.label}`,
|
|
31
|
+
`Automatic fallback after: ${formatTimestamp(policy.timeoutAt, context.timeZone)}`
|
|
32
|
+
]),
|
|
33
|
+
`Inspect: yui task input show ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")}`,
|
|
34
|
+
request.choices.length === 0
|
|
35
|
+
? `After the user replies: yui task input answer ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")} --text "<answer>"`
|
|
36
|
+
: `After the user chooses: yui task input answer ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")} --choice <key>`
|
|
37
|
+
].join("\n")
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function createLeaderRecoveryOperatorPresentation(notification) {
|
|
41
|
+
return {
|
|
42
|
+
category: "attention",
|
|
43
|
+
taskId: notification.taskId,
|
|
44
|
+
receiptId: `leader-recovery:${notification.taskId}:${notification.createdAt}`,
|
|
45
|
+
source: { kind: "leader-recovery", id: notification.taskId },
|
|
46
|
+
text: [
|
|
47
|
+
"A Task cannot recover its Leader automatically and needs user attention.",
|
|
48
|
+
`Task: ${notification.taskId}`,
|
|
49
|
+
`Failure: ${notification.message}`,
|
|
50
|
+
`Inspect: yui task show ${notification.taskId}`,
|
|
51
|
+
"Recovery status: yui jobs list",
|
|
52
|
+
`Retry after inspection: yui jobs retry leader-recovery:${notification.taskId}`
|
|
53
|
+
].join("\n")
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function createLeaderStallOperatorPresentation(notification) {
|
|
57
|
+
return {
|
|
58
|
+
category: "attention",
|
|
59
|
+
taskId: notification.taskId,
|
|
60
|
+
receiptId: `leader-stall:${notification.taskId}:${notification.runId}:${notification.progressAt}`,
|
|
61
|
+
source: {
|
|
62
|
+
kind: "leader-stall",
|
|
63
|
+
id: `${notification.runId}:${notification.progressAt}`
|
|
64
|
+
},
|
|
65
|
+
text: [
|
|
66
|
+
"A Task Leader is truly stalled and needs user attention.",
|
|
67
|
+
`Task: ${notification.taskId}`,
|
|
68
|
+
`Leader Run: ${notification.runId}`,
|
|
69
|
+
`lastProgressAt: ${notification.progressAt}`,
|
|
70
|
+
"Classification: truly-stalled",
|
|
71
|
+
`Evidence: ${notification.evidenceKey}`,
|
|
72
|
+
"Controller preserved the exact Run/Session fence and performed no automatic Enter, reset, retry, kill, or Session replacement.",
|
|
73
|
+
`Inspect: yui task context ${notification.taskId}`,
|
|
74
|
+
"Choose the supported continuation, reset, retry, Agent change, or user-input action only after inspection."
|
|
75
|
+
].join("\n")
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function createTaskTerminalOperatorPresentation(notification) {
|
|
79
|
+
const action = notification.status === "completed" ? "completed" : "retired";
|
|
80
|
+
const actor = notification.by === "leader"
|
|
81
|
+
? "its Leader"
|
|
82
|
+
: notification.by === "operator"
|
|
83
|
+
? "the Operator"
|
|
84
|
+
: "the user";
|
|
85
|
+
return {
|
|
86
|
+
category: "information",
|
|
87
|
+
taskId: notification.taskId,
|
|
88
|
+
receiptId: `task-terminal:${notification.taskId}:${notification.status}:${notification.createdAt}`,
|
|
89
|
+
source: { kind: "task-terminal", id: notification.taskId },
|
|
90
|
+
text: [
|
|
91
|
+
`Task ${notification.taskId} was ${action} by ${actor}.`,
|
|
92
|
+
`Summary: ${notification.summary}`,
|
|
93
|
+
`Inspect: yui task show ${notification.taskId}`
|
|
94
|
+
].join("\n")
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
export function isPreInputReadinessSupported(capability) {
|
|
2
|
+
return capability.status === "supported";
|
|
3
|
+
}
|
|
4
|
+
export class CanonicalLifecycleError extends Error {
|
|
5
|
+
reason;
|
|
6
|
+
constructor(reason) {
|
|
7
|
+
super(reason);
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
this.name = "CanonicalLifecycleError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const RUN_SCOPED_PHASES = new Set([
|
|
13
|
+
"prompt-pushed",
|
|
14
|
+
"provider-accepted",
|
|
15
|
+
"turn-progress",
|
|
16
|
+
"turn-terminal"
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Phases that assert durable provider truth about a Run. Every one must carry
|
|
20
|
+
* the complete run-scoped identity — runId AND the generation fences
|
|
21
|
+
* (nativeSessionId + launchId) AND, where the transport owns it, the receiptId —
|
|
22
|
+
* because a missing or wrong fence must fail closed rather than advance a Run or
|
|
23
|
+
* its successor.
|
|
24
|
+
*/
|
|
25
|
+
const PROVIDER_TRUTH_PHASES = new Set([
|
|
26
|
+
"provider-accepted",
|
|
27
|
+
"turn-progress",
|
|
28
|
+
"turn-terminal"
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* The (source, evidence) combinations each phase is allowed to carry. This is
|
|
32
|
+
* where "receipt/pane/PID/liveness remain non-acceptance evidence" is enforced
|
|
33
|
+
* structurally: only `provider-native-durable` may advance provider truth
|
|
34
|
+
* (acceptance/progress/terminal), so neither a transport/liveness signal nor a
|
|
35
|
+
* shape-only `adapter-mapped` observation can ever forge or terminalize a turn.
|
|
36
|
+
* `adapter-mapped` remains permissible only for the pre-acceptance
|
|
37
|
+
* session-started/ready phases, whose fold never advances Run truth.
|
|
38
|
+
*/
|
|
39
|
+
const PHASE_EVIDENCE_MATRIX = {
|
|
40
|
+
"host-process-created": { sources: ["controller"], evidence: ["controller"] },
|
|
41
|
+
"provider-session-started": {
|
|
42
|
+
sources: ["provider-native"],
|
|
43
|
+
evidence: ["adapter-mapped", "provider-native-durable"]
|
|
44
|
+
},
|
|
45
|
+
"provider-ready": {
|
|
46
|
+
sources: ["provider-native"],
|
|
47
|
+
evidence: ["adapter-mapped", "provider-native-durable"]
|
|
48
|
+
},
|
|
49
|
+
"prompt-pushed": { sources: ["transport"], evidence: ["transport"] },
|
|
50
|
+
"provider-accepted": {
|
|
51
|
+
sources: ["provider-native"],
|
|
52
|
+
evidence: ["provider-native-durable"]
|
|
53
|
+
},
|
|
54
|
+
"turn-progress": {
|
|
55
|
+
sources: ["provider-native"],
|
|
56
|
+
evidence: ["provider-native-durable"]
|
|
57
|
+
},
|
|
58
|
+
"turn-terminal": {
|
|
59
|
+
sources: ["provider-native"],
|
|
60
|
+
evidence: ["provider-native-durable"]
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Builds a validated canonical event, failing closed if the phase, source,
|
|
65
|
+
* evidence, or identity fence are inconsistent. Callers (adapters, transport,
|
|
66
|
+
* the controller) must go through here so downstream folds can trust the shape.
|
|
67
|
+
*/
|
|
68
|
+
export function createCanonicalLifecycleEvent(input) {
|
|
69
|
+
const rule = PHASE_EVIDENCE_MATRIX[input.phase];
|
|
70
|
+
if (rule === undefined) {
|
|
71
|
+
throw new CanonicalLifecycleError(`Unknown canonical lifecycle phase: ${String(input.phase)}.`);
|
|
72
|
+
}
|
|
73
|
+
if (!rule.sources.includes(input.source)) {
|
|
74
|
+
throw new CanonicalLifecycleError(`Phase ${input.phase} rejects source ${input.source}; only ${rule.sources.join("/")} may prove it.`);
|
|
75
|
+
}
|
|
76
|
+
if (!rule.evidence.includes(input.evidence)) {
|
|
77
|
+
throw new CanonicalLifecycleError(`Phase ${input.phase} rejects evidence ${input.evidence}; only ${rule.evidence.join("/")} may prove it.`);
|
|
78
|
+
}
|
|
79
|
+
const fence = normalizeFence(input.fence);
|
|
80
|
+
if (RUN_SCOPED_PHASES.has(input.phase) && fence.runId === undefined) {
|
|
81
|
+
throw new CanonicalLifecycleError(`Phase ${input.phase} requires a runId fence.`);
|
|
82
|
+
}
|
|
83
|
+
// Provider-truth phases must carry the complete generation fence so a stale or
|
|
84
|
+
// wrong generation can never advance the Run.
|
|
85
|
+
if (PROVIDER_TRUTH_PHASES.has(input.phase)) {
|
|
86
|
+
if (fence.nativeSessionId === undefined) {
|
|
87
|
+
throw new CanonicalLifecycleError(`Phase ${input.phase} requires a nativeSessionId fence.`);
|
|
88
|
+
}
|
|
89
|
+
if (fence.launchId === undefined) {
|
|
90
|
+
throw new CanonicalLifecycleError(`Phase ${input.phase} requires a launchId fence.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (input.phase === "prompt-pushed" && fence.receiptId === undefined) {
|
|
94
|
+
throw new CanonicalLifecycleError("A prompt-pushed event requires a transport receiptId.");
|
|
95
|
+
}
|
|
96
|
+
if (input.phase === "provider-accepted" && fence.receiptId === undefined) {
|
|
97
|
+
throw new CanonicalLifecycleError("A provider-accepted event requires the transport receiptId it accepts.");
|
|
98
|
+
}
|
|
99
|
+
// Readiness must be explicit: provider-ready requires an explicit boolean and
|
|
100
|
+
// the provider-native variant that justifies it; every other phase forbids
|
|
101
|
+
// both so an ambiguous readiness claim can never be silently coerced.
|
|
102
|
+
if (input.phase === "provider-ready") {
|
|
103
|
+
if (typeof input.preInputReady !== "boolean") {
|
|
104
|
+
throw new CanonicalLifecycleError("A provider-ready event requires an explicit boolean preInputReady.");
|
|
105
|
+
}
|
|
106
|
+
if (input.readinessVariant !== undefined) {
|
|
107
|
+
requireEventText(input.readinessVariant, "readinessVariant");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
if (input.preInputReady !== undefined) {
|
|
112
|
+
throw new CanonicalLifecycleError("preInputReady is only meaningful on provider-ready.");
|
|
113
|
+
}
|
|
114
|
+
if (input.readinessVariant !== undefined) {
|
|
115
|
+
throw new CanonicalLifecycleError("readinessVariant is only meaningful on provider-ready.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (input.sequence !== undefined && !Number.isSafeInteger(input.sequence)) {
|
|
119
|
+
throw new CanonicalLifecycleError("sequence must be a safe integer.");
|
|
120
|
+
}
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
phase: input.phase,
|
|
123
|
+
source: input.source,
|
|
124
|
+
evidence: input.evidence,
|
|
125
|
+
fence,
|
|
126
|
+
...(input.preInputReady === undefined ? {} : { preInputReady: input.preInputReady }),
|
|
127
|
+
...(input.readinessVariant === undefined ? {} : { readinessVariant: input.readinessVariant }),
|
|
128
|
+
...(input.sequence === undefined ? {} : { sequence: input.sequence }),
|
|
129
|
+
...(input.summary === undefined ? {} : { summary: input.summary })
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The provider-neutral decision. Given a validated canonical event and the
|
|
134
|
+
* consumer's current expectation, it returns what should happen — idempotent for
|
|
135
|
+
* duplicates, deferred for not-yet-applicable facts, obsolete for stale/wrong
|
|
136
|
+
* generation, fail-closed for contract violations. It reads only phase / source
|
|
137
|
+
* / evidence / identity, never the adapter name, and mutates nothing.
|
|
138
|
+
*/
|
|
139
|
+
export function foldCanonicalLifecycleEvent(event, expectation) {
|
|
140
|
+
const identity = matchesFence(expectation, event);
|
|
141
|
+
if (identity !== null) {
|
|
142
|
+
return { outcome: "obsolete", reason: identity };
|
|
143
|
+
}
|
|
144
|
+
switch (event.phase) {
|
|
145
|
+
case "host-process-created":
|
|
146
|
+
return { outcome: "mark-host-created" };
|
|
147
|
+
case "provider-session-started": {
|
|
148
|
+
if (expectation.terminal)
|
|
149
|
+
return { outcome: "obsolete", reason: "session-started-after-terminal" };
|
|
150
|
+
// Idempotent on replay: a duplicate hook for an already-applied
|
|
151
|
+
// session-started fact is a no-op, read from existing durable state.
|
|
152
|
+
if (expectation.sessionStarted) {
|
|
153
|
+
return { outcome: "idempotent", reason: "already-session-started" };
|
|
154
|
+
}
|
|
155
|
+
// Discovery may bind a previously-unknown native id, but only under the
|
|
156
|
+
// exact launch fence and only when none is bound yet.
|
|
157
|
+
if (expectation.boundNativeSessionId === undefined
|
|
158
|
+
&& event.fence.nativeSessionId !== undefined
|
|
159
|
+
&& event.fence.launchId !== undefined
|
|
160
|
+
&& event.fence.launchId === expectation.fence.launchId) {
|
|
161
|
+
return { outcome: "bind-native-session", nativeSessionId: event.fence.nativeSessionId };
|
|
162
|
+
}
|
|
163
|
+
return { outcome: "mark-session-started" };
|
|
164
|
+
}
|
|
165
|
+
case "provider-ready": {
|
|
166
|
+
if (expectation.terminal)
|
|
167
|
+
return { outcome: "obsolete", reason: "ready-after-terminal" };
|
|
168
|
+
// Idempotent on replay against existing durable ready state.
|
|
169
|
+
if (expectation.ready)
|
|
170
|
+
return { outcome: "idempotent", reason: "already-ready" };
|
|
171
|
+
// preInputReady is guaranteed an explicit boolean by construction.
|
|
172
|
+
return { outcome: "mark-ready", preInputReady: event.preInputReady === true };
|
|
173
|
+
}
|
|
174
|
+
case "prompt-pushed":
|
|
175
|
+
if (expectation.terminal)
|
|
176
|
+
return { outcome: "obsolete", reason: "push-after-terminal" };
|
|
177
|
+
if (expectation.accepted)
|
|
178
|
+
return { outcome: "obsolete", reason: "push-after-accepted" };
|
|
179
|
+
// Exactly one push: a repeat for an already-pushed Run is a no-op, never a
|
|
180
|
+
// second Enter.
|
|
181
|
+
if (expectation.pushed)
|
|
182
|
+
return { outcome: "idempotent", reason: "already-pushed" };
|
|
183
|
+
return { outcome: "mark-pushed" };
|
|
184
|
+
case "provider-accepted":
|
|
185
|
+
// Only an identity-matched durable native event can move accepted/delivered,
|
|
186
|
+
// and only after the independently committed transport receipt. Provider
|
|
187
|
+
// acceptance and transport acknowledgement are deliberately separate
|
|
188
|
+
// evidence layers: neither may repair or infer the other.
|
|
189
|
+
if (!expectation.pushed)
|
|
190
|
+
return { outcome: "fail-closed", reason: "accept-without-push" };
|
|
191
|
+
if (expectation.terminal)
|
|
192
|
+
return { outcome: "obsolete", reason: "accept-after-terminal" };
|
|
193
|
+
if (expectation.accepted)
|
|
194
|
+
return { outcome: "idempotent", reason: "already-accepted" };
|
|
195
|
+
return { outcome: "advance-accepted" };
|
|
196
|
+
case "turn-progress":
|
|
197
|
+
// Progress only exists inside an accepted turn; a completion never promotes
|
|
198
|
+
// acceptance, so progress before acceptance is a contract violation.
|
|
199
|
+
if (!expectation.accepted)
|
|
200
|
+
return { outcome: "fail-closed", reason: "progress-without-accept" };
|
|
201
|
+
if (expectation.terminal)
|
|
202
|
+
return { outcome: "obsolete", reason: "progress-after-terminal" };
|
|
203
|
+
return { outcome: "advance-progress" };
|
|
204
|
+
case "turn-terminal":
|
|
205
|
+
if (expectation.terminal)
|
|
206
|
+
return { outcome: "idempotent", reason: "already-terminal" };
|
|
207
|
+
if (expectation.accepted)
|
|
208
|
+
return { outcome: "advance-terminal" };
|
|
209
|
+
// A terminal for a pushed-but-unaccepted Run must NOT promote acceptance;
|
|
210
|
+
// hold the immutable fact until (or unless) acceptance folds first.
|
|
211
|
+
if (expectation.pushed)
|
|
212
|
+
return { outcome: "deferred", reason: "terminal-before-accept" };
|
|
213
|
+
return { outcome: "fail-closed", reason: "terminal-without-push" };
|
|
214
|
+
default:
|
|
215
|
+
return { outcome: "fail-closed", reason: `unhandled-phase:${String(event.phase)}` };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Returns null when the event belongs to the expected generation/Run, or a
|
|
220
|
+
* mismatch reason otherwise. Owner/adapter and run-scoped runId are always
|
|
221
|
+
* compared. Provider-truth phases (acceptance/progress/terminal) require the
|
|
222
|
+
* COMPLETE generation fence — nativeSessionId, launchId, and (for acceptance)
|
|
223
|
+
* receiptId must be present and equal, so a missing or wrong generation/receipt
|
|
224
|
+
* fails closed rather than advancing a Run or its successor. Where a native id
|
|
225
|
+
* is already bound, every run-scoped fact must match it.
|
|
226
|
+
*/
|
|
227
|
+
function matchesFence(expectation, event) {
|
|
228
|
+
const expected = expectation.fence;
|
|
229
|
+
const actual = event.fence;
|
|
230
|
+
const phase = event.phase;
|
|
231
|
+
if (expected.taskId !== actual.taskId)
|
|
232
|
+
return "fence-mismatch:taskId";
|
|
233
|
+
if (expected.roleName !== actual.roleName)
|
|
234
|
+
return "fence-mismatch:roleName";
|
|
235
|
+
if (expected.agentId !== actual.agentId)
|
|
236
|
+
return "fence-mismatch:agentId";
|
|
237
|
+
if (expected.adapterId !== actual.adapterId)
|
|
238
|
+
return "fence-mismatch:adapterId";
|
|
239
|
+
// Run-scoped phases must name the exact expected Run.
|
|
240
|
+
if (RUN_SCOPED_PHASES.has(phase)) {
|
|
241
|
+
if (expected.runId === undefined)
|
|
242
|
+
return "fence-mismatch:expected-run-missing";
|
|
243
|
+
if (actual.runId !== expected.runId)
|
|
244
|
+
return "fence-mismatch:runId";
|
|
245
|
+
}
|
|
246
|
+
else if (actual.runId !== undefined
|
|
247
|
+
&& expected.runId !== undefined
|
|
248
|
+
&& actual.runId !== expected.runId) {
|
|
249
|
+
return "fence-mismatch:runId";
|
|
250
|
+
}
|
|
251
|
+
// Provider-truth phases require the complete, equal generation fence. A
|
|
252
|
+
// provider-accepted event additionally must carry the exact receipt it accepts.
|
|
253
|
+
if (PROVIDER_TRUTH_PHASES.has(phase)) {
|
|
254
|
+
if (actual.nativeSessionId === undefined)
|
|
255
|
+
return "fence-mismatch:missing-native-session";
|
|
256
|
+
if (actual.launchId === undefined)
|
|
257
|
+
return "fence-mismatch:missing-launch";
|
|
258
|
+
if (expected.launchId !== undefined && actual.launchId !== expected.launchId) {
|
|
259
|
+
return "fence-mismatch:launchId";
|
|
260
|
+
}
|
|
261
|
+
// Once a native id is bound, every provider-truth fact must match it. Before
|
|
262
|
+
// binding (discovery still open) the expected fence's own nativeSessionId, if
|
|
263
|
+
// present, still constrains it.
|
|
264
|
+
const boundNative = expectation.boundNativeSessionId ?? expected.nativeSessionId;
|
|
265
|
+
if (boundNative !== undefined && actual.nativeSessionId !== boundNative) {
|
|
266
|
+
return "fence-mismatch:nativeSessionId";
|
|
267
|
+
}
|
|
268
|
+
if (phase === "provider-accepted") {
|
|
269
|
+
if (actual.receiptId === undefined)
|
|
270
|
+
return "fence-mismatch:missing-receipt";
|
|
271
|
+
if (expected.receiptId !== undefined && actual.receiptId !== expected.receiptId) {
|
|
272
|
+
return "fence-mismatch:receiptId";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
// Pre-acceptance phases (host/session/ready and the transport push): compare
|
|
278
|
+
// the generation dimensions that are present on both sides. A push must match
|
|
279
|
+
// its expected receipt when both name one.
|
|
280
|
+
if (actual.nativeSessionId !== undefined
|
|
281
|
+
&& expectation.boundNativeSessionId !== undefined
|
|
282
|
+
&& actual.nativeSessionId !== expectation.boundNativeSessionId) {
|
|
283
|
+
return "fence-mismatch:nativeSessionId";
|
|
284
|
+
}
|
|
285
|
+
if (actual.nativeSessionId !== undefined
|
|
286
|
+
&& expected.nativeSessionId !== undefined
|
|
287
|
+
&& actual.nativeSessionId !== expected.nativeSessionId) {
|
|
288
|
+
return "fence-mismatch:nativeSessionId";
|
|
289
|
+
}
|
|
290
|
+
if (actual.launchId !== undefined
|
|
291
|
+
&& expected.launchId !== undefined
|
|
292
|
+
&& actual.launchId !== expected.launchId) {
|
|
293
|
+
return "fence-mismatch:launchId";
|
|
294
|
+
}
|
|
295
|
+
if (phase === "prompt-pushed"
|
|
296
|
+
&& actual.receiptId !== undefined
|
|
297
|
+
&& expected.receiptId !== undefined
|
|
298
|
+
&& actual.receiptId !== expected.receiptId) {
|
|
299
|
+
return "fence-mismatch:receiptId";
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
function normalizeFence(fence) {
|
|
304
|
+
const taskId = requireFenceText(fence.taskId, "taskId");
|
|
305
|
+
const roleName = requireFenceText(fence.roleName, "roleName");
|
|
306
|
+
const agentId = requireFenceText(fence.agentId, "agentId");
|
|
307
|
+
if (fence.adapterId !== "codex" && fence.adapterId !== "claude") {
|
|
308
|
+
throw new CanonicalLifecycleError(`Unsupported adapter fence: ${String(fence.adapterId)}.`);
|
|
309
|
+
}
|
|
310
|
+
return Object.freeze({
|
|
311
|
+
taskId,
|
|
312
|
+
roleName,
|
|
313
|
+
agentId,
|
|
314
|
+
adapterId: fence.adapterId,
|
|
315
|
+
...(fence.runId === undefined ? {} : { runId: requireFenceText(fence.runId, "runId") }),
|
|
316
|
+
...(fence.nativeSessionId === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: { nativeSessionId: requireFenceText(fence.nativeSessionId, "nativeSessionId") }),
|
|
319
|
+
...(fence.launchId === undefined ? {} : { launchId: requireFenceText(fence.launchId, "launchId") }),
|
|
320
|
+
...(fence.receiptId === undefined ? {} : { receiptId: requireFenceText(fence.receiptId, "receiptId") })
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function requireFenceText(value, label) {
|
|
324
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
325
|
+
throw new CanonicalLifecycleError(`Fence ${label} is invalid.`);
|
|
326
|
+
}
|
|
327
|
+
const text = value.trim();
|
|
328
|
+
if (text.length === 0 || text.length > 1_024) {
|
|
329
|
+
throw new CanonicalLifecycleError(`Fence ${label} is invalid.`);
|
|
330
|
+
}
|
|
331
|
+
return text;
|
|
332
|
+
}
|
|
333
|
+
function requireEventText(value, label) {
|
|
334
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
335
|
+
throw new CanonicalLifecycleError(`${label} is invalid.`);
|
|
336
|
+
}
|
|
337
|
+
const text = value.trim();
|
|
338
|
+
if (text.length === 0 || text.length > 1_024) {
|
|
339
|
+
throw new CanonicalLifecycleError(`${label} is invalid.`);
|
|
340
|
+
}
|
|
341
|
+
return text;
|
|
342
|
+
}
|