@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,228 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
3
|
+
import { resolveProject } from "../repository/project.js";
|
|
4
|
+
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
5
|
+
import { enqueueIntegrationQueueEntry, processIntegrationQueue, reconcileIntegrationQueueEntry, requeueIntegrationQueueEntry, supersedeIntegrationQueueEntry } from "../integration/integrationQueueService.js";
|
|
6
|
+
import { taskActor } from "./taskActor.js";
|
|
7
|
+
import { parseRepeatable } from "./taskIntegrationCommands.js";
|
|
8
|
+
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
9
|
+
/**
|
|
10
|
+
* `yui task integration queue` — the serialized merge queue. Only terminal
|
|
11
|
+
* Candidates explicitly enqueued enter the queue; the queue never blocks
|
|
12
|
+
* parallel development, it orders exact-commit integration.
|
|
13
|
+
*/
|
|
14
|
+
export async function runTaskIntegrationQueueCommand(args, store, home, options = {}) {
|
|
15
|
+
const [command, ...rest] = args;
|
|
16
|
+
if (command === "enqueue")
|
|
17
|
+
return enqueue(rest, store, options, home);
|
|
18
|
+
if (command === "list")
|
|
19
|
+
return list(rest, store);
|
|
20
|
+
if (command === "show")
|
|
21
|
+
return show(rest, store, options.environment);
|
|
22
|
+
if (command === "process")
|
|
23
|
+
return process(rest, store, home, options);
|
|
24
|
+
if (command === "supersede")
|
|
25
|
+
return supersede(rest, store, options);
|
|
26
|
+
if (command === "requeue")
|
|
27
|
+
return requeue(rest, store, options);
|
|
28
|
+
if (command === "reconcile")
|
|
29
|
+
return reconcile(rest, store, options);
|
|
30
|
+
throw usageError(command === undefined
|
|
31
|
+
? "Task Integration queue command is required."
|
|
32
|
+
: `Unknown command: task integration queue ${command}`);
|
|
33
|
+
}
|
|
34
|
+
async function enqueue(args, store, options, home) {
|
|
35
|
+
const usage = "Task Integration queue enqueue usage: yui task integration queue enqueue <task> --project <project> --change-set <id> [--target <ref>] [--check <command> ...].";
|
|
36
|
+
const parsed = parseRepeatable(args, new Set(["--check"]), new Set(["--project", "--change-set", "--target"]), usage);
|
|
37
|
+
if (parsed.positionals.length !== 1)
|
|
38
|
+
throw usageError(usage);
|
|
39
|
+
const task = requireActiveTask(store, parsed.positionals[0]);
|
|
40
|
+
requireLeader(options.environment, task.id);
|
|
41
|
+
const projectRef = parsed.one.get("--project");
|
|
42
|
+
const changeSetId = parsed.one.get("--change-set");
|
|
43
|
+
if (projectRef === undefined || changeSetId === undefined)
|
|
44
|
+
throw usageError(usage);
|
|
45
|
+
const projectId = resolveProjectId(store, projectRef);
|
|
46
|
+
const result = await enqueueIntegrationQueueEntry({
|
|
47
|
+
store,
|
|
48
|
+
taskId: task.id,
|
|
49
|
+
projectId,
|
|
50
|
+
changeSetId,
|
|
51
|
+
targetRef: parsed.one.get("--target"),
|
|
52
|
+
checkCommands: parsed.many.get("--check") ?? [],
|
|
53
|
+
now: options.now
|
|
54
|
+
});
|
|
55
|
+
const label = {
|
|
56
|
+
queued: "Enqueued",
|
|
57
|
+
"already-queued": "Already queued",
|
|
58
|
+
"already-committed": "Already committed",
|
|
59
|
+
converged: "Converged"
|
|
60
|
+
}[result.outcome];
|
|
61
|
+
return {
|
|
62
|
+
output: `${label} ${result.entry.changeSetId} as ${result.entry.id} (${result.entry.status})\n`,
|
|
63
|
+
data: result
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function list(args, store) {
|
|
67
|
+
const usage = "Task Integration queue list usage: yui task integration queue list <task> [--project <project>].";
|
|
68
|
+
const parsed = parseRepeatable(args, new Set(), new Set(["--project"]), usage);
|
|
69
|
+
if (parsed.positionals.length !== 1)
|
|
70
|
+
throw usageError(usage);
|
|
71
|
+
const task = requireActiveTask(store, parsed.positionals[0]);
|
|
72
|
+
const projectRef = parsed.one.get("--project");
|
|
73
|
+
const projectId = projectRef === undefined ? undefined : resolveProjectId(store, projectRef);
|
|
74
|
+
// The store already returns entries in numeric id order; trust it instead of
|
|
75
|
+
// re-sorting with a lexicographic compare (which yields 1, 10, 2, ...).
|
|
76
|
+
const entries = store.listIntegrationQueueEntries(task.id)
|
|
77
|
+
.filter((entry) => projectId === undefined || entry.projectId === projectId);
|
|
78
|
+
const output = entries.length === 0
|
|
79
|
+
? "Integration queue is empty.\n"
|
|
80
|
+
: `${renderTable(`Integration queue: ${task.id}`, [
|
|
81
|
+
{ header: "Entry", minWidth: 12, maxWidth: 24 },
|
|
82
|
+
{ header: "Project", minWidth: 8, maxWidth: 20 },
|
|
83
|
+
{ header: "ChangeSet", minWidth: 10, maxWidth: 20 },
|
|
84
|
+
{ header: "Target", minWidth: 8, maxWidth: 30 },
|
|
85
|
+
{ header: "Status", minWidth: 8, maxWidth: 16 }
|
|
86
|
+
], entries.map((entry) => [
|
|
87
|
+
entry.id,
|
|
88
|
+
entry.projectId,
|
|
89
|
+
entry.changeSetId,
|
|
90
|
+
entry.targetRef,
|
|
91
|
+
entry.status
|
|
92
|
+
]), defaultTableWidth())}\n`;
|
|
93
|
+
return { output, data: { entries } };
|
|
94
|
+
}
|
|
95
|
+
function show(args, store, environment) {
|
|
96
|
+
const usage = "Task Integration queue show usage: yui task integration queue show <task>/<entry>.";
|
|
97
|
+
if (args.length !== 1)
|
|
98
|
+
throw usageError(usage);
|
|
99
|
+
const entry = requireQueueEntry(store, args[0], environment);
|
|
100
|
+
const lines = [
|
|
101
|
+
`Integration queue entry: ${entry.id}`,
|
|
102
|
+
`Task: ${entry.taskId}`,
|
|
103
|
+
`Project: ${entry.projectId}`,
|
|
104
|
+
`ChangeSet: ${entry.changeSetId}`,
|
|
105
|
+
`Target: ${entry.targetRef}`,
|
|
106
|
+
`Status: ${entry.status}`,
|
|
107
|
+
`Target before: ${entry.targetBefore ?? "-"}`,
|
|
108
|
+
`Target after: ${entry.targetAfter ?? "-"}`,
|
|
109
|
+
`Integration: ${entry.integrationAttemptId ?? "-"}`,
|
|
110
|
+
`Affected paths: ${entry.affectedPaths === undefined ? "-" : entry.affectedPaths.join(", ")}`,
|
|
111
|
+
`Evidence: ${entry.evidenceRefs.length === 0 ? "-" : entry.evidenceRefs.join(", ")}`,
|
|
112
|
+
`Conflict: ${entry.conflictSummary ?? "-"}`,
|
|
113
|
+
`Superseded: ${entry.supersedeReason ?? "-"}`
|
|
114
|
+
];
|
|
115
|
+
return { output: `${lines.join("\n")}\n`, data: { entry } };
|
|
116
|
+
}
|
|
117
|
+
async function process(args, store, home, options) {
|
|
118
|
+
const usage = "Task Integration queue process usage: yui task integration queue process <task> [--project <project>] [--limit <n>].";
|
|
119
|
+
const parsed = parseRepeatable(args, new Set(), new Set(["--project", "--limit"]), usage);
|
|
120
|
+
if (parsed.positionals.length !== 1)
|
|
121
|
+
throw usageError(usage);
|
|
122
|
+
const task = requireActiveTask(store, parsed.positionals[0]);
|
|
123
|
+
requireLeader(options.environment, task.id);
|
|
124
|
+
const limitValue = parsed.one.get("--limit");
|
|
125
|
+
const limit = limitValue === undefined ? undefined : Number.parseInt(limitValue, 10);
|
|
126
|
+
if (limitValue !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
|
|
127
|
+
throw usageError(`--limit must be a positive integer: ${limitValue}.`);
|
|
128
|
+
}
|
|
129
|
+
const projectRef = parsed.one.get("--project");
|
|
130
|
+
const projectId = projectRef === undefined ? undefined : resolveProjectId(store, projectRef);
|
|
131
|
+
const processed = await processIntegrationQueue(store, home, task.id, {
|
|
132
|
+
projectId,
|
|
133
|
+
limit,
|
|
134
|
+
now: options.now,
|
|
135
|
+
environment: options.environment
|
|
136
|
+
});
|
|
137
|
+
const lines = processed.map(({ entry, result }) => {
|
|
138
|
+
if (result.status === "committed") {
|
|
139
|
+
return `${entry.id} committed ${entry.changeSetId} -> ${entry.targetAfter ?? "-"}`;
|
|
140
|
+
}
|
|
141
|
+
if (result.status === "blocked") {
|
|
142
|
+
return `${entry.id} conflicted ${entry.changeSetId}: ${entry.conflictSummary ?? "blocked"}`;
|
|
143
|
+
}
|
|
144
|
+
return `${entry.id} conflicted ${entry.changeSetId}: ${entry.conflictSummary ?? "gate failure"}`;
|
|
145
|
+
});
|
|
146
|
+
const output = processed.length === 0
|
|
147
|
+
? "Integration queue: nothing to process.\n"
|
|
148
|
+
: `${lines.join("\n")}\n`;
|
|
149
|
+
return { output, data: { processed } };
|
|
150
|
+
}
|
|
151
|
+
function supersede(args, store, options) {
|
|
152
|
+
const usage = "Task Integration queue supersede usage: yui task integration queue supersede <task>/<entry> --reason <text>.";
|
|
153
|
+
const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
|
|
154
|
+
if (parsed.positionals.length !== 1)
|
|
155
|
+
throw usageError(usage);
|
|
156
|
+
const entry = requireQueueEntry(store, parsed.positionals[0], options.environment);
|
|
157
|
+
requireLeader(options.environment, entry.taskId);
|
|
158
|
+
const reason = parsed.one.get("--reason");
|
|
159
|
+
if (reason === undefined)
|
|
160
|
+
throw usageError(usage);
|
|
161
|
+
const superseded = supersedeIntegrationQueueEntry(store, entry.taskId, entry.id, reason, options.now ?? (() => new Date()));
|
|
162
|
+
return {
|
|
163
|
+
output: `Superseded ${superseded.id}: ${reason}\n`,
|
|
164
|
+
data: { entry: superseded }
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function requeue(args, store, options) {
|
|
168
|
+
const usage = "Task Integration queue requeue usage: yui task integration queue requeue <task>/<entry>.";
|
|
169
|
+
if (args.length !== 1)
|
|
170
|
+
throw usageError(usage);
|
|
171
|
+
const entry = requireQueueEntry(store, args[0], options.environment);
|
|
172
|
+
requireLeader(options.environment, entry.taskId);
|
|
173
|
+
const waiting = requeueIntegrationQueueEntry(store, entry.taskId, entry.id, options.now ?? (() => new Date()));
|
|
174
|
+
return {
|
|
175
|
+
output: `Requeued ${waiting.id}; it will be processed again on the next queue run\n`,
|
|
176
|
+
data: { entry: waiting }
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function reconcile(args, store, options) {
|
|
180
|
+
const usage = "Task Integration queue reconcile usage: yui task integration queue reconcile <task>/<entry>.";
|
|
181
|
+
if (args.length !== 1)
|
|
182
|
+
throw usageError(usage);
|
|
183
|
+
const entry = requireQueueEntry(store, args[0], options.environment);
|
|
184
|
+
requireLeader(options.environment, entry.taskId);
|
|
185
|
+
const committed = await reconcileIntegrationQueueEntry(store, entry.taskId, entry.id, new NodeGitWorkspace(), options.now ?? (() => new Date()));
|
|
186
|
+
return {
|
|
187
|
+
output: `Reconciled ${committed.id} as committed -> ${committed.targetAfter ?? "-"}\n`,
|
|
188
|
+
data: { entry: committed }
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function requireActiveTask(store, taskId) {
|
|
192
|
+
const task = store.getTask(taskId);
|
|
193
|
+
if (task === null)
|
|
194
|
+
throw usageError(`Task not found: ${taskId}.`);
|
|
195
|
+
if (task.status !== "active") {
|
|
196
|
+
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
197
|
+
}
|
|
198
|
+
return task;
|
|
199
|
+
}
|
|
200
|
+
function resolveProjectId(store, reference) {
|
|
201
|
+
const project = resolveProject(store.listProjects(), reference);
|
|
202
|
+
if (project === null)
|
|
203
|
+
throw usageError(`Project not found: ${reference}.`);
|
|
204
|
+
return project.id;
|
|
205
|
+
}
|
|
206
|
+
function requireQueueEntry(store, value, environment) {
|
|
207
|
+
let reference;
|
|
208
|
+
try {
|
|
209
|
+
reference = resolveTaskRecordReference(value, {
|
|
210
|
+
kind: "integrationQueue",
|
|
211
|
+
contextTaskId: environment?.YUI_TASK_ID,
|
|
212
|
+
label: "Integration queue entry"
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
217
|
+
}
|
|
218
|
+
const entry = store.getIntegrationQueueEntry(reference.taskId, reference.localId);
|
|
219
|
+
if (entry === null) {
|
|
220
|
+
throw usageError(`Integration queue entry not found: ${reference.taskId}/${reference.localId}.`);
|
|
221
|
+
}
|
|
222
|
+
return entry;
|
|
223
|
+
}
|
|
224
|
+
function requireLeader(environment, taskId) {
|
|
225
|
+
if (taskActor(environment, taskId) !== "leader") {
|
|
226
|
+
throw usageError("Only the Task Leader can change the integration queue.");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { taskNotFound, usageError } from "../errors/cliError.js";
|
|
2
|
+
import { projectNextAction } from "../task/nextAction.js";
|
|
3
|
+
import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
|
|
4
|
+
/**
|
|
5
|
+
* Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
|
|
6
|
+
* Folds the existing durable records into exactly one protocol-level next
|
|
7
|
+
* action with exact refs, preconditions, recommended command, alternatives, and
|
|
8
|
+
* the judgment that remains owned by the Leader.
|
|
9
|
+
* The command never mutates state; when the action is `route-review-findings`
|
|
10
|
+
* it also prints the minimal repair wave for the failing Review.
|
|
11
|
+
*/
|
|
12
|
+
export function runTaskNextActionCommand(args, store) {
|
|
13
|
+
const usage = "Task next-action usage: yui task next-action <task> [--json].";
|
|
14
|
+
const positionals = args.filter((arg) => arg !== "--json");
|
|
15
|
+
const asJson = args.includes("--json");
|
|
16
|
+
if (positionals.length !== 1 || positionals[0]?.trim().length === 0) {
|
|
17
|
+
throw usageError(usage);
|
|
18
|
+
}
|
|
19
|
+
const taskId = positionals[0].trim();
|
|
20
|
+
const data = store.transaction((reader) => {
|
|
21
|
+
const facts = reader.readNextActionFacts(taskId);
|
|
22
|
+
if (facts === null)
|
|
23
|
+
throw taskNotFound(taskId);
|
|
24
|
+
const action = projectNextAction(facts);
|
|
25
|
+
const repairWave = repairWaveFor(action, facts);
|
|
26
|
+
return { action, repairWave };
|
|
27
|
+
});
|
|
28
|
+
if (asJson) {
|
|
29
|
+
return {
|
|
30
|
+
kind: "output",
|
|
31
|
+
output: `${JSON.stringify(data, null, 2)}\n`,
|
|
32
|
+
data
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
kind: "output",
|
|
37
|
+
output: renderNextAction(data.action, data.repairWave),
|
|
38
|
+
data
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function repairWaveFor(action, facts) {
|
|
42
|
+
if (action.kind !== "route-review-findings")
|
|
43
|
+
return null;
|
|
44
|
+
const reviewRef = action.refs.find((ref) => ref.kind === "review-round");
|
|
45
|
+
if (reviewRef === undefined)
|
|
46
|
+
return null;
|
|
47
|
+
const round = facts.reviewRounds.find((candidate) => candidate.id === reviewRef.id);
|
|
48
|
+
if (round === undefined)
|
|
49
|
+
return null;
|
|
50
|
+
const findings = extractReviewFindings(round);
|
|
51
|
+
if (findings.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
return planRepairWave(round.id, findings);
|
|
54
|
+
}
|
|
55
|
+
function renderNextAction(action, repairWave) {
|
|
56
|
+
const lines = [
|
|
57
|
+
`Task: ${action.taskId}`,
|
|
58
|
+
`Next action: ${action.kind}`,
|
|
59
|
+
`Reason: ${action.reason}`,
|
|
60
|
+
...(action.refs.length === 0
|
|
61
|
+
? ["Refs: none"]
|
|
62
|
+
: ["Refs:", ...action.refs.map((ref) => ` ${ref.kind}: ${ref.id}`)]),
|
|
63
|
+
"Preconditions:",
|
|
64
|
+
...action.preconditions.map((precondition) => {
|
|
65
|
+
const ref = precondition.ref === undefined
|
|
66
|
+
? ""
|
|
67
|
+
: ` (${precondition.ref.kind} ${precondition.ref.id})`;
|
|
68
|
+
return ` ${precondition.satisfied ? "[x]" : "[ ]"} ${precondition.fact}${ref}`;
|
|
69
|
+
}),
|
|
70
|
+
...(action.recommendedCommand === undefined
|
|
71
|
+
? []
|
|
72
|
+
: [`Recommended: ${action.recommendedCommand}`]),
|
|
73
|
+
...(action.judgmentRequired === undefined
|
|
74
|
+
? []
|
|
75
|
+
: [`Judgment: ${action.judgmentRequired}`]),
|
|
76
|
+
...(action.alternatives === undefined || action.alternatives.length === 0
|
|
77
|
+
? []
|
|
78
|
+
: [
|
|
79
|
+
"Alternatives:",
|
|
80
|
+
...action.alternatives.map((alternative) => {
|
|
81
|
+
const command = alternative.recommendedCommand === undefined
|
|
82
|
+
? ""
|
|
83
|
+
: ` — ${alternative.recommendedCommand}`;
|
|
84
|
+
return ` ${alternative.kind}: ${alternative.reason}${command}`;
|
|
85
|
+
})
|
|
86
|
+
]),
|
|
87
|
+
...(action.conflicts === undefined || action.conflicts.length === 0
|
|
88
|
+
? []
|
|
89
|
+
: [
|
|
90
|
+
"Conflicts:",
|
|
91
|
+
...action.conflicts.map((conflict) => ` ${conflict.kind}: ${conflict.id}`)
|
|
92
|
+
])
|
|
93
|
+
];
|
|
94
|
+
if (repairWave !== null) {
|
|
95
|
+
lines.push(`Repair wave (${repairWave.openFindingCount} open finding(s), ${repairWave.groups.length} group(s)):`, ...repairWave.groups.map((group) => ` ${group.id}: findings ${group.findingIds.join(", ")}`
|
|
96
|
+
+ ` — paths ${group.paths.length === 0 ? "none" : group.paths.join(", ")}`
|
|
97
|
+
+ ` — ${group.reason}`));
|
|
98
|
+
}
|
|
99
|
+
return `${lines.join("\n")}\n`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { diagnoseOverlap, overlapSubjectFromChangeSet, overlapSubjectKey } from "../integration/overlapDiagnostics.js";
|
|
3
|
+
import { resolveProject } from "../repository/project.js";
|
|
4
|
+
/**
|
|
5
|
+
* Read-only cross-Task overlap diagnostics. Gathers the latest ChangeSet
|
|
6
|
+
* per (Task, WorkItem, Project) and reports semantic overlap, a suggested
|
|
7
|
+
* integration order, and semantic review areas. Never mutates state.
|
|
8
|
+
*/
|
|
9
|
+
export async function runTaskOverlapCommand(args, store) {
|
|
10
|
+
const parsed = parseOverlapArgs(args);
|
|
11
|
+
const projectId = resolveProjectFilter(store, parsed.project);
|
|
12
|
+
const tasks = store.listTasks()
|
|
13
|
+
.filter((task) => parsed.tasks.size === 0 || parsed.tasks.has(task.id));
|
|
14
|
+
const latest = new Map();
|
|
15
|
+
for (const task of tasks) {
|
|
16
|
+
for (const changeSet of store.listChangeSets(task.id)) {
|
|
17
|
+
if (projectId !== undefined && changeSet.projectId !== projectId)
|
|
18
|
+
continue;
|
|
19
|
+
if (parsed.base !== undefined && changeSet.baseCommit !== parsed.base)
|
|
20
|
+
continue;
|
|
21
|
+
const key = `${changeSet.taskId}/${changeSet.workItemId}/${changeSet.projectId}`;
|
|
22
|
+
const current = latest.get(key);
|
|
23
|
+
if (current === undefined || isNewer(changeSet, current)) {
|
|
24
|
+
latest.set(key, changeSet);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const subjects = [...latest.values()].map(overlapSubjectFromChangeSet);
|
|
29
|
+
const report = diagnoseOverlap(subjects);
|
|
30
|
+
return {
|
|
31
|
+
output: renderOverlapReport(report.findings, report.suggestedOrder, report.reviewAreas, subjects),
|
|
32
|
+
data: { report }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function isNewer(candidate, current) {
|
|
36
|
+
if (candidate.createdAt !== current.createdAt) {
|
|
37
|
+
return candidate.createdAt > current.createdAt;
|
|
38
|
+
}
|
|
39
|
+
return candidate.id.localeCompare(current.id) > 0;
|
|
40
|
+
}
|
|
41
|
+
function resolveProjectFilter(store, reference) {
|
|
42
|
+
if (reference === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
const project = resolveProject(store.listProjects(), reference);
|
|
45
|
+
if (project === null)
|
|
46
|
+
throw usageError(`Project not found: ${reference}.`);
|
|
47
|
+
return project.id;
|
|
48
|
+
}
|
|
49
|
+
function parseOverlapArgs(args) {
|
|
50
|
+
let project;
|
|
51
|
+
let base;
|
|
52
|
+
const tasks = new Set();
|
|
53
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
54
|
+
const arg = args[index];
|
|
55
|
+
if (arg === "--project") {
|
|
56
|
+
project = requireValue(args, index, "--project");
|
|
57
|
+
index += 1;
|
|
58
|
+
}
|
|
59
|
+
else if (arg === "--base") {
|
|
60
|
+
base = requireValue(args, index, "--base");
|
|
61
|
+
index += 1;
|
|
62
|
+
}
|
|
63
|
+
else if (arg === "--task") {
|
|
64
|
+
tasks.add(requireValue(args, index, "--task"));
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
throw usageError("Task overlap usage: yui task overlap [--project <project>] [--base <sha>] [--task <task>] ...");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { project, base, tasks };
|
|
72
|
+
}
|
|
73
|
+
function requireValue(args, index, option) {
|
|
74
|
+
const value = args[index + 1];
|
|
75
|
+
if (value === undefined || value.startsWith("--")) {
|
|
76
|
+
throw usageError(`Task overlap option ${option} requires a value.`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function renderOverlapReport(findings, suggestedOrder, reviewAreas, subjects) {
|
|
81
|
+
const lines = [
|
|
82
|
+
`Overlap: ${subjects.length} subjects, ${findings.length} findings`
|
|
83
|
+
];
|
|
84
|
+
if (subjects.length === 0) {
|
|
85
|
+
lines.push("No ChangeSets match the given filters.");
|
|
86
|
+
return `${lines.join("\n")}\n`;
|
|
87
|
+
}
|
|
88
|
+
lines.push("Subjects:");
|
|
89
|
+
for (const subject of subjects) {
|
|
90
|
+
const tags = subject.manifestTags.length === 0 ? "-" : subject.manifestTags.join(",");
|
|
91
|
+
lines.push(` ${subject.changeSetId} ${subject.taskId}/${subject.workItemId} ${subject.projectId} `
|
|
92
|
+
+ `base=${subject.baseCommit.slice(0, 12)} head=${subject.headCommit.slice(0, 12)} tags=${tags}`);
|
|
93
|
+
}
|
|
94
|
+
if (findings.length === 0) {
|
|
95
|
+
lines.push("No semantic overlap detected.");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
lines.push("Findings:");
|
|
99
|
+
for (const finding of findings) {
|
|
100
|
+
lines.push(` [${finding.risk}] ${finding.kind}: ${finding.changeSetIds.join(" vs ")}`);
|
|
101
|
+
for (const path of finding.paths)
|
|
102
|
+
lines.push(` ${path}`);
|
|
103
|
+
lines.push(` ${finding.detail}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push("Suggested integration order:");
|
|
107
|
+
suggestedOrder.forEach((ref, index) => {
|
|
108
|
+
const subject = subjects.find((candidate) => overlapSubjectKey(candidate) === ref);
|
|
109
|
+
lines.push(` ${index + 1}. ${ref}${subject === undefined ? "" : ` (${subject.workItemId})`}`);
|
|
110
|
+
});
|
|
111
|
+
if (reviewAreas.length === 0) {
|
|
112
|
+
lines.push("Review areas: none (no high/medium findings)");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
lines.push("Review areas:");
|
|
116
|
+
for (const area of reviewAreas)
|
|
117
|
+
lines.push(` - ${area}`);
|
|
118
|
+
}
|
|
119
|
+
return `${lines.join("\n")}\n`;
|
|
120
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { usageError } from "../errors/cliError.js";
|
|
2
2
|
import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
|
|
3
3
|
import { formatTimestamp } from "../output/timePresentation.js";
|
|
4
|
+
import { pendingWakeupProjection } from "../storage/taskStore.js";
|
|
4
5
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
5
|
-
import {
|
|
6
|
+
import { projectTaskExecutionFromFacts } from "../scheduler/taskExecutionProjection.js";
|
|
6
7
|
export function parseTaskListOptions(args) {
|
|
7
8
|
const allowed = new Set(["--all", "--verbose"]);
|
|
8
9
|
if (args.some((argument) => !allowed.has(argument))
|
|
@@ -53,15 +54,27 @@ export function renderTaskOverview(result, options, timeZone, width = defaultTab
|
|
|
53
54
|
}
|
|
54
55
|
function buildTaskOverviewEntry(task, store) {
|
|
55
56
|
const brief = store.getTaskBrief(task.id);
|
|
56
|
-
const
|
|
57
|
+
const roles = store.listRoles(task.id);
|
|
58
|
+
const leaderRole = roles.find((role) => role.name === "leader") ?? null;
|
|
57
59
|
const workItems = store.listWorkItems(task.id);
|
|
58
60
|
const inputRequests = store.listInputRequests(task.id);
|
|
59
61
|
const openInputRequests = inputRequests.filter((request) => request.status === "open");
|
|
60
62
|
const agentRuns = store.listAgentRuns(task.id);
|
|
61
63
|
const events = store.listEvents(task.id);
|
|
62
|
-
const
|
|
64
|
+
const leaderFailure = store.getLeaderFailure(task.id);
|
|
65
|
+
const operatorNotification = store.getOperatorNotification(task.id);
|
|
66
|
+
const leaderMailbox = store.getWorkMailbox({ kind: "role", taskId: task.id, roleName: "leader" });
|
|
67
|
+
const pendingWakeup = pendingWakeupProjection(leaderMailbox);
|
|
68
|
+
const reviewRounds = store.listReviewRounds(task.id);
|
|
69
|
+
const changeSets = store.listChangeSets(task.id);
|
|
70
|
+
const integrations = store.listIntegrationAttempts(task.id);
|
|
71
|
+
const roleSessions = roles.flatMap((role) => {
|
|
72
|
+
const session = store.getRoleSession(task.id, role.name);
|
|
73
|
+
return session === null ? [] : [{ roleName: role.name, ...session }];
|
|
74
|
+
});
|
|
75
|
+
const attention = collectAttention(task, agentRuns, events, leaderFailure, operatorNotification);
|
|
63
76
|
const blockers = collectBlockers(workItems, openInputRequests, attention);
|
|
64
|
-
const next = deriveNextAction(task, brief, workItems, openInputRequests, blockers,
|
|
77
|
+
const next = deriveNextAction(task, brief, workItems, openInputRequests, blockers, pendingWakeup);
|
|
65
78
|
const leader = {
|
|
66
79
|
role: "leader",
|
|
67
80
|
roleStatus: leaderRole?.status ?? "missing",
|
|
@@ -88,10 +101,25 @@ function buildTaskOverviewEntry(task, store) {
|
|
|
88
101
|
activeRunCount: runtimeRuns.length,
|
|
89
102
|
pendingDeliveryCount: runtimeRuns.filter((run) => run.delivery === "pending").length
|
|
90
103
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
// Fold the execution projection from the facts already read above instead of
|
|
105
|
+
// reading the store a second time for the same unchanged revision.
|
|
106
|
+
const execution = projectTaskExecutionFromFacts({
|
|
107
|
+
task,
|
|
108
|
+
roles,
|
|
109
|
+
runs: agentRuns,
|
|
110
|
+
workItems,
|
|
111
|
+
inputRequests,
|
|
112
|
+
reviewRounds,
|
|
113
|
+
changeSets,
|
|
114
|
+
integrations,
|
|
115
|
+
events,
|
|
116
|
+
brief,
|
|
117
|
+
pendingWakeup,
|
|
118
|
+
leaderMailbox,
|
|
119
|
+
leaderFailure,
|
|
120
|
+
operatorNotification,
|
|
121
|
+
roleSessions
|
|
122
|
+
});
|
|
95
123
|
return {
|
|
96
124
|
...task,
|
|
97
125
|
brief,
|