peaks-loop 4.0.48 → 4.0.50

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 (156) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/audit-commands.js +1 -0
  5. package/dist/cli/commands/baseline-commands.js +163 -25
  6. package/dist/cli/commands/compact-command.js +1 -3
  7. package/dist/cli/commands/core/skill-command.js +53 -4
  8. package/dist/cli/commands/core/standards-command.d.ts +24 -0
  9. package/dist/cli/commands/core/standards-command.js +74 -0
  10. package/dist/cli/commands/feedback-commands.d.ts +11 -7
  11. package/dist/cli/commands/feedback-commands.js +49 -17
  12. package/dist/cli/commands/final-review-commands.js +12 -0
  13. package/dist/cli/commands/hooks-commands.js +55 -38
  14. package/dist/cli/commands/loop-eval-commands.js +22 -6
  15. package/dist/cli/commands/share-commands.js +37 -11
  16. package/dist/cli/commands/slice-integrate-commands.js +17 -0
  17. package/dist/cli/commands/web-commands.js +8 -1
  18. package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
  19. package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
  20. package/dist/services/adapter/adapter.d.ts +30 -0
  21. package/dist/services/adapter/auto-adapter.d.ts +13 -0
  22. package/dist/services/adapter/claude-adapter.js +12 -0
  23. package/dist/services/adapter/codex-adapter.d.ts +12 -0
  24. package/dist/services/adapter/codex-adapter.js +12 -0
  25. package/dist/services/adapter/copilot-adapter.d.ts +12 -0
  26. package/dist/services/adapter/copilot-adapter.js +12 -0
  27. package/dist/services/artifacts/artifact-prerequisites.js +10 -0
  28. package/dist/services/artifacts/request-artifact-service.js +59 -38
  29. package/dist/services/audit/backing-detector.d.ts +25 -7
  30. package/dist/services/audit/backing-detector.js +33 -17
  31. package/dist/services/audit/enforcer-liveness.d.ts +12 -0
  32. package/dist/services/audit/enforcer-liveness.js +100 -0
  33. package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
  34. package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
  35. package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
  36. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
  37. package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
  38. package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
  39. package/dist/services/audit/enforcers/lint-style.js +38 -2
  40. package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
  41. package/dist/services/audit/prose-ratio-calculator.js +25 -18
  42. package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
  43. package/dist/services/audit/red-lines-service.js +51 -7
  44. package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
  45. package/dist/services/capability-audit-service/independent-checker.js +140 -0
  46. package/dist/services/capability-audit-service/index.d.ts +3 -1
  47. package/dist/services/capability-audit-service/index.js +1 -0
  48. package/dist/services/capability-audit-service/runner.d.ts +17 -13
  49. package/dist/services/capability-audit-service/runner.js +76 -15
  50. package/dist/services/capability-audit-service/types.d.ts +48 -0
  51. package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
  52. package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
  53. package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
  54. package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
  55. package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
  56. package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
  57. package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
  58. package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
  59. package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
  60. package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
  61. package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
  62. package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
  63. package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
  64. package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
  65. package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
  66. package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
  67. package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
  68. package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
  69. package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
  70. package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
  71. package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
  72. package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
  73. package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
  74. package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
  75. package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
  76. package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
  77. package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
  78. package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
  79. package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
  80. package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
  81. package/dist/services/capability-guard-runner/registry.d.ts +5 -0
  82. package/dist/services/capability-guard-runner/registry.js +140 -0
  83. package/dist/services/capability-guard-runner/runner.d.ts +26 -0
  84. package/dist/services/capability-guard-runner/runner.js +63 -6
  85. package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
  86. package/dist/services/code/auto-compact-lifecycle.js +65 -16
  87. package/dist/services/code/auto-compact-modes.d.ts +13 -2
  88. package/dist/services/code/auto-compact-modes.js +20 -4
  89. package/dist/services/code/auto-compact-orchestrator.js +119 -19
  90. package/dist/services/code/compact-event-settle.d.ts +20 -8
  91. package/dist/services/code/compact-event-settle.js +21 -0
  92. package/dist/services/code/post-compact-detector.js +20 -11
  93. package/dist/services/code/step-08-gate.js +21 -6
  94. package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
  95. package/dist/services/config/config-safety.js +11 -9
  96. package/dist/services/context/auto-compact-types.d.ts +20 -2
  97. package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
  98. package/dist/services/feedback/feedback-promotion-service.js +341 -20
  99. package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
  100. package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
  101. package/dist/services/final-review/pre-post-diff.js +10 -2
  102. package/dist/services/job/job-progress-store.js +18 -3
  103. package/dist/services/observability/jsonl-store.d.ts +19 -0
  104. package/dist/services/observability/jsonl-store.js +27 -2
  105. package/dist/services/observability/observability-service.d.ts +11 -4
  106. package/dist/services/observability/observability-service.js +16 -3
  107. package/dist/services/prd/handoff-service.js +43 -0
  108. package/dist/services/qa/qa-business-review-state.js +19 -5
  109. package/dist/services/sc/sc-service.d.ts +8 -0
  110. package/dist/services/sc/sc-service.js +8 -1
  111. package/dist/services/scan/api-diff-types.js +20 -2
  112. package/dist/services/security/safe-settings-path.js +19 -1
  113. package/dist/services/session/getSessionDir.d.ts +33 -0
  114. package/dist/services/session/getSessionDir.js +60 -0
  115. package/dist/services/skill/skill-search-service.d.ts +3 -3
  116. package/dist/services/slice/slice-review-state.js +19 -4
  117. package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
  118. package/dist/services/standards/loop-engineering-lint.js +6 -0
  119. package/dist/services/web/daemon-registry.js +27 -2
  120. package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
  121. package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
  122. package/dist/services/workflow/pipeline-verify-service.js +23 -10
  123. package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
  124. package/dist/services/workspace/claude-settings-template.d.ts +53 -37
  125. package/dist/services/workspace/claude-settings-template.js +105 -83
  126. package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
  127. package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
  128. package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
  129. package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
  130. package/dist/services/workspace/workspace-service.js +11 -1
  131. package/dist/shared/fs-utils.d.ts +26 -0
  132. package/dist/shared/fs-utils.js +35 -0
  133. package/dist/shared/runtime-root.d.ts +73 -0
  134. package/dist/shared/runtime-root.js +77 -0
  135. package/package.json +9 -7
  136. package/scripts/copy-templates.mjs +0 -12
  137. package/scripts/install-skills.mjs +154 -53
  138. package/skills/bee/peaks-qa/SKILL.md +0 -1
  139. package/skills/bee/peaks-rd/SKILL.md +0 -1
  140. package/skills/peaks-code/SKILL.md +12 -10
  141. package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
  142. package/skills/peaks-code/references/runbook.md +3 -0
  143. package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
  144. package/skills/peaks-code/references/startup-sequence.md +2 -2
  145. package/skills/peaks-code/references/step-0-8-gate.md +1 -1
  146. package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
  147. package/dist/cli/commands/context-builder-commands.d.ts +0 -11
  148. package/dist/cli/commands/context-builder-commands.js +0 -85
  149. package/dist/services/hooks/write-gate.js +0 -111
  150. package/skills/bee/peaks-prd/references/command-migration.md +0 -3
  151. package/skills/bee/peaks-qa/references/command-migration.md +0 -3
  152. package/skills/bee/peaks-rd/references/command-migration.md +0 -3
  153. package/skills/bee/peaks-sc/references/command-migration.md +0 -3
  154. package/skills/bee/peaks-txt/references/command-migration.md +0 -3
  155. package/skills/bee/peaks-ui/references/command-migration.md +0 -3
  156. package/skills/peaks-code/references/command-migration.md +0 -3
