gentle-pi 2.4.0 → 2.6.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 +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +39 -11
- package/assets/orchestrator.md +5 -5
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +240 -0
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +656 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +156 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync } from "node:fs";
|
|
3
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, utimesSync } from "node:fs";
|
|
4
4
|
import { mkdtempSync } from "node:fs";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
8
|
+
import { assertCandidateOwnerParent, createCandidateOwner, prepareCandidateOwnerParent, removeCandidateOwner, samePath, sweepCandidateOwners, type CandidateViewOwner } from "./review-candidate-view-owner.ts";
|
|
8
9
|
|
|
9
10
|
const REVIEW_LENS = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
|
|
10
11
|
export type ReviewLens = (typeof REVIEW_LENS)[number];
|
|
@@ -12,6 +13,11 @@ const CANDIDATE_GIT_TIMEOUT_MS = 10_000;
|
|
|
12
13
|
const CANDIDATE_GIT_TIMEOUT_MAX_MS = 120_000;
|
|
13
14
|
const CANDIDATE_GIT_TIMEOUT_ENV = "GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS";
|
|
14
15
|
const CANDIDATE_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
16
|
+
// Keep the copied index at least one timestamp tick behind its source. The
|
|
17
|
+
// two-second target also clears filesystems whose mtime granularity is a second
|
|
18
|
+
// or coarser without needlessly assigning an arbitrary historical timestamp.
|
|
19
|
+
const PRIVATE_INDEX_RACY_SAFETY_NS = 1_000_000_000n;
|
|
20
|
+
const PRIVATE_INDEX_RACY_BACKDATE_NS = 2_000_000_000n;
|
|
15
21
|
|
|
16
22
|
// Candidate views may materialize full repository trees. Large repositories can
|
|
17
23
|
// raise this bounded deadline without creating an unbounded child process.
|
|
@@ -94,6 +100,7 @@ interface CandidateViewScope {
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
interface CandidateViewRecord {
|
|
103
|
+
owner: CandidateViewOwner;
|
|
97
104
|
token: string;
|
|
98
105
|
root: string;
|
|
99
106
|
parent: string;
|
|
@@ -231,8 +238,8 @@ export interface CandidateViewDiagnostic {
|
|
|
231
238
|
export class CandidateViewError extends Error {
|
|
232
239
|
readonly reason: string;
|
|
233
240
|
readonly diagnostics?: CandidateViewDiagnostic;
|
|
234
|
-
constructor(message: string, reason = "candidate-view-invalid", diagnostics?: CandidateViewDiagnostic) {
|
|
235
|
-
super(message);
|
|
241
|
+
constructor(message: string, reason = "candidate-view-invalid", diagnostics?: CandidateViewDiagnostic, options?: ErrorOptions) {
|
|
242
|
+
super(message, options);
|
|
236
243
|
this.name = "CandidateViewError";
|
|
237
244
|
this.reason = reason;
|
|
238
245
|
this.diagnostics = diagnostics === undefined ? undefined : sanitizeCandidateViewDiagnostic(diagnostics);
|
|
@@ -333,12 +340,37 @@ function isMaterializedCandidateMode(mode: string): boolean {
|
|
|
333
340
|
return mode === "100644" || mode === "100755" || mode === "120000";
|
|
334
341
|
}
|
|
335
342
|
|
|
343
|
+
export function hasExpectedExecutableBits(filesystemMode: number, gitMode: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
344
|
+
return gitMode === "100755" && platform === "win32"
|
|
345
|
+
|| (filesystemMode & 0o111) === (gitMode === "100755" ? 0o111 : 0);
|
|
346
|
+
}
|
|
347
|
+
|
|
336
348
|
function isCanonicalObjectId(objectId: string): boolean { return /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(objectId); }
|
|
337
349
|
function gitlinkMapsEqual(left: Readonly<Record<string, string>>, right: Readonly<Record<string, string>>): boolean {
|
|
338
350
|
const entries = Object.entries(left);
|
|
339
351
|
return entries.length === Object.keys(right).length && entries.every(([path, objectId]) => right[path] === objectId);
|
|
340
352
|
}
|
|
341
353
|
|
|
354
|
+
// Two materialized candidate views describe the exact same reviewable content
|
|
355
|
+
// when their base, candidate tree, commit-state, and changed scope all match.
|
|
356
|
+
// This is what makes a retried native START's freshly-materialized duplicate
|
|
357
|
+
// view safely discardable in favor of an already-bound one (gentle-pi
|
|
358
|
+
// candidate-view rebind defect, ga#4085 / ga#4050): the comparison never
|
|
359
|
+
// trusts a caller-supplied claim, only Git-derived identity already computed
|
|
360
|
+
// by materializeCandidateView for both records.
|
|
361
|
+
function candidateRecordsShareIdentity(left: CandidateViewRecord, right: CandidateViewRecord): boolean {
|
|
362
|
+
return left.contributorRoot === right.contributorRoot &&
|
|
363
|
+
left.baseCommit === right.baseCommit &&
|
|
364
|
+
left.baseTree === right.baseTree &&
|
|
365
|
+
left.candidateTree === right.candidateTree &&
|
|
366
|
+
left.committedOnly === right.committedOnly &&
|
|
367
|
+
JSON.stringify(left.intendedUntracked ?? null) === JSON.stringify(right.intendedUntracked ?? null) &&
|
|
368
|
+
JSON.stringify(left.scope.paths) === JSON.stringify(right.scope.paths) &&
|
|
369
|
+
JSON.stringify(left.scope.modes) === JSON.stringify(right.scope.modes) &&
|
|
370
|
+
gitlinkMapsEqual(left.scope.gitlinks, right.scope.gitlinks) &&
|
|
371
|
+
JSON.stringify(left.scope.deletedPaths) === JSON.stringify(right.scope.deletedPaths);
|
|
372
|
+
}
|
|
373
|
+
|
|
342
374
|
function decodeCanonicalPath(value: Buffer): string {
|
|
343
375
|
const path = value.toString("utf8");
|
|
344
376
|
if (!Buffer.from(path, "utf8").equals(value) || !isSafeCandidatePath(path)) {
|
|
@@ -602,12 +634,20 @@ function makeWritableForCleanup(path: string): void {
|
|
|
602
634
|
chmodSync(path, 0o644);
|
|
603
635
|
}
|
|
604
636
|
|
|
605
|
-
function candidateViewParent(commonDir: string): string {
|
|
606
|
-
const
|
|
637
|
+
function candidateViewParent(commonDir: string, platform: NodeJS.Platform): string {
|
|
638
|
+
const control = join(commonDir, "gentle-ai");
|
|
639
|
+
mkdirSync(control, { recursive: true, mode: 0o700 });
|
|
640
|
+
const controlStat = lstatSync(control);
|
|
641
|
+
if (!controlStat.isDirectory() || controlStat.isSymbolicLink() || realpathSync(control) !== control) throw new CandidateViewError("candidate view ancestor is unsafe");
|
|
642
|
+
const parent = join(control, "candidate-views");
|
|
607
643
|
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
608
644
|
const stat = lstatSync(parent);
|
|
609
645
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new CandidateViewError("candidate view parent is unsafe");
|
|
610
|
-
|
|
646
|
+
try {
|
|
647
|
+
return prepareCandidateOwnerParent(commonDir, platform);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
throw new CandidateViewError("candidate view owner preparation failed", "candidate-owner-preparation-failed", undefined, { cause: error });
|
|
650
|
+
}
|
|
611
651
|
}
|
|
612
652
|
|
|
613
653
|
export interface ResolvedCandidateBase {
|
|
@@ -780,11 +820,29 @@ function isErrnoCode(error: unknown, code: string): boolean {
|
|
|
780
820
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
781
821
|
}
|
|
782
822
|
|
|
823
|
+
function timestampSeconds(timestampNs: bigint): number {
|
|
824
|
+
// Node's utimes API takes Unix seconds as a number. Splitting the value keeps
|
|
825
|
+
// the seconds conversion exact; the explicit racy-clean backdate below is far
|
|
826
|
+
// larger than the fractional precision a Number can represent at this epoch.
|
|
827
|
+
return Number(timestampNs / 1_000_000_000n) + Number(timestampNs % 1_000_000_000n) / 1_000_000_000;
|
|
828
|
+
}
|
|
829
|
+
|
|
783
830
|
function seedPrivateIndexFromLiveIndex(cwd: string, indexPath: string, executor: CandidateGitExecutor): boolean {
|
|
784
831
|
const liveIndex = resolve(cwd, git(cwd, ["rev-parse", "--path-format=absolute", "--git-path", "index"], process.env, executor));
|
|
785
|
-
const entry = lstatSync(liveIndex, { throwIfNoEntry: false });
|
|
832
|
+
const entry = lstatSync(liveIndex, { bigint: true, throwIfNoEntry: false });
|
|
786
833
|
if (entry === undefined) return false; if (!entry.isFile()) throw new CandidateViewError("candidate live Git index is not a regular file");
|
|
834
|
+
if (entry.mtimeNs < PRIVATE_INDEX_RACY_BACKDATE_NS) throw new CandidateViewError("candidate live Git index timestamp is too early for racy-clean protection");
|
|
787
835
|
copyFileSync(liveIndex, indexPath);
|
|
836
|
+
// Git's racy-clean check compares index and tracked-file mtimes. copyFileSync
|
|
837
|
+
// gives the private index a new timestamp; restoring a Date can also lose
|
|
838
|
+
// nanoseconds. Backdate the bigint live timestamp instead, then verify the
|
|
839
|
+
// filesystem applied enough of that bounded interval for Git to refresh
|
|
840
|
+
// content rather than trusting a racy-clean stat match.
|
|
841
|
+
utimesSync(indexPath, timestampSeconds(entry.atimeNs), timestampSeconds(entry.mtimeNs - PRIVATE_INDEX_RACY_BACKDATE_NS));
|
|
842
|
+
const privateMtimeNs = lstatSync(indexPath, { bigint: true }).mtimeNs;
|
|
843
|
+
if (privateMtimeNs >= entry.mtimeNs || entry.mtimeNs - privateMtimeNs < PRIVATE_INDEX_RACY_SAFETY_NS) {
|
|
844
|
+
throw new CandidateViewError("candidate private Git index timestamp cannot preserve racy-clean protection");
|
|
845
|
+
}
|
|
788
846
|
for (const name of readdirSync(dirname(liveIndex))) if (/^sharedindex\.[0-9a-f]+$/.test(name)) {
|
|
789
847
|
try {
|
|
790
848
|
const sharedIndex = join(dirname(liveIndex), name);
|
|
@@ -796,7 +854,7 @@ function seedPrivateIndexFromLiveIndex(cwd: string, indexPath: string, executor:
|
|
|
796
854
|
return true;
|
|
797
855
|
}
|
|
798
856
|
|
|
799
|
-
function materializeCandidateView(request: CreateCandidateViewRequest, executor: CandidateGitExecutor): CandidateViewRecord {
|
|
857
|
+
function materializeCandidateView(request: CreateCandidateViewRequest, executor: CandidateGitExecutor, platform: NodeJS.Platform = process.platform): CandidateViewRecord {
|
|
800
858
|
const contributorRoot = realpathSync(request.contributorRoot);
|
|
801
859
|
if (!lstatSync(contributorRoot).isDirectory()) throw new CandidateViewError("contributor root is not a directory");
|
|
802
860
|
if (request.committedOnly === true && request.baseRef === undefined) throw new CandidateViewError("committed-only candidate views require an explicit base reference", "committed-only-base-required");
|
|
@@ -808,7 +866,8 @@ function materializeCandidateView(request: CreateCandidateViewRequest, executor:
|
|
|
808
866
|
const candidateCommit = committedOnly
|
|
809
867
|
? resolveCandidateBase(contributorRoot, "HEAD", process.env, executor)
|
|
810
868
|
: base;
|
|
811
|
-
const parent = candidateViewParent(canonicalCommonDir);
|
|
869
|
+
const parent = candidateViewParent(canonicalCommonDir, platform);
|
|
870
|
+
sweepCandidateOwners(canonicalCommonDir, (args) => git(canonicalCommonDir, args, process.env, executor), makeWritableForCleanup, platform);
|
|
812
871
|
const index = mkdtempSync(join(tmpdir(), "gentle-ai-candidate-index-"));
|
|
813
872
|
const indexPath = join(index, "index");
|
|
814
873
|
const environment = { ...process.env, GIT_INDEX_FILE: indexPath };
|
|
@@ -832,6 +891,12 @@ function materializeCandidateView(request: CreateCandidateViewRequest, executor:
|
|
|
832
891
|
}
|
|
833
892
|
const candidateTree = git(contributorRoot, ["write-tree"], environment, executor);
|
|
834
893
|
const root = join(parent, randomUUID());
|
|
894
|
+
let owner: CandidateViewOwner;
|
|
895
|
+
try {
|
|
896
|
+
owner = createCandidateOwner(canonicalCommonDir, root, platform);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
throw new CandidateViewError("candidate view owner preparation failed", "candidate-owner-preparation-failed", undefined, { cause: error });
|
|
899
|
+
}
|
|
835
900
|
// The worktree is created under the same try/catch cleanup boundary as
|
|
836
901
|
// the read-tree materialization that follows. addUnbornWorktree's
|
|
837
902
|
// fallback path can register a worktree with `worktree add` and then
|
|
@@ -852,9 +917,9 @@ function materializeCandidateView(request: CreateCandidateViewRequest, executor:
|
|
|
852
917
|
const scope = deriveChangedScope(contributorRoot, base.tree, candidateTree, [...tree.entries, ...tree.gitlinks], executor);
|
|
853
918
|
for (const gitlink of tree.gitlinks) if (lstatSync(join(root, gitlink.path), { throwIfNoEntry: false })) throw new CandidateViewError("candidate view materialized a metadata-only gitlink");
|
|
854
919
|
makeReadonly(root, entries);
|
|
855
|
-
return { token: basename(root), root: realpathSync(root), parent, contributorRoot, commonDir: canonicalCommonDir, baseCommit, baseTree: base.tree, candidateTree, committedOnly, intendedUntracked, entries, gitlinks: tree.gitlinks, scope, gitExecutor: executor };
|
|
920
|
+
return { owner, token: basename(root), root: realpathSync(root), parent, contributorRoot, commonDir: canonicalCommonDir, baseCommit, baseTree: base.tree, candidateTree, committedOnly, intendedUntracked, entries, gitlinks: tree.gitlinks, scope, gitExecutor: executor };
|
|
856
921
|
} catch (error) {
|
|
857
|
-
try {
|
|
922
|
+
try { removeCandidateOwner(owner, (args) => git(canonicalCommonDir, args, process.env, executor), makeWritableForCleanup, false, platform); } catch { /* Preserve the marker and any partial worktree for conservative recovery. */ }
|
|
858
923
|
throw error;
|
|
859
924
|
}
|
|
860
925
|
} finally {
|
|
@@ -862,7 +927,7 @@ function materializeCandidateView(request: CreateCandidateViewRequest, executor:
|
|
|
862
927
|
}
|
|
863
928
|
}
|
|
864
929
|
|
|
865
|
-
function assertRecordSafe(record: CandidateViewRecord): void {
|
|
930
|
+
function assertRecordSafe(record: CandidateViewRecord, platform: NodeJS.Platform = process.platform): void {
|
|
866
931
|
try {
|
|
867
932
|
const contributor = lstatSync(record.contributorRoot);
|
|
868
933
|
if (!contributor.isDirectory() || contributor.isSymbolicLink() || realpathSync(record.contributorRoot) !== record.contributorRoot) {
|
|
@@ -870,7 +935,7 @@ function assertRecordSafe(record: CandidateViewRecord): void {
|
|
|
870
935
|
}
|
|
871
936
|
const toplevel = realpathSync(git(record.contributorRoot, ["rev-parse", "--show-toplevel"], process.env, record.gitExecutor));
|
|
872
937
|
const commonDir = realpathSync(resolve(record.contributorRoot, git(record.contributorRoot, ["rev-parse", "--git-common-dir"], process.env, record.gitExecutor)));
|
|
873
|
-
if (toplevel
|
|
938
|
+
if (!samePath(toplevel, record.contributorRoot, platform) || !samePath(commonDir, record.commonDir, platform)) {
|
|
874
939
|
throw new CandidateViewError("candidate contributor root Git identity changed", "contributor-root-drift");
|
|
875
940
|
}
|
|
876
941
|
} catch (error) {
|
|
@@ -878,6 +943,7 @@ function assertRecordSafe(record: CandidateViewRecord): void {
|
|
|
878
943
|
throw new CandidateViewError("candidate contributor root identity cannot be verified", "contributor-root-drift");
|
|
879
944
|
}
|
|
880
945
|
const root = record.root;
|
|
946
|
+
assertCandidateOwnerParent(record.commonDir, platform);
|
|
881
947
|
if (!isWithin(record.parent, root) || !existsSync(root)) throw new CandidateViewError("candidate view is missing or moved");
|
|
882
948
|
const rootStat = lstatSync(root);
|
|
883
949
|
if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || realpathSync(root) !== root) throw new CandidateViewError("candidate view root is unsafe");
|
|
@@ -902,7 +968,7 @@ function assertRecordSafe(record: CandidateViewRecord): void {
|
|
|
902
968
|
if (!item) throw new CandidateViewError("candidate view entry is missing or moved");
|
|
903
969
|
if (entry.mode === "120000") {
|
|
904
970
|
if (!item.isSymbolicLink()) throw new CandidateViewError("candidate view symlink is unsafe or changed");
|
|
905
|
-
} else if (!item.isFile() || item.isSymbolicLink() || (item.mode & 0o222) !== 0 || (
|
|
971
|
+
} else if (!item.isFile() || item.isSymbolicLink() || (item.mode & 0o222) !== 0 || !hasExpectedExecutableBits(item.mode, entry.mode, platform)) {
|
|
906
972
|
throw new CandidateViewError("candidate view entry is unsafe, writable, or has a changed mode");
|
|
907
973
|
}
|
|
908
974
|
const actualHash = entryContentHash(root, entry);
|
|
@@ -913,8 +979,10 @@ function assertRecordSafe(record: CandidateViewRecord): void {
|
|
|
913
979
|
export class CandidateViewRegistry {
|
|
914
980
|
private readonly records = new Map<string, CandidateViewRecord>();
|
|
915
981
|
private readonly gitExecutor: CandidateGitExecutor;
|
|
916
|
-
|
|
982
|
+
private readonly platform: NodeJS.Platform;
|
|
983
|
+
constructor(gitExecutor: CandidateGitExecutor = defaultCandidateGitExecutor, platform: NodeJS.Platform = process.platform) {
|
|
917
984
|
this.gitExecutor = gitExecutor;
|
|
985
|
+
this.platform = platform;
|
|
918
986
|
}
|
|
919
987
|
// Lifecycle state is scoped to the canonical target worktree as well as the
|
|
920
988
|
// provider lineage. Lineage text is repository-local and may legitimately be
|
|
@@ -1000,15 +1068,25 @@ export class CandidateViewRegistry {
|
|
|
1000
1068
|
const key = this.lineageKey(contributorRoot, lineageId);
|
|
1001
1069
|
const token = this.lineages.get(key);
|
|
1002
1070
|
const record = token === undefined ? undefined : this.records.get(token);
|
|
1003
|
-
if (record !== undefined) assertRecordSafe(record);
|
|
1071
|
+
if (record !== undefined) assertRecordSafe(record, this.platform);
|
|
1004
1072
|
}
|
|
1005
1073
|
|
|
1006
1074
|
create(request: CreateCandidateViewRequest): CandidateView {
|
|
1007
1075
|
return this.createOrReuse(request);
|
|
1008
1076
|
}
|
|
1009
1077
|
|
|
1078
|
+
sweepOrphans(contributorRoot: string): void {
|
|
1079
|
+
try {
|
|
1080
|
+
const cwd = realpathSync(contributorRoot);
|
|
1081
|
+
const common = realpathSync(resolve(cwd, git(cwd, ["rev-parse", "--git-common-dir"], process.env, this.gitExecutor)));
|
|
1082
|
+
sweepCandidateOwners(common, (args) => git(common, args, process.env, this.gitExecutor), makeWritableForCleanup, this.platform);
|
|
1083
|
+
} catch { /* Non-repositories and unavailable ownership are preserved. */ }
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1010
1086
|
cleanupAll(): void {
|
|
1011
|
-
for (const token of [...this.records.keys()])
|
|
1087
|
+
for (const token of [...this.records.keys()]) {
|
|
1088
|
+
try { this.cleanup(token); } catch { /* Continue other owned views; failed records remain retryable. */ }
|
|
1089
|
+
}
|
|
1012
1090
|
}
|
|
1013
1091
|
|
|
1014
1092
|
createOrReuse(request: CreateCandidateViewRequest): CandidateView {
|
|
@@ -1017,8 +1095,8 @@ export class CandidateViewRegistry {
|
|
|
1017
1095
|
const scopedReplayKey = normalizedRequest.replayKey === undefined ? undefined : this.replayKey(contributorRoot, normalizedRequest.replayKey);
|
|
1018
1096
|
const token = scopedReplayKey === undefined ? undefined : this.replays.get(scopedReplayKey);
|
|
1019
1097
|
const existing = token === undefined ? undefined : this.records.get(token);
|
|
1020
|
-
if (existing) { assertRecordSafe(existing); return this.expose(existing); }
|
|
1021
|
-
const record = materializeCandidateView(normalizedRequest, this.gitExecutor);
|
|
1098
|
+
if (existing) { assertRecordSafe(existing, this.platform); return this.expose(existing); }
|
|
1099
|
+
const record = materializeCandidateView(normalizedRequest, this.gitExecutor, this.platform);
|
|
1022
1100
|
this.records.set(record.token, record);
|
|
1023
1101
|
if (scopedReplayKey !== undefined) this.replays.set(scopedReplayKey, record.token);
|
|
1024
1102
|
return this.expose(record);
|
|
@@ -1030,6 +1108,32 @@ export class CandidateViewRegistry {
|
|
|
1030
1108
|
|
|
1031
1109
|
bindCurrent(request: BindCandidateViewRequest): void {
|
|
1032
1110
|
const selectedLenses = this.validateSelectedLenses(request.selectedLenses);
|
|
1111
|
+
const candidate = this.records.get(request.token);
|
|
1112
|
+
const key = candidate === undefined ? undefined : this.lineageKey(candidate.contributorRoot, request.lineageId);
|
|
1113
|
+
const existingToken = key === undefined ? undefined : this.lineages.get(key);
|
|
1114
|
+
// A retried native START for a lineage this controller already bound
|
|
1115
|
+
// (native reports it "resumed") re-materializes a fresh, content-
|
|
1116
|
+
// identical candidate view under a new token before it reaches here.
|
|
1117
|
+
// Rebinding that duplicate to the same lineage key used to fail closed
|
|
1118
|
+
// with "candidate view lineage binding is missing or ambiguous" even
|
|
1119
|
+
// though nothing is actually ambiguous: it is the exact same reviewable
|
|
1120
|
+
// content, re-verified from Git. Discard the redundant duplicate and
|
|
1121
|
+
// keep the already-bound view current instead of failing the retry
|
|
1122
|
+
// (gentle-pi candidate-view rebind defect, ga#4085 / ga#4050).
|
|
1123
|
+
if (candidate !== undefined && existingToken !== undefined && existingToken !== request.token) {
|
|
1124
|
+
const existing = this.records.get(existingToken);
|
|
1125
|
+
let existingSafe = false;
|
|
1126
|
+
if (existing !== undefined && existing.lineageId === request.lineageId) {
|
|
1127
|
+
try { assertRecordSafe(existing, this.platform); existingSafe = true; } catch { existingSafe = false; }
|
|
1128
|
+
}
|
|
1129
|
+
if (existing !== undefined && existingSafe && candidateRecordsShareIdentity(existing, candidate)) {
|
|
1130
|
+
existing.selectedLenses = selectedLenses;
|
|
1131
|
+
this.remove(candidate);
|
|
1132
|
+
this.forget(candidate);
|
|
1133
|
+
this.current.set(existing.contributorRoot, { lineageId: request.lineageId, token: existingToken });
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1033
1137
|
const record = this.bindRecord(request.token, request.lineageId, selectedLenses);
|
|
1034
1138
|
this.current.set(record.contributorRoot, { lineageId: request.lineageId, token: request.token });
|
|
1035
1139
|
}
|
|
@@ -1043,7 +1147,7 @@ export class CandidateViewRegistry {
|
|
|
1043
1147
|
const record = this.records.get(request.token);
|
|
1044
1148
|
if (!record || record.lineageId !== undefined) throw new CandidateViewError("native reviewing candidate view is missing or already bound", "authoritative-current-match-missing");
|
|
1045
1149
|
if (this.current.has(record.contributorRoot)) throw new CandidateViewError("candidate view already has a current lineage binding", "current-binding-already-established");
|
|
1046
|
-
assertRecordSafe(record);
|
|
1150
|
+
assertRecordSafe(record, this.platform);
|
|
1047
1151
|
this.assertCurrentBindingMatchesLiveCandidate(record);
|
|
1048
1152
|
this.bindCurrent(request);
|
|
1049
1153
|
}
|
|
@@ -1060,7 +1164,7 @@ export class CandidateViewRegistry {
|
|
|
1060
1164
|
if (this.current.has(root)) throw new CandidateViewError("candidate view already has a current lineage binding", "current-binding-already-established");
|
|
1061
1165
|
if (states.length === 0) throw new CandidateViewError("no authoritative reviewing lineage exactly matches the live candidate", "authoritative-current-match-missing");
|
|
1062
1166
|
if (states.length !== 1) throw new CandidateViewError("multiple authoritative reviewing lineages exactly match the live candidate", "authoritative-current-match-ambiguous");
|
|
1063
|
-
const live = materializeCandidateView({ contributorRoot: root, baseRef: states[0]!.baseCommit, committedOnly: states[0]!.committedOnly === true, ...(states[0]!.intendedUntracked === undefined ? {} : { intendedUntracked: states[0]!.intendedUntracked }) }, this.gitExecutor);
|
|
1167
|
+
const live = materializeCandidateView({ contributorRoot: root, baseRef: states[0]!.baseCommit, committedOnly: states[0]!.committedOnly === true, ...(states[0]!.intendedUntracked === undefined ? {} : { intendedUntracked: states[0]!.intendedUntracked }) }, this.gitExecutor, this.platform);
|
|
1064
1168
|
try {
|
|
1065
1169
|
const matches = states.filter((state) => this.matchesAuthoritativeState(live, state));
|
|
1066
1170
|
if (matches.length === 0) throw new CandidateViewError("no authoritative reviewing lineage exactly matches the live candidate", "authoritative-current-match-missing");
|
|
@@ -1085,10 +1189,10 @@ export class CandidateViewRegistry {
|
|
|
1085
1189
|
const existing = existingToken === undefined ? undefined : this.records.get(existingToken);
|
|
1086
1190
|
if (existing) {
|
|
1087
1191
|
if (existing.lineageId !== undefined) throw new CandidateViewError("corrected candidate replay is no longer pending");
|
|
1088
|
-
assertRecordSafe(existing);
|
|
1192
|
+
assertRecordSafe(existing, this.platform);
|
|
1089
1193
|
return this.expose(existing);
|
|
1090
1194
|
}
|
|
1091
|
-
const record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(projection.intendedUntracked === undefined ? {} : { intendedUntracked: projection.intendedUntracked }) }, this.gitExecutor);
|
|
1195
|
+
const record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(projection.intendedUntracked === undefined ? {} : { intendedUntracked: projection.intendedUntracked }) }, this.gitExecutor, this.platform);
|
|
1092
1196
|
try {
|
|
1093
1197
|
if (record.baseCommit !== projection.baseCommit || record.baseTree !== projection.baseTree) throw new CandidateViewError("corrected candidate base does not match the frozen genesis base");
|
|
1094
1198
|
if (!record.scope.paths.every((path) => projection.paths.includes(path))) throw new CandidateViewError("corrected candidate scope escapes the frozen genesis paths");
|
|
@@ -1115,8 +1219,8 @@ export class CandidateViewRegistry {
|
|
|
1115
1219
|
if (currentBinding !== undefined && currentBinding.lineageId !== lineageId) {
|
|
1116
1220
|
throw new CandidateViewError("corrected candidate replacement conflicts with the current lineage binding");
|
|
1117
1221
|
}
|
|
1118
|
-
assertRecordSafe(replacement);
|
|
1119
|
-
if (current) assertRecordSafe(current);
|
|
1222
|
+
assertRecordSafe(replacement, this.platform);
|
|
1223
|
+
if (current) assertRecordSafe(current, this.platform);
|
|
1120
1224
|
if (
|
|
1121
1225
|
replacement.contributorRoot !== projection.contributorRoot ||
|
|
1122
1226
|
replacement.baseCommit !== projection.baseCommit ||
|
|
@@ -1177,7 +1281,7 @@ export class CandidateViewRegistry {
|
|
|
1177
1281
|
const record = this.records.get(token);
|
|
1178
1282
|
const key = record === undefined ? undefined : this.lineageKey(record.contributorRoot, lineageId);
|
|
1179
1283
|
if (!record || record.lineageId !== undefined || !key || this.lineages.has(key)) throw new CandidateViewError("candidate view lineage binding is missing or ambiguous");
|
|
1180
|
-
assertRecordSafe(record);
|
|
1284
|
+
assertRecordSafe(record, this.platform);
|
|
1181
1285
|
record.lineageId = lineageId;
|
|
1182
1286
|
record.selectedLenses = selectedLenses;
|
|
1183
1287
|
this.lineages.set(key, record.token);
|
|
@@ -1276,21 +1380,18 @@ export class CandidateViewRegistry {
|
|
|
1276
1380
|
* correction.
|
|
1277
1381
|
*/
|
|
1278
1382
|
rebindForFinalizeFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor): CandidateView {
|
|
1279
|
-
|
|
1280
|
-
const key = this.lineageKey(root, lineageId);
|
|
1281
|
-
const staleToken = this.lineages.get(key);
|
|
1282
|
-
const stale = staleToken === undefined ? undefined : this.records.get(staleToken);
|
|
1283
|
-
this.projections.delete(key);
|
|
1284
|
-
if (stale !== undefined) {
|
|
1285
|
-
this.remove(stale);
|
|
1286
|
-
this.forget(stale);
|
|
1287
|
-
}
|
|
1288
|
-
return this.restoreForFinalizeFromNative(lineageId, root, descriptor);
|
|
1383
|
+
return this.restoreForFinalizeFromNative(lineageId, contributorRoot, descriptor);
|
|
1289
1384
|
}
|
|
1290
1385
|
|
|
1386
|
+
/**
|
|
1387
|
+
* Restores a lineage's FINALIZE binding (gentle-pi #185): the stale entry
|
|
1388
|
+
* is only detached, not destroyed, so a failed restore can undo it.
|
|
1389
|
+
*/
|
|
1291
1390
|
restoreForFinalizeFromNative(lineageId: string, contributorRoot: string, descriptor: NativeCandidateProjectionDescriptor): CandidateView {
|
|
1292
1391
|
const root = this.canonicalRoot(contributorRoot);
|
|
1293
1392
|
const key = this.lineageKey(root, lineageId);
|
|
1393
|
+
const staleToken = this.lineages.get(key), staleProjection = this.projections.get(key);
|
|
1394
|
+
this.lineages.delete(key); this.projections.delete(key);
|
|
1294
1395
|
let projectionRestored = false;
|
|
1295
1396
|
let record: CandidateViewRecord | undefined;
|
|
1296
1397
|
try {
|
|
@@ -1299,15 +1400,17 @@ export class CandidateViewRegistry {
|
|
|
1299
1400
|
const projection = this.resolveProjection(lineageId, root);
|
|
1300
1401
|
const matchesProjection = (candidate: CandidateViewRecord): boolean => candidate.baseTree === projection.baseTree && candidate.candidateTree === projection.candidateTree && JSON.stringify(candidate.scope.paths) === JSON.stringify(projection.paths);
|
|
1301
1402
|
const emptyIntendedUntracked = projection.intendedUntracked?.length === 0;
|
|
1302
|
-
record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(!emptyIntendedUntracked && projection.intendedUntracked !== undefined ? { intendedUntracked: projection.intendedUntracked } : {}) }, this.gitExecutor);
|
|
1403
|
+
record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(!emptyIntendedUntracked && projection.intendedUntracked !== undefined ? { intendedUntracked: projection.intendedUntracked } : {}) }, this.gitExecutor, this.platform);
|
|
1303
1404
|
if (!matchesProjection(record) && emptyIntendedUntracked) {
|
|
1304
1405
|
this.remove(record);
|
|
1305
1406
|
record = undefined;
|
|
1306
|
-
record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, intendedUntracked: [] }, this.gitExecutor);
|
|
1407
|
+
record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, intendedUntracked: [] }, this.gitExecutor, this.platform);
|
|
1307
1408
|
}
|
|
1308
1409
|
if (!matchesProjection(record)) throw new CandidateViewError("live candidate does not match the native frozen projection");
|
|
1309
1410
|
this.records.set(record.token, record);
|
|
1310
1411
|
this.bindRecord(record.token, lineageId, []);
|
|
1412
|
+
const stale = staleToken === undefined ? undefined : this.records.get(staleToken);
|
|
1413
|
+
if (stale !== undefined) { this.remove(stale); this.forget(stale); }
|
|
1311
1414
|
return this.expose(record);
|
|
1312
1415
|
} catch (error) {
|
|
1313
1416
|
if (projectionRestored) this.projections.delete(key);
|
|
@@ -1315,6 +1418,8 @@ export class CandidateViewRegistry {
|
|
|
1315
1418
|
this.forget(record);
|
|
1316
1419
|
this.remove(record);
|
|
1317
1420
|
}
|
|
1421
|
+
if (staleToken !== undefined) this.lineages.set(key, staleToken);
|
|
1422
|
+
if (staleProjection !== undefined) this.projections.set(key, staleProjection);
|
|
1318
1423
|
throw error;
|
|
1319
1424
|
}
|
|
1320
1425
|
}
|
|
@@ -1339,7 +1444,7 @@ export class CandidateViewRegistry {
|
|
|
1339
1444
|
this.restoreProjectionFromNative(lineageId, root, descriptor);
|
|
1340
1445
|
projectionRestored = true;
|
|
1341
1446
|
const projection = this.resolveProjection(lineageId, root);
|
|
1342
|
-
record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(projection.intendedUntracked === undefined ? {} : { intendedUntracked: projection.intendedUntracked }) }, this.gitExecutor);
|
|
1447
|
+
record = materializeCandidateView({ contributorRoot: root, baseRef: projection.baseCommit, committedOnly: projection.committedOnly, ...(projection.intendedUntracked === undefined ? {} : { intendedUntracked: projection.intendedUntracked }) }, this.gitExecutor, this.platform);
|
|
1343
1448
|
if (record.baseTree !== projection.baseTree || record.candidateTree !== projection.candidateTree || JSON.stringify(record.scope.paths) !== JSON.stringify(projection.paths)) {
|
|
1344
1449
|
throw new CandidateViewError("live candidate does not match the native frozen projection");
|
|
1345
1450
|
}
|
|
@@ -1375,7 +1480,7 @@ export class CandidateViewRegistry {
|
|
|
1375
1480
|
const token = this.lineages.get(key);
|
|
1376
1481
|
const record = token === undefined ? undefined : this.records.get(token);
|
|
1377
1482
|
if (!record || record.lineageId !== lineageId || !record.selectedLenses?.includes(lens as ReviewLens)) throw new CandidateViewError("candidate view context is missing, ambiguous, stale, or lens-unselected");
|
|
1378
|
-
assertRecordSafe(record);
|
|
1483
|
+
assertRecordSafe(record, this.platform);
|
|
1379
1484
|
return this.expose(record);
|
|
1380
1485
|
}
|
|
1381
1486
|
|
|
@@ -1426,14 +1531,14 @@ export class CandidateViewRegistry {
|
|
|
1426
1531
|
const current = this.currentBinding(contributorRoot);
|
|
1427
1532
|
const record = this.records.get(current.token);
|
|
1428
1533
|
if (!record || record.lineageId !== current.lineageId || this.lineages.get(this.lineageKey(current.root, current.lineageId)) !== current.token) throw new CandidateViewError("review subagent dispatch current lineage binding is stale or ambiguous", "current-binding-stale");
|
|
1429
|
-
assertRecordSafe(record);
|
|
1534
|
+
assertRecordSafe(record, this.platform);
|
|
1430
1535
|
this.assertCurrentBindingMatchesLiveCandidate(record);
|
|
1431
1536
|
if (!lenses.every((lens) => record.selectedLenses?.includes(lens as ReviewLens))) throw new CandidateViewError("candidate view context is missing, ambiguous, stale, or lens-unselected", "current-binding-lens-unselected");
|
|
1432
1537
|
return lenses.map(() => this.expose(record));
|
|
1433
1538
|
}
|
|
1434
1539
|
|
|
1435
1540
|
private assertCurrentBindingMatchesLiveCandidate(record: CandidateViewRecord): void {
|
|
1436
|
-
const live = materializeCandidateView({ contributorRoot: record.contributorRoot, baseRef: record.baseCommit, committedOnly: record.committedOnly, ...(record.intendedUntracked === undefined ? {} : { intendedUntracked: record.intendedUntracked }) }, this.gitExecutor);
|
|
1541
|
+
const live = materializeCandidateView({ contributorRoot: record.contributorRoot, baseRef: record.baseCommit, committedOnly: record.committedOnly, ...(record.intendedUntracked === undefined ? {} : { intendedUntracked: record.intendedUntracked }) }, this.gitExecutor, this.platform);
|
|
1437
1542
|
try {
|
|
1438
1543
|
if (
|
|
1439
1544
|
live.baseCommit !== record.baseCommit ||
|
|
@@ -1455,7 +1560,7 @@ export class CandidateViewRegistry {
|
|
|
1455
1560
|
const token = this.lineages.get(key);
|
|
1456
1561
|
const record = token === undefined ? undefined : this.records.get(token);
|
|
1457
1562
|
if (!record || record.lineageId !== lineageId) throw new CandidateViewError("candidate view context is missing or ambiguous for FINALIZE");
|
|
1458
|
-
assertRecordSafe(record);
|
|
1563
|
+
assertRecordSafe(record, this.platform);
|
|
1459
1564
|
return this.expose(record);
|
|
1460
1565
|
}
|
|
1461
1566
|
|
|
@@ -1476,10 +1581,7 @@ export class CandidateViewRegistry {
|
|
|
1476
1581
|
|
|
1477
1582
|
private remove(record: CandidateViewRecord): void {
|
|
1478
1583
|
if (!isWithin(record.parent, record.root)) throw new CandidateViewError("candidate view cleanup escaped its owned parent");
|
|
1479
|
-
|
|
1480
|
-
try { git(record.contributorRoot, ["worktree", "remove", "--force", record.root], process.env, record.gitExecutor); } catch {}
|
|
1481
|
-
// Git removes worktree metadata; physical removal remains this owner's duty.
|
|
1482
|
-
rmSync(record.root, { recursive: true, force: true });
|
|
1584
|
+
removeCandidateOwner(record.owner, (args) => git(record.commonDir, args, process.env, record.gitExecutor), makeWritableForCleanup, false, this.platform);
|
|
1483
1585
|
}
|
|
1484
1586
|
|
|
1485
1587
|
private forget(record: CandidateViewRecord): void {
|
|
@@ -1511,7 +1613,7 @@ export class CandidateViewRegistry {
|
|
|
1511
1613
|
modes: record.scope.modes,
|
|
1512
1614
|
gitlinks: record.scope.gitlinks,
|
|
1513
1615
|
deletedPaths: record.scope.deletedPaths,
|
|
1514
|
-
verify: () => assertRecordSafe(record),
|
|
1616
|
+
verify: () => assertRecordSafe(record, this.platform),
|
|
1515
1617
|
cleanup: () => this.cleanup(record.token),
|
|
1516
1618
|
};
|
|
1517
1619
|
}
|