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
@@ -0,0 +1,23 @@
1
+ /** Reserved batch id for the session capsule (matches the channel path pattern). */
2
+ export declare const SESSION_CAPSULE_BATCH_ID = "session-capsule";
3
+ /** Reserved key inside that channel. */
4
+ export declare const SESSION_CAPSULE_KEY = "orchestrator.capsule";
5
+ export interface SessionCapsuleRef {
6
+ readonly batchId: string;
7
+ readonly key: string;
8
+ /** Byte size of the published value (for the pointer line). */
9
+ readonly bytes: number;
10
+ /** ISO8601 timestamp of the last write. */
11
+ readonly updatedAt: string;
12
+ }
13
+ /**
14
+ * Read the session capsule, if one was published. Returns `null` when the
15
+ * channel is absent, empty, or unreadable — the dispatch prompt then
16
+ * renders neither the pointer nor the precedence line (byte-identical to
17
+ * the pre-slice shape).
18
+ */
19
+ export declare function readSessionCapsule(opts: {
20
+ projectRoot: string;
21
+ sid: string;
22
+ rid: string;
23
+ }): SessionCapsuleRef | null;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Slice 2026-09-10-dispatch-token-and-swarm §4 — session capsule reader.
3
+ *
4
+ * The orchestrator publishes already-known background ONCE per session
5
+ * through the existing G8.4 channel:
6
+ *
7
+ * peaks sub-agent share \
8
+ * --batch session-capsule \
9
+ * --key orchestrator.capsule \
10
+ * --value '{"rootCauses":[...],"decisions":[...],"fileMap":{...}}'
11
+ *
12
+ * Dispatch prompts then carry a one-line `shared-read` pointer instead of
13
+ * re-explaining that background in every task spec.
14
+ *
15
+ * QUALITY GUARD: the capsule is ADVISORY BACKGROUND ONLY. Nothing a
16
+ * sub-agent must ACT on may live only in the capsule — the task spec is
17
+ * authoritative and wins on conflict. The precedence sentence is rendered
18
+ * by `renderCapsulePointer` in build-dispatch-system-prompt.ts and is not
19
+ * optional when the pointer is emitted.
20
+ *
21
+ * This module only READS. Publishing is the orchestrator's job through the
22
+ * already-shipped `peaks sub-agent share` primitive — no new write path.
23
+ */
24
+ import { readSharedChannel } from 'peaks-loop-shared-channel';
25
+ /** Reserved batch id for the session capsule (matches the channel path pattern). */
26
+ export const SESSION_CAPSULE_BATCH_ID = 'session-capsule';
27
+ /** Reserved key inside that channel. */
28
+ export const SESSION_CAPSULE_KEY = 'orchestrator.capsule';
29
+ /**
30
+ * Read the session capsule, if one was published. Returns `null` when the
31
+ * channel is absent, empty, or unreadable — the dispatch prompt then
32
+ * renders neither the pointer nor the precedence line (byte-identical to
33
+ * the pre-slice shape).
34
+ */
35
+ export function readSessionCapsule(opts) {
36
+ try {
37
+ const channel = readSharedChannel({
38
+ projectRoot: opts.projectRoot,
39
+ sid: opts.sid,
40
+ rid: opts.rid,
41
+ batchId: SESSION_CAPSULE_BATCH_ID
42
+ });
43
+ const entry = channel.entries[SESSION_CAPSULE_KEY];
44
+ if (entry === undefined)
45
+ return null;
46
+ return {
47
+ batchId: SESSION_CAPSULE_BATCH_ID,
48
+ key: SESSION_CAPSULE_KEY,
49
+ bytes: entry.valueSize,
50
+ updatedAt: entry.at
51
+ };
52
+ }
53
+ catch {
54
+ return null; // fail-soft: a missing capsule never blocks a dispatch
55
+ }
56
+ }
@@ -31,6 +31,15 @@ export interface SliceNode {
31
31
  * (complex = user-attended, simple/trivial = overnight). Optional.
32
32
  */
33
33
  readonly complexity?: SliceComplexity;
34
+ /**
35
+ * Slice 2026-09-10-dispatch-token-and-swarm §3: files this slice is
36
+ * expected to touch. When EVERY node of a topological level declares
37
+ * `files`, `--from-dag` emits a file-overlap wave plan (`firstLevelWaves`)
38
+ * so the LLM can fan the level out without serializing on a shared file.
39
+ * Optional and additive: absent → DAG hash and dispatch behavior are
40
+ * byte-identical to before this slice.
41
+ */
42
+ readonly files?: readonly string[];
34
43
  }
