peaks-loop 4.0.47 → 4.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/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 +110 -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/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +4 -4
- package/dist/cli/commands/job-commands.js +8 -0
- package/dist/cli/commands/loop-eval-commands.js +31 -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 +22 -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 +140 -65
- package/dist/services/artifacts/request-artifact-service.d.ts +8 -0
- package/dist/services/artifacts/request-artifact-service.js +77 -46
- 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/enforcers/active-skill-resolver.js +14 -1
- 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 +194 -0
- package/dist/services/code/auto-compact-lifecycle.js +229 -11
- package/dist/services/code/auto-compact-orchestrator.js +118 -7
- package/dist/services/code/compact-event-settle.d.ts +134 -0
- package/dist/services/code/compact-event-settle.js +240 -0
- package/dist/services/compact-history/compact-history-service.d.ts +14 -0
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- 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/auto-compact-types.d.ts +20 -2
- 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/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- package/dist/services/final-review/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-progress-store.js +18 -3
- package/dist/services/job/job-state-store.js +7 -0
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +10 -3
- package/dist/services/observability/observability-service.js +16 -3
- 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 +124 -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 +23 -0
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/karpathy-service.js +2 -2
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- 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 +23 -0
- package/dist/services/workflow/pipeline-verify-gate-support.d.ts +47 -10
- package/dist/services/workflow/pipeline-verify-gate-support.js +221 -103
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +47 -33
- package/dist/services/workflow/pipeline-verify-types.d.ts +15 -6
- 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/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -0
- 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
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rid 2026-09-14-gate-h-promotion (R2; repaired by R8) — what a promotion's
|
|
3
|
+
* artifact has to PROVE.
|
|
4
|
+
*
|
|
5
|
+
* The three layers used to be checked with `text.includes(<rule name>)` over the
|
|
6
|
+
* whole file. A substring test cannot tell "the rule is registered" from "a
|
|
7
|
+
* comment saying the rule is absent", so a refusal tree passed every layer:
|
|
8
|
+
*
|
|
9
|
+
* - layer A: a `.peaks/sops/registry.json` that is not valid JSON, with the id
|
|
10
|
+
* still somewhere in its bytes;
|
|
11
|
+
* - layer B: a settings template whose only mention of the rule reads
|
|
12
|
+
* "do NOT add a matcher for <rule>";
|
|
13
|
+
* - layer C: a `mode-gate.ts` whose only mention reads
|
|
14
|
+
* "// TODO: <rule> is DELIBERATELY NOT a hard-floor category".
|
|
15
|
+
*
|
|
16
|
+
* All three are the same defect facing the other way: an unreadable artifact is
|
|
17
|
+
* treated as a permitted one. Each check below therefore parses its evidence and
|
|
18
|
+
* asserts the SHAPE, and every failure is a finding — never a warning that
|
|
19
|
+
* permits. Nothing here throws: an unparseable file is reported, not swallowed.
|
|
20
|
+
*
|
|
21
|
+
* R8 (same rid) — the same defect survived inside R2's repair, in two places:
|
|
22
|
+
*
|
|
23
|
+
* - layer C matched a member's `doc` by name MENTION, so an adverse line
|
|
24
|
+
* sitting between two vocabulary members became the following member's doc
|
|
25
|
+
* and the verdict flipped on where the comment sat. The predicate is now this
|
|
26
|
+
* repo's own citation form — the memory cited by PATH,
|
|
27
|
+
* `.peaks/memory/<id>.md`, exactly as `mode-gate.ts:41` does — rather than
|
|
28
|
+
* the memory merely named. That citation requirement is what closes the
|
|
29
|
+
* placement dependence; the doc is also read from the comment SPANS attached
|
|
30
|
+
* to the member rather than from the raw slice, which states "code is not a
|
|
31
|
+
* doc" as an invariant rather than relying on the raw slice to honour it;
|
|
32
|
+
* - layer B kept `matcher.includes(id)` / `command.includes(id)` over free
|
|
33
|
+
* text, so `command: "echo 'do NOT add a matcher for rule-x'"` registered a
|
|
34
|
+
* rule by writing a sentence about it. Both fields are now parsed: a matcher
|
|
35
|
+
* is a tool selector, a command is argv.
|
|
36
|
+
*
|
|
37
|
+
* R10 (same rid) — layer C read the `HardFloorCategory` union and the
|
|
38
|
+
* `HARD_FLOOR_CATEGORIES` array as ONE flattened member list, so a member of the
|
|
39
|
+
* union alone satisfied the check. Measured on the R8 bytes: a union-only literal
|
|
40
|
+
* — and a union-only member whose doc cited `.peaks/memory/<id>.md` — were both
|
|
41
|
+
* reported BACKED, while `isHardFloorCategory` (which reads the array) returned
|
|
42
|
+
* false and `shouldPauseAtGate` returned `shouldPause: false`. The gate certified
|
|
43
|
+
* a hard floor that paused nothing, contradicting its own message. Only the array
|
|
44
|
+
* enforces, so only the array is now evidence — for the name and for the citation
|
|
45
|
+
* alike. The union is still READ, for one reason only: a category's doc belongs
|
|
46
|
+
* to the category, and this repo documents `commit-boundary-side-effect` beside
|
|
47
|
+
* the union member while the array enforces it.
|
|
48
|
+
*/
|
|
49
|
+
/** Is `value` a JSON object (not null, not an array)? */
|
|
50
|
+
function isRecord(value) {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function manifestFailure(parsed, id) {
|
|
54
|
+
if (!isRecord(parsed))
|
|
55
|
+
return 'not a JSON object';
|
|
56
|
+
if (parsed.id !== id)
|
|
57
|
+
return `does not declare id "${id}"`;
|
|
58
|
+
if (!Array.isArray(parsed.gates))
|
|
59
|
+
return 'has no "gates" array';
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function registryFailure(parsed, id) {
|
|
63
|
+
if (!isRecord(parsed))
|
|
64
|
+
return 'not a JSON object';
|
|
65
|
+
if (!Array.isArray(parsed.sops))
|
|
66
|
+
return 'has no "sops" array';
|
|
67
|
+
const registered = parsed.sops.some((entry) => isRecord(entry) && entry.id === id);
|
|
68
|
+
return registered ? null : `registry has no SOP entry with id "${id}"`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A tool selector: `Name` or `Name(pattern)`, one or more separated by `|` —
|
|
72
|
+
* `Bash`, `Write|Edit|MultiEdit`, `Bash(git push:*)`. A segment that is not a
|
|
73
|
+
* tool-name token (a hyphenated rule name, say) makes the selector malformed.
|
|
74
|
+
*/
|
|
75
|
+
const MATCHER_SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*\s*(\(.*\))?$/;
|
|
76
|
+
function isToolMatcher(matcher) {
|
|
77
|
+
const segments = matcher.split('|');
|
|
78
|
+
return segments.every((segment) => MATCHER_SEGMENT_RE.test(segment.trim()));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Split a hook command the way a shell does: whitespace separates words, and
|
|
82
|
+
* quotes group whitespace INTO a word rather than ending it. So a phrase inside
|
|
83
|
+
* a string literal arrives as one long word, while a path arrives short.
|
|
84
|
+
*/
|
|
85
|
+
function commandWords(command) {
|
|
86
|
+
const words = [];
|
|
87
|
+
let current = '';
|
|
88
|
+
let started = false;
|
|
89
|
+
let quote = null;
|
|
90
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
91
|
+
const char = command[i];
|
|
92
|
+
if (char === '\\' && quote !== "'" && i + 1 < command.length) {
|
|
93
|
+
current += command[i + 1];
|
|
94
|
+
started = true;
|
|
95
|
+
i += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (quote !== null) {
|
|
99
|
+
if (char === quote)
|
|
100
|
+
quote = null;
|
|
101
|
+
else
|
|
102
|
+
current += char;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (char === '"' || char === "'") {
|
|
106
|
+
quote = char;
|
|
107
|
+
started = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (/\s/.test(char)) {
|
|
111
|
+
if (started)
|
|
112
|
+
words.push(current);
|
|
113
|
+
current = '';
|
|
114
|
+
started = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
current += char;
|
|
118
|
+
started = true;
|
|
119
|
+
}
|
|
120
|
+
if (started)
|
|
121
|
+
words.push(current);
|
|
122
|
+
return words;
|
|
123
|
+
}
|
|
124
|
+
const SCRIPT_EXTENSION_RE = /\.(?:c|m)?[jt]s$|\.(?:sh|bash|ps1|py|rb)$/i;
|
|
125
|
+
/**
|
|
126
|
+
* Does this hook `command` RUN something named after the rule?
|
|
127
|
+
*
|
|
128
|
+
* A command is argv, not prose. The rule counts only when its name sits inside a
|
|
129
|
+
* single word that (a) is not a phrase — no embedded whitespace, so a sentence
|
|
130
|
+
* inside a string literal is out — and (b) designates a file the hook executes:
|
|
131
|
+
* a path, or a script by extension.
|
|
132
|
+
*
|
|
133
|
+
* `echo 'do NOT add a matcher for rule-x'` parses to two words, the second of
|
|
134
|
+
* which holds the rule's name and four spaces: a sentence ABOUT the rule, not an
|
|
135
|
+
* invocation of it. `node scripts/enforce-rule-x.js` parses to two words, the
|
|
136
|
+
* second a path. That difference is the check.
|
|
137
|
+
*/
|
|
138
|
+
function commandRegistersRule(command, id) {
|
|
139
|
+
return commandWords(command).some((word) => {
|
|
140
|
+
if (/\s/.test(word))
|
|
141
|
+
return false;
|
|
142
|
+
if (!word.includes(id))
|
|
143
|
+
return false;
|
|
144
|
+
return word.includes('/') || word.includes('\\') || SCRIPT_EXTENSION_RE.test(word);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Layer B. A hook group is `{ matcher?, hooks: [...] }`. The matcher SELECTS
|
|
149
|
+
* TOOLS, so it can never name a rule: a group whose selector is not a tool
|
|
150
|
+
* matcher fires nothing and registers nothing. The registration itself is a hook
|
|
151
|
+
* command that runs something named after the rule — prose elsewhere in the file
|
|
152
|
+
* (an `env` note saying "do NOT add a matcher") is not a registration, and
|
|
153
|
+
* neither is a sentence in a command.
|
|
154
|
+
*/
|
|
155
|
+
function hookFailure(parsed, id) {
|
|
156
|
+
if (!isRecord(parsed))
|
|
157
|
+
return 'not a JSON object';
|
|
158
|
+
if (!isRecord(parsed.hooks))
|
|
159
|
+
return 'has no "hooks" object';
|
|
160
|
+
for (const groups of Object.values(parsed.hooks)) {
|
|
161
|
+
if (!Array.isArray(groups))
|
|
162
|
+
continue;
|
|
163
|
+
for (const group of groups) {
|
|
164
|
+
if (!isRecord(group))
|
|
165
|
+
continue;
|
|
166
|
+
if (typeof group.matcher === 'string' && !isToolMatcher(group.matcher))
|
|
167
|
+
continue;
|
|
168
|
+
const hooks = group.hooks;
|
|
169
|
+
if (!Array.isArray(hooks))
|
|
170
|
+
continue;
|
|
171
|
+
const registers = hooks.some((h) => isRecord(h) && typeof h.command === 'string' && commandRegistersRule(h.command, id));
|
|
172
|
+
if (registers)
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return `no hook registration runs anything named "${id}" (a hook command must invoke a file named after the rule)`;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* One pattern, used by BOTH the masker and the doc reader, so a comment can
|
|
180
|
+
* never be blanked by one and read as a doc by the other.
|
|
181
|
+
*/
|
|
182
|
+
const COMMENT_RE = /\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
|
|
183
|
+
/** Comments are blanked to spaces, which keeps every offset in the file valid. */
|
|
184
|
+
function maskComments(text) {
|
|
185
|
+
return text.replace(COMMENT_RE, (comment) => ' '.repeat(comment.length));
|
|
186
|
+
}
|
|
187
|
+
function commentSpans(source) {
|
|
188
|
+
const spans = [];
|
|
189
|
+
COMMENT_RE.lastIndex = 0;
|
|
190
|
+
let match;
|
|
191
|
+
while ((match = COMMENT_RE.exec(source)) !== null) {
|
|
192
|
+
spans.push({ start: match.index, end: match.index + match[0].length, text: match[0] });
|
|
193
|
+
}
|
|
194
|
+
return spans;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Collect the members of the hard-floor vocabulary, each with the comment text
|
|
198
|
+
* that governs it. `masked` decides where a member STARTS (so a quoted string
|
|
199
|
+
* inside a comment is not a member, and no comment can inject the `;` / `]` that
|
|
200
|
+
* bounds a declaration); the doc is read from the comment SPANS inside the
|
|
201
|
+
* member's slot. Taking it from the raw slice instead — as R2 did — let CODE
|
|
202
|
+
* between two members become a doc, which is what made the verdict depend on
|
|
203
|
+
* where an adverse line sat.
|
|
204
|
+
*/
|
|
205
|
+
function collectMembers(masked, comments, start, end, enforcing, out) {
|
|
206
|
+
const body = masked.slice(start, end);
|
|
207
|
+
const literalRe = /'([^']*)'/g;
|
|
208
|
+
let match;
|
|
209
|
+
let previousEnd = 0;
|
|
210
|
+
while ((match = literalRe.exec(body)) !== null) {
|
|
211
|
+
// A member literal is preceded by nothing but whitespace, a `|` (union) or a
|
|
212
|
+
// `,` (array). Anything else — a `'` inside prose, say — is not a member.
|
|
213
|
+
if (/^[\s|,]*$/.test(body.slice(previousEnd, match.index))) {
|
|
214
|
+
const from = start + previousEnd;
|
|
215
|
+
const to = start + match.index;
|
|
216
|
+
out.push({
|
|
217
|
+
literal: match[1],
|
|
218
|
+
doc: comments
|
|
219
|
+
.filter((comment) => comment.start >= from && comment.end <= to)
|
|
220
|
+
.map((comment) => comment.text)
|
|
221
|
+
.join('\n'),
|
|
222
|
+
enforcing
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
previousEnd = match.index + match[0].length;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The two declarations that spell the vocabulary: the `HardFloorCategory` union
|
|
230
|
+
* (a type) and `HARD_FLOOR_CATEGORIES` (the array `isHardFloorCategory` reads).
|
|
231
|
+
* They are the same vocabulary, but only the ARRAY enforces — see
|
|
232
|
+
* `mergeByLiteral` for why the union is still read.
|
|
233
|
+
*/
|
|
234
|
+
function vocabularyMembers(source) {
|
|
235
|
+
const masked = maskComments(source);
|
|
236
|
+
const comments = commentSpans(source);
|
|
237
|
+
const raw = [];
|
|
238
|
+
const union = /export\s+type\s+HardFloorCategory\s*=/.exec(masked);
|
|
239
|
+
if (union) {
|
|
240
|
+
const start = union.index + union[0].length;
|
|
241
|
+
const end = masked.indexOf(';', start);
|
|
242
|
+
if (end > start)
|
|
243
|
+
collectMembers(masked, comments, start, end, false, raw);
|
|
244
|
+
}
|
|
245
|
+
const array = /HARD_FLOOR_CATEGORIES\s*:/.exec(masked);
|
|
246
|
+
if (array) {
|
|
247
|
+
const equals = masked.indexOf('=', array.index);
|
|
248
|
+
const open = masked.indexOf('[', equals);
|
|
249
|
+
const close = masked.indexOf(']', open + 1);
|
|
250
|
+
if (equals > array.index && open > equals && close > open) {
|
|
251
|
+
collectMembers(masked, comments, open + 1, close, true, raw);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return mergeByLiteral(raw);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* R10 — one entry per literal, and `enforcing` is only ever set by the array.
|
|
258
|
+
*
|
|
259
|
+
* Reading the two declarations as one flat member list let a member of the
|
|
260
|
+
* `HardFloorCategory` union ALONE satisfy this check while
|
|
261
|
+
* `isHardFloorCategory` — and therefore `shouldPauseAtGate` — did not recognise
|
|
262
|
+
* it. The gate reported BACKED for a hard floor that paused nothing. Splitting
|
|
263
|
+
* the provenance of `enforcing` out of the member list is what makes the two
|
|
264
|
+
* agree: what the predicate accepts is exactly what the array contains.
|
|
265
|
+
*
|
|
266
|
+
* The docs are joined across occurrences rather than kept per-declaration,
|
|
267
|
+
* because a category's doc belongs to the CATEGORY. This repo's own layer-C
|
|
268
|
+
* promotion cites `.peaks/memory/2026-06-28-full-auto-boundary.md` from the
|
|
269
|
+
* comment beside the UNION member of `commit-boundary-side-effect`, while the
|
|
270
|
+
* ARRAY is what enforces it; reading the doc from one declaration only would
|
|
271
|
+
* make that citation unreadable — a false negative on a file already known good.
|
|
272
|
+
*/
|
|
273
|
+
function mergeByLiteral(members) {
|
|
274
|
+
const merged = new Map();
|
|
275
|
+
for (const member of members) {
|
|
276
|
+
const previous = merged.get(member.literal);
|
|
277
|
+
merged.set(member.literal, {
|
|
278
|
+
literal: member.literal,
|
|
279
|
+
doc: [previous?.doc, member.doc].filter((doc) => doc !== undefined && doc !== '').join('\n'),
|
|
280
|
+
enforcing: (previous?.enforcing ?? false) || member.enforcing
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return [...merged.values()];
|
|
284
|
+
}
|
|
285
|
+
/** A memory cited by path, the way this repo cites one (`mode-gate.ts:41`). */
|
|
286
|
+
const MEMORY_CITATION_RE = /\.peaks\/memory\/([A-Za-z0-9._-]+)\.md/g;
|
|
287
|
+
function citedMemories(doc) {
|
|
288
|
+
return Array.from(doc.matchAll(MEMORY_CITATION_RE), (match) => match[1]);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Layer C. The rule counts when it IS a member of `HARD_FLOOR_CATEGORIES`, or
|
|
292
|
+
* when one of THAT ARRAY's members has a doc block that CITES it the way this
|
|
293
|
+
* repo cites a memory — by path: `commit-boundary-side-effect` cites
|
|
294
|
+
* `.peaks/memory/2026-06-28-full-auto-boundary.md`.
|
|
295
|
+
*
|
|
296
|
+
* R10 — both clauses are gated on `enforcing`. Certifying membership in the
|
|
297
|
+
* `HardFloorCategory` union alone is what let the check report BACKED for a
|
|
298
|
+
* category that `isHardFloorCategory` rejects and `shouldPauseAtGate` ignores.
|
|
299
|
+
*
|
|
300
|
+
* A citation is a deliberate act with a shape; a name is a word that any
|
|
301
|
+
* sentence can carry. `// TODO: <rule> is DELIBERATELY NOT a hard-floor
|
|
302
|
+
* category` names the rule while saying the opposite, and is rejected in every
|
|
303
|
+
* placement — the predicate never reads the sentence the citation sits in, so
|
|
304
|
+
* prose that negates a citation is a residual this cannot see. What it does see
|
|
305
|
+
* is the difference between being cited and being mentioned.
|
|
306
|
+
*/
|
|
307
|
+
function hardFloorFailure(source, id) {
|
|
308
|
+
const registered = vocabularyMembers(source).some((member) => member.enforcing && (member.literal === id || citedMemories(member.doc).includes(id)));
|
|
309
|
+
return registered
|
|
310
|
+
? null
|
|
311
|
+
: `no hard-floor category names "${id}" (it must be a member of HARD_FLOOR_CATEGORIES, or cited by one as ".peaks/memory/${id}.md")`;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The reason `check` is unsatisfied by `text`, or `null` when it is satisfied.
|
|
315
|
+
* Never throws: a file that cannot be parsed yields the reason it could not be.
|
|
316
|
+
*/
|
|
317
|
+
export function artifactEvidenceFailure(check, text) {
|
|
318
|
+
if (check.evidence === 'hard-floor-category')
|
|
319
|
+
return hardFloorFailure(text, check.id);
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = JSON.parse(text);
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
return `not valid JSON: ${err.message}`;
|
|
326
|
+
}
|
|
327
|
+
if (check.evidence === 'sop-manifest')
|
|
328
|
+
return manifestFailure(parsed, check.id);
|
|
329
|
+
if (check.evidence === 'sop-registry-entry')
|
|
330
|
+
return registryFailure(parsed, check.id);
|
|
331
|
+
return hookFailure(parsed, check.id);
|
|
332
|
+
}
|
|
@@ -252,6 +252,15 @@ interface EvidenceSource {
|
|
|
252
252
|
readonly label: string;
|
|
253
253
|
/** Path segments under `.peaks/_runtime/<sessionId>/`. */
|
|
254
254
|
readonly segments: readonly string[];
|
|
255
|
+
/**
|
|
256
|
+
* An older location of the SAME artifact, tried only when `segments` is not
|
|
257
|
+
* on disk. Slice `2026-09-14-prd-capsule-rid-scoping` moved the PRD handoff
|
|
258
|
+
* capsule to `prd/handoff-<rid>.md`; sessions written before it hold only
|
|
259
|
+
* the bare `prd/handoff.md`, and this module's delivery gate keys on that
|
|
260
|
+
* source — so a source that goes missing does not fail the gate, it stops
|
|
261
|
+
* it (`enforceScopeContractDelivery` returns early on `missing`).
|
|
262
|
+
*/
|
|
263
|
+
readonly legacySegments?: readonly string[];
|
|
255
264
|
/** Dimensions this source can supply evidence for. */
|
|
256
265
|
readonly supports: readonly DimensionKind[];
|
|
257
266
|
/** What DELIVERED means for this source. See `isDelivered()` — every source
|
|
@@ -474,7 +474,10 @@ function evidenceSourcesFor(rid, prePostDiffAvailable) {
|
|
|
474
474
|
{
|
|
475
475
|
key: SCOPE_CONTRACT_SOURCE_KEY,
|
|
476
476
|
label: 'PRD handoff (approved scope + non-goals)',
|
|
477
|
-
|
|
477
|
+
// One capsule per slice since `2026-09-14-prd-capsule-rid-scoping`; the
|
|
478
|
+
// bare name is the pre-scoping tier and still lives on 3 sessions.
|
|
479
|
+
segments: ['prd', `handoff-${rid}.md`],
|
|
480
|
+
legacySegments: ['prd', 'handoff.md'],
|
|
478
481
|
supports: ['functional-completeness', 'existing-functionality-intact'],
|
|
479
482
|
delivery: { kind: 'whole' }
|
|
480
483
|
}
|
|
@@ -508,22 +511,43 @@ function classifyReadFailure(error) {
|
|
|
508
511
|
function readEvidence(projectRoot, sessionId, rid, prePostDiffAvailable) {
|
|
509
512
|
const runtimeRoot = join(projectRoot, '.peaks', '_runtime', sessionId);
|
|
510
513
|
return evidenceSourcesFor(rid, prePostDiffAvailable).map(source => {
|
|
511
|
-
|
|
512
|
-
|
|
514
|
+
// The canonical location first; an older one is tried only when the
|
|
515
|
+
// canonical file is genuinely ABSENT (see `legacySegments`). A file that
|
|
516
|
+
// is present but unreadable stops the walk — falling through to an older
|
|
517
|
+
// copy would silently swap the evidence this run reports.
|
|
518
|
+
const attempts = [
|
|
519
|
+
source.segments,
|
|
520
|
+
...(source.legacySegments !== undefined ? [source.legacySegments] : [])
|
|
521
|
+
];
|
|
522
|
+
// The path reported when nothing resolved stays the CANONICAL one, so the
|
|
523
|
+
// operator is sent to where the artifact belongs, not to its old home.
|
|
524
|
+
let resolved = source.segments;
|
|
513
525
|
let raw = null;
|
|
514
526
|
let error = '';
|
|
515
|
-
let read = '
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
527
|
+
let read = 'missing';
|
|
528
|
+
for (const segments of attempts) {
|
|
529
|
+
try {
|
|
530
|
+
raw = readFileSync(join(runtimeRoot, ...segments));
|
|
531
|
+
read = 'ok';
|
|
532
|
+
resolved = segments;
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
read = classifyReadFailure(err);
|
|
537
|
+
error = err instanceof Error ? err.message : String(err);
|
|
538
|
+
// A file that exists but cannot be read IS this source's file, so it
|
|
539
|
+
// is also the path the report must name — walking on to an older copy
|
|
540
|
+
// would silently swap the evidence this run reports.
|
|
541
|
+
if (read === 'unreadable') {
|
|
542
|
+
resolved = segments;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
522
546
|
}
|
|
523
547
|
return {
|
|
524
548
|
source,
|
|
525
|
-
relativePath,
|
|
526
|
-
absolutePath,
|
|
549
|
+
relativePath: ['.peaks', '_runtime', sessionId, ...resolved].join('/'),
|
|
550
|
+
absolutePath: join(runtimeRoot, ...resolved),
|
|
527
551
|
raw,
|
|
528
552
|
read,
|
|
529
553
|
error,
|
|
@@ -15,6 +15,25 @@ export declare function getAdapter(ide: IdeId): IdeAdapter;
|
|
|
15
15
|
export declare function tryGetAdapter(ide: string): IdeAdapter | undefined;
|
|
16
16
|
/** All registered adapter ids (insertion order). */
|
|
17
17
|
export declare function listAdapterIds(): readonly IdeId[];
|
|
18
|
+
/**
|
|
19
|
+
* Help text for every `--ide <id>` option, derived from the registry.
|
|
20
|
+
*
|
|
21
|
+
* It used to be a hand-written literal — `"target adapter id (claude-code |
|
|
22
|
+
* trae); default: auto-detect from env/cwd"` — copy-pasted into six option
|
|
23
|
+
* declarations across `hooks-commands.ts` and `statusline-commands.ts`. By the
|
|
24
|
+
* time anyone read it the registry had nine adapters, so the help named two of
|
|
25
|
+
* the nine values the option accepts and stayed silent about the other seven.
|
|
26
|
+
* A help string that enumerates a set it does not own cannot be kept true by
|
|
27
|
+
* discipline; deriving it here is the only form that cannot drift.
|
|
28
|
+
*
|
|
29
|
+
* Note for whoever is tempted to point the help at `peaks adapter list`
|
|
30
|
+
* instead: that command lists USER-REGISTERED vendor adapters from
|
|
31
|
+
* `.peaks/runtime/adapters.json` (a different registry, empty on a fresh
|
|
32
|
+
* project). It is not this set. The nine ids below have no CLI surface of
|
|
33
|
+
* their own; `peaks ide model --current` is the closest read-only viewer and
|
|
34
|
+
* it reports one adapter, not the list.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveIdeOptionHelp(): string;
|
|
18
37
|
/** All registered adapters (insertion order). */
|
|
19
38
|
export declare function listAdapters(): readonly IdeAdapter[];
|
|
20
39
|
/**
|
|
@@ -57,6 +57,27 @@ export function tryGetAdapter(ide) {
|
|
|
57
57
|
export function listAdapterIds() {
|
|
58
58
|
return Array.from(ADAPTERS.keys());
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Help text for every `--ide <id>` option, derived from the registry.
|
|
62
|
+
*
|
|
63
|
+
* It used to be a hand-written literal — `"target adapter id (claude-code |
|
|
64
|
+
* trae); default: auto-detect from env/cwd"` — copy-pasted into six option
|
|
65
|
+
* declarations across `hooks-commands.ts` and `statusline-commands.ts`. By the
|
|
66
|
+
* time anyone read it the registry had nine adapters, so the help named two of
|
|
67
|
+
* the nine values the option accepts and stayed silent about the other seven.
|
|
68
|
+
* A help string that enumerates a set it does not own cannot be kept true by
|
|
69
|
+
* discipline; deriving it here is the only form that cannot drift.
|
|
70
|
+
*
|
|
71
|
+
* Note for whoever is tempted to point the help at `peaks adapter list`
|
|
72
|
+
* instead: that command lists USER-REGISTERED vendor adapters from
|
|
73
|
+
* `.peaks/runtime/adapters.json` (a different registry, empty on a fresh
|
|
74
|
+
* project). It is not this set. The nine ids below have no CLI surface of
|
|
75
|
+
* their own; `peaks ide model --current` is the closest read-only viewer and
|
|
76
|
+
* it reports one adapter, not the list.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveIdeOptionHelp() {
|
|
79
|
+
return `target adapter id (${listAdapterIds().join(' | ')}); default: auto-detect from env/cwd`;
|
|
80
|
+
}
|
|
60
81
|
/** All registered adapters (insertion order). */
|
|
61
82
|
export function listAdapters() {
|
|
62
83
|
return Array.from(ADAPTERS.values());
|
|
@@ -22,6 +22,21 @@
|
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { z } from 'zod';
|
|
25
|
+
import { guardRuntimeSegment, runtimeRoot } from '../../shared/runtime-root.js';
|
|
26
|
+
/**
|
|
27
|
+
* The single place a job-progress directory is built, and therefore the single
|
|
28
|
+
* place the two ids that reach it are guarded.
|
|
29
|
+
*
|
|
30
|
+
* Slice 2026-09-15 (runtime-path-unrepresentable): this join used to be written
|
|
31
|
+
* at three sites, none of them guarded, and the shipped text rule could not see
|
|
32
|
+
* them — they sit in the service layer, outside the command layer it scans.
|
|
33
|
+
* Both ids are caller-supplied (`peaks job checkpoint` takes them from flags).
|
|
34
|
+
* The seam now requires `GuardedSegment`, so dropping either guard is a compile
|
|
35
|
+
* error rather than a finding someone has to notice.
|
|
36
|
+
*/
|
|
37
|
+
function jobProgressDir(projectRoot, sessionId, jobId) {
|
|
38
|
+
return runtimeRoot(projectRoot).join(guardRuntimeSegment(sessionId, 'session id'), guardRuntimeSegment('job', 'role'), guardRuntimeSegment(jobId, 'job id'));
|
|
39
|
+
}
|
|
25
40
|
export const JOB_PROGRESS_SCHEMA_VERSION = 1;
|
|
26
41
|
export const JobProgressSchema = z.object({
|
|
27
42
|
schemaVersion: z.literal(JOB_PROGRESS_SCHEMA_VERSION),
|
|
@@ -33,7 +48,7 @@ export const JobProgressSchema = z.object({
|
|
|
33
48
|
updatedAt: z.string().datetime()
|
|
34
49
|
});
|
|
35
50
|
export function writeJobProgress(projectRoot, sessionId, input) {
|
|
36
|
-
const dir =
|
|
51
|
+
const dir = jobProgressDir(projectRoot, sessionId, input.jobId);
|
|
37
52
|
mkdirSync(dir, { recursive: true });
|
|
38
53
|
const record = {
|
|
39
54
|
schemaVersion: JOB_PROGRESS_SCHEMA_VERSION,
|
|
@@ -49,7 +64,7 @@ export function writeJobProgress(projectRoot, sessionId, input) {
|
|
|
49
64
|
return record;
|
|
50
65
|
}
|
|
51
66
|
export function readJobProgress(projectRoot, sessionId, jobId) {
|
|
52
|
-
const path = join(projectRoot,
|
|
67
|
+
const path = join(jobProgressDir(projectRoot, sessionId, jobId), 'progress.json');
|
|
53
68
|
if (!existsSync(path)) {
|
|
54
69
|
throw new Error(`JobProgressStore: no progress for ${jobId} at ${path}`);
|
|
55
70
|
}
|
|
@@ -57,7 +72,7 @@ export function readJobProgress(projectRoot, sessionId, jobId) {
|
|
|
57
72
|
return JobProgressSchema.parse(JSON.parse(raw));
|
|
58
73
|
}
|
|
59
74
|
export function tryReadJobProgress(projectRoot, sessionId, jobId) {
|
|
60
|
-
const path = join(projectRoot,
|
|
75
|
+
const path = join(jobProgressDir(projectRoot, sessionId, jobId), 'progress.json');
|
|
61
76
|
if (!existsSync(path))
|
|
62
77
|
return null;
|
|
63
78
|
try {
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { JobStateSchema } from './job-types.js';
|
|
4
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
4
5
|
export class JobStateStore {
|
|
5
6
|
rootDir;
|
|
6
7
|
constructor(rootDir) {
|
|
7
8
|
this.rootDir = rootDir;
|
|
8
9
|
}
|
|
9
10
|
jobDir(jobId) {
|
|
11
|
+
// JobId axis. This is the only join the store performs, so one guard here
|
|
12
|
+
// covers every `peaks job *` subcommand — ten CLI call sites hand the
|
|
13
|
+
// store a caller-supplied `--job-id` and none of them checked it.
|
|
14
|
+
if (isUnsafePathInput(jobId)) {
|
|
15
|
+
throw new Error(`Invalid job id: ${jobId} (must be a single path segment)`);
|
|
16
|
+
}
|
|
10
17
|
return join(this.rootDir, jobId);
|
|
11
18
|
}
|
|
12
19
|
init(input) {
|
|
@@ -21,6 +21,19 @@ export declare const MAX_METRICS_FILES = 10;
|
|
|
21
21
|
export declare function metricsFilePath(projectRoot: string, sessionId: string): string;
|
|
22
22
|
/** Absolute path to a session's metrics directory. */
|
|
23
23
|
export declare function metricsDirPath(projectRoot: string, sessionId: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Absolute path to a session's metrics JSONL file, or `null` when the
|
|
26
|
+
* session id names no session directory.
|
|
27
|
+
*
|
|
28
|
+
* Total sibling of `metricsFilePath`, built on the axis's own total
|
|
29
|
+
* entry (`tryGetSessionDir`) so the two agree on the predicate. It
|
|
30
|
+
* exists because this module has frames whose contract is never-throws
|
|
31
|
+
* (`readMetricLines`, and through it `readObservabilityEvents`) and
|
|
32
|
+
* `metricsFilePath` was the one call in them that could still throw.
|
|
33
|
+
* `null` here means exactly one thing — the id could not be resolved —
|
|
34
|
+
* and every caller below handles it in the same statement that reads it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function tryMetricsFilePath(projectRoot: string, sessionId: string): string | null;
|
|
24
37
|
/**
|
|
25
38
|
* Append one line to the session's metrics JSONL file. Creates the
|
|
26
39
|
* directory tree on demand. Returns true on success, false on any
|
|
@@ -32,6 +45,12 @@ export declare function appendMetricLine(projectRoot: string, sessionId: string,
|
|
|
32
45
|
* Returns [] when the file does not exist. Does NOT parse — callers
|
|
33
46
|
* decide what counts as valid (see `observability-service.ts`
|
|
34
47
|
* `readObservabilityEvents` for the schema-aware reader).
|
|
48
|
+
*
|
|
49
|
+
* Returns [] for an unresolvable session id too: this is the read half
|
|
50
|
+
* of a fire-and-forget store, and `readObservabilityEvents` documents
|
|
51
|
+
* that a session with no readable metrics file has no events. Throwing
|
|
52
|
+
* here used to escape as a raw guard error out of a documented
|
|
53
|
+
* never-throws reader (repair R6).
|
|
35
54
|
*/
|
|
36
55
|
export declare function readMetricLines(projectRoot: string, sessionId: string): string[];
|
|
37
56
|
export type SessionMetricsEntry = {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
|
-
import { getSessionDir } from '../session/getSessionDir.js';
|
|
19
|
+
import { getSessionDir, tryGetSessionDir } from '../session/getSessionDir.js';
|
|
20
20
|
export const METRICS_DIR = 'metrics';
|
|
21
21
|
export const METRICS_FILENAME = 'slices.jsonl';
|
|
22
22
|
export const MAX_METRICS_FILES = 10;
|
|
@@ -28,6 +28,22 @@ export function metricsFilePath(projectRoot, sessionId) {
|
|
|
28
28
|
export function metricsDirPath(projectRoot, sessionId) {
|
|
29
29
|
return join(getSessionDir(projectRoot, sessionId), METRICS_DIR);
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Absolute path to a session's metrics JSONL file, or `null` when the
|
|
33
|
+
* session id names no session directory.
|
|
34
|
+
*
|
|
35
|
+
* Total sibling of `metricsFilePath`, built on the axis's own total
|
|
36
|
+
* entry (`tryGetSessionDir`) so the two agree on the predicate. It
|
|
37
|
+
* exists because this module has frames whose contract is never-throws
|
|
38
|
+
* (`readMetricLines`, and through it `readObservabilityEvents`) and
|
|
39
|
+
* `metricsFilePath` was the one call in them that could still throw.
|
|
40
|
+
* `null` here means exactly one thing — the id could not be resolved —
|
|
41
|
+
* and every caller below handles it in the same statement that reads it.
|
|
42
|
+
*/
|
|
43
|
+
export function tryMetricsFilePath(projectRoot, sessionId) {
|
|
44
|
+
const resolved = tryGetSessionDir(projectRoot, sessionId);
|
|
45
|
+
return resolved.ok ? join(resolved.dir, METRICS_DIR, METRICS_FILENAME) : null;
|
|
46
|
+
}
|
|
31
47
|
/**
|
|
32
48
|
* Append one line to the session's metrics JSONL file. Creates the
|
|
33
49
|
* directory tree on demand. Returns true on success, false on any
|
|
@@ -51,9 +67,18 @@ export function appendMetricLine(projectRoot, sessionId, line) {
|
|
|
51
67
|
* Returns [] when the file does not exist. Does NOT parse — callers
|
|
52
68
|
* decide what counts as valid (see `observability-service.ts`
|
|
53
69
|
* `readObservabilityEvents` for the schema-aware reader).
|
|
70
|
+
*
|
|
71
|
+
* Returns [] for an unresolvable session id too: this is the read half
|
|
72
|
+
* of a fire-and-forget store, and `readObservabilityEvents` documents
|
|
73
|
+
* that a session with no readable metrics file has no events. Throwing
|
|
74
|
+
* here used to escape as a raw guard error out of a documented
|
|
75
|
+
* never-throws reader (repair R6).
|
|
54
76
|
*/
|
|
55
77
|
export function readMetricLines(projectRoot, sessionId) {
|
|
56
|
-
const path =
|
|
78
|
+
const path = tryMetricsFilePath(projectRoot, sessionId);
|
|
79
|
+
if (path === null) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
57
82
|
if (!existsSync(path)) {
|
|
58
83
|
return [];
|
|
59
84
|
}
|
|
@@ -56,11 +56,15 @@ export type EmitOptions = {
|
|
|
56
56
|
/** Absolute path to the project root (where `.peaks/_runtime/` lives). */
|
|
57
57
|
projectRoot: string;
|
|
58
58
|
};
|
|
59
|
-
export type EmitFailureReason = 'invalid-schema' | 'write-failed';
|
|
59
|
+
export type EmitFailureReason = 'invalid-schema' | 'write-failed' | 'invalid-session-id';
|
|
60
60
|
export type EmitResult = {
|
|
61
61
|
/** True when the JSONL line was appended; false on any error path. */
|
|
62
62
|
written: boolean;
|
|
63
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Absolute path to the metrics file the event was written to (or would
|
|
65
|
+
* be). Empty string when the session id named no session directory —
|
|
66
|
+
* there is no path to report, and `reason` says so.
|
|
67
|
+
*/
|
|
64
68
|
path: string;
|
|
65
69
|
/** Set only when `written` is false. */
|
|
66
70
|
reason?: EmitFailureReason;
|
|
@@ -81,7 +85,10 @@ export declare function emitObservabilityEvent(event: ObservabilityEvent, option
|
|
|
81
85
|
* lines and any record whose `schemaVersion` does not match the
|
|
82
86
|
* current `OBSERVABILITY_SCHEMA_VERSION` (forward-compat per Q3).
|
|
83
87
|
*
|
|
84
|
-
* Returns [] when the session has no metrics file yet
|
|
88
|
+
* Returns [] when the session has no metrics file yet, and [] when the
|
|
89
|
+
* session id names no session directory — both are "no events are
|
|
90
|
+
* readable here", and this reader does not throw (repair R6: it used to,
|
|
91
|
+
* via `readMetricLines` → `metricsFilePath`).
|
|
85
92
|
*/
|
|
86
93
|
export declare function readObservabilityEvents(projectRoot: string, sessionId: string): ObservabilityEvent[];
|
|
87
94
|
/**
|