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.
Files changed (128) hide show
  1. package/CHANGELOG.md +40 -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.d.ts +5 -2
  10. package/dist/cli/commands/code-runtime-commands.js +78 -7
  11. package/dist/cli/commands/core/doctor-command.d.ts +8 -0
  12. package/dist/cli/commands/core/doctor-command.js +44 -2
  13. package/dist/cli/commands/core/memory-command.js +5 -1
  14. package/dist/cli/commands/dispatch-commands.js +15 -3
  15. package/dist/cli/commands/dispatch-from-dag.js +17 -0
  16. package/dist/cli/commands/hooks-commands.js +10 -1
  17. package/dist/cli/commands/job-commands.js +107 -25
  18. package/dist/cli/commands/memory-commands.d.ts +24 -0
  19. package/dist/cli/commands/memory-commands.js +77 -10
  20. package/dist/cli/commands/request-commands.d.ts +8 -0
  21. package/dist/cli/commands/request-commands.js +23 -2
  22. package/dist/cli/commands/scan-commands.js +1 -1
  23. package/dist/cli/commands/sub-agent-commands.js +2 -0
  24. package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
  25. package/dist/cli/commands/wave-plan-commands.js +93 -0
  26. package/dist/cli/commands/web-commands.d.ts +28 -0
  27. package/dist/cli/commands/web-commands.js +327 -0
  28. package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
  29. package/dist/cli/commands/web-lifecycle-commands.js +321 -0
  30. package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
  31. package/dist/services/best-practice/scan-orchestrator.js +14 -5
  32. package/dist/services/code/orchestrator-can-do.js +27 -4
  33. package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
  34. package/dist/services/context/build-dispatch-system-prompt.js +132 -17
  35. package/dist/services/context/context-audit-hint.d.ts +79 -0
  36. package/dist/services/context/context-audit-hint.js +150 -0
  37. package/dist/services/context/context-audit.d.ts +100 -0
  38. package/dist/services/context/context-audit.js +322 -0
  39. package/dist/services/context/summary-view.d.ts +54 -0
  40. package/dist/services/context/summary-view.js +114 -0
  41. package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
  42. package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
  43. package/dist/services/dispatch/session-capsule.d.ts +23 -0
  44. package/dist/services/dispatch/session-capsule.js +56 -0
  45. package/dist/services/dispatch/slice-dag.d.ts +9 -0
  46. package/dist/services/dispatch/slice-dag.js +9 -1
  47. package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
  48. package/dist/services/dispatch/test-tool-detection.js +14 -13
  49. package/dist/services/hooks/auto-compact-hook-install.js +10 -1
  50. package/dist/services/hooks/write-gate.js +88 -0
  51. package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
  52. package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
  53. package/dist/services/ide/ide-types.d.ts +15 -0
  54. package/dist/services/lint/detect-eslint.d.ts +2 -0
  55. package/dist/services/lint/detect-eslint.js +23 -9
  56. package/dist/services/lint/npx-resolver.d.ts +6 -0
  57. package/dist/services/lint/npx-resolver.js +38 -14
  58. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
  59. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
  60. package/dist/services/release/version-precheck-service.js +9 -2
  61. package/dist/services/scan/file-size-scan.d.ts +29 -0
  62. package/dist/services/scan/file-size-scan.js +63 -0
  63. package/dist/services/session/caller-binding-service.d.ts +24 -0
  64. package/dist/services/session/caller-binding-service.js +34 -0
  65. package/dist/services/session/getSessionDir.js +15 -10
  66. package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
  67. package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
  68. package/dist/services/skills/hooks-settings-service.d.ts +10 -0
  69. package/dist/services/skills/hooks-settings-service.js +152 -61
  70. package/dist/services/slice/slice-check-service.d.ts +14 -0
  71. package/dist/services/slice/slice-check-service.js +110 -50
  72. package/dist/services/slice/slice-check-types.d.ts +12 -7
  73. package/dist/services/slice/slice-check-types.js +8 -3
  74. package/dist/services/slice/slice-decompose-runners.js +24 -21
  75. package/dist/services/sop/sop-check-service.js +12 -1
  76. package/dist/services/web/bounded-output.d.ts +34 -0
  77. package/dist/services/web/bounded-output.js +68 -0
  78. package/dist/services/web/browser-acquire.d.ts +14 -0
  79. package/dist/services/web/browser-acquire.js +84 -0
  80. package/dist/services/web/browser-session-manager.d.ts +111 -0
  81. package/dist/services/web/browser-session-manager.js +413 -0
  82. package/dist/services/web/daemon-entry.d.ts +1 -0
  83. package/dist/services/web/daemon-entry.js +65 -0
  84. package/dist/services/web/daemon-registry.d.ts +42 -0
  85. package/dist/services/web/daemon-registry.js +164 -0
  86. package/dist/services/web/daemon-supervisor.d.ts +144 -0
  87. package/dist/services/web/daemon-supervisor.js +455 -0
  88. package/dist/services/web/playwright-loader.d.ts +89 -0
  89. package/dist/services/web/playwright-loader.js +253 -0
  90. package/dist/services/web/snapshot-pruner.d.ts +48 -0
  91. package/dist/services/web/snapshot-pruner.js +241 -0
  92. package/dist/services/web/untrusted-envelope.d.ts +27 -0
  93. package/dist/services/web/untrusted-envelope.js +44 -0
  94. package/dist/services/web/web-artifact-paths.d.ts +79 -0
  95. package/dist/services/web/web-artifact-paths.js +163 -0
  96. package/dist/services/web/web-client.d.ts +19 -0
  97. package/dist/services/web/web-client.js +55 -0
  98. package/dist/services/web/web-daemon-service.d.ts +38 -0
  99. package/dist/services/web/web-daemon-service.js +416 -0
  100. package/dist/services/web/web-fallback.d.ts +70 -0
  101. package/dist/services/web/web-fallback.js +121 -0
  102. package/dist/services/web/web-install-service.d.ts +91 -0
  103. package/dist/services/web/web-install-service.js +346 -0
  104. package/dist/services/web/web-login-profile.d.ts +89 -0
  105. package/dist/services/web/web-login-profile.js +612 -0
  106. package/dist/services/web/web-login-staging.d.ts +27 -0
  107. package/dist/services/web/web-login-staging.js +173 -0
  108. package/dist/services/web/web-protocol.d.ts +58 -0
  109. package/dist/services/web/web-protocol.js +58 -0
  110. package/dist/services/web/web-status-report.d.ts +33 -0
  111. package/dist/services/web/web-status-report.js +47 -0
  112. package/dist/services/workspace/claude-settings-template.d.ts +41 -5
  113. package/dist/services/workspace/claude-settings-template.js +116 -64
  114. package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
  115. package/dist/services/workspace/workspace-service.js +33 -0
  116. package/package.json +5 -5
  117. package/scripts/copy-templates.mjs +12 -0
  118. package/scripts/sync-version.mjs +20 -0
  119. package/skills/bee/peaks-qa/SKILL.md +2 -0
  120. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
  121. package/skills/bee/peaks-rd/SKILL.md +2 -0
  122. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
  123. package/skills/bee/peaks-txt/SKILL.md +2 -0
  124. package/skills/bee/peaks-ui/SKILL.md +2 -0
  125. package/skills/peaks-code/SKILL.md +18 -0
  126. package/skills/peaks-code/references/browser-workflow.md +10 -1
  127. package/skills/peaks-code/references/context-governance.md +29 -0
  128. package/skills/peaks-doctor/SKILL.md +2 -0
