@zq-silk/yui 2.2.0 → 2.4.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.
- package/README.md +19 -3
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +25 -11
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +30 -9
- package/dist/commands/taskIntegrationCommands.js +5 -16
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +10 -1
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +51 -39
- package/dist/controller/clientRuntime.js +3 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +13 -5
- package/dist/integration/gitIntegrationService.js +15 -79
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobCleanupInspection.js +67 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +86 -80
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/repository/taskWorkspaceCoordinator.js +72 -145
- package/dist/repository/taskWorkspacePreparer.js +39 -43
- package/dist/repository/workspaceCleanupInspection.js +88 -7
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +18 -11
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/integrationRuntimeIsolation.js +21 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskRuntimeIsolation.js +91 -21
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/archiveDiagnostics.js +26 -8
- package/dist/task/archivePreflight.js +26 -9
- package/dist/task/leaderArchive.js +9 -15
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +306 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +114 -9
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
- package/docs/observability/README.md +13 -3
- package/docs/observability/README.zh-CN.md +10 -3
- package/docs/release-workflow.md +53 -0
- package/docs/release-workflow.zh-CN.md +40 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-delivery.md +36 -8
- package/docs/task-delivery.zh-CN.md +24 -7
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +4 -3
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-leader/references/integration.md +20 -0
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +3 -0
- package/skills/yui-runtime/SKILL.md +4 -0
- package/skills/yui-runtime/references/publication.md +20 -4
- package/skills/yui-worker/SKILL.md +3 -0
- package/dist/web/assets/client/app.js +0 -804
- package/dist/web/assets/client/components.js +0 -418
- package/dist/web/assets/client/detail.js +0 -333
- package/dist/web/assets/client/dock.js +0 -225
- package/dist/web/assets/client/evidence.js +0 -172
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1161
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -63
- package/dist/web/assets/client/sections.js +0 -313
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -169
- package/dist/web/assets/styles/views.js +0 -292
|
@@ -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",
|
|
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",
|
|
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
|
-
"
|
|
247
|
+
"option no-deref",
|
|
243
248
|
`verify refs/heads/${stableBranch} ${fetchedCommit}`,
|
|
244
249
|
"option no-deref",
|
|
245
250
|
`update ${mapping.ref} ${fetchedCommit} ${old}`,
|
|
246
|
-
"
|
|
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",
|
|
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
|
-
|
|
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
|
|
419
|
-
const gitDirectory = await canonicalDirectory(await
|
|
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",
|
|
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
|
|
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
|
-
|
|
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");
|
|
@@ -821,7 +797,7 @@ export class NodeGitWorkspace {
|
|
|
821
797
|
const state = await this.inspectWorktree(input);
|
|
822
798
|
if (state === "dirty")
|
|
823
799
|
return state;
|
|
824
|
-
const container = await
|
|
800
|
+
const container = await cleanupContainer(input.container);
|
|
825
801
|
const branch = worktreeIdentity(input.taskSegment, input.roleName).branch;
|
|
826
802
|
const path = managedPath(container, input.directory);
|
|
827
803
|
const project = await this.inspect(input.repositoryPath);
|
|
@@ -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 =
|
|
923
|
-
|
|
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";
|
|
@@ -970,13 +945,13 @@ export class NodeGitWorkspace {
|
|
|
970
945
|
return "removed";
|
|
971
946
|
}
|
|
972
947
|
async inspectWorktree(input) {
|
|
973
|
-
const container = await
|
|
948
|
+
const container = await cleanupContainer(input.container);
|
|
974
949
|
const path = managedPath(container, input.directory);
|
|
975
950
|
const kind = await pathKind(path);
|
|
976
951
|
const branch = input.expectedBranch ?? worktreeIdentity(input.taskSegment, input.roleName).branch;
|
|
977
952
|
if (kind === undefined) {
|
|
978
953
|
const project = await this.inspect(input.repositoryPath);
|
|
979
|
-
await inspectMissingWorktreeRegistration(project.root, path, branch);
|
|
954
|
+
await inspectMissingWorktreeRegistration(project.root, path, branch, input.expectedBranch !== undefined);
|
|
980
955
|
return "missing";
|
|
981
956
|
}
|
|
982
957
|
if (kind === "symlink")
|
|
@@ -984,7 +959,7 @@ export class NodeGitWorkspace {
|
|
|
984
959
|
const canonicalContainerPath = await canonicalContainer(container, false);
|
|
985
960
|
const project = await this.inspect(input.repositoryPath);
|
|
986
961
|
await assertOwnedWorktree(project, canonicalContainerPath, path);
|
|
987
|
-
await assertExpectedBranch(path, branch);
|
|
962
|
+
await assertExpectedBranch(path, branch, input.expectedBranch !== undefined);
|
|
988
963
|
const porcelain = await readWorktreeStatus(path);
|
|
989
964
|
return porcelain.length > 0 ? "dirty" : "clean";
|
|
990
965
|
}
|
|
@@ -1000,19 +975,19 @@ export class NodeGitWorkspace {
|
|
|
1000
975
|
});
|
|
1001
976
|
}
|
|
1002
977
|
async #removeManagedWorktree(input) {
|
|
1003
|
-
const container = await
|
|
978
|
+
const container = await cleanupContainer(input.container);
|
|
1004
979
|
const path = managedPath(container, input.identity.directory);
|
|
1005
980
|
const kind = await pathKind(path);
|
|
1006
981
|
if (kind === "symlink")
|
|
1007
982
|
throw new Error("Managed worktree path must not be a symbolic link.");
|
|
1008
983
|
const repository = await this.inspect(input.repositoryPath);
|
|
1009
984
|
if (kind === undefined) {
|
|
1010
|
-
await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch);
|
|
985
|
+
await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch, true);
|
|
1011
986
|
}
|
|
1012
987
|
if (kind === "directory") {
|
|
1013
988
|
const canonicalContainerPath = await canonicalContainer(container, false);
|
|
1014
989
|
await assertOwnedWorktree(repository, canonicalContainerPath, path);
|
|
1015
|
-
await assertExpectedBranch(path, input.identity.branch);
|
|
990
|
+
await assertExpectedBranch(path, input.identity.branch, true);
|
|
1016
991
|
if (input.discardChanges === true) {
|
|
1017
992
|
await git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
|
|
1018
993
|
}
|
|
@@ -1043,21 +1018,22 @@ export class NodeGitWorkspace {
|
|
|
1043
1018
|
/** Missing paths do not imply missing Git metadata. Never prune another
|
|
1044
1019
|
* workspace's registration as a side effect of cleaning this exact owner.
|
|
1045
1020
|
*/
|
|
1046
|
-
async function removeMissingWorktreeRegistration(repositoryRoot, path, branch) {
|
|
1047
|
-
if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch))
|
|
1021
|
+
async function removeMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
|
|
1022
|
+
if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached))
|
|
1048
1023
|
return;
|
|
1049
1024
|
if (await pathKind(path) !== undefined)
|
|
1050
1025
|
throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
|
|
1051
1026
|
await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
|
|
1052
1027
|
}
|
|
1053
|
-
async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch) {
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
if (fields === undefined)
|
|
1028
|
+
async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
|
|
1029
|
+
const entry = (await readGitWorktrees(repositoryRoot)).find(record => record.path === path);
|
|
1030
|
+
if (entry === undefined)
|
|
1057
1031
|
return false;
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1032
|
+
const pinnedDetached = allowDetached && entry.detached
|
|
1033
|
+
&& await gitSucceeds(["-C", repositoryRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`])
|
|
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 });
|
|
1061
1037
|
}
|
|
1062
1038
|
return true;
|
|
1063
1039
|
}
|
|
@@ -1100,8 +1076,7 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
|
|
|
1100
1076
|
// Rebase temporarily detaches HEAD. Git's per-worktree metadata must still
|
|
1101
1077
|
// identify this exact Integration branch; unrelated detachment is invalid.
|
|
1102
1078
|
for (const backend of ["rebase-merge", "rebase-apply"]) {
|
|
1103
|
-
const headNamePath = await
|
|
1104
|
-
"--git-path", `${backend}/head-name`]);
|
|
1079
|
+
const headNamePath = await gitPath(path, ["--git-path", `${backend}/head-name`]);
|
|
1105
1080
|
let headName;
|
|
1106
1081
|
try {
|
|
1107
1082
|
headName = (await readFile(headNamePath, "utf8")).trim();
|
|
@@ -1115,7 +1090,12 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
|
|
|
1115
1090
|
return;
|
|
1116
1091
|
throw new Error(`Managed worktree is rebasing an unexpected branch: ${headName}.`);
|
|
1117
1092
|
}
|
|
1118
|
-
|
|
1093
|
+
// A terminal Integration can legitimately retain a detached checkout.
|
|
1094
|
+
// Its exact owned path/common-dir were checked by the caller; require its
|
|
1095
|
+
// retained branch to pin the same commit before allowing clean removal.
|
|
1096
|
+
if (await resolveRefCommit(path, "HEAD") === await resolveRefCommit(path, `refs/heads/${expected}`))
|
|
1097
|
+
return;
|
|
1098
|
+
cleanupFailure("workspace-identity-mismatch", "Detached worktree is not pinned by its owned branch or rebase.", expected, "unproven detached HEAD");
|
|
1119
1099
|
}
|
|
1120
1100
|
const branch = await gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
|
|
1121
1101
|
if (branch !== expected) {
|
|
@@ -1127,10 +1107,10 @@ async function assertOwnedWorktree(project, container, path) {
|
|
|
1127
1107
|
assertContained(container, canonicalPath);
|
|
1128
1108
|
if (canonicalPath !== path)
|
|
1129
1109
|
throw new Error("Managed worktree resolves through a symbolic link.");
|
|
1130
|
-
const root = await canonicalDirectory(await
|
|
1110
|
+
const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Managed worktree root");
|
|
1131
1111
|
if (root !== path)
|
|
1132
1112
|
throw new Error("Managed worktree root does not match its deterministic path.");
|
|
1133
|
-
const common = await canonicalDirectory(await
|
|
1113
|
+
const common = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Managed Git common directory");
|
|
1134
1114
|
if (common !== project.gitDirectory) {
|
|
1135
1115
|
cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
|
|
1136
1116
|
}
|
|
@@ -1154,7 +1134,7 @@ async function inspectExactRecordedWorktree(input) {
|
|
|
1154
1134
|
if (path !== expectedPath) {
|
|
1155
1135
|
throw new Error("Recorded managed worktree resolves through a symbolic link.");
|
|
1156
1136
|
}
|
|
1157
|
-
const root = await canonicalDirectory(await
|
|
1137
|
+
const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Recorded managed worktree root");
|
|
1158
1138
|
if (root !== path) {
|
|
1159
1139
|
throw new Error("Recorded managed worktree root does not match its deterministic path.");
|
|
1160
1140
|
}
|
|
@@ -1162,7 +1142,7 @@ async function inspectExactRecordedWorktree(input) {
|
|
|
1162
1142
|
if (branch !== input.branch) {
|
|
1163
1143
|
throw new Error(`Recorded managed worktree is on an unexpected branch: ${branch}.`);
|
|
1164
1144
|
}
|
|
1165
|
-
const gitDirectory = await canonicalDirectory(await
|
|
1145
|
+
const gitDirectory = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Recorded managed Git common directory");
|
|
1166
1146
|
const destination = await new NodeGitWorkspace().inspect(input.repositoryPath);
|
|
1167
1147
|
const head = await resolveRefCommit(path, "HEAD");
|
|
1168
1148
|
const retained = await resolveRefCommit(destination.root, `refs/heads/${input.branch}`);
|
|
@@ -1237,9 +1217,30 @@ async function canonicalContainer(path, create) {
|
|
|
1237
1217
|
// stay rejected by the managed-path lstat checks.
|
|
1238
1218
|
return canonicalDirectory(lexical, "Worktree container");
|
|
1239
1219
|
}
|
|
1220
|
+
/** A previous removal may have deleted the whole owner container before its
|
|
1221
|
+
* receipt committed. Resolve only surviving ancestors, without creating it.
|
|
1222
|
+
* Git registration inspection remains mandatory even when this path is absent.
|
|
1223
|
+
*/
|
|
1224
|
+
async function cleanupContainer(path) {
|
|
1225
|
+
const lexical = resolve(path);
|
|
1226
|
+
if (await pathKind(lexical) === "symlink") {
|
|
1227
|
+
cleanupFailure("workspace-identity-mismatch", "Cleanup container must not be a symbolic link.", "owned directory or missing path", "symbolic link");
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
return await canonicalContainer(lexical, false);
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
if (!isErrno(error, "ENOENT"))
|
|
1234
|
+
throw error;
|
|
1235
|
+
const parent = dirname(lexical);
|
|
1236
|
+
if (parent === lexical)
|
|
1237
|
+
throw error;
|
|
1238
|
+
return join(await cleanupContainer(parent), relative(parent, lexical));
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1240
1241
|
async function canonicalDirectory(path, label) {
|
|
1241
|
-
|
|
1242
|
-
const canonical = await realpath(isAbsolute(
|
|
1242
|
+
requireText(path, label);
|
|
1243
|
+
const canonical = await realpath(isAbsolute(path) ? path : resolve(path));
|
|
1243
1244
|
return canonical;
|
|
1244
1245
|
}
|
|
1245
1246
|
function managedPath(container, directory) {
|
|
@@ -1278,7 +1279,7 @@ async function readWorktreeStatus(path) {
|
|
|
1278
1279
|
return readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
|
|
1279
1280
|
}
|
|
1280
1281
|
async function readStatusGit(path, args) {
|
|
1281
|
-
return git(["--no-optional-locks", "-c", "core.fsmonitor=
|
|
1282
|
+
return git(["--no-optional-locks", "-c", "core.fsmonitor=",
|
|
1282
1283
|
"-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
|
|
1283
1284
|
"-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
|
|
1284
1285
|
}
|
|
@@ -1324,8 +1325,10 @@ async function assertStatusProgramsAbsent(path) {
|
|
|
1324
1325
|
}
|
|
1325
1326
|
}
|
|
1326
1327
|
async function git(args, options = {}) {
|
|
1328
|
+
const program = supportedGit();
|
|
1329
|
+
const invocation = compatibleGitArguments(args);
|
|
1327
1330
|
try {
|
|
1328
|
-
const execution = executeFile(
|
|
1331
|
+
const execution = executeFile(program.path, invocation, {
|
|
1329
1332
|
encoding: "utf8",
|
|
1330
1333
|
maxBuffer: 1024 * 1024,
|
|
1331
1334
|
timeout: 30_000,
|
|
@@ -1346,6 +1349,7 @@ async function git(args, options = {}) {
|
|
|
1346
1349
|
}
|
|
1347
1350
|
}
|
|
1348
1351
|
async function gitSucceeds(args) {
|
|
1352
|
+
supportedGit();
|
|
1349
1353
|
try {
|
|
1350
1354
|
await git(args);
|
|
1351
1355
|
return true;
|
|
@@ -1355,7 +1359,7 @@ async function gitSucceeds(args) {
|
|
|
1355
1359
|
}
|
|
1356
1360
|
}
|
|
1357
1361
|
async function gitLine(args) {
|
|
1358
|
-
const lines = (await git(args)).
|
|
1362
|
+
const lines = gitOutputLine(await git(args)).split("\n");
|
|
1359
1363
|
if (lines.length !== 1 || lines[0]?.length === 0 || lines[0]?.includes("\0")) {
|
|
1360
1364
|
throw new Error("Git returned invalid output.");
|
|
1361
1365
|
}
|
|
@@ -1504,6 +1508,7 @@ function safeRef(value) {
|
|
|
1504
1508
|
return ref;
|
|
1505
1509
|
}
|
|
1506
1510
|
async function safeFetchBranch(value) {
|
|
1511
|
+
supportedGit();
|
|
1507
1512
|
const configuredRef = safeRef(value);
|
|
1508
1513
|
if (configuredRef === "HEAD")
|
|
1509
1514
|
return configuredRef;
|
|
@@ -1595,7 +1600,8 @@ function isCommit(value) {
|
|
|
1595
1600
|
return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
|
|
1596
1601
|
}
|
|
1597
1602
|
function safeRemote(value) {
|
|
1598
|
-
|
|
1603
|
+
requireText(value, "Git remote URL");
|
|
1604
|
+
const remote = value;
|
|
1599
1605
|
if (remote.startsWith("-") || /[\r\n]/.test(remote)) {
|
|
1600
1606
|
throw new Error("Git remote URL is invalid.");
|
|
1601
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
|
+
}
|