@crouton-kit/crouter 0.3.17 → 0.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/human-surface-target.test.ts
|
|
2
|
+
//
|
|
3
|
+
// BUG REGRESSION: `crtr human ask|approve|review|notify` surfaced its humanloop
|
|
4
|
+
// TUI in the backstage `crtr` session (the asking node's own pane) — a session
|
|
5
|
+
// the user never watches — because spawnAndDetach was called with no `-t`
|
|
6
|
+
// target. The fix routes the TUI to the HIGHEST FOCUSED node of the asking
|
|
7
|
+
// node's graph (the viewport the user is actually watching the work in).
|
|
8
|
+
//
|
|
9
|
+
// This locks in the PURE selection (`graphSurfaceTarget`, db-only, no tmux):
|
|
10
|
+
// walk the asking node's spine to its root, enumerate the tree root-first, and
|
|
11
|
+
// return the focus row of the node closest to the root that occupies a viewport.
|
|
12
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { createNode, subscribe } from '../canvas/canvas.js';
|
|
18
|
+
import { openFocusRow, closeFocusRow, getFocusByNode } from '../canvas/focuses.js';
|
|
19
|
+
import { closeDb } from '../canvas/db.js';
|
|
20
|
+
import { graphSurfaceTarget } from '../runtime/placement.js';
|
|
21
|
+
let home;
|
|
22
|
+
let savedTmux;
|
|
23
|
+
function node(id, parent) {
|
|
24
|
+
return {
|
|
25
|
+
node_id: id,
|
|
26
|
+
name: id,
|
|
27
|
+
created: new Date().toISOString(),
|
|
28
|
+
cwd: '/tmp/work',
|
|
29
|
+
kind: 'developer',
|
|
30
|
+
mode: 'base',
|
|
31
|
+
lifecycle: 'terminal',
|
|
32
|
+
status: 'active',
|
|
33
|
+
parent,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
before(() => {
|
|
37
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-human-surface-'));
|
|
38
|
+
process.env['CRTR_HOME'] = home;
|
|
39
|
+
savedTmux = process.env['TMUX'];
|
|
40
|
+
delete process.env['TMUX']; // PURE: graphSurfaceTarget never touches tmux
|
|
41
|
+
});
|
|
42
|
+
after(() => {
|
|
43
|
+
closeDb();
|
|
44
|
+
if (savedTmux !== undefined)
|
|
45
|
+
process.env['TMUX'] = savedTmux;
|
|
46
|
+
rmSync(home, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
// Graph R → M → W (parent edges + the auto-subscribe spine the runtime builds:
|
|
49
|
+
// a parent subscribes_to its child, so view(R) walks down to M then W).
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
closeDb();
|
|
52
|
+
rmSync(home, { recursive: true, force: true });
|
|
53
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-human-surface-'));
|
|
54
|
+
process.env['CRTR_HOME'] = home;
|
|
55
|
+
createNode(node('R', null));
|
|
56
|
+
createNode(node('M', 'R'));
|
|
57
|
+
createNode(node('W', 'M'));
|
|
58
|
+
subscribe('R', 'M');
|
|
59
|
+
subscribe('M', 'W');
|
|
60
|
+
});
|
|
61
|
+
test('highest focused = the root when only the root is on screen', () => {
|
|
62
|
+
openFocusRow('f-r', '%10', 'work', 'R');
|
|
63
|
+
const t = graphSurfaceTarget('W');
|
|
64
|
+
assert.equal(t?.node_id, 'R');
|
|
65
|
+
assert.equal(t?.pane, '%10');
|
|
66
|
+
});
|
|
67
|
+
test('falls to the focused mid-orchestrator when the root is NOT on screen', () => {
|
|
68
|
+
openFocusRow('f-m', '%20', 'work', 'M');
|
|
69
|
+
assert.equal(graphSurfaceTarget('W')?.node_id, 'M');
|
|
70
|
+
});
|
|
71
|
+
test('picks the SHALLOWEST focused node when several are on screen', () => {
|
|
72
|
+
openFocusRow('f-m', '%20', 'work', 'M');
|
|
73
|
+
openFocusRow('f-w', '%30', 'work', 'W');
|
|
74
|
+
// M is closer to the root than W → M wins.
|
|
75
|
+
assert.equal(graphSurfaceTarget('W')?.node_id, 'M');
|
|
76
|
+
openFocusRow('f-r', '%10', 'work', 'R');
|
|
77
|
+
// Root trumps everything.
|
|
78
|
+
assert.equal(graphSurfaceTarget('W')?.node_id, 'R');
|
|
79
|
+
});
|
|
80
|
+
test('null when nothing in the graph is on screen (caller falls back)', () => {
|
|
81
|
+
assert.equal(graphSurfaceTarget('W'), null);
|
|
82
|
+
});
|
|
83
|
+
test('a focus row with no pane is skipped, not selected', () => {
|
|
84
|
+
openFocusRow('f-r', null, null, 'R'); // focus exists but not yet placed on a pane
|
|
85
|
+
openFocusRow('f-m', '%20', 'work', 'M');
|
|
86
|
+
assert.equal(graphSurfaceTarget('W')?.node_id, 'M');
|
|
87
|
+
});
|
|
88
|
+
test('the asking node itself, when it is the focused root, is returned', () => {
|
|
89
|
+
openFocusRow('f-r', '%10', 'work', 'R');
|
|
90
|
+
assert.equal(graphSurfaceTarget('R')?.node_id, 'R');
|
|
91
|
+
});
|
|
92
|
+
test('sanity: a closed focus drops out of the selection', () => {
|
|
93
|
+
openFocusRow('f-m', '%20', 'work', 'M');
|
|
94
|
+
assert.equal(graphSurfaceTarget('W')?.node_id, 'M');
|
|
95
|
+
closeFocusRow('f-m');
|
|
96
|
+
assert.equal(getFocusByNode('M'), null);
|
|
97
|
+
assert.equal(graphSurfaceTarget('W'), null);
|
|
98
|
+
});
|
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
// the regression guard for the old "build had hidden side effects" smell.
|
|
8
8
|
import { test, before, after, beforeEach } from 'node:test';
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
|
-
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
11
|
-
import {
|
|
10
|
+
import { mkdtempSync, rmSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { createNode, subscribe } from '../canvas/canvas.js';
|
|
14
|
+
import { reportsDir } from '../canvas/paths.js';
|
|
14
15
|
import { closeDb } from '../canvas/db.js';
|
|
15
16
|
import { drainBearings, buildReviveKickoff, writeYieldMessage, yieldMessagePath, } from '../runtime/kickoff.js';
|
|
16
17
|
import { appendInbox, readCursor } from '../feed/inbox.js';
|
|
@@ -28,7 +29,9 @@ function node(id) {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
before(() => {
|
|
31
|
-
|
|
32
|
+
// Under homedir (not tmpdir) so report paths fall under the home prefix and
|
|
33
|
+
// exercise the tildify() collapse in reportHistoryLines.
|
|
34
|
+
home = mkdtempSync(join(homedir(), '.crtr-kickoff-'));
|
|
32
35
|
process.env['CRTR_HOME'] = home;
|
|
33
36
|
});
|
|
34
37
|
beforeEach(() => {
|
|
@@ -70,6 +73,37 @@ test('the feed block frames awaiting workers as alive + auto-waking, so a fresh
|
|
|
70
73
|
assert.ok(/wake you the moment/.test(msg), 'states the wake is automatic on push');
|
|
71
74
|
assert.ok(/still working, not stalled/.test(msg), 'frames the empty feed as expected, not a problem');
|
|
72
75
|
});
|
|
76
|
+
test('a fresh revive is pointed at its subscriptions\' on-disk report history (catch-up bug)', () => {
|
|
77
|
+
// Regression for the fresh-revive catch-up bug: on a refresh-yield
|
|
78
|
+
// (resume:false) the old conversation is gone AND the inbox cursor has already
|
|
79
|
+
// advanced past everything drained pre-yield, so the revived node loses sight
|
|
80
|
+
// of reports its subscriptions pushed BEFORE the yield. The bodies persist
|
|
81
|
+
// forever at reports/<ts>-<kind>.md; the kickoff must point the node at those
|
|
82
|
+
// existing paths so it can catch up. Asserts the paths are rendered.
|
|
83
|
+
const parent = createNode(node('p1'));
|
|
84
|
+
const worker = createNode(node('w1'));
|
|
85
|
+
subscribe(parent.node_id, worker.node_id);
|
|
86
|
+
// The worker pushed two reports BEFORE the parent's refresh-yield.
|
|
87
|
+
const dir = reportsDir(worker.node_id);
|
|
88
|
+
mkdirSync(dir, { recursive: true });
|
|
89
|
+
const older = join(dir, '20260608T120000-update.md');
|
|
90
|
+
const newer = join(dir, '20260608T130000-final.md');
|
|
91
|
+
writeFileSync(older, '---\n---\nearly progress', 'utf8');
|
|
92
|
+
writeFileSync(newer, '---\n---\nthe result', 'utf8');
|
|
93
|
+
const msg = buildReviveKickoff(parent, drainBearings(parent));
|
|
94
|
+
// Both existing report PATHS surface (most recent first), so the revived node
|
|
95
|
+
// can dereference the history its advanced cursor would otherwise hide. They
|
|
96
|
+
// render home-relative with a leading ~ (the absolute home prefix is wasted
|
|
97
|
+
// context tokens; ~ is still dereferenceable by the revived node).
|
|
98
|
+
const olderRel = '~' + older.slice(homedir().length);
|
|
99
|
+
const newerRel = '~' + newer.slice(homedir().length);
|
|
100
|
+
assert.ok(msg.includes(olderRel), 'older report path surfaced, home-relative (~)');
|
|
101
|
+
assert.ok(msg.includes(newerRel), 'newer report path surfaced, home-relative (~)');
|
|
102
|
+
assert.ok(!msg.includes(older), 'absolute home prefix not rendered');
|
|
103
|
+
assert.ok(msg.indexOf(newerRel) < msg.indexOf(olderRel), 'most recent report listed first');
|
|
104
|
+
// ...and the hint that the full inbox history is replayable cursor-independently.
|
|
105
|
+
assert.ok(/feed read --all/.test(msg), 'points at the cursor-independent full-history replay');
|
|
106
|
+
});
|
|
73
107
|
test('buildReviveKickoff is pure — building twice eats nothing', () => {
|
|
74
108
|
const id = 'n2';
|
|
75
109
|
const meta = createNode(node(id));
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// AXIS: LIVE MUTATION of the 2×2 state vector (mode {base,orchestrator} ×
|
|
4
4
|
// lifecycle {terminal,resident}) while a node is ACTIVE/LIVE — driven through
|
|
5
|
-
// the REAL `crtr node lifecycle` / `node promote` / `node demote`
|
|
6
|
-
// against a live fake-pi, with the REAL stophook / kickoff / daemon hooks doing
|
|
5
|
+
// the REAL `crtr node lifecycle` / `node promote` / `node demote` / `node recycle`
|
|
6
|
+
// CLI verbs against a live fake-pi, with the REAL stophook / kickoff / daemon hooks doing
|
|
7
7
|
// the work. Every assertion reads the canvas data layer and is checked against
|
|
8
8
|
// the state-model ORACLE (mq1su40t .../state-model.md).
|
|
9
9
|
//
|
|
@@ -219,54 +219,71 @@ test('live mode flip: promote base→orchestrator recomposes persona_ack at turn
|
|
|
219
219
|
}
|
|
220
220
|
});
|
|
221
221
|
// ===========================================================================
|
|
222
|
-
// (b)
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
222
|
+
// (b) THE demote / recycle SPLIT — two DISTINCT verbs after the rename:
|
|
223
|
+
// • `node demote` flips a LIVE node's lifecycle→TERMINAL IN PLACE — it keeps
|
|
224
|
+
// its pane, its MODE, and its parentage, keeps running, is NOT finalized; it
|
|
225
|
+
// now merely owes a final up the spine (vision F5). It is NOT an
|
|
226
|
+
// orchestrator→base mode flip — MODE is untouched (so persona.ts
|
|
227
|
+
// `baseModeGuidance` stays unreachable via live mutation, as before).
|
|
228
|
+
// • `node recycle` is FINISH+RECYCLE — push final → done, then recycle the
|
|
229
|
+
// pane into a FRESH general/base/resident root (a DIFFERENT node). The
|
|
230
|
+
// recycled node keeps mode=orchestrator (it is merely `done`).
|
|
231
|
+
// This test drives BOTH real verbs on one live node and pins each behavior.
|
|
232
232
|
// ===========================================================================
|
|
233
|
-
test('node demote
|
|
233
|
+
test('node demote flips lifecycle→terminal IN PLACE; node recycle is FINISH+RECYCLE', { skip: SKIP, timeout: 120_000 }, async () => {
|
|
234
234
|
const h = await createHarness({ sessionPrefix: 'crtr-live-demote' });
|
|
235
235
|
try {
|
|
236
236
|
const A = h.spawnRoot('resident root');
|
|
237
237
|
const B = await h.spawnChild(A, 'do the work', { kind: 'developer' });
|
|
238
|
-
//
|
|
238
|
+
// Make B resident + orchestrator so the demote's flip→terminal is visible
|
|
239
|
+
// and we can prove MODE/parentage survive it.
|
|
240
|
+
assert.equal(h.cli(B, ['node', 'lifecycle', 'resident', '--node', B]).code, 0, 'B → resident');
|
|
239
241
|
assert.equal(h.cli(B, ['node', 'promote', '--kind', 'developer']).code, 0, 'promote B');
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
{
|
|
243
|
+
const b = h.node(B);
|
|
244
|
+
assert.equal(b.lifecycle, 'resident', 'B resident before demote');
|
|
245
|
+
assert.equal(b.mode, 'orchestrator', 'B orchestrator before demote');
|
|
246
|
+
}
|
|
247
|
+
const bParent = h.node(B).parent;
|
|
248
|
+
// --- node demote: flip-to-terminal IN PLACE. Keeps B alive, MODE/parentage
|
|
249
|
+
// untouched, NOT finalized.
|
|
250
|
+
const dem = h.cli(B, ['node', 'demote', '--node', B]);
|
|
251
|
+
assert.equal(dem.code, 0, `node demote exit 0\n${dem.stderr}`);
|
|
252
|
+
assert.match(dem.stdout, /<demoted /, `demote rendered\n${dem.stdout}`);
|
|
253
|
+
{
|
|
254
|
+
const b = h.node(B);
|
|
255
|
+
assert.equal(b.lifecycle, 'terminal', 'demote flips lifecycle→terminal IN PLACE');
|
|
256
|
+
assert.equal(b.mode, 'orchestrator', 'demote leaves MODE untouched (not an orchestrator→base flip)');
|
|
257
|
+
assert.equal(b.parent, bParent, 'demote leaves parentage unchanged');
|
|
258
|
+
assert.equal(b.status, 'active', 'demote does NOT finish B — it keeps running in place');
|
|
259
|
+
assert.notEqual(b.intent ?? null, 'done', 'demote does NOT finalize B');
|
|
260
|
+
}
|
|
261
|
+
// --- node recycle: FINISH + RECYCLE the SAME pane into a fresh root.
|
|
262
|
+
// Resolve B's live %pane_id from its window (the row's `pane` is null
|
|
263
|
+
// until a reconcile; the spawn path records only window+session).
|
|
264
|
+
const pane = firstPaneOf(h.node(B).window);
|
|
245
265
|
assert.ok(typeof pane === 'string' && pane !== '', 'B has a live pane to recycle');
|
|
246
|
-
//
|
|
247
|
-
const res = h.cli(B, ['node', '
|
|
248
|
-
assert.equal(res.code, 0, `
|
|
249
|
-
// The leaf renders `<
|
|
250
|
-
assert.match(res.stdout, /<
|
|
266
|
+
// RECYCLE via the real verb (TMUX_PANE is scrubbed from child env → pass --pane).
|
|
267
|
+
const res = h.cli(B, ['node', 'recycle', '--node', B, '--pane', pane]);
|
|
268
|
+
assert.equal(res.code, 0, `recycle exit 0\n${res.stderr}`);
|
|
269
|
+
// The leaf renders `<recycled ... finalized=".." new_root=".."/>` (not JSON).
|
|
270
|
+
assert.match(res.stdout, /<recycled /, `recycle recycled the pane\n${res.stdout}`);
|
|
251
271
|
const newRoot = /new_root="([^"]+)"/.exec(res.stdout)?.[1];
|
|
252
272
|
const finalized = /finalized="true"/.test(res.stdout);
|
|
253
|
-
//
|
|
273
|
+
// The recycled node is FINISHED, not mode-flipped.
|
|
254
274
|
{
|
|
255
275
|
const b = h.node(B);
|
|
256
|
-
assert.equal(b.status, 'done', '
|
|
276
|
+
assert.equal(b.status, 'done', 'recycled node → done (finished), NOT re-roled');
|
|
257
277
|
assert.equal(b.intent, 'done', 'intent=done (finalize), per the push-final path');
|
|
258
|
-
assert.equal(b.mode, 'orchestrator', '
|
|
259
|
-
assert.ok(finalized, '
|
|
278
|
+
assert.equal(b.mode, 'orchestrator', 'recycled node KEEPS mode=orchestrator — recycle is NOT a mode flip');
|
|
279
|
+
assert.ok(finalized, 'recycle pushed a final for the node');
|
|
260
280
|
}
|
|
261
|
-
// The fresh root is a DIFFERENT, BASE×RESIDENT node
|
|
262
|
-
// comes from, not a mutation of B.
|
|
281
|
+
// The fresh root is a DIFFERENT, BASE×RESIDENT node.
|
|
263
282
|
assert.ok(typeof newRoot === 'string' && newRoot !== B, 'a fresh root (≠ B) was minted');
|
|
264
283
|
{
|
|
265
284
|
const fresh = h.node(newRoot);
|
|
266
285
|
assert.deepEqual(persona(fresh), { mode: 'base', lifecycle: 'resident' }, 'recycled root is born base×resident (general)');
|
|
267
|
-
|
|
268
|
-
// drift steer: that persona path is unreachable through live mutation.
|
|
269
|
-
assert.deepEqual(fresh.persona_ack, { mode: 'base', lifecycle: 'resident' }, 'fresh root born acked base×resident — no orchestrator→base drift will ever fire');
|
|
286
|
+
assert.deepEqual(fresh.persona_ack, { mode: 'base', lifecycle: 'resident' }, 'fresh root born acked base×resident');
|
|
270
287
|
}
|
|
271
288
|
}
|
|
272
289
|
finally {
|
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
// Tests for the
|
|
2
|
-
// 1. The
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// 3. seed* are guarded/idempotent (never clobber an evolved index).
|
|
6
|
-
// 4. promote() seeds ALL THREE stores, guarded across re-promotion.
|
|
7
|
-
// 5. The kernel + promotion guidance name the type\u2192store mapping.
|
|
8
|
-
//
|
|
9
|
-
// CRTR_HOME isolation, like context-intro.test.ts.
|
|
1
|
+
// Tests for the substrate memory surface in the orchestration kernel:
|
|
2
|
+
// 1. The kernel names the three memory scopes and the document substrate flow
|
|
3
|
+
// (crtr memory write/list/find/read).
|
|
4
|
+
// 2. Promotion guidance names the three stores without re-listing their dirs.
|
|
10
5
|
//
|
|
11
6
|
// Run: node --import tsx/esm --test src/core/__tests__/memory.test.ts
|
|
12
7
|
import { test, before, beforeEach, after } from 'node:test';
|
|
13
8
|
import assert from 'node:assert/strict';
|
|
14
|
-
import { mkdtempSync,
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
15
10
|
import { tmpdir } from 'node:os';
|
|
16
11
|
import { join } from 'node:path';
|
|
17
12
|
import { closeDb } from '../canvas/db.js';
|
|
18
|
-
import { crtrHome } from '../canvas/paths.js';
|
|
19
|
-
import { mangleCwd } from '../artifact.js';
|
|
20
13
|
import { spawnNode } from '../runtime/nodes.js';
|
|
21
14
|
import { promote } from '../runtime/promote.js';
|
|
22
15
|
import { personaDrift } from '../runtime/persona.js';
|
|
23
16
|
import { loadKernel } from '../personas/index.js';
|
|
24
|
-
import { memoryDir, memoryPath, userMemoryDir, userMemoryPath, hasUserMemory, readUserMemory, seedUserMemory, projectKey, projectMemoryDir, projectMemoryPath, hasProjectMemory, readProjectMemory, seedProjectMemory, hasMemory, USER_MEMORY_TEMPLATE, PROJECT_MEMORY_TEMPLATE, } from '../runtime/memory.js';
|
|
25
17
|
let home;
|
|
26
|
-
// Scratch repos created per-test for the git-root keying cases; tracked so they
|
|
27
|
-
// can be removed in `after` regardless of which test created them.
|
|
28
|
-
const scratch = [];
|
|
29
18
|
before(() => {
|
|
30
19
|
home = mkdtempSync(join(tmpdir(), 'crtr-memory-'));
|
|
31
20
|
process.env['CRTR_HOME'] = home;
|
|
@@ -37,116 +26,35 @@ beforeEach(() => {
|
|
|
37
26
|
after(() => {
|
|
38
27
|
closeDb();
|
|
39
28
|
rmSync(home, { recursive: true, force: true });
|
|
40
|
-
for (const d of scratch)
|
|
41
|
-
rmSync(d, { recursive: true, force: true });
|
|
42
29
|
delete process.env['CRTR_HOME'];
|
|
43
30
|
});
|
|
44
31
|
// ---------------------------------------------------------------------------
|
|
45
|
-
//
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
test('the three stores all live under crtrHome, at their scoped paths', () => {
|
|
48
|
-
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
49
|
-
// user-global: <crtrHome>/memory/
|
|
50
|
-
assert.equal(userMemoryDir(), join(crtrHome(), 'memory'));
|
|
51
|
-
assert.equal(userMemoryPath(), join(crtrHome(), 'memory', 'MEMORY.md'));
|
|
52
|
-
// project: <crtrHome>/projects/<key>/memory/
|
|
53
|
-
assert.equal(projectMemoryDir('/tmp/work'), join(crtrHome(), 'projects', projectKey('/tmp/work'), 'memory'));
|
|
54
|
-
assert.equal(projectMemoryPath('/tmp/work'), join(projectMemoryDir('/tmp/work'), 'MEMORY.md'));
|
|
55
|
-
// node-local: <crtrHome>/nodes/<id>/context/memory/ (unchanged)
|
|
56
|
-
assert.ok(memoryDir(meta.node_id).startsWith(join(crtrHome(), 'nodes', meta.node_id)));
|
|
57
|
-
assert.ok(memoryPath(meta.node_id).endsWith('/context/memory/MEMORY.md'));
|
|
58
|
-
});
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// projectKey: git-root vs not-in-a-repo fallback
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
test('projectKey resolves the git-repo-root by walking up for a .git entry', () => {
|
|
63
|
-
const repo = mkdtempSync(join(tmpdir(), 'crtr-repo-'));
|
|
64
|
-
scratch.push(repo);
|
|
65
|
-
mkdirSync(join(repo, '.git')); // a .git directory marks the repo root
|
|
66
|
-
const nested = join(repo, 'pkg', 'src');
|
|
67
|
-
mkdirSync(nested, { recursive: true });
|
|
68
|
-
// A cwd deep inside the repo keys to the repo ROOT, not the cwd.
|
|
69
|
-
assert.equal(projectKey(nested), mangleCwd(repo));
|
|
70
|
-
assert.equal(projectKey(repo), mangleCwd(repo));
|
|
71
|
-
// A .git FILE (worktree/submodule) is recognized the same way.
|
|
72
|
-
const wt = mkdtempSync(join(tmpdir(), 'crtr-wt-'));
|
|
73
|
-
scratch.push(wt);
|
|
74
|
-
writeFileSync(join(wt, '.git'), 'gitdir: /elsewhere\n');
|
|
75
|
-
assert.equal(projectKey(join(wt, 'a')), mangleCwd(wt));
|
|
76
|
-
});
|
|
77
|
-
test('projectKey falls back to the mangled cwd when not inside a repo', () => {
|
|
78
|
-
const bare = mkdtempSync(join(tmpdir(), 'crtr-bare-'));
|
|
79
|
-
scratch.push(bare);
|
|
80
|
-
const sub = join(bare, 'x', 'y');
|
|
81
|
-
mkdirSync(sub, { recursive: true });
|
|
82
|
-
// No .git anywhere up the tree \u2192 the cwd itself is the key.
|
|
83
|
-
assert.equal(projectKey(sub), mangleCwd(sub));
|
|
84
|
-
});
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// seed*: guarded / idempotent
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
test('seedUserMemory / seedProjectMemory write the template once, then never clobber', () => {
|
|
89
|
-
assert.equal(hasUserMemory(), false);
|
|
90
|
-
assert.equal(seedUserMemory(), true, 'first seed writes');
|
|
91
|
-
assert.equal(readUserMemory(), USER_MEMORY_TEMPLATE);
|
|
92
|
-
assert.equal(hasUserMemory(), true);
|
|
93
|
-
const evolved = '# Memory\n\n- [Likes terse replies](terse.md) \u2014 keep it short\n';
|
|
94
|
-
writeFileSync(userMemoryPath(), evolved);
|
|
95
|
-
assert.equal(seedUserMemory(), false, 'second seed is a no-op');
|
|
96
|
-
assert.equal(readUserMemory(), evolved, 'evolved index left untouched');
|
|
97
|
-
assert.equal(seedProjectMemory('/tmp/work'), true, 'project first seed writes');
|
|
98
|
-
assert.equal(readProjectMemory('/tmp/work'), PROJECT_MEMORY_TEMPLATE);
|
|
99
|
-
assert.equal(seedProjectMemory('/tmp/work'), false, 'project second seed is a no-op');
|
|
100
|
-
});
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// promote(): seeds all three, guarded across re-promotion
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
test('promote() seeds all three stores and surfaces all three paths', () => {
|
|
105
|
-
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
106
|
-
assert.equal(hasMemory(meta.node_id), false);
|
|
107
|
-
assert.equal(hasUserMemory(), false);
|
|
108
|
-
assert.equal(hasProjectMemory('/tmp/work'), false);
|
|
109
|
-
const res = promote(meta.node_id);
|
|
110
|
-
assert.ok(hasMemory(meta.node_id), 'node-local seeded');
|
|
111
|
-
assert.ok(hasUserMemory(), 'user-global seeded');
|
|
112
|
-
assert.ok(hasProjectMemory('/tmp/work'), 'project seeded');
|
|
113
|
-
assert.ok(existsSync(userMemoryDir()), 'user dir created for direct writes');
|
|
114
|
-
assert.ok(existsSync(projectMemoryDir('/tmp/work')), 'project dir created for direct writes');
|
|
115
|
-
assert.equal(res.memoryPath, memoryPath(meta.node_id));
|
|
116
|
-
assert.equal(res.userMemoryPath, userMemoryPath());
|
|
117
|
-
assert.equal(res.projectMemoryPath, projectMemoryPath('/tmp/work'));
|
|
118
|
-
});
|
|
119
|
-
test('promote() is idempotent across re-promotion \u2014 never clobbers evolved stores', () => {
|
|
120
|
-
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
121
|
-
promote(meta.node_id);
|
|
122
|
-
const evolvedUser = '# Memory\n\n- [CTO, terse](cto.md) \u2014 senior, wants density\n';
|
|
123
|
-
const evolvedProject = '# Memory\n\n- [ESM only](esm.md) \u2014 .js extensions required\n';
|
|
124
|
-
writeFileSync(userMemoryPath(), evolvedUser);
|
|
125
|
-
writeFileSync(projectMemoryPath('/tmp/work'), evolvedProject);
|
|
126
|
-
promote(meta.node_id); // re-promote
|
|
127
|
-
assert.equal(readUserMemory(), evolvedUser, 'user store survived re-promotion');
|
|
128
|
-
assert.equal(readProjectMemory('/tmp/work'), evolvedProject, 'project store survived re-promotion');
|
|
129
|
-
});
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// The kernel + guidance name the type\u2192store mapping
|
|
32
|
+
// Kernel + guidance: substrate flow, not MEMORY.md
|
|
132
33
|
// ---------------------------------------------------------------------------
|
|
133
|
-
test('the orchestration kernel names the three stores and the
|
|
34
|
+
test('the orchestration kernel names the three stores and the substrate commands', () => {
|
|
134
35
|
const kernel = loadKernel();
|
|
135
36
|
for (const store of ['user-global', 'project', 'node-local']) {
|
|
136
37
|
assert.ok(kernel.includes(store), `kernel names the ${store} store`);
|
|
137
38
|
}
|
|
138
|
-
// The
|
|
139
|
-
assert.
|
|
140
|
-
assert.
|
|
141
|
-
|
|
142
|
-
|
|
39
|
+
// The substrate commands must be present.
|
|
40
|
+
assert.ok(kernel.includes('crtr memory write'), 'kernel mentions crtr memory write');
|
|
41
|
+
assert.ok(kernel.includes('crtr memory list'), 'kernel mentions crtr memory list');
|
|
42
|
+
assert.ok(kernel.includes('crtr memory find'), 'kernel mentions crtr memory find');
|
|
43
|
+
assert.ok(kernel.includes('crtr memory read'), 'kernel mentions crtr memory read');
|
|
44
|
+
// The old MEMORY.md pointer-line flow must NOT be present.
|
|
45
|
+
assert.ok(!kernel.includes('MEMORY.md'), 'kernel does not mention MEMORY.md');
|
|
46
|
+
assert.ok(!kernel.includes('pointer line'), 'kernel does not teach the pointer-line flow');
|
|
47
|
+
});
|
|
48
|
+
test('promotion guidance references the three stores; no <memory> block', () => {
|
|
143
49
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
144
50
|
promote(meta.node_id);
|
|
145
|
-
|
|
146
|
-
|
|
51
|
+
const drift = personaDrift(meta.node_id);
|
|
52
|
+
assert.ok(drift !== null, 'promotion drifts the persona');
|
|
53
|
+
const guidance = drift.guidance;
|
|
54
|
+
// It NAMES the three stores so a promoting node knows its memory scopes.
|
|
147
55
|
for (const store of ['user-global', 'project', 'node-local']) {
|
|
148
56
|
assert.ok(guidance.includes(store), `guidance names the ${store} store`);
|
|
149
57
|
}
|
|
150
|
-
|
|
151
|
-
assert.ok(guidance.includes(
|
|
58
|
+
// The old <memory> block reference must NOT be present in guidance.
|
|
59
|
+
assert.ok(!guidance.includes('<memory>'), 'guidance does not point at the removed <memory> block');
|
|
152
60
|
});
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// Run: node --import tsx/esm --test src/core/__tests__/persona-subkind.test.ts
|
|
2
2
|
//
|
|
3
|
-
// Scoped persona sub-
|
|
4
|
-
// `<kind>/reviewers/<name>/
|
|
3
|
+
// Scoped persona sub-personas: a kind has specialist reviewer personas at
|
|
4
|
+
// `<kind>/reviewers/<name>/PERSONA.md`, enumerated by `subPersonasFor(kind)` and
|
|
5
5
|
// rendered into that kind's composed prompt (and nowhere else) by `resolve`.
|
|
6
|
-
// Visibility = membership
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// Visibility = membership (the sub-persona's `availableTo`, default = its
|
|
7
|
+
// top-level ancestor kind): only `plan` sees the `plan/reviewers/*` menu; the
|
|
8
|
+
// `reviewers/` grouping dir is transparent so the kind string keeps it; the
|
|
9
|
+
// sub-personas never pollute the global `availableKinds()` list; and a
|
|
10
|
+
// sub-persona itself boots as a real composed persona with the terminal finish
|
|
11
|
+
// contract.
|
|
9
12
|
import { test } from 'node:test';
|
|
10
13
|
import assert from 'node:assert/strict';
|
|
11
14
|
import { resolve } from '../personas/resolve.js';
|
|
12
|
-
import {
|
|
15
|
+
import { subPersonasFor, availableKinds } from '../personas/loader.js';
|
|
13
16
|
const PLAN_REVIEWER_KINDS = [
|
|
14
17
|
'plan/reviewers/architecture-fit',
|
|
15
18
|
'plan/reviewers/code-smells',
|
|
@@ -17,19 +20,19 @@ const PLAN_REVIEWER_KINDS = [
|
|
|
17
20
|
'plan/reviewers/requirements-coverage',
|
|
18
21
|
'plan/reviewers/security',
|
|
19
22
|
];
|
|
20
|
-
const MENU_HEADER = '
|
|
21
|
-
test('
|
|
22
|
-
const subs =
|
|
23
|
-
assert.deepEqual(subs.map((s) => s.kind), PLAN_REVIEWER_KINDS, 'the five plan reviewer kind strings in sorted order');
|
|
23
|
+
const MENU_HEADER = 'Sub-personas you may spawn';
|
|
24
|
+
test('subPersonasFor("plan") returns the five reviewers sorted, each with a non-empty whenToUse', () => {
|
|
25
|
+
const subs = subPersonasFor('plan');
|
|
26
|
+
assert.deepEqual(subs.map((s) => s.kind), PLAN_REVIEWER_KINDS, 'the five plan reviewer kind strings in sorted order — the transparent reviewers/ dir keeps the full kind path');
|
|
24
27
|
for (const s of subs) {
|
|
25
|
-
assert.ok(s.
|
|
28
|
+
assert.ok(s.whenToUse.length > 0, `${s.kind} carries a non-empty whenToUse`);
|
|
26
29
|
}
|
|
27
30
|
});
|
|
28
|
-
test('
|
|
29
|
-
assert.deepEqual(
|
|
30
|
-
assert.deepEqual(
|
|
31
|
+
test('availability is membership: a kind with no available sub-personas yields []', () => {
|
|
32
|
+
assert.deepEqual(subPersonasFor('explore'), [], 'no sub-persona is availableTo explore');
|
|
33
|
+
assert.deepEqual(subPersonasFor('plan/reviewers/security'), [], 'the five reviewers default availableTo:[plan] — none are available to a reviewer kind');
|
|
31
34
|
});
|
|
32
|
-
test('availableKinds() contains no plan/reviewers/* — sub-
|
|
35
|
+
test('availableKinds() contains no plan/reviewers/* — sub-personas never pollute the global list', () => {
|
|
33
36
|
const kinds = availableKinds();
|
|
34
37
|
for (const k of PLAN_REVIEWER_KINDS) {
|
|
35
38
|
assert.ok(!kinds.includes(k), `${k} must not appear in availableKinds()`);
|
|
@@ -267,6 +267,11 @@ test('detachToBackground: a FOCUSED node sent to the backstage CLOSES its focus
|
|
|
267
267
|
createNode(node('N', { pane: focusPane, tmux_session: user, window: userWindow, status: 'active', pi_pid: process.pid, home_session: back }));
|
|
268
268
|
openFocusRow('f1', focusPane, user, 'N'); // N is the focus occupant
|
|
269
269
|
assert.equal(getFocusByNode('N')?.focus_id, 'f1', 'precondition: N is focused');
|
|
270
|
+
// N must read as GENERATING for the relocate-to-backstage path: detach now
|
|
271
|
+
// gates the break-pane (Bug 1 / Invariant P) on isGenerating = busy &&
|
|
272
|
+
// pidAlive. pi_pid is this (alive) process; set the busy marker so N is
|
|
273
|
+
// genuinely mid-turn (otherwise the gate would RELEASE + reap it instead).
|
|
274
|
+
markBusy('N');
|
|
270
275
|
const ok = detachToBackground('N', focusPane);
|
|
271
276
|
assert.equal(ok, true, 'the break to the backstage succeeded');
|
|
272
277
|
// The fix: N is now generating-but-UNFOCUSED, so its focus row is CLOSED.
|