@north-light/crouter 0.3.231 → 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
package/dist/commands/profile.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// identities defined at the user root, each owning a manifest naming project
|
|
3
3
|
// directories plus its own memory store.
|
|
4
4
|
//
|
|
5
|
-
// Every manifest verb but `new`/`list` takes <profile> as an exact id
|
|
6
|
-
//
|
|
5
|
+
// Every manifest verb but `new`/`list` takes <profile> as an exact id, a
|
|
6
|
+
// manifest name, or the closest match to one. `delete` is the one exception:
|
|
7
|
+
// an irreversible verb never runs against a guessed target.
|
|
7
8
|
import { defineBranch } from '../core/command.js';
|
|
8
9
|
import { newLeaf } from './profile/new.js';
|
|
9
10
|
import { listLeaf } from './profile/list.js';
|
|
@@ -27,7 +28,7 @@ export function registerProfile() {
|
|
|
27
28
|
help: {
|
|
28
29
|
name: 'profile',
|
|
29
30
|
summary: 'create, inspect, and manage profiles \u2014 agent identities with their own project purview and memory store',
|
|
30
|
-
model: 'A profile owns a `home` — the first of its project dirs unless `project add --home` re-points it — which is where a node under the profile runs when its create names no directory at all. A node lands in the directory it was launched from when there is one: `node new --cwd`, an inherited spawner directory (notably inside a managed worktree), or the directory a root was started in. So one profile can cover many working directories — cwd selects the identity, and the launch decides where the work happens. `new` creates one from a name (+ at most one initial `--project`, which becomes its home). `list` inventories every profile as JSONL. `show` prints one manifest in full. `rename` changes only the manifest name \u2014 the profile_id is stable across a rename. `delete --force` removes the profile after cascading through its nodes, matching schedules, originating inbox/review state, and default pins; `--detach --force` instead preserves nodes and human history while clearing the profile identity and globalizing its schedules. Both modes block while a matching node owns an open managed worktree and never touch the project directories the profile pointed at. `project` widens/narrows a profile\u2019s project purview (the dirs it resolves memory + config from). `default` pins which profile a directory defaults to at startup so the chooser stops asking \u2014 directory-scoped, independent of purview. `env` manages a profile-scoped 0600 store of env vars injected directly into every broker launched under that profile \u2014 set on stdin, list names only (never values), or remove one; no `spawnEnv.allow` entry needed. `meta` edits the manifest\u2019s readable metadata map \u2014 identity facts each broker under the profile sees as `CRTR_PROFILE_META_<KEY>` env (secrets stay in `env`). Every manifest verb but `new`/`list` takes <profile> as an exact id or a
|
|
31
|
+
model: 'A profile owns a `home` — the first of its project dirs unless `project add --home` re-points it — which is where a node under the profile runs when its create names no directory at all. A node lands in the directory it was launched from when there is one: `node new --cwd`, an inherited spawner directory (notably inside a managed worktree), or the directory a root was started in. So one profile can cover many working directories — cwd selects the identity, and the launch decides where the work happens. `new` creates one from a name (+ at most one initial `--project`, which becomes its home). `list` inventories every profile as JSONL. `show` prints one manifest in full. `rename` changes only the manifest name \u2014 the profile_id is stable across a rename. `delete --force` removes the profile after cascading through its nodes, matching schedules, originating inbox/review state, and default pins; `--detach --force` instead preserves nodes and human history while clearing the profile identity and globalizing its schedules. Both modes block while a matching node owns an open managed worktree and never touch the project directories the profile pointed at. `project` widens/narrows a profile\u2019s project purview (the dirs it resolves memory + config from). `default` pins which profile a directory defaults to at startup so the chooser stops asking \u2014 directory-scoped, independent of purview. `env` manages a profile-scoped 0600 store of env vars injected directly into every broker launched under that profile \u2014 set on stdin, list names only (never values), or remove one; no `spawnEnv.allow` entry needed. `meta` edits the manifest\u2019s readable metadata map \u2014 identity facts each broker under the profile sees as `CRTR_PROFILE_META_<KEY>` env (secrets stay in `env`). Every manifest verb but `new`/`list` takes <profile> as an exact id, a manifest name, or a close-enough name that resolves to the nearest profile (reported on stderr); a name shared by several profiles, or one equally close to several, fails listing every matching id. `delete` alone never guesses — an irreversible verb requires an exact id or name.',
|
|
31
32
|
},
|
|
32
33
|
children: [newLeaf, listLeaf, showLeaf, renameLeaf, pauseLeaf, resumeLeaf, kindLeaf, metaLeaf, deleteLeaf, projectBranch, defaultBranch, envBranch],
|
|
33
34
|
});
|
|
@@ -92,7 +92,6 @@ export const tmuxSpreadLeaf = defineLeaf({
|
|
|
92
92
|
output: [
|
|
93
93
|
{ name: 'window', type: 'string', required: false, constraint: 'The new window id holding the tiled spread.' },
|
|
94
94
|
{ name: 'session', type: 'string', required: false, constraint: 'The session the spread window lives in (the caller\'s session — break-pane keeps it).' },
|
|
95
|
-
{ name: 'panes', type: 'number', required: true, constraint: 'How many panes the spread window ended with (self + opened siblings).' },
|
|
96
95
|
{ name: 'nodes', type: 'string', required: true, constraint: 'Space-joined ids tiled into the window (self first).' },
|
|
97
96
|
{ name: 'overflow', type: 'number', required: true, constraint: 'Broker-alive siblings beyond the cap that were NOT shown.' },
|
|
98
97
|
],
|
|
@@ -207,7 +206,6 @@ export const tmuxSpreadLeaf = defineLeaf({
|
|
|
207
206
|
return {
|
|
208
207
|
window,
|
|
209
208
|
session: session ?? undefined,
|
|
210
|
-
panes: spreadIds.length,
|
|
211
209
|
nodes: spreadIds.join(' '),
|
|
212
210
|
overflow,
|
|
213
211
|
};
|
|
@@ -215,6 +213,6 @@ export const tmuxSpreadLeaf = defineLeaf({
|
|
|
215
213
|
render: (out) => {
|
|
216
214
|
const o = out;
|
|
217
215
|
const tail = o.overflow > 0 ? ` (+${o.overflow} more not shown)` : '';
|
|
218
|
-
return `spread
|
|
216
|
+
return `spread viewers into window ${o.window ?? '?'}${tail}`;
|
|
219
217
|
},
|
|
220
218
|
});
|
|
@@ -186,8 +186,7 @@ const configSet = defineLeaf({
|
|
|
186
186
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Scope to write to. Default: user. Project scope is supported only for modelLadders.' },
|
|
187
187
|
],
|
|
188
188
|
output: [
|
|
189
|
-
{ name: '
|
|
190
|
-
{ name: 'value', type: 'unknown', required: true, constraint: 'Value as written.' },
|
|
189
|
+
{ name: 'value', type: 'unknown', required: true, constraint: 'Resolved value as written after parsing and setting-specific coercion.' },
|
|
191
190
|
{ name: 'scope', type: 'string', required: true, constraint: 'Scope the value was written to.' },
|
|
192
191
|
],
|
|
193
192
|
outputKind: 'object',
|
|
@@ -210,9 +209,9 @@ const configSet = defineLeaf({
|
|
|
210
209
|
const cfg = ladder !== null
|
|
211
210
|
? (persistModelLadders(requireScopeRoot(scope), ladder), readConfig(scope))
|
|
212
211
|
: updateConfig(scope, (c) => setNestedValue(c, key, rawValue, parsed));
|
|
213
|
-
// Read back the
|
|
212
|
+
// Read back the resolved value that was written
|
|
214
213
|
const written = getNestedValue(cfg, key);
|
|
215
|
-
return {
|
|
214
|
+
return { value: written, scope };
|
|
216
215
|
},
|
|
217
216
|
});
|
|
218
217
|
const configPath = defineLeaf({
|
|
@@ -212,10 +212,8 @@ export const supportPrepareLeaf = defineLeaf({
|
|
|
212
212
|
},
|
|
213
213
|
],
|
|
214
214
|
output: [
|
|
215
|
-
{ name: '
|
|
216
|
-
{ name: 'reference', type: 'string', required: true, constraint: 'YYYYMMDDTHHmmssSSSZ-xxxxxxxx — pass this to `crtr sys support submit`.' },
|
|
215
|
+
{ name: 'reference', type: 'string', required: true, constraint: 'Generated YYYYMMDDTHHmmssSSSZ-xxxxxxxx — pass this to `crtr sys support submit`.' },
|
|
217
216
|
{ name: 'path', type: 'string', required: true, constraint: 'Absolute path to the new ~/.crouter/support/<reference>/ directory.' },
|
|
218
|
-
{ name: 'node_id', type: 'string', required: true, constraint: 'The node the bundle was collected for.' },
|
|
219
217
|
{ name: 'source_count', type: 'number', required: true, constraint: 'Number of distinct sources declared in the manifest.' },
|
|
220
218
|
{ name: 'bytes', type: 'number', required: true, constraint: 'Combined size of manifest.json + bundle.jsonl, always \u2264 2,097,152.' },
|
|
221
219
|
],
|
|
@@ -230,6 +228,12 @@ export const supportPrepareLeaf = defineLeaf({
|
|
|
230
228
|
const nodeId = input['node_id'];
|
|
231
229
|
if (nodeId === undefined || nodeId === '')
|
|
232
230
|
throw usage('node_id is required. Run `crtr sys support prepare -h`.');
|
|
233
|
-
|
|
231
|
+
const result = prepareSupportBundle(nodeId);
|
|
232
|
+
return {
|
|
233
|
+
reference: result.reference,
|
|
234
|
+
path: result.path,
|
|
235
|
+
source_count: result.source_count,
|
|
236
|
+
bytes: result.bytes,
|
|
237
|
+
};
|
|
234
238
|
},
|
|
235
239
|
});
|
|
@@ -504,8 +504,6 @@ export const supportSubmitLeaf = defineLeaf({
|
|
|
504
504
|
},
|
|
505
505
|
],
|
|
506
506
|
output: [
|
|
507
|
-
{ name: 'status', type: 'string', required: true, constraint: 'Always "submitted" on success; every failure throws.' },
|
|
508
|
-
{ name: 'reference', type: 'string', required: true, constraint: 'The submitted reference.' },
|
|
509
507
|
{ name: 'repository', type: 'string', required: true, constraint: 'Canonical owner/name of the private destination.' },
|
|
510
508
|
{ name: 'url', type: 'string', required: true, constraint: 'Canonical github.com URL of support/<reference>/.' },
|
|
511
509
|
],
|
|
@@ -520,6 +518,7 @@ export const supportSubmitLeaf = defineLeaf({
|
|
|
520
518
|
const reference = input.reference;
|
|
521
519
|
if (reference === undefined || reference === '')
|
|
522
520
|
throw usage('reference is required. Run `crtr sys support submit -h`.');
|
|
523
|
-
|
|
521
|
+
const result = await submitPreparedBundle(reference, process.env, defaultGhRunner);
|
|
522
|
+
return { repository: result.repository, url: result.url };
|
|
524
523
|
},
|
|
525
524
|
});
|
|
@@ -481,10 +481,6 @@ export const sysSyncDepsLeaf = defineLeaf({
|
|
|
481
481
|
{ kind: 'flag', name: 'dry-run', type: 'bool', required: false, constraint: 'Report what would be vendored without writing anything.' },
|
|
482
482
|
],
|
|
483
483
|
output: [
|
|
484
|
-
{ name: 'imported', type: 'number', required: true, constraint: 'Count of memory docs written (bundle INDEX.md, sidecar docs, and the catalog INDEX.md).' },
|
|
485
|
-
{ name: 'wouldImport', type: 'number', required: true, constraint: 'Count of memory docs that would be written under --dry-run.' },
|
|
486
|
-
{ name: 'skipped', type: 'number', required: true, constraint: 'Count of same-named bundle collisions skipped (first-sorted package wins).' },
|
|
487
|
-
{ name: 'excluded', type: 'number', required: true, constraint: 'Count of dependency skill bundles excluded by exact package.json crtr.sync.deps.exclude selectors.' },
|
|
488
484
|
{ name: 'results', type: 'object[]', required: true, constraint: 'Each: {source,target,name,scope,status,reason?}; status is one of imported, would-import, skipped, excluded. `name` is the full canonical name the vendored doc answers at (`<namespace>/deps/<bundle>`), except on an excluded row, which names the `<package>@<version>:<skill>` selector. An excluded row is a dependency bundle rejected before its files enter the vendoring planner.' },
|
|
489
485
|
],
|
|
490
486
|
outputKind: 'object',
|
|
@@ -498,11 +494,7 @@ export const sysSyncDepsLeaf = defineLeaf({
|
|
|
498
494
|
const sourceArg = input['source'];
|
|
499
495
|
const dryRun = input['dryRun'] ?? false;
|
|
500
496
|
const results = runDepsSync({ sourceArg, dryRun });
|
|
501
|
-
|
|
502
|
-
const wouldImport = results.filter((r) => r.status === 'would-import').length;
|
|
503
|
-
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
504
|
-
const excluded = results.filter((r) => r.status === 'excluded').length;
|
|
505
|
-
return { imported, wouldImport, skipped, excluded, results };
|
|
497
|
+
return { results };
|
|
506
498
|
},
|
|
507
499
|
render: (result) => renderDepsSummary(result.results),
|
|
508
500
|
});
|
|
@@ -379,9 +379,6 @@ export const sysSyncProjectGuidanceLeaf = defineLeaf({
|
|
|
379
379
|
{ kind: 'flag', name: 'overwrite', type: 'bool', required: false, constraint: 'Replace an existing target memory doc. Default: skip existing docs to avoid clobbering user-authored memory.' },
|
|
380
380
|
],
|
|
381
381
|
output: [
|
|
382
|
-
{ name: 'imported', type: 'number', required: true, constraint: 'Count of memory docs written.' },
|
|
383
|
-
{ name: 'wouldImport', type: 'number', required: true, constraint: 'Count of memory docs that would be written under --dry-run.' },
|
|
384
|
-
{ name: 'skipped', type: 'number', required: true, constraint: 'Count of source files skipped (existing target without --overwrite).' },
|
|
385
382
|
{ name: 'results', type: 'object[]', required: true, constraint: 'Each: {source,target,name,scope,status,reason?}; status is one of imported, skipped, would-import.' },
|
|
386
383
|
],
|
|
387
384
|
outputKind: 'object',
|
|
@@ -477,10 +474,7 @@ export const sysSyncProjectGuidanceLeaf = defineLeaf({
|
|
|
477
474
|
for (const { prepared } of ordered) {
|
|
478
475
|
results.push(convertProjectDoc(prepared, { dryRun, overwrite }));
|
|
479
476
|
}
|
|
480
|
-
|
|
481
|
-
const wouldImport = results.filter((r) => r.status === 'would-import').length;
|
|
482
|
-
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
483
|
-
return { imported, wouldImport, skipped, results };
|
|
477
|
+
return { results };
|
|
484
478
|
},
|
|
485
479
|
render: (result) => renderSummary(result.results),
|
|
486
480
|
});
|
|
@@ -250,11 +250,6 @@ export const sysSyncSkillsLeaf = defineLeaf({
|
|
|
250
250
|
{ kind: 'flag', name: 'include-ignored', type: 'bool', required: false, constraint: 'Include ignored candidates in the report without making sync read-only. Pair with --dry-run to audit the full candidate set without importing non-ignored skills.' },
|
|
251
251
|
],
|
|
252
252
|
output: [
|
|
253
|
-
{ name: 'imported', type: 'number', required: true, constraint: 'Count of memory docs written.' },
|
|
254
|
-
{ name: 'wouldImport', type: 'number', required: true, constraint: 'Count of memory docs that would be written under --dry-run.' },
|
|
255
|
-
{ name: 'skipped', type: 'number', required: true, constraint: 'Count of source files skipped (existing target without --overwrite, or an unimportable SKILL.md).' },
|
|
256
|
-
{ name: 'ignored', type: 'number', required: true, constraint: 'Count of selected SKILL.md files that are permanently ignored.' },
|
|
257
|
-
{ name: 'wouldIgnore', type: 'number', required: true, constraint: 'Count of selected SKILL.md files that would be ignored under --dry-run.' },
|
|
258
253
|
{ name: 'results', type: 'object[]', required: true, constraint: 'Each: {source,target,name,scope,status,reason?}; status is one of imported, skipped, would-import, ignored, would-ignore. `name` is the full canonical name the imported doc answers at — a user import’s bundle name, a project import’s `<namespace>/<bundle name>`.' },
|
|
259
254
|
],
|
|
260
255
|
outputKind: 'object',
|
|
@@ -310,12 +305,7 @@ export const sysSyncSkillsLeaf = defineLeaf({
|
|
|
310
305
|
}
|
|
311
306
|
if (changedIgnoreState)
|
|
312
307
|
writeIgnoreState(ignoreState);
|
|
313
|
-
|
|
314
|
-
const wouldImport = results.filter((r) => r.status === 'would-import').length;
|
|
315
|
-
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
316
|
-
const ignored = results.filter((r) => r.status === 'ignored').length;
|
|
317
|
-
const wouldIgnore = results.filter((r) => r.status === 'would-ignore').length;
|
|
318
|
-
return { imported, wouldImport, skipped, ignored, wouldIgnore, results, ignoreFile: ignoreFilePath(), ignoreMode };
|
|
308
|
+
return { results, ignoreFile: ignoreFilePath(), ignoreMode };
|
|
319
309
|
},
|
|
320
310
|
render: (result) => renderSummary(result.results, result.ignoreMode === true),
|
|
321
311
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Run with: npm test -- --test-name-pattern='cron node sink'
|
|
2
|
+
//
|
|
3
|
+
// A resident root parked by the unattended clock is status=done without a
|
|
4
|
+
// final-report latch. Its anchored cron is the explicit next cycle that must
|
|
5
|
+
// append durably and resume the existing session; finalized and canceled rows
|
|
6
|
+
// remain ordinary undeliverable node sinks.
|
|
7
|
+
import { after, afterEach, before, beforeEach, test } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { createNode, getNode, listNodes } from '../canvas/canvas.js';
|
|
13
|
+
import { armCron, getCron } from '../canvas/crons.js';
|
|
14
|
+
import { closeDb } from '../canvas/db.js';
|
|
15
|
+
import { readInboxSince } from '../feed/inbox.js';
|
|
16
|
+
import { headlessBrokerHost } from '../runtime/host.js';
|
|
17
|
+
import { resetFleetForTests } from '../runtime/fleet.js';
|
|
18
|
+
import { applyParkEvent, parkEvent } from '../../daemon/reconcilers/live-obligation.js';
|
|
19
|
+
import { executeCron } from '../../daemon/cron-run.js';
|
|
20
|
+
import { bindTestFleet } from './helpers/fleet.js';
|
|
21
|
+
let home;
|
|
22
|
+
let priorHome;
|
|
23
|
+
let fleet;
|
|
24
|
+
let launches;
|
|
25
|
+
let originalLaunch;
|
|
26
|
+
function node(id, over = {}) {
|
|
27
|
+
return {
|
|
28
|
+
node_id: id,
|
|
29
|
+
name: id,
|
|
30
|
+
created: new Date().toISOString(),
|
|
31
|
+
cwd: home,
|
|
32
|
+
kind: 'general',
|
|
33
|
+
mode: 'base',
|
|
34
|
+
lifecycle: 'resident',
|
|
35
|
+
status: 'active',
|
|
36
|
+
...over,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function arm({ cron_id, ...overrides }) {
|
|
40
|
+
armCron({
|
|
41
|
+
cron_id,
|
|
42
|
+
name: cron_id,
|
|
43
|
+
created_by: null,
|
|
44
|
+
command: "printf 'cron output'",
|
|
45
|
+
fire_at: new Date(Date.now() - 1_000).toISOString(),
|
|
46
|
+
recur: JSON.stringify({ every: '1h' }),
|
|
47
|
+
tz: null,
|
|
48
|
+
expires_at: null,
|
|
49
|
+
anchor_node: null,
|
|
50
|
+
cancel_on_wake: false,
|
|
51
|
+
cwd: home,
|
|
52
|
+
env_json: null,
|
|
53
|
+
profile: null,
|
|
54
|
+
scope: 'profile',
|
|
55
|
+
run_timeout_s: 30,
|
|
56
|
+
overlap: 'skip',
|
|
57
|
+
on_output: 'always',
|
|
58
|
+
sink: '{}',
|
|
59
|
+
tier: 'normal',
|
|
60
|
+
...overrides,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Produce the exact unattended resident-root shape: `park` writes done with
|
|
64
|
+
* the park marker and leaves the final-report latch empty. A real 15-minute
|
|
65
|
+
* clock reaches this same transition, through a summary turn on the good path
|
|
66
|
+
* and directly on the degraded one. */
|
|
67
|
+
function parkResidentRoot(id) {
|
|
68
|
+
const sessionFile = join(home, `${id}.jsonl`);
|
|
69
|
+
writeFileSync(sessionFile, '{}\n');
|
|
70
|
+
createNode(node(id, { pi_session_id: `${id}-session`, pi_session_file: sessionFile }));
|
|
71
|
+
const event = parkEvent(id, 'resident');
|
|
72
|
+
assert.equal(event, 'park');
|
|
73
|
+
applyParkEvent(id, event);
|
|
74
|
+
const parked = getNode(id);
|
|
75
|
+
assert.equal(parked.status, 'done');
|
|
76
|
+
assert.equal(parked.intent, 'parked');
|
|
77
|
+
assert.equal(parked.final_report ?? null, null);
|
|
78
|
+
}
|
|
79
|
+
before(() => {
|
|
80
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-cron-node-sink-parked-root-'));
|
|
81
|
+
priorHome = process.env['CRTR_HOME'];
|
|
82
|
+
process.env['CRTR_HOME'] = home;
|
|
83
|
+
});
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
closeDb();
|
|
86
|
+
rmSync(home, { recursive: true, force: true });
|
|
87
|
+
mkdirSync(home, { recursive: true });
|
|
88
|
+
resetFleetForTests();
|
|
89
|
+
fleet = bindTestFleet();
|
|
90
|
+
launches = [];
|
|
91
|
+
originalLaunch = headlessBrokerHost.launch;
|
|
92
|
+
headlessBrokerHost.launch = ((nodeId, _inv, opts) => {
|
|
93
|
+
launches.push({ nodeId, resuming: opts.resuming });
|
|
94
|
+
const handle = { pid: 0x7ffffffe, exited: new Promise(() => { }) };
|
|
95
|
+
fleet.fleet.register(nodeId, handle);
|
|
96
|
+
return handle;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
101
|
+
closeDb();
|
|
102
|
+
resetFleetForTests();
|
|
103
|
+
});
|
|
104
|
+
after(() => {
|
|
105
|
+
rmSync(home, { recursive: true, force: true });
|
|
106
|
+
if (priorHome === undefined)
|
|
107
|
+
delete process.env['CRTR_HOME'];
|
|
108
|
+
else
|
|
109
|
+
process.env['CRTR_HOME'] = priorHome;
|
|
110
|
+
});
|
|
111
|
+
test('cron node sink delivers to a parked resident root and resumes its existing session without escalation', async () => {
|
|
112
|
+
parkResidentRoot('parked-root');
|
|
113
|
+
arm({ cron_id: 'parked-root-cron', sink: JSON.stringify({ node: 'parked-root' }), anchor_node: 'parked-root' });
|
|
114
|
+
const record = await executeCron(getCron('parked-root-cron'), { outOfBand: false });
|
|
115
|
+
assert.equal(record?.delivered, 'delivered: node parked-root');
|
|
116
|
+
assert.equal(getCron('parked-root-cron').state, 'active', 'delivery does not pause the cron');
|
|
117
|
+
assert.deepEqual(readInboxSince('parked-root', undefined).map((entry) => entry.data?.['body']), ['cron output']);
|
|
118
|
+
assert.deepEqual(launches, [{ nodeId: 'parked-root', resuming: true }], 'the parked session is resumed exactly once');
|
|
119
|
+
assert.equal(getNode('parked-root').status, 'active', 'revive restores the parked root to its active cycle');
|
|
120
|
+
assert.deepEqual(listNodes().map((entry) => entry.node_id), ['parked-root'], 'no escalation node was created');
|
|
121
|
+
});
|
|
122
|
+
test('finalized and canceled node-sink targets still pause and escalate without delivery', async () => {
|
|
123
|
+
const targets = [
|
|
124
|
+
['finalized-root', { status: 'done', final_report: 'final.md', finalized_at: new Date().toISOString() }],
|
|
125
|
+
['canceled-root', { status: 'canceled' }],
|
|
126
|
+
];
|
|
127
|
+
for (const [id, state] of targets) {
|
|
128
|
+
createNode(node(id, state));
|
|
129
|
+
arm({ cron_id: `${id}-cron`, sink: JSON.stringify({ node: id }) });
|
|
130
|
+
const before = listNodes().length;
|
|
131
|
+
const record = await executeCron(getCron(`${id}-cron`), { outOfBand: false });
|
|
132
|
+
assert.match(record?.delivered ?? '', /^failure: cron paused, escalation node /, `${id}: failure is escalated`);
|
|
133
|
+
assert.equal(getCron(`${id}-cron`).state, 'paused', `${id}: cron is paused`);
|
|
134
|
+
assert.equal(readInboxSince(id, undefined).length, 0, `${id}: no message was appended`);
|
|
135
|
+
assert.equal(listNodes().length, before + 1, `${id}: exactly one escalation node was created`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
test('a deferred node-sink entry reaches the parked root durably but never revives it', async () => {
|
|
139
|
+
parkResidentRoot('deferred-root');
|
|
140
|
+
arm({ cron_id: 'deferred-root-cron', sink: JSON.stringify({ node: 'deferred-root' }), tier: 'deferred' });
|
|
141
|
+
const record = await executeCron(getCron('deferred-root-cron'), { outOfBand: false });
|
|
142
|
+
assert.equal(record?.delivered, 'delivered: node deferred-root');
|
|
143
|
+
assert.equal(getCron('deferred-root-cron').state, 'active');
|
|
144
|
+
assert.deepEqual(readInboxSince('deferred-root', undefined).map((entry) => entry.tier), ['deferred']);
|
|
145
|
+
assert.deepEqual(launches, [], 'deferred delivery never launches a dormant target');
|
|
146
|
+
assert.equal(getNode('deferred-root').status, 'done', 'the parked target remains parked');
|
|
147
|
+
});
|
|
@@ -13,7 +13,7 @@ import { createNode } from '../canvas/canvas.js';
|
|
|
13
13
|
import { closeDb } from '../canvas/db.js';
|
|
14
14
|
import { contextDir } from '../canvas/paths.js';
|
|
15
15
|
import { appendInbox } from '../feed/inbox.js';
|
|
16
|
-
import { buildCorpus, resolveRef } from '../canvas/history.js';
|
|
16
|
+
import { buildCorpus, nodeArtifacts, resolveRef } from '../canvas/history.js';
|
|
17
17
|
let home;
|
|
18
18
|
function node(id, cwd) {
|
|
19
19
|
return {
|
|
@@ -49,6 +49,16 @@ test('history corpus skips malformed frontmatter in another cwd', () => {
|
|
|
49
49
|
assert.equal(corpus.some((a) => a.ref === `${broken.node_id}:context/copied-source.md`), false, 'invalid artifact is skipped');
|
|
50
50
|
assert.ok(corpus.some((a) => a.loadBody().includes('0a0afd613')), 'valid artifacts remain searchable');
|
|
51
51
|
});
|
|
52
|
+
test('one node\'s artifact listing skips malformed frontmatter instead of failing', () => {
|
|
53
|
+
const n = createNode(node('single-node-broken', '/tmp/hist-single'));
|
|
54
|
+
// A context dir often holds foreign markdown (a checked-out repo) whose
|
|
55
|
+
// `---` header is not crouter frontmatter.
|
|
56
|
+
writeFileSync(join(contextDir(n.node_id), 'copied-source.md'), '---\npattern: "@(IsString)\\("\n---\nsource text\n');
|
|
57
|
+
writeFileSync(join(contextDir(n.node_id), 'result.md'), '---\ntitle: result\n---\n0a0afd613\n');
|
|
58
|
+
const arts = nodeArtifacts(n.node_id);
|
|
59
|
+
assert.equal(arts.some((a) => a.relpath === 'context/copied-source.md'), false, 'invalid artifact is skipped');
|
|
60
|
+
assert.ok(arts.some((a) => a.relpath === 'context/result.md'), 'valid artifacts still listed');
|
|
61
|
+
});
|
|
52
62
|
test('default history excludes inbox; --type inbox includes it, with full sender-attributed bodies', () => {
|
|
53
63
|
const cwd = '/tmp/hist-work';
|
|
54
64
|
createNode(node('sender-1', cwd));
|
|
@@ -9,7 +9,8 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { ApiError } from '../../api/index.js';
|
|
10
10
|
import { createApiServer } from '../../daemon/api/server.js';
|
|
11
11
|
import { cancelHumanTicket, deliverTerminalResult, resolvePageTicket, settleFeedbackCompanion } from '../../daemon/human/finish.js';
|
|
12
|
-
import { humanSend } from '../../commands/human/prompts.js';
|
|
12
|
+
import { humanSend as buildHumanSend } from '../../commands/human/prompts.js';
|
|
13
|
+
const humanSend = buildHumanSend();
|
|
13
14
|
import { getNode, listNodes, closeDb } from '../canvas/index.js';
|
|
14
15
|
import { coalesce, readInboxSince } from '../feed/inbox.js';
|
|
15
16
|
import { createPageFeedbackComment } from '../human/feedback.js';
|
|
@@ -739,7 +739,6 @@ describe('memory corpus gate across the plugin lifecycle', () => {
|
|
|
739
739
|
// The package gains an old-format document while it sits disabled.
|
|
740
740
|
writeDoc(join(source, 'memory', 'guide', 'INDEX.md'), 'kind: knowledge\nname: guide/INDEX', 'Guide.');
|
|
741
741
|
const enabled = await pluginEnable.run({ name: 'lifecycle' });
|
|
742
|
-
assert.equal(enabled['enabled'], true);
|
|
743
742
|
const memory = enabled['memory'];
|
|
744
743
|
assert.deepEqual(memory.migrated, ['guide/INDEX.md']);
|
|
745
744
|
assert.equal(memory.linkedSource, realpathSync(join(source, 'memory')));
|
|
@@ -160,7 +160,6 @@ test('node message send --tier deferred to an ordinary dormant (idle-released) t
|
|
|
160
160
|
});
|
|
161
161
|
try {
|
|
162
162
|
const res = (await messageSendLeaf().run({ to: id, tier: 'deferred', body: 'a held note' }));
|
|
163
|
-
assert.equal(res['delivered'], true, 'the message is still delivered (appended), just not woken');
|
|
164
163
|
assert.equal(res['revived'], false, 'deferred never revives a dormant target');
|
|
165
164
|
assert.equal(res['guidance'], "Delivered — held for the target's next natural cycle; not revived.", 'guidance is truthful about the hold, not a claimed revive');
|
|
166
165
|
}
|
|
@@ -12,7 +12,7 @@ import { tmpdir } from 'node:os';
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { createNode, getNode, setStatus, setIntent } from '../canvas/canvas.js';
|
|
14
14
|
import { closeDb } from '../canvas/db.js';
|
|
15
|
-
import { transition } from '../runtime/lifecycle.js';
|
|
15
|
+
import { isParked, transition } from '../runtime/lifecycle.js';
|
|
16
16
|
let home;
|
|
17
17
|
function node(id) {
|
|
18
18
|
return {
|
|
@@ -67,6 +67,34 @@ test('finalize: illegal from done|dead|canceled → throws, status untouched', (
|
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
69
|
// ---------------------------------------------------------------------------
|
|
70
|
+
// park → done + intent='parked', legal only from active|idle. The intent IS the
|
|
71
|
+
// durable park marker: it must land atomically with the `done` it qualifies,
|
|
72
|
+
// read only through isParked, and be cleared by revival.
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
test('park: active|idle → done + intent=parked, and isParked reads exactly that pair', () => {
|
|
75
|
+
for (const from of LIVE) {
|
|
76
|
+
mk(`n_${from}`, from);
|
|
77
|
+
const m = transition(`n_${from}`, 'park');
|
|
78
|
+
assert.equal(m.status, 'done', `park from ${from}`);
|
|
79
|
+
assert.equal(m.intent, 'parked', `park from ${from} sets intent=parked`);
|
|
80
|
+
assert.equal(isParked(m), true, `park from ${from} is readable as parked`);
|
|
81
|
+
assert.equal(m.final_report ?? null, null, `park from ${from} leaves the finalization latch unset`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
test('park: illegal from done|dead|canceled → throws', () => {
|
|
85
|
+
for (const from of TERMINAL) {
|
|
86
|
+
mk(`n_${from}`, from);
|
|
87
|
+
assert.throws(() => transition(`n_${from}`, 'park'), /illegal lifecycle transition/);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
test('park: revival clears the marker, and no other done is parked', () => {
|
|
91
|
+
mk('reopened', 'active');
|
|
92
|
+
transition('reopened', 'park');
|
|
93
|
+
assert.equal(isParked(transition('reopened', 'revive')), false, 'revive clears intent=parked');
|
|
94
|
+
mk('finished', 'active');
|
|
95
|
+
assert.equal(isParked(transition('finished', 'finish')), false, 'a finished done is not a park');
|
|
96
|
+
});
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
70
98
|
// cancel → canceled + intent cleared, legal from ANY status (forced teardown).
|
|
71
99
|
// A5 (human-confirmed 2026-06-06): the `reap` event was COLLAPSED into `cancel`
|
|
72
100
|
// — they were identical (status differed only; both intent=null, from=ANY, no
|
|
@@ -159,7 +187,7 @@ test('revive: any status → active + intent cleared', () => {
|
|
|
159
187
|
// unknown node → throws for every event
|
|
160
188
|
// ---------------------------------------------------------------------------
|
|
161
189
|
test('transition on an unknown node throws', () => {
|
|
162
|
-
for (const ev of ['finish', 'cancel', 'crash', 'yield', 'release', 'refresh-launch', 'revive']) {
|
|
190
|
+
for (const ev of ['finish', 'park', 'cancel', 'crash', 'yield', 'release', 'refresh-launch', 'revive']) {
|
|
163
191
|
assert.throws(() => transition('ghost', ev), /unknown node/);
|
|
164
192
|
}
|
|
165
193
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Run with: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/revive-parked-fresh.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Re-entering a parked node starts a FRESH cycle grounded in goal + roadmap
|
|
4
|
+
// rather than replaying the transcript its parking turn already concluded. The
|
|
5
|
+
// rule is a property of the node, evaluated once inside reviveNode, so every
|
|
6
|
+
// doorway gets it from one strict-resume veto. Its one exception is a park with
|
|
7
|
+
// nothing on disk to stand on (reachable only on the degraded path, which
|
|
8
|
+
// writes no summary): resuming an old transcript beats waking amnesiac.
|
|
9
|
+
import { after, afterEach, before, beforeEach, test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { createNode, getNode } from '../canvas/canvas.js';
|
|
15
|
+
import { contextDir } from '../canvas/paths.js';
|
|
16
|
+
import { closeDb } from '../canvas/db.js';
|
|
17
|
+
import { transition } from '../runtime/lifecycle.js';
|
|
18
|
+
import { reviveNode } from '../runtime/revive.js';
|
|
19
|
+
import { headlessBrokerHost } from '../runtime/host.js';
|
|
20
|
+
import { resetFleetForTests } from '../runtime/fleet.js';
|
|
21
|
+
import { bindTestFleet } from './helpers/fleet.js';
|
|
22
|
+
let home;
|
|
23
|
+
let priorHome;
|
|
24
|
+
let fleet;
|
|
25
|
+
let launches;
|
|
26
|
+
let originalLaunch;
|
|
27
|
+
/** A parked resident with a real `.jsonl` on disk, so the choice between
|
|
28
|
+
* resuming that transcript and cycling it is genuinely open. */
|
|
29
|
+
function parkResident(id, ground = {}) {
|
|
30
|
+
const sessionFile = join(home, `${id}.jsonl`);
|
|
31
|
+
writeFileSync(sessionFile, '{}\n');
|
|
32
|
+
const meta = {
|
|
33
|
+
node_id: id,
|
|
34
|
+
name: id,
|
|
35
|
+
created: new Date().toISOString(),
|
|
36
|
+
cwd: home,
|
|
37
|
+
kind: 'general',
|
|
38
|
+
mode: 'base',
|
|
39
|
+
lifecycle: 'resident',
|
|
40
|
+
status: 'active',
|
|
41
|
+
pi_session_id: `${id}-session`,
|
|
42
|
+
pi_session_file: sessionFile,
|
|
43
|
+
};
|
|
44
|
+
createNode(meta);
|
|
45
|
+
mkdirSync(contextDir(id), { recursive: true });
|
|
46
|
+
if (ground.roadmap !== undefined)
|
|
47
|
+
writeFileSync(join(contextDir(id), 'roadmap.md'), ground.roadmap);
|
|
48
|
+
if (ground.goal !== undefined)
|
|
49
|
+
writeFileSync(join(contextDir(id), 'initial-prompt.md'), ground.goal);
|
|
50
|
+
transition(id, 'park');
|
|
51
|
+
}
|
|
52
|
+
before(() => {
|
|
53
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-revive-parked-'));
|
|
54
|
+
priorHome = process.env['CRTR_HOME'];
|
|
55
|
+
process.env['CRTR_HOME'] = home;
|
|
56
|
+
});
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
closeDb();
|
|
59
|
+
rmSync(home, { recursive: true, force: true });
|
|
60
|
+
mkdirSync(home, { recursive: true });
|
|
61
|
+
resetFleetForTests();
|
|
62
|
+
fleet = bindTestFleet();
|
|
63
|
+
launches = [];
|
|
64
|
+
originalLaunch = headlessBrokerHost.launch;
|
|
65
|
+
headlessBrokerHost.launch = ((nodeId, _inv, opts) => {
|
|
66
|
+
launches.push({ nodeId, resuming: opts.resuming });
|
|
67
|
+
const handle = { pid: 0x7ffffffe, exited: new Promise(() => { }) };
|
|
68
|
+
fleet.fleet.register(nodeId, handle);
|
|
69
|
+
return handle;
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
headlessBrokerHost.launch = originalLaunch;
|
|
74
|
+
closeDb();
|
|
75
|
+
resetFleetForTests();
|
|
76
|
+
});
|
|
77
|
+
after(() => {
|
|
78
|
+
rmSync(home, { recursive: true, force: true });
|
|
79
|
+
if (priorHome === undefined)
|
|
80
|
+
delete process.env['CRTR_HOME'];
|
|
81
|
+
else
|
|
82
|
+
process.env['CRTR_HOME'] = priorHome;
|
|
83
|
+
});
|
|
84
|
+
test('a parked node with a roadmap re-enters fresh even when the caller asked to resume, and the marker clears', () => {
|
|
85
|
+
parkResident('parked', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
86
|
+
const result = reviveNode('parked', { resume: true });
|
|
87
|
+
assert.equal(result.resumed, false, 'the concluded transcript is not replayed');
|
|
88
|
+
assert.deepEqual(launches, [{ nodeId: 'parked', resuming: false }]);
|
|
89
|
+
const reopened = getNode('parked');
|
|
90
|
+
assert.equal(reopened.status, 'active');
|
|
91
|
+
assert.equal(reopened.intent ?? null, null, 'the park marker clears on revival');
|
|
92
|
+
assert.equal(reopened.cycle_pending, true, 'the fresh cycle is provisional until session_start confirms it');
|
|
93
|
+
});
|
|
94
|
+
test('a parked node grounded only by a goal still re-enters fresh', () => {
|
|
95
|
+
parkResident('goal-only', { goal: 'Watch the deploy lane.' });
|
|
96
|
+
assert.equal(reviveNode('goal-only', { resume: true }).resumed, false);
|
|
97
|
+
});
|
|
98
|
+
test('a parked node with no goal and no roadmap resumes its transcript rather than waking amnesiac', () => {
|
|
99
|
+
parkResident('bare');
|
|
100
|
+
const result = reviveNode('bare', { resume: true });
|
|
101
|
+
assert.equal(result.resumed, true, 'the degraded park has nothing on disk to ground a fresh window');
|
|
102
|
+
assert.deepEqual(launches, [{ nodeId: 'bare', resuming: true }]);
|
|
103
|
+
});
|
|
104
|
+
test('an ordinary done node with a roadmap still resumes — only the park marker vetoes', () => {
|
|
105
|
+
parkResident('finished', { roadmap: '# Roadmap\n\n- one open item\n' });
|
|
106
|
+
transition('finished', 'revive');
|
|
107
|
+
transition('finished', 'finish');
|
|
108
|
+
assert.equal(reviveNode('finished', { resume: true }).resumed, true);
|
|
109
|
+
});
|