@crouton-kit/crouter 0.3.20 → 0.3.22

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.
Files changed (144) hide show
  1. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -4
  2. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +3 -5
  3. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +3 -6
  4. package/dist/builtin-memory/crouter-development/personas.md +2 -6
  5. package/dist/builtin-memory/crouter-development/plugins.md +2 -4
  6. package/dist/builtin-memory/design.md +2 -4
  7. package/dist/builtin-memory/development.md +3 -3
  8. package/dist/builtin-memory/planning.md +3 -4
  9. package/dist/builtin-memory/spec.md +3 -10
  10. package/dist/builtin-personas/orchestration-kernel.md +1 -1
  11. package/dist/commands/canvas-history/read.d.ts +1 -0
  12. package/dist/commands/canvas-history/read.js +61 -0
  13. package/dist/commands/canvas-history/search.d.ts +1 -0
  14. package/dist/commands/canvas-history/search.js +270 -0
  15. package/dist/commands/canvas-history/show.d.ts +1 -0
  16. package/dist/commands/canvas-history/show.js +79 -0
  17. package/dist/commands/canvas-history.d.ts +2 -0
  18. package/dist/commands/canvas-history.js +33 -0
  19. package/dist/commands/canvas.js +3 -2
  20. package/dist/commands/human/queue.js +2 -2
  21. package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
  22. package/dist/commands/memory/find.js +14 -52
  23. package/dist/commands/memory/lint.d.ts +7 -0
  24. package/dist/commands/memory/lint.js +29 -35
  25. package/dist/commands/memory/list.js +8 -18
  26. package/dist/commands/memory/read.js +4 -37
  27. package/dist/commands/memory/shared.js +1 -2
  28. package/dist/commands/memory/write.js +6 -9
  29. package/dist/commands/node.js +47 -25
  30. package/dist/commands/pkg/bridge.d.ts +1 -0
  31. package/dist/commands/pkg/bridge.js +137 -0
  32. package/dist/commands/pkg/plugin-inspect.js +7 -6
  33. package/dist/commands/pkg.js +2 -1
  34. package/dist/commands/push.js +10 -15
  35. package/dist/commands/skill/author.js +35 -44
  36. package/dist/commands/skill/shared.d.ts +1 -5
  37. package/dist/commands/skill/shared.js +9 -63
  38. package/dist/commands/skill.js +5 -6
  39. package/dist/commands/sys/doctor.js +6 -144
  40. package/dist/commands/tmux-spread.js +16 -1
  41. package/dist/commands/view-cycle.js +9 -5
  42. package/dist/commands/view-run.js +3 -2
  43. package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
  44. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
  45. package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
  46. package/dist/core/__tests__/child-death-wake.test.js +133 -185
  47. package/dist/core/__tests__/context-intro.test.js +23 -27
  48. package/dist/core/__tests__/daemon-liveness.test.js +28 -258
  49. package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
  50. package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
  51. package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
  52. package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
  53. package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
  54. package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
  55. package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
  56. package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +4 -3
  57. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
  58. package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
  59. package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +7 -7
  60. package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
  61. package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
  62. package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
  63. package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
  64. package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
  65. package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
  66. package/dist/core/__tests__/full/spike-harness.test.d.ts +1 -0
  67. package/dist/core/__tests__/full/spike-harness.test.js +117 -0
  68. package/dist/core/__tests__/grace-clock.test.js +72 -75
  69. package/dist/core/__tests__/helpers/harness.d.ts +35 -1
  70. package/dist/core/__tests__/helpers/harness.js +70 -12
  71. package/dist/core/__tests__/live-mutation-verbs.test.js +100 -105
  72. package/dist/core/__tests__/live-mutation.test.js +111 -134
  73. package/dist/core/__tests__/memory.test.js +4 -4
  74. package/dist/core/__tests__/revive.test.js +96 -86
  75. package/dist/core/__tests__/subscription-delivery.test.js +116 -138
  76. package/dist/core/__tests__/wake-origin.test.js +54 -50
  77. package/dist/core/bootstrap.js +18 -14
  78. package/dist/core/bridge-map.d.ts +19 -0
  79. package/dist/core/bridge-map.js +73 -0
  80. package/dist/core/canvas/history.d.ts +91 -0
  81. package/dist/core/canvas/history.js +389 -0
  82. package/dist/core/canvas/index.d.ts +1 -0
  83. package/dist/core/canvas/index.js +1 -0
  84. package/dist/core/config.js +3 -22
  85. package/dist/core/help.d.ts +2 -2
  86. package/dist/core/help.js +1 -1
  87. package/dist/core/memory-resolver.d.ts +9 -2
  88. package/dist/core/memory-resolver.js +47 -7
  89. package/dist/core/render.d.ts +4 -3
  90. package/dist/core/render.js +38 -41
  91. package/dist/core/resolver.d.ts +1 -30
  92. package/dist/core/resolver.js +6 -546
  93. package/dist/core/runtime/bearings.d.ts +2 -2
  94. package/dist/core/runtime/bearings.js +2 -2
  95. package/dist/core/runtime/persona.js +5 -7
  96. package/dist/core/scope.d.ts +7 -5
  97. package/dist/core/scope.js +16 -17
  98. package/dist/core/substrate/ceiling.d.ts +28 -0
  99. package/dist/core/substrate/ceiling.js +87 -0
  100. package/dist/core/substrate/index.d.ts +2 -0
  101. package/dist/core/substrate/index.js +1 -0
  102. package/dist/core/substrate/on-read.js +18 -2
  103. package/dist/core/substrate/render.d.ts +3 -5
  104. package/dist/core/substrate/render.js +68 -59
  105. package/dist/core/substrate/schema.d.ts +18 -13
  106. package/dist/core/substrate/schema.js +12 -11
  107. package/dist/pi-extensions/canvas-context-intro.js +5 -3
  108. package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
  109. package/dist/types.d.ts +3 -18
  110. package/dist/types.js +0 -8
  111. package/package.json +4 -3
  112. package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
  113. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
  114. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
  115. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
  116. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
  117. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
  118. package/dist/builtin-skills/skills/design/SKILL.md +0 -51
  119. package/dist/builtin-skills/skills/development/SKILL.md +0 -109
  120. package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
  121. package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
  122. package/dist/commands/skill/state.d.ts +0 -3
  123. package/dist/commands/skill/state.js +0 -71
  124. package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
  125. package/dist/core/__tests__/broker-dialogs.test.js +0 -126
  126. package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
  127. package/dist/core/__tests__/resolver.test.js +0 -181
  128. package/dist/core/__tests__/spike-harness.test.js +0 -242
  129. /package/dist/{core/__tests__/broker-attach-limits.test.d.ts → commands/memory/__tests__/lint-schema.test.d.ts} +0 -0
  130. /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
  131. /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
  132. /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
  133. /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
  134. /package/dist/core/__tests__/{cascade-close.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
  135. /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/cascade-close.test.d.ts} +0 -0
  136. /package/dist/core/__tests__/{detach-focus.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
  137. /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
  138. /package/dist/core/__tests__/{placement-focus.test.d.ts → full/detach-focus.test.d.ts} +0 -0
  139. /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
  140. /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-focus.test.d.ts} +0 -0
  141. /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
  142. /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-revive.test.d.ts} +0 -0
  143. /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
  144. /package/dist/core/__tests__/{spike-harness.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
@@ -1,39 +1,46 @@
1
1
  // Run with: node --import tsx/esm --test src/core/__tests__/child-death-wake.test.ts
2
2
  //
3
- // D-1 BUG REGRESSION + node/canvas LIFECYCLE — "the runtime wakes a dormant
4
- // parent on ANY terminal child outcome."
3
+ // HEADLESS RETARGET (foundation-spec §C.7 + §E). D-1 BUG REGRESSION — "the
4
+ // runtime wakes a dormant parent on EVERY terminal child outcome, and ONLY on a
5
+ // genuine one." Drives the REAL daemon decision pass (superviseTick) + the REAL
6
+ // closeNode against canvas rows fabricated DIRECTLY in an isolated home — NO real
7
+ // tmux session, NO remain-on-exit pane, NO broker boot.
5
8
  //
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.
9
+ // (1) BUG LOCKED — the D-1 finding (context/d1-finding.md): a parent that
10
+ // delegated and then JUST STOPPED (inbox-wait retained, no self-wake armed)
11
+ // was woken ONLY when a child `push final`d or never booted. A child that
12
+ // CRASHED after booting, was daemon-FINALIZED on a quiet turn, or was `node
13
+ // close`d marked the child dead/done/canceled with NO push to the parent so
14
+ // a purely-inbox-waiting parent hung dormant forever. The fix fans a system
15
+ // inbox entry to subscribersOf(child) on those three previously-silent
16
+ // terminal outcomes (surfaceChildDeath crash/finalize + close.ts), mirroring
17
+ // surfaceBootFailure. THE CRUX: it must fire ONLY on genuine death — NEVER on
18
+ // healthy dormancy (a child that ended its turn still awaiting a LIVE
19
+ // grandchild, or holding a pending self-wake, is alive-and-dormant; waking its
20
+ // parent then re-creates the spurious-wake storm the doctrine exists to kill).
14
21
  //
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)
22
+ // (2) WHY MODEL-LEVEL, NOT TMUX CHROME the wake travels through ONE pure
23
+ // data-layer channel the d1-finding identified: a system entry appended to
24
+ // the parent's inbox.jsonl (push() → appendInbox, no tmux). The death-vs-
25
+ // dormancy boundary is pure daemon logic finishedTurn (isBusy) +
26
+ // hasActiveLiveSubscription || hasPendingSelfWake, the SAME boundary the
27
+ // stop-guard draws (crtrd.ts). The daemon reaches the pane-gone routing for
28
+ // ANY node whose pane is not alive; a row carrying a bogus pane id reads
29
+ // "gone" (paneExists echoes empty for an unknown/absent pane — and with no
30
+ // tmux server at all, the probe simply fails to false) with NO live session
31
+ // to stand up. So the exact "booted child, pane gone, branch on what it was
32
+ // doing" state reproduces with no real pane, and the parent stays an inline
33
+ // node (no placement → the supervise loop's inline-root carve-out skips it),
34
+ // so nothing in the test opens or needs a window.
20
35
  //
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).
36
+ // (3) HOW THE FABRICATED DRIVE STILL FAILS IF THE BUG REGRESSES each direction
37
+ // asserts the inbox channel: a genuine terminal outcome (crash/finalize/close)
38
+ // MUST land a system entry from the child in the parent's inbox; healthy
39
+ // dormancy MUST leave it empty. Drop the crash-branch surfaceChildDeath call
40
+ // and the CRASH assertion goes RED (no entry); drop the stillWaiting guard and
41
+ // a healthy-dormant child finalizes the CRUX asserts go RED (a spurious
42
+ // entry appears). Verified by reverting the crash-branch surfaceChildDeath
43
+ // call (see bug-injection report).
37
44
  import { test, before, after, beforeEach } from 'node:test';
