peaks-loop 4.0.3 → 4.0.5
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/CHANGELOG.md +55 -0
- package/README-en.md +26 -0
- package/README.md +26 -0
- package/dist/cli/commands/_register.js +3 -0
- package/dist/cli/commands/code-runtime-commands.js +15 -2
- package/dist/cli/commands/core/skill-command.js +2 -0
- package/dist/cli/commands/dispatch-commands.js +41 -2
- package/dist/cli/commands/e2e-verify.d.ts +47 -0
- package/dist/cli/commands/e2e-verify.js +211 -0
- package/dist/cli/commands/gate-commands.js +17 -4
- package/dist/cli/commands/statusline-commands.d.ts +6 -0
- package/dist/cli/commands/statusline-commands.js +48 -11
- package/dist/cli/commands/sub-agent-shutdown-commands.d.ts +31 -0
- package/dist/cli/commands/sub-agent-shutdown-commands.js +110 -0
- package/dist/cli/commands/worktree-auth-commands.js +27 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +39 -0
- package/dist/services/code/auto-compact-orchestrator.js +358 -41
- package/dist/services/code/post-compact-detector.js +12 -4
- package/dist/services/code/step-08-gate.js +6 -2
- package/dist/services/compact-statusline/compact-lifecycle-store.d.ts +68 -0
- package/dist/services/compact-statusline/compact-lifecycle-store.js +222 -0
- package/dist/services/compact-statusline/compact-statusline-service.d.ts +25 -5
- package/dist/services/compact-statusline/compact-statusline-service.js +244 -54
- package/dist/services/context/auto-compact-reader.d.ts +33 -4
- package/dist/services/context/auto-compact-reader.js +77 -22
- package/dist/services/context/auto-compact-types.d.ts +10 -2
- package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
- package/dist/services/context/build-dispatch-system-prompt.js +33 -2
- package/dist/services/dispatch/conflict-replay.d.ts +32 -0
- package/dist/services/dispatch/conflict-replay.js +44 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +31 -1
- package/dist/services/dispatch/dispatch-record-writer.js +27 -7
- package/dist/services/dispatch/e2e-fixtures.d.ts +18 -0
- package/dist/services/dispatch/e2e-fixtures.js +72 -0
- package/dist/services/dispatch/merge-back-runner.d.ts +18 -0
- package/dist/services/dispatch/merge-back-runner.js +83 -0
- package/dist/services/dispatch/post-merge.d.ts +21 -0
- package/dist/services/dispatch/post-merge.js +13 -0
- package/dist/services/dispatch/service-shutdown.d.ts +22 -0
- package/dist/services/dispatch/service-shutdown.js +51 -0
- package/dist/services/hooks/presence-marker-detector.js +12 -4
- package/dist/services/hooks/worktree-authorization-gate.d.ts +4 -0
- package/dist/services/hooks/worktree-authorization-gate.js +22 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +86 -3
- package/dist/services/skills/skill-statusline-renderer.js +374 -29
- package/dist/services/skills/skill-statusline-service.d.ts +2 -0
- package/dist/services/skills/skill-statusline-service.js +34 -5
- package/dist/services/skills/sync-service.d.ts +3 -0
- package/dist/services/skills/sync-service.js +1 -0
- package/dist/services/standards/project-context.d.ts +2 -1
- package/dist/services/standards/project-context.js +17 -2
- package/dist/services/standards/project-standards-service.d.ts +9 -0
- package/dist/services/standards/project-standards-service.js +26 -0
- package/dist/services/worktree/dispatch-provenance.d.ts +25 -0
- package/dist/services/worktree/dispatch-provenance.js +47 -0
- package/dist/services/worktree/git-worktree-parser.d.ts +8 -0
- package/dist/services/worktree/git-worktree-parser.js +45 -0
- package/dist/services/worktree/host-worktree-reconciler.d.ts +15 -0
- package/dist/services/worktree/host-worktree-reconciler.js +39 -0
- package/dist/services/worktree/long-path-cleanup.d.ts +10 -0
- package/dist/services/worktree/long-path-cleanup.js +38 -0
- package/dist/services/worktree/playwright-profile.d.ts +8 -0
- package/dist/services/worktree/playwright-profile.js +23 -0
- package/dist/services/worktree/worktree-gc-guard.d.ts +8 -0
- package/dist/services/worktree/worktree-gc-guard.js +38 -0
- package/package.json +4 -4
- package/scripts/install-skills.mjs +11 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const DISPATCH_PROVENANCE_ENV = "PEAKS_SUB_AGENT_DISPATCH_PROVENANCE";
|
|
2
|
+
export type DispatchProvenance = {
|
|
3
|
+
readonly schemaVersion: 1;
|
|
4
|
+
readonly token: string;
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
readonly requestId: string;
|
|
7
|
+
readonly leaseId: string;
|
|
8
|
+
readonly isolation: 'worktree';
|
|
9
|
+
readonly issuedAt: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function createDispatchProvenanceToken(input: {
|
|
12
|
+
readonly sessionId: string;
|
|
13
|
+
readonly requestId: string;
|
|
14
|
+
readonly leaseId: string;
|
|
15
|
+
}): string;
|
|
16
|
+
export declare function dispatchProvenanceFilePath(projectRoot: string, sessionId: string, token: string): string;
|
|
17
|
+
export declare function writeDispatchProvenance(input: {
|
|
18
|
+
readonly projectRoot: string;
|
|
19
|
+
readonly record: DispatchProvenance;
|
|
20
|
+
}): void;
|
|
21
|
+
export declare function readDispatchProvenance(input: {
|
|
22
|
+
readonly projectRoot: string;
|
|
23
|
+
readonly sessionId: string;
|
|
24
|
+
readonly token: string;
|
|
25
|
+
}): DispatchProvenance | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export const DISPATCH_PROVENANCE_ENV = 'PEAKS_SUB_AGENT_DISPATCH_PROVENANCE';
|
|
5
|
+
const PROVENANCE_DIR = 'dispatch-provenance';
|
|
6
|
+
export function createDispatchProvenanceToken(input) {
|
|
7
|
+
return createHash('sha256')
|
|
8
|
+
.update(`${input.sessionId}|${input.requestId}|${input.leaseId}`)
|
|
9
|
+
.digest('hex')
|
|
10
|
+
.slice(0, 32);
|
|
11
|
+
}
|
|
12
|
+
export function dispatchProvenanceFilePath(projectRoot, sessionId, token) {
|
|
13
|
+
return join(projectRoot, '.peaks', '_runtime', sessionId, PROVENANCE_DIR, `${token}.json`);
|
|
14
|
+
}
|
|
15
|
+
export function writeDispatchProvenance(input) {
|
|
16
|
+
const path = dispatchProvenanceFilePath(input.projectRoot, input.record.sessionId, input.record.token);
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
18
|
+
const tempPath = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
19
|
+
writeFileSync(tempPath, `${JSON.stringify(input.record, null, 2)}\n`, 'utf8');
|
|
20
|
+
renameSync(tempPath, path);
|
|
21
|
+
}
|
|
22
|
+
export function readDispatchProvenance(input) {
|
|
23
|
+
if (!/^[a-f0-9]{32}$/.test(input.token))
|
|
24
|
+
return null;
|
|
25
|
+
const path = dispatchProvenanceFilePath(input.projectRoot, input.sessionId, input.token);
|
|
26
|
+
if (!existsSync(path))
|
|
27
|
+
return null;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
30
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
31
|
+
return null;
|
|
32
|
+
const value = parsed;
|
|
33
|
+
if (value.schemaVersion !== 1 ||
|
|
34
|
+
value.token !== input.token ||
|
|
35
|
+
value.sessionId !== input.sessionId ||
|
|
36
|
+
typeof value.requestId !== 'string' ||
|
|
37
|
+
typeof value.leaseId !== 'string' ||
|
|
38
|
+
value.isolation !== 'worktree' ||
|
|
39
|
+
typeof value.issuedAt !== 'string') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type GitWorktreeRecord = {
|
|
2
|
+
readonly path: string;
|
|
3
|
+
readonly head: string | null;
|
|
4
|
+
readonly branch: string | null;
|
|
5
|
+
readonly prunable: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function parseGitWorktreePorcelain(raw: string): ReadonlyArray<GitWorktreeRecord>;
|
|
8
|
+
export declare function isPathInside(parent: string, candidate: string): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { relative, resolve, sep } from 'node:path';
|
|
2
|
+
export function parseGitWorktreePorcelain(raw) {
|
|
3
|
+
const records = [];
|
|
4
|
+
let current = null;
|
|
5
|
+
const flush = () => {
|
|
6
|
+
if (current !== null) {
|
|
7
|
+
records.push({
|
|
8
|
+
path: current.path,
|
|
9
|
+
head: current.head,
|
|
10
|
+
branch: current.branch,
|
|
11
|
+
prunable: current.prunable,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
current = null;
|
|
15
|
+
};
|
|
16
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
17
|
+
if (line.length === 0) {
|
|
18
|
+
flush();
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const space = line.indexOf(' ');
|
|
22
|
+
const key = space === -1 ? line : line.slice(0, space);
|
|
23
|
+
const value = space === -1 ? '' : line.slice(space + 1);
|
|
24
|
+
if (key === 'worktree') {
|
|
25
|
+
flush();
|
|
26
|
+
const next = { path: value, head: null, branch: null, prunable: false };
|
|
27
|
+
current = next;
|
|
28
|
+
}
|
|
29
|
+
else if (current !== null && key === 'HEAD') {
|
|
30
|
+
current = { path: current.path, head: value, branch: current.branch, prunable: current.prunable };
|
|
31
|
+
}
|
|
32
|
+
else if (current !== null && key === 'branch') {
|
|
33
|
+
current = { path: current.path, head: current.head, branch: value.replace(/^refs\/heads\//, ''), prunable: current.prunable };
|
|
34
|
+
}
|
|
35
|
+
else if (current !== null && key === 'prunable') {
|
|
36
|
+
current = { path: current.path, head: current.head, branch: current.branch, prunable: true };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
flush();
|
|
40
|
+
return records;
|
|
41
|
+
}
|
|
42
|
+
export function isPathInside(parent, candidate) {
|
|
43
|
+
const rel = relative(resolve(parent), resolve(candidate));
|
|
44
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !rel.startsWith(sep));
|
|
45
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type GitWorktreeRecord } from './git-worktree-parser.js';
|
|
2
|
+
export type HostWorktreeFinding = GitWorktreeRecord & {
|
|
3
|
+
readonly managedLeaseId: string | null;
|
|
4
|
+
readonly state: 'managed' | 'unleased' | 'prunable';
|
|
5
|
+
};
|
|
6
|
+
export type HostWorktreeReconcileResult = {
|
|
7
|
+
readonly hostRoot: string;
|
|
8
|
+
readonly findings: ReadonlyArray<HostWorktreeFinding>;
|
|
9
|
+
readonly unmanaged: ReadonlyArray<HostWorktreeFinding>;
|
|
10
|
+
};
|
|
11
|
+
export declare function reconcileHostWorktrees(input: {
|
|
12
|
+
readonly projectRoot: string;
|
|
13
|
+
readonly sessionId: string;
|
|
14
|
+
readonly hostRoot?: string;
|
|
15
|
+
}): HostWorktreeReconcileResult;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import { leaseStoreDir, listLeasesSync } from './worktree-lease.js';
|
|
5
|
+
import { isPathInside, parseGitWorktreePorcelain } from './git-worktree-parser.js';
|
|
6
|
+
export function reconcileHostWorktrees(input) {
|
|
7
|
+
const hostRoot = resolve(input.hostRoot ?? join(input.projectRoot, '.claude', 'worktrees'));
|
|
8
|
+
let records = [];
|
|
9
|
+
try {
|
|
10
|
+
const raw = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
11
|
+
cwd: input.projectRoot,
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
14
|
+
});
|
|
15
|
+
records = parseGitWorktreePorcelain(raw);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
records = [];
|
|
19
|
+
}
|
|
20
|
+
const storeDir = leaseStoreDir(join(input.projectRoot, '.peaks', '_runtime', input.sessionId));
|
|
21
|
+
const leaseResult = listLeasesSync(storeDir, {
|
|
22
|
+
existsSync,
|
|
23
|
+
readdir: (path) => readdirSync(path),
|
|
24
|
+
readFile: (path) => readFileSync(path, 'utf8'),
|
|
25
|
+
});
|
|
26
|
+
const leases = leaseResult.kind === 'ok' ? leaseResult.leases : [];
|
|
27
|
+
const leaseByPath = new Map(leases.map((lease) => [resolve(lease.path), lease.leaseId]));
|
|
28
|
+
const findings = records
|
|
29
|
+
.filter((record) => isPathInside(hostRoot, record.path) && basename(record.path).startsWith('agent-'))
|
|
30
|
+
.map((record) => {
|
|
31
|
+
const managedLeaseId = leaseByPath.get(resolve(record.path)) ?? null;
|
|
32
|
+
return {
|
|
33
|
+
...record,
|
|
34
|
+
managedLeaseId,
|
|
35
|
+
state: record.prunable ? 'prunable' : managedLeaseId === null ? 'unleased' : 'managed',
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
return { hostRoot, findings, unmanaged: findings.filter((finding) => finding.state !== 'managed') };
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type LongPathCleanupResult = {
|
|
2
|
+
readonly removed: boolean;
|
|
3
|
+
readonly strategy: 'git' | 'windows-long-path' | 'none';
|
|
4
|
+
readonly error?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function windowsLongPath(path: string): string;
|
|
7
|
+
export declare function removeRegisteredWorktree(input: {
|
|
8
|
+
readonly projectRoot: string;
|
|
9
|
+
readonly worktreePath: string;
|
|
10
|
+
}): LongPathCleanupResult;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
export function windowsLongPath(path) {
|
|
5
|
+
const absolute = resolve(path);
|
|
6
|
+
if (process.platform !== 'win32' || absolute.startsWith('\\\\?\\'))
|
|
7
|
+
return absolute;
|
|
8
|
+
if (absolute.startsWith('\\\\'))
|
|
9
|
+
return `\\\\?\\UNC\\${absolute.slice(2)}`;
|
|
10
|
+
return `\\\\?\\${absolute}`;
|
|
11
|
+
}
|
|
12
|
+
export function removeRegisteredWorktree(input) {
|
|
13
|
+
try {
|
|
14
|
+
execFileSync('git', ['worktree', 'remove', '--force', input.worktreePath], {
|
|
15
|
+
cwd: input.projectRoot,
|
|
16
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
});
|
|
18
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: input.projectRoot, stdio: 'ignore' });
|
|
19
|
+
return { removed: !existsSync(input.worktreePath), strategy: 'git' };
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (process.platform !== 'win32') {
|
|
23
|
+
return { removed: false, strategy: 'none', error: error instanceof Error ? error.message : String(error) };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
rmSync(windowsLongPath(input.worktreePath), { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
27
|
+
execFileSync('git', ['worktree', 'prune'], { cwd: input.projectRoot, stdio: 'ignore' });
|
|
28
|
+
return { removed: !existsSync(input.worktreePath), strategy: 'windows-long-path' };
|
|
29
|
+
}
|
|
30
|
+
catch (fallbackError) {
|
|
31
|
+
return {
|
|
32
|
+
removed: false,
|
|
33
|
+
strategy: 'none',
|
|
34
|
+
error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* playwright-profile — deterministic Chromium user-data-dir +
|
|
3
|
+
* profile-name pair generator for a (session, dispatch) tuple.
|
|
4
|
+
*
|
|
5
|
+
* Slice 2026-08-01-subagent-merge-and-e2e (Task 2). Each sub-agent
|
|
6
|
+
* dispatch with `--isolation worktree` MUST land its Playwright MCP
|
|
7
|
+
* browser session in a unique Chromium profile so concurrent
|
|
8
|
+
* dispatches do not share cookies / localStorage / IndexedDB. The
|
|
9
|
+
* pair is:
|
|
10
|
+
*
|
|
11
|
+
* userDataDir = <projectRoot>/.peaks/_runtime/<sessionId>/pw-profiles/<dispatchId>
|
|
12
|
+
* profileName = "dispatch-<dispatchId>"
|
|
13
|
+
*
|
|
14
|
+
* The userDataDir is gitignored (it lives under `.peaks/_runtime/`)
|
|
15
|
+
* so cookies do not leak across slices. The profileName is the
|
|
16
|
+
* Chromium `--profile-directory` flag the Playwright MCP server
|
|
17
|
+
* reads when it spawns the browser.
|
|
18
|
+
*/
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
export function playwrightProfilePaths(input) {
|
|
21
|
+
const userDataDir = join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'pw-profiles', input.dispatchId);
|
|
22
|
+
return { userDataDir, profileName: `dispatch-${input.dispatchId}` };
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type SkillJunctionReference = {
|
|
2
|
+
readonly junctionPath: string;
|
|
3
|
+
readonly target: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function findManagedSkillJunctionReferences(input: {
|
|
6
|
+
readonly worktreePath: string;
|
|
7
|
+
readonly skillRoots: ReadonlyArray<string>;
|
|
8
|
+
}): ReadonlyArray<SkillJunctionReference>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
export function findManagedSkillJunctionReferences(input) {
|
|
4
|
+
const targetRoot = resolve(input.worktreePath);
|
|
5
|
+
const refs = [];
|
|
6
|
+
for (const skillRoot of input.skillRoots) {
|
|
7
|
+
if (!existsSync(skillRoot))
|
|
8
|
+
continue;
|
|
9
|
+
for (const name of readdirSync(skillRoot)) {
|
|
10
|
+
if (!name.startsWith('peaks-') || name.endsWith('.peaks-managed'))
|
|
11
|
+
continue;
|
|
12
|
+
const junctionPath = resolve(skillRoot, name);
|
|
13
|
+
const markerPath = `${junctionPath}.peaks-managed`;
|
|
14
|
+
if (!existsSync(markerPath))
|
|
15
|
+
continue;
|
|
16
|
+
try {
|
|
17
|
+
if (!lstatSync(junctionPath).isSymbolicLink())
|
|
18
|
+
continue;
|
|
19
|
+
const link = readlinkSync(junctionPath);
|
|
20
|
+
const resolvedTarget = resolve(dirname(junctionPath), link);
|
|
21
|
+
if (isInside(targetRoot, resolvedTarget))
|
|
22
|
+
refs.push({ junctionPath, target: resolvedTarget });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
const managedTarget = readFileSync(markerPath, 'utf8').trim();
|
|
26
|
+
if (managedTarget.length > 0 && isInside(targetRoot, managedTarget)) {
|
|
27
|
+
refs.push({ junctionPath, target: managedTarget });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return refs;
|
|
33
|
+
}
|
|
34
|
+
function isInside(parent, candidate) {
|
|
35
|
+
const normalizedParent = resolve(parent).toLowerCase();
|
|
36
|
+
const normalizedCandidate = resolve(candidate).toLowerCase();
|
|
37
|
+
return normalizedCandidate === normalizedParent || normalizedCandidate.startsWith(`${normalizedParent}\\`) || normalizedCandidate.startsWith(`${normalizedParent}/`);
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.5",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -98,9 +98,9 @@
|
|
|
98
98
|
"headroom-ai": "0.22.4",
|
|
99
99
|
"yaml": "^2.9.0",
|
|
100
100
|
"zod": "^3.25.76",
|
|
101
|
-
"peaks-loop-mut": "0.1.
|
|
102
|
-
"peaks-loop-shared
|
|
103
|
-
"peaks-loop-shared": "0.0.
|
|
101
|
+
"peaks-loop-mut": "0.1.9",
|
|
102
|
+
"peaks-loop-shared": "0.0.35",
|
|
103
|
+
"peaks-loop-shared-channel": "0.0.14"
|
|
104
104
|
},
|
|
105
105
|
"peerDependencies": {
|
|
106
106
|
"@alibaba-group/open-code-review": "1.3.1"
|
|
@@ -641,11 +641,20 @@ export function installBundledSkills(options = {}) {
|
|
|
641
641
|
if (current) {
|
|
642
642
|
const managedTarget = getManagedTarget(targetPath);
|
|
643
643
|
const linkTarget = current.isSymbolicLink() ? readlinkSync(targetPath) : null;
|
|
644
|
-
|
|
644
|
+
const linkTargetExists = typeof linkTarget === 'string' && existsSync(linkTarget);
|
|
645
|
+
if (linkTarget === sourcePath && linkTargetExists) {
|
|
645
646
|
installed.push(skillName);
|
|
646
647
|
continue;
|
|
647
648
|
}
|
|
648
|
-
|
|
649
|
+
// Managed Junctions may point into an ephemeral host worktree. If that
|
|
650
|
+
// worktree was removed, Windows keeps the Junction entry but its target
|
|
651
|
+
// no longer resolves. Repair only when reconciliation was explicitly
|
|
652
|
+
// requested; normal postinstall keeps its existing ownership behavior.
|
|
653
|
+
if (
|
|
654
|
+
(current.isSymbolicLink() || isBrokenSymlink(current, targetPath)) &&
|
|
655
|
+
(managedTarget === linkTarget ||
|
|
656
|
+
(options.reconcileJunctions === true && managedTarget !== null && !linkTargetExists))
|
|
657
|
+
) {
|
|
649
658
|
validateSkillsRoot();
|
|
650
659
|
unlinkSync(targetPath);
|
|
651
660
|
validateSkillsRoot();
|