@crouton-kit/crouter 0.3.18 → 0.3.20
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/PERSONA.md +1 -0
- package/dist/builtin-personas/design/orchestrator.md +2 -1
- package/dist/builtin-personas/developer/PERSONA.md +1 -0
- package/dist/builtin-personas/developer/orchestrator.md +2 -1
- package/dist/builtin-personas/explore/PERSONA.md +1 -0
- package/dist/builtin-personas/explore/orchestrator.md +4 -0
- package/dist/builtin-personas/general/PERSONA.md +1 -0
- package/dist/builtin-personas/general/orchestrator.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/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/orchestrator.md +2 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
- package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
- package/dist/builtin-personas/review/PERSONA.md +1 -0
- package/dist/builtin-personas/review/orchestrator.md +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/PERSONA.md +1 -0
- package/dist/builtin-personas/spec/orchestrator.md +2 -1
- package/dist/builtin-personas/waiting.md +8 -0
- 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 +3 -2
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/queue.js +54 -8
- package/dist/commands/human/shared.d.ts +2 -0
- 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 +610 -61
- 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-attach-limits.test.d.ts +1 -0
- package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
- package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
- package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
- package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
- package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
- package/dist/core/__tests__/broker-dialogs.test.js +126 -0
- package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
- package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +87 -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__/canvas-inbox-watcher-hold.test.d.ts +1 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
- 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/broker-clients.d.ts +43 -0
- package/dist/core/__tests__/helpers/broker-clients.js +178 -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__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
- package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
- package/dist/core/__tests__/live-mutation.test.js +6 -132
- package/dist/core/__tests__/memory.test.js +23 -115
- 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 +16 -23
- 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/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 +91 -3
- 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/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/loader.d.ts +7 -0
- package/dist/core/personas/loader.js +36 -5
- package/dist/core/personas/resolve.js +13 -3
- 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 +54 -4
- package/dist/core/runtime/launch.js +85 -5
- 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 +37 -0
- package/dist/core/runtime/nodes.js +60 -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 +32 -14
- package/dist/core/runtime/placement.js +206 -52
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +4 -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 +11 -8
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +23 -0
- package/dist/core/runtime/spawn.js +77 -23
- 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 +5 -0
- package/dist/core/spawn.js +20 -2
- 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 +87 -23
- 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,245 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/child-death-wake.test.ts
|
|
2
|
+
//
|
|
3
|
+
// D-1 BUG REGRESSION + node/canvas LIFECYCLE — "the runtime wakes a dormant
|
|
4
|
+
// parent on ANY terminal child outcome."
|
|
5
|
+
//
|
|
6
|
+
// The bug (D-1 finding, context/d1-finding.md): a parent that delegates and
|
|
7
|
+
// then JUST STOPS — retaining its inbox-wait, arming no self-wake — was woken
|
|
8
|
+
// ONLY when a child `push final`s or never boots. A child that crashes AFTER
|
|
9
|
+
// booting, is daemon-finalized after a quiet turn, or is `node close`d marked
|
|
10
|
+
// the child dead/done/canceled with NO push to the parent, so a purely-inbox-
|
|
11
|
+
// waiting parent hung dormant forever. The wake doctrine ("delegate and stop;
|
|
12
|
+
// the runtime wakes you") is only honest if EVERY terminal child outcome reaches
|
|
13
|
+
// the parent — this locks that in.
|
|
14
|
+
//
|
|
15
|
+
// The fix fans a system inbox entry to subscribersOf(child) on three previously-
|
|
16
|
+
// silent terminal outcomes — mirroring surfaceBootFailure:
|
|
17
|
+
// • daemon CRASH after boot (crtrd pane-gone, mid-generation → 'dead')
|
|
18
|
+
// • daemon FINALIZE on quiet (crtrd pane-gone, turn ended, nothing live → 'done')
|
|
19
|
+
// • `node close` (close.ts, the surviving manager outside the set)
|
|
20
|
+
//
|
|
21
|
+
// THE CRUX (the whole risk): the wake must fire ONLY on genuine death/close —
|
|
22
|
+
// NEVER on healthy dormancy. A child that ended its turn STILL waiting (an active
|
|
23
|
+
// live grandchild, or a pending self-wake) is alive-and-dormant, not dead; waking
|
|
24
|
+
// its parent then would re-create the spurious-wake storm the doctrine exists to
|
|
25
|
+
// kill (the concrete case: a developer child that finished its edits, spawned its
|
|
26
|
+
// own reviewer, and went dormant waiting on it — its parent must stay asleep).
|
|
27
|
+
// The daemon draws that boundary EXACTLY where the stop-guard does:
|
|
28
|
+
// hasActiveLiveSubscription || hasPendingSelfWake ⇒ healthy dormancy ⇒ release,
|
|
29
|
+
// no wake.
|
|
30
|
+
//
|
|
31
|
+
// Faithful: REAL canvas data layer, REAL tmux panes, the REAL daemon decision
|
|
32
|
+
// pass (superviseTick) and the REAL closeNode. The wake travels through the ONE
|
|
33
|
+
// channel the d1-finding identified — an entry appended to the parent's
|
|
34
|
+
// inbox.jsonl — so each direction asserts that channel: the system entry appears
|
|
35
|
+
// (parent woken: its dormant pi is then revived by the daemon's second pass,
|
|
36
|
+
// observed via a cycles bump) or it does NOT (parent stays asleep, cycles flat).
|
|
37
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
38
|
+
import assert from 'node:assert/strict';
|
|
39
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
40
|
+
import { tmpdir } from 'node:os';
|
|
41
|
+
import { join } from 'node:path';
|
|
42
|
+
import { spawnSync } from 'node:child_process';
|
|
43
|
+
import { createNode, getNode, subscribe } from '../canvas/canvas.js';
|
|
44
|
+
import { armWake } from '../canvas/wakeups.js';
|
|
45
|
+
import { closeDb } from '../canvas/db.js';
|
|
46
|
+
import { readInboxSince } from '../feed/inbox.js';
|
|
47
|
+
import { markBusy, clearBusy } from '../runtime/busy.js';
|
|
48
|
+
import { closeNode } from '../runtime/close.js';
|
|
49
|
+
import { superviseTick } from '../../daemon/crtrd.js';
|
|
50
|
+
let home;
|
|
51
|
+
function node(id, over = {}) {
|
|
52
|
+
return {
|
|
53
|
+
node_id: id,
|
|
54
|
+
name: id,
|
|
55
|
+
created: new Date().toISOString(),
|
|
56
|
+
cwd: '/tmp/work',
|
|
57
|
+
kind: 'general',
|
|
58
|
+
mode: 'base',
|
|
59
|
+
lifecycle: 'terminal',
|
|
60
|
+
status: 'active',
|
|
61
|
+
...over,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function hasTmux() {
|
|
65
|
+
return spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0;
|
|
66
|
+
}
|
|
67
|
+
/** A pid that is guaranteed dead. */
|
|
68
|
+
function deadPid() {
|
|
69
|
+
const r = spawnSync('true', [], { stdio: 'ignore' });
|
|
70
|
+
return r.pid ?? 0x7ffffffe;
|
|
71
|
+
}
|
|
72
|
+
const cyclesOf = (id) => getNode(id)?.cycles ?? 0;
|
|
73
|
+
before(() => {
|
|
74
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-child-death-wake-'));
|
|
75
|
+
process.env['CRTR_HOME'] = home;
|
|
76
|
+
});
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
closeDb();
|
|
79
|
+
rmSync(home, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
after(() => {
|
|
82
|
+
closeDb();
|
|
83
|
+
rmSync(home, { recursive: true, force: true });
|
|
84
|
+
delete process.env['CRTR_HOME'];
|
|
85
|
+
});
|
|
86
|
+
/** Run `fn` with a real, live tmux session + its window's live `%pane_id`. The
|
|
87
|
+
* PARENT occupies this (frozen) pane while pi-dead + idle-released — the
|
|
88
|
+
* faithful shape of an unfocused dormant orchestrator the daemon's second pass
|
|
89
|
+
* revives on an unseen inbox entry. */
|
|
90
|
+
async function withLivePane(tag, fn) {
|
|
91
|
+
const session = `crtr-cdw-${process.pid}-${tag}`;
|
|
92
|
+
spawnSync('tmux', ['new-session', '-d', '-s', session, '-c', '/tmp', 'sleep 600']);
|
|
93
|
+
try {
|
|
94
|
+
const w = spawnSync('tmux', ['list-windows', '-t', session, '-F', '#{window_id}'], { encoding: 'utf8' });
|
|
95
|
+
const window = (w.stdout ?? '').trim().split('\n')[0];
|
|
96
|
+
const p = spawnSync('tmux', ['display-message', '-p', '-t', `${session}:${window}`, '#{pane_id}'], { encoding: 'utf8' });
|
|
97
|
+
const pane = (p.stdout ?? '').trim();
|
|
98
|
+
await fn(session, window, pane);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
spawnSync('tmux', ['kill-session', '-t', session], { stdio: 'ignore' });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** A guaranteed-DEAD pane id inside `window`: split a fresh pane, then kill it.
|
|
105
|
+
* The window survives via its original pane. This is how a child's pane reads
|
|
106
|
+
* "gone" to the daemon while the session/window are still alive. */
|
|
107
|
+
function deadPaneIn(session, window) {
|
|
108
|
+
const sp = spawnSync('tmux', ['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', `${session}:${window}`, 'sleep 600'], { encoding: 'utf8' });
|
|
109
|
+
const dead = (sp.stdout ?? '').trim();
|
|
110
|
+
spawnSync('tmux', ['kill-pane', '-t', dead], { stdio: 'ignore' });
|
|
111
|
+
return dead;
|
|
112
|
+
}
|
|
113
|
+
/** A dormant, unfocused PARENT: pi-dead, idle-released, frozen on the live pane,
|
|
114
|
+
* home_session set so the daemon's second pass can revive it. */
|
|
115
|
+
function dormantParent(id, session, window, pane) {
|
|
116
|
+
createNode(node(id, {
|
|
117
|
+
pane,
|
|
118
|
+
tmux_session: session,
|
|
119
|
+
window,
|
|
120
|
+
pi_pid: deadPid(),
|
|
121
|
+
pi_session_id: 'booted',
|
|
122
|
+
intent: 'idle-release',
|
|
123
|
+
status: 'idle',
|
|
124
|
+
home_session: session,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
// ===========================================================================
|
|
128
|
+
// POSITIVE — a genuine terminal child outcome WAKES the dormant parent.
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
test('CRASH after boot (mid-generation, pane gone) wakes the dormant inbox-waiting parent', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
131
|
+
await withLivePane('crash', async (session, window, pane) => {
|
|
132
|
+
dormantParent('PARENT', session, window, pane);
|
|
133
|
+
const dead = deadPaneIn(session, window);
|
|
134
|
+
createNode(node('CHILD', {
|
|
135
|
+
pane: dead, tmux_session: session, window,
|
|
136
|
+
pi_pid: deadPid(), pi_session_id: 'booted', intent: null, status: 'active',
|
|
137
|
+
}));
|
|
138
|
+
subscribe('PARENT', 'CHILD', true); // the spawn-time spine edge
|
|
139
|
+
markBusy('CHILD'); // pane died INSIDE a turn → genuine mid-run crash
|
|
140
|
+
const before = cyclesOf('PARENT');
|
|
141
|
+
await superviseTick();
|
|
142
|
+
assert.equal(getNode('CHILD').status, 'dead', 'CHILD crashed → dead');
|
|
143
|
+
// THE WAKE CHANNEL: a system entry from CHILD lands in PARENT's inbox.
|
|
144
|
+
const wake = readInboxSince('PARENT').find((e) => e.from === 'CHILD');
|
|
145
|
+
assert.ok(wake, 'crash fanned a system inbox entry to the dormant parent (mirrors surfaceBootFailure)');
|
|
146
|
+
assert.match(wake.label, /died/i, 'the entry tells the parent WHICH child died and how');
|
|
147
|
+
// END-TO-END: the dormant parent is REVIVED on the same tick (second pass).
|
|
148
|
+
assert.equal(cyclesOf('PARENT'), before + 1, 'dormant PARENT revived on the same tick — no longer hangs');
|
|
149
|
+
clearBusy('CHILD');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
test('quiet-turn FINALIZE (pane gone, nothing live to wait for) wakes the dormant parent', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
153
|
+
await withLivePane('finalize', async (session, window, pane) => {
|
|
154
|
+
dormantParent('PARENT', session, window, pane);
|
|
155
|
+
const dead = deadPaneIn(session, window);
|
|
156
|
+
// Booted, turn finished (NO busy marker), no live subscription, no pending
|
|
157
|
+
// self-wake → the daemon finalizes it (done): a dismissal of a finished node.
|
|
158
|
+
createNode(node('CHILD', {
|
|
159
|
+
pane: dead, tmux_session: session, window,
|
|
160
|
+
pi_pid: deadPid(), pi_session_id: 'booted', intent: null, status: 'active',
|
|
161
|
+
}));
|
|
162
|
+
subscribe('PARENT', 'CHILD', true);
|
|
163
|
+
const before = cyclesOf('PARENT');
|
|
164
|
+
await superviseTick();
|
|
165
|
+
assert.equal(getNode('CHILD').status, 'done', 'CHILD finalized → done');
|
|
166
|
+
const wake = readInboxSince('PARENT').find((e) => e.from === 'CHILD');
|
|
167
|
+
assert.ok(wake, 'finalize fanned a system inbox entry to the dormant parent');
|
|
168
|
+
assert.match(wake.label, /without a final report/i, 'the entry tells the parent the child ended with no final');
|
|
169
|
+
assert.equal(cyclesOf('PARENT'), before + 1, 'dormant PARENT revived on the same tick');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
test('node close of a child wakes its SURVIVING manager (the parent outside the closing set)', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
173
|
+
await withLivePane('close', async (session, window, pane) => {
|
|
174
|
+
dormantParent('PARENT', session, window, pane);
|
|
175
|
+
const childPane = deadPaneIn(session, window); // any pane handle; closeNode tears it down
|
|
176
|
+
createNode(node('CHILD', {
|
|
177
|
+
pane: childPane, tmux_session: session, window,
|
|
178
|
+
pi_session_id: 'booted', status: 'active',
|
|
179
|
+
}));
|
|
180
|
+
subscribe('PARENT', 'CHILD', true); // PARENT is a manager OUTSIDE the close set
|
|
181
|
+
// Closing CHILD: closingSet({CHILD}) cannot pull in PARENT (PARENT is a
|
|
182
|
+
// SUBSCRIBER of CHILD, never a descendant), so PARENT survives + must wake.
|
|
183
|
+
const res = closeNode('CHILD');
|
|
184
|
+
assert.deepEqual(res.closed, ['CHILD'], 'only CHILD is closed; PARENT spared');
|
|
185
|
+
assert.equal(getNode('CHILD').status, 'canceled', 'CHILD canceled by close');
|
|
186
|
+
const wake = readInboxSince('PARENT').find((e) => e.from === 'CHILD');
|
|
187
|
+
assert.ok(wake, 'node close fanned a child-closed entry to the surviving manager (D-1: previously none)');
|
|
188
|
+
assert.match(wake.label, /closed/i, 'the entry names the closed child');
|
|
189
|
+
// The dormant parent is then revived by the daemon's second pass.
|
|
190
|
+
const before = cyclesOf('PARENT');
|
|
191
|
+
await superviseTick();
|
|
192
|
+
assert.equal(cyclesOf('PARENT'), before + 1, 'dormant PARENT revived on the close entry — no longer hangs');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// ===========================================================================
|
|
196
|
+
// NEGATIVE — HEALTHY DORMANCY must NOT wake the parent (the correctness crux).
|
|
197
|
+
// ===========================================================================
|
|
198
|
+
test('CRUX: a child dormant while awaiting its OWN live grandchild does NOT wake the parent', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
199
|
+
await withLivePane('dorm-sub', async (session, window, pane) => {
|
|
200
|
+
dormantParent('PARENT', session, window, pane);
|
|
201
|
+
const dead = deadPaneIn(session, window);
|
|
202
|
+
// CHILD finished its turn (no busy) and its pane is gone, BUT it actively
|
|
203
|
+
// subscribes to a LIVE grandchild — exactly the developer-spawned-reviewer
|
|
204
|
+
// case. This is HEALTHY dormancy, not death.
|
|
205
|
+
createNode(node('CHILD', {
|
|
206
|
+
pane: dead, tmux_session: session, window,
|
|
207
|
+
pi_pid: deadPid(), pi_session_id: 'booted', intent: null, status: 'active',
|
|
208
|
+
}));
|
|
209
|
+
createNode(node('GRANDCHILD', { status: 'active', pi_session_id: 'booted' })); // live, no pane (inline)
|
|
210
|
+
subscribe('PARENT', 'CHILD', true);
|
|
211
|
+
subscribe('CHILD', 'GRANDCHILD', true); // CHILD awaits a LIVE grandchild
|
|
212
|
+
const before = cyclesOf('PARENT');
|
|
213
|
+
await superviseTick();
|
|
214
|
+
// CHILD is RELEASED (revivable), NOT finalized — the daemon must not orphan
|
|
215
|
+
// its in-flight grandchild and must not wake the parent.
|
|
216
|
+
assert.equal(getNode('CHILD').status, 'idle', 'CHILD released (revivable), NOT finalized');
|
|
217
|
+
assert.equal(getNode('CHILD').intent, 'idle-release', 'CHILD routed to idle-release');
|
|
218
|
+
assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — healthy dormancy raises no wake');
|
|
219
|
+
assert.equal(cyclesOf('PARENT'), before, 'dormant PARENT stays asleep (no revive) — no spurious wake');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
test('CRUX: a child dormant on a pending self-wake (no live sub) does NOT wake the parent', { skip: !hasTmux() ? 'tmux unavailable' : false, timeout: 60_000 }, async () => {
|
|
223
|
+
await withLivePane('dorm-wake', async (session, window, pane) => {
|
|
224
|
+
dormantParent('PARENT', session, window, pane);
|
|
225
|
+
const dead = deadPaneIn(session, window);
|
|
226
|
+
createNode(node('CHILD', {
|
|
227
|
+
pane: dead, tmux_session: session, window,
|
|
228
|
+
pi_pid: deadPid(), pi_session_id: 'booted', intent: null, status: 'active',
|
|
229
|
+
}));
|
|
230
|
+
subscribe('PARENT', 'CHILD', true);
|
|
231
|
+
// CHILD has NO live subscription, but holds a pending self-wake (far-future
|
|
232
|
+
// so the wakeups pass never fires it this tick). hasPendingSelfWake(CHILD)
|
|
233
|
+
// ⇒ the daemon treats it as STILL WAITING (the stop-guard's boundary), so
|
|
234
|
+
// it RELEASES instead of finalizing. NON-VACUOUS: drop the hasPendingSelfWake
|
|
235
|
+
// guard and CHILD finalizes → PARENT is spuriously woken.
|
|
236
|
+
const future = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
|
|
237
|
+
armWake({ wakeup_id: 'wk-cdw-child', node_id: 'CHILD', owner_id: 'CHILD', fire_at: future, kind: 'bare' });
|
|
238
|
+
const before = cyclesOf('PARENT');
|
|
239
|
+
await superviseTick();
|
|
240
|
+
assert.equal(getNode('CHILD').status, 'idle', 'CHILD with a pending self-wake is RELEASED, not finalized');
|
|
241
|
+
assert.equal(getNode('CHILD').intent, 'idle-release', 'CHILD routed to idle-release (its clock will wake it)');
|
|
242
|
+
assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — a pending clock is healthy dormancy');
|
|
243
|
+
assert.equal(cyclesOf('PARENT'), before, 'dormant PARENT stays asleep — pending self-wake is not death');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
// Tests for the <crtr-context> bearings preamble
|
|
2
|
-
// 1.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// live index POINTER LINES only (the how-to boilerplate is dropped — it
|
|
8
|
-
// lives once in the kernel); promotion delivers that same orchestrator
|
|
1
|
+
// Tests for the <crtr-context> bearings preamble:
|
|
2
|
+
// 1. Worker and orchestrator bearings carry the `## References` block
|
|
3
|
+
// (substrate reference docs + node-local docs as `###` sub-sections) in
|
|
4
|
+
// place of the removed `<memory>` block — no `label · dir` stanzas, no
|
|
5
|
+
// `(empty)` markers, no MEMORY.md index inlining. Orchestrators add the
|
|
6
|
+
// across-cycles framing; promotion delivers that same orchestrator
|
|
9
7
|
// context-dir framing to a node that spawned base.
|
|
10
|
-
//
|
|
8
|
+
// 2. canvas-context-intro injects the block as its own session message at
|
|
11
9
|
// session_start (before the first prompt), idempotent across resumes.
|
|
12
10
|
//
|
|
13
11
|
// Run: node --import tsx/esm --test src/core/__tests__/context-intro.test.ts
|
|
14
12
|
import { test, before, beforeEach, after } from 'node:test';
|
|
15
13
|
import assert from 'node:assert/strict';
|
|
16
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
15
|
import { tmpdir } from 'node:os';
|
|
18
16
|
import { join } from 'node:path';
|
|
19
|
-
import { existsSync } from 'node:fs';
|
|
20
17
|
import { closeDb } from '../canvas/db.js';
|
|
21
18
|
import { contextDir } from '../canvas/paths.js';
|
|
22
19
|
import { spawnNode } from '../runtime/nodes.js';
|
|
23
20
|
import { promote } from '../runtime/promote.js';
|
|
24
21
|
import { personaDrift } from '../runtime/persona.js';
|
|
25
|
-
import {
|
|
22
|
+
import { memoryDir } from '../runtime/memory.js';
|
|
26
23
|
import registerCanvasContextIntro, { buildContextIntro, renderContextMessage, CONTEXT_INTRO_CUSTOM_TYPE, } from '../../pi-extensions/canvas-context-intro.js';
|
|
27
24
|
let home;
|
|
28
25
|
before(() => {
|
|
@@ -39,69 +36,80 @@ after(() => {
|
|
|
39
36
|
delete process.env['CRTR_HOME'];
|
|
40
37
|
delete process.env['CRTR_NODE_ID'];
|
|
41
38
|
});
|
|
42
|
-
test('
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
assert.ok(existsSync(memoryDir(meta.node_id)), 'memory dir created for direct writes');
|
|
48
|
-
assert.ok(memoryPath(meta.node_id).endsWith('/memory/MEMORY.md'), 'index lives inside the memory dir');
|
|
49
|
-
assert.equal(readMemory(meta.node_id), MEMORY_TEMPLATE);
|
|
50
|
-
});
|
|
51
|
-
test('seedMemory is idempotent — never clobbers an evolved memory', () => {
|
|
52
|
-
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
53
|
-
seedMemory(meta.node_id);
|
|
54
|
-
const evolved = '# Memory\n\n## Lessons\n- never trust the cache\n';
|
|
55
|
-
writeFileSync(memoryPath(meta.node_id), evolved);
|
|
56
|
-
assert.equal(seedMemory(meta.node_id), false, 'returns false when one exists');
|
|
57
|
-
assert.equal(readMemory(meta.node_id), evolved, 'left untouched');
|
|
58
|
-
});
|
|
59
|
-
test('worker bearings: shared-doc framing only, no orchestrator/memory framing', () => {
|
|
39
|
+
test('worker bearings: base framing + ## References block (no <memory>, no label·dir stanzas), NO across-cycles framing', () => {
|
|
40
|
+
// Bug-regression: review finding M1 — buildContextBearings was changed from
|
|
41
|
+
// buildMemoryBlock (<memory> + label·dir per-store stanzas) to
|
|
42
|
+
// renderReferencesBlock (## References + ### <name> sub-sections). This test
|
|
43
|
+
// locks in the new contract.
|
|
60
44
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
45
|
+
// Seed a node-local substrate doc so the ## References block is non-empty.
|
|
46
|
+
const dir = memoryDir(meta.node_id);
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
writeFileSync(join(dir, 'test-ref.md'), '---\nkind: reference\nwhen: when testing\nwhy: regression fixture\nsystem-prompt-visibility: preview\n---\nTest body.\n');
|
|
61
49
|
const block = buildContextIntro(meta.node_id);
|
|
62
50
|
assert.match(block, new RegExp(`<crtr-context dir="${contextDir(meta.node_id)}">`));
|
|
63
51
|
assert.match(block, /shared document store, not a task tracker/, 'base = shared docs, not tasks');
|
|
64
|
-
|
|
65
|
-
assert.doesNotMatch(block, /<memory>/, 'no memory block
|
|
52
|
+
// New contract: NO <memory> block — replaced by ## References.
|
|
53
|
+
assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
|
|
54
|
+
// New contract: NO label·dir stanza headers (the removed per-store format).
|
|
55
|
+
assert.doesNotMatch(block, /user-global · /, 'no user-global label·dir stanza');
|
|
56
|
+
assert.doesNotMatch(block, /node-local · /, 'no node-local label·dir stanza');
|
|
57
|
+
// New contract: NO (empty) placeholder (the removed empty-store marker).
|
|
58
|
+
assert.doesNotMatch(block, /\(empty\)/, 'no (empty) placeholder');
|
|
59
|
+
// Reference content renders as ## References + ### sub-sections — no other shape.
|
|
60
|
+
assert.match(block, /## References\n\n###/, '## References followed by ### sub-sections');
|
|
61
|
+
// A terminal worker must NOT carry the orchestrator across-cycles framing.
|
|
66
62
|
assert.doesNotMatch(block, /refresh cycles/, 'no across-cycles framing for a terminal worker');
|
|
67
63
|
assert.match(block, /<\/crtr-context>/);
|
|
68
64
|
});
|
|
69
|
-
test('orchestrator bearings: across-cycles framing +
|
|
65
|
+
test('orchestrator bearings: across-cycles framing + node-local substrate docs ride into ## References; the MEMORY.md index never renders', () => {
|
|
66
|
+
// Bug-regression: review finding M1 — buildContextBearings replaced the old
|
|
67
|
+
// <memory> pointer-line block with ## References. Node-local substrate docs
|
|
68
|
+
// render as ### sub-sections at their rung; the old index file never surfaces.
|
|
70
69
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
promote(meta.node_id); // flip to orchestrator mode — the across-cycles gate
|
|
71
|
+
const dir = memoryDir(meta.node_id);
|
|
72
|
+
mkdirSync(dir, { recursive: true });
|
|
73
|
+
// Write a MEMORY.md file to verify it is never surfaced in the block.
|
|
74
|
+
const legacyIndexPath = join(dir, 'MEMORY.md');
|
|
75
|
+
writeFileSync(legacyIndexPath, '# memory index — one pointer line per memory; how-to in "Your long-term memory".\n\n- [Flaky build](flaky-build.md) — first run fails\n');
|
|
76
|
+
// A node-local substrate doc DOES ride into ## References at its rung.
|
|
77
|
+
writeFileSync(join(dir, 'flaky-build.md'), '---\nkind: reference\nwhen: when the build flakes\nwhy: first run fails\nsystem-prompt-visibility: preview\n---\nFirst run always fails; rerun once.\n');
|
|
75
78
|
const block = buildContextIntro(meta.node_id);
|
|
76
79
|
assert.match(block, /shared document store, not a task tracker/, 'still carries the base framing');
|
|
77
80
|
assert.match(block, /refresh cycles/, 'orchestrator gets the across-cycles framing');
|
|
78
|
-
assert.
|
|
79
|
-
|
|
80
|
-
assert.
|
|
81
|
-
assert.match(block,
|
|
82
|
-
//
|
|
83
|
-
assert.ok(!block.includes('index:'), 'no index: substring');
|
|
84
|
-
assert.ok(!block.includes(memoryPath(meta.node_id)), 'no absolute index (MEMORY.md) path');
|
|
85
|
-
assert.ok(!block.includes('how-to in'), 'the index how-to boilerplate is NOT inlined');
|
|
81
|
+
assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
|
|
82
|
+
assert.match(block, /## References\n/, 'references block present');
|
|
83
|
+
assert.match(block, /### flaky-build\n/, 'node-local doc gets its ### sub-section');
|
|
84
|
+
assert.match(block, /when the build flakes, read this reference\. first run fails\./, 'preview rung renders the routing line');
|
|
85
|
+
// The index file itself never renders: no header comment, no pointer line, no path.
|
|
86
86
|
assert.ok(!block.includes('# memory index'), 'the index header comment is NOT inlined');
|
|
87
|
+
assert.ok(!block.includes('- [Flaky build](flaky-build.md)'), 'index pointer lines are NOT inlined');
|
|
88
|
+
assert.ok(!block.includes(legacyIndexPath), 'no absolute index (MEMORY.md) path');
|
|
89
|
+
assert.ok(!block.includes('node-local · '), 'no label·dir stanza header');
|
|
87
90
|
assert.match(block, /<\/crtr-context>/);
|
|
88
91
|
});
|
|
89
|
-
test('orchestrator bearings: a
|
|
92
|
+
test('orchestrator bearings: no per-store stanzas or (empty) markers; a rung-none node-local doc still surfaces as a ### title stub', () => {
|
|
93
|
+
// Bug-regression: review findings M1 + M6 — the three-store `label · dir`
|
|
94
|
+
// stanzas and (empty) markers are gone, and node-local docs are NOT filtered
|
|
95
|
+
// on rung: a migrated node-local reference defaults
|
|
96
|
+
// system-prompt-visibility: none and must still ride into ## References as
|
|
97
|
+
// its bare title (never its body).
|
|
90
98
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
91
|
-
promote(meta.node_id); //
|
|
99
|
+
promote(meta.node_id); // flip to orchestrator mode
|
|
100
|
+
const dir = memoryDir(meta.node_id);
|
|
101
|
+
mkdirSync(dir, { recursive: true });
|
|
102
|
+
writeFileSync(join(dir, 'rung-none-fact.md'), '---\nkind: reference\n---\nbody that must not render at the none rung\n');
|
|
92
103
|
const block = buildContextIntro(meta.node_id);
|
|
93
|
-
//
|
|
94
|
-
assert.ok(block.includes(`
|
|
95
|
-
assert.ok(block.includes(
|
|
96
|
-
assert.ok(block.includes(
|
|
97
|
-
|
|
98
|
-
assert.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
assert.ok(!block.includes(
|
|
102
|
-
assert.ok(!block.includes(projectMemoryPath('/tmp/work')), 'no project MEMORY.md path');
|
|
103
|
-
// The type→store mapping is taught by the kernel; the block points at it.
|
|
104
|
-
assert.match(block, /type/, 'the block references the type that decides the store');
|
|
104
|
+
// No per-store stanza headers, no (empty) markers, no index paths.
|
|
105
|
+
assert.ok(!block.includes(`node-local · ${memoryDir(meta.node_id)}`), 'no node-local stanza header');
|
|
106
|
+
assert.ok(!block.includes('user-global · '), 'no user-global stanza header');
|
|
107
|
+
assert.ok(!block.includes('project · '), 'no project stanza header');
|
|
108
|
+
assert.doesNotMatch(block, /\(empty\)/, 'no (empty) markers');
|
|
109
|
+
assert.ok(!block.includes('MEMORY.md'), 'no MEMORY.md path in the block');
|
|
110
|
+
// M6: rung-none node-local doc surfaces as its title stub only.
|
|
111
|
+
assert.match(block, /### rung-none-fact\s*\n/, 'rung-none node-local doc surfaces as its title stub');
|
|
112
|
+
assert.ok(!block.includes('body that must not render'), 'none rung renders the title only, not the body');
|
|
105
113
|
});
|
|
106
114
|
test('promotion guidance delivers the orchestrator context-dir framing', () => {
|
|
107
115
|
// A node spawns as a base worker (no orchestrator bearings). On promotion the
|
|
@@ -145,12 +153,18 @@ test('session_start injects the block as the first message of an empty session',
|
|
|
145
153
|
test('session_start is idempotent across resume (skips if already in history)', () => {
|
|
146
154
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
147
155
|
process.env['CRTR_NODE_ID'] = meta.node_id;
|
|
148
|
-
// Restored session already carries
|
|
156
|
+
// Restored session already carries OUR block (which names this node id) → no
|
|
157
|
+
// duplicate injection. The content matters: the guard is fork-aware and only
|
|
158
|
+
// skips when the present bearings name THIS node (a fork inherits the
|
|
159
|
+
// SOURCE's bearings, which name a different node — see canvas-context-intro
|
|
160
|
+
// tests), so a realistic resume entry must carry the real block content.
|
|
149
161
|
const pi = makeFakePi();
|
|
150
162
|
registerCanvasContextIntro(pi);
|
|
151
|
-
const entries = [
|
|
163
|
+
const entries = [
|
|
164
|
+
{ type: 'custom_message', customType: CONTEXT_INTRO_CUSTOM_TYPE, content: buildContextIntro(meta.node_id) },
|
|
165
|
+
];
|
|
152
166
|
pi.handler({ reason: 'resume' }, fakeCtx(entries));
|
|
153
|
-
assert.equal(pi.sent.length, 0, 'block already in history → skip');
|
|
167
|
+
assert.equal(pi.sent.length, 0, 'our own block already in history → skip');
|
|
154
168
|
});
|
|
155
169
|
test('session_start is inert when CRTR_NODE_ID is absent', () => {
|
|
156
170
|
delete process.env['CRTR_NODE_ID'];
|
|
@@ -64,10 +64,14 @@ test('never-booted node is marked dead and surfaced to the parent as urgent', as
|
|
|
64
64
|
assert.equal(inbox[0].from, 'C');
|
|
65
65
|
assert.match(inbox[0].label, /Spawn failed/);
|
|
66
66
|
});
|
|
67
|
-
// A node that HAD booted (pi_session_id set) then
|
|
68
|
-
// crash, not a boot failure
|
|
69
|
-
//
|
|
70
|
-
|
|
67
|
+
// A node that HAD booted (pi_session_id set) then died mid-generation is a
|
|
68
|
+
// genuine crash, not a boot failure. It is marked dead AND wakes the inbox-
|
|
69
|
+
// waiting parent — but with a "child died" notice (surfaceChildDeath), NOT a
|
|
70
|
+
// "Spawn failed" boot-failure pointer (surfaceBootFailure). The parent-wake
|
|
71
|
+
// itself was added by main's b5abf6e (parent-wake on child death) and is locked
|
|
72
|
+
// in by child-death-wake.test.ts; this test preserves the discriminating intent
|
|
73
|
+
// — a crash is never surfaced as a false spawn-failure alarm.
|
|
74
|
+
test('a crash after boot wakes the parent with a child-death notice, not a boot-failure push', async () => {
|
|
71
75
|
createNode(node('P2', { kind: 'developer', lifecycle: 'resident' }));
|
|
72
76
|
createNode(node('C2', {
|
|
73
77
|
parent: 'P2',
|
|
@@ -85,7 +89,12 @@ test('a crash after boot is marked dead without a boot-failure push', async () =
|
|
|
85
89
|
markBusy('C2');
|
|
86
90
|
await superviseTick();
|
|
87
91
|
assert.equal(getNode('C2').status, 'dead', 'crashed child is dead');
|
|
88
|
-
|
|
92
|
+
const inbox = readInboxSince('P2');
|
|
93
|
+
assert.equal(inbox.length, 1, 'a genuine mid-run crash wakes the inbox-waiting parent (D-1 parent-wake)');
|
|
94
|
+
assert.equal(inbox[0].tier, 'urgent', 'mid-run crash is delivered as urgent');
|
|
95
|
+
assert.equal(inbox[0].from, 'C2');
|
|
96
|
+
assert.match(inbox[0].label, /died/i, 'the wake is a child-death notice (surfaceChildDeath), not silence');
|
|
97
|
+
assert.doesNotMatch(inbox[0].label, /Spawn failed/, 'a crash is NOT surfaced as a false boot-failure alarm');
|
|
89
98
|
clearBusy('C2');
|
|
90
99
|
});
|
|
91
100
|
// A still-booting node whose window is alive must be left untouched — boot is
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|
|
3
3
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
7
|
import { createNode, getNode, subscribe } from '../canvas/canvas.js';
|
|
8
8
|
import { closeDb } from '../canvas/db.js';
|
|
9
9
|
import { appendInbox } from '../feed/inbox.js';
|
|
@@ -32,6 +32,17 @@ function deadPid() {
|
|
|
32
32
|
// spawnSync has reaped it by the time it returns; pid may be on r.pid.
|
|
33
33
|
return r.pid ?? 0x7ffffffe; // fall back to an implausibly-high pid
|
|
34
34
|
}
|
|
35
|
+
/** A pid that is ALIVE for the test's duration but expendable. Pane-gone
|
|
36
|
+
* fixtures must NEVER use process.pid for an alive pi: the gone-pane zombie
|
|
37
|
+
* sweep (b92082e) SIGKILLs a pane-gone node's live pid, which took the whole
|
|
38
|
+
* test runner down with it. The daemon (or the after hook) reaps these. */
|
|
39
|
+
const livePids = [];
|
|
40
|
+
function disposableLivePid() {
|
|
41
|
+
const child = spawn('sleep', ['600'], { stdio: 'ignore', detached: true });
|
|
42
|
+
child.unref();
|
|
43
|
+
livePids.push(child.pid);
|
|
44
|
+
return child.pid;
|
|
45
|
+
}
|
|
35
46
|
before(() => {
|
|
36
47
|
home = mkdtempSync(join(tmpdir(), 'crtr-daemon-liveness-'));
|
|
37
48
|
process.env['CRTR_HOME'] = home;
|
|
@@ -44,6 +55,14 @@ after(() => {
|
|
|
44
55
|
closeDb();
|
|
45
56
|
rmSync(home, { recursive: true, force: true });
|
|
46
57
|
delete process.env['CRTR_HOME'];
|
|
58
|
+
for (const pid of livePids) {
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 'SIGKILL');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* already reaped by the daemon's zombie sweep — fine */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
47
66
|
});
|
|
48
67
|
// ---------------------------------------------------------------------------
|
|
49
68
|
// livenessVerdict — the pure grace-window decision
|
|
@@ -157,7 +176,7 @@ test('pane GONE while its old window is still alive → the gone-branch fires (c
|
|
|
157
176
|
pane: dead,
|
|
158
177
|
tmux_session: session,
|
|
159
178
|
window,
|
|
160
|
-
pi_pid:
|
|
179
|
+
pi_pid: disposableLivePid(), // alive zombie — the gone-pane sweep SIGKILLs it
|
|
161
180
|
pi_session_id: 'booted',
|
|
162
181
|
intent: null,
|
|
163
182
|
}));
|
|
@@ -172,7 +191,7 @@ test('pane GONE while its old window is still alive → the gone-branch fires (c
|
|
|
172
191
|
// unconditional 'dead'. It routes on what the node was DOING at pane-kill time:
|
|
173
192
|
// • mid-generation (busy marker PRESENT) → crash ('dead')
|
|
174
193
|
// • finished its turn, awaiting nothing live → finalize ('done')
|
|
175
|
-
// • finished its turn, still awaiting a LIVE child →
|
|
194
|
+
// • finished its turn, still awaiting a LIVE child → release ('idle' + idle-release), revivable
|
|
176
195
|
// (The mid-generation → dead leg is covered by node 'G' above.)
|
|
177
196
|
// ---------------------------------------------------------------------------
|
|
178
197
|
test('pane gone + booted + busy ABSENT + no live subscription → finalize (done): the pane was closed to dismiss a finished node', { skip: !hasTmux() }, async () => {
|
|
@@ -187,7 +206,7 @@ test('pane gone + booted + busy ABSENT + no live subscription → finalize (done
|
|
|
187
206
|
pane: dead,
|
|
188
207
|
tmux_session: session,
|
|
189
208
|
window,
|
|
190
|
-
pi_pid:
|
|
209
|
+
pi_pid: disposableLivePid(), // alive zombie — swept before finalize routing
|
|
191
210
|
pi_session_id: 'booted',
|
|
192
211
|
intent: null,
|
|
193
212
|
}));
|
|
@@ -196,7 +215,7 @@ test('pane gone + booted + busy ABSENT + no live subscription → finalize (done
|
|
|
196
215
|
assert.equal(getNode('FIN').status, 'done', 'a finished, unsubscribed node whose pane was closed finalizes to done');
|
|
197
216
|
});
|
|
198
217
|
});
|
|
199
|
-
test('pane gone + booted + busy ABSENT but AWAITING a LIVE child →
|
|
218
|
+
test('pane gone + booted + busy ABSENT but AWAITING a LIVE child → idle-release (revivable, NOT dead)', { skip: !hasTmux() }, async () => {
|
|
200
219
|
await withLivePane('fin2', async (session, window) => {
|
|
201
220
|
const sp = spawnSync('tmux', ['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', `${session}:${window}`, 'sleep 600'], { encoding: 'utf8' });
|
|
202
221
|
const dead = (sp.stdout ?? '').trim();
|
|
@@ -207,15 +226,21 @@ test('pane gone + booted + busy ABSENT but AWAITING a LIVE child → crash (dead
|
|
|
207
226
|
pane: dead,
|
|
208
227
|
tmux_session: session,
|
|
209
228
|
window,
|
|
210
|
-
pi_pid:
|
|
229
|
+
pi_pid: deadPid(), // the user closed the pane → pi is dead
|
|
211
230
|
pi_session_id: 'booted',
|
|
212
231
|
intent: null,
|
|
213
232
|
}));
|
|
214
233
|
subscribe('PARENT', 'CHILD', true); // active subscription to a LIVE node
|
|
215
|
-
//
|
|
216
|
-
//
|
|
234
|
+
// REGRESSION: closing the pane of a waiting orchestrator must NOT kill it.
|
|
235
|
+
// PARENT finished its turn (no busy marker) but still awaits a live child, so
|
|
236
|
+
// a hard 'dead' would orphan the child's report. It routes to idle-release
|
|
237
|
+
// (revivable); the second pass revives it on the next inbox push. No inbox
|
|
238
|
+
// entry here, so it stays dormant (idle + idle-release) — revivable, not dead.
|
|
217
239
|
await superviseTick();
|
|
218
|
-
|
|
240
|
+
const p = getNode('PARENT');
|
|
241
|
+
assert.equal(p.status, 'idle', 'a finished node still awaiting a live child is RELEASED, not killed');
|
|
242
|
+
assert.equal(p.intent, 'idle-release', 'routed to idle-release so a child push revives it');
|
|
243
|
+
assert.equal(p.window ?? null, null, 'the stale window ref is dropped');
|
|
219
244
|
});
|
|
220
245
|
});
|
|
221
246
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|