peaks-loop 4.0.36 → 4.0.38
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 +42 -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.js +21 -5
- package/dist/cli/commands/hooks-commands.js +41 -7
- package/dist/cli/commands/job-commands.js +107 -25
- package/dist/cli/commands/scan-commands.js +1 -1
- 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 +35 -1
- package/dist/services/context/build-dispatch-system-prompt.js +55 -3
- package/dist/services/context/context-audit-hint.d.ts +79 -0
- package/dist/services/context/context-audit-hint.js +150 -0
- package/dist/services/hooks/auto-compact-hook-install.js +10 -1
- package/dist/services/hooks/write-gate.js +88 -0
- package/dist/services/lint/detect-eslint.d.ts +2 -0
- package/dist/services/lint/detect-eslint.js +23 -9
- package/dist/services/lint/detect-ocr-18.d.ts +2 -0
- package/dist/services/lint/detect-ocr-18.js +36 -5
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +38 -14
- package/dist/services/lint/ocr-multilang-adapter.js +9 -2
- 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 +74 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +129 -3
- package/dist/services/skills/hooks-settings-service.d.ts +26 -0
- package/dist/services/skills/hooks-settings-service.js +186 -62
- 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 +59 -7
- package/dist/services/workspace/claude-settings-template.js +139 -67
- package/dist/services/workspace/workspace-claude-settings-materializer.js +46 -19
- 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/peaks-code/SKILL.md +10 -0
- package/skills/peaks-code/references/browser-workflow.md +10 -1
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* the in-memory template byte-for-byte.
|
|
15
15
|
*
|
|
16
16
|
* One matcher is emitted:
|
|
17
|
-
* 1. `Write|Edit|MultiEdit` — a node
|
|
18
|
-
*
|
|
19
|
-
* for
|
|
20
|
-
* everything else.
|
|
17
|
+
* 1. `Write|Edit|MultiEdit` — a `node <script>` handler that runs the
|
|
18
|
+
* path gate shipped at `src/services/hooks/write-gate.js`. Exits 0
|
|
19
|
+
* (allow) for the paths the gate skips, exit 1 (deny → fall through to
|
|
20
|
+
* gate) for everything else. TEMPLATE_VERSION 1.6.0 moved the decision
|
|
21
|
+
* out of an inlined `node -e "<js>"` one-liner, whose escaping was
|
|
22
|
+
* bash-specific and therefore could not take a platform `shell` pin.
|
|
21
23
|
*
|
|
22
24
|
* The previous `Bash` matcher (which whitelisted a fixed `peaks
|
|
23
25
|
* <subcommand>` prefix) was removed in TEMPLATE_VERSION 1.2.0. The
|
|
@@ -31,6 +33,9 @@
|
|
|
31
33
|
* consumer project's `.claude/settings.json` and which exits 0
|
|
32
34
|
* silently for any command not guarded by a registered SOP gate.
|
|
33
35
|
*/
|
|
36
|
+
import { dirname, resolve } from 'node:path';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
import { EXTERNAL_GATE_EXEMPT_ENV, hasExternalGateExemptions, resolveHookShell, resolveHookSpec } from '../skills/hooks-codegate-superpowers.js';
|
|
34
39
|
export const CLAUDE_SETTINGS_LOCAL_FILENAME = '.claude/settings.local.json';
|
|
35
40
|
/**
|
|
36
41
|
* Informational version of the offline template shape. Bumped when the
|
|
@@ -55,14 +60,41 @@ export const CLAUDE_SETTINGS_LOCAL_FILENAME = '.claude/settings.local.json';
|
|
|
55
60
|
* gate). The existing Write|Edit|MultiEdit matcher is
|
|
56
61
|
* preserved. The new matcher's exit code is the load-bearing
|
|
57
62
|
* signal: 0 = allow, 2 = block (with stderr BLOCKED reason).
|
|
63
|
+
* 1.4.0 — added the `peaks gate enforce` `Bash` PreToolUse entry. It
|
|
64
|
+
* lives here (machine-local, gitignored file) rather than in
|
|
65
|
+
* the committed `.claude/settings.json` because its `shell`
|
|
66
|
+
* is machine-specific: on Windows the default Git-Bash shell
|
|
67
|
+
* force-allocates a console window on every Bash tool call.
|
|
68
|
+
* This template is the second writer of that file, so it must
|
|
69
|
+
* emit the entry too — otherwise `peaks workspace init` would
|
|
70
|
+
* overwrite whatever `peaks hooks install` put there.
|
|
71
|
+
* 1.5.0 — pinned the same platform `shell` on the `peaks code
|
|
72
|
+
* gate-step-08` handler. It runs on the same `Bash` matcher,
|
|
73
|
+
* so leaving it un-pinned left the console-window defect in
|
|
74
|
+
* place for half of every Bash tool call.
|
|
75
|
+
* 1.6.0 — the `Write|Edit|MultiEdit` handler no longer inlines its
|
|
76
|
+
* JavaScript as `node -e "<js>"`. It invokes the shipped script
|
|
77
|
+
* `src/services/hooks/write-gate.js` instead, so the command
|
|
78
|
+
* string carries no shell-escaped payload at all and the handler
|
|
79
|
+
* can take the same platform `shell` pin as its siblings. The
|
|
80
|
+
* decision itself is a verbatim relocation — see that file.
|
|
81
|
+
* 1.7.0 — added the `env` block declaring Peaks' workspace tree exempt
|
|
82
|
+
* from a THIRD-PARTY PreToolUse fact-forcing gate
|
|
83
|
+
* (`EXTERNAL_GATE_EXEMPT_ENV`). The comparator now requires the
|
|
84
|
+
* on-disk file to declare those exemptions too, so a project
|
|
85
|
+
* installed by an earlier release refreshes once and converges.
|
|
58
86
|
*/
|
|
59
|
-
export const TEMPLATE_VERSION = '1.
|
|
87
|
+
export const TEMPLATE_VERSION = '1.7.0';
|
|
60
88
|
/**
|
|
61
|
-
* Compare two serialized template strings for semantic equivalence
|
|
89
|
+
* Compare two serialized template strings for semantic equivalence: does the
|
|
90
|
+
* on-disk file already declare everything the generated template declares?
|
|
62
91
|
*
|
|
63
92
|
* Returns `true` iff both strings parse to objects whose
|
|
64
93
|
* `hooks.PreToolUse` arrays are structurally identical (same length;
|
|
65
|
-
* each entry's `matcher`, `hooks[].type`, `hooks[].command` match)
|
|
94
|
+
* each entry's `matcher`, `hooks[].type`, `hooks[].command` match) AND the
|
|
95
|
+
* on-disk `env` already carries every exemption the template declares (extra
|
|
96
|
+
* on-disk keys and extra globs are allowed — a user may exempt other trees,
|
|
97
|
+
* and a requirement the file already exceeds must not re-trigger a write).
|
|
66
98
|
*
|
|
67
99
|
* Returns `false` on any `JSON.parse` error, shape mismatch, or
|
|
68
100
|
* missing `hooks.PreToolUse`. Whitespace and key order do NOT affect
|
|
@@ -104,7 +136,12 @@ export function templateContentMatches(generated, onDisk) {
|
|
|
104
136
|
return false;
|
|
105
137
|
}
|
|
106
138
|
}
|
|
107
|
-
|
|
139
|
+
// A project installed by a release that predates a template-declared
|
|
140
|
+
// exemption still needs the refresh this comparator gates — otherwise the
|
|
141
|
+
// entry would only ever appear on a machine that re-ran `peaks hooks
|
|
142
|
+
// install`. `hasExternalGateExemptions` is the same predicate the installer
|
|
143
|
+
// uses, so the two writers cannot drift apart.
|
|
144
|
+
return hasExternalGateExemptions({ env: parsedOnDisk.env });
|
|
108
145
|
}
|
|
109
146
|
function isTemplateShape(value) {
|
|
110
147
|
if (typeof value !== 'object' || value === null) {
|
|
@@ -124,70 +161,70 @@ function sameHooksArray(a, b) {
|
|
|
124
161
|
for (let i = 0; i < a.length; i += 1) {
|
|
125
162
|
const ha = a[i];
|
|
126
163
|
const hb = b[i];
|
|
127
|
-
|
|
164
|
+
// `shell` participates in the comparison: it is machine-specific (see
|
|
165
|
+
// `resolveHookShell`), so a file written on one platform must be
|
|
166
|
+
// recognized as drifted on the other instead of silently kept.
|
|
167
|
+
if (ha.type !== hb.type || ha.command !== hb.command || ha.shell !== hb.shell) {
|
|
128
168
|
return false;
|
|
129
169
|
}
|
|
130
170
|
}
|
|
131
171
|
return true;
|
|
132
172
|
}
|
|
133
173
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* into `.claude/settings.local.json` under the `command` field. Per
|
|
137
|
-
* Node.js docs (https://nodejs.org/api/process.html#processargv), when
|
|
138
|
-
* using `-e` there is no script-file slot, so `process.argv[1]` is the
|
|
139
|
-
* first user-passed extra argument. This is consistent across Windows,
|
|
140
|
-
* macOS, and Linux.
|
|
174
|
+
* This module's own directory — `<root>/src/services/workspace` in the
|
|
175
|
+
* source tree, `<root>/dist/services/workspace` in a build.
|
|
141
176
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
177
|
+
* Anchored on the running module rather than on `process.argv[1]`: the
|
|
178
|
+
* same reason `daemon-supervisor.ts` documents — `argv[1]` is a different
|
|
179
|
+
* file in each way the CLI is entered (`bin/peaks.js`, `src/cli/index.ts`
|
|
180
|
+
* under tsx, `dist/cli/index.js` when invoked directly), whereas the
|
|
181
|
+
* module's own location is the one fact that is always true.
|
|
182
|
+
*/
|
|
183
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
184
|
+
/**
|
|
185
|
+
* Absolute path of the shipped Write|Edit|MultiEdit gate script.
|
|
147
186
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
187
|
+
* `write-gate.js` is a plain `.js` (not a compiled `.ts`) precisely so this
|
|
188
|
+
* single relative filename resolves in BOTH trees: `src/services/hooks/` for
|
|
189
|
+
* `tsx` / vitest, `dist/services/hooks/` for an installed consumer (copied by
|
|
190
|
+
* `scripts/copy-templates.mjs`; the `dist/**` `*.js` glob in
|
|
191
|
+
* `package.json#files` already ships it).
|
|
192
|
+
*
|
|
193
|
+
* Separators are normalized to `/` so the emitted command contains no
|
|
194
|
+
* backslash at all. That is what makes the handler shell-agnostic: bash
|
|
195
|
+
* reduces `\X` inside `"..."` and PowerShell escapes with a backtick, so any
|
|
196
|
+
* backslash in the string is one dialect's problem waiting to happen.
|
|
153
197
|
*/
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
// unescaped inner `"` would close the wrapper prematurely. Backslashes
|
|
157
|
-
// do NOT need escaping here — bash inside a `"..."` wrapper reduces
|
|
158
|
-
// `\\` to `\`, so any `\X` in the inner JS reaches Node as `\X`,
|
|
159
|
-
// which is what regex literals like `/\.peaks\//` need. Adding a
|
|
160
|
-
// second `\\` → `\\` pass would double-escape backslashes and break
|
|
161
|
-
// every regex literal the inner JS contains.
|
|
162
|
-
const escaped = js.replace(/"/g, '\\"');
|
|
163
|
-
return `node -e "${escaped}"`;
|
|
198
|
+
export function writeGateScriptPath() {
|
|
199
|
+
return resolve(MODULE_DIR, '..', 'hooks', 'write-gate.js').replaceAll('\\', '/');
|
|
164
200
|
}
|
|
165
201
|
/**
|
|
166
|
-
* Build the Write|Edit|MultiEdit matcher command.
|
|
167
|
-
* the candidate file path from argv[2] and exits 0 iff the path
|
|
168
|
-
* contains `.peaks/_runtime/` or `.peaks/_runtime/<sessionId>/` (the change-id
|
|
169
|
-
* segment is the next path component after `.peaks/`). All other
|
|
170
|
-
* paths exit 1 so the gate fires normally.
|
|
202
|
+
* Build the Write|Edit|MultiEdit matcher command.
|
|
171
203
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* peaks-code workflow needs more paths.
|
|
204
|
+
* TEMPLATE_VERSION 1.6.0: `node "<script>"` with NO inline payload. The
|
|
205
|
+
* decision lives in `src/services/hooks/write-gate.js` and was relocated
|
|
206
|
+
* there verbatim. Because there is nothing left to escape, this handler is
|
|
207
|
+
* shell-dialect-independent and can carry the same platform `shell` pin as
|
|
208
|
+
* its Bash siblings (see `resolveHookShell`).
|
|
178
209
|
*/
|
|
179
210
|
function buildWriteHookCommand() {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
return `node "${writeGateScriptPath()}"`;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* TEMPLATE_VERSION 1.4.0 — the SOP gate-enforce handler, read from the same
|
|
215
|
+
* canonical hook spec `peaks hooks install` uses. Deriving it here (rather
|
|
216
|
+
* than re-typing the literal) is what keeps the two writers of this file
|
|
217
|
+
* byte-compatible: `templateContentMatches` compares the `command` string,
|
|
218
|
+
* so any drift would make every `peaks workspace init` rewrite the file and
|
|
219
|
+
* drop whatever `peaks hooks install` had merged in.
|
|
220
|
+
*/
|
|
221
|
+
function buildGateEnforceHandler() {
|
|
222
|
+
const spec = resolveHookSpec('claude-code');
|
|
223
|
+
return {
|
|
224
|
+
type: 'command',
|
|
225
|
+
command: spec.hookEnforceCommand,
|
|
226
|
+
...(spec.hookEnforceShell !== undefined ? { shell: spec.hookEnforceShell } : {})
|
|
227
|
+
};
|
|
191
228
|
}
|
|
192
229
|
/**
|
|
193
230
|
* Build the full template object. The shape is the subset of Claude
|
|
@@ -202,7 +239,21 @@ function buildWriteHookCommand() {
|
|
|
202
239
|
* `.claude/settings.json` (not `.claude/settings.local.json`).
|
|
203
240
|
*/
|
|
204
241
|
export function buildClaudeSettingsLocalJson() {
|
|
242
|
+
// TEMPLATE_VERSION 1.6.0 — the write handler can now be shell-pinned for the
|
|
243
|
+
// same Windows reason as the two Bash handlers below: a shell-form command is
|
|
244
|
+
// executed by Git Bash / MSYS2, which force-allocates a console window on
|
|
245
|
+
// every matching tool call. It could NOT take the pin while its payload was
|
|
246
|
+
// inlined JavaScript, because PowerShell does not perform bash's backslash
|
|
247
|
+
// reduction and would have corrupted the payload. `undefined` on POSIX omits
|
|
248
|
+
// the key entirely.
|
|
249
|
+
const writeShell = resolveHookShell();
|
|
205
250
|
return {
|
|
251
|
+
// Slice emit-gateguard-exemption — the third-party gate exemption. Peaks
|
|
252
|
+
// already bypasses its OWN fact-forcing gate for `.peaks/**` (the
|
|
253
|
+
// Write|Edit|MultiEdit handler below); this is the same intent declared in
|
|
254
|
+
// the currency an external PreToolUse gate reads. `peaks hooks install`
|
|
255
|
+
// merges the same row into this file, so the two writers agree.
|
|
256
|
+
env: { ...EXTERNAL_GATE_EXEMPT_ENV },
|
|
206
257
|
hooks: {
|
|
207
258
|
PreToolUse: [
|
|
208
259
|
{
|
|
@@ -210,7 +261,8 @@ export function buildClaudeSettingsLocalJson() {
|
|
|
210
261
|
hooks: [
|
|
211
262
|
{
|
|
212
263
|
type: 'command',
|
|
213
|
-
command: buildWriteHookCommand()
|
|
264
|
+
command: buildWriteHookCommand(),
|
|
265
|
+
...(writeShell !== undefined ? { shell: writeShell } : {})
|
|
214
266
|
}
|
|
215
267
|
]
|
|
216
268
|
},
|
|
@@ -223,12 +275,22 @@ export function buildClaudeSettingsLocalJson() {
|
|
|
223
275
|
// 2 = block (stderr contains the BLOCKED: ... reason).
|
|
224
276
|
// The existing Write|Edit|MultiEdit matcher is preserved.
|
|
225
277
|
matcher: 'Bash',
|
|
226
|
-
hooks: [
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
278
|
+
hooks: [buildBashGateStep08Handler()]
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
// TEMPLATE_VERSION 1.4.0 — SOP gate enforcement. Lives in this
|
|
282
|
+
// machine-local file (not the committed settings.json) because
|
|
283
|
+
// `shell` is machine-specific; see the version history above and
|
|
284
|
+
// `resolveHookShell`.
|
|
285
|
+
//
|
|
286
|
+
// It sits in its own matcher group on purpose: a group counts as
|
|
287
|
+
// peaks-managed only when EVERY handler in it carries a peaks
|
|
288
|
+
// sentinel, and the `peaks code gate-step-08` handler above does
|
|
289
|
+
// not. Sharing a group with it would make the whole group
|
|
290
|
+
// unmanaged, so `peaks hooks install` would append a second Bash
|
|
291
|
+
// group and the gate would run twice per Bash call.
|
|
292
|
+
matcher: 'Bash',
|
|
293
|
+
hooks: [buildGateEnforceHandler()]
|
|
232
294
|
}
|
|
233
295
|
]
|
|
234
296
|
}
|
|
@@ -244,11 +306,21 @@ export function buildClaudeSettingsLocalJson() {
|
|
|
244
306
|
* re-running `peaks workspace init` on a project that already has the
|
|
245
307
|
* Bash hook is a no-op (already-current).
|
|
246
308
|
*/
|
|
247
|
-
function
|
|
309
|
+
function buildBashGateStep08Handler() {
|
|
248
310
|
// The hook receives the tool call on stdin. We ignore stdin and
|
|
249
311
|
// delegate entirely to `peaks code gate-step-08`, which reads
|
|
250
312
|
// .peaks/_runtime/<sessionId>/job-shape.json and last-prompt.txt.
|
|
251
313
|
// `${CLAUDE_PROJECT_DIR}` resolves to the consumer project's root
|
|
252
314
|
// (Claude Code's standard convention).
|
|
253
|
-
|
|
315
|
+
//
|
|
316
|
+
// TEMPLATE_VERSION 1.5.0 — shell-pinned on Windows for the same reason
|
|
317
|
+
// as the gate-enforce handler below: this runs on the same `Bash`
|
|
318
|
+
// matcher, so a shell-form command is executed by Git Bash / MSYS2,
|
|
319
|
+
// which force-allocates a console window on every Bash tool call.
|
|
320
|
+
const shell = resolveHookShell();
|
|
321
|
+
return {
|
|
322
|
+
type: 'command',
|
|
323
|
+
command: 'peaks code gate-step-08 --project "${CLAUDE_PROJECT_DIR}"',
|
|
324
|
+
...(shell !== undefined ? { shell } : {})
|
|
325
|
+
};
|
|
254
326
|
}
|
|
@@ -9,10 +9,37 @@
|
|
|
9
9
|
* the parent module and calls into this sibling. Function signatures
|
|
10
10
|
* and behaviour are unchanged (verbatim move).
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync } from 'node:fs';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
13
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
+
import { withExternalGateExemptions } from '../skills/hooks-codegate-superpowers.js';
|
|
15
16
|
import { buildClaudeSettingsLocalJson, CLAUDE_SETTINGS_LOCAL_FILENAME, templateContentMatches } from './claude-settings-template.js';
|
|
17
|
+
/** Read a file as text, or `undefined` when it cannot be read. */
|
|
18
|
+
function readTextIfPresent(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
return readFileSync(filePath, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The `env` object of a serialized settings file, or `undefined` when the file
|
|
28
|
+
* is malformed or has no `env` object. Tolerant on purpose: a bad on-disk file
|
|
29
|
+
* must not stop the materialization.
|
|
30
|
+
*/
|
|
31
|
+
function readEnvObject(serialized) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(serialized);
|
|
34
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
35
|
+
return undefined;
|
|
36
|
+
const env = parsed.env;
|
|
37
|
+
return typeof env === 'object' && env !== null && !Array.isArray(env) ? env : undefined;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
16
43
|
/**
|
|
17
44
|
* The peaks-managed snippet appended to the consumer project's
|
|
18
45
|
* `.peaks/.gitignore` so the local-only settings file never lands
|
|
@@ -66,7 +93,15 @@ export async function materializeClaudeSettingsLocal(projectRoot, noClaudeHooks)
|
|
|
66
93
|
const settingsRel = CLAUDE_SETTINGS_LOCAL_FILENAME;
|
|
67
94
|
const settingsPath = join(projectRoot, settingsRel);
|
|
68
95
|
const template = buildClaudeSettingsLocalJson();
|
|
69
|
-
const
|
|
96
|
+
const fileExists = existsSync(settingsPath);
|
|
97
|
+
const existing = fileExists ? readTextIfPresent(settingsPath) : undefined;
|
|
98
|
+
// `.claude/settings.local.json` has a second writer: `peaks hooks install`
|
|
99
|
+
// unions the user's own exemption globs into `env`. Carry that value across
|
|
100
|
+
// the rewrite this function is about to do, or a refresh would silently
|
|
101
|
+
// drop someone else's exemptions. The template's own row is added on top, so
|
|
102
|
+
// the result is a union either way.
|
|
103
|
+
const onDiskEnv = existing === undefined ? undefined : readEnvObject(existing);
|
|
104
|
+
const serialized = JSON.stringify(withExternalGateExemptions(onDiskEnv === undefined ? template : { ...template, env: onDiskEnv }), null, 2) + '\n';
|
|
70
105
|
// Always drop (or self-heal) a copy of the template under .peaks/
|
|
71
106
|
// so the --no-claude-hooks recovery flow has a known source-of-truth
|
|
72
107
|
// on disk. The file is gitignored by the snippet below.
|
|
@@ -84,23 +119,15 @@ export async function materializeClaudeSettingsLocal(projectRoot, noClaudeHooks)
|
|
|
84
119
|
// hooks-settings-service applies the safety check for the Bash
|
|
85
120
|
// gate-enforce path).
|
|
86
121
|
await mkdir(join(projectRoot, '.claude'), { recursive: true });
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
action = 'refreshed';
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
// Treat any read failure as "needs refresh" so the consumer
|
|
101
|
-
// always ends up with a valid template on disk.
|
|
102
|
-
action = 'refreshed';
|
|
103
|
-
}
|
|
122
|
+
// An existing-but-unreadable file is treated as drifted, so the consumer
|
|
123
|
+
// always ends up with a valid template on disk.
|
|
124
|
+
let action = fileExists ? 'refreshed' : 'written';
|
|
125
|
+
// Structural comparison (not a byte comparison): `peaks hooks install` also
|
|
126
|
+
// writes this file, through a different serializer, so an equal hooks tree
|
|
127
|
+
// must be recognized as current or every init would rewrite the file and
|
|
128
|
+
// drop the installer's entries.
|
|
129
|
+
if (existing !== undefined && templateContentMatches(serialized, existing)) {
|
|
130
|
+
action = 'already-current';
|
|
104
131
|
}
|
|
105
132
|
if (action !== 'already-current') {
|
|
106
133
|
await writeFile(settingsPath, serialized, 'utf8');
|
|
@@ -3,7 +3,31 @@ import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { isDirectory } from 'peaks-loop-shared/fs';
|
|
5
5
|
import { getSessionIdCanonical, setCurrentSessionBinding, setSessionMeta } from '../session/session-manager.js';
|
|
6
|
+
import { updateCallerBindingSessionId } from '../session/caller-binding-service.js';
|
|
7
|
+
import { resolveCallerProjection } from '../session/resolve-caller-id.js';
|
|
6
8
|
import { normalizePath } from '../../shared/path-utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Slice 2026-09-10 (rid=rebind-must-update-caller-binding): repoint the
|
|
11
|
+
* calling process's own per-caller binding at the session id this init just
|
|
12
|
+
* bound. `getSessionIdCanonical` reads `callers/<callerId>.json` FIRST, so a
|
|
13
|
+
* binding left behind by an earlier session shadows every explicit rebind.
|
|
14
|
+
*
|
|
15
|
+
* Best-effort: an unresolvable callerId (`PEAKS_CALLER_NOT_RESOLVED`, e.g. a
|
|
16
|
+
* stock shell with no IDE adapter) means there is no binding file to
|
|
17
|
+
* repoint, and `session.json` alone answers both resolvers.
|
|
18
|
+
*
|
|
19
|
+
* @returns `true` when a binding file existed and was repointed.
|
|
20
|
+
*/
|
|
21
|
+
function rebindCurrentCallerBinding(projectRoot, sessionId) {
|
|
22
|
+
let callerId;
|
|
23
|
+
try {
|
|
24
|
+
callerId = resolveCallerProjection({ projectRoot, env: process.env }).callerId;
|
|
25
|
+
}
|
|
26
|
+
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return updateCallerBindingSessionId(projectRoot, callerId, sessionId);
|
|
30
|
+
}
|
|
7
31
|
/**
|
|
8
32
|
* Slice 2026-06-29-change-id-root-removal: list the immediate children of
|
|
9
33
|
* `.peaks/` so the legacy sibling-dir guard can enumerate date-stamped
|
|
@@ -299,6 +323,15 @@ export async function initWorkspace(options) {
|
|
|
299
323
|
// Either: existing session dir is empty (true leftover, no user data),
|
|
300
324
|
// or the caller explicitly authorised a rebind. Overwrite.
|
|
301
325
|
setCurrentSessionBinding(options.projectRoot, options.sessionId);
|
|
326
|
+
// Slice 2026-09-10 (rid=rebind-must-update-caller-binding): an
|
|
327
|
+
// explicit rebind must ALSO repoint this caller's per-caller binding.
|
|
328
|
+
// `getSessionIdCanonical` prefers `callers/<callerId>.json`, so leaving
|
|
329
|
+
// it untouched shadowed the rebind for every command resolving through
|
|
330
|
+
// it (session checkpoint / 24h-mode wrote into the stale session dir)
|
|
331
|
+
// while `getCurrentSessionId` (session.json) reported the new one.
|
|
332
|
+
// Only THIS caller's binding is repointed — a second caller keeps its
|
|
333
|
+
// own session by design.
|
|
334
|
+
rebindCurrentCallerBinding(options.projectRoot, options.sessionId);
|
|
302
335
|
bound = true;
|
|
303
336
|
}
|
|
304
337
|
// Slice 2026-06-16-peaks-code-auto-scaffold (RD#7):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.38",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
"fzf": "^0.5.2",
|
|
102
102
|
"yaml": "^2.9.0",
|
|
103
103
|
"zod": "^4.4.3",
|
|
104
|
-
"peaks-loop-internal-runtime": "0.0.
|
|
105
|
-
"peaks-loop-shared
|
|
106
|
-
"peaks-loop-
|
|
107
|
-
"peaks-loop-
|
|
104
|
+
"peaks-loop-internal-runtime": "0.0.23",
|
|
105
|
+
"peaks-loop-shared": "0.0.72",
|
|
106
|
+
"peaks-loop-mut": "0.1.36",
|
|
107
|
+
"peaks-loop-shared-channel": "0.0.40"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
110
|
"@changesets/cli": "2.31.1",
|
|
@@ -60,6 +60,18 @@ const targets = [
|
|
|
60
60
|
src: join(packageRoot, 'src/services/skillhub/migrations'),
|
|
61
61
|
dest: join(packageRoot, 'dist/services/skillhub/migrations'),
|
|
62
62
|
extensions: ['.sql']
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
// Slice c5-write-hook-exec-form: the Write|Edit|MultiEdit PreToolUse gate
|
|
66
|
+
// emitted by `peaks workspace init` is invoked as `node <path>`, and the
|
|
67
|
+
// path is resolved relative to this module — `dist/services/hooks/` in an
|
|
68
|
+
// installed consumer, `src/services/hooks/` under tsx. A plain `.js` asset
|
|
69
|
+
// (not a `.ts` compiled by tsc) is what lets ONE relative filename be valid
|
|
70
|
+
// in both trees. Without this copy the hook would be a broken path in every
|
|
71
|
+
// installed consumer while every test in this repo still passed.
|
|
72
|
+
src: join(packageRoot, 'src/services/hooks'),
|
|
73
|
+
dest: join(packageRoot, 'dist/services/hooks'),
|
|
74
|
+
extensions: ['.js']
|
|
63
75
|
}
|
|
64
76
|
];
|
|
65
77
|
|
package/scripts/sync-version.mjs
CHANGED
|
@@ -18,6 +18,26 @@ writeFileSync(
|
|
|
18
18
|
`export const CLI_VERSION = ${JSON.stringify(version)};\n`,
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
+
// Slice 2026-09-11 (runtime-version-lockstep) — sync RUNTIME_VERSION.
|
|
22
|
+
// `packages/peaks-loop-internal-runtime/src/index.ts` declares
|
|
23
|
+
// `RUNTIME_VERSION` under a comment stating it tracks the peaks-loop root
|
|
24
|
+
// version, but nothing wrote it: v4.0.37 was tagged with the constant still
|
|
25
|
+
// at 4.0.36 and publish.yml's gate-cli-version step aborted before npm
|
|
26
|
+
// publish. The literal is replaced in place, single-quoted — the exact
|
|
27
|
+
// shape the gate greps — because the file also holds the package's public
|
|
28
|
+
// exports and must never be regenerated wholesale. A literal we cannot
|
|
29
|
+
// find throws instead of no-op'ing: that silence is how the drift reached CI.
|
|
30
|
+
const runtimeIndexPath = resolve('packages/peaks-loop-internal-runtime/src/index.ts');
|
|
31
|
+
const runtimeIndex = readFileSync(runtimeIndexPath, 'utf8');
|
|
32
|
+
const runtimeDecl = /(export const RUNTIME_VERSION = ')[^']*(';)/;
|
|
33
|
+
if (!runtimeDecl.test(runtimeIndex)) {
|
|
34
|
+
throw new Error(`${runtimeIndexPath}: could not find "export const RUNTIME_VERSION = '...';"`);
|
|
35
|
+
}
|
|
36
|
+
const syncedRuntimeIndex = runtimeIndex.replace(runtimeDecl, `$1${version}$2`);
|
|
37
|
+
if (syncedRuntimeIndex !== runtimeIndex) {
|
|
38
|
+
writeFileSync(runtimeIndexPath, syncedRuntimeIndex);
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
// 2026-07-23 follow-up (peaks-publish-stale fix, AC6): the shared
|
|
22
42
|
// bump used to live here, gated on `PEAKS_AUTO_BUMP_SHARED === '1'`.
|
|
23
43
|
// That gate was the Layer 2 root cause: publish.yml set the env on
|
|
@@ -235,6 +235,16 @@ triggered for this intent.
|
|
|
235
235
|
5. LLM picks one recommendation (★ marker) with reasoning block.
|
|
236
236
|
6. **Mandatory ⚠️ catch gate** — user explicitly acks / picks alt / rejects + reason.
|
|
237
237
|
|
|
238
|
+
**When the scan cannot be performed, the sub-step is SKIPPED — never fabricated.** The lookups behind
|
|
239
|
+
this scan are stubs in the current build: they return synthetic fragments marked internally as
|
|
240
|
+
synthetic. When the scan runs on a synthetic result it **refuses** — `ok:false`,
|
|
241
|
+
`BEST_PRACTICE_SCAN_SYNTHETIC_LOOKUP`, exit 1 — and prints no recommendation, no comparison table and
|
|
242
|
+
no ⚠️ catch gate, because there is nothing real for the user to judge. Record the sub-step as
|
|
243
|
+
**skipped with reason `synthetic-lookup`** and carry on; do not synthesise a recommendation to satisfy
|
|
244
|
+
the gate. `--intent <text>` is required (the business goal, not the project path) — omitting it is
|
|
245
|
+
`BEST_PRACTICE_SCAN_INTENT_REQUIRED`, also exit 1. A real scan renders the full table and the gate
|
|
246
|
+
normally; wiring the real Context7 / WebSearch lookup is a future slice.
|
|
247
|
+
|
|
238
248
|
**Out of scope for this sub-step:**
|
|
239
249
|
- ❌ No changes to RD dispatch Karpathy prose (Step 3 unchanged)
|
|
240
250
|
- ❌ No changes to QA / SC / TXT skills
|
|
@@ -105,7 +105,7 @@ If the page redirects to a login challenge:
|
|
|
105
105
|
|
|
106
106
|
## Sensitive data sanitization
|
|
107
107
|
|
|
108
|
-
Never persist any of the following in `.peaks/_runtime/<session-id>/**` artifacts:
|
|
108
|
+
**Default: never persist.** Never persist any of the following in `.peaks/_runtime/<session-id>/**` artifacts:
|
|
109
109
|
|
|
110
110
|
- Login URLs, redirect URLs, OAuth callback URLs containing tokens or state.
|
|
111
111
|
- Cookies, request or response headers, session tokens, storage state, QR payloads.
|
|
@@ -113,6 +113,15 @@ Never persist any of the following in `.peaks/_runtime/<session-id>/**` artifact
|
|
|
113
113
|
- Raw browser state, browser traces.
|
|
114
114
|
- Screenshots or logs containing PII, SSO challenge content, or MFA material.
|
|
115
115
|
|
|
116
|
+
**The one exception, and it is narrow.** `peaks web login --profile <name>` persists a Playwright `storageState.json` to `~/.peaks/web-profiles/<name>/`. It is allowed only when the user explicitly asks for a persistent login. An LLM must never choose it on its own initiative, and it is never part of a default workflow. That name at that path is the whole of the exception: **no other artifact may hold these values, anywhere** — not under `.peaks/_runtime/`, not elsewhere in the project tree, not elsewhere under the user's home. This file is written outside the project tree and outside git; that is a property of this one path, not a licence to persist these values wherever the project tree happens to end.
|
|
117
|
+
|
|
118
|
+
**Two named carve-outs, so the rule above can be read literally.**
|
|
119
|
+
|
|
120
|
+
1. **The staging file.** The state is published atomically, which means writing it to a staging file **in the same profile directory** (`0600`) and renaming it into place. The staging name is **per-run** (`storageState.json.<pid>.staging`) so two concurrent logins cannot overwrite each other's in-flight bytes — the rule sanctions *a* staging file, and that is the only form it takes. It is sanctioned **only** while a publish is in flight: it must be deleted on every failure path, must never outlive the command that created it, and nothing may ever read it as a profile. A single non-atomic write is **not** an acceptable alternative — it truncates the previous session before writing the new one, so a failed write destroys a working login and the caller is told nothing happened.
|
|
121
|
+
2. **The browser's own profile directory.** A headed Chromium writes its live session cookies to an **ephemeral profile directory the browser manages itself** (under the OS temp directory for the duration of the session, normally removed on close). This is not something a login flow can prevent and it is not what this rule is about — but it is a place those values are held, so it is named here rather than left implied. Launching a headed browser to log in is therefore **never zero-exposure**, and anyone weighing the exception above should weigh that too.
|
|
122
|
+
|
|
123
|
+
**The risk, stated plainly.** That file holds live session cookies and tokens for the sites that were logged into. Any process running as this user can read it. It does not expire with the browser session, and it survives until it is deleted. Deleting `~/.peaks/web-profiles/<name>/` removes what the tool stored — it does not revoke the session at the site, so log out there as well if the account matters. Only persist a login for an account that may safely stay logged in on this machine.
|
|
124
|
+
|
|
116
125
|
Redact sensitive values before retention. Store evidence as sanitized observations (e.g., "user reached settings page; first 3 list items had a missing-image regression") rather than raw captures.
|
|
117
126
|
|
|
118
127
|
## Fallback when Playwright MCP is not installed
|