@zq-silk/yui 0.10.0 → 0.11.0

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.
Files changed (46) hide show
  1. package/README.md +53 -0
  2. package/dist/cli/commandCatalog.js +28 -7
  3. package/dist/cli.js +50 -47
  4. package/dist/commands/projectCommands.js +69 -6
  5. package/dist/commands/taskCommands.js +639 -89
  6. package/dist/commands/taskContextCommand.js +78 -27
  7. package/dist/commands/taskNextActionCommand.js +13 -2
  8. package/dist/commands/taskOverviewCommand.js +21 -5
  9. package/dist/commands/taskUpstreamCommands.js +136 -0
  10. package/dist/context/runContextPack.js +184 -17
  11. package/dist/controller/agentRuntimeObserver.js +31 -20
  12. package/dist/controller/fileSchedulerStoreAdapter.js +7 -13
  13. package/dist/execution/candidateConvergence.js +623 -0
  14. package/dist/execution/executionGroup.js +255 -13
  15. package/dist/execution/executionHealth.js +324 -0
  16. package/dist/execution/resourceBroker.js +425 -0
  17. package/dist/executor/fileRoleLaunchPlanner.js +6 -9
  18. package/dist/executor/workspacePreflightClassification.js +117 -0
  19. package/dist/lifecycle/exactRunTerminalization.js +13 -2
  20. package/dist/lifecycle/taskRoleSessionReset.js +4 -2
  21. package/dist/repository/taskBaseFreshness.js +26 -1
  22. package/dist/repository/taskWorkspacePreparer.js +17 -1
  23. package/dist/review/reviewRound.js +27 -6
  24. package/dist/run/agentRun.js +2 -2
  25. package/dist/run/recoveryProjection.js +15 -0
  26. package/dist/runtime/runtimeContinuationProjection.js +7 -0
  27. package/dist/runtime/tmuxAdapters.js +8 -2
  28. package/dist/scheduler/actionability.js +169 -3
  29. package/dist/scheduler/activeTaskProgress.js +15 -10
  30. package/dist/scheduler/leaderWakeupProcessor.js +17 -1
  31. package/dist/scheduler/taskExecutionProjection.js +105 -8
  32. package/dist/scheduler/taskObservabilityProjection.js +282 -0
  33. package/dist/storage/migration/productionRegistry.js +14 -0
  34. package/dist/storage/sqliteStore.js +12 -0
  35. package/dist/storage/taskStore.js +1 -1
  36. package/dist/task/completionReadiness.js +1 -1
  37. package/dist/task/nextAction.js +314 -2
  38. package/dist/web/assets/client/components.js +116 -0
  39. package/dist/web/assets/client/i18n.js +66 -0
  40. package/dist/web/assets/client/view.js +15 -0
  41. package/dist/web/assets/styles/cards.js +23 -0
  42. package/dist/web/assets/styles/responsive.js +2 -0
  43. package/dist/web/webSnapshot.js +8 -2
  44. package/dist/workItem/workItem.js +262 -5
  45. package/i18n/README.zh-CN.md +42 -0
  46. 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(nextActionFacts)
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 = summarizeExecutionGroup(group);
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
- ...summary.laneSummaries.flatMap((lane) => [
507
- ` Lane ${lane.laneId} (#${lane.ordinal}, ${lane.roleName}) [${lane.status}]${lane.summary === undefined ? "" : `: ${compactText(lane.summary)}`}`,
508
- ...(lane.report === undefined ? [] : [` Report: ${compactText(lane.report)}`]),
509
- ...(lane.checks === undefined || lane.checks.length === 0
510
- ? []
511
- : [` Checks: ${lane.checks.map(({ name, outcome }) => `${name}:${outcome}`).join(", ")}`]),
512
- ...(lane.findings === undefined || lane.findings.length === 0
513
- ? []
514
- : [` Findings: ${lane.findings.map(({ id, severity, status }) => `${id}:${severity}/${status}`).join(", ")}`]),
515
- ...(lane.evidence === undefined || lane.evidence.length === 0
516
- ? []
517
- : [` Evidence: ${lane.evidence.length} item(s)`]),
518
- ...(lane.evidenceCommit === undefined
519
- ? []
520
- : [` Evidence commit: ${lane.evidenceCommit}`]),
521
- ...(lane.decision === undefined ? [] : [` Decision: ${lane.decision}`])
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 action = projectNextAction(facts);
36
- const repairWave = repairWaveFor(action, facts);
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 next = deriveNextAction(task, brief, workItems, openInputRequests, blockers, pendingWakeup);
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
+ }