peaks-loop 4.0.36 → 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.
Files changed (88) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/bin/peaks.js +71 -1
  5. package/dist/cli/cli-helpers.js +7 -0
  6. package/dist/cli/commands/_register.js +2 -0
  7. package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
  8. package/dist/cli/commands/best-practice-scan-command.js +67 -9
  9. package/dist/cli/commands/code-runtime-commands.js +21 -5
  10. package/dist/cli/commands/hooks-commands.js +10 -1
  11. package/dist/cli/commands/job-commands.js +107 -25
  12. package/dist/cli/commands/scan-commands.js +1 -1
  13. package/dist/cli/commands/web-commands.d.ts +28 -0
  14. package/dist/cli/commands/web-commands.js +327 -0
  15. package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
  16. package/dist/cli/commands/web-lifecycle-commands.js +321 -0
  17. package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
  18. package/dist/services/best-practice/scan-orchestrator.js +14 -5
  19. package/dist/services/code/orchestrator-can-do.js +27 -4
  20. package/dist/services/context/context-audit-hint.d.ts +79 -0
  21. package/dist/services/context/context-audit-hint.js +150 -0
  22. package/dist/services/hooks/auto-compact-hook-install.js +10 -1
  23. package/dist/services/hooks/write-gate.js +88 -0
  24. package/dist/services/lint/detect-eslint.d.ts +2 -0
  25. package/dist/services/lint/detect-eslint.js +23 -9
  26. package/dist/services/lint/npx-resolver.d.ts +6 -0
  27. package/dist/services/lint/npx-resolver.js +38 -14
  28. package/dist/services/release/version-precheck-service.js +9 -2
  29. package/dist/services/scan/file-size-scan.d.ts +29 -0
  30. package/dist/services/scan/file-size-scan.js +63 -0
  31. package/dist/services/session/caller-binding-service.d.ts +24 -0
  32. package/dist/services/session/caller-binding-service.js +34 -0
  33. package/dist/services/session/getSessionDir.js +15 -10
  34. package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
  35. package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
  36. package/dist/services/skills/hooks-settings-service.d.ts +10 -0
  37. package/dist/services/skills/hooks-settings-service.js +152 -61
  38. package/dist/services/slice/slice-check-service.d.ts +14 -0
  39. package/dist/services/slice/slice-check-service.js +110 -50
  40. package/dist/services/slice/slice-check-types.d.ts +12 -7
  41. package/dist/services/slice/slice-check-types.js +8 -3
  42. package/dist/services/slice/slice-decompose-runners.js +24 -21
  43. package/dist/services/sop/sop-check-service.js +12 -1
  44. package/dist/services/web/bounded-output.d.ts +34 -0
  45. package/dist/services/web/bounded-output.js +68 -0
  46. package/dist/services/web/browser-acquire.d.ts +14 -0
  47. package/dist/services/web/browser-acquire.js +84 -0
  48. package/dist/services/web/browser-session-manager.d.ts +111 -0
  49. package/dist/services/web/browser-session-manager.js +413 -0
  50. package/dist/services/web/daemon-entry.d.ts +1 -0
  51. package/dist/services/web/daemon-entry.js +65 -0
  52. package/dist/services/web/daemon-registry.d.ts +42 -0
  53. package/dist/services/web/daemon-registry.js +164 -0
  54. package/dist/services/web/daemon-supervisor.d.ts +144 -0
  55. package/dist/services/web/daemon-supervisor.js +455 -0
  56. package/dist/services/web/playwright-loader.d.ts +89 -0
  57. package/dist/services/web/playwright-loader.js +253 -0
  58. package/dist/services/web/snapshot-pruner.d.ts +48 -0
  59. package/dist/services/web/snapshot-pruner.js +241 -0
  60. package/dist/services/web/untrusted-envelope.d.ts +27 -0
  61. package/dist/services/web/untrusted-envelope.js +44 -0
  62. package/dist/services/web/web-artifact-paths.d.ts +79 -0
  63. package/dist/services/web/web-artifact-paths.js +163 -0
  64. package/dist/services/web/web-client.d.ts +19 -0
  65. package/dist/services/web/web-client.js +55 -0
  66. package/dist/services/web/web-daemon-service.d.ts +38 -0
  67. package/dist/services/web/web-daemon-service.js +416 -0
  68. package/dist/services/web/web-fallback.d.ts +70 -0
  69. package/dist/services/web/web-fallback.js +121 -0
  70. package/dist/services/web/web-install-service.d.ts +91 -0
  71. package/dist/services/web/web-install-service.js +346 -0
  72. package/dist/services/web/web-login-profile.d.ts +89 -0
  73. package/dist/services/web/web-login-profile.js +612 -0
  74. package/dist/services/web/web-login-staging.d.ts +27 -0
  75. package/dist/services/web/web-login-staging.js +173 -0
  76. package/dist/services/web/web-protocol.d.ts +58 -0
  77. package/dist/services/web/web-protocol.js +58 -0
  78. package/dist/services/web/web-status-report.d.ts +33 -0
  79. package/dist/services/web/web-status-report.js +47 -0
  80. package/dist/services/workspace/claude-settings-template.d.ts +41 -5
  81. package/dist/services/workspace/claude-settings-template.js +116 -64
  82. package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
  83. package/dist/services/workspace/workspace-service.js +33 -0
  84. package/package.json +5 -5
  85. package/scripts/copy-templates.mjs +12 -0
  86. package/scripts/sync-version.mjs +20 -0
  87. package/skills/peaks-code/SKILL.md +10 -0
  88. 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 one-liner that path-matches
