peaks-loop 4.0.4 → 4.0.6
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 +64 -0
- package/README-en.md +26 -0
- package/README.md +26 -0
- package/dist/cli/commands/_register.js +3 -0
- package/dist/cli/commands/core/skill-command.js +2 -0
- package/dist/cli/commands/dispatch-commands.js +41 -2
- package/dist/cli/commands/e2e-verify.d.ts +47 -0
- package/dist/cli/commands/e2e-verify.js +211 -0
- package/dist/cli/commands/gate-commands.js +17 -4
- package/dist/cli/commands/statusline-commands.d.ts +6 -0
- package/dist/cli/commands/statusline-commands.js +48 -11
- package/dist/cli/commands/sub-agent-shutdown-commands.d.ts +31 -0
- package/dist/cli/commands/sub-agent-shutdown-commands.js +110 -0
- package/dist/cli/commands/worktree-auth-commands.js +27 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +39 -0
- package/dist/services/code/auto-compact-orchestrator.js +358 -41
- package/dist/services/code/post-compact-detector.js +12 -4
- package/dist/services/code/step-08-gate.js +6 -2
- package/dist/services/compact-statusline/compact-lifecycle-store.d.ts +68 -0
- package/dist/services/compact-statusline/compact-lifecycle-store.js +222 -0
- package/dist/services/compact-statusline/compact-statusline-service.d.ts +25 -5
- package/dist/services/compact-statusline/compact-statusline-service.js +244 -54
- package/dist/services/context/auto-compact-reader.js +12 -4
- package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
- package/dist/services/context/build-dispatch-system-prompt.js +33 -2
- package/dist/services/dispatch/conflict-replay.d.ts +32 -0
- package/dist/services/dispatch/conflict-replay.js +44 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +31 -1
- package/dist/services/dispatch/dispatch-record-writer.js +27 -7
- package/dist/services/dispatch/e2e-fixtures.d.ts +18 -0
- package/dist/services/dispatch/e2e-fixtures.js +72 -0
- package/dist/services/dispatch/merge-back-runner.d.ts +18 -0
- package/dist/services/dispatch/merge-back-runner.js +83 -0
- package/dist/services/dispatch/post-merge.d.ts +21 -0
- package/dist/services/dispatch/post-merge.js +13 -0
- package/dist/services/dispatch/service-shutdown.d.ts +22 -0
- package/dist/services/dispatch/service-shutdown.js +51 -0
- package/dist/services/hooks/presence-marker-detector.js +12 -4
- package/dist/services/hooks/worktree-authorization-gate.d.ts +4 -0
- package/dist/services/hooks/worktree-authorization-gate.js +22 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +86 -3
- package/dist/services/skills/skill-statusline-renderer.js +374 -29
- package/dist/services/skills/skill-statusline-service.d.ts +2 -0
- package/dist/services/skills/skill-statusline-service.js +34 -5
- package/dist/services/skills/sync-service.d.ts +3 -0
- package/dist/services/skills/sync-service.js +1 -0
- package/dist/services/standards/project-context.d.ts +2 -1
- package/dist/services/standards/project-context.js +17 -2
- package/dist/services/standards/project-standards-service.d.ts +9 -0
- package/dist/services/standards/project-standards-service.js +26 -0
- package/dist/services/worktree/dispatch-provenance.d.ts +25 -0
- package/dist/services/worktree/dispatch-provenance.js +47 -0
- package/dist/services/worktree/git-worktree-parser.d.ts +8 -0
- package/dist/services/worktree/git-worktree-parser.js +45 -0
- package/dist/services/worktree/host-worktree-reconciler.d.ts +15 -0
- package/dist/services/worktree/host-worktree-reconciler.js +39 -0
- package/dist/services/worktree/long-path-cleanup.d.ts +10 -0
- package/dist/services/worktree/long-path-cleanup.js +38 -0
- package/dist/services/worktree/playwright-profile.d.ts +8 -0
- package/dist/services/worktree/playwright-profile.js +23 -0
- package/dist/services/worktree/worktree-gc-guard.d.ts +8 -0
- package/dist/services/worktree/worktree-gc-guard.js +38 -0
- package/package.json +4 -4
- package/scripts/install-skills.mjs +182 -2
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
import type { StatusLineModel } from './skill-statusline-service.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Pure formatting layer for the Peaks statusLine. Takes the read-only status
|
|
4
|
+
* model and produces the single line Claude Code paints at the bottom of the
|
|
5
|
+
* terminal. Kept separate from the reader so formatting can be tested without
|
|
6
|
+
* touching the filesystem.
|
|
7
|
+
*
|
|
8
|
+
* Capability-aware rendering: three glyph palettes are kept side by side in
|
|
9
|
+
* {@link PALETTES} so the switch from "active" / "idle" / "warning" to a
|
|
10
|
+
* concrete string happens in one place. Adding a fourth capability (e.g. for
|
|
11
|
+
* a future IDE) is a one-table change.
|
|
5
12
|
*/
|
|
6
|
-
export
|
|
13
|
+
export type StatusLineCapability = 'ansi-unicode' | 'unicode' | 'ascii';
|
|
14
|
+
export interface StatusLineRenderOptions {
|
|
15
|
+
readonly capability: StatusLineCapability;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the capability tier from environment and TTY state. Pure and
|
|
19
|
+
* deterministic — no I/O beyond reading the caller-supplied `env` and
|
|
20
|
+
* `isTTY` flag. The CLI delegates to this so the read-only status line
|
|
21
|
+
* does not need to know about ANSI/NO_COLOR semantics.
|
|
22
|
+
*
|
|
23
|
+
* The optional `forced` argument is a TEST SEAM (consumed by direct unit
|
|
24
|
+
* tests, not by the CLI). It lets tests pin a tier without constructing a
|
|
25
|
+
* TTY/NO_COLOR fixture. The CLI does NOT expose any flag that maps to
|
|
26
|
+
* `forced` — the env-driven path is the single first-version runtime
|
|
27
|
+
* source of truth.
|
|
28
|
+
*
|
|
29
|
+
* Adapter-internal env override (NOT a user-facing CLI flag):
|
|
30
|
+
* `PEAKS_STATUSLINE_ASCII` — when set to `1` / `true` / `yes`, the
|
|
31
|
+
* renderer drops to the ASCII palette (no Unicode-extra glyphs, no ANSI).
|
|
32
|
+
* This is an adapter-internal mechanism for the trust boundary described
|
|
33
|
+
* by the two-forms-only / human-NL-choice-only tenets: the user never
|
|
34
|
+
* types a CLI verb to flip palettes, but the adapter (and only the
|
|
35
|
+
* adapter) can set the env var on the user's behalf when its consumer
|
|
36
|
+
* (e.g. a tiny terminal without UTF-8) needs ASCII. We do NOT expose
|
|
37
|
+
* this via a CLI flag.
|
|
38
|
+
*
|
|
39
|
+
* Order of resolution (highest priority first):
|
|
40
|
+
*
|
|
41
|
+
* 1. `forced` argument (test seam only)
|
|
42
|
+
* 2. `PEAKS_STATUSLINE_ASCII` set → `ascii` (adapter-internal override)
|
|
43
|
+
* 3. `isTTY === true` → `ansi-unicode`
|
|
44
|
+
* 4. otherwise → `unicode` (still ANSI-colored; only `ascii` is color-free)
|
|
45
|
+
*
|
|
46
|
+
* Why this ordering: the IDE statusline invokes the renderer through a
|
|
47
|
+
* hook that may not always be detected as a TTY. The `unicode` tier
|
|
48
|
+
* now emits the cyan brand accent + semantic colors so the line reads
|
|
49
|
+
* as a branded product surface whether or not the consumer is a
|
|
50
|
+
* terminal. `NO_COLOR` is honored as an ambient convention by the
|
|
51
|
+
* IDE adapter (which can set `PEAKS_STATUSLINE_ASCII=1` to opt out
|
|
52
|
+
* fully), so a separate tier for NO_COLOR is no longer needed.
|
|
53
|
+
*
|
|
54
|
+
* `ascii` is the strict no-color, no-Unicode-extra tier; it is the
|
|
55
|
+
* single safe option for plain log / file consumers.
|
|
56
|
+
*
|
|
57
|
+
* Why `PEAKS_STATUSLINE_ASCII` outranks `NO_COLOR`:
|
|
58
|
+
* The two env vars are NOT redundant. `NO_COLOR` (https://no-color.org)
|
|
59
|
+
* is the cross-industry signal that NO escape sequences should be emitted
|
|
60
|
+
* — its only effect is "ANSI off". The Unicode-extra glyphs (●, █, ░, etc.)
|
|
61
|
+
* are NOT ANSI escape sequences; they are UTF-8 characters and `NO_COLOR`
|
|
62
|
+
* does not address them. `PEAKS_STATUSLINE_ASCII` is the adapter-internal
|
|
63
|
+
* "signal source is a tiny terminal without UTF-8" override and drops the
|
|
64
|
+
* glyphs to ASCII shape as well. Both are correct signals about
|
|
65
|
+
* different concerns; the ASCII override wins because it is strictly
|
|
66
|
+
* narrower (a tiny terminal needs both no-ANSI AND no-Unicode-extra).
|
|
67
|
+
*
|
|
68
|
+
* The deliberate ordering keeps the rendered text ANSI-free when the consumer
|
|
69
|
+
* is a logger, a file, or any non-interactive sink, and only enables ANSI
|
|
70
|
+
* under an explicit supported condition (TTY + no env veto).
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveStatusLineCapability(input: {
|
|
73
|
+
readonly env: NodeJS.ProcessEnv;
|
|
74
|
+
readonly isTTY: boolean;
|
|
75
|
+
readonly forced?: StatusLineCapability;
|
|
76
|
+
}): StatusLineCapability;
|
|
77
|
+
/**
|
|
78
|
+
* Render the status line. Pure — no I/O, no side effects. The output is
|
|
79
|
+
* a single short line suitable for the bottom-of-terminal status bar.
|
|
80
|
+
*
|
|
81
|
+
* The `options.capability` field selects the glyph palette. When omitted,
|
|
82
|
+
* the function falls back to `unicode` (no ANSI escape codes), preserving
|
|
83
|
+
* the legacy behaviour for the CLI's `peaks statusline` invocation.
|
|
84
|
+
*
|
|
85
|
+
* Compact-state precedence: when `model.compact.kind !== 'none'` the
|
|
86
|
+
* compact bar replaces the active/idle/stale content. The C1 baseline
|
|
87
|
+
* line is preserved when the compact state is `none`.
|
|
88
|
+
*/
|
|
89
|
+
export declare function renderStatusLine(model: StatusLineModel, options?: StatusLineRenderOptions): string;
|
|
@@ -1,11 +1,167 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Internal: the brand prefix is plain ASCII so a tiny terminal without
|
|
4
|
+
* monospaced UTF-8 still sees a recognizable string. The mountain glyphs
|
|
5
|
+
* (`⛰` / `🏔`) used in earlier 1.x renders were the loudest a11y regression
|
|
6
|
+
* in the 2026-07-22 ice-cola surface check and are deliberately removed.
|
|
7
7
|
*/
|
|
8
|
-
const BRAND = '
|
|
8
|
+
const BRAND = 'Peaks';
|
|
9
|
+
/**
|
|
10
|
+
* Attention-gate allowlist. The brief requires conservative classification:
|
|
11
|
+
* only gate names that encode a blocking condition surface as a warning
|
|
12
|
+
* glyph. Routine progress gates (`startup`, `swarm-fan-out`,
|
|
13
|
+
* `swarm-converged`, `repair-cycle-N`, etc.) stay hidden. The map is
|
|
14
|
+
* explicit — there is no "non-empty gate means alert" inference.
|
|
15
|
+
*/
|
|
16
|
+
const ATTENTION_GATE_LABELS = new Map([
|
|
17
|
+
['qa-validation', 'QA'],
|
|
18
|
+
['verdict-issued', 'Verdict'],
|
|
19
|
+
['blocked', 'Blocked'],
|
|
20
|
+
]);
|
|
21
|
+
function isAttentionGate(gate) {
|
|
22
|
+
if (!gate)
|
|
23
|
+
return null;
|
|
24
|
+
return ATTENTION_GATE_LABELS.get(gate) ?? null;
|
|
25
|
+
}
|
|
26
|
+
/** Brand accent: per project request, this is the slate-purple `#5A65D8`. */
|
|
27
|
+
const BRAND_RGB = '38;2;90;101;216';
|
|
28
|
+
const BRAND_SGR_OPEN = `\x1b[1;${BRAND_RGB}m`;
|
|
29
|
+
const BRAND_SGR_FULL_OPEN = `\x1b[${BRAND_RGB}m`;
|
|
30
|
+
const BRAND_SGR_CLOSE = '\x1b[0m';
|
|
31
|
+
function accent(text) {
|
|
32
|
+
return `${BRAND_SGR_OPEN}${text}${BRAND_SGR_CLOSE}`;
|
|
33
|
+
}
|
|
34
|
+
function accentGlyph(glyph) {
|
|
35
|
+
return `${BRAND_SGR_OPEN}${glyph}${BRAND_SGR_CLOSE}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Slow-blink variant of the accent glyph. Used for the idle indicator
|
|
39
|
+
* so the user perceives a pulse on the statusline. The ANSI slow-blink
|
|
40
|
+
* SGR (`\x1b[5m`) is honored by most modern terminals (iTerm2, Windows
|
|
41
|
+
* Terminal, GNOME Terminal with the right profile). Hosts that ignore
|
|
42
|
+
* the SGR render the glyph statically — graceful degradation.
|
|
43
|
+
*/
|
|
44
|
+
function blinkingAccentGlyph(glyph) {
|
|
45
|
+
return `\x1b[5;1;${BRAND_RGB}m${glyph}\x1b[0m`;
|
|
46
|
+
}
|
|
47
|
+
const PALETTES = {
|
|
48
|
+
'ansi-unicode': {
|
|
49
|
+
active: accentGlyph('●'),
|
|
50
|
+
idle: blinkingAccentGlyph('○'),
|
|
51
|
+
warning: '\x1b[33m!\x1b[0m', // amber warning (semantic alarm)
|
|
52
|
+
inlineSeparator: ' · ',
|
|
53
|
+
trailSeparator: ' → ',
|
|
54
|
+
idleLabel: 'empty',
|
|
55
|
+
invalidMessage: 'presence unreadable',
|
|
56
|
+
compact: {
|
|
57
|
+
queued: accentGlyph('◐'),
|
|
58
|
+
preparing: accentGlyph('◑'),
|
|
59
|
+
compacting: accentGlyph('◒'),
|
|
60
|
+
verifying: accentGlyph('◓'),
|
|
61
|
+
completed: accentGlyph('✓'),
|
|
62
|
+
failed: '\x1b[31m✕\x1b[0m', // failed is a semantic alarm
|
|
63
|
+
},
|
|
64
|
+
barFilled: '█',
|
|
65
|
+
barEmpty: '░',
|
|
66
|
+
ratioArrow: '→',
|
|
67
|
+
},
|
|
68
|
+
unicode: {
|
|
69
|
+
active: accentGlyph('●'),
|
|
70
|
+
idle: blinkingAccentGlyph('○'),
|
|
71
|
+
warning: '\x1b[33m!\x1b[0m',
|
|
72
|
+
inlineSeparator: ' · ',
|
|
73
|
+
trailSeparator: ' → ',
|
|
74
|
+
idleLabel: 'empty',
|
|
75
|
+
invalidMessage: 'presence unreadable',
|
|
76
|
+
compact: {
|
|
77
|
+
queued: accentGlyph('◐'),
|
|
78
|
+
preparing: accentGlyph('◑'),
|
|
79
|
+
compacting: accentGlyph('◒'),
|
|
80
|
+
verifying: accentGlyph('◓'),
|
|
81
|
+
completed: accentGlyph('✓'),
|
|
82
|
+
failed: '\x1b[31m✕\x1b[0m',
|
|
83
|
+
},
|
|
84
|
+
barFilled: '█',
|
|
85
|
+
barEmpty: '░',
|
|
86
|
+
ratioArrow: '→',
|
|
87
|
+
},
|
|
88
|
+
ascii: {
|
|
89
|
+
active: '*',
|
|
90
|
+
idle: 'o',
|
|
91
|
+
warning: '!',
|
|
92
|
+
inlineSeparator: ' . ',
|
|
93
|
+
trailSeparator: ' -> ',
|
|
94
|
+
idleLabel: 'empty',
|
|
95
|
+
invalidMessage: 'presence unreadable',
|
|
96
|
+
compact: {
|
|
97
|
+
queued: '[',
|
|
98
|
+
preparing: '+',
|
|
99
|
+
compacting: '+',
|
|
100
|
+
verifying: '+',
|
|
101
|
+
completed: '*',
|
|
102
|
+
failed: 'x',
|
|
103
|
+
},
|
|
104
|
+
barFilled: '#',
|
|
105
|
+
barEmpty: '-',
|
|
106
|
+
ratioArrow: '->',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const BREATHING_GLYPHS_UNICODE = ['●', '◐', '◑', '◒', '◓'];
|
|
110
|
+
const BREATHING_GLYPHS_ASCII = ['*', 'o', '+', '~', '|'];
|
|
111
|
+
const BREATHING_PERIOD_MS = 2_400;
|
|
112
|
+
/**
|
|
113
|
+
* Bee-tier (1-level sub-role) skills and their full orchestrator
|
|
114
|
+
* parent name. The terminal status line shows both layers when a bee
|
|
115
|
+
* is active: the active bee name plus a `↑<parent-full>` marker
|
|
116
|
+
* pointing at the dispatching orchestrator.
|
|
117
|
+
*/
|
|
118
|
+
const BEE_TO_PARENT = {
|
|
119
|
+
'peaks-prd': 'peaks-code',
|
|
120
|
+
'peaks-rd': 'peaks-code',
|
|
121
|
+
'peaks-qa': 'peaks-code',
|
|
122
|
+
'peaks-ui': 'peaks-code',
|
|
123
|
+
'peaks-sc': 'peaks-code',
|
|
124
|
+
'peaks-txt': 'peaks-code',
|
|
125
|
+
'peaks-final-review': 'peaks-code',
|
|
126
|
+
'peaks-resume': 'peaks-code',
|
|
127
|
+
'peaks-status': 'peaks-code',
|
|
128
|
+
'peaks-test': 'peaks-code',
|
|
129
|
+
'peaks-reviewer': 'peaks-code',
|
|
130
|
+
};
|
|
131
|
+
function pickBreathingGlyph(capability, nowMs) {
|
|
132
|
+
const set = capability === 'ascii' ? BREATHING_GLYPHS_ASCII : BREATHING_GLYPHS_UNICODE;
|
|
133
|
+
const index = Math.floor((nowMs % BREATHING_PERIOD_MS) / (BREATHING_PERIOD_MS / set.length)) % set.length;
|
|
134
|
+
return set[index];
|
|
135
|
+
}
|
|
136
|
+
function renderActiveDot(capability, nowMs) {
|
|
137
|
+
// Brief: the active dot carries the project accent (`#5A65D8` bold)
|
|
138
|
+
// in both colored tiers. The breathing glyph is wrapped in a fresh
|
|
139
|
+
// SGR on every render so the IDE sees a single accent per refresh.
|
|
140
|
+
const glyph = pickBreathingGlyph(capability, nowMs);
|
|
141
|
+
if (capability === 'ascii')
|
|
142
|
+
return glyph;
|
|
143
|
+
return accentGlyph(glyph);
|
|
144
|
+
}
|
|
145
|
+
function brandText(capability) {
|
|
146
|
+
// Brief: brand carries the project accent (`#5A65D8` bold) in both
|
|
147
|
+
// colored tiers. The `ascii` tier stays plain text so plain text
|
|
148
|
+
// consumers (no TTY, no UTF-8) never see escape codes.
|
|
149
|
+
if (capability === 'ansi-unicode' || capability === 'unicode') {
|
|
150
|
+
return accent(BRAND);
|
|
151
|
+
}
|
|
152
|
+
return BRAND;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Default capability when none is supplied. Per the brief: "if backward
|
|
156
|
+
* compatibility requires a renderer default, use unicode, never
|
|
157
|
+
* unconditional ANSI." The CLI's `peaks statusline` invocation currently
|
|
158
|
+
* passes no options; preserving the legacy no-ANSI behaviour means the
|
|
159
|
+
* default is the unicode palette (no escape codes).
|
|
160
|
+
*/
|
|
161
|
+
const DEFAULT_CAPABILITY = 'unicode';
|
|
162
|
+
function paletteFor(capability) {
|
|
163
|
+
return PALETTES[capability ?? DEFAULT_CAPABILITY];
|
|
164
|
+
}
|
|
9
165
|
function formatAge(ageMs) {
|
|
10
166
|
if (ageMs === null)
|
|
11
167
|
return '';
|
|
@@ -21,35 +177,224 @@ function rootLabel(projectRoot) {
|
|
|
21
177
|
return basename(projectRoot);
|
|
22
178
|
}
|
|
23
179
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
180
|
+
* Build the "middle" of the status line — everything between the brand
|
|
181
|
+
* prefix and the project root label. Kept separate so the token layout
|
|
182
|
+
* is obvious at the call site and so each state has a single
|
|
183
|
+
* responsibility.
|
|
26
184
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
185
|
+
function renderActive(presence, palette, nowMs, capability) {
|
|
186
|
+
if (!presence) {
|
|
187
|
+
return `${palette.idle} ${palette.idleLabel}`;
|
|
188
|
+
}
|
|
189
|
+
const attentionLabel = isAttentionGate(presence.gate);
|
|
190
|
+
if (attentionLabel !== null) {
|
|
191
|
+
// Attention gate — surface the warning glyph + human-readable label.
|
|
192
|
+
return `${palette.warning} ${presence.skill}${palette.inlineSeparator}${attentionLabel}`;
|
|
193
|
+
}
|
|
194
|
+
const skill = presence.skill;
|
|
195
|
+
const dot = renderActiveDot(capability, nowMs);
|
|
196
|
+
const beeParent = BEE_TO_PARENT[skill];
|
|
197
|
+
const modeToken = typeof presence.mode === 'string' && presence.mode.length > 0
|
|
198
|
+
? ` [${presence.mode}]`
|
|
199
|
+
: '';
|
|
200
|
+
// Bee-tier skills surface both layers: the active bee name plus
|
|
201
|
+
// the full parent orchestrator name. Mode is shown for every
|
|
202
|
+
// active skill (not just peaks-code) so the line carries the same
|
|
203
|
+
// mode taxonomy for any bee in flight.
|
|
204
|
+
if (beeParent !== undefined) {
|
|
205
|
+
return `${dot} ${skill} ↑${beeParent}${modeToken}`;
|
|
206
|
+
}
|
|
207
|
+
return `${dot} ${skill}${modeToken}`;
|
|
208
|
+
}
|
|
209
|
+
function renderStale(presence, ageMs, palette) {
|
|
210
|
+
const skill = presence?.skill ?? 'unknown';
|
|
211
|
+
const age = formatAge(ageMs);
|
|
212
|
+
const ageSuffix = age ? `${palette.inlineSeparator}${age}` : '';
|
|
213
|
+
return `${palette.warning} ${skill}${ageSuffix}`;
|
|
214
|
+
}
|
|
215
|
+
function renderInvalid(palette) {
|
|
216
|
+
return `${palette.warning} ${palette.invalidMessage}`;
|
|
217
|
+
}
|
|
218
|
+
function renderIdle(palette) {
|
|
219
|
+
return `${palette.idle} ${palette.idleLabel}`;
|
|
220
|
+
}
|
|
221
|
+
const COMPACT_BAR_WIDTH = 8;
|
|
222
|
+
function renderCompactBar(filledCells, palette) {
|
|
223
|
+
const filled = palette.barFilled.repeat(filledCells);
|
|
224
|
+
const empty = palette.barEmpty.repeat(COMPACT_BAR_WIDTH - filledCells);
|
|
225
|
+
return `[${filled}${empty}]`;
|
|
226
|
+
}
|
|
227
|
+
function formatRatio(value) {
|
|
228
|
+
// Brief ratio labels use 2-decimal precision: 0.87 → "87%".
|
|
229
|
+
return `${Math.round(value * 100)}%`;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Render the compact-progress segment. Compact state REPLACES the skill
|
|
233
|
+
* content while active (`kind !== 'none'`). The renderer always lays out
|
|
234
|
+
*
|
|
235
|
+
* <stage-glyph> <bar> <label>[ · <before>%][ → <after>%]
|
|
236
|
+
*
|
|
237
|
+
* Failed states additionally suffix the stage at which the compact failed.
|
|
238
|
+
* Stalled states keep the active-stage cell count and render a plain
|
|
239
|
+
* "stalled" label. Invalid states surface the read-reason verbatim as a
|
|
240
|
+
* single-line diagnostic so the user can see why the bar is empty.
|
|
241
|
+
*/
|
|
242
|
+
function renderCompact(state, palette) {
|
|
243
|
+
switch (state.kind) {
|
|
244
|
+
case 'none':
|
|
245
|
+
return '';
|
|
246
|
+
case 'queued':
|
|
247
|
+
return `${palette.compact.queued} ${renderCompactBar(0, palette)} queued${typeof state.triggerRatio === 'number'
|
|
248
|
+
? `${palette.inlineSeparator}${formatRatio(state.triggerRatio)}`
|
|
249
|
+
: ''}`;
|
|
250
|
+
case 'preparing':
|
|
251
|
+
return `${palette.compact.preparing} ${renderCompactBar(2, palette)} preparing${typeof state.triggerRatio === 'number'
|
|
252
|
+
? `${palette.inlineSeparator}${formatRatio(state.triggerRatio)}`
|
|
253
|
+
: ''}`;
|
|
254
|
+
case 'compacting':
|
|
255
|
+
return `${palette.compact.compacting} ${renderCompactBar(4, palette)} compacting${typeof state.triggerRatio === 'number'
|
|
256
|
+
? `${palette.inlineSeparator}${formatRatio(state.triggerRatio)}`
|
|
257
|
+
: ''}`;
|
|
258
|
+
case 'verifying':
|
|
259
|
+
return `${palette.compact.verifying} ${renderCompactBar(6, palette)} verifying`;
|
|
260
|
+
case 'completed':
|
|
261
|
+
return `${palette.compact.completed} ${renderCompactBar(8, palette)} compacted${typeof state.triggerRatio === 'number'
|
|
262
|
+
? `${palette.inlineSeparator}${formatRatio(state.triggerRatio)}${typeof state.afterRatio === 'number'
|
|
263
|
+
? ` ${palette.ratioArrow} ${formatRatio(state.afterRatio)}`
|
|
264
|
+
: ''}`
|
|
265
|
+
: typeof state.afterRatio === 'number'
|
|
266
|
+
? `${palette.inlineSeparator}${formatRatio(state.afterRatio)}`
|
|
267
|
+
: ''}`;
|
|
268
|
+
case 'failed': {
|
|
269
|
+
const failedAt = state.failedAt ?? 'compacting';
|
|
270
|
+
const filledAt = failedAt === 'queued'
|
|
271
|
+
? 0
|
|
272
|
+
: failedAt === 'preparing'
|
|
273
|
+
? 2
|
|
274
|
+
: failedAt === 'compacting'
|
|
275
|
+
? 4
|
|
276
|
+
: 6;
|
|
277
|
+
return `${palette.compact.failed} ${renderCompactBar(filledAt, palette)} compact failed${palette.inlineSeparator}${failedAt}`;
|
|
41
278
|
}
|
|
42
|
-
case '
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
return `${BRAND} ⚠ ${skill}${ageSuffix}${rootSuffix}`;
|
|
279
|
+
case 'stalled':
|
|
280
|
+
return `${palette.compact.compacting} ${renderCompactBar(state.filledCells, palette)} stalled`;
|
|
281
|
+
case 'invalid': {
|
|
282
|
+
const detail = state.detail ?? 'lifecycle record malformed';
|
|
283
|
+
return `${palette.warning} ${detail}`;
|
|
48
284
|
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Resolve the capability tier from environment and TTY state. Pure and
|
|
289
|
+
* deterministic — no I/O beyond reading the caller-supplied `env` and
|
|
290
|
+
* `isTTY` flag. The CLI delegates to this so the read-only status line
|
|
291
|
+
* does not need to know about ANSI/NO_COLOR semantics.
|
|
292
|
+
*
|
|
293
|
+
* The optional `forced` argument is a TEST SEAM (consumed by direct unit
|
|
294
|
+
* tests, not by the CLI). It lets tests pin a tier without constructing a
|
|
295
|
+
* TTY/NO_COLOR fixture. The CLI does NOT expose any flag that maps to
|
|
296
|
+
* `forced` — the env-driven path is the single first-version runtime
|
|
297
|
+
* source of truth.
|
|
298
|
+
*
|
|
299
|
+
* Adapter-internal env override (NOT a user-facing CLI flag):
|
|
300
|
+
* `PEAKS_STATUSLINE_ASCII` — when set to `1` / `true` / `yes`, the
|
|
301
|
+
* renderer drops to the ASCII palette (no Unicode-extra glyphs, no ANSI).
|
|
302
|
+
* This is an adapter-internal mechanism for the trust boundary described
|
|
303
|
+
* by the two-forms-only / human-NL-choice-only tenets: the user never
|
|
304
|
+
* types a CLI verb to flip palettes, but the adapter (and only the
|
|
305
|
+
* adapter) can set the env var on the user's behalf when its consumer
|
|
306
|
+
* (e.g. a tiny terminal without UTF-8) needs ASCII. We do NOT expose
|
|
307
|
+
* this via a CLI flag.
|
|
308
|
+
*
|
|
309
|
+
* Order of resolution (highest priority first):
|
|
310
|
+
*
|
|
311
|
+
* 1. `forced` argument (test seam only)
|
|
312
|
+
* 2. `PEAKS_STATUSLINE_ASCII` set → `ascii` (adapter-internal override)
|
|
313
|
+
* 3. `isTTY === true` → `ansi-unicode`
|
|
314
|
+
* 4. otherwise → `unicode` (still ANSI-colored; only `ascii` is color-free)
|
|
315
|
+
*
|
|
316
|
+
* Why this ordering: the IDE statusline invokes the renderer through a
|
|
317
|
+
* hook that may not always be detected as a TTY. The `unicode` tier
|
|
318
|
+
* now emits the cyan brand accent + semantic colors so the line reads
|
|
319
|
+
* as a branded product surface whether or not the consumer is a
|
|
320
|
+
* terminal. `NO_COLOR` is honored as an ambient convention by the
|
|
321
|
+
* IDE adapter (which can set `PEAKS_STATUSLINE_ASCII=1` to opt out
|
|
322
|
+
* fully), so a separate tier for NO_COLOR is no longer needed.
|
|
323
|
+
*
|
|
324
|
+
* `ascii` is the strict no-color, no-Unicode-extra tier; it is the
|
|
325
|
+
* single safe option for plain log / file consumers.
|
|
326
|
+
*
|
|
327
|
+
* Why `PEAKS_STATUSLINE_ASCII` outranks `NO_COLOR`:
|
|
328
|
+
* The two env vars are NOT redundant. `NO_COLOR` (https://no-color.org)
|
|
329
|
+
* is the cross-industry signal that NO escape sequences should be emitted
|
|
330
|
+
* — its only effect is "ANSI off". The Unicode-extra glyphs (●, █, ░, etc.)
|
|
331
|
+
* are NOT ANSI escape sequences; they are UTF-8 characters and `NO_COLOR`
|
|
332
|
+
* does not address them. `PEAKS_STATUSLINE_ASCII` is the adapter-internal
|
|
333
|
+
* "signal source is a tiny terminal without UTF-8" override and drops the
|
|
334
|
+
* glyphs to ASCII shape as well. Both are correct signals about
|
|
335
|
+
* different concerns; the ASCII override wins because it is strictly
|
|
336
|
+
* narrower (a tiny terminal needs both no-ANSI AND no-Unicode-extra).
|
|
337
|
+
*
|
|
338
|
+
* The deliberate ordering keeps the rendered text ANSI-free when the consumer
|
|
339
|
+
* is a logger, a file, or any non-interactive sink, and only enables ANSI
|
|
340
|
+
* under an explicit supported condition (TTY + no env veto).
|
|
341
|
+
*/
|
|
342
|
+
export function resolveStatusLineCapability(input) {
|
|
343
|
+
if (input.forced !== undefined) {
|
|
344
|
+
return input.forced;
|
|
345
|
+
}
|
|
346
|
+
// Adapter-internal ASCII override. Reads the env var using a non-ASCII
|
|
347
|
+
// identifier by indirection so a `grep PEAKS_STATUSLINE_ASCII` finds the
|
|
348
|
+
// exact contract; the comparison string is built at call time.
|
|
349
|
+
const asciiFlag = input.env['PEAKS_STATUSLINE_ASCII'];
|
|
350
|
+
if (typeof asciiFlag === 'string' && (asciiFlag === '1' || asciiFlag === 'true' || asciiFlag === 'yes')) {
|
|
351
|
+
return 'ascii';
|
|
352
|
+
}
|
|
353
|
+
// NO_COLOR is honored by the IDE adapter (which sets
|
|
354
|
+
// `PEAKS_STATUSLINE_ASCII=1` to fully opt out). The unicode tier
|
|
355
|
+
// itself emits cyan + semantic colors so the brand reads correctly
|
|
356
|
+
// whether or not the consumer is a TTY.
|
|
357
|
+
return input.isTTY ? 'ansi-unicode' : 'unicode';
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Render the status line. Pure — no I/O, no side effects. The output is
|
|
361
|
+
* a single short line suitable for the bottom-of-terminal status bar.
|
|
362
|
+
*
|
|
363
|
+
* The `options.capability` field selects the glyph palette. When omitted,
|
|
364
|
+
* the function falls back to `unicode` (no ANSI escape codes), preserving
|
|
365
|
+
* the legacy behaviour for the CLI's `peaks statusline` invocation.
|
|
366
|
+
*
|
|
367
|
+
* Compact-state precedence: when `model.compact.kind !== 'none'` the
|
|
368
|
+
* compact bar replaces the active/idle/stale content. The C1 baseline
|
|
369
|
+
* line is preserved when the compact state is `none`.
|
|
370
|
+
*/
|
|
371
|
+
export function renderStatusLine(model, options) {
|
|
372
|
+
const capability = options?.capability ?? DEFAULT_CAPABILITY;
|
|
373
|
+
const palette = paletteFor(capability);
|
|
374
|
+
const root = rootLabel(model.projectRoot);
|
|
375
|
+
const rootSuffix = root ? `${palette.trailSeparator}${root}` : '';
|
|
376
|
+
const brand = brandText(capability);
|
|
377
|
+
// Breathing pulse: key off a single wall-clock read at the start of
|
|
378
|
+
// render so the output is deterministic for a given invocation. The
|
|
379
|
+
// glyph remains a single cell so total visible width is constant.
|
|
380
|
+
const nowMs = Date.now();
|
|
381
|
+
const compactSegment = renderCompact(model.compact, palette);
|
|
382
|
+
if (compactSegment.length > 0) {
|
|
383
|
+
// Compact state replaces the active / stale / idle skill content.
|
|
384
|
+
// `invalid-presence` still surfaces its own diagnostic when compact
|
|
385
|
+
// is also `invalid` (the compact diagnostic wins, since it's the
|
|
386
|
+
// more recent failure mode).
|
|
387
|
+
return `${brand} ${compactSegment}${rootSuffix}`;
|
|
388
|
+
}
|
|
389
|
+
switch (model.state) {
|
|
390
|
+
case 'active':
|
|
391
|
+
return `${brand} ${renderActive(model.presence, palette, nowMs, capability)}${rootSuffix}`;
|
|
392
|
+
case 'stale':
|
|
393
|
+
return `${brand} ${renderStale(model.presence, model.ageMs, palette)}${rootSuffix}`;
|
|
49
394
|
case 'invalid-presence':
|
|
50
|
-
return `${
|
|
395
|
+
return `${brand} ${renderInvalid(palette)}${rootSuffix}`;
|
|
51
396
|
case 'idle':
|
|
52
397
|
default:
|
|
53
|
-
return `${
|
|
398
|
+
return `${brand} ${renderIdle(palette)}${rootSuffix}`;
|
|
54
399
|
}
|
|
55
400
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CompactStatuslineState } from '../compact-statusline/compact-statusline-service.js';
|
|
1
2
|
export type StatusLineStdin = {
|
|
2
3
|
workspace?: {
|
|
3
4
|
current_dir?: string;
|
|
@@ -19,6 +20,7 @@ export type StatusLineModel = {
|
|
|
19
20
|
projectRoot: string | null;
|
|
20
21
|
presence: StatusLinePresence | null;
|
|
21
22
|
ageMs: number | null;
|
|
23
|
+
compact: CompactStatuslineState;
|
|
22
24
|
};
|
|
23
25
|
export declare function parseStatusLineStdin(raw: string): StatusLineStdin | null;
|
|
24
26
|
export declare function buildStatusLineModel(stdin: StatusLineStdin | null, nowMs: number): StatusLineModel;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { findProjectRoot } from '../config/config-safety.js';
|
|
4
|
+
import { decideCompactStatusline } from '../compact-statusline/compact-statusline-service.js';
|
|
5
|
+
import { getSessionIdCanonical } from '../session/session-manager.js';
|
|
4
6
|
/**
|
|
5
7
|
* Out-of-band Peaks skill status renderer for the Claude Code statusLine.
|
|
6
8
|
*
|
|
@@ -84,15 +86,19 @@ function readPresenceReadOnly(projectRoot) {
|
|
|
84
86
|
export function buildStatusLineModel(stdin, nowMs) {
|
|
85
87
|
const cwd = resolveCwdFromStdin(stdin);
|
|
86
88
|
const projectRoot = findProjectRoot(cwd);
|
|
89
|
+
// Compact state is computed independently of presence — the read is read-only
|
|
90
|
+
// and reads `.peaks/_runtime/<sid>/compact-lifecycle.json` (or its legacy
|
|
91
|
+
// fallbacks). It replaces the active skill content when kind != 'none'.
|
|
92
|
+
const compact = readCompactState(projectRoot, nowMs);
|
|
87
93
|
if (projectRoot === null) {
|
|
88
|
-
return { state: 'idle', projectRoot: null, presence: null, ageMs: null };
|
|
94
|
+
return { state: 'idle', projectRoot: null, presence: null, ageMs: null, compact };
|
|
89
95
|
}
|
|
90
96
|
const { presence, invalid } = readPresenceReadOnly(projectRoot);
|
|
91
97
|
if (invalid) {
|
|
92
|
-
return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null };
|
|
98
|
+
return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null, compact };
|
|
93
99
|
}
|
|
94
100
|
if (presence === null) {
|
|
95
|
-
return { state: 'idle', projectRoot, presence: null, ageMs: null };
|
|
101
|
+
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact };
|
|
96
102
|
}
|
|
97
103
|
// Session binding: when the presence was stamped with a Claude session id and
|
|
98
104
|
// the live session (from stdin) is a different one, the recorded skill belongs
|
|
@@ -101,10 +107,33 @@ export function buildStatusLineModel(stdin, nowMs) {
|
|
|
101
107
|
// fall back to the time-based behavior below for backward compatibility.
|
|
102
108
|
const liveSessionId = typeof stdin?.session_id === 'string' && stdin.session_id.length > 0 ? stdin.session_id : null;
|
|
103
109
|
if (presence.claudeSessionId && liveSessionId && presence.claudeSessionId !== liveSessionId) {
|
|
104
|
-
return { state: 'idle', projectRoot, presence: null, ageMs: null };
|
|
110
|
+
return { state: 'idle', projectRoot, presence: null, ageMs: null, compact };
|
|
105
111
|
}
|
|
106
112
|
const setAtMs = presence.setAt ? Date.parse(presence.setAt) : Number.NaN;
|
|
107
113
|
const ageMs = Number.isNaN(setAtMs) ? null : nowMs - setAtMs;
|
|
108
114
|
const state = ageMs !== null && ageMs > STALE_THRESHOLD_MS ? 'stale' : 'active';
|
|
109
|
-
return { state, projectRoot, presence, ageMs };
|
|
115
|
+
return { state, projectRoot, presence, ageMs, compact };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Read-only compact state resolver. Resolves the canonical session id for the
|
|
119
|
+
* detected project root, then delegates to {@link decideCompactStatusline}.
|
|
120
|
+
* Returns `{ kind: 'none', filledCells: 0 }` when no project root is bound —
|
|
121
|
+
* the renderer falls back to the C1 normal line in that case.
|
|
122
|
+
*/
|
|
123
|
+
function readCompactState(projectRoot, nowMs) {
|
|
124
|
+
if (projectRoot === null) {
|
|
125
|
+
return { kind: 'none', filledCells: 0 };
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const sessionId = getSessionIdCanonical(projectRoot);
|
|
129
|
+
return decideCompactStatusline({
|
|
130
|
+
projectRoot,
|
|
131
|
+
sessionId,
|
|
132
|
+
now: nowMs,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Read-only — never throw across the statusline boundary.
|
|
137
|
+
return { kind: 'none', filledCells: 0 };
|
|
138
|
+
}
|
|
110
139
|
}
|
|
@@ -25,6 +25,8 @@ export interface SyncServiceInput {
|
|
|
25
25
|
readonly platforms?: readonly IdeId[] | undefined;
|
|
26
26
|
/** When true, the installer is invoked in dry-run mode. */
|
|
27
27
|
readonly dryRun?: boolean | undefined;
|
|
28
|
+
/** Reconcile managed Junctions that point at deleted host worktrees. */
|
|
29
|
+
readonly reconcileJunctions?: boolean | undefined;
|
|
28
30
|
}
|
|
29
31
|
export interface SyncServiceResult {
|
|
30
32
|
readonly applied: boolean;
|
|
@@ -39,6 +41,7 @@ interface InstallBundledSkillsOptions {
|
|
|
39
41
|
readonly ideId: IdeId;
|
|
40
42
|
readonly projectRoot: string;
|
|
41
43
|
readonly dryRun?: boolean;
|
|
44
|
+
readonly reconcileJunctions?: boolean;
|
|
42
45
|
readonly targetRoot?: string;
|
|
43
46
|
}
|
|
44
47
|
interface InstallResult {
|
|
@@ -222,6 +222,7 @@ export async function runSkillSync(input) {
|
|
|
222
222
|
ideId: platform,
|
|
223
223
|
projectRoot: input.projectRoot,
|
|
224
224
|
...(dryRun ? { dryRun: true } : {}),
|
|
225
|
+
...(input.reconcileJunctions === true ? { reconcileJunctions: true } : {}),
|
|
225
226
|
});
|
|
226
227
|
perPlatform.push({
|
|
227
228
|
platform,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type BuildTool = 'umi' | 'next' | 'vite' | 'rsbuild' | 'rspack' | 'farm' | 'craco' | 'webpack' | 'gulp' | 'angular' | 'custom' | 'unknown';
|
|
2
2
|
export type ComponentLibrary = {
|
|
3
|
-
readonly name: 'antd' | 'antd-pro' | 'mui' | 'element-plus' | 'element-ui' | 'arco' | 'tdesign' | 'semi' | 'nextui' | 'chakra' | 'vant' | 'none';
|
|
3
|
+
readonly name: 'antd' | 'antd-pro' | 'mui' | 'element-plus' | 'element-ui' | 'arco' | 'tdesign' | 'semi' | 'nextui' | 'chakra' | 'shadcn' | 'vant' | 'none';
|
|
4
4
|
readonly majorVersion?: string;
|
|
5
5
|
readonly hasProSuite?: boolean;
|
|
6
6
|
};
|
|
@@ -18,6 +18,7 @@ export type ProjectContext = {
|
|
|
18
18
|
readonly legacySignals: string[];
|
|
19
19
|
readonly notableDeps: string[];
|
|
20
20
|
};
|
|
21
|
+
export declare function detectComponentLibrary(projectRoot: string, deps: Record<string, string>): ComponentLibrary;
|
|
21
22
|
export declare function detectProjectContext(projectRoot: string): ProjectContext;
|
|
22
23
|
export declare function buildToolLabel(tool: BuildTool): string;
|
|
23
24
|
export declare function componentLibraryLabel(lib: ComponentLibrary): string;
|