35
44
  export interface DependsOn {
36
45
  readonly from: string;
@@ -103,6 +103,11 @@ export function validateDag(dag) {
103
103
  if (n.complexity !== undefined && !isSliceComplexity(n.complexity)) {
104
104
  throw new InvalidSliceDagError(`node ${n.id} complexity must be one of ${SLICE_COMPLEXITIES.join('|')} when present`);
105
105
  }
106
+ // Slice 2026-09-10 §3: optional file list. Only shape-checked when
107
+ // present so pre-existing DAGs stay valid.
108
+ if (n.files !== undefined && (!Array.isArray(n.files) || n.files.some((f) => typeof f !== 'string' || f.length === 0))) {
109
+ throw new InvalidSliceDagError(`node ${n.id} files must be an array of non-empty strings when present`);
110
+ }
106
111
  }
107
112
  // v2.15.0 follow-up — G12 defensive rule: foundation slice can only
108
113
  // depend on another foundation slice. Business depending on foundation
@@ -219,7 +224,10 @@ export function serializeDag(dag) {
219
224
  // (only when present, preserving hash stability for old DAGs).
220
225
  ...(n.foundation !== undefined ? { foundation: n.foundation } : {}),
221
226
  ...(n.upstreamSync !== undefined ? { upstreamSync: n.upstreamSync } : {}),
222
- ...(n.complexity !== undefined ? { complexity: n.complexity } : {})
227
+ ...(n.complexity !== undefined ? { complexity: n.complexity } : {}),
228
+ // Slice 2026-09-10 §3: only present when declared, so the hash of a
229
+ // file-less DAG is unchanged.
230
+ ...(n.files !== undefined ? { files: [...n.files] } : {})
223
231
  }));
224
232
  const edges = [...dag.edges]
