peaks-loop 4.0.16 → 4.0.18
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 +55 -0
- package/README-en.md +12 -9
- package/README.md +15 -4
- package/config/eslint/.peaks-rules.cjs +39 -26
- package/dist/cli/commands/_register.js +2 -1
- package/dist/cli/commands/audit-commands.d.ts +1 -1
- package/dist/cli/commands/code-gate-command.d.ts +25 -0
- package/dist/cli/commands/code-gate-command.js +86 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -4
- package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
- package/dist/cli/commands/cron-commands.js +16 -4
- package/dist/cli/commands/dashboard-long-run.js +8 -4
- package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
- package/dist/cli/commands/hooks-commands.js +56 -3
- package/dist/cli/commands/primer-command.d.ts +59 -0
- package/dist/cli/commands/primer-command.js +160 -0
- package/dist/cli/commands/qa-commands.js +1 -2
- package/dist/cli/commands/sediment-commands.d.ts +1 -1
- package/dist/cli/commands/slice-commands.js +20 -8
- package/dist/cli/commands/statusline-commands.d.ts +1 -0
- package/dist/cli/commands/statusline-commands.js +17 -3
- package/dist/cli/commands/tech-commands.d.ts +1 -1
- package/dist/cli/program.js +8 -0
- package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
- package/dist/services/artifacts/request-artifact-service.js +10 -1
- package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
- package/dist/services/code/auto-compact-orchestrator.js +9 -1
- package/dist/services/code/batch-heartbeat-poller.js +26 -0
- package/dist/services/code/orchestrator-can-do.d.ts +24 -0
- package/dist/services/code/orchestrator-can-do.js +48 -2
- package/dist/services/code/post-compact-detector.d.ts +1 -1
- package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
- package/dist/services/config/config-safety.d.ts +23 -0
- package/dist/services/config/config-safety.js +62 -0
- package/dist/services/config/config-service.d.ts +1 -1
- package/dist/services/config/config-service.js +2 -4
- package/dist/services/container/container-lease.js +21 -8
- package/dist/services/context/spillover-store.d.ts +1 -1
- package/dist/services/crystallization/crystallization-types.js +20 -8
- package/dist/services/crystallization/evidence-brief-builder.js +22 -11
- package/dist/services/dispatch/dispatch-record-writer.js +231 -141
- package/dist/services/evolution/evolution-types.js +38 -21
- package/dist/services/feedback/feedback-promotion-service.js +9 -1
- package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
- package/dist/services/hooks/pre-tool-code-gate.js +91 -0
- package/dist/services/ide/current-model-detector.js +1 -2
- package/dist/services/lint/detect-eslint.js +8 -5
- package/dist/services/lint/eslint-runner.d.ts +1 -2
- package/dist/services/lint/eslint-runner.js +86 -23
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +47 -0
- package/dist/services/loop/loop-release-types.js +32 -13
- package/dist/services/loop/spec-service.d.ts +7 -1
- package/dist/services/loop/spec-service.js +218 -182
- package/dist/services/rd/rd-service.js +22 -9
- package/dist/services/scan/archetype-service.js +21 -9
- package/dist/services/session/binding-store.js +1 -2
- package/dist/services/share/bundle-reader.d.ts +6 -9
- package/dist/services/share/bundle-reader.js +297 -223
- package/dist/services/skills/hooks-settings-service.d.ts +33 -0
- package/dist/services/skills/hooks-settings-service.js +48 -3
- package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
- package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
- package/dist/services/skills/skill-statusline-renderer.js +96 -8
- package/dist/services/skills/skill-statusline-service.d.ts +41 -0
- package/dist/services/skills/skill-statusline-service.js +56 -6
- package/dist/services/slice/calibration-store.js +21 -8
- package/dist/services/slice/slice-check-service.js +35 -14
- package/dist/services/slice/slice-decompose-service.js +459 -223
- package/dist/services/standards/project-context.js +135 -73
- package/dist/services/verdict/envelopes.d.ts +1 -2
- package/dist/services/verdict/envelopes.js +1 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
- package/dist/services/vm/vm-lease.js +19 -8
- package/dist/services/worktree/worktree-lease.d.ts +0 -10
- package/dist/services/worktree/worktree-lease.js +19 -8
- package/dist/shared/fs-utils.d.ts +1 -1
- package/package.json +7 -4
- package/skills/peaks-code/SKILL.md +46 -0
|
@@ -43,7 +43,16 @@ function listExpectedEntriesForIde(ide, _skipProgress = false) {
|
|
|
43
43
|
if (ide === 'trae') {
|
|
44
44
|
return [{ matcher: adapter.toolMatcher, sentinel: 'peaks hook handle' }];
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
// Slice 2026-08-06-codegate-vendor-neutral: Claude Code install
|
|
47
|
+
// also emits the Edit|Write|MultiEdit code-gate entry. The summary
|
|
48
|
+
// mirrors the install shape, NOT a hardcoded expected list. The
|
|
49
|
+
// hook source of truth is `src/services/hooks/pre-tool-code-gate.sh`
|
|
50
|
+
// (vendor-neutral); the runtime adapter is `peaks code-gate --json`
|
|
51
|
+
// registered as a PreToolUse entry.
|
|
52
|
+
return [
|
|
53
|
+
{ matcher: adapter.toolMatcher, sentinel: 'peaks gate enforce' },
|
|
54
|
+
{ matcher: 'Edit|Write|MultiEdit', sentinel: 'peaks code-gate' }
|
|
55
|
+
];
|
|
47
56
|
}
|
|
48
57
|
/**
|
|
49
58
|
* Slice 2026-07-24-peaks-code-bridge-002-rootcause (G6b / G10): copy the
|
|
@@ -98,6 +107,26 @@ function copyBridgeHookIfPresent(userHome) {
|
|
|
98
107
|
copyFileSync(source, target);
|
|
99
108
|
return { copied: true, source, target };
|
|
100
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Slice 2026-08-06-codegate-vendor-neutral: copy the code-gate hook
|
|
112
|
+
* source (`src/services/hooks/pre-tool-code-gate.sh`) into the user-
|
|
113
|
+
* global peaks-code hooks dir, mirroring the bridge-hook copy. The
|
|
114
|
+
* hook itself is vendor-neutral (no `claude` / `anthropic` strings in
|
|
115
|
+
* the script) — this installer is the vendor adapter that decides
|
|
116
|
+
* where the file lands. The CLI command `peaks code-gate` is the
|
|
117
|
+
* runtime entry; the shell script is the canonical artifact distributed
|
|
118
|
+
* alongside the bridge hook.
|
|
119
|
+
*/
|
|
120
|
+
function copyCodeGateHookIfPresent(userHome) {
|
|
121
|
+
const source = resolve(__dirname, '..', '..', 'services', 'hooks', 'pre-tool-code-gate.sh');
|
|
122
|
+
const target = resolve(userHome, '.claude', 'skills', 'peaks-code', 'hooks', 'pre-tool-code-gate.sh');
|
|
123
|
+
if (!existsSync(source)) {
|
|
124
|
+
return { copied: false, source, target };
|
|
125
|
+
}
|
|
126
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
127
|
+
copyFileSync(source, target);
|
|
128
|
+
return { copied: true, source, target };
|
|
129
|
+
}
|
|
101
130
|
export function registerHooksCommands(program, io) {
|
|
102
131
|
const hooks = program
|
|
103
132
|
.command('hooks')
|
|
@@ -121,6 +150,14 @@ export function registerHooksCommands(program, io) {
|
|
|
121
150
|
const bridgeCopy = scope === 'global'
|
|
122
151
|
? copyBridgeHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
|
|
123
152
|
: { copied: false, source: '', target: '' };
|
|
153
|
+
// Slice 2026-08-06-codegate-vendor-neutral: also copy the
|
|
154
|
+
// code-gate hook script. The runtime gate lives at
|
|
155
|
+
// `peaks code-gate --json` (registered as a PreToolUse entry
|
|
156
|
+
// in the install output); the script is the build artifact
|
|
157
|
+
// distributed alongside the bridge hook.
|
|
158
|
+
const codeGateCopy = scope === 'global'
|
|
159
|
+
? copyCodeGateHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
|
|
160
|
+
: { copied: false, source: '', target: '' };
|
|
124
161
|
printResult(io, ok('hooks.install', {
|
|
125
162
|
...plan,
|
|
126
163
|
ide,
|
|
@@ -129,6 +166,7 @@ export function registerHooksCommands(program, io) {
|
|
|
129
166
|
skipProgress,
|
|
130
167
|
entries: dryRunEntries,
|
|
131
168
|
bridgeHookCopy: bridgeCopy,
|
|
169
|
+
codeGateHookCopy: codeGateCopy,
|
|
132
170
|
// Slice 2026-07-29-worktree-layer3-deny: surface the
|
|
133
171
|
// Layer 3 deny entries the install WOULD write. The
|
|
134
172
|
// dry-run branch never actually invokes
|
|
@@ -141,7 +179,10 @@ export function registerHooksCommands(program, io) {
|
|
|
141
179
|
`would write ${listSuperpowersDenyEntries().length} permissions.deny entries (Layer 3 worktree governance)`,
|
|
142
180
|
bridgeCopy.copied
|
|
143
181
|
? `would copy bridge hook from ${bridgeCopy.source} to ${bridgeCopy.target}`
|
|
144
|
-
: 'would not copy bridge hook (source missing or non-global scope)'
|
|
182
|
+
: 'would not copy bridge hook (source missing or non-global scope)',
|
|
183
|
+
codeGateCopy.copied
|
|
184
|
+
? `would copy code-gate hook from ${codeGateCopy.source} to ${codeGateCopy.target}`
|
|
185
|
+
: 'would not copy code-gate hook (source missing or non-global scope)'
|
|
145
186
|
]), options.json);
|
|
146
187
|
return;
|
|
147
188
|
}
|
|
@@ -161,6 +202,14 @@ export function registerHooksCommands(program, io) {
|
|
|
161
202
|
const bridgeCopy = scope === 'global'
|
|
162
203
|
? copyBridgeHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
|
|
163
204
|
: { copied: false, source: '', target: '' };
|
|
205
|
+
// Slice 2026-08-06-codegate-vendor-neutral: also copy the
|
|
206
|
+
// code-gate hook script. The runtime gate lives at
|
|
207
|
+
// `peaks code-gate --json` (registered as a PreToolUse entry
|
|
208
|
+
// in the install output); the script is the build artifact
|
|
209
|
+
// distributed alongside the bridge hook.
|
|
210
|
+
const codeGateCopy = scope === 'global'
|
|
211
|
+
? copyCodeGateHookIfPresent(process.env.USERPROFILE ?? process.env.HOME ?? '')
|
|
212
|
+
: { copied: false, source: '', target: '' };
|
|
164
213
|
const nextActions = result.applied
|
|
165
214
|
? [
|
|
166
215
|
'Restart the IDE (or reload the workspace) so the hook entries take effect',
|
|
@@ -170,7 +219,10 @@ export function registerHooksCommands(program, io) {
|
|
|
170
219
|
`Layer 3 deny: wrote ${listSuperpowersDenyEntries().length} permissions.deny entries (worktree governance)`,
|
|
171
220
|
bridgeCopy.copied
|
|
172
221
|
? `Copied bridge hook: ${bridgeCopy.target}`
|
|
173
|
-
: 'Bridge hook not copied (source missing or non-global scope)'
|
|
222
|
+
: 'Bridge hook not copied (source missing or non-global scope)',
|
|
223
|
+
codeGateCopy.copied
|
|
224
|
+
? `Copied code-gate hook: ${codeGateCopy.target}`
|
|
225
|
+
: 'Code-gate hook not copied (source missing or non-global scope)'
|
|
174
226
|
]
|
|
175
227
|
: (bridgeCopy.copied
|
|
176
228
|
? [`Bridge hook copied: ${bridgeCopy.target}`]
|
|
@@ -189,6 +241,7 @@ export function registerHooksCommands(program, io) {
|
|
|
189
241
|
skipProgress,
|
|
190
242
|
entries: installedEntries.map((e) => ({ matcher: e.matcher, sentinel: e.sentinel })),
|
|
191
243
|
bridgeHookCopy: bridgeCopy,
|
|
244
|
+
codeGateHookCopy: codeGateCopy,
|
|
192
245
|
permissionsDenyApplied: result.applied,
|
|
193
246
|
permissionsDenyEntries: denyEntries
|
|
194
247
|
}, [], nextActions), options.json);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice rid-statusline-stale-ux AC-2: `peaks session primer --project <path>`.
|
|
3
|
+
*
|
|
4
|
+
* Lightweight SessionStart primer that fires rotation + presence
|
|
5
|
+
* cleanup BEFORE the first statusline render of a fresh session.
|
|
6
|
+
* Independent subcommand (NOT a flag on `peaks workspace init` per
|
|
7
|
+
* task regulation: "don't add a flag to workspace init"). Cleaner
|
|
8
|
+
* permission boundary — primer cannot be mistaken for init by users.
|
|
9
|
+
*
|
|
10
|
+
* Performs only 3 things:
|
|
11
|
+
* 1. ensureSessionWithRotation(projectRoot) — the real rotation
|
|
12
|
+
* entry point (verified at
|
|
13
|
+
* src/services/session/session-binding-bridge.ts:461-527).
|
|
14
|
+
* 2. If outer-mismatch rotation occurred: call
|
|
15
|
+
* clearStalePresenceOnRotation with the verified single-options-
|
|
16
|
+
* object signature (mirrors init-command.ts:324-329 pattern with
|
|
17
|
+
* all 3 fields populated). Note: on 4.0.11-A sid-scoped leases
|
|
18
|
+
* this is a legacy-compat no-op; the real cleanup mechanism is
|
|
19
|
+
* rotation itself (rebinding changes the bound sid so the
|
|
20
|
+
* reader looks in the new sid's empty lease dir).
|
|
21
|
+
* 3. gcStalePresenceLeases({ projectRoot, trigger: 'manual' }) —
|
|
22
|
+
* sync; documented as legacy-compat no-op on 4.0.11-A sid-scoped
|
|
23
|
+
* data.
|
|
24
|
+
*
|
|
25
|
+
* SKIPS (per RD §4.2.1):
|
|
26
|
+
* - bootstrapProjectScan
|
|
27
|
+
* - materializeClaudeSettingsLocal
|
|
28
|
+
* - resolveFirstTimeHooksInstall
|
|
29
|
+
* - applyHookInstall (prevents accidental first-launch hook install)
|
|
30
|
+
*
|
|
31
|
+
* Mounted as a CHILD of the existing `session` commander group at
|
|
32
|
+
* `src/cli/commands/core/session-command.ts:32` (verified pattern:
|
|
33
|
+
* `session.command('list')...`). NOT `program.command('session primer')`.
|
|
34
|
+
*
|
|
35
|
+
* The action handler is exported as `runPrimerAction` so the
|
|
36
|
+
* integration test suite can drive it directly without going through
|
|
37
|
+
* Commander's argv-parsing wrapper (which would mangle empty /
|
|
38
|
+
* whitespace / NUL-byte inputs before reaching the action body).
|
|
39
|
+
*/
|
|
40
|
+
import type { Command } from 'commander';
|
|
41
|
+
import type { ProgramIO } from '../cli-helpers.js';
|
|
42
|
+
export type PrimerOptions = {
|
|
43
|
+
project: string;
|
|
44
|
+
json?: boolean;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Pure action body for `peaks session primer`. Exported so
|
|
48
|
+
* integration tests can drive it directly with crafted inputs (empty
|
|
49
|
+
* string, whitespace, NUL byte) that would otherwise be rejected by
|
|
50
|
+
* Commander's argv parser.
|
|
51
|
+
*
|
|
52
|
+
* Returns `{ exitCode: 0 | 1 }` so callers (CLI path or test path)
|
|
53
|
+
* can apply the exit code uniformly. NEVER calls `process.exit` —
|
|
54
|
+
* exits are the responsibility of the CLI wrapper.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runPrimerAction(opts: PrimerOptions, io: ProgramIO): Promise<{
|
|
57
|
+
exitCode: 0 | 1;
|
|
58
|
+
}>;
|
|
59
|
+
export declare function registerPrimerCommand(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice rid-statusline-stale-ux AC-2: `peaks session primer --project <path>`.
|
|
3
|
+
*
|
|
4
|
+
* Lightweight SessionStart primer that fires rotation + presence
|
|
5
|
+
* cleanup BEFORE the first statusline render of a fresh session.
|
|
6
|
+
* Independent subcommand (NOT a flag on `peaks workspace init` per
|
|
7
|
+
* task regulation: "don't add a flag to workspace init"). Cleaner
|
|
8
|
+
* permission boundary — primer cannot be mistaken for init by users.
|
|
9
|
+
*
|
|
10
|
+
* Performs only 3 things:
|
|
11
|
+
* 1. ensureSessionWithRotation(projectRoot) — the real rotation
|
|
12
|
+
* entry point (verified at
|
|
13
|
+
* src/services/session/session-binding-bridge.ts:461-527).
|
|
14
|
+
* 2. If outer-mismatch rotation occurred: call
|
|
15
|
+
* clearStalePresenceOnRotation with the verified single-options-
|
|
16
|
+
* object signature (mirrors init-command.ts:324-329 pattern with
|
|
17
|
+
* all 3 fields populated). Note: on 4.0.11-A sid-scoped leases
|
|
18
|
+
* this is a legacy-compat no-op; the real cleanup mechanism is
|
|
19
|
+
* rotation itself (rebinding changes the bound sid so the
|
|
20
|
+
* reader looks in the new sid's empty lease dir).
|
|
21
|
+
* 3. gcStalePresenceLeases({ projectRoot, trigger: 'manual' }) —
|
|
22
|
+
* sync; documented as legacy-compat no-op on 4.0.11-A sid-scoped
|
|
23
|
+
* data.
|
|
24
|
+
*
|
|
25
|
+
* SKIPS (per RD §4.2.1):
|
|
26
|
+
* - bootstrapProjectScan
|
|
27
|
+
* - materializeClaudeSettingsLocal
|
|
28
|
+
* - resolveFirstTimeHooksInstall
|
|
29
|
+
* - applyHookInstall (prevents accidental first-launch hook install)
|
|
30
|
+
*
|
|
31
|
+
* Mounted as a CHILD of the existing `session` commander group at
|
|
32
|
+
* `src/cli/commands/core/session-command.ts:32` (verified pattern:
|
|
33
|
+
* `session.command('list')...`). NOT `program.command('session primer')`.
|
|
34
|
+
*
|
|
35
|
+
* The action handler is exported as `runPrimerAction` so the
|
|
36
|
+
* integration test suite can drive it directly without going through
|
|
37
|
+
* Commander's argv-parsing wrapper (which would mangle empty /
|
|
38
|
+
* whitespace / NUL-byte inputs before reaching the action body).
|
|
39
|
+
*/
|
|
40
|
+
import { printResult } from '../cli-helpers.js';
|
|
41
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
42
|
+
import { resolveCanonicalProjectRootStrict, InvalidProjectRootError } from '../../services/config/config-safety.js';
|
|
43
|
+
import { ensureSessionWithRotation } from '../../services/session/session-manager.js';
|
|
44
|
+
import { clearStalePresenceOnRotation } from '../../services/skills/skill-presence-service.js';
|
|
45
|
+
import { gcStalePresenceLeases } from '../../services/skills/presence-lease-service.js';
|
|
46
|
+
/**
|
|
47
|
+
* Pure action body for `peaks session primer`. Exported so
|
|
48
|
+
* integration tests can drive it directly with crafted inputs (empty
|
|
49
|
+
* string, whitespace, NUL byte) that would otherwise be rejected by
|
|
50
|
+
* Commander's argv parser.
|
|
51
|
+
*
|
|
52
|
+
* Returns `{ exitCode: 0 | 1 }` so callers (CLI path or test path)
|
|
53
|
+
* can apply the exit code uniformly. NEVER calls `process.exit` —
|
|
54
|
+
* exits are the responsibility of the CLI wrapper.
|
|
55
|
+
*/
|
|
56
|
+
export async function runPrimerAction(opts, io) {
|
|
57
|
+
// Explicit empty / whitespace-only guard BEFORE the strict
|
|
58
|
+
// resolver. Commander's requiredOption checks presence, not
|
|
59
|
+
// emptiness; node's `resolve('')` returns cwd (silent
|
|
60
|
+
// fall-through), so we must short-circuit explicitly.
|
|
61
|
+
if (!opts.project || opts.project.trim() === '') {
|
|
62
|
+
printResult(io, fail('session.primer', 'PRIMER_EMPTY_PROJECT', '--project <path> required and must be non-empty', { project: opts.project }, ['Pass a non-empty absolute path to --project']), opts.json);
|
|
63
|
+
return { exitCode: 1 };
|
|
64
|
+
}
|
|
65
|
+
// P1 H1 option A: use NEW strict helper. Throws on NUL /
|
|
66
|
+
// non-canonical / non-existent path. The fail-open
|
|
67
|
+
// `resolveCanonicalProjectRoot` is not safe here because primer
|
|
68
|
+
// is invoked from a SessionStart hook on a path pulled from
|
|
69
|
+
// `${CLAUDE_PROJECT_DIR}` — the env var is user-controlled and
|
|
70
|
+
// may contain path traversal payloads.
|
|
71
|
+
let projectRoot;
|
|
72
|
+
try {
|
|
73
|
+
projectRoot = resolveCanonicalProjectRootStrict(opts.project);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof InvalidProjectRootError) {
|
|
77
|
+
printResult(io, fail('session.primer', `PRIMER_INVALID_PROJECT_ROOT_${error.reason.replace(/-/g, '_').toUpperCase()}`, error.message, { project: opts.project, reason: error.reason }, ['Pass a non-empty absolute canonical path to --project']), opts.json);
|
|
78
|
+
return { exitCode: 1 };
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
// Real rotation entry point (verified
|
|
83
|
+
// src/services/session/session-binding-bridge.ts:461-527).
|
|
84
|
+
// NOT the invented `runRotationCheck` from cycle-2 RD.
|
|
85
|
+
const rotation = await ensureSessionWithRotation(projectRoot);
|
|
86
|
+
const rotationOccurred = rotation.previousSessionId !== null
|
|
87
|
+
&& rotation.rotationReason === 'outer-session-mismatch';
|
|
88
|
+
let clearOutcome = null;
|
|
89
|
+
if (rotationOccurred) {
|
|
90
|
+
// VERIFIED single-options-object signature (see
|
|
91
|
+
// skill-presence-service.ts:584-588). Mirror the real caller
|
|
92
|
+
// pattern at init-command.ts:324-329 with all 3 fields
|
|
93
|
+
// populated. NOTE: on 4.0.11-A sid-scoped leases this is a
|
|
94
|
+
// legacy-compat no-op (clearSkillPresence only unlinks legacy
|
|
95
|
+
// single-slot files; the real cleanup mechanism is rotation
|
|
96
|
+
// itself).
|
|
97
|
+
clearOutcome = clearStalePresenceOnRotation({
|
|
98
|
+
projectRootOverride: projectRoot,
|
|
99
|
+
currentOuterSessionId: process.env.PEAKS_OUTER_SESSION_ID
|
|
100
|
+
?? process.env.CLAUDE_CODE_SESSION_ID,
|
|
101
|
+
rotatedOutSessionId: rotation.previousSessionId
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Sync call (verified presence-lease-service.ts:383-414) —
|
|
105
|
+
// documented as legacy-compat no-op on 4.0.11-A sid-scoped
|
|
106
|
+
// data; iterates `input.leases ?? []` and gets `[]` on the
|
|
107
|
+
// real init-command.ts:463-466 caller.
|
|
108
|
+
const gcOutcome = gcStalePresenceLeases({
|
|
109
|
+
projectRoot,
|
|
110
|
+
trigger: 'manual'
|
|
111
|
+
});
|
|
112
|
+
printResult(io, ok('session.primer', {
|
|
113
|
+
projectRoot,
|
|
114
|
+
sessionId: rotation.sessionId,
|
|
115
|
+
rotationOccurred,
|
|
116
|
+
...(rotation.previousSessionId !== null
|
|
117
|
+
? { previousSessionId: rotation.previousSessionId }
|
|
118
|
+
: {}),
|
|
119
|
+
clearOutcome,
|
|
120
|
+
gcOutcome: {
|
|
121
|
+
removed: gcOutcome.removed,
|
|
122
|
+
retained: gcOutcome.retained
|
|
123
|
+
}
|
|
124
|
+
}), opts.json);
|
|
125
|
+
return { exitCode: 0 };
|
|
126
|
+
}
|
|
127
|
+
export function registerPrimerCommand(program, io) {
|
|
128
|
+
// Reuse the existing `session` commander group registered earlier
|
|
129
|
+
// in `createProgram` (via autoRegisterAllCommands → session-command.ts:32).
|
|
130
|
+
// Creating a new `program.command('session')` here throws
|
|
131
|
+
// `cannot add command 'session' as already have command 'session'`
|
|
132
|
+
// because commander disallows duplicate top-level command names.
|
|
133
|
+
// This regression was caught in publish.yml gate-changeset step
|
|
134
|
+
// (run #134, exit 1) on the first 4.0.18 release attempt.
|
|
135
|
+
const session = program.commands.find((c) => c.name() === 'session');
|
|
136
|
+
if (!session) {
|
|
137
|
+
throw new Error('registerPrimerCommand: session group not registered; ' +
|
|
138
|
+
'autoRegisterAllCommands must run before registerPrimerCommand');
|
|
139
|
+
}
|
|
140
|
+
session
|
|
141
|
+
.command('primer')
|
|
142
|
+
.description('Lightweight SessionStart primer: rotation + presence cleanup. ' +
|
|
143
|
+
'Does NOT bootstrap project scan / materialize settings / install hooks. ' +
|
|
144
|
+
'Idempotent; safe to run on every SessionStart.')
|
|
145
|
+
.requiredOption('--project <path>', 'Project root (must be non-empty canonical path)')
|
|
146
|
+
.option('--json', 'emit a JSON envelope { ok, data } to stdout')
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
try {
|
|
149
|
+
const { exitCode } = await runPrimerAction(opts, io);
|
|
150
|
+
if (exitCode !== 0) {
|
|
151
|
+
process.exitCode = exitCode;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
156
|
+
printResult(io, fail('session.primer', 'PRIMER_FAILED', message, { project: opts.project }, ['Verify the project path exists, is writable, and is canonical']), opts.json);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
* with reason `--no-browser`. This is a slice-level opt-out for
|
|
26
26
|
* backend-only or already-covered-by-unit-tests work.
|
|
27
27
|
*/
|
|
28
|
-
import { join } from 'node:path';
|
|
29
|
-
import { resolve } from 'node:path';
|
|
28
|
+
import { join, resolve } from 'node:path';
|
|
30
29
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
31
30
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
32
31
|
import { archiveScreenshots } from '../../services/qa/screenshot-archive-service.js';
|
|
@@ -9,6 +9,17 @@ import { decompose as multiPassDecompose } from '../../services/slice/multi-pass
|
|
|
9
9
|
import { readResult as readDecompositionResult, writeResult as writeSchemaResult } from '../../services/slice/schema-router.js';
|
|
10
10
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
11
11
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
12
|
+
/**
|
|
13
|
+
* PRD-002b slice 2 — extract CLI listing-pagination + table-format
|
|
14
|
+
* constants. Values are bytewise-identical to the original literals.
|
|
15
|
+
*/
|
|
16
|
+
const SLICE_LIST_DEFAULT_LIMIT = 50;
|
|
17
|
+
const SLICE_LIST_MAX_LIMIT = 500;
|
|
18
|
+
const SLICE_LIST_RID_COL_WIDTH = 34;
|
|
19
|
+
const SLICE_LIST_MTIME_COL_WIDTH = 22;
|
|
20
|
+
const SLICE_LIST_SIZE_COL_WIDTH = 8;
|
|
21
|
+
const SLICE_LIST_PICKED_COL_WIDTH = 6;
|
|
22
|
+
const SLICE_LIST_SEPARATOR_WIDTH = 76;
|
|
12
23
|
export function registerSliceCommands(program, io) {
|
|
13
24
|
const slice = program.command('slice').description('Slice lifecycle: check (boundary), decompose (PRD -> 6-stage algorithm), ' +
|
|
14
25
|
'pick (fzf multi-select), plan (apply via peaks request init). ' +
|
|
@@ -67,12 +78,12 @@ export function registerSliceCommands(program, io) {
|
|
|
67
78
|
'(mtime > 30d). Use --stale-only to filter, --rid <substring> to narrow, ' +
|
|
68
79
|
'--limit <n> to cap. To remove stale entries, use `peaks slice cleanup` (separate slice).')
|
|
69
80
|
.option('--project <path>', 'target project root', '.')
|
|
70
|
-
.option('--limit <n>', 'cap result count (default 50, max 500)', (v) => parseInt(v, 10),
|
|
81
|
+
.option('--limit <n>', 'cap result count (default 50, max 500)', (v) => parseInt(v, 10), SLICE_LIST_DEFAULT_LIMIT)
|
|
71
82
|
.option('--stale-only', 'only include rids older than the stale threshold', false)
|
|
72
83
|
.option('--rid <substring>', 'case-insensitive substring filter on rid', '')).action((options) => {
|
|
73
84
|
try {
|
|
74
85
|
const projectRoot = resolveCanonicalProjectRoot(options.project);
|
|
75
|
-
const limit = Math.max(1, Math.min(
|
|
86
|
+
const limit = Math.max(1, Math.min(SLICE_LIST_MAX_LIMIT, options.limit ?? SLICE_LIST_DEFAULT_LIMIT));
|
|
76
87
|
const all = listDecompositions(projectRoot);
|
|
77
88
|
let filtered = all;
|
|
78
89
|
const ridSub = (options.rid ?? '').toLowerCase();
|
|
@@ -97,13 +108,13 @@ export function registerSliceCommands(program, io) {
|
|
|
97
108
|
}
|
|
98
109
|
// Plaintext mode: header + table
|
|
99
110
|
const lines = [];
|
|
100
|
-
lines.push('RID'.padEnd(
|
|
101
|
-
lines.push('-'.repeat(
|
|
111
|
+
lines.push('RID'.padEnd(SLICE_LIST_RID_COL_WIDTH) + 'MTIME'.padEnd(SLICE_LIST_MTIME_COL_WIDTH) + 'SIZE'.padEnd(SLICE_LIST_SIZE_COL_WIDTH) + 'PICKED'.padEnd(SLICE_LIST_PICKED_COL_WIDTH) + 'STALE');
|
|
112
|
+
lines.push('-'.repeat(SLICE_LIST_SEPARATOR_WIDTH));
|
|
102
113
|
for (const r of page) {
|
|
103
|
-
lines.push(r.rid.padEnd(
|
|
104
|
-
r.mtime.padEnd(
|
|
105
|
-
String(r.sizeBytes).padEnd(
|
|
106
|
-
(r.pickedPath ? 'yes' : 'no').padEnd(
|
|
114
|
+
lines.push(r.rid.padEnd(SLICE_LIST_RID_COL_WIDTH) +
|
|
115
|
+
r.mtime.padEnd(SLICE_LIST_MTIME_COL_WIDTH) +
|
|
116
|
+
String(r.sizeBytes).padEnd(SLICE_LIST_SIZE_COL_WIDTH) +
|
|
117
|
+
(r.pickedPath ? 'yes' : 'no').padEnd(SLICE_LIST_PICKED_COL_WIDTH) +
|
|
107
118
|
(r.isStale ? 'yes' : 'no'));
|
|
108
119
|
}
|
|
109
120
|
if (truncated) {
|
|
@@ -365,6 +376,7 @@ function writeDecompositionFile(rid, result, projectRoot) {
|
|
|
365
376
|
* recorded in .peaks/memory/ for follow-up). If that slice ships with a
|
|
366
377
|
* different default, both must update here.
|
|
367
378
|
*/
|
|
379
|
+
// eslint-disable-next-line no-magic-numbers -- canonical 30-day-in-ms math
|
|
368
380
|
const STALE_THRESHOLD_MS = 30 * 24 * 60 * 60 * 1000;
|
|
369
381
|
/**
|
|
370
382
|
* Enumerate every slice decomposition artifact under
|
|
@@ -74,7 +74,14 @@ export async function runDefaultStatuslineRender(options, io) {
|
|
|
74
74
|
const seeded = options.project
|
|
75
75
|
? { ...(stdin ?? {}), workspace: { current_dir: options.project } }
|
|
76
76
|
: stdin;
|
|
77
|
-
|
|
77
|
+
// Slice 2026-08-07-statusline-flake: accept `--now <epoch-ms>` to pin
|
|
78
|
+
// `Date.now()` for deterministic lifecycle-window checks under full-
|
|
79
|
+
// suite concurrency. The integration test passes a test-time `now`
|
|
80
|
+
// pinned at the moment `seedLifecycle` was called so the completed-
|
|
81
|
+
// expiry window stays in-range regardless of how long the spawned
|
|
82
|
+
// subprocess sat descheduled. Production callers omit the flag.
|
|
83
|
+
const now = options.now !== undefined ? Number(options.now) : Date.now();
|
|
84
|
+
const model = buildStatusLineModel(seeded, now);
|
|
78
85
|
const capability = resolveStatusLineCapability({
|
|
79
86
|
env: process.env,
|
|
80
87
|
isTTY: Boolean(process.stdout.isTTY),
|
|
@@ -98,7 +105,8 @@ export function registerStatusLineCommands(program, io) {
|
|
|
98
105
|
const statusline = addJsonOption(program
|
|
99
106
|
.command('statusline')
|
|
100
107
|
.description('Render the Peaks skill status line for the current session, or manage the adapter-driven statusLine entry. Run with no subcommand to render; with a subcommand (install | uninstall | status) to manage.')
|
|
101
|
-
.option('--project <path>', 'project root path (used to label the status line when stdin is absent; applies to the default render path)')
|
|
108
|
+
.option('--project <path>', 'project root path (used to label the status line when stdin is absent; applies to the default render path)')
|
|
109
|
+
.option('--now <ms>', 'override current time (epoch ms) — for testing / deterministic rendering of the lifecycle expiry window'));
|
|
102
110
|
// Default behavior: when the user types `peaks statusline` with no
|
|
103
111
|
// subcommand, render the status line. Without this default action,
|
|
104
112
|
// commander falls back to printing usage (Bug-02, ice-cola surface check
|
|
@@ -225,6 +233,7 @@ export function registerStatusLineCommands(program, io) {
|
|
|
225
233
|
'--session-id to target a non-canonical session explicitly.')
|
|
226
234
|
.option('--project <path>', 'project root (auto-detected from cwd when omitted)')
|
|
227
235
|
.option('--session-id <sid>', 'override the active session id (defaults to the canonical binding; supported for QA / internal tooling)')
|
|
236
|
+
.option('--now <ms>', 'override current time (epoch ms) — for testing / deterministic rendering of the lifecycle expiry window')
|
|
228
237
|
.action((options, command) => {
|
|
229
238
|
// Re-resolve options via `command.optsWithGlobals()` (Commander 12.x
|
|
230
239
|
// compatibility): when the parent `statusline` command has its own
|
|
@@ -243,10 +252,15 @@ export function registerStatusLineCommands(program, io) {
|
|
|
243
252
|
? merged.project
|
|
244
253
|
: (findProjectRoot(process.cwd()) ?? process.cwd());
|
|
245
254
|
const sid = merged.sessionId ?? getSessionIdCanonical(projectRoot) ?? null;
|
|
255
|
+
// Slice 2026-08-07-statusline-flake: honor `--now` (epoch ms)
|
|
256
|
+
// when present so deterministic lifecycle-window checks stay
|
|
257
|
+
// in-range under full-suite concurrency. See `--now` doc on
|
|
258
|
+
// the parent `statusline` command.
|
|
259
|
+
const now = merged.now !== undefined ? Number(merged.now) : Date.now();
|
|
246
260
|
const state = decideCompactStatusline({
|
|
247
261
|
projectRoot,
|
|
248
262
|
sessionId: sid,
|
|
249
|
-
now
|
|
263
|
+
now,
|
|
250
264
|
});
|
|
251
265
|
const label = renderCompactStatusline(state);
|
|
252
266
|
// For non-JSON mode we print the LABEL DIRECTLY to stdout
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* (B2 contract preservation rule from the rid-009 closure record).
|
|
19
19
|
*/
|
|
20
20
|
import type { Command } from 'commander';
|
|
21
|
-
import type
|
|
21
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
22
22
|
import type { WorkspaceConfig } from '../../services/config/config-types.js';
|
|
23
23
|
import { type TechChangeStatus, type WorkspaceConfig as _WorkspaceConfig } from '../../services/tech/tech-change-id-service.js';
|
|
24
24
|
export type { WorkspaceConfig };
|
package/dist/cli/program.js
CHANGED
|
@@ -16,6 +16,7 @@ import { registerCronSchedulerCommand } from './commands/cron-scheduler-commands
|
|
|
16
16
|
import { registerWorkspaceCommands } from './commands/workspace-commands.js';
|
|
17
17
|
import { registerSopCommands } from './commands/sop-commands.js';
|
|
18
18
|
import { registerSkillVisibilityCommand } from './commands/skill-visibility.js';
|
|
19
|
+
import { registerPrimerCommand } from './commands/primer-command.js';
|
|
19
20
|
import { applyRetention, cleanupEccCache } from '../services/log/retention.js';
|
|
20
21
|
import { writeLogEntry, maybeWriteStderr } from '../services/log/logger.js';
|
|
21
22
|
import { printSuperCommandCatalog } from './cli-helpers.js';
|
|
@@ -209,5 +210,12 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
209
210
|
// registerCoreAndArtifactCommands instead of being created twice.
|
|
210
211
|
autoRegisterAllCommands(program, io);
|
|
211
212
|
registerSkillVisibilityCommand(program, repoRoot);
|
|
213
|
+
// Slice rid-statusline-stale-ux AC-2: register `peaks session primer`
|
|
214
|
+
// so it appears in `peaks session --help` for LLM `<TAB>`-discovery.
|
|
215
|
+
// Mounted as a CHILD of the existing `session` commander group
|
|
216
|
+
// (verified at src/cli/commands/core/session-command.ts:32). NOT
|
|
217
|
+
// `program.command('session primer')` (that registers a single
|
|
218
|
+
// literal command name, not a child of the session group).
|
|
219
|
+
registerPrimerCommand(program, io);
|
|
212
220
|
return program;
|
|
213
221
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_REQUEST_TYPE, isRequestType, VALID_REQUEST_TYPES, type PrerequisiteCheckResult, type RequestType } from './artifact-prerequisites.js';
|
|
2
2
|
export { VALID_REQUEST_TYPES, DEFAULT_REQUEST_TYPE, isRequestType, type RequestType };
|
|
3
|
-
import type
|
|
3
|
+
import { type RequestArtifactRole } from './artifact-templates.js';
|
|
4
4
|
export type { RequestArtifactRole } from './artifact-templates.js';
|
|
5
5
|
export { formatHandoffPath, formatCommitBoundaryPath, formatSkillUsageLessonsPath } from './artifact-templates.js';
|
|
6
6
|
export type CreateRequestArtifactOptions = {
|
|
@@ -110,7 +110,7 @@ export type TransitionRequestArtifactResult = RequestArtifactSummary & {
|
|
|
110
110
|
content: string;
|
|
111
111
|
bypassedPrerequisites?: PrerequisiteCheckResult;
|
|
112
112
|
};
|
|
113
|
-
import type
|
|
113
|
+
import { type RequestArtifactState } from './request-artifact-state-helpers.js';
|
|
114
114
|
export { allowedStatesForRole, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock } from './request-artifact-state-helpers.js';
|
|
115
115
|
export type { RequestArtifactState } from './request-artifact-state-helpers.js';
|
|
116
116
|
export declare function transitionRequestArtifact(options: TransitionRequestArtifactOptions): Promise<TransitionRequestArtifactResult | null>;
|
|
@@ -16,6 +16,15 @@ import { requireUserConfirmation } from '../mode/mode-enforcement.js';
|
|
|
16
16
|
import { scanFileSize } from '../scan/file-size-scan.js';
|
|
17
17
|
import { emitObservabilityEvent } from '../observability/observability-service.js';
|
|
18
18
|
export { VALID_REQUEST_TYPES, DEFAULT_REQUEST_TYPE, isRequestType };
|
|
19
|
+
// Re-export the pure-function template surface so external callers (CLI,
|
|
20
|
+
// tests) keep importing from this module unchanged. The render templates,
|
|
21
|
+
// handoff-path helpers, and `RequestArtifactRole` type all live in the
|
|
22
|
+
// sibling `artifact-templates.ts` module — see slice
|
|
23
|
+
// 2026-06-24-handoff-path-canonicalization for the rationale.
|
|
24
|
+
//
|
|
25
|
+
// `import type` is required so the type is in scope as a bare identifier
|
|
26
|
+
// within this file (the value re-export alone does not bring types into
|
|
27
|
+
// the local namespace under `isolatedModules`).
|
|
19
28
|
import { renderTemplate } from './artifact-templates.js';
|
|
20
29
|
export { formatHandoffPath, formatCommitBoundaryPath, formatSkillUsageLessonsPath } from './artifact-templates.js';
|
|
21
30
|
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -324,7 +333,7 @@ async function readRequestArtifact(projectRoot, scope, role, found) {
|
|
|
324
333
|
// the sibling `request-artifact-state-helpers.ts` module — see
|
|
325
334
|
// v2.18.3 file-split for the rationale. Function signatures and
|
|
326
335
|
// behaviour are unchanged (verbatim move).
|
|
327
|
-
import { ALLOWED_STATES_PER_ROLE, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock } from './request-artifact-state-helpers.js';
|
|
336
|
+
import { ALLOWED_STATES_PER_ROLE, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock, } from './request-artifact-state-helpers.js';
|
|
328
337
|
export { allowedStatesForRole, FileSizeViolationError, LintGateError, PrerequisitesNotSatisfiedError, TypeSanityViolationError, updateStatusBlock } from './request-artifact-state-helpers.js';
|
|
329
338
|
export async function transitionRequestArtifact(options) {
|
|
330
339
|
if (!VALID_ROLES.has(options.role)) {
|
|
@@ -35,6 +35,20 @@ const ABSOLUTE_PATH_WINDOWS = /[A-Z]:\\[\w\\]/;
|
|
|
35
35
|
const ABSOLUTE_PATH_UNIX = /\/usr\/(?:local|bin|opt)\b/;
|
|
36
36
|
const MAGIC_NUMBER = /\b(\d{3,})\b/;
|
|
37
37
|
const LOAD_STRATEGY_PATTERN = /loadStrategy:\s*(always|on-demand)/i;
|
|
38
|
+
/**
|
|
39
|
+
* PRD-002b slice 2 — extract cross-cutting thresholds used by
|
|
40
|
+
* multiple enforcers (file-size cap, magic-number digit floor).
|
|
41
|
+
* Per-rule spec literals (≤800 lines, ≤12 H2 headings, ≤10 magic
|
|
42
|
+
* digits, top-N aggregation cap) are kept inline because they ARE
|
|
43
|
+
* the rule spec; extracting them would change the rule semantics to
|
|
44
|
+
* depend on a constant with no semantic gain. Names here describe
|
|
45
|
+
* the role only.
|
|
46
|
+
*/
|
|
47
|
+
const KB_PER_FILE = 1024;
|
|
48
|
+
const LOAD_STRATEGY_FILE_SIZE_KB = 5;
|
|
49
|
+
const OVERVIEW_TOP_SEARCH_LINES = 30;
|
|
50
|
+
const OVERVIEW_MIN_LINE_COUNT = 200;
|
|
51
|
+
const LOAD_STRATEGY_PROBE_LINES = 20;
|
|
38
52
|
function findLine(lines, pattern) {
|
|
39
53
|
for (let i = 0; i < lines.length; i += 1) {
|
|
40
54
|
if (pattern.test(lines[i] ?? ''))
|
|
@@ -129,9 +143,9 @@ export function lintH2CountLe12(ref) {
|
|
|
129
143
|
return [hit('rl-ref-h2-count-le-12-001', 'at most 12 `## <heading>` per reference', ref.path, 1, `(h2 count ${matches.length} > 12)`)];
|
|
130
144
|
}
|
|
131
145
|
export function lintOverviewNearTop(ref) {
|
|
132
|
-
if (ref.lines.length <=
|
|
146
|
+
if (ref.lines.length <= OVERVIEW_MIN_LINE_COUNT)
|
|
133
147
|
return [];
|
|
134
|
-
if (findLine(ref.lines.slice(0,
|
|
148
|
+
if (findLine(ref.lines.slice(0, OVERVIEW_TOP_SEARCH_LINES), OVERVIEW_HEADING) !== -1)
|
|
135
149
|
return [];
|
|
136
150
|
return [hit('rl-ref-overview-section-near-top-001', 'long references (>200 lines) must have `## Overview` within the first 30 lines', ref.path, 1, '(missing `## Overview` near top of long reference)')];
|
|
137
151
|
}
|
|
@@ -149,7 +163,7 @@ export function lintLoadStrategyAlwaysCacheable(ref) {
|
|
|
149
163
|
// Heuristic: a loadStrategy: always reference must not have a
|
|
150
164
|
// top-level shell command. This is a soft check — we look at
|
|
151
165
|
// the first 10 non-frontmatter lines for I/O patterns.
|
|
152
|
-
const lines = ref.lines.slice(0,
|
|
166
|
+
const lines = ref.lines.slice(0, LOAD_STRATEGY_PROBE_LINES);
|
|
153
167
|
for (const line of lines) {
|
|
154
168
|
if (/^\s*(npm|pnpm|yarn|npx|git|curl|wget|docker)\s+/.test(line)) {
|
|
155
169
|
return [hit('rl-ref-loadstrategy-always-cacheable-001', 'loadStrategy: always references must not run I/O at top of file', ref.path, 1, `(I/O pattern at top of file: ${line.trim()})`)];
|
|
@@ -249,7 +263,7 @@ export function lintSkillCitesEveryReference(ref, skill) {
|
|
|
249
263
|
}
|
|
250
264
|
export function lintLoadStrategyMatchesSize(ref) {
|
|
251
265
|
const sizeBytes = Buffer.byteLength(ref.body, 'utf8');
|
|
252
|
-
if (sizeBytes <=
|
|
266
|
+
if (sizeBytes <= LOAD_STRATEGY_FILE_SIZE_KB * KB_PER_FILE)
|
|
253
267
|
return [];
|
|
254
268
|
// >5KB file should declare loadStrategy: on-demand (always
|
|
255
269
|
// is a context-budget bug).
|
|
@@ -36,6 +36,14 @@ import { describeMode, thresholdFor } from './auto-compact-modes.js';
|
|
|
36
36
|
import { read24hState } from '../24h-mode/store.js';
|
|
37
37
|
import { readCompactLifecycle, writeCompactLifecycle } from '../compact-statusline/compact-lifecycle-store.js';
|
|
38
38
|
const PRE_COMPACT_REASON = 'pre-compact-auto';
|
|
39
|
+
/**
|
|
40
|
+
* PRD-002b slice 2 — extract the few cross-cutting magic numbers
|
|
41
|
+
* that actually appear at runtime call sites in this orchestrator.
|
|
42
|
+
* Threshold ratios (0.85 / 0.95) live in `auto-compact-types.ts`
|
|
43
|
+
* and are NOT extracted here because they ARE the contract; comment
|
|
44
|
+
* + spec prose references must keep the literal value visible.
|
|
45
|
+
*/
|
|
46
|
+
const COLLAPSED_ERROR_MAX_CHARS = 160;
|
|
39
47
|
/**
|
|
40
48
|
* Map a context ratio to a `CompactTrigger` action. Pure; the side
|
|
41
49
|
* effects (checkpoint + IDE dispatch) live in `runAutoCompact`. Two
|
|
@@ -409,7 +417,7 @@ function summarizeLifecycleError(error) {
|
|
|
409
417
|
raw = 'unknown error';
|
|
410
418
|
const firstLine = raw.split('\n')[0] ?? '';
|
|
411
419
|
const collapsed = firstLine.replace(/\s+/g, ' ').trim();
|
|
412
|
-
return collapsed.length === 0 ? 'unknown error' : (collapsed.length >
|
|
420
|
+
return collapsed.length === 0 ? 'unknown error' : (collapsed.length > COLLAPSED_ERROR_MAX_CHARS ? collapsed.slice(0, COLLAPSED_ERROR_MAX_CHARS) : collapsed);
|
|
413
421
|
}
|
|
414
422
|
/**
|
|
415
423
|
* Slice 2026-08-01-compact-lifecycle (Task 5, Step 4): close out an
|