@@ -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.35",
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.20",
105
- "peaks-loop-mut": "0.1.33",
106
- "peaks-loop-shared": "0.0.69",
107
- "peaks-loop-shared-channel": "0.0.37"
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
@@ -210,6 +210,8 @@ Do not own product scope or implementation. Do not modify runtime configuration.
210
210
 
211
211
  QA sub-agents (qa / qa-business / qa-perf / qa-security) follow the same G7 metadata-only + G8.6 share protocol as RD. Detailed: `skills/peaks-code/references/context-governance.md`.
212
212
 
213
+ **Large tool output (> 2 KB) MUST NOT be dumped into the orchestrator's context** — use `--summary` (`peaks doctor --summary`, `peaks memory list --summary`, `peaks request list --summary`) or write to a file and `Read` selectively. Rationale (measured, session 2026-09-07-session-245530): full-envelope dumps cost ≈ 40K tokens of a 68%-full 1M window. `--summary` is an additive view — no information is removed. → `skills/peaks-code/references/context-governance.md` §G0.
214
+
213
215
  → see `references/qa-context-governance.md` for the full G7 / G8.6 / G9 protocol + QA sub-agent prompt template.
214
216
 
215
217
  ## References
@@ -59,6 +59,18 @@ What the sub-agent **MUST** still do:
59
59
 
