peaks-loop 4.0.6 → 4.0.7
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 +290 -0
- package/dist/cli/commands/code-job-shape-commands.js +11 -8
- package/dist/cli/commands/code-mode-gate-commands.js +43 -1
- package/dist/cli/commands/code-runtime-commands.js +11 -6
- package/dist/cli/commands/core/standards-command.js +50 -5
- package/dist/cli/commands/dispatch-commands.js +39 -3
- package/dist/cli/commands/hooks-commands.js +26 -9
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/sub-agent-exec-command.d.ts +25 -0
- package/dist/cli/commands/sub-agent-exec-command.js +132 -0
- package/dist/cli/commands/sub-agent-shared.d.ts +11 -0
- package/dist/services/audit/backing-detector.d.ts +19 -2
- package/dist/services/audit/backing-detector.js +66 -7
- package/dist/services/audit/enforcers/lint-audit-regression.js +10 -1
- package/dist/services/context/build-dispatch-system-prompt.js +18 -0
- package/dist/services/dispatch/test-tool-detection.d.ts +14 -0
- package/dist/services/dispatch/test-tool-detection.js +22 -0
- package/dist/services/scan/archetype-service.js +52 -2
- package/dist/services/session/index.d.ts +1 -1
- package/dist/services/session/index.js +1 -1
- package/dist/services/session/session-manager.d.ts +29 -0
- package/dist/services/session/session-manager.js +33 -0
- package/dist/services/skills/hooks-settings-service.d.ts +41 -0
- package/dist/services/skills/hooks-settings-service.js +61 -5
- package/dist/services/slice/slice-check-service.js +59 -0
- package/dist/services/standards/find-skills-integration.d.ts +59 -0
- package/dist/services/standards/find-skills-integration.js +225 -0
- package/dist/services/standards/project-standards-service.js +44 -0
- package/package.json +4 -4
- package/skills/peaks-code/SKILL.md +43 -5
|
@@ -38,12 +38,23 @@ function resolveIdeForCommand(options, projectRoot) {
|
|
|
38
38
|
* the install shape so the JSON envelope doesn't claim a hook the
|
|
39
39
|
* service did not write.
|
|
40
40
|
*/
|
|
41
|
-
function listExpectedEntriesForIde(ide,
|
|
41
|
+
function listExpectedEntriesForIde(ide, options = {}) {
|
|
42
42
|
const adapter = getAdapter(ide);
|
|
43
|
+
const out = [];
|
|
43
44
|
if (ide === 'trae') {
|
|
44
|
-
|
|
45
|
+
out.push({ matcher: adapter.toolMatcher, sentinel: 'peaks hook handle' });
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
+
else {
|
|
48
|
+
out.push({ matcher: adapter.toolMatcher, sentinel: 'peaks gate enforce' });
|
|
49
|
+
}
|
|
50
|
+
// Slice 4.0.7-PR-meta-5: opt-in meta-1 / meta-3 hook entries.
|
|
51
|
+
if (options.withEditEnforcement === true) {
|
|
52
|
+
out.push({ matcher: 'Edit|Write|MultiEdit|NotebookEdit', sentinel: 'peaks edit enforcement' });
|
|
53
|
+
}
|
|
54
|
+
if (options.withScopeCounter === true) {
|
|
55
|
+
out.push({ matcher: 'Edit|Write|MultiEdit|NotebookEdit', sentinel: 'peaks scope counter' });
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
47
58
|
}
|
|
48
59
|
/**
|
|
49
60
|
* Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): copy the
|
|
@@ -104,20 +115,24 @@ export function registerHooksCommands(program, io) {
|
|
|
104
115
|
.description("Manage the Peaks-managed hook entry in the adapter's settings.json (default: .claude/settings.json for Claude, .trae/settings.json for Trae). Slice #014: the only installed entry is the gate-enforce hook (SOP gate). The legacy progress-start hook (auto-spawn sub-agent progress terminal) is no longer installed — sub-agent progress is now surfaced via the dispatch + heartbeat flow (`peaks sub-agent dispatch` / `peaks sub-agent heartbeat`). The IDE is auto-detected from env / cwd; override with --ide <id>.");
|
|
105
116
|
addJsonOption(hooks
|
|
106
117
|
.command('install')
|
|
107
|
-
.description(`Install the peaks-managed gate-enforce hook entry into the adapter's settings.json. Slice #014: only the gate-enforce entry is installed; the legacy progress-start entry is no longer installed. Idempotent: re-runs are no-ops. Project scope by default.`)
|
|
118
|
+
.description(`Install the peaks-managed gate-enforce hook entry into the adapter's settings.json. Slice #014: only the gate-enforce entry is installed by default; the legacy progress-start entry is no longer installed. Idempotent: re-runs are no-ops. Project scope by default. Slice 4.0.7-PR-meta-5: --with-edit-enforcement and --with-scope-counter opt-in flags add the corresponding PreToolUse hook entries (off by default to preserve the existing install behavior).`)
|
|
108
119
|
.option('--global', 'install into the user-level ~/.claude/settings.json instead of the project')
|
|
109
120
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
110
121
|
.option('--ide <id>', "target adapter id (claude-code | trae); default: auto-detect from env/cwd")
|
|
111
122
|
.option('--dry-run', 'show what would change without writing')
|
|
112
|
-
.option('--no-progress', 'skip the progress-start PreToolUse hook entry; install ONLY the gate-enforce entry')
|
|
123
|
+
.option('--no-progress', 'skip the progress-start PreToolUse hook entry; install ONLY the gate-enforce entry')
|
|
124
|
+
.option('--with-edit-enforcement', 'Slice 4.0.7-PR-meta-5: opt-in. Add the Edit-enforcement PreToolUse hook entry (pre-tool-edit-enforcement.sh). Default false to preserve the existing install behavior. Once installed, the hook blocks src/** edits without an RD artifact on disk.')
|
|
125
|
+
.option('--with-scope-counter', 'Slice 4.0.7-PR-meta-5: opt-in. Add the scope-counter PreToolUse hook entry (pre-tool-scope-counter.sh). Default false. The hook counts cumulative application-source edits and emits a fail-LOUD reflection reminder at thresholds 5, 10, 20.')).action((options) => {
|
|
113
126
|
const scope = resolveScope(options);
|
|
114
127
|
const projectRoot = resolveProjectRoot(scope, options.project);
|
|
115
128
|
const ide = resolveIdeForCommand(options, projectRoot);
|
|
116
129
|
const skipProgress = options.progress === false;
|
|
130
|
+
const withEditEnforcement = options.withEditEnforcement === true;
|
|
131
|
+
const withScopeCounter = options.withScopeCounter === true;
|
|
117
132
|
try {
|
|
118
133
|
if (options.dryRun === true) {
|
|
119
134
|
const plan = planHookInstall(scope, projectRoot, { ide, skipProgress });
|
|
120
|
-
const dryRunEntries = listExpectedEntriesForIde(ide, skipProgress);
|
|
135
|
+
const dryRunEntries = listExpectedEntriesForIde(ide, { skipProgress, withEditEnforcement, withScopeCounter });
|
|
121
136
|
const bridgeCopy = scope === 'global'
|
|
122
137
|
? copyBridgeHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
|
|
123
138
|
: { copied: false, source: '', target: '' };
|
|
@@ -145,14 +160,16 @@ export function registerHooksCommands(program, io) {
|
|
|
145
160
|
]), options.json);
|
|
146
161
|
return;
|
|
147
162
|
}
|
|
148
|
-
const result = applyHookInstall(scope, projectRoot, { ide, skipProgress });
|
|
163
|
+
const result = applyHookInstall(scope, projectRoot, { ide, skipProgress, withEditEnforcement, withScopeCounter });
|
|
149
164
|
// Slice #3: build the per-IDE entries summary from the actual installed
|
|
150
165
|
// entries, not the slice #1 PEAKS_HOOK_ENTRIES constant (which is the
|
|
151
166
|
// claude-code default). The user's JSON envelope must reflect the IDE
|
|
152
167
|
// they targeted. Slice #014: the install only emits the gate-enforce
|
|
153
168
|
// entry; the summary mirrors the install shape, NOT a hardcoded
|
|
154
|
-
// expected list.
|
|
155
|
-
|
|
169
|
+
// expected list. Slice 4.0.7-PR-meta-5: the summary now also
|
|
170
|
+
// includes the opt-in Edit-enforcement + scope-counter entries
|
|
171
|
+
// when the user passed --with-edit-enforcement / --with-scope-counter.
|
|
172
|
+
const installedEntries = listExpectedEntriesForIde(ide, { skipProgress, withEditEnforcement, withScopeCounter });
|
|
156
173
|
// Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): when
|
|
157
174
|
// the install targets global scope, also copy the superpowers-bridge
|
|
158
175
|
// hook script from src/services/hooks/ to the user-global hooks
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerDispatchCommand } from './dispatch-commands.js';
|
|
2
|
+
import { registerSubAgentExecCommand } from './sub-agent-exec-command.js';
|
|
2
3
|
import { registerHeartbeatCommand } from './heartbeat-commands.js';
|
|
3
4
|
import { registerShareCommand, registerSharedReadCommand, registerAwaitCommand, registerFinalizeCommand } from './share-commands.js';
|
|
4
5
|
// Re-export `validateRole` for backward compat — the integration test
|
|
@@ -12,6 +13,7 @@ export function registerSubAgentCommands(program, io) {
|
|
|
12
13
|
'These commands are the primitives that peaks-code / peaks-rd / ' +
|
|
13
14
|
'peaks-qa SKILL.md compose. Users do not invoke this directly.');
|
|
14
15
|
registerDispatchCommand(subAgent, io);
|
|
16
|
+
registerSubAgentExecCommand(subAgent, io); // 4.0.7-PR-meta-6
|
|
15
17
|
registerHeartbeatCommand(subAgent, io);
|
|
16
18
|
registerShareCommand(subAgent, io);
|
|
17
19
|
registerSharedReadCommand(subAgent, io);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shape of a dispatch record on disk (slice 2026-06-07 +
|
|
5
|
+
* dispatch-record metadata). We only read the fields `exec` needs
|
|
6
|
+
* to re-emit the toolCall envelope; everything else (heartbeat,
|
|
7
|
+
* artifacts, lease, isolation) is preserved by the dispatch record
|
|
8
|
+
* itself and surfaced in the re-emitted envelope.
|
|
9
|
+
*/
|
|
10
|
+
interface DispatchRecordToolCall {
|
|
11
|
+
name: string;
|
|
12
|
+
args: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
interface DispatchRecord {
|
|
15
|
+
toolCall?: DispatchRecordToolCall;
|
|
16
|
+
promptSize?: number;
|
|
17
|
+
originalPromptSize?: number;
|
|
18
|
+
batchId?: string;
|
|
19
|
+
rid?: string;
|
|
20
|
+
role?: string;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function registerSubAgentExecCommand(parent: Command, io: ProgramIO): void;
|
|
24
|
+
export declare function readDispatchRecordForExec(dispatchRecordPath: string): DispatchRecord;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks sub-agent exec` — slice 4.0.7-PR-meta-6.
|
|
3
|
+
*
|
|
4
|
+
* Companion to `peaks sub-agent dispatch` (slice 2026-06-07). The
|
|
5
|
+
* dispatch CLI is dry-run by design (per skill-first / CLI-auxiliary
|
|
6
|
+
* red line PB-4): it returns a `toolCall` envelope that the LLM
|
|
7
|
+
* extracts args from and re-executes via the Agent tool. The 4.0.7
|
|
8
|
+
* ice-cola dogfood pass surfaced the friction: the LLM often
|
|
9
|
+
* skipped the dispatch altogether instead of paying the parse
|
|
10
|
+
* cost, and routed `Edit` calls directly. PR-meta-4 lowered the
|
|
11
|
+
* friction by adding `--emit-bash-script`, which prints a
|
|
12
|
+
* ready-to-exec bash script alongside the JSON envelope. PR-meta-6
|
|
13
|
+
* ships the actual surface the script invokes: this `exec`
|
|
14
|
+
* sub-command reads the dispatch record from disk (the same
|
|
15
|
+
* record the dispatch CLI wrote) and re-emits the canonical
|
|
16
|
+
* toolCall envelope so the LLM does not have to hand-parse the
|
|
17
|
+
* JSON to retry a dispatch.
|
|
18
|
+
*
|
|
19
|
+
* Architecture (PR-meta-6):
|
|
20
|
+
* 1. The user (or the LLM via Bash tool) runs the script
|
|
21
|
+
* emitted by `peaks sub-agent dispatch --emit-bash-script`.
|
|
22
|
+
* 2. The script invokes `peaks sub-agent exec --session-id ...
|
|
23
|
+
* --request-id ... --role ... --batch-id ... --dispatch-record ...
|
|
24
|
+
* --project ...`.
|
|
25
|
+
* 3. `exec` reads the dispatch record from disk, re-emits the
|
|
26
|
+
* same toolCall envelope, and prints a single next-action
|
|
27
|
+
* line: "Run this Task tool call to dispatch."
|
|
28
|
+
*
|
|
29
|
+
* The dry-run architecture is preserved: the LLM still has the
|
|
30
|
+
* final say on whether to actually spawn the Task tool. The
|
|
31
|
+
* script + exec pair is a transparent retry path, not a side-step.
|
|
32
|
+
*/
|
|
33
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
34
|
+
import { dirname } from 'node:path';
|
|
35
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
36
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
37
|
+
function failExec(command, message, options, hint) {
|
|
38
|
+
printResult(undefined, // io is plumbed below
|
|
39
|
+
fail(command, 'SUB_AGENT_EXEC_FAILED', message, null, [hint]), options.json);
|
|
40
|
+
// The above `printResult` expects io — the real call site passes
|
|
41
|
+
// io explicitly. This helper is only used for type-level
|
|
42
|
+
// documentation; the actual printing is inline in the action.
|
|
43
|
+
throw new Error(message);
|
|
44
|
+
}
|
|
45
|
+
export function registerSubAgentExecCommand(parent, io) {
|
|
46
|
+
addJsonOption(parent
|
|
47
|
+
.command('exec')
|
|
48
|
+
.description('Slice 4.0.7-PR-meta-6. Re-emit a stored dispatch envelope. Companion to `peaks sub-agent dispatch --emit-bash-script`. Reads the dispatch record from `--dispatch-record` and prints the canonical toolCall envelope so the LLM does not have to hand-parse the JSON to retry. Dry-run: the LLM still owns the actual Agent tool invocation.')
|
|
49
|
+
.requiredOption('--session-id <sid>', 'session id the dispatch was created in')
|
|
50
|
+
.requiredOption('--request-id <rid>', 'request id the dispatch was created for')
|
|
51
|
+
.requiredOption('--role <role>', 'sub-agent role (rd | qa | ui | sc | txt | qa-business | qa-business-api)')
|
|
52
|
+
.requiredOption('--batch-id <uuid>', 'batch id the dispatch was created in')
|
|
53
|
+
.requiredOption('--dispatch-record <path>', 'path to the dispatch record JSON (the same file the dispatch CLI wrote under .peaks/_sub_agents/<sid>/)')
|
|
54
|
+
.requiredOption('--project <path>', 'target project root (echoed back in the envelope; not used to read files)')).action((options) => {
|
|
55
|
+
if (options.dispatchRecord === undefined || !existsSync(options.dispatchRecord)) {
|
|
56
|
+
printResult(io, fail('sub-agent.exec', 'DISPATCH_RECORD_MISSING', `Dispatch record not found at ${options.dispatchRecord ?? '(unset)'}`, null, [
|
|
57
|
+
'Re-run `peaks sub-agent dispatch <role> --prompt <text> --request-id <rid> --json` to create the record.',
|
|
58
|
+
'Then pass the emitted `dispatchRecordPath` to `peaks sub-agent exec --dispatch-record <path>`.'
|
|
59
|
+
]), options.json);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let record;
|
|
64
|
+
try {
|
|
65
|
+
record = JSON.parse(readFileSync(options.dispatchRecord, 'utf8'));
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
printResult(io, fail('sub-agent.exec', 'DISPATCH_RECORD_INVALID', `Failed to parse dispatch record: ${err instanceof Error ? err.message : String(err)}`, { dispatchRecord: options.dispatchRecord }, [
|
|
69
|
+
'The dispatch record may be truncated. Re-run `peaks sub-agent dispatch` to create a fresh record.'
|
|
70
|
+
]), options.json);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (record.toolCall === undefined) {
|
|
75
|
+
printResult(io, fail('sub-agent.exec', 'DISPATCH_RECORD_NO_TOOL_CALL', 'Dispatch record has no `toolCall` field. The record may be from a pre-toolCall era; re-run `peaks sub-agent dispatch` to create a fresh record.', { dispatchRecord: options.dispatchRecord }, [
|
|
76
|
+
'Re-run `peaks sub-agent dispatch` to create a fresh record with a `toolCall` envelope.'
|
|
77
|
+
]), options.json);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Sanity check: the record's sessionId / rid / batchId / role
|
|
82
|
+
// should match the CLI flags (if the caller passed them). This
|
|
83
|
+
// is defensive: a mismatch usually means the operator is
|
|
84
|
+
// running the wrong script. We surface a warning but still
|
|
85
|
+
// re-emit the envelope (do not block — the dispatch record
|
|
86
|
+
// is the canonical source of truth).
|
|
87
|
+
const warnings = [];
|
|
88
|
+
if (options.sessionId !== undefined && record.sessionId !== undefined && record.sessionId !== options.sessionId) {
|
|
89
|
+
warnings.push(`session-id mismatch: record=${record.sessionId} vs --session-id=${options.sessionId}; using the record's value as the canonical source.`);
|
|
90
|
+
}
|
|
91
|
+
if (options.requestId !== undefined && record.rid !== undefined && record.rid !== options.requestId) {
|
|
92
|
+
warnings.push(`request-id mismatch: record=${record.rid} vs --request-id=${options.requestId}; using the record's value.`);
|
|
93
|
+
}
|
|
94
|
+
if (options.batchId !== undefined && record.batchId !== undefined && record.batchId !== options.batchId) {
|
|
95
|
+
warnings.push(`batch-id mismatch: record=${record.batchId} vs --batch-id=${options.batchId}; using the record's value.`);
|
|
96
|
+
}
|
|
97
|
+
if (options.role !== undefined && record.role !== undefined && record.role !== options.role) {
|
|
98
|
+
warnings.push(`role mismatch: record=${record.role} vs --role=${options.role}; using the record's value.`);
|
|
99
|
+
}
|
|
100
|
+
const nextActions = [
|
|
101
|
+
'This is a dry-run re-emit. Run the Task tool with the printed `toolCall.args` to actually dispatch the sub-agent.',
|
|
102
|
+
`After dispatching, the sub-agent should call \`peaks sub-agent heartbeat --record ${options.dispatchRecord}\` periodically.`
|
|
103
|
+
];
|
|
104
|
+
// Re-emit the same envelope shape as `peaks sub-agent dispatch`
|
|
105
|
+
// so consumers (LLM or downstream tooling) can treat both CLIs
|
|
106
|
+
// as the same surface. Slice 4.0.7-PR-meta-6: the envelope
|
|
107
|
+
// version is bumped to 2.4.0 to mark the new `viaExec` field
|
|
108
|
+
// (true here; false in dispatch CLI output).
|
|
109
|
+
printResult(io, ok('sub-agent.exec', {
|
|
110
|
+
envelopeVersion: '2.4.0',
|
|
111
|
+
viaExec: true,
|
|
112
|
+
sourceDispatchRecord: options.dispatchRecord,
|
|
113
|
+
role: record.role ?? options.role,
|
|
114
|
+
sessionId: record.sessionId ?? options.sessionId,
|
|
115
|
+
rid: record.rid ?? options.requestId,
|
|
116
|
+
batchId: record.batchId ?? options.batchId,
|
|
117
|
+
promptSize: record.promptSize ?? 0,
|
|
118
|
+
originalPromptSize: record.originalPromptSize ?? 0,
|
|
119
|
+
toolCall: record.toolCall
|
|
120
|
+
}, warnings, nextActions), options.json);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Helper export so tests can drive the underlying logic without
|
|
124
|
+
// invoking the action handler. The action handler is the
|
|
125
|
+
// integration path; the helper is the unit path.
|
|
126
|
+
export function readDispatchRecordForExec(dispatchRecordPath) {
|
|
127
|
+
if (!existsSync(dispatchRecordPath)) {
|
|
128
|
+
throw new Error(`DISPATCH_RECORD_MISSING: ${dispatchRecordPath}`);
|
|
129
|
+
}
|
|
130
|
+
return JSON.parse(readFileSync(dispatchRecordPath, 'utf8'));
|
|
131
|
+
}
|
|
132
|
+
void dirname;
|
|
@@ -40,6 +40,17 @@ export type DispatchOptions = {
|
|
|
40
40
|
* worktree without needing a separate `peaks worktree auth grant`.
|
|
41
41
|
*/
|
|
42
42
|
isolation?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Slice 4.0.7-PR-meta-4: when true, the dispatch CLI also emits a
|
|
45
|
+
* ready-to-exec bash script to stdout (in addition to the JSON
|
|
46
|
+
* envelope) so the LLM can run the script via the Bash tool
|
|
47
|
+
* instead of having to parse toolCall args and re-execute them
|
|
48
|
+
* via the Agent tool. Lowers the dispatch friction documented
|
|
49
|
+
* in `.peaks/memory/2026-07-28-sub-agent-visibility-issue.md`.
|
|
50
|
+
* The dry-run architecture is preserved: the script invokes
|
|
51
|
+
* the canonical `peaks sub-agent exec` surface, not a side-step.
|
|
52
|
+
*/
|
|
53
|
+
emitBashScript?: boolean;
|
|
43
54
|
json?: boolean;
|
|
44
55
|
};
|
|
45
56
|
export type HeartbeatOptions = {
|
|
@@ -5,20 +5,37 @@
|
|
|
5
5
|
* handle the post-classification nuance: heuristics for the "partial" tier
|
|
6
6
|
* (a gate exists but the LLM can bypass it) and verification that the
|
|
7
7
|
* enforcer file actually exists on disk.
|
|
8
|
+
*
|
|
9
|
+
* Slice 4.0.7-dogfood-PR-1 (ice-cola surface probe 2026-08-02): the
|
|
10
|
+
* `enforcerRef` paths in the catalog are written relative to the
|
|
11
|
+
* peaks-loop source root (every `enforcerRef` is `src/services/audit/...`
|
|
12
|
+
* and the catalog itself is at `src/services/audit/red-line-catalog.ts`).
|
|
13
|
+
* Resolving them against the audited `--project` root produces false
|
|
14
|
+
* orphans on every downstream project. Fix: resolve against the
|
|
15
|
+
* peaks-loop install root by default, with an explicit
|
|
16
|
+
* `peaksLoopRoot` override for tests.
|
|
8
17
|
*/
|
|
9
18
|
import type { RedLineEntry } from './types.js';
|
|
10
19
|
export interface BackingResult {
|
|
11
20
|
readonly entry: RedLineEntry;
|
|
12
21
|
readonly enforcerExists: boolean;
|
|
13
22
|
}
|
|
23
|
+
export declare function resolvePeaksLoopRoot(): string;
|
|
24
|
+
/** Test-only: clear the cached peaks-loop root. */
|
|
25
|
+
export declare function _resetPeaksLoopRootCache(): void;
|
|
14
26
|
/**
|
|
15
27
|
* Re-classify a single RedLineEntry. Returns a new entry with the
|
|
16
28
|
* `backing` field updated and `enforcerRef` possibly nulled if the
|
|
17
29
|
* referenced file does not exist on disk.
|
|
30
|
+
*
|
|
31
|
+
* `projectRoot` is the audited project (kept for callers that still
|
|
32
|
+
* pass it for context, but no longer used to resolve `enforcerRef`).
|
|
33
|
+
* `peaksLoopRoot` is the peaks-loop source root; defaults to
|
|
34
|
+
* `resolvePeaksLoopRoot()`.
|
|
18
35
|
*/
|
|
19
|
-
export declare function classifyBacking(entry: RedLineEntry, projectRoot: string): BackingResult;
|
|
36
|
+
export declare function classifyBacking(entry: RedLineEntry, projectRoot: string, peaksLoopRoot?: string): BackingResult;
|
|
20
37
|
export interface BackingBatchResult {
|
|
21
38
|
readonly entries: readonly RedLineEntry[];
|
|
22
39
|
readonly warnings: readonly string[];
|
|
23
40
|
}
|
|
24
|
-
export declare function classifyBackingBatch(entries: readonly RedLineEntry[], projectRoot: string): BackingBatchResult;
|
|
41
|
+
export declare function classifyBackingBatch(entries: readonly RedLineEntry[], projectRoot: string, peaksLoopRoot?: string): BackingBatchResult;
|
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
* handle the post-classification nuance: heuristics for the "partial" tier
|
|
6
6
|
* (a gate exists but the LLM can bypass it) and verification that the
|
|
7
7
|
* enforcer file actually exists on disk.
|
|
8
|
+
*
|
|
9
|
+
* Slice 4.0.7-dogfood-PR-1 (ice-cola surface probe 2026-08-02): the
|
|
10
|
+
* `enforcerRef` paths in the catalog are written relative to the
|
|
11
|
+
* peaks-loop source root (every `enforcerRef` is `src/services/audit/...`
|
|
12
|
+
* and the catalog itself is at `src/services/audit/red-line-catalog.ts`).
|
|
13
|
+
* Resolving them against the audited `--project` root produces false
|
|
14
|
+
* orphans on every downstream project. Fix: resolve against the
|
|
15
|
+
* peaks-loop install root by default, with an explicit
|
|
16
|
+
* `peaksLoopRoot` override for tests.
|
|
8
17
|
*/
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
10
|
-
import { resolve } from 'node:path';
|
|
18
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
19
|
+
import { dirname, resolve } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
11
21
|
const PARTIAL_PHRASES = [
|
|
12
22
|
'if llm cooperates',
|
|
13
23
|
'llm-cooperation',
|
|
@@ -21,33 +31,82 @@ function detectPartial(context) {
|
|
|
21
31
|
const lower = context.toLowerCase();
|
|
22
32
|
return PARTIAL_PHRASES.some((phrase) => lower.includes(phrase));
|
|
23
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the peaks-loop source root (where the catalog itself lives).
|
|
36
|
+
* Walks upward from this very file until a `package.json` with
|
|
37
|
+
* `name === 'peaks-loop'` is found. Cached after the first call.
|
|
38
|
+
* Exported so tests can reset the cache between cases.
|
|
39
|
+
*/
|
|
40
|
+
let cachedPeaksLoopRoot = null;
|
|
41
|
+
export function resolvePeaksLoopRoot() {
|
|
42
|
+
if (cachedPeaksLoopRoot !== null)
|
|
43
|
+
return cachedPeaksLoopRoot;
|
|
44
|
+
// This file lives at <root>/src/services/audit/backing-detector.ts.
|
|
45
|
+
// Walk up to find peaks-loop's package.json.
|
|
46
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
let dir = here;
|
|
48
|
+
for (let i = 0; i < 8; i++) {
|
|
49
|
+
const candidate = resolve(dir, 'package.json');
|
|
50
|
+
if (existsSync(candidate)) {
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
53
|
+
if (pkg.name === 'peaks-loop') {
|
|
54
|
+
cachedPeaksLoopRoot = dir;
|
|
55
|
+
return dir;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// malformed package.json; keep walking up
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const parent = dirname(dir);
|
|
63
|
+
if (parent === dir)
|
|
64
|
+
break;
|
|
65
|
+
dir = parent;
|
|
66
|
+
}
|
|
67
|
+
// Fallback: assume 4 levels up from this file (catalog + enforcers
|
|
68
|
+
// share the same depth). Keeps the unit test that constructs a fake
|
|
69
|
+
// projectRoot working when no real package.json is in the walk.
|
|
70
|
+
cachedPeaksLoopRoot = resolve(here, '..', '..', '..', '..');
|
|
71
|
+
return cachedPeaksLoopRoot;
|
|
72
|
+
}
|
|
73
|
+
/** Test-only: clear the cached peaks-loop root. */
|
|
74
|
+
export function _resetPeaksLoopRootCache() {
|
|
75
|
+
cachedPeaksLoopRoot = null;
|
|
76
|
+
}
|
|
24
77
|
/**
|
|
25
78
|
* Re-classify a single RedLineEntry. Returns a new entry with the
|
|
26
79
|
* `backing` field updated and `enforcerRef` possibly nulled if the
|
|
27
80
|
* referenced file does not exist on disk.
|
|
81
|
+
*
|
|
82
|
+
* `projectRoot` is the audited project (kept for callers that still
|
|
83
|
+
* pass it for context, but no longer used to resolve `enforcerRef`).
|
|
84
|
+
* `peaksLoopRoot` is the peaks-loop source root; defaults to
|
|
85
|
+
* `resolvePeaksLoopRoot()`.
|
|
28
86
|
*/
|
|
29
|
-
export function classifyBacking(entry, projectRoot) {
|
|
87
|
+
export function classifyBacking(entry, projectRoot, peaksLoopRoot = resolvePeaksLoopRoot()) {
|
|
88
|
+
void projectRoot; // intentionally unused; preserved for API stability
|
|
30
89
|
if (detectPartial(entry.source.context)) {
|
|
31
90
|
return {
|
|
32
91
|
entry: { ...entry, backing: 'partial' },
|
|
33
|
-
enforcerExists: entry.enforcerRef !== null && existsSync(resolve(
|
|
92
|
+
enforcerExists: entry.enforcerRef !== null && existsSync(resolve(peaksLoopRoot, entry.enforcerRef)),
|
|
34
93
|
};
|
|
35
94
|
}
|
|
36
95
|
if (entry.enforcerRef === null) {
|
|
37
96
|
return { entry, enforcerExists: false };
|
|
38
97
|
}
|
|
39
|
-
const enforcerPath = resolve(
|
|
98
|
+
const enforcerPath = resolve(peaksLoopRoot, entry.enforcerRef);
|
|
40
99
|
const exists = existsSync(enforcerPath);
|
|
41
100
|
return {
|
|
42
101
|
entry: { ...entry, backing: exists ? 'cli-backed' : 'prose-only' },
|
|
43
102
|
enforcerExists: exists,
|
|
44
103
|
};
|
|
45
104
|
}
|
|
46
|
-
export function classifyBackingBatch(entries, projectRoot) {
|
|
105
|
+
export function classifyBackingBatch(entries, projectRoot, peaksLoopRoot = resolvePeaksLoopRoot()) {
|
|
47
106
|
const updated = [];
|
|
48
107
|
const warnings = [];
|
|
49
108
|
for (const entry of entries) {
|
|
50
|
-
const { entry: reclassified, enforcerExists } = classifyBacking(entry, projectRoot);
|
|
109
|
+
const { entry: reclassified, enforcerExists } = classifyBacking(entry, projectRoot, peaksLoopRoot);
|
|
51
110
|
updated.push(reclassified);
|
|
52
111
|
if (reclassified.backing === 'cli-backed' && !enforcerExists) {
|
|
53
112
|
// Defensive: should not happen because classifyBacking downgrades to
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { existsSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { RED_LINE_CATALOG } from '../red-line-catalog.js';
|
|
18
|
+
import { resolvePeaksLoopRoot } from '../backing-detector.js';
|
|
18
19
|
export const CATALOG_STABILITY_GROWTH_CAP = 0.20;
|
|
19
20
|
export const CATALOG_STABILITY_WINDOW_DAYS = 90;
|
|
20
21
|
export const RUNTIME_BUDGET_MS = 2000;
|
|
@@ -41,11 +42,19 @@ export function lintCatalogStability(input) {
|
|
|
41
42
|
return [syntheticHit('rl-audit-catalog-stability-001', 'catalog size has not grown > 20% in the last 90 days', `(growth ${(growth * 100).toFixed(1)}% over 90 days; currentSize=${input.currentSize}, priorSize=${input.sizeNinetyDaysAgo})`)];
|
|
42
43
|
}
|
|
43
44
|
export function lintNoOrphanEnforcer(projectRoot) {
|
|
45
|
+
// Slice 4.0.7-dogfood-PR-1: catalog enforcerRef paths are written
|
|
46
|
+
// relative to the peaks-loop source root (catalog + enforcers both
|
|
47
|
+
// live at <root>/src/services/audit/...). Resolving against the
|
|
48
|
+
// audited --project root produces false orphans on every downstream
|
|
49
|
+
// project. Use resolvePeaksLoopRoot() and keep `projectRoot` for
|
|
50
|
+
// test/legacy callers (the parameter is intentionally ignored here).
|
|
51
|
+
void projectRoot;
|
|
52
|
+
const peaksLoopRoot = resolvePeaksLoopRoot();
|
|
44
53
|
const hits = [];
|
|
45
54
|
for (const entry of RED_LINE_CATALOG) {
|
|
46
55
|
if (!entry.enforcerRef)
|
|
47
56
|
continue;
|
|
48
|
-
const absPath = join(
|
|
57
|
+
const absPath = join(peaksLoopRoot, entry.enforcerRef);
|
|
49
58
|
if (!existsSync(absPath)) {
|
|
50
59
|
hits.push(syntheticHit('rl-audit-no-orphan-enforcer-001', 'every enforcerRef points to a real file', `(enforcerRef "${entry.enforcerRef}" for ${entry.id} does not exist on disk)`));
|
|
51
60
|
}
|
|
@@ -123,6 +123,24 @@ function renderContextBlock(probe) {
|
|
|
123
123
|
if (probe !== null && probe !== undefined) {
|
|
124
124
|
const usedPct = (probe.ratio * 100).toFixed(1);
|
|
125
125
|
const freePct = ((1 - probe.ratio) * 100).toFixed(1);
|
|
126
|
+
// Slice 4.0.7-dogfood-PR-11 (ice-cola surface probe 2026-08-02):
|
|
127
|
+
// when the source is conservative-fallback, the ratio is
|
|
128
|
+
// hard-coded to 0 (no signal available). The pre-rid prompt
|
|
129
|
+
// rendered "0.0% used (100.0% free)" without flagging that
|
|
130
|
+
// this is a fallback, so the sub-agent thought it had 100%
|
|
131
|
+
// free and never re-checked. The fix short-circuits to the
|
|
132
|
+
// "no probe available" message when the source is not one of
|
|
133
|
+
// the trusted values emitted by peaks-loop's own readers.
|
|
134
|
+
const KNOWN_SOURCES = new Set(['token-counted', 'transcript-estimate', 'statusline']);
|
|
135
|
+
if (!KNOWN_SOURCES.has(probe.source)) {
|
|
136
|
+
return `## Context window (no probe available)
|
|
137
|
+
|
|
138
|
+
The orchestrator captured a probe with source: \`${probe.source}\` (ratio ${usedPct}%) but the dispatch layer does not trust this source. The pre-rid dispatch prompt rendered this as "0.0% used (100.0% free)" without flagging the fallback, which caused sub-agents to think they had 100% free and never re-check. **Treat the displayed ratio as unverified.**
|
|
139
|
+
|
|
140
|
+
If you need to evaluate context pressure, run \`peaks code context-now --project <root>\` and trust its \`ratio\` field. Do not estimate from message length.
|
|
141
|
+
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
126
144
|
const action = probe.ratio >= 0.95
|
|
127
145
|
? 'RED-LINE — call `peaks compact auto --execute` immediately.'
|
|
128
146
|
: probe.ratio >= 0.85
|
|
@@ -34,6 +34,20 @@ export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandat
|
|
|
34
34
|
* (e.g. projectRoot for context) without churning every call site.
|
|
35
35
|
*/
|
|
36
36
|
export declare function formatTestToolDetection(): string;
|
|
37
|
+
/**
|
|
38
|
+
* Slice 4.0.7-dogfood-PR-9: optional monorepo block prepended when
|
|
39
|
+
* `<projectRoot>/pnpm-workspace.yaml` exists. The pre-rid static
|
|
40
|
+
* template did not mention monorepos at all, so sub-agents on a
|
|
41
|
+
* monorepo project (e.g. ice-cola, 4 packages) ran `pnpm test` which
|
|
42
|
+
* fanned out to every sub-package — a 36-minute wall clock for what
|
|
43
|
+
* was a single-slice change. The block tells the sub-agent to scope
|
|
44
|
+
* to a single sub-package via `pnpm -r --filter @scope/name test`
|
|
45
|
+
* and to detect the layout via `peaks test --json` first.
|
|
46
|
+
*
|
|
47
|
+
* The block is empty when the project is not a monorepo, so the
|
|
48
|
+
* single-package case is byte-identical to the pre-rid prompt.
|
|
49
|
+
*/
|
|
50
|
+
export declare const MONOREPO_TEST_TOOL_BLOCK = "## Monorepo Test Tool Detection (only when pnpm-workspace.yaml exists)\n\nWhen `<projectRoot>/pnpm-workspace.yaml` is present, the project is a pnpm monorepo. The root `package.json#scripts.test` is typically `pnpm -r test` (recursive across all packages) and `pnpm test` from the root will fan out to every sub-package. **Do not run that.** Scope to a single sub-package:\n\n- detect the active sub-package by looking for `packages/<name>/package.json` (or via `pnpm -r --filter ${YOUR_PACKAGE_NAME} test`)\n- run `pnpm --filter @<scope>/<name> test <pattern>` for one sub-package only\n- if a top-level test script is genuinely needed across multiple packages, prefix with `PEAKS_FULL_TEST=1` AND justify in the final report\n\nA repo without `pnpm-workspace.yaml` has no monorepo block; the pre-rid static template applies unchanged.";
|
|
37
51
|
/**
|
|
38
52
|
* Slice 2026-07-29-dispatch-stall-governance / S5 (AC-4.1 / AC-4.2) —
|
|
39
53
|
* pure scope classifier. Returns a typed result so callers (the
|
|
@@ -54,6 +54,28 @@ If unsure which framework the consumer project uses, run \`peaks test --json\` f
|
|
|
54
54
|
export function formatTestToolDetection() {
|
|
55
55
|
return TEST_TOOL_DETECTION_BLOCK;
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Slice 4.0.7-dogfood-PR-9: optional monorepo block prepended when
|
|
59
|
+
* `<projectRoot>/pnpm-workspace.yaml` exists. The pre-rid static
|
|
60
|
+
* template did not mention monorepos at all, so sub-agents on a
|
|
61
|
+
* monorepo project (e.g. ice-cola, 4 packages) ran `pnpm test` which
|
|
62
|
+
* fanned out to every sub-package — a 36-minute wall clock for what
|
|
63
|
+
* was a single-slice change. The block tells the sub-agent to scope
|
|
64
|
+
* to a single sub-package via `pnpm -r --filter @scope/name test`
|
|
65
|
+
* and to detect the layout via `peaks test --json` first.
|
|
66
|
+
*
|
|
67
|
+
* The block is empty when the project is not a monorepo, so the
|
|
68
|
+
* single-package case is byte-identical to the pre-rid prompt.
|
|
69
|
+
*/
|
|
70
|
+
export const MONOREPO_TEST_TOOL_BLOCK = `## Monorepo Test Tool Detection (only when pnpm-workspace.yaml exists)
|
|
71
|
+
|
|
72
|
+
When \`<projectRoot>/pnpm-workspace.yaml\` is present, the project is a pnpm monorepo. The root \`package.json#scripts.test\` is typically \`pnpm -r test\` (recursive across all packages) and \`pnpm test\` from the root will fan out to every sub-package. **Do not run that.** Scope to a single sub-package:
|
|
73
|
+
|
|
74
|
+
- detect the active sub-package by looking for \`packages/<name>/package.json\` (or via \`pnpm -r --filter \${YOUR_PACKAGE_NAME} test\`)
|
|
75
|
+
- run \`pnpm --filter @<scope>/<name> test <pattern>\` for one sub-package only
|
|
76
|
+
- if a top-level test script is genuinely needed across multiple packages, prefix with \`PEAKS_FULL_TEST=1\` AND justify in the final report
|
|
77
|
+
|
|
78
|
+
A repo without \`pnpm-workspace.yaml\` has no monorepo block; the pre-rid static template applies unchanged.`;
|
|
57
79
|
export function classifyTestCommand(command, env = process.env) {
|
|
58
80
|
if (typeof command !== 'string') {
|
|
59
81
|
return {
|
|
@@ -44,6 +44,46 @@ async function readPackageJsonDeps(projectRoot) {
|
|
|
44
44
|
return { exists: true, deps: {} };
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Slice 4.0.7-dogfood-PR-8: walk packages/[name]/package.json one
|
|
49
|
+
* level deep and merge each sub-package's deps into a single map
|
|
50
|
+
* (later sub-packages win on key collision; this is the conservative
|
|
51
|
+
* "first detected framework" behavior). Returns empty when the
|
|
52
|
+
* project is not a monorepo or has no packages dir. Deliberately
|
|
53
|
+
* does NOT recurse into nested workspaces.
|
|
54
|
+
*/
|
|
55
|
+
async function readMonorepoSubPackageDeps(projectRoot) {
|
|
56
|
+
const packagesDir = join(projectRoot, 'packages');
|
|
57
|
+
if (!(await pathExists(packagesDir)))
|
|
58
|
+
return {};
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = await readdir(packagesDir);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
const merged = {};
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (entry === '.' || entry === '..')
|
|
69
|
+
continue;
|
|
70
|
+
const subPkgPath = join(packagesDir, entry, 'package.json');
|
|
71
|
+
if (!(await pathExists(subPkgPath)))
|
|
72
|
+
continue;
|
|
73
|
+
try {
|
|
74
|
+
const raw = await readText(subPkgPath);
|
|
75
|
+
const parsed = JSON.parse(raw);
|
|
76
|
+
Object.assign(merged, parsed.dependencies ?? {});
|
|
77
|
+
Object.assign(merged, parsed.devDependencies ?? {});
|
|
78
|
+
Object.assign(merged, parsed.peerDependencies ?? {});
|
|
79
|
+
Object.assign(merged, parsed.optionalDependencies ?? {});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// skip malformed sub-package; keep walking
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return merged;
|
|
86
|
+
}
|
|
47
87
|
async function detectBackendFrameworks(deps) {
|
|
48
88
|
return BACKEND_DEP_NAMES.filter((name) => name !== 'next' && Object.prototype.hasOwnProperty.call(deps, name));
|
|
49
89
|
}
|
|
@@ -226,8 +266,18 @@ function decideFrontendOnly(report) {
|
|
|
226
266
|
export async function scanArchetype(options) {
|
|
227
267
|
const { projectRoot } = options;
|
|
228
268
|
const { exists: hasPackageJson, deps } = await readPackageJsonDeps(projectRoot);
|
|
229
|
-
|
|
230
|
-
|
|
269
|
+
// Slice 4.0.7-dogfood-PR-8 (ice-cola surface probe 2026-08-02): a
|
|
270
|
+
// monorepo root often carries only a thin workspace manifest (no
|
|
271
|
+
// framework deps). Ice-cola root has peaks-loop only, and the
|
|
272
|
+
// @nestjs/core dep lives in packages[server].package.json. Pre-rid
|
|
273
|
+
// this reported `hasBackendFramework: false` for a NestJS monorepo
|
|
274
|
+
// because the dep scan only looked at the root. The fix adds a
|
|
275
|
+
// 1-level-deep walk into the packages dir and merges each
|
|
276
|
+
// sub-package's deps into the dedup result.
|
|
277
|
+
const monorepoSubDeps = await readMonorepoSubPackageDeps(projectRoot);
|
|
278
|
+
const mergedDeps = { ...deps, ...monorepoSubDeps };
|
|
279
|
+
const backendFrameworks = await detectBackendFrameworks(mergedDeps);
|
|
280
|
+
const hasNext = Object.prototype.hasOwnProperty.call(mergedDeps, 'next');
|
|
231
281
|
const hasNextApiRoutes = await detectNextApiRoutes(projectRoot, hasNext);
|
|
232
282
|
const backendDirsPresent = await detectBackendDirs(projectRoot);
|
|
233
283
|
const swaggerPaths = await detectSwagger(projectRoot);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
1
|
+
export { ensureSession, getSessionId, getSessionIdCanonical, resolveActiveSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
2
2
|
export { getSessionDir } from './getSessionDir.js';
|
|
3
3
|
export { resolveCallerId, type ResolveCallerIdOptions } from './resolve-caller-id.js';
|
|
4
4
|
export { getCallerBindingFile, getActiveSkillFileForCaller, synthesiseLegacyCallerId, getCallerBinding, setCallerBinding, listCallerBindings } from './caller-binding-service.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding } from './session-manager.js';
|
|
1
|
+
export { ensureSession, getSessionId, getSessionIdCanonical, resolveActiveSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding } from './session-manager.js';
|
|
2
2
|
export { getSessionDir } from './getSessionDir.js';
|
|
3
3
|
// Slice 020 — caller-keyed session binding. The new canonical path.
|
|
4
4
|
export { resolveCallerId } from './resolve-caller-id.js';
|