@crouton-kit/crouter 0.3.48 → 0.3.50
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/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/clients/attach/attach-cmd.js +825 -863
- package/dist/clients/attach/titled-editor.js +6 -3
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/shared.js +7 -3
- package/dist/commands/memory/write.js +2 -0
- package/dist/commands/node.js +96 -11
- package/dist/commands/profile/default.d.ts +2 -0
- package/dist/commands/profile/default.js +143 -0
- package/dist/commands/profile/new.js +3 -3
- package/dist/commands/profile/project.d.ts +2 -0
- package/dist/commands/profile/project.js +97 -0
- package/dist/commands/profile/show.js +1 -1
- package/dist/commands/profile.js +10 -11
- package/dist/commands/sys/__tests__/sync-import.test.js +156 -3
- package/dist/commands/sys/sync.js +82 -25
- package/dist/core/__tests__/broker-sdk-wiring.test.js +4 -7
- package/dist/core/__tests__/context-intro.test.js +15 -7
- package/dist/core/__tests__/memory-resolver-precedence.test.d.ts +1 -0
- package/dist/core/__tests__/memory-resolver-precedence.test.js +144 -0
- package/dist/core/__tests__/on-read-identity.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-identity.test.js +68 -0
- package/dist/core/fs-utils.d.ts +1 -1
- package/dist/core/fs-utils.js +5 -3
- package/dist/core/memory-resolver.d.ts +27 -11
- package/dist/core/memory-resolver.js +105 -109
- package/dist/core/profiles/default-binding.d.ts +10 -0
- package/dist/core/profiles/default-binding.js +50 -0
- package/dist/core/profiles/select.d.ts +13 -1
- package/dist/core/profiles/select.js +92 -26
- package/dist/core/runtime/bearings.d.ts +4 -9
- package/dist/core/runtime/bearings.js +10 -17
- package/dist/core/runtime/front-door.js +11 -4
- package/dist/core/runtime/revive.js +2 -1
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +1 -1
- package/dist/core/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +3 -1
- package/dist/core/substrate/on-read.js +14 -9
- package/dist/core/substrate/render.js +7 -3
- package/dist/core/substrate/schema.d.ts +8 -2
- package/dist/core/substrate/schema.js +19 -2
- package/dist/daemon/crtrd.js +44 -2
- package/dist/daemon/manage.d.ts +20 -0
- package/dist/daemon/manage.js +64 -2
- package/package.json +3 -3
- package/dist/commands/profile/add-project.d.ts +0 -1
- package/dist/commands/profile/add-project.js +0 -42
- package/dist/commands/profile/remove-project.d.ts +0 -1
- package/dist/commands/profile/remove-project.js +0 -42
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// Startup profile selection — the CTO-decided order applied at the front door
|
|
2
|
-
// and at any non-inherited `crtr node new`: explicit `--profile` > a
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// and at any non-inherited `crtr node new`: explicit `--profile` > a per-cwd
|
|
3
|
+
// PINNED default (set from the menu with `d`, wins over MRU while it still
|
|
4
|
+
// covers cwd) > a profile whose project dir IS cwd exactly (auto) > an
|
|
5
|
+
// interactive menu of profiles covering cwd from a parent/workspace dir +
|
|
6
|
+
// create (MRU auto when headless) > synchronous create-or-root decision when
|
|
7
|
+
// nothing covers. This is the ONLY place that decision runs; front-door.ts/
|
|
8
|
+
// spawn.ts call it, never re-derive it.
|
|
7
9
|
import { existsSync, realpathSync } from 'node:fs';
|
|
8
10
|
import { homedir } from 'node:os';
|
|
9
11
|
import { basename, resolve as resolvePath, sep } from 'node:path';
|
|
10
12
|
import { createInterface } from 'node:readline/promises';
|
|
11
13
|
import { listProfiles, loadProfileManifest, updateProfileLastUsed, createProfile, addProfileProject, } from './manifest.js';
|
|
14
|
+
import { getDefaultProfileId, setDefaultProfileId, clearDefaultProfile, } from './default-binding.js';
|
|
12
15
|
import { stdoutColor } from '../output.js';
|
|
13
16
|
/** Resolve cwd to an absolute, realpath'd form so it compares against the
|
|
14
17
|
* realpath'd project dirs `createProfile`/`addProfileProject` already store
|
|
@@ -38,7 +41,11 @@ function projectCovers(cwd, project) {
|
|
|
38
41
|
return true;
|
|
39
42
|
return false;
|
|
40
43
|
}
|
|
41
|
-
|
|
44
|
+
/** Whether a profile's purview covers `cwd` (cwd at/under one of its project
|
|
45
|
+
* dirs, or a project dir under cwd — the workspace-root case). Exported for
|
|
46
|
+
* `crtr profile default set`, which rejects pinning a profile that doesn't
|
|
47
|
+
* cover cwd (the selector would ignore/self-heal such a pin anyway). */
|
|
48
|
+
export function profileCoversCwd(entry, cwd) {
|
|
42
49
|
return entry.manifest.projects.some((p) => projectCovers(cwd, p));
|
|
43
50
|
}
|
|
44
51
|
/** `a` more recent than `b`, treating null as the oldest possible value (never
|
|
@@ -114,10 +121,11 @@ function writeHeader(title, subtitle) {
|
|
|
114
121
|
process.stdout.write(`\n ${bold(title)}\n${sub}\n`);
|
|
115
122
|
}
|
|
116
123
|
/** One option row: ` <key> <label> <dim detail> · default`. `label` is
|
|
117
|
-
* passed pre-styled; `detail` is dimmed here; the default row gets a marker
|
|
118
|
-
|
|
124
|
+
* passed pre-styled; `detail` is dimmed here; the default row gets a marker
|
|
125
|
+
* whose text (`default` vs `default (pinned)`) the caller supplies. */
|
|
126
|
+
function optionRow(k, label, detail, isDefault, defaultText = 'default') {
|
|
119
127
|
const det = detail !== '' ? ' ' + dim(detail) : '';
|
|
120
|
-
const def = isDefault ? ' ' + accent('\u00b7
|
|
128
|
+
const def = isDefault ? ' ' + accent('\u00b7 ' + defaultText) : '';
|
|
121
129
|
return ` ${key(k)} ${label}${det}${def}\n`;
|
|
122
130
|
}
|
|
123
131
|
/** The trailing readline prompt: ` › <dim hint> `. */
|
|
@@ -141,7 +149,7 @@ function recoveryMessage(cwd) {
|
|
|
141
149
|
/** Explicit `--profile` names a profile whose manifest does NOT cover cwd.
|
|
142
150
|
* Interactive only: offer to widen the profile's purview to include this
|
|
143
151
|
* directory (default yes), so the next node started here is covered without a
|
|
144
|
-
* separate `crtr profile add
|
|
152
|
+
* separate `crtr profile project add`. Declining leaves the manifest untouched
|
|
145
153
|
* and runs the node under the profile anyway. Same raw-readline rationale as
|
|
146
154
|
* `promptCreateOrRoot` — this gates pi's boot on the same TTY. */
|
|
147
155
|
async function promptAddDirToProfile(entry, cwd) {
|
|
@@ -196,10 +204,15 @@ async function promptCreateOrRoot(cwd) {
|
|
|
196
204
|
* here: a claimed/covered directory always has at least a covering profile as a
|
|
197
205
|
* sane identity, so `[r]oot` belongs only to the no-coverage prompt
|
|
198
206
|
* (`promptCreateOrRoot`). Same raw-readline / boot-gating rationale. */
|
|
199
|
-
async function promptPickProfileOrCreate(candidates, cwd, exact) {
|
|
200
|
-
// MRU-first so the most-recently-used candidate is
|
|
201
|
-
//
|
|
202
|
-
|
|
207
|
+
async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId) {
|
|
208
|
+
// MRU-first so the most-recently-used candidate is the bare-Enter default —
|
|
209
|
+
// the same profile the non-interactive path auto-picks. A per-cwd pin (set
|
|
210
|
+
// here with `d`) overrides that: it sorts to the top and becomes the
|
|
211
|
+
// Enter-default, so a single Enter keeps picking the user's chosen profile
|
|
212
|
+
// for this directory even as global-MRU drifts to other workspaces.
|
|
213
|
+
const mru = [...candidates].sort((a, b) => isMoreRecent(a.manifest.last_used_at, b.manifest.last_used_at) ? -1 : 1);
|
|
214
|
+
const pinned = pinnedId !== null ? mru.find((e) => e.profileId === pinnedId) ?? null : null;
|
|
215
|
+
const ordered = pinned !== null ? [pinned, ...mru.filter((e) => e !== pinned)] : mru;
|
|
203
216
|
// Pad names to a shared column so the dim detail lines up down the menu.
|
|
204
217
|
const nameWidth = Math.max(...ordered.map((e) => e.manifest.name.length));
|
|
205
218
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -215,21 +228,46 @@ async function promptPickProfileOrCreate(candidates, cwd, exact) {
|
|
|
215
228
|
? relativeUsed(entry.manifest.last_used_at)
|
|
216
229
|
: `covers ${tildify(entry.manifest.projects.find((p) => projectCovers(cwd, p)) ?? entry.manifest.projects[0])}`;
|
|
217
230
|
const label = bold(entry.manifest.name.padEnd(nameWidth));
|
|
218
|
-
|
|
231
|
+
const defaultText = pinned !== null && ordered[0] === pinned ? 'default (pinned)' : 'default';
|
|
232
|
+
process.stdout.write(optionRow(String(i + 1), label, detail, i === 0, defaultText));
|
|
219
233
|
});
|
|
220
234
|
process.stdout.write(optionRow('c', dim('create a new profile here'), '', false));
|
|
221
235
|
process.stdout.write('\n');
|
|
236
|
+
// Action suffixes on a pick: `<n>d` also pins #n as this directory's
|
|
237
|
+
// default; `<n>a` also widens #n's purview to include cwd (meaningless when
|
|
238
|
+
// the candidates already claim cwd exactly, so hidden there). `u` clears an
|
|
239
|
+
// existing pin. A bare letter applies to the default row (choice 1).
|
|
240
|
+
const parts = ['d=set default'];
|
|
241
|
+
if (!exact)
|
|
242
|
+
parts.push('a=add dir here');
|
|
243
|
+
if (pinned !== null)
|
|
244
|
+
parts.push('u=unpin');
|
|
245
|
+
process.stdout.write(` ${dim(parts.join(' '))}\n\n`);
|
|
222
246
|
const answer = (await rl.question(caret('[1]'))).trim().toLowerCase();
|
|
223
247
|
if (answer === 'c' || answer === 'create')
|
|
224
248
|
return await createProfileHere(rl, cwd);
|
|
225
|
-
if (answer === '')
|
|
249
|
+
if (answer === 'u' || answer === 'unpin') {
|
|
250
|
+
clearDefaultProfile(cwd);
|
|
251
|
+
process.stdout.write(` ${accent('\u2713')} ${dim('cleared the default for this directory')}\n`);
|
|
226
252
|
return ordered[0].profileId;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
253
|
+
}
|
|
254
|
+
const m = /^(\d+)?([da])?$/.exec(answer);
|
|
255
|
+
// Unrecognized input → the safe default (choice 1); never loop, since this
|
|
256
|
+
// prompt is gating pi's boot on the shared TTY.
|
|
257
|
+
if (m === null)
|
|
258
|
+
return ordered[0].profileId;
|
|
259
|
+
const [, numStr, action] = m;
|
|
260
|
+
const idx = numStr !== undefined ? Number.parseInt(numStr, 10) - 1 : 0;
|
|
261
|
+
const picked = idx >= 0 && idx < ordered.length ? ordered[idx] : ordered[0];
|
|
262
|
+
if (action === 'd') {
|
|
263
|
+
setDefaultProfileId(cwd, picked.profileId);
|
|
264
|
+
process.stdout.write(` ${accent('\u2713')} ${dim(`"${picked.manifest.name}" is now the default here`)}\n`);
|
|
265
|
+
}
|
|
266
|
+
else if (action === 'a') {
|
|
267
|
+
addProfileProject(picked.profileId, cwd);
|
|
268
|
+
process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${picked.manifest.name}"`)}\n`);
|
|
269
|
+
}
|
|
270
|
+
return picked.profileId;
|
|
233
271
|
}
|
|
234
272
|
finally {
|
|
235
273
|
rl.close();
|
|
@@ -241,6 +279,12 @@ async function promptPickProfileOrCreate(candidates, cwd, exact) {
|
|
|
241
279
|
* its manifest does not already cover `cwd` and the session is interactive,
|
|
242
280
|
* offer to add `cwd` to its purview (default yes). Bump `last_used_at`,
|
|
243
281
|
* return the id.
|
|
282
|
+
* 1b. Else, a per-cwd PINNED default (menu `d`) that still covers `cwd` is
|
|
283
|
+
* auto-picked outright — interactive and headless alike, no prompt (that is
|
|
284
|
+
* what "default" means). Interactive prints a breadcrumb naming it and how
|
|
285
|
+
* to change it. `forcePicker` (`crtr --pick-profile`) bypasses the pin to
|
|
286
|
+
* re-open the menu. A stale pin (profile gone / no longer covering) is
|
|
287
|
+
* cleared and ignored.
|
|
244
288
|
* 2. Else, if EXACTLY ONE profile's project dir IS `cwd` (you're at a project
|
|
245
289
|
* root, unambiguously one owner), auto-pick it with no prompt — the
|
|
246
290
|
* strongest signal. If SEVERAL profiles claim `cwd` exactly, it's genuinely
|
|
@@ -255,7 +299,7 @@ async function promptPickProfileOrCreate(candidates, cwd, exact) {
|
|
|
255
299
|
* create a profile here or proceed as root (root lives ONLY here). Non-
|
|
256
300
|
* interactive (no TTY): default to root (null) and print the recovery
|
|
257
301
|
* instruction to STDERR — never stdout, which the caller may be piping. */
|
|
258
|
-
export async function selectProfileForCwd(cwd, explicitProfile) {
|
|
302
|
+
export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = false) {
|
|
259
303
|
const resolvedCwd = resolveCwd(cwd);
|
|
260
304
|
if (explicitProfile !== undefined && explicitProfile !== null && explicitProfile !== '') {
|
|
261
305
|
const entry = loadProfileManifest(explicitProfile);
|
|
@@ -269,9 +313,30 @@ export async function selectProfileForCwd(cwd, explicitProfile) {
|
|
|
269
313
|
}
|
|
270
314
|
const covering = listProfiles().filter((p) => profileCoversCwd(p, resolvedCwd));
|
|
271
315
|
const exact = covering.filter((p) => p.manifest.projects.some((proj) => proj === resolvedCwd));
|
|
316
|
+
// A per-cwd pinned default (set from the menu with `d`) is the user's
|
|
317
|
+
// explicit "default to THIS profile HERE". It outranks both the exact-single
|
|
318
|
+
// auto-pick and global-MRU, so long as it still covers cwd; a stale pin
|
|
319
|
+
// (profile deleted, or its dir removed from purview) is self-healed away.
|
|
320
|
+
const pinnedId = getDefaultProfileId(resolvedCwd);
|
|
321
|
+
const pinned = pinnedId !== null ? covering.find((p) => p.profileId === pinnedId) ?? null : null;
|
|
322
|
+
if (pinnedId !== null && pinned === null)
|
|
323
|
+
clearDefaultProfile(resolvedCwd);
|
|
324
|
+
// A valid pin decides it outright and STOPS ASKING — the whole point of
|
|
325
|
+
// setting a default. Auto-pick it (interactive too), printing a one-line
|
|
326
|
+
// breadcrumb so the choice isn't invisible and the user knows how to change
|
|
327
|
+
// it. `crtr --pick-profile` (forcePicker) re-opens the menu to re-pin/unpin.
|
|
328
|
+
if (pinned !== null && !forcePicker) {
|
|
329
|
+
updateProfileLastUsed(pinned.profileId);
|
|
330
|
+
if (isInteractive()) {
|
|
331
|
+
process.stdout.write(` ${accent('\u2713')} ${bold(pinned.manifest.name)} ${dim('\u00b7 default for this directory')}` +
|
|
332
|
+
` ${dim('(crtr --pick-profile to change)')}\n`);
|
|
333
|
+
}
|
|
334
|
+
return pinned.profileId;
|
|
335
|
+
}
|
|
272
336
|
// Exactly one profile's project dir IS cwd — unambiguous project root, use it
|
|
273
337
|
// silently, never a prompt. (Several exact claims fall through to the menu.)
|
|
274
|
-
|
|
338
|
+
// Suppressed under forcePicker so `--pick-profile` can pin even a lone owner.
|
|
339
|
+
if (!forcePicker && pinned === null && exact.length === 1) {
|
|
275
340
|
updateProfileLastUsed(exact[0].profileId);
|
|
276
341
|
return exact[0].profileId;
|
|
277
342
|
}
|
|
@@ -290,10 +355,11 @@ export async function selectProfileForCwd(cwd, explicitProfile) {
|
|
|
290
355
|
// Interactive. Several exact claimants → choose among them. Else covered from
|
|
291
356
|
// a parent/workspace dir → menu of covering profiles. Else nothing covers →
|
|
292
357
|
// the create-or-root prompt (root is only ever offered there).
|
|
358
|
+
const validPinnedId = pinned !== null ? pinned.profileId : null;
|
|
293
359
|
const chosen = exact.length > 1
|
|
294
|
-
? await promptPickProfileOrCreate(exact, resolvedCwd, true)
|
|
360
|
+
? await promptPickProfileOrCreate(exact, resolvedCwd, true, validPinnedId)
|
|
295
361
|
: covering.length > 0
|
|
296
|
-
? await promptPickProfileOrCreate(covering, resolvedCwd, false)
|
|
362
|
+
? await promptPickProfileOrCreate(covering, resolvedCwd, false, validPinnedId)
|
|
297
363
|
: await promptCreateOrRoot(resolvedCwd);
|
|
298
364
|
if (chosen !== null)
|
|
299
365
|
updateProfileLastUsed(chosen);
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { type Trigger } from '../canvas/index.js';
|
|
2
|
-
/** The `<project_context>` block —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* in the document substrate (`crtr sys sync` migrates each CLAUDE.md/AGENTS.md
|
|
7
|
-
* into that dir's own `.crouter/memory/AGENTS.md` at content/content, so it
|
|
8
|
-
* surfaces via the `<memory kind="knowledge">` block instead). Always emits
|
|
9
|
-
* the wrapper because the environment snapshot is always present. Exported
|
|
10
|
-
* for testing. */
|
|
2
|
+
/** The `<project_context>` block — cwd/git `<environment>` snapshot only
|
|
3
|
+
* (dir listing + git status/worktrees). Project guidance lives in the document
|
|
4
|
+
* substrate, so this wrapper stays environment-only. It always emits because
|
|
5
|
+
* the environment snapshot is always present. Exported for testing. */
|
|
11
6
|
export declare function buildProjectContextBlock(cwd: string): string;
|
|
12
7
|
/** The context-directory note — names the path INLINE (no XML attribute, so it
|
|
13
8
|
* never scopes a sibling block) and frames the dir as durable, shared scratch.
|
|
@@ -13,15 +13,13 @@
|
|
|
13
13
|
// the source's whole conversation and then impersonates it. A non-fork
|
|
14
14
|
// node's bearings are its FIRST entry, so there is nothing earlier to
|
|
15
15
|
// disown; it gets only the declarative identity line. The message also
|
|
16
|
-
// carries the <project_context> block (buildProjectContextBlock)
|
|
17
|
-
// the cwd/git <environment> snapshot. Project
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// dir's own `.crouter/memory/AGENTS.md` at content/content, so it rides the
|
|
16
|
+
// carries the <project_context> block (buildProjectContextBlock), which is
|
|
17
|
+
// now only the cwd/git <environment> snapshot. Project guidance rides the
|
|
18
|
+
// document substrate instead: migrated CLAUDE.md/AGENTS.md docs live in the
|
|
19
|
+
// source dir's own `.crouter/memory/AGENTS.md` and surface through the
|
|
21
20
|
// <memory kind="knowledge"> block below (renderKnowledgeBlock) via the
|
|
22
|
-
// profile-widened boot render
|
|
23
|
-
//
|
|
24
|
-
// anymore (see `crtr memory read taste/document-substrate`);
|
|
21
|
+
// profile-widened boot render — no parallel project-context loader lives
|
|
22
|
+
// here anymore (see `crtr memory read taste/document-substrate`);
|
|
25
23
|
// • promote.ts folds orchestratorContextNote() into the promotion guidance
|
|
26
24
|
// dump, so a node that becomes an orchestrator MID-LIFE gets the
|
|
27
25
|
// orchestrator framing it never received at spawn — it spawned as a base
|
|
@@ -112,15 +110,10 @@ function parseWorktrees(lines) {
|
|
|
112
110
|
}
|
|
113
111
|
return worktrees;
|
|
114
112
|
}
|
|
115
|
-
/** The `<project_context>` block —
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* in the document substrate (`crtr sys sync` migrates each CLAUDE.md/AGENTS.md
|
|
120
|
-
* into that dir's own `.crouter/memory/AGENTS.md` at content/content, so it
|
|
121
|
-
* surfaces via the `<memory kind="knowledge">` block instead). Always emits
|
|
122
|
-
* the wrapper because the environment snapshot is always present. Exported
|
|
123
|
-
* for testing. */
|
|
113
|
+
/** The `<project_context>` block — cwd/git `<environment>` snapshot only
|
|
114
|
+
* (dir listing + git status/worktrees). Project guidance lives in the document
|
|
115
|
+
* substrate, so this wrapper stays environment-only. It always emits because
|
|
116
|
+
* the environment snapshot is always present. Exported for testing. */
|
|
124
117
|
export function buildProjectContextBlock(cwd) {
|
|
125
118
|
const envLines = directoryListingLines(cwd);
|
|
126
119
|
const git = gitSnapshot(cwd);
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
// crtr [dir] ["prompt"] → root with a starter prompt
|
|
6
6
|
// crtr --name NAME ... → named root
|
|
7
7
|
// crtr --profile <id-or-name> → root under an explicit profile (else the
|
|
8
|
-
// startup selector:
|
|
9
|
-
//
|
|
8
|
+
// startup selector: a per-cwd pinned default,
|
|
9
|
+
// else MRU covering cwd, else a synchronous
|
|
10
|
+
// create-or-root prompt)
|
|
11
|
+
// crtr --pick-profile → force the profile chooser open even when a
|
|
12
|
+
// per-cwd default is pinned (to re-pin/unpin)
|
|
10
13
|
// crtr <subcommand> ... → falls through to the normal dispatcher
|
|
11
14
|
// crtr -h | --help → root help (dispatcher)
|
|
12
15
|
//
|
|
@@ -28,7 +31,7 @@ function isDir(p) {
|
|
|
28
31
|
* positional dir/prompt). A leading token in this set still boots a root —
|
|
29
32
|
* without it, `crtr --name X` would fall through to the dispatcher and error as
|
|
30
33
|
* an unknown subcommand. */
|
|
31
|
-
const FRONT_DOOR_FLAGS = new Set(['--name', '--kind', '--profile']);
|
|
34
|
+
const FRONT_DOOR_FLAGS = new Set(['--name', '--kind', '--profile', '--pick-profile']);
|
|
32
35
|
/** Parse `[dir] [prompt]` positionals + the front-door flags out of the leftover
|
|
33
36
|
* tokens after the bare `crtr`. */
|
|
34
37
|
function parseRootArgs(tokens) {
|
|
@@ -36,6 +39,7 @@ function parseRootArgs(tokens) {
|
|
|
36
39
|
let name;
|
|
37
40
|
let kind;
|
|
38
41
|
let profile;
|
|
42
|
+
let pickProfile = false;
|
|
39
43
|
const positionals = [];
|
|
40
44
|
for (let i = 0; i < tokens.length; i++) {
|
|
41
45
|
const t = tokens[i];
|
|
@@ -48,6 +52,9 @@ function parseRootArgs(tokens) {
|
|
|
48
52
|
else if (t === '--profile') {
|
|
49
53
|
profile = tokens[++i];
|
|
50
54
|
}
|
|
55
|
+
else if (t === '--pick-profile') {
|
|
56
|
+
pickProfile = true;
|
|
57
|
+
}
|
|
51
58
|
else if (t.startsWith('--')) {
|
|
52
59
|
// ignore unknown flags for the front door
|
|
53
60
|
}
|
|
@@ -60,7 +67,7 @@ function parseRootArgs(tokens) {
|
|
|
60
67
|
cwd = resolvePath(positionals.shift());
|
|
61
68
|
}
|
|
62
69
|
const prompt = positionals.length > 0 ? positionals.join(' ') : undefined;
|
|
63
|
-
return { cwd, prompt, name, kind, profile };
|
|
70
|
+
return { cwd, prompt, name, kind, profile, pickProfile };
|
|
64
71
|
}
|
|
65
72
|
/** Env marker set on every pi the front door boots. Its presence means we are
|
|
66
73
|
* already inside a front-door-booted root, so a nested front-door launch must
|
|
@@ -203,7 +203,8 @@ export function reviveNode(nodeId, opts) {
|
|
|
203
203
|
// without this a manager waiting dormant on this child hangs forever,
|
|
204
204
|
// never told it will not come back on its own. Mirrors closeNode's step-4
|
|
205
205
|
// fan-out (same subscribers table, same active/passive split).
|
|
206
|
-
fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child crashed — ${fullName(meta)} (${nodeId}) failed to relaunch and is now dead
|
|
206
|
+
fanDoctrineWake(nodeId, subscribersOf(nodeId), `Child crashed — ${fullName(meta)} (${nodeId}) failed to relaunch and is now dead: ${error.message}\n\n` +
|
|
207
|
+
`It stays dead until you revive it — \`crtr node lifecycle revive ${nodeId}\`.`, { reason: 'child-crashed', child: nodeId });
|
|
207
208
|
}
|
|
208
209
|
catch {
|
|
209
210
|
/* best-effort cleanup */
|
|
@@ -11,6 +11,10 @@ export interface BootRootOpts {
|
|
|
11
11
|
* selector for `cwd` (see `selectProfileForCwd`) — a root has no spawner to
|
|
12
12
|
* inherit from, so this is the front door's only source of profile identity. */
|
|
13
13
|
profile?: string | null;
|
|
14
|
+
/** `crtr --pick-profile`: force the startup profile chooser to open even when
|
|
15
|
+
* a per-cwd default is pinned, so the user can re-pin or unpin it. Without
|
|
16
|
+
* this a pinned default auto-picks silently (that is what "default" means). */
|
|
17
|
+
pickProfile?: boolean;
|
|
14
18
|
}
|
|
15
19
|
/** Create the front-door root: launch its broker engine, then exec `crtr surface attach`
|
|
16
20
|
* inline so THIS terminal becomes the broker's controller-viewer. Does not
|
|
@@ -69,7 +69,7 @@ export async function bootRoot(opts) {
|
|
|
69
69
|
// The front door's only source of profile identity — a root has no spawner
|
|
70
70
|
// to inherit from. Runs BEFORE spawnNode: explicit --profile > MRU profile
|
|
71
71
|
// covering cwd > a synchronous create-or-root prompt (or root, headless).
|
|
72
|
-
const profileId = await selectProfileForCwd(opts.cwd, opts.profile);
|
|
72
|
+
const profileId = await selectProfileForCwd(opts.cwd, opts.profile, opts.pickProfile ?? false);
|
|
73
73
|
// A born-resident root starts in base mode; it earns the orchestrator persona
|
|
74
74
|
// the first time it delegates (or on promotion). Resident lifecycle either way.
|
|
75
75
|
const { launch } = buildLaunchSpec(kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { KINDS, isDocKind, RUNGS, rungRank, rungAtLeast, FALLBACK_RUNG, parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, normalizeNameSegment, normalizeDocName, } from './schema.js';
|
|
1
|
+
export { KINDS, isDocKind, RUNGS, rungRank, rungAtLeast, FALLBACK_RUNG, parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, normalizeNameSegment, normalizeDocName, resolveDocName, } from './schema.js';
|
|
2
2
|
export type { DocKind, Rung, GatePredicate, SubstrateSchema, SubstrateDoc } from './schema.js';
|
|
3
3
|
export { scopeForCwd, spineDepth, assembleNodeSubject, profileNameFor } from './subject.js';
|
|
4
4
|
export type { NodeConfigSubject } from './subject.js';
|
|
@@ -13,7 +13,9 @@ FALLBACK_RUNG,
|
|
|
13
13
|
// parse + render-shared helpers
|
|
14
14
|
parseSubstrateFrontmatter, parseSubstrateDoc, previewLine,
|
|
15
15
|
// display-name normalization
|
|
16
|
-
normalizeNameSegment, normalizeDocName,
|
|
16
|
+
normalizeNameSegment, normalizeDocName,
|
|
17
|
+
// substrate identity (explicit-name-then-path-fallback)
|
|
18
|
+
resolveDocName, } from './schema.js';
|
|
17
19
|
export { scopeForCwd, spineDepth, assembleNodeSubject, profileNameFor } from './subject.js';
|
|
18
20
|
export { gatePasses } from './gate.js';
|
|
19
21
|
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, } from './ceiling.js';
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// ancestor PROJECT `.crouter/memory/` surfaces (a doc surfaces when a file
|
|
11
11
|
// beside/under its own project scope dir is read), UNLESS it carries an
|
|
12
12
|
// explicit read-trigger (see D5 below). This is the substrate's own
|
|
13
|
-
// ancestor walk keyed on `.crouter/memory/` —
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// ancestor walk keyed on `.crouter/memory/` — project guidance that starts
|
|
14
|
+
// in CLAUDE.md/AGENTS.md or `.claude/rules` now lives here after
|
|
15
|
+
// `crtr sys sync` migrates it. The user-global
|
|
16
16
|
// `~/.crouter/memory/` store is NOT positional; user docs only fire on
|
|
17
17
|
// reads through explicit `applies-to` / `read-when` triggers.
|
|
18
18
|
// • applies-to GLOB — any RESOLVED substrate doc (user/project/builtin scope)
|
|
@@ -58,16 +58,16 @@ import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
|
58
58
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
59
59
|
import { evalCondition } from '../predicate.js';
|
|
60
60
|
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
61
|
-
import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
61
|
+
import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, } from './index.js';
|
|
62
62
|
import { cachedSubstrateDocs } from './session-cache.js';
|
|
63
63
|
// Ancestor dirs we never look inside for a `.crouter/memory/` store (the read
|
|
64
64
|
// file may live under a build/dependency tree; `.crouter` is NOT junk here — it
|
|
65
65
|
// is the segment we explicitly join onto each surviving ancestor).
|
|
66
66
|
const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
|
|
67
67
|
// ---------------------------------------------------------------------------
|
|
68
|
-
// Small path helpers (mirror the on-read precedent
|
|
69
|
-
//
|
|
70
|
-
//
|
|
68
|
+
// Small path helpers (mirror the on-read precedent established by
|
|
69
|
+
// frontmatter-rules so the injected envelope keeps the faithful substrate
|
|
70
|
+
// shape).
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
72
72
|
function realpathOrSelf(p) {
|
|
73
73
|
try {
|
|
@@ -120,13 +120,18 @@ function loadPositionalDoc(file, memDir, scope) {
|
|
|
120
120
|
.replace(/\.md$/i, '')
|
|
121
121
|
.split(sep)
|
|
122
122
|
.join('/');
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
123
|
+
const fallbackName = normalizeDocName(raw);
|
|
124
|
+
if (fallbackName === '')
|
|
125
125
|
return null;
|
|
126
126
|
const { data, body } = parseFrontmatterGeneric(readText(file));
|
|
127
127
|
const schema = parseSubstrateFrontmatter(data);
|
|
128
128
|
if (schema === null)
|
|
129
129
|
return null;
|
|
130
|
+
// ONE substrate-identity rule (schema.ts's resolveDocName), shared with the
|
|
131
|
+
// resolver's loadMemoryDoc: an explicit frontmatter `name` wins over the
|
|
132
|
+
// path-derived fallback, so a positionally-discovered doc surfaces on-read
|
|
133
|
+
// under the SAME identity the boot resolver would give it.
|
|
134
|
+
const name = resolveDocName(data, fallbackName);
|
|
130
135
|
return { ...schema, name, scope, path: file, body };
|
|
131
136
|
}
|
|
132
137
|
catch {
|
|
@@ -54,7 +54,7 @@ import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
|
54
54
|
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
55
55
|
import { memoryDir } from '../runtime/memory.js';
|
|
56
56
|
import { projectScopeRoots } from '../scope.js';
|
|
57
|
-
import { assembleNodeSubject, buildCeilingIndex, effectiveRung, gatePasses, indexDirOf, isIndexName, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, rungRank, } from './index.js';
|
|
57
|
+
import { assembleNodeSubject, buildCeilingIndex, effectiveRung, gatePasses, indexDirOf, isIndexName, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, rungRank, } from './index.js';
|
|
58
58
|
import { cachedNodeSubject, cachedSubstrateDocs } from './session-cache.js';
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
// Step 1 — winner selection (ceiling + gate + first-wins dedup).
|
|
@@ -209,14 +209,18 @@ function nodeLocalDocs(nodeId, subject) {
|
|
|
209
209
|
const out = [];
|
|
210
210
|
for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
|
|
211
211
|
const raw = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
|
|
212
|
-
const
|
|
213
|
-
if (!
|
|
212
|
+
const fallbackName = normalizeDocName(raw);
|
|
213
|
+
if (!fallbackName)
|
|
214
214
|
continue;
|
|
215
215
|
try {
|
|
216
216
|
const { data, body } = parseFrontmatterGeneric(readText(file));
|
|
217
217
|
const schema = parseSubstrateFrontmatter(data);
|
|
218
218
|
if (schema === null)
|
|
219
219
|
continue;
|
|
220
|
+
// ONE substrate-identity rule (schema.ts's resolveDocName), shared with
|
|
221
|
+
// the resolver and the on-read positional loader: explicit frontmatter
|
|
222
|
+
// `name` wins over the physical-path-derived fallback.
|
|
223
|
+
const name = resolveDocName(data, fallbackName);
|
|
220
224
|
// node-local is NOT a resolver scope; `scope` is a placeholder never read
|
|
221
225
|
// by gate eval (keyed off the NODE subject, not the doc) nor by the
|
|
222
226
|
// renderers (keyed off name / body / rung).
|
|
@@ -20,6 +20,10 @@ export declare function normalizeNameSegment(segment: string): string;
|
|
|
20
20
|
* `spine/has-manager`. This is the identity a doc displays, dedups, and
|
|
21
21
|
* resolves under — distinct from its physical path, which keeps the prefix. */
|
|
22
22
|
export declare function normalizeDocName(name: string): string;
|
|
23
|
+
/** Resolve a doc's identity from its raw frontmatter record: an explicit
|
|
24
|
+
* `name` field wins (trimmed + normalized), else `fallbackName` (the
|
|
25
|
+
* normalized path-derived name). */
|
|
26
|
+
export declare function resolveDocName(fm: Record<string, unknown> | null | undefined, fallbackName: string): string;
|
|
23
27
|
/** A gate predicate tree, evaluated by predicate.ts (`evalCondition`) against
|
|
24
28
|
* the node-config subject. Typed loosely on purpose — the matcher engine owns
|
|
25
29
|
* validation; structurally it is a field→matcher map with optional
|
|
@@ -75,10 +79,12 @@ export interface SubstrateSchema {
|
|
|
75
79
|
rationale?: string;
|
|
76
80
|
}
|
|
77
81
|
/** A fully-resolved substrate document: the parsed schema PLUS the resolver's
|
|
78
|
-
*
|
|
82
|
+
* identity (explicit frontmatter `name` when present, otherwise the normalized
|
|
83
|
+
* path-derived fallback) and body. This single object flows through the whole
|
|
79
84
|
* pipeline (gate eval → boot/on-read render), so a renderer never re-parses. */
|
|
80
85
|
export interface SubstrateDoc extends SubstrateSchema {
|
|
81
|
-
/**
|
|
86
|
+
/** Resolver-supplied identity, e.g. `taste/document-substrate` or an explicit
|
|
87
|
+
* frontmatter name when one is present. */
|
|
82
88
|
name: string;
|
|
83
89
|
/** The scope this doc resolved from. */
|
|
84
90
|
scope: MemoryScope;
|
|
@@ -51,8 +51,9 @@ export const FALLBACK_RUNG = 'none';
|
|
|
51
51
|
// prefix is NEVER part of the doc's identity: every identity-facing surface —
|
|
52
52
|
// MemoryDoc.name, direct lookup, leaf fallback, `crtr memory read`/`list`,
|
|
53
53
|
// prompt render, on-read display — derives from the NORMALIZED segments, so
|
|
54
|
-
// `00-runtime-base` displays/dedups/resolves as `runtime-base`.
|
|
55
|
-
//
|
|
54
|
+
// `00-runtime-base` displays/dedups/resolves as `runtime-base`. When a doc
|
|
55
|
+
// sets an explicit frontmatter `name`, that normalized name wins instead of
|
|
56
|
+
// the path-derived fallback. Only the physical path keeps the prefix.
|
|
56
57
|
// ---------------------------------------------------------------------------
|
|
57
58
|
const NUMERIC_PREFIX_RE = /^\d{2}-/;
|
|
58
59
|
/** Strip an optional `NN-` ordering prefix from ONE path segment (file or
|
|
@@ -69,6 +70,22 @@ export function normalizeDocName(name) {
|
|
|
69
70
|
return name.split('/').map(normalizeNameSegment).join('/');
|
|
70
71
|
}
|
|
71
72
|
// ---------------------------------------------------------------------------
|
|
73
|
+
// Substrate identity — the ONE explicit-name-then-path-fallback rule. A doc's
|
|
74
|
+
// resolver identity is its explicit frontmatter `name` when present (normalized),
|
|
75
|
+
// otherwise the caller-supplied path-derived fallback. Every loader that turns a
|
|
76
|
+
// physical .md file into a named doc (the resolver's `listMemoryDocsInDir`, the
|
|
77
|
+
// on-read positional loader) calls this ONE helper, so a doc's identity never
|
|
78
|
+
// depends on which path loaded it.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/** Resolve a doc's identity from its raw frontmatter record: an explicit
|
|
81
|
+
* `name` field wins (trimmed + normalized), else `fallbackName` (the
|
|
82
|
+
* normalized path-derived name). */
|
|
83
|
+
export function resolveDocName(fm, fallbackName) {
|
|
84
|
+
const raw = fm?.['name'];
|
|
85
|
+
const explicit = typeof raw === 'string' && raw.trim() !== '' ? normalizeDocName(raw.trim()) : '';
|
|
86
|
+
return explicit !== '' ? explicit : fallbackName;
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
72
89
|
// Parse / validate.
|
|
73
90
|
// ---------------------------------------------------------------------------
|
|
74
91
|
/** Parse a raw frontmatter record (from `parseFrontmatterGeneric`, via the
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -119,6 +119,26 @@ const REVIVE_GRACE_MS = 20_000;
|
|
|
119
119
|
// Per-node first-observed-dead timestamps, for the grace window above. In-memory
|
|
120
120
|
// only — a daemon restart resets it (worst case: one extra grace interval).
|
|
121
121
|
const unhealthySince = new Map();
|
|
122
|
+
// Stranded-relaunch retry cap (2026-07-06 diagnosis, Q4): a relaunch whose
|
|
123
|
+
// broker never re-records its pid (dies before session_start, over and over)
|
|
124
|
+
// used to retry via the boot-grace clock above FOREVER — no attempt cap, no
|
|
125
|
+
// backoff — observed at 5,642 iterations on real nodes before the daemon was
|
|
126
|
+
// finally restarted. Bound the attempts with exponential backoff, then give up
|
|
127
|
+
// ONCE with a doctrine wake instead of spinning quietly for days.
|
|
128
|
+
const STRANDED_RELAUNCH_MAX_ATTEMPTS = 6;
|
|
129
|
+
const STRANDED_RELAUNCH_MAX_BACKOFF_MS = 10 * 60_000; // 10 minutes
|
|
130
|
+
// Per-node stranded-relaunch attempt count + the wall-clock time of the last
|
|
131
|
+
// attempt, backing the cap/backoff above. In-memory only, like unhealthySince
|
|
132
|
+
// — a daemon restart resets them (worst case: one extra round of retries).
|
|
133
|
+
const strandedRelaunchAttempts = new Map();
|
|
134
|
+
const strandedRelaunchLastAttempt = new Map();
|
|
135
|
+
function strandedRelaunchBackoffMs(attempts) {
|
|
136
|
+
return Math.min(REVIVE_GRACE_MS * 2 ** attempts, STRANDED_RELAUNCH_MAX_BACKOFF_MS);
|
|
137
|
+
}
|
|
138
|
+
function clearStrandedRelaunchState(id) {
|
|
139
|
+
strandedRelaunchAttempts.delete(id);
|
|
140
|
+
strandedRelaunchLastAttempt.delete(id);
|
|
141
|
+
}
|
|
122
142
|
// §H refresh-authority grace: how long a node may sit with intent='refresh', its
|
|
123
143
|
// turn OVER (busy marker absent) and its engine still ALIVE before the daemon
|
|
124
144
|
// concludes the refresh stalled and force-kills the engine. The healthy window is
|
|
@@ -686,6 +706,7 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
|
|
|
686
706
|
// engine so the dead-pid refresh branch below revives it fresh.
|
|
687
707
|
if (pid != null && isPidAlive(pid)) {
|
|
688
708
|
unhealthySince.delete(id);
|
|
709
|
+
clearStrandedRelaunchState(id); // a live engine means the stranding resolved
|
|
689
710
|
handleYieldStall(row, pid, now);
|
|
690
711
|
handleWedgeDetection(id, pid, now);
|
|
691
712
|
handleFatalFault(id, now);
|
|
@@ -741,8 +762,29 @@ async function handleNodeLiveness(row, now, revivedThisTick) {
|
|
|
741
762
|
// the pid-clear, before re-record. Resume it on the saved session — unless
|
|
742
763
|
// that dead relaunch was itself a cycling attempt (cycle_pending), in which
|
|
743
764
|
// case retry it AS a cycle (retryResumeMode) instead of stranding it as a
|
|
744
|
-
// strict resume into an empty marker branch.
|
|
745
|
-
|
|
765
|
+
// strict resume into an empty marker branch. Capped + backed off (above) so
|
|
766
|
+
// a node whose broker NEVER re-records a pid can't spin this branch forever.
|
|
767
|
+
const attempts = strandedRelaunchAttempts.get(id) ?? 0;
|
|
768
|
+
if (attempts >= STRANDED_RELAUNCH_MAX_ATTEMPTS) {
|
|
769
|
+
process.stderr.write(`[crtrd] giving up on ${id} after ${attempts} stranded-relaunch attempts (pid never re-recorded)\n`);
|
|
770
|
+
clearStrandedRelaunchState(id);
|
|
771
|
+
transition(id, 'crash');
|
|
772
|
+
try {
|
|
773
|
+
fanDoctrineWake(id, subscribersOf(id), `Child crashed — ${fullName(meta)} (${id}) failed to relaunch after ${attempts} attempts (its engine kept dying before it could re-record a pid) and is now dead. ` +
|
|
774
|
+
`It stays dead until you revive it — \`crtr node lifecycle revive ${id}\`.`, { reason: 'child-crashed', child: id });
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
/* best-effort, mirrors every other fan-out in this file */
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const lastAttempt = strandedRelaunchLastAttempt.get(id);
|
|
782
|
+
const backoff = strandedRelaunchBackoffMs(attempts);
|
|
783
|
+
if (lastAttempt !== undefined && now - lastAttempt < backoff)
|
|
784
|
+
return; // backing off before the next attempt
|
|
785
|
+
strandedRelaunchAttempts.set(id, attempts + 1);
|
|
786
|
+
strandedRelaunchLastAttempt.set(id, now);
|
|
787
|
+
process.stderr.write(`[crtrd] revive ${id} (stranded relaunch — pid never re-recorded, attempt ${attempts + 1}/${STRANDED_RELAUNCH_MAX_ATTEMPTS})\n`);
|
|
746
788
|
reviveNode(id, { resume: retryResumeMode(meta) });
|
|
747
789
|
revivedThisTick.add(id); // third-pass bare double-spawn guard (Maj-4)
|
|
748
790
|
return;
|
package/dist/daemon/manage.d.ts
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/** Env keys that must NEVER reach the daemon process. Restarting crtrd is
|
|
2
|
+
* overwhelmingly done from inside an agent node's own bash tool (`crtr sys
|
|
3
|
+
* daemon stop && start`), which inherits that node's FULL env — its identity
|
|
4
|
+
* (`CRTR_NODE_ID`/`CRTR_KIND`/…, the `nodeEnv()` shape in `core/runtime/
|
|
5
|
+
* nodes.ts`), its front-door recursion-guard flag, and any pi-engine
|
|
6
|
+
* resolution seam a prior test/dev session left exported. The daemon is a
|
|
7
|
+
* singleton supervisor, never "a node" itself, so none of this belongs in its
|
|
8
|
+
* env regardless of whether today's code happens to read it — and at least one
|
|
9
|
+
* of these IS actively read: `host.ts`'s broker-engine resolution falls back to
|
|
10
|
+
* `process.env['CRTR_BROKER_ENGINE']` verbatim (nodeEnv() never sets that key,
|
|
11
|
+
* so nothing overrides it per child launch), so a daemon that inherits a
|
|
12
|
+
* stale/dev override throws inside `headlessBrokerHost.launch()` on EVERY
|
|
13
|
+
* relaunch it ever attempts, for its whole lifetime — the 2026-07-06 diagnosis
|
|
14
|
+
* root cause behind 19 nodes killed with "failed to relaunch and is now dead". */
|
|
15
|
+
export declare const DAEMON_ENV_STRIP_KEYS: readonly string[];
|
|
16
|
+
/** A copy of `process.env` with every `DAEMON_ENV_STRIP_KEYS` entry removed.
|
|
17
|
+
* Deliberate global config the user actually wants the daemon to see —
|
|
18
|
+
* `CRTR_HOME`, `CRTR_SUBTREE`, `CRTR_LOG`, `CRTR_DEBUG`, etc. — passes through
|
|
19
|
+
* untouched; only the node-identity/engine-poisoning surface is stripped. */
|
|
20
|
+
export declare function sanitizedDaemonEnv(): NodeJS.ProcessEnv;
|
|
1
21
|
export interface SpawnDaemonResult {
|
|
2
22
|
/** True when a new daemon process was spawned. */
|
|
3
23
|
started: boolean;
|