peaks-loop 3.1.1 → 3.1.2
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/dist/src/cli/commands/job-commands.js +54 -1
- package/dist/src/cli/commands/solo-commands.js +238 -12
- package/dist/src/services/job/job-progress-store.d.ts +60 -0
- package/dist/src/services/job/job-progress-store.js +70 -0
- package/dist/src/services/job/job-types.d.ts +4 -4
- package/dist/src/services/solo/emit-handoff.d.ts +49 -0
- package/dist/src/services/solo/emit-handoff.js +79 -0
- package/dist/src/services/solo/step-08-gate.d.ts +90 -0
- package/dist/src/services/solo/step-08-gate.js +136 -0
- package/dist/src/services/workspace/claude-settings-template.d.ts +6 -1
- package/dist/src/services/workspace/claude-settings-template.js +40 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-solo/SKILL.md +47 -98
- package/skills/peaks-solo/references/runbook.md +20 -0
- package/skills/peaks-solo/references/step-0-8-gate.md +60 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3.1.2 Step 11 / final handoff — Size-fear ban.
|
|
3
|
+
*
|
|
4
|
+
* Refuses to emit a final handoff while a Job has remaining slices.
|
|
5
|
+
* Closes the v3.1.0 incident pattern: LLM writes a fake-completion
|
|
6
|
+
* message at high context-fill before Job mode kicks in.
|
|
7
|
+
*
|
|
8
|
+
* Decision tree:
|
|
9
|
+
* - job-shape.json missing OR isJob === false → ALLOW (normal handoff)
|
|
10
|
+
* - isJob === true, no state.json → JOB_NOT_INITIALIZED (LLM
|
|
11
|
+
* skipped `peaks job init`)
|
|
12
|
+
* - isJob === true, state.json, remaining > 0,
|
|
13
|
+
* no --force-under-job → JOB_REMAINING_BLOCKED
|
|
14
|
+
* - isJob === true, state.json, remaining === 0 → ALLOW (Job done)
|
|
15
|
+
* - isJob === true, state.json, remaining > 0,
|
|
16
|
+
* --force-under-job → ALLOW (override path)
|
|
17
|
+
*
|
|
18
|
+
* Karpathy §2 (Simplicity First): ~70 lines, no LLM call, no regex.
|
|
19
|
+
* Pure read + arithmetic. The CLI wrapper in solo-commands.ts
|
|
20
|
+
* translates the verdict into exit code + JSON envelope.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { readJobShapeDecision, JobShapeDecisionError, JOB_SHAPE_NOT_DECIDED } from './job-shape-decision.js';
|
|
25
|
+
export const JOB_NOT_INITIALIZED = 'JOB_NOT_INITIALIZED';
|
|
26
|
+
export const JOB_REMAINING_BLOCKED = 'JOB_REMAINING_BLOCKED';
|
|
27
|
+
function countRemaining(statePath) {
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = readFileSync(statePath, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return Number.NaN;
|
|
34
|
+
}
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return Number.NaN;
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(parsed.slices))
|
|
43
|
+
return Number.NaN;
|
|
44
|
+
return parsed.slices.filter((sl) => sl.status !== 'done' && sl.status !== 'skipped').length;
|
|
45
|
+
}
|
|
46
|
+
function statePath(projectRoot, sessionId, jobId) {
|
|
47
|
+
return join(projectRoot, '.peaks', '_runtime', sessionId, 'job', jobId, 'state.json');
|
|
48
|
+
}
|
|
49
|
+
export function evaluateEmitHandoff(input) {
|
|
50
|
+
let decision = null;
|
|
51
|
+
try {
|
|
52
|
+
const record = readJobShapeDecision(input.projectRoot, input.sessionId);
|
|
53
|
+
decision = record.decision;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err instanceof JobShapeDecisionError && err.code === JOB_SHAPE_NOT_DECIDED) {
|
|
57
|
+
return { kind: 'allow-not-job' };
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
if (!decision.isJob)
|
|
62
|
+
return { kind: 'allow-not-job' };
|
|
63
|
+
const jobId = input.jobId ?? decision.suggestedJobId;
|
|
64
|
+
const path = statePath(input.projectRoot, input.sessionId, jobId);
|
|
65
|
+
if (!existsSync(path)) {
|
|
66
|
+
return { kind: 'block-not-initialized', code: JOB_NOT_INITIALIZED, jobId };
|
|
67
|
+
}
|
|
68
|
+
const remaining = countRemaining(path);
|
|
69
|
+
if (!Number.isFinite(remaining)) {
|
|
70
|
+
return { kind: 'block-not-initialized', code: JOB_NOT_INITIALIZED, jobId };
|
|
71
|
+
}
|
|
72
|
+
if (remaining === 0) {
|
|
73
|
+
return { kind: 'allow-done', remaining };
|
|
74
|
+
}
|
|
75
|
+
if (input.forceUnderJob === true) {
|
|
76
|
+
return { kind: 'allow-force-override', remaining };
|
|
77
|
+
}
|
|
78
|
+
return { kind: 'block-remaining', code: JOB_REMAINING_BLOCKED, jobId, remaining };
|
|
79
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3.1.2 Step 0.8 — Mechanical PreToolUse gate.
|
|
3
|
+
*
|
|
4
|
+
* Wire-installed by `peaks workspace init` (extends the existing hook
|
|
5
|
+
* installer; does NOT replace the Write|Edit|MultiEdit fact-forcing
|
|
6
|
+
* bypass). The hook runs `peaks solo gate-step-08 --project .` before
|
|
7
|
+
* every Bash tool call. Exit code is the load-bearing contract:
|
|
8
|
+
*
|
|
9
|
+
* exit 0 → allow (with structured stdout describing the decision)
|
|
10
|
+
* exit 2 → block (stderr contains the BLOCKED: ... reason)
|
|
11
|
+
*
|
|
12
|
+
* Three decision paths:
|
|
13
|
+
*
|
|
14
|
+
* 1. `job-shape.json` exists with `decision.isJob === true`.
|
|
15
|
+
* → allow, print `{ ok, allow, mode: 'job', decision }`. When a
|
|
16
|
+
* matching `job/<jid>/progress.json` exists, also print
|
|
17
|
+
* `Next: slice #<N+1> of <M> (<currentSlice>)` so the LLM sees
|
|
18
|
+
* its resume context BEFORE any Bash call lands.
|
|
19
|
+
* 2. `job-shape.json` exists with `decision.isJob === false`.
|
|
20
|
+
* → allow, print `{ ok, allow, mode: 'single' }`.
|
|
21
|
+
* 3. `job-shape.json` is MISSING — fail-closed guard. The LLM is the
|
|
22
|
+
* source of truth, but if it never recorded a decision we run a
|
|
23
|
+
* lightweight backup regex against the user's prompt:
|
|
24
|
+
* /until|全部|until all done|disavow cost|不用考虑费用|all of them/i
|
|
25
|
+
* match → block (exit 2) with the BLOCKED: stderr message.
|
|
26
|
+
* no match → allow (exit 0) — most prompts are not Job-shaped.
|
|
27
|
+
*
|
|
28
|
+
* Karpathy §2 (Simplicity First): ~120 lines, no LLM call inside. The
|
|
29
|
+
* regex is the FIRST time peaks-loop accepts hardcoded keywords —
|
|
30
|
+
* explicitly scoped as a *fail-closed backup* (case 3), NOT a primary
|
|
31
|
+
* judgement. The LLM still owns the semantic call via
|
|
32
|
+
* `peaks solo detect-job`. The regex is a safety net for the v3.1.1
|
|
33
|
+
* incident class (LLM under load never records the decision at all).
|
|
34
|
+
*/
|
|
35
|
+
export declare const STEP_08_GATE_FILE_NAME: "job-shape.json";
|
|
36
|
+
export declare const STEP_08_PROGRESS_FILE_NAME: "progress.json";
|
|
37
|
+
/**
|
|
38
|
+
* Fail-closed backup regex — see module docstring case 3 for why this
|
|
39
|
+
* exists. Kept short and explicit; if the prompt mentions any of these
|
|
40
|
+
* triggers and `job-shape.json` is missing, the LLM is treated as
|
|
41
|
+
* having skipped `peaks solo detect-job` and the gate blocks.
|
|
42
|
+
*
|
|
43
|
+
* The list mirrors the v3.1.0 / v3.1.1 incident triggers verbatim:
|
|
44
|
+
* 继续执行下个 slice (until next slice) → until / 直到
|
|
45
|
+
* 全部添加完 / all of them → 全部 / all of them
|
|
46
|
+
* 不用考虑费用 → 不用考虑费用 / disavow cost
|
|
47
|
+
* until all done → until all done
|
|
48
|
+
*/
|
|
49
|
+
export declare const STEP_08_BACKUP_REGEX: RegExp;
|
|
50
|
+
export interface Step08Progress {
|
|
51
|
+
readonly jobId: string;
|
|
52
|
+
readonly done: number;
|
|
53
|
+
readonly total: number;
|
|
54
|
+
readonly currentSlice: string;
|
|
55
|
+
readonly lastCommitSha: string | null;
|
|
56
|
+
readonly updatedAt: string;
|
|
57
|
+
}
|
|
58
|
+
export type Step08Verdict = {
|
|
59
|
+
readonly kind: 'allow-job';
|
|
60
|
+
readonly decision: import('./job-shape-decision.js').JobShapeDecision;
|
|
61
|
+
readonly progress: Step08Progress | null;
|
|
62
|
+
} | {
|
|
63
|
+
readonly kind: 'allow-single';
|
|
64
|
+
} | {
|
|
65
|
+
readonly kind: 'block-missing-decision';
|
|
66
|
+
readonly promptHit: boolean;
|
|
67
|
+
readonly promptSource: 'flag' | 'last-prompt-file' | 'stdin-empty';
|
|
68
|
+
};
|
|
69
|
+
export declare function runtimeSessionDir(projectRoot: string, sessionId: string): string;
|
|
70
|
+
export interface EvaluateStep08Input {
|
|
71
|
+
readonly projectRoot: string;
|
|
72
|
+
readonly sessionId: string;
|
|
73
|
+
/**
|
|
74
|
+
* Explicit prompt text. When omitted, falls back to
|
|
75
|
+
* `.peaks/_runtime/<sid>/txt/last-prompt.txt`, then to stdin (the
|
|
76
|
+
* caller passes stdin verbatim — usually empty in tests).
|
|
77
|
+
*/
|
|
78
|
+
readonly prompt?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface EvaluateStep08Result {
|
|
81
|
+
readonly allow: boolean;
|
|
82
|
+
readonly verdict: Step08Verdict;
|
|
83
|
+
readonly nextSliceLine: string | null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Pure evaluator — does NOT write, does NOT exit. CLI wrapper in
|
|
87
|
+
* `src/cli/commands/solo-commands.ts` reads `allow` to set the process
|
|
88
|
+
* exit code (0 allow / 2 block) and prints the structured envelope.
|
|
89
|
+
*/
|
|
90
|
+
export declare function evaluateStep08(input: EvaluateStep08Input): EvaluateStep08Result;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v3.1.2 Step 0.8 — Mechanical PreToolUse gate.
|
|
3
|
+
*
|
|
4
|
+
* Wire-installed by `peaks workspace init` (extends the existing hook
|
|
5
|
+
* installer; does NOT replace the Write|Edit|MultiEdit fact-forcing
|
|
6
|
+
* bypass). The hook runs `peaks solo gate-step-08 --project .` before
|
|
7
|
+
* every Bash tool call. Exit code is the load-bearing contract:
|
|
8
|
+
*
|
|
9
|
+
* exit 0 → allow (with structured stdout describing the decision)
|
|
10
|
+
* exit 2 → block (stderr contains the BLOCKED: ... reason)
|
|
11
|
+
*
|
|
12
|
+
* Three decision paths:
|
|
13
|
+
*
|
|
14
|
+
* 1. `job-shape.json` exists with `decision.isJob === true`.
|
|
15
|
+
* → allow, print `{ ok, allow, mode: 'job', decision }`. When a
|
|
16
|
+
* matching `job/<jid>/progress.json` exists, also print
|
|
17
|
+
* `Next: slice #<N+1> of <M> (<currentSlice>)` so the LLM sees
|
|
18
|
+
* its resume context BEFORE any Bash call lands.
|
|
19
|
+
* 2. `job-shape.json` exists with `decision.isJob === false`.
|
|
20
|
+
* → allow, print `{ ok, allow, mode: 'single' }`.
|
|
21
|
+
* 3. `job-shape.json` is MISSING — fail-closed guard. The LLM is the
|
|
22
|
+
* source of truth, but if it never recorded a decision we run a
|
|
23
|
+
* lightweight backup regex against the user's prompt:
|
|
24
|
+
* /until|全部|until all done|disavow cost|不用考虑费用|all of them/i
|
|
25
|
+
* match → block (exit 2) with the BLOCKED: stderr message.
|
|
26
|
+
* no match → allow (exit 0) — most prompts are not Job-shaped.
|
|
27
|
+
*
|
|
28
|
+
* Karpathy §2 (Simplicity First): ~120 lines, no LLM call inside. The
|
|
29
|
+
* regex is the FIRST time peaks-loop accepts hardcoded keywords —
|
|
30
|
+
* explicitly scoped as a *fail-closed backup* (case 3), NOT a primary
|
|
31
|
+
* judgement. The LLM still owns the semantic call via
|
|
32
|
+
* `peaks solo detect-job`. The regex is a safety net for the v3.1.1
|
|
33
|
+
* incident class (LLM under load never records the decision at all).
|
|
34
|
+
*/
|
|
35
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { readJobShapeDecision, JobShapeDecisionError, JOB_SHAPE_NOT_DECIDED } from './job-shape-decision.js';
|
|
38
|
+
export const STEP_08_GATE_FILE_NAME = 'job-shape.json';
|
|
39
|
+
export const STEP_08_PROGRESS_FILE_NAME = 'progress.json';
|
|
40
|
+
/**
|
|
41
|
+
* Fail-closed backup regex — see module docstring case 3 for why this
|
|
42
|
+
* exists. Kept short and explicit; if the prompt mentions any of these
|
|
43
|
+
* triggers and `job-shape.json` is missing, the LLM is treated as
|
|
44
|
+
* having skipped `peaks solo detect-job` and the gate blocks.
|
|
45
|
+
*
|
|
46
|
+
* The list mirrors the v3.1.0 / v3.1.1 incident triggers verbatim:
|
|
47
|
+
* 继续执行下个 slice (until next slice) → until / 直到
|
|
48
|
+
* 全部添加完 / all of them → 全部 / all of them
|
|
49
|
+
* 不用考虑费用 → 不用考虑费用 / disavow cost
|
|
50
|
+
* until all done → until all done
|
|
51
|
+
*/
|
|
52
|
+
export const STEP_08_BACKUP_REGEX = /直到|全部|until all done|disavow cost|不用考虑费用|all of them/i;
|
|
53
|
+
export function runtimeSessionDir(projectRoot, sessionId) {
|
|
54
|
+
return join(projectRoot, '.peaks', '_runtime', sessionId);
|
|
55
|
+
}
|
|
56
|
+
function progressPath(projectRoot, sessionId, jid) {
|
|
57
|
+
// The progress file lives under job/<jid>/progress.json. The jid
|
|
58
|
+
// comes from job-shape.json (decision.suggestedJobId) — there is
|
|
59
|
+
// exactly one canonical jid per session under Job mode, so we read
|
|
60
|
+
// the matching path directly rather than enumerating the job/ dir.
|
|
61
|
+
// (Rotating mode spawns new jids but only one is "active" at a time;
|
|
62
|
+
// the active jid is the latest write to job-shape.json.)
|
|
63
|
+
return join(runtimeSessionDir(projectRoot, sessionId), 'job', jid, STEP_08_PROGRESS_FILE_NAME);
|
|
64
|
+
}
|
|
65
|
+
function readProgressIfAny(projectRoot, sessionId, jid) {
|
|
66
|
+
const path = progressPath(projectRoot, sessionId, jid);
|
|
67
|
+
if (!existsSync(path))
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
const raw = readFileSync(path, 'utf8');
|
|
71
|
+
const parsed = JSON.parse(raw);
|
|
72
|
+
if (typeof parsed.jobId === 'string' &&
|
|
73
|
+
typeof parsed.done === 'number' &&
|
|
74
|
+
typeof parsed.total === 'number' &&
|
|
75
|
+
typeof parsed.currentSlice === 'string' &&
|
|
76
|
+
typeof parsed.updatedAt === 'string') {
|
|
77
|
+
return {
|
|
78
|
+
jobId: parsed.jobId,
|
|
79
|
+
done: parsed.done,
|
|
80
|
+
total: parsed.total,
|
|
81
|
+
currentSlice: parsed.currentSlice,
|
|
82
|
+
lastCommitSha: typeof parsed.lastCommitSha === 'string' ? parsed.lastCommitSha : null,
|
|
83
|
+
updatedAt: parsed.updatedAt
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function readPromptFromLastPromptFile(projectRoot, sessionId) {
|
|
93
|
+
const path = join(runtimeSessionDir(projectRoot, sessionId), 'txt', 'last-prompt.txt');
|
|
94
|
+
if (!existsSync(path))
|
|
95
|
+
return '';
|
|
96
|
+
try {
|
|
97
|
+
return readFileSync(path, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Pure evaluator — does NOT write, does NOT exit. CLI wrapper in
|
|
105
|
+
* `src/cli/commands/solo-commands.ts` reads `allow` to set the process
|
|
106
|
+
* exit code (0 allow / 2 block) and prints the structured envelope.
|
|
107
|
+
*/
|
|
108
|
+
export function evaluateStep08(input) {
|
|
109
|
+
try {
|
|
110
|
+
const record = readJobShapeDecision(input.projectRoot, input.sessionId);
|
|
111
|
+
if (record.decision.isJob) {
|
|
112
|
+
const progress = readProgressIfAny(input.projectRoot, input.sessionId, record.decision.suggestedJobId);
|
|
113
|
+
const nextSliceLine = progress !== null
|
|
114
|
+
? `Next: slice #${progress.done + 1} of ${progress.total} (${progress.currentSlice})`
|
|
115
|
+
: null;
|
|
116
|
+
return {
|
|
117
|
+
allow: true,
|
|
118
|
+
verdict: { kind: 'allow-job', decision: record.decision, progress },
|
|
119
|
+
nextSliceLine
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return { allow: true, verdict: { kind: 'allow-single' }, nextSliceLine: null };
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (err instanceof JobShapeDecisionError && err.code === JOB_SHAPE_NOT_DECIDED) {
|
|
126
|
+
const promptText = input.prompt ?? readPromptFromLastPromptFile(input.projectRoot, input.sessionId);
|
|
127
|
+
const hit = promptText.length > 0 && STEP_08_BACKUP_REGEX.test(promptText);
|
|
128
|
+
const source = input.prompt !== undefined ? 'flag' : promptText.length > 0 ? 'last-prompt-file' : 'stdin-empty';
|
|
129
|
+
if (hit) {
|
|
130
|
+
return { allow: false, verdict: { kind: 'block-missing-decision', promptHit: true, promptSource: source }, nextSliceLine: null };
|
|
131
|
+
}
|
|
132
|
+
return { allow: true, verdict: { kind: 'block-missing-decision', promptHit: false, promptSource: source }, nextSliceLine: null };
|
|
133
|
+
}
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -50,8 +50,13 @@ export declare const CLAUDE_SETTINGS_LOCAL_FILENAME = ".claude/settings.local.js
|
|
|
50
50
|
* 1.2.0 — removed the `Bash` matcher; Edit/Write fact-forcing
|
|
51
51
|
* bypass is the only emit. Bash enforcement is owned by
|
|
52
52
|
* `peaks gate enforce` in `settings.json`.
|
|
53
|
+
* 1.3.0 — added the v3.1.2 `Bash` PreToolUse matcher that runs
|
|
54
|
+
* `peaks solo gate-step-08 --project .` (Step 0.8 mechanical
|
|
55
|
+
* gate). The existing Write|Edit|MultiEdit matcher is
|
|
56
|
+
* preserved. The new matcher's exit code is the load-bearing
|
|
57
|
+
* signal: 0 = allow, 2 = block (with stderr BLOCKED reason).
|
|
53
58
|
*/
|
|
54
|
-
export declare const TEMPLATE_VERSION = "1.
|
|
59
|
+
export declare const TEMPLATE_VERSION = "1.3.0";
|
|
55
60
|
/**
|
|
56
61
|
* Compare two serialized template strings for semantic equivalence.
|
|
57
62
|
*
|
|
@@ -50,8 +50,13 @@ export const CLAUDE_SETTINGS_LOCAL_FILENAME = '.claude/settings.local.json';
|
|
|
50
50
|
* 1.2.0 — removed the `Bash` matcher; Edit/Write fact-forcing
|
|
51
51
|
* bypass is the only emit. Bash enforcement is owned by
|
|
52
52
|
* `peaks gate enforce` in `settings.json`.
|
|
53
|
+
* 1.3.0 — added the v3.1.2 `Bash` PreToolUse matcher that runs
|
|
54
|
+
* `peaks solo gate-step-08 --project .` (Step 0.8 mechanical
|
|
55
|
+
* gate). The existing Write|Edit|MultiEdit matcher is
|
|
56
|
+
* preserved. The new matcher's exit code is the load-bearing
|
|
57
|
+
* signal: 0 = allow, 2 = block (with stderr BLOCKED reason).
|
|
53
58
|
*/
|
|
54
|
-
export const TEMPLATE_VERSION = '1.
|
|
59
|
+
export const TEMPLATE_VERSION = '1.3.0';
|
|
55
60
|
/**
|
|
56
61
|
* Compare two serialized template strings for semantic equivalence.
|
|
57
62
|
*
|
|
@@ -208,8 +213,42 @@ export function buildClaudeSettingsLocalJson() {
|
|
|
208
213
|
command: buildWriteHookCommand()
|
|
209
214
|
}
|
|
210
215
|
]
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
// v3.1.2 Step 0.8 — Mechanical PreToolUse gate. Runs
|
|
219
|
+
// `peaks solo gate-step-08 --project .` before every Bash
|
|
220
|
+
// tool call. Exit 0 = allow (with structured stdout
|
|
221
|
+
// describing the decision + optional `Next: slice #N+1 of
|
|
222
|
+
// M (<currentSlice>)` line when progress.json exists). Exit
|
|
223
|
+
// 2 = block (stderr contains the BLOCKED: ... reason).
|
|
224
|
+
// The existing Write|Edit|MultiEdit matcher is preserved.
|
|
225
|
+
matcher: 'Bash',
|
|
226
|
+
hooks: [
|
|
227
|
+
{
|
|
228
|
+
type: 'command',
|
|
229
|
+
command: buildBashGateStep08Command()
|
|
230
|
+
}
|
|
231
|
+
]
|
|
211
232
|
}
|
|
212
233
|
]
|
|
213
234
|
}
|
|
214
235
|
};
|
|
215
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* v3.1.2: build the Bash matcher command that runs `peaks solo
|
|
239
|
+
* gate-step-08`. We invoke the CLI directly (not via `node -e "..."`)
|
|
240
|
+
* because the CLI is the only legitimate source of the structured
|
|
241
|
+
* decision + Next: slice context. Exit code is the load-bearing
|
|
242
|
+
* signal — 0 = allow, 2 = block. The hook installer is idempotent:
|
|
243
|
+
* `templateContentMatches` compares the parsed matcher entries, so
|
|
244
|
+
* re-running `peaks workspace init` on a project that already has the
|
|
245
|
+
* Bash hook is a no-op (already-current).
|
|
246
|
+
*/
|
|
247
|
+
function buildBashGateStep08Command() {
|
|
248
|
+
// The hook receives the tool call on stdin. We ignore stdin and
|
|
249
|
+
// delegate entirely to `peaks solo gate-step-08`, which reads
|
|
250
|
+
// .peaks/_runtime/<sessionId>/job-shape.json and last-prompt.txt.
|
|
251
|
+
// `${CLAUDE_PROJECT_DIR}` resolves to the consumer project's root
|
|
252
|
+
// (Claude Code's standard convention).
|
|
253
|
+
return 'peaks solo gate-step-08 --project "${CLAUDE_PROJECT_DIR}"';
|
|
254
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "3.1.
|
|
1
|
+
export declare const CLI_VERSION = "3.1.2";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "3.1.
|
|
1
|
+
export const CLI_VERSION = "3.1.2";
|