225
233
  .sort((a, b) => {
@@ -26,8 +26,19 @@
26
26
  * "remove the redundancy" of a test-tool-detection instruction because
27
27
  * the dispatch CLI always prepends it. This is a guarantee, not a
28
28
  * suggestion.
29
+ *
30
+ * 2026-09-10-dispatch-block-d (Option D): ONE block for EVERY role. The
31
+ * runner-table EXAMPLES were dropped — they were never rules, and
32
+ * `package.json#scripts.test` is the source of truth at run time — while
33
+ * the one missing real rule is stated inline: PB-5, i.e. repo-defined
34
+ * `test` / `test:*` scripts are the human/LLM direct path and are NOT
35
+ * gated by the scope rule. The soft fallback ("only as a last resort, ask
36
+ * the user before assuming a runner") and the Windows-aware note on
37
+ * `peaks test <file>` are RETAINED — they are quality guidance, not
38
+ * examples. No role split remains, so every role receives a byte-identical
39
+ * block.
29
40
  */
30
- export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandatory)\n\nBefore running any test, read `package.json#scripts.test` to identify the project's test framework. Use the project-local runner \u2014 do NOT invoke `npx <runner>`:\n\n- **vitest** \u2192 `./node_modules/.bin/vitest run <file>` (or `pnpm test -- <file>`)\n- **jest** \u2192 `./node_modules/.bin/jest <file>` (or `pnpm test -- <file>`)\n- **mocha** \u2192 `./node_modules/.bin/mocha <file>` (or `pnpm test -- <file>`)\n\n## Test Scope (mandatory)\n\nThe dispatched test command MUST be **scoped** to a single file or pattern. An unscoped `./node_modules/.bin/vitest run` (no path filter) is **refused** because the 483-file suite is one keystroke from a 36-minute wall clock:\n\n- **scoped** \u2192 `./node_modules/.bin/vitest run tests/unit/foo.test.ts` (or any explicit file/pattern)\n- **intentional full run** \u2192 prefix with the explicit opt-in token `PEAKS_FULL_TEST=1` to override the scope gate. Use only for CI / release verification, never for routine verification during a slice.\n- **refused** \u2192 bare `./node_modules/.bin/vitest run` (no argument after `run`) without the opt-in token.\n\n`pnpm test` / `pnpm test:unit` / `pnpm test:cli` / `pnpm test:integration` and any repo-defined `test*` script remain the **human / LLM direct path** and are not gated by this rule (PB-5).\n\nIf unsure which framework the consumer project uses, run `peaks test --json` first to introspect the resolved framework + argv. Only as a last resort, ask the user before assuming a runner. The CLI command `peaks test <file>` already resolves the local binary for you (Windows-aware).";
41
+ export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandatory)\n\nRead `package.json#scripts.test` for the project's framework and use the project-local runner \u2014 do NOT invoke `npx <runner>`. If unsure, run `peaks test --json` first. Only as a last resort, ask the user before assuming a runner. `peaks test <file>` already resolves the local binary for you (Windows-aware).\n\n## Test Scope (mandatory)\n\nAny test command MUST be **scoped** to a single file or pattern; a bare `./node_modules/.bin/vitest run` is **refused** unless you prefix the explicit opt-in token `PEAKS_FULL_TEST=1` (CI / release verification only, never routine slice verification).\n\nRepo-defined `test` / `test:*` scripts are the human/LLM direct path and are NOT gated by this scope rule (PB-5).";
31
42
  /**
32
43
  * Pure helper that returns the block. Exists as a function (not just an
33
44
  * exported constant) so future variants can take a runtime parameter
@@ -26,26 +26,27 @@
26
26
  * "remove the redundancy" of a test-tool-detection instruction because
27
27
  * the dispatch CLI always prepends it. This is a guarantee, not a
28
28
  * suggestion.
29
+ *
30
+ * 2026-09-10-dispatch-block-d (Option D): ONE block for EVERY role. The
31
+ * runner-table EXAMPLES were dropped — they were never rules, and
32
+ * `package.json#scripts.test` is the source of truth at run time — while
33
+ * the one missing real rule is stated inline: PB-5, i.e. repo-defined
34
+ * `test` / `test:*` scripts are the human/LLM direct path and are NOT
35
+ * gated by the scope rule. The soft fallback ("only as a last resort, ask
36
+ * the user before assuming a runner") and the Windows-aware note on
37
+ * `peaks test <file>` are RETAINED — they are quality guidance, not
38
+ * examples. No role split remains, so every role receives a byte-identical
39
+ * block.
29
40
  */
30
41
  export const TEST_TOOL_DETECTION_BLOCK = `## Test Tool Detection (mandatory)
31
42
 
32
- Before running any test, read \`package.json#scripts.test\` to identify the project's test framework. Use the project-local runner — do NOT invoke \`npx <runner>\`:
33
-
34
- - **vitest** → \`./node_modules/.bin/vitest run <file>\` (or \`pnpm test -- <file>\`)
35
- - **jest** → \`./node_modules/.bin/jest <file>\` (or \`pnpm test -- <file>\`)
36
- - **mocha** → \`./node_modules/.bin/mocha <file>\` (or \`pnpm test -- <file>\`)
43
+ Read \`package.json#scripts.test\` for the project's framework and use the project-local runner — do NOT invoke \`npx <runner>\`. If unsure, run \`peaks test --json\` first. Only as a last resort, ask the user before assuming a runner. \`peaks test <file>\` already resolves the local binary for you (Windows-aware).
37
44
 
38
45
  ## Test Scope (mandatory)
39
46
 
40
- The dispatched test command MUST be **scoped** to a single file or pattern. An unscoped \`./node_modules/.bin/vitest run\` (no path filter) is **refused** because the 483-file suite is one keystroke from a 36-minute wall clock:
41
-
42
- - **scoped** → \`./node_modules/.bin/vitest run tests/unit/foo.test.ts\` (or any explicit file/pattern)
43
- - **intentional full run** → prefix with the explicit opt-in token \`PEAKS_FULL_TEST=1\` to override the scope gate. Use only for CI / release verification, never for routine verification during a slice.
44
- - **refused** → bare \`./node_modules/.bin/vitest run\` (no argument after \`run\`) without the opt-in token.
45
-
46
- \`pnpm test\` / \`pnpm test:unit\` / \`pnpm test:cli\` / \`pnpm test:integration\` and any repo-defined \`test*\` script remain the **human / LLM direct path** and are not gated by this rule (PB-5).
47
+ Any test command MUST be **scoped** to a single file or pattern; a bare \`./node_modules/.bin/vitest run\` is **refused** unless you prefix the explicit opt-in token \`PEAKS_FULL_TEST=1\` (CI / release verification only, never routine slice verification).
47
48
 
48
- If unsure which framework the consumer project uses, run \`peaks test --json\` first to introspect the resolved framework + argv. Only as a last resort, ask the user before assuming a runner. The CLI command \`peaks test <file>\` already resolves the local binary for you (Windows-aware).`;
49
+ Repo-defined \`test\` / \`test:*\` scripts are the human/LLM direct path and are NOT gated by this scope rule (PB-5).`;
49
50
  /**
50
51
  * Pure helper that returns the block. Exists as a function (not just an
51
52
  * exported constant) so future variants can take a runtime parameter
@@ -32,6 +32,7 @@
32
32
  */
33
33
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
34
34
  import { dirname, join } from 'node:path';
35
+ import { resolveHookShell } from '../skills/hooks-codegate-superpowers.js';
35
36
  /**
36
37
  * Stable matcher for the auto-compact hook. Single source of truth
37
38
  * — `install` and `remove` both key off this constant so the
@@ -105,6 +106,13 @@ export function installAutoCompactHook(input) {
105
106
  if (alreadyInstalled) {
106
107
  return { action: 'already-installed', settingsPath };
107
108
  }
109
+ // The matcher is `Bash|Task`, so on Windows this runs on the same
110
+ // Git-Bash / MSYS2 shell-form path as the other peaks Bash hooks, which
111
+ // force-allocates a console window on every matching tool call. The
112
+ // entry is written only into the machine-local, gitignored
113
+ // `.claude/settings.local.json`, so a machine-specific `shell` cannot
114
+ // leak into a shared file. `undefined` on POSIX omits the key entirely.
115
+ const shell = resolveHookShell();
108
116
  const nextPreToolUse = [
109
117
  ...preToolUse,
110
118
  {
@@ -112,7 +120,8 @@ export function installAutoCompactHook(input) {
112
120
  hooks: [
113
121
  {
114
122
  type: 'command',
115
- command: AUTO_COMPACT_HOOK_COMMAND
123
+ command: AUTO_COMPACT_HOOK_COMMAND,
124
+ ...(shell !== undefined ? { shell } : {})
116
125
  }
117
126
  ]
118
127
  }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * write-gate.js — peaks Write|Edit|MultiEdit PreToolUse path gate.
4
+ *
5
+ * Slice c5-write-hook-exec-form (session 2026-09-10-session-528a63).
6
+ * VERBATIM RELOCATION: the predicate chain below was character-for-character
7
+ * the chain that used to be inlined into `claude-settings-template.ts` as a
8
+ * `node -e "<js>"` one-liner.
9
+ *
10
+ * Slice c5b-write-gate-polarity (same session) NARROWED the decision to match
11
+ * the contract `.claude/HOOKS.md` documents for this handler. The relocated
12
+ * chain read its eight directory names as an EXCLUSION list, so `_runtime`
13
+ * AND every other `.peaks/<slug>/` were allowed; it now allows only paths
14
+ * under `.peaks/_runtime/` and falls through on everything else. That earlier
15
+ * polarity also permitted a top-level `.peaks/<change-id>/` write, which
16
+ * `CLAUDE.md`'s hard ban forbids outright.
17
+ *
18
+ * Why it moved into a file: the inlined form was shell-dialect-coupled. Its
19
+ * escaping contract was defined in terms of bash reducing `\\` to `\` inside a
20
+ * `"..."` wrapper, which PowerShell does NOT do — so the handler could not take
21
+ * the platform `shell` pin its Bash siblings carry. `node <path>` has no inline
22
+ * payload, so there is nothing left to escape and no dialect to couple to.
23
+ *
24
+ * Why `.js` and not `.sh` (the convention for the other hook scripts here):
25
+ * - `.sh` needs `bash`; on Windows that means Git Bash, so it is itself a
26
+ * shell dependency and the escaping problem merely moves to the
27
+ * `bash <script>` boundary.
28
+ * - `node` is already a hard dependency — the previous form invoked it.
29
+ * - A plain `.js` needs no compile step, so the SAME relative filename
30
+ * exists in `src/` (used by `tsx` + vitest) and in `dist/` (copied by
31
+ * `scripts/copy-templates.mjs`), which lets one emitted path string be
32
+ * valid for both the repo and an installed consumer.
33
+ *
34
+ * Contract (`.claude/HOOKS.md`): exit 0 = allow, exit 1 = fall through to the
35
+ * gate — NOT a deny. Only exit 2 blocks a tool call, and this handler never
36
+ * returns it. This handler's job is to stay silent on the paths the gate is
37
+ * meant to skip.
38
+ *
39
+ * Path source: the hook payload arrives as JSON on STDIN (Claude Code's
40
+ * documented channel; it appends no argv). `process.argv[2]` is honoured as a
41
+ * fallback so a positional-arg invocation keeps working.
42
+ */
43
+
44
+ /** First string found at any of the candidate keys of `root`. */
45
+ function pathFrom(root) {
46
+ if (!root || typeof root !== 'object') return '';
47
+ for (const key of ['file_path', 'path', 'notebook_path']) {
48
+ if (typeof root[key] === 'string') return root[key];
49
+ }
50
+ return '';
51
+ }
52
+
53
+ /** Pull the candidate file path out of a parsed hook payload. */
54
+ function candidatePath(payload) {
55
+ if (!payload || typeof payload !== 'object') return '';
56
+ return pathFrom(payload.tool_input) || pathFrom(payload);
57
+ }
58
+
59
+ /**
60
+ * The gate decision: allow (0) only for paths under `.peaks/_runtime/`;
61
+ * everything else falls through to the gate (1).
62
+ */
63
+ function decide(p) {
64
+ return p.includes('.peaks/_runtime/') ? 0 : 1;
65
+ }
66
+
67
+ const ARGV_PATH = typeof process.argv[2] === 'string' ? process.argv[2] : '';
68
+
69
+ // No stdin to read (a human running this by hand): decide on argv alone
70
+ // instead of blocking forever waiting for an 'end' that never comes.
71
+ if (process.stdin.isTTY) {
72
+ process.exit(decide(ARGV_PATH));
73
+ }
74
+
75
+ let raw = '';
76
+ process.stdin.setEncoding('utf8');
77
+ process.stdin.on('data', (chunk) => { raw += chunk; });
78
+ process.stdin.on('end', () => {
79
+ let payload;
80
+ try {
81
+ payload = JSON.parse(raw);
82
+ } catch {
83
+ // Malformed or empty payload → no path → deny, which is the same
84
+ // outcome the old form produced for an absent `process.argv[1]`.
85
+ payload = undefined;
86
+ }
87
+ process.exit(decide(candidatePath(payload) || ARGV_PATH));
88
+ });
@@ -1,4 +1,14 @@
1
1
  import type { IdeAdapter } from '../ide-types.js';
2
+ /**
3
+ * Resolve the absolute path of a Claude Code transcript jsonl by OUTER
4
+ * session id, searching `~/.claude/projects/**` recursively.
5
+ *
6
+ * Exported (slice 2026-09-10-context-audit-and-discipline, Slice A) so
7
+ * `peaks code context-audit` reuses THIS locator instead of re-implementing
8
+ * the recursive find. Returns `null` when the transcript does not exist —
9
+ * callers MUST treat that as "unavailable", never as an error.
10
+ */
11
+ export declare function resolveClaudeTranscriptPath(outerSessionId: string, projectsDir?: string): string | null;
2
12
  /**
3
13
  * Resolve the currently-active Claude Code model id from a runtime env map.
4
14
  * Returns the first non-empty `CLAUDE_CODE_MODEL_ENV_VARS` value (trimmed), or
@@ -90,6 +90,20 @@ function findTranscriptJsonl(projectsDir, outerSessionId) {
90
90
  }
91
91
  return null;
92
92
  }
93
+ /**
94
+ * Resolve the absolute path of a Claude Code transcript jsonl by OUTER
95
+ * session id, searching `~/.claude/projects/**` recursively.
96
+ *
97
+ * Exported (slice 2026-09-10-context-audit-and-discipline, Slice A) so
98
+ * `peaks code context-audit` reuses THIS locator instead of re-implementing
99
+ * the recursive find. Returns `null` when the transcript does not exist —
100
+ * callers MUST treat that as "unavailable", never as an error.
101
+ */
102
+ export function resolveClaudeTranscriptPath(outerSessionId, projectsDir = join(homedir(), '.claude', 'projects')) {
103
+ if (typeof outerSessionId !== 'string' || outerSessionId.length === 0)
104
+ return null;
105
+ return findTranscriptJsonl(projectsDir, outerSessionId);
106
+ }
93
107
  /** 1M-context window size in tokens (documented single choice: 1,000,000). */
94
108
  const ONE_MILLION_CONTEXT_TOKENS = 1_000_000;
95
109
  /** Safe-default (non-1M) context window size in tokens. */
@@ -483,7 +497,12 @@ export const CLAUDE_CODE_ADAPTER = {
483
497
  compactCommand: 'claude --compact',
484
498
  compactPathway: 'ide-native',
485
499
  postCompactDetectCommand: 'peaks code auto-compact --json',
486
- readContextPercentFallback
500
+ readContextPercentFallback,
501
+ // Slice 2026-09-10-context-audit-and-discipline (Slice A): the vendor
502
+ // layout knowledge (`~/.claude/projects/**/<outerSessionId>.jsonl`)
503
+ // stays here; `peaks code context-audit` resolves it through the
504
+ // adapter registry, never by naming this adapter directly.
505
+ resolveTranscriptPath: (outerSessionId) => resolveClaudeTranscriptPath(outerSessionId),
487
506
  },
488
507
  // Slice #011: standards profile. Claude Code reads its constitution at
489
508
  // CLAUDE.md + module-level rules under .claude/rules/**. The values mirror
@@ -217,6 +217,21 @@ export interface IdeCompactProfile {
217
217
  * Added in slice 2026-09-02-vendor-neutral-context-probe.
218
218
  */
219
219
  readonly readContextPercentFallback?: (input: ContextPercentFallbackInput) => ContextPercentProbe | null;
220
+ /**
221
+ * Optional locator for the IDE's per-session transcript file (jsonl),
222
+ * keyed by the OUTER (harness) session id. `peaks code context-audit`
223
+ * (slice 2026-09-10-context-audit-and-discipline, Slice A) uses it to
224
+ * group the session's tool results by tool + short input key, so the
225
+ * generic audit service never learns any vendor's on-disk layout.
226
+ *
227
+ * Returns the absolute path, or `null` when the transcript does not
228
+ * exist. Adapters MUST NOT throw on a missing file — the audit treats
229
+ * `null` as `available: false` and continues.
230
+ *
231
+ * Adapters that do not opt in simply omit the field; the audit then
232
+ * reports `transcript-locator-unavailable`.
233
+ */
234
+ readonly resolveTranscriptPath?: (outerSessionId: string) => string | null;
220
235
  }
221
236
  /**
222
237
  * Input the generic `readContextPercent` reader passes to an adapter's
@@ -7,4 +7,6 @@ export type EslintDetectResult = {
7
7
  readonly warnings: readonly string[];
8
8
  readonly nextActions: readonly string[];
9
9
  };
10
+ /** Named code for "npm itself could not be launched" — distinct from a registry miss. */
11
+ export declare const NPM_PROBE_UNRESOLVED_CODE = "NPM_PROBE_UNRESOLVED";
10
12
  export declare function detectEslint(): EslintDetectResult;
@@ -4,7 +4,7 @@
4
4
  * unified Gate B5 verdict.
5
5
  */
6
6
  import { spawnSync } from 'node:child_process';
7
- import { resolveNpxInvocation } from './npx-resolver.js';
7
+ import { resolveNpmInvocation, resolveNpxInvocation } from './npx-resolver.js';
8
8
  import { ESLINT_PACKAGE_PINS } from './eslint-runner.js';
9
9
  const PACKAGES_TO_PROBE = [
10
10
  'eslint',
@@ -23,14 +23,23 @@ function probeNpx() {
23
23
  const probe = spawnSync(command, args, { encoding: 'utf8', env: baseEnv });
24
24
  return probe.status === 0;
25
25
  }
26
+ /** Named code for "npm itself could not be launched" — distinct from a registry miss. */
27
+ export const NPM_PROBE_UNRESOLVED_CODE = 'NPM_PROBE_UNRESOLVED';
26
28
  function probePackage(key) {
27
29
  const pkg = packageNameFor(key);
28
30
  const pin = ESLINT_PACKAGE_PINS[key];
29
- // shell:true required on Windows because `npm` (like npx) is a `.cmd`
30
- // shim; Node 22 refuses to invoke it without shell wrapper.
31
- // 2026-08-06 lint-dogfood cycle-2 follow-up.
32
- const result = spawnSync('npm', ['view', `${pkg}@${pin}`, 'version'], { encoding: 'utf8', shell: true });
33
- return result.status === 0;
31
+ // 2026-09-10: `npm` on Windows is an `npm.cmd` shim, which Node >= 20 refuses
32
+ // to spawn at all without `shell: true` — and `shell: true` concatenates the
33
+ // argv unescaped (DEP0190 on every call, and any argument containing a space
34
+ // is split). So the shim is bypassed: `resolveNpmInvocation` resolves npm's
35
+ // own JS entry and this runs it through `process.execPath`. Same shape as the
36
+ // npx probe above and as `eslint-runner.ts`.
37
+ const { command, args, baseEnv } = resolveNpmInvocation(['view', `${pkg}@${pin}`, 'version']);
38
+ const result = spawnSync(command, args, { encoding: 'utf8', env: baseEnv });
39
+ return {
40
+ ok: result.status === 0,
41
+ error: result.error === undefined || result.error === null ? null : result.error.message
42
+ };
34
43
  }
35
44
  export function detectEslint() {
36
45
  const nextActions = [];
@@ -45,9 +54,14 @@ export function detectEslint() {
45
54
  };
46
55
  }
47
56
  for (const key of PACKAGES_TO_PROBE) {
48
- if (!probePackage(key)) {
49
- warnings.push(`npm registry cannot resolve ${packageNameFor(key)}@${ESLINT_PACKAGE_PINS[key]}`);
50
- }
57
+ const probe = probePackage(key);
58
+ if (probe.ok)
59
+ continue;
60
+ const target = `${packageNameFor(key)}@${ESLINT_PACKAGE_PINS[key]}`;
61
+ warnings.push(probe.error === null
62
+ ? `npm registry cannot resolve ${target}`
63
+ : `${NPM_PROBE_UNRESOLVED_CODE}: could not launch npm to probe ${target} (${probe.error}). ` +
64
+ 'Ensure Node.js >= 20 with its bundled npm is installed.');
51
65
  }
52
66
  if (warnings.length > 0) {
53
67
  nextActions.push('Re-run `peaks code lint --json` after npm connectivity is restored.');
@@ -4,3 +4,9 @@ export type NpxInvocation = {
4
4
  readonly baseEnv: NodeJS.ProcessEnv;
5
5
  };
6
6
  export declare function resolveNpxInvocation(npxArgs: readonly string[]): NpxInvocation;
7
+ /**
8
+ * `npm` sibling of `resolveNpxInvocation`, same shim bypass: `node
9
+ * <npm-cli.js> <args>`. A caller must NOT reach for bare `npm` + `shell: true`
10
+ * instead — the shim is the defect, not the justification.
11
+ */
12
+ export declare function resolveNpmInvocation(npmArgs: readonly string[]): NpxInvocation;
@@ -1,26 +1,28 @@
1
1
  /**
2
- * Cross-platform `npx` resolver.
2
+ * Cross-platform `npm` / `npx` resolver.
3
3
  *
4
- * On Windows, `npm` installs `npx` as a `.cmd` shim; Node 22's
5
- * `child_process.spawnSync` refuses to invoke it unless `shell: true`
6
- * is set, and `shell: true` corrupts quoted `--package` arguments.
7
- * Rather than depend on shell quoting, this helper resolves the
8
- * npx script bundled with the user's `npm` install and invokes it
9
- * via `node <npx-cli.js>` with the same argv. macOS / Linux continue
10
- * to use the regular `npx` binary.
4
+ * On Windows, `npm` installs `npx` and `npm` as `.cmd` shims; Node's
5
+ * `child_process.spawnSync` refuses to invoke a `.cmd` unless `shell: true`
6
+ * is set, and `shell: true` concatenates (does not escape) the argv — it
7
+ * corrupts quoted `--package` arguments, splits any argument containing a
8
+ * space, and emits DEP0190 on every call. Rather than depend on shell
9
+ * quoting, these helpers resolve the CLI script bundled with the user's
10
+ * `npm` install and invoke it via `node <npm|npx>-cli.js` with the same
11
+ * argv. macOS / Linux continue to use the regular binaries.
11
12
  */
12
13
  import { existsSync } from 'node:fs';
13
14
  import { join } from 'node:path';
14
15
  import process from 'node:process';
15
- function locateNpxCliScript() {
16
+ /** Locate `<npm install>/bin/<scriptFile>`, or `null` when not on disk. */
17
+ function locateNpmCliScript(scriptFile) {
16
18
  const candidates = process.platform === 'win32'
17
19
  ? [
18
- join(process.execPath, '..', '..', 'node_modules', 'npm', 'bin', 'npx-cli.js'),
19
- 'C:/nvm4w/nodejs/node_modules/npm/bin/npx-cli.js',
20
- 'C:/Program Files/nodejs/node_modules/npm/bin/npx-cli.js'
20
+ join(process.execPath, '..', '..', 'node_modules', 'npm', 'bin', scriptFile),
21
+ `C:/nvm4w/nodejs/node_modules/npm/bin/${scriptFile}`,
22
+ `C:/Program Files/nodejs/node_modules/npm/bin/${scriptFile}`
21
23
  ]
22
24
  : [
23
- join(process.execPath, '..', '..', 'lib', 'node_modules', 'npm', 'bin', 'npx-cli.js')
25
+ join(process.execPath, '..', '..', 'lib', 'node_modules', 'npm', 'bin', scriptFile)
24
26
  ];
25
27
  for (const candidate of candidates) {
26
28
  if (existsSync(candidate))
@@ -30,7 +32,7 @@ function locateNpxCliScript() {
30
32
  }
31
33
  export function resolveNpxInvocation(npxArgs) {
32
34
  if (process.platform === 'win32') {
33
- const cliScript = locateNpxCliScript();
35
+ const cliScript = locateNpmCliScript('npx-cli.js');
34
36
  if (cliScript !== null) {
35
37
  return {
36
38
  command: process.execPath,
@@ -45,3 +47,25 @@ export function resolveNpxInvocation(npxArgs) {
45
47
  baseEnv: process.env
46
48
  };
47
49
  }
50
+ /**
51
+ * `npm` sibling of `resolveNpxInvocation`, same shim bypass: `node
52
+ * <npm-cli.js> <args>`. A caller must NOT reach for bare `npm` + `shell: true`
53
+ * instead — the shim is the defect, not the justification.
54
+ */
55
+ export function resolveNpmInvocation(npmArgs) {
56
+ if (process.platform === 'win32') {
57
+ const cliScript = locateNpmCliScript('npm-cli.js');
58
+ if (cliScript !== null) {
59
+ return {
60
+ command: process.execPath,
61
+ args: [cliScript, ...npmArgs],
62
+ baseEnv: process.env
63
+ };
64
+ }
65
+ }
66
+ return {
67
+ command: 'npm',
68
+ args: npmArgs,
69
+ baseEnv: process.env
70
+ };
71
+ }
@@ -55,6 +55,11 @@ export declare function resolveMemoryKind(content: string): MemoryKindResolution
55
55
  * `parseStoredMemoryFile` (read path) and the reindex / ingest / doctor
56
56
  * classifiers consume this so there is exactly one kind-resolution rule
57
57
  * in the codebase.
58
+ *
59
+ * Tolerates a leading run of HTML-comment lines (e.g. the
60
+ * `<!-- peaks-memory:start -->` sediment marker) before the opening `---`
61
+ * fence. The closing fence is still required and body extraction is
62
+ * unchanged: the body is the text after the closing fence.
58
63
  */
59
64
  export declare function parseMemoryFrontmatter(content: string): ParsedMemoryFrontmatter;
60
65
  /** Which frontmatter field (or the filename) supplied a memory's name. */
@@ -10,7 +10,10 @@
10
10
  //
11
11
  // 2. `parseStoredMemoryFile` — read-path side. Files in `.peaks/memory/`
12
12
  // are stored as standard YAML frontmatter (name / description /
13
- // metadata.type / metadata.sourceArtifact) followed by the body.
13
+ // metadata.type / metadata.sourceArtifact) followed by the body. A file
14
+ // may also open with HTML-comment lines (the `<!-- peaks-memory:start -->`
15
+ // sediment marker) before the frontmatter; `parseMemoryFrontmatter`
16
+ // skips that leading comment run before looking for the `---` fence.
14
17
  //
15
18
  // Both parsers share the `VALID_MEMORY_KINDS` allow-list (derived from the
16
19
  // canonical `PROJECT_MEMORY_KINDS` tuple in `../types.ts`) and the
@@ -83,23 +86,70 @@ export function resolveMemoryKind(content) {
83
86
  const parsed = parseMemoryFrontmatter(content);
84
87
  return parsed.kind;
85
88
  }
89
+ /** Start of an HTML comment line, allowing leading horizontal whitespace. */
90
+ const LEADING_COMMENT_OPEN = /^[ \t]*<!--/;
91
+ /**
92
+ * Length of the leading run of blank lines and standalone HTML-comment lines.
93
+ *
94
+ * A stored memory may be written with the documented sediment marker
95
+ * (`<!-- peaks-memory:start -->`) — or any HTML comment — BEFORE its YAML
96
+ * frontmatter. This helper reports how much of the file to skip so the fence
97
+ * can still be found. At least one comment line must be present: a file that
98
+ * merely starts with blank lines is not treated as marker-prefixed, so the
99
+ * pre-existing (fence-at-byte-0) behaviour is preserved exactly.
100
+ */
101
+ function leadingCommentPrefixLength(normalized) {
102
+ let offset = 0;
103
+ let sawComment = false;
104
+ for (;;) {
105
+ const rest = normalized.slice(offset);
106
+ const blank = /^[ \t]*\n/.exec(rest);
107
+ if (blank !== null) {
108
+ offset += blank[0].length;
109
+ continue;
110
+ }
111
+ const open = LEADING_COMMENT_OPEN.exec(rest);
112
+ if (open === null)
113
+ break;
114
+ const closeIndex = rest.indexOf('-->', open[0].length);
115
+ if (closeIndex < 0)
116
+ break;
117
+ const afterClose = closeIndex + '-->'.length;
118
+ const lineEnd = rest.indexOf('\n', afterClose);
119
+ if (lineEnd < 0)
120
+ break;
121
+ // Only a whole comment line counts; trailing prose after `-->` means the
122
+ // file does not open with a comment block.
123
+ if (rest.slice(afterClose, lineEnd).trim() !== '')
124
+ break;
125
+ offset += lineEnd + 1;
126
+ sawComment = true;
127
+ }
128
+ return sawComment ? offset : 0;
129
+ }
86
130
  /**
87
131
  * Single parse surface for stored memory frontmatter. Both
88
132
  * `parseStoredMemoryFile` (read path) and the reindex / ingest / doctor
89
133
  * classifiers consume this so there is exactly one kind-resolution rule
90
134
  * in the codebase.
135
+ *
136
+ * Tolerates a leading run of HTML-comment lines (e.g. the
137
+ * `<!-- peaks-memory:start -->` sediment marker) before the opening `---`
138
+ * fence. The closing fence is still required and body extraction is
139
+ * unchanged: the body is the text after the closing fence.
91
140
  */
92
141
  export function parseMemoryFrontmatter(content) {
93
142
  const normalized = content.replace(/\r\n/g, '\n');
94
- if (!normalized.startsWith('---\n')) {
143
+ const head = normalized.slice(leadingCommentPrefixLength(normalized));
144
+ if (!head.startsWith('---\n')) {
95
145
  return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
96
146
  }
97
- const endIndex = normalized.indexOf('\n---\n', 4);
147
+ const endIndex = head.indexOf('\n---\n', 4);
98
148
  if (endIndex < 0) {
99
149
  return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
100
150
  }
101
- const frontmatter = normalized.slice(4, endIndex);
102
- const body = normalized.slice(endIndex + '\n---\n'.length).trim();
151
+ const frontmatter = head.slice(4, endIndex);
152
+ const body = head.slice(endIndex + '\n---\n'.length).trim();
103
153
  let name;
104
154
  let titleField;
105
155
  let description;