60
60
  If `--type` is `docs` or `chore`, return `{"status":"skipped","reason":"type=<type>"}` and exit — there is no acceptance surface to plan tests for.
61
61
 
62
+ ## Final report cap (mandatory, slice 2026-09-10-context-audit-and-discipline)
63
+
64
+ Your FINAL report to the parent MUST be **≤ 40 lines and ≤ 2 KB**. Write any longer detail into the artifact file you already own; the parent can `Read` that file for the full detail, so nothing is lost. The report itself MUST still carry:
65
+
66
+ - changed files (one line each)
67
+ - the exact commands you ran
68
+ - pass/fail counts
69
+ - tsc status
70
+ - any blocker
71
+
72
+ Do NOT paste file contents, full tool output, or logs into the report. **Rationale (measured, session 2026-09-07-session-245530):** 20 sub-agent final reports ≈ 60 KB ≈ 15K tokens of the orchestrator's window — the report is an index into the artifact, not a copy of it. This rule is also machine-injected into every dispatch prompt (`REPORT_CAP_BLOCK` in `src/services/context/build-dispatch-system-prompt.ts`); this doc and that constant MUST stay in lockstep.
73
+
62
74
  ## Test Tool Detection (mandatory)
63
75
 
64
76
  The dispatch CLI (`peaks sub-agent dispatch`) automatically prepends a Test Tool Detection block to every sub-agent prompt — telling the sub-agent to read `package.json#scripts.test` first and use the project-local runner (`./node_modules/.bin/<runner>` or `pnpm test -- <file>`). NEVER use `npx <runner>`. This rule is machine-injected, not a prompt ritual — every sub-agent gets it including rd/qa/ui/txt/sc.
@@ -239,6 +239,8 @@ The main RD loop MUST call `peaks job karpathy-cost-check` after every `peaks re
239
239
 
240
240
  RD sub-agent prompt template MUST include the G7 path convention + G8.6 share protocol. Detailed protocol: `skills/peaks-code/references/context-governance.md`.
241
241
 
242
+ **Large tool output (> 2 KB) MUST NOT be dumped into the orchestrator's context** — use `--summary` (`peaks memory reindex --summary`, `peaks memory list --summary`, `peaks doctor --summary`, `peaks request list --summary`) or write to a file and `Read` selectively. Rationale (measured, session 2026-09-07-session-245530): 4 full `reindex --json` dumps ≈ 40K tokens of a 68%-full 1M window. `--summary` is an additive view — no information is removed. → `skills/peaks-code/references/context-governance.md` §G0.
243
+
242
244
  → see `references/rd-context-governance.md` for the full G7 / G8.6 / G9 protocol + RD sub-agent prompt template.
243
245
 
244
246
  ## Sub-stages (Plan 3 — strategic + tactical split)
@@ -113,6 +113,20 @@ Any RD/QA/SC sub-agent dispatched by `peaks sub-agent dispatch --from-dag` (or b
113
113
 
114
114
  ---
115
115
 
116
+ ## Final report cap (mandatory, slice 2026-09-10-context-audit-and-discipline)
117
+
118
+ Your FINAL report to the parent MUST be **≤ 40 lines and ≤ 2 KB**. Write any longer detail into the artifact file you already own (registered via `--write-artifact`); the parent can `Read` that file for the full detail, so nothing is lost. The report itself MUST still carry:
119
+
120
+ - changed files (one line each)
121
+ - the exact commands you ran
122
+ - pass/fail counts
123
+ - tsc status
124
+ - any blocker
125
+
126
+ Do NOT paste file contents, full tool output, or logs into the report. **Rationale (measured, session 2026-09-07-session-245530):** 20 sub-agent final reports ≈ 60 KB ≈ 15K tokens of the orchestrator's window — the report is an index into the artifact, not a copy of it. This rule is also machine-injected into every dispatch prompt (`REPORT_CAP_BLOCK` in `src/services/context/build-dispatch-system-prompt.ts`); this doc and that constant MUST stay in lockstep.
127
+
128
+ ---
129
+
116
130
  ## Test Tool Detection (mandatory)
117
131
 
118
132
  The dispatch CLI (`peaks sub-agent dispatch`) automatically prepends a Test Tool Detection block to every sub-agent prompt — telling the sub-agent to read `package.json#scripts.test` first and use the project-local runner (`./node_modules/.bin/<runner>` or `pnpm test -- <file>`). NEVER use `npx <runner>`. This rule is machine-injected, not a prompt ritual — every sub-agent gets it including rd/qa/ui/txt/sc.
