peaks-loop 4.0.33 → 4.0.34
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 +20 -0
- package/dist/cli/commands/code-commands.js +2 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -3
- package/dist/cli/commands/code-mode-status-command.d.ts +17 -0
- package/dist/cli/commands/code-mode-status-command.js +51 -0
- package/dist/cli/commands/code-run-command.js +8 -1
- package/dist/cli/commands/code-runtime-commands.js +13 -5
- package/dist/cli/commands/core/skill-command.js +1 -1
- package/dist/cli/commands/request-commands.js +3 -7
- package/dist/cli/commands/session-24h-mode.js +9 -1
- package/dist/services/24h-mode/auto-engage.d.ts +39 -0
- package/dist/services/24h-mode/auto-engage.js +46 -0
- package/dist/services/24h-mode/decider.d.ts +11 -5
- package/dist/services/24h-mode/decider.js +13 -13
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +3 -1
- package/dist/services/audit/enforcers/active-skill-resolver.js +3 -5
- package/dist/services/code/auto-compact-lifecycle.d.ts +1 -1
- package/dist/services/code/auto-compact-lifecycle.js +1 -1
- package/dist/services/code/auto-compact-modes.d.ts +1 -1
- package/dist/services/code/auto-compact-modes.js +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +18 -22
- package/dist/services/code/mode-gate.d.ts +6 -2
- package/dist/services/code/mode-gate.js +9 -5
- package/dist/services/code/orchestrator-can-do.js +2 -2
- package/dist/services/config/config-service.d.ts +17 -0
- package/dist/services/config/config-service.js +34 -0
- package/dist/services/context/auto-compact-dispatcher.d.ts +2 -2
- package/dist/services/context/auto-compact-dispatcher.js +4 -4
- package/dist/services/context/auto-compact-reader.js +8 -1
- package/dist/services/context/auto-compact-types.d.ts +13 -1
- package/dist/services/context/build-dispatch-system-prompt.js +2 -2
- package/dist/services/context/context-schema.d.ts +1 -1
- package/dist/services/hooks/auto-compact-hook-install.d.ts +2 -2
- package/dist/services/hooks/auto-compact-hook-install.js +2 -2
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +51 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +84 -14
- package/dist/services/ide/ide-types.d.ts +10 -1
- package/dist/services/mode/mode-enforcement.d.ts +4 -1
- package/dist/services/mode/mode-enforcement.js +26 -28
- package/dist/services/mode/mode-status-service.d.ts +81 -0
- package/dist/services/mode/mode-status-service.js +108 -0
- package/dist/services/skills/presence-lease-service.d.ts +21 -0
- package/dist/services/skills/presence-lease-service.js +26 -0
- package/dist/services/skills/skill-presence-service.d.ts +10 -1
- package/dist/services/skills/skill-presence-service.js +32 -4
- package/dist/services/skills/skill-statusline-service.js +6 -3
- package/package.json +5 -5
- package/skills/peaks-code/SKILL.md +9 -9
- package/skills/peaks-code/references/mode-selection.md +7 -3
- package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
- package/skills/peaks-code/references/runbook.md +10 -5
- package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
- package/skills/peaks-code/references/standards-preflight.md +1 -1
- package/skills/peaks-code/references/startup-sequence.md +1 -1
- package/skills/peaks-code/references/step-0-8-gate.md +1 -1
- package/skills/peaks-code/references/swarm-dispatch-contract.md +2 -2
- package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as readline from 'node:readline';
|
|
2
1
|
import { getSkillPresence } from '../skills/skill-presence-service.js';
|
|
3
2
|
const ASSISTED_CONFIRM_TRANSITIONS = new Set([
|
|
4
3
|
'prd:confirmed-by-user',
|
|
@@ -6,7 +5,9 @@ const ASSISTED_CONFIRM_TRANSITIONS = new Set([
|
|
|
6
5
|
'qa:verdict-issued'
|
|
7
6
|
]);
|
|
8
7
|
export function requiresConfirmation(mode, transitionKey) {
|
|
9
|
-
|
|
8
|
+
// Slice 2026-09-09-mode-consolidation: `swarm` removed as a mode; the
|
|
9
|
+
// two auto-proceed peers are `full-auto` and `24h`.
|
|
10
|
+
if (mode === 'full-auto' || mode === '24h') {
|
|
10
11
|
return false;
|
|
11
12
|
}
|
|
12
13
|
if (mode === 'strict') {
|
|
@@ -22,12 +23,23 @@ function describeTransition(transitionKey) {
|
|
|
22
23
|
return `Transition ${role.toUpperCase()} → ${state}`;
|
|
23
24
|
}
|
|
24
25
|
export class ConfirmationRequiredError extends Error {
|
|
25
|
-
|
|
26
|
+
transitionKey;
|
|
27
|
+
mode;
|
|
28
|
+
nextActions;
|
|
29
|
+
constructor(transitionKey, mode) {
|
|
26
30
|
const description = describeTransition(transitionKey);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'
|
|
31
|
+
const nextActions = [
|
|
32
|
+
`Ask the user via AskUserQuestion whether to proceed with ${description} in ${mode} mode.`,
|
|
33
|
+
'If the user approves, re-run the same command with --confirm.'
|
|
34
|
+
];
|
|
35
|
+
super(`Confirmation required for: ${description} (mode: ${mode})\n` +
|
|
36
|
+
`${nextActions[0]}\n` +
|
|
37
|
+
`${nextActions[1]}\n` +
|
|
38
|
+
'No terminal prompt is available: this gate never reads stdin.');
|
|
30
39
|
this.name = 'ConfirmationRequiredError';
|
|
40
|
+
this.transitionKey = transitionKey;
|
|
41
|
+
this.mode = mode;
|
|
42
|
+
this.nextActions = nextActions;
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
45
|
export async function requireUserConfirmation(options) {
|
|
@@ -40,11 +52,11 @@ export async function requireUserConfirmation(options) {
|
|
|
40
52
|
if (!requiresConfirmation(mode, options.transitionKey)) {
|
|
41
53
|
return;
|
|
42
54
|
}
|
|
43
|
-
// --confirm flag bypasses
|
|
55
|
+
// --confirm flag bypasses the gate
|
|
44
56
|
if (options.confirmed) {
|
|
45
57
|
return;
|
|
46
58
|
}
|
|
47
|
-
// PEAKS_AUTO_CONFIRM=1 only works for full-auto/
|
|
59
|
+
// PEAKS_AUTO_CONFIRM=1 only works for full-auto/24h (already returned above)
|
|
48
60
|
// For assisted/strict, env var is ignored unless --force-confirm is also set
|
|
49
61
|
if (process.env.PEAKS_AUTO_CONFIRM === '1') {
|
|
50
62
|
if (options.forceConfirm) {
|
|
@@ -53,7 +65,7 @@ export async function requireUserConfirmation(options) {
|
|
|
53
65
|
'This bypasses user confirmation. Use with caution.');
|
|
54
66
|
return;
|
|
55
67
|
}
|
|
56
|
-
throw new ConfirmationRequiredError(options.transitionKey);
|
|
68
|
+
throw new ConfirmationRequiredError(options.transitionKey, mode);
|
|
57
69
|
}
|
|
58
70
|
// --force-confirm without env var
|
|
59
71
|
if (options.forceConfirm) {
|
|
@@ -62,23 +74,9 @@ export async function requireUserConfirmation(options) {
|
|
|
62
74
|
'This bypasses user confirmation. Use with caution.');
|
|
63
75
|
return;
|
|
64
76
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return new Promise((resolve, reject) => {
|
|
71
|
-
const description = describeTransition(options.transitionKey);
|
|
72
|
-
const prompt = `\n[CONFIRM] ${description}\nProceed? (y/N) `;
|
|
73
|
-
rl.question(prompt, (answer) => {
|
|
74
|
-
rl.close();
|
|
75
|
-
const normalized = answer.trim().toLowerCase();
|
|
76
|
-
if (normalized === 'y' || normalized === 'yes') {
|
|
77
|
-
resolve();
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
reject(new ConfirmationRequiredError(options.transitionKey));
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
77
|
+
// No bypass flag: refuse immediately. Never read stdin — in an LLM-driven
|
|
78
|
+
// session there is no TTY, so a prompt would hang forever, and a `y/N`
|
|
79
|
+
// terminal prompt would also violate the Human-NL-Choice-Only rule (the
|
|
80
|
+
// user answers via AskUserQuestion, never by typing into a shell).
|
|
81
|
+
throw new ConfirmationRequiredError(options.transitionKey, mode);
|
|
84
82
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-09-09-mode-consolidation (Slice D): one read for the whole
|
|
3
|
+
* stacked mode picture.
|
|
4
|
+
*
|
|
5
|
+
* The autonomy model has two layers that used to be read by nothing in
|
|
6
|
+
* common:
|
|
7
|
+
*
|
|
8
|
+
* - MODE — `SkillPresenceMode` on the presence lease: the autonomy
|
|
9
|
+
* level (`full-auto | assisted | strict | 24h`).
|
|
10
|
+
* - STATE — `.peaks/_runtime/<sid>/24h-state.json`, the 6-state
|
|
11
|
+
* machine (`IDLE / BRAINSTORM / USER_CONFIRM / 24H_ACTIVE /
|
|
12
|
+
* WAITING_USER / HANDOFF`): the long-run PHASE *inside* the `24h`
|
|
13
|
+
* mode.
|
|
14
|
+
*
|
|
15
|
+
* This module is the single place that reads both, plus the Job-mode
|
|
16
|
+
* decision and the resolved auto-compact profile, so `peaks code mode
|
|
17
|
+
* status` (and any future consumer) does not re-derive the stack.
|
|
18
|
+
*
|
|
19
|
+
* Read-only + fail-soft: every sub-read is wrapped so a missing or
|
|
20
|
+
* malformed artifact degrades to `null` / `false` rather than throwing.
|
|
21
|
+
*/
|
|
22
|
+
import { type SkillPresenceMode } from '../skills/skill-presence-service.js';
|
|
23
|
+
import type { State24hSnapshot } from '../24h-mode/state.js';
|
|
24
|
+
import { type AutoCompactMode } from '../code/auto-compact-modes.js';
|
|
25
|
+
export interface ResolvedPresenceMode {
|
|
26
|
+
readonly mode: SkillPresenceMode | null;
|
|
27
|
+
readonly skill: string | null;
|
|
28
|
+
readonly source: 'canonical' | 'file' | 'env' | 'none';
|
|
29
|
+
}
|
|
30
|
+
export interface ModeStatus {
|
|
31
|
+
/** Autonomy level from the presence lease; `null` when no lease / no mode recorded. */
|
|
32
|
+
readonly mode: SkillPresenceMode | null;
|
|
33
|
+
/** Active skill name from the same lease read (display aid). */
|
|
34
|
+
readonly skill: string | null;
|
|
35
|
+
/** Where the mode came from. */
|
|
36
|
+
readonly modeSource: 'canonical' | 'file' | 'env' | 'none';
|
|
37
|
+
/** `true` when the resolved mode is `24h`. */
|
|
38
|
+
readonly is24h: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* The 24h internal state machine snapshot. Populated only when
|
|
41
|
+
* `is24h` is true (the phase is meaningless outside the 24h mode);
|
|
42
|
+
* `null` when the mode is not `24h` or the snapshot is unreadable.
|
|
43
|
+
*/
|
|
44
|
+
readonly h24State: State24hSnapshot | null;
|
|
45
|
+
/** Job-mode flag from `job-shape.json` (`false` when undecided / unreadable). */
|
|
46
|
+
readonly jobMode: boolean;
|
|
47
|
+
/** Resolved auto-compact profile: `partial` iff the mode is `24h`. */
|
|
48
|
+
readonly autoCompactProfile: AutoCompactMode;
|
|
49
|
+
/** Thresholds backing `autoCompactProfile` (display aid). */
|
|
50
|
+
readonly autoCompactThresholds: {
|
|
51
|
+
readonly autoFire: number;
|
|
52
|
+
readonly preCompact: number;
|
|
53
|
+
readonly redLine: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read the active presence mode via the canonical lease projection.
|
|
58
|
+
* Never throws.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolvePresenceMode(projectRoot: string): ResolvedPresenceMode;
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the auto-compact profile from the MODE, not from the 24h
|
|
63
|
+
* state machine. `24h` mode → `partial` (earlier thresholds for
|
|
64
|
+
* long-run sessions); every other mode → `standard`.
|
|
65
|
+
*
|
|
66
|
+
* Slice 2026-09-09-mode-consolidation: this replaced the previous
|
|
67
|
+
* `24h-state.state === '24H_ACTIVE'` mapping. The state machine is the
|
|
68
|
+
* long-run *phase*; the mode is the autonomy level the user (or the
|
|
69
|
+
* T3/T4 auto-engage) selected. Keying off the mode means a `24h`
|
|
70
|
+
* session keeps the partial profile even while it is in
|
|
71
|
+
* `WAITING_USER`, which is the correct long-run behaviour.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveAutoCompactProfile(projectRoot: string): AutoCompactMode;
|
|
74
|
+
/**
|
|
75
|
+
* Assemble the full stacked status. `sessionId` defaults to the
|
|
76
|
+
* canonical binding when omitted.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveModeStatus(input: {
|
|
79
|
+
projectRoot: string;
|
|
80
|
+
sessionId?: string | null | undefined;
|
|
81
|
+
}): ModeStatus;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-09-09-mode-consolidation (Slice D): one read for the whole
|
|
3
|
+
* stacked mode picture.
|
|
4
|
+
*
|
|
5
|
+
* The autonomy model has two layers that used to be read by nothing in
|
|
6
|
+
* common:
|
|
7
|
+
*
|
|
8
|
+
* - MODE — `SkillPresenceMode` on the presence lease: the autonomy
|
|
9
|
+
* level (`full-auto | assisted | strict | 24h`).
|
|
10
|
+
* - STATE — `.peaks/_runtime/<sid>/24h-state.json`, the 6-state
|
|
11
|
+
* machine (`IDLE / BRAINSTORM / USER_CONFIRM / 24H_ACTIVE /
|
|
12
|
+
* WAITING_USER / HANDOFF`): the long-run PHASE *inside* the `24h`
|
|
13
|
+
* mode.
|
|
14
|
+
*
|
|
15
|
+
* This module is the single place that reads both, plus the Job-mode
|
|
16
|
+
* decision and the resolved auto-compact profile, so `peaks code mode
|
|
17
|
+
* status` (and any future consumer) does not re-derive the stack.
|
|
18
|
+
*
|
|
19
|
+
* Read-only + fail-soft: every sub-read is wrapped so a missing or
|
|
20
|
+
* malformed artifact degrades to `null` / `false` rather than throwing.
|
|
21
|
+
*/
|
|
22
|
+
import { resolveActiveSkillForCaller } from '../audit/enforcers/active-skill-resolver.js';
|
|
23
|
+
import { getCurrentSessionId } from '../skills/skill-presence-service.js';
|
|
24
|
+
// Import the 24h store directly (NOT the `24h-mode/index.js` barrel) —
|
|
25
|
+
// the barrel re-exports `decider.ts`, which imports this module, and
|
|
26
|
+
// that would form an import cycle.
|
|
27
|
+
import { read24hState } from '../24h-mode/store.js';
|
|
28
|
+
import { readJobShapeDecision } from '../code/job-shape-decision.js';
|
|
29
|
+
import { AUTO_COMPACT_THRESHOLDS } from '../code/auto-compact-modes.js';
|
|
30
|
+
/**
|
|
31
|
+
* Read the active presence mode via the canonical lease projection.
|
|
32
|
+
* Never throws.
|
|
33
|
+
*/
|
|
34
|
+
export function resolvePresenceMode(projectRoot) {
|
|
35
|
+
try {
|
|
36
|
+
const resolved = resolveActiveSkillForCaller(projectRoot);
|
|
37
|
+
const mode = resolved.mode;
|
|
38
|
+
return {
|
|
39
|
+
mode: mode === null ? null : mode,
|
|
40
|
+
skill: resolved.skill,
|
|
41
|
+
source: resolved.source
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return { mode: null, skill: null, source: 'none' };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the auto-compact profile from the MODE, not from the 24h
|
|
50
|
+
* state machine. `24h` mode → `partial` (earlier thresholds for
|
|
51
|
+
* long-run sessions); every other mode → `standard`.
|
|
52
|
+
*
|
|
53
|
+
* Slice 2026-09-09-mode-consolidation: this replaced the previous
|
|
54
|
+
* `24h-state.state === '24H_ACTIVE'` mapping. The state machine is the
|
|
55
|
+
* long-run *phase*; the mode is the autonomy level the user (or the
|
|
56
|
+
* T3/T4 auto-engage) selected. Keying off the mode means a `24h`
|
|
57
|
+
* session keeps the partial profile even while it is in
|
|
58
|
+
* `WAITING_USER`, which is the correct long-run behaviour.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveAutoCompactProfile(projectRoot) {
|
|
61
|
+
return resolvePresenceMode(projectRoot).mode === '24h' ? 'partial' : 'standard';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Assemble the full stacked status. `sessionId` defaults to the
|
|
65
|
+
* canonical binding when omitted.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveModeStatus(input) {
|
|
68
|
+
const { projectRoot } = input;
|
|
69
|
+
const presence = resolvePresenceMode(projectRoot);
|
|
70
|
+
const is24h = presence.mode === '24h';
|
|
71
|
+
let sessionId = input.sessionId ?? null;
|
|
72
|
+
if (sessionId === null) {
|
|
73
|
+
try {
|
|
74
|
+
sessionId = getCurrentSessionId(projectRoot);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
sessionId = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let h24State = null;
|
|
81
|
+
if (is24h && sessionId !== null) {
|
|
82
|
+
try {
|
|
83
|
+
h24State = read24hState(projectRoot, sessionId);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
h24State = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let jobMode = false;
|
|
90
|
+
if (sessionId !== null) {
|
|
91
|
+
try {
|
|
92
|
+
jobMode = readJobShapeDecision(projectRoot, sessionId).decision.isJob;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
jobMode = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
mode: presence.mode,
|
|
100
|
+
skill: presence.skill,
|
|
101
|
+
modeSource: presence.source,
|
|
102
|
+
is24h,
|
|
103
|
+
h24State,
|
|
104
|
+
jobMode,
|
|
105
|
+
autoCompactProfile: is24h ? 'partial' : 'standard',
|
|
106
|
+
autoCompactThresholds: AUTO_COMPACT_THRESHOLDS[is24h ? 'partial' : 'standard']
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -66,6 +66,27 @@ export interface GcPresenceLeasesInput {
|
|
|
66
66
|
readonly trigger?: 'manual' | 'workspace-init' | 'presence-set';
|
|
67
67
|
}
|
|
68
68
|
export declare function setPresenceLease(input: SetPresenceLeaseInput): SetPresenceLeaseResult;
|
|
69
|
+
/**
|
|
70
|
+
* Slice 2026-09-09-mode-consolidation (Slice B): stamp `mode` onto every
|
|
71
|
+
* in-flight lease of the bound session, in place.
|
|
72
|
+
*
|
|
73
|
+
* Used by the 24h auto-engage path (T3/T4, `peaks code run --24h`,
|
|
74
|
+
* `peaks session 24h-mode transition --state 24H_ACTIVE`) to record the
|
|
75
|
+
* autonomy level without re-creating the lease (a fresh `setPresenceLease`
|
|
76
|
+
* would reset `startedAt` / `status`). `lastHeartbeat` is intentionally
|
|
77
|
+
* NOT touched — stamping a mode is not a liveness signal.
|
|
78
|
+
*
|
|
79
|
+
* Returns the number of leases updated; 0 when there is nothing in flight.
|
|
80
|
+
* Callers are responsible for the "only `24h` may be auto-set" guard.
|
|
81
|
+
*/
|
|
82
|
+
export declare function stampPresenceLeaseMode(input: {
|
|
83
|
+
readonly projectRoot: string;
|
|
84
|
+
readonly sessionId: string;
|
|
85
|
+
readonly mode: string;
|
|
86
|
+
}): {
|
|
87
|
+
updated: number;
|
|
88
|
+
leasePaths: string[];
|
|
89
|
+
};
|
|
69
90
|
export declare function readPresenceLease(input: ReadPresenceLeaseInput): PresenceProjection;
|
|
70
91
|
export declare function markPresenceLost(input: MarkPresenceLostInput): SkillPresenceLease;
|
|
71
92
|
export declare function gcStalePresenceLeases(input: GcPresenceLeasesInput): GcResult;
|
|
@@ -175,6 +175,32 @@ export function setPresenceLease(input) {
|
|
|
175
175
|
gc,
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Slice 2026-09-09-mode-consolidation (Slice B): stamp `mode` onto every
|
|
180
|
+
* in-flight lease of the bound session, in place.
|
|
181
|
+
*
|
|
182
|
+
* Used by the 24h auto-engage path (T3/T4, `peaks code run --24h`,
|
|
183
|
+
* `peaks session 24h-mode transition --state 24H_ACTIVE`) to record the
|
|
184
|
+
* autonomy level without re-creating the lease (a fresh `setPresenceLease`
|
|
185
|
+
* would reset `startedAt` / `status`). `lastHeartbeat` is intentionally
|
|
186
|
+
* NOT touched — stamping a mode is not a liveness signal.
|
|
187
|
+
*
|
|
188
|
+
* Returns the number of leases updated; 0 when there is nothing in flight.
|
|
189
|
+
* Callers are responsible for the "only `24h` may be auto-set" guard.
|
|
190
|
+
*/
|
|
191
|
+
export function stampPresenceLeaseMode(input) {
|
|
192
|
+
const sessionId = validateSessionId(input.sessionId);
|
|
193
|
+
const leasePaths = [];
|
|
194
|
+
for (const lease of listPresenceLeases(input.projectRoot, sessionId)) {
|
|
195
|
+
if (lease.status !== 'preparing' && lease.status !== 'running')
|
|
196
|
+
continue;
|
|
197
|
+
const path = leaseFilePath(input.projectRoot, sessionId, lease.callerId, lease.workflowId);
|
|
198
|
+
const next = { ...lease, mode: input.mode };
|
|
199
|
+
writeAtomic(path, JSON.stringify(next, null, 2));
|
|
200
|
+
leasePaths.push(path);
|
|
201
|
+
}
|
|
202
|
+
return { updated: leasePaths.length, leasePaths };
|
|
203
|
+
}
|
|
178
204
|
export function readPresenceLease(input) {
|
|
179
205
|
const sessionId = validateSessionId(input.sessionId ?? null);
|
|
180
206
|
const callerId = validateCallerId(input.callerId);
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
export type SkillPresenceMode = 'full-auto' | 'assisted' | '
|
|
1
|
+
export type SkillPresenceMode = 'full-auto' | 'assisted' | 'strict' | '24h';
|
|
2
2
|
export declare const VALID_SKILL_PRESENCE_MODES: ReadonlyArray<SkillPresenceMode>;
|
|
3
|
+
export declare const LEGACY_SKILL_PRESENCE_MODE_ALIASES: Readonly<Record<string, SkillPresenceMode>>;
|
|
3
4
|
export declare function isSkillPresenceMode(value: string): value is SkillPresenceMode;
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a raw on-disk / CLI mode token to a current
|
|
7
|
+
* `SkillPresenceMode`. Current values pass through; the legacy
|
|
8
|
+
* `'swarm'` alias maps to `'full-auto'`; anything else returns
|
|
9
|
+
* `undefined` so the caller can decide (drop the field, not the
|
|
10
|
+
* presence). Never throws.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeSkillPresenceMode(value: string | undefined | null): SkillPresenceMode | undefined;
|
|
4
13
|
export type SkillPresence = {
|
|
5
14
|
skill: string;
|
|
6
15
|
mode?: SkillPresenceMode;
|
|
@@ -20,12 +20,33 @@ void null;
|
|
|
20
20
|
export const VALID_SKILL_PRESENCE_MODES = [
|
|
21
21
|
'full-auto',
|
|
22
22
|
'assisted',
|
|
23
|
-
'
|
|
24
|
-
'
|
|
23
|
+
'strict',
|
|
24
|
+
'24h'
|
|
25
25
|
];
|
|
26
|
+
// Migration note (slice 2026-09-09-mode-consolidation): `swarm` was REMOVED
|
|
27
|
+
// as a mode — parallel fan-out is now the default execution strategy in
|
|
28
|
+
// every mode. A legacy on-disk `mode: 'swarm'` is normalized on read to
|
|
29
|
+
// `'full-auto'` (never crash, never drop the presence).
|
|
30
|
+
export const LEGACY_SKILL_PRESENCE_MODE_ALIASES = {
|
|
31
|
+
swarm: 'full-auto'
|
|
32
|
+
};
|
|
26
33
|
export function isSkillPresenceMode(value) {
|
|
27
34
|
return VALID_SKILL_PRESENCE_MODES.includes(value);
|
|
28
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a raw on-disk / CLI mode token to a current
|
|
38
|
+
* `SkillPresenceMode`. Current values pass through; the legacy
|
|
39
|
+
* `'swarm'` alias maps to `'full-auto'`; anything else returns
|
|
40
|
+
* `undefined` so the caller can decide (drop the field, not the
|
|
41
|
+
* presence). Never throws.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeSkillPresenceMode(value) {
|
|
44
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
45
|
+
return undefined;
|
|
46
|
+
if (isSkillPresenceMode(value))
|
|
47
|
+
return value;
|
|
48
|
+
return LEGACY_SKILL_PRESENCE_MODE_ALIASES[value];
|
|
49
|
+
}
|
|
29
50
|
/**
|
|
30
51
|
* The current outer session id, exposed to Bash tool calls via the
|
|
31
52
|
* `PEAKS_OUTER_SESSION_ID` environment variable. Stamping it onto the
|
|
@@ -118,8 +139,15 @@ function readSkillPresenceFromLease(projectRootOverride) {
|
|
|
118
139
|
// The lease carries `callerId` (the adapter / harness session id),
|
|
119
140
|
// not the legacy `outerSessionId` field. Project it for back-compat
|
|
120
141
|
// with `clearStalePresenceOnRotation`'s outer-mismatch check.
|
|
142
|
+
//
|
|
143
|
+
// Slice 2026-09-09-mode-consolidation: `mode` is projected too (it was
|
|
144
|
+
// silently dropped before, which made `getSkillPresence().mode` always
|
|
145
|
+
// undefined and left every `presence.mode` consumer dead). Normalize on
|
|
146
|
+
// read so a legacy `'swarm'` lease surfaces as `'full-auto'`.
|
|
147
|
+
const leaseMode = normalizeSkillPresenceMode(lease.mode);
|
|
121
148
|
return {
|
|
122
149
|
skill: lease.skill,
|
|
150
|
+
...(leaseMode !== undefined ? { mode: leaseMode } : {}),
|
|
123
151
|
sessionId,
|
|
124
152
|
outerSessionId: lease.callerId,
|
|
125
153
|
setAt: lease.startedAt,
|
|
@@ -208,7 +236,7 @@ function getPreviousOuterSessionId(projectRootOverride) {
|
|
|
208
236
|
* `peakSessionId`, OR via `ensureSession` for the first-time case).
|
|
209
237
|
*/
|
|
210
238
|
export function setSkillPresenceForCaller(projectRootOverride, callerId, peakSessionId, skill, mode, gate) {
|
|
211
|
-
const validatedMode =
|
|
239
|
+
const validatedMode = normalizeSkillPresenceMode(mode);
|
|
212
240
|
const now = new Date().toISOString();
|
|
213
241
|
const presence = {
|
|
214
242
|
skill,
|
|
@@ -257,7 +285,7 @@ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
|
|
|
257
285
|
// the old shape don't break) but we surface a warning to the
|
|
258
286
|
// CLI boundary via the `outerSessionMismatch` field.
|
|
259
287
|
// 3. delegates the canonical write to `setPresenceLease`.
|
|
260
|
-
const validatedMode =
|
|
288
|
+
const validatedMode = normalizeSkillPresenceMode(mode);
|
|
261
289
|
const sessionId = getCurrentSessionId(projectRootOverride);
|
|
262
290
|
const outerSessionId = getCurrentOuterSessionId();
|
|
263
291
|
const previousOuterSessionId = getPreviousOuterSessionId(projectRootOverride);
|
|
@@ -30,6 +30,7 @@ import { readActiveDispatchIndex } from '../dispatch/dispatch-record-writer.js';
|
|
|
30
30
|
* picks the most recent in-flight lease across all callers; this is the
|
|
31
31
|
* documented back-compat path.
|
|
32
32
|
*/
|
|
33
|
+
import { normalizeSkillPresenceMode } from './skill-presence-service.js';
|
|
33
34
|
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
34
35
|
/**
|
|
35
36
|
* Slice rid-statusline-24h-overlay (2026-08-10): name-distinct from
|
|
@@ -245,12 +246,14 @@ function readPresenceReadOnly(projectRoot, callerId) {
|
|
|
245
246
|
}
|
|
246
247
|
// Leases on disk may carry an optional `mode` field (the lease constructor
|
|
247
248
|
// spreads `input.mode` when present); the typed `SkillPresenceLease` does
|
|
248
|
-
// not declare it, so widen the read shape here.
|
|
249
|
-
|
|
249
|
+
// not declare it, so widen the read shape here. Normalized on read: a
|
|
250
|
+
// legacy `'swarm'` lease renders as `'full-auto'` (slice
|
|
251
|
+
// 2026-09-09-mode-consolidation).
|
|
252
|
+
const latestMode = normalizeSkillPresenceMode(latest.mode);
|
|
250
253
|
return {
|
|
251
254
|
presence: {
|
|
252
255
|
skill: latest.skill,
|
|
253
|
-
...(
|
|
256
|
+
...(latestMode !== undefined ? { mode: latestMode } : {}),
|
|
254
257
|
setAt: latest.startedAt,
|
|
255
258
|
},
|
|
256
259
|
invalid: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.34",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
"fzf": "^0.5.2",
|
|
102
102
|
"yaml": "^2.9.0",
|
|
103
103
|
"zod": "^4.4.3",
|
|
104
|
-
"peaks-loop-internal-runtime": "0.0.
|
|
105
|
-
"peaks-loop-shared": "0.0.
|
|
106
|
-
"peaks-loop-
|
|
107
|
-
"peaks-loop-
|
|
104
|
+
"peaks-loop-internal-runtime": "0.0.19",
|
|
105
|
+
"peaks-loop-shared": "0.0.68",
|
|
106
|
+
"peaks-loop-shared-channel": "0.0.36",
|
|
107
|
+
"peaks-loop-mut": "0.1.32"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
110
|
"@changesets/cli": "2.31.1",
|
|
@@ -104,7 +104,7 @@ peaks sub-agent dispatch rd --prompt '<your task>' --graph-node <nid> --workflow
|
|
|
104
104
|
**Integration surfaces:** `peaks code run --24h` / `peaks dashboard long-run --since 24h` / `peaks session 24h-mode state|transition|attempts|reset`.
|
|
105
105
|
|
|
106
106
|
**Red lines (peaks-code side):**
|
|
107
|
-
- No auto-compact prose ban: never write "ask the user to compact" / "prompt the user to run `/compact`" / "the user should run `peaks
|
|
107
|
+
- No auto-compact prose ban: never write "ask the user to compact" / "prompt the user to run `/compact`" / "the user should run `peaks code auto-compact` manually" / "the user is responsible for context management" / legacy 50/75/90 percent tiers. **0.85 / 0.95 contract is mandatory.**
|
|
108
108
|
- SquabbyZ sole-author rule: no `Co-Authored-By: Claude/Anthropic` trailer.
|
|
109
109
|
- 24h mode is a flag on `peaks-code`; MUST NOT introduce a sibling `peaks-24h` skill or competing top-level verb.
|
|
110
110
|
|
|
@@ -151,7 +151,7 @@ Before the first planning action, run `peaks fresh-context preflight --prompt "<
|
|
|
151
151
|
1. **PreToolUse hook — `peaks code gate-step-08`.** Installed by `peaks workspace init` on the `Bash` matcher; checks `job-shape.json` presence + fail-closed backup regex. If `job-shape.json` AND `progress.json` exist, surfaces `Next: slice #N of M (<currentSlice>)` so the LLM cannot wake up cold.
|
|
152
152
|
2. **Size-fear ban — `peaks code emit-handoff`.** Refuses to emit a final handoff while `remaining > 0` under Job mode. Pass `--force-under-job` only with explicit user approval.
|
|
153
153
|
3. **On-disk slice progress — `peaks job progress`.** `peaks job checkpoint --state done` writes `progress.json`. `peaks job progress --job-id <jid> [--allow-missing]` is the canonical reader.
|
|
154
|
-
4. **Forced auto-compact — `--enforce-job-mode`.** `peaks code context-now --enforce-job-mode` returns `action: 'auto-compact-now'` at ≥ 0.85. **Job mode at ≥ 0.85 is MANDATORY auto-compact** — Code MUST call `peaks
|
|
154
|
+
4. **Forced auto-compact — `--enforce-job-mode`.** `peaks code context-now --enforce-job-mode` returns `action: 'auto-compact-now'` at ≥ 0.85. **Job mode at ≥ 0.85 is MANDATORY auto-compact** — Code MUST call `peaks code auto-compact` without confirmation.
|
|
155
155
|
|
|
156
156
|
**Step 0.7 resume rule (read-FIRST):** on resume, `peaks code gate-step-08` reads `progress.json` first and surfaces `Next: slice #N of M (<currentSlice>)` so the orchestrator picks up at the right slice without re-reading the artifact tree.
|
|
157
157
|
|
|
@@ -161,26 +161,26 @@ Before the first planning action, run `peaks fresh-context preflight --prompt "<
|
|
|
161
161
|
|
|
162
162
|
### Peaks-Loop Step N+2: Auto-compact at the warning line (v2.13.0 zero-pause contract)
|
|
163
163
|
|
|
164
|
-
> **Zero-pause contract.** When context usage crosses the pre-compact threshold, peaks-loop **automatically fires `peaks
|
|
164
|
+
> **Zero-pause contract.** When context usage crosses the pre-compact threshold, peaks-loop **automatically fires `peaks code auto-compact` — the LLM does NOT prompt the user to run `/compact` manually**. Stale prose that says "ask the user to compact" silently stalls the workflow. The v2.13.0 contract makes auto-compact a system responsibility, not a user action.
|
|
165
165
|
|
|
166
166
|
**Thresholds (v2.13.0, replacing legacy 50/75/90):**
|
|
167
167
|
|
|
168
168
|
| ratio zone | zone name | action |
|
|
169
169
|
|---|---|---|
|
|
170
170
|
| `< 0.85` | normal | skip — LLM keeps working |
|
|
171
|
-
| `0.85 ≤ ratio < 0.95` | **pre-compact zone** | `peaks
|
|
172
|
-
| `ratio ≥ 0.95` | **red-line (Karpathy §4)** | synchronous gate — `peaks
|
|
171
|
+
| `0.85 ≤ ratio < 0.95` | **pre-compact zone** | `peaks code auto-compact` fires **automatically** (deferred only when in-flight sub-agent batch is running; fires the moment the batch lands). The LLM does not prompt the user. |
|
|
172
|
+
| `ratio ≥ 0.95` | **red-line (Karpathy §4)** | synchronous gate — `peaks code auto-compact` invoked immediately; `peaks code context-now` returns `action: 'red-line'` and refuses to advance until ratio drops below 0.85. **Karpathy §4 automatic exception** — LLM cannot opt out. |
|
|
173
173
|
|
|
174
|
-
**Probe primitive (single source of truth):** `peaks code context-now --json`. Do NOT use `peaks context check --prompt-size` (deprecated, will silently under-report ratio). Returns `{ ratio, action: 'ok' | 'soft-warn' | 'auto-compact-now' | 'red-line' }` — Code reads `action` and dispatches `peaks
|
|
174
|
+
**Probe primitive (single source of truth):** `peaks code context-now --json`. Do NOT use `peaks context check --prompt-size` (deprecated, will silently under-report ratio). Returns `{ ratio, action: 'ok' | 'soft-warn' | 'auto-compact-now' | 'red-line' }` — Code reads `action` and dispatches `peaks code auto-compact` on `auto-compact-now` or `red-line` without user confirmation.
|
|
175
175
|
|
|
176
176
|
**Enforcement layers (defense in depth):**
|
|
177
177
|
1. `src/services/code/auto-compact-orchestrator.ts` — `evaluateAutoCompactDecision` default-returns `shouldCompact: true` for both `pre-compact` and `red-line`. Only deferral is `inFlightBatch.hasInFlightBatch` (D6.e); no LLM/human approval branch.
|
|
178
178
|
2. `--enforce-job-mode` (v3.1.2) — Job mode elevates ≥0.85 to MANDATORY regardless of in-flight batch.
|
|
179
179
|
3. `peaks code gate-step-08` (PreToolUse hook) — surfaces `auto-compact-now` on every Bash call when ratio is in the zone, so the LLM cannot wake up cold and forget.
|
|
180
|
-
4. Karpathy §4 exception: `peaks
|
|
180
|
+
4. Karpathy §4 exception: `peaks code auto-compact` is fired *by the orchestrator*, not by the user. If you find yourself about to write "ask the user to compact" / "prompt the user to run `/compact`", STOP — that is the regression.
|
|
181
181
|
|
|
182
182
|
**Anti-pattern (DO NOT introduce):** any of these strings in skills/* or comments signals the zero-pause contract has been broken:
|
|
183
|
-
- "ask the user to compact" / "prompt the user to run `/compact`" / "the user should run `peaks
|
|
183
|
+
- "ask the user to compact" / "prompt the user to run `/compact`" / "the user should run `peaks code auto-compact` manually" / "the user is responsible for context management" / legacy mid/seventy-five/ninety percent tiers (current is 0.85 / 0.95).
|
|
184
184
|
|
|
185
185
|
If the prose audit (`peaks audit red-lines`) flags any of the above, the slice is **blocked** until the prose is rewritten.
|
|
186
186
|
|
|
@@ -270,7 +270,7 @@ After `peaks-rd` finishes, Code MUST auto-route to `peaks-qa` without waiting fo
|
|
|
270
270
|
|
|
271
271
|
## Default runbook
|
|
272
272
|
|
|
273
|
-
The end-to-end CLI sequence for `full-auto` lives in `references/runbook.md`. `assisted`/`strict` pause at `[CONFIRM]`; `full-auto`/`
|
|
273
|
+
The end-to-end CLI sequence for `full-auto` lives in `references/runbook.md`. `assisted`/`strict` pause at `[CONFIRM]`; `full-auto`/`24h` auto-proceed. At a `[CONFIRM]` gate the CLI never prompts: the transition throws `CONFIRMATION_REQUIRED`, so Code asks the user via `AskUserQuestion` and, on approval, re-runs the same command with `--confirm`. Never expect or ask for a terminal `y/N` prompt.
|
|
274
274
|
|
|
275
275
|
When adding new CLI commands, mirror into `references/runbook.md` and `tests/unit/skill-default-runbook.test.ts` (test falls back to the reference).
|
|
276
276
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> Body of `### Peaks-Loop Step 1`. After Step 0 has anchored the workspace and presence, when the user invokes Peaks-Loop Code without explicitly naming an execution profile, use `AskUserQuestion` to pick the profile. Present the recommended full-auto path as the first/default option with a practical description for each:
|
|
4
4
|
|
|
5
5
|
1. **Full auto (Recommended)** — Peaks-Loop handles planning, role coordination, validation, and compact handoff end-to-end while preserving required confirmation gates for risky or shared-state actions.
|
|
6
|
-
2. **
|
|
7
|
-
3. **
|
|
6
|
+
2. **24h** — Long-run autonomy profile: auto-proceed gates, periodic checkpoints, and the partial auto-compact cadence for overnight / multi-hour runs. It may be auto-engaged by the T1–T5 triggers without a pick.
|
|
7
|
+
3. **Assisted** — Peaks-Loop proposes plans, artifacts, and checks, then pauses for user decisions at major workflow boundaries.
|
|
8
8
|
4. **Strict** — Peaks-Loop uses the most conservative gates: explicit confirmations, strict slice specs, coverage evidence, QA acceptance, and commit boundaries before continuing.
|
|
9
9
|
|
|
10
10
|
Map the user's selection to the `--mode` flag value (used by `peaks skill presence:set`; `presence:set --mode` accepts any string, so the name matches the user-facing label rather than overloading "code" which is also the skill name):
|
|
@@ -12,10 +12,14 @@ Map the user's selection to the `--mode` flag value (used by `peaks skill presen
|
|
|
12
12
|
| User selects | `--mode` value |
|
|
13
13
|
|---|---|
|
|
14
14
|
| Full auto | `full-auto` |
|
|
15
|
+
| 24h | `24h` |
|
|
15
16
|
| Assisted | `assisted` |
|
|
16
|
-
| Swarm | `swarm` |
|
|
17
17
|
| Strict | `strict` |
|
|
18
18
|
|
|
19
|
+
> Note: in `assisted` / `strict`, a workflow-boundary confirmation is resolved by asking the user via `AskUserQuestion` and, on approval, re-running the same CLI command with `--confirm`. The CLI never opens a terminal prompt (no stdin read, no `y/N`) — an LLM-driven session has no TTY.
|
|
20
|
+
|
|
21
|
+
> Note: parallel role/worker fan-out is the default execution strategy in **every** mode — it is not a mode of its own. The old `swarm` profile was removed; a legacy on-disk `mode: 'swarm'` is read as `full-auto`.
|
|
22
|
+
|
|
19
23
|
> Note: `peaks workflow route --mode code|team` is a **different** CLI dimension (code developer vs team flow) and is unrelated to the profile choice here. Do not conflate them.
|
|
20
24
|
|
|
21
25
|
If the user already names a profile in their invocation (e.g. `/peaks-code --full-auto`, "用全自动模式"), skip this question and use the named profile directly.
|
|
@@ -72,10 +72,10 @@ This step is strictly IDE-agnostic. The LLM calls the peaks CLI; no IDE-specific
|
|
|
72
72
|
|
|
73
73
|
## Companion surface — auto-compact (zero-pause)
|
|
74
74
|
|
|
75
|
-
When the runner crosses the v2.13.0 auto-compact thresholds (0.85 pre-compact / 0.95 red-line), `peaks session checkpoint` is NOT enough on its own — the LLM must also fire `peaks
|
|
75
|
+
When the runner crosses the v2.13.0 auto-compact thresholds (0.85 pre-compact / 0.95 red-line), `peaks session checkpoint` is NOT enough on its own — the LLM must also fire `peaks code auto-compact` (or wait for the in-band PreToolUse hook at ratio ≥ 0.95 to compact the runner itself). See:
|
|
76
76
|
|
|
77
|
-
- `peaks
|
|
78
|
-
- `peaks
|
|
77
|
+
- `peaks code auto-compact` — orchestrator that writes the convergence plan + dispatches the compact pathway
|
|
78
|
+
- `peaks code auto-compact` — the PreToolUse hook entrypoint fired by `.claude/settings.local.json` (slice 2026-07-02-auto-compact-zero-pause)
|
|
79
79
|
- `.peaks/memory/2026-06-27-auto-compact-design.md` — zero-human-intervention design rationale
|
|
80
80
|
|
|
81
|
-
The two surfaces compose: `peaks session checkpoint` persists the context state, `peaks
|
|
81
|
+
The two surfaces compose: `peaks session checkpoint` persists the context state, `peaks code auto-compact` collapses the runner's window. The LLM fires both in the 0.85–0.95 zone; the hook fires alone at ≥ 0.95.
|