@zq-silk/yui 0.2.0 → 0.4.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.
Files changed (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -1,15 +1,289 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { lstat, mkdir, realpath } from "node:fs/promises";
3
+ import { lstat, mkdir, realpath, rm } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  const executeFile = promisify(execFile);
7
- /** The small Git boundary used by repository registration and Task workspaces. */
7
+ /** A remote baseline merge stopped for an explicit Leader resolution. */
8
+ export class RemoteBaselineConflictError extends Error {
9
+ affectedPaths;
10
+ constructor(affectedPaths, message, options) {
11
+ super(message, options);
12
+ this.name = "RemoteBaselineConflictError";
13
+ this.affectedPaths = [...affectedPaths];
14
+ }
15
+ }
16
+ /** The small Git boundary used by project registration and Task workspaces. */
8
17
  export class NodeGitWorkspace {
18
+ async findCommitWithSameTreeInHistory(input) {
19
+ const source = (await this.inspect(input.repositoryPath, input.sourceCommit)).baseCommit;
20
+ const history = (await this.inspect(input.repositoryPath, input.historyHead)).baseCommit;
21
+ if (await this.isAncestor(input.repositoryPath, source, history))
22
+ return source;
23
+ const sourceTree = await gitLine([
24
+ "-C", input.repositoryPath,
25
+ "rev-parse", "--verify", "--end-of-options", `${source}^{tree}`
26
+ ]);
27
+ const pageSize = 1000;
28
+ for (let skip = 0;; skip += pageSize) {
29
+ const output = await git([
30
+ "-C", input.repositoryPath,
31
+ "log", `--max-count=${pageSize}`, `--skip=${skip}`,
32
+ "--format=%H%x09%T", history
33
+ ]);
34
+ const lines = output.trimEnd().split("\n").filter(Boolean);
35
+ for (const line of lines) {
36
+ const [commit, tree, ...extra] = line.split("\t");
37
+ if (extra.length > 0 || commit === undefined || tree === undefined) {
38
+ throw new Error("Git returned invalid publication history.");
39
+ }
40
+ if (tree === sourceTree)
41
+ return commit;
42
+ }
43
+ if (lines.length < pageSize)
44
+ return null;
45
+ }
46
+ }
47
+ /**
48
+ * Resolve the configured Project branch directly from its remote. This is
49
+ * deliberately read-only: unlike `refresh`, it never advances the stable
50
+ * Project checkout or changes its refs.
51
+ */
52
+ async resolveRemoteHead(input) {
53
+ const remote = safeRemote(input.remoteUrl);
54
+ const configuredBranch = await safeFetchBranch(input.branch);
55
+ const branch = configuredBranch === "HEAD"
56
+ ? await resolveRemoteHeadBranch(remote)
57
+ : configuredBranch;
58
+ const commit = await resolveRemoteBranchCommit(remote, branch);
59
+ return { branch, commit };
60
+ }
61
+ /**
62
+ * Merge a remote Project baseline into a clean, managed worktree. The
63
+ * fetch is performed from that worktree and the stable checkout is never
64
+ * touched. Fast-forward updates are preferred; a diverged baseline gets a
65
+ * deterministic merge commit so the caller can run its normal Integration
66
+ * checks and CAS the target ref.
67
+ */
68
+ async mergeRemoteIntoWorktree(input) {
69
+ const initial = await this.inspect(input.repositoryPath, "HEAD");
70
+ if (!await this.isClean(initial.root)) {
71
+ throw new Error("Managed Task/Integration worktree must be clean before remote merge.");
72
+ }
73
+ const fetched = await this.#fetchRemoteHeadIntoWorktree(input);
74
+ const fetchedCommit = fetched.commit;
75
+ const fetchedRef = fetched.fetchedRef;
76
+ try {
77
+ if (fetchedCommit === initial.baseCommit) {
78
+ return { fromCommit: initial.baseCommit, toCommit: fetchedCommit, changed: false };
79
+ }
80
+ if (await gitSucceeds([
81
+ "-C", initial.root,
82
+ "merge-base", "--is-ancestor", fetchedCommit, initial.baseCommit
83
+ ])) {
84
+ // The Task already contains the current remote baseline.
85
+ return { fromCommit: initial.baseCommit, toCommit: initial.baseCommit, changed: false };
86
+ }
87
+ if (await gitSucceeds([
88
+ "-C", initial.root,
89
+ "merge-base", "--is-ancestor", initial.baseCommit, fetchedCommit
90
+ ])) {
91
+ await git(["-C", initial.root, "merge", "--ff-only", "--no-edit", fetchedRef]);
92
+ }
93
+ else {
94
+ try {
95
+ await git([
96
+ "-C", initial.root,
97
+ "-c", "user.name=Yui",
98
+ "-c", "user.email=yui@local",
99
+ "merge", "--no-edit", "--no-ff", fetchedRef
100
+ ]);
101
+ }
102
+ catch (error) {
103
+ const affected = (await git([
104
+ "-C", initial.root,
105
+ "diff", "--name-only", "--diff-filter=U"
106
+ ])).trim().split("\n").filter(Boolean);
107
+ const suffix = affected.length === 0 ? "" : ` (${affected.join(", ")})`;
108
+ throw new RemoteBaselineConflictError(affected, `Remote baseline merge conflicts in managed worktree${suffix}; resolve it in the Integration workspace.`, { cause: error });
109
+ }
110
+ }
111
+ const mergedCommit = (await gitLine([
112
+ "-C", initial.root,
113
+ "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
114
+ ])).toLowerCase();
115
+ if (!await this.isClean(initial.root)) {
116
+ throw new Error("Remote baseline merge left the managed worktree dirty.");
117
+ }
118
+ return { fromCommit: initial.baseCommit, toCommit: mergedCommit, changed: true };
119
+ }
120
+ finally {
121
+ await gitSucceeds(["-C", initial.root, "update-ref", "-d", fetchedRef]);
122
+ }
123
+ }
124
+ /** Fetch a remote branch into a managed worktree without changing HEAD. */
125
+ async fetchRemoteHeadIntoWorktree(input) {
126
+ const fetched = await this.#fetchRemoteHeadIntoWorktree(input);
127
+ try {
128
+ return { branch: fetched.branch, commit: fetched.commit };
129
+ }
130
+ finally {
131
+ await gitSucceeds(["-C", input.repositoryPath, "update-ref", "-d", fetched.fetchedRef]);
132
+ }
133
+ }
134
+ async #fetchRemoteHeadIntoWorktree(input) {
135
+ const repositoryPath = (await this.inspect(input.repositoryPath, "HEAD")).root;
136
+ const remote = safeRemote(input.remoteUrl);
137
+ const configuredBranch = await safeFetchBranch(input.branch);
138
+ const branch = configuredBranch === "HEAD"
139
+ ? await resolveRemoteHeadBranch(remote)
140
+ : configuredBranch;
141
+ const fetchedRef = remoteBaselineRef(remote, branch);
142
+ try {
143
+ await git([
144
+ "-C", repositoryPath,
145
+ "fetch", "--no-tags", "--no-write-fetch-head",
146
+ remote,
147
+ `refs/heads/${branch}:${fetchedRef}`
148
+ ]);
149
+ const commit = await resolveFetchedCommit(repositoryPath, fetchedRef);
150
+ return { branch, commit, fetchedRef };
151
+ }
152
+ catch (error) {
153
+ await gitSucceeds(["-C", repositoryPath, "update-ref", "-d", fetchedRef]);
154
+ throw error;
155
+ }
156
+ }
157
+ async refresh(input) {
158
+ const remote = safeRemote(input.remoteUrl);
159
+ const configuredStableRef = await safeFetchBranch(input.stableRef);
160
+ const initial = await this.inspect(input.repositoryPath, "HEAD");
161
+ await this.#assertRefreshCheckout(initial.root, configuredStableRef, initial.baseCommit);
162
+ const stableBranch = configuredStableRef === "HEAD"
163
+ ? await resolveRemoteHeadBranch(remote)
164
+ : configuredStableRef;
165
+ if (stableBranch !== configuredStableRef) {
166
+ await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
167
+ }
168
+ const stableRemoteRef = `refs/heads/${stableBranch}`;
169
+ await git(["-C", initial.root, "fetch", "--no-tags", remote, stableRemoteRef]);
170
+ const fetchedCommit = (await gitLine([
171
+ "-C", initial.root,
172
+ "rev-parse", "--verify", "--end-of-options", "FETCH_HEAD^{commit}"
173
+ ])).toLowerCase();
174
+ await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
175
+ if (fetchedCommit === initial.baseCommit) {
176
+ return {
177
+ fromCommit: initial.baseCommit,
178
+ toCommit: fetchedCommit,
179
+ changed: false
180
+ };
181
+ }
182
+ if (!await gitSucceeds([
183
+ "-C", initial.root,
184
+ "merge-base", "--is-ancestor", initial.baseCommit, fetchedCommit
185
+ ])) {
186
+ throw new Error(`Project checkout cannot be fast-forwarded from ${initial.baseCommit} to ${fetchedCommit}.`);
187
+ }
188
+ await git([
189
+ "-C", initial.root,
190
+ "merge", "--ff-only", "--no-edit", fetchedCommit
191
+ ]);
192
+ await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
193
+ return {
194
+ fromCommit: initial.baseCommit,
195
+ toCommit: fetchedCommit,
196
+ changed: true
197
+ };
198
+ }
199
+ /**
200
+ * Fetch a Project's configured development branch into a temporary ref and
201
+ * return the commit confirmed by that fetch. The stable checkout is only
202
+ * inspected; it is never checked out, reset, rebased, or fast-forwarded.
203
+ */
204
+ async resolveRemoteBaseline(input) {
205
+ const remote = safeRemote(input.remoteUrl);
206
+ const configuredRef = await safeFetchBranch(input.developmentRef);
207
+ const branch = configuredRef === "HEAD"
208
+ ? await resolveRemoteHeadBranch(remote)
209
+ : configuredRef;
210
+ const repository = await this.inspect(input.repositoryPath, "HEAD");
211
+ const temporaryRef = remoteBaselineRef(remote, branch);
212
+ try {
213
+ await git([
214
+ "-C", repository.root,
215
+ "fetch", "--no-tags", "--no-write-fetch-head",
216
+ remote,
217
+ `refs/heads/${branch}:${temporaryRef}`
218
+ ]);
219
+ const fetchedCommit = await gitLine([
220
+ "-C", repository.root,
221
+ "rev-parse", "--verify", "--end-of-options", `${temporaryRef}^{commit}`
222
+ ]);
223
+ const advertisedCommit = await resolveRemoteBranchCommit(remote, branch);
224
+ if (fetchedCommit.toLowerCase() !== advertisedCommit) {
225
+ throw new Error(`Project remote development branch changed while it was fetched: ${branch}.`);
226
+ }
227
+ return {
228
+ branch,
229
+ commit: fetchedCommit.toLowerCase()
230
+ };
231
+ }
232
+ finally {
233
+ // A temporary namespace keeps the fetch independent from the stable
234
+ // branch and FETCH_HEAD. Cleanup is best effort so the original fetch
235
+ // or consistency error remains the actionable diagnosis.
236
+ await gitSucceeds([
237
+ "-C", repository.root,
238
+ "update-ref", "-d", temporaryRef
239
+ ]);
240
+ }
241
+ }
242
+ async clone(input) {
243
+ const remote = safeRemote(input.remoteUrl);
244
+ const destination = resolve(requireText(input.destination, "Project destination"));
245
+ if (await pathKind(destination) !== undefined) {
246
+ throw new Error(`Project destination already exists: ${destination}.`);
247
+ }
248
+ await canonicalContainer(dirname(destination), true);
249
+ const branch = input.branch === undefined ? undefined : safeRef(input.branch);
250
+ try {
251
+ await git([
252
+ "clone",
253
+ ...(branch === undefined ? [] : ["--branch", branch]),
254
+ "--",
255
+ remote,
256
+ destination
257
+ ]);
258
+ }
259
+ catch (error) {
260
+ await rm(destination, { recursive: true, force: true });
261
+ throw error;
262
+ }
263
+ return this.inspect(destination, "HEAD");
264
+ }
265
+ async ensureLocalBranch(repositoryPath, branch) {
266
+ try {
267
+ return await this.inspect(repositoryPath, branch);
268
+ }
269
+ catch (originalError) {
270
+ const root = (await this.inspect(repositoryPath, "HEAD")).root;
271
+ const name = safeRef(branch);
272
+ if (!await gitSucceeds(["check-ref-format", "--branch", name]))
273
+ throw originalError;
274
+ const remoteRef = `refs/remotes/origin/${name}`;
275
+ if (!await gitSucceeds([
276
+ "-C", root, "show-ref", "--verify", "--quiet", remoteRef
277
+ ]))
278
+ throw originalError;
279
+ await git(["-C", root, "branch", "--", name, remoteRef]);
280
+ return this.inspect(root, name);
281
+ }
282
+ }
9
283
  async inspect(repositoryPath, baseRef = "HEAD") {
10
- const requested = await canonicalDirectory(repositoryPath, "Repository");
284
+ const requested = await canonicalDirectory(repositoryPath, "Project");
11
285
  const ref = safeRef(baseRef);
12
- const root = await canonicalDirectory(await gitLine(["-C", requested, "rev-parse", "--show-toplevel"]), "Repository root");
286
+ const root = await canonicalDirectory(await gitLine(["-C", requested, "rev-parse", "--show-toplevel"]), "Project root");
13
287
  const gitDirectory = await canonicalDirectory(await gitLine([
14
288
  "-C", root, "rev-parse", "--path-format=absolute", "--git-common-dir"
15
289
  ]), "Git common directory");
@@ -19,11 +293,115 @@ export class NodeGitWorkspace {
19
293
  if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/i.test(baseCommit)) {
20
294
  throw new Error("Git returned an invalid base commit.");
21
295
  }
22
- return { root, gitDirectory, baseRef: ref, baseCommit: baseCommit.toLowerCase() };
296
+ return {
297
+ root,
298
+ gitDirectory,
299
+ baseRef: ref,
300
+ baseCommit: baseCommit.toLowerCase()
301
+ };
302
+ }
303
+ async isAncestor(repositoryPath, ancestor, descendant) {
304
+ const root = (await this.inspect(repositoryPath)).root;
305
+ return gitSucceeds([
306
+ "-C", root,
307
+ "merge-base", "--is-ancestor",
308
+ safeRef(ancestor),
309
+ safeRef(descendant)
310
+ ]);
311
+ }
312
+ async headRef(repositoryPath) {
313
+ const requested = await canonicalDirectory(repositoryPath, "Project");
314
+ return gitLine(["-C", requested, "rev-parse", "--abbrev-ref", "HEAD"]);
315
+ }
316
+ async isClean(repositoryPath) {
317
+ const root = (await this.inspect(repositoryPath)).root;
318
+ const status = await git([
319
+ "-C", root, "status", "--porcelain=v1", "--untracked-files=all"
320
+ ]);
321
+ return status.length === 0;
322
+ }
323
+ async mergeWorktree(input) {
324
+ const target = await canonicalDirectory(input.targetPath, "Merge target");
325
+ const sources = input.sourceRefs.map((source) => safeRef(source));
326
+ if (sources.length === 0) {
327
+ throw new Error(`Git merge requires at least one source ref: ${target}.`);
328
+ }
329
+ if (!await this.isClean(target)) {
330
+ throw new Error(`Git merge target must be clean: ${target}.`);
331
+ }
332
+ try {
333
+ // Merge all selected Lane heads in one Git transaction. A conflict in
334
+ // any Lane therefore aborts the complete Candidate materialization
335
+ // instead of leaving an earlier Lane merged into the WorkItem target.
336
+ await git(["-C", target, "merge", "--no-edit", "--no-ff", ...sources]);
337
+ }
338
+ catch (error) {
339
+ try {
340
+ await git(["-C", target, "merge", "--abort"]);
341
+ }
342
+ catch (abortError) {
343
+ throw new Error(`Git merge failed and conflict cleanup could not be completed for ${target}.`, { cause: abortError });
344
+ }
345
+ throw new Error(`Git merge failed for ${target} from ${sources.join(", ")}; Candidate materialization was not completed.`, { cause: error });
346
+ }
347
+ }
348
+ async resetWorktree(input) {
349
+ const target = await canonicalDirectory(input.targetPath, "Reset target");
350
+ const expectedHead = safeRef(input.expectedHead);
351
+ const restoreHead = safeRef(input.restoreHead);
352
+ if (!await this.isClean(target)) {
353
+ throw new Error(`Git reset target must be clean: ${target}.`);
354
+ }
355
+ const currentHead = (await this.inspect(target, "HEAD")).baseCommit;
356
+ if (currentHead !== expectedHead) {
357
+ throw new Error(`Git reset target changed before compensation: ${target} (${currentHead}).`);
358
+ }
359
+ try {
360
+ await git(["-C", target, "reset", "--hard", restoreHead]);
361
+ const restoredHead = (await this.inspect(target, "HEAD")).baseCommit;
362
+ if (restoredHead !== restoreHead || !await this.isClean(target)) {
363
+ throw new Error(`Git reset target did not restore its exact head: ${target}.`);
364
+ }
365
+ }
366
+ catch (error) {
367
+ throw new Error(`Git reset compensation failed for ${target}; Candidate materialization remains retained for diagnosis.`, { cause: error });
368
+ }
369
+ }
370
+ async #assertRefreshCheckout(repositoryPath, stableRef, expectedCommit) {
371
+ if (stableRef !== "HEAD") {
372
+ const currentBranch = await this.headRef(repositoryPath);
373
+ if (currentBranch !== stableRef) {
374
+ const found = currentBranch === "HEAD" ? "detached HEAD" : currentBranch;
375
+ throw new Error(`Project checkout must be on its stable branch: expected ${stableRef}, found ${found}.`);
376
+ }
377
+ }
378
+ const head = await this.inspect(repositoryPath, "HEAD");
379
+ if (head.baseCommit !== expectedCommit) {
380
+ throw new Error("Project checkout changed while it was being refreshed.");
381
+ }
382
+ if (!await this.isClean(head.root)) {
383
+ throw new Error("Project checkout must be clean before it can be refreshed.");
384
+ }
23
385
  }
