peaks-loop 4.0.7 → 4.0.9
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 +58 -288
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/baseline-commands.d.ts +3 -0
- package/dist/cli/commands/baseline-commands.js +146 -0
- package/dist/cli/commands/code-job-shape-commands.js +8 -11
- package/dist/cli/commands/code-mode-gate-commands.js +1 -43
- package/dist/cli/commands/code-runtime-commands.js +36 -14
- package/dist/cli/commands/core/skill-command.js +75 -13
- package/dist/cli/commands/core/standards-command.js +5 -50
- package/dist/cli/commands/dispatch-commands.d.ts +20 -0
- package/dist/cli/commands/dispatch-commands.js +39 -38
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/heartbeat-commands.d.ts +21 -0
- package/dist/cli/commands/heartbeat-commands.js +40 -0
- package/dist/cli/commands/hook-handle.js +5 -2
- package/dist/cli/commands/hooks-commands.js +9 -26
- package/dist/cli/commands/loop-eval-commands.js +5 -2
- package/dist/cli/commands/statusline-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +0 -2
- package/dist/cli/commands/sub-agent-shared.d.ts +7 -9
- package/dist/cli/commands/workflow-commands.js +8 -0
- package/dist/cli/commands/workflow-lifecycle-commands.d.ts +63 -0
- package/dist/cli/commands/workflow-lifecycle-commands.js +302 -0
- package/dist/cli/commands/workspace/init-command.js +19 -0
- package/dist/services/audit/backing-detector.d.ts +2 -19
- package/dist/services/audit/backing-detector.js +7 -66
- package/dist/services/audit/enforcers/active-skill-resolver.d.ts +25 -9
- package/dist/services/audit/enforcers/active-skill-resolver.js +83 -21
- package/dist/services/audit/enforcers/lint-audit-regression.js +1 -10
- package/dist/services/capability-audit-service/cross-check.d.ts +8 -0
- package/dist/services/capability-audit-service/cross-check.js +11 -0
- package/dist/services/capability-audit-service/index.d.ts +3 -0
- package/dist/services/capability-audit-service/index.js +2 -0
- package/dist/services/capability-audit-service/runner.d.ts +27 -0
- package/dist/services/capability-audit-service/runner.js +40 -0
- package/dist/services/capability-audit-service/staleness.d.ts +1 -0
- package/dist/services/capability-audit-service/staleness.js +7 -0
- package/dist/services/capability-audit-service/types.d.ts +24 -0
- package/dist/services/capability-audit-service/types.js +1 -0
- package/dist/services/capability-baseline/store.d.ts +33 -0
- package/dist/services/capability-baseline/store.js +80 -0
- package/dist/services/capability-baseline/types.d.ts +45 -0
- package/dist/services/capability-baseline/types.js +5 -0
- package/dist/services/capability-baseline/validator.d.ts +19 -0
- package/dist/services/capability-baseline/validator.js +38 -0
- package/dist/services/capability-guard-runner/contracts/J01.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J01.js +48 -0
- package/dist/services/capability-guard-runner/contracts/J02.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J02.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J03.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J03.js +24 -0
- package/dist/services/capability-guard-runner/contracts/J04.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J04.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J05.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J05.js +21 -0
- package/dist/services/capability-guard-runner/contracts/J06.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J06.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J07.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J07.js +53 -0
- package/dist/services/capability-guard-runner/contracts/J08.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J08.js +43 -0
- package/dist/services/capability-guard-runner/contracts/J09.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J09.js +44 -0
- package/dist/services/capability-guard-runner/contracts/J10.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J10.js +40 -0
- package/dist/services/capability-guard-runner/contracts/J11.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J11.js +39 -0
- package/dist/services/capability-guard-runner/contracts/J12.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J12.js +34 -0
- package/dist/services/capability-guard-runner/contracts/J13.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J13.js +45 -0
- package/dist/services/capability-guard-runner/contracts/J14.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J14.js +36 -0
- package/dist/services/capability-guard-runner/contracts/J15.d.ts +2 -0
- package/dist/services/capability-guard-runner/contracts/J15.js +40 -0
- package/dist/services/capability-guard-runner/diff.d.ts +2 -0
- package/dist/services/capability-guard-runner/diff.js +7 -0
- package/dist/services/capability-guard-runner/runner.d.ts +9 -0
- package/dist/services/capability-guard-runner/runner.js +22 -0
- package/dist/services/capability-guard-runner/types.d.ts +39 -0
- package/dist/services/capability-guard-runner/types.js +1 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +35 -2
- package/dist/services/code/auto-compact-orchestrator.js +29 -9
- package/dist/services/context/build-dispatch-system-prompt.js +0 -18
- package/dist/services/dispatch/dispatch-record-writer.d.ts +25 -1
- package/dist/services/dispatch/dispatch-record-writer.js +85 -15
- package/dist/services/dispatch/test-tool-detection.d.ts +0 -14
- package/dist/services/dispatch/test-tool-detection.js +0 -22
- package/dist/services/doctor/doctor-service/checks/skill-presence.d.ts +8 -0
- package/dist/services/doctor/doctor-service/checks/skill-presence.js +9 -1
- package/dist/services/evolution/evolution-types.d.ts +16 -16
- package/dist/services/final-review/final-review-service.d.ts +8 -0
- package/dist/services/final-review/final-review-service.js +14 -0
- package/dist/services/final-review/final-review-types.d.ts +1 -1
- package/dist/services/hooks/presence-marker-detector.d.ts +6 -0
- package/dist/services/hooks/presence-marker-detector.js +48 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -0
- package/dist/services/ide/adapters/codex-adapter.js +20 -1
- package/dist/services/ide/adapters/cursor-adapter.js +21 -1
- package/dist/services/ide/adapters/hermes-adapter.js +20 -1
- package/dist/services/ide/adapters/openclaw-adapter.js +20 -1
- package/dist/services/ide/adapters/qoder-adapter.js +20 -1
- package/dist/services/ide/adapters/tongyi-lingma-adapter.js +20 -1
- package/dist/services/ide/adapters/trae-adapter.js +21 -1
- package/dist/services/ide/adapters/zcode-adapter.js +19 -0
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/job/job-progress-store.d.ts +2 -2
- package/dist/services/job/job-types.d.ts +26 -26
- package/dist/services/observability/observability-service.d.ts +4 -4
- package/dist/services/scan/archetype-service.js +2 -52
- package/dist/services/sediment/json-schema.d.ts +4 -4
- package/dist/services/session/caller-binding-service.d.ts +21 -0
- package/dist/services/session/caller-binding-service.js +35 -0
- package/dist/services/session/caller-id-types.d.ts +62 -13
- package/dist/services/session/caller-id-types.js +25 -13
- package/dist/services/session/index.d.ts +4 -2
- package/dist/services/session/index.js +8 -2
- package/dist/services/session/platform-fallbacks.d.ts +25 -17
- package/dist/services/session/platform-fallbacks.js +26 -28
- package/dist/services/session/resolve-caller-id.d.ts +64 -29
- package/dist/services/session/resolve-caller-id.js +122 -48
- package/dist/services/session/session-manager.d.ts +0 -29
- package/dist/services/session/session-manager.js +0 -33
- package/dist/services/session/session-resume-service.d.ts +4 -0
- package/dist/services/session/session-resume-service.js +4 -0
- package/dist/services/skills/hooks-settings-service.d.ts +0 -41
- package/dist/services/skills/hooks-settings-service.js +5 -61
- package/dist/services/skills/presence-lease-service.d.ts +73 -0
- package/dist/services/skills/presence-lease-service.js +358 -0
- package/dist/services/skills/presence-lease-types.d.ts +75 -0
- package/dist/services/skills/presence-lease-types.js +16 -0
- package/dist/services/skills/skill-presence-service.js +86 -22
- package/dist/services/skills/skill-statusline-renderer.d.ts +46 -56
- package/dist/services/skills/skill-statusline-renderer.js +312 -100
- package/dist/services/slice/slice-check-service.js +0 -59
- package/dist/services/standards/project-standards-service.js +0 -44
- package/dist/services/workflow/workflow-graph-store.d.ts +77 -0
- package/dist/services/workflow/workflow-graph-store.js +278 -0
- package/dist/services/workflow/workflow-graph-types.d.ts +56 -0
- package/dist/services/workflow/workflow-graph-types.js +67 -0
- package/dist/services/workflow/workflow-inflight-probe.d.ts +52 -0
- package/dist/services/workflow/workflow-inflight-probe.js +86 -0
- package/dist/services/workflow/workflow-node-lifecycle.d.ts +67 -0
- package/dist/services/workflow/workflow-node-lifecycle.js +348 -0
- package/dist/services/workflow/workflow-presence-lifecycle.d.ts +58 -0
- package/dist/services/workflow/workflow-presence-lifecycle.js +196 -0
- package/dist/services/workspace/reconcile-service.d.ts +31 -0
- package/dist/services/workspace/reconcile-service.js +132 -1
- package/package.json +5 -4
- package/skills/peaks-code/SKILL.md +5 -43
- package/skills/peaks-code/references/completion-handoff.md +1 -1
- package/skills/peaks-code/references/skill-presence-and-title.md +1 -1
- package/dist/cli/commands/sub-agent-exec-command.d.ts +0 -25
- package/dist/cli/commands/sub-agent-exec-command.js +0 -132
- package/dist/services/standards/find-skills-integration.d.ts +0 -59
- package/dist/services/standards/find-skills-integration.js +0 -225
|
@@ -28,12 +28,39 @@ const BRAND_RGB = '38;2;90;101;216';
|
|
|
28
28
|
const BRAND_SGR_OPEN = `\x1b[1;${BRAND_RGB}m`;
|
|
29
29
|
const BRAND_SGR_FULL_OPEN = `\x1b[${BRAND_RGB}m`;
|
|
30
30
|
const BRAND_SGR_CLOSE = '\x1b[0m';
|
|
31
|
+
/**
|
|
32
|
+
* Highlight color used by the marquee scan band. `#E0E0E0` bright-grey
|
|
33
|
+
* + bold = "bleached" foreground that reads as a scanner sweeping
|
|
34
|
+
* over the colored text. Kept distinct from brand white (`#FFFFFF`)
|
|
35
|
+
* because pure white on `#5A65D8` brand purple reads as a strobe;
|
|
36
|
+
* the off-white tint gives the same "scanner" affordance without the
|
|
37
|
+
* flicker.
|
|
38
|
+
*/
|
|
39
|
+
const HIGHLIGHT_RGB = '38;2;224;224;224';
|
|
40
|
+
const HIGHLIGHT_SGR_OPEN = `\x1b[1;${HIGHLIGHT_RGB}m`;
|
|
31
41
|
function accent(text) {
|
|
32
42
|
return `${BRAND_SGR_OPEN}${text}${BRAND_SGR_CLOSE}`;
|
|
33
43
|
}
|
|
34
44
|
function accentGlyph(glyph) {
|
|
35
45
|
return `${BRAND_SGR_OPEN}${glyph}${BRAND_SGR_CLOSE}`;
|
|
36
46
|
}
|
|
47
|
+
// (kept for the brandText/active-dot helpers that inline the same SGR; the
|
|
48
|
+
// buildPalette factory owns per-token SGR injection now and does not call
|
|
49
|
+
// through these wrappers.)
|
|
50
|
+
/**
|
|
51
|
+
* Render the supplied text in the brand purple, honouring the
|
|
52
|
+
* `noColor` flag. Returns plain text when NO_COLOR is set or the
|
|
53
|
+
* tier is ASCII. Used by the active / stale / compact renders for
|
|
54
|
+
* tokens that the {@link buildPalette} cannot pre-stamp (skill
|
|
55
|
+
* name, mode, age, attention labels).
|
|
56
|
+
*/
|
|
57
|
+
function brandRun(text, noColor, capability) {
|
|
58
|
+
if (text.length === 0)
|
|
59
|
+
return text;
|
|
60
|
+
if (noColor || capability === 'ascii')
|
|
61
|
+
return text;
|
|
62
|
+
return `${BRAND_SGR_OPEN}${text}${BRAND_SGR_CLOSE}`;
|
|
63
|
+
}
|
|
37
64
|
/**
|
|
38
65
|
* Slow-blink variant of the accent glyph. Used for the idle indicator
|
|
39
66
|
* so the user perceives a pulse on the statusline. The ANSI slow-blink
|
|
@@ -44,68 +71,70 @@ function accentGlyph(glyph) {
|
|
|
44
71
|
function blinkingAccentGlyph(glyph) {
|
|
45
72
|
return `\x1b[5;1;${BRAND_RGB}m${glyph}\x1b[0m`;
|
|
46
73
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Build the per-capability palette. `noColor=true` strips every brand
|
|
76
|
+
* ANSI SGR (warning / failed remain because they are SEMANTIC alarms,
|
|
77
|
+
* not brand colour). The unicode glyphs are kept as-is — NO_COLOR
|
|
78
|
+
* (https://no-color.org) addresses ANSI sequences, not UTF-8 chars.
|
|
79
|
+
*/
|
|
80
|
+
function buildPalette(capability, noColor) {
|
|
81
|
+
// Brand SGR helpers, noColor-aware.
|
|
82
|
+
const brand = (text) => noColor || capability === 'ascii' ? text : `${BRAND_SGR_OPEN}${text}${BRAND_SGR_CLOSE}`;
|
|
83
|
+
const brandGlyph = (glyph) => noColor || capability === 'ascii' ? glyph : `${BRAND_SGR_OPEN}${glyph}${BRAND_SGR_CLOSE}`;
|
|
84
|
+
const dimBrand = (text) => {
|
|
85
|
+
if (noColor || capability === 'ascii')
|
|
86
|
+
return text;
|
|
87
|
+
return `\x1b[2;${BRAND_RGB}m${text}\x1b[0m`;
|
|
88
|
+
};
|
|
89
|
+
const blinkBrand = (glyph) => {
|
|
90
|
+
if (noColor || capability === 'ascii')
|
|
91
|
+
return glyph;
|
|
92
|
+
return `\x1b[5;1;${BRAND_RGB}m${glyph}\x1b[0m`;
|
|
93
|
+
};
|
|
94
|
+
// Warning + failed stay semantic — they are alarms, not brand colour.
|
|
95
|
+
// noColor still suppresses them because a tiny log consumer that
|
|
96
|
+
// sets NO_COLOR expects raw text.
|
|
97
|
+
const warning = noColor || capability === 'ascii' ? '!' : '\x1b[33m!\x1b[0m';
|
|
98
|
+
const failed = noColor || capability === 'ascii' ? 'x' : '\x1b[31m✕\x1b[0m';
|
|
99
|
+
if (capability === 'ascii') {
|
|
100
|
+
return {
|
|
101
|
+
active: '*',
|
|
102
|
+
idle: 'o',
|
|
103
|
+
warning,
|
|
104
|
+
inlineSeparator: ' . ',
|
|
105
|
+
trailSeparator: ' -> ',
|
|
106
|
+
idleLabel: 'empty',
|
|
107
|
+
invalidMessage: 'presence unreadable',
|
|
108
|
+
compact: {
|
|
109
|
+
queued: '[', preparing: '+', compacting: '+', verifying: '+',
|
|
110
|
+
completed: '*', failed,
|
|
111
|
+
},
|
|
112
|
+
barFilled: '#',
|
|
113
|
+
barEmpty: '-',
|
|
114
|
+
ratioArrow: '->',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
active: brandGlyph('●'),
|
|
119
|
+
idle: blinkBrand('○'),
|
|
120
|
+
warning,
|
|
121
|
+
inlineSeparator: brand(' · '),
|
|
122
|
+
trailSeparator: brand(' → '),
|
|
123
|
+
idleLabel: brand('empty'),
|
|
124
|
+
invalidMessage: brand('presence unreadable'),
|
|
96
125
|
compact: {
|
|
97
|
-
queued: '
|
|
98
|
-
preparing: '
|
|
99
|
-
compacting: '
|
|
100
|
-
verifying: '
|
|
101
|
-
completed: '
|
|
102
|
-
failed
|
|
126
|
+
queued: brandGlyph('◐'),
|
|
127
|
+
preparing: brandGlyph('◑'),
|
|
128
|
+
compacting: brandGlyph('◒'),
|
|
129
|
+
verifying: brandGlyph('◓'),
|
|
130
|
+
completed: brandGlyph('✓'),
|
|
131
|
+
failed,
|
|
103
132
|
},
|
|
104
|
-
barFilled: '
|
|
105
|
-
barEmpty: '
|
|
106
|
-
ratioArrow: '
|
|
107
|
-
}
|
|
108
|
-
}
|
|
133
|
+
barFilled: brand('█'),
|
|
134
|
+
barEmpty: dimBrand('░'),
|
|
135
|
+
ratioArrow: dimBrand('→'),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
109
138
|
const BREATHING_GLYPHS_UNICODE = ['●', '◐', '◑', '◒', '◓'];
|
|
110
139
|
const BREATHING_GLYPHS_ASCII = ['*', 'o', '+', '~', '|'];
|
|
111
140
|
const BREATHING_PERIOD_MS = 2_400;
|
|
@@ -133,23 +162,23 @@ function pickBreathingGlyph(capability, nowMs) {
|
|
|
133
162
|
const index = Math.floor((nowMs % BREATHING_PERIOD_MS) / (BREATHING_PERIOD_MS / set.length)) % set.length;
|
|
134
163
|
return set[index];
|
|
135
164
|
}
|
|
136
|
-
function renderActiveDot(capability, nowMs) {
|
|
165
|
+
function renderActiveDot(capability, nowMs, noColor) {
|
|
137
166
|
// Brief: the active dot carries the project accent (`#5A65D8` bold)
|
|
138
167
|
// in both colored tiers. The breathing glyph is wrapped in a fresh
|
|
139
168
|
// SGR on every render so the IDE sees a single accent per refresh.
|
|
169
|
+
// NO_COLOR strips the SGR but keeps the glyph (still rotates).
|
|
140
170
|
const glyph = pickBreathingGlyph(capability, nowMs);
|
|
141
|
-
if (capability === 'ascii')
|
|
171
|
+
if (noColor || capability === 'ascii')
|
|
142
172
|
return glyph;
|
|
143
|
-
return
|
|
173
|
+
return `${BRAND_SGR_OPEN}${glyph}${BRAND_SGR_CLOSE}`;
|
|
144
174
|
}
|
|
145
|
-
function brandText(capability) {
|
|
175
|
+
function brandText(capability, noColor) {
|
|
146
176
|
// Brief: brand carries the project accent (`#5A65D8` bold) in both
|
|
147
|
-
// colored tiers. The `ascii` tier
|
|
148
|
-
//
|
|
149
|
-
if (
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
return BRAND;
|
|
177
|
+
// colored tiers. The `ascii` tier and NO_COLOR stay plain text so
|
|
178
|
+
// log / file consumers never see escape codes.
|
|
179
|
+
if (noColor || capability === 'ascii')
|
|
180
|
+
return BRAND;
|
|
181
|
+
return `${BRAND_SGR_OPEN}${BRAND}${BRAND_SGR_CLOSE}`;
|
|
153
182
|
}
|
|
154
183
|
/**
|
|
155
184
|
* Default capability when none is supplied. Per the brief: "if backward
|
|
@@ -159,8 +188,8 @@ function brandText(capability) {
|
|
|
159
188
|
* default is the unicode palette (no escape codes).
|
|
160
189
|
*/
|
|
161
190
|
const DEFAULT_CAPABILITY = 'unicode';
|
|
162
|
-
function paletteFor(capability) {
|
|
163
|
-
return
|
|
191
|
+
function paletteFor(capability, noColor) {
|
|
192
|
+
return buildPalette(capability ?? DEFAULT_CAPABILITY, noColor);
|
|
164
193
|
}
|
|
165
194
|
function formatAge(ageMs) {
|
|
166
195
|
if (ageMs === null)
|
|
@@ -182,35 +211,35 @@ function rootLabel(projectRoot) {
|
|
|
182
211
|
* is obvious at the call site and so each state has a single
|
|
183
212
|
* responsibility.
|
|
184
213
|
*/
|
|
185
|
-
function renderActive(presence, palette, nowMs, capability) {
|
|
214
|
+
function renderActive(presence, palette, nowMs, capability, noColor) {
|
|
186
215
|
if (!presence) {
|
|
187
216
|
return `${palette.idle} ${palette.idleLabel}`;
|
|
188
217
|
}
|
|
189
218
|
const attentionLabel = isAttentionGate(presence.gate);
|
|
190
219
|
if (attentionLabel !== null) {
|
|
191
220
|
// Attention gate — surface the warning glyph + human-readable label.
|
|
192
|
-
return `${palette.warning} ${presence.skill}${palette.inlineSeparator}${attentionLabel}`;
|
|
221
|
+
return `${palette.warning} ${brandRun(presence.skill, noColor, capability)}${palette.inlineSeparator}${brandRun(attentionLabel, noColor, capability)}`;
|
|
193
222
|
}
|
|
194
223
|
const skill = presence.skill;
|
|
195
|
-
const dot = renderActiveDot(capability, nowMs);
|
|
224
|
+
const dot = renderActiveDot(capability, nowMs, noColor);
|
|
196
225
|
const beeParent = BEE_TO_PARENT[skill];
|
|
197
226
|
const modeToken = typeof presence.mode === 'string' && presence.mode.length > 0
|
|
198
|
-
? ` [${presence.mode}]
|
|
227
|
+
? brandRun(` [${presence.mode}]`, noColor, capability)
|
|
199
228
|
: '';
|
|
200
229
|
// Bee-tier skills surface both layers: the active bee name plus
|
|
201
230
|
// the full parent orchestrator name. Mode is shown for every
|
|
202
231
|
// active skill (not just peaks-code) so the line carries the same
|
|
203
232
|
// mode taxonomy for any bee in flight.
|
|
204
233
|
if (beeParent !== undefined) {
|
|
205
|
-
return `${dot} ${skill}
|
|
234
|
+
return `${dot} ${brandRun(skill, noColor, capability)} ${brandRun(`↑${beeParent}`, noColor, capability)}${modeToken}`;
|
|
206
235
|
}
|
|
207
|
-
return `${dot} ${skill}${modeToken}`;
|
|
236
|
+
return `${dot} ${brandRun(skill, noColor, capability)}${modeToken}`;
|
|
208
237
|
}
|
|
209
|
-
function renderStale(presence, ageMs, palette) {
|
|
238
|
+
function renderStale(presence, ageMs, palette, capability, noColor) {
|
|
210
239
|
const skill = presence?.skill ?? 'unknown';
|
|
211
240
|
const age = formatAge(ageMs);
|
|
212
|
-
const ageSuffix = age ? `${palette.inlineSeparator}${age}` : '';
|
|
213
|
-
return `${palette.warning} ${skill}${ageSuffix}`;
|
|
241
|
+
const ageSuffix = age ? `${palette.inlineSeparator}${brandRun(age, noColor, capability)}` : '';
|
|
242
|
+
return `${palette.warning} ${brandRun(skill, noColor, capability)}${ageSuffix}`;
|
|
214
243
|
}
|
|
215
244
|
function renderInvalid(palette) {
|
|
216
245
|
return `${palette.warning} ${palette.invalidMessage}`;
|
|
@@ -339,6 +368,18 @@ function renderCompact(state, palette) {
|
|
|
339
368
|
* is a logger, a file, or any non-interactive sink, and only enables ANSI
|
|
340
369
|
* under an explicit supported condition (TTY + no env veto).
|
|
341
370
|
*/
|
|
371
|
+
/**
|
|
372
|
+
* NO_COLOR (https://no-color.org) is the cross-industry signal that NO
|
|
373
|
+
* ANSI escape sequences should be emitted. The unicode/ansi-unicode
|
|
374
|
+
* tiers honour it by stripping every brand SGR while keeping the UTF-8
|
|
375
|
+
* glyphs (●, █, ░, ◐, …) — NO_COLOR addresses ANSI specifically.
|
|
376
|
+
* PEAKS_STATUSLINE_ASCII is the stricter override that also drops the
|
|
377
|
+
* unicode glyphs; both can coexist and the ASCII override wins.
|
|
378
|
+
*/
|
|
379
|
+
function isNoColor(env) {
|
|
380
|
+
const v = env['NO_COLOR'];
|
|
381
|
+
return typeof v === 'string' && v.length > 0 && v !== '0' && v.toLowerCase() !== 'false';
|
|
382
|
+
}
|
|
342
383
|
export function resolveStatusLineCapability(input) {
|
|
343
384
|
if (input.forced !== undefined) {
|
|
344
385
|
return input.forced;
|
|
@@ -350,12 +391,158 @@ export function resolveStatusLineCapability(input) {
|
|
|
350
391
|
if (typeof asciiFlag === 'string' && (asciiFlag === '1' || asciiFlag === 'true' || asciiFlag === 'yes')) {
|
|
351
392
|
return 'ascii';
|
|
352
393
|
}
|
|
353
|
-
// NO_COLOR is
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
// whether or not the consumer is a TTY.
|
|
394
|
+
// NO_COLOR is honoured at render time (see {@link renderStatusLine});
|
|
395
|
+
// capability stays unicode so the UTF-8 glyphs survive — only the
|
|
396
|
+
// brand SGR is suppressed.
|
|
357
397
|
return input.isTTY ? 'ansi-unicode' : 'unicode';
|
|
358
398
|
}
|
|
399
|
+
export function isNoColorEnv(env) {
|
|
400
|
+
return isNoColor(env);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Marquee scan band — a single-pass light band that sweeps left ↔ right
|
|
404
|
+
* across the entire status line on a 2 s round trip. The band's
|
|
405
|
+
* foreground color is `#E0E0E0` (off-white) with `1;` (bold) — see
|
|
406
|
+
* {@link HIGHLIGHT_SGR_OPEN}. Cells OUTSIDE the band keep their
|
|
407
|
+
* original SGR (brand purple or semantic warning/failed); only cells
|
|
408
|
+
* INSIDE the band are temporarily re-painted to the highlight color.
|
|
409
|
+
*
|
|
410
|
+
* Why it does NOT use SGR 7 (reverse video):
|
|
411
|
+
* - Reverse video inverts BOTH background and foreground, which on a
|
|
412
|
+
* `#5A65D8` background swaps to a white background — visually that
|
|
413
|
+
* reads as a solid white block instead of a scan band.
|
|
414
|
+
* - The off-white foreground + bold keeps the original background
|
|
415
|
+
* visible behind each glyph, so the band reads as "lit text" not
|
|
416
|
+
* as a solid stripe — closer to the CSDN-style scan-band reference
|
|
417
|
+
* image.
|
|
418
|
+
*
|
|
419
|
+
* Phase formula:
|
|
420
|
+
* phase ∈ [0, 1) over MARQUEE_PERIOD_MS
|
|
421
|
+
* sweep = phase < 0.5 ? phase * 2 : (1 - phase) * 2 ← triangle wave 0→1→0
|
|
422
|
+
* center = sweep * (width - 1)
|
|
423
|
+
* band covers visible-cell range [center - half, center + half]
|
|
424
|
+
*
|
|
425
|
+
* When the band would land outside the visible range (center ≤ half
|
|
426
|
+
* OR center ≥ width-1-half) it is clipped — the band never appears
|
|
427
|
+
* off-line. At `nowMs = 0` the band sits at the LEFT edge covering
|
|
428
|
+
* cells [0, bandWidth); this is a deliberate deterministic anchor
|
|
429
|
+
* for tests that pin `withPinnedClock(0, ...)`.
|
|
430
|
+
*
|
|
431
|
+
* ASCII tier: skipped — there are no SGR codes to inject.
|
|
432
|
+
*/
|
|
433
|
+
const MARQUEE_PERIOD_MS = 2_000;
|
|
434
|
+
const MARQUEE_BAND_WIDTH = 5;
|
|
435
|
+
/**
|
|
436
|
+
* Visible-character width of an ANSI-bearing string. Skips every
|
|
437
|
+
* `\x1b[...m` escape so the count reflects what the terminal paints,
|
|
438
|
+
* not the byte length on the wire.
|
|
439
|
+
*/
|
|
440
|
+
export function visibleCharWidth(s) {
|
|
441
|
+
let w = 0;
|
|
442
|
+
for (let i = 0; i < s.length; i++) {
|
|
443
|
+
if (s[i] === '\x1b' && s[i + 1] === '[') {
|
|
444
|
+
const m = s.indexOf('m', i + 2);
|
|
445
|
+
if (m !== -1) {
|
|
446
|
+
i = m;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
w++;
|
|
451
|
+
}
|
|
452
|
+
return w;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Split an ANSI string into a flat token stream. Escape sequences are
|
|
456
|
+
* grouped as `\x1b[...m` (single token, kind='esc'); everything else
|
|
457
|
+
* is grouped as a single 'text' run. Used by {@link applyMarquee} to
|
|
458
|
+
* locate visible-cell ranges inside an SGR-bearing string.
|
|
459
|
+
*/
|
|
460
|
+
export function tokenizeAnsi(s) {
|
|
461
|
+
const out = [];
|
|
462
|
+
let i = 0;
|
|
463
|
+
let buf = '';
|
|
464
|
+
const flush = () => {
|
|
465
|
+
if (buf.length > 0) {
|
|
466
|
+
out.push({ kind: 'text', value: buf });
|
|
467
|
+
buf = '';
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
while (i < s.length) {
|
|
471
|
+
if (s[i] === '\x1b' && s[i + 1] === '[') {
|
|
472
|
+
const m = s.indexOf('m', i + 2);
|
|
473
|
+
if (m !== -1) {
|
|
474
|
+
flush();
|
|
475
|
+
out.push({ kind: 'esc', value: s.slice(i, m + 1) });
|
|
476
|
+
i = m + 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
buf += s[i];
|
|
481
|
+
i++;
|
|
482
|
+
}
|
|
483
|
+
flush();
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Apply the marquee highlight band to an ANSI-bearing string. Pure.
|
|
488
|
+
* - `bandStart` / `bandEnd` are visible-cell indices (0-based).
|
|
489
|
+
* - Cells in [bandStart, bandEnd] receive a `\x1b[1;38;2;224;224;224m`
|
|
490
|
+
* prefix; cells after the band receive the closing `\x1b[0m` reset
|
|
491
|
+
* so the rest of the line keeps its original SGR.
|
|
492
|
+
* - Empty strings and zero-width bands return the input unchanged.
|
|
493
|
+
*/
|
|
494
|
+
export function applyMarqueeHighlight(s, bandStart, bandEnd) {
|
|
495
|
+
if (s.length === 0)
|
|
496
|
+
return s;
|
|
497
|
+
if (bandEnd < 0 || bandStart > bandEnd)
|
|
498
|
+
return s;
|
|
499
|
+
const tokens = tokenizeAnsi(s);
|
|
500
|
+
const parts = [];
|
|
501
|
+
let visibleIdx = 0;
|
|
502
|
+
let inside = false;
|
|
503
|
+
for (const tok of tokens) {
|
|
504
|
+
if (tok.kind === 'esc') {
|
|
505
|
+
parts.push(tok.value);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
for (let k = 0; k < tok.value.length; k++) {
|
|
509
|
+
const cellPos = visibleIdx + k;
|
|
510
|
+
const inBand = cellPos >= bandStart && cellPos <= bandEnd;
|
|
511
|
+
if (inBand && !inside) {
|
|
512
|
+
parts.push(HIGHLIGHT_SGR_OPEN);
|
|
513
|
+
inside = true;
|
|
514
|
+
}
|
|
515
|
+
else if (!inBand && inside) {
|
|
516
|
+
parts.push('\x1b[0m');
|
|
517
|
+
inside = false;
|
|
518
|
+
}
|
|
519
|
+
parts.push(tok.value[k]);
|
|
520
|
+
}
|
|
521
|
+
visibleIdx += tok.value.length;
|
|
522
|
+
}
|
|
523
|
+
if (inside)
|
|
524
|
+
parts.push('\x1b[0m');
|
|
525
|
+
return parts.join('');
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Compute the marquee band for a string at time `nowMs` and re-emit
|
|
529
|
+
* the string with the band applied. ASCII tier is a pass-through (no
|
|
530
|
+
* SGR to inject) — the scanner has no visible effect on plain text.
|
|
531
|
+
*/
|
|
532
|
+
export function applyMarquee(s, nowMs, capability) {
|
|
533
|
+
if (capability === 'ascii')
|
|
534
|
+
return s;
|
|
535
|
+
const width = visibleCharWidth(s);
|
|
536
|
+
if (width === 0)
|
|
537
|
+
return s;
|
|
538
|
+
const phase = (nowMs % MARQUEE_PERIOD_MS) / MARQUEE_PERIOD_MS;
|
|
539
|
+
const sweep = phase < 0.5 ? phase * 2 : (1 - phase) * 2;
|
|
540
|
+
const center = Math.round(sweep * (width - 1));
|
|
541
|
+
const halfBand = Math.floor(MARQUEE_BAND_WIDTH / 2);
|
|
542
|
+
const bandStart = Math.max(0, center - halfBand);
|
|
543
|
+
const bandEnd = Math.min(width - 1, center + halfBand);
|
|
544
|
+
return applyMarqueeHighlight(s, bandStart, bandEnd);
|
|
545
|
+
}
|
|
359
546
|
/**
|
|
360
547
|
* Render the status line. Pure — no I/O, no side effects. The output is
|
|
361
548
|
* a single short line suitable for the bottom-of-terminal status bar.
|
|
@@ -367,34 +554,59 @@ export function resolveStatusLineCapability(input) {
|
|
|
367
554
|
* Compact-state precedence: when `model.compact.kind !== 'none'` the
|
|
368
555
|
* compact bar replaces the active/idle/stale content. The C1 baseline
|
|
369
556
|
* line is preserved when the compact state is `none`.
|
|
557
|
+
*
|
|
558
|
+
* The full line is wrapped by {@link applyMarquee} so the brand-purple
|
|
559
|
+
* text + non-focal bar cells share a single colour surface while the
|
|
560
|
+
* scanner band paints its `#E0E0E0` highlight over a moving slice.
|
|
561
|
+
*
|
|
562
|
+
* Idle suppression: the marquee is OFF when `model.state === 'idle'`
|
|
563
|
+
* (and no compact state is active). Idle keeps the slow-blink `○`
|
|
564
|
+
* as the only motion — the user is asking whether the harness is
|
|
565
|
+
* running a skill; the scan band would compete for attention and
|
|
566
|
+
* obscure that question. Active / stale / invalid-presence / compact
|
|
567
|
+
* states all keep the marquee.
|
|
370
568
|
*/
|
|
371
|
-
export function renderStatusLine(model, options) {
|
|
569
|
+
export function renderStatusLine(model, options, env) {
|
|
372
570
|
const capability = options?.capability ?? DEFAULT_CAPABILITY;
|
|
373
|
-
const
|
|
571
|
+
const noColor = env !== undefined ? isNoColor(env) : false;
|
|
572
|
+
const palette = paletteFor(capability, noColor);
|
|
374
573
|
const root = rootLabel(model.projectRoot);
|
|
375
574
|
const rootSuffix = root ? `${palette.trailSeparator}${root}` : '';
|
|
376
|
-
const brand = brandText(capability);
|
|
575
|
+
const brand = brandText(capability, noColor);
|
|
377
576
|
// Breathing pulse: key off a single wall-clock read at the start of
|
|
378
577
|
// render so the output is deterministic for a given invocation. The
|
|
379
578
|
// glyph remains a single cell so total visible width is constant.
|
|
380
579
|
const nowMs = Date.now();
|
|
381
580
|
const compactSegment = renderCompact(model.compact, palette);
|
|
382
|
-
|
|
581
|
+
let line;
|
|
582
|
+
const hasCompact = compactSegment.length > 0;
|
|
583
|
+
if (hasCompact) {
|
|
383
584
|
// Compact state replaces the active / stale / idle skill content.
|
|
384
585
|
// `invalid-presence` still surfaces its own diagnostic when compact
|
|
385
586
|
// is also `invalid` (the compact diagnostic wins, since it's the
|
|
386
587
|
// more recent failure mode).
|
|
387
|
-
|
|
588
|
+
line = `${brand} ${compactSegment}${rootSuffix}`;
|
|
388
589
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
590
|
+
else {
|
|
591
|
+
switch (model.state) {
|
|
592
|
+
case 'active':
|
|
593
|
+
line = `${brand} ${renderActive(model.presence, palette, nowMs, capability, noColor)}${rootSuffix}`;
|
|
594
|
+
break;
|
|
595
|
+
case 'stale':
|
|
596
|
+
line = `${brand} ${renderStale(model.presence, model.ageMs, palette, capability, noColor)}${rootSuffix}`;
|
|
597
|
+
break;
|
|
598
|
+
case 'invalid-presence':
|
|
599
|
+
line = `${brand} ${renderInvalid(palette)}${rootSuffix}`;
|
|
600
|
+
break;
|
|
601
|
+
case 'idle':
|
|
602
|
+
default:
|
|
603
|
+
line = `${brand} ${renderIdle(palette)}${rootSuffix}`;
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
399
606
|
}
|
|
607
|
+
// Marquee is OFF for idle (and only idle). Compact states always
|
|
608
|
+
// carry the band because the compact bar IS the headline. NO_COLOR
|
|
609
|
+
// skips the band entirely — there's no SGR to inject.
|
|
610
|
+
const shouldMarquee = !noColor && (hasCompact || model.state !== 'idle');
|
|
611
|
+
return shouldMarquee ? applyMarquee(line, nowMs, capability) : line;
|
|
400
612
|
}
|
|
@@ -83,19 +83,6 @@ function tailLines(text, max) {
|
|
|
83
83
|
}
|
|
84
84
|
async function runTypecheck(projectRoot) {
|
|
85
85
|
const start = Date.now();
|
|
86
|
-
// Slice 4.0.7-dogfood-PR-10 (ice-cola surface probe 2026-08-02):
|
|
87
|
-
// detect pnpm-workspace.yaml and run a per-package typecheck instead
|
|
88
|
-
// of the root `tsc --noEmit`. Pre-rid the typecheck stage ran
|
|
89
|
-
// against the root, which has no `tsconfig.json` (only
|
|
90
|
-
// `tsconfig.base.json`) in ice-cola, and reported
|
|
91
|
-
// `tsc exited with code 1` in 2ms — a fake-fail with no actionable
|
|
92
|
-
// detail. The fix dispatches to `pnpm -r --parallel exec tsc
|
|
93
|
-
// --noEmit` (or `pnpm -r typecheck` if defined) and reports
|
|
94
|
-
// per-package failures in `data.failedPackages`.
|
|
95
|
-
const isMonorepo = existsSync(join(projectRoot, 'pnpm-workspace.yaml'));
|
|
96
|
-
if (isMonorepo) {
|
|
97
|
-
return runMonorepoTypecheck(projectRoot, start);
|
|
98
|
-
}
|
|
99
86
|
// Per Windows npx ENOENT (observations 2317+2792 from
|
|
100
87
|
// 2026-06-09), prefer the project-local `node_modules/.bin/tsc`
|
|
101
88
|
// (symlink on Unix, .cmd on Windows). The local binary is
|
|
@@ -115,52 +102,6 @@ async function runTypecheck(projectRoot) {
|
|
|
115
102
|
data: { exitCode: result.exitCode }
|
|
116
103
|
};
|
|
117
104
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Slice 4.0.7-dogfood-PR-10: per-package typecheck for pnpm
|
|
120
|
-
* monorepos. Tries `pnpm -r typecheck` first (the convention), then
|
|
121
|
-
* falls back to `pnpm -r --parallel exec tsc --noEmit`. Captures
|
|
122
|
-
* per-package failures so callers can see which sub-packages
|
|
123
|
-
* actually failed (the pre-rid "tsc exited with code 1" was an
|
|
124
|
-
* unhelpful 1-line summary that took 2ms — clear fake-fail).
|
|
125
|
-
*/
|
|
126
|
-
async function runMonorepoTypecheck(projectRoot, start) {
|
|
127
|
-
const pnpm = resolveLocalBinary(projectRoot, 'pnpm');
|
|
128
|
-
// First try the convention script.
|
|
129
|
-
const r1 = runCommand(pnpm.command, [...pnpm.args, '-r', '--workspace-concurrency=4', 'typecheck'], projectRoot, 300_000, pnpm.shell);
|
|
130
|
-
let result = r1;
|
|
131
|
-
let mode = 'pnpm -r typecheck';
|
|
132
|
-
if (result.status === 'fail') {
|
|
133
|
-
// Convention script missing in some sub-packages. Fall back to
|
|
134
|
-
// direct tsc invocation in parallel.
|
|
135
|
-
const tsc = resolveLocalBinary(projectRoot, 'tsc');
|
|
136
|
-
result = runCommand(pnpm.command, [...pnpm.args, '-r', '--parallel', 'exec', tsc.command, ...tsc.args, '--noEmit'], projectRoot, 300_000, pnpm.shell);
|
|
137
|
-
mode = 'pnpm -r exec tsc --noEmit';
|
|
138
|
-
}
|
|
139
|
-
const failedPackages = [];
|
|
140
|
-
// pnpm -r prints "<pkg>> error TS..." lines on failure. Extract
|
|
141
|
-
// them so the operator can see which sub-package broke.
|
|
142
|
-
const failLines = result.stdout.split('\n');
|
|
143
|
-
for (const line of failLines) {
|
|
144
|
-
const m = line.match(/^([^>\s]+)>\s+(.+)$/);
|
|
145
|
-
if (m && m[1] !== undefined && m[2] !== undefined && /error TS/.test(m[2])) {
|
|
146
|
-
failedPackages.push({
|
|
147
|
-
package: m[1],
|
|
148
|
-
exitCode: result.exitCode,
|
|
149
|
-
stderrTail: m[2].slice(0, 200)
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
name: 'typecheck',
|
|
155
|
-
description: `${mode} (monorepo per-package typecheck)`,
|
|
156
|
-
status: result.status,
|
|
157
|
-
durationMs: Date.now() - start,
|
|
158
|
-
detail: result.status === 'pass'
|
|
159
|
-
? `Monorepo typecheck passed in ${Date.now() - start}ms (${mode}).`
|
|
160
|
-
: tailLines(result.stdout + result.stderr, 10) || `pnpm exited with code ${result.exitCode}.`,
|
|
161
|
-
data: { exitCode: result.exitCode, mode, failedPackages }
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
105
|
function parseVitestSummary(stdout, fallbackDuration) {
|
|
165
106
|
// Vitest 2.x prints e.g. "Test Files 1 passed (1)" and "Tests 1 passed (1)"
|
|
166
107
|
// and "Duration 0.50s" or "Duration 1.23s". Be lenient with regex.
|
|
@@ -85,23 +85,8 @@ function parseLanguage(value) {
|
|
|
85
85
|
throw new Error('Unsupported standards language');
|
|
86
86
|
}
|
|
87
87
|
function detectLanguageInternal(projectRoot) {
|
|
88
|
-
// Slice 4.0.7-dogfood-PR-7 (ice-cola surface probe 2026-08-02): a
|
|
89
|
-
// monorepo root often has no `tsconfig.json` (it has
|
|
90
|
-
// `tsconfig.base.json` + per-package `tsconfig.json`), and the root
|
|
91
|
-
// `package.json` may be a thin workspace manifest (no language-bearing
|
|
92
|
-
// deps). Pre-rid, ice-cola (4-package TypeScript monorepo:
|
|
93
|
-
// NestJS server / React admin / Tauri client / Hermes agent) was
|
|
94
|
-
// misclassified as `javascript` because the root has no `tsconfig.json`
|
|
95
|
-
// and the root `package.json#devDependencies` only lists `peaks-loop`
|
|
96
|
-
// (a string of "javascript" in the standards devDep pack). The fix
|
|
97
|
-
// walks `packages/*/` recursively and prefers the first match of a
|
|
98
|
-
// language-bearing manifest over the root.
|
|
99
|
-
if (scanMonorepoPackages(projectRoot, 'tsconfig.json'))
|
|
100
|
-
return 'typescript';
|
|
101
88
|
if (existsSync(join(projectRoot, 'tsconfig.json')))
|
|
102
89
|
return 'typescript';
|
|
103
|
-
if (scanMonorepoPackages(projectRoot, 'package.json'))
|
|
104
|
-
return 'javascript';
|
|
105
90
|
if (existsSync(join(projectRoot, 'package.json')))
|
|
106
91
|
return 'javascript';
|
|
107
92
|
if (existsSync(join(projectRoot, 'pyproject.toml')) || existsSync(join(projectRoot, 'requirements.txt')))
|
|
@@ -112,35 +97,6 @@ function detectLanguageInternal(projectRoot) {
|
|
|
112
97
|
return 'rust';
|
|
113
98
|
return 'generic';
|
|
114
99
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Slice 4.0.7-dogfood-PR-7: look one level deep into the standard
|
|
117
|
-
* pnpm monorepo layout (`packages/<name>/`) and return true if any
|
|
118
|
-
* sub-package contains the named manifest. We deliberately do NOT
|
|
119
|
-
* recurse arbitrarily (no fs.walk) — the convention is one
|
|
120
|
-
* packages/[name]/ level. A repo with a nested
|
|
121
|
-
* workspaces/[name]/[name]/package.json is not in the supported
|
|
122
|
-
* monorepo shape and will fall through to the root scan.
|
|
123
|
-
*/
|
|
124
|
-
function scanMonorepoPackages(projectRoot, manifestName) {
|
|
125
|
-
const packagesDir = join(projectRoot, 'packages');
|
|
126
|
-
if (!existsSync(packagesDir))
|
|
127
|
-
return false;
|
|
128
|
-
let entries;
|
|
129
|
-
try {
|
|
130
|
-
entries = readdirSync(packagesDir);
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
for (const entry of entries) {
|
|
136
|
-
if (entry === '.' || entry === '..')
|
|
137
|
-
continue;
|
|
138
|
-
const candidate = join(packagesDir, entry, manifestName);
|
|
139
|
-
if (existsSync(candidate))
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
100
|
/**
|
|
145
101
|
* Public alias for `detectLanguageInternal` so callers outside this
|
|
146
102
|
* module (e.g. `workspace-service.ts` for the slice 2026-06-16 RD#7
|