@zq-silk/yui 0.10.1 → 0.11.1
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 +53 -0
- package/dist/cli/commandCatalog.js +28 -7
- package/dist/cli.js +50 -47
- package/dist/commands/projectCommands.js +69 -6
- package/dist/commands/taskCommands.js +639 -89
- package/dist/commands/taskContextCommand.js +78 -27
- package/dist/commands/taskNextActionCommand.js +13 -2
- package/dist/commands/taskOverviewCommand.js +21 -5
- package/dist/commands/taskUpstreamCommands.js +136 -0
- package/dist/context/runContextPack.js +184 -17
- package/dist/controller/agentRuntimeObserver.js +31 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
- package/dist/execution/candidateConvergence.js +623 -0
- package/dist/execution/executionGroup.js +255 -13
- package/dist/execution/executionHealth.js +324 -0
- package/dist/execution/resourceBroker.js +425 -0
- package/dist/executor/fileRoleLaunchPlanner.js +6 -9
- package/dist/executor/workspacePreflightClassification.js +117 -0
- package/dist/lifecycle/exactRunTerminalization.js +13 -2
- package/dist/lifecycle/taskRoleSessionReset.js +4 -2
- package/dist/repository/taskBaseFreshness.js +26 -1
- package/dist/repository/taskWorkspacePreparer.js +17 -1
- package/dist/review/reviewRound.js +27 -6
- package/dist/run/agentRun.js +2 -2
- package/dist/run/recoveryProjection.js +15 -0
- package/dist/runtime/runtimeContinuationProjection.js +7 -0
- package/dist/scheduler/actionability.js +169 -3
- package/dist/scheduler/activeTaskProgress.js +15 -10
- package/dist/scheduler/leaderWakeupProcessor.js +17 -1
- package/dist/scheduler/taskExecutionProjection.js +105 -8
- package/dist/scheduler/taskObservabilityProjection.js +282 -0
- package/dist/storage/migration/productionRegistry.js +14 -0
- package/dist/storage/sqliteStore.js +12 -0
- package/dist/storage/taskStore.js +1 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +7 -3
- package/dist/task/completionReadiness.js +1 -1
- package/dist/task/nextAction.js +314 -2
- package/dist/web/assets/client/components.js +116 -0
- package/dist/web/assets/client/i18n.js +66 -0
- package/dist/web/assets/client/view.js +15 -0
- package/dist/web/assets/styles/cards.js +23 -0
- package/dist/web/assets/styles/responsive.js +2 -0
- package/dist/web/webSnapshot.js +8 -2
- package/dist/workItem/workItem.js +262 -5
- package/i18n/README.zh-CN.md +42 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { inspectTaskRoleSessionRecovery } from "./taskRoleRuntimeStatus.js";
|
|
|
8
8
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
9
9
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
10
10
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
11
|
+
import { projectExecutionLaneRunRecoveries } from "../run/recoveryProjection.js";
|
|
11
12
|
const RECENT_RECORD_LIMIT = 5;
|
|
12
13
|
const RELATED_RECORD_LIMIT = 5;
|
|
13
14
|
const SUMMARY_TEXT_LIMIT = 400;
|
|
@@ -21,6 +22,7 @@ export function runTaskContextCommand(args, store) {
|
|
|
21
22
|
throw usageError("Task context usage: yui task context <task>.");
|
|
22
23
|
}
|
|
23
24
|
const taskId = args[0].trim();
|
|
25
|
+
const now = new Date();
|
|
24
26
|
const data = store.transaction((reader) => {
|
|
25
27
|
const task = reader.getTask(taskId);
|
|
26
28
|
if (task === null)
|
|
@@ -28,7 +30,7 @@ export function runTaskContextCommand(args, store) {
|
|
|
28
30
|
const workItems = reader.listWorkItems(task.id);
|
|
29
31
|
const inputRequests = reader.listInputRequests(task.id);
|
|
30
32
|
const roles = reader.listRoles(task.id);
|
|
31
|
-
const execution = buildTaskExecutionProjection(reader, task.id, task);
|
|
33
|
+
const execution = buildTaskExecutionProjection(reader, task.id, task, now);
|
|
32
34
|
if (execution === null) {
|
|
33
35
|
throw new Error(`Task execution projection disappeared: ${task.id}.`);
|
|
34
36
|
}
|
|
@@ -74,7 +76,11 @@ export function runTaskContextCommand(args, store) {
|
|
|
74
76
|
openInputRequests: inputRequests.filter((request) => request.status === "open"),
|
|
75
77
|
resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
|
|
76
78
|
events,
|
|
77
|
-
nextAction: projectNextAction(
|
|
79
|
+
nextAction: projectNextAction({
|
|
80
|
+
...nextActionFacts,
|
|
81
|
+
executionGroups: execution.executionGroups,
|
|
82
|
+
runRecoveries: projectExecutionLaneRunRecoveries(reader, task.id, execution.executionGroups)
|
|
83
|
+
})
|
|
78
84
|
};
|
|
79
85
|
});
|
|
80
86
|
const { task, execution, reviewConfig, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, publications, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
|
|
@@ -86,6 +92,11 @@ export function runTaskContextCommand(args, store) {
|
|
|
86
92
|
const displayedExecutionCarriers = execution.monitoring === "active"
|
|
87
93
|
? execution.activeRuns
|
|
88
94
|
: [];
|
|
95
|
+
const executionGroupsById = new Map(execution.executionGroups.map((group) => [
|
|
96
|
+
group.groupId,
|
|
97
|
+
group
|
|
98
|
+
]));
|
|
99
|
+
const observability = execution.observability;
|
|
89
100
|
const lines = [
|
|
90
101
|
`Task context: ${task.id}`,
|
|
91
102
|
`Title: ${compactText(task.title)}`,
|
|
@@ -128,6 +139,10 @@ export function runTaskContextCommand(args, store) {
|
|
|
128
139
|
...(displayedExecutionCarriers.length === 0
|
|
129
140
|
? [" None."]
|
|
130
141
|
: displayedExecutionCarriers.map((run) => (` ${run.roleName}: ${run.id} [${run.status}; ${run.delivered ? "accepted" : "delivery-pending"}]`))),
|
|
142
|
+
"Observability:",
|
|
143
|
+
` DAG: ${observability.dag.nodes.length} node(s), ${observability.dag.edges.length} edge(s); ready=${observability.dag.readyIds.join(", ") || "none"}; blocked=${observability.dag.blockedIds.join(", ") || "none"}`,
|
|
144
|
+
` Cost: tokens=${resourceUsageLabel(observability.cost.tokens, undefined, observability.cost.tokensObservable)}; tools=${resourceUsageLabel(observability.cost.toolCalls, undefined, observability.cost.toolCallsObservable)}; wall=${observability.cost.wallClockSeconds}s; lanes=${observability.cost.laneCount}; groups=${observability.cost.groupCount}; retries=${observability.cost.retryCount}; marginal-value=unavailable`,
|
|
145
|
+
` Context: snapshots=${observability.context.snapshotCount}; bytes=${observability.context.totalBytes === null ? "partial" : observability.context.totalBytes}; peak-input=${observability.context.observedInputPeakTokens}; compression=unavailable`,
|
|
131
146
|
...(task.projectBindings.length === 0
|
|
132
147
|
? []
|
|
133
148
|
: [
|
|
@@ -232,9 +247,12 @@ export function runTaskContextCommand(args, store) {
|
|
|
232
247
|
` Writable Projects: ${item.writeProjectIds.length === 0
|
|
233
248
|
? "none"
|
|
234
249
|
: item.writeProjectIds.join(", ")}`,
|
|
250
|
+
...(observability.workItems.find(({ workItemId }) => workItemId === item.id) === undefined
|
|
251
|
+
? []
|
|
252
|
+
: [renderWorkItemObservability(observability.workItems.find(({ workItemId }) => workItemId === item.id))]),
|
|
235
253
|
...(currentWorkItemExecutionGroup(item) === undefined
|
|
236
254
|
? []
|
|
237
|
-
: renderExecutionGroup(currentWorkItemExecutionGroup(item))),
|
|
255
|
+
: renderExecutionGroup(currentWorkItemExecutionGroup(item), executionGroupsById.get(currentWorkItemExecutionGroup(item).id))),
|
|
238
256
|
...(item.acceptance.length === 0
|
|
239
257
|
? []
|
|
240
258
|
: [` Acceptance: ${item.acceptance.map(compactText).join("; ")}`]),
|
|
@@ -260,12 +278,12 @@ export function runTaskContextCommand(args, store) {
|
|
|
260
278
|
? []
|
|
261
279
|
: [` Summary: ${compactText(latestRun.summary)}`])
|
|
262
280
|
]),
|
|
263
|
-
...renderReviewRounds(reviewRounds.filter((round) => round.workItemId === item.id))
|
|
281
|
+
...renderReviewRounds(reviewRounds.filter((round) => round.workItemId === item.id), executionGroupsById)
|
|
264
282
|
];
|
|
265
283
|
})),
|
|
266
284
|
"",
|
|
267
285
|
"Task-final reviews:",
|
|
268
|
-
...renderReviewRounds(reviewRounds.filter((round) => (round.scope ?? "work-item") === "task")),
|
|
286
|
+
...renderReviewRounds(reviewRounds.filter((round) => (round.scope ?? "work-item") === "task"), executionGroupsById),
|
|
269
287
|
"",
|
|
270
288
|
...recentSection("AgentRuns", agentRuns, (run) => [
|
|
271
289
|
` ${run.id} [${run.status}/${run.purpose}] ${run.roleName} via ${run.effective.agentId}/${run.effective.adapterId}`,
|
|
@@ -362,7 +380,7 @@ function latestStallKind(events, runId) {
|
|
|
362
380
|
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
|
|
363
381
|
return event?.payload.kind ?? "workflow-not-progressing";
|
|
364
382
|
}
|
|
365
|
-
function renderReviewRounds(rounds) {
|
|
383
|
+
function renderReviewRounds(rounds, executionGroupsById = new Map()) {
|
|
366
384
|
const latest = rounds.at(-1);
|
|
367
385
|
if (latest === undefined)
|
|
368
386
|
return [" ReviewRounds: none."];
|
|
@@ -406,7 +424,7 @@ function renderReviewRounds(rounds) {
|
|
|
406
424
|
: [` Review summary: ${compactText(latest.summary)}`]),
|
|
407
425
|
...(latest.executionGroup === undefined
|
|
408
426
|
? []
|
|
409
|
-
: renderExecutionGroup(latest.executionGroup).map((line) => ` ${line.trimStart()}`))
|
|
427
|
+
: renderExecutionGroup(latest.executionGroup, executionGroupsById.get(latest.executionGroup.id)).map((line) => ` ${line.trimStart()}`))
|
|
410
428
|
];
|
|
411
429
|
}
|
|
412
430
|
function renderResolvedInputRequest(request, timeZone) {
|
|
@@ -499,27 +517,51 @@ function managedWorkspaceLabel(workspace) {
|
|
|
499
517
|
return `execution-lane ${workspace.owner.executionGroupId}/${workspace.owner.executionLaneId}`;
|
|
500
518
|
}
|
|
501
519
|
}
|
|
502
|
-
function renderExecutionGroup(group) {
|
|
503
|
-
const summary =
|
|
520
|
+
function renderExecutionGroup(group, projected) {
|
|
521
|
+
const summary = projected
|
|
522
|
+
?? summarizeExecutionGroup(group);
|
|
523
|
+
const health = projected?.health;
|
|
524
|
+
const resources = projected?.resources;
|
|
504
525
|
return [
|
|
505
|
-
` Execution Group ${summary.groupId} [${summary.purpose}/${summary.strategy.mode}]: ${summary.activeLaneCount} active / ${summary.terminalLaneCount} terminal; ${summary.failedLaneCount} failed`,
|
|
506
|
-
...
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
?
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
526
|
+
` Execution Group ${summary.groupId} [${summary.purpose}/${summary.strategy.mode}]: ${summary.activeLaneCount} active / ${summary.terminalLaneCount} terminal; ${summary.failedLaneCount} failed; ${summary.skippedLaneCount} skipped`,
|
|
527
|
+
...(health === undefined
|
|
528
|
+
? []
|
|
529
|
+
: [
|
|
530
|
+
` Health: active=${health.activeLaneCount}, silent=${health.silentLaneCount}, suspected-stalled=${health.suspectedStalledLaneCount}, confirmed-dead=${health.confirmedDeadLaneCount}`,
|
|
531
|
+
` Recovery: reusable=${health.reusableLaneIds.length}, retryable=${health.retryableLaneIds.length}`
|
|
532
|
+
]),
|
|
533
|
+
...(resources === undefined
|
|
534
|
+
? []
|
|
535
|
+
: [
|
|
536
|
+
` Resources: tokens=${resourceUsageLabel(resources.tokens, resources.tokensRemaining, resources.tokensObservable)}; tools=${resourceUsageLabel(resources.toolCalls, resources.toolCallsRemaining, resources.toolCallsObservable)}; wall=${resources.wallClockSeconds}s${resources.wallClockSecondsRemaining === undefined ? "" : `, remaining=${resources.wallClockSecondsRemaining}s`}`,
|
|
537
|
+
` Completion: usable=${resources.usableLaneCount}/${group.stage?.resources?.quorum ?? "legacy"}; quorum=${resources.quorumMet ? "met" : "open"}; deadline=${resources.deadlineReached ? "reached" : "open"}; budgets=${resources.exhaustedBudgets.join(",") || "open"}; queued=${resources.pendingLaneIds.length}; stragglers=${resources.stragglerLaneIds.length}`
|
|
538
|
+
]),
|
|
539
|
+
...summary.laneSummaries.flatMap((lane) => {
|
|
540
|
+
const laneHealth = projected?.laneSummaries.find(({ laneId }) => laneId === lane.laneId);
|
|
541
|
+
return [
|
|
542
|
+
` Lane ${lane.laneId} (#${lane.ordinal}, ${lane.roleName}${lane.runId === undefined ? "" : `, run ${lane.runId}`}) [${lane.status}${laneHealth?.runtimeHealth === undefined ? "" : `/${laneHealth.runtimeHealth}`}]${lane.summary === undefined ? "" : `: ${compactText(lane.summary)}`}`,
|
|
543
|
+
...(lane.effective === undefined
|
|
544
|
+
? []
|
|
545
|
+
: [` Config: ${lane.effective.adapterId}/${lane.effective.model ?? "default"}/${lane.effective.effort ?? "default"}; profile=${lane.effective.profileAccess}`]),
|
|
546
|
+
...(laneHealth !== undefined && laneHealth.recovery !== "none"
|
|
547
|
+
? [` Recovery: ${laneHealth.recovery}; ${compactText(laneHealth.reason)}`]
|
|
548
|
+
: []),
|
|
549
|
+
...(lane.report === undefined ? [] : [` Report: ${compactText(lane.report)}`]),
|
|
550
|
+
...(lane.checks === undefined || lane.checks.length === 0
|
|
551
|
+
? []
|
|
552
|
+
: [` Checks: ${lane.checks.map(({ name, outcome }) => `${name}:${outcome}`).join(", ")}`]),
|
|
553
|
+
...(lane.findings === undefined || lane.findings.length === 0
|
|
554
|
+
? []
|
|
555
|
+
: [` Findings: ${lane.findings.map(({ id, severity, status }) => `${id}:${severity}/${status}`).join(", ")}`]),
|
|
556
|
+
...(lane.evidence === undefined || lane.evidence.length === 0
|
|
557
|
+
? []
|
|
558
|
+
: [` Evidence: ${lane.evidence.length} item(s)`]),
|
|
559
|
+
...(lane.evidenceCommit === undefined
|
|
560
|
+
? []
|
|
561
|
+
: [` Evidence commit: ${lane.evidenceCommit}`]),
|
|
562
|
+
...(lane.decision === undefined ? [] : [` Decision: ${lane.decision}`])
|
|
563
|
+
];
|
|
564
|
+
}),
|
|
523
565
|
...(summary.openHighPriorityFindingIds.length === 0
|
|
524
566
|
? []
|
|
525
567
|
: [` Open high findings: ${summary.openHighPriorityFindingIds.join(", ")}`]),
|
|
@@ -528,3 +570,12 @@ function renderExecutionGroup(group) {
|
|
|
528
570
|
: [` Resolution: ${summary.resolution.decision} — ${compactText(summary.resolution.summary)}`])
|
|
529
571
|
];
|
|
530
572
|
}
|
|
573
|
+
function resourceUsageLabel(used, remaining, observable) {
|
|
574
|
+
if (!observable)
|
|
575
|
+
return `${used} observed (partial)`;
|
|
576
|
+
return `${used}${remaining === undefined ? "" : `, remaining=${remaining}`}`;
|
|
577
|
+
}
|
|
578
|
+
function renderWorkItemObservability(item) {
|
|
579
|
+
const stages = item.stages.map((stage) => (`${stage.stage ?? "single"}${stage.round === undefined ? "" : `#${stage.round}`}${stage.stageAttempt === undefined ? "" : `/a${stage.stageAttempt}`}`)).join(", ");
|
|
580
|
+
return ` Observability: stages=${stages || "none"}; tokens=${item.cost.tokens}; tools=${item.cost.toolCalls}; wall=${item.cost.wallClockSeconds}s; retries=${item.cost.retryCount}; snapshots=${item.context.snapshotCount}; peak-input=${item.context.observedInputPeakTokens}; evidence=${item.evidenceCount}; open-findings=${item.openFindingCount}; compression=${item.context.compressionStatus}`;
|
|
581
|
+
}
|
|
@@ -4,6 +4,8 @@ import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
|
4
4
|
import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
|
|
5
5
|
import { projectTaskOrchestration } from "../observability/orchestrationMetrics.js";
|
|
6
6
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
7
|
+
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
8
|
+
import { projectExecutionLaneRunRecoveries } from "../run/recoveryProjection.js";
|
|
7
9
|
/**
|
|
8
10
|
* Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
|
|
9
11
|
* Folds the existing durable records into exactly one protocol-level next
|
|
@@ -24,6 +26,7 @@ export function runTaskNextActionCommand(args, store) {
|
|
|
24
26
|
throw usageError(usage);
|
|
25
27
|
}
|
|
26
28
|
const taskId = positionals[0].trim();
|
|
29
|
+
const now = new Date();
|
|
27
30
|
const data = store.transaction((reader) => {
|
|
28
31
|
// Issue 06: the lightweight facts drive the action on every call; the
|
|
29
32
|
// heavier readiness facts (full event fold, workspaces, jobs, ledger) are
|
|
@@ -32,8 +35,16 @@ export function runTaskNextActionCommand(args, store) {
|
|
|
32
35
|
const facts = reader.readNextActionFacts(taskId);
|
|
33
36
|
if (facts === null)
|
|
34
37
|
throw taskNotFound(taskId);
|
|
35
|
-
const
|
|
36
|
-
|
|
38
|
+
const execution = buildTaskExecutionProjection(reader, taskId, undefined, now);
|
|
39
|
+
if (execution === null)
|
|
40
|
+
throw taskNotFound(taskId);
|
|
41
|
+
const actionFacts = {
|
|
42
|
+
...facts,
|
|
43
|
+
executionGroups: execution.executionGroups,
|
|
44
|
+
runRecoveries: projectExecutionLaneRunRecoveries(reader, taskId, execution.executionGroups)
|
|
45
|
+
};
|
|
46
|
+
const action = projectNextAction(actionFacts);
|
|
47
|
+
const repairWave = repairWaveFor(action, actionFacts);
|
|
37
48
|
// Issue 12: surface pending Knowledge promotion proposals for the Task's
|
|
38
49
|
// bound Projects as a non-blocking advisory. The proposals are workflow
|
|
39
50
|
// state, not completion blockers: an Operator reviews them separately.
|
|
@@ -4,6 +4,7 @@ import { formatTimestamp } from "../output/timePresentation.js";
|
|
|
4
4
|
import { pendingWakeupProjection } from "../storage/taskStore.js";
|
|
5
5
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
6
|
import { projectTaskExecutionFromFacts } from "../scheduler/taskExecutionProjection.js";
|
|
7
|
+
import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
7
8
|
export function parseTaskListOptions(args) {
|
|
8
9
|
const allowed = new Set(["--all", "--verbose"]);
|
|
9
10
|
if (args.some((argument) => !allowed.has(argument))
|
|
@@ -15,10 +16,11 @@ export function parseTaskListOptions(args) {
|
|
|
15
16
|
verbose: args.includes("--verbose")
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
export function buildTaskOverview(store, options) {
|
|
19
|
+
export function buildTaskOverview(store, options, now = new Date()) {
|
|
20
|
+
const runtimeHealthPolicy = resolveRuntimeHealth(store.getConfig().runtimeHealth);
|
|
19
21
|
const tasks = store.listTasks()
|
|
20
22
|
.filter((task) => options.all || task.status !== "archived")
|
|
21
|
-
.map((task) => buildTaskOverviewEntry(task, store));
|
|
23
|
+
.map((task) => buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy));
|
|
22
24
|
return { tasks };
|
|
23
25
|
}
|
|
24
26
|
export function renderTaskOverview(result, options, timeZone, width = defaultTableWidth()) {
|
|
@@ -52,7 +54,7 @@ export function renderTaskOverview(result, options, timeZone, width = defaultTab
|
|
|
52
54
|
return `${output}\n`;
|
|
53
55
|
return `${output}\n\n${renderVerboseDetails(result.tasks, timeZone)}\n`;
|
|
54
56
|
}
|
|
55
|
-
function buildTaskOverviewEntry(task, store) {
|
|
57
|
+
function buildTaskOverviewEntry(task, store, now, runtimeHealthPolicy) {
|
|
56
58
|
const brief = store.getTaskBrief(task.id);
|
|
57
59
|
const roles = store.listRoles(task.id);
|
|
58
60
|
const leaderRole = roles.find((role) => role.name === "leader") ?? null;
|
|
@@ -74,7 +76,7 @@ function buildTaskOverviewEntry(task, store) {
|
|
|
74
76
|
});
|
|
75
77
|
const attention = collectAttention(task, agentRuns, events, leaderFailure, operatorNotification);
|
|
76
78
|
const blockers = collectBlockers(workItems, openInputRequests, attention);
|
|
77
|
-
const
|
|
79
|
+
const legacyNext = deriveNextAction(task, brief, workItems, openInputRequests, blockers, pendingWakeup);
|
|
78
80
|
const leader = {
|
|
79
81
|
role: "leader",
|
|
80
82
|
roleStatus: leaderRole?.status ?? "missing",
|
|
@@ -118,8 +120,19 @@ function buildTaskOverviewEntry(task, store) {
|
|
|
118
120
|
leaderMailbox,
|
|
119
121
|
leaderFailure,
|
|
120
122
|
operatorNotification,
|
|
121
|
-
roleSessions
|
|
123
|
+
roleSessions,
|
|
124
|
+
contextSnapshots: store.listContextSnapshots(task.id),
|
|
125
|
+
now,
|
|
126
|
+
runtimeHealthPolicy
|
|
122
127
|
});
|
|
128
|
+
const next = execution.action === "recover-execution"
|
|
129
|
+
? {
|
|
130
|
+
action: execution.action,
|
|
131
|
+
owner: execution.owner,
|
|
132
|
+
kind: "execution",
|
|
133
|
+
summary: execution.summary
|
|
134
|
+
}
|
|
135
|
+
: legacyNext;
|
|
123
136
|
return {
|
|
124
137
|
...task,
|
|
125
138
|
brief,
|
|
@@ -384,6 +397,9 @@ function renderVerboseDetails(tasks, timeZone) {
|
|
|
384
397
|
: formatTimestamp(task.summaryUpdatedAt, timeZone)}`,
|
|
385
398
|
` Execution: ${task.execution.status} (${task.execution.owner}); ${task.execution.summary}`,
|
|
386
399
|
` Monitoring: ${task.execution.monitoring}; attention: ${task.execution.attention.length}`,
|
|
400
|
+
` DAG: ${task.execution.observability.dag.nodes.length} nodes, ${task.execution.observability.dag.edges.length} edges; ready=${task.execution.observability.dag.readyIds.join(", ") || "none"}; blocked=${task.execution.observability.dag.blockedIds.join(", ") || "none"}`,
|
|
401
|
+
` Cost: tokens=${task.execution.observability.cost.tokens}${task.execution.observability.cost.tokensObservable ? "" : " (partial)"}; tools=${task.execution.observability.cost.toolCalls}${task.execution.observability.cost.toolCallsObservable ? "" : " (partial)"}; wall=${task.execution.observability.cost.wallClockSeconds}s; retries=${task.execution.observability.cost.retryCount}`,
|
|
402
|
+
` Context: snapshots=${task.execution.observability.context.snapshotCount}; bytes=${task.execution.observability.context.totalBytes ?? "partial"}; peak-input=${task.execution.observability.context.observedInputPeakTokens}; compression=unavailable`,
|
|
387
403
|
` Projects: ${task.projectBindings.length === 0
|
|
388
404
|
? "none"
|
|
389
405
|
: task.projectBindings.map(({ directory, projectId, baseRef }) => (`${directory} (${projectId} @ ${baseRef})`)).join(", ")}`
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
|
+
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
4
|
+
import { acquireProjectMaintenanceLock } from "../repository/projectMaintenanceLock.js";
|
|
5
|
+
import { workspaceProjectEntry } from "../worktree/managedWorkspace.js";
|
|
6
|
+
export async function runTaskUpstreamCommand(args, store, options = {}) {
|
|
7
|
+
const [command, ...rest] = args;
|
|
8
|
+
if (command === "integrate") {
|
|
9
|
+
return integrateUpstream(rest, store, options);
|
|
10
|
+
}
|
|
11
|
+
throw usageError(command === undefined
|
|
12
|
+
? "Task upstream command is required."
|
|
13
|
+
: `Unknown command: task upstream ${command}`);
|
|
14
|
+
}
|
|
15
|
+
async function integrateUpstream(args, store, options) {
|
|
16
|
+
const usage = "Task upstream integrate usage: yui task upstream integrate <task> [--latest] [--project <project>].";
|
|
17
|
+
const taskId = args[0];
|
|
18
|
+
if (taskId === undefined)
|
|
19
|
+
throw usageError(usage);
|
|
20
|
+
const flags = new Set(args.slice(1));
|
|
21
|
+
if (flags.size !== args.length - 1) {
|
|
22
|
+
throw usageError(usage);
|
|
23
|
+
}
|
|
24
|
+
const latest = flags.has("--latest");
|
|
25
|
+
const projectFlag = args.find((arg) => arg.startsWith("--project="));
|
|
26
|
+
const projectRef = projectFlag?.slice("--project=".length);
|
|
27
|
+
if (!latest && projectRef === undefined) {
|
|
28
|
+
throw usageError("Specify --latest to integrate the remote development head, or --project=<project> to target one Project.");
|
|
29
|
+
}
|
|
30
|
+
if (latest && projectRef !== undefined) {
|
|
31
|
+
throw usageError("--latest and --project are mutually exclusive.");
|
|
32
|
+
}
|
|
33
|
+
for (const flag of flags) {
|
|
34
|
+
if (flag !== "--latest" && !flag.startsWith("--project=")) {
|
|
35
|
+
throw usageError(usage);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const task = store.getTask(taskId);
|
|
39
|
+
if (task === null)
|
|
40
|
+
throw usageError(`Task not found: ${taskId}.`);
|
|
41
|
+
if (task.status !== "active") {
|
|
42
|
+
throw usageError(`Task must be active to integrate upstream: ${taskId}/${task.status}.`);
|
|
43
|
+
}
|
|
44
|
+
// RFC Phase 4: integrating upstream rewrites the Task worktree HEAD. Refuse
|
|
45
|
+
// while the Leader has an active Run so the merge never lands under a
|
|
46
|
+
// running Session.
|
|
47
|
+
const activeLeaderRun = store.getActiveAgentRun(taskId, "leader");
|
|
48
|
+
if (activeLeaderRun !== null) {
|
|
49
|
+
throw usageError(`Task has an active Leader Run (${activeLeaderRun.id}); stop it before integrating upstream: ${taskId}.`);
|
|
50
|
+
}
|
|
51
|
+
const workspace = store.getTaskWorkspace(taskId);
|
|
52
|
+
if (workspace === null) {
|
|
53
|
+
throw usageError(`Task has no workspace: ${taskId}. Activate it first.`);
|
|
54
|
+
}
|
|
55
|
+
const git = options.git ?? new NodeGitWorkspace();
|
|
56
|
+
const now = options.now ?? (() => new Date());
|
|
57
|
+
const results = [];
|
|
58
|
+
for (const binding of task.projectBindings) {
|
|
59
|
+
if (projectRef !== undefined && binding.projectId !== projectRef)
|
|
60
|
+
continue;
|
|
61
|
+
const project = store.getProject(binding.projectId);
|
|
62
|
+
if (project === null) {
|
|
63
|
+
throw usageError(`Project not found: ${binding.projectId}.`);
|
|
64
|
+
}
|
|
65
|
+
if (project.remoteUrl === undefined) {
|
|
66
|
+
throw usageError(`Project has no remote URL: ${project.id}.`);
|
|
67
|
+
}
|
|
68
|
+
const entry = workspaceProjectEntry(workspace, project.id);
|
|
69
|
+
if (entry === undefined) {
|
|
70
|
+
throw usageError(`Task workspace has no entry for Project: ${project.id}.`);
|
|
71
|
+
}
|
|
72
|
+
// Hold the per-Project maintenance fence while resolving the remote
|
|
73
|
+
// baseline so a concurrent `project refresh` cannot interleave with the
|
|
74
|
+
// fetch.
|
|
75
|
+
const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
|
|
76
|
+
let oldHead;
|
|
77
|
+
let newHead;
|
|
78
|
+
let upstreamCommit;
|
|
79
|
+
try {
|
|
80
|
+
// Resolve the exact upstream commit.
|
|
81
|
+
const upstream = await git.resolveRemoteBaseline({
|
|
82
|
+
repositoryPath: project.path,
|
|
83
|
+
remoteUrl: project.remoteUrl,
|
|
84
|
+
developmentRef: project.developmentBranch
|
|
85
|
+
});
|
|
86
|
+
upstreamCommit = upstream.commit;
|
|
87
|
+
// Record the current HEAD as a backup before merging.
|
|
88
|
+
const current = await git.inspect(entry.path, "HEAD");
|
|
89
|
+
oldHead = current.baseCommit;
|
|
90
|
+
try {
|
|
91
|
+
// mergeWorktree always creates a merge commit (--no-ff); record the
|
|
92
|
+
// actual resulting HEAD rather than assuming a fast-forward.
|
|
93
|
+
await git.mergeWorktree({
|
|
94
|
+
targetPath: entry.path,
|
|
95
|
+
sourceRefs: [upstream.commit]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Restore the original HEAD on failure. mergeWorktree already aborts
|
|
100
|
+
// a conflicted merge, so HEAD is usually already at oldHead; reset
|
|
101
|
+
// only when the merge left HEAD moved or the tree dirty.
|
|
102
|
+
let restoreNote;
|
|
103
|
+
try {
|
|
104
|
+
await git.resetWorktree({
|
|
105
|
+
targetPath: entry.path,
|
|
106
|
+
expectedHead: oldHead,
|
|
107
|
+
restoreHead: oldHead
|
|
108
|
+
});
|
|
109
|
+
restoreNote = ` The workspace has been restored to ${oldHead}.`;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
restoreNote = ` The workspace may be left in a conflicted state; inspect ${entry.path} manually.`;
|
|
113
|
+
}
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
throw new Error(`Upstream integration failed for Project ${project.id}: ${message}.` + restoreNote);
|
|
116
|
+
}
|
|
117
|
+
const after = await git.inspect(entry.path, "HEAD");
|
|
118
|
+
newHead = after.baseCommit;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
releaseMaintenance();
|
|
122
|
+
}
|
|
123
|
+
// Record the integration event immediately so a later Project's failure
|
|
124
|
+
// does not lose this Project's audit trail.
|
|
125
|
+
const result = { projectId: project.id, oldHead, newHead, upstreamCommit };
|
|
126
|
+
store.transaction((tx) => {
|
|
127
|
+
tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, "task.upstream-integrated", result, now()));
|
|
128
|
+
});
|
|
129
|
+
results.push(result);
|
|
130
|
+
}
|
|
131
|
+
const lines = results.map((r) => ` ${r.projectId}: ${r.oldHead.slice(0, 12)} -> ${r.newHead.slice(0, 12)} (upstream: ${r.upstreamCommit.slice(0, 12)})`);
|
|
132
|
+
return {
|
|
133
|
+
output: `Integrated upstream for Task ${taskId}:\n${lines.join("\n")}\n`,
|
|
134
|
+
data: { taskId, integrations: results }
|
|
135
|
+
};
|
|
136
|
+
}
|