38
45
  import assert from 'node:assert/strict';
39
46
  import { mkdtempSync, rmSync } from 'node:fs';
@@ -61,15 +68,39 @@ function node(id, over = {}) {
61
68
  ...over,
62
69
  };
63
70
  }
64
- function hasTmux() {
65
- return spawnSync('tmux', ['-V'], { stdio: 'ignore' }).status === 0;
66
- }
67
71
  /** A pid that is guaranteed dead. */
68
72
  function deadPid() {
69
73
  const r = spawnSync('true', [], { stdio: 'ignore' });
70
74
  return r.pid ?? 0x7ffffffe;
71
75
  }
72
- const cyclesOf = (id) => getNode(id)?.cycles ?? 0;
76
+ // A pane id no tmux server knows: paneExists echoes empty for it → isNodePaneAlive
77
+ // is false → the supervise loop routes the row down its pane-gone branch with NO
78
+ // live session required (and with no server at all the probe fails to false too).
79
+ const GONE_PANE = '%999999';
80
+ /** A booted child whose pane is GONE — the daemon's pane-gone routing input.
81
+ * `intent:null` so it takes the user-close branch (neither refresh nor
82
+ * idle-release); pi dead so the zombie-kill is skipped; pi_session_id set so it
83
+ * is "booted" (not the never-booted boot-failure leg). The caller layers on the
84
+ * busy marker / subscriptions that decide crash vs finalize vs release. */
85
+ function paneGoneChild(id, over = {}) {
86
+ createNode(node(id, {
87
+ pane: GONE_PANE,
88
+ tmux_session: 'crtr-cdw',
89
+ window: '@1',
90
+ pi_pid: deadPid(),
91
+ pi_session_id: 'booted',
92
+ intent: null,
93
+ status: 'active',
94
+ ...over,
95
+ }));
96
+ }
97
+ /** A dormant inbox-waiting PARENT — an INLINE node (no tmux placement), so the
98
+ * supervise loop's inline-root carve-out skips it entirely: it is only the wake
99
+ * TARGET, asserted via its inbox. */
100
+ function inboxWaitingParent(id) {
101
+ createNode(node(id, { status: 'active' }));
102
+ }
103
+ const wakeFromChild = (parent, child) => readInboxSince(parent).find((e) => e.from === child);
73
104
  before(() => {
74
105
  home = mkdtempSync(join(tmpdir(), 'crtr-child-death-wake-'));
75
106
  process.env['CRTR_HOME'] = home;
@@ -83,163 +114,80 @@ after(() => {
83
114
  rmSync(home, { recursive: true, force: true });
84
115
  delete process.env['CRTR_HOME'];
85
116
  });
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
117
  // ===========================================================================
128
- // POSITIVE — a genuine terminal child outcome WAKES the dormant parent.
118
+ // POSITIVE — a genuine terminal child outcome WAKES the inbox-waiting parent.
129
119
  // ===========================================================================
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
- });
120
+ test('CRASH after boot (mid-generation, pane gone) wakes the inbox-waiting parent', async () => {
121
+ inboxWaitingParent('PARENT');
122
+ paneGoneChild('CHILD');
123
+ subscribe('PARENT', 'CHILD', true); // the spawn-time spine edge
124
+ markBusy('CHILD'); // pane died INSIDE a turn → genuine mid-run crash
125
+ await superviseTick();
126
+ assert.equal(getNode('CHILD').status, 'dead', 'CHILD crashed dead');
127
+ // THE WAKE CHANNEL: a system entry from CHILD lands in PARENT's inbox.
128
+ const wake = wakeFromChild('PARENT', 'CHILD');
129
+ assert.ok(wake, 'crash fanned a system inbox entry to the dormant parent (mirrors surfaceBootFailure)');
130
+ assert.match(wake.label, /died/i, 'the entry tells the parent WHICH child died and how');
131
+ clearBusy('CHILD');
151
132
  });
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
- });
133
+ test('quiet-turn FINALIZE (pane gone, nothing live to wait for) wakes the inbox-waiting parent', async () => {
134
+ inboxWaitingParent('PARENT');
135
+ // Booted, turn finished (NO busy marker), no live subscription, no pending
136
+ // self-wake the daemon finalizes it (done): a dismissal of a finished node.
137
+ paneGoneChild('CHILD');
138
+ subscribe('PARENT', 'CHILD', true);
139
+ await superviseTick();
140
+ assert.equal(getNode('CHILD').status, 'done', 'CHILD finalized → done');
141
+ const wake = wakeFromChild('PARENT', 'CHILD');
142
+ assert.ok(wake, 'finalize fanned a system inbox entry to the dormant parent');
143
+ assert.match(wake.label, /without a final report/i, 'the entry tells the parent the child ended with no final');
171
144
  });
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
- });
145
+ test('node close of a child wakes its SURVIVING manager (the parent outside the closing set)', async () => {
146
+ inboxWaitingParent('PARENT');
147
+ // A booted child; closeNode tears its (paneless-for-the-test) engine down.
148
+ createNode(node('CHILD', { pane: GONE_PANE, tmux_session: 'crtr-cdw', window: '@1', pi_session_id: 'booted', status: 'active' }));
149
+ subscribe('PARENT', 'CHILD', true); // PARENT is a manager OUTSIDE the close set
150
+ // Closing CHILD: closingSet({CHILD}) cannot pull in PARENT (PARENT is a
151
+ // SUBSCRIBER of CHILD, never a descendant), so PARENT survives + must wake.
152
+ const res = closeNode('CHILD');
153
+ assert.deepEqual(res.closed, ['CHILD'], 'only CHILD is closed; PARENT spared');
154
+ assert.equal(getNode('CHILD').status, 'canceled', 'CHILD canceled by close');
155
+ const wake = wakeFromChild('PARENT', 'CHILD');
156
+ assert.ok(wake, 'node close fanned a child-closed entry to the surviving manager (D-1: previously none)');
157
+ assert.match(wake.label, /closed/i, 'the entry names the closed child');
194
158
  });