24
386
  async ensureWorktree(input) {
387
+ return this.#ensureManagedWorktree({
388
+ repositoryPath: input.repositoryPath,
389
+ container: input.container,
390
+ identity: worktreeIdentity(input.taskId, input.roleName),
391
+ baseRef: input.baseRef
392
+ });
393
+ }
394
+ async ensureIntegrationWorktree(input) {
395
+ return this.#ensureManagedWorktree({
396
+ repositoryPath: input.repositoryPath,
397
+ container: input.container,
398
+ identity: integrationWorktreeIdentity(input.taskId, input.integrationId),
399
+ baseRef: input.baseRef
400
+ });
401
+ }
402
+ async #ensureManagedWorktree(input) {
25
403
  const container = await canonicalContainer(input.container, true);
26
- const identity = worktreeIdentity(input.taskId, input.roleName);
404
+ const identity = input.identity;
27
405
  const path = managedPath(container, identity.directory);
28
406
  const kind = await pathKind(path);
29
407
  if (kind === "symlink")
@@ -31,31 +409,55 @@ export class NodeGitWorkspace {
31
409
  if (kind === "directory") {
32
410
  // A worktree already created before a crash remains usable even if the
33
411
  // original base branch/tag is later deleted.
34
- const repository = await this.inspect(input.repositoryPath);
35
- await assertOwnedWorktree(repository, container, path);
412
+ const project = await this.inspect(input.repositoryPath);
413
+ await assertOwnedWorktree(project, container, path);
36
414
  await assertExpectedBranch(path, identity.branch);
37
415
  const head = await gitLine([
38
416
  "-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
39
417
  ]);
40
418
  return { path, branch: identity.branch, baseCommit: head.toLowerCase() };
41
419
  }
42
- const repository = await this.inspect(input.repositoryPath, input.baseRef);
420
+ const project = await this.inspect(input.repositoryPath, input.baseRef);
43
421
  await canonicalContainer(dirname(path), true);
44
422
  const branchRef = `refs/heads/${identity.branch}`;
45
423
  const branchExists = await gitSucceeds([
46
- "-C", repository.root, "show-ref", "--verify", "--quiet", branchRef
424
+ "-C", project.root, "show-ref", "--verify", "--quiet", branchRef
47
425
  ]);
48
426
  await git(branchExists
49
- ? ["-C", repository.root, "worktree", "add", "--", path, identity.branch]
427
+ ? ["-C", project.root, "worktree", "add", "--", path, identity.branch]
50
428
  : [
51
- "-C", repository.root, "worktree", "add", "-b", identity.branch,
52
- "--", path, repository.baseCommit
429
+ "-C", project.root, "worktree", "add", "-b", identity.branch,
430
+ "--", path, project.baseCommit
53
431
  ]);
54
- await assertOwnedWorktree(repository, container, path);
432
+ await assertOwnedWorktree(project, container, path);
55
433
  await assertExpectedBranch(path, identity.branch);
56
- return { path, branch: identity.branch, baseCommit: repository.baseCommit };
434
+ const head = await gitLine([
435
+ "-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
436
+ ]);
437
+ return { path, branch: identity.branch, baseCommit: head.toLowerCase() };
57
438
  }
