peaks-loop 4.0.4 → 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 +34 -0
- package/README-en.md +26 -0
- package/README.md +26 -0
- package/dist/cli/commands/_register.js +3 -0
- 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.js +12 -4
- 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
|
@@ -54,7 +54,7 @@ function majorOf(version) {
|
|
|
54
54
|
const major = cleaned.split('.')[0];
|
|
55
55
|
return major !== undefined && /^\d+$/.test(major) ? major : undefined;
|
|
56
56
|
}
|
|
57
|
-
function detectComponentLibrary(deps) {
|
|
57
|
+
export function detectComponentLibrary(projectRoot, deps) {
|
|
58
58
|
if ('antd' in deps) {
|
|
59
59
|
const major = majorOf(deps['antd']);
|
|
60
60
|
const hasProSuite = '@ant-design/pro-components' in deps || Object.keys(deps).some((d) => d.startsWith('@ant-design/pro-'));
|
|
@@ -80,6 +80,19 @@ function detectComponentLibrary(deps) {
|
|
|
80
80
|
return { name: 'chakra' };
|
|
81
81
|
if ('vant' in deps)
|
|
82
82
|
return { name: 'vant' };
|
|
83
|
+
// shadcn / ui is a Tailwind-based component primitive set shipped as
|
|
84
|
+
// copyable source files under `components/ui/`. It has no top-level
|
|
85
|
+
// package of its own, so the signal we use is: Tailwind is present
|
|
86
|
+
// AND at least one of its peer utilities is also present.
|
|
87
|
+
if (('class-variance-authority' in deps
|
|
88
|
+
|| 'clsx' in deps
|
|
89
|
+
|| 'tailwind-merge' in deps
|
|
90
|
+
|| 'lucide-react' in deps)
|
|
91
|
+
&& ('tailwindcss' in deps
|
|
92
|
+
|| existsSync(join(projectRoot, 'components', 'ui'))
|
|
93
|
+
|| existsSync(join(projectRoot, 'src', 'components', 'ui')))) {
|
|
94
|
+
return { name: 'shadcn' };
|
|
95
|
+
}
|
|
83
96
|
return { name: 'none' };
|
|
84
97
|
}
|
|
85
98
|
function detectCssFrameworks(projectRoot, deps) {
|
|
@@ -235,7 +248,7 @@ function notableDepsList(deps) {
|
|
|
235
248
|
export function detectProjectContext(projectRoot) {
|
|
236
249
|
const { exists, deps } = readPackageJson(projectRoot);
|
|
237
250
|
const { tool, path } = detectBuildTool(projectRoot);
|
|
238
|
-
const componentLibrary = detectComponentLibrary(deps);
|
|
251
|
+
const componentLibrary = detectComponentLibrary(projectRoot, deps);
|
|
239
252
|
const cssFrameworks = detectCssFrameworks(projectRoot, deps);
|
|
240
253
|
return {
|
|
241
254
|
hasPackageJson: exists,
|
|
@@ -291,6 +304,8 @@ export function componentLibraryLabel(lib) {
|
|
|
291
304
|
return 'NextUI';
|
|
292
305
|
case 'chakra':
|
|
293
306
|
return 'Chakra UI';
|
|
307
|
+
case 'shadcn':
|
|
308
|
+
return 'shadcn / ui';
|
|
294
309
|
case 'vant':
|
|
295
310
|
return 'Vant (mobile)';
|
|
296
311
|
case 'none':
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProjectContext } from './project-context.js';
|
|
1
2
|
/**
|
|
2
3
|
* Typed error raised when a planned `peaks standards` write target would
|
|
3
4
|
* land outside `projectRoot` (or collide with the user-level baseline
|
|
@@ -106,6 +107,14 @@ export type ProjectStandardsInitOptions = {
|
|
|
106
107
|
* the file-probe logic.
|
|
107
108
|
*/
|
|
108
109
|
export declare function detectLanguage(projectRoot: string): StandardsLanguage;
|
|
110
|
+
/**
|
|
111
|
+
* UI library priority rule (effective 2026-08-01). Public so unit tests
|
|
112
|
+
* can pin the contract without reaching through the plan builder.
|
|
113
|
+
*
|
|
114
|
+
* Returns the rule line, or `null` when the project has no detected
|
|
115
|
+
* component library (in which case the rule is intentionally silent).
|
|
116
|
+
*/
|
|
117
|
+
export declare function renderUiLibraryPriorityRule(ctx: ProjectContext): string | null;
|
|
109
118
|
export declare function createProjectStandardsInitPlan(options: ProjectStandardsInitOptions): ProjectStandardsInitPlan;
|
|
110
119
|
export declare function createProjectStandardsUpdatePlan(options: ProjectStandardsInitOptions): ProjectStandardsUpdatePlan;
|
|
111
120
|
export declare function executeProjectStandardsInit(options: ProjectStandardsInitOptions): ProjectStandardsInitResult;
|
|
@@ -245,6 +245,25 @@ function renderSecurity(ctx) {
|
|
|
245
245
|
const rules = extra.length > 0 ? [...baseRules, '', '## Project-specific security focus', ...extra] : baseRules;
|
|
246
246
|
return `${renderHeader('Security Review Standards')}${rules.join('\n')}\n`;
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* UI library priority rule (effective 2026-08-01). Public so unit tests
|
|
250
|
+
* can pin the contract without reaching through the plan builder.
|
|
251
|
+
*
|
|
252
|
+
* Returns the rule line, or `null` when the project has no detected
|
|
253
|
+
* component library (in which case the rule is intentionally silent).
|
|
254
|
+
*/
|
|
255
|
+
export function renderUiLibraryPriorityRule(ctx) {
|
|
256
|
+
const lib = ctx.componentLibrary.name;
|
|
257
|
+
const UI_LIB_NAMES = [
|
|
258
|
+
'antd', 'antd-pro', 'mui', 'shadcn',
|
|
259
|
+
'chakra', 'element-plus', 'element-ui',
|
|
260
|
+
'arco', 'tdesign', 'semi', 'nextui'
|
|
261
|
+
];
|
|
262
|
+
if (!UI_LIB_NAMES.includes(lib)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return `- UI library priority (peaks-loop, effective 2026-08-01): this project uses \`${lib}\`. Prefer the library's exported components over hand-rolled native DOM / HTML primitives. Native DOM is acceptable only for primitives the library does not ship; leave a one-line comment naming the library primitive that was unavailable. Library themes and tokens are the source of truth; do NOT introduce a parallel CSS framework or inline styles that fight the library.`;
|
|
266
|
+
}
|
|
248
267
|
function renderLanguageCodingStyle(language, ctx) {
|
|
249
268
|
const languageName = language === 'generic' ? 'Generic' : language[0].toUpperCase() + language.slice(1);
|
|
250
269
|
const typeSafetyRule = language === 'typescript' || language === 'javascript'
|
|
@@ -267,6 +286,13 @@ function renderLanguageCodingStyle(language, ctx) {
|
|
|
267
286
|
if ((language === 'typescript' || language === 'javascript') && ctx.buildTool === 'umi') {
|
|
268
287
|
extra.push('- Use the project\'s existing service-layer pattern (`src/services/**`) for API calls; do not hand-roll `fetch` in components.');
|
|
269
288
|
}
|
|
289
|
+
// UI library priority (effective 2026-08-01). Applies to any project whose
|
|
290
|
+
// scan identified one of the supported component libraries; the rule
|
|
291
|
+
// is generated by the standards update so it ships to every downstream
|
|
292
|
+
// project through `peaks standards init/update`.
|
|
293
|
+
const uiRule = renderUiLibraryPriorityRule(ctx);
|
|
294
|
+
if (uiRule !== null)
|
|
295
|
+
extra.push(uiRule);
|
|
270
296
|
const rules = extra.length > 0 ? [...baseRules, '', '## Project-specific rules', ...extra] : baseRules;
|
|
271
297
|
return `${renderHeader(`${languageName} Coding Standards`)}${rules.join('\n')}\n`;
|
|
272
298
|
}
|
|
@@ -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": "0.0.
|
|
103
|
-
"peaks-loop-shared-channel": "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();
|