@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
@@ -0,0 +1,650 @@
1
+ import { readdir, rm } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import { usageError } from "../errors/cliError.js";
4
+ import { defaultTableWidth, renderTable } from "../output/table.js";
5
+ import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
6
+ import { addProjectKnowledge, createProject, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProjectName } from "../repository/project.js";
7
+ export async function runProjectCommand(args, store, options = {}) {
8
+ const [command, ...rest] = args;
9
+ if (command === "add" || command === "clone") {
10
+ const project = command === "add"
11
+ ? await addProject(rest, store, options)
12
+ : await cloneProject(rest, store, options);
13
+ return { output: renderAddedProject(project), data: { project } };
14
+ }
15
+ if (command === "list") {
16
+ return { output: listProjects(rest, store), data: { projects: store.listProjects() } };
17
+ }
18
+ if (command === "refresh") {
19
+ const refreshed = await refreshProject(rest, store, options);
20
+ return {
21
+ output: refreshed.changed
22
+ ? `Refreshed project ${refreshed.project.id}: ${refreshed.fromCommit} -> ${refreshed.toCommit}\n`
23
+ : `Project ${refreshed.project.id} is already current at ${refreshed.toCommit}\n`,
24
+ data: refreshed
25
+ };
26
+ }
27
+ if (command === "update") {
28
+ const project = await updateProject(rest, store, options);
29
+ return {
30
+ output: `Updated project ${project.id}\n`,
31
+ data: { project }
32
+ };
33
+ }
34
+ if (command === "discover") {
35
+ const discovered = await discoverProjects(rest, store, options);
36
+ return { output: renderDiscoveredProjects(discovered), data: { projects: discovered } };
37
+ }
38
+ if (command === "show") {
39
+ const project = rest[0] === undefined ? null : resolveProject(store.listProjects(), rest[0]);
40
+ return { output: showProject(rest, store), ...(project === null ? {} : { data: { project } }) };
41
+ }
42
+ if (command === "knowledge") {
43
+ const result = projectKnowledge(rest, store, options);
44
+ const project = resolveProject(store.listProjects(), result.projectId);
45
+ if (project === null)
46
+ return { output: result.output };
47
+ if (result.knowledgeId !== undefined) {
48
+ const knowledge = project.knowledge.find(({ id }) => id === result.knowledgeId);
49
+ return { output: result.output, data: { project, knowledge } };
50
+ }
51
+ const knowledge = result.all
52
+ ? project.knowledge
53
+ : project.knowledge.filter(({ status }) => status === "active");
54
+ return { output: result.output, data: { project, knowledge } };
55
+ }
56
+ throw usageError(command === undefined
57
+ ? "Project command is required."
58
+ : `Unknown command: project ${command}`);
59
+ }
60
+ async function refreshProject(args, store, options) {
61
+ if (args.length !== 1) {
62
+ throw usageError("Project refresh usage: yui project refresh <project>.");
63
+ }
64
+ const project = requireProject(store, args[0]);
65
+ if (project.remoteUrl === undefined) {
66
+ throw usageError(`Project refresh requires a remote URL: ${project.id}.`);
67
+ }
68
+ if (project.stableBranch !== project.developmentBranch) {
69
+ throw usageError(`Project refresh requires matching stable and development branches: ${project.id}.`);
70
+ }
71
+ const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
72
+ repositoryPath: project.path,
73
+ remoteUrl: project.remoteUrl,
74
+ stableRef: project.stableBranch
75
+ });
76
+ return { project, ...refreshed };
77
+ }
78
+ async function discoverProjects(args, store, options) {
79
+ if (args.length > 1) {
80
+ throw usageError("Project discover usage: yui project discover [name].");
81
+ }
82
+ const query = args[0]?.trim().toLocaleLowerCase();
83
+ const workspace = store.getConfig().defaultWorkspace;
84
+ if (workspace === undefined)
85
+ throw usageError("No default workspace is configured; run yui setup.");
86
+ const git = options.git ?? new NodeGitWorkspace();
87
+ const entries = await readdir(workspace, { withFileTypes: true });
88
+ const discovered = [];
89
+ for (const entry of entries) {
90
+ if (!entry.isDirectory() || entry.name === "worktree"
91
+ || (query !== undefined && !entry.name.toLocaleLowerCase().includes(query)))
92
+ continue;
93
+ try {
94
+ const candidate = resolve(workspace, entry.name);
95
+ const inspected = await git.inspect(candidate, "HEAD");
96
+ if (inspected.root !== candidate)
97
+ continue;
98
+ const registered = store.listProjects().find(({ path }) => path === inspected.root);
99
+ discovered.push({
100
+ name: entry.name,
101
+ path: inspected.root,
102
+ branch: await git.headRef(inspected.root),
103
+ ...(registered === undefined ? {} : { registeredProjectId: registered.id })
104
+ });
105
+ }
106
+ catch {
107
+ // The workspace may contain ordinary directories; only Git roots are candidates.
108
+ }
109
+ }
110
+ return discovered.sort((left, right) => left.name.localeCompare(right.name));
111
+ }
112
+ function renderDiscoveredProjects(projects) {
113
+ if (projects.length === 0)
114
+ return "No Git projects found in the workspace.\n";
115
+ return `${renderTable("Workspace projects", [
116
+ { header: "Name", minWidth: 4, maxWidth: 28 },
117
+ { header: "Path", minWidth: 8, maxWidth: 64 },
118
+ { header: "Project", minWidth: 8, maxWidth: 24 }
119
+ ], projects.map((project) => [
120
+ project.name,
121
+ project.path,
122
+ project.registeredProjectId ?? "-"
123
+ ]), defaultTableWidth())}\n`;
124
+ }
125
+ async function cloneProject(args, store, options) {
126
+ const usage = "Project clone usage: yui project clone <name> <remote> [--alias <name> ...] [--stable <ref>] [--development <ref>].";
127
+ const parsed = parseCloneArguments(args, usage);
128
+ const workspace = store.getConfig().defaultWorkspace;
129
+ if (workspace === undefined)
130
+ throw usageError("No default workspace is configured; run yui setup.");
131
+ const git = options.git ?? new NodeGitWorkspace();
132
+ const name = validateProjectName(parsed.name);
133
+ const workspaceRoot = resolve(workspace);
134
+ const destination = resolve(workspaceRoot, name);
135
+ if (dirname(destination) !== workspaceRoot) {
136
+ throw usageError("Project clone destination must be directly inside the configured workspace.");
137
+ }
138
+ assertOutsideManagedWorktrees(destination, workspace);
139
+ const stableRef = parsed.stable ?? "HEAD";
140
+ const developmentRef = parsed.development ?? stableRef;
141
+ const projectId = store.nextProjectId();
142
+ const now = (options.now ?? (() => new Date()))();
143
+ const candidate = createProject(projectId, name, destination, { stable: stableRef, development: developmentRef }, now, { aliases: parsed.aliases, remoteUrl: parsed.remote });
144
+ assertProjectAvailable(store, candidate);
145
+ let cloned = false;
146
+ try {
147
+ const head = await git.clone({
148
+ remoteUrl: parsed.remote,
149
+ destination,
150
+ ...(stableRef === "HEAD" ? {} : { branch: stableRef })
151
+ });
152
+ cloned = true;
153
+ const stable = stableRef === "HEAD" ? head : await git.inspect(destination, stableRef);
154
+ if (head.baseCommit !== stable.baseCommit) {
155
+ throw new Error(`Project checkout is not on its stable ref: ${stableRef}.`);
156
+ }
157
+ if (developmentRef !== stableRef) {
158
+ await git.ensureLocalBranch(destination, developmentRef);
159
+ }
160
+ const project = createProject(projectId, name, head.root, { stable: stableRef, development: developmentRef }, now, { aliases: parsed.aliases, remoteUrl: parsed.remote });
161
+ assertProjectAvailable(store, project);
162
+ const created = store.createProjectIfAbsent(project);
163
+ if (created === null)
164
+ throw usageError(`Project already exists: ${project.name}.`);
165
+ return created;
166
+ }
167
+ catch (error) {
168
+ if (cloned && !store.listProjects().some(({ path }) => path === destination)) {
169
+ await rm(destination, { recursive: true, force: true });
170
+ }
171
+ throw error;
172
+ }
173
+ }
174
+ async function addProject(args, store, options) {
175
+ const usage = "Project add usage: yui project add <name> <path> [--alias <name> ...] [--remote <url>] [--stable <ref>] [--development <ref>].";
176
+ const parsed = parseAddArguments(args, usage);
177
+ assertOutsideManagedWorktrees(parsed.path, store.getConfig().defaultWorkspace);
178
+ const git = options.git ?? new NodeGitWorkspace();
179
+ const head = await git.inspect(parsed.path, "HEAD");
180
+ assertOutsideManagedWorktrees(head.root, store.getConfig().defaultWorkspace);
181
+ if (!await git.isClean(head.root)) {
182
+ throw usageError("Project checkout must be clean before it can be registered.");
183
+ }
184
+ const stableRef = parsed.stable ?? head.baseRef;
185
+ const stable = stableRef === "HEAD" ? head : await git.inspect(head.root, stableRef);
186
+ if (head.baseCommit !== stable.baseCommit) {
187
+ throw usageError(`Project checkout must be on its stable ref: ${stableRef}.`);
188
+ }
189
+ const developmentRef = parsed.development ?? stableRef;
190
+ const project = createProject(store.nextProjectId(), parsed.name, head.root, {
191
+ stable: stableRef,
192
+ development: developmentRef
193
+ }, (options.now ?? (() => new Date()))(), { aliases: parsed.aliases, ...(parsed.remote === undefined ? {} : { remoteUrl: parsed.remote }) });
194
+ assertProjectAvailable(store, project);
195
+ if (developmentRef !== stableRef)
196
+ await git.ensureLocalBranch(head.root, developmentRef);
197
+ const created = store.createProjectIfAbsent(project);
198
+ if (created === null)
199
+ throw usageError(`Project already exists: ${project.name}.`);
200
+ return created;
201
+ }
202
+ async function updateProject(args, store, options) {
203
+ const usage = "Project update usage: yui project update <project> [--alias <name> ...|--clear-aliases] [--remote <url>|--clear-remote] [--stable <ref>] [--development <ref>].";
204
+ const parsed = parseProjectUpdateArguments(args, usage);
205
+ const current = requireProject(store, parsed.reference);
206
+ const now = (options.now ?? (() => new Date()))();
207
+ const patch = {
208
+ ...(parsed.aliases === undefined ? {} : { aliases: parsed.aliases }),
209
+ ...(parsed.remote === undefined ? {} : { remoteUrl: parsed.remote }),
210
+ ...(parsed.stable === undefined ? {} : { stableBranch: parsed.stable }),
211
+ ...(parsed.development === undefined
212
+ ? {}
213
+ : { developmentBranch: parsed.development })
214
+ };
215
+ assertProjectAvailable(store, updateProjectMetadata(current, patch, now), current.id);
216
+ const git = options.git ?? new NodeGitWorkspace();
217
+ if (parsed.stable !== undefined) {
218
+ const head = await git.inspect(current.path, "HEAD");
219
+ if (!await git.isClean(head.root)) {
220
+ throw usageError("Project checkout must be clean before its stable ref can be changed.");
221
+ }
222
+ const stable = parsed.stable === "HEAD"
223
+ ? head
224
+ : await git.inspect(current.path, parsed.stable);
225
+ if (head.baseCommit !== stable.baseCommit) {
226
+ throw usageError(`Project checkout must be on its stable ref: ${parsed.stable}.`);
227
+ }
228
+ }
229
+ const developmentBranch = parsed.development ?? current.developmentBranch;
230
+ const stableBranch = parsed.stable ?? current.stableBranch;
231
+ if (parsed.development !== undefined && developmentBranch !== stableBranch) {
232
+ await git.ensureLocalBranch(current.path, developmentBranch);
233
+ }
234
+ return store.transaction((tx) => {
235
+ const latest = requireProject(tx, current.id);
236
+ if (latest.path !== current.path) {
237
+ throw new Error(`Project path changed while updating: ${current.id}.`);
238
+ }
239
+ const updated = updateProjectMetadata(latest, patch, now);
240
+ assertProjectAvailable(tx, updated, current.id);
241
+ tx.saveProject(updated);
242
+ return updated;
243
+ });
244
+ }
245
+ function renderAddedProject(created) {
246
+ return [
247
+ `Added project ${created.id}`,
248
+ `Name: ${created.name}`,
249
+ `Path: ${created.path}`,
250
+ `Stable: ${created.stableBranch}`,
251
+ `Development: ${created.developmentBranch}`
252
+ ].join("\n").concat("\n");
253
+ }
254
+ function assertProjectAvailable(store, project, exceptId) {
255
+ const others = store.listProjects().filter(({ id }) => id !== exceptId);
256
+ if (others.some((entry) => entry.path === project.path)) {
257
+ throw usageError(`Project path is already registered: ${project.path}.`);
258
+ }
259
+ for (const reference of [project.id, project.name, ...project.aliases]) {
260
+ if (resolveProject(others, reference) !== null) {
261
+ throw usageError(`Project reference is already registered: ${reference}.`);
262
+ }
263
+ }
264
+ }
265
+ function assertOutsideManagedWorktrees(path, workspace) {
266
+ if (workspace === undefined)
267
+ return;
268
+ const managedRoot = join(resolve(workspace), "worktree");
269
+ const candidate = resolve(path);
270
+ const fromManagedRoot = relative(managedRoot, candidate);
271
+ const toManagedRoot = relative(candidate, managedRoot);
272
+ if (isContainedPath(fromManagedRoot) || isContainedPath(toManagedRoot)) {
273
+ throw usageError("Project path is reserved for managed worktrees.");
274
+ }
275
+ }
276
+ function isContainedPath(relativePath) {
277
+ return relativePath === ""
278
+ || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
279
+ }
280
+ function listProjects(args, store) {
281
+ if (args.length !== 0)
282
+ throw usageError("Project list usage: yui project list.");
283
+ const projects = store.listProjects();
284
+ if (projects.length === 0)
285
+ return "No projects found.\n";
286
+ return `${renderTable("Projects", [
287
+ { header: "Project", minWidth: 8, maxWidth: 24 },
288
+ { header: "Name", minWidth: 4, maxWidth: 28 },
289
+ { header: "Path", minWidth: 8, maxWidth: 64 },
290
+ { header: "Stable", minWidth: 6, maxWidth: 24 },
291
+ { header: "Development", minWidth: 11, maxWidth: 24 }
292
+ ], projects.map((project) => [
293
+ project.id, project.name, project.path, project.stableBranch, project.developmentBranch
294
+ ]), defaultTableWidth())}\n`;
295
+ }
296
+ function showProject(args, store) {
297
+ if (args.length !== 1)
298
+ throw usageError("Project show usage: yui project show <project>.");
299
+ const project = requireProject(store, args[0]);
300
+ return [
301
+ `Project: ${project.id}`,
302
+ `Name: ${project.name}`,
303
+ `Aliases: ${project.aliases.length === 0 ? "-" : project.aliases.join(", ")}`,
304
+ `Path: ${project.path}`,
305
+ ...(project.remoteUrl === undefined ? [] : [`Remote: ${project.remoteUrl}`]),
306
+ `Stable: ${project.stableBranch}`,
307
+ `Development: ${project.developmentBranch}`,
308
+ `Knowledge: ${project.knowledge.filter(({ status }) => status === "active").length} active`
309
+ ].join("\n").concat("\n");
310
+ }
311
+ function projectKnowledge(args, store, options) {
312
+ const [command, ...rest] = args;
313
+ if (command === "add") {
314
+ const usage = "Project knowledge add usage: yui project knowledge add <project> <title> --body <text>.";
315
+ const parsed = parseSingleOption(rest, "--body", usage);
316
+ if (parsed.positionals.length !== 2)
317
+ throw usageError(usage);
318
+ const added = store.transaction((tx) => {
319
+ const project = requireProject(tx, parsed.positionals[0]);
320
+ const id = nextKnowledgeId(project);
321
+ tx.saveProject(addProjectKnowledge(project, id, parsed.positionals[1], parsed.value, (options.now ?? (() => new Date()))()));
322
+ return { id, projectId: project.id };
323
+ });
324
+ return {
325
+ output: `Added project knowledge ${added.id} to ${added.projectId}\n`,
326
+ projectId: added.projectId,
327
+ knowledgeId: added.id
328
+ };
329
+ }
330
+ if (command === "list") {
331
+ const all = rest.includes("--all");
332
+ const positionals = rest.filter((value) => value !== "--all");
333
+ if (positionals.length !== 1 || rest.some((value) => value.startsWith("--") && value !== "--all")) {
334
+ throw usageError("Project knowledge list usage: yui project knowledge list <project> [--all].");
335
+ }
336
+ const project = requireProject(store, positionals[0]);
337
+ const entries = all
338
+ ? project.knowledge
339
+ : project.knowledge.filter(({ status }) => status === "active");
340
+ if (entries.length === 0) {
341
+ return {
342
+ output: "No project knowledge found.\n",
343
+ projectId: project.id,
344
+ all
345
+ };
346
+ }
347
+ return {
348
+ output: `${renderTable(`Project knowledge: ${project.name}`, [
349
+ { header: "Knowledge", minWidth: 9, maxWidth: 24 },
350
+ { header: "Title", minWidth: 8, maxWidth: 64 },
351
+ { header: "Status", minWidth: 6, maxWidth: 10 }
352
+ ], entries.map((entry) => [entry.id, entry.title, entry.status]), defaultTableWidth())}\n`,
353
+ projectId: project.id,
354
+ all
355
+ };
356
+ }
357
+ if (command === "show") {
358
+ if (rest.length !== 2) {
359
+ throw usageError("Project knowledge show usage: yui project knowledge show <project> <knowledge>.");
360
+ }
361
+ const project = requireProject(store, rest[0]);
362
+ const entry = project.knowledge.find(({ id }) => id === rest[1]);
363
+ if (entry === undefined)
364
+ throw usageError(`Project knowledge not found: ${rest[1]}.`);
365
+ return {
366
+ output: [
367
+ `Knowledge: ${entry.id}`,
368
+ `Project: ${project.id}`,
369
+ `Title: ${entry.title}`,
370
+ `Status: ${entry.status}`,
371
+ "",
372
+ entry.body
373
+ ].join("\n").concat("\n"),
374
+ projectId: project.id,
375
+ knowledgeId: entry.id
376
+ };
377
+ }
378
+ if (command === "update") {
379
+ const usage = "Project knowledge update usage: yui project knowledge update <project> <knowledge> [--title <text>] [--body <text>].";
380
+ const parsed = parseKnowledgeUpdateArguments(rest, usage);
381
+ const updated = store.transaction((tx) => {
382
+ const project = requireProject(tx, parsed.project);
383
+ const next = updateProjectKnowledge(project, parsed.id, {
384
+ ...(parsed.title === undefined ? {} : { title: parsed.title }),
385
+ ...(parsed.body === undefined ? {} : { body: parsed.body })
386
+ }, (options.now ?? (() => new Date()))());
387
+ tx.saveProject(next);
388
+ return next;
389
+ });
390
+ return {
391
+ output: `Updated project knowledge ${parsed.id} in ${updated.id}\n`,
392
+ projectId: updated.id,
393
+ knowledgeId: parsed.id
394
+ };
395
+ }
396
+ if (command === "retire") {
397
+ if (rest.length !== 2) {
398
+ throw usageError("Project knowledge retire usage: yui project knowledge retire <project> <knowledge>.");
399
+ }
400
+ const updated = store.transaction((tx) => {
401
+ const project = requireProject(tx, rest[0]);
402
+ const next = retireProjectKnowledge(project, rest[1], (options.now ?? (() => new Date()))());
403
+ tx.saveProject(next);
404
+ return next;
405
+ });
406
+ return {
407
+ output: `Retired project knowledge ${rest[1]} in ${updated.id}\n`,
408
+ projectId: updated.id,
409
+ knowledgeId: rest[1]
410
+ };
411
+ }
412
+ throw usageError(command === undefined
413
+ ? "Project knowledge command is required."
414
+ : `Unknown command: project knowledge ${command}`);
415
+ }
416
+ function requireProject(store, reference) {
417
+ const project = resolveProject(store.listProjects(), reference);
418
+ if (project === null)
419
+ throw usageError(`Project not found: ${reference}.`);
420
+ return project;
421
+ }
422
+ function nextKnowledgeId(project) {
423
+ const used = new Set(project.knowledge.map(({ id }) => id));
424
+ for (let index = 1;; index += 1) {
425
+ const id = `knowledge-${index}`;
426
+ if (!used.has(id))
427
+ return id;
428
+ }
429
+ }
430
+ function parseAddArguments(args, usage) {
431
+ const positionals = [];
432
+ const aliases = [];
433
+ let remote;
434
+ let stable;
435
+ let development;
436
+ for (let index = 0; index < args.length; index += 1) {
437
+ const value = args[index];
438
+ if (["--alias", "--remote", "--stable", "--development"].includes(value)) {
439
+ const next = args[index + 1];
440
+ if (next === undefined || next.startsWith("--"))
441
+ throw usageError(`${value} is required. ${usage}`);
442
+ if (value === "--alias")
443
+ aliases.push(requireText(next, value));
444
+ else if (value === "--remote") {
445
+ if (remote !== undefined)
446
+ throw usageError(`--remote may only be provided once. ${usage}`);
447
+ remote = requireText(next, value);
448
+ }
449
+ else if (value === "--stable") {
450
+ if (stable !== undefined)
451
+ throw usageError(`--stable may only be provided once. ${usage}`);
452
+ stable = requireText(next, value);
453
+ }
454
+ else {
455
+ if (development !== undefined)
456
+ throw usageError(`--development may only be provided once. ${usage}`);
457
+ development = requireText(next, value);
458
+ }
459
+ index += 1;
460
+ continue;
461
+ }
462
+ if (value.startsWith("--"))
463
+ throw usageError(`Unknown option: ${value}. ${usage}`);
464
+ positionals.push(value);
465
+ }
466
+ if (positionals.length !== 2)
467
+ throw usageError(usage);
468
+ return {
469
+ name: requireText(positionals[0], "Project name"),
470
+ path: requireText(positionals[1], "Project path"),
471
+ aliases,
472
+ ...(remote === undefined ? {} : { remote }),
473
+ ...(stable === undefined ? {} : { stable }),
474
+ ...(development === undefined ? {} : { development })
475
+ };
476
+ }
477
+ function parseProjectUpdateArguments(args, usage) {
478
+ const positionals = [];
479
+ const aliases = [];
480
+ let aliasesProvided = false;
481
+ let clearAliases = false;
482
+ let remote;
483
+ let stable;
484
+ let development;
485
+ for (let index = 0; index < args.length; index += 1) {
486
+ const value = args[index];
487
+ if (value === "--clear-aliases") {
488
+ clearAliases = true;
489
+ continue;
490
+ }
491
+ if (value === "--clear-remote") {
492
+ if (remote !== undefined)
493
+ throw usageError(`Remote may only be changed once. ${usage}`);
494
+ remote = null;
495
+ continue;
496
+ }
497
+ if (["--alias", "--remote", "--stable", "--development"].includes(value)) {
498
+ const next = args[index + 1];
499
+ if (next === undefined || next.startsWith("--"))
500
+ throw usageError(`${value} is required. ${usage}`);
501
+ if (value === "--alias") {
502
+ aliasesProvided = true;
503
+ aliases.push(requireText(next, value));
504
+ }
505
+ else if (value === "--remote") {
506
+ if (remote !== undefined)
507
+ throw usageError(`Remote may only be changed once. ${usage}`);
508
+ remote = requireText(next, value);
509
+ }
510
+ else if (value === "--stable") {
511
+ if (stable !== undefined)
512
+ throw usageError(`--stable may only be provided once. ${usage}`);
513
+ stable = requireText(next, value);
514
+ }
515
+ else {
516
+ if (development !== undefined)
517
+ throw usageError(`--development may only be provided once. ${usage}`);
518
+ development = requireText(next, value);
519
+ }
520
+ index += 1;
521
+ continue;
522
+ }
523
+ if (value.startsWith("--"))
524
+ throw usageError(`Unknown option: ${value}. ${usage}`);
525
+ positionals.push(value);
526
+ }
527
+ if (positionals.length !== 1 || (aliasesProvided && clearAliases))
528
+ throw usageError(usage);
529
+ if (!aliasesProvided && !clearAliases && remote === undefined
530
+ && stable === undefined && development === undefined)
531
+ throw usageError(usage);
532
+ return {
533
+ reference: requireText(positionals[0], "Project reference"),
534
+ ...(aliasesProvided || clearAliases ? { aliases: clearAliases ? [] : aliases } : {}),
535
+ ...(remote === undefined ? {} : { remote }),
536
+ ...(stable === undefined ? {} : { stable }),
537
+ ...(development === undefined ? {} : { development })
538
+ };
539
+ }
540
+ function parseKnowledgeUpdateArguments(args, usage) {
541
+ const positionals = [];
542
+ let title;
543
+ let body;
544
+ for (let index = 0; index < args.length; index += 1) {
545
+ const value = args[index];
546
+ if (value === "--title" || value === "--body") {
547
+ const next = args[index + 1];
548
+ if (next === undefined || next.startsWith("--"))
549
+ throw usageError(`${value} is required. ${usage}`);
550
+ if (value === "--title") {
551
+ if (title !== undefined)
552
+ throw usageError(`--title may only be provided once. ${usage}`);
553
+ title = requireText(next, value);
554
+ }
555
+ else {
556
+ if (body !== undefined)
557
+ throw usageError(`--body may only be provided once. ${usage}`);
558
+ body = requireText(next, value);
559
+ }
560
+ index += 1;
561
+ continue;
562
+ }
563
+ if (value.startsWith("--"))
564
+ throw usageError(`Unknown option: ${value}. ${usage}`);
565
+ positionals.push(value);
566
+ }
567
+ if (positionals.length !== 2 || (title === undefined && body === undefined)) {
568
+ throw usageError(usage);
569
+ }
570
+ return {
571
+ project: positionals[0],
572
+ id: positionals[1],
573
+ ...(title === undefined ? {} : { title }),
574
+ ...(body === undefined ? {} : { body })
575
+ };
576
+ }
577
+ function parseCloneArguments(args, usage) {
578
+ const positionals = [];
579
+ const aliases = [];
580
+ let stable;
581
+ let development;
582
+ for (let index = 0; index < args.length; index += 1) {
583
+ const value = args[index];
584
+ if (["--alias", "--stable", "--development"].includes(value)) {
585
+ const next = args[index + 1];
586
+ if (next === undefined || next.startsWith("--")) {
587
+ throw usageError(`${value} is required. ${usage}`);
588
+ }
589
+ if (value === "--alias")
590
+ aliases.push(requireText(next, value));
591
+ else if (value === "--stable") {
592
+ if (stable !== undefined)
593
+ throw usageError(`--stable may only be provided once. ${usage}`);
594
+ stable = requireText(next, value);
595
+ }
596
+ else {
597
+ if (development !== undefined) {
598
+ throw usageError(`--development may only be provided once. ${usage}`);
599
+ }
600
+ development = requireText(next, value);
601
+ }
602
+ index += 1;
603
+ continue;
604
+ }
605
+ if (value.startsWith("--"))
606
+ throw usageError(`Unknown option: ${value}. ${usage}`);
607
+ positionals.push(value);
608
+ }
609
+ if (positionals.length !== 2)
610
+ throw usageError(usage);
611
+ return {
612
+ name: requireText(positionals[0], "Project name"),
613
+ remote: requireText(positionals[1], "Project remote"),
614
+ aliases,
615
+ ...(stable === undefined ? {} : { stable }),
616
+ ...(development === undefined ? {} : { development })
617
+ };
618
+ }
619
+ function parseSingleOption(args, option, usage) {
620
+ const positionals = [];
621
+ let found;
622
+ for (let index = 0; index < args.length; index += 1) {
623
+ const value = args[index];
624
+ if (value === option) {
625
+ if (found !== undefined)
626
+ throw usageError(`${option} may only be provided once. ${usage}`);
627
+ const next = args[index + 1];
628
+ if (next === undefined || next.startsWith("--"))
629
+ throw usageError(`${option} is required. ${usage}`);
630
+ found = requireText(next, option);
631
+ index += 1;
632
+ }
633
+ else if (value.startsWith("--")) {
634
+ throw usageError(`Unknown option: ${value}. ${usage}`);
635
+ }
636
+ else {
637
+ positionals.push(value);
638
+ }
639
+ }
640
+ if (found === undefined)
641
+ throw usageError(`${option} is required. ${usage}`);
642
+ return { positionals, value: found };
643
+ }
644
+ function requireText(value, label) {
645
+ const normalized = value.trim();
646
+ if (normalized.length === 0 || normalized.includes("\0")) {
647
+ throw usageError(`${label} is required.`);
648
+ }
649
+ return normalized;
650
+ }