58
439
  async removeWorktree(input) {
440
+ const state = await this.inspectWorktree(input);
441
+ if (state === "dirty")
442
+ return state;
443
+ const container = resolve(input.container);
444
+ const identity = worktreeIdentity(input.taskId, input.roleName);
445
+ const path = managedPath(container, identity.directory);
446
+ const project = await this.inspect(input.repositoryPath);
447
+ if (state === "missing") {
448
+ if (input.deleteBranch === true) {
449
+ await git(["-C", project.root, "worktree", "prune"]);
450
+ await deleteBranchIfPresent(project.root, identity.branch);
451
+ }
452
+ return state;
453
+ }
454
+ await git(["-C", project.root, "worktree", "remove", "--", path]);
455
+ if (input.deleteBranch === true) {
456
+ await deleteBranchIfPresent(project.root, identity.branch);
457
+ }
458
+ return "removed";
459
+ }
460
+ async inspectWorktree(input) {
59
461
  const container = resolve(input.container);
60
462
  const path = managedPath(container, worktreeIdentity(input.taskId, input.roleName).directory);
61
463
  const kind = await pathKind(path);
@@ -64,15 +466,67 @@ export class NodeGitWorkspace {
64
466
  if (kind === "symlink")
65
467
  throw new Error("Managed worktree path must not be a symbolic link.");
66
468
  const canonicalContainerPath = await canonicalContainer(container, false);
67
- const repository = await this.inspect(input.repositoryPath);
68
- await assertOwnedWorktree(repository, canonicalContainerPath, path);
469
+ const project = await this.inspect(input.repositoryPath);
470
+ await assertOwnedWorktree(project, canonicalContainerPath, path);
69
471
  const porcelain = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
70
- if (porcelain.length > 0)
71
- return "dirty";
72
- await git(["-C", repository.root, "worktree", "remove", "--", path]);
73
- return "removed";
472
+ return porcelain.length > 0 ? "dirty" : "clean";
473
+ }
474
+ async removeIntegrationWorktree(input) {
475
+ return this.#removeManagedWorktree({
476
+ repositoryPath: input.repositoryPath,
477
+ container: input.container,
478
+ identity: integrationWorktreeIdentity(input.taskId, input.integrationId),
479
+ discardChanges: input.discardChanges
480
+ });
481
+ }
482
+ async #removeManagedWorktree(input) {
483
+ const container = resolve(input.container);
484
+ const path = managedPath(container, input.identity.directory);
485
+ const kind = await pathKind(path);
486
+ if (kind === "symlink")
487
+ throw new Error("Managed worktree path must not be a symbolic link.");
488
+ const repository = await this.inspect(input.repositoryPath);
489
+ if (kind === "directory") {
490
+ const canonicalContainerPath = await canonicalContainer(container, false);
491
+ await assertOwnedWorktree(repository, canonicalContainerPath, path);
492
+ await assertExpectedBranch(path, input.identity.branch);
493
+ if (input.discardChanges === true) {
494
+ await git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
495
+ }
496
+ else {
497
+ const porcelain = await git([
498
+ "-C", path, "status", "--porcelain=v1", "--untracked-files=all"
499
+ ]);
500
+ if (porcelain.length > 0)
501
+ return "dirty";
502
+ if (input.expectedBaseCommit !== undefined
503
+ && input.allowCommittedChanges !== true) {
504
+ const head = await gitLine([
505
+ "-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
506
+ ]);
507
+ if (head.toLowerCase() !== input.expectedBaseCommit.toLowerCase())
508
+ return "dirty";
509
+ }
510
+ await git(["-C", repository.root, "worktree", "remove", "--", path]);
511
+ }
512
+ }
513
+ const branchRef = `refs/heads/${input.identity.branch}`;
514
+ if (await gitSucceeds([
515
+ "-C", repository.root, "show-ref", "--verify", "--quiet", branchRef
516
+ ])) {
517
+ await git(["-C", repository.root, "branch", "-D", "--", input.identity.branch]);
518
+ }
519
+ return kind === "directory" ? "removed" : "missing";
74
520
  }