195
159
  // ===========================================================================
196
160
  // NEGATIVE — HEALTHY DORMANCY must NOT wake the parent (the correctness crux).
197
161
  // ===========================================================================
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
- });
162
+ test('CRUX: a child dormant while awaiting its OWN live grandchild does NOT wake the parent', async () => {
163
+ inboxWaitingParent('PARENT');
164
+ // CHILD finished its turn (no busy) and its pane is gone, BUT it actively
165
+ // subscribes to a LIVE grandchild — exactly the developer-spawned-reviewer
166
+ // case. This is HEALTHY dormancy, not death.
167
+ paneGoneChild('CHILD');
168
+ createNode(node('GRANDCHILD', { status: 'active', pi_session_id: 'booted' })); // live, inline
169
+ subscribe('PARENT', 'CHILD', true);
170
+ subscribe('CHILD', 'GRANDCHILD', true); // CHILD awaits a LIVE grandchild
171
+ await superviseTick();
172
+ // CHILD is RELEASED (revivable), NOT finalized — the daemon must not orphan
173
+ // its in-flight grandchild and must not wake the parent.
174
+ assert.equal(getNode('CHILD').status, 'idle', 'CHILD released (revivable), NOT finalized');
175
+ assert.equal(getNode('CHILD').intent, 'idle-release', 'CHILD routed to idle-release');
176
+ assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY — healthy dormancy raises no wake');
221
177
  });
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
- });
178
+ test('CRUX: a child dormant on a pending self-wake (no live sub) does NOT wake the parent', async () => {
179
+ inboxWaitingParent('PARENT');
180
+ paneGoneChild('CHILD');
181
+ subscribe('PARENT', 'CHILD', true);
182
+ // CHILD has NO live subscription, but holds a pending self-wake (far-future so
183
+ // the wakeups pass never fires it this tick). hasPendingSelfWake(CHILD) ⇒ the
184
+ // daemon treats it as STILL WAITING (the stop-guard's boundary), so it RELEASES
185
+ // instead of finalizing. NON-VACUOUS: drop the hasPendingSelfWake guard and
186
+ // CHILD finalizes → PARENT is spuriously woken.
187
+ const future = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();
188
+ armWake({ wakeup_id: 'wk-cdw-child', node_id: 'CHILD', owner_id: 'CHILD', fire_at: future, kind: 'bare' });
189
+ await superviseTick();
190
+ assert.equal(getNode('CHILD').status, 'idle', 'CHILD with a pending self-wake is RELEASED, not finalized');
191
+ assert.equal(getNode('CHILD').intent, 'idle-release', 'CHILD routed to idle-release (its clock will wake it)');
192
+ assert.equal(readInboxSince('PARENT').length, 0, 'PARENT inbox EMPTY a pending clock is healthy dormancy');
245
193
  });
