@zq-silk/yui 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +15 -1
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +18 -9
  7. package/dist/cli/invocationAuthority.js +1 -1
  8. package/dist/commands/executionAuditCommands.js +5 -0
  9. package/dist/commands/globalRoleCommands.js +41 -3
  10. package/dist/commands/projectCommands.js +89 -94
  11. package/dist/commands/roleSkillValidation.js +4 -8
  12. package/dist/commands/taskCommands.js +65 -14
  13. package/dist/commands/taskUsageCommand.js +38 -0
  14. package/dist/context/roleSessionContext.js +18 -2
  15. package/dist/context/runContextPack.js +5 -1
  16. package/dist/context/sessionBootstrapManifest.js +18 -3
  17. package/dist/context/skillCatalog.js +108 -0
  18. package/dist/context/skillPackage.js +161 -0
  19. package/dist/context/taskContext.js +3 -2
  20. package/dist/context/taskSearch.js +127 -0
  21. package/dist/controlPlaneCli.js +25 -20
  22. package/dist/controller/fileSchedulerStoreAdapter.js +39 -8
  23. package/dist/controller/globalInputDelivery.js +2 -4
  24. package/dist/controller/jobControl.js +2 -0
  25. package/dist/controller/nativeHostInput.js +37 -0
  26. package/dist/controller/providerRetryDelivery.js +4 -2
  27. package/dist/controller/runtime.js +93 -2
  28. package/dist/controller/runtimeEventInbox.js +2 -2
  29. package/dist/controller/runtimeEventProcessor.js +3 -3
  30. package/dist/doctor/doctor.js +10 -0
  31. package/dist/execution/reviewMainRun.js +1 -0
  32. package/dist/execution/workItemMainRun.js +1 -0
  33. package/dist/executor/agentAdapter.js +3 -5
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +15 -7
  37. package/dist/integration/gitIntegrationService.js +7 -12
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/interaction/notificationPresentation.js +85 -0
  40. package/dist/interaction/operatorPresentation.js +1 -2
  41. package/dist/job/httpPreview.js +61 -0
  42. package/dist/job/jobRunner.js +12 -2
  43. package/dist/kernel/builtinCapabilities.js +3 -1
  44. package/dist/message/message.js +15 -1
  45. package/dist/output/rolePresentation.js +4 -1
  46. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  47. package/dist/repository/gitCompatibility.js +62 -0
  48. package/dist/repository/gitWorkspace.js +48 -71
  49. package/dist/repository/gitWorktreeInventory.js +121 -0
  50. package/dist/repository/project.js +74 -25
  51. package/dist/resources/resourceDiscovery.js +2 -2
  52. package/dist/resources/resourceGc.js +7 -4
  53. package/dist/resources/resourceRegistrar.js +2 -1
  54. package/dist/runtime/agentEndpoint.js +8 -0
  55. package/dist/runtime/agentHost.js +170 -3
  56. package/dist/runtime/codexAppServerRuntime.js +1 -1
  57. package/dist/runtime/codexNativeAccess.js +175 -0
  58. package/dist/runtime/controlledCodexTui.js +116 -0
  59. package/dist/runtime/publicReply.js +23 -0
  60. package/dist/runtime/runtimeObservation.js +48 -0
  61. package/dist/runtime/structuredProviderHost.js +16 -7
  62. package/dist/runtime/taskCostMetrics.js +133 -0
  63. package/dist/runtime/taskUsageMetrics.js +11 -2
  64. package/dist/runtime/taskUsageQuery.js +38 -0
  65. package/dist/scheduler/leaderWakeupProcessor.js +1 -9
  66. package/dist/scheduler/taskExecutionProjection.js +2 -0
  67. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  68. package/dist/storage/homeLayout.js +13 -0
  69. package/dist/storage/sqliteSchema.js +44 -0
  70. package/dist/storage/sqliteStore.js +53 -0
  71. package/dist/storage/storageVersions.js +1 -1
  72. package/dist/storage/storeRpc.js +2 -0
  73. package/dist/storage/taskSearch.js +35 -0
  74. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  75. package/dist/task/ordinaryArchive.js +42 -0
  76. package/dist/tmux/tmuxManager.js +8 -8
  77. package/dist/web/assets/assetManifest.js +11 -51
  78. package/dist/web/assets/client/app/catalog.js +215 -0
  79. package/dist/web/assets/client/app/main.js +148 -0
  80. package/dist/web/assets/client/app/selection.js +139 -0
  81. package/dist/web/assets/client/app/settings.js +326 -0
  82. package/dist/web/assets/client/app/taskView.js +280 -0
  83. package/dist/web/assets/client/domain/context.js +96 -0
  84. package/dist/web/assets/client/domain/records.js +126 -0
  85. package/dist/web/assets/client/domain/runs.js +119 -0
  86. package/dist/web/assets/client/domain/taskForms.js +197 -0
  87. package/dist/web/assets/client/domain/vocab.js +69 -0
  88. package/dist/web/assets/client/domain/work.js +116 -0
  89. package/dist/web/assets/client/i18n/en.js +900 -0
  90. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  91. package/dist/web/assets/client/index.js +114 -0
  92. package/dist/web/assets/client/layout/resizer.js +37 -0
  93. package/dist/web/assets/client/layout/router.js +35 -0
  94. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  95. package/dist/web/assets/client/layout/sizing.js +95 -0
  96. package/dist/web/assets/client/layout/workspace.js +247 -0
  97. package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
  98. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  99. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  100. package/dist/web/assets/client/lib/i18n.js +54 -0
  101. package/dist/web/assets/client/lib/markdown.js +167 -0
  102. package/dist/web/assets/client/lib/prefs.js +28 -0
  103. package/dist/web/assets/client/lib/theme.js +68 -0
  104. package/dist/web/assets/client/ui/containers.js +101 -0
  105. package/dist/web/assets/client/ui/controls.js +51 -0
  106. package/dist/web/assets/client/ui/forms.js +48 -0
  107. package/dist/web/assets/client/ui/metrics.js +137 -0
  108. package/dist/web/assets/client/ui/primitives.js +92 -0
  109. package/dist/web/assets/client/ui/text.js +33 -0
  110. package/dist/web/assets/client/ui/toast.js +12 -0
  111. package/dist/web/assets/client/views/bodySearch.js +172 -0
  112. package/dist/web/assets/client/views/createTask.js +85 -0
  113. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  114. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  115. package/dist/web/assets/client/views/dock/preview.js +129 -0
  116. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  117. package/dist/web/assets/client/views/globalInput.js +67 -0
  118. package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
  119. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  120. package/dist/web/assets/client/views/skills.js +152 -0
  121. package/dist/web/assets/client/views/task/actions.js +36 -0
  122. package/dist/web/assets/client/views/task/delivery.js +185 -0
  123. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  124. package/dist/web/assets/client/views/task/files.js +272 -0
  125. package/dist/web/assets/client/views/task/observation.js +153 -0
  126. package/dist/web/assets/client/views/task/overview.js +161 -0
  127. package/dist/web/assets/client/views/task/page.js +131 -0
  128. package/dist/web/assets/client/views/task/records.js +163 -0
  129. package/dist/web/assets/client/views/task/runtime.js +130 -0
  130. package/dist/web/assets/client/views/task/timeline.js +190 -0
  131. package/dist/web/assets/shared/geometry.js +19 -0
  132. package/dist/web/assets/shared/taskTabs.js +8 -0
  133. package/dist/web/assets/shared/themes.js +93 -0
  134. package/dist/web/assets/shell/dialogs.js +63 -0
  135. package/dist/web/assets/shell/dock.js +40 -0
  136. package/dist/web/assets/shell/index.js +40 -0
  137. package/dist/web/assets/shell/settings.js +34 -0
  138. package/dist/web/assets/shell/sidebar.js +45 -0
  139. package/dist/web/assets/styles/index.js +41 -0
  140. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
  141. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  142. package/dist/web/assets/styles/markdown.js +22 -3
  143. package/dist/web/assets/styles/tokens.js +10 -63
  144. package/dist/web/assets/styles/ui/controls.js +76 -0
  145. package/dist/web/assets/styles/ui/forms.js +46 -0
  146. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  147. package/dist/web/assets/styles/views/delivery.js +29 -0
  148. package/dist/web/assets/styles/views/dock.js +71 -0
  149. package/dist/web/assets/styles/views/page.js +32 -0
  150. package/dist/web/assets/styles/views/records.js +70 -0
  151. package/dist/web/assets/styles/views/settings.js +26 -0
  152. package/dist/web/assets/styles/views/sidebar.js +74 -0
  153. package/dist/web/assets/styles/views/task.js +80 -0
  154. package/dist/web/assets/styles/views/terminal.js +38 -0
  155. package/dist/web/taskPreviews.js +205 -0
  156. package/dist/web/tmuxWebTerminal.js +16 -9
  157. package/dist/web/webConversation.js +373 -0
  158. package/dist/web/webMaterials.js +155 -0
  159. package/dist/web/webServer.js +295 -18
  160. package/dist/web/webSettings.js +331 -0
  161. package/dist/web/webSkills.js +86 -0
  162. package/dist/web/webSnapshot.js +40 -8
  163. package/dist/web/webTaskSurface.js +106 -8
  164. package/dist/workspace/gitChangeSetCapture.js +3 -1
  165. package/docs/cli-information-contract.md +1 -1
  166. package/docs/cli-information-contract.zh-CN.md +1 -1
  167. package/docs/managed-turn-and-session-runtime.md +31 -0
  168. package/docs/managed-turn-and-session-runtime.zh-CN.md +22 -0
  169. package/docs/release-workflow.md +54 -0
  170. package/docs/release-workflow.zh-CN.md +39 -0
  171. package/docs/storage-baseline.md +38 -1
  172. package/docs/storage-baseline.zh-CN.md +22 -0
  173. package/docs/task-discovery.md +28 -0
  174. package/docs/testing/verification-levels.md +2 -2
  175. package/i18n/README.zh-CN.md +1 -1
  176. package/package.json +1 -1
  177. package/skills/yui-leader/SKILL.md +5 -4
  178. package/skills/yui-leader/references/execution.md +2 -1
  179. package/skills/yui-operator/SKILL.md +3 -0
  180. package/skills/yui-reviewer/SKILL.md +4 -2
  181. package/skills/yui-runtime/SKILL.md +43 -2
  182. package/skills/yui-worker/SKILL.md +3 -2
  183. package/dist/web/assets/client/app.js +0 -808
  184. package/dist/web/assets/client/components.js +0 -477
  185. package/dist/web/assets/client/detail.js +0 -379
  186. package/dist/web/assets/client/dock.js +0 -309
  187. package/dist/web/assets/client/evidence.js +0 -364
  188. package/dist/web/assets/client/forms.js +0 -230
  189. package/dist/web/assets/client/i18n.js +0 -1277
  190. package/dist/web/assets/client/markdown.js +0 -114
  191. package/dist/web/assets/client/records.js +0 -198
  192. package/dist/web/assets/client/sections.js +0 -349
  193. package/dist/web/assets/client/theme.js +0 -71
  194. package/dist/web/assets/shell.js +0 -161
  195. package/dist/web/assets/styles/components.js +0 -216
  196. package/dist/web/assets/styles/views.js +0 -309
  197. /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
