@zq-silk/yui 0.6.0 → 0.6.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/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { dataError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
2
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
4
|
+
import { formatTimestamp } from "../output/timePresentation.js";
|
|
5
|
+
import { createReleaseWorkflow, workflowStatus } from "../release/releaseWorkflow.js";
|
|
6
|
+
import { runReleaseWorkflow } from "../release/releaseWorkflowEngine.js";
|
|
7
|
+
import { openConfiguredTaskStore } from "../storage/sqliteStore.js";
|
|
8
|
+
const STEP_KINDS = new Set([
|
|
9
|
+
"pr-create-or-reuse", "ci-confirm", "merge", "version-tag",
|
|
10
|
+
"npm-publish", "fresh-install-smoke", "cli-update",
|
|
11
|
+
"controller-replace", "project-migrate", "post-verify"
|
|
12
|
+
]);
|
|
13
|
+
const IRREVERSIBILITY_LEVELS = new Set([
|
|
14
|
+
"none", "reversible", "irreversible"
|
|
15
|
+
]);
|
|
16
|
+
export function runWorkflowCommand(args, store, options) {
|
|
17
|
+
const [command, ...rest] = args;
|
|
18
|
+
switch (command) {
|
|
19
|
+
case "create": return createWorkflow(rest, store, options);
|
|
20
|
+
case "show": return showWorkflow(rest, store);
|
|
21
|
+
case "list": return listWorkflows(rest, store);
|
|
22
|
+
case "status": return workflowStatusCommand(rest, store);
|
|
23
|
+
case "run":
|
|
24
|
+
case "resume":
|
|
25
|
+
throw usageError(`Task workflow ${command} is asynchronous; use the yui CLI or runWorkflowCommandAsync.`);
|
|
26
|
+
default:
|
|
27
|
+
throw usageError(command === undefined
|
|
28
|
+
? "Task workflow command is required."
|
|
29
|
+
: `Unknown command: task workflow ${command}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function runWorkflowCommandAsync(args, store, options) {
|
|
33
|
+
const [command, ...rest] = args;
|
|
34
|
+
if (command !== "run" && command !== "resume") {
|
|
35
|
+
throw usageError(`Task workflow async command must be run or resume: ${String(command)}`);
|
|
36
|
+
}
|
|
37
|
+
const usage = `Task workflow ${command} usage: yui task workflow ${command} <task> <workflow-id> [--grant <grant-id>] [--max-steps <int>].`;
|
|
38
|
+
const parsed = parseTail(rest, new Set(["--grant", "--max-steps"]), usage);
|
|
39
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
40
|
+
const taskId = parsed.positionals[0];
|
|
41
|
+
requireTask(store, taskId);
|
|
42
|
+
const workflowId = parsed.positionals[1];
|
|
43
|
+
const existing = store.getReleaseWorkflow(taskId, workflowId);
|
|
44
|
+
if (existing === null) {
|
|
45
|
+
throw dataError(`Release workflow not found: ${taskId}/${workflowId}.`);
|
|
46
|
+
}
|
|
47
|
+
const ports = options.ports;
|
|
48
|
+
if (ports === undefined) {
|
|
49
|
+
throw usageError(`Task workflow ${command} requires ports; the yui CLI wires the real adapter.`, usage);
|
|
50
|
+
}
|
|
51
|
+
const grantId = optionalOption(parsed.options, "--grant");
|
|
52
|
+
let maxSteps = options.maxSteps;
|
|
53
|
+
if (parsed.options.has("--max-steps")) {
|
|
54
|
+
const value = Number.parseInt(parsed.options.get("--max-steps"), 10);
|
|
55
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
56
|
+
throw usageError("--max-steps must be a positive integer.", usage);
|
|
57
|
+
}
|
|
58
|
+
maxSteps = value;
|
|
59
|
+
}
|
|
60
|
+
// The immutable run-intent event commits BEFORE the engine persists the
|
|
61
|
+
// first transition or calls an external system. A run that is killed,
|
|
62
|
+
// crashes, or whose engine rethrows after this point can never leave
|
|
63
|
+
// mutation or effect evidence without an audit record saying the run was
|
|
64
|
+
// attempted. FileTaskStore transactions are synchronous, so the async
|
|
65
|
+
// engine run cannot join this transaction; committing the intent first is
|
|
66
|
+
// what makes the trail crash-safe across the whole run.
|
|
67
|
+
const intentNow = clock(options);
|
|
68
|
+
store.transaction((tx) => {
|
|
69
|
+
recordTaskEvent(tx, taskId, "release-workflow.run-started", {
|
|
70
|
+
workflowId,
|
|
71
|
+
command,
|
|
72
|
+
grantId: grantId ?? existing.grantId
|
|
73
|
+
}, intentNow);
|
|
74
|
+
});
|
|
75
|
+
const result = await runReleaseWorkflow(store, taskId, workflowId, ports, {
|
|
76
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
77
|
+
...(maxSteps === undefined ? {} : { maxSteps }),
|
|
78
|
+
...(grantId === undefined ? {} : { grantId })
|
|
79
|
+
});
|
|
80
|
+
// The engine persisted every step transition for crash recovery. The
|
|
81
|
+
// outcome event and the workflow's final state commit together so the
|
|
82
|
+
// audit trail cannot lag the record on a normal engine return.
|
|
83
|
+
const eventNow = clock(options);
|
|
84
|
+
const eventPayload = {
|
|
85
|
+
workflowId,
|
|
86
|
+
grantId: result.workflow.grantId,
|
|
87
|
+
outcome: result.outcome,
|
|
88
|
+
...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),
|
|
89
|
+
attempted: result.attempted.join(",")
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
store.transaction((tx) => {
|
|
93
|
+
tx.saveReleaseWorkflow(taskId, result.workflow);
|
|
94
|
+
recordTaskEvent(tx, taskId, "release-workflow.run", eventPayload, eventNow);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// The instance-level event save failed after a normal engine return. The
|
|
99
|
+
// engine already committed the final workflow state for crash recovery
|
|
100
|
+
// and the run-intent event above is durable, so the trail cannot be
|
|
101
|
+
// empty; fall back to a fresh store of the SAME backend so the outcome
|
|
102
|
+
// record commits to the authoritative store, then surface the original
|
|
103
|
+
// failure so the caller knows this path was compromised.
|
|
104
|
+
if (typeof store.rootDirectory === "function") {
|
|
105
|
+
const auditStore = openConfiguredTaskStore(store.rootDirectory());
|
|
106
|
+
recordTaskEvent(auditStore, taskId, "release-workflow.run", eventPayload, eventNow);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
return output(renderRun(result), result);
|
|
111
|
+
}
|
|
112
|
+
function workflowStatusCommand(args, store) {
|
|
113
|
+
const usage = "Task workflow status usage: yui task workflow status <task> <workflow-id>.";
|
|
114
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
115
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
116
|
+
const taskId = parsed.positionals[0];
|
|
117
|
+
requireTask(store, taskId);
|
|
118
|
+
const workflow = store.getReleaseWorkflow(taskId, parsed.positionals[1]);
|
|
119
|
+
if (workflow === null) {
|
|
120
|
+
throw dataError(`Release workflow not found: ${taskId}/${parsed.positionals[1]}.`);
|
|
121
|
+
}
|
|
122
|
+
const lines = [`Workflow: ${workflow.id}`, `Status: ${workflowStatus(workflow)}`];
|
|
123
|
+
for (const entry of workflow.plan) {
|
|
124
|
+
const step = workflow.steps[entry.id];
|
|
125
|
+
const external = step.externalId === undefined ? "-" : step.externalId;
|
|
126
|
+
lines.push(` [${step.status}] ${entry.id} (${entry.kind}) attempts=${step.attempts} ext=${external}`);
|
|
127
|
+
}
|
|
128
|
+
return output(lines.join("\n").concat("\n"), workflow);
|
|
129
|
+
}
|
|
130
|
+
function renderRun(result) {
|
|
131
|
+
const lines = [
|
|
132
|
+
`Workflow ${result.workflow.id}: ${result.outcome} (status ${result.status})`,
|
|
133
|
+
...(result.stopReason === undefined ? [] : [`Stop: ${result.stopReason}`]),
|
|
134
|
+
`Attempted: ${result.attempted.length === 0 ? "-" : result.attempted.join(", ")}`
|
|
135
|
+
];
|
|
136
|
+
return lines.join("\n").concat("\n");
|
|
137
|
+
}
|
|
138
|
+
function createWorkflow(args, store, options) {
|
|
139
|
+
const usage = "Task workflow create usage: yui task workflow create <task> --grant <grant-id> --source-repo <owner/name> --source-commit <sha> [--source-artifact <name@integrity>] --step <id>:<kind> (repeatable) [--step-irreversibility <id>=<level> (repeatable)] [--step-param <id>:<key>=<value> (repeatable)].";
|
|
140
|
+
const parsed = parseMultiValueTail(args, new Set(["--grant", "--source-repo", "--source-commit", "--source-artifact"]), new Set(["--step", "--step-irreversibility", "--step-param"]), usage);
|
|
141
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
142
|
+
const taskId = parsed.positionals[0];
|
|
143
|
+
requireTask(store, taskId);
|
|
144
|
+
const grantId = requiredOption(parsed.options, "--grant");
|
|
145
|
+
const grant = store.getCapabilityGrant(taskId, grantId);
|
|
146
|
+
if (grant === null) {
|
|
147
|
+
throw dataError(`Capability grant not found: ${taskId}/${grantId}.`);
|
|
148
|
+
}
|
|
149
|
+
const source = buildSource(parsed, usage);
|
|
150
|
+
const plan = buildPlan(parsed, usage);
|
|
151
|
+
const now = clock(options);
|
|
152
|
+
// The workflow record and its audit event commit in one transaction: a
|
|
153
|
+
// failed event save cannot leave a persisted workflow without its audit
|
|
154
|
+
// trail.
|
|
155
|
+
const workflow = store.transaction((tx) => {
|
|
156
|
+
const created = createReleaseWorkflow(tx.nextReleaseWorkflowId(taskId), taskId, { grantId, source, plan }, now);
|
|
157
|
+
tx.saveReleaseWorkflow(taskId, created);
|
|
158
|
+
recordTaskEvent(tx, taskId, "release-workflow.created", {
|
|
159
|
+
workflowId: created.id,
|
|
160
|
+
grantId: created.grantId,
|
|
161
|
+
commit: created.source.commit,
|
|
162
|
+
steps: created.plan.map((entry) => entry.id).join(",")
|
|
163
|
+
}, now);
|
|
164
|
+
return created;
|
|
165
|
+
});
|
|
166
|
+
return output(`Created release workflow ${workflow.id} with ${workflow.plan.length} steps.\n`, workflow);
|
|
167
|
+
}
|
|
168
|
+
function showWorkflow(args, store) {
|
|
169
|
+
const usage = "Task workflow show usage: yui task workflow show <task> <workflow-id>.";
|
|
170
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
171
|
+
exactPositionals(parsed.positionals, 2, usage);
|
|
172
|
+
const taskId = parsed.positionals[0];
|
|
173
|
+
requireTask(store, taskId);
|
|
174
|
+
const workflow = store.getReleaseWorkflow(taskId, parsed.positionals[1]);
|
|
175
|
+
if (workflow === null) {
|
|
176
|
+
throw dataError(`Release workflow not found: ${taskId}/${parsed.positionals[1]}.`);
|
|
177
|
+
}
|
|
178
|
+
return output(renderWorkflow(workflow, store.getConfig().timeZone), workflow);
|
|
179
|
+
}
|
|
180
|
+
function listWorkflows(args, store) {
|
|
181
|
+
const usage = "Task workflow list usage: yui task workflow list <task>.";
|
|
182
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
183
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
184
|
+
const taskId = parsed.positionals[0];
|
|
185
|
+
requireTask(store, taskId);
|
|
186
|
+
const workflows = store.listReleaseWorkflows(taskId);
|
|
187
|
+
if (workflows.length === 0) {
|
|
188
|
+
return output("No release workflows found.\n", []);
|
|
189
|
+
}
|
|
190
|
+
const rendered = `${renderTable(`Release workflows: ${taskId}`, [
|
|
191
|
+
{ header: "ID", minWidth: 6, maxWidth: 22 },
|
|
192
|
+
{ header: "Grant", minWidth: 6, maxWidth: 22 },
|
|
193
|
+
{ header: "Source", minWidth: 10, maxWidth: 40 },
|
|
194
|
+
{ header: "Steps", minWidth: 5, maxWidth: 8 },
|
|
195
|
+
{ header: "Status", minWidth: 7, maxWidth: 12 }
|
|
196
|
+
], workflows.map((workflow) => [
|
|
197
|
+
workflow.id,
|
|
198
|
+
workflow.grantId,
|
|
199
|
+
sourceSummary(workflow),
|
|
200
|
+
`${workflow.plan.length}`,
|
|
201
|
+
workflowStatus(workflow)
|
|
202
|
+
]), defaultTableWidth())}\n`;
|
|
203
|
+
return output(rendered, workflows);
|
|
204
|
+
}
|
|
205
|
+
function buildSource(parsed, usage) {
|
|
206
|
+
const repo = requiredOption(parsed.options, "--source-repo");
|
|
207
|
+
const separator = repo.indexOf("/");
|
|
208
|
+
if (separator <= 0 || separator === repo.length - 1) {
|
|
209
|
+
throw usageError("--source-repo must use owner/name.", usage);
|
|
210
|
+
}
|
|
211
|
+
const commit = requiredOption(parsed.options, "--source-commit");
|
|
212
|
+
const artifactValue = optionalOption(parsed.options, "--source-artifact");
|
|
213
|
+
if (artifactValue === undefined) {
|
|
214
|
+
return {
|
|
215
|
+
repository: {
|
|
216
|
+
owner: repo.slice(0, separator).trim(),
|
|
217
|
+
name: repo.slice(separator + 1).trim()
|
|
218
|
+
},
|
|
219
|
+
commit
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const at = artifactValue.indexOf("@");
|
|
223
|
+
if (at <= 0 || at === artifactValue.length - 1) {
|
|
224
|
+
throw usageError("--source-artifact must use name@integrity.", usage);
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
repository: {
|
|
228
|
+
owner: repo.slice(0, separator).trim(),
|
|
229
|
+
name: repo.slice(separator + 1).trim()
|
|
230
|
+
},
|
|
231
|
+
commit,
|
|
232
|
+
artifact: {
|
|
233
|
+
name: artifactValue.slice(0, at).trim(),
|
|
234
|
+
integrity: artifactValue.slice(at + 1).trim()
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function buildPlan(parsed, usage) {
|
|
239
|
+
const stepValues = parsed.multiOptions.get("--step") ?? [];
|
|
240
|
+
if (stepValues.length === 0) {
|
|
241
|
+
throw usageError("--step is required.", usage);
|
|
242
|
+
}
|
|
243
|
+
const plan = stepValues.map((value) => {
|
|
244
|
+
const separator = value.indexOf(":");
|
|
245
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
246
|
+
throw usageError("--step must use id:kind.", usage);
|
|
247
|
+
}
|
|
248
|
+
const id = value.slice(0, separator).trim();
|
|
249
|
+
const kind = value.slice(separator + 1).trim();
|
|
250
|
+
if (!STEP_KINDS.has(kind)) {
|
|
251
|
+
throw usageError(`--step kind is invalid: ${kind}.`, usage);
|
|
252
|
+
}
|
|
253
|
+
return { id, kind: kind };
|
|
254
|
+
});
|
|
255
|
+
const irreversibility = parsed.multiOptions.get("--step-irreversibility") ?? [];
|
|
256
|
+
for (const value of irreversibility) {
|
|
257
|
+
const separator = value.indexOf("=");
|
|
258
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
259
|
+
throw usageError("--step-irreversibility must use id=level.", usage);
|
|
260
|
+
}
|
|
261
|
+
const id = value.slice(0, separator).trim();
|
|
262
|
+
const level = value.slice(separator + 1).trim();
|
|
263
|
+
if (!IRREVERSIBILITY_LEVELS.has(level)) {
|
|
264
|
+
throw usageError("--step-irreversibility must be one of: none, reversible, irreversible.", usage);
|
|
265
|
+
}
|
|
266
|
+
const entry = plan.find((candidate) => candidate.id === id);
|
|
267
|
+
if (entry === undefined) {
|
|
268
|
+
throw usageError(`--step-irreversibility names an unknown step: ${id}.`, usage);
|
|
269
|
+
}
|
|
270
|
+
entry.irreversibility = level;
|
|
271
|
+
}
|
|
272
|
+
const stepParams = parsed.multiOptions.get("--step-param") ?? [];
|
|
273
|
+
for (const value of stepParams) {
|
|
274
|
+
const separator = value.indexOf(":");
|
|
275
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
276
|
+
throw usageError("--step-param must use id:key=value.", usage);
|
|
277
|
+
}
|
|
278
|
+
const id = value.slice(0, separator).trim();
|
|
279
|
+
const assignment = value.slice(separator + 1);
|
|
280
|
+
const equals = assignment.indexOf("=");
|
|
281
|
+
if (equals <= 0 || equals === assignment.length - 1) {
|
|
282
|
+
throw usageError("--step-param must use id:key=value.", usage);
|
|
283
|
+
}
|
|
284
|
+
const entry = plan.find((candidate) => candidate.id === id);
|
|
285
|
+
if (entry === undefined) {
|
|
286
|
+
throw usageError(`--step-param names an unknown step: ${id}.`, usage);
|
|
287
|
+
}
|
|
288
|
+
const key = assignment.slice(0, equals).trim();
|
|
289
|
+
const paramValue = assignment.slice(equals + 1).trim();
|
|
290
|
+
if (key.length === 0 || paramValue.length === 0) {
|
|
291
|
+
throw usageError("--step-param must use id:key=value.", usage);
|
|
292
|
+
}
|
|
293
|
+
entry.params = { ...(entry.params ?? {}), [key]: paramValue };
|
|
294
|
+
}
|
|
295
|
+
return plan;
|
|
296
|
+
}
|
|
297
|
+
function sourceSummary(workflow) {
|
|
298
|
+
const { owner, name } = workflow.source.repository;
|
|
299
|
+
const shortCommit = workflow.source.commit.slice(0, 7);
|
|
300
|
+
return `${owner}/${name}@${shortCommit}`;
|
|
301
|
+
}
|
|
302
|
+
function renderWorkflow(workflow, timeZone) {
|
|
303
|
+
const lines = [
|
|
304
|
+
`Workflow: ${workflow.id}`,
|
|
305
|
+
`Task: ${workflow.taskId}`,
|
|
306
|
+
`Grant: ${workflow.grantId}`,
|
|
307
|
+
`Source: ${sourceSummary(workflow)}`,
|
|
308
|
+
`Status: ${workflowStatus(workflow)}`,
|
|
309
|
+
"Steps:"
|
|
310
|
+
];
|
|
311
|
+
for (const entry of workflow.plan) {
|
|
312
|
+
const step = workflow.steps[entry.id];
|
|
313
|
+
const external = step.externalId === undefined ? "-" : step.externalId;
|
|
314
|
+
lines.push(` [${step.status}] ${entry.id} (${entry.kind}) attempts=${step.attempts} ext=${external}`);
|
|
315
|
+
}
|
|
316
|
+
lines.push(`Created: ${formatTimestamp(workflow.createdAt, timeZone)}`);
|
|
317
|
+
lines.push(`Updated: ${formatTimestamp(workflow.updatedAt, timeZone)}`);
|
|
318
|
+
return lines.join("\n").concat("\n");
|
|
319
|
+
}
|
|
320
|
+
function requireTask(store, taskId) {
|
|
321
|
+
const id = requiredText(taskId, "Task id");
|
|
322
|
+
const task = store.getTask(id);
|
|
323
|
+
if (task === null)
|
|
324
|
+
throw taskNotFound(id);
|
|
325
|
+
return task;
|
|
326
|
+
}
|
|
327
|
+
function recordTaskEvent(store, taskId, type, payload, now) {
|
|
328
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, type, payload, now));
|
|
329
|
+
}
|
|
330
|
+
function requiredOption(options, name) {
|
|
331
|
+
return requiredText(options.get(name), name);
|
|
332
|
+
}
|
|
333
|
+
function optionalOption(options, name) {
|
|
334
|
+
if (!options.has(name))
|
|
335
|
+
return undefined;
|
|
336
|
+
return requiredText(options.get(name), name);
|
|
337
|
+
}
|
|
338
|
+
function requiredText(value, label) {
|
|
339
|
+
const normalized = value?.trim();
|
|
340
|
+
if (normalized === undefined || normalized.length === 0) {
|
|
341
|
+
throw usageError(`${label} is required.`);
|
|
342
|
+
}
|
|
343
|
+
return normalized;
|
|
344
|
+
}
|
|
345
|
+
function clock(options) {
|
|
346
|
+
return options.now?.() ?? new Date();
|
|
347
|
+
}
|
|
348
|
+
function output(value, data) {
|
|
349
|
+
return data === undefined
|
|
350
|
+
? { kind: "output", output: value }
|
|
351
|
+
: { kind: "output", output: value, data };
|
|
352
|
+
}
|
|
353
|
+
function exactPositionals(values, count, usage) {
|
|
354
|
+
if (values.length !== count || values.some((value) => value.trim().length === 0)) {
|
|
355
|
+
throw usageError(usage);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function parseTail(args, valueOptions, usage) {
|
|
359
|
+
const positionals = [];
|
|
360
|
+
const options = new Map();
|
|
361
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
362
|
+
const value = args[index];
|
|
363
|
+
if (!value.startsWith("--")) {
|
|
364
|
+
positionals.push(value);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (!valueOptions.has(value)) {
|
|
368
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
369
|
+
}
|
|
370
|
+
if (options.has(value)) {
|
|
371
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
372
|
+
}
|
|
373
|
+
const optionValue = args[index + 1];
|
|
374
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
375
|
+
throw usageError(`${value} is required.`, usage);
|
|
376
|
+
}
|
|
377
|
+
options.set(value, optionValue);
|
|
378
|
+
index += 1;
|
|
379
|
+
}
|
|
380
|
+
return { positionals, options };
|
|
381
|
+
}
|
|
382
|
+
function parseMultiValueTail(args, valueOptions, repeatOptions, usage) {
|
|
383
|
+
const positionals = [];
|
|
384
|
+
const options = new Map();
|
|
385
|
+
const multiOptions = new Map();
|
|
386
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
387
|
+
const value = args[index];
|
|
388
|
+
if (!value.startsWith("--")) {
|
|
389
|
+
positionals.push(value);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (!valueOptions.has(value) && !repeatOptions.has(value)) {
|
|
393
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
394
|
+
}
|
|
395
|
+
if (repeatOptions.has(value)) {
|
|
396
|
+
const optionValue = args[index + 1];
|
|
397
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
398
|
+
throw usageError(`${value} is required.`, usage);
|
|
399
|
+
}
|
|
400
|
+
multiOptions.set(value, [...(multiOptions.get(value) ?? []), optionValue]);
|
|
401
|
+
index += 1;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (options.has(value)) {
|
|
405
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
406
|
+
}
|
|
407
|
+
const optionValue = args[index + 1];
|
|
408
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
409
|
+
throw usageError(`${value} is required.`, usage);
|
|
410
|
+
}
|
|
411
|
+
options.set(value, optionValue);
|
|
412
|
+
index += 1;
|
|
413
|
+
}
|
|
414
|
+
return { positionals, options, multiOptions };
|
|
415
|
+
}
|
package/dist/config/yuiConfig.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
export const DEFAULT_RECONCILIATION_INTERVAL_SECONDS = 120;
|
|
2
2
|
export const MIN_RECONCILIATION_INTERVAL_SECONDS = 5;
|
|
3
3
|
export const MAX_RECONCILIATION_INTERVAL_SECONDS = 300;
|
|
4
|
+
export const DEFAULT_RESOURCES_GC_MODE = "report";
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the Resource GC mode. `report` (default) only reports candidates;
|
|
7
|
+
* `quarantine` allows `yui resources gc --apply` to quarantine releasable
|
|
8
|
+
* resources.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveResourcesGcMode(value) {
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
return DEFAULT_RESOURCES_GC_MODE;
|
|
13
|
+
if (value === "report" || value === "quarantine")
|
|
14
|
+
return value;
|
|
15
|
+
throw new TypeError("resourcesGcMode must be 'report' or 'quarantine'.");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether the Controller may automatically quarantine resources for terminal
|
|
19
|
+
* Tasks. Defaults to false: automatic GC is always opt-in, and permanent
|
|
20
|
+
* deletion remains a manual, delayed step.
|
|
21
|
+
*/
|
|
22
|
+
export function resolveResourcesGcAutoQuarantine(value) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return false;
|
|
25
|
+
if (typeof value === "boolean")
|
|
26
|
+
return value;
|
|
27
|
+
throw new TypeError("resourcesGcAutoQuarantine must be a boolean.");
|
|
28
|
+
}
|
|
4
29
|
/**
|
|
5
30
|
* Resolves the durable Yui setting used for low-frequency recovery
|
|
6
31
|
* reconciliation. Normal durable state changes wake the Controller through
|
|
@@ -16,3 +41,40 @@ export function reconciliationIntervalMilliseconds(value) {
|
|
|
16
41
|
}
|
|
17
42
|
return seconds * 1_000;
|
|
18
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Issue 07 (Leader convergence) feature mode. `display` only shows the
|
|
46
|
+
* read-only next-action decision support; `warn` additionally reports
|
|
47
|
+
* duplicate deliveries and semantic-budget warnings; `enforce` hard-blocks
|
|
48
|
+
* only exact duplicates, while semantic-budget exhaustion remains a warning
|
|
49
|
+
* because it must not override Leader judgment. The mode is additive and
|
|
50
|
+
* optional — Homes without it keep the `display` default, so no config
|
|
51
|
+
* migration is required.
|
|
52
|
+
*/
|
|
53
|
+
export const LEADER_NEXT_ACTION_MODES = ["display", "warn", "enforce"];
|
|
54
|
+
export const DEFAULT_LEADER_NEXT_ACTION_MODE = "display";
|
|
55
|
+
export function resolveLeaderNextActionMode(value) {
|
|
56
|
+
if (value === undefined || value === null)
|
|
57
|
+
return DEFAULT_LEADER_NEXT_ACTION_MODE;
|
|
58
|
+
if (typeof value !== "string") {
|
|
59
|
+
throw new TypeError("leaderNextActionMode must be display, warn, or enforce.");
|
|
60
|
+
}
|
|
61
|
+
const normalized = value.trim().toLowerCase();
|
|
62
|
+
if (normalized.length === 0)
|
|
63
|
+
return DEFAULT_LEADER_NEXT_ACTION_MODE;
|
|
64
|
+
if (!LEADER_NEXT_ACTION_MODES.includes(normalized)) {
|
|
65
|
+
throw new TypeError("leaderNextActionMode must be display, warn, or enforce.");
|
|
66
|
+
}
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the effective mode. An explicit environment override
|
|
71
|
+
* (`YUI_LEADER_NEXT_ACTION_MODE`) wins over the durable config value so a
|
|
72
|
+
* single CLI invocation can be tightened or loosened without a config write.
|
|
73
|
+
*/
|
|
74
|
+
export function leaderNextActionMode(configured, env = process.env) {
|
|
75
|
+
const override = env.YUI_LEADER_NEXT_ACTION_MODE;
|
|
76
|
+
if (override !== undefined && override.trim().length > 0) {
|
|
77
|
+
return resolveLeaderNextActionMode(override);
|
|
78
|
+
}
|
|
79
|
+
return resolveLeaderNextActionMode(configured);
|
|
80
|
+
}
|
|
@@ -8,6 +8,8 @@ import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
|
|
|
8
8
|
import { assertControllerStatusIdentity } from "../runtime/exactControlPlane.js";
|
|
9
9
|
import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
|
|
10
10
|
import { YUI_VERSION, yuiVersionIdentity } from "../version.js";
|
|
11
|
+
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
12
|
+
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
11
13
|
const STARTUP_TIMEOUT_MS = 5_000;
|
|
12
14
|
// A lifecycle RPC may legitimately occupy the Controller for 30 seconds.
|
|
13
15
|
// Restart must allow that request to drain before deciding shutdown is stuck.
|
|
@@ -19,6 +21,15 @@ const CONFIGURATION_REFRESH_TIMEOUT_MS = 500;
|
|
|
19
21
|
const CONTROLLER_OPERATIONAL_ENVIRONMENT = [
|
|
20
22
|
...AGENT_OPERATIONAL_ENVIRONMENT_NAMES,
|
|
21
23
|
"YUI_TMUX_BIN",
|
|
24
|
+
// Issue 01: YUI_STORE_BACKEND/YUI_STORE_WORKER are reserved for tests and
|
|
25
|
+
// explicit recovery commands. A Controller spawned by such a command must
|
|
26
|
+
// inherit the forced backend; otherwise a layout-7 test Home silently opens
|
|
27
|
+
// SQLite, creates yui.db, and diverges from the file store the test writes.
|
|
28
|
+
"YUI_STORE_BACKEND",
|
|
29
|
+
"YUI_STORE_WORKER",
|
|
30
|
+
// rr13/test: Forward the liveness seam so an integration test's Controller
|
|
31
|
+
// subprocess does not reap a saved active Leader Run without a real tmux role.
|
|
32
|
+
"YUI_TEST_ROLE_LIVENESS_PRESENT",
|
|
22
33
|
...EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES
|
|
23
34
|
];
|
|
24
35
|
/**
|
|
@@ -255,9 +266,14 @@ function controllerSpawnEnvironment(home, source) {
|
|
|
255
266
|
// The Controller remains authoritative for reporting an invalid/unavailable
|
|
256
267
|
// home. Environment filtering must never fall back to forwarding all names.
|
|
257
268
|
}
|
|
269
|
+
// Merge with process.env so a partial source (e.g. a managed-Session env
|
|
270
|
+
// that only carries YUI_SESSION_SCOPE/YUI_TASK_ID) still forwards
|
|
271
|
+
// operational vars like YUI_STORE_BACKEND and PATH. The source wins on
|
|
272
|
+
// conflict; the whitelist still gates every forwarded name.
|
|
273
|
+
const effectiveSource = { ...process.env, ...source };
|
|
258
274
|
const environment = {};
|
|
259
275
|
for (const name of allowed) {
|
|
260
|
-
const value =
|
|
276
|
+
const value = effectiveSource[name];
|
|
261
277
|
if (value !== undefined)
|
|
262
278
|
environment[name] = value;
|
|
263
279
|
}
|
|
@@ -439,6 +455,31 @@ export class FileTaskWorkflowRuntime {
|
|
|
439
455
|
}
|
|
440
456
|
}
|
|
441
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Issue 03 archive postcondition. Re-verifies physical absence after the
|
|
460
|
+
* runtime stop and blocks archive while any owned Provider root is still
|
|
461
|
+
* live, preserving the owner records for Operator recovery.
|
|
462
|
+
*/
|
|
463
|
+
async assertTaskPhysicalResourcesReleased(taskId) {
|
|
464
|
+
const reconciliation = new SessionOwnerReconciliation({
|
|
465
|
+
home: this.home,
|
|
466
|
+
store: this.store,
|
|
467
|
+
environment: this.clientOptions.environment,
|
|
468
|
+
tmux: this.tmux
|
|
469
|
+
});
|
|
470
|
+
const records = this.store.listSessionOwners().filter((record) => (record.owner.scope === "task" && record.owner.taskId === taskId));
|
|
471
|
+
if (records.length === 0)
|
|
472
|
+
return;
|
|
473
|
+
const report = reconciliation.report();
|
|
474
|
+
const blockers = report.entries.filter((entry) => (entry.owner.scope === "task"
|
|
475
|
+
&& entry.owner.taskId === taskId
|
|
476
|
+
&& entry.archiveBlocked));
|
|
477
|
+
if (blockers.length === 0)
|
|
478
|
+
return;
|
|
479
|
+
throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task archive blocked: ${blockers.length} owned physical resource(s) still live: `
|
|
480
|
+
+ blockers.map((entry) => (`${entry.owner.roleName}/${entry.launchId}`
|
|
481
|
+
+ ` (pid ${entry.physical?.alive === true ? entry.physical.pid : "?"})`)).join("; "));
|
|
482
|
+
}
|
|
442
483
|
async stopGlobalRoleSession(roleName) {
|
|
443
484
|
if (this.store.getGlobalRole(roleName) === null) {
|
|
444
485
|
throw new Error(`Global Role not found: ${roleName}.`);
|