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,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File + lock IO for the `peaks web` daemon (slice S1, file 7).
|
|
3
|
+
*
|
|
4
|
+
* Isolated from the HTTP client so each is testable alone (tech-doc §2). Every
|
|
5
|
+
* WRITE goes through `assertUnder` first — the slice-wide guard against an
|
|
6
|
+
* artifact escaping `<root>/.peaks/_runtime/<sid>/web/` (AC1).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import { assertUnder, webDaemonDir, webDaemonInfoPath, webSpawnLockPath } from './web-artifact-paths.js';
|
|
11
|
+
import { parseDaemonInfo } from './web-protocol.js';
|
|
12
|
+
/** A spawn lock older than this is reclaimed even if its owner pid is alive. */
|
|
13
|
+
const SPAWN_LOCK_STALE_MS = 120_000;
|
|
14
|
+
/**
|
|
15
|
+
* `daemon.json` holds the 64-hex bearer token that is the ONLY lock on the
|
|
16
|
+
* loopback port — 127.0.0.1 is reachable by every local user, so the file mode
|
|
17
|
+
* is the boundary. 0644 would let any local account read the token and drive
|
|
18
|
+
* the browser (`open`/`text`/`snap`/`shot`) with it. The repo's other
|
|
19
|
+
* secret-bearing writers already use `0o600`
|
|
20
|
+
* (`src/services/ide/shared/atomic-json.ts`, `logger.ts`, `config-safety.ts`).
|
|
21
|
+
*/
|
|
22
|
+
const DAEMON_FILE_MODE = 0o600;
|
|
23
|
+
/** The token's directory: readable only by its owner, same reasoning as above. */
|
|
24
|
+
export const DAEMON_DIR_MODE = 0o700;
|
|
25
|
+
export function writeDaemonInfo(projectRoot, sessionId, info) {
|
|
26
|
+
const target = webDaemonInfoPath(projectRoot, sessionId);
|
|
27
|
+
assertUnder(target, webDaemonDir(projectRoot, sessionId));
|
|
28
|
+
mkdirSync(dirname(target), { recursive: true, mode: DAEMON_DIR_MODE });
|
|
29
|
+
writeFileSync(target, JSON.stringify(info, null, 2), { encoding: 'utf8', mode: DAEMON_FILE_MODE });
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read + validate `daemon.json`. `null` when absent, malformed, a stale
|
|
33
|
+
* protocol, or — R6 — when the file claims to describe a DIFFERENT
|
|
34
|
+
* `(projectRoot, sessionId)` than the caller's.
|
|
35
|
+
*
|
|
36
|
+
* The ownership comparison belongs here rather than in `parseDaemonInfo`,
|
|
37
|
+
* because only the caller knows what the record is supposed to describe. A
|
|
38
|
+
* planted `daemon.json` naming someone else's session would otherwise redirect
|
|
39
|
+
* every op (page URL, selectors, bearer token) to a port of the attacker's
|
|
40
|
+
* choosing, and — via `stopDaemon` — aim `process.kill` at an unrelated pid.
|
|
41
|
+
*/
|
|
42
|
+
export function readDaemonInfo(projectRoot, sessionId) {
|
|
43
|
+
const target = webDaemonInfoPath(projectRoot, sessionId);
|
|
44
|
+
if (!existsSync(target)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const info = parseDaemonInfo(readFileSync(target, 'utf8'));
|
|
49
|
+
if (info === null || info.projectRoot !== projectRoot || info.sessionId !== sessionId) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return info;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Remove `daemon.json`. Idempotent: a missing file is not an error. */
|
|
59
|
+
export function removeDaemonInfo(projectRoot, sessionId) {
|
|
60
|
+
const target = webDaemonInfoPath(projectRoot, sessionId);
|
|
61
|
+
if (!existsSync(target)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
unlinkSync(target);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Another process removed it between the probe and the unlink.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `process.kill(pid, 0)` is the cross-platform existence probe: it throws
|
|
73
|
+
* `ESRCH` when no such process exists and `EPERM` when one exists but belongs
|
|
74
|
+
* to another user and so may not be signalled. Only `ESRCH` means dead — an
|
|
75
|
+
* alive-but-unsignalable pid must not be reported as dead, or `stopDaemon`
|
|
76
|
+
* skips the kill and `acquireSpawnLock` reclaims a lock whose owner still
|
|
77
|
+
* holds it.
|
|
78
|
+
*/
|
|
79
|
+
export function isProcessAlive(pid) {
|
|
80
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
process.kill(pid, 0);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return error.code === 'EPERM';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Take the cold-start lock (O_EXCL create). Returns `false` when another
|
|
93
|
+
* process holds a live lock, and reclaims a STALE one — owner pid dead, or
|
|
94
|
+
* older than `SPAWN_LOCK_STALE_MS` — before retrying once (tech-doc §1.4).
|
|
95
|
+
*/
|
|
96
|
+
export function acquireSpawnLock(projectRoot, sessionId) {
|
|
97
|
+
const target = webSpawnLockPath(projectRoot, sessionId);
|
|
98
|
+
assertUnder(target, webDaemonDir(projectRoot, sessionId));
|
|
99
|
+
mkdirSync(dirname(target), { recursive: true, mode: DAEMON_DIR_MODE });
|
|
100
|
+
if (tryCreateSpawnLock(target)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
const existing = readSpawnLock(target);
|
|
104
|
+
const ageMs = existing === null ? Number.NaN : Date.now() - Date.parse(existing.startedAt);
|
|
105
|
+
if (existing !== null && isProcessAlive(existing.pid) && ageMs <= SPAWN_LOCK_STALE_MS) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
unlinkSync(target);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Racers: whoever wins the unlink still has to win the O_EXCL create below.
|
|
113
|
+
}
|
|
114
|
+
return tryCreateSpawnLock(target);
|
|
115
|
+
}
|
|
116
|
+
/** Release the spawn lock, but only when this process is its owner. */
|
|
117
|
+
export function releaseSpawnLock(projectRoot, sessionId) {
|
|
118
|
+
const target = webSpawnLockPath(projectRoot, sessionId);
|
|
119
|
+
const existing = readSpawnLock(target);
|
|
120
|
+
if (existing !== null && existing.pid !== process.pid) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
unlinkSync(target);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Already released (or never held).
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The daemon instances for a session. There is one daemon per
|
|
132
|
+
* `(projectRoot, sessionId)` (design §10.2), so this list holds at most one
|
|
133
|
+
* entry; it is an array because the S2 status report folds it together with the
|
|
134
|
+
* lock and `/health` classification of each instance.
|
|
135
|
+
*/
|
|
136
|
+
export function listSessionDaemons(projectRoot, sessionId) {
|
|
137
|
+
const info = readDaemonInfo(projectRoot, sessionId);
|
|
138
|
+
return info === null ? [] : [info];
|
|
139
|
+
}
|
|
140
|
+
function tryCreateSpawnLock(target) {
|
|
141
|
+
const body = { pid: process.pid, startedAt: new Date().toISOString() };
|
|
142
|
+
try {
|
|
143
|
+
writeFileSync(target, JSON.stringify(body), { flag: 'wx', encoding: 'utf8' });
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// `EEXIST` (held) and any other write failure both mean "not acquired".
|
|
148
|
+
// The caller either waits for the holder or surfaces a readiness timeout.
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function readSpawnLock(target) {
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(readFileSync(target, 'utf8'));
|
|
155
|
+
const { pid, startedAt } = parsed;
|
|
156
|
+
if (typeof pid !== 'number' || typeof startedAt !== 'string') {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return { pid, startedAt };
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { WebDaemonInfo } from './web-protocol.js';
|
|
2
|
+
export interface StopDaemonResult {
|
|
3
|
+
/** Daemons confirmed GONE at return — not merely asked to stop. */
|
|
4
|
+
readonly stopped: number;
|
|
5
|
+
/** Every pid we asked to stop (gracefully or by signal). */
|
|
6
|
+
readonly pids: readonly number[];
|
|
7
|
+
/** Alive but not answering `/health`: recorded, deliberately not signalled. */
|
|
8
|
+
readonly orphanedPids: readonly number[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Absolute path of the daemon entry. The NAME is fixed by the tree we are
|
|
12
|
+
* running from, so the two supported modes each get a target that exists:
|
|
13
|
+
* `dist/services/web/daemon-entry.js` after `pnpm build`, and
|
|
14
|
+
* `src/services/web/daemon-entry.ts` for `pnpm dev` / the test suite.
|
|
15
|
+
*/
|
|
16
|
+
export declare function daemonEntryPath(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Absolute path of the peaks CLI entry — the file that owns
|
|
19
|
+
* `sub-agent shutdown register`.
|
|
20
|
+
*
|
|
21
|
+
* Explicit, never `process.argv[1]`: the caller of `registerWithParent` is the
|
|
22
|
+
* DAEMON, whose `argv[1]` is the daemon entry, so spawning it with CLI
|
|
23
|
+
* arguments boots a second daemon instead of registering anything (R8).
|
|
24
|
+
*/
|
|
25
|
+
export declare function cliEntryPath(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Arguments that make `node` run `entry`.
|
|
28
|
+
*
|
|
29
|
+
* A `.ts` entry needs the TypeScript loader, and it is attached with
|
|
30
|
+
* `--require preflight.cjs --import loader.mjs` — the exact flags `tsx` passes
|
|
31
|
+
* to its own child — rather than by running the `tsx` CLI.
|
|
32
|
+
*
|
|
33
|
+
* That distinction is not cosmetic. `windowsHide` applies to the process we
|
|
34
|
+
* spawn and to nothing it spawns in turn: launching `node tsx-cli.mjs entry.ts`
|
|
35
|
+
* makes tsx spawn a GRANDCHILD, and the grandchild has no `windowsHide` of ours
|
|
36
|
+
* — so on Windows every dev-mode `peaks web` cold start popped a console window
|
|
37
|
+
* the user could not close. Invoking the loader directly keeps it one process,
|
|
38
|
+
* so the flag we already pass covers everything.
|
|
39
|
+
*
|
|
40
|
+
* Exported so the race test's caller process is launched through the same
|
|
41
|
+
* shape as the daemon: two copies of these flags would drift.
|
|
42
|
+
*
|
|
43
|
+
* Both callers prepend `process.execPath` themselves, so this returns ONLY the
|
|
44
|
+
* interpreter flags and the entry.
|
|
45
|
+
*/
|
|
46
|
+
export declare function interpreterArgs(entry: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Whether this Node spells the ESM loader flag `--import`.
|
|
49
|
+
*
|
|
50
|
+
* It arrived in 20.6.0 (backported to 18.19.0); below that the spelling is
|
|
51
|
+
* `--loader`, and passing `--import` is a bad option that kills the daemon at
|
|
52
|
+
* startup — inside a `READY_TIMEOUT_MS = 20 s` wait whose only diagnosis is a
|
|
53
|
+
* line in `daemon.log`. `package.json` declares `engines.node >= 20.0.0`, which
|
|
54
|
+
* includes 20.0–20.5, so the flag cannot be unconditional. `tsx` gates its own
|
|
55
|
+
* child the same way and this mirrors it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function supportsImportFlag(nodeVersion?: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The exact command `spawnDaemon` hands to `spawn`, resolved without launching
|
|
60
|
+
* anything so a test can assert its shape.
|
|
61
|
+
*
|
|
62
|
+
* `process.execPath` directly — never `npx`, and therefore never the
|
|
63
|
+
* `cmd.exe /d /s /c node …` that npm's own `run-script` puts between us and the
|
|
64
|
+
* daemon (`@npmcli/run-script/lib/make-spawn-args.js` spawns with `shell: true`
|
|
65
|
+
* and no `windowsHide`). That layer was three processes per cold start instead
|
|
66
|
+
* of one, it re-parsed our `--require` / `--import` paths through a command
|
|
67
|
+
* string, it cost 2.8–7.1 s of the ~3 s cold start, and the two intermediate
|
|
68
|
+
* processes ignored `windowsHide` — the console-window incident, one layer down.
|
|
69
|
+
*/
|
|
70
|
+
export declare function daemonSpawnCommand(): {
|
|
71
|
+
command: string;
|
|
72
|
+
args: string[];
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Spawn the detached daemon. Both stdio pipes are redirected to
|
|
76
|
+
* `web/daemon/daemon.log` — a daemon that inherited our stdout, or wrote to
|
|
77
|
+
* `cwd`, would be the sneakiest way to break AC1 (tech-doc §7.2 rule 6).
|
|
78
|
+
*/
|
|
79
|
+
export declare function spawnDaemon(projectRoot: string, sessionId: string): {
|
|
80
|
+
pid: number | undefined;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Return a healthy daemon for this `(projectRoot, sessionId)`, spawning one only
|
|
84
|
+
* when the session has none. A record whose pid is GONE is replaced (a stale
|
|
85
|
+
* file from a SIGKILLed parent, R3); a record whose pid is ALIVE is never
|
|
86
|
+
* replaced, even when `/health` does not answer — see `DaemonProbe`.
|
|
87
|
+
*
|
|
88
|
+
* Cold start is **double-checked locking**: the record, its pid liveness and
|
|
89
|
+
* `/health` are re-probed AFTER the lock is acquired, with the same oracle the
|
|
90
|
+
* pre-lock check used, and the lock is held across the spawn AND the readiness
|
|
91
|
+
* wait. Q10 lets the orchestrator and a sub-agent call
|
|
92
|
+
* `peaks web` at the same moment, and `spawnDaemon` returns as soon as `spawn()`
|
|
93
|
+
* does, so releasing the lock there left the whole ~20 s startup window
|
|
94
|
+
* unprotected: both callers would see no daemon, take the lock in turn, and
|
|
95
|
+
* launch a browser each — two processes per session in violation of Q8, one of
|
|
96
|
+
* them unreachable by `peaks web stop`. With the lock held, the loser waits for
|
|
97
|
+
* the winner's daemon instead of spawning its own.
|
|
98
|
+
*/
|
|
99
|
+
export declare function ensureDaemon(projectRoot: string, sessionId: string): Promise<WebDaemonInfo>;
|
|
100
|
+
/**
|
|
101
|
+
* Stop this session's daemon and clear its records. Scoped to
|
|
102
|
+
* `(projectRoot, sessionId)` by construction — another worktree's daemon is
|
|
103
|
+
* untouched (design §10.2).
|
|
104
|
+
*
|
|
105
|
+
* Ownership is proven before asking the process to stop (R6/R7): a planted
|
|
106
|
+
* `daemon.json` naming an unrelated pid must not make `peaks web stop` kill a
|
|
107
|
+
* process the caller does not own, and on Windows `SIGTERM` is
|
|
108
|
+
* `TerminateProcess` with no chance to say "no". `/health` is NOT evidence —
|
|
109
|
+
* it is answered before the auth check, so any unrelated local listener that
|
|
110
|
+
* returns 2xx for a `GET` satisfies it. The proof is an AUTHENTICATED `/op`
|
|
111
|
+
* whose reply reports the very identity the record claims (`isOwnDaemon`): a
|
|
112
|
+
* recycled pid pointing at somebody else's dev server cannot produce it.
|
|
113
|
+
*
|
|
114
|
+
* The graceful path comes FIRST, and it is not a nicety: on Windows a signal
|
|
115
|
+
* cannot be handled, so signalling would terminate the daemon without running
|
|
116
|
+
* its teardown, leaving its chromium process behind — the exact false pass
|
|
117
|
+
* AC6 is written against. `/op {op:'stop'}` lets the daemon close every context,
|
|
118
|
+
* persist storage state, close the browser and exit on its own terms; the
|
|
119
|
+
* signal is only the fallback for a daemon that accepts the request and then
|
|
120
|
+
* fails to leave.
|
|
121
|
+
*
|
|
122
|
+
* An instance that is alive but cannot be proven ours is **not** signalled and
|
|
123
|
+
* **not** de-recorded. Its record is the only handle on a running process:
|
|
124
|
+
* clearing it would leave the daemon and its chromium detached with no verb
|
|
125
|
+
* able to reach them, and would let the next cold start spawn a second daemon
|
|
126
|
+
* beside a live one (Q8). It is reported in `orphanedPids`, and a daemon that
|
|
127
|
+
* survives its own stop request stays recorded too, so `status` and a second
|
|
128
|
+
* `stop` still see it.
|
|
129
|
+
*/
|
|
130
|
+
export declare function stopDaemon(projectRoot: string, sessionId: string): Promise<StopDaemonResult>;
|
|
131
|
+
/**
|
|
132
|
+
* Best-effort registration with the existing sub-agent shutdown registry
|
|
133
|
+
* (Q7 / tech-doc §7.3).
|
|
134
|
+
*
|
|
135
|
+
* The entry point is `cliEntryPath()`, not `process.argv[1]`: this is called by
|
|
136
|
+
* the daemon, whose `argv[1]` is the daemon entry, so using it spawned a second
|
|
137
|
+
* daemon-entry with CLI arguments instead of registering anything (R8).
|
|
138
|
+
*
|
|
139
|
+
* Registration failing is non-fatal — the caller keeps running — but it must
|
|
140
|
+
* not be silent: the daemon's stderr is `web/daemon/daemon.log`
|
|
141
|
+
* (see `spawnDaemon`), so a spawn failure is written there. A detached child
|
|
142
|
+
* whose `error` event has no listener would otherwise crash the daemon.
|
|
143
|
+
*/
|
|
144
|
+
export declare function registerWithParent(daemonPid: number, dispatchId: string): void;
|