@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.
- package/README.md +15 -1
- 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 +18 -9
- 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 +65 -14
- 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 +18 -3
- 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 +25 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +39 -8
- package/dist/controller/globalInputDelivery.js +2 -4
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/providerRetryDelivery.js +4 -2
- 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/agentAdapter.js +3 -5
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +15 -7
- package/dist/integration/gitIntegrationService.js +7 -12
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/interaction/notificationPresentation.js +85 -0
- package/dist/interaction/operatorPresentation.js +1 -2
- package/dist/job/httpPreview.js +61 -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 +48 -71
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +2 -1
- 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/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/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/leaderWakeupProcessor.js +1 -9
- 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/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} +20 -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} +61 -51
- 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/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} +10 -10
- 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 +295 -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 +106 -8
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/cli-information-contract.md +1 -1
- package/docs/cli-information-contract.zh-CN.md +1 -1
- package/docs/managed-turn-and-session-runtime.md +31 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +22 -0
- package/docs/release-workflow.md +54 -0
- package/docs/release-workflow.zh-CN.md +39 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -4
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-operator/SKILL.md +3 -0
- package/skills/yui-reviewer/SKILL.md +4 -2
- package/skills/yui-runtime/SKILL.md +43 -2
- package/skills/yui-worker/SKILL.md +3 -2
- package/dist/web/assets/client/app.js +0 -808
- package/dist/web/assets/client/components.js +0 -477
- package/dist/web/assets/client/detail.js +0 -379
- package/dist/web/assets/client/dock.js +0 -309
- package/dist/web/assets/client/evidence.js +0 -364
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1277
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -198
- package/dist/web/assets/client/sections.js +0 -349
- 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 -216
- package/dist/web/assets/styles/views.js +0 -309
- /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",
|
|
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");
|
|
@@ -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";
|
|
@@ -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
|
|
1055
|
-
|
|
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 &&
|
|
1032
|
+
const pinnedDetached = allowDetached && entry.detached
|
|
1059
1033
|
&& await gitSucceeds(["-C", repositoryRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`])
|
|
1060
|
-
&&
|
|
1061
|
-
if ((
|
|
1062
|
-
cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1271
|
-
const canonical = await realpath(isAbsolute(
|
|
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=
|
|
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(
|
|
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)).
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
152
|
-
*
|
|
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
|
|
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(
|
|
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 !==
|
|
338
|
-
throw new Error("Project knowledge must use schemaVersion
|
|
367
|
+
if (entry.schemaVersion !== 2)
|
|
368
|
+
throw new Error("Project knowledge must use schemaVersion 2.");
|
|
339
369
|
requireIdentity(entry.id, "Project knowledge id");
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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 !==
|
|
398
|
-
throw new Error("Knowledge proposal must use schemaVersion
|
|
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
|
-
|
|
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}: `
|