peaks-loop 4.0.48 → 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 +44 -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/compact-command.js +1 -3
- 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/feedback-commands.d.ts +11 -7
- package/dist/cli/commands/feedback-commands.js +49 -17
- package/dist/cli/commands/final-review-commands.js +12 -0
- package/dist/cli/commands/hooks-commands.js +55 -38
- package/dist/cli/commands/loop-eval-commands.js +22 -6
- package/dist/cli/commands/share-commands.js +37 -11
- package/dist/cli/commands/slice-integrate-commands.js +17 -0
- 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/artifacts/artifact-prerequisites.js +10 -0
- package/dist/services/artifacts/request-artifact-service.js +59 -38
- 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/active-skill-resolver.js +14 -1
- 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-lifecycle.d.ts +75 -0
- package/dist/services/code/auto-compact-lifecycle.js +65 -16
- 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/auto-compact-orchestrator.js +119 -19
- package/dist/services/code/compact-event-settle.d.ts +20 -8
- package/dist/services/code/compact-event-settle.js +21 -0
- package/dist/services/code/post-compact-detector.js +20 -11
- package/dist/services/code/step-08-gate.js +21 -6
- package/dist/services/compact-statusline/compact-statusline-service.js +56 -22
- package/dist/services/config/config-safety.js +11 -9
- package/dist/services/context/auto-compact-types.d.ts +20 -2
- package/dist/services/feedback/feedback-promotion-service.d.ts +137 -14
- package/dist/services/feedback/feedback-promotion-service.js +341 -20
- package/dist/services/feedback/promotion-artifact-evidence.d.ts +69 -0
- package/dist/services/feedback/promotion-artifact-evidence.js +332 -0
- package/dist/services/final-review/pre-post-diff.js +10 -2
- package/dist/services/job/job-progress-store.js +18 -3
- package/dist/services/observability/jsonl-store.d.ts +19 -0
- package/dist/services/observability/jsonl-store.js +27 -2
- package/dist/services/observability/observability-service.d.ts +11 -4
- package/dist/services/observability/observability-service.js +16 -3
- package/dist/services/prd/handoff-service.js +43 -0
- package/dist/services/qa/qa-business-review-state.js +19 -5
- package/dist/services/sc/sc-service.d.ts +8 -0
- package/dist/services/sc/sc-service.js +8 -1
- package/dist/services/scan/api-diff-types.js +20 -2
- package/dist/services/security/safe-settings-path.js +19 -1
- package/dist/services/session/getSessionDir.d.ts +33 -0
- package/dist/services/session/getSessionDir.js +60 -0
- package/dist/services/skill/skill-search-service.d.ts +3 -3
- package/dist/services/slice/slice-review-state.js +19 -4
- 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/workflow/pipeline-verify-gate-support.js +10 -11
- package/dist/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/services/workflow/pipeline-verify-service.js +23 -10
- package/dist/services/workflow/pipeline-verify-types.d.ts +5 -3
- 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/dist/shared/runtime-root.d.ts +73 -0
- package/dist/shared/runtime-root.js +77 -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
|
@@ -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,6 +2,7 @@ import { join, dirname, basename } from 'node:path';
|
|
|
2
2
|
import { readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { pathExists } from 'peaks-loop-shared/fs';
|
|
4
4
|
import { emitObservabilityEvent } from '../observability/observability-service.js';
|
|
5
|
+
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
5
6
|
export const VALID_REQUEST_TYPES = [
|
|
6
7
|
'feature',
|
|
7
8
|
'bugfix',
|
|
@@ -426,6 +427,15 @@ export async function checkPrerequisites(options) {
|
|
|
426
427
|
if (requirements.length === 0) {
|
|
427
428
|
return { ok: true, missing: [], warnings: [] };
|
|
428
429
|
}
|
|
430
|
+
// Repair R5. The session id is joined into BOTH roots below and then probed
|
|
431
|
+
// on disk. `transitionRequestArtifact` is the only caller and it passes
|
|
432
|
+
// `existing.sessionId`, which pre-R5 was the caller's raw `--session-id`: with
|
|
433
|
+
// `../../../PWNED-R34` the two joins resolved outside the project root and
|
|
434
|
+
// the prerequisite probes ran there. Guarded at the sink, not at the caller,
|
|
435
|
+
// for the same reason `requestArtifactRequestsDir` is.
|
|
436
|
+
if (options.sessionId !== undefined && isUnsafePathInput(options.sessionId)) {
|
|
437
|
+
throw new Error(`Invalid session id: ${options.sessionId} (must be a single path segment)`);
|
|
438
|
+
}
|
|
429
439
|
// Slice 006 simplifies the resolution to a 2-tier fallback. The
|
|
430
440
|
// per-change-id scope (`.peaks/_runtime/<sessionId>/<role>/`) is gone — new
|
|
431
441
|
// artifacts go to the session dir directly. The 2 tiers are:
|
|
@@ -12,6 +12,7 @@ import { ensureSession, getSessionIdCanonical } from '../session/session-manager
|
|
|
12
12
|
import { getNextNumber, buildNumberedFilename, slugifyDescription } from '../../shared/incrementing-number.js';
|
|
13
13
|
import { lintRequestArtifact } from './artifact-lint-service.js';
|
|
14
14
|
import { isUnsafePathInput } from '../../shared/path-safety.js';
|
|
15
|
+
import { guardRuntimeSegment, runtimeRoot } from '../../shared/runtime-root.js';
|
|
15
16
|
import { checkTypeSanity } from '../scan/type-sanity-service.js';
|
|
16
17
|
import { requireUserConfirmation } from '../mode/mode-enforcement.js';
|
|
17
18
|
import { scanFileSize } from '../scan/file-size-scan.js';
|
|
@@ -46,6 +47,30 @@ function dateSlugFromIso(iso) {
|
|
|
46
47
|
function defaultSessionId(iso) {
|
|
47
48
|
return `${dateSlugFromIso(iso)}-session`;
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The single place a request-artifact directory is built, and therefore the
|
|
52
|
+
* single place the ids that reach it are guarded.
|
|
53
|
+
*
|
|
54
|
+
* Repair R5. This join used to be written at four sites; `createRequestArtifact`
|
|
55
|
+
* guarded its session id and the other three did not. The two ids are the whole
|
|
56
|
+
* of the invariant — `role` is a path segment too, and the closed-set check its
|
|
57
|
+
* callers perform is not visible from here.
|
|
58
|
+
*
|
|
59
|
+
* The guard belongs HERE and not at the entry points because the invariant is
|
|
60
|
+
* the resolved path, not the flag. `transitionRequestArtifact` is the measured
|
|
61
|
+
* cost: it performs no join of its own (it delegates the path to
|
|
62
|
+
* `showRequestArtifact`), so it had no guard and no join to hang one on.
|
|
63
|
+
* `--session-id ../../../PWNED-R34` resolved `.peaks/_runtime/../../../PWNED-R34`,
|
|
64
|
+
* wrote `state: blocked` to a file outside the project root, and only then threw
|
|
65
|
+
* — from `emitObservabilityEvent`'s own session-id check, i.e. after the write.
|
|
66
|
+
*/
|
|
67
|
+
function requestArtifactRequestsDir(projectRoot, sessionId, role) {
|
|
68
|
+
// Slice 2026-09-15 (runtime-path-unrepresentable): the two ids are branded by
|
|
69
|
+
// `guardRuntimeSegment`, which performs the same `isUnsafePathInput` check
|
|
70
|
+
// this function used to spell inline. The join itself now *requires* the
|
|
71
|
+
// brand, so a caller that reaches this dir without a guard does not compile.
|
|
72
|
+
return runtimeRoot(projectRoot).join(guardRuntimeSegment(sessionId, 'session id'), guardRuntimeSegment(role, 'role'), guardRuntimeSegment('requests', 'leaf'));
|
|
73
|
+
}
|
|
49
74
|
export async function createRequestArtifact(options) {
|
|
50
75
|
if (!VALID_ROLES.has(options.role)) {
|
|
51
76
|
throw new Error(`Invalid role: ${String(options.role)} (expected prd, ui, rd, qa, or sc)`);
|
|
@@ -95,7 +120,7 @@ export async function createRequestArtifact(options) {
|
|
|
95
120
|
// `mkdir(..., { recursive: true })`.
|
|
96
121
|
const LOOKS_LIKE_SESSION_ID = /^\d{4}-\d{2}-\d{2}-session-/;
|
|
97
122
|
if (LOOKS_LIKE_SESSION_ID.test(sessionId)) {
|
|
98
|
-
const sessionDir =
|
|
123
|
+
const sessionDir = runtimeRoot(options.projectRoot).join(guardRuntimeSegment(sessionId, 'session id'));
|
|
99
124
|
if (!(await isDirectory(sessionDir))) {
|
|
100
125
|
const canonicalSid = getSessionIdCanonical(options.projectRoot);
|
|
101
126
|
const hint = canonicalSid !== null
|
|
@@ -105,7 +130,7 @@ export async function createRequestArtifact(options) {
|
|
|
105
130
|
}
|
|
106
131
|
}
|
|
107
132
|
// Build numbered path under the session dir (canonical post-F3 home).
|
|
108
|
-
const requestsDir =
|
|
133
|
+
const requestsDir = requestArtifactRequestsDir(options.projectRoot, sessionId, options.role);
|
|
109
134
|
// Check if a file with this requestId already exists (regardless of number prefix)
|
|
110
135
|
if (await isDirectory(requestsDir)) {
|
|
111
136
|
const existingFiles = await listMarkdownFiles(requestsDir);
|
|
@@ -133,7 +158,7 @@ export async function createRequestArtifact(options) {
|
|
|
133
158
|
// Slice 2026-06-29-change-id-root-removal: scopeDir is the
|
|
134
159
|
// session-axis dir (`.peaks/_runtime/<sid>/`). Pre-resolved here
|
|
135
160
|
// so dry-run output reports the canonical scope location.
|
|
136
|
-
const scopeDir =
|
|
161
|
+
const scopeDir = runtimeRoot(options.projectRoot).join(guardRuntimeSegment(sessionId, 'session id'));
|
|
137
162
|
return {
|
|
138
163
|
role: options.role,
|
|
139
164
|
requestId: options.requestId,
|
|
@@ -150,7 +175,7 @@ export async function createRequestArtifact(options) {
|
|
|
150
175
|
// Create QA initiated marker so rd:qa-handoff gate can verify QA was invoked.
|
|
151
176
|
// The marker lives under the SESSION dir (canonical post-F3 home).
|
|
152
177
|
if (options.role === 'qa') {
|
|
153
|
-
const qaDir =
|
|
178
|
+
const qaDir = runtimeRoot(options.projectRoot).join(guardRuntimeSegment(sessionId, 'session id'), guardRuntimeSegment('qa', 'role'));
|
|
154
179
|
const initiatedPath = join(qaDir, '.initiated');
|
|
155
180
|
if (!existsSync(initiatedPath)) {
|
|
156
181
|
await mkdir(qaDir, { recursive: true });
|
|
@@ -164,7 +189,7 @@ export async function createRequestArtifact(options) {
|
|
|
164
189
|
path,
|
|
165
190
|
content,
|
|
166
191
|
applied: true,
|
|
167
|
-
scopeDir:
|
|
192
|
+
scopeDir: runtimeRoot(options.projectRoot).join(guardRuntimeSegment(sessionId, 'session id')),
|
|
168
193
|
...(options.callerId !== undefined ? { callerId: options.callerId } : {})
|
|
169
194
|
};
|
|
170
195
|
}
|
|
@@ -202,31 +227,27 @@ function extractMetadata(markdown) {
|
|
|
202
227
|
base.sessionId = sessionId;
|
|
203
228
|
return base;
|
|
204
229
|
}
|
|
205
|
-
async function readSummary(
|
|
206
|
-
const path = join(
|
|
230
|
+
async function readSummary(dir, role, fileName, sessionId) {
|
|
231
|
+
const path = join(dir, fileName);
|
|
207
232
|
const body = await readFile(path, 'utf8');
|
|
208
233
|
const { state, createdAt, requestType, sessionId: bodySessionId } = extractMetadata(body);
|
|
209
234
|
// Strip numbered prefix (e.g., "001-requestId.md" -> "requestId")
|
|
210
235
|
// Only strip 3-digit zero-padded prefixes (our incrementing number format)
|
|
211
236
|
const requestId = fileName.replace(/^0\d{2}-/, '').replace(/\.md$/, '');
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
// the
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
// `.peaks/_runtime/_runtime/<sid>/...` instead of the canonical
|
|
219
|
-
// `.peaks/_runtime/<sid>/metrics/...`. `writerSessionId` falls back
|
|
220
|
-
// to the parsed body session line (or the bare sid) — same intent.
|
|
221
|
-
const bareSessionId = sessionId.replace(/^_runtime[\\/]/, '');
|
|
237
|
+
// Repair R5: this used to take the *scope* path fragment (`_runtime/<sid>`)
|
|
238
|
+
// and re-join it onto the project root, then strip the prefix back off to
|
|
239
|
+
// recover the bare session id. The round-trip was the escape's carrier — an
|
|
240
|
+
// unsafe id rode it into `path` unguarded. The directory now arrives already
|
|
241
|
+
// resolved and already guarded (`requestArtifactRequestsDir`), and the bare
|
|
242
|
+
// session id arrives as itself, so neither is re-derived here.
|
|
222
243
|
const summary = {
|
|
223
244
|
role,
|
|
224
|
-
sessionId
|
|
245
|
+
sessionId,
|
|
225
246
|
requestId,
|
|
226
247
|
path,
|
|
227
248
|
state,
|
|
228
249
|
requestType,
|
|
229
|
-
writerSessionId: bodySessionId ??
|
|
250
|
+
writerSessionId: bodySessionId ?? sessionId
|
|
230
251
|
};
|
|
231
252
|
if (createdAt !== undefined) {
|
|
232
253
|
summary.createdAt = createdAt;
|
|
@@ -254,26 +275,28 @@ export async function listRequestArtifacts(options) {
|
|
|
254
275
|
// scanned. The user has forbidden the `.peaks/_runtime/<id>/` root layout —
|
|
255
276
|
// the CLI guarantees no such dirs are created. See
|
|
256
277
|
// `.peaks/memory/2026-06-21-peaks-request-session-id-leaks-into-change-id.md`.
|
|
278
|
+
// Repair R5: `scopes` holds bare session ids. It used to hold the joined
|
|
279
|
+
// fragment `_runtime/<sid>` so that `readSummary` could re-join it to the
|
|
280
|
+
// project root; the directory is built once, below, by the guard.
|
|
257
281
|
const scopes = [];
|
|
258
282
|
if (options.sessionId !== undefined) {
|
|
259
|
-
scopes.push(
|
|
283
|
+
scopes.push(options.sessionId);
|
|
260
284
|
}
|
|
261
285
|
else {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
286
|
+
// Read-only enumeration of the root itself, so `dir()` and not `join()`.
|
|
287
|
+
const runtimeDir = runtimeRoot(options.projectRoot).dir();
|
|
288
|
+
if (await isDirectory(runtimeDir)) {
|
|
289
|
+
scopes.push(...(await listDirectories(runtimeDir)));
|
|
267
290
|
}
|
|
268
291
|
}
|
|
269
292
|
const roles = options.role !== undefined ? [options.role] : Array.from(VALID_ROLES);
|
|
270
293
|
const summaries = [];
|
|
271
294
|
for (const scope of scopes) {
|
|
272
295
|
for (const role of roles) {
|
|
273
|
-
const dir =
|
|
296
|
+
const dir = requestArtifactRequestsDir(options.projectRoot, scope, role);
|
|
274
297
|
const fileNames = await listMarkdownFiles(dir);
|
|
275
298
|
for (const fileName of fileNames) {
|
|
276
|
-
summaries.push(await readSummary(
|
|
299
|
+
summaries.push(await readSummary(dir, role, fileName, scope));
|
|
277
300
|
}
|
|
278
301
|
}
|
|
279
302
|
}
|
|
@@ -311,24 +334,22 @@ export async function showRequestArtifact(options) {
|
|
|
311
334
|
// `.peaks/_runtime/<sid>/<role>/requests/` legacy home is no longer
|
|
312
335
|
// scanned. The user has forbidden the `.peaks/_runtime/<id>/` root layout.
|
|
313
336
|
if (options.sessionId !== undefined) {
|
|
314
|
-
const dir =
|
|
315
|
-
const scope = join('_runtime', options.sessionId);
|
|
337
|
+
const dir = requestArtifactRequestsDir(options.projectRoot, options.sessionId, options.role);
|
|
316
338
|
const found = await findFileInDir(dir);
|
|
317
339
|
if (found === null) {
|
|
318
340
|
return null;
|
|
319
341
|
}
|
|
320
|
-
return await readRequestArtifact(options.
|
|
342
|
+
return await readRequestArtifact(dir, options.role, found, options.sessionId);
|
|
321
343
|
}
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
if (!(await isDirectory(runtimeRoot))) {
|
|
344
|
+
const runtimeDir = runtimeRoot(options.projectRoot).dir();
|
|
345
|
+
if (!(await isDirectory(runtimeDir))) {
|
|
325
346
|
return null;
|
|
326
347
|
}
|
|
327
|
-
for (const sid of await listDirectories(
|
|
328
|
-
const dir =
|
|
348
|
+
for (const sid of await listDirectories(runtimeDir)) {
|
|
349
|
+
const dir = requestArtifactRequestsDir(options.projectRoot, sid, options.role);
|
|
329
350
|
const found = await findFileInDir(dir);
|
|
330
351
|
if (found !== null) {
|
|
331
|
-
return await readRequestArtifact(
|
|
352
|
+
return await readRequestArtifact(dir, options.role, found, sid);
|
|
332
353
|
}
|
|
333
354
|
}
|
|
334
355
|
return null;
|
|
@@ -336,8 +357,8 @@ export async function showRequestArtifact(options) {
|
|
|
336
357
|
/** Read the summary + content for a found request file; treat a read
|
|
337
358
|
* error on the content as "not found" so the caller can fall through
|
|
338
359
|
* to the next candidate (the on-disk file may be partially written). */
|
|
339
|
-
async function readRequestArtifact(
|
|
340
|
-
const summary = await readSummary(
|
|
360
|
+
async function readRequestArtifact(dir, role, found, sessionId) {
|
|
361
|
+
const summary = await readSummary(dir, role, found.fileName, sessionId);
|
|
341
362
|
try {
|
|
342
363
|
const content = await readFile(found.path, 'utf8');
|
|
343
364
|
return { ...summary, content };
|
|
@@ -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;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enforcer liveness — A9 of `docs/diagnosis-2026-09-15-peaks-loop-state.md`.
|
|
3
|
+
*
|
|
4
|
+
* `cli-backed` used to mean "the `enforcerRef` path exists on disk"
|
|
5
|
+
* (`backing-detector.ts`). It did not mean "the enforcer runs". Ten catalog
|
|
6
|
+
* enforcerRefs name a file no module imports — `lint-rd-handoff-coverage.ts`
|
|
7
|
+
* among them — so the audit reported 108/152 red lines as CLI-enforced while
|
|
8
|
+
* a quarter of those had no caller at all.
|
|
9
|
+
*
|
|
10
|
+
* This module answers the narrower, checkable question: is the enforcer
|
|
11
|
+
* module imported by some file OUTSIDE the enforcers directory? A relative
|
|
12
|
+
* import is cheap proof that a call site exists — production code cannot
|
|
13
|
+
* invoke the module without one.
|
|
14
|
+
*
|
|
15
|
+
* Deliberate limits, stated rather than hidden:
|
|
16
|
+
* - An import is not a call. A module can be imported and never invoked.
|
|
17
|
+
* This check is necessary, not sufficient: it eliminates the "no caller
|
|
18
|
+
* exists at all" class and nothing beyond it.
|
|
19
|
+
* - Imports written inside `src/…/audit/enforcers/` do not count. A dead
|
|
20
|
+
* enforcer that imports another dead enforcer must not resurrect it.
|
|
21
|
+
* - A project with no `src/` tree yields `unknown: true`, never an empty
|
|
22
|
+
* live-set: peaks-loop audits consumer projects, and "cannot tell" must
|
|
23
|
+
* not read as "dead".
|
|
24
|
+
*/
|
|
25
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
26
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
27
|
+
const SOURCE_FILE = /\.(ts|tsx|mts|cts)$/;
|
|
28
|
+
const ENFORCER_DIR_FRAGMENT = '/services/audit/enforcers/';
|
|
29
|
+
/** Matches `from './x.js'`, `import('./x.js')` and `export … from './x.js'`. */
|
|
30
|
+
const RELATIVE_JS_IMPORT = /(?:from|import)\s*\(?\s*['"](\.\.?\/[^'"]+\.js)['"]/g;
|
|
31
|
+
function toPosix(value) {
|
|
32
|
+
return value.split('\\').join('/');
|
|
33
|
+
}
|
|
34
|
+
function* walkSourceFiles(root, warnings) {
|
|
35
|
+
const stack = [root];
|
|
36
|
+
while (stack.length > 0) {
|
|
37
|
+
const dir = stack.pop();
|
|
38
|
+
if (dir === undefined)
|
|
39
|
+
continue;
|
|
40
|
+
let dirents;
|
|
41
|
+
try {
|
|
42
|
+
dirents = readdirSync(dir, { withFileTypes: true });
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
warnings.push(`enforcer-liveness: cannot read ${toPosix(dir)} (${String(error)})`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const dirent of dirents) {
|
|
49
|
+
const full = join(dir, dirent.name);
|
|
50
|
+
if (dirent.isDirectory()) {
|
|
51
|
+
if (dirent.name === 'node_modules' || dirent.name === 'dist' || dirent.name.startsWith('.'))
|
|
52
|
+
continue;
|
|
53
|
+
stack.push(full);
|
|
54
|
+
}
|
|
55
|
+
else if (dirent.isFile() && SOURCE_FILE.test(dirent.name)) {
|
|
56
|
+
yield full;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Scan `projectRoot/src` for relative imports that name one of `enforcerRefs`.
|
|
63
|
+
* Returns the subset of refs with a call site outside the enforcers directory.
|
|
64
|
+
*/
|
|
65
|
+
export function computeLiveEnforcers(projectRoot, enforcerRefs) {
|
|
66
|
+
const srcRoot = join(projectRoot, 'src');
|
|
67
|
+
const warnings = [];
|
|
68
|
+
const live = new Set();
|
|
69
|
+
const wanted = new Set(enforcerRefs);
|
|
70
|
+
let sawAnySource = false;
|
|
71
|
+
for (const absFile of walkSourceFiles(srcRoot, warnings)) {
|
|
72
|
+
sawAnySource = true;
|
|
73
|
+
const relFile = toPosix(relative(projectRoot, absFile));
|
|
74
|
+
// An import written inside the enforcers directory proves nothing.
|
|
75
|
+
if (relFile.includes(ENFORCER_DIR_FRAGMENT))
|
|
76
|
+
continue;
|
|
77
|
+
let source;
|
|
78
|
+
try {
|
|
79
|
+
source = readFileSync(absFile, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
warnings.push(`enforcer-liveness: cannot read ${relFile} (${String(error)})`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
RELATIVE_JS_IMPORT.lastIndex = 0;
|
|
86
|
+
let match = RELATIVE_JS_IMPORT.exec(source);
|
|
87
|
+
while (match !== null) {
|
|
88
|
+
const specifier = match[1] ?? '';
|
|
89
|
+
const resolved = toPosix(relative(projectRoot, resolve(dirname(absFile), specifier)));
|
|
90
|
+
const asTs = resolved.replace(/\.js$/, '.ts');
|
|
91
|
+
if (wanted.has(asTs))
|
|
92
|
+
live.add(asTs);
|
|
93
|
+
match = RELATIVE_JS_IMPORT.exec(source);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!sawAnySource) {
|
|
97
|
+
return { live, unknown: true, warnings };
|
|
98
|
+
}
|
|
99
|
+
return { live, unknown: false, warnings };
|
|
100
|
+
}
|
|
@@ -64,7 +64,20 @@ export function resolveActiveSkillForCaller(projectRoot, opts) {
|
|
|
64
64
|
// single skill per resolution. When the lease dir is empty (e.g.
|
|
65
65
|
// ad-hoc / pre-migration projects) we fall through to the legacy
|
|
66
66
|
// walk below.
|
|
67
|
-
|
|
67
|
+
// `getSessionDir` refuses an unsafe session id by throwing (slice
|
|
68
|
+
// 2026-09-14-getsessiondir-guard). This function's contract is the
|
|
69
|
+
// resolution order's "graceful degradation — never throws", so an unsafe
|
|
70
|
+
// id degrades to the same `source: 'none'` shape an absent session dir
|
|
71
|
+
// produces, exactly as it did before that guard existed. Without this,
|
|
72
|
+
// the throw escapes to the nearest caller `catch` — for `hook handle`
|
|
73
|
+
// that catch is a fail-open that skips the SOP gate.
|
|
74
|
+
let sessionDir;
|
|
75
|
+
try {
|
|
76
|
+
sessionDir = getSessionDir(projectRoot, sessionId);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { skill: null, callerId: null, sessionId: null, mode: null, source: 'none' };
|
|
80
|
+
}
|
|
68
81
|
if (!existsSync(sessionDir)) {
|
|
69
82
|
return { skill: null, callerId: null, sessionId, mode: null, source: 'none' };
|
|
70
83
|
}
|