peaks-loop 4.0.47 → 4.0.48
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/agents/karpathy-reviewer.md +11 -10
- package/dist/cli/cli-helpers.d.ts +34 -0
- package/dist/cli/cli-helpers.js +57 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -0
- package/dist/cli/commands/code-runtime-commands.js +48 -8
- package/dist/cli/commands/compact-command.js +112 -0
- package/dist/cli/commands/config-commands.js +15 -9
- package/dist/cli/commands/dashboard-long-run.js +6 -0
- package/dist/cli/commands/dispatch-commands.js +11 -1
- package/dist/cli/commands/doctor/invoke-from-code.js +6 -0
- package/dist/cli/commands/hooks-commands.js +4 -4
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-eval-commands.js +15 -0
- package/dist/cli/commands/perf-audit-commands.js +2 -0
- package/dist/cli/commands/playwright-commands.js +12 -0
- package/dist/cli/commands/prd-commands.js +1 -1
- package/dist/cli/commands/qa-commands.js +22 -0
- package/dist/cli/commands/request-commands.js +8 -0
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/security-audit-commands.js +2 -0
- package/dist/cli/commands/slice-integrate-commands.js +5 -0
- package/dist/cli/commands/statusline-commands.js +44 -4
- package/dist/cli/commands/sub-agent/detached.d.ts +14 -1
- package/dist/cli/commands/sub-agent/detached.js +47 -22
- package/dist/cli/commands/sub-agent-shutdown-commands.js +11 -0
- package/dist/cli/commands/verdict-aggregate-command.js +95 -13
- package/dist/cli/commands/workflow-commands.js +1 -1
- package/dist/cli/index.js +5 -45
- package/dist/services/artifacts/artifact-prerequisites.d.ts +38 -7
- package/dist/services/artifacts/artifact-prerequisites.js +130 -65
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +18 -8
- package/dist/services/artifacts/request-artifact-state-helpers.d.ts +57 -0
- package/dist/services/artifacts/request-artifact-state-helpers.js +91 -10
- package/dist/services/audit-independent/perf-audit-service.d.ts +9 -0
- package/dist/services/audit-independent/perf-audit-service.js +27 -5
- package/dist/services/audit-independent/security-audit-service.d.ts +12 -2
- package/dist/services/audit-independent/security-audit-service.js +28 -6
- package/dist/services/code/auto-compact-lifecycle.d.ts +119 -0
- package/dist/services/code/auto-compact-lifecycle.js +169 -0
- package/dist/services/code/auto-compact-orchestrator.js +13 -2
- package/dist/services/code/compact-event-settle.d.ts +122 -0
- package/dist/services/code/compact-event-settle.js +219 -0
- package/dist/services/compact-history/compact-history-service.d.ts +14 -0
- package/dist/services/config/config-restore.d.ts +12 -1
- package/dist/services/config/config-restore.js +35 -4
- package/dist/services/config/config-rollback.js +6 -1
- package/dist/services/context/harness-context-witness.d.ts +310 -0
- package/dist/services/context/harness-context-witness.js +606 -0
- package/dist/services/evidence/evidence-generator.js +86 -49
- package/dist/services/final-review/final-review-service.d.ts +9 -0
- package/dist/services/final-review/final-review-service.js +36 -12
- package/dist/services/ide/ide-registry.d.ts +19 -0
- package/dist/services/ide/ide-registry.js +21 -0
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/polyrepo/polyrepo-dispatcher.js +11 -0
- package/dist/services/prd/handoff-auto-regen.js +31 -27
- package/dist/services/prd/handoff-frontmatter.d.ts +44 -0
- package/dist/services/prd/handoff-frontmatter.js +75 -0
- package/dist/services/prd/handoff-service.d.ts +41 -2
- package/dist/services/prd/handoff-service.js +81 -8
- package/dist/services/prd/handoff-types.d.ts +3 -2
- package/dist/services/prd/handoff-types.js +3 -2
- package/dist/services/qa/qa-business-review-state.js +9 -0
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/session/session-checkpoint-service.js +8 -0
- package/dist/services/skill/resume-detector.js +29 -11
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +6 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +61 -2
- package/dist/services/skills/hooks-settings-service.js +14 -4
- package/dist/services/skills/session-start-hook-constants.d.ts +45 -0
- package/dist/services/skills/session-start-hook-constants.js +45 -0
- package/dist/services/skills/skill-statusline-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +29 -11
- package/dist/services/slice/slice-review-state.js +8 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +212 -93
- package/dist/services/workflow/pipeline-verify-service.js +24 -23
- package/dist/services/workflow/pipeline-verify-types.d.ts +10 -3
- package/dist/services/workspace/claude-settings-template.d.ts +56 -8
- package/dist/services/workspace/claude-settings-template.js +98 -20
- package/dist/services/workspace/workspace-claude-settings-materializer.js +78 -7
- package/package.json +6 -6
- package/skills/bee/peaks-prd/SKILL.md +7 -5
- package/skills/bee/peaks-qa/SKILL.md +5 -5
- package/skills/bee/peaks-qa/references/qa-runbook.md +2 -2
- package/skills/bee/peaks-qa/references/qa-transition-gates.md +7 -7
- package/skills/bee/peaks-rd/SKILL.md +8 -6
- package/skills/bee/peaks-rd/references/artifact-per-request.md +2 -2
- package/skills/bee/peaks-rd/references/parallel-review-fanout.md +7 -5
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +13 -13
- package/skills/bee/peaks-rd/references/rd-runbook.md +9 -5
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +9 -7
- package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +6 -6
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/a2a-artifact-mapping.md +3 -3
- package/skills/peaks-code/references/local-artifact-workspace.md +1 -1
- package/skills/peaks-code/references/resume-detection.md +13 -7
- package/skills/peaks-code/references/runbook.md +3 -2
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +8 -6
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* `v2-11-rm-rd-techdoc-immutable-handoff`).
|
|
4
4
|
*
|
|
5
5
|
* Owns the immutable handoff at
|
|
6
|
-
* `.peaks/_runtime/<sessionId>/prd/handoff
|
|
6
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
|
|
7
|
+
* the pre-rid-scoping `.peaks/_runtime/<sessionId>/prd/handoff.md` stays
|
|
8
|
+
* readable through `resolveHandoffPath`):
|
|
7
9
|
*
|
|
8
10
|
* - `initHandoff` — pure; computes sha256 of the body and returns
|
|
9
11
|
* a Handoff whose frontmatter `handoffHash` matches.
|
|
@@ -23,6 +25,43 @@
|
|
|
23
25
|
import type { Handoff, HandoffProbe } from './handoff-types.js';
|
|
24
26
|
/** Compute the lowercase hex sha256 of a UTF-8 string. */
|
|
25
27
|
export declare function sha256OfBody(body: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* The canonical capsule path for ONE SLICE, relative to the project root.
|
|
30
|
+
*
|
|
31
|
+
* Slice `2026-09-14-prd-capsule-rid-scoping`: the capsule used to be one slot
|
|
32
|
+
* per SESSION (`prd/handoff.md`), so the second slice's handoff silently
|
|
33
|
+
* overwrote the first slice's — and `AUDIT_REQUIRES_HANDOFF` stayed green
|
|
34
|
+
* because it never checked WHOSE rid the file named. Measured on
|
|
35
|
+
* `2026-09-13-session-21878f`: a four-slice job passed that prerequisite on a
|
|
36
|
+
* capsule left by a different line of work.
|
|
37
|
+
*
|
|
38
|
+
* This is the WRITE target, so it always carries the rid — a consumer-side
|
|
39
|
+
* fallback here would leave a rid-scoped requirement with a bare-name writer,
|
|
40
|
+
* which is the defect shape this slice exists to remove. Readers that must
|
|
41
|
+
* tolerate pre-rid-scoping sessions call `resolveHandoffPath` instead.
|
|
42
|
+
*/
|
|
43
|
+
export declare function handoffRelativePath(sessionId: string, requestId: string): string;
|
|
44
|
+
/**
|
|
45
|
+
* The capsule a CONSUMER should read for (session, requestId): the rid-scoped
|
|
46
|
+
* path when it is on disk, else the pre-rid-scoping bare name. Returns null
|
|
47
|
+
* when neither exists.
|
|
48
|
+
*
|
|
49
|
+
* Three sessions on disk still hold only the bare file
|
|
50
|
+
* (`2026-09-06-session-a87ca4`, `2026-09-12-session-e37ef0`,
|
|
51
|
+
* `2026-09-13-session-21878f`), so the legacy tier has to keep resolving for
|
|
52
|
+
* the gate and for every reader below.
|
|
53
|
+
*
|
|
54
|
+
* `requestId` is optional because the detect-only audit surface reaches its
|
|
55
|
+
* service without one. Such a caller can name only the bare path: a session
|
|
56
|
+
* holding nothing but rid-scoped capsules reports missing rather than picking
|
|
57
|
+
* among its siblings' capsules, which would re-open the cross-slice mix-up
|
|
58
|
+
* this scoping exists to close (fail closed, not "some capsule is there").
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveHandoffPath(opts: {
|
|
61
|
+
projectRoot: string;
|
|
62
|
+
sessionId: string;
|
|
63
|
+
requestId?: string;
|
|
64
|
+
}): string | null;
|
|
26
65
|
/** Pure: produce a Handoff with the frontmatter populated. Hash is
|
|
27
66
|
* computed here; callers MUST NOT pre-populate `handoffHash`. */
|
|
28
67
|
export declare function initHandoff(opts: {
|
|
@@ -33,7 +72,7 @@ export declare function initHandoff(opts: {
|
|
|
33
72
|
goals: readonly string[];
|
|
34
73
|
acceptanceCriteria: readonly string[];
|
|
35
74
|
preservedBehavior: readonly string[];
|
|
36
|
-
/** Override path; defaults to `.peaks/_runtime/<sid>/prd/handoff
|
|
75
|
+
/** Override path; defaults to `.peaks/_runtime/<sid>/prd/handoff-<rid>.md`. */
|
|
37
76
|
handoffPath?: string;
|
|
38
77
|
}): Handoff;
|
|
39
78
|
/** Write a Handoff to disk. `projectRoot` is the absolute project
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* `v2-11-rm-rd-techdoc-immutable-handoff`).
|
|
4
4
|
*
|
|
5
5
|
* Owns the immutable handoff at
|
|
6
|
-
* `.peaks/_runtime/<sessionId>/prd/handoff
|
|
6
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
|
|
7
|
+
* the pre-rid-scoping `.peaks/_runtime/<sessionId>/prd/handoff.md` stays
|
|
8
|
+
* readable through `resolveHandoffPath`):
|
|
7
9
|
*
|
|
8
10
|
* - `initHandoff` — pure; computes sha256 of the body and returns
|
|
9
11
|
* a Handoff whose frontmatter `handoffHash` matches.
|
|
@@ -21,20 +23,68 @@
|
|
|
21
23
|
* markdown source — no normalization, no trailing-newline padding.
|
|
22
24
|
*/
|
|
23
25
|
import { createHash } from 'node:crypto';
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
24
27
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
25
28
|
import { dirname, join } from 'node:path';
|
|
26
|
-
import { parse as parseYaml
|
|
29
|
+
import { parse as parseYaml } from 'yaml';
|
|
30
|
+
import { serializeHandoffFrontmatter } from './handoff-frontmatter.js';
|
|
27
31
|
/** Required schema version for new handoffs. */
|
|
28
32
|
const HANDOFF_SCHEMA_VERSION = '2';
|
|
29
33
|
/** Compute the lowercase hex sha256 of a UTF-8 string. */
|
|
30
34
|
export function sha256OfBody(body) {
|
|
31
35
|
return createHash('sha256').update(body, 'utf8').digest('hex');
|
|
32
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The canonical capsule path for ONE SLICE, relative to the project root.
|
|
39
|
+
*
|
|
40
|
+
* Slice `2026-09-14-prd-capsule-rid-scoping`: the capsule used to be one slot
|
|
41
|
+
* per SESSION (`prd/handoff.md`), so the second slice's handoff silently
|
|
42
|
+
* overwrote the first slice's — and `AUDIT_REQUIRES_HANDOFF` stayed green
|
|
43
|
+
* because it never checked WHOSE rid the file named. Measured on
|
|
44
|
+
* `2026-09-13-session-21878f`: a four-slice job passed that prerequisite on a
|
|
45
|
+
* capsule left by a different line of work.
|
|
46
|
+
*
|
|
47
|
+
* This is the WRITE target, so it always carries the rid — a consumer-side
|
|
48
|
+
* fallback here would leave a rid-scoped requirement with a bare-name writer,
|
|
49
|
+
* which is the defect shape this slice exists to remove. Readers that must
|
|
50
|
+
* tolerate pre-rid-scoping sessions call `resolveHandoffPath` instead.
|
|
51
|
+
*/
|
|
52
|
+
export function handoffRelativePath(sessionId, requestId) {
|
|
53
|
+
return join('.peaks', '_runtime', sessionId, 'prd', `handoff-${requestId}.md`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The capsule a CONSUMER should read for (session, requestId): the rid-scoped
|
|
57
|
+
* path when it is on disk, else the pre-rid-scoping bare name. Returns null
|
|
58
|
+
* when neither exists.
|
|
59
|
+
*
|
|
60
|
+
* Three sessions on disk still hold only the bare file
|
|
61
|
+
* (`2026-09-06-session-a87ca4`, `2026-09-12-session-e37ef0`,
|
|
62
|
+
* `2026-09-13-session-21878f`), so the legacy tier has to keep resolving for
|
|
63
|
+
* the gate and for every reader below.
|
|
64
|
+
*
|
|
65
|
+
* `requestId` is optional because the detect-only audit surface reaches its
|
|
66
|
+
* service without one. Such a caller can name only the bare path: a session
|
|
67
|
+
* holding nothing but rid-scoped capsules reports missing rather than picking
|
|
68
|
+
* among its siblings' capsules, which would re-open the cross-slice mix-up
|
|
69
|
+
* this scoping exists to close (fail closed, not "some capsule is there").
|
|
70
|
+
*/
|
|
71
|
+
export function resolveHandoffPath(opts) {
|
|
72
|
+
const candidates = [
|
|
73
|
+
...(opts.requestId !== undefined ? [handoffRelativePath(opts.sessionId, opts.requestId)] : []),
|
|
74
|
+
join('.peaks', '_runtime', opts.sessionId, 'prd', 'handoff.md')
|
|
75
|
+
];
|
|
76
|
+
for (const relative of candidates) {
|
|
77
|
+
const absolute = join(opts.projectRoot, relative);
|
|
78
|
+
if (existsSync(absolute))
|
|
79
|
+
return absolute;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
33
83
|
/** Pure: produce a Handoff with the frontmatter populated. Hash is
|
|
34
84
|
* computed here; callers MUST NOT pre-populate `handoffHash`. */
|
|
35
85
|
export function initHandoff(opts) {
|
|
36
86
|
const handoffPath = opts.handoffPath ??
|
|
37
|
-
|
|
87
|
+
handoffRelativePath(opts.sessionId, opts.requestId);
|
|
38
88
|
const handoffHash = sha256OfBody(opts.body);
|
|
39
89
|
const frontmatter = {
|
|
40
90
|
requestId: opts.requestId,
|
|
@@ -146,8 +196,11 @@ function parseHandoffContent(content) {
|
|
|
146
196
|
};
|
|
147
197
|
}
|
|
148
198
|
function serializeHandoff(handoff) {
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
// The one canonical frontmatter rendering, shared with
|
|
200
|
+
// `handoff-auto-regen.ts`. `yaml.stringify` used to render this block and
|
|
201
|
+
// emitted `schemaVersion: "2"` + a bare `handoffHash:`, which the
|
|
202
|
+
// `AUDIT_REQUIRES_HANDOFF` gate and both audit loaders all reject.
|
|
203
|
+
return `${serializeHandoffFrontmatter(handoff.frontmatter)}${handoff.body}`;
|
|
151
204
|
}
|
|
152
205
|
/**
|
|
153
206
|
* N1: accept BOTH shapes of `schemaVersion` — the string `'2'` and the bare
|
|
@@ -161,13 +214,33 @@ function serializeHandoff(handoff) {
|
|
|
161
214
|
* readable handoff was satisfied by a handoff the parser would not read, and
|
|
162
215
|
* `peaks prd handoff verify` exited 1 on a healthy file.
|
|
163
216
|
*
|
|
164
|
-
*
|
|
165
|
-
* the
|
|
166
|
-
*
|
|
217
|
+
* Slice `2026-09-14-handoff-writer-gate-divergence` then fixed the other half:
|
|
218
|
+
* the writer no longer emits the quoted form at all (see
|
|
219
|
+
* `handoff-frontmatter.ts`). This tolerance stays because handoffs already on
|
|
220
|
+
* disk were written by the old writer and by hand; the writer fix must not
|
|
221
|
+
* retroactively make them unreadable.
|
|
167
222
|
*/
|
|
168
223
|
function isSchemaVersion2(value) {
|
|
169
224
|
return value === HANDOFF_SCHEMA_VERSION || value === 2;
|
|
170
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* The READER's shape check — deliberately looser than `verifyHandoff`:
|
|
228
|
+
* `handoffHash` must be a string, and nothing about its VALUE is validated here.
|
|
229
|
+
*
|
|
230
|
+
* The accepted-but-unverifiable shape, stated rather than left to be
|
|
231
|
+
* discovered: a capsule carrying `handoffHash: sha256:<hex>` — the form the
|
|
232
|
+
* pre-fix writer and this session's hand-corrected capsules use — is READ by
|
|
233
|
+
* `readHandoff` and can NEVER pass `verifyHandoff`. The verifier compares the
|
|
234
|
+
* value to `sha256OfBody(body)` byte for byte (`:127-135`) and no prefix
|
|
235
|
+
* normalization exists anywhere in this module, so the leading `sha256:`
|
|
236
|
+
* guarantees `hash-mismatch`. ONLY the bare-hex form verifies, which is what
|
|
237
|
+
* every writer now emits (`handoff-frontmatter.ts`).
|
|
238
|
+
*
|
|
239
|
+
* So `readHandoff` succeeding is NOT evidence that a capsule is verifiable —
|
|
240
|
+
* the tolerance above exists so old capsules stay READABLE, not so they become
|
|
241
|
+
* acceptable. Request §三 bullet 3 asked for exactly this confirmation
|
|
242
|
+
* (rid `2026-09-14-handoff-writer-gate-divergence`).
|
|
243
|
+
*/
|
|
171
244
|
function isHandoffFrontmatter(value) {
|
|
172
245
|
if (!value || typeof value !== 'object')
|
|
173
246
|
return false;
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* (D1 in `v2-11-rm-rd-techdoc-immutable-handoff`).
|
|
15
15
|
*
|
|
16
16
|
* Path convention: the file lands at
|
|
17
|
-
* `.peaks/_runtime/<sessionId>/prd/handoff
|
|
18
|
-
*
|
|
17
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
|
|
18
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff.md` is the pre-rid-scoping tier and
|
|
19
|
+
* stays readable). Gitignored session artifact; the binding to `<sessionId>` lives in
|
|
19
20
|
* `.peaks/_runtime/current-change`). NEVER write under
|
|
20
21
|
* `.peaks/_runtime/<change-id>/...` directly (slice 2.8.3 hard ban).
|
|
21
22
|
*/
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* (D1 in `v2-11-rm-rd-techdoc-immutable-handoff`).
|
|
15
15
|
*
|
|
16
16
|
* Path convention: the file lands at
|
|
17
|
-
* `.peaks/_runtime/<sessionId>/prd/handoff
|
|
18
|
-
*
|
|
17
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff-<rid>.md` (one capsule per slice;
|
|
18
|
+
* `.peaks/_runtime/<sessionId>/prd/handoff.md` is the pre-rid-scoping tier and
|
|
19
|
+
* stays readable). Gitignored session artifact; the binding to `<sessionId>` lives in
|
|
19
20
|
* `.peaks/_runtime/current-change`). NEVER write under
|
|
20
21
|
* `.peaks/_runtime/<change-id>/...` directly (slice 2.8.3 hard ban).
|
|
21
22
|
*/
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
19
20
|
/** 6-item business checklist (12 Gaps QA perspective). */
|
|
20
21
|
export const QA_BUSINESS_ITEMS = [
|
|
21
22
|
{ id: 'business-flow', question: '这个功能"用起来"对吗?(业务流程顺不顺,操作路径是否反人类,跟现有系统交互有没有断层)' },
|
|
@@ -36,6 +37,14 @@ export function buildEmptyQaReview(requestId, sessionId, now = new Date()) {
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
export function getQaReviewDir(projectRoot, sessionId) {
|
|
40
|
+
// Sid axis. Every `peaks qa-business-review|score|accept|reject` subcommand
|
|
41
|
+
// reaches the runtime tree through this one constructor, so one guard here
|
|
42
|
+
// covers the whole family — measured: `--session-id ../../../../…/PWNED`
|
|
43
|
+
// wrote `qa-business-reviews/<rid>.json` outside every project root under an
|
|
44
|
+
// `ok: true` envelope (RD sweep case A17).
|
|
45
|
+
if (isUnsafePathInput(sessionId)) {
|
|
46
|
+
throw new Error(`Invalid session id: ${sessionId} (must be a single path segment)`);
|
|
47
|
+
}
|
|
39
48
|
return resolve(projectRoot, '.peaks', '_runtime', sessionId, 'qa-business-reviews');
|
|
40
49
|
}
|
|
41
50
|
export function getQaReviewPath(projectRoot, sessionId, requestId) {
|
|
@@ -68,7 +68,7 @@ export async function scanKarpathy(options) {
|
|
|
68
68
|
warnings: [
|
|
69
69
|
`Karpathy review file missing: ${reviewRel}`,
|
|
70
70
|
'Per karpathy §1 Think Before Coding: state your assumptions. Without a review file, no 5-way fanout evidence is available.',
|
|
71
|
-
'Per karpathy §3 Surgical Changes: touch only what the request requires.
|
|
71
|
+
'Per karpathy §3 Surgical Changes: touch only what the request requires. The rd:qa-handoff gate is satisfied by rd/karpathy-review-<rid>.md; this scanner has no rid and reads only the back-compat name rd/karpathy-review.md, so a rid-scoped-only slice still reads as missing here. `peaks request transition --state qa-handoff` is the authoritative gate.'
|
|
72
72
|
]
|
|
73
73
|
};
|
|
74
74
|
}
|
|
@@ -223,6 +223,6 @@ export function formatKarpathyMarkdown(report, opts = {}) {
|
|
|
223
223
|
}
|
|
224
224
|
lines.push('### Karpathy-Gate');
|
|
225
225
|
lines.push('');
|
|
226
|
-
lines.push('Per `andrej-karpathy-skills:karpathy-guidelines` §1 Think Before Coding / §3 Surgical Changes, the hard Karpathy-Gate requires `rd/karpathy-review
|
|
226
|
+
lines.push('Per `andrej-karpathy-skills:karpathy-guidelines` §1 Think Before Coding / §3 Surgical Changes, the hard Karpathy-Gate requires `rd/karpathy-review-<rid>.md` to be present with all 4 guideline sections before `peaks request transition --state qa-handoff`. (`rd/karpathy-review.md` is the accepted back-compat tier, and the only name this scanner probes.)');
|
|
227
227
|
return lines.join('\n');
|
|
228
228
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { join, sep } from 'node:path';
|
|
14
14
|
import { emitObservabilityEvent } from '../observability/observability-service.js';
|
|
15
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
15
16
|
const CHECKPOINTS_DIR = 'checkpoints';
|
|
16
17
|
const CHECKPOINT_FILENAME_EXT = '.json';
|
|
17
18
|
const MAX_CHECKPOINTS = 10;
|
|
@@ -76,6 +77,13 @@ function pruneOldest(dir) {
|
|
|
76
77
|
return removed;
|
|
77
78
|
}
|
|
78
79
|
export function writeCheckpoint(projectRoot, options) {
|
|
80
|
+
// Sid axis, placed before the first read and the first `mkdir`. Both
|
|
81
|
+
// `peaks session checkpoint --session-id` and `peaks compact force
|
|
82
|
+
// --session-id` reach this same function, so one guard closes both measured
|
|
83
|
+
// surfaces — they were never two joins.
|
|
84
|
+
if (isUnsafePathInput(options.sessionId)) {
|
|
85
|
+
throw new Error(`Invalid session id: ${options.sessionId} (must be a single path segment)`);
|
|
86
|
+
}
|
|
79
87
|
const now = (options.now ?? (() => new Date()))();
|
|
80
88
|
const createdAt = now.toISOString();
|
|
81
89
|
const lastActivity = readSessionLastActivity(projectRoot, options.sessionId) || createdAt;
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
34
34
|
import { join } from 'node:path';
|
|
35
|
+
import { readArtifactState } from '../artifacts/request-artifact-state-helpers.js';
|
|
35
36
|
const MID_IMPL_RD_STATES = new Set([
|
|
36
37
|
'spec-locked',
|
|
37
38
|
'implemented',
|
|
@@ -192,13 +193,21 @@ function classifyTerminalGates(sessionDir, ctx) {
|
|
|
192
193
|
// RD qa-handoff → deepest gate is C. If the review artifacts are
|
|
193
194
|
// missing the state is inconsistent; fall back to rd-review-fanout.
|
|
194
195
|
if (ctx.primaryRd !== null && ctx.primaryRd.state === 'qa-handoff') {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
// Slice `2026-09-14-audit-artifact-rid-scoping`: the fan-out evidence
|
|
197
|
+
// filenames carry the rid. Probe the canonical rid-scoped name first and
|
|
198
|
+
// the pre-rid names behind it — the same order the transition gate
|
|
199
|
+
// resolves in, so an inconsistent-looking slice here means the fan-out
|
|
200
|
+
// never ran, not that it wrote to a name this reader does not know.
|
|
201
|
+
const rid = ridOf(ctx.primaryRd.filename);
|
|
197
202
|
const missing = [];
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!existsSync(
|
|
201
|
-
missing.push(
|
|
203
|
+
const codeReviewCandidates = [`rd/code-review-${rid}.md`, 'rd/code-review.md'];
|
|
204
|
+
const securityCandidates = [`audit/security-${rid}.md`, 'audit/security.md', 'rd/security-review.md'];
|
|
205
|
+
if (!codeReviewCandidates.some((rel) => existsSync(join(sessionDir, rel)))) {
|
|
206
|
+
missing.push(codeReviewCandidates[0]);
|
|
207
|
+
}
|
|
208
|
+
if (!securityCandidates.some((rel) => existsSync(join(sessionDir, rel)))) {
|
|
209
|
+
missing.push(securityCandidates[0]);
|
|
210
|
+
}
|
|
202
211
|
if (missing.length > 0) {
|
|
203
212
|
return {
|
|
204
213
|
kind: 'resume',
|
|
@@ -317,12 +326,21 @@ function readRequestStates(sessionDir, role) {
|
|
|
317
326
|
};
|
|
318
327
|
});
|
|
319
328
|
}
|
|
329
|
+
/** Empty string (not 'unknown') when the artifact has no state line — `primaryPrd.state.length > 0` below depends on that. */
|
|
320
330
|
function extractState(content) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
331
|
+
return readArtifactState(content) ?? '';
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The `<rid>` embedded in a request filename: drop the `.md` suffix and the
|
|
335
|
+
* zero-padded `NNN-` prefix `request init` writes.
|
|
336
|
+
*
|
|
337
|
+
* Uses the same `^0\d{2}-` rule as the request loader
|
|
338
|
+
* (`request-artifact-service.ts`: "Only strip 3-digit zero-padded prefixes").
|
|
339
|
+
* A looser `^\d+-` would eat the leading year of every real rid, which is
|
|
340
|
+
* itself date-shaped (`2026-09-14-<slug>`).
|
|
341
|
+
*/
|
|
342
|
+
function ridOf(filename) {
|
|
343
|
+
return filename.replace(/^0\d{2}-/, '').replace(/\.md$/, '');
|
|
326
344
|
}
|
|
327
345
|
function hasAbandonedTransitionNote(content) {
|
|
328
346
|
return /user-requested-abandon/.test(content);
|
|
@@ -39,6 +39,12 @@ interface ResolvedHookSpec {
|
|
|
39
39
|
* cannot help; pinning the hook's `shell` is the only lever the hook schema
|
|
40
40
|
* offers. The platform-neutral default (`undefined` → omit the key) is kept
|
|
41
41
|
* everywhere else.
|
|
42
|
+
*
|
|
43
|
+
* Scope note: this is applied to the `Bash`-matcher handlers only. The three
|
|
44
|
+
* `SessionStart` entries are deliberately NOT pinned, and not because the
|
|
45
|
+
* mechanism is believed absent there — see the block comment in
|
|
46
|
+
* `resolveHookEntries` for what is and is not established, and for why adding
|
|
47
|
+
* the pin in place would break every non-Windows teammate.
|
|
42
48
|
*/
|
|
43
49
|
export declare function resolveHookShell(platform?: NodeJS.Platform): string | undefined;
|
|
44
50
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* type contract re-exported by the caller.
|
|
9
9
|
*/
|
|
10
10
|
import { getAdapter } from '../ide/ide-registry.js';
|
|
11
|
-
import { HOOK_OUTER_CACHE_COMMAND, HOOK_OUTER_CACHE_EVENT, HOOK_OUTER_CACHE_SENTINEL, HOOK_POST_COMPACT_REINJECT_COMMAND, HOOK_POST_COMPACT_REINJECT_EVENT, HOOK_POST_COMPACT_REINJECT_MATCHER, HOOK_POST_COMPACT_REINJECT_SENTINEL, HOOK_WORKSPACE_INIT_COMMAND, HOOK_WORKSPACE_INIT_EVENT, HOOK_WORKSPACE_INIT_SENTINEL } from './session-start-hook-constants.js';
|
|
11
|
+
import { HOOK_COMPACT_SETTLE_COMMAND, HOOK_COMPACT_SETTLE_EVENT, HOOK_COMPACT_SETTLE_MATCHER, HOOK_COMPACT_SETTLE_SENTINEL, HOOK_OUTER_CACHE_COMMAND, HOOK_OUTER_CACHE_EVENT, HOOK_OUTER_CACHE_SENTINEL, HOOK_POST_COMPACT_REINJECT_COMMAND, HOOK_POST_COMPACT_REINJECT_EVENT, HOOK_POST_COMPACT_REINJECT_MATCHER, HOOK_POST_COMPACT_REINJECT_SENTINEL, HOOK_WORKSPACE_INIT_COMMAND, HOOK_WORKSPACE_INIT_EVENT, HOOK_WORKSPACE_INIT_SENTINEL } from './session-start-hook-constants.js';
|
|
12
12
|
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
13
13
|
export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
|
|
14
14
|
/**
|
|
@@ -20,6 +20,12 @@ export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
|
|
|
20
20
|
* cannot help; pinning the hook's `shell` is the only lever the hook schema
|
|
21
21
|
* offers. The platform-neutral default (`undefined` → omit the key) is kept
|
|
22
22
|
* everywhere else.
|
|
23
|
+
*
|
|
24
|
+
* Scope note: this is applied to the `Bash`-matcher handlers only. The three
|
|
25
|
+
* `SessionStart` entries are deliberately NOT pinned, and not because the
|
|
26
|
+
* mechanism is believed absent there — see the block comment in
|
|
27
|
+
* `resolveHookEntries` for what is and is not established, and for why adding
|
|
28
|
+
* the pin in place would break every non-Windows teammate.
|
|
23
29
|
*/
|
|
24
30
|
export function resolveHookShell(platform = process.platform) {
|
|
25
31
|
return platform === 'win32' ? 'powershell' : undefined;
|
|
@@ -112,6 +118,37 @@ export function resolveHookEntries(ide, _skipProgress = false) {
|
|
|
112
118
|
...(spec.hookEnforceShell !== undefined ? { shell: spec.hookEnforceShell } : {})
|
|
113
119
|
}
|
|
114
120
|
];
|
|
121
|
+
// ── Why the three SessionStart entries below carry NO `shell` pin ─────────
|
|
122
|
+
//
|
|
123
|
+
// The gate-enforce entry above is shell-pinned on Windows (see
|
|
124
|
+
// `resolveHookShell`) because Claude Code runs a shell-form hook command
|
|
125
|
+
// through a shell that defaults to bash — Git Bash / MSYS2 on Windows — and
|
|
126
|
+
// MSYS2 bash force-allocates its own console window. That reason is a
|
|
127
|
+
// property of the hook RUNNER's shell resolution, not of the `PreToolUse`
|
|
128
|
+
// event: nothing in it exempts `SessionStart`, so the same command-form
|
|
129
|
+
// entry on this event goes through the same shell. The pin was applied only
|
|
130
|
+
// to the `Bash`-matcher handlers because those were the ones the reporter
|
|
131
|
+
// could see (they run on EVERY Bash tool call); these three run once per
|
|
132
|
+
// session, so a window here — if there is one — is a single flash rather
|
|
133
|
+
// than a per-tool-call nuisance. That is a difference in frequency, not
|
|
134
|
+
// evidence that the window does not appear, and no A/B measurement on this
|
|
135
|
+
// event exists.
|
|
136
|
+
//
|
|
137
|
+
// The pin is nonetheless NOT applied here, and adding it would be a defect
|
|
138
|
+
// rather than a fix: `shell: "powershell"` resolves to `pwsh`, these entries
|
|
139
|
+
// are written to the COMMITTED `.claude/settings.json`, and a macOS / Linux
|
|
140
|
+
// teammate reading that file has no `pwsh` — the exact cross-platform damage
|
|
141
|
+
// the machine-local split exists to prevent (see the gate-enforce entry's
|
|
142
|
+
// `machineLocal` flag, and commit 4637baa8's rationale).
|
|
143
|
+
//
|
|
144
|
+
// A correct fix therefore has to move these three entries to the
|
|
145
|
+
// machine-local file as well, which changes where a fresh clone gets its
|
|
146
|
+
// SessionStart hooks: today they arrive with the repository, after such a
|
|
147
|
+
// change they would require `peaks hooks install` / `peaks workspace init`.
|
|
148
|
+
// That is a product-shape decision, not a leftover; it is recorded here so
|
|
149
|
+
// that whoever makes it starts from the reason the pin is absent instead of
|
|
150
|
+
// re-deriving it — or, worse, adding the pin in place and breaking every
|
|
151
|
+
// non-Windows teammate.
|
|
115
152
|
if (ide === 'claude-code') {
|
|
116
153
|
entries.push({
|
|
117
154
|
sentinel: HOOK_OUTER_CACHE_SENTINEL,
|
|
@@ -161,6 +198,24 @@ export function resolveHookEntries(ide, _skipProgress = false) {
|
|
|
161
198
|
command: HOOK_POST_COMPACT_REINJECT_COMMAND,
|
|
162
199
|
event: HOOK_POST_COMPACT_REINJECT_EVENT
|
|
163
200
|
});
|
|
201
|
+
// rid 2026-09-13-compact-event-settle: the harness's OWN "a compaction
|
|
202
|
+
// completed" event, which is the only signal that settles a compact as a
|
|
203
|
+
// FACT rather than as an inference from a ratio that fell.
|
|
204
|
+
//
|
|
205
|
+
// No `machineLocal` flag, so this lands in the shared, committed
|
|
206
|
+
// `.claude/settings.json` — the same file as the three SessionStart entries
|
|
207
|
+
// above, and NOT the machine-local file the workspace-init materializer
|
|
208
|
+
// owns. That routing IS the answer to "who owns the `hooks` key for this
|
|
209
|
+
// entry": the only writer that could delete it is the one that installed
|
|
210
|
+
// it. See `session-start-hook-constants.ts` for why it also carries no
|
|
211
|
+
// `shell` pin, and `mergeHooksTree` for the second line of defence if it
|
|
212
|
+
// ever moves.
|
|
213
|
+
entries.push({
|
|
214
|
+
sentinel: HOOK_COMPACT_SETTLE_SENTINEL,
|
|
215
|
+
matcher: HOOK_COMPACT_SETTLE_MATCHER,
|
|
216
|
+
command: HOOK_COMPACT_SETTLE_COMMAND,
|
|
217
|
+
event: HOOK_COMPACT_SETTLE_EVENT
|
|
218
|
+
});
|
|
164
219
|
}
|
|
165
220
|
return entries;
|
|
166
221
|
}
|
|
@@ -191,7 +246,11 @@ export function resolveLegacySentinels(ide) {
|
|
|
191
246
|
// it) exactly like the other two SessionStart entries. Without this the
|
|
192
247
|
// entry would be unremovable by `peaks hooks uninstall` — the rollback
|
|
193
248
|
// path T2 requires.
|
|
194
|
-
|
|
249
|
+
// rid 2026-09-13-compact-event-settle: the PostCompact settle sentinel joins
|
|
250
|
+
// too — same rollback argument as the reinject entry above. A hook that
|
|
251
|
+
// `peaks hooks uninstall` cannot remove is a hook the user cannot get rid
|
|
252
|
+
// of, and this one fires on every compaction.
|
|
253
|
+
return [...base, HOOK_OUTER_CACHE_SENTINEL, HOOK_WORKSPACE_INIT_SENTINEL, HOOK_POST_COMPACT_REINJECT_SENTINEL, HOOK_COMPACT_SETTLE_SENTINEL];
|
|
195
254
|
}
|
|
196
255
|
return base;
|
|
197
256
|
}
|
|
@@ -322,10 +322,20 @@ function shapeMatchesDesired(settings, entries, allPeaksSentinels) {
|
|
|
322
322
|
return false;
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
|
-
// (b) every desired entry must be on disk.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
// (b) every desired entry must be on disk AND carry the matcher it declares.
|
|
326
|
+
// Presence alone cannot see a WRONG matcher, and a wrong matcher is not
|
|
327
|
+
// cosmetic: `''` and `Bash|Task` route the same command to different
|
|
328
|
+
// tool sets, so the entry is present while the hook never fires on the
|
|
329
|
+
// tools it was installed for. Measured (rid `2026-09-13-compact-event-settle`,
|
|
330
|
+
// residual R2): a `PostCompact` entry hand-corrupted to matcher
|
|
331
|
+
// `auto|manual` survived `peaks hooks install` unchanged, and the
|
|
332
|
+
// installer was structurally unable to repair it.
|
|
333
|
+
// An absent matcher reads as `''` — the form Claude Code takes as "every
|
|
334
|
+
// source" — so a file written before matchers were explicit converges
|
|
335
|
+
// once instead of churning on every install.
|
|
336
|
+
for (const desired of entries.filter((e) => e.event === eventKey)) {
|
|
337
|
+
const onDisk = peaksPresent.find((entry) => (entry.hooks ?? []).some((h) => String(h.command ?? '').includes(desired.sentinel)));
|
|
338
|
+
if (onDisk === undefined || (onDisk.matcher ?? '') !== desired.matcher)
|
|
329
339
|
return false;
|
|
330
340
|
}
|
|
331
341
|
}
|
|
@@ -83,3 +83,48 @@ export declare const HOOK_WORKSPACE_INIT_SENTINEL = "peaks session primer";
|
|
|
83
83
|
export declare const HOOK_WORKSPACE_INIT_COMMAND = "peaks session primer --project \"${CLAUDE_PROJECT_DIR}\"";
|
|
84
84
|
/** SessionStart hook event key (same as outer-cache). */
|
|
85
85
|
export declare const HOOK_WORKSPACE_INIT_EVENT = "SessionStart";
|
|
86
|
+
/**
|
|
87
|
+
* rid `2026-09-13-compact-event-settle` — the `PostCompact` entry that lets the
|
|
88
|
+
* harness's own event settle a compact, instead of the next `context-now` probe
|
|
89
|
+
* inferring one from a ratio that fell.
|
|
90
|
+
*
|
|
91
|
+
* WHY THIS ENTRY IS NOT A `SessionStart` ONE, despite living in this file: the
|
|
92
|
+
* three entries above all ride `SessionStart` and differ only by matcher. A
|
|
93
|
+
* `PostCompact` hook is a different EVENT that carries the one fact no
|
|
94
|
+
* `SessionStart` payload has — whether the compaction the harness just
|
|
95
|
+
* completed was `auto` or `manual`. That distinction is the whole question
|
|
96
|
+
* ("has this machine ever auto-compacted?"), and without it peaks-loop can only
|
|
97
|
+
* ever see that SOMETHING compacted. See `compact-event-settle.ts` for what the
|
|
98
|
+
* command does with it.
|
|
99
|
+
*
|
|
100
|
+
* WHY THE MATCHER IS THE EMPTY STRING and not the documented `auto|manual`:
|
|
101
|
+
* both trigger values are wanted, so the matcher must filter nothing. An empty
|
|
102
|
+
* matcher is the convention the three `SessionStart` entries already rely on to
|
|
103
|
+
* match every source, and it is the only form that cannot fail SILENTLY — an
|
|
104
|
+
* alternation string is a match-everything pattern under regex semantics but
|
|
105
|
+
* matches NEITHER value under exact-equality semantics, and a hook that never
|
|
106
|
+
* fires looks exactly like a hook with nothing to report.
|
|
107
|
+
*/
|
|
108
|
+
export declare const HOOK_COMPACT_SETTLE_SENTINEL = "peaks compact settle";
|
|
109
|
+
/**
|
|
110
|
+
* The settle hook command. `--project "${CLAUDE_PROJECT_DIR}"` is byte-for-byte
|
|
111
|
+
* the shape of the three `SessionStart` entries above — Claude Code's standard
|
|
112
|
+
* project-root convention, resolved strictly on the CLI side (a hook payload is
|
|
113
|
+
* env-driven and must not be trusted as a path).
|
|
114
|
+
*
|
|
115
|
+
* The command prints NOTHING on the hook path and exits 0 for every outcome.
|
|
116
|
+
* `PostCompact`'s stdin/stdout contract is truncated in the retrievable docs,
|
|
117
|
+
* so the safe assumption is the `SessionStart` one — stdout may be added to the
|
|
118
|
+
* model's context. An error message there would be read as a fact. See
|
|
119
|
+
* `compact-event-settle.ts`.
|
|
120
|
+
*
|
|
121
|
+
* No `shell` pin, deliberately: this entry lands in the shared, committed
|
|
122
|
+
* `.claude/settings.json`, and a `powershell` pin there would break every
|
|
123
|
+
* macOS / Linux reader of the file. See `resolveHookEntries`' comment block for
|
|
124
|
+
* the full reason the three `SessionStart` entries are unpinned too.
|
|
125
|
+
*/
|
|
126
|
+
export declare const HOOK_COMPACT_SETTLE_COMMAND = "peaks compact settle --project \"${CLAUDE_PROJECT_DIR}\"";
|
|
127
|
+
/** The event this entry rides. Claude Code fires it after a compaction completes. */
|
|
128
|
+
export declare const HOOK_COMPACT_SETTLE_EVENT = "PostCompact";
|
|
129
|
+
/** Matcher: empty = every `trigger` (`auto` and `manual`). See the sentinel doc. */
|
|
130
|
+
export declare const HOOK_COMPACT_SETTLE_MATCHER = "";
|
|
@@ -83,3 +83,48 @@ export const HOOK_WORKSPACE_INIT_SENTINEL = 'peaks session primer';
|
|
|
83
83
|
export const HOOK_WORKSPACE_INIT_COMMAND = `peaks session primer --project "\${CLAUDE_PROJECT_DIR}"`;
|
|
84
84
|
/** SessionStart hook event key (same as outer-cache). */
|
|
85
85
|
export const HOOK_WORKSPACE_INIT_EVENT = 'SessionStart';
|
|
86
|
+
/**
|
|
87
|
+
* rid `2026-09-13-compact-event-settle` — the `PostCompact` entry that lets the
|
|
88
|
+
* harness's own event settle a compact, instead of the next `context-now` probe
|
|
89
|
+
* inferring one from a ratio that fell.
|
|
90
|
+
*
|
|
91
|
+
* WHY THIS ENTRY IS NOT A `SessionStart` ONE, despite living in this file: the
|
|
92
|
+
* three entries above all ride `SessionStart` and differ only by matcher. A
|
|
93
|
+
* `PostCompact` hook is a different EVENT that carries the one fact no
|
|
94
|
+
* `SessionStart` payload has — whether the compaction the harness just
|
|
95
|
+
* completed was `auto` or `manual`. That distinction is the whole question
|
|
96
|
+
* ("has this machine ever auto-compacted?"), and without it peaks-loop can only
|
|
97
|
+
* ever see that SOMETHING compacted. See `compact-event-settle.ts` for what the
|
|
98
|
+
* command does with it.
|
|
99
|
+
*
|
|
100
|
+
* WHY THE MATCHER IS THE EMPTY STRING and not the documented `auto|manual`:
|
|
101
|
+
* both trigger values are wanted, so the matcher must filter nothing. An empty
|
|
102
|
+
* matcher is the convention the three `SessionStart` entries already rely on to
|
|
103
|
+
* match every source, and it is the only form that cannot fail SILENTLY — an
|
|
104
|
+
* alternation string is a match-everything pattern under regex semantics but
|
|
105
|
+
* matches NEITHER value under exact-equality semantics, and a hook that never
|
|
106
|
+
* fires looks exactly like a hook with nothing to report.
|
|
107
|
+
*/
|
|
108
|
+
export const HOOK_COMPACT_SETTLE_SENTINEL = 'peaks compact settle';
|
|
109
|
+
/**
|
|
110
|
+
* The settle hook command. `--project "${CLAUDE_PROJECT_DIR}"` is byte-for-byte
|
|
111
|
+
* the shape of the three `SessionStart` entries above — Claude Code's standard
|
|
112
|
+
* project-root convention, resolved strictly on the CLI side (a hook payload is
|
|
113
|
+
* env-driven and must not be trusted as a path).
|
|
114
|
+
*
|
|
115
|
+
* The command prints NOTHING on the hook path and exits 0 for every outcome.
|
|
116
|
+
* `PostCompact`'s stdin/stdout contract is truncated in the retrievable docs,
|
|
117
|
+
* so the safe assumption is the `SessionStart` one — stdout may be added to the
|
|
118
|
+
* model's context. An error message there would be read as a fact. See
|
|
119
|
+
* `compact-event-settle.ts`.
|
|
120
|
+
*
|
|
121
|
+
* No `shell` pin, deliberately: this entry lands in the shared, committed
|
|
122
|
+
* `.claude/settings.json`, and a `powershell` pin there would break every
|
|
123
|
+
* macOS / Linux reader of the file. See `resolveHookEntries`' comment block for
|
|
124
|
+
* the full reason the three `SessionStart` entries are unpinned too.
|
|
125
|
+
*/
|
|
126
|
+
export const HOOK_COMPACT_SETTLE_COMMAND = `peaks compact settle --project "\${CLAUDE_PROJECT_DIR}"`;
|
|
127
|
+
/** The event this entry rides. Claude Code fires it after a compaction completes. */
|
|
128
|
+
export const HOOK_COMPACT_SETTLE_EVENT = 'PostCompact';
|
|
129
|
+
/** Matcher: empty = every `trigger` (`auto` and `manual`). See the sentinel doc. */
|
|
130
|
+
export const HOOK_COMPACT_SETTLE_MATCHER = '';
|
|
@@ -7,6 +7,20 @@ export type StatusLineStdin = {
|
|
|
7
7
|
cwd?: string;
|
|
8
8
|
session_id?: string;
|
|
9
9
|
caller_id?: string;
|
|
10
|
+
/**
|
|
11
|
+
* The harness's own context numbers. Declared here because this type IS the
|
|
12
|
+
* documented shape of the payload the harness pipes in; omitting a documented
|
|
13
|
+
* field would make the type lie by omission, and a consumer that reached for
|
|
14
|
+
* it would have to cast. Read (never written) by
|
|
15
|
+
* `harness-context-witness.ts` — see that module for why
|
|
16
|
+
* `context_window_size` is NOT a denominator.
|
|
17
|
+
*/
|
|
18
|
+
context_window?: {
|
|
19
|
+
context_window_size?: unknown;
|
|
20
|
+
used_percentage?: unknown;
|
|
21
|
+
remaining_percentage?: unknown;
|
|
22
|
+
current_usage?: Record<string, unknown> | undefined;
|
|
23
|
+
} | undefined;
|
|
10
24
|
};
|
|
11
25
|
export type StatusLineState = 'active' | 'idle' | 'stale' | 'invalid-presence';
|
|
12
26
|
export type StatusLinePresence = {
|