@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.
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/scheduler/actionability.js +169 -3
  28. package/dist/scheduler/activeTaskProgress.js +15 -10
  29. package/dist/scheduler/leaderWakeupProcessor.js +17 -1
  30. package/dist/scheduler/taskExecutionProjection.js +105 -8
  31. package/dist/scheduler/taskObservabilityProjection.js +282 -0
  32. package/dist/storage/migration/productionRegistry.js +14 -0
  33. package/dist/storage/sqliteStore.js +12 -0
  34. package/dist/storage/taskStore.js +1 -1
  35. package/dist/storage/upgrade/sqliteStateMigration.js +7 -3
  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
package/README.md CHANGED
@@ -532,6 +532,59 @@ yui task work isolate <task-id>/<work-item-id>
532
532
  yui task work dispatch <task-id>/<work-item-id> --input "Implement and run focused tests"
533
533
  ```
534
534
 
535
+ Dispatch remains `single` by default. A Leader can explicitly enable bounded
536
+ multi-route exploration on a fresh WorkItem; each accepted stage advances
537
+ `Plan → Generate → Compare → Synthesize → Verify → Resolve`, and only the
538
+ accepted Resolve stage materializes the existing single Candidate:
539
+
540
+ ```sh
541
+ yui task work dispatch <task-id>/<work-item-id> \
542
+ --mode parallel-diverse --max-rounds 2 --stage-max-attempts 2 \
543
+ --strategy fixed:2 --lane-role critic \
544
+ --stage-max-tokens 240000 --stage-max-tool-calls 200 \
545
+ --stage-max-seconds 1800 --stage-quorum 2
546
+ yui task work group resolve <task-id>/<work-item-id> \
547
+ --decision accept --summary "Plan evidence is sufficient"
548
+ ```
549
+
550
+ Every stage is a new immutable ExecutionGroup. Its ContextSnapshot and selected
551
+ parent Lane results are durable references; `retry` repeats a stage within its
552
+ attempt budget, while `retry` at Resolve begins the next bounded round.
553
+
554
+ Each new stage also freezes one Resource Broker contract: token, tool-call and
555
+ wall-clock budgets; quorum and deadline; a straggler window; and the minimum
556
+ marginal value for more Lane spend. Omitted values reuse the existing context
557
+ budget and runtime-health windows; stage retries share the original cumulative
558
+ spend and absolute deadline. Execution, Lane retry, and Reviewer-panel
559
+ admission all count active Lanes at Home, Task, WorkItem, Group, Provider,
560
+ Agent, and model scopes. Capacity pressure keeps the excess Lane durably
561
+ pending instead of failing the Group. Capacity release or deadline arrival
562
+ wakes the Leader through the existing actionability path; rerunning the same
563
+ dispatch resumes the frozen input. Released capacity is reserved for the
564
+ oldest currently admissible waiter, while a Provider- or Agent-blocked queue
565
+ head does not prevent independent scopes from making progress. Provider rate
566
+ limits still use the existing in-place retry window and therefore never fan
567
+ out into sibling failures.
568
+
569
+ The Leader may add `--early-stop <0-100>` to an accepting Group resolution.
570
+ Yui permits it only after quorum and T5's passed Verify/Resolve evidence prove
571
+ sufficiency. It may skip Lanes that never started; active stragglers are
572
+ reported and retained, never killed automatically for cost. If evidence is
573
+ insufficient, budget or deadline exhaustion blocks the stage for Leader
574
+ judgment instead of turning thin evidence into success.
575
+
576
+ New exploration histories also freeze the structured candidate-convergence
577
+ contract. Yui appends the exact stage-local JSON shape to every Lane assignment
578
+ and validates selected reports before the Leader can advance: Compare must
579
+ partition duplicate clusters and justify each selected route with a direct
580
+ source, executable check, or frozen artifact; Synthesize uses a claim/evidence
581
+ table for research, a decision matrix for architecture, and one frozen Git
582
+ snapshot for code. Verify must use a Role independent from the selected
583
+ Synthesize author. Only complete criterion evidence can produce `passed` and
584
+ an accepted Resolve Candidate; explicit gaps produce `next-round` and can only
585
+ continue through bounded Resolve `retry`. Votes and derived analysis remain
586
+ reportable context, but never substitute for direct evidence.
587
+
535
588
  Permission is one adapter-specific enum configuration on each Agent binding:
536
589
  `default` follows the provider, `bypass` compiles the provider's supported
537
590
  bypass flag, and `configured` retains whichever native options are explicitly
@@ -343,6 +343,19 @@ const taskChildren = [
343
343
  usage: "yui task rebuild <task> [--latest]",
344
344
  options: ["--latest"]
345
345
  },
346
+ {
347
+ name: "upstream",
348
+ summary: "Integrate upstream changes into an Active Task workspace.",
349
+ sections: [{ id: "manage", title: "Commands", entries: ["integrate"] }],
350
+ children: [
351
+ {
352
+ name: "integrate",
353
+ summary: "Merge the remote development head into the Task workspace.",
354
+ usage: "yui task upstream integrate <task> [--latest] [--project <project>]",
355
+ options: ["--latest", "--project"]
356
+ }
357
+ ]
358
+ },
346
359
  {
347
360
  name: "history",
348
361
  summary: "Inspect and archive legacy Task refs in the Home repository.",
@@ -619,8 +632,11 @@ const taskChildren = [
619
632
  {
620
633
  name: "dispatch",
621
634
  summary: "Dispatch a work item to its Role.",
622
- usage: "yui task work dispatch <task>/<work> [--input <text>] [--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...]",
623
- options: ["--input", "--strategy", "--lane-role"]
635
+ usage: "yui task work dispatch <task>/<work> [--input <text>] [--strategy fixed:<count>|adaptive:<max>] [--lane-role <role> ...] [--mode <single|parallel-diverse|ensemble-replicated|adversarial|adaptive-exploration>] [--max-rounds <count>] [--stage-max-attempts <count>] [--stage-max-tokens <count>] [--stage-max-tool-calls <count>] [--stage-max-seconds <count>] [--stage-quorum <count>] [--stage-straggler-seconds <count>] [--stage-min-marginal-value <0-100>]",
636
+ options: ["--input", "--strategy", "--lane-role", "--mode", "--max-rounds", "--stage-max-attempts", "--stage-max-tokens", "--stage-max-tool-calls", "--stage-max-seconds", "--stage-quorum", "--stage-straggler-seconds", "--stage-min-marginal-value"],
637
+ optionValues: {
638
+ "--mode": ["single", "parallel-diverse", "ensemble-replicated", "adversarial", "adaptive-exploration"]
639
+ }
624
640
  },
625
641
  {
626
642
  name: "group",
@@ -630,9 +646,9 @@ const taskChildren = [
630
646
  children: [{
631
647
  name: "resolve",
632
648
  summary: "Select Lane outputs and resolve the Worker group.",
633
- usage: "yui task work group resolve <task>/<work> --decision <accept|reject|blocked> --summary <text> [--lane <lane-id> ...]",
634
- options: ["--decision", "--summary", "--lane"],
635
- optionValues: { "--decision": ["accept", "reject", "blocked"] }
649
+ usage: "yui task work group resolve <task>/<work> --decision <accept|reject|retry|blocked> --summary <text> [--lane <lane-id> ...] [--early-stop <marginal-value-percent>]",
650
+ options: ["--decision", "--summary", "--lane", "--early-stop"],
651
+ optionValues: { "--decision": ["accept", "reject", "retry", "blocked"] }
636
652
  }]
637
653
  },
638
654
  {
@@ -1244,7 +1260,7 @@ export const ROOT_COMMAND = buildNode({
1244
1260
  {
1245
1261
  name: "project",
1246
1262
  summary: "Manage Projects, stable checkouts, branches, and Yui knowledge.",
1247
- sections: [{ id: "manage", title: "Commands", entries: ["add", "clone", "refresh", "migrate", "update", "discover", "list", "show", "knowledge"] }],
1263
+ sections: [{ id: "manage", title: "Commands", entries: ["add", "clone", "refresh", "diagnose", "migrate", "update", "discover", "list", "show", "knowledge"] }],
1248
1264
  children: [
1249
1265
  {
1250
1266
  name: "add",
@@ -1264,6 +1280,11 @@ export const ROOT_COMMAND = buildNode({
1264
1280
  summary: "Fast-forward a clean stable checkout from its configured remote.",
1265
1281
  usage: "yui project refresh <project>"
1266
1282
  },
1283
+ {
1284
+ name: "diagnose",
1285
+ summary: "Show canonical HEAD vs remote head without mutating the checkout.",
1286
+ usage: "yui project diagnose <project>"
1287
+ },
1267
1288
  {
1268
1289
  name: "migrate",
1269
1290
  summary: "Move an external Project into a Home-managed repository.",
@@ -1381,7 +1402,7 @@ export const ROOT_COMMAND = buildNode({
1381
1402
  name: "task",
1382
1403
  summary: "Manage Tasks, WorkItems, Agent Runs, and integration.",
1383
1404
  sections: [
1384
- { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "complete", "reopen", "retire", "list", "show", "context", "next-action", "archive", "rebuild", "history", "replace", "reconcile"] },
1405
+ { id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "complete", "reopen", "retire", "list", "show", "context", "next-action", "archive", "rebuild", "history", "replace", "reconcile", "upstream"] },
1385
1406
  { id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "overlap", "change-set"] },
1386
1407
  { id: "knowledge", title: "Task Knowledge", entries: ["brief", "decision", "milestone", "event", "continuation", "wake"] }
1387
1408
  ],
package/dist/cli.js CHANGED
@@ -37,7 +37,7 @@ import { runResourcesCommand } from "./commands/resourcesCommands.js";
37
37
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
38
38
  import { runProjectCommand } from "./commands/projectCommands.js";
39
39
  import { runProfileCommand } from "./commands/profileCommands.js";
40
- import { dispatchPreparedReviewRound, failPendingReviewRound, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, parseTaskFinalReviewContractRebindRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, validateTaskArchiveRequest } from "./commands/taskCommands.js";
40
+ import { dispatchPreparedReviewRound, failPendingReviewRound, RESUMED_PENDING_FINAL_REVIEW, TERMINALIZED_LEADER_BEFORE_FINAL_REVIEW, TaskFinalReviewDispatchDriftError, preserveReviewRoundWorkspace, parseTaskCompletionRequest, parseTaskFinalReviewContractRebindRequest, preflightTaskCompletion, runTaskCommand, normalizedExecutionLanePlan, resolvedExecutionStageRetryGroup, validateTaskArchiveRequest } from "./commands/taskCommands.js";
41
41
  import { taskActor } from "./commands/taskActor.js";
42
42
  import { isCurrentGlobalOperator } from "./commands/taskInputCommands.js";
43
43
  import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
@@ -52,6 +52,7 @@ import { acquireHandoverLock, readRuntimeIdentity } from "./release/runtimeRelea
52
52
  import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, resolveReleaseActivationDriver, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
53
53
  import { reconcileTaskRemoteBaselines, verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
54
54
  import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
55
+ import { runTaskUpstreamCommand } from "./commands/taskUpstreamCommands.js";
55
56
  import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
56
57
  import { FileCompletionManager, resolveCliIdentity } from "./completion/fileCompletionManager.js";
57
58
  import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
@@ -1005,6 +1006,11 @@ export async function main() {
1005
1006
  emit(result.output, false, result.data);
1006
1007
  return;
1007
1008
  }
1009
+ if (resolved[1] === "upstream") {
1010
+ const result = await runTaskUpstreamCommand(resolved.slice(2), store);
1011
+ emit(result.output, false, result.data);
1012
+ return;
1013
+ }
1008
1014
  if (resolved[1] === "complete" && resolved[2] !== undefined) {
1009
1015
  const completionRequest = parseTaskCompletionRequest(resolved.slice(2));
1010
1016
  completionSummary = completionRequest.summary;
@@ -1117,6 +1123,16 @@ export async function main() {
1117
1123
  const laneSnapshotPreflight = executionLaneGitSnapshot === undefined
1118
1124
  ? undefined
1119
1125
  : executionLaneGitSnapshot;
1126
+ // RFC Phase 3: prepare the Task workspace before activation so a
1127
+ // preparation failure keeps the Task in Draft instead of leaving it
1128
+ // Active without a workspace.
1129
+ if (resolved[1] === "activate") {
1130
+ const taskId = resolved[2];
1131
+ const task = taskId === undefined ? null : store.getTask(taskId);
1132
+ if (task !== null && task.status === "draft") {
1133
+ await workspacePreparer.prepareTaskWorkspace(task.id);
1134
+ }
1135
+ }
1120
1136
  const result = runTaskCommand(resolved.slice(1), store, {
1121
1137
  runtime,
1122
1138
  environment: process.env,
@@ -1166,10 +1182,12 @@ export async function main() {
1166
1182
  : store.getReviewRound(requestedRound.taskId, requestedRound.id);
1167
1183
  let reviewOutput = "";
1168
1184
  let reviewData;
1185
+ const resumesReviewDispatch = (resolved[1] === "review" && resolved[2] === "request")
1186
+ || (resolved[1] === "work" && resolved[2] === "review")
1187
+ || (resolved[1] === "run" && resolved[2] === "retry");
1169
1188
  const reviewDispatchNeeded = requestedRound?.status === "pending"
1170
1189
  || (requestedRound?.status === "running"
1171
- && resolved[1] === "review"
1172
- && resolved[2] === "request"
1190
+ && resumesReviewDispatch
1173
1191
  && persistedRequestedRound?.executionGroup?.lanes.some((lane) => (lane.status === "pending" && lane.runId === undefined)) === true);
1174
1192
  if (reviewDispatchNeeded) {
1175
1193
  try {
@@ -1203,10 +1221,12 @@ export async function main() {
1203
1221
  ? {}
1204
1222
  : { deltaRecheckDiff: deltaRecheckPreflight.diffByProject })
1205
1223
  });
1206
- reviewOutput = `Review queued as ${requestedRound.id} (${run.id})\n`;
1224
+ reviewOutput = run === null
1225
+ ? `Review ${requestedRound.id} retained pending by Resource Broker\n`
1226
+ : `Review queued as ${requestedRound.id} (${run.id})\n`;
1207
1227
  reviewData = {
1208
1228
  reviewRound: store.getReviewRound(requestedRound.taskId, requestedRound.id),
1209
- reviewRun: run,
1229
+ ...(run === null ? {} : { reviewRun: run }),
1210
1230
  workspace
1211
1231
  };
1212
1232
  }
@@ -1226,45 +1246,6 @@ export async function main() {
1226
1246
  reviewData = { reviewRound: failed };
1227
1247
  }
1228
1248
  }
1229
- if (resolved[1] === "create") {
1230
- const created = result.data;
1231
- if (created?.task?.id !== undefined) {
1232
- let workspace;
1233
- try {
1234
- workspace = await workspacePreparer.prepareTaskWorkspace(created.task.id);
1235
- }
1236
- catch (error) {
1237
- const message = error instanceof Error ? error.message : String(error);
1238
- workspace = {
1239
- taskId: created.task.id,
1240
- status: "failed",
1241
- error: message
1242
- };
1243
- }
1244
- const latest = store.getTask(created.task.id);
1245
- const leader = store.getRole(created.task.id, "leader");
1246
- if (latest !== null && leader !== null) {
1247
- const warning = workspace.status === "failed"
1248
- ? `Main worktree is not ready: ${workspace.error ?? "unknown error"}.\n`
1249
- + `After correcting the Git problem, run yui task reconcile ${created.task.id}.\n`
1250
- : "";
1251
- emit(`${result.output}${warning}`, false, {
1252
- ...created,
1253
- task: latest,
1254
- leader,
1255
- workspace
1256
- });
1257
- return;
1258
- }
1259
- }
1260
- }
1261
- if (resolved[1] === "activate") {
1262
- const taskId = resolved[2];
1263
- const task = taskId === undefined ? null : store.getTask(taskId);
1264
- if (task?.status === "active") {
1265
- await workspacePreparer.prepareTaskWorkspace(task.id);
1266
- }
1267
- }
1268
1249
  if (resolved[1] === "project" && resolved[2] === "add") {
1269
1250
  const taskId = resolved[3];
1270
1251
  const task = taskId === undefined ? null : store.getTask(taskId);
@@ -1683,6 +1664,7 @@ async function candidateSnapshotForTaskCommand(args, store, preparer, environmen
1683
1664
  const fixedSingleLane = group?.strategy.mode === "fixed"
1684
1665
  && group.strategy.count === 1
1685
1666
  && group.lanes.length === 1
1667
+ && group.stage === undefined
1686
1668
  && run.workspace.owner.type === "work-item";
1687
1669
  if (fixedSingleLane)
1688
1670
  return preparer.snapshotCandidateWorkspace(run.workspace);
@@ -1725,9 +1707,28 @@ async function candidateMaterializationForTaskCommand(args, store, preparer, env
1725
1707
  : currentWorkItemExecutionGroup(item);
1726
1708
  if (item === null || item === undefined || group === undefined)
1727
1709
  return undefined;
1710
+ if (group.stage !== undefined && group.stage.stage !== "resolve")
1711
+ return undefined;
1712
+ // Early termination first stops never-started spend. Active stragglers are
1713
+ // deliberately retained, so the command records that stop and leaves the
1714
+ // group unresolved. Do not merge Lane output into the WorkItem Candidate
1715
+ // until those active Lanes settle and the Leader resolves the group again.
1716
+ if (args.includes("--early-stop")
1717
+ && group.lanes.some(({ status }) => status === "running"))
1718
+ return undefined;
1728
1719
  const selected = args.flatMap((value, index) => value === "--lane" && args[index + 1] !== undefined ? [args[index + 1]] : []);
1720
+ const materializedLaneIds = selected.length === 0
1721
+ ? group.lanes
1722
+ .filter((lane) => lane.status === "yielded" || lane.status === "completed")
1723
+ .map(({ id }) => id)
1724
+ : selected;
1725
+ if (group.stage?.convergence !== undefined
1726
+ && group.stage.stage === "resolve"
1727
+ && materializedLaneIds.length !== 1) {
1728
+ throw usageError("Candidate convergence Resolve must select exactly one Lane before materialization.");
1729
+ }
1729
1730
  try {
1730
- return await preparer.materializeExecutionGroupCandidate(item.taskId, item.id, group.id, selected);
1731
+ return await preparer.materializeExecutionGroupCandidate(item.taskId, item.id, group.id, materializedLaneIds);
1731
1732
  }
1732
1733
  catch (error) {
1733
1734
  throw usageError(error instanceof Error ? error.message : String(error));
@@ -1781,6 +1782,9 @@ async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, e
1781
1782
  requestedRoles: roles,
1782
1783
  requestedStrategy,
1783
1784
  existingGroup: group === undefined ? undefined : group,
1785
+ resolvedRetryGroup: isDispatch
1786
+ ? resolvedExecutionStageRetryGroup(currentGroup)
1787
+ : undefined,
1784
1788
  status: item.status,
1785
1789
  nextGroupId: `execution-group-${store.peekNextAgentRunId(item.taskId)}`,
1786
1790
  retryLaneId,
@@ -1799,8 +1803,7 @@ async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, e
1799
1803
  throw usageError(`ExecutionGroup strategy is frozen: ${group.id}.`);
1800
1804
  }
1801
1805
  const laneCount = plan.requestedCount;
1802
- const strategyArg = args.find((value) => value.startsWith("adaptive:") || value.startsWith("fixed:"));
1803
- const adaptive = strategyArg?.startsWith("adaptive:") === true || group?.strategy.mode === "adaptive";
1806
+ const adaptive = plan.strategy.mode === "adaptive";
1804
1807
  const needsIsolation = adaptive || laneCount > 1 || (group?.lanes.length ?? 0) > 1;
1805
1808
  if (!needsIsolation)
1806
1809
  return undefined;
@@ -27,6 +27,9 @@ export async function runProjectCommand(args, store, options = {}) {
27
27
  data: refreshed
28
28
  };
29
29
  }
30
+ if (command === "diagnose") {
31
+ return diagnoseProject(rest, store, options);
32
+ }
30
33
  if (command === "migrate") {
31
34
  const migrated = await migrateProject(rest, store, options);
32
35
  return {
@@ -80,12 +83,72 @@ async function refreshProject(args, store, options) {
80
83
  if (project.stableBranch !== project.developmentBranch) {
81
84
  throw usageError(`Project refresh requires matching stable and development branches: ${project.id}.`);
82
85
  }
83
- const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
84
- repositoryPath: project.path,
85
- remoteUrl: project.remoteUrl,
86
- stableRef: project.stableBranch
87
- });
88
- return { project, ...refreshed };
86
+ // RFC Phase 1: hold the per-Project maintenance fence for the whole refresh
87
+ // so Task workspace preparation and other maintenance cannot interleave
88
+ // with the canonical branch/working-tree move.
89
+ const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
90
+ try {
91
+ const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
92
+ repositoryPath: project.path,
93
+ remoteUrl: project.remoteUrl,
94
+ stableRef: project.stableBranch
95
+ });
96
+ return { project, ...refreshed };
97
+ }
98
+ finally {
99
+ releaseMaintenance();
100
+ }
101
+ }
102
+ /**
103
+ * RFC Phase 1: diagnose a managed Project's divergence without mutating it.
104
+ * Shows the canonical HEAD, remote advertised SHA, and whether the checkout
105
+ * can be fast-forwarded. Does not auto-reset or rebase.
106
+ */
107
+ async function diagnoseProject(args, store, options) {
108
+ if (args.length !== 1) {
109
+ throw usageError("Project diagnose usage: yui project diagnose <project>.");
110
+ }
111
+ const project = requireProject(store, args[0]);
112
+ if (project.remoteUrl === undefined) {
113
+ return {
114
+ output: `Project ${project.id} has no remote URL; nothing to diagnose.\n`,
115
+ data: { project, diagnosis: "no-remote" }
116
+ };
117
+ }
118
+ const git = options.git ?? new NodeGitWorkspace();
119
+ const releaseMaintenance = acquireProjectMaintenanceLock(store.rootDirectory(), project.id);
120
+ try {
121
+ const current = await git.inspect(project.path, "HEAD");
122
+ const remote = await git.resolveRemoteBaseline({
123
+ repositoryPath: project.path,
124
+ remoteUrl: project.remoteUrl,
125
+ developmentRef: project.developmentBranch
126
+ });
127
+ const isAncestor = await git.isAncestor(project.path, current.baseCommit, remote.commit);
128
+ const diverged = !isAncestor && current.baseCommit !== remote.commit;
129
+ const lines = [
130
+ `Project: ${project.id}`,
131
+ ` canonical HEAD: ${current.baseCommit}`,
132
+ ` remote ${project.developmentBranch}: ${remote.commit}`,
133
+ ` status: ${current.baseCommit === remote.commit ? "up-to-date" : diverged ? "diverged" : "behind"}`,
134
+ ...(diverged
135
+ ? [" The canonical checkout has local commits not on the remote.",
136
+ " Use `yui project refresh` only after resolving the divergence manually."]
137
+ : [])
138
+ ];
139
+ return {
140
+ output: lines.join("\n") + "\n",
141
+ data: {
142
+ project,
143
+ diagnosis: diverged ? "diverged" : current.baseCommit === remote.commit ? "up-to-date" : "behind",
144
+ canonicalHead: current.baseCommit,
145
+ remoteHead: remote.commit
146
+ }
147
+ };
148
+ }
149
+ finally {
150
+ releaseMaintenance();
151
+ }
89
152
  }
90
153
  async function discoverProjects(args, store, options) {
91
154
  if (args.length > 1) {