@@ -5,7 +5,13 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { cleanupFailure } from "../workspace/cleanupInspection.js";
7
7
  import { externalProgramConfigViolations } from "../artifacts/managedGit.js";
8
+ import { compatibleGitArguments, gitOutputLine, supportedGit } from "./gitCompatibility.js";
9
+ import { gitPath, readGitWorktrees } from "./gitWorktreeInventory.js";
8
10
  const executeFile = promisify(execFile);
11
+ const isolatedFetchOptions = [
12
+ "--no-tags", "--no-prune", "--no-prune-tags", "--no-recurse-submodules",
13
+ "--no-auto-gc", "--no-write-commit-graph", "--no-write-fetch-head", "--refmap="
14
+ ];
9
15
  /** Observed partial effects, not a rollback or permission to replay a refresh. */
10
16
  export class GitWorkspaceRefreshError extends Error {
11
17
  result;
@@ -160,13 +166,13 @@ export class NodeGitWorkspace {
160
166
  const remote = safeRemote(input.remoteUrl);
161
167
  const configuredBranch = await safeFetchBranch(input.branch);
162
168
  const branch = configuredBranch === "HEAD"
163
- ? await resolveRemoteHeadBranch(remote)
169
+ ? await resolveRemoteHeadBranch(remote, repositoryPath)
164
170
  : configuredBranch;
165
171
  const fetchedRef = remoteBaselineRef(remote, branch);
166
172
  try {
167
173
  await git([
168
174
  "-C", repositoryPath,
169
- "fetch", "--no-tags", "--no-write-fetch-head",
175
+ "fetch", ...isolatedFetchOptions,
170
176
  remote,
171
177
  `refs/heads/${branch}:${fetchedRef}`
172
178
  ]);
@@ -212,8 +218,7 @@ export class NodeGitWorkspace {
212
218
  // An empty refmap disables opportunistic tracking updates even when the
213
219
  // Project URL is a named remote. Ignore configured prune/submodule effects.
214
220
  await git([
215
- "-C", initial.root, "fetch", "--no-tags", "--no-prune", "--no-prune-tags",
216
- "--no-recurse-submodules", "--no-auto-maintenance", "--no-write-fetch-head", "--refmap=",
221
+ "-C", initial.root, "fetch", ...isolatedFetchOptions,
217
222
  remote, `refs/heads/${stableBranch}:${temporaryRef}`
218
223
  ]);
219
224
  fetchedCommit = await resolveFetchedCommit(initial.root, temporaryRef);
@@ -239,11 +244,11 @@ export class NodeGitWorkspace {
239
244
  // Verify the stable branch and CAS the one configured destination in
240
245
  // the same Git transaction. HEAD/index/worktree still use normal ff.
241
246
  await git(["-C", initial.root, "update-ref", "--stdin"], { input: [
242
- "start", "option no-deref",
247
+ "option no-deref",
243
248
  `verify refs/heads/${stableBranch} ${fetchedCommit}`,
244
249
  "option no-deref",
245
250
  `update ${mapping.ref} ${fetchedCommit} ${old}`,
246
- "prepare", "commit", ""
251
+ ""
247
252
  ].join("\n") });
248
253
  tracking = {
249
254
  status: previous === fetchedCommit ? "current" : "updated",
@@ -305,15 +310,15 @@ export class NodeGitWorkspace {
305
310
  async resolveRemoteBaseline(input) {
306
311
  const remote = safeRemote(input.remoteUrl);
307
312
  const configuredRef = await safeFetchBranch(input.developmentRef);
313
+ const repository = await this.inspect(input.repositoryPath, "HEAD");
308
314
  const branch = configuredRef === "HEAD"
309
- ? await resolveRemoteHeadBranch(remote)
315
+ ? await resolveRemoteHeadBranch(remote, repository.root)
310
316
  : configuredRef;
311
- const repository = await this.inspect(input.repositoryPath, "HEAD");
312
317
  const temporaryRef = remoteBaselineRef(remote, branch);
313
318
  try {
314
319
  await git([
315
320
  "-C", repository.root,
316
- "fetch", "--no-tags", "--no-write-fetch-head",
321
+ "fetch", ...isolatedFetchOptions,
317
322
  remote,
318
323
  `refs/heads/${branch}:${temporaryRef}`
319
324
  ]);
@@ -321,7 +326,7 @@ export class NodeGitWorkspace {
321
326
  "-C", repository.root,
322
327
  "rev-parse", "--verify", "--end-of-options", `${temporaryRef}^{commit}`
323
328
  ]);
324
- const advertisedCommit = await resolveRemoteBranchCommit(remote, branch);
329
+ const advertisedCommit = await resolveRemoteBranchCommit(remote, branch, repository.root);
325
330
  if (fetchedCommit.toLowerCase() !== advertisedCommit) {
326
331
  throw new Error(`Project remote development branch changed while it was fetched: ${branch}.`);
327
332
  }
@@ -341,8 +346,10 @@ export class NodeGitWorkspace {
341
346
  }
342
347
  }
343
348
  async clone(input) {
349
+ supportedGit();
344
350
  const remote = safeRemote(input.remoteUrl);
345
- const destination = resolve(requireText(input.destination, "Project destination"));
351
+ requireText(input.destination, "Project destination");
352
+ const destination = resolve(input.destination);
346
353
  if (await pathKind(destination) !== undefined) {
347
354
  throw new Error(`Project destination already exists: ${destination}.`);
348
355
  }
@@ -415,10 +422,8 @@ export class NodeGitWorkspace {
415
422
  async inspect(repositoryPath, baseRef = "HEAD") {
416
423
  const requested = await canonicalDirectory(repositoryPath, "Project");
417
424
  const ref = safeRef(baseRef);
418
- const root = await canonicalDirectory(await gitLine(["-C", requested, "rev-parse", "--show-toplevel"]), "Project root");
419
- const gitDirectory = await canonicalDirectory(await gitLine([
420
- "-C", root, "rev-parse", "--path-format=absolute", "--git-common-dir"
421
- ]), "Git common directory");
425
+ const root = await canonicalDirectory(await gitPath(requested, ["--show-toplevel"]), "Project root");
426
+ const gitDirectory = await canonicalDirectory(await gitPath(root, ["--git-common-dir"]), "Git common directory");
422
427
  const baseCommit = await gitLine([
423
428
  "-C", root, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`
424
429
  ]);
@@ -485,7 +490,7 @@ export class NodeGitWorkspace {
485
490
  const importedRef = `${importRoot}/${index}`;
486
491
  await git([
487
492
  "-C", destination.root,
488
- "fetch", "--no-tags", "--no-write-fetch-head", "--",
493
+ "fetch", ...isolatedFetchOptions, "--",
489
494
  source.root, `${snapshot.ref}:${importedRef}`
490
495
  ]);
491
496
  const imported = await resolveRefCommit(destination.root, importedRef);
@@ -567,36 +572,11 @@ export class NodeGitWorkspace {
567
572
  const excluded = input.excludeWorktreePath === undefined
568
573
  ? undefined
569
574
  : await realpath(resolve(input.excludeWorktreePath)).catch(() => undefined);
570
- const porcelain = await git(["-C", root, "worktree", "list", "--porcelain"]);
571
- const records = [];
572
- let current = { prunable: false };
573
- for (const line of porcelain.split("\n")) {
574
- if (line.length === 0) {
575
- const path = current.path;
576
- if (path !== undefined) {
577
- records.push({ path, branch: current.branch, prunable: current.prunable });
578
- }
579
- current = { prunable: false };
580
- continue;
581
- }
582
- if (line.startsWith("worktree ")) {
583
- current.path = line.slice("worktree ".length);
584
- }
585
- else if (line.startsWith("branch ")) {
586
- current.branch = line.slice("branch ".length);
587
- }
588
- else if (line.startsWith("prunable")) {
589
- current.prunable = true;
590
- }
591
- }
592
- const lastPath = current.path;
593
- if (lastPath !== undefined) {
594
- records.push({ path: lastPath, branch: current.branch, prunable: current.prunable });
595
- }
575
+ const records = await readGitWorktrees(root);
596
576
  for (const record of records) {
597
577
  // A dead registration (its directory is gone) occupies nothing; a
598
578
  // worktree on another ref or a detached HEAD does not occupy this one.
599
- if (record.prunable || record.branch !== wanted)
579
+ if ((record.prunable && !record.locked) || record.branch !== wanted)
600
580
  continue;
601
581
  // git reports canonical absolute paths; compare canonicals so a
602
582
  // symlinked workspace root cannot make the recorded worktree look
@@ -724,11 +704,7 @@ export class NodeGitWorkspace {
724
704
  }
725
705
  async listWorktrees(repositoryPath) {
726
706
  const root = await canonicalDirectory(repositoryPath, "Project");
727
- const output = await git(["-C", root, "worktree", "list", "--porcelain"]);
728
- return output
729
- .split(/\r?\n/u)
730
- .filter((line) => line.startsWith("worktree "))
731
- .map((line) => line.slice("worktree ".length).trim());
707
+ return (await readGitWorktrees(root)).map(entry => entry.path);
732
708
  }
733
709
  async listCommitsBetween(input) {
734
710
  const root = await canonicalDirectory(input.repositoryPath, "Project");
@@ -919,9 +895,8 @@ export class NodeGitWorkspace {
919
895
  await assertExpectedBranch(expected, input.branch);
920
896
  if (!await this.inspectClean(expected))
921
897
  return "dirty";
922
- const worktrees = (await git(["-C", expected, "worktree", "list", "--porcelain", "-z"]))
923
- .split("\0").filter(line => line.startsWith("worktree "));
924
- if (worktrees.length !== 1 || worktrees[0] !== `worktree ${expected}`) {
898
+ const worktrees = await readGitWorktrees(expected);
899
+ if (worktrees.length !== 1 || worktrees[0].path !== expected) {
925
900
  cleanupFailure("git-worktree-registrations", "Task clone still owns Git worktree registrations; retained.", 1, worktrees.length);
926
901
  }
927
902
  return "clean";
@@ -1051,16 +1026,14 @@ async function removeMissingWorktreeRegistration(repositoryRoot, path, branch, a
1051
1026
  await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
1052
1027
  }
1053
1028
  async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
1054
- const records = (await git(["-C", repositoryRoot, "worktree", "list", "--porcelain", "-z"])).split("\0\0");
1055
- const fields = records.map(record => record.split("\0")).find(record => record.includes(`worktree ${path}`));
1056
- if (fields === undefined)
1029
+ const entry = (await readGitWorktrees(repositoryRoot)).find(record => record.path === path);
1030
+ if (entry === undefined)
1057
1031
  return false;
1058
- const pinnedDetached = allowDetached && fields.includes("detached")
1032
+ const pinnedDetached = allowDetached && entry.detached
1059
1033
  && await gitSucceeds(["-C", repositoryRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`])
1060
- && fields.includes(`HEAD ${await resolveRefCommit(repositoryRoot, `refs/heads/${branch}`)}`);
1061
- if ((!fields.includes(`branch refs/heads/${branch}`) && !pinnedDetached) || fields.some(field => field.startsWith("locked"))) {
1062
- cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch: fields.find(field => field.startsWith("branch "))?.slice(7) ?? null,
1063
- locked: fields.some(field => field.startsWith("locked")) });
1034
+ && entry.head === await resolveRefCommit(repositoryRoot, `refs/heads/${branch}`);
1035
+ if ((entry.branch !== `refs/heads/${branch}` && !pinnedDetached) || entry.locked) {
1036
+ cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch: entry.branch ?? null, locked: entry.locked });
1064
1037
  }
1065
1038
  return true;
1066
1039
  }
@@ -1103,8 +1076,7 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
1103
1076
  // Rebase temporarily detaches HEAD. Git's per-worktree metadata must still
1104
1077
  // identify this exact Integration branch; unrelated detachment is invalid.
1105
1078
  for (const backend of ["rebase-merge", "rebase-apply"]) {
1106
- const headNamePath = await gitLine(["-C", path, "rev-parse", "--path-format=absolute",
1107
- "--git-path", `${backend}/head-name`]);
1079
+ const headNamePath = await gitPath(path, ["--git-path", `${backend}/head-name`]);
1108
1080
  let headName;
1109
1081
  try {
1110
1082
  headName = (await readFile(headNamePath, "utf8")).trim();
@@ -1135,10 +1107,10 @@ async function assertOwnedWorktree(project, container, path) {
1135
1107
  assertContained(container, canonicalPath);
1136
1108
  if (canonicalPath !== path)
1137
1109
  throw new Error("Managed worktree resolves through a symbolic link.");
1138
- const root = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--show-toplevel"]), "Managed worktree root");
1110
+ const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Managed worktree root");
1139
1111
  if (root !== path)
1140
1112
  throw new Error("Managed worktree root does not match its deterministic path.");
1141
- const common = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"]), "Managed Git common directory");
1113
+ const common = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Managed Git common directory");
1142
1114
  if (common !== project.gitDirectory) {
1143
1115
  cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
1144
1116
  }
@@ -1162,7 +1134,7 @@ async function inspectExactRecordedWorktree(input) {
1162
1134
  if (path !== expectedPath) {
1163
1135
  throw new Error("Recorded managed worktree resolves through a symbolic link.");
1164
1136
  }
1165
- const root = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--show-toplevel"]), "Recorded managed worktree root");
1137
+ const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Recorded managed worktree root");
1166
1138
  if (root !== path) {
1167
1139
  throw new Error("Recorded managed worktree root does not match its deterministic path.");
1168
1140
  }
@@ -1170,7 +1142,7 @@ async function inspectExactRecordedWorktree(input) {
1170
1142
  if (branch !== input.branch) {
1171
1143
  throw new Error(`Recorded managed worktree is on an unexpected branch: ${branch}.`);
1172
1144
  }
1173
- const gitDirectory = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"]), "Recorded managed Git common directory");
1145
+ const gitDirectory = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Recorded managed Git common directory");
1174
1146
  const destination = await new NodeGitWorkspace().inspect(input.repositoryPath);
1175
1147
  const head = await resolveRefCommit(path, "HEAD");
1176
1148
  const retained = await resolveRefCommit(destination.root, `refs/heads/${input.branch}`);
@@ -1267,8 +1239,8 @@ async function cleanupContainer(path) {
1267
1239
  }
1268
1240
  }
1269
1241
  async function canonicalDirectory(path, label) {
1270
- const value = requireText(path, label);
1271
- const canonical = await realpath(isAbsolute(value) ? value : resolve(value));
1242
+ requireText(path, label);
1243
+ const canonical = await realpath(isAbsolute(path) ? path : resolve(path));
1272
1244
  return canonical;
1273
1245
  }
1274
1246
  function managedPath(container, directory) {
@@ -1307,7 +1279,7 @@ async function readWorktreeStatus(path) {
1307
1279
  return readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
1308
1280
  }
1309
1281
  async function readStatusGit(path, args) {
1310
- return git(["--no-optional-locks", "-c", "core.fsmonitor=false",
1282
+ return git(["--no-optional-locks", "-c", "core.fsmonitor=",
1311
1283
  "-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
1312
1284
  "-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
1313
1285
  }
@@ -1353,8 +1325,10 @@ async function assertStatusProgramsAbsent(path) {
1353
1325
  }
1354
1326
  }
1355
1327
  async function git(args, options = {}) {
1328
+ const program = supportedGit();
1329
+ const invocation = compatibleGitArguments(args);
1356
1330
  try {
1357
- const execution = executeFile("git", [...args], {
1331
+ const execution = executeFile(program.path, invocation, {
1358
1332
  encoding: "utf8",
1359
1333
  maxBuffer: 1024 * 1024,
1360
1334
  timeout: 30_000,
@@ -1375,6 +1349,7 @@ async function git(args, options = {}) {
1375
1349
  }
1376
1350
  }
1377
1351
  async function gitSucceeds(args) {
1352
+ supportedGit();
1378
1353
  try {
1379
1354
  await git(args);
1380
1355
  return true;
@@ -1384,7 +1359,7 @@ async function gitSucceeds(args) {
1384
1359
  }
1385
1360
  }
1386
1361
  async function gitLine(args) {
1387
- const lines = (await git(args)).trimEnd().split("\n");
1362
+ const lines = gitOutputLine(await git(args)).split("\n");
1388
1363
  if (lines.length !== 1 || lines[0]?.length === 0 || lines[0]?.includes("\0")) {
1389
1364
  throw new Error("Git returned invalid output.");
1390
1365
  }
@@ -1533,6 +1508,7 @@ function safeRef(value) {
1533
1508
  return ref;
1534
1509
  }
1535
1510
  async function safeFetchBranch(value) {
1511
+ supportedGit();
1536
1512
  const configuredRef = safeRef(value);
1537
1513
  if (configuredRef === "HEAD")
1538
1514
  return configuredRef;
@@ -1624,7 +1600,8 @@ function isCommit(value) {
1624
1600
  return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
1625
1601
  }
1626
1602
  function safeRemote(value) {
1627
- const remote = requireText(value, "Git remote URL");
1603
+ requireText(value, "Git remote URL");
1604
+ const remote = value;
1628
1605
  if (remote.startsWith("-") || /[\r\n]/.test(remote)) {
1629
1606
  throw new Error("Git remote URL is invalid.");
1630
1607
  }
@@ -0,0 +1,121 @@
1
+ import { execFile } from "node:child_process";
2
+ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { gitOutputLine, supportedGit } from "./gitCompatibility.js";
6
+ const execute = promisify(execFile);
7
+ async function run(args) {
8
+ return (await execute(supportedGit().path, args, {
9
+ encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024
10
+ })).stdout;
11
+ }
12
+ export async function gitPath(cwd, args) {
13
+ return resolve(cwd, gitOutputLine(await run(["-C", cwd, "rev-parse", ...args])));
14
+ }
15
+ async function exists(path) {
16
+ try {
17
+ await lstat(path);
18
+ return true;
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ return false;
23
+ throw error;
24
+ }
25
+ }
26
+ /**
27
+ * Old porcelain omits locks and does not delimit/quote arbitrary paths. Read
28
+ * Git's worktree administrative records instead. These are observations only:
29
+ * never write/prune them, and never infer ownership merely from a directory.
30
+ * Mutating consumers still check the recorded owner and ask Git to remove it.
31
+ */
32
+ export async function readGitWorktrees(cwd) {
33
+ const common = await realpath(await gitPath(cwd, ["--git-common-dir"]));
34
+ const result = [];
35
+ // This is also Git's own main-worktree listing convention (including bare
36
+ // and separate-git-dir repositories), not a claim of managed ownership.
37
+ const main = basename(common) === ".git" ? dirname(common) : common;
38
+ result.push(await inspect(common, main, false));
39
+ const directory = join(common, "worktrees");
40
+ let entries;
41
+ try {
42
+ entries = await readdir(directory, { withFileTypes: true });
43
+ }
44
+ catch (error) {
45
+ if (error.code === "ENOENT")
46
+ return result;
47
+ throw error;
48
+ }
49
+ for (const entry of entries) {
50
+ if (!entry.isDirectory())
51
+ throw new Error(`Uncertain Git worktree administration: ${join(directory, entry.name)}; retained.`);
52
+ const admin = join(directory, entry.name);
53
+ const pointer = gitOutputLine(await readFile(join(admin, "gitdir"), "utf8"));
54
+ if (basename(pointer) !== ".git") {
55
+ throw new Error(`Invalid Git worktree back-pointer: ${admin}; retained.`);
56
+ }
57
+ // Relative back-pointers are based at the administrative directory,
58
+ // not the caller's cwd (Git's worktree.useRelativePaths setting).
59
+ const path = dirname(resolve(admin, pointer));
60
+ const adminCommon = await realpath(resolve(cwd, gitOutputLine(await run([
61
+ "-C", cwd, `--git-dir=${admin}`, "rev-parse", "--git-common-dir"
62
+ ]))));
63
+ if (adminCommon !== common)
64
+ throw new Error(`Git worktree common directory mismatch: ${admin}; retained.`);
65
+ if (await exists(path)) {
66
+ const actual = await realpath(await gitPath(path, ["--git-dir"]));
67
+ if (actual !== admin)
68
+ throw new Error(`Git worktree back-pointer mismatch: ${path}; retained.`);
69
+ }
70
+ result.push(await inspect(admin, path, true));
71
+ }
72
+ if (new Set(result.map(entry => entry.path)).size !== result.length) {
73
+ throw new Error("Duplicate Git worktree registrations; retained.");
74
+ }
75
+ return result;
76
+ }
77
+ async function inspect(admin, path, linked) {
78
+ // Ask Git rather than parsing HEAD: Git owns ref storage (including newer
79
+ // backends), symbolic resolution and per-worktree pseudorefs.
80
+ let branch;
81
+ try {
82
+ branch = gitOutputLine(await run([`--git-dir=${admin}`, "symbolic-ref", "--quiet", "HEAD"]));
83
+ }
84
+ catch (error) {
85
+ if (error.code !== 1)
86
+ throw error;
87
+ }
88
+ if (branch !== undefined && !branch.startsWith("refs/")) {
89
+ throw new Error(`Invalid Git worktree HEAD: ${admin}; retained.`);
90
+ }
91
+ let head;
92
+ try {
93
+ head = gitOutputLine(await run([`--git-dir=${admin}`, "rev-parse", "--verify", "HEAD"]));
94
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(head))
95
+ throw new Error("Invalid Git HEAD");
96
+ }
97
+ catch (error) {
98
+ // A genuinely unborn branch (main or linked) is valid discovery, not cleanup proof.
99
+ if (branch === undefined)
100
+ throw error;
101
+ try {
102
+ await run([`--git-dir=${admin}`, "show-ref", "--verify", "--quiet", branch]);
103
+ }
104
+ catch (missing) {
105
+ if (missing.code === 1) {
106
+ return {
107
+ path, branch, detached: false, prunable: !await exists(path),
108
+ locked: linked && await exists(join(admin, "locked"))
109
+ };
110
+ }
111
+ throw missing;
112
+ }
113
+ throw error;
114
+ }
115
+ return {
116
+ path, ...(head === undefined ? {} : { head }),
117
+ ...(branch === undefined ? {} : { branch }),
118
+ detached: branch === undefined, prunable: !await exists(path),
119
+ locked: linked && await exists(join(admin, "locked"))
120
+ };
121
+ }
@@ -63,13 +63,17 @@ export function assertProjectActive(project, action) {
63
63
  throw new Error(`Project is retired and cannot ${action}: ${project.id}.`);
64
64
  }
65
65
  }
66
- export function addProjectKnowledge(project, id, title, body, now, provenance) {
66
+ export function addProjectKnowledge(project, id, title, body, now, provenance, applicability = {}) {
67
67
  const timestamp = now.toISOString();
68
68
  const knowledge = {
69
- schemaVersion: 1,
69
+ schemaVersion: 2,
70
+ version: 1,
71
+ history: [],
70
72
  id: requireIdentity(id, "Project knowledge id"),
71
73
  title: requireText(title, "Project knowledge title"),
72
74
  body: requireText(body, "Project knowledge body"),
75
+ ...(applicability.scope === undefined ? {} : { scope: requireText(applicability.scope, "Knowledge scope") }),
76
+ ...(applicability.expiresWhen === undefined ? {} : { expiresWhen: requireText(applicability.expiresWhen, "Knowledge expiry condition") }),
73
77
  status: "active",
74
78
  ...(provenance === undefined ? {} : { provenance: validateKnowledgeProvenance(provenance) }),
75
79
  createdAt: timestamp,
@@ -114,19 +118,34 @@ export function updateProjectKnowledge(project, id, patch, now) {
114
118
  if (entry.status === "retired") {
115
119
  throw new Error(`Project knowledge is retired: ${knowledgeId}.`);
116
120
  }
117
- return {
118
- ...entry,
121
+ const { scope: _scope, expiresWhen: _expiresWhen, ...base } = entry;
122
+ const scope = patch.scope === undefined ? entry.scope
123
+ : patch.scope === null ? undefined : requireText(patch.scope, "Knowledge scope");
124
+ const expiresWhen = patch.expiresWhen === undefined ? entry.expiresWhen
125
+ : patch.expiresWhen === null ? undefined : requireText(patch.expiresWhen, "Knowledge expiry condition");
126
+ const next = {
127
+ ...base,
128
+ ...(scope === undefined ? {} : { scope }),
129
+ ...(expiresWhen === undefined ? {} : { expiresWhen }),
119
130
  title: patch.title === undefined
120
131
  ? entry.title
121
132
  : requireText(patch.title, "Project knowledge title"),
122
133
  body: patch.body === undefined
123
134
  ? entry.body
124
135
  : requireText(patch.body, "Project knowledge body"),
136
+ ...(patch.provenance === undefined ? {} : { provenance: validateKnowledgeProvenance(patch.provenance) }),
125
137
  updatedAt: now.toISOString()
126
138
  };
139
+ if (next.title === entry.title && next.body === entry.body
140
+ && next.scope === entry.scope && next.expiresWhen === entry.expiresWhen
141
+ && JSON.stringify(next.provenance) === JSON.stringify(entry.provenance))
142
+ return entry;
143
+ return { ...next, version: entry.version + 1, history: [...entry.history, knowledgeRevision(entry)] };
127
144
  });
128
145
  if (!found)
129
146
  throw new Error(`Project knowledge not found: ${knowledgeId}.`);
147
+ if (knowledge.every((entry, index) => entry === project.knowledge[index]))
148
+ return project;
130
149
  return validateProject({ ...project, knowledge, updatedAt: now.toISOString() });
131
150
  }
132
151
  export function retireProjectKnowledge(project, id, now) {
@@ -136,31 +155,42 @@ export function retireProjectKnowledge(project, id, now) {
136
155
  if (entry.id !== knowledgeId)
137
156
  return entry;
138
157
  found = true;
158
+ if (entry.status === "retired")
159
+ return entry;
139
160
  return {
140
161
  ...entry,
162
+ version: entry.version + 1,
163
+ history: [...entry.history, knowledgeRevision(entry)],
141
164
  status: "retired",
142
165
  updatedAt: now.toISOString()
143
166
  };
144
167
  });
145
168
  if (!found)
146
169
  throw new Error(`Project knowledge not found: ${knowledgeId}.`);
170
+ if (knowledge.every((entry, index) => entry === project.knowledge[index]))
171
+ return project;
147
172
  return validateProject({ ...project, knowledge, updatedAt: now.toISOString() });
148
173
  }
174
+ function knowledgeRevision(entry) {
175
+ const { schemaVersion: _schema, id: _id, history: _history, createdAt: _created, ...revision } = entry;
176
+ return revision;
177
+ }
149
178
  /**
150
179
  * Stable dedup fingerprint for a promotion candidate. Two proposals with the
151
- * same source identity, title, and body are the same conclusion regardless of
152
- * who proposed them or when.
180
+ * same source, content, applicability and replacement intent are identical.
181
+ * Trim outer whitespace only; preserve case, internal whitespace and Unicode.
182
+ * A JSON tuple avoids collisions between user-supplied identity delimiters.
153
183
  */
154
184
  export function knowledgeProposalFingerprint(input) {
155
- const sourceKey = [
156
- input.projectId,
157
- input.source.taskId,
158
- input.source.decisionId ?? "",
159
- input.source.milestoneId ?? "",
160
- input.source.commitSha ?? ""
161
- ].join("|");
185
+ const optional = (value) => value === undefined ? null : requireText(value, "Knowledge fingerprint field");
162
186
  return createHash("sha256")
163
- .update(`${sourceKey}\u0000${input.title}\u0000${input.body}`)
187
+ .update(JSON.stringify([
188
+ "knowledge-proposal-v2", requireText(input.projectId, "Project id"),
189
+ requireText(input.source.taskId, "Source Task"), optional(input.source.decisionId),
190
+ optional(input.source.milestoneId), optional(input.source.commitSha),
191
+ requireText(input.title, "Knowledge title"), requireText(input.body, "Knowledge body"),
192
+ optional(input.scope), optional(input.expiresWhen), optional(input.supersedesKnowledgeId)
193
+ ]))
164
194
  .digest("hex");
165
195
  }
166
196
  /** Digest of the cited source evidence, so a later Agent can verify it. */
@@ -334,17 +364,18 @@ export function validateProject(project) {
334
364
  requireGitRef(project.developmentBranch, "Project development branch");
335
365
  const knowledgeIds = new Set();
336
366
  for (const entry of project.knowledge) {
337
- if (entry.schemaVersion !== 1)
338
- throw new Error("Project knowledge must use schemaVersion 1.");
367
+ if (entry.schemaVersion !== 2)
368
+ throw new Error("Project knowledge must use schemaVersion 2.");
339
369
  requireIdentity(entry.id, "Project knowledge id");
340
- requireText(entry.title, "Project knowledge title");
341
- requireText(entry.body, "Project knowledge body");
342
- if (!["active", "retired"].includes(entry.status)) {
343
- throw new Error("Project knowledge status is invalid.");
344
- }
345
- if (entry.provenance !== undefined) {
346
- validateKnowledgeProvenance(entry.provenance);
370
+ validateKnowledgeRevision(entry);
371
+ if (!Array.isArray(entry.history) || entry.history.length !== entry.version - 1) {
372
+ throw new Error("Project knowledge history must contain every prior version.");
347
373
  }
374
+ entry.history.forEach((revision, index) => {
375
+ validateKnowledgeRevision(revision);
376
+ if (revision.version !== index + 1)
377
+ throw new Error("Project knowledge history is out of order.");
378
+ });
348
379
  requireTimestamp(entry.createdAt, "Project knowledge createdAt");
349
380
  requireTimestamp(entry.updatedAt, "Project knowledge updatedAt");
350
381
  if (knowledgeIds.has(entry.id))
@@ -363,6 +394,21 @@ export function validateProject(project) {
363
394
  requireTimestamp(project.updatedAt, "Project updatedAt");
364
395
  return project;
365
396
  }
397
+ function validateKnowledgeRevision(entry) {
398
+ if (!Number.isSafeInteger(entry.version) || entry.version < 1)
399
+ throw new Error("Invalid knowledge version.");
400
+ requireText(entry.title, "Project knowledge title");
401
+ requireText(entry.body, "Project knowledge body");
402
+ if (entry.scope !== undefined)
403
+ requireText(entry.scope, "Knowledge scope");
404
+ if (entry.expiresWhen !== undefined)
405
+ requireText(entry.expiresWhen, "Knowledge expiry condition");
406
+ if (!["active", "retired"].includes(entry.status))
407
+ throw new Error("Project knowledge status is invalid.");
408
+ if (entry.provenance !== undefined)
409
+ validateKnowledgeProvenance(entry.provenance);
410
+ requireTimestamp(entry.updatedAt, "Project knowledge updatedAt");
411
+ }
366
412
  function validateKnowledgeProvenance(provenance) {
367
413
  requireIdentity(provenance.taskId, "Knowledge provenance task id");
368
414
  if (provenance.decisionId !== undefined) {
@@ -394,8 +440,8 @@ function validateKnowledgeProvenance(provenance) {
394
440
  return provenance;
395
441
  }
396
442
  function validateKnowledgeProposal(proposal, projectId) {
397
- if (proposal.schemaVersion !== 1) {
398
- throw new Error("Knowledge proposal must use schemaVersion 1.");
443
+ if (proposal.schemaVersion !== 2) {
444
+ throw new Error("Knowledge proposal must use schemaVersion 2.");
399
445
  }
400
446
  requireIdentity(proposal.id, "Knowledge proposal id");
401
447
  if (proposal.projectId !== projectId) {
@@ -403,6 +449,9 @@ function validateKnowledgeProposal(proposal, projectId) {
403
449
  }
404
450
  requireText(proposal.title, "Knowledge proposal title");
405
451
  requireText(proposal.body, "Knowledge proposal body");
452
+ if (proposal.fingerprint !== knowledgeProposalFingerprint(proposal)) {
453
+ throw new Error("Knowledge proposal fingerprint does not match its content.");
454
+ }
406
455
  if (!["pending", "accepted", "rejected"].includes(proposal.status)) {
407
456
  throw new Error("Knowledge proposal status is invalid.");
408
457
  }
@@ -10,6 +10,7 @@ import { execFile } from "node:child_process";
10
10
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
11
11
  import { basename, join, resolve } from "node:path";
12
12
  import { promisify } from "node:util";
13
+ import { readGitWorktrees } from "../repository/gitWorktreeInventory.js";
13
14
  import { managedProjectPath } from "../repository/project.js";
14
15
  import { managedRuntimeRoot } from "../storage/homeLayout.js";
15
16
  import { isResourceQuarantinePath } from "./resourceRegistry.js";
@@ -54,8 +55,7 @@ export function parseGitWorktreePorcelain(output) {
54
55
  }
55
56
  async function listGitWorktrees(repositoryPath) {
56
57
  try {
57
- const { stdout } = await executeFile("git", ["-C", repositoryPath, "worktree", "list", "--porcelain"], { timeout: 10_000 });
58
- return parseGitWorktreePorcelain(stdout);
58
+ return await readGitWorktrees(repositoryPath);
59
59
  }
60
60
  catch (error) {
61
61
  throw new Error(`Failed to list Git worktrees for ${repositoryPath}: `