@@ -2,24 +2,31 @@
2
2
  * Slice 2.0.1-bug3-fact-forcing-bypass — pure-data template for the
3
3
  * consumer-project `.claude/settings.local.json` file.
4
4
  *
5
- * The template is a PreToolUse hook allow-list that bypasses the
6
- * Claude Code [Fact-Forcing Gate] for tool calls whose paths target
7
- * the peaks-managed `.peaks/` workspace. Without this bypass,
8
- * `peaks workspace init` (Step 0 of every peaks-code session) is
9
- * unrunnable in a consumer project because the gate blocks the very
10
- * first Write.
5
+ * The template exempts the peaks-managed `.peaks/` workspace from the
6
+ * Claude Code [Fact-Forcing Gate], so `peaks workspace init` (Step 0 of
7
+ * every peaks-code session) is runnable in a consumer project — without
8
+ * the exemption the gate blocks the very first Write.
9
+ *
10
+ * The exemption is declared in the `env` block
11
+ * (`EXTERNAL_GATE_EXEMPT_ENV`), which is what the gate actually reads.
12
+ * TEMPLATE_VERSION 1.7.0 moved it there; before that it was declared by a
13
+ * `Write|Edit|MultiEdit` handler that exited non-zero for non-`.peaks/`
14
+ * paths and was documented as "fall through to the gate". That concept does
15
+ * not exist in the Claude Code hook protocol (only exit 2 blocks; any other
16
+ * non-zero exit is a NON-BLOCKING ERROR reported once per edit), so the
17
+ * handler was corrected to abstain on every path — and an abstaining handler
18
+ * that is still INSTALLED is a no-op carrying a machine-specific absolute
19
+ * script path. TEMPLATE_VERSION 1.8.0 removed it rather than re-point it:
20
+ * it decided nothing, and the exemption it was written for lives in `env`.
11
21
  *
12
22
  * The template is a pure-data function (no filesystem, no clock) so
13
23
  * it can be unit-tested in isolation and so the on-disk file matches
14
24
  * the in-memory template byte-for-byte.
15
25
  *