@@ -283,6 +283,8 @@ Reference: `references/context-capsule.md`.
283
283
 
284
284
  > peaks-txt is the TXT reducer; it sees the metadata-only view from G7 + the share entries from G8. The TXT handoff summarizes the slice at the slice-close gate. Detailed: `skills/peaks-code/references/context-governance.md`.
285
285
 
286
+ **Large tool output (> 2 KB) MUST NOT be dumped into the orchestrator's context** — use `--summary` (`peaks request list --summary`, `peaks memory list --summary`, `peaks memory reindex --summary`) or write to a file and `Read` selectively. Rationale (measured, session 2026-09-07-session-245530): full-envelope dumps cost ≈ 40K tokens of a 68%-full 1M window. `--summary` is an additive view — no information is removed. → `skills/peaks-code/references/context-governance.md` §G0.
287
+
286
288
  ### G8 — TXT reducer sees share entries on completion
287
289
 
288
290
  When TXT reduces a batch, it consumes:
@@ -333,6 +333,8 @@ Do not own backend architecture, non-UI implementation, runtime hook installatio
333
333
 
334
334
  UI sub-agents follow the same G7 metadata-only + G8.6 share protocol. UI artifacts are large binary-ish; the 1MB artifact size limit (G7.3) applies. Detailed: `skills/peaks-code/references/context-governance.md`.
335
335
 
336
+ **Large tool output (> 2 KB) MUST NOT be dumped into the orchestrator's context** — use `--summary` (`peaks request list --summary`, `peaks memory list --summary`) or write to a file and `Read` selectively. Rationale (measured, session 2026-09-07-session-245530): full-envelope dumps cost ≈ 40K tokens of a 68%-full 1M window. `--summary` is an additive view — no information is removed. → `skills/peaks-code/references/context-governance.md` §G0.
337
+
336
338
  ### G7 — UI sub-agent protocol
337
339
 
338
340
  1. Write design draft / component scaffold to `.peaks/_sub_agents/<sid>/artifacts/<rid>-ui-001.md` (size ≤ 1MB).
