peaks-loop 4.0.35 → 4.0.37
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 +40 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/bin/peaks.js +71 -1
- package/dist/cli/cli-helpers.js +7 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
- package/dist/cli/commands/best-practice-scan-command.js +67 -9
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +78 -7
- package/dist/cli/commands/core/doctor-command.d.ts +8 -0
- package/dist/cli/commands/core/doctor-command.js +44 -2
- package/dist/cli/commands/core/memory-command.js +5 -1
- package/dist/cli/commands/dispatch-commands.js +15 -3
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/hooks-commands.js +10 -1
- package/dist/cli/commands/job-commands.js +107 -25
- package/dist/cli/commands/memory-commands.d.ts +24 -0
- package/dist/cli/commands/memory-commands.js +77 -10
- package/dist/cli/commands/request-commands.d.ts +8 -0
- package/dist/cli/commands/request-commands.js +23 -2
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
- package/dist/cli/commands/wave-plan-commands.js +93 -0
- package/dist/cli/commands/web-commands.d.ts +28 -0
- package/dist/cli/commands/web-commands.js +327 -0
- package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
- package/dist/cli/commands/web-lifecycle-commands.js +321 -0
- package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
- package/dist/services/best-practice/scan-orchestrator.js +14 -5
- package/dist/services/code/orchestrator-can-do.js +27 -4
- package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
- package/dist/services/context/build-dispatch-system-prompt.js +132 -17
- package/dist/services/context/context-audit-hint.d.ts +79 -0
- package/dist/services/context/context-audit-hint.js +150 -0
- package/dist/services/context/context-audit.d.ts +100 -0
- package/dist/services/context/context-audit.js +322 -0
- package/dist/services/context/summary-view.d.ts +54 -0
- package/dist/services/context/summary-view.js +114 -0
- package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
- package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
- package/dist/services/dispatch/session-capsule.d.ts +23 -0
- package/dist/services/dispatch/session-capsule.js +56 -0
- package/dist/services/dispatch/slice-dag.d.ts +9 -0
- package/dist/services/dispatch/slice-dag.js +9 -1
- package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
- package/dist/services/dispatch/test-tool-detection.js +14 -13
- package/dist/services/hooks/auto-compact-hook-install.js +10 -1
- package/dist/services/hooks/write-gate.js +88 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/lint/detect-eslint.d.ts +2 -0
- package/dist/services/lint/detect-eslint.js +23 -9
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +38 -14
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
- package/dist/services/release/version-precheck-service.js +9 -2
- package/dist/services/scan/file-size-scan.d.ts +29 -0
- package/dist/services/scan/file-size-scan.js +63 -0
- package/dist/services/session/caller-binding-service.d.ts +24 -0
- package/dist/services/session/caller-binding-service.js +34 -0
- package/dist/services/session/getSessionDir.js +15 -10
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
- package/dist/services/skills/hooks-settings-service.d.ts +10 -0
- package/dist/services/skills/hooks-settings-service.js +152 -61
- package/dist/services/slice/slice-check-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +110 -50
- package/dist/services/slice/slice-check-types.d.ts +12 -7
- package/dist/services/slice/slice-check-types.js +8 -3
- package/dist/services/slice/slice-decompose-runners.js +24 -21
- package/dist/services/sop/sop-check-service.js +12 -1
- package/dist/services/web/bounded-output.d.ts +34 -0
- package/dist/services/web/bounded-output.js +68 -0
- package/dist/services/web/browser-acquire.d.ts +14 -0
- package/dist/services/web/browser-acquire.js +84 -0
- package/dist/services/web/browser-session-manager.d.ts +111 -0
- package/dist/services/web/browser-session-manager.js +413 -0
- package/dist/services/web/daemon-entry.d.ts +1 -0
- package/dist/services/web/daemon-entry.js +65 -0
- package/dist/services/web/daemon-registry.d.ts +42 -0
- package/dist/services/web/daemon-registry.js +164 -0
- package/dist/services/web/daemon-supervisor.d.ts +144 -0
- package/dist/services/web/daemon-supervisor.js +455 -0
- package/dist/services/web/playwright-loader.d.ts +89 -0
- package/dist/services/web/playwright-loader.js +253 -0
- package/dist/services/web/snapshot-pruner.d.ts +48 -0
- package/dist/services/web/snapshot-pruner.js +241 -0
- package/dist/services/web/untrusted-envelope.d.ts +27 -0
- package/dist/services/web/untrusted-envelope.js +44 -0
- package/dist/services/web/web-artifact-paths.d.ts +79 -0
- package/dist/services/web/web-artifact-paths.js +163 -0
- package/dist/services/web/web-client.d.ts +19 -0
- package/dist/services/web/web-client.js +55 -0
- package/dist/services/web/web-daemon-service.d.ts +38 -0
- package/dist/services/web/web-daemon-service.js +416 -0
- package/dist/services/web/web-fallback.d.ts +70 -0
- package/dist/services/web/web-fallback.js +121 -0
- package/dist/services/web/web-install-service.d.ts +91 -0
- package/dist/services/web/web-install-service.js +346 -0
- package/dist/services/web/web-login-profile.d.ts +89 -0
- package/dist/services/web/web-login-profile.js +612 -0
- package/dist/services/web/web-login-staging.d.ts +27 -0
- package/dist/services/web/web-login-staging.js +173 -0
- package/dist/services/web/web-protocol.d.ts +58 -0
- package/dist/services/web/web-protocol.js +58 -0
- package/dist/services/web/web-status-report.d.ts +33 -0
- package/dist/services/web/web-status-report.js +47 -0
- package/dist/services/workspace/claude-settings-template.d.ts +41 -5
- package/dist/services/workspace/claude-settings-template.js +116 -64
- package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
- package/dist/services/workspace/workspace-service.js +33 -0
- package/package.json +5 -5
- package/scripts/copy-templates.mjs +12 -0
- package/scripts/sync-version.mjs +20 -0
- package/skills/bee/peaks-qa/SKILL.md +2 -0
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
- package/skills/bee/peaks-rd/SKILL.md +2 -0
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
- package/skills/bee/peaks-txt/SKILL.md +2 -0
- package/skills/bee/peaks-ui/SKILL.md +2 -0
- package/skills/peaks-code/SKILL.md +18 -0
- package/skills/peaks-code/references/browser-workflow.md +10 -1
- package/skills/peaks-code/references/context-governance.md +29 -0
- package/skills/peaks-doctor/SKILL.md +2 -0
|
@@ -114,9 +114,16 @@ export function runRootVsShared(opts) {
|
|
|
114
114
|
export function runTagCollision(opts) {
|
|
115
115
|
const rootVersion = readRootVersion(opts.projectRoot);
|
|
116
116
|
const tagName = `v${rootVersion}`;
|
|
117
|
-
//
|
|
117
|
+
// 2026-09-10: no shell. `git` is `git.exe` on Windows, so the wrapper bought
|
|
118
|
+
// nothing — and it actively broke this layer, because `projectRoot` is an
|
|
119
|
+
// ARGUMENT to git and a shell wraps the command line unescaped. On a project
|
|
120
|
+
// whose path contains a space the shell split it, git exited 128
|
|
121
|
+
// ("cannot change to '…'"), and the layer fell through to its
|
|
122
|
+
// "git tag --list exited with code 128; layer skipped" WARNING — so a real
|
|
123
|
+
// tag collision was reported as merely deferred. Reproduced on
|
|
124
|
+
// `…\Temp\peaks space demo` with tag v9.9.9 present: warning (should be
|
|
125
|
+
// blocker). It also emitted DEP0190 on every run, on every layer.
|
|
118
126
|
const res = spawnSync('git', ['-C', opts.projectRoot, 'tag', '--list', tagName], {
|
|
119
|
-
shell: process.platform === 'win32',
|
|
120
127
|
encoding: 'utf8',
|
|
121
128
|
timeout: 5_000
|
|
122
129
|
});
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
export declare const DEFAULT_FILE_SIZE_THRESHOLD = 800;
|
|
2
|
+
/**
|
|
3
|
+
* Paths exempt from the file-size cap. The cap is Karpathy's "Simplicity
|
|
4
|
+
* First": it exists to make a human *simplify* an over-long file. A path is
|
|
5
|
+
* therefore exempt exactly when no such simplification exists — which is two
|
|
6
|
+
* kinds of file, both listed below so the reason is visible next to the rule.
|
|
7
|
+
*
|
|
8
|
+
* Tool output (whole-cloth generated, nobody maintains it by hand):
|
|
9
|
+
* `.peaks/**` is Peaks-Loop's own state store and holds three derived indexes
|
|
10
|
+
* already over the cap (`memory/index.json`, `lint/baseline.json`,
|
|
11
|
+
* `retrospective/index.json`); exempting only `memory/` would leave the other
|
|
12
|
+
* two false positives intact. No source module lives there, and its largest
|
|
13
|
+
* hand-authored file is under 500 lines. Lockfiles are regenerated on every
|
|
14
|
+
* install.
|
|
15
|
+
*
|
|
16
|
+
* Append-only records: `CHANGELOG.md` is history, so its length is a function
|
|
17
|
+
* of how long the project has existed, not of anyone's design choices — the
|
|
18
|
+
* only way to "fix" a violation would be to delete the record. Left checked,
|
|
19
|
+
* this gate is reliably red on every release, precisely when it cannot be
|
|
20
|
+
* acted on, which trains people to ignore it. A nested changelog under
|
|
21
|
+
* `packages/` is the same kind of record.
|
|
22
|
+
*
|
|
23
|
+
* Declared once, here — do not add special-cases in the scan loop.
|
|
24
|
+
*/
|
|
25
|
+
export declare const SIZE_CAP_EXEMPT_PATTERNS: readonly string[];
|
|
26
|
+
export declare function isSizeCapExempt(file: string): boolean;
|
|
2
27
|
export type FileSizeViolation = {
|
|
3
28
|
file: string;
|
|
4
29
|
lines: number;
|
|
@@ -7,6 +32,10 @@ export type FileSizeScanResult = {
|
|
|
7
32
|
ok: boolean;
|
|
8
33
|
threshold: number;
|
|
9
34
|
checkedFiles: number;
|
|
35
|
+
/** Paths skipped by SIZE_CAP_EXEMPT_PATTERNS (tool output + append-only
|
|
36
|
+
* records). Reported so the exemption is auditable rather than a silent
|
|
37
|
+
* skip. */
|
|
38
|
+
exemptFiles: string[];
|
|
10
39
|
/** Files that appeared in `git diff` but no longer exist on disk (e.g.
|
|
11
40
|
* deleted in the working tree). Pre-#015 the scan crashed on these via
|
|
12
41
|
* ENOENT; now they are reported here as informational data. */
|
|
@@ -2,6 +2,63 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
export const DEFAULT_FILE_SIZE_THRESHOLD = 800;
|
|
5
|
+
/**
|
|
6
|
+
* Paths exempt from the file-size cap. The cap is Karpathy's "Simplicity
|
|
7
|
+
* First": it exists to make a human *simplify* an over-long file. A path is
|
|
8
|
+
* therefore exempt exactly when no such simplification exists — which is two
|
|
9
|
+
* kinds of file, both listed below so the reason is visible next to the rule.
|
|
10
|
+
*
|
|
11
|
+
* Tool output (whole-cloth generated, nobody maintains it by hand):
|
|
12
|
+
* `.peaks/**` is Peaks-Loop's own state store and holds three derived indexes
|
|
13
|
+
* already over the cap (`memory/index.json`, `lint/baseline.json`,
|
|
14
|
+
* `retrospective/index.json`); exempting only `memory/` would leave the other
|
|
15
|
+
* two false positives intact. No source module lives there, and its largest
|
|
16
|
+
* hand-authored file is under 500 lines. Lockfiles are regenerated on every
|
|
17
|
+
* install.
|
|
18
|
+
*
|
|
19
|
+
* Append-only records: `CHANGELOG.md` is history, so its length is a function
|
|
20
|
+
* of how long the project has existed, not of anyone's design choices — the
|
|
21
|
+
* only way to "fix" a violation would be to delete the record. Left checked,
|
|
22
|
+
* this gate is reliably red on every release, precisely when it cannot be
|
|
23
|
+
* acted on, which trains people to ignore it. A nested changelog under
|
|
24
|
+
* `packages/` is the same kind of record.
|
|
25
|
+
*
|
|
26
|
+
* Declared once, here — do not add special-cases in the scan loop.
|
|
27
|
+
*/
|
|
28
|
+
export const SIZE_CAP_EXEMPT_PATTERNS = [
|
|
29
|
+
// tool output
|
|
30
|
+
'.peaks/**',
|
|
31
|
+
'**/pnpm-lock.yaml',
|
|
32
|
+
'**/package-lock.json',
|
|
33
|
+
'**/yarn.lock',
|
|
34
|
+
// append-only records
|
|
35
|
+
'**/CHANGELOG.md'
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Glob → RegExp for the shapes above only, in a single split pass (chained
|
|
39
|
+
* string replaces would re-expand the `.*` they had just produced). The
|
|
40
|
+
* directory-wildcard prefix matches zero directories, so a root-level
|
|
41
|
+
* lockfile still counts.
|
|
42
|
+
*/
|
|
43
|
+
function exemptPatternToRegExp(pattern) {
|
|
44
|
+
const body = pattern
|
|
45
|
+
.split(/(\*\*\/|\*\*|\*)/)
|
|
46
|
+
.map((part) => {
|
|
47
|
+
if (part === '**/')
|
|
48
|
+
return '(?:.*/)?';
|
|
49
|
+
if (part === '**')
|
|
50
|
+
return '.*';
|
|
51
|
+
if (part === '*')
|
|
52
|
+
return '[^/]*';
|
|
53
|
+
return part.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
54
|
+
})
|
|
55
|
+
.join('');
|
|
56
|
+
return new RegExp(`^${body}$`);
|
|
57
|
+
}
|
|
58
|
+
export function isSizeCapExempt(file) {
|
|
59
|
+
const normalized = file.replace(/\\/g, '/');
|
|
60
|
+
return SIZE_CAP_EXEMPT_PATTERNS.some((pattern) => exemptPatternToRegExp(pattern).test(normalized));
|
|
61
|
+
}
|
|
5
62
|
function getChangedFiles(projectRoot, baseRef) {
|
|
6
63
|
try {
|
|
7
64
|
// --diff-filter=AM keeps only Added + Modified entries. Deleted files
|
|
@@ -28,8 +85,13 @@ export function scanFileSize(options) {
|
|
|
28
85
|
const files = getChangedFiles(options.projectRoot, baseRef);
|
|
29
86
|
const violations = [];
|
|
30
87
|
const deletedFiles = [];
|
|
88
|
+
const exemptFiles = [];
|
|
31
89
|
let checkedFiles = 0;
|
|
32
90
|
for (const file of files) {
|
|
91
|
+
if (isSizeCapExempt(file)) {
|
|
92
|
+
exemptFiles.push(file);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
33
95
|
const absolute = join(options.projectRoot, file);
|
|
34
96
|
// Pre-#015: readFileSync threw ENOENT for files that appear in
|
|
35
97
|
// `git diff --name-only` but no longer exist on disk (e.g. a refactor
|
|
@@ -62,6 +124,7 @@ export function scanFileSize(options) {
|
|
|
62
124
|
ok: violations.length === 0,
|
|
63
125
|
threshold,
|
|
64
126
|
checkedFiles,
|
|
127
|
+
exemptFiles,
|
|
65
128
|
deletedFiles,
|
|
66
129
|
violations
|
|
67
130
|
};
|
|
@@ -68,6 +68,30 @@ export declare function getCallerBinding(projectRoot: string, callerId: string):
|
|
|
68
68
|
* documented 4.0.14 carry-forward micro-fix from QA's issue #1.
|
|
69
69
|
*/
|
|
70
70
|
export declare function setCallerBinding(projectRoot: string, callerId: string, binding: CallerBinding): void;
|
|
71
|
+
/**
|
|
72
|
+
* Repoint an EXISTING per-caller binding at a new peak session id.
|
|
73
|
+
*
|
|
74
|
+
* Slice 2026-09-10 (rid=rebind-must-update-caller-binding): an explicit
|
|
75
|
+
* `peaks workspace init --session-id <X> --allow-session-rebind` rewrites
|
|
76
|
+
* the project-global `.peaks/_runtime/session.json`. Without this call the
|
|
77
|
+
* per-caller file keeps shadowing it for `getSessionIdCanonical`, so the
|
|
78
|
+
* rebind silently did not take for every command that resolves through
|
|
79
|
+
* that variant (`peaks session checkpoint`, `peaks session 24h-mode`, ...)
|
|
80
|
+
* while `getCurrentSessionId` reported the new session.
|
|
81
|
+
*
|
|
82
|
+
* Only the binding of the caller that performed the rebind is repointed —
|
|
83
|
+
* a second caller keeps its own session, which is the multi-caller
|
|
84
|
+
* isolation the per-caller design exists for.
|
|
85
|
+
*
|
|
86
|
+
* Every other field is preserved: `createdAt` is the session creation
|
|
87
|
+
* stamp the legacy `session.json` dual-write reuses, and `skill` / `mode`
|
|
88
|
+
* / `gate` are live presence state.
|
|
89
|
+
*
|
|
90
|
+
* @returns `true` when a binding existed and was repointed, `false` when
|
|
91
|
+
* the caller had no binding file (nothing was shadowing the rebind, and
|
|
92
|
+
* we do not create one speculatively).
|
|
93
|
+
*/
|
|
94
|
+
export declare function updateCallerBindingSessionId(projectRoot: string, callerId: string, peakSessionId: string): boolean;
|
|
71
95
|
/**
|
|
72
96
|
* Enumerate the per-caller binding files under
|
|
73
97
|
* `.peaks/_runtime/callers/`. Returns the parsed bindings plus the
|
|
@@ -120,6 +120,40 @@ export function setCallerBinding(projectRoot, callerId, binding) {
|
|
|
120
120
|
};
|
|
121
121
|
atomicWriteJson(bindingPath, payload);
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Repoint an EXISTING per-caller binding at a new peak session id.
|
|
125
|
+
*
|
|
126
|
+
* Slice 2026-09-10 (rid=rebind-must-update-caller-binding): an explicit
|
|
127
|
+
* `peaks workspace init --session-id <X> --allow-session-rebind` rewrites
|
|
128
|
+
* the project-global `.peaks/_runtime/session.json`. Without this call the
|
|
129
|
+
* per-caller file keeps shadowing it for `getSessionIdCanonical`, so the
|
|
130
|
+
* rebind silently did not take for every command that resolves through
|
|
131
|
+
* that variant (`peaks session checkpoint`, `peaks session 24h-mode`, ...)
|
|
132
|
+
* while `getCurrentSessionId` reported the new session.
|
|
133
|
+
*
|
|
134
|
+
* Only the binding of the caller that performed the rebind is repointed —
|
|
135
|
+
* a second caller keeps its own session, which is the multi-caller
|
|
136
|
+
* isolation the per-caller design exists for.
|
|
137
|
+
*
|
|
138
|
+
* Every other field is preserved: `createdAt` is the session creation
|
|
139
|
+
* stamp the legacy `session.json` dual-write reuses, and `skill` / `mode`
|
|
140
|
+
* / `gate` are live presence state.
|
|
141
|
+
*
|
|
142
|
+
* @returns `true` when a binding existed and was repointed, `false` when
|
|
143
|
+
* the caller had no binding file (nothing was shadowing the rebind, and
|
|
144
|
+
* we do not create one speculatively).
|
|
145
|
+
*/
|
|
146
|
+
export function updateCallerBindingSessionId(projectRoot, callerId, peakSessionId) {
|
|
147
|
+
const existing = getCallerBinding(projectRoot, callerId);
|
|
148
|
+
if (existing === null)
|
|
149
|
+
return false;
|
|
150
|
+
setCallerBinding(projectRoot, callerId, {
|
|
151
|
+
...existing,
|
|
152
|
+
peakSessionId,
|
|
153
|
+
lastActivityAt: new Date().toISOString()
|
|
154
|
+
});
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
123
157
|
/**
|
|
124
158
|
* Enumerate the per-caller binding files under
|
|
125
159
|
* `.peaks/_runtime/callers/`. Returns the parsed bindings plus the
|
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
* Canonical session-directory resolver.
|
|
3
3
|
*
|
|
4
4
|
* As of slice 2026-06-05-peaks-runtime-layer the per-session workspace
|
|
5
|
-
* lives at `<root>/.peaks/_runtime/<sessionId
|
|
6
|
-
* `<root>/.peaks
|
|
7
|
-
* through this helper. The legacy top-level path
|
|
8
|
-
* back-compat **read** fallback
|
|
9
|
-
* `src/services/artifacts/
|
|
5
|
+
* lives at `<root>/.peaks/_runtime/<sessionId>/`, NOT at the legacy
|
|
6
|
+
* `<root>/.peaks/<sessionId>/` location. All **write** paths MUST route
|
|
7
|
+
* through this helper. The legacy top-level path survives only as a
|
|
8
|
+
* back-compat **read** fallback (see `legacySessionRoot` in
|
|
9
|
+
* `src/services/artifacts/artifact-prerequisites.ts` and `sessionOwnsSlice`
|
|
10
|
+
* in `src/services/sc/sc-service.ts`).
|
|
10
11
|
*
|
|
11
|
-
* The corresponding test in
|
|
12
12
|
* `tests/unit/services/session/session-dir-canonical.test.ts` enforces
|
|
13
|
-
*
|
|
13
|
+
* three invariants:
|
|
14
14
|
*
|
|
15
15
|
* (a) `getSessionDir(root, sid)` returns `<root>/.peaks/_runtime/<sid>`.
|
|
16
|
-
* (b) A static scan of `src
|
|
17
|
-
* `
|
|
18
|
-
* back-compat **read** sites
|
|
16
|
+
* (b) A static scan of `src/**` flags any `join()` chain that names
|
|
17
|
+
* `.peaks` plus a session id without routing through this resolver,
|
|
18
|
+
* wherever the id sits in the chain. The back-compat **read** sites
|
|
19
|
+
* are exempted by an allow-list whose entries are asserted to be
|
|
20
|
+
* load-bearing (an inert entry fails the scan).
|
|
21
|
+
* (c) A static scan of every markdown file under `skills/` flags a
|
|
22
|
+
* legacy `.peaks/<sid>/...` artifact path that a sub-agent would
|
|
23
|
+
* follow verbatim.
|
|
19
24
|
*
|
|
20
25
|
* @param projectRoot - Absolute path to the project root.
|
|
21
26
|
* @param sessionId - The session identifier (e.g. `2026-06-06-session-5b1095`).
|
|
@@ -15,7 +15,32 @@ interface ResolvedHookSpec {
|
|
|
15
15
|
readonly hookEnforceSentinel: string;
|
|
16
16
|
readonly hookEnforceMatcher: string;
|
|
17
17
|
readonly hookEnforceEvent: string;
|
|
18
|
+
/**
|
|
19
|
+
* True when the gate-enforce entry must be materialized into the IDE's
|
|
20
|
+
* MACHINE-LOCAL settings file rather than the shared one (see
|
|
21
|
+
* `hookEnforceShell` below). Set for Claude Code, whose project-scope
|
|
22
|
+
* hooks have a gitignored per-machine sibling file.
|
|
23
|
+
*/
|
|
24
|
+
readonly hookEnforceMachineLocal: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The `shell` field for the gate-enforce handler, or `undefined` to omit
|
|
27
|
+
* the key entirely and let the IDE use its documented default.
|
|
28
|
+
*
|
|
29
|
+
* Claude Code only. See `resolveHookShell` for why Windows needs this.
|
|
30
|
+
*/
|
|
31
|
+
readonly hookEnforceShell: string | undefined;
|
|
18
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Claude Code runs a shell-form hook command through a shell that defaults
|
|
35
|
+
* to bash — which on Windows means Git Bash, and MSYS2's bash
|
|
36
|
+
* force-allocates its own console window. The result is a visible window on
|
|
37
|
+
* EVERY Bash tool call. The window is created by the spawner (Claude Code)
|
|
38
|
+
* before any peaks code runs, so `windowsHide` and any in-process hiding
|
|
39
|
+
* cannot help; pinning the hook's `shell` is the only lever the hook schema
|
|
40
|
+
* offers. The platform-neutral default (`undefined` → omit the key) is kept
|
|
41
|
+
* everywhere else.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveHookShell(platform?: NodeJS.Platform): string | undefined;
|
|
19
44
|
export declare function resolveHookSpec(ide: IdeId): ResolvedHookSpec;
|
|
20
45
|
/** A typed descriptor for a single peaks-managed hook entry. */
|
|
21
46
|
export type PeaksHookEntry = {
|
|
@@ -23,6 +48,14 @@ export type PeaksHookEntry = {
|
|
|
23
48
|
matcher: string;
|
|
24
49
|
command: string;
|
|
25
50
|
event: string;
|
|
51
|
+
/**
|
|
52
|
+
* When true the entry is written to the machine-local, gitignored settings
|
|
53
|
+
* file (`.claude/settings.local.json`) instead of the shared one, because
|
|
54
|
+
* the entry carries a machine-specific value (the `shell` field).
|
|
55
|
+
*/
|
|
56
|
+
machineLocal?: boolean;
|
|
57
|
+
/** Optional `shell` field for the emitted handler. */
|
|
58
|
+
shell?: string;
|
|
26
59
|
};
|
|
27
60
|
/**
|
|
28
61
|
* Slice 2026-08-06-codegate-vendor-neutral — code-gate hook entry.
|
|
@@ -11,6 +11,19 @@ import { getAdapter } from '../ide/ide-registry.js';
|
|
|
11
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';
|
|
12
12
|
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
13
13
|
export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
|
|
14
|
+
/**
|
|
15
|
+
* Claude Code runs a shell-form hook command through a shell that defaults
|
|
16
|
+
* to bash — which on Windows means Git Bash, and MSYS2's bash
|
|
17
|
+
* force-allocates its own console window. The result is a visible window on
|
|
18
|
+
* EVERY Bash tool call. The window is created by the spawner (Claude Code)
|
|
19
|
+
* before any peaks code runs, so `windowsHide` and any in-process hiding
|
|
20
|
+
* cannot help; pinning the hook's `shell` is the only lever the hook schema
|
|
21
|
+
* offers. The platform-neutral default (`undefined` → omit the key) is kept
|
|
22
|
+
* everywhere else.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveHookShell(platform = process.platform) {
|
|
25
|
+
return platform === 'win32' ? 'powershell' : undefined;
|
|
26
|
+
}
|
|
14
27
|
/**
|
|
15
28
|
* Per-IDE hook command + sentinel. The default (Claude Code) uses the
|
|
16
29
|
* legacy `peaks gate enforce` surface; Trae / Cursor / Codex (Cursor-style
|
|
@@ -42,11 +55,22 @@ export function resolveHookSpec(ide) {
|
|
|
42
55
|
// silently writing a Claude-shaped entry to a non-Claude settings.json.
|
|
43
56
|
throw new Error(`peaks hooks install: unsupported IDE '${ide}' (no HOOK_COMMAND_BY_IDE entry; add one to hooks-settings-service.ts)`);
|
|
44
57
|
}
|
|
58
|
+
const isClaudeCode = ide === 'claude-code';
|
|
59
|
+
// Claude Code's gate hook must emit its structured decision as JSON:
|
|
60
|
+
// without `--json` the hook validator rejects the plain `{}` stdout with
|
|
61
|
+
// "Hook JSON output validation failed". See
|
|
62
|
+
// .peaks/memory/bash-pretooluse-hook-json-error-fix.md.
|
|
63
|
+
const jsonFlag = isClaudeCode ? ' --json' : '';
|
|
45
64
|
return {
|
|
46
|
-
hookEnforceCommand: `${spec.command} --project "\${${adapter.envVar}}"`,
|
|
65
|
+
hookEnforceCommand: `${spec.command} --project "\${${adapter.envVar}}"${jsonFlag}`,
|
|
47
66
|
hookEnforceSentinel: spec.sentinel,
|
|
48
67
|
hookEnforceMatcher: adapter.toolMatcher,
|
|
49
|
-
hookEnforceEvent: adapter.hookEvent
|
|
68
|
+
hookEnforceEvent: adapter.hookEvent,
|
|
69
|
+
// Only Claude Code has the machine-local sibling settings file the
|
|
70
|
+
// routing depends on, and only Claude Code's hook schema accepts a
|
|
71
|
+
// `shell` key.
|
|
72
|
+
hookEnforceMachineLocal: isClaudeCode,
|
|
73
|
+
hookEnforceShell: isClaudeCode ? resolveHookShell() : undefined
|
|
50
74
|
};
|
|
51
75
|
}
|
|
52
76
|
/**
|
|
@@ -64,7 +88,14 @@ export const HOOK_CODE_GATE_COMMAND = `peaks code-gate --json`;
|
|
|
64
88
|
export function resolveHookEntries(ide, _skipProgress = false) {
|
|
65
89
|
const spec = resolveHookSpec(ide);
|
|
66
90
|
const entries = [
|
|
67
|
-
{
|
|
91
|
+
{
|
|
92
|
+
sentinel: spec.hookEnforceSentinel,
|
|
93
|
+
matcher: spec.hookEnforceMatcher,
|
|
94
|
+
command: spec.hookEnforceCommand,
|
|
95
|
+
event: spec.hookEnforceEvent,
|
|
96
|
+
machineLocal: spec.hookEnforceMachineLocal,
|
|
97
|
+
...(spec.hookEnforceShell !== undefined ? { shell: spec.hookEnforceShell } : {})
|
|
98
|
+
}
|
|
68
99
|
];
|
|
69
100
|
if (ide === 'claude-code') {
|
|
70
101
|
entries.push({
|
|
@@ -76,6 +76,13 @@ export type HookInstallPlan = {
|
|
|
76
76
|
desiredCommand: string;
|
|
77
77
|
sentinel: string;
|
|
78
78
|
matcher: string;
|
|
79
|
+
/**
|
|
80
|
+
* Machine-local, gitignored settings file that the gate-enforce entry is
|
|
81
|
+
* written to instead of `settingsPath`. Undefined when the IDE has no such
|
|
82
|
+
* file (or the scope is global, where the settings file is already
|
|
83
|
+
* machine-local). See `resolveHookTargets`.
|
|
84
|
+
*/
|
|
85
|
+
localSettingsPath?: string;
|
|
79
86
|
};
|
|
80
87
|
export type HookInstallResult = HookInstallPlan & {
|
|
81
88
|
applied: boolean;
|
|
@@ -83,12 +90,15 @@ export type HookInstallResult = HookInstallPlan & {
|
|
|
83
90
|
export type HookRemoveResult = {
|
|
84
91
|
scope: HookScope;
|
|
85
92
|
settingsPath: string;
|
|
93
|
+
localSettingsPath?: string;
|
|
86
94
|
removed: boolean;
|
|
87
95
|
};
|
|
88
96
|
export type HookStatus = {
|
|
89
97
|
scope: HookScope;
|
|
90
98
|
settingsPath: string;
|
|
91
99
|
exists: boolean;
|
|
100
|
+
localSettingsPath?: string;
|
|
101
|
+
localExists?: boolean;
|
|
92
102
|
installed: boolean;
|
|
93
103
|
};
|
|
94
104
|
/**
|