@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
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
// never realized, so closePane no-ops and only the DB effects are asserted):
|
|
6
6
|
//
|
|
7
7
|
// • handFocusToManager(focusId, managerId) — the §1.6 manager-takeover, a PURE
|
|
8
|
-
// DB occupant swap. Returns true (TAKEOVER) only when
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// DB occupant swap. Returns true (TAKEOVER) only when a successor will
|
|
9
|
+
// GENUINELY claim the frozen pane — a live manager (swapped in) or a dormant
|
|
10
|
+
// manager the daemon will revive (status='idle' && intent='idle-release').
|
|
11
|
+
// Returns false (caller disarms the freeze + closes the focus) for every
|
|
12
|
+
// other case; each guard is asserted distinctly.
|
|
11
13
|
// • tearDownNode(nodeId) — close/reset teardown: close the focus row it
|
|
12
14
|
// occupies and null its LOCATION.
|
|
13
15
|
import { test, before, after, beforeEach } from 'node:test';
|
|
@@ -62,10 +64,13 @@ after(() => {
|
|
|
62
64
|
// ---------------------------------------------------------------------------
|
|
63
65
|
// handFocusToManager (pure DB) — §1.6 manager-takeover + its three false-guards.
|
|
64
66
|
// ---------------------------------------------------------------------------
|
|
65
|
-
test('handFocusToManager:
|
|
67
|
+
test('handFocusToManager: dormant idle-release manager → TAKEOVER (repoints the row, returns true)', () => {
|
|
66
68
|
openFocusRow('f', '%m', 'Sa', 'M');
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
// The daemon's superviseTick second pass revives a node ONLY when it is idle +
|
|
70
|
+
// idle-release (crtrd.ts ~309) — that is the ONLY dormant manager that will be
|
|
71
|
+
// brought into the frozen %m, so it is the ONLY dormant takeover that returns true.
|
|
72
|
+
createNode(node('mgr', { status: 'idle', intent: 'idle-release' }));
|
|
73
|
+
assert.equal(handFocusToManager('f', 'mgr'), true, 'a manager the daemon WILL revive takes the focus');
|
|
69
74
|
assert.equal(getFocusByNode('mgr')?.focus_id, 'f', 'the row now shows the manager');
|
|
70
75
|
assert.equal(getFocusByNode('M'), null, 'the finished node no longer occupies it');
|
|
71
76
|
// Non-vacuous: a no-op impl returns false / leaves M as occupant → both the
|
|
@@ -140,12 +145,14 @@ test('handFocusToManager: LIVE backstage manager → swaps its pane INTO the foc
|
|
|
140
145
|
spawnSync('tmux', ['kill-session', '-t', back], { stdio: 'ignore' });
|
|
141
146
|
}
|
|
142
147
|
});
|
|
143
|
-
test('handFocusToManager: DORMANT manager (dead pane) → occupant repointed, NO swap, focus pane UNCHANGED', () => {
|
|
148
|
+
test('handFocusToManager: DORMANT idle-release manager (dead pane) → occupant repointed, NO swap, focus pane UNCHANGED', () => {
|
|
144
149
|
openFocusRow('f', '%focus', 'Suser', 'M');
|
|
145
|
-
// The manager
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
150
|
+
// The manager is dormant + idle-release (the ONLY dormant manager the daemon
|
|
151
|
+
// revives): its pane is recorded but NOT a live tmux pane (its dead-pi window
|
|
152
|
+
// collapsed) and its pi is dead. isNodePaneAlive(mgr) is therefore false, so the
|
|
153
|
+
// live-swap is skipped — the external daemon later respawns it into the frozen
|
|
154
|
+
// %focus, exactly because status='idle' && intent='idle-release'.
|
|
155
|
+
createNode(node('mgr', { pane: '%mgr-dead', tmux_session: 'back', window: '@wb', pi_pid: null, status: 'idle', intent: 'idle-release' }));
|
|
149
156
|
assert.equal(handFocusToManager('f', 'mgr'), true, 'still a takeover (occupant repointed)');
|
|
150
157
|
assert.equal(getFocusByNode('mgr')?.focus_id, 'f', 'the manager occupies the focus row (DB repoint)');
|
|
151
158
|
assert.equal(getFocusByNode('M'), null, 'the finished node no longer occupies it');
|
|
@@ -162,6 +169,42 @@ test('handFocusToManager: DORMANT manager (dead pane) → occupant repointed, NO
|
|
|
162
169
|
// manager having a pane.
|
|
163
170
|
});
|
|
164
171
|
// ---------------------------------------------------------------------------
|
|
172
|
+
// BUG REGRESSION (dead-focus-pane): handFocusToManager must return FALSE — so
|
|
173
|
+
// the stophook caller runs closeFocusToShell to DISARM remain-on-exit and the
|
|
174
|
+
// pane REAPS on exit — whenever NO successor will actually claim the frozen pane.
|
|
175
|
+
// Pre-fix it repointed the occupant and returned true for ANY non-null,
|
|
176
|
+
// not-already-focused manager (even done/dead/canceled, idle-but-not-idle-
|
|
177
|
+
// release, or a live-but-paneless inline root). The daemon revives NONE of those
|
|
178
|
+
// into the frozen pane, so closeFocusToShell was skipped, remain-on-exit stayed
|
|
179
|
+
// ON, and the focus pane FROZE forever as a dead pane with no reaper.
|
|
180
|
+
// Diagnosis: nodes/mq32wjve-68de0c31/context/dead-focus-pane-fix.md
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
test('handFocusToManager (BUG REGRESSION): a manager the daemon will NEVER revive → false, occupant UNCHANGED (caller disarms + reaps)', () => {
|
|
183
|
+
openFocusRow('f', '%focus', 'Suser', 'M'); // M occupies the frozen focus pane
|
|
184
|
+
// (a) DONE manager — the daemon ignores done nodes entirely (the most common
|
|
185
|
+
// trigger: demoting a child to terminal whose manager already finished).
|
|
186
|
+
createNode(node('mgrDone', { status: 'done', pane: '%d', tmux_session: 'back', window: '@wd' }));
|
|
187
|
+
assert.equal(handFocusToManager('f', 'mgrDone'), false, 'a DONE manager will never be revived → caller must disarm + reap');
|
|
188
|
+
assert.equal(getFocusByNode('mgrDone'), null, 'a DONE manager is NOT repointed into the focus row');
|
|
189
|
+
// (b) DORMANT but NOT idle-release — idle with a different intent; crtrd's
|
|
190
|
+
// second pass (idle && idle-release) skips it, so it never enters the pane.
|
|
191
|
+
createNode(node('mgrIdleDone', { status: 'idle', intent: 'done', pane: '%i', tmux_session: 'back', window: '@wi' }));
|
|
192
|
+
assert.equal(handFocusToManager('f', 'mgrIdleDone'), false, 'idle but NOT idle-release → daemon never revives it → false');
|
|
193
|
+
assert.equal(getFocusByNode('mgrIdleDone'), null, 'a non-idle-release manager is NOT repointed');
|
|
194
|
+
// (c) LIVE-but-PANELESS manager (an inline root, pane == null) — the live-swap
|
|
195
|
+
// branch is skipped (no pane to swap) and, being active not idle-release,
|
|
196
|
+
// the daemon never revives it either.
|
|
197
|
+
createNode(node('mgrPaneless', { status: 'active', pi_pid: process.pid, pane: null }));
|
|
198
|
+
assert.equal(handFocusToManager('f', 'mgrPaneless'), false, 'a live-but-paneless inline root cannot claim the pane → false');
|
|
199
|
+
assert.equal(getFocusByNode('mgrPaneless'), null, 'a paneless manager is NOT repointed');
|
|
200
|
+
// The occupant is untouched across all three → the caller (canvas-stophook
|
|
201
|
+
// agent_end done-branch) runs closeFocusToShell, disarming the freeze.
|
|
202
|
+
assert.equal(getFocusById('f')?.node_id, 'M', 'M still occupies its focus → caller disarms remain-on-exit + closes the row');
|
|
203
|
+
// Non-vacuous: the pre-fix impl repointed the occupant + returned true for each
|
|
204
|
+
// of (a)/(b)/(c), so every false return AND every "NOT repointed" assert fails
|
|
205
|
+
// against it — and that stray true would have stranded the pane frozen forever.
|
|
206
|
+
});
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
165
208
|
// tearDownNode (pure DB; no tmux — pane is null so closePane never runs).
|
|
166
209
|
// ---------------------------------------------------------------------------
|
|
167
210
|
test('tearDownNode: closes the focus row M occupied and nulls its LOCATION', () => {
|
|
@@ -109,8 +109,9 @@ test('relaunchRoot parks the old root (canceled, edges intact, no wipe) and mint
|
|
|
109
109
|
assert.equal(existsSync(roadmapPath('root')), true, 'roadmap preserved');
|
|
110
110
|
assert.equal(existsSync(inboxPath('root')), true, 'inbox preserved');
|
|
111
111
|
assert.equal(existsSync(join(reportsDir('root'), '20260101T000000-update.md')), true, 'report preserved');
|
|
112
|
-
// New root: fresh base resident, active, intent=refresh,
|
|
113
|
-
// spawned_by=old,
|
|
112
|
+
// New root: fresh base resident, active, intent=refresh, a clean-slate context
|
|
113
|
+
// dir (only the born-with memory store, nothing inherited), spawned_by=old,
|
|
114
|
+
// focused.
|
|
114
115
|
const fresh = getNode(newId);
|
|
115
116
|
assert.equal(fresh?.parent, null, 'new node is a root');
|
|
116
117
|
assert.equal(fresh?.mode, 'base');
|
|
@@ -122,7 +123,7 @@ test('relaunchRoot parks the old root (canceled, edges intact, no wipe) and mint
|
|
|
122
123
|
assert.equal(fresh?.tmux_session, 'crtr', 'adopted the old root window location');
|
|
123
124
|
assert.equal(fresh?.window, '@7');
|
|
124
125
|
assert.ok(fresh?.launch, 'a fresh base launch spec was written');
|
|
125
|
-
assert.
|
|
126
|
+
assert.deepEqual(readdirSync(contextDir(newId)).sort(), ['memory'], 'fresh context dir holds only the born-with node-local memory store — nothing inherited from the old root');
|
|
126
127
|
// Focus follows content: the focus row the old root held now shows the new root.
|
|
127
128
|
assert.equal(getFocusByNode(newId)?.focus_id, 'fRoot', 'focus row repointed to the new root');
|
|
128
129
|
assert.equal(getFocusByNode('root'), null, 'old root no longer occupies the focus');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/review-render-pane-regression.test.ts
|
|
2
|
+
//
|
|
3
|
+
// BUG REGRESSION: `crtr human review` showed the human RAW markdown — literal
|
|
4
|
+
// `:::callout{...}` / `#` / `**bold**` source — instead of the termrender-
|
|
5
|
+
// rendered document the leaf help promises ("directive-flavored markdown
|
|
6
|
+
// rendered by termrender (panels, columns, trees, callouts, mermaid)").
|
|
7
|
+
//
|
|
8
|
+
// Root cause: the review surface was ONLY the read-only nvim buffer. nvim must
|
|
9
|
+
// show the raw source (anchored comments hang off source line/col numbers), and
|
|
10
|
+
// `-u NONE` treesitter/render-markdown styling never interprets ::: directives —
|
|
11
|
+
// so nothing anywhere rendered the doc. There was no termrender invocation in
|
|
12
|
+
// the entire review path (crouter `_run` review branch + humanloop
|
|
13
|
+
// launchReview): the managed venv binary was healthy and never asked.
|
|
14
|
+
//
|
|
15
|
+
// THE FIX (src/commands/human/queue.ts, `_run` review branch): open a live
|
|
16
|
+
// termrender watch pane via humanloop `display()` BESIDE the editor, record its
|
|
17
|
+
// pane id as `render_pane_id` on run.json (so `human cancel` can clear it), and
|
|
18
|
+
// kill it in a `finally` when the editor exits. The editor keeps the raw
|
|
19
|
+
// source; the human reads the rendered pane.
|
|
20
|
+
//
|
|
21
|
+
// This test drives the REAL `crtr human _run` worker (review mode) in a REAL
|
|
22
|
+
// but isolated tmux session — no mocks — and asserts:
|
|
23
|
+
// (a) a second pane appears beside the editor running termrender on the file,
|
|
24
|
+
// (b) that pane's content is RENDERED ANSI (box-drawing panels), not raw `:::`,
|
|
25
|
+
// (c) run.json carries render_pane_id for the cancel path,
|
|
26
|
+
// (d) the render pane is torn down when the editor exits.
|
|
27
|
+
import { test } from 'node:test';
|
|
28
|
+
import assert from 'node:assert/strict';
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
31
|
+
import { tmpdir } from 'node:os';
|
|
32
|
+
import { join, dirname } from 'node:path';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
import { createRequire } from 'node:module';
|
|
35
|
+
import { hasTmux } from './helpers/harness.js';
|
|
36
|
+
const CROUTER = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
37
|
+
const CLI_SRC = join(CROUTER, 'src', 'cli.ts');
|
|
38
|
+
const TSX_ESM = createRequire(import.meta.url).resolve('tsx/esm');
|
|
39
|
+
function tmux(args) {
|
|
40
|
+
const r = spawnSync('tmux', args, { encoding: 'utf8' });
|
|
41
|
+
return { code: r.status ?? -1, out: (r.stdout ?? '').trim(), err: (r.stderr ?? '').trim() };
|
|
42
|
+
}
|
|
43
|
+
function sessionPanes(session) {
|
|
44
|
+
const r = tmux(['list-panes', '-s', '-t', session, '-F', '#{pane_id}\t#{pane_current_command}']);
|
|
45
|
+
if (r.code !== 0)
|
|
46
|
+
return [];
|
|
47
|
+
return r.out
|
|
48
|
+
.split('\n')
|
|
49
|
+
.filter((l) => l.trim() !== '')
|
|
50
|
+
.map((l) => {
|
|
51
|
+
const [id, cmd] = l.split('\t');
|
|
52
|
+
return { id: id ?? '', cmd: cmd ?? '' };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function paneContent(paneId) {
|
|
56
|
+
return tmux(['capture-pane', '-p', '-t', paneId]).out;
|
|
57
|
+
}
|
|
58
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
59
|
+
async function waitFor(fn, timeoutMs, label) {
|
|
60
|
+
const deadline = Date.now() + timeoutMs;
|
|
61
|
+
for (;;) {
|
|
62
|
+
const v = fn();
|
|
63
|
+
if (v !== undefined)
|
|
64
|
+
return v;
|
|
65
|
+
if (Date.now() > deadline)
|
|
66
|
+
throw new Error(`timeout waiting for ${label}`);
|
|
67
|
+
await sleep(300);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const SKIP = !hasTmux();
|
|
71
|
+
test('review-render-pane regression: `human _run` (review) opens a live termrender pane showing RENDERED panels, not raw ::: source', { skip: SKIP ? 'tmux unavailable' : false, timeout: 90_000 }, async () => {
|
|
72
|
+
const dir = mkdtempSync(join(tmpdir(), 'crtr-review-render-'));
|
|
73
|
+
const session = `crtr-revrender-${process.pid}-${Date.now().toString(36)}`;
|
|
74
|
+
const mdPath = join(dir, 'doc.md');
|
|
75
|
+
// A directive doc: rendered output has box-drawing panels; raw output has `:::`.
|
|
76
|
+
writeFileSync(mdPath, '# Render check\n\n:::callout{type="info"}\nRENDER-MARKER body\n:::\n');
|
|
77
|
+
writeFileSync(join(dir, 'run.json'), JSON.stringify({ mode: 'review', job_id: '', file: mdPath, output: join(dir, 'feedback.json') }));
|
|
78
|
+
try {
|
|
79
|
+
// The worker runs the real `crtr human _run` from src via tsx, exactly the
|
|
80
|
+
// command shape detachHumanTui bakes into the spawned pane. job_id is ''
|
|
81
|
+
// so pushFinal is skipped (no canvas home needed) — the surface under
|
|
82
|
+
// test is the panes, not the report fan-out.
|
|
83
|
+
const workerCmd = `CRTR_HUMAN_DIR='${dir}' '${process.execPath}' --import '${TSX_ESM}' '${CLI_SRC}' human _run; sleep 120`;
|
|
84
|
+
const created = tmux(['new-session', '-d', '-s', session, '-x', '200', '-y', '50', workerCmd]);
|
|
85
|
+
assert.equal(created.code, 0, `create isolated session failed: ${created.err}`);
|
|
86
|
+
// (a) a SECOND pane appears: the termrender watch pane beside the editor.
|
|
87
|
+
const renderPane = await waitFor(() => {
|
|
88
|
+
const panes = sessionPanes(session);
|
|
89
|
+
if (panes.length < 2)
|
|
90
|
+
return undefined;
|
|
91
|
+
// The render pane is the one NOT hosting the editor/worker shell.
|
|
92
|
+
const candidates = panes.filter((p) => !/zsh|bash|sh|node|nvim|vim/i.test(p.cmd));
|
|
93
|
+
return candidates[0]?.id;
|
|
94
|
+
}, 30_000, 'termrender render pane to open');
|
|
95
|
+
// (b) its content is RENDERED: panel box-drawing present, raw ::: absent.
|
|
96
|
+
const rendered = await waitFor(() => {
|
|
97
|
+
const c = paneContent(renderPane);
|
|
98
|
+
return c.includes('RENDER-MARKER') ? c : undefined;
|
|
99
|
+
}, 30_000, 'rendered body to paint');
|
|
100
|
+
assert.ok(/[┌│└]/.test(rendered), `render pane draws panels (box chars): ${rendered.slice(0, 200)}`);
|
|
101
|
+
assert.ok(!rendered.includes(':::'), 'render pane does NOT show raw ::: directive source');
|
|
102
|
+
// (c) run.json carries render_pane_id so `human cancel` can clear the pane.
|
|
103
|
+
const rc = JSON.parse(readFileSync(join(dir, 'run.json'), 'utf8'));
|
|
104
|
+
assert.equal(rc.render_pane_id, renderPane, 'run.json records the render pane id');
|
|
105
|
+
// (d) editor exit (any quit submits) tears the render pane down. The
|
|
106
|
+
// editor pane is the OTHER pane (nvim runs as a child of the worker, so
|
|
107
|
+
// #{pane_current_command} may report node/zsh — match on its content:
|
|
108
|
+
// the read-only buffer shows the RAW ::: source).
|
|
109
|
+
const editorPane = await waitFor(() => {
|
|
110
|
+
const other = sessionPanes(session).find((p) => p.id !== renderPane);
|
|
111
|
+
if (other === undefined)
|
|
112
|
+
return undefined;
|
|
113
|
+
return paneContent(other.id).includes(':::') ? other.id : undefined;
|
|
114
|
+
}, 30_000, 'editor pane showing the raw source buffer');
|
|
115
|
+
tmux(['send-keys', '-t', editorPane, 'Space', 's']);
|
|
116
|
+
await waitFor(() => (existsSync(join(dir, 'feedback.json')) ? true : undefined), 30_000, 'feedback.json after submit');
|
|
117
|
+
await waitFor(() => (sessionPanes(session).some((p) => p.id === renderPane) ? undefined : true), 15_000, 'render pane to be killed after editor exit');
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
tmux(['kill-session', '-t', session]);
|
|
121
|
+
// The worker may still be flushing job files as it exits; retry the rm
|
|
122
|
+
// briefly so a write racing the first attempt can't fail the test.
|
|
123
|
+
for (let i = 0; i < 10; i++) {
|
|
124
|
+
try {
|
|
125
|
+
rmSync(dir, { recursive: true, force: true });
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
await sleep(300);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
@@ -10,6 +10,7 @@ import { createNode, getNode, getRow, subscribersOf } from '../canvas/canvas.js'
|
|
|
10
10
|
import { closeDb } from '../canvas/db.js';
|
|
11
11
|
import { nodeDir } from '../canvas/paths.js';
|
|
12
12
|
import { spawnNode } from '../runtime/nodes.js';
|
|
13
|
+
import { resolveSpawner } from '../runtime/spawn.js';
|
|
13
14
|
let home;
|
|
14
15
|
function spawner(id) {
|
|
15
16
|
return {
|
|
@@ -62,6 +63,15 @@ test('independent root: provenance only, no parent, no subscription', () => {
|
|
|
62
63
|
assert.equal(meta.lifecycle, 'resident', 'a root is resident');
|
|
63
64
|
assert.equal(subscribersOf(root.node_id).length, 0, 'nobody is subscribed to an independent root — the spawner is not woken by it');
|
|
64
65
|
});
|
|
66
|
+
test('resolveSpawner: a --root spawn needs no caller; a managed child does (regression)', () => {
|
|
67
|
+
// Bug-regression: `crtr node new --root` from a human shell (no CRTR_NODE_ID,
|
|
68
|
+
// no --parent) threw "spawnChild requires a calling node" even though a root
|
|
69
|
+
// has no spine parent and the spawner id is provenance only.
|
|
70
|
+
assert.equal(resolveSpawner(undefined, null, true), null, 'human root spawn: no caller is fine');
|
|
71
|
+
assert.equal(resolveSpawner(undefined, 'A', true), 'A', 'node root spawn: provenance recorded');
|
|
72
|
+
assert.equal(resolveSpawner('A', null, false), 'A', 'explicit parent satisfies a child spawn');
|
|
73
|
+
assert.throws(() => resolveSpawner(undefined, null, false), /requires a calling node/, 'a managed child with no caller still throws');
|
|
74
|
+
});
|
|
65
75
|
test('unknown parent: spawnNode throws and mints NO node dir / row (validate before create)', () => {
|
|
66
76
|
// Pre-allocate the would-be id so we can prove nothing was scaffolded for it.
|
|
67
77
|
const orphanId = 'orphan-under-ghost';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/wake-bearings.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Wake-origin self-knowledge (the <crtr-wake> provenance block). Regression
|
|
4
|
+
// guard for the observed gap: a node woken or BORN by a scheduled TIMER could
|
|
5
|
+
// not tell it came from a timer — a scheduled wake arrived indistinguishable
|
|
6
|
+
// from an ordinary refresh/message (the Invariant B/D anti-goal). These lock:
|
|
7
|
+
// • buildWakeBearings renders the right decision-first block per wake kind /
|
|
8
|
+
// cadence, and never crashes when the armer was reaped (no resolved name);
|
|
9
|
+
// • the bare-wake revive kickoff carries that block (so a scheduled alarm is
|
|
10
|
+
// distinguishable from a generic context-refresh), placed AFTER the kickoff
|
|
11
|
+
// sentinel (goal-capture keys on it) and BEFORE the roadmap/disk bearings;
|
|
12
|
+
// • an ordinary fresh revive (no wakeReason) carries NO block.
|
|
13
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { createNode } from '../canvas/canvas.js';
|
|
19
|
+
import { closeDb } from '../canvas/db.js';
|
|
20
|
+
import { buildWakeBearings } from '../runtime/bearings.js';
|
|
21
|
+
import { buildReviveKickoff, drainBearings, REVIVE_KICKOFF_SENTINEL, } from '../runtime/kickoff.js';
|
|
22
|
+
let home;
|
|
23
|
+
function node(id) {
|
|
24
|
+
return {
|
|
25
|
+
node_id: id,
|
|
26
|
+
name: id,
|
|
27
|
+
created: new Date().toISOString(),
|
|
28
|
+
cwd: '/tmp/work',
|
|
29
|
+
kind: 'general',
|
|
30
|
+
mode: 'base',
|
|
31
|
+
lifecycle: 'terminal',
|
|
32
|
+
status: 'active',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const ARMED = '2026-06-08T13:30:00.000Z';
|
|
36
|
+
before(() => {
|
|
37
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-wake-bearings-'));
|
|
38
|
+
process.env['CRTR_HOME'] = home;
|
|
39
|
+
});
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
closeDb();
|
|
42
|
+
rmSync(home, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
after(() => {
|
|
45
|
+
closeDb();
|
|
46
|
+
rmSync(home, { recursive: true, force: true });
|
|
47
|
+
delete process.env['CRTR_HOME'];
|
|
48
|
+
});
|
|
49
|
+
// --- buildWakeBearings: one per kind / cadence variant ----------------------
|
|
50
|
+
test('spawn one-shot: names a deferred birth + the armer, and says it does not recur', () => {
|
|
51
|
+
const o = {
|
|
52
|
+
kind: 'spawn',
|
|
53
|
+
ownerId: 'arm-1',
|
|
54
|
+
ownerName: 'release-orch',
|
|
55
|
+
armedAt: ARMED,
|
|
56
|
+
recur: null,
|
|
57
|
+
};
|
|
58
|
+
const b = buildWakeBearings(o);
|
|
59
|
+
assert.ok(b.startsWith('<crtr-wake>') && b.endsWith('</crtr-wake>'));
|
|
60
|
+
assert.match(b, /BORN by a scheduled wake/);
|
|
61
|
+
assert.match(b, /deferred birth/);
|
|
62
|
+
// Armer named ROLE-explicitly so the newborn never reads the id as its own.
|
|
63
|
+
assert.match(b, /armed by node arm-1 \("release-orch"\)/);
|
|
64
|
+
assert.match(b, /only run, not a recurring job/);
|
|
65
|
+
assert.match(b, /Your task follows\./);
|
|
66
|
+
});
|
|
67
|
+
test('spawn cron: surfaces the cadence + frames it as one run of a standing job', () => {
|
|
68
|
+
const o = {
|
|
69
|
+
kind: 'spawn',
|
|
70
|
+
ownerId: 'arm-1',
|
|
71
|
+
ownerName: 'nightly',
|
|
72
|
+
armedAt: ARMED,
|
|
73
|
+
recur: JSON.stringify({ cron: '0 9 * * *', tz: 'America/New_York' }),
|
|
74
|
+
};
|
|
75
|
+
const b = buildWakeBearings(o);
|
|
76
|
+
assert.match(b, /recurring spawn-cron armed by node arm-1 \("nightly"\)/);
|
|
77
|
+
// The cadence renders via the shared cadenceDisplay (matches `wake list`).
|
|
78
|
+
assert.match(b, /firing cron `0 9 \* \* \*` \(America\/New_York\)/);
|
|
79
|
+
assert.match(b, /one run of a standing job, not a one-off/);
|
|
80
|
+
assert.match(b, /inherit nothing from prior runs but this task/);
|
|
81
|
+
});
|
|
82
|
+
test('spawn interval cron: renders the every-N cadence', () => {
|
|
83
|
+
const o = {
|
|
84
|
+
kind: 'spawn',
|
|
85
|
+
ownerId: 'arm-1',
|
|
86
|
+
ownerName: 'poller',
|
|
87
|
+
armedAt: ARMED,
|
|
88
|
+
recur: JSON.stringify({ every: '6h' }),
|
|
89
|
+
};
|
|
90
|
+
assert.match(buildWakeBearings(o), /firing every 6h/);
|
|
91
|
+
});
|
|
92
|
+
test('spawn with a reaped armer: renders the bare id, no name, never crashes', () => {
|
|
93
|
+
const o = {
|
|
94
|
+
kind: 'spawn',
|
|
95
|
+
ownerId: 'gone-9',
|
|
96
|
+
ownerName: undefined, // armer no longer exists → no resolved name
|
|
97
|
+
armedAt: ARMED,
|
|
98
|
+
recur: JSON.stringify({ every: '12h' }),
|
|
99
|
+
};
|
|
100
|
+
const b = buildWakeBearings(o);
|
|
101
|
+
assert.match(b, /armed by node gone-9 \(now gone\)/); // bare id, flagged reaped
|
|
102
|
+
assert.doesNotMatch(b, /\("/); // never an empty/dangling name
|
|
103
|
+
});
|
|
104
|
+
test('bare one-shot: frames a timed re-check, NOT a new request', () => {
|
|
105
|
+
const o = { kind: 'bare', ownerId: 'n1', ownerName: 'n1', armedAt: ARMED, recur: null };
|
|
106
|
+
const b = buildWakeBearings(o);
|
|
107
|
+
assert.match(b, /a scheduled alarm fired/);
|
|
108
|
+
assert.match(b, /a timer, NOT a new message or request/);
|
|
109
|
+
assert.match(b, /timed re-check, not a new task/);
|
|
110
|
+
assert.match(b, /re-read your roadmap/);
|
|
111
|
+
// No armer attribution on bare (could be `--node`-armed by another), no ISO.
|
|
112
|
+
assert.doesNotMatch(b, /you set/);
|
|
113
|
+
assert.doesNotMatch(b, new RegExp(ARMED));
|
|
114
|
+
});
|
|
115
|
+
test('bare recurring: frames one tick of a standing re-check + the cadence', () => {
|
|
116
|
+
const o = {
|
|
117
|
+
kind: 'bare',
|
|
118
|
+
ownerId: 'n1',
|
|
119
|
+
ownerName: 'n1',
|
|
120
|
+
armedAt: ARMED,
|
|
121
|
+
recur: JSON.stringify({ every: '6h' }),
|
|
122
|
+
};
|
|
123
|
+
const b = buildWakeBearings(o);
|
|
124
|
+
assert.match(b, /recurring scheduled alarm fired \(every 6h\)/);
|
|
125
|
+
assert.match(b, /one tick of a standing re-check/);
|
|
126
|
+
});
|
|
127
|
+
// --- the bare-wake revive seam (Seam 2) -------------------------------------
|
|
128
|
+
test('a bare scheduled wake makes the fresh-revive kickoff distinguishable from a refresh', () => {
|
|
129
|
+
const id = 'b1';
|
|
130
|
+
const meta = createNode(node(id));
|
|
131
|
+
const wakeReason = {
|
|
132
|
+
kind: 'bare',
|
|
133
|
+
ownerId: id,
|
|
134
|
+
ownerName: id,
|
|
135
|
+
armedAt: ARMED,
|
|
136
|
+
recur: null,
|
|
137
|
+
};
|
|
138
|
+
const msg = buildReviveKickoff(meta, drainBearings(meta), wakeReason);
|
|
139
|
+
// The block is present, and the kickoff STILL starts with the sentinel that
|
|
140
|
+
// goal-capture keys on (so the prompt is never mistaken for a user mandate).
|
|
141
|
+
assert.ok(msg.startsWith(REVIVE_KICKOFF_SENTINEL), 'kickoff still leads with the sentinel');
|
|
142
|
+
assert.match(msg, /<crtr-wake>/);
|
|
143
|
+
assert.match(msg, /a scheduled alarm fired/);
|
|
144
|
+
// Placement: sentinel < <crtr-wake> < <roadmap> — "why you woke" precedes
|
|
145
|
+
// "what to rebuild from".
|
|
146
|
+
const iSentinel = msg.indexOf(REVIVE_KICKOFF_SENTINEL);
|
|
147
|
+
const iWake = msg.indexOf('<crtr-wake>');
|
|
148
|
+
const iRoadmap = msg.indexOf('<roadmap');
|
|
149
|
+
assert.ok(iSentinel === 0 && iSentinel < iWake && iWake < iRoadmap, 'block sits after sentinel, before roadmap');
|
|
150
|
+
});
|
|
151
|
+
test('an ordinary fresh revive (no wakeReason) carries NO wake block', () => {
|
|
152
|
+
const id = 'b2';
|
|
153
|
+
const meta = createNode(node(id));
|
|
154
|
+
const msg = buildReviveKickoff(meta, drainBearings(meta));
|
|
155
|
+
assert.doesNotMatch(msg, /<crtr-wake>/);
|
|
156
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/wake-origin.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Wake-origin self-knowledge at the DAEMON FIRING seams (the other half of the
|
|
4
|
+
// regression — wake-bearings.test.ts covers the pure block + the bare-revive
|
|
5
|
+
// kickoff). The observed gap: a scheduled wake arrived indistinguishable from an
|
|
6
|
+
// ordinary message/spawn (Invariant D). These lock the daemon's third pass:
|
|
7
|
+
// • noted — the delivered inbox label carries the ⏰ scheduled-wake marker, so
|
|
8
|
+
// a timed note is distinguishable from a plain `node msg` (no tmux needed).
|
|
9
|
+
// • spawn — a node BORN by a spawn wake gets the <crtr-wake> block prepended
|
|
10
|
+
// to its kickoff prompt (naming the armer + cadence); an ordinary `node new`
|
|
11
|
+
// spawn does NOT. The headline. (tmux-gated integration via the harness.)
|
|
12
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { mkdtempSync, rmSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join, dirname } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { createNode } from '../canvas/canvas.js';
|
|
19
|
+
import { armWake } from '../canvas/wakeups.js';
|
|
20
|
+
import { closeDb } from '../canvas/db.js';
|
|
21
|
+
import { readInboxSince } from '../feed/inbox.js';
|
|
22
|
+
import { superviseTick } from '../../daemon/crtrd.js';
|
|
23
|
+
import { hasTmux, createHarness } from './helpers/harness.js';
|
|
24
|
+
const CROUTER = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
25
|
+
function node(id) {
|
|
26
|
+
return {
|
|
27
|
+
node_id: id,
|
|
28
|
+
name: id,
|
|
29
|
+
created: new Date().toISOString(),
|
|
30
|
+
cwd: '/tmp/work',
|
|
31
|
+
kind: 'general',
|
|
32
|
+
mode: 'base',
|
|
33
|
+
lifecycle: 'terminal',
|
|
34
|
+
status: 'active',
|
|
35
|
+
parent: null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// --- noted seam (no tmux) ---------------------------------------------------
|
|
39
|
+
let home;
|
|
40
|
+
before(() => {
|
|
41
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-wake-origin-'));
|
|
42
|
+
process.env['CRTR_HOME'] = home;
|
|
43
|
+
});
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
closeDb();
|
|
46
|
+
rmSync(home, { recursive: true, force: true });
|
|
47
|
+
});
|
|
48
|
+
after(() => {
|
|
49
|
+
closeDb();
|
|
50
|
+
rmSync(home, { recursive: true, force: true });
|
|
51
|
+
delete process.env['CRTR_HOME'];
|
|
52
|
+
});
|
|
53
|
+
test('a noted scheduled wake delivers a ⏰-marked inbox label, distinct from a plain message', async () => {
|
|
54
|
+
const armer = createNode(node('armer'));
|
|
55
|
+
const target = createNode(node('tgt')); // no tmux placement → pass 1 skips it
|
|
56
|
+
armWake({
|
|
57
|
+
wakeup_id: 'wk-noted-1',
|
|
58
|
+
node_id: target.node_id,
|
|
59
|
+
owner_id: armer.node_id,
|
|
60
|
+
fire_at: new Date(Date.now() - 1000).toISOString(),
|
|
61
|
+
kind: 'noted',
|
|
62
|
+
recur: null,
|
|
63
|
+
payload: { body: 'CI should be green by now — check it', label: 'check CI' },
|
|
64
|
+
});
|
|
65
|
+
closeDb();
|
|
66
|
+
await superviseTick(Date.now());
|
|
67
|
+
closeDb();
|
|
68
|
+
const entries = readInboxSince(target.node_id);
|
|
69
|
+
assert.equal(entries.length, 1, 'the noted wake delivered exactly one inbox entry');
|
|
70
|
+
// Invariant D: the timer signal rides the VISIBLE label (digests never surface
|
|
71
|
+
// arbitrary data keys), so this is distinguishable from a plain `node msg`.
|
|
72
|
+
assert.equal(entries[0].label, '⏰ scheduled wake — check CI');
|
|
73
|
+
assert.notEqual(entries[0].label, 'check CI', 'a plain message would carry the bare label');
|
|
74
|
+
});
|
|
75
|
+
// --- spawn seam (the headline; tmux-gated integration) ----------------------
|
|
76
|
+
test('a spawn scheduled wake births a node whose kickoff carries <crtr-wake>; node new does NOT', { skip: hasTmux() ? false : 'tmux unavailable' }, async () => {
|
|
77
|
+
const h = await createHarness();
|
|
78
|
+
try {
|
|
79
|
+
const armer = h.spawnRoot('armer');
|
|
80
|
+
// Contrast: an ordinary `node new` spawn — its kickoff has NO wake block.
|
|
81
|
+
const plainChild = await h.spawnChild(armer, 'plain task');
|
|
82
|
+
const plainBoot = await h.awaitBoot(plainChild);
|
|
83
|
+
assert.doesNotMatch(plainBoot.prompt ?? '', /<crtr-wake>/, 'node new carries no wake block');
|
|
84
|
+
// Arm a recurring spawn-cron (detached), fire one tick, inspect the BORN
|
|
85
|
+
// node's kickoff prompt (the message it actually wakes on).
|
|
86
|
+
const before = new Set(readdirSync(join(h.home, 'nodes')));
|
|
87
|
+
armWake({
|
|
88
|
+
wakeup_id: 'wk-spawn-1',
|
|
89
|
+
node_id: null, // canvas-detached deferred birth
|
|
90
|
+
owner_id: armer,
|
|
91
|
+
fire_at: new Date(Date.now() - 1000).toISOString(),
|
|
92
|
+
kind: 'spawn',
|
|
93
|
+
recur: JSON.stringify({ every: '6h' }),
|
|
94
|
+
payload: { kind: 'general', cwd: CROUTER, prompt: 'do the recurring job', parent: armer },
|
|
95
|
+
});
|
|
96
|
+
await h.tick(Date.now());
|
|
97
|
+
const born = readdirSync(join(h.home, 'nodes')).filter((d) => !before.has(d));
|
|
98
|
+
assert.equal(born.length, 1, 'exactly one node born from the spawn wake');
|
|
99
|
+
const bornBoot = await h.awaitBoot(born[0]);
|
|
100
|
+
const prompt = bornBoot.prompt ?? '';
|
|
101
|
+
assert.match(prompt, /<crtr-wake>/, 'the born node learns a timer birthed it');
|
|
102
|
+
assert.match(prompt, /recurring spawn-cron armed by node /, 'names the armer role-explicitly');
|
|
103
|
+
assert.match(prompt, /firing every 6h/, 'surfaces the cadence');
|
|
104
|
+
assert.match(prompt, new RegExp(armer), 'the still-alive armer is named');
|
|
105
|
+
assert.match(prompt, /do the recurring job/, 'the real task follows the block');
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await h.dispose();
|
|
109
|
+
}
|
|
110
|
+
});
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -30,7 +30,7 @@ const BOOT_SKILL_MARKER_PREFIX = '<!-- crtr-boot-skill v';
|
|
|
30
30
|
function bootSkillBody() {
|
|
31
31
|
return `---
|
|
32
32
|
name: crtr-skills
|
|
33
|
-
description: Capture, list, search, and load skills via the crtr CLI. Skills are durable agent memory — markdown future LLM sessions load on demand. Use when the user wants to remember/save knowledge, build a context primer, or recall a previously saved skill. Triggers: "save", "remember", "build context for", "what skills do we have", "skill for X".
|
|
33
|
+
description: 'Capture, list, search, and load skills via the crtr CLI. Skills are durable agent memory — markdown future LLM sessions load on demand. Use when the user wants to remember/save knowledge, build a context primer, or recall a previously saved skill. Triggers: "save", "remember", "build context for", "what skills do we have", "skill for X".'
|
|
34
34
|
argument-hint: [topic or verb]
|
|
35
35
|
---
|
|
36
36
|
|