@crouton-kit/crouter 0.3.45 → 0.3.46
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/build-root.js +0 -1
- package/dist/builtin-memory/00-runtime-base.md +3 -0
- package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
- package/dist/builtin-views/chat/web.jsx +1 -1
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +11 -9
- package/dist/clients/attach/attach-cmd.js +557 -546
- package/dist/clients/attach/chat-view.d.ts +4 -0
- package/dist/clients/attach/chat-view.js +13 -2
- package/dist/clients/attach/mermaid-render.d.ts +23 -0
- package/dist/clients/attach/mermaid-render.js +124 -0
- package/dist/clients/web/source-cache.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +2 -2
- package/dist/commands/canvas-history/read.js +3 -3
- package/dist/commands/canvas-history/search.js +1 -1
- package/dist/commands/canvas-history.js +11 -12
- package/dist/commands/canvas-rebuild-index.js +2 -2
- package/dist/commands/canvas.js +4 -4
- package/dist/commands/node-inspect-artifacts.d.ts +1 -0
- package/dist/commands/{canvas-history/show.js → node-inspect-artifacts.js} +7 -7
- package/dist/commands/node-lifecycle-revive.d.ts +12 -0
- package/dist/commands/node-lifecycle-revive.js +166 -0
- package/dist/commands/node-worktree.d.ts +2 -0
- package/dist/commands/{worktree.js → node-worktree.js} +15 -16
- package/dist/commands/node.js +16 -13
- package/dist/commands/push.js +1 -1
- package/dist/commands/revive.d.ts +0 -10
- package/dist/commands/revive.js +31 -181
- package/dist/core/__tests__/kickoff.test.js +2 -2
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +6 -6
- package/dist/core/canvas/browse/render.js +1 -1
- package/dist/core/canvas/status-glyph.d.ts +1 -1
- package/dist/core/canvas/status-glyph.js +1 -1
- package/dist/core/preview-registry.js +27 -26
- package/dist/core/profiles/select.d.ts +12 -4
- package/dist/core/profiles/select.js +182 -22
- package/dist/core/runtime/bearings.js +1 -1
- package/dist/core/runtime/close.js +1 -1
- package/dist/core/runtime/kickoff.js +3 -3
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/view/stream-local.js +1 -1
- package/dist/core/worktree.js +4 -4
- package/dist/daemon/crtrd.js +3 -3
- package/dist/web/transport-stream.js +1 -1
- package/dist/web-client/assets/{index-B00YpRQ1.js → index-Di-gSsVn.js} +2 -2
- package/dist/web-client/index.html +1 -1
- package/package.json +2 -2
- package/scripts/postinstall.mjs +73 -0
- package/dist/commands/canvas-history/show.d.ts +0 -1
- package/dist/commands/worktree.d.ts +0 -2
|
@@ -76,9 +76,10 @@ const LEAF_ICON_CP = {
|
|
|
76
76
|
'canvas attention list': 0xf03a,
|
|
77
77
|
'canvas attention map': 0xf279,
|
|
78
78
|
'canvas revive': 0xf021,
|
|
79
|
+
'node lifecycle revive': 0xf021,
|
|
79
80
|
'canvas history search': 0xf002,
|
|
80
81
|
'canvas history read': 0xf02d,
|
|
81
|
-
'
|
|
82
|
+
'node inspect artifacts': 0xf1da,
|
|
82
83
|
'sys daemon start': 0xf04b,
|
|
83
84
|
'sys daemon status': 0xf111,
|
|
84
85
|
'sys daemon stop': 0xf04d,
|
|
@@ -558,10 +559,10 @@ function summarizeCanvasAttentionMap(text) {
|
|
|
558
559
|
}
|
|
559
560
|
/** Find the first argv token after `skipWords` that isn't a boolean flag
|
|
560
561
|
* (`--foo`) — recovers a leaf's own positional id when its render() output
|
|
561
|
-
* carries no id of its own (see
|
|
562
|
-
* specifically because every `
|
|
563
|
-
* separate value token, so a naive forward scan can't mistake a
|
|
564
|
-
* value for the positional. */
|
|
562
|
+
* carries no id of its own (see summarizeNodeLifecycleRevive below). Safe
|
|
563
|
+
* here specifically because every `node lifecycle revive` flag is a boolean
|
|
564
|
+
* with no separate value token, so a naive forward scan can't mistake a
|
|
565
|
+
* flag's value for the positional. */
|
|
565
566
|
function firstPositionalAfter(tokens, skipWords) {
|
|
566
567
|
for (let i = skipWords; i < tokens.length; i++) {
|
|
567
568
|
if (!tokens[i].startsWith('--'))
|
|
@@ -569,31 +570,24 @@ function firstPositionalAfter(tokens, skipWords) {
|
|
|
569
570
|
}
|
|
570
571
|
return undefined;
|
|
571
572
|
}
|
|
572
|
-
/** `crtr
|
|
573
|
-
* its object output falls through to generic
|
|
574
|
-
* depending on flags:
|
|
575
|
-
* - `--
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
* - `--now` kick: the only shape with `- kicked: true` (revive.ts:192).
|
|
581
|
-
* - plain single-node revive: the only shape with `- resumed: true|false`.
|
|
582
|
-
* `window` (revive.ts:83) is documented "Always null", so it's never
|
|
583
|
-
* bulleted — and there is no node id anywhere in the output, so it's
|
|
584
|
-
* recovered from argv (the leaf's own positional) via
|
|
573
|
+
/** `crtr node lifecycle revive` (src/commands/node-lifecycle-revive.ts) has no
|
|
574
|
+
* custom `render`, so its object output falls through to generic
|
|
575
|
+
* renderResult. Two shapes depending on flags:
|
|
576
|
+
* - `--now` kick: the only shape with `- kicked: true`.
|
|
577
|
+
* - plain single-node revive (default or `--fresh`): the only shape with
|
|
578
|
+
* `- resumed: true|false`. `window` is documented "Always null", so it's
|
|
579
|
+
* never bulleted — and there is no node id anywhere in the output, so
|
|
580
|
+
* it's recovered from argv (the leaf's own positional) via
|
|
585
581
|
* firstPositionalAfter.
|
|
586
582
|
* Returns null on any other/unrecognized shape. */
|
|
587
|
-
function
|
|
588
|
-
if (/^- mode:\s*\S+\s*$/m.test(text))
|
|
589
|
-
return null;
|
|
583
|
+
function summarizeNodeLifecycleRevive(text, tokens) {
|
|
590
584
|
if (/^- kicked:\s*true\s*$/m.test(text)) {
|
|
591
585
|
return [{ text: 'kicked — daemon resumes in ~20s', tone: 'warning' }];
|
|
592
586
|
}
|
|
593
587
|
const resumedMatch = /^- resumed:\s*(true|false)\s*$/m.exec(text);
|
|
594
588
|
if (resumedMatch === null)
|
|
595
589
|
return null;
|
|
596
|
-
const id = firstPositionalAfter(tokens,
|
|
590
|
+
const id = firstPositionalAfter(tokens, 3); // 'node','lifecycle','revive' = 3 words
|
|
597
591
|
const lines = [{ text: id !== undefined ? `revived ${id}` : 'revived', tone: 'success' }];
|
|
598
592
|
const readyMatch = /^- ready:\s*(true|false)\s*$/m.exec(text);
|
|
599
593
|
if (readyMatch !== null && readyMatch[1] === 'false') {
|
|
@@ -601,6 +595,13 @@ function summarizeCanvasRevive(text, tokens) {
|
|
|
601
595
|
}
|
|
602
596
|
return lines;
|
|
603
597
|
}
|
|
598
|
+
// `crtr canvas revive --all` (src/commands/revive.ts) has no custom `render`,
|
|
599
|
+
// so its object output falls through to generic renderResult; both the
|
|
600
|
+
// preview and executed shapes carry a `- mode: preview|revived` bullet, which
|
|
601
|
+
// core/render.ts's `isProse` makes genuinely ambiguous to parse strictly
|
|
602
|
+
// (`candidates`/`revived`/`failed` are prose-or-bullet depending on whether
|
|
603
|
+
// they contain a newline). Rare admin path — no summarizer registered; the
|
|
604
|
+
// generic truncated render is fine.
|
|
604
605
|
/** `crtr canvas history search` (renderSearch, src/commands/canvas-history/
|
|
605
606
|
* search.ts:246) has three shapes: `0 hits.`; `--full` (### heading blocks
|
|
606
607
|
* per hit, not tabular — skipped, returns null); or the normal `N of M
|
|
@@ -685,7 +686,7 @@ function summarizeCanvasHistoryRead(text) {
|
|
|
685
686
|
}
|
|
686
687
|
return lines;
|
|
687
688
|
}
|
|
688
|
-
/** `crtr
|
|
689
|
+
/** `crtr node inspect artifacts` (src/commands/node-inspect-artifacts.ts):
|
|
689
690
|
* `node: <name (id)>\n\n0 artifacts. | N artifacts:\n\n<table
|
|
690
691
|
* cols=ref,source,ts,detail>\n\n<follow_up>`. `source` values are
|
|
691
692
|
* colon-prefixed types (`report:final`, `report:update`, `doc`,
|
|
@@ -695,7 +696,7 @@ function summarizeCanvasHistoryRead(text) {
|
|
|
695
696
|
* match — or when the parsed row count doesn't match the leading `N
|
|
696
697
|
* artifacts:` count (this render has no pagination, so the two must
|
|
697
698
|
* agree exactly). */
|
|
698
|
-
function
|
|
699
|
+
function summarizeNodeInspectArtifacts(text) {
|
|
699
700
|
const nodeMatch = /^node: (.+)$/m.exec(text);
|
|
700
701
|
if (nodeMatch === null)
|
|
701
702
|
return null;
|
|
@@ -1897,10 +1898,10 @@ const SUMMARIZERS = [
|
|
|
1897
1898
|
{ path: 'canvas attention count', run: summarizeCanvasAttentionCount },
|
|
1898
1899
|
{ path: 'canvas attention list', run: summarizeCanvasAttentionList },
|
|
1899
1900
|
{ path: 'canvas attention map', run: summarizeCanvasAttentionMap },
|
|
1900
|
-
{ path: '
|
|
1901
|
+
{ path: 'node lifecycle revive', run: summarizeNodeLifecycleRevive },
|
|
1901
1902
|
{ path: 'canvas history search', run: summarizeCanvasHistorySearch },
|
|
1902
1903
|
{ path: 'canvas history read', run: summarizeCanvasHistoryRead },
|
|
1903
|
-
{ path: '
|
|
1904
|
+
{ path: 'node inspect artifacts', run: summarizeNodeInspectArtifacts },
|
|
1904
1905
|
{ path: 'sys daemon start', run: summarizeSysDaemonStart },
|
|
1905
1906
|
{ path: 'sys daemon status', run: summarizeSysDaemonStatus },
|
|
1906
1907
|
{ path: 'sys daemon stop', run: summarizeSysDaemonStop },
|
|
@@ -4,10 +4,18 @@
|
|
|
4
4
|
* its manifest does not already cover `cwd` and the session is interactive,
|
|
5
5
|
* offer to add `cwd` to its purview (default yes). Bump `last_used_at`,
|
|
6
6
|
* return the id.
|
|
7
|
-
* 2. Else,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 2. Else, if EXACTLY ONE profile's project dir IS `cwd` (you're at a project
|
|
8
|
+
* root, unambiguously one owner), auto-pick it with no prompt — the
|
|
9
|
+
* strongest signal. If SEVERAL profiles claim `cwd` exactly, it's genuinely
|
|
10
|
+
* ambiguous: interactive → menu among them + create; headless → MRU.
|
|
11
|
+
* 3. Else, among profiles whose project list otherwise COVERS `cwd` (cwd sits
|
|
12
|
+
* in a subdir of, or a workspace above, one of their project dirs):
|
|
13
|
+
* - Interactive: prompt with a MENU of those covering profiles + "create
|
|
14
|
+
* a new profile here", BEFORE pi boots (no root option — the directory
|
|
15
|
+
* is covered). Default is the MRU covering profile.
|
|
16
|
+
* - Non-interactive: auto-pick the MRU covering profile, no prompt.
|
|
17
|
+
* 4. Else — nothing covers cwd walking all the way up. Interactive: prompt to
|
|
18
|
+
* create a profile here or proceed as root (root lives ONLY here). Non-
|
|
11
19
|
* interactive (no TTY): default to root (null) and print the recovery
|
|
12
20
|
* instruction to STDERR — never stdout, which the caller may be piping. */
|
|
13
21
|
export declare function selectProfileForCwd(cwd: string, explicitProfile?: string | null): Promise<string | null>;
|
|
@@ -1,11 +1,15 @@
|
|
|
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` >
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// and at any non-inherited `crtr node new`: explicit `--profile` > a profile
|
|
3
|
+
// whose project dir IS cwd exactly (auto) > an interactive menu of profiles
|
|
4
|
+
// covering cwd from a parent/workspace dir + create (MRU auto when headless) >
|
|
5
|
+
// synchronous create-or-root decision when nothing covers. This is the ONLY
|
|
6
|
+
// place that decision runs; front-door.ts/spawn.ts call it, never re-derive it.
|
|
5
7
|
import { existsSync, realpathSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
6
9
|
import { basename, resolve as resolvePath, sep } from 'node:path';
|
|
7
10
|
import { createInterface } from 'node:readline/promises';
|
|
8
11
|
import { listProfiles, loadProfileManifest, updateProfileLastUsed, createProfile, addProfileProject, } from './manifest.js';
|
|
12
|
+
import { stdoutColor } from '../output.js';
|
|
9
13
|
/** Resolve cwd to an absolute, realpath'd form so it compares against the
|
|
10
14
|
* realpath'd project dirs `createProfile`/`addProfileProject` already store
|
|
11
15
|
* (manifest.ts resolves every project dir through `realpathSync`). Falls back
|
|
@@ -52,6 +56,74 @@ function pickMostRecent(entries) {
|
|
|
52
56
|
function isInteractive() {
|
|
53
57
|
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
54
58
|
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Pre-boot prompt presentation. These prompts print to the raw controlling
|
|
61
|
+
// terminal BEFORE pi takes it over (not the attach viewer), so styling is
|
|
62
|
+
// plain ANSI via the house `stdoutColor` helper (NO_COLOR / non-TTY aware) and
|
|
63
|
+
// portable glyphs only — no Nerd Font (the pre-boot terminal may lack it).
|
|
64
|
+
// One shared visual language across all four gates: a bold title + optional
|
|
65
|
+
// dim subtitle, then accent-keyed option rows. We deliberately do NOT echo the
|
|
66
|
+
// user's own cwd back at them (they know where they are) — the only paths
|
|
67
|
+
// worth showing are OTHER dirs: which project root a covering profile owns.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
const dim = stdoutColor.dim;
|
|
70
|
+
const bold = stdoutColor.bold;
|
|
71
|
+
const accent = stdoutColor.cyan;
|
|
72
|
+
/** Selectable key ([1], [c], [r]) — the one thing the eye hunts for. */
|
|
73
|
+
function key(k) {
|
|
74
|
+
return accent(bold(k));
|
|
75
|
+
}
|
|
76
|
+
/** Collapse the home prefix to `~` so project paths read short. */
|
|
77
|
+
function tildify(p) {
|
|
78
|
+
const home = homedir();
|
|
79
|
+
if (p === home)
|
|
80
|
+
return '~';
|
|
81
|
+
if (p.startsWith(home + sep))
|
|
82
|
+
return '~' + p.slice(home.length);
|
|
83
|
+
return p;
|
|
84
|
+
}
|
|
85
|
+
/** Coarse "last used" for disambiguating profiles that otherwise look alike
|
|
86
|
+
* (notably several profiles claiming the SAME dir). null → never used. */
|
|
87
|
+
function relativeUsed(iso) {
|
|
88
|
+
if (iso === null)
|
|
89
|
+
return 'never used';
|
|
90
|
+
const then = Date.parse(iso);
|
|
91
|
+
if (Number.isNaN(then))
|
|
92
|
+
return 'used recently';
|
|
93
|
+
const secs = Math.max(0, Math.floor((Date.now() - then) / 1000));
|
|
94
|
+
if (secs < 60)
|
|
95
|
+
return 'used just now';
|
|
96
|
+
const mins = Math.floor(secs / 60);
|
|
97
|
+
if (mins < 60)
|
|
98
|
+
return `used ${mins}m ago`;
|
|
99
|
+
const hours = Math.floor(mins / 60);
|
|
100
|
+
if (hours < 24)
|
|
101
|
+
return `used ${hours}h ago`;
|
|
102
|
+
const days = Math.floor(hours / 24);
|
|
103
|
+
if (days < 30)
|
|
104
|
+
return `used ${days}d ago`;
|
|
105
|
+
const months = Math.floor(days / 30);
|
|
106
|
+
if (months < 12)
|
|
107
|
+
return `used ${months}mo ago`;
|
|
108
|
+
return `used ${Math.floor(months / 12)}y ago`;
|
|
109
|
+
}
|
|
110
|
+
/** Blank-line-padded bold title with an optional dim subtitle beneath it. No
|
|
111
|
+
* cwd echo — the user already knows their own directory. */
|
|
112
|
+
function writeHeader(title, subtitle) {
|
|
113
|
+
const sub = subtitle !== undefined ? ` ${dim(subtitle)}\n` : '';
|
|
114
|
+
process.stdout.write(`\n ${bold(title)}\n${sub}\n`);
|
|
115
|
+
}
|
|
116
|
+
/** 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
|
+
function optionRow(k, label, detail, isDefault) {
|
|
119
|
+
const det = detail !== '' ? ' ' + dim(detail) : '';
|
|
120
|
+
const def = isDefault ? ' ' + accent('\u00b7 default') : '';
|
|
121
|
+
return ` ${key(k)} ${label}${det}${def}\n`;
|
|
122
|
+
}
|
|
123
|
+
/** The trailing readline prompt: ` › <dim hint> `. */
|
|
124
|
+
function caret(hint) {
|
|
125
|
+
return ` ${accent('\u203a')} ${dim(hint)} `;
|
|
126
|
+
}
|
|
55
127
|
function recoveryMessage(cwd) {
|
|
56
128
|
return (`crtr: no profile covers ${cwd}, and this session is non-interactive — starting without a profile (root).\n` +
|
|
57
129
|
`Run \`crtr profile new --name <name> --project ${cwd}\` to create one, or pass --profile <id-or-name> next time.\n`);
|
|
@@ -75,8 +147,10 @@ function recoveryMessage(cwd) {
|
|
|
75
147
|
async function promptAddDirToProfile(entry, cwd) {
|
|
76
148
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
77
149
|
try {
|
|
78
|
-
|
|
79
|
-
const choice = (await rl.question('
|
|
150
|
+
writeHeader(`Profile "${entry.manifest.name}" doesn't cover this directory`);
|
|
151
|
+
const choice = (await rl.question(caret('add it to the profile? [Y/n]')))
|
|
152
|
+
.trim()
|
|
153
|
+
.toLowerCase();
|
|
80
154
|
if (choice === 'n' || choice === 'no')
|
|
81
155
|
return;
|
|
82
156
|
addProfileProject(entry.profileId, cwd);
|
|
@@ -85,19 +159,77 @@ async function promptAddDirToProfile(entry, cwd) {
|
|
|
85
159
|
rl.close();
|
|
86
160
|
}
|
|
87
161
|
}
|
|
162
|
+
/** Ask for a name (defaulting to cwd's basename) and create a profile pointing
|
|
163
|
+
* at cwd on the SAME open readline — shared by both interactive create paths
|
|
164
|
+
* so the name prompt reads identically whether reached via the covering menu
|
|
165
|
+
* or the no-coverage fallback. */
|
|
166
|
+
async function createProfileHere(rl, cwd) {
|
|
167
|
+
const defaultName = basename(cwd) || 'profile';
|
|
168
|
+
const nameInput = (await rl.question(caret(`name the new profile [${defaultName}]`))).trim();
|
|
169
|
+
const name = nameInput !== '' ? nameInput : defaultName;
|
|
170
|
+
const profileId = createProfile(name, [cwd]).profileId;
|
|
171
|
+
process.stdout.write(` ${accent('\u2713')} ${dim(`created profile "${name}"`)}\n`);
|
|
172
|
+
return profileId;
|
|
173
|
+
}
|
|
88
174
|
async function promptCreateOrRoot(cwd) {
|
|
89
175
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
90
176
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
177
|
+
writeHeader('No profile covers this directory');
|
|
178
|
+
process.stdout.write(optionRow('c', 'create a profile here', '', false));
|
|
179
|
+
process.stdout.write(optionRow('r', dim('run without a profile (root)'), '', true));
|
|
180
|
+
process.stdout.write('\n');
|
|
181
|
+
const choice = (await rl.question(caret('[c/r]'))).trim().toLowerCase();
|
|
95
182
|
if (choice !== 'c' && choice !== 'create')
|
|
96
183
|
return null;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
184
|
+
return await createProfileHere(rl, cwd);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
rl.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Choose among a set of candidate profiles for `cwd`, or create a new one.
|
|
191
|
+
* Reached in two ambiguous cases: MULTIPLE profiles claim `cwd` as their exact
|
|
192
|
+
* project dir (`exact` true), or `cwd` merely sits within one or more profiles'
|
|
193
|
+
* project dirs from a parent/workspace dir (`exact` false). Present them
|
|
194
|
+
* most-recently-used first (choice 1 / the bare-Enter default) plus "create a
|
|
195
|
+
* new profile here", and block until answered. Root is deliberately NOT offered
|
|
196
|
+
* here: a claimed/covered directory always has at least a covering profile as a
|
|
197
|
+
* sane identity, so `[r]oot` belongs only to the no-coverage prompt
|
|
198
|
+
* (`promptCreateOrRoot`). Same raw-readline / boot-gating rationale. */
|
|
199
|
+
async function promptPickProfileOrCreate(candidates, cwd, exact) {
|
|
200
|
+
// MRU-first so the most-recently-used candidate is choice 1 / the bare-Enter
|
|
201
|
+
// default — the same profile the non-interactive path auto-picks.
|
|
202
|
+
const ordered = [...candidates].sort((a, b) => isMoreRecent(a.manifest.last_used_at, b.manifest.last_used_at) ? -1 : 1);
|
|
203
|
+
// Pad names to a shared column so the dim detail lines up down the menu.
|
|
204
|
+
const nameWidth = Math.max(...ordered.map((e) => e.manifest.name.length));
|
|
205
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
206
|
+
try {
|
|
207
|
+
writeHeader(exact ? 'Choose a profile' : 'Select a profile', exact
|
|
208
|
+
? 'Several profiles claim this directory as their project'
|
|
209
|
+
: 'This directory falls under existing profile(s)');
|
|
210
|
+
ordered.forEach((entry, i) => {
|
|
211
|
+
// For exact ties the covered path is identical, so surface last-used to
|
|
212
|
+
// tell them apart; for parent/workspace coverage, WHICH dir covers cwd
|
|
213
|
+
// is the useful differentiator.
|
|
214
|
+
const detail = exact
|
|
215
|
+
? relativeUsed(entry.manifest.last_used_at)
|
|
216
|
+
: `covers ${tildify(entry.manifest.projects.find((p) => projectCovers(cwd, p)) ?? entry.manifest.projects[0])}`;
|
|
217
|
+
const label = bold(entry.manifest.name.padEnd(nameWidth));
|
|
218
|
+
process.stdout.write(optionRow(String(i + 1), label, detail, i === 0));
|
|
219
|
+
});
|
|
220
|
+
process.stdout.write(optionRow('c', dim('create a new profile here'), '', false));
|
|
221
|
+
process.stdout.write('\n');
|
|
222
|
+
const answer = (await rl.question(caret('[1]'))).trim().toLowerCase();
|
|
223
|
+
if (answer === 'c' || answer === 'create')
|
|
224
|
+
return await createProfileHere(rl, cwd);
|
|
225
|
+
if (answer === '')
|
|
226
|
+
return ordered[0].profileId;
|
|
227
|
+
const n = Number.parseInt(answer, 10);
|
|
228
|
+
if (Number.isInteger(n) && n >= 1 && n <= ordered.length)
|
|
229
|
+
return ordered[n - 1].profileId;
|
|
230
|
+
// Unrecognized input → the safe default (the MRU candidate); never loop,
|
|
231
|
+
// since this prompt is gating pi's boot on the shared TTY.
|
|
232
|
+
return ordered[0].profileId;
|
|
101
233
|
}
|
|
102
234
|
finally {
|
|
103
235
|
rl.close();
|
|
@@ -109,10 +241,18 @@ async function promptCreateOrRoot(cwd) {
|
|
|
109
241
|
* its manifest does not already cover `cwd` and the session is interactive,
|
|
110
242
|
* offer to add `cwd` to its purview (default yes). Bump `last_used_at`,
|
|
111
243
|
* return the id.
|
|
112
|
-
* 2. Else,
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
244
|
+
* 2. Else, if EXACTLY ONE profile's project dir IS `cwd` (you're at a project
|
|
245
|
+
* root, unambiguously one owner), auto-pick it with no prompt — the
|
|
246
|
+
* strongest signal. If SEVERAL profiles claim `cwd` exactly, it's genuinely
|
|
247
|
+
* ambiguous: interactive → menu among them + create; headless → MRU.
|
|
248
|
+
* 3. Else, among profiles whose project list otherwise COVERS `cwd` (cwd sits
|
|
249
|
+
* in a subdir of, or a workspace above, one of their project dirs):
|
|
250
|
+
* - Interactive: prompt with a MENU of those covering profiles + "create
|
|
251
|
+
* a new profile here", BEFORE pi boots (no root option — the directory
|
|
252
|
+
* is covered). Default is the MRU covering profile.
|
|
253
|
+
* - Non-interactive: auto-pick the MRU covering profile, no prompt.
|
|
254
|
+
* 4. Else — nothing covers cwd walking all the way up. Interactive: prompt to
|
|
255
|
+
* create a profile here or proceed as root (root lives ONLY here). Non-
|
|
116
256
|
* interactive (no TTY): default to root (null) and print the recovery
|
|
117
257
|
* instruction to STDERR — never stdout, which the caller may be piping. */
|
|
118
258
|
export async function selectProfileForCwd(cwd, explicitProfile) {
|
|
@@ -128,14 +268,34 @@ export async function selectProfileForCwd(cwd, explicitProfile) {
|
|
|
128
268
|
return entry.profileId;
|
|
129
269
|
}
|
|
130
270
|
const covering = listProfiles().filter((p) => profileCoversCwd(p, resolvedCwd));
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
271
|
+
const exact = covering.filter((p) => p.manifest.projects.some((proj) => proj === resolvedCwd));
|
|
272
|
+
// Exactly one profile's project dir IS cwd — unambiguous project root, use it
|
|
273
|
+
// silently, never a prompt. (Several exact claims fall through to the menu.)
|
|
274
|
+
if (exact.length === 1) {
|
|
275
|
+
updateProfileLastUsed(exact[0].profileId);
|
|
276
|
+
return exact[0].profileId;
|
|
135
277
|
}
|
|
278
|
+
// Non-interactive: cannot prompt. Prefer an exact claimant, else any covering
|
|
279
|
+
// profile (both MRU), else root.
|
|
136
280
|
if (!isInteractive()) {
|
|
281
|
+
const pool = exact.length > 0 ? exact : covering;
|
|
282
|
+
if (pool.length > 0) {
|
|
283
|
+
const winner = pickMostRecent(pool);
|
|
284
|
+
updateProfileLastUsed(winner.profileId);
|
|
285
|
+
return winner.profileId;
|
|
286
|
+
}
|
|
137
287
|
process.stderr.write(recoveryMessage(resolvedCwd));
|
|
138
288
|
return null;
|
|
139
289
|
}
|
|
140
|
-
|
|
290
|
+
// Interactive. Several exact claimants → choose among them. Else covered from
|
|
291
|
+
// a parent/workspace dir → menu of covering profiles. Else nothing covers →
|
|
292
|
+
// the create-or-root prompt (root is only ever offered there).
|
|
293
|
+
const chosen = exact.length > 1
|
|
294
|
+
? await promptPickProfileOrCreate(exact, resolvedCwd, true)
|
|
295
|
+
: covering.length > 0
|
|
296
|
+
? await promptPickProfileOrCreate(covering, resolvedCwd, false)
|
|
297
|
+
: await promptCreateOrRoot(resolvedCwd);
|
|
298
|
+
if (chosen !== null)
|
|
299
|
+
updateProfileLastUsed(chosen);
|
|
300
|
+
return chosen;
|
|
141
301
|
}
|
|
@@ -448,7 +448,7 @@ export function buildContextBearings(nodeId) {
|
|
|
448
448
|
bearings.push('', profileSection);
|
|
449
449
|
const wt = node?.managed_worktree;
|
|
450
450
|
if (wt?.state === 'open') {
|
|
451
|
-
bearings.push('', '## Your managed worktree', `Branch: \`${wt.branch}\`\nWorktree: \`${wt.path}\`\nBase: \`${wt.base_ref}\` @ \`${wt.base_sha}\`\nCommit work here. Land it with \`crtr worktree close\` before finishing; \`crtr push final\` is blocked until this worktree is closed.`);
|
|
451
|
+
bearings.push('', '## Your managed worktree', `Branch: \`${wt.branch}\`\nWorktree: \`${wt.path}\`\nBase: \`${wt.base_ref}\` @ \`${wt.base_sha}\`\nCommit work here. Land it with \`crtr node worktree close\` before finishing; \`crtr push final\` is blocked until this worktree is closed.`);
|
|
452
452
|
}
|
|
453
453
|
// Orchestrator-only: the across-cycles framing (a terminal has no future cycle).
|
|
454
454
|
if (node?.mode === 'orchestrator')
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// EXCLUSIVELY owns, walking DOWN the subscribes_to spine (subscriptionsOf = a
|
|
5
5
|
// node's reports/children). Nothing is deleted: pi_session_id, the canvas
|
|
6
6
|
// edges, and all on-disk state persist, so any closed node can later be revived
|
|
7
|
-
// (`crtr
|
|
7
|
+
// (`crtr node lifecycle revive` / focus → `pi --session <id>`). A close is a pause, not a reap.
|
|
8
8
|
//
|
|
9
9
|
// Per node, in this order — the order matters twice:
|
|
10
10
|
//
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The revive kickoff — the message auto-injected as a node's first turn when it
|
|
2
|
-
// comes back FRESH (a refresh-yield, or `
|
|
2
|
+
// comes back FRESH (a refresh-yield, or `node lifecycle revive --fresh`). The node's
|
|
3
3
|
// in-memory context is gone, so this message IS its bearings: everything is
|
|
4
4
|
// read from disk and framed so the node can rebuild and continue without a
|
|
5
5
|
// round-trip. Resuming a saved conversation needs none of this (the
|
|
@@ -140,7 +140,7 @@ function reportKind(filename) {
|
|
|
140
140
|
/** Condensed report-history index for each ACTIVE subscription of `nodeId` —
|
|
141
141
|
* independent of publisher liveness, so a finished worker's history still
|
|
142
142
|
* surfaces for catch-up. Each source carries only its count and newest report
|
|
143
|
-
* ref; older refs stay one command away via `
|
|
143
|
+
* ref; older refs stay one command away via `node inspect artifacts`. */
|
|
144
144
|
function reportHistorySources(nodeId) {
|
|
145
145
|
const out = [];
|
|
146
146
|
for (const sub of subscriptionsOf(nodeId).filter((s) => s.active)) {
|
|
@@ -201,7 +201,7 @@ function feedBlock(nodeId, unreadDigest) {
|
|
|
201
201
|
lines.push('', `Report history on disk (collapsed): ${total} report${total === 1 ? '' : 's'} across ${history.length} subscribed node${history.length === 1 ? '' : 's'}. This is a catch-up index for reports your cursor already passed; read only what matters.`, ...visible.map((h) => {
|
|
202
202
|
const ref = `${h.nodeId}:reports/${h.latest}`;
|
|
203
203
|
return ` - ${h.name} (${h.nodeId}): ${h.count} report${h.count === 1 ? '' : 's'}; latest ${reportKind(h.latest)}: ${ref}`;
|
|
204
|
-
}), ...(hidden > 0 ? [` - … ${hidden} more subscribed node${hidden === 1 ? '' : 's'} with reports hidden`] : []), 'Expand: `crtr canvas history read <ref>` for one listed report; `crtr
|
|
204
|
+
}), ...(hidden > 0 ? [` - … ${hidden} more subscribed node${hidden === 1 ? '' : 's'} with reports hidden`] : []), 'Expand: `crtr canvas history read <ref>` for one listed report; `crtr node inspect artifacts <node-id> --type report` for that node\'s full report list; `crtr node inspect artifacts <node-id> --type inbox` for cursor-independent inbox history including full message bodies.');
|
|
205
205
|
}
|
|
206
206
|
return `<feed>\n${lines.join('\n')}\n</feed>`;
|
|
207
207
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The revive primitive — restores a node to active status by relaunching its
|
|
2
2
|
// broker engine. Used by both the supervisor daemon (on crash/refresh
|
|
3
|
-
// detection) and the explicit `crtr
|
|
3
|
+
// detection) and the explicit `crtr node lifecycle revive` command (or `crtr canvas revive --all`).
|
|
4
4
|
//
|
|
5
5
|
// A revive replays the node's persisted LaunchSpec + cwd (the canonical recipe)
|
|
6
6
|
// and launches the headless broker host (the only host after the
|
|
@@ -87,7 +87,7 @@ export function reviveNode(nodeId, opts) {
|
|
|
87
87
|
// right below into believing a dead broker is still alive. Idempotent and
|
|
88
88
|
// cheap once reconciled (a single small file read) except the one time it
|
|
89
89
|
// actually matters, so it's safe to call on every revive regardless of
|
|
90
|
-
// entry point (daemon tick, or a direct/manual `
|
|
90
|
+
// entry point (daemon tick, or a direct/manual `node lifecycle revive`).
|
|
91
91
|
reconcileBootLiveness();
|
|
92
92
|
// Double-revive guard: the broker's isAlive IS isPidAlive(pi_pid), so a node
|
|
93
93
|
// whose broker pid is still running was already revived by another path —
|
|
@@ -31,7 +31,7 @@ function makeNoBrokerError(target) {
|
|
|
31
31
|
display: {
|
|
32
32
|
headline: `node ${target} has no running broker`,
|
|
33
33
|
explanation: 'The stream transport connects only to an already-running broker.',
|
|
34
|
-
nextStep: `Run crtr node focus ${target} or crtr
|
|
34
|
+
nextStep: `Run crtr node focus ${target} or crtr node lifecycle revive ${target}, then reconnect.`,
|
|
35
35
|
level: 'error',
|
|
36
36
|
blocking: false,
|
|
37
37
|
},
|
package/dist/core/worktree.js
CHANGED
|
@@ -86,22 +86,22 @@ export function closeManagedWorktree(nodeId) {
|
|
|
86
86
|
throw new WorktreeError('worktree_missing', `managed worktree path is missing: ${wt.path}`, 'Ask the human with `crtr human ask` before changing node state; the checkout disappeared outside crouter.');
|
|
87
87
|
}
|
|
88
88
|
if (isRebaseInProgress(wt.path)) {
|
|
89
|
-
throw new WorktreeError('rebase_in_progress', 'this managed worktree is mid-rebase', `Resolve conflicts in ${wt.path}, run \`git rebase --continue\` there, then rerun \`crtr worktree close\`.`);
|
|
89
|
+
throw new WorktreeError('rebase_in_progress', 'this managed worktree is mid-rebase', `Resolve conflicts in ${wt.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`.`);
|
|
90
90
|
}
|
|
91
91
|
const dirty = statusPorcelain(wt.path);
|
|
92
92
|
if (dirty !== '') {
|
|
93
|
-
throw new WorktreeError('dirty_worktree', 'managed worktree has uncommitted changes', `Commit the work in ${wt.path} (or ask the human with \`crtr human ask\` if you do not know whether to keep it), then rerun \`crtr worktree close\`.`);
|
|
93
|
+
throw new WorktreeError('dirty_worktree', 'managed worktree has uncommitted changes', `Commit the work in ${wt.path} (or ask the human with \`crtr human ask\` if you do not know whether to keep it), then rerun \`crtr node worktree close\`.`);
|
|
94
94
|
}
|
|
95
95
|
const branch = currentBranch(wt.path);
|
|
96
96
|
if (branch !== wt.branch) {
|
|
97
97
|
const found = branch === 'HEAD' ? 'detached HEAD' : branch;
|
|
98
|
-
throw new WorktreeError('wrong_branch', `managed worktree is not on its recorded branch (expected ${wt.branch}, found ${found})`, `Check out ${wt.branch} in ${wt.path} before closing, or ask the human with \`crtr human ask\` if you are unsure why it changed, then rerun \`crtr worktree close\`.`);
|
|
98
|
+
throw new WorktreeError('wrong_branch', `managed worktree is not on its recorded branch (expected ${wt.branch}, found ${found})`, `Check out ${wt.branch} in ${wt.path} before closing, or ask the human with \`crtr human ask\` if you are unsure why it changed, then rerun \`crtr node worktree close\`.`);
|
|
99
99
|
}
|
|
100
100
|
runGit(wt.path, ['fetch', 'origin', 'main'], 'fetch_failed', 'Network/remote state blocked landing. Ask the human with `crtr human ask` if this is not a transient git/network issue.');
|
|
101
101
|
const rebase = gitSync(['rebase', 'origin/main'], wt.path);
|
|
102
102
|
if (rebase.status !== 0) {
|
|
103
103
|
const detail = (rebase.stderr.trim() || rebase.stdout.trim()).trim();
|
|
104
|
-
throw new WorktreeError('rebase_failed', 'rebase onto origin/main did not complete', `Resolve conflicts in ${wt.path}, run \`git rebase --continue\` there, then rerun \`crtr worktree close\`. If the blocker is not mechanical, ask the human with \`crtr human ask\`.`, detail);
|
|
104
|
+
throw new WorktreeError('rebase_failed', 'rebase onto origin/main did not complete', `Resolve conflicts in ${wt.path}, run \`git rebase --continue\` there, then rerun \`crtr node worktree close\`. If the blocker is not mechanical, ask the human with \`crtr human ask\`.`, detail);
|
|
105
105
|
}
|
|
106
106
|
const push = gitSync(['push', 'origin', 'HEAD:main'], wt.path);
|
|
107
107
|
if (push.status !== 0) {
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
// out-of-band launch.
|
|
44
44
|
// dead-pid crash path grace-revives RESUME on the saved session (or, for a
|
|
45
45
|
// crash mid-cycle, retries AS a cycle — see cycle_pending above) — the manual
|
|
46
|
-
// recovery (kill broker +
|
|
46
|
+
// recovery (kill broker + node lifecycle revive) that fixes these by hand.
|
|
47
47
|
//
|
|
48
48
|
// Single-instance guarantee
|
|
49
49
|
// A PID file prevents double-runs. On start, if the file exists and the
|
|
@@ -235,7 +235,7 @@ function handleYieldStall(row, pid, now) {
|
|
|
235
235
|
// • no descendants (the broker pid stands alone — e.g. the engine itself
|
|
236
236
|
// stalled after a model_change, with nothing to kill) → the daemon
|
|
237
237
|
// performs the ONLY remaining remediation itself: SIGTERM the broker, the
|
|
238
|
-
// same on-demand kick `
|
|
238
|
+
// same on-demand kick `node lifecycle revive --now` does, so its own dead-pid
|
|
239
239
|
// crash-grace path (REVIVE_GRACE_MS) resumes it on the saved session with
|
|
240
240
|
// no lost context.
|
|
241
241
|
//
|
|
@@ -451,7 +451,7 @@ function handleWedgeDetection(id, pid, now) {
|
|
|
451
451
|
else {
|
|
452
452
|
// No descendant to kill — the engine itself stalled (e.g. mid model_change)
|
|
453
453
|
// with nothing a human could kill. The daemon performs the same kick
|
|
454
|
-
// `
|
|
454
|
+
// `node lifecycle revive --now` does on demand: SIGTERM the broker, then its own
|
|
455
455
|
// dead-pid crash-grace path (REVIVE_GRACE_MS) resumes it on the saved
|
|
456
456
|
// session (issue #119).
|
|
457
457
|
label =
|
|
@@ -23,7 +23,7 @@ function makeNoBrokerError(target) {
|
|
|
23
23
|
display: {
|
|
24
24
|
headline: `node ${target} is asleep or has no running broker`,
|
|
25
25
|
explanation: 'This browser stream connects only to an already-running local broker.',
|
|
26
|
-
nextStep: `Run crtr node focus ${target} or crtr
|
|
26
|
+
nextStep: `Run crtr node focus ${target} or crtr node lifecycle revive ${target}, then reconnect.`,
|
|
27
27
|
level: 'error',
|
|
28
28
|
blocking: false,
|
|
29
29
|
},
|