peaks-loop 4.0.45 → 4.0.47
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 +48 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/code-runtime-commands.d.ts +22 -0
- package/dist/cli/commands/code-runtime-commands.js +128 -34
- package/dist/cli/commands/compact-command.js +129 -1
- package/dist/cli/commands/container-commands.js +3 -3
- package/dist/cli/commands/core/skill-command.js +45 -10
- package/dist/cli/commands/cron-commands.js +2 -1
- package/dist/cli/commands/e2e-verify.js +3 -3
- package/dist/cli/commands/governance-classify-contract-commands.js +1 -0
- package/dist/cli/commands/hooks-commands.js +10 -1
- package/dist/cli/commands/loop-commands.js +1 -0
- package/dist/cli/commands/playwright-commands.js +2 -1
- package/dist/cli/commands/reinject-command.d.ts +72 -0
- package/dist/cli/commands/reinject-command.js +174 -0
- package/dist/cli/commands/request-commands.js +6 -3
- package/dist/cli/commands/shadcn-commands.js +1 -0
- package/dist/cli/commands/test-commands.js +2 -1
- package/dist/cli/commands/vm-commands.js +7 -7
- package/dist/cli/commands/workspace/init-command.js +24 -2
- package/dist/cli/commands/worktree-lease-commands.js +4 -4
- package/dist/cli/index.js +49 -2
- package/dist/cli/program.js +5 -0
- package/dist/hooks/pre-tool-use-sub-agent.js +1 -1
- package/dist/services/adapter/adapter-registry.js +1 -1
- package/dist/services/artifacts/artifact-service.js +1 -1
- package/dist/services/capability-guard-runner/contracts/J01.js +2 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +3 -3
- package/dist/services/capability-guard-runner/contracts/J04.js +4 -2
- package/dist/services/capability-guard-runner/contracts/J07.js +2 -1
- package/dist/services/code/auto-compact-lifecycle.d.ts +50 -4
- package/dist/services/code/auto-compact-lifecycle.js +52 -10
- package/dist/services/code/auto-compact-orchestrator.d.ts +53 -9
- package/dist/services/code/auto-compact-orchestrator.js +173 -43
- package/dist/services/code/orchestrator-can-do.d.ts +4 -2
- package/dist/services/code/orchestrator-can-do.js +37 -5
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +2 -1
- package/dist/services/codegraph/codegraph-process-runner.js +3 -2
- package/dist/services/compact/request-transition-hook.js +5 -2
- package/dist/services/compact-history/compact-history-service.d.ts +61 -0
- package/dist/services/compact-history/compact-history-service.js +49 -0
- package/dist/services/compact-statusline/compact-lifecycle-store.d.ts +11 -2
- package/dist/services/compact-statusline/compact-lifecycle-store.js +19 -1
- package/dist/services/compact-statusline/compact-statusline-service.d.ts +1 -1
- package/dist/services/compact-statusline/compact-statusline-service.js +22 -0
- package/dist/services/config/config-safety.d.ts +52 -0
- package/dist/services/config/config-safety.js +75 -1
- package/dist/services/context/auto-compact-dispatcher.d.ts +7 -37
- package/dist/services/context/auto-compact-dispatcher.js +113 -40
- package/dist/services/context/auto-compact-reader.d.ts +68 -28
- package/dist/services/context/auto-compact-reader.js +155 -1
- package/dist/services/context/auto-compact-types.d.ts +89 -12
- package/dist/services/context/auto-compact-types.js +16 -32
- package/dist/services/context/harness-window-config.d.ts +412 -0
- package/dist/services/context/harness-window-config.js +607 -0
- package/dist/services/context/main-session-monitor.d.ts +27 -0
- package/dist/services/context/main-session-monitor.js +32 -1
- package/dist/services/context/post-compact-reinjection.d.ts +221 -0
- package/dist/services/context/post-compact-reinjection.js +491 -0
- package/dist/services/dispatch/merge-back-runner.js +5 -5
- package/dist/services/dispatch/service-shutdown.js +3 -3
- package/dist/services/doc/doc-generator.js +2 -1
- package/dist/services/env/shell-probe.js +1 -1
- package/dist/services/fuzzy-matching/fzf-pick-service.js +2 -0
- package/dist/services/hooks/auto-compact-hook-install.d.ts +10 -2
- package/dist/services/hooks/auto-compact-hook-install.js +8 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +107 -3
- package/dist/services/ide/adapters/claude-code-adapter.js +154 -7
- package/dist/services/ide/ide-registry.d.ts +12 -0
- package/dist/services/ide/ide-registry.js +14 -0
- package/dist/services/ide/ide-types.d.ts +59 -0
- package/dist/services/lint/detect-eslint.js +2 -2
- package/dist/services/lint/eslint-runner.js +3 -1
- package/dist/services/loop/evaluator-dispatcher.js +2 -1
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +1 -1
- package/dist/services/memory/project-memory-service/store/paths.d.ts +9 -1
- package/dist/services/memory/project-memory-service/store/paths.js +15 -6
- package/dist/services/prd/best-practice-auto-trigger.js +1 -0
- package/dist/services/release/version-precheck-service.d.ts +2 -1
- package/dist/services/release/version-precheck-service.js +82 -12
- package/dist/services/runtime/vendor-adapter.d.ts +29 -4
- package/dist/services/runtime/vendors/claude-code.js +1 -1
- package/dist/services/runtime/vendors/codex.js +1 -1
- package/dist/services/runtime/vendors/copilot.js +1 -1
- package/dist/services/sc/sc-service.js +1 -1
- package/dist/services/scan/diff-scope-service.js +2 -2
- package/dist/services/scan/file-size-scan.js +2 -2
- package/dist/services/scan/orphan-service.js +2 -1
- package/dist/services/scan/type-sanity-service.js +2 -2
- package/dist/services/skillhub/tar-runtime.js +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +8 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +40 -3
- package/dist/services/skills/hooks-settings-service.d.ts +12 -0
- package/dist/services/skills/hooks-settings-service.js +77 -10
- package/dist/services/skills/session-start-hook-constants.d.ts +41 -0
- package/dist/services/skills/session-start-hook-constants.js +41 -0
- package/dist/services/skills/skill-presence-service.js +9 -0
- package/dist/services/skills/skill-statusline-renderer.js +30 -5
- package/dist/services/skills/statusline-palette.d.ts +6 -0
- package/dist/services/skills/statusline-palette.js +4 -1
- package/dist/services/slice/slice-check-service.js +2 -1
- package/dist/services/slice/slice-decompose-runners.js +2 -1
- package/dist/services/upgrade/upgrade-service.js +1 -0
- package/dist/services/workflow/workflow-skip-service.js +2 -1
- package/dist/services/workspace/migrate-service.js +1 -1
- package/dist/services/workspace/workspace-claude-settings-materializer.js +51 -7
- package/dist/services/workspace/workspace-service.js +8 -0
- package/dist/services/worktree/host-worktree-reconciler.js +1 -0
- package/dist/services/worktree/long-path-cleanup.js +3 -2
- package/dist/shared/process.js +1 -1
- package/package.json +5 -5
- package/scripts/install-skills.mjs +1 -0
- package/scripts/watch.mjs +3 -1
- package/skills/bee/peaks-perf-audit/SKILL.md +1 -1
- package/skills/bee/peaks-prd/SKILL.md +1 -1
- package/skills/bee/peaks-qa/SKILL.md +2 -2
- package/skills/bee/peaks-rd/SKILL.md +2 -2
- package/skills/bee/peaks-reviewer/SKILL.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-security-audit/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-audit/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +4 -4
- package/skills/peaks-code/references/sub-agent-dispatch.md +1 -1
- package/skills/peaks-content/SKILL.md +1 -1
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-final-review/SKILL.md +1 -1
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +1 -1
- package/skills/peaks-resume/SKILL.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +1 -1
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-sop/SKILL.md +1 -1
- package/skills/peaks-status/SKILL.md +1 -1
- package/skills/peaks-test/SKILL.md +1 -1
|
@@ -37,12 +37,45 @@ function upgrade(result, strict) {
|
|
|
37
37
|
}
|
|
38
38
|
return result;
|
|
39
39
|
}
|
|
40
|
+
/** Read + parse a JSON manifest, mapping both failure modes onto a blocker description. */
|
|
41
|
+
function readPackageJson(path, label) {
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = readFileSync(path, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
message: `${label} is missing or unreadable at ${path}`,
|
|
50
|
+
remediation: `run \`peaks release precheck\` from a project root that has a readable ${label}`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: `${label} at ${path} is not valid JSON`,
|
|
60
|
+
remediation: `fix the ${label} syntax; \`peaks release precheck\` cannot gate a version it cannot parse`
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
40
64
|
function readRootVersion(projectRoot) {
|
|
41
|
-
const
|
|
65
|
+
const path = join(projectRoot, 'package.json');
|
|
66
|
+
const read = readPackageJson(path, 'root package.json');
|
|
67
|
+
if (!read.ok) {
|
|
68
|
+
return read;
|
|
69
|
+
}
|
|
70
|
+
const pkg = read.value;
|
|
42
71
|
if (typeof pkg.version !== 'string' || !SEMVER_RE.test(pkg.version)) {
|
|
43
|
-
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
message: `invalid root package.json#version: ${JSON.stringify(pkg.version)}`,
|
|
75
|
+
remediation: 'set root package.json#version to a clean semver (e.g. 4.0.0) before running `peaks release precheck`'
|
|
76
|
+
};
|
|
44
77
|
}
|
|
45
|
-
return pkg.version;
|
|
78
|
+
return { ok: true, version: pkg.version };
|
|
46
79
|
}
|
|
47
80
|
function rollup(layers, strict) {
|
|
48
81
|
const statuses = Object.values(layers).map((l) => l.status);
|
|
@@ -58,7 +91,16 @@ function rollup(layers, strict) {
|
|
|
58
91
|
// Layer A — rootVsShared (mirrors publish.yml §(A) on-disk gate only)
|
|
59
92
|
// ---------------------------------------------------------------------------
|
|
60
93
|
export function runRootVsShared(opts) {
|
|
61
|
-
const
|
|
94
|
+
const rootRead = readRootVersion(opts.projectRoot);
|
|
95
|
+
if (!rootRead.ok) {
|
|
96
|
+
return {
|
|
97
|
+
status: 'blocker',
|
|
98
|
+
message: rootRead.message,
|
|
99
|
+
remediation: rootRead.remediation,
|
|
100
|
+
observed: { rootVersion: null, sharedVersion: null }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const rootVersion = rootRead.version;
|
|
62
104
|
const sharedDist = join(opts.projectRoot, 'packages', 'peaks-loop-shared', 'dist', 'version.js');
|
|
63
105
|
let sharedVersion = null;
|
|
64
106
|
let distExists = false;
|
|
@@ -112,7 +154,18 @@ export function runRootVsShared(opts) {
|
|
|
112
154
|
// Layer B — tagCollision (CLI-only; not in publish.yml today)
|
|
113
155
|
// ---------------------------------------------------------------------------
|
|
114
156
|
export function runTagCollision(opts) {
|
|
115
|
-
const
|
|
157
|
+
const rootRead = readRootVersion(opts.projectRoot);
|
|
158
|
+
if (!rootRead.ok) {
|
|
159
|
+
// Fail closed: without a version there is no tag name to check, so this
|
|
160
|
+
// layer cannot claim "safe to publish".
|
|
161
|
+
return {
|
|
162
|
+
status: 'blocker',
|
|
163
|
+
message: rootRead.message,
|
|
164
|
+
remediation: rootRead.remediation,
|
|
165
|
+
observed: { tagName: null }
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const rootVersion = rootRead.version;
|
|
116
169
|
const tagName = `v${rootVersion}`;
|
|
117
170
|
// 2026-09-10: no shell. `git` is `git.exe` on Windows, so the wrapper bought
|
|
118
171
|
// nothing — and it actively broke this layer, because `projectRoot` is an
|
|
@@ -125,7 +178,8 @@ export function runTagCollision(opts) {
|
|
|
125
178
|
// blocker). It also emitted DEP0190 on every run, on every layer.
|
|
126
179
|
const res = spawnSync('git', ['-C', opts.projectRoot, 'tag', '--list', tagName], {
|
|
127
180
|
encoding: 'utf8',
|
|
128
|
-
timeout: 5_000
|
|
181
|
+
timeout: 5_000,
|
|
182
|
+
windowsHide: true
|
|
129
183
|
});
|
|
130
184
|
if (res.error !== null && res.error !== undefined) {
|
|
131
185
|
return {
|
|
@@ -198,8 +252,16 @@ export function runChangesetStaged(opts) {
|
|
|
198
252
|
// Layer D — workspaceLockstep (CLI-only; not in publish.yml today)
|
|
199
253
|
// ---------------------------------------------------------------------------
|
|
200
254
|
export function runWorkspaceLockstep(opts) {
|
|
201
|
-
const
|
|
202
|
-
|
|
255
|
+
const rootRead = readPackageJson(join(opts.projectRoot, 'package.json'), 'root package.json');
|
|
256
|
+
if (!rootRead.ok) {
|
|
257
|
+
return {
|
|
258
|
+
status: 'blocker',
|
|
259
|
+
message: rootRead.message,
|
|
260
|
+
remediation: rootRead.remediation,
|
|
261
|
+
observed: { sharedDep: null }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const rootPkg = rootRead.value;
|
|
203
265
|
const allDeps = {
|
|
204
266
|
...(rootPkg.dependencies ?? {}),
|
|
205
267
|
...(rootPkg.devDependencies ?? {})
|
|
@@ -222,9 +284,16 @@ export function runWorkspaceLockstep(opts) {
|
|
|
222
284
|
observed: { sharedDep }
|
|
223
285
|
};
|
|
224
286
|
}
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
287
|
+
const sharedRead = readPackageJson(join(opts.projectRoot, 'packages', 'peaks-loop-shared', 'package.json'), 'packages/peaks-loop-shared/package.json');
|
|
288
|
+
if (!sharedRead.ok) {
|
|
289
|
+
return {
|
|
290
|
+
status: 'blocker',
|
|
291
|
+
message: sharedRead.message,
|
|
292
|
+
remediation: sharedRead.remediation,
|
|
293
|
+
observed: { sharedDep, sharedVersion: null }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const sharedVersion = sharedRead.value.version ?? '';
|
|
228
297
|
if (!SEMVER_RE.test(sharedVersion)) {
|
|
229
298
|
return {
|
|
230
299
|
status: 'blocker',
|
|
@@ -244,7 +313,8 @@ export function runWorkspaceLockstep(opts) {
|
|
|
244
313
|
// runAllLayers — orchestrator
|
|
245
314
|
// ---------------------------------------------------------------------------
|
|
246
315
|
export function runAllLayers(opts) {
|
|
247
|
-
const
|
|
316
|
+
const rootRead = readRootVersion(opts.projectRoot);
|
|
317
|
+
const rootVersion = rootRead.ok ? rootRead.version : null;
|
|
248
318
|
const strict = opts.strict === true;
|
|
249
319
|
const layers = {
|
|
250
320
|
rootVsShared: upgrade(runRootVsShared(opts), strict),
|
|
@@ -20,11 +20,36 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Vendor-specific verb strings (`claude --compact`, `codex --compact`,
|
|
22
22
|
* `copilot compact`) MUST live ONLY in adapter implementations under
|
|
23
|
-
* `src/services/runtime/vendors/<vendor>.ts
|
|
23
|
+
* `src/services/runtime/vendors/<vendor>.ts` and
|
|
24
|
+
* `src/services/ide/adapters/<vendor>-adapter.ts`. The runtime service that
|
|
24
25
|
* orchestrates these adapters (runtime-service.ts) and the CLI commands
|
|
25
|
-
* (`peaks runtime compact --via <id>`) MUST NOT contain vendor verbs
|
|
26
|
-
*
|
|
27
|
-
* compact
|
|
26
|
+
* (`peaks runtime compact --via <id>`) MUST NOT contain vendor verbs.
|
|
27
|
+
*
|
|
28
|
+
* Slice 2026-09-12-auto-compact-vendor-neutrality widened AC-1's scope from
|
|
29
|
+
* `src/services/code/` to ALL of `src/`, and had to change how it is
|
|
30
|
+
* enforced. Both changes came from measurement:
|
|
31
|
+
*
|
|
32
|
+
* - The old scope excluded the files that carried the defect AC-1
|
|
33
|
+
* describes — the compact dispatcher (`src/services/context/`) and the
|
|
34
|
+
* hook installer (`src/services/hooks/`) — so it returned 0 by
|
|
35
|
+
* construction. A grep over a directory that does not contain the code
|
|
36
|
+
* being checked is not evidence of anything.
|
|
37
|
+
* - Widened as a raw grep over `src/`, the check returns 13, not 0. Twelve
|
|
38
|
+
* hits are the phrase quoted in COMMENTS and doc strings (this block
|
|
39
|
+
* among them); the thirteenth is `compactCommand: 'claude --compact'`
|
|
40
|
+
* in `src/services/ide/adapters/claude-code-adapter.ts` — the one place
|
|
41
|
+
* the string is supposed to be. A text grep measures prose, not code.
|
|
42
|
+
*
|
|
43
|
+
* AC-1 is therefore enforced by the AST-based check in
|
|
44
|
+
* `tests/unit/runtime/vendor-neutral-identity-guard.test.ts`: no vendor
|
|
45
|
+
* verb in any STRING LITERAL of `src/` outside the adapter implementations.
|
|
46
|
+
* Parsed, the count is 1 and the single hit is that adapter — which is the
|
|
47
|
+
* property AC-1 was always about.
|
|
48
|
+
*
|
|
49
|
+
* AC-1 covers VERB STRINGS only. The compact path's defects were IDENTITY
|
|
50
|
+
* COMPARISONS (`ideId !== 'claude-code'`), a second shape the same guard
|
|
51
|
+
* file covers and AC-1 never could. Read that guard's header for what
|
|
52
|
+
* neither check catches before treating vendor neutrality as proven.
|
|
28
53
|
*
|
|
29
54
|
* See `.peaks/_runtime/2026-07-08-session-17918f/prd/002-adapter-runtime-and-polyrepo.md`
|
|
30
55
|
* for the source PRD.
|
|
@@ -23,7 +23,7 @@ export class ClaudeCodeAdapter {
|
|
|
23
23
|
if (args.force === true)
|
|
24
24
|
argv.push('--force');
|
|
25
25
|
return new Promise((resolveRun) => {
|
|
26
|
-
const proc = spawn('claude', argv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
26
|
+
const proc = spawn('claude', argv, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
27
27
|
let stdout = '';
|
|
28
28
|
let stderr = '';
|
|
29
29
|
proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
@@ -27,7 +27,7 @@ export class CodexAdapter {
|
|
|
27
27
|
if (args.force === true)
|
|
28
28
|
argv.push('--force');
|
|
29
29
|
return new Promise((resolveRun) => {
|
|
30
|
-
const proc = spawn('codex', argv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
30
|
+
const proc = spawn('codex', argv, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
31
31
|
let stdout = '';
|
|
32
32
|
let stderr = '';
|
|
33
33
|
proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
@@ -24,7 +24,7 @@ export class CopilotAdapter {
|
|
|
24
24
|
if (args.force === true)
|
|
25
25
|
argv.push('--force');
|
|
26
26
|
return new Promise((resolveRun) => {
|
|
27
|
-
const proc = spawn('copilot', argv, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
27
|
+
const proc = spawn('copilot', argv, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
28
28
|
let stdout = '';
|
|
29
29
|
let stderr = '';
|
|
30
30
|
proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
@@ -60,7 +60,7 @@ function getCurrentCommitHash(workspaceRoot) {
|
|
|
60
60
|
if (!workspaceRoot)
|
|
61
61
|
return null;
|
|
62
62
|
try {
|
|
63
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspaceRoot, encoding: 'utf-8' }).trim();
|
|
63
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspaceRoot, encoding: 'utf-8', windowsHide: true }).trim();
|
|
64
64
|
}
|
|
65
65
|
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
66
66
|
return null;
|
|
@@ -119,9 +119,9 @@ function parseRedLineScope(rdBody) {
|
|
|
119
119
|
}
|
|
120
120
|
function tryGitChangedFiles(projectRoot, baseRef) {
|
|
121
121
|
try {
|
|
122
|
-
const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', baseRef], { encoding: 'utf8' });
|
|
122
|
+
const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', baseRef], { encoding: 'utf8', windowsHide: true });
|
|
123
123
|
const tracked = trackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
124
|
-
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' });
|
|
124
|
+
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8', windowsHide: true });
|
|
125
125
|
const untracked = untrackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
126
126
|
return { ok: true, files: Array.from(new Set([...tracked, ...untracked])) };
|
|
127
127
|
}
|
|
@@ -65,9 +65,9 @@ function getChangedFiles(projectRoot, baseRef) {
|
|
|
65
65
|
// (--diff-filter=D) are intentionally excluded: they have no on-disk
|
|
66
66
|
// body to count, and a refactor that deletes large files is exactly
|
|
67
67
|
// when the gate should NOT block. Slice #015 fix.
|
|
68
|
-
const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', '--diff-filter=AM', baseRef], { encoding: 'utf8' });
|
|
68
|
+
const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', '--diff-filter=AM', baseRef], { encoding: 'utf8', windowsHide: true });
|
|
69
69
|
const tracked = trackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
70
|
-
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' });
|
|
70
|
+
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8', windowsHide: true });
|
|
71
71
|
const untracked = untrackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
72
72
|
return Array.from(new Set([...tracked, ...untracked]));
|
|
73
73
|
}
|
|
@@ -233,7 +233,8 @@ function diffVsHead(projectRoot, baseRef) {
|
|
|
233
233
|
const res = spawnSync('git', args, {
|
|
234
234
|
cwd: projectRoot,
|
|
235
235
|
encoding: 'utf8',
|
|
236
|
-
timeout: 5000
|
|
236
|
+
timeout: 5000,
|
|
237
|
+
windowsHide: true
|
|
237
238
|
});
|
|
238
239
|
if (res.status !== 0 || !res.stdout) {
|
|
239
240
|
return { added: [], removed: [], modified: [] };
|
|
@@ -39,9 +39,9 @@ function isArtifactWorkspaceFile(filePath) {
|
|
|
39
39
|
function tryGitDiffFiles(projectRoot, baseRef) {
|
|
40
40
|
try {
|
|
41
41
|
// Combine: tracked changes vs baseRef + untracked files. Use porcelain status for untracked too.
|
|
42
|
-
const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', baseRef], { encoding: 'utf8' });
|
|
42
|
+
const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', baseRef], { encoding: 'utf8', windowsHide: true });
|
|
43
43
|
const tracked = trackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
44
|
-
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' });
|
|
44
|
+
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8', windowsHide: true });
|
|
45
45
|
const untracked = untrackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
46
46
|
const merged = Array.from(new Set([...tracked, ...untracked])).filter((file) => !isArtifactWorkspaceFile(file));
|
|
47
47
|
return { ok: true, files: merged };
|
|
@@ -62,6 +62,7 @@ export function runTar(args) {
|
|
|
62
62
|
try {
|
|
63
63
|
execFileSync(cmd, args, {
|
|
64
64
|
stdio: ["ignore", "ignore", "pipe"],
|
|
65
|
+
windowsHide: true,
|
|
65
66
|
// Surface stderr text rather than dumping a Buffer at the caller.
|
|
66
67
|
// execFileSync with stdio: ["ignore","ignore","pipe"] captures the
|
|
67
68
|
// stderr stream into the Error's `.stderr` Buffer when the child
|
|
@@ -41,6 +41,14 @@ interface ResolvedHookSpec {
|
|
|
41
41
|
* everywhere else.
|
|
42
42
|
*/
|
|
43
43
|
export declare function resolveHookShell(platform?: NodeJS.Platform): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* True when the IDE is tabled in `HOOK_COMMAND_BY_IDE`, i.e. peaks can write a
|
|
46
|
+
* hook entry for it. `qoder` / `tongyi-lingma` / `zcode` are reserved adapter
|
|
47
|
+
* ids the registry knows but this table does not, so no hook can exist for
|
|
48
|
+
* them — which makes "uninstall" a no-op (nothing was ever installed) while
|
|
49
|
+
* "install" remains a hard failure.
|
|
50
|
+
*/
|
|
51
|
+
export declare function hasHookSpec(ide: IdeId): boolean;
|
|
44
52
|
export declare function resolveHookSpec(ide: IdeId): ResolvedHookSpec;
|
|
45
53
|
/** A typed descriptor for a single peaks-managed hook entry. */
|
|
46
54
|
export type PeaksHookEntry = {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* type contract re-exported by the caller.
|
|
9
9
|
*/
|
|
10
10
|
import { getAdapter } from '../ide/ide-registry.js';
|
|
11
|
-
import { HOOK_OUTER_CACHE_COMMAND, HOOK_OUTER_CACHE_EVENT, HOOK_OUTER_CACHE_SENTINEL, HOOK_WORKSPACE_INIT_COMMAND, HOOK_WORKSPACE_INIT_EVENT, HOOK_WORKSPACE_INIT_SENTINEL } from './session-start-hook-constants.js';
|
|
11
|
+
import { HOOK_OUTER_CACHE_COMMAND, HOOK_OUTER_CACHE_EVENT, HOOK_OUTER_CACHE_SENTINEL, HOOK_POST_COMPACT_REINJECT_COMMAND, HOOK_POST_COMPACT_REINJECT_EVENT, HOOK_POST_COMPACT_REINJECT_MATCHER, HOOK_POST_COMPACT_REINJECT_SENTINEL, HOOK_WORKSPACE_INIT_COMMAND, HOOK_WORKSPACE_INIT_EVENT, HOOK_WORKSPACE_INIT_SENTINEL } from './session-start-hook-constants.js';
|
|
12
12
|
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
13
13
|
export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
|
|
14
14
|
/**
|
|
@@ -45,6 +45,16 @@ const HOOK_COMMAND_BY_IDE = {
|
|
|
45
45
|
// registered. When a slice adds them, add a HOOK_COMMAND_BY_IDE entry
|
|
46
46
|
// here — the function below fail-closes on missing entries.
|
|
47
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* True when the IDE is tabled in `HOOK_COMMAND_BY_IDE`, i.e. peaks can write a
|
|
50
|
+
* hook entry for it. `qoder` / `tongyi-lingma` / `zcode` are reserved adapter
|
|
51
|
+
* ids the registry knows but this table does not, so no hook can exist for
|
|
52
|
+
* them — which makes "uninstall" a no-op (nothing was ever installed) while
|
|
53
|
+
* "install" remains a hard failure.
|
|
54
|
+
*/
|
|
55
|
+
export function hasHookSpec(ide) {
|
|
56
|
+
return HOOK_COMMAND_BY_IDE[ide] !== undefined;
|
|
57
|
+
}
|
|
48
58
|
export function resolveHookSpec(ide) {
|
|
49
59
|
// getAdapter throws on unregistered IDEs — the registry is the source of truth.
|
|
50
60
|
const adapter = getAdapter(ide);
|
|
@@ -53,7 +63,12 @@ export function resolveHookSpec(ide) {
|
|
|
53
63
|
// Defensive fallback: if an adapter is added to the registry without a
|
|
54
64
|
// HOOK_COMMAND_BY_IDE entry, fail-closed with a clear error instead of
|
|
55
65
|
// silently writing a Claude-shaped entry to a non-Claude settings.json.
|
|
56
|
-
|
|
66
|
+
//
|
|
67
|
+
// Caller-neutral on purpose: this one resolver backs install, status,
|
|
68
|
+
// uninstall and the dry-run plan, so the message names neither a verb
|
|
69
|
+
// (naming the install verb made an uninstall failure read as an install
|
|
70
|
+
// failure) nor a file that is not the table.
|
|
71
|
+
throw new Error(`unsupported IDE '${ide}': no HOOK_COMMAND_BY_IDE entry (add one in hooks-codegate-superpowers.ts)`);
|
|
57
72
|
}
|
|
58
73
|
const isClaudeCode = ide === 'claude-code';
|
|
59
74
|
// Claude Code's gate hook must emit its structured decision as JSON:
|
|
@@ -129,6 +144,23 @@ export function resolveHookEntries(ide, _skipProgress = false) {
|
|
|
129
144
|
command: HOOK_CODE_GATE_COMMAND,
|
|
130
145
|
event: HOOK_CODE_GATE_EVENT
|
|
131
146
|
});
|
|
147
|
+
// rid 2026-09-13-a2-post-compact-reinject: the post-compaction
|
|
148
|
+
// engineering-state re-injection. A THIRD SessionStart entry, on a
|
|
149
|
+
// DIFFERENT matcher (`compact`) than the outer-cache and primer entries
|
|
150
|
+
// (matcher `''`, i.e. every source). The matcher is what scopes it: the
|
|
151
|
+
// card belongs in the context after a compaction emptied it, not on a
|
|
152
|
+
// fresh `startup` where the dispatch context is still present.
|
|
153
|
+
//
|
|
154
|
+
// It rides this table rather than a bespoke installer so install,
|
|
155
|
+
// status, uninstall, the dry-run plan and the workspace-init drift
|
|
156
|
+
// comparator all see it as one more peaks-managed entry — the same
|
|
157
|
+
// contract the other two SessionStart entries have.
|
|
158
|
+
entries.push({
|
|
159
|
+
sentinel: HOOK_POST_COMPACT_REINJECT_SENTINEL,
|
|
160
|
+
matcher: HOOK_POST_COMPACT_REINJECT_MATCHER,
|
|
161
|
+
command: HOOK_POST_COMPACT_REINJECT_COMMAND,
|
|
162
|
+
event: HOOK_POST_COMPACT_REINJECT_EVENT
|
|
163
|
+
});
|
|
132
164
|
}
|
|
133
165
|
return entries;
|
|
134
166
|
}
|
|
@@ -154,7 +186,12 @@ export function resolveLegacySentinels(ide) {
|
|
|
154
186
|
// the gate-enforce entry, and so hand-added entries matching this
|
|
155
187
|
// sentinel are recognized as peaks-managed (not stripped as
|
|
156
188
|
// non-Peaks).
|
|
157
|
-
|
|
189
|
+
// rid 2026-09-13-a2-post-compact-reinject: the post-compact re-injection
|
|
190
|
+
// sentinel joins the same set, so uninstall strips it (and status counts
|
|
191
|
+
// it) exactly like the other two SessionStart entries. Without this the
|
|
192
|
+
// entry would be unremovable by `peaks hooks uninstall` — the rollback
|
|
193
|
+
// path T2 requires.
|
|
194
|
+
return [...base, HOOK_OUTER_CACHE_SENTINEL, HOOK_WORKSPACE_INIT_SENTINEL, HOOK_POST_COMPACT_REINJECT_SENTINEL];
|
|
158
195
|
}
|
|
159
196
|
return base;
|
|
160
197
|
}
|
|
@@ -116,6 +116,18 @@ export type HookStatus = {
|
|
|
116
116
|
localSettingsPath?: string;
|
|
117
117
|
localExists?: boolean;
|
|
118
118
|
installed: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Whether this IDE can host a peaks hook AT ALL (`hasHookSpec`).
|
|
121
|
+
*
|
|
122
|
+
* `status` answers "what is on disk", and for an IDE with no
|
|
123
|
+
* `HOOK_COMMAND_BY_IDE` entry the answer is "nothing, and nothing ever will
|
|
124
|
+
* be" — which is a successful query returning a negative fact, not a failed
|
|
125
|
+
* one. The exit code says the query succeeded; this field says what it found.
|
|
126
|
+
* Without it `installed: false` would be indistinguishable from a supported
|
|
127
|
+
* IDE that simply has no hook installed yet, which is a state the user can
|
|
128
|
+
* act on.
|
|
129
|
+
*/
|
|
130
|
+
supportsHooks: boolean;
|
|
119
131
|
};
|
|
120
132
|
/**
|
|
121
133
|
* Slice #014: read the *actually-installed* peaks-managed hook entries
|
|
@@ -4,7 +4,7 @@ import { join, resolve } from 'node:path';
|
|
|
4
4
|
import { assertSafeSettingsFile } from '../ide/shared/safe-path.js';
|
|
5
5
|
import { atomicWriteJson, readJsonObjectFile } from '../ide/shared/atomic-json.js';
|
|
6
6
|
import { getAdapter } from '../ide/ide-registry.js';
|
|
7
|
-
import { resolveHookSpec, resolveHookEntries, resolveLegacySentinels, SUPERPOWERS_DENIED_SKILLS, formatSuperpowersDenyEntry, SUPERPOWERS_DENY_SENTINELS, hasExternalGateExemptions, withExternalGateExemptions, withoutExternalGateExemptions } from './hooks-codegate-superpowers.js';
|
|
7
|
+
import { resolveHookSpec, resolveHookEntries, hasHookSpec, resolveLegacySentinels, SUPERPOWERS_DENIED_SKILLS, formatSuperpowersDenyEntry, SUPERPOWERS_DENY_SENTINELS, hasExternalGateExemptions, withExternalGateExemptions, withoutExternalGateExemptions } from './hooks-codegate-superpowers.js';
|
|
8
8
|
export { HOOK_ENFORCE_SENTINEL, HOOK_CODE_GATE_SENTINEL, HOOK_CODE_GATE_MATCHER, HOOK_CODE_GATE_EVENT, HOOK_CODE_GATE_COMMAND, SUPERPOWERS_DENIED_SKILLS } from './hooks-codegate-superpowers.js';
|
|
9
9
|
// --- Module-level defaults (claude-code) -----------------------------------
|
|
10
10
|
// These exports remain for backward compat — tests and downstream callers
|
|
@@ -44,25 +44,49 @@ function assertSafeSettingsPathCompat(scope, ide, root, settingsPath) {
|
|
|
44
44
|
throw new Error(`settings path drift: ${expected} vs ${settingsPath}`);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
/** Machine-local sibling of the adapter's settings file (Claude Code convention). */
|
|
48
|
-
const LOCAL_SETTINGS_FILE_NAME = 'settings.local.json';
|
|
49
|
-
/** IDEs whose hook settings have a machine-local, gitignored sibling file. */
|
|
50
|
-
const IDES_WITH_LOCAL_SETTINGS = new Set(['claude-code']);
|
|
51
47
|
/**
|
|
52
48
|
* Resolve the machine-local settings file an IDE may carry (e.g. Claude
|
|
53
49
|
* Code's `.claude/settings.local.json`), or `undefined` when there is none.
|
|
54
50
|
*
|
|
51
|
+
* B3(b) 2026-09-13: the machine-local layer is an adapter-DECLARED capability,
|
|
52
|
+
* not a name list. It used to be two hardcoded facts here — a `Set(['claude-code'])`
|
|
53
|
+
* membership test and the filename literal `'settings.local.json'` — i.e. a
|
|
54
|
+
* module that routes per-IDE was itself naming one IDE and one IDE's file.
|
|
55
|
+
* Both now read off the adapter that owns them
|
|
56
|
+
* (`IdeSettingsLocation.localSettingsFileName`), exactly as
|
|
57
|
+
* `auto-compact-dispatcher.ts` already did for the same field.
|
|
58
|
+
*
|
|
59
|
+
* Behaviour is UNCHANGED, and that was verified rather than assumed: over all
|
|
60
|
+
* nine registered `IdeId`s, `IDES_WITH_LOCAL_SETTINGS.has(id)` was `true` for
|
|
61
|
+
* exactly the ids whose adapter declares `localSettingsFileName` (claude-code
|
|
62
|
+
* alone, `'settings.local.json'`). So the set was an exact duplicate of a
|
|
63
|
+
* declaration the adapters already carried, not an independent policy — and
|
|
64
|
+
* a new adapter that declares the field now gets this path for free instead
|
|
65
|
+
* of being silently denied it.
|
|
66
|
+
*
|
|
67
|
+
* This ALSO removes a live instance of the vendor guard's limit 1 (indirect
|
|
68
|
+
* identity: `SET.has(ide)`), which no AST check over comparisons or literals
|
|
69
|
+
* can see. The guard's header named this exact line as the example.
|
|
70
|
+
*
|
|
55
71
|
* Global scope resolves to `undefined`: the global settings file
|
|
56
72
|
* (`~/.claude/settings.json`) is already machine-local, so nothing needs
|
|
57
73
|
* relocating there.
|
|
58
74
|
*/
|
|
59
75
|
function resolveLocalSettingsPath(scope, ide, projectRoot) {
|
|
60
|
-
if (scope === 'global'
|
|
76
|
+
if (scope === 'global')
|
|
61
77
|
return undefined;
|
|
62
|
-
const root = resolveSettingsRoot(scope, projectRoot);
|
|
63
78
|
const adapter = getAdapter(ide);
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
const localFileName = adapter.settings.localSettingsFileName;
|
|
80
|
+
// `undefined` = this IDE has no machine-local layer. Note the order: a
|
|
81
|
+
// caller reaching here with an id the registry does not know still throws
|
|
82
|
+
// from `getAdapter` above, exactly as it did before — every path into this
|
|
83
|
+
// function already called `getAdapter(ide)` via `resolveSettingsPath`, so
|
|
84
|
+
// no new throw path is introduced.
|
|
85
|
+
if (localFileName === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
const root = resolveSettingsRoot(scope, projectRoot);
|
|
88
|
+
assertSafeSettingsFile(scope, root, adapter.settings.dirName, localFileName);
|
|
89
|
+
return join(root, adapter.settings.dirName, localFileName);
|
|
66
90
|
}
|
|
67
91
|
function resolveHookTargets(scope, ide, projectRoot) {
|
|
68
92
|
const sharedPath = resolveSettingsPath(scope, ide, projectRoot);
|
|
@@ -453,6 +477,24 @@ export function removeHookInstall(scope, projectRoot, options) {
|
|
|
453
477
|
const root = resolveSettingsRoot(scope, projectRoot);
|
|
454
478
|
const settingsPath = resolveSettingsPath(scope, ide, projectRoot);
|
|
455
479
|
assertSafeSettingsPathCompat(scope, ide, root, settingsPath);
|
|
480
|
+
// An IDE with no HOOK_COMMAND_BY_IDE entry can never carry a peaks hook, so
|
|
481
|
+
// "remove it" is already true. That is a no-op success, not a failure: the
|
|
482
|
+
// post-condition the caller asked for holds. This stays asymmetric with
|
|
483
|
+
// `applyHookInstall`, which still fails closed for the same IDEs — reporting
|
|
484
|
+
// a successful install there would claim an enforcement that cannot exist.
|
|
485
|
+
//
|
|
486
|
+
// `resolveHookTargets` below is what throws for these IDEs, so the check has
|
|
487
|
+
// to come first. Note `resolveSettingsPath` above still runs: an IDE the
|
|
488
|
+
// adapter registry does not know at all remains a hard error.
|
|
489
|
+
if (!hasHookSpec(ide)) {
|
|
490
|
+
const localSettingsPath = resolveLocalSettingsPath(scope, ide, projectRoot);
|
|
491
|
+
return {
|
|
492
|
+
scope,
|
|
493
|
+
settingsPath,
|
|
494
|
+
removed: false,
|
|
495
|
+
...(localSettingsPath !== undefined && localSettingsPath !== settingsPath ? { localSettingsPath } : {})
|
|
496
|
+
};
|
|
497
|
+
}
|
|
456
498
|
const targets = resolveHookTargets(scope, ide, projectRoot);
|
|
457
499
|
const localTarget = targets.find((t) => t.settingsPath !== settingsPath);
|
|
458
500
|
const present = targets.filter((t) => existsSync(t.settingsPath));
|
|
@@ -528,6 +570,30 @@ export function readHookStatus(scope, projectRoot, options) {
|
|
|
528
570
|
const root = resolveSettingsRoot(scope, projectRoot);
|
|
529
571
|
const settingsPath = resolveSettingsPath(scope, ide, projectRoot);
|
|
530
572
|
assertSafeSettingsPathCompat(scope, ide, root, settingsPath);
|
|
573
|
+
// An IDE with no HOOK_COMMAND_BY_IDE entry has no hook on disk and can never
|
|
574
|
+
// have one, so "what is installed?" has a definite answer: nothing. That is a
|
|
575
|
+
// no-op success, exactly like `removeHookInstall` — the query ran, the disk
|
|
576
|
+
// was read, and the honest reply is `supportsHooks: false, installed: false`.
|
|
577
|
+
// This stays asymmetric with `applyHookInstall`, which still fails closed for
|
|
578
|
+
// the same IDEs: reporting a successful INSTALL would claim an enforcement
|
|
579
|
+
// that cannot exist, whereas reporting a successful STATUS claims nothing.
|
|
580
|
+
//
|
|
581
|
+
// `resolveHookTargets` below is what throws for these IDEs, so the check has
|
|
582
|
+
// to come first. `resolveSettingsPath` above still runs: an IDE the adapter
|
|
583
|
+
// registry does not know at all remains a hard error.
|
|
584
|
+
if (!hasHookSpec(ide)) {
|
|
585
|
+
const localSettingsPath = resolveLocalSettingsPath(scope, ide, projectRoot);
|
|
586
|
+
return {
|
|
587
|
+
scope,
|
|
588
|
+
settingsPath,
|
|
589
|
+
exists: existsSync(settingsPath),
|
|
590
|
+
...(localSettingsPath !== undefined && localSettingsPath !== settingsPath
|
|
591
|
+
? { localSettingsPath, localExists: existsSync(localSettingsPath) }
|
|
592
|
+
: {}),
|
|
593
|
+
installed: false,
|
|
594
|
+
supportsHooks: false
|
|
595
|
+
};
|
|
596
|
+
}
|
|
531
597
|
const targets = resolveHookTargets(scope, ide, projectRoot);
|
|
532
598
|
const exists = existsSync(settingsPath);
|
|
533
599
|
const localTarget = targets.find((t) => t.settingsPath !== settingsPath);
|
|
@@ -541,7 +607,8 @@ export function readHookStatus(scope, projectRoot, options) {
|
|
|
541
607
|
installed: targets.some((t) => {
|
|
542
608
|
const settings = readSettingsFile(t.settingsPath);
|
|
543
609
|
return Object.keys(settings).length > 0 && isInstalledForEntries(settings, t.entries);
|
|
544
|
-
})
|
|
610
|
+
}),
|
|
611
|
+
supportsHooks: true
|
|
545
612
|
};
|
|
546
613
|
}
|
|
547
614
|
/**
|
|
@@ -25,6 +25,47 @@
|
|
|
25
25
|
*/
|
|
26
26
|
/** Sentinel substring identifying a SessionStart outer-cache hook entry. */
|
|
27
27
|
export declare const HOOK_OUTER_CACHE_SENTINEL = "peaks outer-cache write";
|
|
28
|
+
/**
|
|
29
|
+
* rid `2026-09-13-a2-post-compact-reinject` — SessionStart entry scoped to
|
|
30
|
+
* `matcher: 'compact'` that puts the engineering state back into the context
|
|
31
|
+
* a compaction just emptied.
|
|
32
|
+
*
|
|
33
|
+
* This is the official mechanism, and the `matcher` is the load-bearing part
|
|
34
|
+
* of it: Claude Code adds a `SessionStart` hook's plain-text stdout to the
|
|
35
|
+
* context, and scoping to `compact` means the card is re-injected AFTER a
|
|
36
|
+
* compaction rather than on every fresh session. Compaction itself cannot be
|
|
37
|
+
* initiated from here — the harness performs it, and a hook can only observe
|
|
38
|
+
* or veto. Re-injection is the half that IS available to us.
|
|
39
|
+
*
|
|
40
|
+
* Why a separate entry instead of folding this into the `peaks session
|
|
41
|
+
* primer` entry that already runs on every SessionStart: the primer's own
|
|
42
|
+
* contract is "rotation + presence cleanup", it fires on `startup` too (where
|
|
43
|
+
* the dispatch context is still in the history and the card would be noise),
|
|
44
|
+
* and its output is a rotation envelope rather than an engineering-state
|
|
45
|
+
* card. Two entries, two intents — each one's output means exactly one thing.
|
|
46
|
+
*/
|
|
47
|
+
export declare const HOOK_POST_COMPACT_REINJECT_SENTINEL = "peaks session reinject";
|
|
48
|
+
/**
|
|
49
|
+
* The reinject hook command. `--project "${CLAUDE_PROJECT_DIR}"` matches the
|
|
50
|
+
* other two SessionStart entries: `${CLAUDE_PROJECT_DIR}` is Claude Code's
|
|
51
|
+
* standard project-root convention, and the CLI resolves it strictly (a
|
|
52
|
+
* SessionStart payload is env-driven and must not be trusted as a path).
|
|
53
|
+
*
|
|
54
|
+
* `--quiet-failure` is NOT a flag and deliberately so: printing nothing on
|
|
55
|
+
* failure is this command's unconditional behaviour, not something a caller
|
|
56
|
+
* opts into. See `reinject-command.ts` — stdout IS context here, so an error
|
|
57
|
+
* message on stdout would be injected into the model's context as though
|
|
58
|
+
* peaks-loop meant it.
|
|
59
|
+
*/
|
|
60
|
+
export declare const HOOK_POST_COMPACT_REINJECT_COMMAND = "peaks session reinject --project \"${CLAUDE_PROJECT_DIR}\"";
|
|
61
|
+
/** SessionStart hook event key (same as outer-cache + primer). */
|
|
62
|
+
export declare const HOOK_POST_COMPACT_REINJECT_EVENT = "SessionStart";
|
|
63
|
+
/**
|
|
64
|
+
* The SessionStart `source` value the re-injection is scoped to. Claude Code
|
|
65
|
+
* reports `compact` as the SessionStart source when the session is resuming
|
|
66
|
+
* from a compaction; the hook entry's `matcher` is matched against it.
|
|
67
|
+
*/
|
|
68
|
+
export declare const HOOK_POST_COMPACT_REINJECT_MATCHER = "compact";
|
|
28
69
|
/** Default (claude-code) SessionStart hook command. */
|
|
29
70
|
export declare const HOOK_OUTER_CACHE_COMMAND = "peaks outer-cache write --project \"${CLAUDE_PROJECT_DIR}\"";
|
|
30
71
|
/** SessionStart hook event key for Claude Code. */
|
|
@@ -25,6 +25,47 @@
|
|
|
25
25
|
*/
|
|
26
26
|
/** Sentinel substring identifying a SessionStart outer-cache hook entry. */
|
|
27
27
|
export const HOOK_OUTER_CACHE_SENTINEL = 'peaks outer-cache write';
|
|
28
|
+
/**
|
|
29
|
+
* rid `2026-09-13-a2-post-compact-reinject` — SessionStart entry scoped to
|
|
30
|
+
* `matcher: 'compact'` that puts the engineering state back into the context
|
|
31
|
+
* a compaction just emptied.
|
|
32
|
+
*
|
|
33
|
+
* This is the official mechanism, and the `matcher` is the load-bearing part
|
|
34
|
+
* of it: Claude Code adds a `SessionStart` hook's plain-text stdout to the
|
|
35
|
+
* context, and scoping to `compact` means the card is re-injected AFTER a
|
|
36
|
+
* compaction rather than on every fresh session. Compaction itself cannot be
|
|
37
|
+
* initiated from here — the harness performs it, and a hook can only observe
|
|
38
|
+
* or veto. Re-injection is the half that IS available to us.
|
|
39
|
+
*
|
|
40
|
+
* Why a separate entry instead of folding this into the `peaks session
|
|
41
|
+
* primer` entry that already runs on every SessionStart: the primer's own
|
|
42
|
+
* contract is "rotation + presence cleanup", it fires on `startup` too (where
|
|
43
|
+
* the dispatch context is still in the history and the card would be noise),
|
|
44
|
+
* and its output is a rotation envelope rather than an engineering-state
|
|
45
|
+
* card. Two entries, two intents — each one's output means exactly one thing.
|
|
46
|
+
*/
|
|
47
|
+
export const HOOK_POST_COMPACT_REINJECT_SENTINEL = 'peaks session reinject';
|
|
48
|
+
/**
|
|
49
|
+
* The reinject hook command. `--project "${CLAUDE_PROJECT_DIR}"` matches the
|
|
50
|
+
* other two SessionStart entries: `${CLAUDE_PROJECT_DIR}` is Claude Code's
|
|
51
|
+
* standard project-root convention, and the CLI resolves it strictly (a
|
|
52
|
+
* SessionStart payload is env-driven and must not be trusted as a path).
|
|
53
|
+
*
|
|
54
|
+
* `--quiet-failure` is NOT a flag and deliberately so: printing nothing on
|
|
55
|
+
* failure is this command's unconditional behaviour, not something a caller
|
|
56
|
+
* opts into. See `reinject-command.ts` — stdout IS context here, so an error
|
|
57
|
+
* message on stdout would be injected into the model's context as though
|
|
58
|
+
* peaks-loop meant it.
|
|
59
|
+
*/
|
|
60
|
+
export const HOOK_POST_COMPACT_REINJECT_COMMAND = `peaks session reinject --project "\${CLAUDE_PROJECT_DIR}"`;
|
|
61
|
+
/** SessionStart hook event key (same as outer-cache + primer). */
|
|
62
|
+
export const HOOK_POST_COMPACT_REINJECT_EVENT = 'SessionStart';
|
|
63
|
+
/**
|
|
64
|
+
* The SessionStart `source` value the re-injection is scoped to. Claude Code
|
|
65
|
+
* reports `compact` as the SessionStart source when the session is resuming
|
|
66
|
+
* from a compaction; the hook entry's `matcher` is matched against it.
|
|
67
|
+
*/
|
|
68
|
+
export const HOOK_POST_COMPACT_REINJECT_MATCHER = 'compact';
|
|
28
69
|
/** Default (claude-code) SessionStart hook command. */
|
|
29
70
|
export const HOOK_OUTER_CACHE_COMMAND = `peaks outer-cache write --project "\${CLAUDE_PROJECT_DIR}"`;
|
|
30
71
|
/** SessionStart hook event key for Claude Code. */
|