@north-light/crouter 0.3.230 → 0.3.232
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/api/client.d.ts +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/canvas.d.ts +10 -0
- package/dist/api/dto/common.d.ts +1 -1
- package/dist/api/dto/health.d.ts +2 -1
- package/dist/api/dto/lifecycle.d.ts +3 -4
- package/dist/api/dto/messages.d.ts +5 -4
- package/dist/api/dto/nodes.d.ts +2 -0
- package/dist/api/dto/profiles.d.ts +5 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +8 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +1 -1
- package/dist/builtin-memory/01-spine/00-has-manager.md +1 -1
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +5 -0
- package/dist/builtin-memory/02-turn-lifecycle/02-resident.md +5 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +2 -2
- package/dist/builtin-memory/insights/capture.md +3 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +6 -1
- package/dist/clients/attach/render/diagram.js +13 -5
- package/dist/clients/attach/render/page-block.d.ts +0 -1
- package/dist/clients/attach/render/page-block.js +4 -57
- package/dist/clients/attach/viewer.js +570 -563
- package/dist/clients/inbox/__tests__/integration/inbox-controller.test.js +9 -0
- package/dist/clients/inbox/__tests__/integration/mount-panel.test.js +62 -1
- package/dist/clients/inbox/controller.d.ts +10 -0
- package/dist/clients/inbox/controller.js +56 -12
- package/dist/clients/inbox/tui/input.js +38 -10
- package/dist/clients/inbox/tui/page-body.d.ts +10 -0
- package/dist/clients/inbox/tui/page-body.js +66 -0
- package/dist/clients/inbox/tui/panel.js +6 -4
- package/dist/clients/inbox/tui/render.js +89 -17
- package/dist/clients/inbox/tui/types.d.ts +4 -4
- package/dist/commands/__tests__/human.test.js +18 -3
- package/dist/commands/__tests__/node-message.test.js +3 -3
- package/dist/commands/api-client.js +1 -7
- package/dist/commands/canvas-config.js +6 -14
- package/dist/commands/canvas-use.js +4 -6
- package/dist/commands/cron.js +16 -22
- package/dist/commands/human/prompts.d.ts +1 -1
- package/dist/commands/human/prompts.js +118 -112
- package/dist/commands/human/request.js +13 -14
- package/dist/commands/human/review.js +3 -4
- package/dist/commands/human/shared.d.ts +6 -0
- package/dist/commands/human/shared.js +43 -5
- package/dist/commands/human.js +1 -1
- package/dist/commands/memory/delete.js +4 -6
- package/dist/commands/memory/edit.js +0 -4
- package/dist/commands/memory/move.js +3 -5
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +9 -5
- package/dist/commands/memory/write.js +60 -29
- package/dist/commands/memory.js +1 -1
- package/dist/commands/node/bash.js +6 -9
- package/dist/commands/node/create.js +89 -22
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +30 -27
- package/dist/commands/node/message.js +24 -45
- package/dist/commands/node/subscription.js +6 -15
- package/dist/commands/node/wait.js +2 -3
- package/dist/commands/node-lifecycle-revive.js +1 -12
- package/dist/commands/pkg/browse/actions.js +2 -3
- package/dist/commands/pkg/market-manage.js +2 -5
- package/dist/commands/pkg/plugin-manage.js +7 -8
- package/dist/commands/profile/default.js +5 -5
- package/dist/commands/profile/delete.js +1 -1
- package/dist/commands/profile/env.js +9 -15
- package/dist/commands/profile/kind.js +3 -7
- package/dist/commands/profile/meta.js +3 -5
- package/dist/commands/profile/new.js +0 -6
- package/dist/commands/profile/pause.js +4 -8
- package/dist/commands/profile/project.js +5 -9
- package/dist/commands/profile/rename.js +3 -7
- package/dist/commands/profile/show.js +3 -3
- package/dist/commands/profile.js +4 -3
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/config.js +3 -4
- package/dist/commands/sys/support/prepare.js +8 -4
- package/dist/commands/sys/support/submit.js +2 -3
- package/dist/commands/sys/sync-deps.js +1 -9
- package/dist/commands/sys/sync-project-guidance.js +1 -7
- package/dist/commands/sys/sync-skills.js +1 -11
- package/dist/core/__tests__/cron-node-sink-parked-root.test.d.ts +1 -0
- package/dist/core/__tests__/cron-node-sink-parked-root.test.js +147 -0
- package/dist/core/__tests__/history-inbox.test.js +11 -1
- package/dist/core/__tests__/human-deliver.test.js +2 -1
- package/dist/core/__tests__/integration/command-plugins.test.js +0 -1
- package/dist/core/__tests__/integration/deferred-no-wake.test.js +0 -1
- package/dist/core/__tests__/lifecycle.test.js +30 -2
- package/dist/core/__tests__/revive-parked-fresh.test.d.ts +1 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +109 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +32 -5
- package/dist/core/canvas/attention.d.ts +2 -0
- package/dist/core/canvas/attention.js +25 -18
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/history.js +20 -2
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command.js +33 -8
- package/dist/core/help.d.ts +28 -2
- package/dist/core/help.js +46 -10
- package/dist/core/human/__tests__/page-html-markdown.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-html-markdown.test.js +48 -0
- package/dist/core/human/component-docs.js +4 -4
- package/dist/core/human/page-html-markdown.d.ts +8 -0
- package/dist/core/human/page-html-markdown.js +260 -0
- package/dist/core/memory/lint.d.ts +15 -0
- package/dist/core/memory/lint.js +150 -90
- package/dist/core/profiles/__tests__/fuzzy-match.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/fuzzy-match.test.js +51 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +14 -7
- package/dist/core/profiles/manifest.js +62 -12
- package/dist/core/profiles/select.d.ts +3 -1
- package/dist/core/profiles/select.js +5 -3
- package/dist/core/profiles/state-block.js +4 -3
- package/dist/core/runtime/boot-root.d.ts +3 -2
- package/dist/core/runtime/canvas-extensions.d.ts +7 -1
- package/dist/core/runtime/canvas-extensions.js +8 -1
- package/dist/core/runtime/lifecycle.d.ts +11 -2
- package/dist/core/runtime/lifecycle.js +15 -2
- package/dist/core/runtime/model-selection.d.ts +4 -0
- package/dist/core/runtime/model-selection.js +5 -0
- package/dist/core/runtime/nodes.js +5 -0
- package/dist/core/runtime/reopen.d.ts +6 -0
- package/dist/core/runtime/reopen.js +12 -1
- package/dist/core/runtime/revive.d.ts +6 -0
- package/dist/core/runtime/revive.js +22 -2
- package/dist/core/runtime/spawn.d.ts +5 -2
- package/dist/core/runtime/spawn.js +18 -32
- package/dist/core/runtime/structured-output.d.ts +6 -0
- package/dist/core/runtime/structured-output.js +6 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-command.test.js +89 -0
- package/dist/core/substrate/on-read.js +2 -1
- package/dist/core/substrate/surface-match.js +164 -12
- package/dist/core/termrender/version.d.ts +1 -1
- package/dist/core/termrender/version.js +1 -1
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/api/__tests__/broker-settle-park.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/broker-settle-park.test.js +102 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +188 -0
- package/dist/daemon/api/__tests__/node-create-description.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +83 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/profile-metadata-route.test.js +92 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/reopen-delivery.test.js +173 -0
- package/dist/daemon/api/handlers/broker-ops.js +21 -0
- package/dist/daemon/api/handlers/canvas.js +10 -0
- package/dist/daemon/api/handlers/messages.js +25 -16
- package/dist/daemon/api/handlers/nodes.js +5 -0
- package/dist/daemon/api/handlers/profiles.js +22 -1
- package/dist/daemon/cron-run.js +19 -1
- package/dist/daemon/manage.d.ts +16 -1
- package/dist/daemon/manage.js +20 -1
- package/dist/daemon/park-pending.d.ts +13 -0
- package/dist/daemon/park-pending.js +42 -0
- package/dist/daemon/reconcilers/broker-supervision.d.ts +13 -0
- package/dist/daemon/reconcilers/broker-supervision.js +139 -21
- package/dist/daemon/reconcilers/live-obligation.d.ts +10 -4
- package/dist/daemon/reconcilers/live-obligation.js +7 -3
- package/dist/daemon/reconcilers/storage-maintenance.d.ts +0 -4
- package/dist/daemon/reconcilers/storage-maintenance.js +1 -33
- package/dist/pi-extensions/canvas-prompt-scrub.d.ts +13 -0
- package/dist/pi-extensions/canvas-prompt-scrub.js +53 -0
- package/dist/shared/generated-context.d.ts +7 -0
- package/dist/shared/generated-context.js +11 -0
- package/package.json +4 -4
- package/runtime.lock.json +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +0 -47
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Fuzzy matching for a user-TYPED profile operand. Pure — no fs, no manifest
|
|
2
|
+
// reads — so the resolution policy is testable on its own and `manifest.ts`
|
|
3
|
+
// stays the sole owner of path safety and manifest IO.
|
|
4
|
+
//
|
|
5
|
+
// This exists because nobody remembers a profile's exact registered name, let
|
|
6
|
+
// alone its `<slug>-<8hex>` id. `crtr --profile crout` should run the `crouter`
|
|
7
|
+
// profile instead of failing with a list the user then has to read and retype.
|
|
8
|
+
/** Case-, space-, and punctuation-insensitive comparison key. "My Profile",
|
|
9
|
+
* "my-profile", and "myprofile" all collapse to the same key, so the common
|
|
10
|
+
* near-miss (wrong separator, wrong case) resolves in the first tier. */
|
|
11
|
+
function normalize(value) {
|
|
12
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
13
|
+
}
|
|
14
|
+
/** The generated id's `-<8 hex>` suffix carries no meaning to a human typing a
|
|
15
|
+
* profile, so match against the slug alone as well as the whole id. */
|
|
16
|
+
function idSlug(profileId) {
|
|
17
|
+
return profileId.replace(/-[0-9a-f]{8}$/, '');
|
|
18
|
+
}
|
|
19
|
+
/** Every string a human might plausibly have been aiming at for one profile. */
|
|
20
|
+
function haystacks(candidate) {
|
|
21
|
+
const keys = [normalize(candidate.name), normalize(idSlug(candidate.profileId)), normalize(candidate.profileId)];
|
|
22
|
+
return keys.filter((key, index) => key !== '' && keys.indexOf(key) === index);
|
|
23
|
+
}
|
|
24
|
+
function levenshtein(a, b) {
|
|
25
|
+
if (a === b)
|
|
26
|
+
return 0;
|
|
27
|
+
if (a.length === 0)
|
|
28
|
+
return b.length;
|
|
29
|
+
if (b.length === 0)
|
|
30
|
+
return a.length;
|
|
31
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
32
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
33
|
+
const current = [i];
|
|
34
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
35
|
+
const substitution = previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
36
|
+
current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, substitution);
|
|
37
|
+
}
|
|
38
|
+
previous = current;
|
|
39
|
+
}
|
|
40
|
+
return previous[b.length];
|
|
41
|
+
}
|
|
42
|
+
/** Tiers, strongest first. They are ordered by how confidently a human meant
|
|
43
|
+
* this profile, NOT by string similarity — a prefix the user stopped typing is
|
|
44
|
+
* a stronger signal than a one-character edit that happens to score well. */
|
|
45
|
+
const TIER_NORMALIZED_EQUAL = 0;
|
|
46
|
+
const TIER_PREFIX = 1;
|
|
47
|
+
const TIER_CONTAINS = 2;
|
|
48
|
+
const TIER_TYPO = 3;
|
|
49
|
+
function scoreOne(needle, candidate) {
|
|
50
|
+
let best = null;
|
|
51
|
+
for (const hay of haystacks(candidate)) {
|
|
52
|
+
const distance = levenshtein(needle, hay);
|
|
53
|
+
let tier = null;
|
|
54
|
+
if (needle === hay)
|
|
55
|
+
tier = TIER_NORMALIZED_EQUAL;
|
|
56
|
+
else if (hay.startsWith(needle))
|
|
57
|
+
tier = TIER_PREFIX;
|
|
58
|
+
else if (hay.includes(needle) || needle.includes(hay))
|
|
59
|
+
tier = TIER_CONTAINS;
|
|
60
|
+
// A typo budget that scales with length: one edit for a short name, more
|
|
61
|
+
// for a long one. Without the floor, a 3-character name would tolerate
|
|
62
|
+
// nothing; without the scaling, two unrelated long names would collide.
|
|
63
|
+
else if (distance <= Math.max(1, Math.floor(hay.length / 3)))
|
|
64
|
+
tier = TIER_TYPO;
|
|
65
|
+
if (tier === null)
|
|
66
|
+
continue;
|
|
67
|
+
if (best === null || tier < best.tier || (tier === best.tier && distance < best.distance)) {
|
|
68
|
+
best = { candidate, tier, distance };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return best;
|
|
72
|
+
}
|
|
73
|
+
/** Resolve a typed operand against the profiles on this host. Returns a single
|
|
74
|
+
* match only when it is unambiguously the best one: the strongest tier any
|
|
75
|
+
* candidate reached, and within it the single lowest edit distance. A genuine
|
|
76
|
+
* tie is reported rather than guessed, because silently running under the
|
|
77
|
+
* wrong profile is worse than one more prompt. */
|
|
78
|
+
export function fuzzyMatchProfile(operand, candidates) {
|
|
79
|
+
const needle = normalize(operand);
|
|
80
|
+
if (needle === '')
|
|
81
|
+
return { match: null, tied: [] };
|
|
82
|
+
const scored = candidates.map((c) => scoreOne(needle, c)).filter((s) => s !== null);
|
|
83
|
+
if (scored.length === 0)
|
|
84
|
+
return { match: null, tied: [] };
|
|
85
|
+
const bestTier = Math.min(...scored.map((s) => s.tier));
|
|
86
|
+
const inTier = scored.filter((s) => s.tier === bestTier);
|
|
87
|
+
const bestDistance = Math.min(...inTier.map((s) => s.distance));
|
|
88
|
+
const winners = inTier.filter((s) => s.distance === bestDistance);
|
|
89
|
+
if (winners.length === 1)
|
|
90
|
+
return { match: winners[0].candidate, tied: [] };
|
|
91
|
+
return { match: null, tied: winners.map((s) => s.candidate) };
|
|
92
|
+
}
|
|
@@ -30,17 +30,24 @@ export interface ProfileEntry {
|
|
|
30
30
|
* generated id shape, or whose `profile.json` is missing/corrupt, is skipped
|
|
31
31
|
* rather than crashing the whole listing. */
|
|
32
32
|
export declare function listProfiles(): ProfileEntry[];
|
|
33
|
-
/** Resolve a CLI operand to its profile — exact id first (checked against the
|
|
34
|
-
* actually-enumerated dirs, never a blind join), then a unique manifest
|
|
35
|
-
* `name` match. Ambiguous names fail listing every matching id; no match
|
|
36
|
-
* fails naming `profile list`/`profile new` as recovery. This is the ONLY
|
|
37
|
-
* function every command leaf and runtime consumer should
|
|
38
|
-
* call to turn a `<profile>` operand or `CRTR_PROFILE_ID` into a concrete,
|
|
39
|
-
* safe profile id. */
|
|
40
33
|
/** Read one exact profile id without falling back to manifest-name matching.
|
|
41
34
|
* Used at persistence gates that already hold a resolved durable identity. */
|
|
42
35
|
export declare function loadExactProfileManifest(profileId: string): ProfileEntry;
|
|
43
36
|
export declare function loadProfileManifest(profileIdOrName: string): ProfileEntry;
|
|
37
|
+
/** Resolve a profile the USER TYPED — a `--profile` flag or a `<profile>`
|
|
38
|
+
* operand. Exact id or name wins outright; only when nothing matches exactly
|
|
39
|
+
* does this fall back to fuzzy matching on name and id slug, so a typed
|
|
40
|
+
* `crout`, `Crouter`, or `cruoter` reaches the `crouter` profile instead of
|
|
41
|
+
* failing with a list the user has to read and retype. A fuzzy hit is
|
|
42
|
+
* announced on STDERR (never stdout, which callers pipe) so the user always
|
|
43
|
+
* knows which profile actually ran.
|
|
44
|
+
*
|
|
45
|
+
* NEVER call this with a STORED `profile_id` (a node's `meta.profile_id`,
|
|
46
|
+
* `CRTR_PROFILE_ID`, a pinned default). A durable id that no longer resolves
|
|
47
|
+
* means the profile was deleted — the correct answer there is the not-found
|
|
48
|
+
* those callers already handle, not the nearest surviving profile. Use
|
|
49
|
+
* `loadProfileManifest` (or `loadExactProfileManifest`) for those. */
|
|
50
|
+
export declare function resolveProfileOperand(operand: string): ProfileEntry;
|
|
44
51
|
/** Hold the per-profile manifest lock for the duration of `fn`. ALL manifest
|
|
45
52
|
* mutations (create/rename/add-project/remove-project/delete/last-used)
|
|
46
53
|
* below run inside this. */
|
|
@@ -14,6 +14,7 @@ import { userScopeRoot, resetScopeCache } from '../scope.js';
|
|
|
14
14
|
import { ensureDir, nowIso } from '../fs-utils.js';
|
|
15
15
|
import { usage, notFound, ambiguous, general } from '../errors.js';
|
|
16
16
|
import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
|
|
17
|
+
import { fuzzyMatchProfile } from './fuzzy-match.js';
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Path safety — the only code in the tree that builds a path under the
|
|
19
20
|
// profiles root. Every other module reaches profile paths through this file.
|
|
@@ -197,13 +198,15 @@ export function listProfiles() {
|
|
|
197
198
|
}
|
|
198
199
|
return out;
|
|
199
200
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
201
|
+
// Three resolvers, in descending tolerance. `resolveProfileOperand` is what a
|
|
202
|
+
// command leaf calls for a value the USER TYPED: exact id, exact name, then a
|
|
203
|
+
// fuzzy fallback. `loadProfileManifest` is the strict middle — exact id or
|
|
204
|
+
// exact name, no guessing — and is what every runtime consumer holding a
|
|
205
|
+
// stored `profile_id` or `CRTR_PROFILE_ID` calls. `loadExactProfileManifest`
|
|
206
|
+
// skips name matching entirely, for persistence gates already holding a
|
|
207
|
+
// resolved durable identity. All three check the id shape against the
|
|
208
|
+
// actually-enumerated dirs rather than joining a raw operand onto the root, so
|
|
209
|
+
// no caller can traverse out of the profiles root.
|
|
207
210
|
/** Read one exact profile id without falling back to manifest-name matching.
|
|
208
211
|
* Used at persistence gates that already hold a resolved durable identity. */
|
|
209
212
|
export function loadExactProfileManifest(profileId) {
|
|
@@ -216,8 +219,11 @@ export function loadExactProfileManifest(profileId) {
|
|
|
216
219
|
next: 'Run `crtr profile list` to see available profiles.',
|
|
217
220
|
});
|
|
218
221
|
}
|
|
219
|
-
|
|
220
|
-
|
|
222
|
+
/** Exact resolution only: the generated id, else a unique manifest `name`.
|
|
223
|
+
* Returns `null` when nothing matches exactly; still THROWS when a name is
|
|
224
|
+
* shared by several profiles, because that is a real collision the caller
|
|
225
|
+
* cannot resolve by guessing (and fuzzy matching would only guess harder). */
|
|
226
|
+
function findExactProfile(all, profileIdOrName) {
|
|
221
227
|
if (ID_SHAPE.test(profileIdOrName)) {
|
|
222
228
|
const byId = all.find((p) => p.profileId === profileIdOrName);
|
|
223
229
|
if (byId !== undefined)
|
|
@@ -234,10 +240,13 @@ export function loadProfileManifest(profileIdOrName) {
|
|
|
234
240
|
next: `Re-run with one exact profile id: ${ids.join(', ')}.`,
|
|
235
241
|
});
|
|
236
242
|
}
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
/** Name the actual value set in the failure: a caller that guessed an id or a
|
|
246
|
+
* stale name can correct itself from this error alone, without a lookup hop. */
|
|
247
|
+
function profileNotFound(all, profileIdOrName) {
|
|
239
248
|
const available = all.map((p) => `${p.profileId} (${p.manifest.name})`);
|
|
240
|
-
|
|
249
|
+
return notFound(`profile not found: ${profileIdOrName}`, {
|
|
241
250
|
received: profileIdOrName,
|
|
242
251
|
available,
|
|
243
252
|
next: available.length > 0
|
|
@@ -245,6 +254,47 @@ export function loadProfileManifest(profileIdOrName) {
|
|
|
245
254
|
: 'No profiles exist on this host yet — create one with `crtr profile new --name <name>`.',
|
|
246
255
|
});
|
|
247
256
|
}
|
|
257
|
+
export function loadProfileManifest(profileIdOrName) {
|
|
258
|
+
const all = listProfiles();
|
|
259
|
+
const exact = findExactProfile(all, profileIdOrName);
|
|
260
|
+
if (exact !== null)
|
|
261
|
+
return exact;
|
|
262
|
+
throw profileNotFound(all, profileIdOrName);
|
|
263
|
+
}
|
|
264
|
+
/** Resolve a profile the USER TYPED — a `--profile` flag or a `<profile>`
|
|
265
|
+
* operand. Exact id or name wins outright; only when nothing matches exactly
|
|
266
|
+
* does this fall back to fuzzy matching on name and id slug, so a typed
|
|
267
|
+
* `crout`, `Crouter`, or `cruoter` reaches the `crouter` profile instead of
|
|
268
|
+
* failing with a list the user has to read and retype. A fuzzy hit is
|
|
269
|
+
* announced on STDERR (never stdout, which callers pipe) so the user always
|
|
270
|
+
* knows which profile actually ran.
|
|
271
|
+
*
|
|
272
|
+
* NEVER call this with a STORED `profile_id` (a node's `meta.profile_id`,
|
|
273
|
+
* `CRTR_PROFILE_ID`, a pinned default). A durable id that no longer resolves
|
|
274
|
+
* means the profile was deleted — the correct answer there is the not-found
|
|
275
|
+
* those callers already handle, not the nearest surviving profile. Use
|
|
276
|
+
* `loadProfileManifest` (or `loadExactProfileManifest`) for those. */
|
|
277
|
+
export function resolveProfileOperand(operand) {
|
|
278
|
+
const all = listProfiles();
|
|
279
|
+
const exact = findExactProfile(all, operand);
|
|
280
|
+
if (exact !== null)
|
|
281
|
+
return exact;
|
|
282
|
+
const { match, tied } = fuzzyMatchProfile(operand, all.map((p) => ({ profileId: p.profileId, name: p.manifest.name })));
|
|
283
|
+
if (match !== null) {
|
|
284
|
+
const picked = all.find((p) => p.profileId === match.profileId);
|
|
285
|
+
console.error(`crtr: no profile named "${operand}" — using the closest match: ${picked.manifest.name} (${picked.profileId})`);
|
|
286
|
+
return picked;
|
|
287
|
+
}
|
|
288
|
+
if (tied.length > 1) {
|
|
289
|
+
const ids = tied.map((c) => `${c.profileId} (${c.name})`);
|
|
290
|
+
throw ambiguous(`"${operand}" is equally close to ${tied.length} profiles: ${ids.join(', ')}`, {
|
|
291
|
+
received: operand,
|
|
292
|
+
candidates: tied.map((c) => c.profileId),
|
|
293
|
+
next: `Re-run with one exact profile id or name: ${ids.join(', ')}.`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
throw profileNotFound(all, operand);
|
|
297
|
+
}
|
|
248
298
|
function locksDir() {
|
|
249
299
|
return join(profilesRoot(), '.locks');
|
|
250
300
|
}
|
|
@@ -16,7 +16,9 @@ export declare function selectProfileForCwdReadOnly(cwd: string): string;
|
|
|
16
16
|
export declare function tildify(p: string): string;
|
|
17
17
|
/** Select the profile a node about to boot at `cwd` should run under.
|
|
18
18
|
*
|
|
19
|
-
* 1. `explicitProfile` present → resolve
|
|
19
|
+
* 1. `explicitProfile` present → resolve it as a user-typed operand (exact id,
|
|
20
|
+
* exact name, then the closest fuzzy match — nobody recalls the generated
|
|
21
|
+
* `<slug>-<8hex>` id, and a near-miss name should still start the session). If
|
|
20
22
|
* its manifest does not already cover `cwd` and the session is interactive,
|
|
21
23
|
* offer to add `cwd` to its purview (default yes). Bump `last_used_at`,
|
|
22
24
|
* return the id.
|
|
@@ -13,7 +13,7 @@ import { homedir } from 'node:os';
|
|
|
13
13
|
import { basename, resolve as resolvePath, sep } from 'node:path';
|
|
14
14
|
import { createInterface } from 'node:readline/promises';
|
|
15
15
|
import { emitKeypressEvents } from 'node:readline';
|
|
16
|
-
import { listProfiles, loadProfileManifest, updateProfileLastUsed, createProfile, addProfileProject, ensureRootProfile, ROOT_PROFILE_ID, } from './manifest.js';
|
|
16
|
+
import { listProfiles, loadProfileManifest, resolveProfileOperand, updateProfileLastUsed, createProfile, addProfileProject, ensureRootProfile, ROOT_PROFILE_ID, } from './manifest.js';
|
|
17
17
|
import { getDefaultProfileId, setDefaultProfileId, clearDefaultProfile, } from './default-binding.js';
|
|
18
18
|
import { stdoutColor } from '../output.js';
|
|
19
19
|
import { inTmux } from '../runtime/placement-tmux.js';
|
|
@@ -652,7 +652,9 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId, bindi
|
|
|
652
652
|
}
|
|
653
653
|
/** Select the profile a node about to boot at `cwd` should run under.
|
|
654
654
|
*
|
|
655
|
-
* 1. `explicitProfile` present → resolve
|
|
655
|
+
* 1. `explicitProfile` present → resolve it as a user-typed operand (exact id,
|
|
656
|
+
* exact name, then the closest fuzzy match — nobody recalls the generated
|
|
657
|
+
* `<slug>-<8hex>` id, and a near-miss name should still start the session). If
|
|
656
658
|
* its manifest does not already cover `cwd` and the session is interactive,
|
|
657
659
|
* offer to add `cwd` to its purview (default yes). Bump `last_used_at`,
|
|
658
660
|
* return the id.
|
|
@@ -683,7 +685,7 @@ export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = fa
|
|
|
683
685
|
const resolvedCwd = resolveCwd(cwd);
|
|
684
686
|
const bindings = resolveUserKeybindings();
|
|
685
687
|
if (explicitProfile !== undefined && explicitProfile !== null && explicitProfile !== '') {
|
|
686
|
-
const entry =
|
|
688
|
+
const entry = resolveProfileOperand(explicitProfile);
|
|
687
689
|
// Selecting a profile from a directory it does not yet cover: offer to
|
|
688
690
|
// widen its purview (interactive only, default yes) before pi boots.
|
|
689
691
|
if (!profileCoversCwd(entry, resolvedCwd) && isInteractive()) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// The `<profiles>` help state element
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The `<profiles>` help state element. A leaf carrying a selectable `--profile`
|
|
2
|
+
// renders this beside that flag's full contract — in ordinary leaf help when
|
|
3
|
+
// profile selection is common, or in focused `--profile -h` when omission is
|
|
4
|
+
// normal — so an agent never has to guess an id or take a separate list hop.
|
|
4
5
|
import { stateBlock } from '../help.js';
|
|
5
6
|
import { listProfiles } from './manifest.js';
|
|
6
7
|
/** A live `<profiles count=N>` state element — one `<profile_id> — <name>` line
|
|
@@ -4,8 +4,9 @@ export interface BootRootOpts {
|
|
|
4
4
|
name?: string;
|
|
5
5
|
/** Optional starter prompt (bare `crtr` requires none). */
|
|
6
6
|
prompt?: string;
|
|
7
|
-
/** Explicit `--profile <id-or-name>` on the front-door invocation,
|
|
8
|
-
* through `
|
|
7
|
+
/** Explicit `--profile <id-or-name>` on the front-door invocation, resolved
|
|
8
|
+
* through `resolveProfileOperand` — so a near-miss name still starts the
|
|
9
|
+
* session, under the closest profile. Omit to run the MRU/create-or-root-profile
|
|
9
10
|
* startup selector for `cwd` (see `selectProfileForCwd`) — a root has no
|
|
10
11
|
* spawner to inherit from, so this is the front door's only source of profile
|
|
11
12
|
* identity. */
|
|
@@ -10,10 +10,16 @@ export declare const CANVAS_DOC_SUBSTRATE_PATH: string;
|
|
|
10
10
|
export declare const CANVAS_STRUCTURED_OUTPUT_PATH: string;
|
|
11
11
|
export declare const CANVAS_BASH_VALVE_PATH: string;
|
|
12
12
|
export declare const CANVAS_PREVIEW_RESULT_PATH: string;
|
|
13
|
+
export declare const CANVAS_PROMPT_SCRUB_PATH: string;
|
|
13
14
|
/** The canvas extensions every node loads, in order. The inbox watcher commits
|
|
14
15
|
* its cursor at agent_settled before the stophook checks it for idle release.
|
|
15
16
|
* The review boundary must run before the stophook commits companion session
|
|
16
|
-
* coordinates, and before the context intro appends fork bearings.
|
|
17
|
+
* coordinates, and before the context intro appends fork bearings.
|
|
18
|
+
*
|
|
19
|
+
* Every entry here loads ahead of any installed pi package, which is why the
|
|
20
|
+
* prompt scrub belongs in this list rather than in crouter's pi package: the
|
|
21
|
+
* Claude subscription adapter is a package, and it must not see pi's docs
|
|
22
|
+
* block still in the prompt (see canvas-prompt-scrub.ts). */
|
|
17
23
|
export declare const CANVAS_EXTENSIONS: string[];
|
|
18
24
|
/** Preserve the invariant: mandatory extensions are always present exactly once.
|
|
19
25
|
*
|
|
@@ -22,10 +22,16 @@ export const CANVAS_DOC_SUBSTRATE_PATH = resolveExtension('canvas-doc-substrate'
|
|
|
22
22
|
export const CANVAS_STRUCTURED_OUTPUT_PATH = resolveExtension('canvas-structured-output');
|
|
23
23
|
export const CANVAS_BASH_VALVE_PATH = resolveExtension('canvas-bash-valve');
|
|
24
24
|
export const CANVAS_PREVIEW_RESULT_PATH = resolveExtension('canvas-preview-result');
|
|
25
|
+
export const CANVAS_PROMPT_SCRUB_PATH = resolveExtension('canvas-prompt-scrub');
|
|
25
26
|
/** The canvas extensions every node loads, in order. The inbox watcher commits
|
|
26
27
|
* its cursor at agent_settled before the stophook checks it for idle release.
|
|
27
28
|
* The review boundary must run before the stophook commits companion session
|
|
28
|
-
* coordinates, and before the context intro appends fork bearings.
|
|
29
|
+
* coordinates, and before the context intro appends fork bearings.
|
|
30
|
+
*
|
|
31
|
+
* Every entry here loads ahead of any installed pi package, which is why the
|
|
32
|
+
* prompt scrub belongs in this list rather than in crouter's pi package: the
|
|
33
|
+
* Claude subscription adapter is a package, and it must not see pi's docs
|
|
34
|
+
* block still in the prompt (see canvas-prompt-scrub.ts). */
|
|
29
35
|
export const CANVAS_EXTENSIONS = [
|
|
30
36
|
CANVAS_INBOX_WATCHER_PATH,
|
|
31
37
|
CANVAS_REVIEW_BOUNDARY_PATH,
|
|
@@ -39,6 +45,7 @@ export const CANVAS_EXTENSIONS = [
|
|
|
39
45
|
CANVAS_STRUCTURED_OUTPUT_PATH,
|
|
40
46
|
CANVAS_BASH_VALVE_PATH,
|
|
41
47
|
CANVAS_PREVIEW_RESULT_PATH,
|
|
48
|
+
CANVAS_PROMPT_SCRUB_PATH,
|
|
42
49
|
];
|
|
43
50
|
/** Preserve the invariant: mandatory extensions are always present exactly once.
|
|
44
51
|
*
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { NodeMeta } from '../canvas/types.js';
|
|
1
|
+
import type { NodeMeta, NodeStatus, ExitIntent } from '../canvas/types.js';
|
|
2
2
|
/** The lifecycle events — the only vocabulary for moving a node's status/intent.
|
|
3
3
|
* Each maps (in the table below) to a target status and/or intent plus the set
|
|
4
4
|
* of from-statuses it is legal from. */
|
|
5
|
-
export type LifecycleEvent = 'finish' | 'cancel' | 'crash' | 'yield' | 'release' | 'refresh-launch' | 'revive';
|
|
5
|
+
export type LifecycleEvent = 'finish' | 'park' | 'cancel' | 'crash' | 'yield' | 'release' | 'refresh-launch' | 'revive';
|
|
6
6
|
/** Enact a lifecycle event on a node: validate the from-status against the
|
|
7
7
|
* table, then write status+intent in ONE atomic statement (so they can never
|
|
8
8
|
* disagree). Returns the hydrated node view after the write.
|
|
@@ -10,3 +10,12 @@ export type LifecycleEvent = 'finish' | 'cancel' | 'crash' | 'yield' | 'release'
|
|
|
10
10
|
* Throws on an unknown node or an illegal move. The conditional update makes
|
|
11
11
|
* legality authoritative at SQLite write time rather than at a stale read. */
|
|
12
12
|
export declare function transition(nodeId: string, event: LifecycleEvent): NodeMeta;
|
|
13
|
+
/** Whether this row is a PARKED node — terminalized by the unattended clock
|
|
14
|
+
* rather than by finishing its own work. The one read of the park marker:
|
|
15
|
+
* `done` qualified by `intent='parked'`, both written in the same atomic
|
|
16
|
+
* statement by the `park` event above. Revival clears it (`revive` writes
|
|
17
|
+
* `intent: null`), so a reopened node is an ordinary node again. */
|
|
18
|
+
export declare function isParked(node: {
|
|
19
|
+
status: NodeStatus;
|
|
20
|
+
intent?: ExitIntent;
|
|
21
|
+
}): boolean;
|
|
@@ -39,6 +39,11 @@ const LIVE = ['active', 'idle'];
|
|
|
39
39
|
const TRANSITIONS = {
|
|
40
40
|
// markCleanExitDone (clean quit) · queue cancellation · relaunchRoot park-old.
|
|
41
41
|
finish: { status: 'done', intent: 'done', from: LIVE },
|
|
42
|
+
// The unattended park: a node whose conversation went idle with nothing left
|
|
43
|
+
// to wake it. `intent='parked'` IS the durable park marker — it qualifies the
|
|
44
|
+
// `done` it is written with, and `revive` clears it. Read it through
|
|
45
|
+
// `isParked` below, never by comparing the string at a call site.
|
|
46
|
+
park: { status: 'done', intent: 'parked', from: LIVE },
|
|
42
47
|
// closeNode cascade · reapDescendants. Forced teardown of a node that did NOT
|
|
43
48
|
// finish its own work → canceled, intent cleared. (A5: done is reserved for
|
|
44
49
|
// finish; every external reap unifies on canceled.)
|
|
@@ -98,7 +103,7 @@ export function transition(nodeId, event) {
|
|
|
98
103
|
// Wait-cron cleanup — writes a DIFFERENT table (crons) AFTER the atomic
|
|
99
104
|
// status/intent write above, so the single-(status,intent)-writer rule holds.
|
|
100
105
|
// Event-gated; exactly ONE delegated crons helper, never inline cron SQL here:
|
|
101
|
-
// finish · cancel → cancelCronsOnWake: delete the cancel-on-wake crons
|
|
106
|
+
// finish · park · cancel → cancelCronsOnWake: delete the cancel-on-wake crons
|
|
102
107
|
// ANCHORED to this node — the deadlines it armed to bound a wait. Ending
|
|
103
108
|
// the wait deliberately (finish) or having it ended for you (close cascade
|
|
104
109
|
// · reapDescendants) settles that race, so the deadline must not fire
|
|
@@ -109,7 +114,15 @@ export function transition(nodeId, event) {
|
|
|
109
114
|
// SURVIVES a node ending.
|
|
110
115
|
// crash → NOTHING. A fault is not a deliberate end-of-waiting; a pending
|
|
111
116
|
// deadline MUST survive instance death so the wait still settles.
|
|
112
|
-
if (event === 'finish' || event === 'cancel')
|
|
117
|
+
if (event === 'finish' || event === 'park' || event === 'cancel')
|
|
113
118
|
cancelCronsOnWake(nodeId);
|
|
114
119
|
return getNode(nodeId);
|
|
115
120
|
}
|
|
121
|
+
/** Whether this row is a PARKED node — terminalized by the unattended clock
|
|
122
|
+
* rather than by finishing its own work. The one read of the park marker:
|
|
123
|
+
* `done` qualified by `intent='parked'`, both written in the same atomic
|
|
124
|
+
* statement by the `park` event above. Revival clears it (`revive` writes
|
|
125
|
+
* `intent: null`), so a reopened node is an ordinary node again. */
|
|
126
|
+
export function isParked(node) {
|
|
127
|
+
return node.status === 'done' && node.intent === 'parked';
|
|
128
|
+
}
|
|
@@ -15,6 +15,10 @@ export declare function defaultProvider(ladders?: ScopeConfig['modelLadders']):
|
|
|
15
15
|
export declare const STRENGTH_ALIASES: Record<string, ModelStrength>;
|
|
16
16
|
export declare const MODEL_PROVIDER_KEYS: ModelProvider[];
|
|
17
17
|
export declare const MODEL_STRENGTHS: ModelStrength[];
|
|
18
|
+
/** Canonical user-facing grammar for a durable model override. Help and
|
|
19
|
+
* recovery paths compose from this one phrase so accepted forms cannot drift. */
|
|
20
|
+
export declare const MODEL_SPEC_FORMS: string;
|
|
21
|
+
export declare const MODEL_SPEC_NEXT: string;
|
|
18
22
|
/** The ladder providers a picker/cycle should OFFER: a provider column is kept
|
|
19
23
|
* only when at least one of its ladder cells names a runtime provider the
|
|
20
24
|
* caller reports as signed in (`isConfigured` — e.g. pool occupancy or
|
|
@@ -42,6 +42,11 @@ export const STRENGTH_ALIASES = {
|
|
|
42
42
|
// picker, and the Alt+M cycle. Both participate in automatic turn-time fallback.
|
|
43
43
|
export const MODEL_PROVIDER_KEYS = ['anthropic', 'openai'];
|
|
44
44
|
export const MODEL_STRENGTHS = ['ultra', 'strong', 'medium', 'light'];
|
|
45
|
+
/** Canonical user-facing grammar for a durable model override. Help and
|
|
46
|
+
* recovery paths compose from this one phrase so accepted forms cannot drift. */
|
|
47
|
+
export const MODEL_SPEC_FORMS = `an exact registered provider/id; provider/tier where provider is ${MODEL_PROVIDER_KEYS.join('|')} and tier is ${MODEL_STRENGTHS.join('|')}; ` +
|
|
48
|
+
`a bare tier (${MODEL_STRENGTHS.join('|')}); or a family alias (opus|sonnet|haiku)`;
|
|
49
|
+
export const MODEL_SPEC_NEXT = `Pass ${MODEL_SPEC_FORMS}.`;
|
|
45
50
|
/** The ladder providers a picker/cycle should OFFER: a provider column is kept
|
|
46
51
|
* only when at least one of its ladder cells names a runtime provider the
|
|
47
52
|
* caller reports as signed in (`isConfigured` — e.g. pool occupancy or
|
|
@@ -191,6 +191,11 @@ export function nodeEnv(meta) {
|
|
|
191
191
|
// multi-project pointer set. '' (never omitted) only for historical
|
|
192
192
|
// null-profile rows, matching every other CRTR_* scalar here.
|
|
193
193
|
CRTR_PROFILE_ID: meta.profile_id ?? '',
|
|
194
|
+
// The Claude subscription adapter re-injects pi's docs block as a hidden
|
|
195
|
+
// message whenever a turn's prompt mentions pi. canvas-prompt-scrub already
|
|
196
|
+
// removes that block from the system prompt, so re-injection would only put
|
|
197
|
+
// the adapter's own fallback copy back into the conversation. Never inject.
|
|
198
|
+
PI_CLAUDE_OAUTH_REINJECT_SCOPE: 'never',
|
|
194
199
|
};
|
|
195
200
|
if (meta.parent)
|
|
196
201
|
env['CRTR_PARENT_NODE_ID'] = meta.parent;
|
|
@@ -20,6 +20,12 @@ export declare function assertFinalizedForReopen(nodeId: string): string;
|
|
|
20
20
|
* canonical result. Callers MUST NOT proceed to revive/deliver when this
|
|
21
21
|
* throws. */
|
|
22
22
|
export declare function commitReopen(nodeId: string, expectedFinalReport: string): void;
|
|
23
|
+
/** Commit the wider message-delivery reopen: make the node resident, then
|
|
24
|
+
* retract a final latch when one exists. Unlike the revive/fresh gate, an
|
|
25
|
+
* unlatched node is valid here — it still becomes resident. Writing lifecycle
|
|
26
|
+
* first leaves a failed latch CAS with the canonical-final pointer intact.
|
|
27
|
+
* Both writes finish before either delivery channel begins. */
|
|
28
|
+
export declare function commitReopenResident(nodeId: string): void;
|
|
23
29
|
/** Convenience wrapper for a doorway with NO intervening fallible step between
|
|
24
30
|
* the gate check and the reopen clear (`node lifecycle revive` has nothing
|
|
25
31
|
* fallible between the gate and `reviveNode`). Validates and, when `reopen`,
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// mutating the live pointer — the explicit operation must not retract a
|
|
39
39
|
// canonical final unless it can actually enact the requested re-task.
|
|
40
40
|
import { join } from 'node:path';
|
|
41
|
-
import { openDb, reportsDir } from '../canvas/index.js';
|
|
41
|
+
import { openDb, reportsDir, updateNode } from '../canvas/index.js';
|
|
42
42
|
import { InputError } from '../io.js';
|
|
43
43
|
function currentFinalReport(nodeId) {
|
|
44
44
|
const row = openDb().prepare('SELECT final_report FROM nodes WHERE node_id = ?').get(nodeId);
|
|
@@ -101,6 +101,17 @@ export function commitReopen(nodeId, expectedFinalReport) {
|
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
/** Commit the wider message-delivery reopen: make the node resident, then
|
|
105
|
+
* retract a final latch when one exists. Unlike the revive/fresh gate, an
|
|
106
|
+
* unlatched node is valid here — it still becomes resident. Writing lifecycle
|
|
107
|
+
* first leaves a failed latch CAS with the canonical-final pointer intact.
|
|
108
|
+
* Both writes finish before either delivery channel begins. */
|
|
109
|
+
export function commitReopenResident(nodeId) {
|
|
110
|
+
const finalReport = currentFinalReport(nodeId);
|
|
111
|
+
updateNode(nodeId, { lifecycle: 'resident' });
|
|
112
|
+
if (finalReport !== null)
|
|
113
|
+
commitReopen(nodeId, finalReport);
|
|
114
|
+
}
|
|
104
115
|
/** Convenience wrapper for a doorway with NO intervening fallible step between
|
|
105
116
|
* the gate check and the reopen clear (`node lifecycle revive` has nothing
|
|
106
117
|
* fallible between the gate and `reviveNode`). Validates and, when `reopen`,
|
|
@@ -25,6 +25,12 @@ export declare function resumeArgs(meta: NodeMeta, resume: boolean): {
|
|
|
25
25
|
resumeSessionPath?: string;
|
|
26
26
|
newCycle?: boolean;
|
|
27
27
|
};
|
|
28
|
+
/** Whether a fresh context window would have anything on disk to stand on: a
|
|
29
|
+
* goal, a roadmap, or both. The negative is what makes a fresh cycle worse
|
|
30
|
+
* than a resume — the new window would wake amnesiac, with no statement of
|
|
31
|
+
* what it is for. Read by the parked-resident veto below and by the message
|
|
32
|
+
* handler's explicit fresh-revive gate. */
|
|
33
|
+
export declare function hasFreshGroundState(nodeId: string): boolean;
|
|
28
34
|
export interface ReviveResult {
|
|
29
35
|
/** Always null — the broker engine is never placed in a tmux window. Kept on
|
|
30
36
|
* the result for caller back-compat. */
|
|
@@ -24,7 +24,7 @@ import { existsSync } from 'node:fs';
|
|
|
24
24
|
import { isAbsolute } from 'node:path';
|
|
25
25
|
import { findNodeBySessionFile, getNode, updateNode, clearPid, recordPid, fullName, subscribersOf, } from '../canvas/index.js';
|
|
26
26
|
import { cancelCronsOnWake } from '../canvas/crons.js';
|
|
27
|
-
import { transition } from './lifecycle.js';
|
|
27
|
+
import { isParked, transition } from './lifecycle.js';
|
|
28
28
|
import { fanDoctrineWake } from './close.js';
|
|
29
29
|
import { InputError } from '../io.js';
|
|
30
30
|
import { buildPiArgv } from './launch.js';
|
|
@@ -79,6 +79,18 @@ export function resumeArgs(meta, resume) {
|
|
|
79
79
|
function isUnstartedBirth(nodeId) {
|
|
80
80
|
return readRoadmap(nodeId) === null && readYieldMessage(nodeId) === null;
|
|
81
81
|
}
|
|
82
|
+
/** Whether a fresh context window would have anything on disk to stand on: a
|
|
83
|
+
* goal, a roadmap, or both. The negative is what makes a fresh cycle worse
|
|
84
|
+
* than a resume — the new window would wake amnesiac, with no statement of
|
|
85
|
+
* what it is for. Read by the parked-resident veto below and by the message
|
|
86
|
+
* handler's explicit fresh-revive gate. */
|
|
87
|
+
export function hasFreshGroundState(nodeId) {
|
|
88
|
+
const goal = readGoal(nodeId);
|
|
89
|
+
if (goal !== null && goal.trim() !== '')
|
|
90
|
+
return true;
|
|
91
|
+
const roadmap = readRoadmap(nodeId);
|
|
92
|
+
return roadmap !== null && roadmap.trim() !== '';
|
|
93
|
+
}
|
|
82
94
|
// ---------------------------------------------------------------------------
|
|
83
95
|
// reviveNode
|
|
84
96
|
// ---------------------------------------------------------------------------
|
|
@@ -181,8 +193,16 @@ export function reviveNode(nodeId, opts) {
|
|
|
181
193
|
// crash-retry) outranks a caller's strict-resume request. resumeArgs then
|
|
182
194
|
// selects only an existing absolute session path: strict
|
|
183
195
|
// resume continues its leaf, while fresh/cycle mode roots a new one.
|
|
196
|
+
// A parked node re-enters on a FRESH cycle grounded in goal + roadmap rather
|
|
197
|
+
// than resuming the transcript its parking turn concluded — the second veto
|
|
198
|
+
// on strict resume, and a property of the node, so every revival doorway gets
|
|
199
|
+
// it without learning a predicate. Its one exception is a park with nothing
|
|
200
|
+
// on disk to ground a fresh window (reachable only on the degraded path,
|
|
201
|
+
// which writes no summary): resuming an old transcript beats waking amnesiac.
|
|
202
|
+
// The transition below clears the marker, and this snapshot precedes it.
|
|
184
203
|
const refreshPending = meta.intent === 'refresh' || meta.cycle_pending === true;
|
|
185
|
-
const
|
|
204
|
+
const parkedReentry = isParked(meta) && hasFreshGroundState(nodeId);
|
|
205
|
+
const effectiveResume = refreshPending || parkedReentry ? false : opts.resume;
|
|
186
206
|
const resume = forkBirthPending ? {} : resumeArgs(meta, effectiveResume);
|
|
187
207
|
// hasSessionPath: there is a real `.jsonl` to hand pi via `--session`, whether
|
|
188
208
|
// as a true resume OR a cycling refresh-yield (resumeArgs sets `newCycle` in
|
|
@@ -6,6 +6,8 @@ export interface SpawnChildOpts {
|
|
|
6
6
|
mode?: Mode;
|
|
7
7
|
cwd: string;
|
|
8
8
|
name?: string;
|
|
9
|
+
/** Caller-supplied description preserved against automatic naming. */
|
|
10
|
+
description?: string;
|
|
9
11
|
prompt: string;
|
|
10
12
|
/** Override the parent (defaults to the calling node from env). */
|
|
11
13
|
parent?: string;
|
|
@@ -52,7 +54,8 @@ export interface SpawnChildOpts {
|
|
|
52
54
|
* ladder config edits keep propagating). Omit to use the persona default. */
|
|
53
55
|
model?: string;
|
|
54
56
|
/** Select the profile this node runs under — an exact profile id or a unique
|
|
55
|
-
* manifest name
|
|
57
|
+
* manifest name (or the closest match to one), resolved through
|
|
58
|
+
* `resolveProfileOperand`. Omit to INHERIT the
|
|
56
59
|
* spawner's current `profile_id` (managed child or --root alike — --root only
|
|
57
60
|
* means top-level, it does not reset to a different profile; a shell root
|
|
58
61
|
* falls back to the stable root profile in the startup selector). Passed
|
|
@@ -81,7 +84,7 @@ export interface SpawnChildOpts {
|
|
|
81
84
|
* a uuid resolves to zero or multiple sessions. */
|
|
82
85
|
export declare function resolveForkSource(value: string): string;
|
|
83
86
|
/** Resolve the profile a spawned child runs under: an explicit `--profile`
|
|
84
|
-
* operand (id or
|
|
87
|
+
* operand (id, name, or closest match) resolved through `resolveProfileOperand`, else INHERIT
|
|
85
88
|
* the spawner's current `profile_id` (null when the spawner has none). `--root`
|
|
86
89
|
* never resets this to null on its own; only an explicit override does. When
|
|
87
90
|
* there is NO spawner at all — `crtr node new --root` run directly from a
|