75
521
  }
522
+ async function deleteBranchIfPresent(repositoryRoot, branch) {
523
+ const branchRef = `refs/heads/${branch}`;
524
+ if (!await gitSucceeds([
525
+ "-C", repositoryRoot, "show-ref", "--verify", "--quiet", branchRef
526
+ ]))
527
+ return;
528
+ await git(["-C", repositoryRoot, "branch", "-D", "--", branch]);
529
+ }
76
530
  export function worktreeIdentity(taskId, roleName) {
77
531
  const taskKey = safeIdentity(taskId, "Task id");
78
532
  const roleKey = safeIdentity(roleName, "Role name");
@@ -81,6 +535,14 @@ export function worktreeIdentity(taskId, roleName) {
81
535
  branch: `yui/${gitRefSegment(taskKey)}/${gitRefSegment(roleKey)}`
82
536
  };
83
537
  }
538
+ export function integrationWorktreeIdentity(taskId, integrationId) {
539
+ const taskKey = safeIdentity(taskId, "Task id");
540
+ const integrationKey = safeIdentity(integrationId, "Integration Attempt id");
541
+ return {
542
+ directory: join(taskKey, "integrations", integrationKey),
543
+ branch: `yui/${gitRefSegment(taskKey)}/integration/${gitRefSegment(integrationKey)}`
544
+ };
545
+ }
84
546
  function gitRefSegment(identity) {
85
547
  if (/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(identity)
86
548
  && !identity.includes("..")
@@ -97,7 +559,7 @@ async function assertExpectedBranch(path, expected) {
97
559
  throw new Error(`Managed worktree is on an unexpected branch: ${branch}.`);
98
560
  }
99
561
  }
100
- async function assertOwnedWorktree(repository, container, path) {
562
+ async function assertOwnedWorktree(project, container, path) {
101
563
  const canonicalPath = await canonicalDirectory(path, "Managed worktree");
102
564
  assertContained(container, canonicalPath);
103
565
  if (canonicalPath !== path)
@@ -106,8 +568,8 @@ async function assertOwnedWorktree(repository, container, path) {
106
568
  if (root !== path)
107
569
  throw new Error("Managed worktree root does not match its deterministic path.");
108
570
  const common = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"]), "Managed Git common directory");
109
- if (common !== repository.gitDirectory) {
110
- throw new Error("Managed worktree belongs to another repository.");
571
+ if (common !== project.gitDirectory) {
572
+ throw new Error("Managed worktree belongs to another project.");
111
573
  }
112
574
  }
113
575
  async function canonicalContainer(path, create) {
@@ -198,6 +660,98 @@ function safeRef(value) {
198
660
  throw new Error("Git base ref is invalid.");
199
661
  return ref;
200
662
  }
663
+ async function safeFetchBranch(value) {
664
+ const configuredRef = safeRef(value);
665
+ if (configuredRef === "HEAD")
666
+ return configuredRef;
667
+ const branchPrefix = "refs/heads/";
668
+ const branch = configuredRef.startsWith(branchPrefix)
669
+ ? configuredRef.slice(branchPrefix.length)
670
+ : configuredRef;
671
+ if (!await gitSucceeds(["check-ref-format", "--branch", branch])) {
672
+ throw new Error("Git stable branch is invalid.");
673
+ }
674
+ return branch;
675
+ }
676
+ async function resolveRemoteHeadBranch(remote) {
677
+ let output;
678
+ try {
679
+ output = await git(["ls-remote", "--symref", remote, "HEAD"]);
680
+ }
681
+ catch (error) {
682
+ const detail = error instanceof Error ? error.message : "Git command failed.";
683
+ throw new Error(`Project remote HEAD could not be resolved: ${detail}`, { cause: error });
684
+ }
685
+ const symbolicLines = output.split("\n").filter((line) => line.startsWith("ref: "));
686
+ const prefix = "ref: refs/heads/";
687
+ const suffix = "\tHEAD";
688
+ const symbolic = symbolicLines.length === 1 ? symbolicLines[0] : "";
689
+ if (!symbolic.startsWith(prefix) || !symbolic.endsWith(suffix)) {
690
+ throw invalidRemoteHead();
691
+ }
692
+ const branch = symbolic.slice(prefix.length, -suffix.length);
693
+ if (branch === "HEAD")
694
+ throw invalidRemoteHead();
695
+ try {
696
+ return await safeFetchBranch(branch);
697
+ }
698
+ catch {
699
+ throw invalidRemoteHead();
700
+ }
701
+ }
702
+ async function resolveRemoteBranchCommit(remote, branch) {
703
+ const target = `refs/heads/${branch}`;
704
+ let output;
705
+ try {
706
+ output = await git(["ls-remote", "--refs", remote, target]);
707
+ }
708
+ catch (error) {
709
+ const detail = error instanceof Error ? error.message : "Git command failed.";
710
+ throw new Error(`Project remote development branch could not be resolved: ${branch}: ${detail}`, { cause: error });
711
+ }
712
+ const matches = output
713
+ .split("\n")
714
+ .map((line) => line.trim())
715
+ .filter((line) => line.length > 0)
716
+ .map((line) => line.split(/\s+/u))
717
+ .filter(([commit, ref]) => ref === target && commit !== undefined);
718
+ if (matches.length !== 1) {
719
+ throw new Error(`Project remote development branch could not be resolved: ${branch}.`);
720
+ }
721
+ const commit = matches[0][0];
722
+ if (!isCommit(commit)) {
723
+ throw new Error(`Project remote development branch returned an invalid commit: ${branch}.`);
724
+ }
725
+ return commit.toLowerCase();
726
+ }
727
+ async function resolveFetchedCommit(repositoryPath, fetchedRef) {
728
+ const commit = (await gitLine([
729
+ "-C", repositoryPath,
730
+ "rev-parse", "--verify", "--end-of-options", `${fetchedRef}^{commit}`
731
+ ])).toLowerCase();
732
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit)) {
733
+ throw new Error("Git returned an invalid fetched remote commit.");
734
+ }
735
+ return commit;
736
+ }
737
+ function remoteBaselineRef(remote, branch) {
738
+ const identity = `${remote}\0${branch}\0${process.pid}\0${Date.now()}\0${Math.random()}`;
739
+ const digest = createHash("sha256").update(identity).digest("hex");
740
+ return `refs/yui/task-baselines/${digest}`;
741
+ }
742
+ function invalidRemoteHead() {
743
+ return new Error("Project remote HEAD must identify a valid symbolic branch under refs/heads/.");
744
+ }
745
+ function isCommit(value) {
746
+ return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
747
+ }
748
+ function safeRemote(value) {
749
+ const remote = requireText(value, "Git remote URL");
750
+ if (remote.startsWith("-") || /[\r\n]/.test(remote)) {
751
+ throw new Error("Git remote URL is invalid.");
752
+ }
753
+ return remote;
754
+ }
201
755
  function requireText(value, label) {
202
756
  if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
203
757
  throw new Error(`${label} is required.`);