16
- * One matcher is emitted:
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.
26
+ * Two `Bash` matchers are emitted (the Step 0.8 mechanical gate and the
27
+ * SOP gate-enforce handler). No `Write|Edit|MultiEdit` entry is emitted:
28
+ * that matcher's gate is `peaks code-gate --json`, installed into the
29
+ * committed `.claude/settings.json` by `peaks hooks install`.
23
30
  *
24
31
  * The previous `Bash` matcher (which whitelisted a fixed `peaks
25
32
  * <subcommand>` prefix) was removed in TEMPLATE_VERSION 1.2.0. The
@@ -80,8 +87,20 @@ export declare const CLAUDE_SETTINGS_LOCAL_FILENAME = ".claude/settings.local.js
80
87
  * (`EXTERNAL_GATE_EXEMPT_ENV`). The comparator now requires the
81
88
  * on-disk file to declare those exemptions too, so a project
82
89
  * installed by an earlier release refreshes once and converges.
90
+ * 1.8.0 — REMOVED the `Write|Edit|MultiEdit` handler and the shipped
91
+ * script it invoked (`src/services/hooks/write-gate.js`, also
92
+ * deleted). The handler abstained on every path by design (see
93
+ * that file's header for the rationale — it is the reason this
94
+ * is a deletion and not a repair), so the only things it still
95
+ * contributed were a `node "<abs path>"` command pinned to the
96
+ * Node version directory that happened to be on `$PATH` at
97
+ * install time, and a `shell: powershell` pin. The exemption it
98
+ * was written to provide is declared by the `env` block above.
99
+ * `mergeTemplateOwnedHooks` drops the retired entry from
100
+ * existing on-disk files, so an earlier install converges
101
+ * instead of keeping a no-op with a stale absolute path.
83
102
  */
84
- export declare const TEMPLATE_VERSION = "1.7.0";
103
+ export declare const TEMPLATE_VERSION = "1.8.0";
85
104
  /**
86
105
  * Compare two serialized template strings: does the on-disk file already
87
106
  * declare every entry the generated tree declares?
@@ -108,10 +127,11 @@ export declare const TEMPLATE_VERSION = "1.7.0";
108
127
  * detail).
109
128
  *
110
129
  * Returns `true` iff both strings parse to objects whose `hooks.PreToolUse`
111
- * arrays satisfy that containment AND the on-disk `env` already carries every
112
- * exemption the template declares (extra on-disk keys and extra globs are
113
- * allowed — a user may exempt other trees, and a requirement the file already
114
- * exceeds must not re-trigger a write).
130
+ * arrays satisfy that containment, the on-disk file carries NO entry this
131
+ * template has retired (`isRetiredTemplateEntry`), AND the on-disk `env`
132
+ * already carries every exemption the template declares (extra on-disk keys
133
+ * and extra globs are allowed — a user may exempt other trees, and a
134
+ * requirement the file already exceeds must not re-trigger a write).
115
135
  *
116
136
  * Returns `false` on any `JSON.parse` error, shape mismatch, or
117
137
  * missing `hooks.PreToolUse`. Whitespace and key order do NOT affect
@@ -147,23 +167,18 @@ export declare function templateContentMatches(generated: string, onDisk: string
147
167
  * Non-conforming entries (no string `matcher`, no `hooks` array) are preserved
148
168
  * rather than dropped: guessing at their shape is how a user's entry gets
149
169
  * deleted.
170
+ *
171
+ * ONE exception to "preserve what I do not declare": an entry this template
172
+ * used to declare and RETIRED (TEMPLATE_VERSION 1.8.0's `Write|Edit|MultiEdit`
173
+ * gate — see `isRetiredTemplateEntry`) is dropped rather than preserved.
174
+ * Declaring less cannot retire an entry on its own, because preserving
175
+ * undeclared entries is exactly what this function does; without the drop, a
176
+ * pre-1.8.0 install would keep the no-op handler and its version-pinned script
177
+ * path forever. The predicate is narrow enough that only the exact command
178
+ * this template emitted matches.
150
179
  */
151
180
  export declare function mergeTemplateOwnedHooks(onDisk: ReadonlyArray<unknown>, template: ReadonlyArray<unknown>): unknown[];
