peaks-loop 4.0.49 → 4.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/audit-commands.js +1 -0
- package/dist/cli/commands/baseline-commands.js +163 -25
- package/dist/cli/commands/core/skill-command.js +53 -4
- package/dist/cli/commands/core/standards-command.d.ts +24 -0
- package/dist/cli/commands/core/standards-command.js +74 -0
- package/dist/cli/commands/hooks-commands.js +55 -38
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/web-commands.js +8 -1
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +6 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +64 -3
- package/dist/services/adapter/adapter.d.ts +30 -0
- package/dist/services/adapter/auto-adapter.d.ts +13 -0
- package/dist/services/adapter/claude-adapter.js +12 -0
- package/dist/services/adapter/codex-adapter.d.ts +12 -0
- package/dist/services/adapter/codex-adapter.js +12 -0
- package/dist/services/adapter/copilot-adapter.d.ts +12 -0
- package/dist/services/adapter/copilot-adapter.js +12 -0
- package/dist/services/audit/backing-detector.d.ts +25 -7
- package/dist/services/audit/backing-detector.js +33 -17
- package/dist/services/audit/enforcer-liveness.d.ts +12 -0
- package/dist/services/audit/enforcer-liveness.js +100 -0
- package/dist/services/audit/enforcers/lint-catalog-governance.d.ts +23 -11
- package/dist/services/audit/enforcers/lint-catalog-governance.js +10 -14
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.d.ts +5 -15
- package/dist/services/audit/enforcers/lint-rd-handoff-coverage.js +94 -25
- package/dist/services/audit/enforcers/lint-style.d.ts +9 -1
- package/dist/services/audit/enforcers/lint-style.js +38 -2
- package/dist/services/audit/prose-ratio-calculator.d.ts +28 -17
- package/dist/services/audit/prose-ratio-calculator.js +25 -18
- package/dist/services/audit/red-line-catalog-p2-a.js +1 -1
- package/dist/services/audit/red-lines-service.js +51 -7
- package/dist/services/capability-audit-service/independent-checker.d.ts +15 -0
- package/dist/services/capability-audit-service/independent-checker.js +140 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -1
- package/dist/services/capability-audit-service/index.js +1 -0
- package/dist/services/capability-audit-service/runner.d.ts +17 -13
- package/dist/services/capability-audit-service/runner.js +76 -15
- package/dist/services/capability-audit-service/types.d.ts +48 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +21 -22
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +1 -1
- package/dist/services/capability-guard-runner/contracts/J02.js +114 -28
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +72 -21
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +6 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +65 -32
- package/dist/services/capability-guard-runner/contracts/J05.js +118 -16
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +14 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +57 -39
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +9 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +76 -47
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +66 -39
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +13 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +95 -39
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +69 -35
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +8 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +73 -33
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +12 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +66 -30
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +62 -40
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +60 -31
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +11 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +70 -35
- package/dist/services/capability-guard-runner/contracts/_shared.d.ts +24 -0
- package/dist/services/capability-guard-runner/contracts/_shared.js +67 -0
- package/dist/services/capability-guard-runner/registry.d.ts +5 -0
- package/dist/services/capability-guard-runner/registry.js +140 -0
- package/dist/services/capability-guard-runner/runner.d.ts +26 -0
- package/dist/services/capability-guard-runner/runner.js +63 -6
- package/dist/services/code/auto-compact-modes.d.ts +13 -2
- package/dist/services/code/auto-compact-modes.js +20 -4
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/observability/observability-service.d.ts +1 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/standards/loop-engineering-lint.d.ts +1 -1
- package/dist/services/standards/loop-engineering-lint.js +6 -0
- package/dist/services/web/daemon-registry.js +27 -2
- package/dist/services/workspace/claude-settings-template.d.ts +53 -37
- package/dist/services/workspace/claude-settings-template.js +105 -83
- package/dist/services/workspace/generated-artifacts-stamp.d.ts +119 -0
- package/dist/services/workspace/generated-artifacts-stamp.js +167 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.d.ts +8 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +38 -3
- package/dist/services/workspace/workspace-service.js +11 -1
- package/dist/shared/fs-utils.d.ts +26 -0
- package/dist/shared/fs-utils.js +35 -0
- package/package.json +9 -7
- package/scripts/copy-templates.mjs +0 -12
- package/scripts/install-skills.mjs +154 -53
- package/skills/bee/peaks-qa/SKILL.md +0 -1
- package/skills/bee/peaks-rd/SKILL.md +0 -1
- package/skills/peaks-code/SKILL.md +12 -10
- package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
- package/skills/peaks-code/references/runbook.md +3 -0
- package/skills/peaks-code/references/session-overload-signal-index.md +4 -2
- package/skills/peaks-code/references/startup-sequence.md +2 -2
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/sub-agent-dispatch.md +19 -19
- package/dist/cli/commands/context-builder-commands.d.ts +0 -11
- package/dist/cli/commands/context-builder-commands.js +0 -85
- package/dist/services/hooks/write-gate.js +0 -111
- package/skills/bee/peaks-prd/references/command-migration.md +0 -3
- package/skills/bee/peaks-qa/references/command-migration.md +0 -3
- package/skills/bee/peaks-rd/references/command-migration.md +0 -3
- package/skills/bee/peaks-sc/references/command-migration.md +0 -3
- package/skills/bee/peaks-txt/references/command-migration.md +0 -3
- package/skills/bee/peaks-ui/references/command-migration.md +0 -3
- package/skills/peaks-code/references/command-migration.md +0 -3
|
@@ -5,9 +5,10 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
5
5
|
import { addJsonOption, printResult, getErrorMessage } from '../cli-helpers.js';
|
|
6
6
|
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
7
7
|
import { applyHookInstall, planHookInstall, readHookStatus, readInstalledEntriesFromSettings, removeHookInstall, listSuperpowersDenyEntries } from '../../services/skills/hooks-settings-service.js';
|
|
8
|
+
import { resolveHookEntries, resolveHookSpec } from '../../services/skills/hooks-codegate-superpowers.js';
|
|
8
9
|
import { readJsonObjectFile } from '../../services/ide/shared/atomic-json.js';
|
|
9
10
|
import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
|
|
10
|
-
import {
|
|
11
|
+
import { resolveIdeOptionHelp } from '../../services/ide/ide-registry.js';
|
|
11
12
|
/**
|
|
12
13
|
* This module's own directory — `<root>/src/cli/commands` in the source tree,
|
|
13
14
|
* `<root>/dist/cli/commands` in a build. Same reason as
|
|
@@ -50,20 +51,23 @@ function resolveIdeForCommand(options, projectRoot) {
|
|
|
50
51
|
* service did not write.
|
|
51
52
|
*/
|
|
52
53
|
function listExpectedEntriesForIde(ide, _skipProgress = false) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
// Derived from `resolveHookEntries(ide)` — the same function `planHookInstall`
|
|
55
|
+
// / `applyHookInstall` write from — so the summary can only ever report a
|
|
56
|
+
// shape the install actually produces. The previous version was a
|
|
57
|
+
// hand-written literal under a comment claiming it "mirrors the install
|
|
58
|
+
// shape, NOT a hardcoded expected list" (diagnosis 2026-09-15, C4); the
|
|
59
|
+
// literal had drifted from that claim in both directions: it re-typed the
|
|
60
|
+
// code-gate matcher instead of reading `HOOK_CODE_GATE_MATCHER`, and it
|
|
61
|
+
// reported 2 of the 6 entries a claude-code install writes.
|
|
62
|
+
//
|
|
63
|
+
// The filter is the tool-call event. `resolveHookEntries` also returns the
|
|
64
|
+
// once-per-session SessionStart / PostCompact entries; those are not what a
|
|
65
|
+
// per-call hook-entry summary is read for, and leaving them out is the only
|
|
66
|
+
// place this summary is narrower than the install.
|
|
67
|
+
const event = resolveHookSpec(ide).hookEnforceEvent;
|
|
68
|
+
return resolveHookEntries(ide, _skipProgress)
|
|
69
|
+
.filter((entry) => entry.event === event)
|
|
70
|
+
.map((entry) => ({ matcher: entry.matcher, sentinel: entry.sentinel }));
|
|
67
71
|
}
|
|
68
72
|
/**
|
|
69
73
|
* Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): copy the
|
|
@@ -108,6 +112,31 @@ function readOnDiskDenyEntries(settings) {
|
|
|
108
112
|
return [];
|
|
109
113
|
return deny.filter((d) => typeof d === 'string');
|
|
110
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* One `nextActions` line for a hook-script copy, or `null` when there is
|
|
117
|
+
* nothing to say.
|
|
118
|
+
*
|
|
119
|
+
* Diagnosis 2026-09-15 (C7): the copy failure used to be reported as a single
|
|
120
|
+
* sentence — `'<x> hook not copied (source missing or non-global scope)'` —
|
|
121
|
+
* which merged a real build failure into the expected project-scope no-op.
|
|
122
|
+
* Only `scope === 'global'` copies these scripts at all (see the call sites),
|
|
123
|
+
* so in project scope the sentence described normal behaviour in the wording
|
|
124
|
+
* of a fault, and a healthy `peaks hooks install` read as broken.
|
|
125
|
+
*
|
|
126
|
+
* Scope is therefore what decides the sentence, and it is knowable here — the
|
|
127
|
+
* copy helper cannot tell the two cases apart because both return
|
|
128
|
+
* `copied: false`, but the caller can. Project scope emits no line: there is
|
|
129
|
+
* no action to take, and the envelope's `bridgeHookCopy` / `codeGateHookCopy`
|
|
130
|
+
* field still reports `copied: false` for anyone reading the JSON.
|
|
131
|
+
*/
|
|
132
|
+
function describeHookCopy(label, copy, scope, dryRun) {
|
|
133
|
+
if (scope !== 'global')
|
|
134
|
+
return null;
|
|
135
|
+
if (copy.copied) {
|
|
136
|
+
return dryRun ? `would copy ${label} from ${copy.source} to ${copy.target}` : `Copied ${label}: ${copy.target}`;
|
|
137
|
+
}
|
|
138
|
+
return `${label} NOT copied — source missing at ${copy.source}. Run the build (\`pnpm build\`) so the script ships with the package.`;
|
|
139
|
+
}
|
|
111
140
|
function copyBridgeHookIfPresent(userHome, dryRun = false) {
|
|
112
141
|
const source = resolve(MODULE_DIR, '..', '..', 'services', 'hooks', 'pre-tool-superpowers-bridge.sh');
|
|
113
142
|
const target = resolve(userHome, '.claude', 'skills', 'peaks-code', 'hooks', 'pre-tool-superpowers-bridge.sh');
|
|
@@ -202,22 +231,16 @@ export function registerHooksCommands(program, io) {
|
|
|
202
231
|
// committed, shared file.
|
|
203
232
|
...plan.entryTargets.map((entry) => `would write ${entry.matcher || '(no matcher)'} → ${entry.sentinel} to ${entry.settingsPath}`),
|
|
204
233
|
`would write ${listSuperpowersDenyEntries().length} permissions.deny entries (Layer 3 worktree governance)`,
|
|
205
|
-
bridgeCopy
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
codeGateCopy.copied
|
|
209
|
-
? `would copy code-gate hook from ${codeGateCopy.source} to ${codeGateCopy.target}`
|
|
210
|
-
: 'would not copy code-gate hook (source missing or non-global scope)'
|
|
211
|
-
]), options.json);
|
|
234
|
+
describeHookCopy('bridge hook', bridgeCopy, scope, true),
|
|
235
|
+
describeHookCopy('code-gate hook', codeGateCopy, scope, true)
|
|
236
|
+
].filter((line) => line !== null)), options.json);
|
|
212
237
|
return;
|
|
213
238
|
}
|
|
214
239
|
const result = applyHookInstall(scope, projectRoot, { ide, skipProgress });
|
|
215
|
-
// Slice #3: build the per-IDE entries summary
|
|
216
|
-
//
|
|
217
|
-
// claude-code default). The
|
|
218
|
-
//
|
|
219
|
-
// entry; the summary mirrors the install shape, NOT a hardcoded
|
|
220
|
-
// expected list.
|
|
240
|
+
// Slice #3: build the per-IDE entries summary for the IDE the user
|
|
241
|
+
// targeted, not the slice #1 PEAKS_HOOK_ENTRIES constant (which is the
|
|
242
|
+
// claude-code default). The summary is derived from the install's own
|
|
243
|
+
// entry table — see `listExpectedEntriesForIde`.
|
|
221
244
|
const installedEntries = listExpectedEntriesForIde(ide, skipProgress);
|
|
222
245
|
// Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): when
|
|
223
246
|
// the install targets global scope, also copy the superpowers-bridge
|
|
@@ -242,16 +265,10 @@ export function registerHooksCommands(program, io) {
|
|
|
242
265
|
// Slice 2026-07-29-worktree-layer3-deny: surface L3 deny
|
|
243
266
|
// write alongside the hook install — single atomic write.
|
|
244
267
|
`Layer 3 deny: wrote ${listSuperpowersDenyEntries().length} permissions.deny entries (worktree governance)`,
|
|
245
|
-
bridgeCopy
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
? `Copied code-gate hook: ${codeGateCopy.target}`
|
|
250
|
-
: 'Code-gate hook not copied (source missing or non-global scope)'
|
|
251
|
-
]
|
|
252
|
-
: (bridgeCopy.copied
|
|
253
|
-
? [`Bridge hook copied: ${bridgeCopy.target}`]
|
|
254
|
-
: []);
|
|
268
|
+
describeHookCopy('bridge hook', bridgeCopy, scope, false),
|
|
269
|
+
describeHookCopy('code-gate hook', codeGateCopy, scope, false)
|
|
270
|
+
].filter((line) => line !== null)
|
|
271
|
+
: [describeHookCopy('bridge hook', bridgeCopy, scope, false)].filter((line) => line !== null);
|
|
255
272
|
// Slice 2026-07-29-worktree-layer3-deny: emit L3 deny bookkeeping
|
|
256
273
|
// in the JSON envelope so downstream automation (audit / sc) can
|
|
257
274
|
// confirm Layer 3 was applied without re-reading the file. When
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { realpathSync as realpathSyncNative } from 'node:fs';
|
|
1
2
|
import { resolve } from 'node:path';
|
|
2
3
|
import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
|
|
3
4
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
@@ -275,18 +276,18 @@ export function registerAwaitCommand(parent, io) {
|
|
|
275
276
|
const adapter = getAdapter(ide);
|
|
276
277
|
const dispatcher = adapter.subAgentDispatcher;
|
|
277
278
|
if (typeof dispatcher.awaitBatch !== 'function') {
|
|
278
|
-
printResult(io, fail('sub-agent.await', 'IDE_NOT_SUPPORTED', `IDE ${ide} does not support awaitBatch
|
|
279
|
-
'
|
|
279
|
+
printResult(io, fail('sub-agent.await', 'IDE_NOT_SUPPORTED', `IDE ${ide} does not support awaitBatch`, { ok: false }, [
|
|
280
|
+
'Every built-in adapter has an awaitBatch since slice 1.3; a dispatcher without one is a custom adapter registered outside the built-in set.'
|
|
280
281
|
]), asJson);
|
|
281
282
|
process.exitCode = 1;
|
|
282
283
|
return;
|
|
283
284
|
}
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
285
|
+
// No record-path index is kept for DAG-dispatched batches yet; the caller
|
|
286
|
+
// is expected to have a single shared record directory. We pass the empty
|
|
287
|
+
// list — the runner tracks outcomes through its own contract-store writes,
|
|
288
|
+
// so the dispatcher reaches no slot and reports no results (see the
|
|
289
|
+
// `recordPaths: []` case in tests/unit/services/dispatch/
|
|
290
|
+
// sub-agent-dispatchers.test.ts, which pins that empty shape).
|
|
290
291
|
const input = {
|
|
291
292
|
batchId: options.batch,
|
|
292
293
|
dispatchCount: 1,
|
|
@@ -304,7 +305,14 @@ export function registerAwaitCommand(parent, io) {
|
|
|
304
305
|
results,
|
|
305
306
|
summary
|
|
306
307
|
}, [], [
|
|
307
|
-
|
|
308
|
+
// Slice 2026-09-15-s9: corrected. This used to tell users that the
|
|
309
|
+
// four non-Claude IDEs would report `awaitByLlm: <ide> 1.2 fallback`,
|
|
310
|
+
// the slice-1.2 marker that slice 1.3 replaced with a real
|
|
311
|
+
// file-polling await. The text survived because nothing tested it —
|
|
312
|
+
// no adapter produces that note any more (asserted in
|
|
313
|
+
// sub-agent-dispatchers.test.ts), and the only code that still emits
|
|
314
|
+
// it, `awaitByLlmFallback`, has no callers.
|
|
315
|
+
`Each non-claude-code IDE labels its own results (see the \`note\` field), so a timed-out slot is attributable to the adapter it came from.`
|
|
308
316
|
]), asJson);
|
|
309
317
|
}
|
|
310
318
|
catch (error) {
|
|
@@ -325,6 +333,24 @@ function awaitErrorNextActions(code) {
|
|
|
325
333
|
}
|
|
326
334
|
return 'See error message; check that --batch matches the dispatch envelope and --timeout is a positive integer ms.';
|
|
327
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Resolve a record's on-disk path through `realpathSync` so the value returned
|
|
338
|
+
* here matches the value `writeInitialDispatchRecord` produces — which itself
|
|
339
|
+
* passes through `assertSafeDispatchRecordPath` and ends up canonicalized. On
|
|
340
|
+
* macOS, `mkdtempSync(join(tmpdir(), prefix))` returns `/var/folders/...`
|
|
341
|
+
* while `process.cwd()` after `chdir` returns `/private/var/folders/...`;
|
|
342
|
+
* without this helper the test fixture's `queuedPath` (canonical) and the
|
|
343
|
+
* envelope's `finalized[].recordPath` (raw) never compare equal. Falls back
|
|
344
|
+
* to the lexical path when the file is missing (race between scan and write).
|
|
345
|
+
*/
|
|
346
|
+
function safeRecordPath(p) {
|
|
347
|
+
try {
|
|
348
|
+
return realpathSyncNative(p);
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return p;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
328
354
|
export function registerFinalizeCommand(parent, io) {
|
|
329
355
|
addJsonOption(parent
|
|
330
356
|
.command('finalize')
|
|
@@ -418,7 +444,7 @@ export function registerFinalizeCommand(parent, io) {
|
|
|
418
444
|
// `--batch` branch below has always used this same filter.
|
|
419
445
|
if (!f.startsWith('dispatch-') || !f.endsWith('.json'))
|
|
420
446
|
continue;
|
|
421
|
-
const p = path2.join(dir, f);
|
|
447
|
+
const p = safeRecordPath(path2.join(dir, f));
|
|
422
448
|
const r = tryReadRecord(p);
|
|
423
449
|
if (r === null || r.requestId !== options.requestId)
|
|
424
450
|
continue;
|
|
@@ -470,7 +496,7 @@ export function registerFinalizeCommand(parent, io) {
|
|
|
470
496
|
for (const f of fs2.readdirSync(dir)) {
|
|
471
497
|
if (!f.startsWith('dispatch-') || !f.endsWith('.json'))
|
|
472
498
|
continue;
|
|
473
|
-
const p = path2.join(dir, f);
|
|
499
|
+
const p = safeRecordPath(path2.join(dir, f));
|
|
474
500
|
const r = tryReadRecord(p);
|
|
475
501
|
if (r === null)
|
|
476
502
|
continue;
|
|
@@ -161,7 +161,14 @@ export async function runWebOp(io, op, args, asJson) {
|
|
|
161
161
|
return;
|
|
162
162
|
}
|
|
163
163
|
const info = await ensureDaemon(projectRoot, sessionId);
|
|
164
|
-
|
|
164
|
+
// The daemon reads `projectRoot` back from `daemon.json` — the record it
|
|
165
|
+
// itself wrote — and that round-trip is the only path that survives the
|
|
166
|
+
// macOS `/var` <-> `/private/var` symlink: the writer's prefix may not
|
|
167
|
+
// match `projectRoot` resolved from `process.cwd()` (the kernel resolves
|
|
168
|
+
// symlinks on `chdir`), and the integration test asserts on that exact
|
|
169
|
+
// round-tripped value.
|
|
170
|
+
const daemonProjectRoot = info.projectRoot;
|
|
171
|
+
const response = await new WebDaemonClient(info).call(op, { ...opArgs, dispatchId: dispatchId(), projectRoot: daemonProjectRoot, sessionId }, OP_TIMEOUT_MS);
|
|
165
172
|
if (!response.ok || response.data === null) {
|
|
166
173
|
const code = safeDaemonCode(response.code);
|
|
167
174
|
// The daemon no longer downloads (R3), so "the browser is not installed"
|
|
@@ -60,4 +60,10 @@ export interface WorkflowTerminalizeOptions {
|
|
|
60
60
|
readonly requireConsumed?: boolean;
|
|
61
61
|
readonly json?: boolean;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The literal that means "no session could be resolved". Kept as a named
|
|
65
|
+
* constant because three call sites now have to ASK whether resolution
|
|
66
|
+
* failed rather than consume the value as if it were an id.
|
|
67
|
+
*/
|
|
68
|
+
export declare const UNKNOWN_SESSION_ID = "unknown-sid";
|
|
63
69
|
export declare function registerWorkflowLifecycleCommand(parent: Command, io: ProgramIO): void;
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { fail, ok, getErrorMessage } from 'peaks-loop-shared/result';
|
|
10
10
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
11
11
|
import { resolveCallerId } from '../../services/session/resolve-caller-id.js';
|
|
12
|
+
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
13
|
+
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
12
14
|
import { initWorkflow, terminalizeWorkflow, } from '../../services/workflow/workflow-presence-lifecycle.js';
|
|
13
15
|
import { readGraph, emptyGraph } from '../../services/workflow/workflow-graph-store.js';
|
|
14
16
|
import { WORKFLOW_ID_REGEX, TERMINAL_REASONS } from '../../services/workflow/workflow-graph-types.js';
|
|
@@ -26,8 +28,62 @@ function deriveCallerId() {
|
|
|
26
28
|
function deriveProjectRoot(options) {
|
|
27
29
|
return options.project ?? process.cwd();
|
|
28
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The literal that means "no session could be resolved". Kept as a named
|
|
33
|
+
* constant because three call sites now have to ASK whether resolution
|
|
34
|
+
* failed rather than consume the value as if it were an id.
|
|
35
|
+
*/
|
|
36
|
+
export const UNKNOWN_SESSION_ID = 'unknown-sid';
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the session id for every `workflow *` command.
|
|
39
|
+
*
|
|
40
|
+
* Tier order (matches the idiom every other command family in this CLI
|
|
41
|
+
* already uses — see `job-commands.ts:85`, `container-commands.ts:138`):
|
|
42
|
+
*
|
|
43
|
+
* 1. `--session-id <sid>` — explicit wins
|
|
44
|
+
* 2. `PEAKS_SESSION_ID` env var — scripted / sub-process callers
|
|
45
|
+
* 3. `getCurrentSessionId(projectRoot)` — this caller's OWN binding
|
|
46
|
+
* (`callers/<callerId>.json`) falling back to the project-global
|
|
47
|
+
* `.peaks/_runtime/session.json`, the same resolver `peaks session
|
|
48
|
+
* info --active` and `peaks session checkpoint` use
|
|
49
|
+
* 4. `unknown-sid` — nothing is bound
|
|
50
|
+
*
|
|
51
|
+
* Tier 3 was MISSING until 2026-09-15 (S6), and its absence is the whole
|
|
52
|
+
* defect: `workflow init` resolved on tiers 1–2 only, so with no flag and no
|
|
53
|
+
* env var it wrote the graph into `.peaks/_runtime/unknown-sid/graphs/`
|
|
54
|
+
* regardless of what the binding file said. `peaks workflow node prepare`
|
|
55
|
+
* then ran under the CORRECT session dir and reported `PEAKS_GRAPH_NOT_FOUND`
|
|
56
|
+
* — the graph it was asking for had been written one bucket over. The bucket
|
|
57
|
+
* accumulated artifacts from at least 4 distinct caller ids between 2026-09-01
|
|
58
|
+
* and 2026-09-15, i.e. it had never worked for anyone.
|
|
59
|
+
*
|
|
60
|
+
* The same prior-art sweep (`.peaks/memory/archived/2026-06-26-unknown-sid-root-cause.md`)
|
|
61
|
+
* converted six inline `?? 'unknown-sid'` sites to this 4-tier chain;
|
|
62
|
+
* `workflow-lifecycle-commands.ts` was missed. This is that site.
|
|
63
|
+
*
|
|
64
|
+
* The project root passed to tier 3 goes through the same `findProjectRoot`
|
|
65
|
+
* walk `session info` uses, so a command run from a SUBDIRECTORY of the
|
|
66
|
+
* project resolves the project's binding instead of `cwd`'s (which has none).
|
|
67
|
+
*/
|
|
29
68
|
function deriveSessionId(options) {
|
|
30
|
-
|
|
69
|
+
if (typeof options.sessionId === 'string' && options.sessionId.length > 0)
|
|
70
|
+
return options.sessionId;
|
|
71
|
+
const fromEnv = process.env.PEAKS_SESSION_ID;
|
|
72
|
+
if (typeof fromEnv === 'string' && fromEnv.length > 0)
|
|
73
|
+
return fromEnv;
|
|
74
|
+
return getCurrentSessionId(findProjectRoot(options.project ?? process.cwd()) ?? (options.project ?? process.cwd())) ?? UNKNOWN_SESSION_ID;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Tier 3 resolving to `unknown-sid` means NO session is bound at all. A
|
|
78
|
+
* `workflow` subcommand that WRITES must fail loudly on that rather than
|
|
79
|
+
* create a bucket named after a failure — `graph list` already does (see its
|
|
80
|
+
* `PEAKS_SESSION_NOT_BOUND` guard below); `init` did not.
|
|
81
|
+
*/
|
|
82
|
+
function buildSessionNotBoundEnvelope(sessionId, projectRoot) {
|
|
83
|
+
return fail('workflow.init', 'PEAKS_SESSION_NOT_BOUND', `No peaks session is bound for '${projectRoot}' (derived session id '${sessionId}'): --session-id, PEAKS_SESSION_ID and the project binding are all absent.`, { workflowId: null }, [
|
|
84
|
+
'Run `peaks workspace init --project <p>` to bind a session, then re-run.',
|
|
85
|
+
'Or pass `--session-id <sid>` explicitly.',
|
|
86
|
+
]);
|
|
31
87
|
}
|
|
32
88
|
export function registerWorkflowLifecycleCommand(parent, io) {
|
|
33
89
|
// Reuse the existing `workflow` parent (created by `registerWorkflowCommands`)
|
|
@@ -46,8 +102,13 @@ export function registerWorkflowLifecycleCommand(parent, io) {
|
|
|
46
102
|
const asJson = options.json === true;
|
|
47
103
|
try {
|
|
48
104
|
const callerId = deriveCallerId();
|
|
49
|
-
const sessionId = deriveSessionId(options);
|
|
50
105
|
const projectRoot = deriveProjectRoot(options);
|
|
106
|
+
const sessionId = deriveSessionId(options);
|
|
107
|
+
if (sessionId === UNKNOWN_SESSION_ID) {
|
|
108
|
+
printResult(io, buildSessionNotBoundEnvelope(sessionId, projectRoot), asJson);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
51
112
|
const workflowId = options.workflowId ?? `wf-${Date.now().toString(36)}`;
|
|
52
113
|
if (!WORKFLOW_ID_REGEX.test(workflowId)) {
|
|
53
114
|
throw new Error(`workflowId shape invalid: ${workflowId}`);
|
|
@@ -120,7 +181,7 @@ export function registerWorkflowLifecycleCommand(parent, io) {
|
|
|
120
181
|
const asJson = options.json === true;
|
|
121
182
|
try {
|
|
122
183
|
const sessionId = deriveSessionId(options);
|
|
123
|
-
if (
|
|
184
|
+
if (sessionId === UNKNOWN_SESSION_ID) {
|
|
124
185
|
throw new Error('PEAKS_SESSION_NOT_BOUND: no session id');
|
|
125
186
|
}
|
|
126
187
|
const result = { envelopeVersion: '4.0.8', sessionId, graphs: [] };
|
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠ DEAD CODE — NOT THE LIVE VENDOR LAYER, AND NOT THE LIVE IDE LAYER.
|
|
3
|
+
*
|
|
4
|
+
* Three things in this repo are named `<Vendor>Adapter`, and only one of
|
|
5
|
+
* them is live. Read this table before importing anything from here:
|
|
6
|
+
*
|
|
7
|
+
* 1. `src/services/runtime/vendors/<vendor>.ts`
|
|
8
|
+
* → `class CodexAdapter implements VendorAdapter` (runtime detect +
|
|
9
|
+
* compact). LIVE — wired by `RuntimeService`'s built-in list. This is
|
|
10
|
+
* the one a reader looking for "the codex adapter" wants.
|
|
11
|
+
* 2. `packages/peaks-loop-internal-runtime/src/vendor/<vendor>-adapter.ts`
|
|
12
|
+
* → a second `CodexAdapter`, different surface (binary / headlessArgs /
|
|
13
|
+
* parseStatusLine / detectInstalled) for the dispatched-sub-agent
|
|
14
|
+
* runtime. LIVE — exported by that package's public index.
|
|
15
|
+
* 3. THIS FILE'S SIBLINGS — `claude-adapter.ts` / `codex-adapter.ts` /
|
|
16
|
+
* `copilot-adapter.ts` + `auto-adapter.ts`.
|
|
17
|
+
* → **UNREFERENCED.** Zero importers in `src/`, `tests/`, `packages/`
|
|
18
|
+
* or `scripts/`. Every method throws `ADAPTER_NOT_IMPLEMENTED`; the
|
|
19
|
+
* one exception, `detect()` on the codex/copilot pair, is a hardcoded
|
|
20
|
+
* `return false`. `peaks skill adapter list` (the only CLI surface
|
|
21
|
+
* that names these) does not read them — it returns a literal array.
|
|
22
|
+
*
|
|
23
|
+
* The three layers diverged from a single planned skill-adapter surface and
|
|
24
|
+
* were never reconciled. These placeholder files are kept because they mark
|
|
25
|
+
* intent for unimplemented work; they are NOT kept because anything uses
|
|
26
|
+
* them. If you are here to implement them, the decision you actually need is
|
|
27
|
+
* which of layers 1–3 survives — see
|
|
28
|
+
* `tests/unit/runtime/vendor-adapter-layer.test.ts`, which pins which layer
|
|
29
|
+
* is live and fails the moment that stops being true.
|
|
30
|
+
*/
|
|
1
31
|
export interface AdapterSegment {
|
|
2
32
|
name: string;
|
|
3
33
|
skillMd: string;
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠ DEAD CODE — zero importers in `src/`, `tests/`, `packages/`, `scripts/`.
|
|
3
|
+
* See the block comment on `src/services/adapter/adapter.ts`.
|
|
4
|
+
*
|
|
5
|
+
* `detectAndPick()` picks the first adapter whose `detect()` returns true.
|
|
6
|
+
* It cannot pick anything today: its only candidate implementations are its
|
|
7
|
+
* three dead siblings, and the codex/copilot pair hardcode `detect()` to
|
|
8
|
+
* `false` while the claude one hardcodes `true` — so the "pick" is a
|
|
9
|
+
* constant that never varies with the environment. It is not wired to
|
|
10
|
+
* `peaks skill adapter set-active` either; that CLI verb
|
|
11
|
+
* (`src/cli/commands/adapter-commands.ts`) echoes back the name it was given
|
|
12
|
+
* and reads no adapter file.
|
|
13
|
+
*/
|
|
1
14
|
import { Adapter } from "./adapter.js";
|
|
2
15
|
type Detectable = Pick<Adapter, "name" | "detect">;
|
|
3
16
|
export declare class AutoAdapter {
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠ DEAD CODE — not a live adapter. See the block comment on
|
|
3
|
+
* `src/services/adapter/adapter.ts` for the three same-named layers.
|
|
4
|
+
*
|
|
5
|
+
* This is the only file in the cluster with a real implementation, which is
|
|
6
|
+
* what makes it the most misleading one: it writes a real
|
|
7
|
+
* `~/.claude/skills/peaks-bee-<name>.peaks-generated/SKILL.md` scratch dir if
|
|
8
|
+
* you call it. Nothing calls it — zero importers in `src/`, `tests/`,
|
|
9
|
+
* `packages/` or `scripts/`. The live Claude Code knowledge lives in
|
|
10
|
+
* `src/services/ide/adapters/claude-code-adapter.ts` (IDE surface) and
|
|
11
|
+
* `src/services/runtime/vendors/claude-code.ts` (runtime compact).
|
|
12
|
+
*/
|
|
1
13
|
import { writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
|
|
2
14
|
import { join } from "node:path";
|
|
3
15
|
export class ClaudeAdapter {
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠ DEAD CODE — not the live `CodexAdapter`. See the block comment on
|
|
3
|
+
* `src/services/adapter/adapter.ts` for the three same-named layers, and
|
|
4
|
+
* `tests/unit/runtime/vendor-adapter-layer.test.ts` for the guard.
|
|
5
|
+
*
|
|
6
|
+
* The class below has zero importers anywhere in this repo. The live codex
|
|
7
|
+
* adapter is `src/services/runtime/vendors/codex.ts` (runtime detect +
|
|
8
|
+
* compact, wired into `RuntimeService`) and
|
|
9
|
+
* `packages/peaks-loop-internal-runtime/src/vendor/codex-adapter.ts` (the
|
|
10
|
+
* dispatched-sub-agent runtime). `detect()` here returns a hardcoded
|
|
11
|
+
* `false`; every other method throws `ADAPTER_NOT_IMPLEMENTED`.
|
|
12
|
+
*/
|
|
1
13
|
import { Adapter } from "./adapter.js";
|
|
2
14
|
export declare class CodexAdapter implements Pick<Adapter, "name"> {
|
|
3
15
|
private readonly _o;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠ DEAD CODE — not the live `CodexAdapter`. See the block comment on
|
|
3
|
+
* `src/services/adapter/adapter.ts` for the three same-named layers, and
|
|
4
|
+
* `tests/unit/runtime/vendor-adapter-layer.test.ts` for the guard.
|
|
5
|
+
*
|
|
6
|
+
* The class below has zero importers anywhere in this repo. The live codex
|
|
7
|
+
* adapter is `src/services/runtime/vendors/codex.ts` (runtime detect +
|
|
8
|
+
* compact, wired into `RuntimeService`) and
|
|
9
|
+
* `packages/peaks-loop-internal-runtime/src/vendor/codex-adapter.ts` (the
|
|
10
|
+
* dispatched-sub-agent runtime). `detect()` here returns a hardcoded
|
|
11
|
+
* `false`; every other method throws `ADAPTER_NOT_IMPLEMENTED`.
|
|
12
|
+
*/
|
|
1
13
|
import { ADAPTER_NOT_IMPLEMENTED } from "./adapter.js";
|
|
2
14
|
export class CodexAdapter {
|
|
3
15
|
_o;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠ DEAD CODE — not the live `CopilotAdapter`. See the block comment on
|
|
3
|
+
* `src/services/adapter/adapter.ts` for the three same-named layers, and
|
|
4
|
+
* `tests/unit/runtime/vendor-adapter-layer.test.ts` for the guard.
|
|
5
|
+
*
|
|
6
|
+
* The class below has zero importers anywhere in this repo. The live copilot
|
|
7
|
+
* adapter is `src/services/runtime/vendors/copilot.ts` (runtime detect +
|
|
8
|
+
* compact, wired into `RuntimeService`) and
|
|
9
|
+
* `packages/peaks-loop-internal-runtime/src/vendor/copilot-adapter.ts` (the
|
|
10
|
+
* dispatched-sub-agent runtime). `detect()` here returns a hardcoded
|
|
11
|
+
* `false`; every other method throws `ADAPTER_NOT_IMPLEMENTED`.
|
|
12
|
+
*/
|
|
1
13
|
import { Adapter } from "./adapter.js";
|
|
2
14
|
export declare class CopilotAdapter implements Pick<Adapter, "name"> {
|
|
3
15
|
private readonly _o;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠ DEAD CODE — not the live `CopilotAdapter`. See the block comment on
|
|
3
|
+
* `src/services/adapter/adapter.ts` for the three same-named layers, and
|
|
4
|
+
* `tests/unit/runtime/vendor-adapter-layer.test.ts` for the guard.
|
|
5
|
+
*
|
|
6
|
+
* The class below has zero importers anywhere in this repo. The live copilot
|
|
7
|
+
* adapter is `src/services/runtime/vendors/copilot.ts` (runtime detect +
|
|
8
|
+
* compact, wired into `RuntimeService`) and
|
|
9
|
+
* `packages/peaks-loop-internal-runtime/src/vendor/copilot-adapter.ts` (the
|
|
10
|
+
* dispatched-sub-agent runtime). `detect()` here returns a hardcoded
|
|
11
|
+
* `false`; every other method throws `ADAPTER_NOT_IMPLEMENTED`.
|
|
12
|
+
*/
|
|
1
13
|
import { ADAPTER_NOT_IMPLEMENTED } from "./adapter.js";
|
|
2
14
|
export class CopilotAdapter {
|
|
3
15
|
_o;
|
|
@@ -2,23 +2,41 @@
|
|
|
2
2
|
* Backing detector — classifies each red line as `cli-backed`, `partial`,
|
|
3
3
|
* or `prose-only`. The classifier already sets the backing for catalog hits
|
|
4
4
|
* (cli-backed when an enforcer file path is present). This module exists to
|
|
5
|
-
* handle the post-classification
|
|
6
|
-
* (a gate exists but the LLM can bypass it)
|
|
7
|
-
*
|
|
5
|
+
* handle the post-classification nuances: heuristics for the "partial" tier
|
|
6
|
+
* (a gate exists but the LLM can bypass it), verification that the enforcer
|
|
7
|
+
* file exists on disk, and — since A9 of the 2026-09-15 diagnosis —
|
|
8
|
+
* verification that the enforcer is actually reachable from a call site.
|
|
9
|
+
*
|
|
10
|
+
* `cli-backed` means "a CLI surface runs this rule". A file that exists but
|
|
11
|
+
* that nothing imports runs nothing, so it is `prose-only` with the same
|
|
12
|
+
* weight as an unwritten rule. Callers pass the live set from
|
|
13
|
+
* `enforcer-liveness.ts`; `null` means "liveness undecidable for this
|
|
14
|
+
* project" (no `src/` tree), in which case the existing file is trusted.
|
|
8
15
|
*/
|
|
9
16
|
import type { RedLineEntry } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* `liveEnforcers` is the set of enforcerRefs with a call site (see
|
|
19
|
+
* `enforcer-liveness.ts`), or `null` when the project has no `src/` tree to
|
|
20
|
+
* scan and liveness therefore cannot be decided.
|
|
21
|
+
*/
|
|
22
|
+
export type LiveEnforcerSet = ReadonlySet<string> | null;
|
|
10
23
|
export interface BackingResult {
|
|
11
24
|
readonly entry: RedLineEntry;
|
|
12
25
|
readonly enforcerExists: boolean;
|
|
26
|
+
/** True when the enforcer file exists but no call site imports it. */
|
|
27
|
+
readonly enforcerDead: boolean;
|
|
13
28
|
}
|
|
14
29
|
/**
|
|
15
30
|
* Re-classify a single RedLineEntry. Returns a new entry with the
|
|
16
|
-
* `backing` field updated
|
|
17
|
-
*
|
|
31
|
+
* `backing` field updated; `enforcerRef` is preserved even when the
|
|
32
|
+
* backing is downgraded, because the dead reference is the triage
|
|
33
|
+
* information the report needs.
|
|
18
34
|
*/
|
|
19
|
-
export declare function classifyBacking(entry: RedLineEntry, projectRoot: string): BackingResult;
|
|
35
|
+
export declare function classifyBacking(entry: RedLineEntry, projectRoot: string, liveEnforcers: LiveEnforcerSet): BackingResult;
|
|
20
36
|
export interface BackingBatchResult {
|
|
21
37
|
readonly entries: readonly RedLineEntry[];
|
|
22
38
|
readonly warnings: readonly string[];
|
|
39
|
+
/** Sorted, deduplicated enforcerRefs downgraded for having no call site. */
|
|
40
|
+
readonly deadEnforcers: readonly string[];
|
|
23
41
|
}
|
|
24
|
-
export declare function classifyBackingBatch(entries: readonly RedLineEntry[], projectRoot: string): BackingBatchResult;
|
|
42
|
+
export declare function classifyBackingBatch(entries: readonly RedLineEntry[], projectRoot: string, liveEnforcers: LiveEnforcerSet): BackingBatchResult;
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* Backing detector — classifies each red line as `cli-backed`, `partial`,
|
|
3
3
|
* or `prose-only`. The classifier already sets the backing for catalog hits
|
|
4
4
|
* (cli-backed when an enforcer file path is present). This module exists to
|
|
5
|
-
* handle the post-classification
|
|
6
|
-
* (a gate exists but the LLM can bypass it)
|
|
7
|
-
*
|
|
5
|
+
* handle the post-classification nuances: heuristics for the "partial" tier
|
|
6
|
+
* (a gate exists but the LLM can bypass it), verification that the enforcer
|
|
7
|
+
* file exists on disk, and — since A9 of the 2026-09-15 diagnosis —
|
|
8
|
+
* verification that the enforcer is actually reachable from a call site.
|
|
9
|
+
*
|
|
10
|
+
* `cli-backed` means "a CLI surface runs this rule". A file that exists but
|
|
11
|
+
* that nothing imports runs nothing, so it is `prose-only` with the same
|
|
12
|
+
* weight as an unwritten rule. Callers pass the live set from
|
|
13
|
+
* `enforcer-liveness.ts`; `null` means "liveness undecidable for this
|
|
14
|
+
* project" (no `src/` tree), in which case the existing file is trusted.
|
|
8
15
|
*/
|
|
9
16
|
import { existsSync } from 'node:fs';
|
|
10
17
|
import { resolve } from 'node:path';
|
|
@@ -23,37 +30,46 @@ function detectPartial(context) {
|
|
|
23
30
|
}
|
|
24
31
|
/**
|
|
25
32
|
* Re-classify a single RedLineEntry. Returns a new entry with the
|
|
26
|
-
* `backing` field updated
|
|
27
|
-
*
|
|
33
|
+
* `backing` field updated; `enforcerRef` is preserved even when the
|
|
34
|
+
* backing is downgraded, because the dead reference is the triage
|
|
35
|
+
* information the report needs.
|
|
28
36
|
*/
|
|
29
|
-
export function classifyBacking(entry, projectRoot) {
|
|
37
|
+
export function classifyBacking(entry, projectRoot, liveEnforcers) {
|
|
38
|
+
const enforcerPath = entry.enforcerRef === null ? null : resolve(projectRoot, entry.enforcerRef);
|
|
39
|
+
const exists = enforcerPath !== null && existsSync(enforcerPath);
|
|
40
|
+
const dead = entry.enforcerRef !== null && exists && liveEnforcers !== null && !liveEnforcers.has(entry.enforcerRef);
|
|
30
41
|
if (detectPartial(entry.source.context)) {
|
|
31
42
|
return {
|
|
32
43
|
entry: { ...entry, backing: 'partial' },
|
|
33
|
-
enforcerExists:
|
|
44
|
+
enforcerExists: exists,
|
|
45
|
+
enforcerDead: dead,
|
|
34
46
|
};
|
|
35
47
|
}
|
|
36
48
|
if (entry.enforcerRef === null) {
|
|
37
|
-
return { entry, enforcerExists: false };
|
|
49
|
+
return { entry, enforcerExists: false, enforcerDead: false };
|
|
38
50
|
}
|
|
39
|
-
const
|
|
40
|
-
const exists = existsSync(enforcerPath);
|
|
51
|
+
const backed = exists && !dead;
|
|
41
52
|
return {
|
|
42
|
-
entry: { ...entry, backing:
|
|
53
|
+
entry: { ...entry, backing: backed ? 'cli-backed' : 'prose-only' },
|
|
43
54
|
enforcerExists: exists,
|
|
55
|
+
enforcerDead: dead,
|
|
44
56
|
};
|
|
45
57
|
}
|
|
46
|
-
export function classifyBackingBatch(entries, projectRoot) {
|
|
58
|
+
export function classifyBackingBatch(entries, projectRoot, liveEnforcers) {
|
|
47
59
|
const updated = [];
|
|
48
60
|
const warnings = [];
|
|
61
|
+
const dead = new Set();
|
|
49
62
|
for (const entry of entries) {
|
|
50
|
-
const
|
|
51
|
-
updated.push(
|
|
52
|
-
if (
|
|
63
|
+
const result = classifyBacking(entry, projectRoot, liveEnforcers);
|
|
64
|
+
updated.push(result.entry);
|
|
65
|
+
if (result.enforcerDead && result.entry.enforcerRef !== null) {
|
|
66
|
+
dead.add(result.entry.enforcerRef);
|
|
67
|
+
}
|
|
68
|
+
if (result.entry.backing === 'cli-backed' && !result.enforcerExists) {
|
|
53
69
|
// Defensive: should not happen because classifyBacking downgrades to
|
|
54
70
|
// prose-only, but keep the assertion in case of future drift.
|
|
55
|
-
warnings.push(`enforcer ref "${
|
|
71
|
+
warnings.push(`enforcer ref "${result.entry.enforcerRef}" missing on disk for ${result.entry.id}`);
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
|
-
return { entries: updated, warnings };
|
|
74
|
+
return { entries: updated, warnings, deadEnforcers: [...dead].sort() };
|
|
59
75
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LiveEnforcerScan {
|
|
2
|
+
/** Refs proven imported from a call site. Empty when `unknown` is true. */
|
|
3
|
+
readonly live: ReadonlySet<string>;
|
|
4
|
+
/** True when the project has no `src/` tree, so liveness is undecidable. */
|
|
5
|
+
readonly unknown: boolean;
|
|
6
|
+
readonly warnings: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Scan `projectRoot/src` for relative imports that name one of `enforcerRefs`.
|
|
10
|
+
* Returns the subset of refs with a call site outside the enforcers directory.
|
|
11
|
+
*/
|
|
12
|
+
export declare function computeLiveEnforcers(projectRoot: string, enforcerRefs: readonly string[]): LiveEnforcerScan;
|