@@ -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
@@ -310,6 +320,14 @@ After final validation, refresh project-local standards via `peaks standards ini
310
320
 
311
321
  Main LLM reducer sees metadata-only view (~200 chars/sub-agent); on-demand `Read` for full content. Threshold table: 50% soft warn, 75% `CONTEXT_NEAR_LIMIT`, 80% hard reject (CLI + hook double-guard). → `references/context-governance.md`.
312
322
 
323
+ ## Large tool output discipline (BLOCKING — slice 2026-09-10-context-audit-and-discipline)
324
+
325
+ > **Hard rule.** Any tool output larger than **2 KB** MUST NOT be dumped into the orchestrator's own context. Use `--summary` (bounded counts + names-of-first-N, ≤ 2 KB) when the command offers it — `peaks memory reindex --summary`, `peaks memory list --summary`, `peaks doctor --summary`, `peaks request list --summary` — otherwise write the output to a file and `Read` only the slice you need. The default envelopes are unchanged; `--summary` is strictly opt-in.
326
+ >
327
+ > **Why (measured, not cargo-cult).** In session `2026-09-07-session-245530` the orchestrator spent ~68% of a 1M window on its OWN context. Two offenders dominated: 4 × dumping a full `peaks memory reindex --json` unclassified array ≈ 160 KB ≈ 40K tokens, and 20 × sub-agent final reports ≈ 60 KB ≈ 15K tokens. Neither was dispatch boilerplate — both were the orchestrator reading things it did not need in full. `peaks code context-audit` now measures this per group (`{tool, key, bytes, pctOfTotal, count}`); run it when the window feels heavy.
328
+ >
329
+ > **Quality guard.** `--summary` removes no information — it is an additive view. Every path, name and count stays on disk and is re-readable by re-running the command without the flag. Never silently drop data; shrink the in-context copy instead.
330
+
313
331
  ## Sub-agent cross-batch signal — G8.4 share / shared-read / await
314
332
 
315
333
  Three CLI primitives: `peaks sub-agent share / shared-read / await` (last-write-wins, ≤ 1KB warn / ≥ 64KB reject). Channel gitignored under `.peaks/_sub_agents/<sessionId>/shared/`.
@@ -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
@@ -3,6 +3,35 @@
3
3
  > Slice #010 (G7 + G8 + G9 context-governance push).
4
4
  > See: `.peaks/memory/sub-agent-context-minimal-occupation.md` + `sub-agent-shared-channel-cross-completion.md` for the red lines.
5
5
 
6
+ ## G0 — orchestrator large-tool-output discipline (slice 2026-09-10-context-audit-and-discipline)
7
+
8
+ ### The measurement (session `2026-09-07-session-245530`, ~68% of a 1M window)
9
+
10
+ The real token cost is the ORCHESTRATOR's own context — not dispatch boilerplate:
11
+
12
+ | Offender | Volume | Cost |
13
+ |---|---|---|
14
+ | 4 × full `peaks memory reindex --json` unclassified array | ≈ 160 KB | ≈ 40K tokens |
15
+ | 20 × sub-agent final reports | ≈ 60 KB | ≈ 15K tokens |
16
+ | several `cat` of large docs | ≈ 15 KB | ≈ 4K tokens |
17
+
18
+ `peaks code context-now` reports a RATIO only. `peaks code context-audit --project <root> --json` reports WHAT fills the window — top-N groups of `{tool, key, bytes, pctOfTotal, count}` — so the next session can name the offender instead of guessing. Read-only, fail-soft (`available: false` + reason; never blocks, never exits non-zero).
19
+
20
+ ### The rule (BLOCKING)
21
+
22
+ - Tool output **> 2 KB** MUST NOT be dumped into the orchestrator's context.
23
+ - Prefer the opt-in `--summary` flag, which emits counts + names-of-first-N (≤ 2 KB) instead of the full array:
24
+ - `peaks memory reindex --summary`
25
+ - `peaks memory list --summary`
26
+ - `peaks doctor --summary` (JSON envelope)
27
+ - `peaks request list --summary`
28
+ - When a command has no `--summary`, write the output to a file and `Read` only the needed slice (offset/limit), or pipe through a filter before it reaches the orchestrator.
29
+ - Default (no flag) envelopes are byte-identical to before — `--summary` is strictly opt-in, so back-compat is preserved.
30
+
31
+ ### Quality guard (binding)
32
+
33
+ `--summary` is an ADDITIVE view. It removes no information: every path, name and count remains on disk and is re-readable by re-running the same command without the flag. Silently dropping data is forbidden; shrinking the in-context copy is the goal. The sub-agent FINAL report cap (≤ 40 lines / 2 KB, detail in the artifact the parent can `Read`) follows the same principle — see the dispatch prompt's `## Final report cap (mandatory)` block.
34
+
6
35
  ## G7 — sub-agent context minimal-occupation (metadata-only + 按需 Read)
7
36
 
8
37
  ### Path convention
@@ -47,6 +47,8 @@ If a doctor finding requires a code change, the workflow hands off to `peaks-rd`
47
47
  - `peaks openspec from-doctor` — L3.3 proposal generator
48
48
  - `peaks openspec validate` — gate a draft proposal
49
49
 
50
+ **Large tool output (> 2 KB) MUST NOT be dumped into the orchestrator's context.** `peaks doctor` returns ~69 checks — use `peaks doctor --summary` (counts + names-of-first-N, ≤ 2 KB) and re-run without the flag only for the specific check you need to read. Rationale (measured, session 2026-09-07-session-245530): full-envelope dumps cost ≈ 40K tokens of a 68%-full 1M window; `--summary` is an additive view, so no information is removed.
51
+
50
52
  ## Boundaries
51
53
 
52
54
  - The doctor is read-only. It does NOT modify code, fix bugs, or clean up sessions.