peaks-loop 4.0.10 → 4.0.12
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 +57 -0
- package/config/eslint/.peaks-rules.cjs +123 -0
- package/dist/cli/commands/code-commands.js +2 -0
- package/dist/cli/commands/code-orchestrator-can-do.d.ts +17 -0
- package/dist/cli/commands/code-orchestrator-can-do.js +75 -0
- package/dist/cli/commands/workspace/reconcile-command.js +5 -2
- package/dist/reporters/bdd-reporter.d.ts +36 -0
- package/dist/reporters/bdd-reporter.js +159 -0
- package/dist/services/audit/enforcers/active-skill-resolver.js +4 -1
- package/dist/services/code/orchestrator-can-do.d.ts +114 -0
- package/dist/services/code/orchestrator-can-do.js +205 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +1 -1
- package/dist/services/doctor/doctor-service/checks/workspace-layout.d.ts +5 -2
- package/dist/services/doctor/doctor-service/checks/workspace-layout.js +70 -9
- package/dist/services/doctor/doctor-service/types.d.ts +9 -0
- package/dist/services/hooks/presence-marker-detector.d.ts +7 -5
- package/dist/services/hooks/presence-marker-detector.js +45 -85
- package/dist/services/migration/v2-10-to-v2-11-service.js +1 -3
- package/dist/services/qa/bdd-test-style-verifier.d.ts +88 -0
- package/dist/services/qa/bdd-test-style-verifier.js +268 -0
- package/dist/services/sc/sc-service.d.ts +11 -7
- package/dist/services/sc/sc-service.js +29 -26
- package/dist/services/session/binding-store.d.ts +6 -6
- package/dist/services/skills/hooks-settings-service.d.ts +1 -1
- package/dist/services/skills/hooks-settings-service.js +1 -1
- package/dist/services/skills/presence-lease-service.js +1 -0
- package/dist/services/skills/skill-presence-service.d.ts +0 -1
- package/dist/services/skills/skill-presence-service.js +112 -102
- package/dist/services/skills/skill-statusline-service.js +73 -39
- package/docs/test-style-contract.md +135 -0
- package/package.json +6 -4
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +17 -1
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +21 -1
- package/skills/peaks-code/SKILL.md +6 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +60 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/services/qa/bdd-test-style-verifier.ts
|
|
3
|
+
*
|
|
4
|
+
* rid-2026-08-05-bdd-test-style Slice B — peaks-qa verification-time
|
|
5
|
+
* BDD test-style verifier. This is the read-only, post-edit companion
|
|
6
|
+
* to the `scripts/migrate-to-bdd.mjs` AST migrator shipped in Slice A.
|
|
7
|
+
*
|
|
8
|
+
* Purpose:
|
|
9
|
+
* When peaks-qa runs its verification gate, it picks up the git diff
|
|
10
|
+
* for the slice and asks this module whether the new / modified test
|
|
11
|
+
* files comply with the BDD given-when-then style. The verdict is
|
|
12
|
+
* surfaced as either `ok` (and the slice can advance) or one of two
|
|
13
|
+
* structured failure reasons (`missing-given-when-then` or
|
|
14
|
+
* `description-no-should-when`) that the caller turns into a
|
|
15
|
+
* `qa-handoff` rejection back to peaks-rd.
|
|
16
|
+
*
|
|
17
|
+
* Why a real AST and not a regex:
|
|
18
|
+
* The Slice A migrator established the convention: test files have
|
|
19
|
+
* multi-line `it(...)` calls, nested arrow bodies, and string
|
|
20
|
+
* literals that often contain words like "when" inside the assertion
|
|
21
|
+
* message (not in the description). A regex pass on the raw source
|
|
22
|
+
* would false-positive on string internals. The TypeScript Compiler
|
|
23
|
+
* API (already a dev dep via vitest) lets us:
|
|
24
|
+
* 1. Inspect the first `StringLiteral` argument of an `it` /
|
|
25
|
+
* `test` / `describe` call without scanning comments or
|
|
26
|
+
* string content inside the body.
|
|
27
|
+
* 2. Walk only the leading-comment ranges that sit before the
|
|
28
|
+
* first statement of the callback block, so a `// when:`
|
|
29
|
+
* inside an `expect(actual).toEqual('when X happens')` is
|
|
30
|
+
* correctly ignored.
|
|
31
|
+
*
|
|
32
|
+
* No new dependencies. The verifier is intentionally synchronous and
|
|
33
|
+
* pure (input source + path list -> verdict) so peaks-qa can call it
|
|
34
|
+
* from a deterministic verification step without subprocess overhead.
|
|
35
|
+
*
|
|
36
|
+
* Anti-fake-green (CLI silent-catch rule):
|
|
37
|
+
* This module throws on parse failure. It does NOT swallow parse
|
|
38
|
+
* errors and return `{ ok: true }` — that would silently green-light
|
|
39
|
+
* malformed test files. A parse error is a structural problem; the
|
|
40
|
+
* caller must surface it.
|
|
41
|
+
*/
|
|
42
|
+
/** Structured failure reasons the verifier can return. */
|
|
43
|
+
export type BddStyleFailureReason = 'missing-given-when-then' | 'description-no-should-when';
|
|
44
|
+
/** Successful verdict — includes the count of inspected `it`/`test` calls. */
|
|
45
|
+
export interface BddStyleOk {
|
|
46
|
+
readonly ok: true;
|
|
47
|
+
readonly scanned: number;
|
|
48
|
+
}
|
|
49
|
+
/** Structured failure verdict — the file/line makes the rejection actionable. */
|
|
50
|
+
export interface BddStyleFail {
|
|
51
|
+
readonly ok: false;
|
|
52
|
+
readonly reason: BddStyleFailureReason;
|
|
53
|
+
readonly file: string;
|
|
54
|
+
readonly line: number;
|
|
55
|
+
/** For `description-no-should-when`: the original description. */
|
|
56
|
+
readonly description?: string;
|
|
57
|
+
/** For `missing-given-when-then`: a stable string the caller can compare. */
|
|
58
|
+
readonly expected?: string;
|
|
59
|
+
}
|
|
60
|
+
export type BddStyleVerdict = BddStyleOk | BddStyleFail;
|
|
61
|
+
/** Public input surface — keep small so the contract is hard to misuse. */
|
|
62
|
+
export interface VerifyBddStyleInput {
|
|
63
|
+
readonly projectRoot: string;
|
|
64
|
+
readonly testFiles: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Verify that every `it(...)` / `test(...)` call in the given test
|
|
68
|
+
* files follows the BDD given-when-then contract.
|
|
69
|
+
*
|
|
70
|
+
* Contract:
|
|
71
|
+
* 1. The first `StringLiteral` argument of every `it` / `test` call
|
|
72
|
+
* MUST match `/(\bwhen\b|\bshould\b)/` (word-boundary anchored,
|
|
73
|
+
* case-insensitive). A regex on the raw description is correct
|
|
74
|
+
* here because the description itself is a literal — there is
|
|
75
|
+
* no nested template literal to misread.
|
|
76
|
+
* 2. The callback body (the second argument when it is an arrow /
|
|
77
|
+
* function expression with a block) MUST have a `// given:`,
|
|
78
|
+
* `// when:`, `// then:` triple at the top, in that order,
|
|
79
|
+
* within the first 3 leading-comment ranges before the first
|
|
80
|
+
* statement. The `// arrange:` / `// act:` / `// assert:` AAA
|
|
81
|
+
* legacy is NOT accepted — the contract is given-when-then
|
|
82
|
+
* only.
|
|
83
|
+
*
|
|
84
|
+
* Returns the FIRST failure encountered (file order, then
|
|
85
|
+
* top-to-bottom line order). A structured `BddStyleFail` is what the
|
|
86
|
+
* caller maps to `qa-handoff` rejection.
|
|
87
|
+
*/
|
|
88
|
+
export declare function verifyBddStyle(input: VerifyBddStyleInput): BddStyleVerdict;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/services/qa/bdd-test-style-verifier.ts
|
|
3
|
+
*
|
|
4
|
+
* rid-2026-08-05-bdd-test-style Slice B — peaks-qa verification-time
|
|
5
|
+
* BDD test-style verifier. This is the read-only, post-edit companion
|
|
6
|
+
* to the `scripts/migrate-to-bdd.mjs` AST migrator shipped in Slice A.
|
|
7
|
+
*
|
|
8
|
+
* Purpose:
|
|
9
|
+
* When peaks-qa runs its verification gate, it picks up the git diff
|
|
10
|
+
* for the slice and asks this module whether the new / modified test
|
|
11
|
+
* files comply with the BDD given-when-then style. The verdict is
|
|
12
|
+
* surfaced as either `ok` (and the slice can advance) or one of two
|
|
13
|
+
* structured failure reasons (`missing-given-when-then` or
|
|
14
|
+
* `description-no-should-when`) that the caller turns into a
|
|
15
|
+
* `qa-handoff` rejection back to peaks-rd.
|
|
16
|
+
*
|
|
17
|
+
* Why a real AST and not a regex:
|
|
18
|
+
* The Slice A migrator established the convention: test files have
|
|
19
|
+
* multi-line `it(...)` calls, nested arrow bodies, and string
|
|
20
|
+
* literals that often contain words like "when" inside the assertion
|
|
21
|
+
* message (not in the description). A regex pass on the raw source
|
|
22
|
+
* would false-positive on string internals. The TypeScript Compiler
|
|
23
|
+
* API (already a dev dep via vitest) lets us:
|
|
24
|
+
* 1. Inspect the first `StringLiteral` argument of an `it` /
|
|
25
|
+
* `test` / `describe` call without scanning comments or
|
|
26
|
+
* string content inside the body.
|
|
27
|
+
* 2. Walk only the leading-comment ranges that sit before the
|
|
28
|
+
* first statement of the callback block, so a `// when:`
|
|
29
|
+
* inside an `expect(actual).toEqual('when X happens')` is
|
|
30
|
+
* correctly ignored.
|
|
31
|
+
*
|
|
32
|
+
* No new dependencies. The verifier is intentionally synchronous and
|
|
33
|
+
* pure (input source + path list -> verdict) so peaks-qa can call it
|
|
34
|
+
* from a deterministic verification step without subprocess overhead.
|
|
35
|
+
*
|
|
36
|
+
* Anti-fake-green (CLI silent-catch rule):
|
|
37
|
+
* This module throws on parse failure. It does NOT swallow parse
|
|
38
|
+
* errors and return `{ ok: true }` — that would silently green-light
|
|
39
|
+
* malformed test files. A parse error is a structural problem; the
|
|
40
|
+
* caller must surface it.
|
|
41
|
+
*/
|
|
42
|
+
import { readFileSync } from 'node:fs';
|
|
43
|
+
import { resolve } from 'node:path';
|
|
44
|
+
import ts from 'typescript';
|
|
45
|
+
/** Test runners whose first string-arg is the test description. */
|
|
46
|
+
const TEST_NAMES = new Set(['it', 'test']);
|
|
47
|
+
/**
|
|
48
|
+
* Verify that every `it(...)` / `test(...)` call in the given test
|
|
49
|
+
* files follows the BDD given-when-then contract.
|
|
50
|
+
*
|
|
51
|
+
* Contract:
|
|
52
|
+
* 1. The first `StringLiteral` argument of every `it` / `test` call
|
|
53
|
+
* MUST match `/(\bwhen\b|\bshould\b)/` (word-boundary anchored,
|
|
54
|
+
* case-insensitive). A regex on the raw description is correct
|
|
55
|
+
* here because the description itself is a literal — there is
|
|
56
|
+
* no nested template literal to misread.
|
|
57
|
+
* 2. The callback body (the second argument when it is an arrow /
|
|
58
|
+
* function expression with a block) MUST have a `// given:`,
|
|
59
|
+
* `// when:`, `// then:` triple at the top, in that order,
|
|
60
|
+
* within the first 3 leading-comment ranges before the first
|
|
61
|
+
* statement. The `// arrange:` / `// act:` / `// assert:` AAA
|
|
62
|
+
* legacy is NOT accepted — the contract is given-when-then
|
|
63
|
+
* only.
|
|
64
|
+
*
|
|
65
|
+
* Returns the FIRST failure encountered (file order, then
|
|
66
|
+
* top-to-bottom line order). A structured `BddStyleFail` is what the
|
|
67
|
+
* caller maps to `qa-handoff` rejection.
|
|
68
|
+
*/
|
|
69
|
+
export function verifyBddStyle(input) {
|
|
70
|
+
let scanned = 0;
|
|
71
|
+
for (const rel of input.testFiles) {
|
|
72
|
+
const absPath = resolve(input.projectRoot, rel);
|
|
73
|
+
const source = readFileSync(absPath, 'utf8');
|
|
74
|
+
const sourceFile = ts.createSourceFile(rel, source, ts.ScriptTarget.ESNext,
|
|
75
|
+
/* setParentNodes */ true, ts.ScriptKind.TS);
|
|
76
|
+
let earliestFail = null;
|
|
77
|
+
const recordFail = (fail) => {
|
|
78
|
+
if (earliestFail === null) {
|
|
79
|
+
earliestFail = fail;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const { line: existingLine } = earliestFail;
|
|
83
|
+
if (fail.line < existingLine)
|
|
84
|
+
earliestFail = fail;
|
|
85
|
+
};
|
|
86
|
+
const visit = (node) => {
|
|
87
|
+
if (earliestFail !== null)
|
|
88
|
+
return;
|
|
89
|
+
if (ts.isCallExpression(node)) {
|
|
90
|
+
const callee = node.expression;
|
|
91
|
+
if (ts.isIdentifier(callee) && TEST_NAMES.has(callee.text)) {
|
|
92
|
+
scanned += 1;
|
|
93
|
+
const descCheck = checkDescription(node, sourceFile, rel);
|
|
94
|
+
if (descCheck !== null) {
|
|
95
|
+
recordFail(descCheck);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const bodyCheck = checkBody(node, sourceFile, rel);
|
|
99
|
+
if (bodyCheck !== null) {
|
|
100
|
+
recordFail(bodyCheck);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
ts.forEachChild(node, visit);
|
|
106
|
+
};
|
|
107
|
+
visit(sourceFile);
|
|
108
|
+
if (earliestFail !== null)
|
|
109
|
+
return earliestFail;
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, scanned };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Inspect the first string-literal argument of an `it` / `test` call.
|
|
115
|
+
*
|
|
116
|
+
* - If the first argument is not a string literal, treat it as a
|
|
117
|
+
* failure (the BDD contract requires a literal description).
|
|
118
|
+
* - If the literal text does not contain "when" or "should" as a
|
|
119
|
+
* whole word, return a `description-no-should-when` failure.
|
|
120
|
+
*/
|
|
121
|
+
function checkDescription(call, sourceFile, relPath) {
|
|
122
|
+
const firstArg = call.arguments[0];
|
|
123
|
+
if (firstArg === undefined || !ts.isStringLiteralLike(firstArg)) {
|
|
124
|
+
const pos = call.getStart(sourceFile);
|
|
125
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: 'description-no-should-when',
|
|
129
|
+
file: relPath,
|
|
130
|
+
line: line + 1,
|
|
131
|
+
description: '<non-literal first argument>',
|
|
132
|
+
expected: 'first argument must be a string literal containing "when" or "should"',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const description = firstArg.text;
|
|
136
|
+
if (!hasWhenOrShould(description)) {
|
|
137
|
+
const pos = firstArg.getStart(sourceFile);
|
|
138
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
reason: 'description-no-should-when',
|
|
142
|
+
file: relPath,
|
|
143
|
+
line: line + 1,
|
|
144
|
+
description,
|
|
145
|
+
expected: 'description must contain the word "when" or "should" (BDD style)',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Inspect the callback body of an `it` / `test` call for the
|
|
152
|
+
* `// given:` / `// when:` / `// then:` triple.
|
|
153
|
+
*
|
|
154
|
+
* Rules (Slice A migrator + design §4.B):
|
|
155
|
+
* - The second argument must be an arrow / function expression
|
|
156
|
+
* with a block body. If it is missing or not a block (e.g. an
|
|
157
|
+
* expression-body arrow `it('x', () => expect(y).toBe(z))`),
|
|
158
|
+
* we still need the comments — but expression-body arrows
|
|
159
|
+
* cannot host them. In that case we fall back to inspecting
|
|
160
|
+
* the leading comments before the entire call expression,
|
|
161
|
+
* which matches the Slice A migrator's `isAlreadyMigrated`
|
|
162
|
+
* check shape.
|
|
163
|
+
* - The three comments must be the FIRST THREE leading-comment
|
|
164
|
+
* ranges before the relevant body / first-statement anchor.
|
|
165
|
+
* - The order must be `given` → `when` → `then`. A re-ordered
|
|
166
|
+
* triple is rejected.
|
|
167
|
+
*/
|
|
168
|
+
function checkBody(call, sourceFile, relPath) {
|
|
169
|
+
const body = getCallbackBlock(call);
|
|
170
|
+
if (body !== null) {
|
|
171
|
+
return checkBlockLeadingComments(body, sourceFile, relPath);
|
|
172
|
+
}
|
|
173
|
+
// Expression-body arrow or non-block callback: comments cannot
|
|
174
|
+
// live inside the body. The Slice A migrator only inserts the
|
|
175
|
+
// triple on block bodies, so an expression-body form is by
|
|
176
|
+
// definition non-BDD and must fail. This keeps the contract
|
|
177
|
+
// symmetric with the migrator.
|
|
178
|
+
const pos = call.getStart(sourceFile);
|
|
179
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: 'missing-given-when-then',
|
|
183
|
+
file: relPath,
|
|
184
|
+
line: line + 1,
|
|
185
|
+
expected: 'block-body callback with // given: / // when: / // then: comments at the top',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function getCallbackBlock(call) {
|
|
189
|
+
const callback = call.arguments[1];
|
|
190
|
+
if (callback === undefined)
|
|
191
|
+
return null;
|
|
192
|
+
if (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback))
|
|
193
|
+
return null;
|
|
194
|
+
if (!callback.body || !ts.isBlock(callback.body))
|
|
195
|
+
return null;
|
|
196
|
+
return callback.body;
|
|
197
|
+
}
|
|
198
|
+
function checkBlockLeadingComments(block, sourceFile, relPath) {
|
|
199
|
+
// TypeScript's `getLeadingCommentRanges` API is unreliable for
|
|
200
|
+
// comment-only blocks: with `setParentNodes: true`, an empty
|
|
201
|
+
// block (no statements, only comments) has no anchor to attach
|
|
202
|
+
// the comments to, so the API returns zero ranges. To get a
|
|
203
|
+
// deterministic answer, we scan the block's text directly and
|
|
204
|
+
// pick the first three non-empty lines.
|
|
205
|
+
//
|
|
206
|
+
// The block's text spans `{` ... `}`. We extract the body,
|
|
207
|
+
// split on lines, and check the first three non-empty lines for
|
|
208
|
+
// the BDD triple. This is AST-driven (we use the block's source
|
|
209
|
+
// range from the SourceFile, not a global regex) and survives
|
|
210
|
+
// both empty-body and populated-body cases.
|
|
211
|
+
const blockStart = block.getStart(sourceFile) + 1; // skip `{`
|
|
212
|
+
const blockEnd = block.end - 1; // skip `}`
|
|
213
|
+
const body = sourceFile.text.slice(blockStart, blockEnd);
|
|
214
|
+
const lines = body.split(/\r?\n/);
|
|
215
|
+
const nonEmpty = [];
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
if (line.trim().length === 0)
|
|
218
|
+
continue;
|
|
219
|
+
nonEmpty.push(line);
|
|
220
|
+
if (nonEmpty.length === 3)
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
if (nonEmpty.length < 3 || !matchesBddTriple(nonEmpty)) {
|
|
224
|
+
return makeMissingCommentFailure(block, sourceFile, relPath);
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
function makeMissingCommentFailure(block, sourceFile, relPath) {
|
|
229
|
+
// Report the line of the opening `{` + 1 — the line that should
|
|
230
|
+
// contain the first comment of the BDD triple. This gives the
|
|
231
|
+
// caller a stable pointer even when the block is empty.
|
|
232
|
+
const pos = block.getStart(sourceFile) + 1;
|
|
233
|
+
const { line } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
reason: 'missing-given-when-then',
|
|
237
|
+
file: relPath,
|
|
238
|
+
line: line + 1,
|
|
239
|
+
expected: '// given: / // when: / // then: triple at the top of the block body',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Match the three leading comments against the BDD triple. Each
|
|
244
|
+
* entry must be a `// <keyword>:` line (with optional trailing
|
|
245
|
+
* whitespace); the keywords must appear in `given`, `when`, `then`
|
|
246
|
+
* order, case-insensitive.
|
|
247
|
+
*/
|
|
248
|
+
function matchesBddTriple(triple) {
|
|
249
|
+
if (triple.length !== 3)
|
|
250
|
+
return false;
|
|
251
|
+
// Each entry must be a `// <keyword>:` line, optionally followed
|
|
252
|
+
// by descriptive text. The Slice A migrator's `buildCommentBlock`
|
|
253
|
+
// produces `// given: the test setup` / `// when: the function
|
|
254
|
+
// under test is invoked` / `// then: the result matches the
|
|
255
|
+
// expectation` — the `when` line uses two spaces after the colon
|
|
256
|
+
// for visual alignment with `given:` and `then:`, so the regex
|
|
257
|
+
// is intentionally permissive about trailing text.
|
|
258
|
+
const patterns = [
|
|
259
|
+
/^\s*\/\/\s*given\s*:/i,
|
|
260
|
+
/^\s*\/\/\s*when\s*:/i,
|
|
261
|
+
/^\s*\/\/\s*then\s*:/i,
|
|
262
|
+
];
|
|
263
|
+
return patterns.every((pat, i) => pat.test(triple[i] ?? ''));
|
|
264
|
+
}
|
|
265
|
+
/** True when `text` contains `when` or `should` as a whole word. */
|
|
266
|
+
function hasWhenOrShould(text) {
|
|
267
|
+
return /(\bwhen\b|\bshould\b)/i.test(text);
|
|
268
|
+
}
|
|
@@ -53,10 +53,10 @@ export type CommitBoundary = {
|
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
55
|
* Resolution sources for `resolveArtifactSession`, in priority order.
|
|
56
|
-
* - `active-skill`: the
|
|
57
|
-
* (`.peaks/_runtime/
|
|
58
|
-
*
|
|
59
|
-
* session dir that owns the slice's marker artifact.
|
|
56
|
+
* - `active-skill`: the canonical sid-scoped lease projection
|
|
57
|
+
* (`.peaks/_runtime/<sid>/leases/presence-*.json`) for the bound
|
|
58
|
+
* session id. When at least one in-flight lease exists under the
|
|
59
|
+
* session dir, that session owns the slice's marker artifact.
|
|
60
60
|
* - `session-json`: the workspace binding in
|
|
61
61
|
* `.peaks/_runtime/session.json` (with back-compat fallback to
|
|
62
62
|
* `.peaks/.session.json`) points to a session dir that owns the
|
|
@@ -74,9 +74,13 @@ export type ResolvedArtifactSession = {
|
|
|
74
74
|
* Resolve the session id that owns the slice's artifacts using a 3-tier
|
|
75
75
|
* precedence:
|
|
76
76
|
*
|
|
77
|
-
* 1.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
77
|
+
* 1. The canonical sid-scoped lease projection under the bound
|
|
78
|
+
* session id (workspace binding + at least one in-flight lease
|
|
79
|
+
* in `.peaks/_runtime/<sid>/leases/`). Slice 4.0.11 statusline-
|
|
80
|
+
* sid-scoped-lease C collapsed the previous
|
|
81
|
+
* `.peaks/_runtime/active-skill.json` reader into this tier —
|
|
82
|
+
* the session id is now read from the canonical workspace
|
|
83
|
+
* binding (was tier 2), gated on a live lease.
|
|
80
84
|
* 2. `.peaks/_runtime/session.json` `sessionId` (with back-compat
|
|
81
85
|
* fallback to `.peaks/.session.json`) if it points to a real
|
|
82
86
|
* session that owns the slice.
|
|
@@ -5,6 +5,7 @@ import { isInsidePath } from '../../shared/path-utils.js';
|
|
|
5
5
|
import { getWorkspaceConfigForPath } from '../config/config-service.js';
|
|
6
6
|
import { getArtifactRemoteRepo, getArtifactWorkspaceStatus, getLocalArtifactPath } from '../artifacts/workspace-service.js';
|
|
7
7
|
import { getSessionId } from '../session/session-manager.js';
|
|
8
|
+
import { listPresenceLeases } from '../skills/presence-lease-service.js';
|
|
8
9
|
const REQUIRED_ARTIFACTS = [
|
|
9
10
|
{ name: 'retention-boundary.md', path: ['sc', 'retention-boundary.md'] },
|
|
10
11
|
{ name: 'change-impact.json', path: ['sc', 'change-impact.json'] },
|
|
@@ -120,33 +121,31 @@ function isRetainedArtifactFile(filePath, artifactWorkspacePath, changesRoot, ch
|
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
/**
|
|
123
|
-
*
|
|
124
|
-
* `
|
|
124
|
+
* Resolve the canonical sid-scoped lease session id (replaces the
|
|
125
|
+
* removed `.peaks/_runtime/active-skill.json` single-slot marker
|
|
126
|
+
* reader). Reads the workspace session binding
|
|
127
|
+
* (`.peaks/_runtime/session.json`, with back-compat fallback to
|
|
128
|
+
* `.peaks/.session.json`) and returns its `sessionId` when at least
|
|
129
|
+
* one in-flight canonical lease exists under
|
|
130
|
+
* `.peaks/_runtime/<sid>/leases/`.
|
|
125
131
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
+
* Slice 4.0.11 statusline-sid-scoped-lease C: the project-level
|
|
133
|
+
* `active-skill.json` is removed; the canonical sid-scoped lease
|
|
134
|
+
* projection (`.peaks/_runtime/<sid>/leases/presence-*.json`) is the
|
|
135
|
+
* authoritative source. The session-id is now read from the
|
|
136
|
+
* workspace binding (which was already tier 2 in the old resolver),
|
|
137
|
+
* gated on the existence of an in-flight lease so a stale binding
|
|
138
|
+
* with no live presence does not produce a false `active-skill`
|
|
139
|
+
* tier hit. Returns null when the session binding is missing /
|
|
140
|
+
* malformed, or when no in-flight lease exists.
|
|
132
141
|
*/
|
|
133
142
|
function readActiveSkillSessionId(projectRoot) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const pathToRead = existsSync(newPath) ? newPath : legacyPath;
|
|
137
|
-
if (!existsSync(pathToRead))
|
|
138
|
-
return null;
|
|
139
|
-
try {
|
|
140
|
-
const raw = readFileSync(pathToRead, 'utf8');
|
|
141
|
-
const parsed = JSON.parse(raw);
|
|
142
|
-
if (typeof parsed?.sessionId === 'string' && parsed.sessionId.length > 0) {
|
|
143
|
-
return parsed.sessionId;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
143
|
+
const sessionId = readSessionJsonBinding(projectRoot);
|
|
144
|
+
if (sessionId === null)
|
|
147
145
|
return null;
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const leases = listPresenceLeases(projectRoot, sessionId);
|
|
147
|
+
const hasInFlight = leases.some((l) => l.status === 'running' || l.status === 'preparing');
|
|
148
|
+
return hasInFlight ? sessionId : null;
|
|
150
149
|
}
|
|
151
150
|
/**
|
|
152
151
|
* Read the workspace session binding and return its `sessionId`,
|
|
@@ -284,9 +283,13 @@ function readdirSyncSafe(dir) {
|
|
|
284
283
|
* Resolve the session id that owns the slice's artifacts using a 3-tier
|
|
285
284
|
* precedence:
|
|
286
285
|
*
|
|
287
|
-
* 1.
|
|
288
|
-
*
|
|
289
|
-
*
|
|
286
|
+
* 1. The canonical sid-scoped lease projection under the bound
|
|
287
|
+
* session id (workspace binding + at least one in-flight lease
|
|
288
|
+
* in `.peaks/_runtime/<sid>/leases/`). Slice 4.0.11 statusline-
|
|
289
|
+
* sid-scoped-lease C collapsed the previous
|
|
290
|
+
* `.peaks/_runtime/active-skill.json` reader into this tier —
|
|
291
|
+
* the session id is now read from the canonical workspace
|
|
292
|
+
* binding (was tier 2), gated on a live lease.
|
|
290
293
|
* 2. `.peaks/_runtime/session.json` `sessionId` (with back-compat
|
|
291
294
|
* fallback to `.peaks/.session.json`) if it points to a real
|
|
292
295
|
* session that owns the slice.
|
|
@@ -24,14 +24,14 @@ export declare const InstanceRecordSchema: z.ZodObject<{
|
|
|
24
24
|
callerId: z.ZodString;
|
|
25
25
|
lastHeartbeat: z.ZodString;
|
|
26
26
|
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
lastHeartbeat: string;
|
|
27
28
|
startedAt: string;
|
|
28
29
|
roles: string[];
|
|
29
30
|
callerId: string;
|
|
30
|
-
lastHeartbeat: string;
|
|
31
31
|
}, {
|
|
32
|
+
lastHeartbeat: string;
|
|
32
33
|
startedAt: string;
|
|
33
34
|
callerId: string;
|
|
34
|
-
lastHeartbeat: string;
|
|
35
35
|
roles?: string[] | undefined;
|
|
36
36
|
}>;
|
|
37
37
|
export declare const BindingSchema: z.ZodObject<{
|
|
@@ -45,14 +45,14 @@ export declare const BindingSchema: z.ZodObject<{
|
|
|
45
45
|
callerId: z.ZodString;
|
|
46
46
|
lastHeartbeat: z.ZodString;
|
|
47
47
|
}, "strip", z.ZodTypeAny, {
|
|
48
|
+
lastHeartbeat: string;
|
|
48
49
|
startedAt: string;
|
|
49
50
|
roles: string[];
|
|
50
51
|
callerId: string;
|
|
51
|
-
lastHeartbeat: string;
|
|
52
52
|
}, {
|
|
53
|
+
lastHeartbeat: string;
|
|
53
54
|
startedAt: string;
|
|
54
55
|
callerId: string;
|
|
55
|
-
lastHeartbeat: string;
|
|
56
56
|
roles?: string[] | undefined;
|
|
57
57
|
}>>;
|
|
58
58
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -61,10 +61,10 @@ export declare const BindingSchema: z.ZodObject<{
|
|
|
61
61
|
pid: number;
|
|
62
62
|
scope: string;
|
|
63
63
|
instances: Record<string, {
|
|
64
|
+
lastHeartbeat: string;
|
|
64
65
|
startedAt: string;
|
|
65
66
|
roles: string[];
|
|
66
67
|
callerId: string;
|
|
67
|
-
lastHeartbeat: string;
|
|
68
68
|
}>;
|
|
69
69
|
}, {
|
|
70
70
|
lastHeartbeat: string;
|
|
@@ -72,9 +72,9 @@ export declare const BindingSchema: z.ZodObject<{
|
|
|
72
72
|
pid: number;
|
|
73
73
|
scope: string;
|
|
74
74
|
instances: Record<string, {
|
|
75
|
+
lastHeartbeat: string;
|
|
75
76
|
startedAt: string;
|
|
76
77
|
callerId: string;
|
|
77
|
-
lastHeartbeat: string;
|
|
78
78
|
roles?: string[] | undefined;
|
|
79
79
|
}>;
|
|
80
80
|
}>;
|
|
@@ -67,7 +67,7 @@ export type HookInstallOptions = {
|
|
|
67
67
|
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
68
68
|
export declare const HOOK_ENFORCE_SENTINEL = "peaks gate enforce";
|
|
69
69
|
/** Default (claude-code) hook command — kept as a stable export for tests. */
|
|
70
|
-
export declare const HOOK_ENFORCE_COMMAND = "peaks gate enforce --project \"${CLAUDE_PROJECT_DIR}\"";
|
|
70
|
+
export declare const HOOK_ENFORCE_COMMAND = "peaks gate enforce --project \"${CLAUDE_PROJECT_DIR}\" --json";
|
|
71
71
|
export type HookInstallPlan = {
|
|
72
72
|
scope: HookScope;
|
|
73
73
|
settingsPath: string;
|
|
@@ -11,7 +11,7 @@ import { getAdapter } from '../ide/ide-registry.js';
|
|
|
11
11
|
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
12
12
|
export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
|
|
13
13
|
/** Default (claude-code) hook command — kept as a stable export for tests. */
|
|
14
|
-
export const HOOK_ENFORCE_COMMAND = `peaks gate enforce --project "\${CLAUDE_PROJECT_DIR}"`;
|
|
14
|
+
export const HOOK_ENFORCE_COMMAND = `peaks gate enforce --project "\${CLAUDE_PROJECT_DIR}" --json`;
|
|
15
15
|
/**
|
|
16
16
|
* Per-IDE hook command + sentinel. The default (Claude Code) uses the
|
|
17
17
|
* legacy `peaks gate enforce` surface; Trae / Cursor / Codex (Cursor-style
|
|
@@ -142,6 +142,7 @@ export function setPresenceLease(input) {
|
|
|
142
142
|
graphRef,
|
|
143
143
|
skill: input.skill,
|
|
144
144
|
...(input.parentWorkflowId ? { parentWorkflowId: input.parentWorkflowId } : {}),
|
|
145
|
+
...(input.mode ? { mode: input.mode } : {}),
|
|
145
146
|
depth: input.depth ?? 0,
|
|
146
147
|
startedAt: now,
|
|
147
148
|
lastHeartbeat: now,
|
|
@@ -54,7 +54,6 @@ export type SkillPresence = {
|
|
|
54
54
|
* fallback (slice 2026-06-26-unknown-sid-fallback-fix).
|
|
55
55
|
*/
|
|
56
56
|
export declare function getCurrentSessionId(projectRootOverride?: string): string | null;
|
|
57
|
-
export declare function exportSkillPresence(projectRootOverride?: string): string;
|
|
58
57
|
/**
|
|
59
58
|
* Write the per-caller active-skill marker to
|
|
60
59
|
* `.peaks/_runtime/<peakSid>/active-skill-<callerId>.json` (D6). Returns
|