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.
Files changed (35) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/config/eslint/.peaks-rules.cjs +123 -0
  3. package/dist/cli/commands/code-commands.js +2 -0
  4. package/dist/cli/commands/code-orchestrator-can-do.d.ts +17 -0
  5. package/dist/cli/commands/code-orchestrator-can-do.js +75 -0
  6. package/dist/cli/commands/workspace/reconcile-command.js +5 -2
  7. package/dist/reporters/bdd-reporter.d.ts +36 -0
  8. package/dist/reporters/bdd-reporter.js +159 -0
  9. package/dist/services/audit/enforcers/active-skill-resolver.js +4 -1
  10. package/dist/services/code/orchestrator-can-do.d.ts +114 -0
  11. package/dist/services/code/orchestrator-can-do.js +205 -0
  12. package/dist/services/doctor/doctor-service/checks/skill-presence.js +1 -1
  13. package/dist/services/doctor/doctor-service/checks/workspace-layout.d.ts +5 -2
  14. package/dist/services/doctor/doctor-service/checks/workspace-layout.js +70 -9
  15. package/dist/services/doctor/doctor-service/types.d.ts +9 -0
  16. package/dist/services/hooks/presence-marker-detector.d.ts +7 -5
  17. package/dist/services/hooks/presence-marker-detector.js +45 -85
  18. package/dist/services/migration/v2-10-to-v2-11-service.js +1 -3
  19. package/dist/services/qa/bdd-test-style-verifier.d.ts +88 -0
  20. package/dist/services/qa/bdd-test-style-verifier.js +268 -0
  21. package/dist/services/sc/sc-service.d.ts +11 -7
  22. package/dist/services/sc/sc-service.js +29 -26
  23. package/dist/services/session/binding-store.d.ts +6 -6
  24. package/dist/services/skills/hooks-settings-service.d.ts +1 -1
  25. package/dist/services/skills/hooks-settings-service.js +1 -1
  26. package/dist/services/skills/presence-lease-service.js +1 -0
  27. package/dist/services/skills/skill-presence-service.d.ts +0 -1
  28. package/dist/services/skills/skill-presence-service.js +112 -102
  29. package/dist/services/skills/skill-statusline-service.js +73 -39
  30. package/docs/test-style-contract.md +135 -0
  31. package/package.json +6 -4
  32. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +17 -1
  33. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +21 -1
  34. package/skills/peaks-code/SKILL.md +6 -0
  35. 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 orchestrator's active-skill marker
57
- * (`.peaks/_runtime/active-skill.json`, with a one-minor-release
58
- * fallback to `.peaks/.active-skill.json`) `sessionId` points to a
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. `.peaks/_runtime/active-skill.json` `sessionId` (with back-compat
78
- * fallback to `.peaks/.active-skill.json`) if it points to a real
79
- * session that owns the slice.
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
- * Read the orchestrator's active-skill marker and return its
124
- * `sessionId`, or null when the file is missing / malformed.
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
- * As of slice 2026-06-05-peaks-runtime-layer the canonical home is
127
- * `<projectRoot>/.peaks/_runtime/active-skill.json`. The legacy
128
- * `<projectRoot>/.peaks/.active-skill.json` is consulted as a
129
- * one-minor-release back-compat fallback: if the new path is
130
- * absent but the legacy path is present and valid, we use the
131
- * legacy value. The new path always wins when both exist.
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 newPath = join(projectRoot, '.peaks', '_runtime', 'active-skill.json');
135
- const legacyPath = join(projectRoot, '.peaks', '.active-skill.json');
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
- return null;
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. `.peaks/_runtime/active-skill.json` `sessionId` (with back-compat
288
- * fallback to `.peaks/.active-skill.json`) if it points to a real
289
- * session that owns the slice.
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