peaks-loop 4.0.49 → 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 +24 -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/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/hooks-commands.js +55 -38
- package/dist/cli/commands/share-commands.js +37 -11
- 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/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/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-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/observability/observability-service.d.ts +1 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- 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/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/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
|
@@ -173,15 +173,17 @@ export function resolveCanonicalProjectRootStrict(startPath) {
|
|
|
173
173
|
catch {
|
|
174
174
|
throw new InvalidProjectRootError('non-existent', startPath);
|
|
175
175
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
176
|
+
// Use the realpath form as the canonical input. macOS exposes
|
|
177
|
+
// `/var/folders/...` as a symlink to `/private/var/folders/...`, so any
|
|
178
|
+
// path returned by `os.tmpdir()` (and therefore any `mkdtempSync(...)`
|
|
179
|
+
// result) has `realStart !== start`. Rejecting that case as
|
|
180
|
+
// "non-canonical" would block the legitimate SessionStart hook path
|
|
181
|
+
// and the test fixtures under `tests/unit/hooks/`. The security
|
|
182
|
+
// property is preserved: `realpathSync` collapses any attacker-
|
|
183
|
+
// controlled symlink chain, so `realStart` IS the canonical form
|
|
184
|
+
// regardless of which prefix the caller used.
|
|
185
|
+
const canonical = resolveCanonicalProjectRoot(realStart);
|
|
186
|
+
if (canonical === realStart) {
|
|
185
187
|
return canonical;
|
|
186
188
|
}
|
|
187
189
|
// Canonicalization moved the path (e.g. to a git root) — accept it
|
|
@@ -123,13 +123,21 @@ function isSourcePath(path) {
|
|
|
123
123
|
* read as two deleted cases; a class that is half-counted is worse than a class
|
|
124
124
|
* that is not counted at all.
|
|
125
125
|
*
|
|
126
|
+
* Word-boundary simulation, not `\b`. The JS regex could spell this as `\b`
|
|
127
|
+
* directly, but the POSIX ERE side of the count has to run on macOS too —
|
|
128
|
+
* where `git grep -E` is BSD grep, and BSD grep's POSIX ERE has no `\b`
|
|
129
|
+
* (it is a literal `b`). The two expressions therefore both spell the boundary
|
|
130
|
+
* as `(^|[^A-Za-z0-9_])`: start of line OR a non-word character. The two
|
|
131
|
+
* sides MUST agree, and `git grep -c` counts matched lines (not occurrences)
|
|
132
|
+
* so the extra prefix group does not shift the count.
|
|
133
|
+
*
|
|
126
134
|
* What is deliberately NOT modelled: an occurrence inside a comment or a string
|
|
127
135
|
* literal still matches. That is symmetric — the same expression is evaluated on
|
|
128
136
|
* both sides — so it cancels out of the DELTA, which is the only thing a removal
|
|
129
137
|
* is ever derived from.
|
|
130
138
|
*/
|
|
131
|
-
const TEST_CASE_LINE_RE =
|
|
132
|
-
const TEST_CASE_ERE = '
|
|
139
|
+
const TEST_CASE_LINE_RE = /(?:^|[^A-Za-z0-9_])(it|test)(\.[A-Za-z]+)*\(/;
|
|
140
|
+
const TEST_CASE_ERE = '(^|[^A-Za-z0-9_])(it|test)(\\.[A-Za-z]+)*\\(';
|
|
133
141
|
/**
|
|
134
142
|
* Column-0 `export` — an export STATEMENT, not a type-checked symbol.
|
|
135
143
|
*
|
|
@@ -29,8 +29,8 @@ export declare const ObservabilityEventSchema: z.ZodObject<{
|
|
|
29
29
|
ts: z.ZodString;
|
|
30
30
|
sessionId: z.ZodString;
|
|
31
31
|
category: z.ZodEnum<{
|
|
32
|
-
dispatch: "dispatch";
|
|
33
32
|
"slice-transition": "slice-transition";
|
|
33
|
+
dispatch: "dispatch";
|
|
34
34
|
checkpoint: "checkpoint";
|
|
35
35
|
"mode-gate": "mode-gate";
|
|
36
36
|
"context-trigger": "context-trigger";
|
|
@@ -8,7 +8,25 @@
|
|
|
8
8
|
* is not merely annotated. `confidence: 'exact'` is reserved for lines where
|
|
9
9
|
* both sides were parsed AND the recorded interface was fully readable.
|
|
10
10
|
*/
|
|
11
|
+
import { realpathSync } from 'node:fs';
|
|
11
12
|
import { resolve, sep } from 'node:path';
|
|
13
|
+
/**
|
|
14
|
+
* Canonicalize through symlinks so two paths that name the same directory through
|
|
15
|
+
* different prefixes (the macOS `/var` <-> `/private/var` quirk the brief calls
|
|
16
|
+
* out) line up as the same literal on both sides of the prefix test below.
|
|
17
|
+
* Falls back to `resolve()` when the path does not exist yet — `realpathSync`
|
|
18
|
+
* throws on missing paths, and several callers in the suite inspect not-yet-
|
|
19
|
+
* created paths (e.g. the "string prefix of a sibling" guard at
|
|
20
|
+
* api-diff-service.test.ts:462).
|
|
21
|
+
*/
|
|
22
|
+
function safeRealpath(p) {
|
|
23
|
+
try {
|
|
24
|
+
return realpathSync(p);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return resolve(p);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
12
30
|
/** The honest boundary of the feature. Printed in the output, not just documented. */
|
|
13
31
|
export const NOT_DETECTABLE = [
|
|
14
32
|
'a field whose type is unchanged but whose meaning changed',
|
|
@@ -38,8 +56,8 @@ export function isRecord(value) {
|
|
|
38
56
|
* the misleading `archive/docs/api.json`.
|
|
39
57
|
*/
|
|
40
58
|
export function toDisplayPath(projectRoot, file) {
|
|
41
|
-
const rootParts =
|
|
42
|
-
const fileParts =
|
|
59
|
+
const rootParts = safeRealpath(projectRoot).split(sep);
|
|
60
|
+
const fileParts = safeRealpath(file).split(sep);
|
|
43
61
|
const inside = fileParts.length > rootParts.length
|
|
44
62
|
&& rootParts.every((part, index) => part === fileParts[index]);
|
|
45
63
|
return (inside ? fileParts.slice(rootParts.length) : fileParts).join('/');
|
|
@@ -71,12 +71,30 @@ export function assertSafeDispatchRecordPath(recordPath, projectRoot) {
|
|
|
71
71
|
// to create it). Fall back to lexical comparison against the
|
|
72
72
|
// canonical projectRoot — the write will then create the file,
|
|
73
73
|
// and any symlink in the parent will be caught on the next read.
|
|
74
|
+
//
|
|
75
|
+
// macOS note: the project's own realpath may still resolve the
|
|
76
|
+
// `/var -> /private/var` symlink even when the sub-agent dir does
|
|
77
|
+
// not exist yet, so the caller can compare on a stable prefix
|
|
78
|
+
// instead of having one side of the comparison on `/var/...` and
|
|
79
|
+
// the other on `/private/var/...`. We still validate that the
|
|
80
|
+
// canonical record path lives under the canonical projectRoot.
|
|
74
81
|
const fallback = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
|
|
75
82
|
const rel2 = relative(fallback, recordPath);
|
|
76
83
|
if (rel2.startsWith('..') || isAbsolute(rel2)) {
|
|
77
84
|
throw invalidPathError(recordPath, 'must be under .peaks/_sub_agents/');
|
|
78
85
|
}
|
|
79
|
-
|
|
86
|
+
try {
|
|
87
|
+
realRoot = realpathSync(projectRoot);
|
|
88
|
+
const canonicalRecord = resolve(realRoot, '.peaks', SUB_AGENTS_DIR, recordPath.slice(fallback.length + 1));
|
|
89
|
+
const realRel = relative(realRoot, canonicalRecord);
|
|
90
|
+
if (realRel.startsWith('..' + sep) || realRel === '..' || isAbsolute(realRel)) {
|
|
91
|
+
throw invalidPathError(recordPath, 'escapes project root via symlink');
|
|
92
|
+
}
|
|
93
|
+
return canonicalRecord;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return recordPath;
|
|
97
|
+
}
|
|
80
98
|
}
|
|
81
99
|
const realRel = relative(realRoot, realRecord);
|
|
82
100
|
if (realRel.startsWith('..' + sep) || realRel === '..' || isAbsolute(realRel)) {
|
|
@@ -32,14 +32,14 @@ export declare const SkillSearchInputSchema: z.ZodObject<{
|
|
|
32
32
|
status: "status";
|
|
33
33
|
resume: "resume";
|
|
34
34
|
audit: "audit";
|
|
35
|
-
|
|
35
|
+
"final-review": "final-review";
|
|
36
36
|
content: "content";
|
|
37
|
+
ide: "ide";
|
|
37
38
|
test: "test";
|
|
38
|
-
research: "research";
|
|
39
39
|
doctor: "doctor";
|
|
40
|
+
research: "research";
|
|
40
41
|
triage: "triage";
|
|
41
42
|
sop: "sop";
|
|
42
|
-
"final-review": "final-review";
|
|
43
43
|
"slice-decompose": "slice-decompose";
|
|
44
44
|
"issue-fix-orchestrator": "issue-fix-orchestrator";
|
|
45
45
|
"perf-audit": "perf-audit";
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `peaks standards lint --category loop-engineering` (registered in M0's
|
|
19
19
|
* plan) will read this file from disk and call this function.
|
|
20
20
|
*/
|
|
21
|
-
export declare const EXPECTED_RED_LINE_IDS: readonly ["RL-0", "RL-1", "RL-2", "RL-3", "RL-4", "RL-5", "RL-6", "RL-7", "RL-8", "RL-9"];
|
|
21
|
+
export declare const EXPECTED_RED_LINE_IDS: readonly ["RL-0", "RL-1", "RL-2", "RL-3", "RL-4", "RL-5", "RL-6", "RL-7", "RL-8", "RL-9", "RL-10"];
|
|
22
22
|
export type RedLineId = (typeof EXPECTED_RED_LINE_IDS)[number];
|
|
23
23
|
export declare const REQUIRED_SECTIONS: readonly ["Failure modes", "Rewrite", "Self-check", "Out-of-scope"];
|
|
24
24
|
export type RedLineSection = (typeof REQUIRED_SECTIONS)[number];
|
|
@@ -29,6 +29,12 @@ export const EXPECTED_RED_LINE_IDS = [
|
|
|
29
29
|
'RL-7',
|
|
30
30
|
'RL-8',
|
|
31
31
|
'RL-9',
|
|
32
|
+
// RL-10 was written into the guideline file (its 4 sections included) but
|
|
33
|
+
// never added here, so the lint could not see it: the closed set stopped one
|
|
34
|
+
// short of the file it was supposed to police. The file's own footer said
|
|
35
|
+
// "Total red lines: 9 (RL-0..RL-9)" while RL-10 sat below it — the two
|
|
36
|
+
// statements contradicted each other and only the file was ever read.
|
|
37
|
+
'RL-10',
|
|
32
38
|
];
|
|
33
39
|
export const REQUIRED_SECTIONS = [
|
|
34
40
|
'Failure modes',
|
|
@@ -5,10 +5,29 @@
|
|
|
5
5
|
* WRITE goes through `assertUnder` first — the slice-wide guard against an
|
|
6
6
|
* artifact escaping `<root>/.peaks/_runtime/<sid>/web/` (AC1).
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { dirname } from 'node:path';
|
|
10
10
|
import { assertUnder, webDaemonDir, webDaemonInfoPath, webSpawnLockPath } from './web-artifact-paths.js';
|
|
11
11
|
import { parseDaemonInfo } from './web-protocol.js';
|
|
12
|
+
/**
|
|
13
|
+
* Canonicalize through symlinks so two paths that name the same directory through
|
|
14
|
+
* different prefixes compare equal — the macOS `/var` <-> `/private/var` quirk
|
|
15
|
+
* makes `mkdtempSync(...)` return `/var/folders/...` while `process.cwd()` after
|
|
16
|
+
* `chdir` returns `/private/var/folders/...`. They are the same directory on
|
|
17
|
+
* disk; without `realpathSync` the ownership check below silently rejects a
|
|
18
|
+
* legitimate record and the CLI cold-starts a real daemon instead of reusing
|
|
19
|
+
* the stub. Falls back to `resolve()` when the path is absent: a planted
|
|
20
|
+
* record naming a directory that does not exist still has to fail the
|
|
21
|
+
* containment test, and a `realpathSync` on it would throw.
|
|
22
|
+
*/
|
|
23
|
+
function safeRealpath(p) {
|
|
24
|
+
try {
|
|
25
|
+
return realpathSync(p);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
/** A spawn lock older than this is reclaimed even if its owner pid is alive. */
|
|
13
32
|
const SPAWN_LOCK_STALE_MS = 120_000;
|
|
14
33
|
/**
|
|
@@ -46,7 +65,13 @@ export function readDaemonInfo(projectRoot, sessionId) {
|
|
|
46
65
|
}
|
|
47
66
|
try {
|
|
48
67
|
const info = parseDaemonInfo(readFileSync(target, 'utf8'));
|
|
49
|
-
if (info === null || info.
|
|
68
|
+
if (info === null || info.sessionId !== sessionId) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Canonicalize both sides — a record written from one prefix (say
|
|
72
|
+
// `/var/folders/...`) and read from the canonical one (`/private/var/...`)
|
|
73
|
+
// describes the same project on macOS.
|
|
74
|
+
if (safeRealpath(info.projectRoot) !== safeRealpath(projectRoot)) {
|
|
50
75
|
return null;
|
|
51
76
|
}
|
|
52
77
|
return info;
|
|
@@ -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
|
},
|