@@ -1,10 +1,8 @@
1
1
  // Tests for the <crtr-context> bearings preamble:
2
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
7
- // context-dir framing to a node that spawned base.
3
+ // (substrate reference docs + node-local docs as `###` sub-sections).
4
+ // Orchestrators add the across-cycles framing; promotion delivers that
5
+ // same orchestrator context-dir framing to a node that spawned base.
8
6
  // 2. canvas-context-intro injects the block as its own session message at
9
7
  // session_start (before the first prompt), idempotent across resumes.
10
8
  //
@@ -36,53 +34,51 @@ after(() => {
36
34
  delete process.env['CRTR_HOME'];
37
35
  delete process.env['CRTR_NODE_ID'];
38
36
  });
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.
37
+ test('worker bearings: base framing + ## References block, NO across-cycles framing', () => {
38
+ // Bug-regression: review finding M1 — buildContextBearings renders the
39
+ // ## References block (renderReferencesBlock): ## References + ### <name>
40
+ // sub-sections. This test locks in that contract.
44
41
  const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
45
42
  // Seed a node-local substrate doc so the ## References block is non-empty.
46
43
  const dir = memoryDir(meta.node_id);
47
44
  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');
45
+ writeFileSync(join(dir, 'test-ref.md'), '---\nkind: reference\nwhen-and-why-to-read: When testing, this reference should be read because it is a regression fixture\nsystem-prompt-visibility: preview\n---\nTest body.\n');
49
46
  const block = buildContextIntro(meta.node_id);
50
47
  assert.match(block, new RegExp(`<crtr-context dir="${contextDir(meta.node_id)}">`));
51
48
  assert.match(block, /shared document store, not a task tracker/, 'base = shared docs, not tasks');
52
- // New contract: NO <memory> block replaced by ## References.
49
+ // Reference content renders ONLY as ## References + ### sub-sections.
53
50
  assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
54
- // New contract: NO label·dir stanza headers (the removed per-store format).
51
+ // Per-store stanza headers (label · dir) never appear.
55
52
  assert.doesNotMatch(block, /user-global · /, 'no user-global label·dir stanza');
56
53
  assert.doesNotMatch(block, /node-local · /, 'no node-local label·dir stanza');
57
- // New contract: NO (empty) placeholder (the removed empty-store marker).
54
+ // No (empty) placeholder marker.
58
55
  assert.doesNotMatch(block, /\(empty\)/, 'no (empty) placeholder');
59
- // Reference content renders as ## References + ### sub-sections — no other shape.
60
56
  assert.match(block, /## References\n\n###/, '## References followed by ### sub-sections');
61
57
  // A terminal worker must NOT carry the orchestrator across-cycles framing.
62
58
  assert.doesNotMatch(block, /refresh cycles/, 'no across-cycles framing for a terminal worker');
63
59
  assert.match(block, /<\/crtr-context>/);
64
60
  });
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.
61
+ test('orchestrator bearings: across-cycles framing + node-local substrate docs ride into ## References; a non-substrate .md file is never inlined', () => {
62
+ // Bug-regression: review finding M1 — buildContextBearings renders ## References.
63
+ // Node-local substrate docs render as ### sub-sections at their rung; a
64
+ // non-substrate .md file (no frontmatter `kind`) never surfaces.
69
65
  const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
70
66
  promote(meta.node_id); // flip to orchestrator mode — the across-cycles gate
71
67
  const dir = memoryDir(meta.node_id);
72
68
  mkdirSync(dir, { recursive: true });
73
- // Write a MEMORY.md file to verify it is never surfaced in the block.
69
+ // Write a non-substrate MEMORY.md file to verify it is never surfaced in the block.
74
70
  const legacyIndexPath = join(dir, 'MEMORY.md');
75
71
  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
72
  // 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');
73
+ writeFileSync(join(dir, 'flaky-build.md'), '---\nkind: reference\nwhen-and-why-to-read: When the build flakes, this reference should be read because the first run fails\nsystem-prompt-visibility: preview\n---\nFirst run always fails; rerun once.\n');
78
74
  const block = buildContextIntro(meta.node_id);
79
75
  assert.match(block, /shared document store, not a task tracker/, 'still carries the base framing');
80
76
  assert.match(block, /refresh cycles/, 'orchestrator gets the across-cycles framing');
81
77
  assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
82
78
  assert.match(block, /## References\n/, 'references block present');
83
79
  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.
80
+ assert.match(block, /When the build flakes, this reference should be read because the first run fails\./, 'preview rung renders the routing line');
81
+ // The non-substrate file never renders: no header line, no pointer line, no path.
86
82
  assert.ok(!block.includes('# memory index'), 'the index header comment is NOT inlined');
87
83
  assert.ok(!block.includes('- [Flaky build](flaky-build.md)'), 'index pointer lines are NOT inlined');
88
84
  assert.ok(!block.includes(legacyIndexPath), 'no absolute index (MEMORY.md) path');
@@ -90,9 +86,9 @@ test('orchestrator bearings: across-cycles framing + node-local substrate docs r
90
86
  assert.match(block, /<\/crtr-context>/);
91
87
  });
92
88
  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
89
+ // Bug-regression: review findings M1 + M6 — the ## References block carries no
90
+ // per-store `label · dir` stanzas or (empty) markers, and node-local docs are
91
+ // NOT filtered on rung: a migrated node-local reference defaults
96
92
  // system-prompt-visibility: none and must still ride into ## References as
97
93
  // its bare title (never its body).
98
94
  const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
@@ -101,7 +97,7 @@ test('orchestrator bearings: no per-store stanzas or (empty) markers; a rung-non
101
97
  mkdirSync(dir, { recursive: true });
102
98
  writeFileSync(join(dir, 'rung-none-fact.md'), '---\nkind: reference\n---\nbody that must not render at the none rung\n');
103
99
  const block = buildContextIntro(meta.node_id);
104
- // No per-store stanza headers, no (empty) markers, no index paths.
100
+ // No per-store stanza headers, no (empty) markers, no MEMORY.md paths.
105
101
  assert.ok(!block.includes(`node-local · ${memoryDir(meta.node_id)}`), 'no node-local stanza header');
106
102
  assert.ok(!block.includes('user-global · '), 'no user-global stanza header');
107
103
  assert.ok(!block.includes('project · '), 'no project stanza header');