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.
- package/CHANGELOG.md +44 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/compact-command.js +1 -3
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -0
- package/dist/cli/commands/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +55 -38
- package/dist/cli/commands/loop-eval-commands.js +22 -6
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/slice-integrate-commands.js +17 -0
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/artifacts/artifact-prerequisites.js +10 -0
- package/dist/services/artifacts/request-artifact-service.js +59 -38
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +14 -1
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +75 -0
- package/dist/services/code/auto-compact-lifecycle.js +65 -16
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/auto-compact-orchestrator.js +119 -19
- package/dist/services/code/compact-event-settle.d.ts +20 -8
- package/dist/services/code/compact-event-settle.js +21 -0
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +11 -4
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/prd/handoff-service.js +43 -0
- package/dist/services/qa/qa-business-review-state.js +19 -5
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/slice/slice-review-state.js +19 -4
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workflow/pipeline-verify-gate-support.js +10 -11
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +23 -10
- package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-qa/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +0 -1
- package/skills/peaks-code/SKILL.md +12 -10
- package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
- package/skills/peaks-code/references/runbook.md +3 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
- package/skills/peaks-code/references/startup-sequence.md +2 -2
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- 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
|
|
6
|
-
* Claude Code [Fact-Forcing Gate]
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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.
|
|
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
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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.
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
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
|
|
6
|
-
* Claude Code [Fact-Forcing Gate]
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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.
|
|
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
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
-
*
|
|
253
|
-
*
|
|
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
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
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
|
-
|
|
289
|
-
|
|
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.
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
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.
|
|
330
|
-
//
|
|
331
|
-
// Write|Edit|MultiEdit handler
|
|
332
|
-
//
|
|
333
|
-
//
|
|
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;
|