peaks-loop 4.0.35 → 4.0.37
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 +40 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/bin/peaks.js +71 -1
- package/dist/cli/cli-helpers.js +7 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
- package/dist/cli/commands/best-practice-scan-command.js +67 -9
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +78 -7
- package/dist/cli/commands/core/doctor-command.d.ts +8 -0
- package/dist/cli/commands/core/doctor-command.js +44 -2
- package/dist/cli/commands/core/memory-command.js +5 -1
- package/dist/cli/commands/dispatch-commands.js +15 -3
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/hooks-commands.js +10 -1
- package/dist/cli/commands/job-commands.js +107 -25
- package/dist/cli/commands/memory-commands.d.ts +24 -0
- package/dist/cli/commands/memory-commands.js +77 -10
- package/dist/cli/commands/request-commands.d.ts +8 -0
- package/dist/cli/commands/request-commands.js +23 -2
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
- package/dist/cli/commands/wave-plan-commands.js +93 -0
- package/dist/cli/commands/web-commands.d.ts +28 -0
- package/dist/cli/commands/web-commands.js +327 -0
- package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
- package/dist/cli/commands/web-lifecycle-commands.js +321 -0
- package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
- package/dist/services/best-practice/scan-orchestrator.js +14 -5
- package/dist/services/code/orchestrator-can-do.js +27 -4
- package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
- package/dist/services/context/build-dispatch-system-prompt.js +132 -17
- package/dist/services/context/context-audit-hint.d.ts +79 -0
- package/dist/services/context/context-audit-hint.js +150 -0
- package/dist/services/context/context-audit.d.ts +100 -0
- package/dist/services/context/context-audit.js +322 -0
- package/dist/services/context/summary-view.d.ts +54 -0
- package/dist/services/context/summary-view.js +114 -0
- package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
- package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
- package/dist/services/dispatch/session-capsule.d.ts +23 -0
- package/dist/services/dispatch/session-capsule.js +56 -0
- package/dist/services/dispatch/slice-dag.d.ts +9 -0
- package/dist/services/dispatch/slice-dag.js +9 -1
- package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
- package/dist/services/dispatch/test-tool-detection.js +14 -13
- package/dist/services/hooks/auto-compact-hook-install.js +10 -1
- package/dist/services/hooks/write-gate.js +88 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/lint/detect-eslint.d.ts +2 -0
- package/dist/services/lint/detect-eslint.js +23 -9
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +38 -14
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
- package/dist/services/release/version-precheck-service.js +9 -2
- package/dist/services/scan/file-size-scan.d.ts +29 -0
- package/dist/services/scan/file-size-scan.js +63 -0
- package/dist/services/session/caller-binding-service.d.ts +24 -0
- package/dist/services/session/caller-binding-service.js +34 -0
- package/dist/services/session/getSessionDir.js +15 -10
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
- package/dist/services/skills/hooks-settings-service.d.ts +10 -0
- package/dist/services/skills/hooks-settings-service.js +152 -61
- package/dist/services/slice/slice-check-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +110 -50
- package/dist/services/slice/slice-check-types.d.ts +12 -7
- package/dist/services/slice/slice-check-types.js +8 -3
- package/dist/services/slice/slice-decompose-runners.js +24 -21
- package/dist/services/sop/sop-check-service.js +12 -1
- package/dist/services/web/bounded-output.d.ts +34 -0
- package/dist/services/web/bounded-output.js +68 -0
- package/dist/services/web/browser-acquire.d.ts +14 -0
- package/dist/services/web/browser-acquire.js +84 -0
- package/dist/services/web/browser-session-manager.d.ts +111 -0
- package/dist/services/web/browser-session-manager.js +413 -0
- package/dist/services/web/daemon-entry.d.ts +1 -0
- package/dist/services/web/daemon-entry.js +65 -0
- package/dist/services/web/daemon-registry.d.ts +42 -0
- package/dist/services/web/daemon-registry.js +164 -0
- package/dist/services/web/daemon-supervisor.d.ts +144 -0
- package/dist/services/web/daemon-supervisor.js +455 -0
- package/dist/services/web/playwright-loader.d.ts +89 -0
- package/dist/services/web/playwright-loader.js +253 -0
- package/dist/services/web/snapshot-pruner.d.ts +48 -0
- package/dist/services/web/snapshot-pruner.js +241 -0
- package/dist/services/web/untrusted-envelope.d.ts +27 -0
- package/dist/services/web/untrusted-envelope.js +44 -0
- package/dist/services/web/web-artifact-paths.d.ts +79 -0
- package/dist/services/web/web-artifact-paths.js +163 -0
- package/dist/services/web/web-client.d.ts +19 -0
- package/dist/services/web/web-client.js +55 -0
- package/dist/services/web/web-daemon-service.d.ts +38 -0
- package/dist/services/web/web-daemon-service.js +416 -0
- package/dist/services/web/web-fallback.d.ts +70 -0
- package/dist/services/web/web-fallback.js +121 -0
- package/dist/services/web/web-install-service.d.ts +91 -0
- package/dist/services/web/web-install-service.js +346 -0
- package/dist/services/web/web-login-profile.d.ts +89 -0
- package/dist/services/web/web-login-profile.js +612 -0
- package/dist/services/web/web-login-staging.d.ts +27 -0
- package/dist/services/web/web-login-staging.js +173 -0
- package/dist/services/web/web-protocol.d.ts +58 -0
- package/dist/services/web/web-protocol.js +58 -0
- package/dist/services/web/web-status-report.d.ts +33 -0
- package/dist/services/web/web-status-report.js +47 -0
- package/dist/services/workspace/claude-settings-template.d.ts +41 -5
- package/dist/services/workspace/claude-settings-template.js +116 -64
- package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
- package/dist/services/workspace/workspace-service.js +33 -0
- package/package.json +5 -5
- package/scripts/copy-templates.mjs +12 -0
- package/scripts/sync-version.mjs +20 -0
- package/skills/bee/peaks-qa/SKILL.md +2 -0
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
- package/skills/bee/peaks-rd/SKILL.md +2 -0
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
- package/skills/bee/peaks-txt/SKILL.md +2 -0
- package/skills/bee/peaks-ui/SKILL.md +2 -0
- package/skills/peaks-code/SKILL.md +18 -0
- package/skills/peaks-code/references/browser-workflow.md +10 -1
- package/skills/peaks-code/references/context-governance.md +29 -0
- package/skills/peaks-doctor/SKILL.md +2 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
|
|
2
|
+
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
3
|
+
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
4
|
+
import { stopDaemon } from '../../services/web/daemon-supervisor.js';
|
|
5
|
+
import { degradedEnvelope } from '../../services/web/web-fallback.js';
|
|
6
|
+
import { INSTALL_SIZE_WARNING, installChromium, isWebDisabled, probeBrowserInstalled } from '../../services/web/web-install-service.js';
|
|
7
|
+
import { buildStatusReport } from '../../services/web/web-status-report.js';
|
|
8
|
+
import { cappedEcho, loginStorageStatePath, resolveProfileName, runHeadedLogin } from '../../services/web/web-login-profile.js';
|
|
9
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
10
|
+
export function registerWebLifecycleCommands(web, io) {
|
|
11
|
+
addJsonOption(web
|
|
12
|
+
.command('status')
|
|
13
|
+
.description('Report this session\'s web daemon instances (live / orphaned / stale). Works with no ' +
|
|
14
|
+
'daemon running and never starts one.')).action(async (options) => {
|
|
15
|
+
await runWebStatus(io, options.json === true);
|
|
16
|
+
});
|
|
17
|
+
addJsonOption(web
|
|
18
|
+
.command('stop')
|
|
19
|
+
.description("Stop this session's web daemon and close its browser. Scoped to this project root and " +
|
|
20
|
+
'session; another worktree is untouched, and a process that cannot be proven to be this ' +
|
|
21
|
+
"session's daemon is left running rather than signalled.")).action(async (options) => {
|
|
22
|
+
await runWebStop(io, options.json === true);
|
|
23
|
+
});
|
|
24
|
+
addJsonOption(web
|
|
25
|
+
.command('install')
|
|
26
|
+
.description('Download the pinned chromium for `peaks web` (one time, ~700 MB on disk). Needed only ' +
|
|
27
|
+
'before the first browser op — a read-only verb never downloads. Refuses while ' +
|
|
28
|
+
'PEAKS_WEB_DISABLED=1.')
|
|
29
|
+
.option('--force', "reinstall even if the browser is already present (Playwright's own recovery path)")).action(async (options) => {
|
|
30
|
+
await runWebInstall(io, options.json === true, options.force === true);
|
|
31
|
+
});
|
|
32
|
+
addJsonOption(web
|
|
33
|
+
.command('login')
|
|
34
|
+
.description('Open a HEADED browser so the user can log in themselves, then persist that session to ' +
|
|
35
|
+
'~/.peaks/web-profiles/<name>/storageState.json. Run it only when the user asks for a ' +
|
|
36
|
+
'persistent login: it writes live session cookies to disk. --profile is required, and ' +
|
|
37
|
+
'without it nothing is persisted.')
|
|
38
|
+
.option('--profile <name>', 'the login profile to persist (required; [a-z0-9._-], 1-64 chars — upper case folds to lower)')).action(async (options) => {
|
|
39
|
+
await runWebLogin(io, options.json === true, options.profile);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** Resolve this project's session, or report `NO_SESSION` exactly once. */
|
|
43
|
+
function resolveSession() {
|
|
44
|
+
const projectRoot = resolveCanonicalProjectRoot(process.cwd());
|
|
45
|
+
const sessionId = getCurrentSessionId(projectRoot);
|
|
46
|
+
return sessionId === null ? null : { projectRoot, sessionId };
|
|
47
|
+
}
|
|
48
|
+
/** The shared `NO_SESSION` envelope for both lifecycle verbs. */
|
|
49
|
+
function noSession(command) {
|
|
50
|
+
return fail(command, 'NO_SESSION', 'No peaks session is bound to this project root', {}, [
|
|
51
|
+
'Bind a session first (the LLM runs `peaks workspace init` on your behalf)'
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
/** Everything `stop` did NOT clean, said out loud rather than left implicit. */
|
|
55
|
+
function stopWarnings(result) {
|
|
56
|
+
const warnings = [];
|
|
57
|
+
const stuck = result.pids.length - result.stopped;
|
|
58
|
+
if (stuck > 0) {
|
|
59
|
+
warnings.push(`${String(stuck)} daemon process(es) did not exit after the stop request; they are recorded, ` +
|
|
60
|
+
'so a later `peaks web stop` can try again');
|
|
61
|
+
}
|
|
62
|
+
if (result.orphanedPids.length > 0) {
|
|
63
|
+
warnings.push(`${String(result.orphanedPids.length)} daemon instance(s) are alive but could not be proven ` +
|
|
64
|
+
'to be this session\'s daemon (no authenticated identity); the processes were left running, ' +
|
|
65
|
+
'because a pid that cannot be proven is never signalled, and their records are kept so ' +
|
|
66
|
+
'`peaks web status` and a later `stop` still see them');
|
|
67
|
+
}
|
|
68
|
+
return warnings;
|
|
69
|
+
}
|
|
70
|
+
export async function runWebStatus(io, asJson) {
|
|
71
|
+
const command = 'peaks.web.status';
|
|
72
|
+
try {
|
|
73
|
+
const session = resolveSession();
|
|
74
|
+
if (session === null) {
|
|
75
|
+
printResult(io, noSession(command), asJson);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
printResult(io, ok(command, await buildStatusReport(session.projectRoot, session.sessionId)), asJson);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
printResult(io, fail(command, 'WEB_STATUS_FAILED', `peaks web status failed: ${getErrorMessage(error)}`, {}, []), asJson);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Stop the daemon and clear its records. `stopped` counts the daemons whose
|
|
88
|
+
* process is confirmed GONE by return — not merely the ones we signalled — so a
|
|
89
|
+
* caller that checks the process table right after this returns is not racing
|
|
90
|
+
* the teardown (AC6).
|
|
91
|
+
*/
|
|
92
|
+
export async function runWebStop(io, asJson) {
|
|
93
|
+
const command = 'peaks.web.stop';
|
|
94
|
+
try {
|
|
95
|
+
const session = resolveSession();
|
|
96
|
+
if (session === null) {
|
|
97
|
+
printResult(io, noSession(command), asJson);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const result = await stopDaemon(session.projectRoot, session.sessionId);
|
|
102
|
+
printResult(io, ok(command, {
|
|
103
|
+
stopped: result.stopped,
|
|
104
|
+
pids: [...result.pids],
|
|
105
|
+
orphanedPids: [...result.orphanedPids]
|
|
106
|
+
}, stopWarnings(result)), asJson);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
printResult(io, fail(command, 'WEB_STOP_FAILED', `peaks web stop failed: ${getErrorMessage(error)}`, {}, []), asJson);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** The MCP fallback for `install`, named so the LLM can act on it (decision C3). */
|
|
114
|
+
const WEB_DISABLED_ACTIONS = [
|
|
115
|
+
'Unset PEAKS_WEB_DISABLED to install locally',
|
|
116
|
+
'Or call mcp__playwright__browser_install instead'
|
|
117
|
+
];
|
|
118
|
+
/** The same fallback, minus the gate, for a download that failed on its own. */
|
|
119
|
+
const INSTALL_FAILED_ACTIONS = [
|
|
120
|
+
'Retry `peaks web install`, or `peaks web install --force` after a partial download',
|
|
121
|
+
'Or call mcp__playwright__browser_install instead'
|
|
122
|
+
];
|
|
123
|
+
/**
|
|
124
|
+
* `peaks web install` — the explicit form of the lazy download, plus R6's
|
|
125
|
+
* recovery path (`--force`).
|
|
126
|
+
*
|
|
127
|
+
* The gate is step 1 of the ordered gate (tech-doc §5.1): checked BEFORE the
|
|
128
|
+
* session lookup, before any lock and before anything that could touch the
|
|
129
|
+
* browser cache, so this verb cannot download under `PEAKS_WEB_DISABLED=1` even
|
|
130
|
+
* if every later step is broken (C2's matrix).
|
|
131
|
+
*/
|
|
132
|
+
export async function runWebInstall(io, asJson, force) {
|
|
133
|
+
const command = 'peaks.web.install';
|
|
134
|
+
try {
|
|
135
|
+
if (isWebDisabled(process.env)) {
|
|
136
|
+
printResult(io, fail(command, 'WEB_DISABLED', '`peaks web install` will not download anything while PEAKS_WEB_DISABLED=1', {}, WEB_DISABLED_ACTIONS), asJson);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const session = resolveSession();
|
|
141
|
+
if (session === null) {
|
|
142
|
+
printResult(io, noSession(command), asJson);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// R6's other half: a missing executable is the only reason to download, so
|
|
147
|
+
// an already-complete install is reported without spawning anything. The
|
|
148
|
+
// shortcut still names `--force`, because it is the only escape if a browser
|
|
149
|
+
// op keeps failing against a probe that says otherwise (R7).
|
|
150
|
+
const before = await probeBrowserInstalled();
|
|
151
|
+
if (before.installed && !force) {
|
|
152
|
+
printResult(io, ok(command, {
|
|
153
|
+
installed: true,
|
|
154
|
+
downloaded: false,
|
|
155
|
+
version: before.version,
|
|
156
|
+
executablePath: before.executablePath
|
|
157
|
+
}, [], ['If browser ops still fail, re-run `peaks web install --force`']), asJson);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// R2: name the size on the human channel BEFORE the blocking download —
|
|
161
|
+
// this is the only point at which "before" is still available.
|
|
162
|
+
io.stderr(`warning: ${INSTALL_SIZE_WARNING}`);
|
|
163
|
+
const outcome = await installChromium({ force });
|
|
164
|
+
if (!outcome.ok) {
|
|
165
|
+
// R2: a failed download is a structured tier-3 envelope, never a stack.
|
|
166
|
+
printResult(io, degradedEnvelope('install', `${outcome.code}: ${outcome.message}`), asJson);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// `after.installed` is CONSULTED, not merely carried (R7). An installer
|
|
171
|
+
// that exits 0 without landing the browser — a proxy that filters the CDN, a
|
|
172
|
+
// pinned npx resolving into a different cache root, a partial install — used
|
|
173
|
+
// to report `ok: true, downloaded: true, installed: false` with exit 0 and
|
|
174
|
+
// nothing to do next.
|
|
175
|
+
const after = await probeBrowserInstalled();
|
|
176
|
+
if (!after.installed) {
|
|
177
|
+
printResult(io, degradedEnvelope('install', 'WEB_INSTALL_INCOMPLETE: `playwright install chromium` exited 0 but the browser it ' +
|
|
178
|
+
'names is still missing'), asJson);
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
printResult(io, ok(command, {
|
|
183
|
+
installed: true,
|
|
184
|
+
downloaded: true,
|
|
185
|
+
version: after.version,
|
|
186
|
+
executablePath: after.executablePath
|
|
187
|
+
}, [...outcome.warnings]), asJson);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
printResult(io, fail(command, 'WEB_INSTALL_FAILED', `peaks web install failed: ${getErrorMessage(error)}`, {}, INSTALL_FAILED_ACTIONS), asJson);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** What a caller can actually do after a refusal or an unclosed login. */
|
|
195
|
+
const LOGIN_NEXT_ACTIONS = [
|
|
196
|
+
'Re-run `peaks web login --profile <name>` when the user is ready to log in',
|
|
197
|
+
'Close the headed browser window when the login is finished — closing it is what saves the session'
|
|
198
|
+
];
|
|
199
|
+
/**
|
|
200
|
+
* The fold, said out loud on the one path that CANNOT fold (S4 repair, code F5 /
|
|
201
|
+
* security S5).
|
|
202
|
+
*
|
|
203
|
+
* The gate is statement #1 (tech-doc §5.1), so on this path the name was never
|
|
204
|
+
* validated and never folded — which is exactly why this refusal must not look
|
|
205
|
+
* like it disagrees with a live run about the profile. It reports what happened
|
|
206
|
+
* (nothing was folded, this is what was typed) rather than implying a canonical
|
|
207
|
+
* name was used, and it claims nothing about whether the name would be accepted.
|
|
208
|
+
* The echo is capped by the same helper the guard uses.
|
|
209
|
+
*/
|
|
210
|
+
function gateFoldNotice(rawProfile) {
|
|
211
|
+
if (rawProfile === undefined || rawProfile === rawProfile.toLowerCase()) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
return [
|
|
215
|
+
`--profile ${JSON.stringify(cappedEcho(rawProfile))} was NOT folded: PEAKS_WEB_DISABLED is ` +
|
|
216
|
+
'checked before the profile name, so this refusal echoes what was typed, not a canonical ' +
|
|
217
|
+
'profile. A run that gets past the gate lower-cases the name first.'
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* `peaks web login --profile <name>` — the only path that persists a login
|
|
222
|
+
* (design §2/§5, PRD R7).
|
|
223
|
+
*
|
|
224
|
+
* `--profile` is enforced HERE rather than with commander's `requiredOption`,
|
|
225
|
+
* because `requiredOption` refuses before this handler runs — and then the
|
|
226
|
+
* `PEAKS_WEB_DISABLED` gate would no longer be statement #1 (tech-doc §5.1,
|
|
227
|
+
* AC5). A profile-less login refuses without touching the profile root: no
|
|
228
|
+
* directory, no storage state, no browser.
|
|
229
|
+
*/
|
|
230
|
+
export async function runWebLogin(io, asJson, rawProfile) {
|
|
231
|
+
const command = 'peaks.web.login';
|
|
232
|
+
// Declared out here so the CATCH can report it too (S4 repair, security S5 /
|
|
233
|
+
// code F3): a launch failure happens AFTER the name was resolved, so a run
|
|
234
|
+
// that dies on `WEB_LAUNCH_FAILED` knows the canonical name just as well as
|
|
235
|
+
// one that succeeds, and must not be the one path where the fold is silent.
|
|
236
|
+
let foldWarnings = [];
|
|
237
|
+
try {
|
|
238
|
+
if (isWebDisabled(process.env)) {
|
|
239
|
+
const gateEnvelope = degradedEnvelope('login', 'PEAKS_WEB_DISABLED=1', 3, rawProfile === undefined ? {} : { profile: cappedEcho(rawProfile) });
|
|
240
|
+
printResult(io, { ...gateEnvelope, warnings: [...gateEnvelope.warnings, ...gateFoldNotice(rawProfile)] }, asJson);
|
|
241
|
+
process.exitCode = 1;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (rawProfile === undefined || rawProfile === '') {
|
|
245
|
+
printResult(io, fail(command, 'WEB_PROFILE_REQUIRED', '`peaks web login` requires --profile <name>: it is the only verb that persists a login, ' +
|
|
246
|
+
'and there is no default profile — without a name nothing is written', {}, LOGIN_NEXT_ACTIONS), asJson);
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
let profile;
|
|
251
|
+
try {
|
|
252
|
+
profile = resolveProfileName(rawProfile);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
// The helper's own message already begins with the code, and `fail()`
|
|
256
|
+
// puts the code in front of the message again — strip it, so human output
|
|
257
|
+
// does not read `WEB_PROFILE_NAME_INVALID: WEB_PROFILE_NAME_INVALID: …`.
|
|
258
|
+
const detail = getErrorMessage(error).replace(/^WEB_PROFILE_NAME_INVALID:\s*/, '');
|
|
259
|
+
printResult(io, fail(command, 'WEB_PROFILE_NAME_INVALID', detail, {}, LOGIN_NEXT_ACTIONS), asJson);
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// The fold (see `resolveProfileName`) is never silent: `--profile Work` and
|
|
264
|
+
// `--profile work` are one profile, and the caller is told which name was
|
|
265
|
+
// used. The two sides are deliberately not the same value — the left is what
|
|
266
|
+
// the caller TYPED (echoed through `cappedEcho`, since it is still caller
|
|
267
|
+
// input), the right is the canonical name the run uses from here on.
|
|
268
|
+
foldWarnings =
|
|
269
|
+
rawProfile === profile
|
|
270
|
+
? []
|
|
271
|
+
: [`--profile ${JSON.stringify(cappedEcho(rawProfile))} resolved to the profile "${profile}"`];
|
|
272
|
+
const outcome = await runHeadedLogin({
|
|
273
|
+
profile,
|
|
274
|
+
announce: () => {
|
|
275
|
+
io.stderr([
|
|
276
|
+
`peaks web login: a headed browser is open for profile "${profile}" — log in there yourself.`,
|
|
277
|
+
'Close the browser window when you are done: that is what saves the session, to ' +
|
|
278
|
+
`${loginStorageStatePath(profile)}.`,
|
|
279
|
+
'The session is captured while the window is open, so the saved state can be up to a ' +
|
|
280
|
+
'second older than what you see — finish logging in before you close it.'
|
|
281
|
+
].join('\n'));
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
if (!outcome.ok) {
|
|
285
|
+
// S1's F1. This is the ONLY path on which the runner returns `ok: false`
|
|
286
|
+
// AND a warning (a login whose context never opened and whose headed
|
|
287
|
+
// browser then refused to close — a browser nothing can then stop,
|
|
288
|
+
// because `stop` is daemon-scoped). `fail()` hard-codes `warnings: []`, so
|
|
289
|
+
// the warning is spread back over it, the shape `degradedEnvelope` uses.
|
|
290
|
+
printResult(io, {
|
|
291
|
+
...fail(command, outcome.code, outcome.message, {}, LOGIN_NEXT_ACTIONS),
|
|
292
|
+
warnings: [...foldWarnings, ...outcome.warnings]
|
|
293
|
+
}, asJson);
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Counts, never the values: the file holds live session cookies and this
|
|
298
|
+
// envelope reaches a terminal (and a transcript).
|
|
299
|
+
const envelope = ok(command, {
|
|
300
|
+
profile: outcome.profile,
|
|
301
|
+
storageStatePath: outcome.storageStatePath,
|
|
302
|
+
bytes: outcome.bytes,
|
|
303
|
+
cookies: outcome.cookies,
|
|
304
|
+
origins: outcome.origins
|
|
305
|
+
}, [...foldWarnings, ...outcome.warnings]);
|
|
306
|
+
// R5: a capture that was written but could not be read back is `ok` — the
|
|
307
|
+
// file did land, so it is not a failure — but it is not a clean success
|
|
308
|
+
// either. The code is what an ok-only consumer cannot miss.
|
|
309
|
+
printResult(io, outcome.code === '' ? envelope : { ...envelope, code: outcome.code }, asJson);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
printResult(io, {
|
|
313
|
+
...fail(command, 'WEB_LOGIN_FAILED', `peaks web login failed: ${getErrorMessage(error)}`, {}, LOGIN_NEXT_ACTIONS),
|
|
314
|
+
// A run that reached `resolveProfileName` knows the canonical name, so a
|
|
315
|
+
// failure before the browser was even up reports the fold like every
|
|
316
|
+
// other path (S4 repair, code F5 / security S5).
|
|
317
|
+
warnings: foldWarnings
|
|
318
|
+
}, asJson);
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
* the orchestrator's source-priority logic is testable end-to-end.
|
|
13
13
|
* Real MCP wiring is a future slice — the stubs are clearly marked.
|
|
14
14
|
*
|
|
15
|
+
* Because a stub result is indistinguishable from a real one by shape,
|
|
16
|
+
* every result carries `synthetic`: true when the fragments came from a
|
|
17
|
+
* built-in stub rather than a caller-injected lookup (`context7Lookup` /
|
|
18
|
+
* `webSearchLookup`). Callers MUST NOT present a synthetic result as a
|
|
19
|
+
* scan result — `source` alone says which transport answered, not whether
|
|
20
|
+
* anything real was consulted. The stub seam stays injectable so the real
|
|
21
|
+
* wiring (and its tests) can replace it without touching this contract.
|
|
22
|
+
*
|
|
15
23
|
* The orchestrator emits structured log lines via the injected `io`
|
|
16
24
|
* (stdout for progress, stderr for warnings) so a CLI caller can see
|
|
17
25
|
* which fallback path was taken.
|
|
@@ -28,6 +36,12 @@ export type ScanResult = {
|
|
|
28
36
|
readonly fragments: readonly DocFragment[];
|
|
29
37
|
readonly source: ScanSource;
|
|
30
38
|
readonly elapsedMs: number;
|
|
39
|
+
/**
|
|
40
|
+
* `true` when the fragments came from the built-in stub lookups instead
|
|
41
|
+
* of a real documentation lookup. A synthetic result is not a scan
|
|
42
|
+
* result: it must never be rendered as one or gated on.
|
|
43
|
+
*/
|
|
44
|
+
readonly synthetic: boolean;
|
|
31
45
|
};
|
|
32
46
|
export type ScanOptions = {
|
|
33
47
|
readonly intent: string;
|
|
@@ -35,5 +49,13 @@ export type ScanOptions = {
|
|
|
35
49
|
readonly projectRoot: string;
|
|
36
50
|
readonly io: ProgramIO;
|
|
37
51
|
readonly context7TimeoutMs?: number;
|
|
52
|
+
/** Test / real-wiring seam: replaces the priority-1 Context7 stub. */
|
|
53
|
+
readonly context7Lookup?: LookupFn;
|
|
54
|
+
/** Test / real-wiring seam: replaces the priority-2 WebSearch stub. */
|
|
55
|
+
readonly webSearchLookup?: LookupFn;
|
|
38
56
|
};
|
|
57
|
+
export type LookupFn = (intent: string, language: string) => Promise<{
|
|
58
|
+
readonly ok: boolean;
|
|
59
|
+
readonly results: readonly DocFragment[];
|
|
60
|
+
}>;
|
|
39
61
|
export declare function scanBestPractice(opts: ScanOptions): Promise<ScanResult>;
|
|
@@ -35,11 +35,17 @@ const defaultWebSearchLookup = async (intent, language) => {
|
|
|
35
35
|
export async function scanBestPractice(opts) {
|
|
36
36
|
const timeoutMs = opts.context7TimeoutMs ?? DEFAULT_CONTEXT7_TIMEOUT_MS;
|
|
37
37
|
const startedAt = Date.now();
|
|
38
|
+
const context7Lookup = opts.context7Lookup ?? defaultContext7Lookup;
|
|
39
|
+
const webSearchLookup = opts.webSearchLookup ?? defaultWebSearchLookup;
|
|
40
|
+
// A branch is synthetic when the lookup that answered it is a stub. The
|
|
41
|
+
// fallback branch counts as synthetic if ANY stub took part in the chain.
|
|
42
|
+
const context7Stub = opts.context7Lookup === undefined;
|
|
43
|
+
const webSearchStub = opts.webSearchLookup === undefined;
|
|
38
44
|
opts.io.stdout(`[scan-orchestrator] querying context7 for "${opts.intent}" (${opts.language})`);
|
|
39
45
|
let context7Outcome = null;
|
|
40
46
|
let context7Error = null;
|
|
41
47
|
try {
|
|
42
|
-
const ctxPromise =
|
|
48
|
+
const ctxPromise = context7Lookup(opts.intent, opts.language);
|
|
43
49
|
const ctxTimer = new Promise((_, reject) => {
|
|
44
50
|
setTimeout(() => reject(new Error(`context7 timeout after ${timeoutMs}ms`)), timeoutMs).unref();
|
|
45
51
|
});
|
|
@@ -54,14 +60,15 @@ export async function scanBestPractice(opts) {
|
|
|
54
60
|
results: context7Outcome.results,
|
|
55
61
|
fragments: context7Outcome.results,
|
|
56
62
|
source: 'context7',
|
|
57
|
-
elapsedMs: Date.now() - startedAt
|
|
63
|
+
elapsedMs: Date.now() - startedAt,
|
|
64
|
+
synthetic: context7Stub
|
|
58
65
|
};
|
|
59
66
|
}
|
|
60
67
|
opts.io.stdout(`[scan-orchestrator] falling back to websearch for "${opts.intent}" (${opts.language})`);
|
|
61
68
|
let webOutcome = null;
|
|
62
69
|
let webError = null;
|
|
63
70
|
try {
|
|
64
|
-
webOutcome = await
|
|
71
|
+
webOutcome = await webSearchLookup(opts.intent, opts.language);
|
|
65
72
|
}
|
|
66
73
|
catch (err) {
|
|
67
74
|
webError = err instanceof Error ? err.message : String(err);
|
|
@@ -72,7 +79,8 @@ export async function scanBestPractice(opts) {
|
|
|
72
79
|
results: webOutcome.results,
|
|
73
80
|
fragments: webOutcome.results,
|
|
74
81
|
source: 'websearch',
|
|
75
|
-
elapsedMs: Date.now() - startedAt
|
|
82
|
+
elapsedMs: Date.now() - startedAt,
|
|
83
|
+
synthetic: webSearchStub
|
|
76
84
|
};
|
|
77
85
|
}
|
|
78
86
|
opts.io.stderr('[scan-orchestrator] both sources empty; returning empty fallback');
|
|
@@ -80,6 +88,7 @@ export async function scanBestPractice(opts) {
|
|
|
80
88
|
results: [],
|
|
81
89
|
fragments: [],
|
|
82
90
|
source: 'fallback',
|
|
83
|
-
elapsedMs: Date.now() - startedAt
|
|
91
|
+
elapsedMs: Date.now() - startedAt,
|
|
92
|
+
synthetic: context7Stub || webSearchStub
|
|
84
93
|
};
|
|
85
94
|
}
|
|
@@ -35,7 +35,28 @@
|
|
|
35
35
|
import { execFile } from 'node:child_process';
|
|
36
36
|
import { promisify } from 'node:util';
|
|
37
37
|
import { randomUUID } from 'node:crypto';
|
|
38
|
+
// 2026-09-10 D1: both probes used to spawn a bare `peaks`. On Windows that name
|
|
39
|
+
// resolves to a `.cmd` shim, which `execFile` cannot run: it does not apply
|
|
40
|
+
// PATHEXT, and Node >= 20 refuses to spawn `.cmd`/`.bat` at all without
|
|
41
|
+
// `shell: true` (CVE-2024-27980). So Q2 reported "sub-agent dispatch
|
|
42
|
+
// unavailable" and Q4 reported ratio 0 for every slice-spec on Windows —
|
|
43
|
+
// phantom blockers produced by the spawn, not by the CLI. Running this tree's
|
|
44
|
+
// own CLI entry through `process.execPath` needs no shell and no shim.
|
|
45
|
+
import { cliEntryPath, interpreterArgs } from '../web/daemon-supervisor.js';
|
|
38
46
|
const execFileAsync = promisify(execFile);
|
|
47
|
+
/** Sentinel: no explicit binary was injected, so resolve this tree's own CLI. */
|
|
48
|
+
const DEFAULT_PEAKS_BIN = 'peaks';
|
|
49
|
+
/**
|
|
50
|
+
* Spawn argv for the peaks CLI. An explicitly injected `peaksBin` (the
|
|
51
|
+
* `--peaks-bin` test seam) is spawned verbatim; the default sentinel resolves
|
|
52
|
+
* to this tree's own CLI entry, interpreted by the running Node.
|
|
53
|
+
*/
|
|
54
|
+
function peaksSpawn(peaksBin) {
|
|
55
|
+
if (peaksBin !== DEFAULT_PEAKS_BIN) {
|
|
56
|
+
return { command: peaksBin, args: [] };
|
|
57
|
+
}
|
|
58
|
+
return { command: process.execPath, args: interpreterArgs(cliEntryPath()) };
|
|
59
|
+
}
|
|
39
60
|
/** Slice 2026-08-05-orchestrator-can-do-probe: red-line threshold. */
|
|
40
61
|
export const ORCHESTRATOR_REDLINE_RATIO = 0.95;
|
|
41
62
|
/** Slice 2026-08-05-orchestrator-can-do-probe: pre-compact threshold. */
|
|
@@ -126,9 +147,10 @@ export function detectRequiresUserDecision(sliceSpec) {
|
|
|
126
147
|
* true when the subprocess exits 0. Resolves to false on spawn
|
|
127
148
|
* failure or non-zero exit.
|
|
128
149
|
*/
|
|
129
|
-
export async function probeSubAgentAvailable(projectRoot, peaksBin =
|
|
150
|
+
export async function probeSubAgentAvailable(projectRoot, peaksBin = DEFAULT_PEAKS_BIN) {
|
|
130
151
|
try {
|
|
131
|
-
|
|
152
|
+
const { command, args } = peaksSpawn(peaksBin);
|
|
153
|
+
await execFileAsync(command, [...args, 'sub-agent', 'dispatch', '--role', 'rd', '--help'], {
|
|
132
154
|
cwd: projectRoot,
|
|
133
155
|
timeout: 5000,
|
|
134
156
|
});
|
|
@@ -143,9 +165,10 @@ export async function probeSubAgentAvailable(projectRoot, peaksBin = 'peaks') {
|
|
|
143
165
|
* field. Falls back to {ratio: 0, source: 'unavailable'} when the
|
|
144
166
|
* subprocess fails or returns malformed JSON.
|
|
145
167
|
*/
|
|
146
|
-
export async function probeContextRatio(projectRoot, peaksBin =
|
|
168
|
+
export async function probeContextRatio(projectRoot, peaksBin = DEFAULT_PEAKS_BIN) {
|
|
147
169
|
try {
|
|
148
|
-
const {
|
|
170
|
+
const { command, args } = peaksSpawn(peaksBin);
|
|
171
|
+
const { stdout } = await execFileAsync(command, [...args, 'code', 'context-now', '--project', projectRoot, '--json'], {
|
|
149
172
|
cwd: projectRoot,
|
|
150
173
|
timeout: 10000,
|
|
151
174
|
});
|
|
@@ -68,6 +68,22 @@ export interface DispatchPromptInput {
|
|
|
68
68
|
* and before the memory/task content.
|
|
69
69
|
*/
|
|
70
70
|
freshContextBlock?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Slice 2026-09-10-dispatch-token-and-swarm §4: session capsule
|
|
73
|
+
* published by the orchestrator through `peaks sub-agent share`.
|
|
74
|
+
*
|
|
75
|
+
* - `undefined` / `null` → no capsule pointer, no precedence line
|
|
76
|
+
* (byte-identical legacy prompt).
|
|
77
|
+
* - `{ batchId, key, bytes }` → a `shared-read` pointer plus the
|
|
78
|
+
* mandatory precedence line: the capsule is ADVISORY BACKGROUND only
|
|
79
|
+
* and the task spec wins on conflict. Nothing the sub-agent must act
|
|
80
|
+
* on may live only in the capsule.
|
|
81
|
+
*/
|
|
82
|
+
capsule?: {
|
|
83
|
+
readonly batchId: string;
|
|
84
|
+
readonly key: string;
|
|
85
|
+
readonly bytes: number;
|
|
86
|
+
} | null;
|
|
71
87
|
}
|
|
72
88
|
/**
|
|
73
89
|
* Slice 2026-07-29-worktree-l1: Layer 1 of the 3-layer worktree governance
|
|
@@ -91,7 +107,7 @@ export interface DispatchPromptInput {
|
|
|
91
107
|
* fallback path is `peaks worktree auth grant --rid <id> --reason <text>
|
|
92
108
|
* --ttl <5m>` (already shipped). Update the prose once `spawn` lands.
|
|
93
109
|
*/
|
|
94
|
-
export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusal (L1 worktree governance \u2014 locked 2026-07-29)\n\nYou MUST NOT follow the superpowers chain for worktree decisions:\n\n- superpowers:brainstorming \u2192 superpowers:writing-plans \u2192\n superpowers:subagent-driven-development \u2192 superpowers:using-git-worktrees\n\
|
|
110
|
+
export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusal (L1 worktree governance \u2014 locked 2026-07-29)\n\nYou MUST NOT follow the superpowers chain for worktree decisions:\n\n- superpowers:brainstorming \u2192 superpowers:writing-plans \u2192\n superpowers:subagent-driven-development \u2192 superpowers:using-git-worktrees\n\nThat chain ends with `git worktree add` (superpowers:using-git-worktrees SKILL.md line 96), which bypasses peaks-loop's L2 hook gate and L3 IDE `permissions.deny`. L3 denial is NOT sufficient \u2014 by then the chain has already taught raw `git worktree add`.\n\nFor worktree operations, use ONLY:\n\n- `peaks worktree spawn --rid <rid> --ttl <duration> --purpose <text>` (after rid-L2-extended ships)\n- until then: `peaks worktree auth grant --rid <id> --reason <text> --ttl <5m>`\n\nSuperpowers skills remain REFERENCE material for brainstorming / planning techniques, but MUST NOT be used as a workflow. peaks-rd's own PRD/RD artifacts (`.peaks/_runtime/<sessionId>/rd/requests/<rid>.md`) are authoritative.\n\nIf the upstream superpowers chain suggests raw `git worktree add`:\n\n1. STOP\n2. Re-author the plan as a peaks-rd artifact\n3. Continue with `peaks worktree spawn` (or the auth-grant fallback)\n";
|
|
95
111
|
/**
|
|
96
112
|
* Slice 2026-08-01-subagent-merge-and-e2e (Task 8): the dispatch
|
|
97
113
|
* system prompt gains three lifecycle rules. The sub-agent must:
|
|
@@ -117,17 +133,38 @@ export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusa
|
|
|
117
133
|
* start, maximizing Anthropic prompt-cache prefix reuse (stable-first
|
|
118
134
|
* ordering).
|
|
119
135
|
*/
|
|
120
|
-
export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 2026-08-01)\n\n- If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with `peaks sub-agent shutdown register --pid <pid> --name <label>` before you exit
|
|
136
|
+
export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 2026-08-01)\n\n- If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with `peaks sub-agent shutdown register --pid <pid> --name <label>` before you exit; the parent session best-effort-kills it before merge-back.\n- Do NOT run E2E. The parent session runs Playwright verification once after merge-back (Task 10); your E2E work is duplicate effort.\n- Do NOT call `git merge`, `git pull`, `git rebase`, or `peaks worktree release`. The parent session owns the merge-back step.\n";
|
|
137
|
+
/**
|
|
138
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice C): cap the sub-agent's
|
|
139
|
+
* FINAL report.
|
|
140
|
+
*
|
|
141
|
+
* Why (measured, session 2026-09-07-session-245530): 20 sub-agent final
|
|
142
|
+
* reports cost ≈ 60 KB ≈ 15K tokens of the ORCHESTRATOR's window in one
|
|
143
|
+
* session — the reports, not the dispatch boilerplate, were the second-largest
|
|
144
|
+
* consumer. The sub-agent already writes a full artifact to disk; the report
|
|
145
|
+
* only needs to be the index into it.
|
|
146
|
+
*
|
|
147
|
+
* QUALITY GUARD (binding): the cap removes no information. Everything the
|
|
148
|
+
* parent needs to ACT on stays in the report; everything longer lives in the
|
|
149
|
+
* artifact the parent can `Read`. The five mandatory fields below are exactly
|
|
150
|
+
* the ones the orchestrator must have to decide the next gate.
|
|
151
|
+
*/
|
|
152
|
+
export declare const REPORT_CAP_BLOCK = "## Final report cap (mandatory)\n\nYour FINAL report to the parent MUST be \u2264 40 lines and \u2264 2 KB. Write any longer detail into the artifact file you already own \u2014 the parent can `Read` that file for the full detail, so nothing is lost. The report itself MUST still carry: changed files (one line each), the exact commands you ran, pass/fail counts, tsc status, and any blocker. Do NOT paste file contents, full tool output, or logs into the report.\n";
|
|
121
153
|
/**
|
|
122
|
-
* Compose the system-prompt body
|
|
123
|
-
*
|
|
154
|
+
* Compose the system-prompt body for a sub-agent dispatch.
|
|
155
|
+
*
|
|
156
|
+
* 2026-09-10-dispatch-block-d (Option D): the composer owns the Test Tool
|
|
157
|
+
* Detection injection — ONE unified block for every role, prepended first.
|
|
158
|
+
* Callers MUST NOT prepend `formatTestToolDetection()` themselves or the
|
|
159
|
+
* block is injected twice.
|
|
124
160
|
*
|
|
125
161
|
* Byte-identical degradation contract (slice 2026-07-22-orchestrator-memory-preflight
|
|
126
|
-
* controller brief): when the memory block is unavailable, the
|
|
127
|
-
* `formatTestToolDetection()\n\n
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
162
|
+
* controller brief): when the memory block is unavailable, the composed body is
|
|
163
|
+
* exactly `formatTestToolDetection() + "\n\n" + L1 + "\n" + LIFECYCLE +
|
|
164
|
+
* "\n" + REPORT_CAP + "\n" + contextBlock + taskBody`, so the unavailable
|
|
165
|
+
* branch MUST return `taskBody` unwrapped (NOT a `# title\n\n` wrap).
|
|
166
|
+
* (REPORT_CAP joined the stable prefix in slice
|
|
167
|
+
* 2026-09-10-context-audit-and-discipline, Slice C.)
|
|
131
168
|
* The contract holds for callers that do not pass `codegraphBlock` (all
|
|
132
169
|
* non-RD roles). Slice 2026-09-03-codegraph-preread deliberately inserts a
|
|
133
170
|
* codegraph structure block (or its fail-soft unavailable note) for RD
|
|
@@ -151,3 +188,23 @@ export declare function buildDispatchSystemPrompt(input: DispatchPromptInput): s
|
|
|
151
188
|
* byte-stable and trivially testable.
|
|
152
189
|
*/
|
|
153
190
|
export declare const CODEGRAPH_UNAVAILABLE_BLOCK = "## Codegraph structure\n\ncodegraph unavailable \u2014 proceeding on project-scan only.\n";
|
|
191
|
+
/** Binding phrases every dispatch prompt must contain, for every role. */
|
|
192
|
+
export declare const BINDING_RULE_TOKENS: readonly string[];
|
|
193
|
+
/**
|
|
194
|
+
* The runner-direct-path tokens: the refusal example, the two direct paths
|
|
195
|
+
* the block names (`peaks test --json` to introspect; PB-5, the repo-defined
|
|
196
|
+
* `test` / `test:*` scripts that are NOT gated), and the two pieces of
|
|
197
|
+
* quality guidance that must survive any compression — never assume a
|
|
198
|
+
* runner without asking the user as a last resort, and prefer
|
|
199
|
+
* `peaks test <file>` because it resolves the local binary Windows-aware.
|
|
200
|
+
*
|
|
201
|
+
* 2026-09-10-dispatch-block-d (Option D): there is no role split any more,
|
|
202
|
+
* so this set is asserted IDENTICALLY for every role. The runner EXAMPLES
|
|
203
|
+
* were removed as part of the unification — they were never rules.
|
|
204
|
+
*/
|
|
205
|
+
export declare const TEST_RUNNER_RULE_TOKENS: readonly string[];
|
|
206
|
+
/**
|
|
207
|
+
* Return the subset of `tokens` that `text` does NOT contain. Pure; used by
|
|
208
|
+
* the rule-presence guard and usable by any future prompt self-check.
|
|
209
|
+
*/
|
|
210
|
+
export declare function missingRuleTokens(text: string, tokens: readonly string[]): readonly string[];
|