18
- * `.peaks/_runtime/` and `.peaks/_runtime/<sessionId>/`. Exits 0 (allow)
19
- * for those paths, non-zero (deny → fall through to gate) 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 { 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,8 +60,26 @@ 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.
58
81
  */
59
- export const TEMPLATE_VERSION = '1.3.0';
82
+ export const TEMPLATE_VERSION = '1.6.0';
60
83
  /**
61
84
  * Compare two serialized template strings for semantic equivalence.
62
85
  *
@@ -124,70 +147,70 @@ function sameHooksArray(a, b) {
124
147
  for (let i = 0; i < a.length; i += 1) {
125
148
  const ha = a[i];
126
149
  const hb = b[i];
127
- if (ha.type !== hb.type || ha.command !== hb.command) {
150
+ // `shell` participates in the comparison: it is machine-specific (see
151
+ // `resolveHookShell`), so a file written on one platform must be
152
+ // recognized as drifted on the other instead of silently kept.
153
+ if (ha.type !== hb.type || ha.command !== hb.command || ha.shell !== hb.shell) {
128
154
  return false;
129
155
  }
130
156
  }
131
157
  return true;
132
158
  }
133
159
  /**
134
- * Wrap an inner JavaScript payload as a shell-evaluable `node -e "..."`
135
- * one-liner. The returned string is what Claude Code writes verbatim
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.
160
+ * This module's own directory — `<root>/src/services/workspace` in the
161
+ * source tree, `<root>/dist/services/workspace` in a build.
141
162
  *
142
- * Every `"` character in the inner JS must be JSON-escaped as `\\"`
143
- * so that the surrounding wrapper `node -e "..."` parses correctly:
144
- * the shell sees the escape and passes a literal `"` to Node. A
145
- * single missed escape closes the wrapper early and the entire hook
146
- * regresses to the bash-syntax-error class of bug.
163
+ * Anchored on the running module rather than on `process.argv[1]`: the
164
+ * same reason `daemon-supervisor.ts` documents — `argv[1]` is a different
165
+ * file in each way the CLI is entered (`bin/peaks.js`, `src/cli/index.ts`
166
+ * under tsx, `dist/cli/index.js` when invoked directly), whereas the
167
+ * module's own location is the one fact that is always true.
168
+ */
169
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
170
+ /**
171
+ * Absolute path of the shipped Write|Edit|MultiEdit gate script.
172
+ *
173
+ * `write-gate.js` is a plain `.js` (not a compiled `.ts`) precisely so this
174
+ * single relative filename resolves in BOTH trees: `src/services/hooks/` for
175
+ * `tsx` / vitest, `dist/services/hooks/` for an installed consumer (copied by
176
+ * `scripts/copy-templates.mjs`; the `dist/**` `*.js` glob in
177
+ * `package.json#files` already ships it).
147
178
  *
148
- * @param js Inner JavaScript payload. Must be a single statement or a
149
- * sequence of statements joined with `;`. The wrapper does
150
- * not insert any `;` between the payload and the closing
151
- * `"` because Node accepts a trailing expression with `;`
152
- * already terminated by the payload itself.
179
+ * Separators are normalized to `/` so the emitted command contains no
180
+ * backslash at all. That is what makes the handler shell-agnostic: bash
181
+ * reduces `\X` inside `"..."` and PowerShell escapes with a backtick, so any
182
+ * backslash in the string is one dialect's problem waiting to happen.
153
183
  */
154
- function wrapAsNodeOneLiner(js) {
155
- // Only `"` needs JSON-escaping: the wrapper uses double quotes, so an
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}"`;
184
+ export function writeGateScriptPath() {
185
+ return resolve(MODULE_DIR, '..', 'hooks', 'write-gate.js').replaceAll('\\', '/');
164
186
  }
165
187
  /**
166
- * Build the Write|Edit|MultiEdit matcher command. The command reads
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.
188
+ * Build the Write|Edit|MultiEdit matcher command.
171
189
  *
172
- * The matcher is intentionally narrow: it only fires for tools that
173
- * take a `file_path` (Write/Edit/MultiEdit) and for the Bash
174
- * subcommand allow-list. It does NOT silently allow arbitrary paths
175
- * under `.peaks/_runtime/<sessionId>/` — only those matching the documented
176
- * pattern. Future slice work can broaden the allow-list if the
177
- * peaks-code workflow needs more paths.
190
+ * TEMPLATE_VERSION 1.6.0: `node "<script>"` with NO inline payload. The
191
+ * decision lives in `src/services/hooks/write-gate.js` and was relocated
192
+ * there verbatim. Because there is nothing left to escape, this handler is
193
+ * shell-dialect-independent and can carry the same platform `shell` pin as
194
+ * its Bash siblings (see `resolveHookShell`).
178
195
  */
179
196
  function buildWriteHookCommand() {
180
- // Path-matching: allow when the path contains `.peaks/_runtime/`
181
- // OR when the second `.peaks/` segment starts with anything that
182
- // looks like a change-id (kebab-case slug). Exit 0 for allow, exit
183
- // 1 for deny. The candidate path arrives on `process.argv[1]` per
184
- // Node.js argv layout under `-e` (cross-platform consistent).
185
- const js = 'const p=process.argv[1]||"";' +
186
- 'if(p.includes(".peaks/_runtime/"))process.exit(0);' +
187
- 'const m=p.match(/\\.peaks\\/([a-z0-9][a-z0-9.-]*)\\//);' +
188
- 'if(m&&m[1]&&m[1]!=="_runtime"&&m[1]!=="_dogfood"&&m[1]!=="_sub_agents"&&m[1]!=="memory"&&m[1]!=="sops"&&m[1]!=="retrospective"&&m[1]!=="project-scan"&&m[1]!=="perf-baseline")process.exit(0);' +
189
- 'process.exit(1)';
190
- return wrapAsNodeOneLiner(js);
197
+ return `node "${writeGateScriptPath()}"`;
198
+ }
199
+ /**
200
+ * TEMPLATE_VERSION 1.4.0 — the SOP gate-enforce handler, read from the same
201
+ * canonical hook spec `peaks hooks install` uses. Deriving it here (rather
202
+ * than re-typing the literal) is what keeps the two writers of this file
203
+ * byte-compatible: `templateContentMatches` compares the `command` string,
204
+ * so any drift would make every `peaks workspace init` rewrite the file and
205
+ * drop whatever `peaks hooks install` had merged in.
206
+ */
207
+ function buildGateEnforceHandler() {
208
+ const spec = resolveHookSpec('claude-code');
209
+ return {
210
+ type: 'command',
211
+ command: spec.hookEnforceCommand,
212
+ ...(spec.hookEnforceShell !== undefined ? { shell: spec.hookEnforceShell } : {})
213
+ };
191
214
  }
192
215
  /**
193
216
  * Build the full template object. The shape is the subset of Claude
@@ -202,6 +225,14 @@ function buildWriteHookCommand() {
202
225
  * `.claude/settings.json` (not `.claude/settings.local.json`).
203
226
  */
204
227
  export function buildClaudeSettingsLocalJson() {
228
+ // TEMPLATE_VERSION 1.6.0 — the write handler can now be shell-pinned for the
229
+ // same Windows reason as the two Bash handlers below: a shell-form command is
230
+ // executed by Git Bash / MSYS2, which force-allocates a console window on
231
+ // every matching tool call. It could NOT take the pin while its payload was
232
+ // inlined JavaScript, because PowerShell does not perform bash's backslash
233
+ // reduction and would have corrupted the payload. `undefined` on POSIX omits
234
+ // the key entirely.
235
+ const writeShell = resolveHookShell();
205
236
  return {
206
237
  hooks: {
207
238
  PreToolUse: [
@@ -210,7 +241,8 @@ export function buildClaudeSettingsLocalJson() {
210
241
  hooks: [
211
242
  {
212
243
  type: 'command',
213
- command: buildWriteHookCommand()
244
+ command: buildWriteHookCommand(),
245
+ ...(writeShell !== undefined ? { shell: writeShell } : {})
214
246
  }
215
247
  ]
216
248
  },
@@ -223,12 +255,22 @@ export function buildClaudeSettingsLocalJson() {
223
255
  // 2 = block (stderr contains the BLOCKED: ... reason).
224
256
  // The existing Write|Edit|MultiEdit matcher is preserved.
225
257
  matcher: 'Bash',
226
- hooks: [
227
- {
228
- type: 'command',
229
- command: buildBashGateStep08Command()
230
- }
231
- ]
258
+ hooks: [buildBashGateStep08Handler()]
259
+ },
260
+ {
261
+ // TEMPLATE_VERSION 1.4.0 — SOP gate enforcement. Lives in this
262
+ // machine-local file (not the committed settings.json) because
263
+ // `shell` is machine-specific; see the version history above and
264
+ // `resolveHookShell`.
265
+ //
266
+ // It sits in its own matcher group on purpose: a group counts as
267
+ // peaks-managed only when EVERY handler in it carries a peaks
268
+ // sentinel, and the `peaks code gate-step-08` handler above does
269
+ // not. Sharing a group with it would make the whole group
270
+ // unmanaged, so `peaks hooks install` would append a second Bash
271
+ // group and the gate would run twice per Bash call.
272
+ matcher: 'Bash',
273
+ hooks: [buildGateEnforceHandler()]
232
274
  }
233
275
  ]
234
276
  }
@@ -244,11 +286,21 @@ export function buildClaudeSettingsLocalJson() {
244
286
  * re-running `peaks workspace init` on a project that already has the
245
287
  * Bash hook is a no-op (already-current).
246
288
  */
247
- function buildBashGateStep08Command() {
289
+ function buildBashGateStep08Handler() {
248
290
  // The hook receives the tool call on stdin. We ignore stdin and
249
291
  // delegate entirely to `peaks code gate-step-08`, which reads
250
292
  // .peaks/_runtime/<sessionId>/job-shape.json and last-prompt.txt.
251
293
  // `${CLAUDE_PROJECT_DIR}` resolves to the consumer project's root
252
294
  // (Claude Code's standard convention).
253
- return 'peaks code gate-step-08 --project "${CLAUDE_PROJECT_DIR}"';
295
+ //
296
+ // TEMPLATE_VERSION 1.5.0 — shell-pinned on Windows for the same reason
297
+ // as the gate-enforce handler below: this runs on the same `Bash`
298
+ // matcher, so a shell-form command is executed by Git Bash / MSYS2,
299
+ // which force-allocates a console window on every Bash tool call.
300
+ const shell = resolveHookShell();
301
+ return {
302
+ type: 'command',
303
+ command: 'peaks code gate-step-08 --project "${CLAUDE_PROJECT_DIR}"',
304
+ ...(shell !== undefined ? { shell } : {})
305
+ };
254
306
  }
@@ -89,7 +89,11 @@ export async function materializeClaudeSettingsLocal(projectRoot, noClaudeHooks)
89
89
  try {
90
90
  const { readFile } = await import('node:fs/promises');
91
91
  const existing = await readFile(settingsPath, 'utf8');
92
- if (existing === serialized) {
92
+ // Structural comparison (not a byte comparison): `peaks hooks
93
+ // install` also writes this file, through a different serializer, so
94
+ // an equal hooks tree must be recognized as current or every init
95
+ // would rewrite the file and drop the installer's entries.
96
+ if (templateContentMatches(serialized, existing)) {
93
97
  action = 'already-current';
94
98
  }
95
99
  else {
@@ -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.36",
3
+ "version": "4.0.37",
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.21",
105
- "peaks-loop-shared-channel": "0.0.38",
106
- "peaks-loop-shared": "0.0.70",
107
- "peaks-loop-mut": "0.1.34"
104
+ "peaks-loop-mut": "0.1.35",
105
+ "peaks-loop-internal-runtime": "0.0.22",
106
+ "peaks-loop-shared-channel": "0.0.39",
107
+ "peaks-loop-shared": "0.0.71"
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
 
@@ -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