peaks-cli 1.3.2 → 1.3.4
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 +6 -2
- package/dist/src/cli/commands/core-artifact-commands.js +6 -3
- package/dist/src/cli/commands/gate-commands.js +28 -19
- package/dist/src/cli/commands/hook-handle.d.ts +17 -0
- package/dist/src/cli/commands/hook-handle.js +111 -0
- package/dist/src/cli/commands/hooks-commands.js +72 -21
- package/dist/src/cli/commands/progress-commands.js +9 -2
- package/dist/src/cli/commands/progress-start-spawn.js +30 -4
- package/dist/src/cli/commands/project-commands.js +8 -4
- package/dist/src/cli/commands/statusline-commands.js +75 -17
- package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
- package/dist/src/cli/commands/sub-agent-commands.js +488 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +2 -1
- package/dist/src/cli/commands/workspace-commands.js +3 -0
- package/dist/src/cli/program.js +9 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
- package/dist/src/services/config/config-types.d.ts +1 -1
- package/dist/src/services/context/artifact-meta.d.ts +72 -0
- package/dist/src/services/context/artifact-meta.js +105 -0
- package/dist/src/services/context/context-guard.d.ts +49 -0
- package/dist/src/services/context/context-guard.js +91 -0
- package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
- package/dist/src/services/context/dispatch-context-guard.js +192 -0
- package/dist/src/services/context/headroom-client.d.ts +34 -0
- package/dist/src/services/context/headroom-client.js +117 -0
- package/dist/src/services/context/shared-channel.d.ts +92 -0
- package/dist/src/services/context/shared-channel.js +285 -0
- package/dist/src/services/context/threshold.d.ts +35 -0
- package/dist/src/services/context/threshold.js +76 -0
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
- package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
- package/dist/src/services/dispatch/batch-counter.js +85 -0
- package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
- package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
- package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
- package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
- package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
- package/dist/src/services/dispatch/leak-detector.js +72 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +80 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +42 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +98 -0
- package/dist/src/services/ide/hook-protocol.d.ts +47 -0
- package/dist/src/services/ide/hook-protocol.js +74 -0
- package/dist/src/services/ide/hook-translator.d.ts +72 -0
- package/dist/src/services/ide/hook-translator.js +128 -0
- package/dist/src/services/ide/ide-detector.d.ts +10 -0
- package/dist/src/services/ide/ide-detector.js +19 -0
- package/dist/src/services/ide/ide-registry.d.ts +14 -0
- package/dist/src/services/ide/ide-registry.js +45 -0
- package/dist/src/services/ide/ide-types.d.ts +180 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/resource-profile.d.ts +52 -0
- package/dist/src/services/ide/resource-profile.js +33 -0
- package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
- package/dist/src/services/ide/shared/atomic-json.js +58 -0
- package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
- package/dist/src/services/ide/shared/safe-path.js +29 -0
- package/dist/src/services/memory/project-context-service.js +2 -1
- package/dist/src/services/memory/project-memory-service.js +4 -3
- package/dist/src/services/perf/perf-baseline-service.js +2 -1
- package/dist/src/services/progress/progress-service.d.ts +1 -1
- package/dist/src/services/progress/progress-service.js +18 -14
- package/dist/src/services/security/safe-settings-path.d.ts +12 -0
- package/dist/src/services/security/safe-settings-path.js +104 -0
- package/dist/src/services/session/getSessionDir.d.ts +1 -0
- package/dist/src/services/session/getSessionDir.js +27 -0
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- package/dist/src/services/signal/cancel-handler.d.ts +14 -0
- package/dist/src/services/signal/cancel-handler.js +76 -0
- package/dist/src/services/skill/resume-detector.d.ts +54 -0
- package/dist/src/services/skill/resume-detector.js +334 -0
- package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
- package/dist/src/services/skill/skill-scheduler.js +53 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
- package/dist/src/services/skills/hooks-settings-service.js +190 -144
- package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
- package/dist/src/services/skills/statusline-settings-service.js +31 -34
- package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
- package/dist/src/services/slice/slice-archive-service.js +111 -0
- package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
- package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
- package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
- package/dist/src/services/solo/status-line-renderer.js +55 -0
- package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
- package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
- package/dist/src/services/standards/project-standards-service.d.ts +1 -2
- package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
- package/dist/src/services/workspace/reconcile-service.js +107 -6
- package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/scripts/install-skills.mjs +112 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-ide/references/audit-log-helper.md +52 -0
- package/skills/peaks-qa/SKILL.md +153 -55
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +134 -62
- package/skills/peaks-solo/SKILL.md +124 -37
- package/skills/peaks-solo/references/browser-workflow.md +22 -20
- package/skills/peaks-solo/references/context-governance.md +144 -0
- package/skills/peaks-solo/references/headroom-integration.md +107 -0
- package/skills/peaks-solo/references/runbook.md +3 -3
- package/skills/peaks-solo/references/sub-agent-dispatch.md +261 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +17 -0
- package/skills/peaks-ui/SKILL.md +45 -10
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IdeId } from './ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Detect which IDE a given project root is using, by looking for the IDE's
|
|
4
|
+
* settings directory (`.claude`, `.trae`, etc.). Returns the first match in
|
|
5
|
+
* adapter insertion order, or `null` if no adapter's directory is present.
|
|
6
|
+
*
|
|
7
|
+
* 启发式:基于 cwd 目录存在性。CLI 后续 slice 会扩展为 env 变量检测、settings
|
|
8
|
+
* 文件内容检测、显式 `--ide` flag 覆盖等。
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectInstalledIde(projectRoot: string): IdeId | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { listAdapters } from './ide-registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* Detect which IDE a given project root is using, by looking for the IDE's
|
|
6
|
+
* settings directory (`.claude`, `.trae`, etc.). Returns the first match in
|
|
7
|
+
* adapter insertion order, or `null` if no adapter's directory is present.
|
|
8
|
+
*
|
|
9
|
+
* 启发式:基于 cwd 目录存在性。CLI 后续 slice 会扩展为 env 变量检测、settings
|
|
10
|
+
* 文件内容检测、显式 `--ide` flag 覆盖等。
|
|
11
|
+
*/
|
|
12
|
+
export function detectInstalledIde(projectRoot) {
|
|
13
|
+
for (const adapter of listAdapters()) {
|
|
14
|
+
if (existsSync(join(projectRoot, adapter.settings.dirName))) {
|
|
15
|
+
return adapter.id;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IdeAdapter, IdeId } from './ide-types.js';
|
|
2
|
+
/** Get the adapter for a given IDE id. Throws on unsupported IDE. */
|
|
3
|
+
export declare function getAdapter(ide: IdeId): IdeAdapter;
|
|
4
|
+
/** All registered adapter ids (insertion order). */
|
|
5
|
+
export declare function listAdapterIds(): readonly IdeId[];
|
|
6
|
+
/** All registered adapters (insertion order). */
|
|
7
|
+
export declare function listAdapters(): readonly IdeAdapter[];
|
|
8
|
+
/**
|
|
9
|
+
* Test seam: register or replace an adapter. Used by future slices when adding
|
|
10
|
+
* a new IDE. Caller is responsible for ensuring the adapter is well-formed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function _setAdapterForTesting(ide: IdeId, adapter: IdeAdapter): void;
|
|
13
|
+
/** Test seam: reset to built-in defaults. */
|
|
14
|
+
export declare function _resetAdaptersForTesting(): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CLAUDE_CODE_ADAPTER } from './adapters/claude-code-adapter.js';
|
|
2
|
+
import { TRAE_ADAPTER } from './adapters/trae-adapter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Built-in IDE adapter registry。Map<IdeId, IdeAdapter> 是单一来源。
|
|
5
|
+
*
|
|
6
|
+
* Slice #1 注册 claude-code。
|
|
7
|
+
* Slice #2 注册 trae —— 这是 slice #1 抽出的 IdeAdapter 形状的
|
|
8
|
+
* 第一个真实客户,验证"填表"承诺。
|
|
9
|
+
* 后续 slice 注入 codex / cursor / qoder / tongyi-lingma 时,只需在此
|
|
10
|
+
* Map 加条目 —— 所有 adapter 使用方(hook-translator、hooks install、statusline
|
|
11
|
+
* install、mcp apply)通过 `getAdapter(ide)` 拿取,无需修改。
|
|
12
|
+
*/
|
|
13
|
+
const ADAPTERS = new Map([
|
|
14
|
+
['claude-code', CLAUDE_CODE_ADAPTER],
|
|
15
|
+
['trae', TRAE_ADAPTER],
|
|
16
|
+
]);
|
|
17
|
+
/** Get the adapter for a given IDE id. Throws on unsupported IDE. */
|
|
18
|
+
export function getAdapter(ide) {
|
|
19
|
+
const adapter = ADAPTERS.get(ide);
|
|
20
|
+
if (!adapter) {
|
|
21
|
+
throw new Error(`Unsupported IDE: ${ide}. Registered: ${listAdapterIds().join(', ') || '(none)'}`);
|
|
22
|
+
}
|
|
23
|
+
return adapter;
|
|
24
|
+
}
|
|
25
|
+
/** All registered adapter ids (insertion order). */
|
|
26
|
+
export function listAdapterIds() {
|
|
27
|
+
return Array.from(ADAPTERS.keys());
|
|
28
|
+
}
|
|
29
|
+
/** All registered adapters (insertion order). */
|
|
30
|
+
export function listAdapters() {
|
|
31
|
+
return Array.from(ADAPTERS.values());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Test seam: register or replace an adapter. Used by future slices when adding
|
|
35
|
+
* a new IDE. Caller is responsible for ensuring the adapter is well-formed.
|
|
36
|
+
*/
|
|
37
|
+
export function _setAdapterForTesting(ide, adapter) {
|
|
38
|
+
ADAPTERS.set(ide, adapter);
|
|
39
|
+
}
|
|
40
|
+
/** Test seam: reset to built-in defaults. */
|
|
41
|
+
export function _resetAdaptersForTesting() {
|
|
42
|
+
ADAPTERS.clear();
|
|
43
|
+
ADAPTERS.set('claude-code', CLAUDE_CODE_ADAPTER);
|
|
44
|
+
ADAPTERS.set('trae', TRAE_ADAPTER);
|
|
45
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks 自有 hook 协议 + slim IDE adapter 接口。
|
|
3
|
+
*
|
|
4
|
+
* peaks-cli 不再适配 IDE 私有的 hook 协议;反之 peaks 定义自己的 canonical
|
|
5
|
+
* schema,每个 IDE 只需要填 4 字符串 + 1 settings 函数,新 IDE 适配变成"填表"。
|
|
6
|
+
*
|
|
7
|
+
* 不可消除的 per-IDE 字段(诚实交代,见 PRD R-1..R-4):
|
|
8
|
+
* - settings.json 物理位置
|
|
9
|
+
* - 项目根 env 变量名
|
|
10
|
+
* - hook 事件名 + matcher 名
|
|
11
|
+
*
|
|
12
|
+
* 其他全部归一化到 peaks 内部模型(见 hook-protocol.ts)。
|
|
13
|
+
*/
|
|
14
|
+
import type { SubAgentDispatcher } from '../dispatch/sub-agent-dispatcher.js';
|
|
15
|
+
export type IdeId = 'claude-code' | 'trae' | 'codex' | 'cursor' | 'qoder' | 'tongyi-lingma';
|
|
16
|
+
export interface IdeCapabilities {
|
|
17
|
+
/** peaks gate enforce 是否适用该 IDE(必备) */
|
|
18
|
+
readonly gateEnforce: true;
|
|
19
|
+
/** peaks progress start(sub-agent 派发)是否适用 */
|
|
20
|
+
readonly progressStart: boolean;
|
|
21
|
+
/** peaks statusline 状态栏是否适用 */
|
|
22
|
+
readonly statusline: boolean;
|
|
23
|
+
/** peaks mcp install 是否适用 */
|
|
24
|
+
readonly mcpInstall: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface IdeSettingsLocation {
|
|
27
|
+
/** 项目根下的 settings 目录名,例如 '.claude' / '.trae' / '.cursor' */
|
|
28
|
+
readonly dirName: string;
|
|
29
|
+
/** settings 文件名(部分 IDE 叫 settings.json / mcp.json) */
|
|
30
|
+
readonly settingsFileName: string;
|
|
31
|
+
/** 解析出 settings.json 绝对路径 */
|
|
32
|
+
resolveSettingsFile(scope: 'project' | 'global', projectRoot: string | undefined): string;
|
|
33
|
+
/** 该 IDE 是否支持此 scope(用于清晰报错) */
|
|
34
|
+
supportsScope(scope: 'project' | 'global'): boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Slim IDE adapter 描述。每 IDE 一个静态常量(无需 DI)。
|
|
38
|
+
* 字段故意保持少:让 adapter 的添加是"填表"而非"重写"。
|
|
39
|
+
*/
|
|
40
|
+
export interface IdeAdapter {
|
|
41
|
+
readonly id: IdeId;
|
|
42
|
+
/** 人类可读名,出现在 CLI help / 命令输出 */
|
|
43
|
+
readonly displayName: string;
|
|
44
|
+
readonly settings: IdeSettingsLocation;
|
|
45
|
+
/** IDE 注入的项目根 env 变量名;`peaks gate enforce` 等命令模板会引用此 env */
|
|
46
|
+
readonly envVar: string;
|
|
47
|
+
/** settings.json 里 hook 数组的 key,例如 'PreToolUse' / 'beforeToolCall' */
|
|
48
|
+
readonly hookEvent: string;
|
|
49
|
+
/** hook 数组元素的 matcher 字段(工具名匹配),例如 'Bash' / 'Task' / 'terminal' */
|
|
50
|
+
readonly toolMatcher: string;
|
|
51
|
+
/**
|
|
52
|
+
* The tool name used by this IDE to invoke a sub-agent (e.g. Claude Code
|
|
53
|
+
* uses 'Task' to dispatch a sub-agent, Trae may use a different name).
|
|
54
|
+
* Consumed by the `peaks progress start` hook entry so each IDE self-
|
|
55
|
+
* reports its sub-agent tool name. Additive on `toolMatcher`: the
|
|
56
|
+
* `toolMatcher` field still drives the gate-enforce hook entry, this
|
|
57
|
+
* one drives the sub-agent-progress hook entry.
|
|
58
|
+
*
|
|
59
|
+
* Added in slice 2026-06-06-sub-agent-spawn-bug-and-decouple.
|
|
60
|
+
*/
|
|
61
|
+
readonly subAgentToolMatcher: string;
|
|
62
|
+
/**
|
|
63
|
+
* Per-IDE sub-agent dispatcher. The `peaks sub-agent dispatch` CLI reads
|
|
64
|
+
* this field, calls `supportsRole` + `buildToolCall`, and returns the
|
|
65
|
+
* resulting tool-call descriptor in the JSON envelope. Additive on
|
|
66
|
+
* `subAgentToolMatcher`: the matcher still drives the gate-enforce hook
|
|
67
|
+
* entry; this field drives the runtime sub-agent dispatch surface.
|
|
68
|
+
*
|
|
69
|
+
* Added in slice 2026-06-07-sub-agent-dispatch-decouple. See PRD #002
|
|
70
|
+
* G1 (AC-1, AC-2) + [[slim-ideadapter-shape-is-the-contract]].
|
|
71
|
+
*/
|
|
72
|
+
readonly subAgentDispatcher: SubAgentDispatcher;
|
|
73
|
+
/**
|
|
74
|
+
* Per-IDE opt-in to the G9 prompt-size gate. When `true`, the
|
|
75
|
+
* `peaks hooks install` command registers the G9 PreToolUse hook
|
|
76
|
+
* (`peaks sub-agent-dispatch-guard`) for this IDE. When `false`,
|
|
77
|
+
* the hook is NOT installed (the IDE either doesn't support the
|
|
78
|
+
* PreToolUse event in a useful form, or the user has opted out).
|
|
79
|
+
*
|
|
80
|
+
* The CLI 兜底 layer in `peaks sub-agent dispatch` still enforces
|
|
81
|
+
* the threshold regardless of this field — `promptSizeAware` only
|
|
82
|
+
* controls the hook layer (R-15: G9 hook is LLM-platform-specific).
|
|
83
|
+
*
|
|
84
|
+
* Added in slice 2026-06-07-sub-agent-context-governance. See PRD
|
|
85
|
+
* #003 G9.2 + AC-56. Default `false` to preserve slice #009's
|
|
86
|
+
* `peaks hooks install` output byte-stability.
|
|
87
|
+
*/
|
|
88
|
+
readonly promptSizeAware: boolean;
|
|
89
|
+
/** install / uninstall 后展示给用户的提示文本(各 IDE 不同,例如 Claude 提示重启窗口) */
|
|
90
|
+
readonly installHints: readonly string[];
|
|
91
|
+
/** 该 IDE 在 peaks 上可启用的能力(用于在不支持的 IDE 上软警告) */
|
|
92
|
+
readonly capabilities: IdeCapabilities;
|
|
93
|
+
/**
|
|
94
|
+
* Where this IDE reads its project-level agent instructions from.
|
|
95
|
+
* When undefined, the postinstall + `peaks standards init` codepath falls
|
|
96
|
+
* back to the legacy Claude Code path (CLAUDE.md + .claude/rules/**)
|
|
97
|
+
* AND emits a stderr warning. Adapters in slice 1.3.2 declare this
|
|
98
|
+
* value (Claude Code), are annotated UNVERIFIED for future slices
|
|
99
|
+
* (Trae, slice #012+), or omit it entirely (not-yet-registered IDEs).
|
|
100
|
+
*
|
|
101
|
+
* Added in slice 011-2026-06-07-ide-adapter-resource-profile.
|
|
102
|
+
*/
|
|
103
|
+
readonly standardsProfile?: IdeStandardsProfile;
|
|
104
|
+
/**
|
|
105
|
+
* Where `scripts/install-skills.mjs` symlinks the bundled skills +
|
|
106
|
+
* output styles. When undefined, the postinstall falls back to
|
|
107
|
+
* `~/.claude/skills` + `~/.claude/output-styles` (legacy) AND emits
|
|
108
|
+
* a stderr warning. Adapters that opt into the dispatch layer fill
|
|
109
|
+
* this; adapters that don't (Trae in slice 1.3.2) leave it undefined
|
|
110
|
+
* and follow the legacy path with a warning.
|
|
111
|
+
*
|
|
112
|
+
* Added in slice 011-2026-06-07-ide-adapter-resource-profile.
|
|
113
|
+
*/
|
|
114
|
+
readonly skillInstall?: IdeSkillInstall;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Per-IDE standards-file location + format profile. Used by the
|
|
118
|
+
* `peaks standards init` dispatch layer (slice 011) to write the
|
|
119
|
+
* project-level standards files at the IDE-specific path, not the
|
|
120
|
+
* Claude Code hardcoded one. Adapters that omit this field trigger
|
|
121
|
+
* the legacy Claude Code path with a stderr warning.
|
|
122
|
+
*/
|
|
123
|
+
export interface IdeStandardsProfile {
|
|
124
|
+
/** Filename for the project-root constitution (e.g. 'CLAUDE.md'), or null if the IDE has no equivalent. */
|
|
125
|
+
readonly rootFile: string | null;
|
|
126
|
+
/** Directory for module-level rules (e.g. '.claude/rules'), or null if the IDE has no equivalent. */
|
|
127
|
+
readonly rulesDir: string | null;
|
|
128
|
+
/** Glob under rulesDir to enumerate rule files. */
|
|
129
|
+
readonly rulesFileGlob: string;
|
|
130
|
+
/** True if the IDE auto-loads these files at session start. */
|
|
131
|
+
readonly autoLoaded: boolean;
|
|
132
|
+
/** Output format. markdown = plain text; markdown+frontmatter = adds YAML frontmatter to each rule file. */
|
|
133
|
+
readonly format: 'markdown' | 'markdown+frontmatter';
|
|
134
|
+
/** Human-readable hint surfaced in the fallback warning. */
|
|
135
|
+
readonly migrationHint?: string;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Per-IDE postinstall target roots. The `scripts/install-skills.mjs`
|
|
139
|
+
* script consumes this to symlink the bundled skills + output styles
|
|
140
|
+
* to the IDE-specific install location, with back-compat for the
|
|
141
|
+
* legacy `PEAKS_CLAUDE_SKILLS_DIR` / `PEAKS_CLAUDE_OUTPUT_STYLES_DIR`
|
|
142
|
+
* env vars (precedence: explicit option > env var > IDE profile > legacy default).
|
|
143
|
+
*/
|
|
144
|
+
export interface IdeSkillInstall {
|
|
145
|
+
/** Absolute path under which the postinstall script symlinks the bundled `skills/` directory. */
|
|
146
|
+
readonly skillsDir: string;
|
|
147
|
+
/** Absolute path under which the postinstall script writes the bundled `output-styles/`. Null if the IDE has no equivalent. */
|
|
148
|
+
readonly outputStylesDir: string | null;
|
|
149
|
+
/** Symlink strategy. */
|
|
150
|
+
readonly installStrategy: 'symlink' | 'copy';
|
|
151
|
+
/** Back-compat env var name (e.g. PEAKS_CLAUDE_SKILLS_DIR). Null if no env var is supported. */
|
|
152
|
+
readonly envVarOverride: string | null;
|
|
153
|
+
}
|
|
154
|
+
/** peaks canonical hook schema 版本标识 */
|
|
155
|
+
export declare const PEAKS_HOOK_SCHEMA: "peaks-hook/v1";
|
|
156
|
+
/** peaks canonical hook 形态 —— 单一协议,所有 IDE 经 hook-translator 归一化到此 */
|
|
157
|
+
export interface PeaksCanonicalHook {
|
|
158
|
+
readonly schema: typeof PEAKS_HOOK_SCHEMA;
|
|
159
|
+
readonly event: 'pre-tool-use' | 'post-tool-use' | 'sub-agent-start';
|
|
160
|
+
readonly toolName: string;
|
|
161
|
+
readonly toolInput: Record<string, unknown>;
|
|
162
|
+
/** 解析自 env 变量或 --project 的项目根 */
|
|
163
|
+
readonly projectRoot: string;
|
|
164
|
+
/** 选输出格式 */
|
|
165
|
+
readonly rawIdeFormat: IdeId;
|
|
166
|
+
/** 原始 stdin,留作回退 */
|
|
167
|
+
readonly rawPayload: unknown;
|
|
168
|
+
}
|
|
169
|
+
/** peaks 决策发回形态枚举(按 IDE 期望的"发回"形式) */
|
|
170
|
+
export type PeaksDecisionTransport = {
|
|
171
|
+
kind: 'stdout-json';
|
|
172
|
+
denyShape: Record<string, unknown>;
|
|
173
|
+
} | {
|
|
174
|
+
kind: 'exit-code';
|
|
175
|
+
denyCode: number;
|
|
176
|
+
} | {
|
|
177
|
+
kind: 'both';
|
|
178
|
+
denyShape: Record<string, unknown>;
|
|
179
|
+
denyCode: number;
|
|
180
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource profile accessors for the per-IDE dispatch layer.
|
|
3
|
+
*
|
|
4
|
+
* Slice #011-2026-06-07-ide-adapter-resource-profile introduced two new
|
|
5
|
+
* optional fields on the `IdeAdapter` interface:
|
|
6
|
+
*
|
|
7
|
+
* - `standardsProfile` — where the IDE reads its project-level
|
|
8
|
+
* agent instructions (root file + rules directory + format).
|
|
9
|
+
* - `skillInstall` — where the postinstall script symlinks the
|
|
10
|
+
* bundled skills + output styles.
|
|
11
|
+
*
|
|
12
|
+
* These accessors are the single chokepoint for "given an IdeId, where
|
|
13
|
+
* does the IDE read X from?". The two consumers that consume them:
|
|
14
|
+
*
|
|
15
|
+
* 1. `src/services/standards/ide-aware-standards-service.ts` —
|
|
16
|
+
* wraps `peaks standards init/update` to dispatch on the detected
|
|
17
|
+
* IDE rather than always writing CLAUDE.md + .claude/rules/**.
|
|
18
|
+
* 2. `scripts/install-skills.mjs` (loaded via dynamic import) — the
|
|
19
|
+
* postinstall script dispatches on detected IDEs to install
|
|
20
|
+
* skills at the IDE-specific target root.
|
|
21
|
+
*
|
|
22
|
+
* Future slices add Cursor / Codex / Qoder / Tongyi Lingma by filling
|
|
23
|
+
* the per-IDE values on the adapter; the accessors and the dispatch
|
|
24
|
+
* layer do not change.
|
|
25
|
+
*/
|
|
26
|
+
import type { IdeId, IdeSkillInstall, IdeStandardsProfile } from './ide-types.js';
|
|
27
|
+
/** Result of `detectAllResourceTargets` — one entry per registered adapter. */
|
|
28
|
+
export interface ResourceTarget {
|
|
29
|
+
readonly ideId: IdeId;
|
|
30
|
+
readonly standardsProfile: IdeStandardsProfile | null;
|
|
31
|
+
readonly skillInstall: IdeSkillInstall | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Look up the standards-file profile for a given IDE. Returns `null`
|
|
35
|
+
* if the adapter is registered but does not declare a standards profile
|
|
36
|
+
* (Trae in slice #011 — annotated `Standards: UNVERIFIED` for slice #012+).
|
|
37
|
+
* Throws if the IDE id is not registered at all.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getStandardsProfile(ideId: IdeId): IdeStandardsProfile | null;
|
|
40
|
+
/**
|
|
41
|
+
* Look up the skill-install profile for a given IDE. Returns `null`
|
|
42
|
+
* if the adapter does not declare one (Trae in slice #011). Throws
|
|
43
|
+
* if the IDE id is not registered.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getSkillInstall(ideId: IdeId): IdeSkillInstall | null;
|
|
46
|
+
/**
|
|
47
|
+
* Enumerate all registered adapters and return their resource profiles.
|
|
48
|
+
* Used by `install-skills.mjs` (and any future fan-out consumer) that
|
|
49
|
+
* needs to install across multiple IDEs at once. Returns the profiles
|
|
50
|
+
* in adapter insertion order.
|
|
51
|
+
*/
|
|
52
|
+
export declare function detectAllResourceTargets(): readonly ResourceTarget[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getAdapter, listAdapterIds } from './ide-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Look up the standards-file profile for a given IDE. Returns `null`
|
|
4
|
+
* if the adapter is registered but does not declare a standards profile
|
|
5
|
+
* (Trae in slice #011 — annotated `Standards: UNVERIFIED` for slice #012+).
|
|
6
|
+
* Throws if the IDE id is not registered at all.
|
|
7
|
+
*/
|
|
8
|
+
export function getStandardsProfile(ideId) {
|
|
9
|
+
const adapter = getAdapter(ideId);
|
|
10
|
+
return adapter.standardsProfile ?? null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Look up the skill-install profile for a given IDE. Returns `null`
|
|
14
|
+
* if the adapter does not declare one (Trae in slice #011). Throws
|
|
15
|
+
* if the IDE id is not registered.
|
|
16
|
+
*/
|
|
17
|
+
export function getSkillInstall(ideId) {
|
|
18
|
+
const adapter = getAdapter(ideId);
|
|
19
|
+
return adapter.skillInstall ?? null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Enumerate all registered adapters and return their resource profiles.
|
|
23
|
+
* Used by `install-skills.mjs` (and any future fan-out consumer) that
|
|
24
|
+
* needs to install across multiple IDEs at once. Returns the profiles
|
|
25
|
+
* in adapter insertion order.
|
|
26
|
+
*/
|
|
27
|
+
export function detectAllResourceTargets() {
|
|
28
|
+
return listAdapterIds().map((ideId) => ({
|
|
29
|
+
ideId,
|
|
30
|
+
standardsProfile: getStandardsProfile(ideId),
|
|
31
|
+
skillInstall: getSkillInstall(ideId),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const ATOMIC_JSON_FILE_MODE = 384;
|
|
2
|
+
/**
|
|
3
|
+
* Read a JSON object file using a no-follow open. Returns an empty object when
|
|
4
|
+
* the file does not exist or is empty. Throws when the file exists but does not
|
|
5
|
+
* contain a JSON object (so callers can distinguish "no settings" from
|
|
6
|
+
* "malformed settings").
|
|
7
|
+
*/
|
|
8
|
+
export declare function readJsonObjectFile<T extends Record<string, unknown> = Record<string, unknown>>(filePath: string): T;
|
|
9
|
+
/**
|
|
10
|
+
* Atomically write a JSON file: create a unique temp file in the same
|
|
11
|
+
* directory, fsync its contents via close, then `rename` over the target. A
|
|
12
|
+
* failure during rename removes the temp file (best effort). The target is
|
|
13
|
+
* created with 0o600 permissions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function atomicWriteJson(filePath: string, value: unknown): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export const ATOMIC_JSON_FILE_MODE = 0o600;
|
|
5
|
+
/**
|
|
6
|
+
* Read a JSON object file using a no-follow open. Returns an empty object when
|
|
7
|
+
* the file does not exist or is empty. Throws when the file exists but does not
|
|
8
|
+
* contain a JSON object (so callers can distinguish "no settings" from
|
|
9
|
+
* "malformed settings").
|
|
10
|
+
*/
|
|
11
|
+
export function readJsonObjectFile(filePath) {
|
|
12
|
+
if (!existsSync(filePath))
|
|
13
|
+
return {};
|
|
14
|
+
const fd = openSync(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
15
|
+
try {
|
|
16
|
+
const raw = readFileSync(fd, 'utf8').trim();
|
|
17
|
+
if (raw.length === 0)
|
|
18
|
+
return {};
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
21
|
+
throw new Error('settings file must contain a JSON object');
|
|
22
|
+
}
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Atomically write a JSON file: create a unique temp file in the same
|
|
31
|
+
* directory, fsync its contents via close, then `rename` over the target. A
|
|
32
|
+
* failure during rename removes the temp file (best effort). The target is
|
|
33
|
+
* created with 0o600 permissions.
|
|
34
|
+
*/
|
|
35
|
+
export function atomicWriteJson(filePath, value) {
|
|
36
|
+
const dir = dirname(filePath);
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
const tempPath = join(dir, `.settings.${randomUUID()}.tmp`);
|
|
39
|
+
const fd = openSync(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, ATOMIC_JSON_FILE_MODE);
|
|
40
|
+
try {
|
|
41
|
+
writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
renameSync(tempPath, filePath);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
try {
|
|
51
|
+
unlinkSync(tempPath);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// best effort cleanup
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type HookScope = 'project' | 'global';
|
|
2
|
+
/** True iff `childPath` resolves to `parentPath` or any path nested inside it. */
|
|
3
|
+
export declare function isInsidePath(childPath: string, parentPath: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Reject settings targets that are symlinked or escape the configured root.
|
|
6
|
+
* Used by the hook / statusline / MCP install paths to keep the project root the
|
|
7
|
+
* sole owner of writable settings files.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertSafeSettingsFile(scope: HookScope, root: string, dirName: string, settingsFileName: string): {
|
|
10
|
+
settingsPath: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, lstatSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, relative } from 'node:path';
|
|
3
|
+
/** True iff `childPath` resolves to `parentPath` or any path nested inside it. */
|
|
4
|
+
export function isInsidePath(childPath, parentPath) {
|
|
5
|
+
const rel = relative(parentPath, childPath);
|
|
6
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reject settings targets that are symlinked or escape the configured root.
|
|
10
|
+
* Used by the hook / statusline / MCP install paths to keep the project root the
|
|
11
|
+
* sole owner of writable settings files.
|
|
12
|
+
*/
|
|
13
|
+
export function assertSafeSettingsFile(scope, root, dirName, settingsFileName) {
|
|
14
|
+
const settingsPath = join(root, dirName, settingsFileName);
|
|
15
|
+
const dirPath = join(root, dirName);
|
|
16
|
+
if (existsSync(dirPath) && lstatSync(dirPath).isSymbolicLink()) {
|
|
17
|
+
throw new Error(`${dirName} directory must not be a symlink`);
|
|
18
|
+
}
|
|
19
|
+
if (existsSync(settingsPath)) {
|
|
20
|
+
if (lstatSync(settingsPath).isSymbolicLink()) {
|
|
21
|
+
throw new Error(`${settingsFileName} must not be a symlink`);
|
|
22
|
+
}
|
|
23
|
+
const realRoot = realpathSync(root);
|
|
24
|
+
if (!isInsidePath(realpathSync(settingsPath), realRoot)) {
|
|
25
|
+
throw new Error(`${settingsFileName} must stay inside the ${scope} root`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { settingsPath };
|
|
29
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { listSessionMetas } from '../session/session-manager.js';
|
|
4
|
+
import { getSessionDir } from '../session/getSessionDir.js';
|
|
4
5
|
const PROJECT_CONTEXT_FILE = '.peaks/PROJECT.md';
|
|
5
6
|
const CONTEXT_HEADER = `# Peaks Project Context
|
|
6
7
|
|
|
@@ -89,7 +90,7 @@ function buildSessionHistory(projectRoot) {
|
|
|
89
90
|
const title = (meta.title ?? 'Untitled').slice(0, 40);
|
|
90
91
|
const skill = meta.skill ?? '-';
|
|
91
92
|
// Extract one-line summary from artifacts for the "What" column
|
|
92
|
-
const sessionRoot =
|
|
93
|
+
const sessionRoot = getSessionDir(projectRoot, meta.sessionId);
|
|
93
94
|
const summary = extractOneLineSummary(sessionRoot);
|
|
94
95
|
const brief = summary ? summary.slice(0, 70) : skill;
|
|
95
96
|
body += `| ${date} | \`${dir}\` | ${title} | ${brief} |\n`;
|
|
@@ -2,6 +2,7 @@ import { closeSync, constants, copyFileSync, existsSync, lstatSync, mkdirSync, o
|
|
|
2
2
|
import { dirname, basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { isInsidePath, isWindowsAbsolutePath, normalizePath, resolveInputPath, stablePath, stableRealPath } from '../../shared/path-utils.js';
|
|
4
4
|
import { containsSensitiveConfigValue, isSensitiveConfigPath } from '../config/config-service.js';
|
|
5
|
+
import { getSessionDir } from '../session/getSessionDir.js';
|
|
5
6
|
// Hot kinds: full body kept in index for always-available context
|
|
6
7
|
const HOT_KINDS = new Set(['feedback', 'decision', 'rule', 'convention', 'module', 'lesson']);
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -237,13 +238,13 @@ function summarizeMemoryBody(body) {
|
|
|
237
238
|
function assertSafeSessionDir(projectRoot, sessionId) {
|
|
238
239
|
const normalizedRoot = normalizeRoot(projectRoot);
|
|
239
240
|
const realRoot = normalizeRealRoot(projectRoot);
|
|
240
|
-
const sessionDir =
|
|
241
|
+
const sessionDir = getSessionDir(normalizedRoot, sessionId);
|
|
241
242
|
if (!existsSync(sessionDir)) {
|
|
242
243
|
// Distinguish "not found" (caller will treat as no-op) from "escapes project
|
|
243
244
|
// root" (caller must surface a hard error). We probe by checking whether the
|
|
244
245
|
// joined path, after realpath, would still be inside the project root.
|
|
245
|
-
if (isAbsolute(
|
|
246
|
-
const realJoined = safeRealpath(
|
|
246
|
+
if (isAbsolute(getSessionDir(normalizedRoot, sessionId))) {
|
|
247
|
+
const realJoined = safeRealpath(getSessionDir(normalizedRoot, sessionId));
|
|
247
248
|
if (realJoined && !isInsidePath(realJoined, realRoot)) {
|
|
248
249
|
throw new Error('Session directory must stay inside the project root');
|
|
249
250
|
}
|
|
@@ -39,6 +39,7 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
39
39
|
import { existsSync } from 'node:fs';
|
|
40
40
|
import { join } from 'node:path';
|
|
41
41
|
import { getSessionId } from '../session/session-manager.js';
|
|
42
|
+
import { getSessionDir } from '../session/getSessionDir.js';
|
|
42
43
|
import { findProjectRoot } from '../config/config-safety.js';
|
|
43
44
|
const README_BODY = `# Performance baseline
|
|
44
45
|
|
|
@@ -117,7 +118,7 @@ function renderBaselineTemplate() {
|
|
|
117
118
|
function buildPlan(projectRoot, apply) {
|
|
118
119
|
const sessionId = getSessionId(projectRoot);
|
|
119
120
|
const sessionRoot = sessionId !== null
|
|
120
|
-
?
|
|
121
|
+
? getSessionDir(projectRoot, sessionId)
|
|
121
122
|
: null;
|
|
122
123
|
const perfBaselinePath = sessionRoot !== null
|
|
123
124
|
? join(sessionRoot, 'rd', 'perf-baseline.md')
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Sub-agent progress surfacing for the RD/QA sub-agents in
|
|
3
3
|
* `peaks-solo`'s Swarm phase. A sub-agent (or the LLM via the
|
|
4
4
|
* `peaks progress step` CLI) writes a stable JSON file at
|
|
5
|
-
* `.peaks/<sid>/
|
|
5
|
+
* `.peaks/_sub_agents/<sid>/subagent-progress.json`. The user-side
|
|
6
6
|
* `peaks progress watch` CLI polls this file in a separate
|
|
7
7
|
* terminal tab and renders elapsed / spinner / sub-step. The
|
|
8
8
|
* `peaks progress start` CLI auto-spawns the watch in a new
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Sub-agent progress surfacing for the RD/QA sub-agents in
|
|
3
3
|
* `peaks-solo`'s Swarm phase. A sub-agent (or the LLM via the
|
|
4
4
|
* `peaks progress step` CLI) writes a stable JSON file at
|
|
5
|
-
* `.peaks/<sid>/
|
|
5
|
+
* `.peaks/_sub_agents/<sid>/subagent-progress.json`. The user-side
|
|
6
6
|
* `peaks progress watch` CLI polls this file in a separate
|
|
7
7
|
* terminal tab and renders elapsed / spinner / sub-step. The
|
|
8
8
|
* `peaks progress start` CLI auto-spawns the watch in a new
|
|
@@ -34,20 +34,24 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
|
|
|
34
34
|
import { dirname, join, resolve } from 'node:path';
|
|
35
35
|
import { getSessionIdCanonical } from '../session/session-manager.js';
|
|
36
36
|
import { findProjectRoot } from '../config/config-safety.js';
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// As of slice 2026-06-06-sub-agent-spawn-bug-and-decouple, the per-session
|
|
38
|
+
// sub-agent state files live under `.peaks/_sub_agents/<sid>/`, NOT under
|
|
39
|
+
// `.peaks/<sid>/system/`. The new path mirrors the existing `_runtime/`
|
|
40
|
+
// and `_dogfood/` convention (leading underscore = meta-classification, not
|
|
41
|
+
// a per-session artifact). The previous `<sid>/system/...` locations are
|
|
42
|
+
// migrated to the new path on first run of `peaks workspace reconcile
|
|
43
|
+
// --apply` (see `migrateSubAgentState` in reconcile-service.ts).
|
|
44
|
+
const SUB_AGENTS_DIR = '_sub_agents';
|
|
45
|
+
const PROGRESS_FILE_NAME = 'subagent-progress.json';
|
|
46
|
+
const SPAWN_FILE_NAME = 'progress-spawn.json';
|
|
39
47
|
function progressPath(projectRoot) {
|
|
40
|
-
// The progress file lives
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// should too. Without the session prefix, a session rotation
|
|
45
|
-
// would orphan the file in the project root, and switching
|
|
46
|
-
// sessions would have the watch reading the wrong slice's
|
|
47
|
-
// progress.
|
|
48
|
+
// The progress file lives at `.peaks/_sub_agents/<sid>/subagent-progress.json`.
|
|
49
|
+
// The leading `_sub_agents/` is a meta-classification (mirrors `_runtime/`,
|
|
50
|
+
// `_dogfood/`) — the SID is the per-session discriminator inside that meta
|
|
51
|
+
// dir. Without the SID, sessions would collide on the same file.
|
|
48
52
|
const sessionId = getSessionIdCanonical(projectRoot);
|
|
49
53
|
const subDir = sessionId ?? 'unbound';
|
|
50
|
-
return join(projectRoot, '.peaks', subDir,
|
|
54
|
+
return join(projectRoot, '.peaks', SUB_AGENTS_DIR, subDir, PROGRESS_FILE_NAME);
|
|
51
55
|
}
|
|
52
56
|
function ensureParentDir(path) {
|
|
53
57
|
const dir = dirname(path);
|
|
@@ -199,12 +203,12 @@ export function subAgentProgressPath(projectRoot) {
|
|
|
199
203
|
export function subAgentSpawnPath(projectRoot) {
|
|
200
204
|
const sessionId = getSessionIdCanonical(projectRoot);
|
|
201
205
|
const subDir = sessionId ?? 'unbound';
|
|
202
|
-
return join(projectRoot, '.peaks', subDir,
|
|
206
|
+
return join(projectRoot, '.peaks', SUB_AGENTS_DIR, subDir, SPAWN_FILE_NAME);
|
|
203
207
|
}
|
|
204
208
|
function spawnRecordPath(projectRoot) {
|
|
205
209
|
const sessionId = getSessionIdCanonical(projectRoot);
|
|
206
210
|
const subDir = sessionId ?? 'unbound';
|
|
207
|
-
return join(projectRoot, '.peaks', subDir,
|
|
211
|
+
return join(projectRoot, '.peaks', SUB_AGENTS_DIR, subDir, SPAWN_FILE_NAME);
|
|
208
212
|
}
|
|
209
213
|
export function writeSpawnRecord(options) {
|
|
210
214
|
const sessionId = getSessionIdCanonical(options.projectRoot);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Build the canonical record path for a given session/rid/timestamp. */
|
|
2
|
+
export declare function dispatchRecordPath(projectRoot: string, sid: string, rid: string, ts?: Date): string;
|
|
3
|
+
/** The directory under which dispatch records live. */
|
|
4
|
+
export declare function dispatchRecordsDir(projectRoot: string, sid: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Assert that `recordPath` lives under `projectRoot/.peaks/_sub_agents/<sid>/`.
|
|
7
|
+
* Rejects symlink/junction escapes and `..` segments.
|
|
8
|
+
*
|
|
9
|
+
* Throws an Error with `.code = 'INVALID_RECORD_PATH'` on rejection so
|
|
10
|
+
* the CLI can map to `{ok: false, code: "INVALID_RECORD_PATH"}`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertSafeDispatchRecordPath(recordPath: string, projectRoot: string): string;
|