152
- /**
153
- * Absolute path of the shipped Write|Edit|MultiEdit gate script.
154
- *
155
- * `write-gate.js` is a plain `.js` (not a compiled `.ts`) precisely so this
156
- * single relative filename resolves in BOTH trees: `src/services/hooks/` for
157
- * `tsx` / vitest, `dist/services/hooks/` for an installed consumer (copied by
158
- * `scripts/copy-templates.mjs`; the `dist/**` `*.js` glob in
159
- * `package.json#files` already ships it).
160
- *
161
- * Separators are normalized to `/` so the emitted command contains no
162
- * backslash at all. That is what makes the handler shell-agnostic: bash
163
- * reduces `\X` inside `"..."` and PowerShell escapes with a backtick, so any
164
- * backslash in the string is one dialect's problem waiting to happen.
165
- */
166
- export declare function writeGateScriptPath(): string;
181
+ export declare function isRetiredTemplateEntry(entry: unknown): boolean;
167
182
  type ClaudeHookCommand = {
168
183
  type: 'command';
169
184
  command: string;
@@ -192,10 +207,11 @@ type ClaudeSettingsLocal = {
192
207
  * forcing gate is a core feature that PreToolUse hooks can short-
193
208
  * circuit but that the `permissions` block cannot.
194
209
  *
195
- * As of TEMPLATE_VERSION 1.2.0, only the `Write|Edit|MultiEdit`
196
- * matcher is emitted. Bash command enforcement is the responsibility
197
- * of `peaks gate enforce`, which `peaks hooks install` injects into
198
- * `.claude/settings.json` (not `.claude/settings.local.json`).
210
+ * As of TEMPLATE_VERSION 1.8.0 the template emits the two `Bash` matchers
211
+ * only. The `Write|Edit|MultiEdit` fact-forcing bypass is no longer a hook:
212
+ * it is the `env` exemption above, and that matcher's gate
213
+ * (`peaks code-gate --json`) is installed into the committed
214
+ * `.claude/settings.json` by `peaks hooks install`.
199
215
  */
200
216
  export declare function buildClaudeSettingsLocalJson(): ClaudeSettingsLocal;
201
217
  export {};
@@ -2,24 +2,31 @@
2
2
  * Slice 2.0.1-bug3-fact-forcing-bypass — pure-data template for the
3
3
  * consumer-project `.claude/settings.local.json` file.
4
4
  *
5
- * The template is a PreToolUse hook allow-list that bypasses the
6
- * Claude Code [Fact-Forcing Gate] for tool calls whose paths target
7
- * the peaks-managed `.peaks/` workspace. Without this bypass,
8
- * `peaks workspace init` (Step 0 of every peaks-code session) is
9
- * unrunnable in a consumer project because the gate blocks the very
10
- * first Write.
5
+ * The template exempts the peaks-managed `.peaks/` workspace from the
6
+ * Claude Code [Fact-Forcing Gate], so `peaks workspace init` (Step 0 of
7
+ * every peaks-code session) is runnable in a consumer project — without
8
+ * the exemption the gate blocks the very first Write.
9
+ *
10
+ * The exemption is declared in the `env` block
11
+ * (`EXTERNAL_GATE_EXEMPT_ENV`), which is what the gate actually reads.
12
+ * TEMPLATE_VERSION 1.7.0 moved it there; before that it was declared by a
13
+ * `Write|Edit|MultiEdit` handler that exited non-zero for non-`.peaks/`
14
+ * paths and was documented as "fall through to the gate". That concept does
15
+ * not exist in the Claude Code hook protocol (only exit 2 blocks; any other
16
+ * non-zero exit is a NON-BLOCKING ERROR reported once per edit), so the
17
+ * handler was corrected to abstain on every path — and an abstaining handler
18
+ * that is still INSTALLED is a no-op carrying a machine-specific absolute
19
+ * script path. TEMPLATE_VERSION 1.8.0 removed it rather than re-point it:
20
+ * it decided nothing, and the exemption it was written for lives in `env`.
11
21
  *
12
22
  * The template is a pure-data function (no filesystem, no clock) so
13
23
  * it can be unit-tested in isolation and so the on-disk file matches
14
24
  * the in-memory template byte-for-byte.
15
25
  *
16
- * One matcher is emitted:
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.
26
+ * Two `Bash` matchers are emitted (the Step 0.8 mechanical gate and the
27
+ * SOP gate-enforce handler). No `Write|Edit|MultiEdit` entry is emitted:
28
+ * that matcher's gate is `peaks code-gate --json`, installed into the
29
+ * committed `.claude/settings.json` by `peaks hooks install`.
23
30
  *
24
31
  * The previous `Bash` matcher (which whitelisted a fixed `peaks
25
32
  * <subcommand>` prefix) was removed in TEMPLATE_VERSION 1.2.0. The
@@ -33,8 +40,6 @@
33
40
  * consumer project's `.claude/settings.json` and which exits 0
34
41
  * silently for any command not guarded by a registered SOP gate.
35
42
  */
36
- import { dirname, resolve } from 'node:path';
37
- import { fileURLToPath } from 'node:url';
38
43
  import { EXTERNAL_GATE_EXEMPT_ENV, hasExternalGateExemptions, resolveHookShell, resolveHookSpec } from '../skills/hooks-codegate-superpowers.js';
39
44
  export const CLAUDE_SETTINGS_LOCAL_FILENAME = '.claude/settings.local.json';
40
45
  /**
@@ -83,8 +88,20 @@ export const CLAUDE_SETTINGS_LOCAL_FILENAME = '.claude/settings.local.json';
83
88
  * (`EXTERNAL_GATE_EXEMPT_ENV`). The comparator now requires the
84
89
  * on-disk file to declare those exemptions too, so a project
85
90
  * installed by an earlier release refreshes once and converges.
91
+ * 1.8.0 — REMOVED the `Write|Edit|MultiEdit` handler and the shipped
92
+ * script it invoked (`src/services/hooks/write-gate.js`, also
93
+ * deleted). The handler abstained on every path by design (see
94
+ * that file's header for the rationale — it is the reason this
95
+ * is a deletion and not a repair), so the only things it still
96
+ * contributed were a `node "<abs path>"` command pinned to the
97
+ * Node version directory that happened to be on `$PATH` at
98
+ * install time, and a `shell: powershell` pin. The exemption it
99
+ * was written to provide is declared by the `env` block above.
100
+ * `mergeTemplateOwnedHooks` drops the retired entry from
101
+ * existing on-disk files, so an earlier install converges
102
+ * instead of keeping a no-op with a stale absolute path.
86
103
  */
87
- export const TEMPLATE_VERSION = '1.7.0';
104
+ export const TEMPLATE_VERSION = '1.8.0';
88
105
  /**
89
106
  * Compare two serialized template strings: does the on-disk file already
90
107
  * declare every entry the generated tree declares?
@@ -111,10 +128,11 @@ export const TEMPLATE_VERSION = '1.7.0';
111
128
  * detail).
112
129
  *
113
130
  * Returns `true` iff both strings parse to objects whose `hooks.PreToolUse`
114
- * arrays satisfy that containment AND the on-disk `env` already carries every
115
- * exemption the template declares (extra on-disk keys and extra globs are
116
- * allowed — a user may exempt other trees, and a requirement the file already
117
- * exceeds must not re-trigger a write).
131
+ * arrays satisfy that containment, the on-disk file carries NO entry this
132
+ * template has retired (`isRetiredTemplateEntry`), AND the on-disk `env`
133
+ * already carries every exemption the template declares (extra on-disk keys
134
+ * and extra globs are allowed — a user may exempt other trees, and a
135
+ * requirement the file already exceeds must not re-trigger a write).
118
136
  *
119
137
  * Returns `false` on any `JSON.parse` error, shape mismatch, or
120
138
  * missing `hooks.PreToolUse`. Whitespace and key order do NOT affect
@@ -151,6 +169,23 @@ export function templateContentMatches(generated, onDisk) {
151
169
  }
152
170
  unmatched.splice(at, 1);
153
171
  }
172
+ // A RETIRED entry on disk is drift, and this clause is what makes the
173
+ // retirement in `mergeTemplateOwnedHooks` reach an installed file at all.
174
+ //
175
+ // Containment alone cannot express it: a file still carrying
176
+ // `Write|Edit|MultiEdit` declares every entry the template declares, so
177
+ // `templateContentMatches` answered "current", no rewrite ran, and the merge
178
+ // never got the chance to drop it. Measured on a throwaway project root
179
+ // before this clause existed — init against the rebuilt CLI reported
180
+ // `already-current` and the retired entry survived verbatim. Declaring less
181
+ // is not a retirement; the comparator has to say so.
182
+ //
183
+ // One extra rewrite per affected install, then the fixed point holds: the
184
+ // merge emits no retired entry, so the next comparison finds none and
185
+ // answers `current`.
186
+ if (parsedOnDisk.hooks.PreToolUse.some((entry) => isRetiredTemplateEntry(entry))) {
187
+ return false;
188
+ }
154
189
  // A project installed by a release that predates a template-declared
155
190
  // exemption still needs the refresh this comparator gates — otherwise the
156
191
  // entry would only ever appear on a machine that re-ran `peaks hooks
@@ -184,6 +219,15 @@ export function templateContentMatches(generated, onDisk) {
184
219
  * Non-conforming entries (no string `matcher`, no `hooks` array) are preserved
185
220
  * rather than dropped: guessing at their shape is how a user's entry gets
186
221
  * deleted.
222
+ *
223
+ * ONE exception to "preserve what I do not declare": an entry this template
224
+ * used to declare and RETIRED (TEMPLATE_VERSION 1.8.0's `Write|Edit|MultiEdit`
225
+ * gate — see `isRetiredTemplateEntry`) is dropped rather than preserved.
226
+ * Declaring less cannot retire an entry on its own, because preserving
227
+ * undeclared entries is exactly what this function does; without the drop, a
228
+ * pre-1.8.0 install would keep the no-op handler and its version-pinned script
229
+ * path forever. The predicate is narrow enough that only the exact command
230
+ * this template emitted matches.
187
231
  */
188
232
  export function mergeTemplateOwnedHooks(onDisk, template) {
189
233
  const slots = new Map();
@@ -195,6 +239,10 @@ export function mergeTemplateOwnedHooks(onDisk, template) {
195
239
  const taken = new Map();
196
240
  const preserved = [];
197
241
  for (const entry of onDisk) {
242
+ // Retired by this template — dropped, not carried across.
243
+ if (isRetiredTemplateEntry(entry)) {
244
+ continue;
245
+ }
198
246
  // Unowned by construction: not a shape the template could have declared.
199
247
  if (!isPreToolUseEntry(entry)) {
200
248
  preserved.push(entry);
@@ -249,44 +297,36 @@ function sameHooksArray(a, b) {
249
297
  return true;
250
298
  }
251
299
  /**
252
- * This module's own directory — `<root>/src/services/workspace` in the
253
- * source tree, `<root>/dist/services/workspace` in a build.
254
- *
255
- * Anchored on the running module rather than on `process.argv[1]`: the
256
- * same reason `daemon-supervisor.ts` documents — `argv[1]` is a different
257
- * file in each way the CLI is entered (`bin/peaks.js`, `src/cli/index.ts`
258
- * under tsx, `dist/cli/index.js` when invoked directly), whereas the
259
- * module's own location is the one fact that is always true.
260
- */
261
- const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
262
- /**
263
- * Absolute path of the shipped Write|Edit|MultiEdit gate script.
264
- *
265
- * `write-gate.js` is a plain `.js` (not a compiled `.ts`) precisely so this
266
- * single relative filename resolves in BOTH trees: `src/services/hooks/` for
267
- * `tsx` / vitest, `dist/services/hooks/` for an installed consumer (copied by
268
- * `scripts/copy-templates.mjs`; the `dist/**` `*.js` glob in
269
- * `package.json#files` already ships it).
300
+ * The retired `Write|Edit|MultiEdit` gate entry, as an on-disk file written by
301
+ * a pre-1.8.0 release holds it.
270
302
  *
271
- * Separators are normalized to `/` so the emitted command contains no
272
- * backslash at all. That is what makes the handler shell-agnostic: bash
273
- * reduces `\X` inside `"..."` and PowerShell escapes with a backtick, so any
274
- * backslash in the string is one dialect's problem waiting to happen.
275
- */
276
- export function writeGateScriptPath() {
277
- return resolve(MODULE_DIR, '..', 'hooks', 'write-gate.js').replaceAll('\\', '/');
278
- }
279
- /**
280
- * Build the Write|Edit|MultiEdit matcher command.
303
+ * TEMPLATE_VERSION 1.8.0 stopped emitting this entry. Declaring less is not
304
+ * enough on its own: `mergeTemplateOwnedHooks` preserves every on-disk entry
305
+ * the template does not declare — that is the whole point of the entry-level
306
+ * ownership rule (it is what keeps `installAutoCompactHook`'s `Bash|Task`
307
+ * entry alive) — so a project installed by an earlier release would keep the
308
+ * no-op handler, and its `node "C:/…/nvm/v24.14.0/…"` path, forever. This
309
+ * predicate is the retirement: `mergeTemplateOwnedHooks` drops a match.
281
310
  *
282
- * TEMPLATE_VERSION 1.6.0: `node "<script>"` with NO inline payload. The
283
- * decision lives in `src/services/hooks/write-gate.js` and was relocated
284
- * there verbatim. Because there is nothing left to escape, this handler is
285
- * shell-dialect-independent and can carry the same platform `shell` pin as
286
- * its Bash siblings (see `resolveHookShell`).
311
+ * Deliberately narrow. It matches the exact command shape this template used
312
+ * to emit — one handler, `node "<…>/services/hooks/write-gate.js"` — under the
313
+ * exact legacy matcher spelling, so a user's OWN `Write|Edit|MultiEdit` entry
314
+ * (a different command, or more than one handler) is preserved like any other
315
+ * entry the template does not declare. A looser "drop anything on this
316
+ * matcher" rule would delete a user's hook, which is the failure the ownership
317
+ * rule exists to prevent.
287
318
  */
288
- function buildWriteHookCommand() {
289
- return `node "${writeGateScriptPath()}"`;
319
+ const RETIRED_WRITE_GATE_MATCHER = 'Write|Edit|MultiEdit';
320
+ const RETIRED_WRITE_GATE_COMMAND = /^node "[^"]*\/services\/hooks\/write-gate\.js"$/;
321
+ export function isRetiredTemplateEntry(entry) {
322
+ if (!isPreToolUseEntry(entry))
323
+ return false;
324
+ if (entry.matcher !== RETIRED_WRITE_GATE_MATCHER)
325
+ return false;
326
+ if (entry.hooks.length !== 1)
327
+ return false;
328
+ const handler = entry.hooks[0];
329
+ return handler.type === 'command' && RETIRED_WRITE_GATE_COMMAND.test(handler.command);
290
330
  }
291
331
  /**
292
332
  * TEMPLATE_VERSION 1.4.0 — the SOP gate-enforce handler, read from the same
@@ -311,39 +351,22 @@ function buildGateEnforceHandler() {
311
351
  * forcing gate is a core feature that PreToolUse hooks can short-
312
352
  * circuit but that the `permissions` block cannot.
313
353
  *
314
- * As of TEMPLATE_VERSION 1.2.0, only the `Write|Edit|MultiEdit`
315
- * matcher is emitted. Bash command enforcement is the responsibility
316
- * of `peaks gate enforce`, which `peaks hooks install` injects into
317
- * `.claude/settings.json` (not `.claude/settings.local.json`).
354
+ * As of TEMPLATE_VERSION 1.8.0 the template emits the two `Bash` matchers
355
+ * only. The `Write|Edit|MultiEdit` fact-forcing bypass is no longer a hook:
356
+ * it is the `env` exemption above, and that matcher's gate
357
+ * (`peaks code-gate --json`) is installed into the committed
358
+ * `.claude/settings.json` by `peaks hooks install`.
318
359
  */
319
360
  export function buildClaudeSettingsLocalJson() {
320
- // TEMPLATE_VERSION 1.6.0 — the write handler can now be shell-pinned for the
321
- // same Windows reason as the two Bash handlers below: a shell-form command is
322
- // executed by Git Bash / MSYS2, which force-allocates a console window on
323
- // every matching tool call. It could NOT take the pin while its payload was
324
- // inlined JavaScript, because PowerShell does not perform bash's backslash
325
- // reduction and would have corrupted the payload. `undefined` on POSIX omits
326
- // the key entirely.
327
- const writeShell = resolveHookShell();
328
361
  return {
329
- // Slice emit-gateguard-exemption — the third-party gate exemption. Peaks
330
- // already bypasses its OWN fact-forcing gate for `.peaks/**` (the
331
- // Write|Edit|MultiEdit handler below); this is the same intent declared in
332
- // the currency an external PreToolUse gate reads. `peaks hooks install`
333
- // merges the same row into this file, so the two writers agree.
362
+ // Slice emit-gateguard-exemption — the third-party gate exemption. This is
363
+ // now the ONLY mechanism carrying the `.peaks/**` bypass (TEMPLATE_VERSION
364
+ // 1.8.0 removed the abstaining `Write|Edit|MultiEdit` handler that used to
365
+ // be described here). `peaks hooks install` merges the same row into this
366
+ // file, so the two writers agree.
334
367
  env: { ...EXTERNAL_GATE_EXEMPT_ENV },
335
368
  hooks: {
336
369
  PreToolUse: [
337
- {
338
- matcher: 'Write|Edit|MultiEdit',
339
- hooks: [
340
- {
341
- type: 'command',
342
- command: buildWriteHookCommand(),
343
- ...(writeShell !== undefined ? { shell: writeShell } : {})
344
- }
345
- ]
346
- },
347
370
  {
348
371
  // v3.1.2 Step 0.8 — Mechanical PreToolUse gate. Runs
349
372
  // `peaks code gate-step-08 --project .` before every Bash
@@ -351,7 +374,6 @@ export function buildClaudeSettingsLocalJson() {
351
374
  // describing the decision + optional `Next: slice #N+1 of
352
375
  // M (<currentSlice>)` line when progress.json exists). Exit
353
376
  // 2 = block (stderr contains the BLOCKED: ... reason).
354
- // The existing Write|Edit|MultiEdit matcher is preserved.
355
377
  matcher: 'Bash',
356
378
  hooks: [buildBashGateStep08Handler()]
357
379
  },
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Version stamp for the artifacts `peaks workspace init` generates into a
3
+ * consumer project (G4, 2026-09-15).
4
+ *
5
+ * WHY THIS EXISTS. `initWorkspace` is the only writer of a consumer project's
6
+ * generated config: `.claude/settings.local.json`, the offline
7
+ * `.peaks/.claude-settings-template.json` copy, and the managed `.gitignore`
8
+ * snippet. `ensureSession` early-returns as soon as a session is bound —
9
+ * deliberately, see its own comment — so after the FIRST init nothing ever
10
+ * re-runs the generator. `npm i -g peaks-loop@<newer>` therefore upgrades the
11
+ * CLI and leaves the project's generated config exactly as the OLD release
12
+ * wrote it.
13
+ *
14
+ * `.claude/settings.local.json` IS drift-checked against the current template
15
+ * — but only when something calls `initWorkspace`, which is precisely the
16
+ * event that stopped happening. The escape hatch (`peaks upgrade
17
+ * --apply-init`) is real and idempotent. What was missing is anything that
18
+ * tells the user — or the LLM driving them — that they need it. That is the
19
+ * gap this module closes: a stamp the generator writes, and a detector that
20
+ * can answer "the file on disk was produced by 4.0.40 while the installed
21
+ * peaks-loop is 4.0.49".
22
+ *
23
+ * WHY A SEPARATE FILE AND NOT A KEY INSIDE THE ARTIFACTS.
24
+ * `.claude/settings.local.json` is read by Claude Code, which owns its schema;
25
+ * a version key peaks invented has no contract there and could be rejected or
26
+ * silently ignored — a stamp nobody reads is worse than none. `.peaks/_runtime/`
27
+ * is peaks' own gitignored tree and already holds exactly this kind of
28
+ * machine-local bookkeeping (`session.json`, `.outer-session-cache.json`), so
29
+ * the stamp lives beside them and needs no new `.gitignore` line.
30
+ *
31
+ * WHAT IS COMPARED. Two versions, because they move independently and a
32
+ * mismatch of either means the same thing to the user (regenerate):
33
+ * - `packageVersion` — the installed peaks-loop release. Moves on `npm i -g`.
34
+ * - `templateVersion` — `TEMPLATE_VERSION`, the shape of the hooks tree this
35
+ * release emits. Can move without a release the user would notice.
36
+ *
37
+ * The stamp records what the generator LAST WROTE. It is not a promise that
38
+ * the on-disk artifacts still match it — a user may have hand-edited
39
+ * `.claude/settings.local.json` since. That question is the existing
40
+ * `templateContentMatches` comparator's, and it is asked on the next init,
41
+ * which is what this detector tells the user to trigger.
42
+ */
43
+ /**
44
+ * Where the stamp lives, relative to the project root.
45
+ *
46
+ * `.peaks/_runtime/` is gitignored by every peaks-loop project already (see
47
+ * the root `.gitignore` snippet), so this file never shows up in
48
+ * `git status` — which matters, because the whole point is that its content
49
+ * changes on the user's machine and not in their repository.
50
+ */
51
+ export declare const GENERATED_ARTIFACTS_STAMP_RELATIVE_PATH: string;
52
+ /**
53
+ * The artifacts whose generation the stamp describes. Used to answer "is
54
+ * there anything on disk that could be stale?" — a project that has never run
55
+ * `peaks workspace init` has nothing to refresh and must not be nagged.
56
+ */
57
+ export declare const GENERATED_ARTIFACT_RELATIVE_PATHS: ReadonlyArray<string>;
58
+ export type GeneratedArtifactsStamp = {
59
+ readonly stampVersion: 1;
60
+ readonly packageVersion: string;
61
+ readonly templateVersion: string;
62
+ readonly writtenAt: string;
63
+ };
64
+ export type GeneratedArtifactsStaleness = {
65
+ /** `true` only when a generated artifact exists AND its stamp is behind. */
66
+ readonly stale: boolean;
67
+ /**
68
+ * Machine-readable reason codes, empty when `stale` is false:
69
+ * - `package-upgraded` — on-disk stamp names an older peaks-loop release
70
+ * - `template-changed` — on-disk stamp names an older template shape
71
+ * - `unstamped` — artifacts exist but predate this stamp
72
+ * - `stamp-unreadable` — a stamp file exists but is not parseable
73
+ */
74
+ readonly reasons: ReadonlyArray<string>;
75
+ readonly onDisk: GeneratedArtifactsStamp | null;
76
+ readonly expected: {
77
+ readonly packageVersion: string;
78
+ readonly templateVersion: string;
79
+ };
80
+ };
81
+ export declare function generatedArtifactsStampPath(projectRoot: string): string;
82
+ /**
83
+ * Read the stamp, or `null` when there is none / it is not the shape this
84
+ * module writes. Tolerant on purpose: a malformed stamp must degrade to
85
+ * "unstamped", never to a crash on the per-turn read path.
86
+ *
87
+ * The catch still NAMES what it tolerates rather than swallowing everything —
88
+ * the same correction the two P1 sites in this slice got (see
89
+ * `isExpectedFsMiss`). A missing file (raced away between `existsSync` and the
90
+ * read) and a stamp we cannot parse both mean "no usable stamp"; a
91
+ * `TypeError` from a broken dependency does not.
92
+ */
93
+ export declare function readGeneratedArtifactsStamp(projectRoot: string): GeneratedArtifactsStamp | null;
94
+ /**
95
+ * Record what the generator just wrote. Called by `initWorkspace` on every
96
+ * successful materialization — including the one that changes nothing —
97
+ * because the stamp's job is "when did a release last regenerate this
98
+ * project", not "when did the bytes change".
99
+ *
100
+ * `packageVersion` / `now` are injectable so a test can write a deliberately
101
+ * old stamp without touching the clock or the package.
102
+ */
103
+ export declare function writeGeneratedArtifactsStamp(projectRoot: string, options?: {
104
+ readonly packageVersion?: string;
105
+ readonly now?: Date;
106
+ }): GeneratedArtifactsStamp;
107
+ /**
108
+ * Is this project's generated config behind the installed peaks-loop?
109
+ *
110
+ * Returns `stale: false` — with no reasons — for the two cases that are NOT
111
+ * staleness:
112
+ * - the project has no generated artifact at all (never initialized), and
113
+ * - the stamp matches both expected versions.
114
+ *
115
+ * `unstamped` is reported only when an artifact EXISTS and no stamp does:
116
+ * those are projects initialized by a release that predates this module, and
117
+ * they are exactly the population the defect was reported against.
118
+ */
119
+ export declare function detectStaleGeneratedArtifacts(projectRoot: string): GeneratedArtifactsStaleness;