@crouton-kit/crouter 0.3.14 → 0.3.15
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.d.ts +8 -0
- package/dist/build-root.js +30 -0
- package/dist/builtin-personas/design/base.md +3 -7
- package/dist/builtin-personas/design/orchestrator.md +4 -3
- package/dist/builtin-personas/developer/base.md +3 -7
- package/dist/builtin-personas/developer/orchestrator.md +5 -4
- package/dist/builtin-personas/explore/base.md +3 -7
- package/dist/builtin-personas/explore/orchestrator.md +1 -5
- package/dist/builtin-personas/general/base.md +2 -4
- package/dist/builtin-personas/general/orchestrator.md +2 -4
- package/dist/builtin-personas/lifecycle/resident.md +2 -0
- package/dist/builtin-personas/lifecycle/terminal.md +6 -0
- package/dist/builtin-personas/orchestration-kernel.md +42 -3
- package/dist/builtin-personas/plan/base.md +3 -5
- package/dist/builtin-personas/plan/orchestrator.md +5 -4
- package/dist/builtin-personas/plan/reviewers/architecture-fit/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/code-smells/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/base.md +9 -0
- package/dist/builtin-personas/plan/reviewers/security/base.md +9 -0
- package/dist/builtin-personas/review/base.md +3 -5
- package/dist/builtin-personas/review/orchestrator.md +2 -6
- package/dist/builtin-personas/runtime-base.md +3 -19
- package/dist/builtin-personas/spec/base.md +3 -5
- package/dist/builtin-personas/spec/orchestrator.md +4 -3
- package/dist/builtin-personas/spine/has-manager.md +10 -0
- package/dist/builtin-personas/spine/no-manager.md +2 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +96 -0
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +49 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +1 -1
- package/dist/builtin-skills/skills/spec/SKILL.md +2 -2
- package/dist/cli.js +6 -29
- package/dist/commands/attention.js +76 -7
- package/dist/commands/canvas-prune.d.ts +2 -0
- package/dist/commands/canvas-prune.js +66 -0
- package/dist/commands/canvas.js +5 -8
- package/dist/commands/chord.d.ts +2 -0
- package/dist/commands/chord.js +143 -0
- package/dist/commands/daemon.js +8 -5
- package/dist/commands/dashboard.js +2 -0
- package/dist/commands/human/prompts.js +28 -27
- package/dist/commands/human/queue.js +30 -14
- package/dist/commands/human/shared.d.ts +26 -21
- package/dist/commands/human/shared.js +44 -66
- package/dist/commands/human.js +4 -14
- package/dist/commands/node.d.ts +11 -0
- package/dist/commands/node.js +221 -98
- package/dist/commands/pkg/market-inspect.js +6 -4
- package/dist/commands/pkg/market-manage.js +10 -6
- package/dist/commands/pkg/market.js +2 -4
- package/dist/commands/pkg/plugin-inspect.js +6 -4
- package/dist/commands/pkg/plugin-manage.js +12 -7
- package/dist/commands/pkg/plugin.js +2 -4
- package/dist/commands/pkg.js +0 -4
- package/dist/commands/push.js +178 -15
- package/dist/commands/revive.js +5 -3
- package/dist/commands/skill/author.js +6 -4
- package/dist/commands/skill/find.js +8 -5
- package/dist/commands/skill/read.js +2 -0
- package/dist/commands/skill/state.js +6 -4
- package/dist/commands/skill.js +0 -6
- package/dist/commands/sys/config.js +21 -7
- package/dist/commands/sys/doctor.js +2 -0
- package/dist/commands/sys/update.js +4 -0
- package/dist/commands/sys.js +0 -6
- package/dist/commands/tmux-spread.d.ts +2 -0
- package/dist/commands/tmux-spread.js +130 -0
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +25 -0
- package/dist/core/__tests__/child-followup.test.d.ts +1 -0
- package/dist/core/__tests__/child-followup.test.js +83 -0
- package/dist/core/__tests__/close.test.d.ts +1 -0
- package/dist/core/__tests__/close.test.js +148 -0
- package/dist/core/__tests__/context-intro.test.d.ts +1 -0
- package/dist/core/__tests__/context-intro.test.js +196 -0
- package/dist/core/__tests__/daemon-boot.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-boot.test.js +93 -0
- package/dist/core/__tests__/daemon-liveness.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-liveness.test.js +223 -0
- package/dist/core/__tests__/focuses.test.d.ts +1 -0
- package/dist/core/__tests__/focuses.test.js +259 -0
- package/dist/core/__tests__/fork.test.d.ts +1 -0
- package/dist/core/__tests__/fork.test.js +91 -0
- package/dist/core/__tests__/home-session.test.d.ts +1 -0
- package/dist/core/__tests__/home-session.test.js +153 -0
- package/dist/core/__tests__/human-cancel-guard.test.d.ts +1 -0
- package/dist/core/__tests__/human-cancel-guard.test.js +49 -0
- package/dist/core/__tests__/keystone.test.d.ts +1 -0
- package/dist/core/__tests__/keystone.test.js +185 -0
- package/dist/core/__tests__/kickoff.test.d.ts +1 -0
- package/dist/core/__tests__/kickoff.test.js +89 -0
- package/dist/core/__tests__/lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle.test.js +178 -0
- package/dist/core/__tests__/listing-completeness.test.d.ts +1 -0
- package/dist/core/__tests__/listing-completeness.test.js +31 -0
- package/dist/core/__tests__/memory.test.d.ts +1 -0
- package/dist/core/__tests__/memory.test.js +152 -0
- package/dist/core/__tests__/migration.test.d.ts +1 -0
- package/dist/core/__tests__/migration.test.js +238 -0
- package/dist/core/__tests__/pane-column.test.d.ts +1 -0
- package/dist/core/__tests__/pane-column.test.js +153 -0
- package/dist/core/__tests__/passive-subscription.test.js +24 -1
- package/dist/core/__tests__/persona-compose.test.d.ts +1 -0
- package/dist/core/__tests__/persona-compose.test.js +53 -0
- package/dist/core/__tests__/persona-subkind.test.d.ts +1 -0
- package/dist/core/__tests__/persona-subkind.test.js +62 -0
- package/dist/core/__tests__/persona.test.d.ts +1 -0
- package/dist/core/__tests__/persona.test.js +107 -0
- package/dist/core/__tests__/placement-focus.test.d.ts +1 -0
- package/dist/core/__tests__/placement-focus.test.js +244 -0
- package/dist/core/__tests__/placement-reconcile.test.d.ts +1 -0
- package/dist/core/__tests__/placement-reconcile.test.js +212 -0
- package/dist/core/__tests__/placement-revive.test.d.ts +1 -0
- package/dist/core/__tests__/placement-revive.test.js +238 -0
- package/dist/core/__tests__/placement-teardown.test.d.ts +1 -0
- package/dist/core/__tests__/placement-teardown.test.js +183 -0
- package/dist/core/__tests__/prune.test.d.ts +1 -0
- package/dist/core/__tests__/prune.test.js +116 -0
- package/dist/core/__tests__/push-final-guard.test.d.ts +1 -0
- package/dist/core/__tests__/push-final-guard.test.js +71 -0
- package/dist/core/__tests__/relaunch.test.d.ts +1 -0
- package/dist/core/__tests__/relaunch.test.js +328 -0
- package/dist/core/__tests__/reset.test.js +26 -7
- package/dist/core/__tests__/revive.test.d.ts +1 -0
- package/dist/core/__tests__/revive.test.js +217 -0
- package/dist/core/__tests__/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/spawn-root.test.js +73 -0
- package/dist/core/__tests__/steer-note.test.d.ts +1 -0
- package/dist/core/__tests__/steer-note.test.js +39 -0
- package/dist/core/__tests__/stop-guard.test.d.ts +1 -0
- package/dist/core/__tests__/stop-guard.test.js +82 -0
- package/dist/core/__tests__/subcommand-tier.test.js +35 -33
- package/dist/core/__tests__/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/tmux-surface.test.js +106 -0
- package/dist/core/__tests__/unknown-path.test.js +8 -2
- package/dist/core/canvas/attention.d.ts +10 -0
- package/dist/core/canvas/attention.js +40 -0
- package/dist/core/canvas/canvas.d.ts +66 -7
- package/dist/core/canvas/canvas.js +209 -21
- package/dist/core/canvas/db.d.ts +8 -0
- package/dist/core/canvas/db.js +206 -4
- package/dist/core/canvas/focuses.d.ts +22 -0
- package/dist/core/canvas/focuses.js +80 -0
- package/dist/core/canvas/index.d.ts +3 -0
- package/dist/core/canvas/index.js +3 -0
- package/dist/core/canvas/labels.d.ts +27 -0
- package/dist/core/canvas/labels.js +36 -0
- package/dist/core/canvas/render.js +25 -10
- package/dist/core/canvas/telemetry.d.ts +14 -0
- package/dist/core/canvas/telemetry.js +35 -0
- package/dist/core/canvas/types.d.ts +115 -12
- package/dist/core/command.d.ts +25 -1
- package/dist/core/command.js +23 -15
- package/dist/core/config.js +36 -2
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.d.ts +3 -1
- package/dist/core/feed/inbox.js +45 -5
- package/dist/core/feed/passive.js +24 -11
- package/dist/core/help.d.ts +26 -13
- package/dist/core/help.js +44 -37
- package/dist/core/personas/index.d.ts +1 -1
- package/dist/core/personas/index.js +1 -1
- package/dist/core/personas/loader.d.ts +40 -1
- package/dist/core/personas/loader.js +63 -1
- package/dist/core/personas/resolve.d.ts +13 -6
- package/dist/core/personas/resolve.js +46 -34
- package/dist/core/runtime/bearings.d.ts +20 -0
- package/dist/core/runtime/bearings.js +92 -0
- package/dist/core/runtime/close.d.ts +14 -0
- package/dist/core/runtime/close.js +151 -0
- package/dist/core/runtime/demote.js +27 -10
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/kickoff.d.ts +23 -6
- package/dist/core/runtime/kickoff.js +92 -36
- package/dist/core/runtime/launch.d.ts +24 -12
- package/dist/core/runtime/launch.js +75 -19
- package/dist/core/runtime/lifecycle.d.ts +13 -0
- package/dist/core/runtime/lifecycle.js +86 -0
- package/dist/core/runtime/memory.d.ts +43 -0
- package/dist/core/runtime/memory.js +165 -0
- package/dist/core/runtime/naming.d.ts +22 -0
- package/dist/core/runtime/naming.js +166 -0
- package/dist/core/runtime/nodes.d.ts +32 -1
- package/dist/core/runtime/nodes.js +60 -10
- package/dist/core/runtime/persona.d.ts +25 -0
- package/dist/core/runtime/persona.js +139 -0
- package/dist/core/runtime/placement.d.ts +287 -0
- package/dist/core/runtime/placement.js +663 -0
- package/dist/core/runtime/presence.d.ts +7 -15
- package/dist/core/runtime/presence.js +90 -66
- package/dist/core/runtime/promote.d.ts +14 -7
- package/dist/core/runtime/promote.js +57 -67
- package/dist/core/runtime/reset.d.ts +47 -4
- package/dist/core/runtime/reset.js +223 -52
- package/dist/core/runtime/revive.d.ts +26 -2
- package/dist/core/runtime/revive.js +166 -39
- package/dist/core/runtime/spawn.d.ts +20 -5
- package/dist/core/runtime/spawn.js +163 -43
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +18 -8
- package/dist/core/runtime/tmux.d.ts +100 -14
- package/dist/core/runtime/tmux.js +201 -28
- package/dist/core/spawn.js +15 -0
- package/dist/daemon/crtrd.d.ts +12 -1
- package/dist/daemon/crtrd.js +152 -34
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +266 -0
- package/dist/pi-extensions/canvas-commands.js +16 -13
- package/dist/pi-extensions/canvas-context-intro.d.ts +70 -0
- package/dist/pi-extensions/canvas-context-intro.js +164 -0
- package/dist/pi-extensions/canvas-goal-capture.d.ts +3 -0
- package/dist/pi-extensions/canvas-goal-capture.js +15 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +11 -0
- package/dist/pi-extensions/canvas-nav.d.ts +12 -4
- package/dist/pi-extensions/canvas-nav.js +586 -262
- package/dist/pi-extensions/canvas-stophook.d.ts +16 -0
- package/dist/pi-extensions/canvas-stophook.js +344 -228
- package/dist/types.d.ts +28 -0
- package/dist/types.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// spawnNode: provenance (spawned_by) is decoupled from the spine (parent +
|
|
2
|
+
// subscription). A managed child gets both; an independent root gets provenance
|
|
3
|
+
// only — no subscription wires back to the spawner.
|
|
4
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { createNode, getNode, getRow, subscribersOf } from '../canvas/canvas.js';
|
|
10
|
+
import { closeDb } from '../canvas/db.js';
|
|
11
|
+
import { nodeDir } from '../canvas/paths.js';
|
|
12
|
+
import { spawnNode } from '../runtime/nodes.js';
|
|
13
|
+
let home;
|
|
14
|
+
function spawner(id) {
|
|
15
|
+
return {
|
|
16
|
+
node_id: id,
|
|
17
|
+
name: id,
|
|
18
|
+
created: new Date().toISOString(),
|
|
19
|
+
cwd: '/tmp/work',
|
|
20
|
+
kind: 'general',
|
|
21
|
+
mode: 'base',
|
|
22
|
+
lifecycle: 'resident',
|
|
23
|
+
status: 'active',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
before(() => {
|
|
27
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-spawn-root-'));
|
|
28
|
+
process.env['CRTR_HOME'] = home;
|
|
29
|
+
});
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
closeDb();
|
|
32
|
+
rmSync(home, { recursive: true, force: true });
|
|
33
|
+
});
|
|
34
|
+
after(() => {
|
|
35
|
+
closeDb();
|
|
36
|
+
rmSync(home, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
test('managed child: parent on the spine + active subscription + provenance', () => {
|
|
39
|
+
createNode(spawner('A'));
|
|
40
|
+
const child = spawnNode({ kind: 'developer', cwd: '/tmp/work', parent: 'A' });
|
|
41
|
+
const meta = getNode(child.node_id);
|
|
42
|
+
assert.equal(meta.parent, 'A', 'child keeps its spine parent');
|
|
43
|
+
assert.equal(meta.spawned_by, 'A', 'child provenance defaults to its parent');
|
|
44
|
+
assert.equal(meta.lifecycle, 'terminal', 'a plain child is terminal');
|
|
45
|
+
const subs = subscribersOf(child.node_id);
|
|
46
|
+
assert.equal(subs.length, 1, 'exactly one subscriber (the parent)');
|
|
47
|
+
assert.equal(subs[0].node_id, 'A');
|
|
48
|
+
assert.equal(subs[0].active, true, 'parent subscription is active');
|
|
49
|
+
});
|
|
50
|
+
test('independent root: provenance only, no parent, no subscription', () => {
|
|
51
|
+
createNode(spawner('A'));
|
|
52
|
+
const root = spawnNode({
|
|
53
|
+
kind: 'developer',
|
|
54
|
+
cwd: '/tmp/work',
|
|
55
|
+
parent: null,
|
|
56
|
+
spawnedBy: 'A',
|
|
57
|
+
lifecycle: 'resident',
|
|
58
|
+
});
|
|
59
|
+
const meta = getNode(root.node_id);
|
|
60
|
+
assert.equal(meta.parent, null, 'a root has no spine parent (top-level)');
|
|
61
|
+
assert.equal(meta.spawned_by, 'A', 'a root still records who spawned it');
|
|
62
|
+
assert.equal(meta.lifecycle, 'resident', 'a root is resident');
|
|
63
|
+
assert.equal(subscribersOf(root.node_id).length, 0, 'nobody is subscribed to an independent root — the spawner is not woken by it');
|
|
64
|
+
});
|
|
65
|
+
test('unknown parent: spawnNode throws and mints NO node dir / row (validate before create)', () => {
|
|
66
|
+
// Pre-allocate the would-be id so we can prove nothing was scaffolded for it.
|
|
67
|
+
const orphanId = 'orphan-under-ghost';
|
|
68
|
+
assert.throws(() => spawnNode({ kind: 'developer', cwd: '/tmp/work', parent: 'ghost', nodeId: orphanId }), /cannot spawn under unknown parent node: ghost/);
|
|
69
|
+
// No half-born orphan: no meta (getNode null), no db row, no node dir on disk.
|
|
70
|
+
assert.equal(getNode(orphanId), null, 'no meta.json written for the orphan');
|
|
71
|
+
assert.equal(getRow(orphanId), null, 'no index row written for the orphan');
|
|
72
|
+
assert.equal(existsSync(nodeDir(orphanId)), false, 'no node dir scaffolded for the orphan');
|
|
73
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Run: node --import tsx/esm --test src/core/__tests__/steer-note.test.ts
|
|
2
|
+
//
|
|
3
|
+
// Context-size steering text (canvas-stophook `steerNote`) is keyed on MODE
|
|
4
|
+
// first, then LIFECYCLE — NOT on lifecycle alone. The key regression this
|
|
5
|
+
// guards: a TERMINAL/ORCHESTRATOR must get roadmap-checkpoint-and-yield
|
|
6
|
+
// guidance (it has a roadmap), never the worker "push final" text.
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { steerNote } from '../../pi-extensions/canvas-stophook.js';
|
|
10
|
+
test('an orchestrator (terminal) is steered to checkpoint roadmap + yield, never push final', () => {
|
|
11
|
+
const msg = steerNote(150_000, 'terminal', 'orchestrator');
|
|
12
|
+
assert.match(msg, /roadmap\.md/, 'points at the roadmap to checkpoint');
|
|
13
|
+
assert.match(msg, /node yield/, 'steers it to yield');
|
|
14
|
+
assert.doesNotMatch(msg, /push final/, 'an orchestrator never finishes with push final');
|
|
15
|
+
});
|
|
16
|
+
test('an orchestrator (resident) gets the same roadmap/yield steering as a terminal orchestrator', () => {
|
|
17
|
+
const t = steerNote(150_000, 'terminal', 'orchestrator');
|
|
18
|
+
const r = steerNote(150_000, 'resident', 'orchestrator');
|
|
19
|
+
assert.equal(r, t, 'mode drives the message, not lifecycle');
|
|
20
|
+
});
|
|
21
|
+
test('a terminal BASE worker is steered to promote / push final', () => {
|
|
22
|
+
const msg = steerNote(170_000, 'terminal', 'base');
|
|
23
|
+
assert.match(msg, /node promote/, 'a base worker is told it can promote');
|
|
24
|
+
assert.match(msg, /push final/, 'and to finish with push final when nearly done');
|
|
25
|
+
assert.doesNotMatch(msg, /roadmap\.md/, 'a base worker has no roadmap to checkpoint');
|
|
26
|
+
});
|
|
27
|
+
test('a resident BASE root is steered to promote-or-wrap-up, never at a roadmap', () => {
|
|
28
|
+
const msg = steerNote(150_000, 'resident', 'base');
|
|
29
|
+
assert.match(msg, /node promote/, 'a root growing into a job is told to promote');
|
|
30
|
+
assert.doesNotMatch(msg, /roadmap\.md/, 'a root has no roadmap to point at');
|
|
31
|
+
assert.doesNotMatch(msg, /push final/, 'a resident node never finishes with push final');
|
|
32
|
+
});
|
|
33
|
+
test('below the first band there is no nudge boundary issue — pushy escalation kicks in at 185k', () => {
|
|
34
|
+
// Sanity: the orchestrator branch escalates (pushy) at/after 185k.
|
|
35
|
+
const firm = steerNote(150_000, 'terminal', 'orchestrator');
|
|
36
|
+
const pushy = steerNote(185_000, 'terminal', 'orchestrator');
|
|
37
|
+
assert.notEqual(firm, pushy, 'the 185k band reads differently from the 150k band');
|
|
38
|
+
assert.match(pushy, /overflow/i, 'the pushy band warns about overflow');
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Run: node --import tsx/esm --test src/core/__tests__/stop-guard.test.ts
|
|
2
|
+
//
|
|
3
|
+
// The stop-guard keys "is this stop legitimate?" on the LIFECYCLE value, not on
|
|
4
|
+
// parent/mode: a resident node is interactable and never forced to submit a
|
|
5
|
+
// final, so it may go dormant regardless of whether it has a parent.
|
|
6
|
+
import { test, before, after, beforeEach } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { createNode, subscribe, setStatus } from '../canvas/canvas.js';
|
|
12
|
+
import { closeDb } from '../canvas/db.js';
|
|
13
|
+
import { evaluateStop } from '../runtime/stop-guard.js';
|
|
14
|
+
let home;
|
|
15
|
+
function node(id, over = {}) {
|
|
16
|
+
return {
|
|
17
|
+
node_id: id,
|
|
18
|
+
name: id,
|
|
19
|
+
created: new Date().toISOString(),
|
|
20
|
+
cwd: '/tmp/work',
|
|
21
|
+
kind: 'general',
|
|
22
|
+
mode: 'base',
|
|
23
|
+
lifecycle: 'terminal',
|
|
24
|
+
status: 'active',
|
|
25
|
+
...over,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const noSignals = { pushedFinal: false, askedHuman: false };
|
|
29
|
+
before(() => {
|
|
30
|
+
home = mkdtempSync(join(tmpdir(), 'crtr-stopguard-'));
|
|
31
|
+
process.env['CRTR_HOME'] = home;
|
|
32
|
+
});
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
closeDb();
|
|
35
|
+
rmSync(home, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
after(() => {
|
|
38
|
+
closeDb();
|
|
39
|
+
rmSync(home, { recursive: true, force: true });
|
|
40
|
+
delete process.env['CRTR_HOME'];
|
|
41
|
+
});
|
|
42
|
+
test('a RESIDENT node is allowed to go dormant — even WITH a parent (keyed on lifecycle, not parent)', () => {
|
|
43
|
+
createNode(node('parent', { parent: null, lifecycle: 'resident' }));
|
|
44
|
+
// A resident sub-orchestrator with a parent and NOTHING live to await: under
|
|
45
|
+
// the old parent===null rule this would have stalled; now it goes dormant.
|
|
46
|
+
createNode(node('sub', { parent: 'parent', lifecycle: 'resident', mode: 'orchestrator' }));
|
|
47
|
+
const d = evaluateStop('sub', noSignals);
|
|
48
|
+
assert.deepEqual(d, { action: 'allow', reason: 'dormant' });
|
|
49
|
+
});
|
|
50
|
+
test('a resident ROOT (no parent) is allowed to go dormant', () => {
|
|
51
|
+
createNode(node('root', { parent: null, lifecycle: 'resident' }));
|
|
52
|
+
const d = evaluateStop('root', noSignals);
|
|
53
|
+
assert.deepEqual(d, { action: 'allow', reason: 'dormant' });
|
|
54
|
+
});
|
|
55
|
+
test('a TERMINAL node awaiting a live worker is "awaiting" (not stalled)', () => {
|
|
56
|
+
createNode(node('mgr', { parent: null, lifecycle: 'terminal', mode: 'orchestrator' }));
|
|
57
|
+
createNode(node('worker', { parent: 'mgr', lifecycle: 'terminal', status: 'active' }));
|
|
58
|
+
subscribe('mgr', 'worker', true); // active sub to a live publisher
|
|
59
|
+
const d = evaluateStop('mgr', noSignals);
|
|
60
|
+
assert.equal(d.action, 'allow');
|
|
61
|
+
assert.equal(d.reason, 'awaiting');
|
|
62
|
+
});
|
|
63
|
+
test('a TERMINAL node with nothing live to await and no final pushed is reprompted (stalled)', () => {
|
|
64
|
+
createNode(node('lonely', { parent: 'mgr', lifecycle: 'terminal' }));
|
|
65
|
+
const d = evaluateStop('lonely', noSignals);
|
|
66
|
+
assert.equal(d.action, 'reprompt');
|
|
67
|
+
assert.equal(d.reason, 'stalled');
|
|
68
|
+
});
|
|
69
|
+
test('a TERMINAL node whose only worker is dead is stalled (no LIVE subscription)', () => {
|
|
70
|
+
createNode(node('mgr', { parent: null, lifecycle: 'terminal' }));
|
|
71
|
+
createNode(node('worker', { parent: 'mgr', lifecycle: 'terminal' }));
|
|
72
|
+
subscribe('mgr', 'worker', true);
|
|
73
|
+
setStatus('worker', 'dead'); // publisher no longer live
|
|
74
|
+
const d = evaluateStop('mgr', noSignals);
|
|
75
|
+
assert.equal(d.action, 'reprompt');
|
|
76
|
+
assert.equal(d.reason, 'stalled');
|
|
77
|
+
});
|
|
78
|
+
test('pushedFinal → finished; askedHuman → escalated (both short-circuit before lifecycle)', () => {
|
|
79
|
+
createNode(node('t', { parent: 'mgr', lifecycle: 'terminal' }));
|
|
80
|
+
assert.deepEqual(evaluateStop('t', { pushedFinal: true, askedHuman: false }), { action: 'allow', reason: 'finished' });
|
|
81
|
+
assert.deepEqual(evaluateStop('t', { pushedFinal: false, askedHuman: true }), { action: 'allow', reason: 'escalated' });
|
|
82
|
+
});
|
|
@@ -1,53 +1,55 @@
|
|
|
1
1
|
// Tests for the subcommand visibility tier (hidden | normal | common | important)
|
|
2
|
-
// and the
|
|
2
|
+
// and the parent-level listing affordance.
|
|
3
3
|
// Run with: node --import tsx/esm --test src/core/__tests__/subcommand-tier.test.ts
|
|
4
4
|
//
|
|
5
5
|
// Contract:
|
|
6
|
+
// - Each child def (defineLeaf/defineBranch) owns its own description /
|
|
7
|
+
// whenToUse / tier; defineBranch assembles `help.listing` from those defs —
|
|
8
|
+
// the parent never copies a child's self-description (principle 16).
|
|
6
9
|
// - renderRoot promotes a subtree's `important` children (name + shortform
|
|
7
10
|
// desc) and `common` children (bare qualified path) into that command's
|
|
8
11
|
// block, then names how many other non-hidden subcommands stay behind
|
|
9
12
|
// `crtr <name> -h`.
|
|
10
13
|
// - `hidden` children never appear (not even in the subtree's own -h) and are
|
|
11
14
|
// not counted in any "[+N]" remainder.
|
|
12
|
-
// - renderBranch
|
|
13
|
-
//
|
|
15
|
+
// - renderBranch renders one self-closing <subcommand> per non-hidden child
|
|
16
|
+
// and adds a `subcommands="N"` attribute when a branch child owns children.
|
|
14
17
|
import { test, describe } from 'node:test';
|
|
15
18
|
import assert from 'node:assert/strict';
|
|
16
19
|
import { defineRoot, defineBranch, defineLeaf } from '../command.js';
|
|
17
20
|
import { renderRoot, renderBranch } from '../help.js';
|
|
18
|
-
const leaf = (name) => defineLeaf({
|
|
21
|
+
const leaf = (name, opts = {}) => defineLeaf({
|
|
19
22
|
name,
|
|
23
|
+
description: opts.description,
|
|
24
|
+
whenToUse: opts.whenToUse,
|
|
25
|
+
tier: opts.tier,
|
|
20
26
|
help: { name, summary: name, output: [], outputKind: 'object', effects: ['None. Read-only.'] },
|
|
21
27
|
run: async () => ({}),
|
|
22
28
|
});
|
|
23
|
-
// A nested branch so we can assert the "
|
|
29
|
+
// A nested branch so we can assert the `subcommands="N"` depth flag. Its own
|
|
30
|
+
// parent-level self-description (description/whenToUse) lives on the def, as a
|
|
31
|
+
// sibling of `name`.
|
|
24
32
|
const inspect = defineBranch({
|
|
25
33
|
name: 'inspect',
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
-
children: [leaf('list'), leaf('show')],
|
|
34
|
+
description: 'inspect things',
|
|
35
|
+
whenToUse: 'x',
|
|
36
|
+
help: { name: 'thing inspect', summary: 'inspect' },
|
|
37
|
+
children: [
|
|
38
|
+
leaf('list', { description: 'list', whenToUse: 'x' }),
|
|
39
|
+
leaf('show', { description: 'show', whenToUse: 'x' }),
|
|
40
|
+
],
|
|
35
41
|
});
|
|
36
42
|
const thing = defineBranch({
|
|
37
43
|
name: 'thing',
|
|
38
44
|
rootEntry: { concept: 'a thing', desc: 'things', useWhen: 'doing things' },
|
|
39
|
-
help: {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{ name: 'plain', desc: 'plain op', useWhen: 'x' },
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
children: [leaf('make'), leaf('promote'), inspect, leaf('secret'), leaf('plain')],
|
|
45
|
+
help: { name: 'thing', summary: 'do things' },
|
|
46
|
+
children: [
|
|
47
|
+
leaf('make', { description: 'make a thing', whenToUse: 'x', tier: 'important' }),
|
|
48
|
+
leaf('promote', { description: 'promote a thing', whenToUse: 'x', tier: 'common' }),
|
|
49
|
+
inspect,
|
|
50
|
+
leaf('secret', { description: 'secret op', whenToUse: 'x', tier: 'hidden' }),
|
|
51
|
+
leaf('plain', { description: 'plain op', whenToUse: 'x' }),
|
|
52
|
+
],
|
|
51
53
|
});
|
|
52
54
|
const root = defineRoot({ tagline: 'test runtime', globals: [], subtrees: [thing] });
|
|
53
55
|
describe('renderRoot: subcommand promotion', () => {
|
|
@@ -70,28 +72,28 @@ describe('renderRoot: commands with no promotions', () => {
|
|
|
70
72
|
const bare = defineBranch({
|
|
71
73
|
name: 'bare',
|
|
72
74
|
rootEntry: { concept: 'bare', desc: 'bare', useWhen: 'x' },
|
|
73
|
-
help: { name: 'bare', summary: 'bare'
|
|
74
|
-
children: [leaf('one')],
|
|
75
|
+
help: { name: 'bare', summary: 'bare' },
|
|
76
|
+
children: [leaf('one', { description: 'one', whenToUse: 'x' })],
|
|
75
77
|
});
|
|
76
78
|
const r = defineRoot({ tagline: 't', globals: [], subtrees: [bare] });
|
|
77
79
|
const out = renderRoot(r.help);
|
|
78
80
|
assert.match(out, /\[\+1 subcommand — `crtr bare -h`\]/); // singular, no "other"
|
|
79
81
|
});
|
|
80
82
|
});
|
|
81
|
-
describe('renderBranch: hidden filter + depth flag', () => {
|
|
83
|
+
describe('renderBranch: hidden filter + depth flag (XML)', () => {
|
|
82
84
|
const out = renderBranch(thing.help);
|
|
83
85
|
test('hidden child is dropped from the branch listing', () => {
|
|
84
86
|
assert.doesNotMatch(out, /secret/);
|
|
85
87
|
});
|
|
86
|
-
test('all non-hidden children are listed', () => {
|
|
88
|
+
test('all non-hidden children are listed as <subcommand> rows', () => {
|
|
87
89
|
for (const n of ['make', 'promote', 'inspect', 'plain']) {
|
|
88
|
-
assert.match(out, new RegExp(
|
|
90
|
+
assert.match(out, new RegExp(`name="${n}"`));
|
|
89
91
|
}
|
|
90
92
|
});
|
|
91
93
|
test('a branch child flags how many subcommands it owns', () => {
|
|
92
|
-
assert.match(out, /inspect
|
|
94
|
+
assert.match(out, /name="inspect"[^>]*subcommands="2"/);
|
|
93
95
|
});
|
|
94
96
|
test('leaf children carry no subcommand flag', () => {
|
|
95
|
-
assert.doesNotMatch(out, /make
|
|
97
|
+
assert.doesNotMatch(out, /name="make"[^>]*subcommands=/);
|
|
96
98
|
});
|
|
97
99
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Run with: node --import tsx/esm --test src/core/__tests__/tmux-surface.test.ts
|
|
2
|
+
//
|
|
3
|
+
// STEP 2 of the placement/focus migration: guards on the tmux Surface (driver).
|
|
4
|
+
//
|
|
5
|
+
// 1. The §2.2 HARD DRIVER INVARIANT (GREEN now): every create/placement verb
|
|
6
|
+
// in the driver — new-window / split-window / swap-pane / break-pane /
|
|
7
|
+
// join-pane / move-pane / respawn-pane — MUST pass an explicit `-t` target.
|
|
8
|
+
// Omitting `-t` lets tmux resolve against its GLOBAL current session, which
|
|
9
|
+
// can leak a pane into a user session — the exact unbidden-window bug this
|
|
10
|
+
// redesign kills. This guards the bug's blast radius and should pass today.
|
|
11
|
+
//
|
|
12
|
+
// 2. The §5.1 "only placement.ts / tmux-chrome.ts import tmux.ts" lint guard
|
|
13
|
+
// (SKIPPED — warn only this step): placement.ts does not exist yet and many
|
|
14
|
+
// modules still import the driver directly, so this CANNOT pass until the
|
|
15
|
+
// migration completes. It flips to a hard assertion in Step 8.
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { dirname, join, basename } from 'node:path';
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const SRC_ROOT = join(__dirname, '..', '..'); // .../src
|
|
23
|
+
const TMUX_TS = join(SRC_ROOT, 'core', 'runtime', 'tmux.ts');
|
|
24
|
+
/** The placement verbs the §2.2 invariant governs. */
|
|
25
|
+
const PLACEMENT_VERBS = [
|
|
26
|
+
'new-window',
|
|
27
|
+
'split-window',
|
|
28
|
+
'swap-pane',
|
|
29
|
+
'break-pane',
|
|
30
|
+
'join-pane',
|
|
31
|
+
'move-pane',
|
|
32
|
+
'respawn-pane',
|
|
33
|
+
];
|
|
34
|
+
/** The smallest `[ … ]` array literal that encloses `matchIdx`: scan back to the
|
|
35
|
+
* nearest `[` (the array open — the verb is always its first element), then
|
|
36
|
+
* forward with bracket-depth counting to the matching `]` (handles nested
|
|
37
|
+
* arrays like splitWindow's `? [] : ['-h']`). Returns the array source slice. */
|
|
38
|
+
function enclosingArray(src, matchIdx) {
|
|
39
|
+
let open = matchIdx;
|
|
40
|
+
while (open >= 0 && src[open] !== '[')
|
|
41
|
+
open--;
|
|
42
|
+
assert.ok(open >= 0, `no enclosing [ for the verb at index ${matchIdx}`);
|
|
43
|
+
let depth = 0;
|
|
44
|
+
for (let i = open; i < src.length; i++) {
|
|
45
|
+
if (src[i] === '[')
|
|
46
|
+
depth++;
|
|
47
|
+
else if (src[i] === ']') {
|
|
48
|
+
depth--;
|
|
49
|
+
if (depth === 0)
|
|
50
|
+
return src.slice(open, i + 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
throw new Error('unbalanced [ ] while bracket-matching the verb array');
|
|
54
|
+
}
|
|
55
|
+
test('§2.2 driver invariant: every placement verb in tmux.ts passes an explicit -t target', () => {
|
|
56
|
+
const src = readFileSync(TMUX_TS, 'utf8');
|
|
57
|
+
// Match only the QUOTED JS string forms (`'new-window'`, …) — these are real
|
|
58
|
+
// args-array elements; the jsdoc prose mentions the verbs unquoted / in
|
|
59
|
+
// backticks, so this never trips on a comment.
|
|
60
|
+
const re = new RegExp(`'(${PLACEMENT_VERBS.join('|')})'`, 'g');
|
|
61
|
+
let found = 0;
|
|
62
|
+
for (let m = re.exec(src); m !== null; m = re.exec(src)) {
|
|
63
|
+
found++;
|
|
64
|
+
const verb = m[1];
|
|
65
|
+
const arr = enclosingArray(src, m.index);
|
|
66
|
+
assert.ok(arr.includes(`'-t'`), `tmux verb '${verb}' is invoked WITHOUT an explicit -t target — a latent ` +
|
|
67
|
+
`instance of the unbidden-window bug (§2.2). Offending args array:\n${arr}`);
|
|
68
|
+
}
|
|
69
|
+
// Sanity: the driver really does contain placement verbs (so a refactor that
|
|
70
|
+
// renames them can't make this assertion vacuously pass).
|
|
71
|
+
assert.ok(found >= 4, `expected to scan ≥4 placement verbs, saw ${found}`);
|
|
72
|
+
});
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Lint guard — §5.1. WARN ONLY this step. Skipped because it cannot pass yet:
|
|
75
|
+
// placement.ts (the sole sanctioned importer) does not exist, and the driver is
|
|
76
|
+
// still imported directly by the runtime/daemon/command/stophook modules the
|
|
77
|
+
// migration has not yet routed through placement. Step 8 deletes the skip and
|
|
78
|
+
// turns the body into the enforced boundary.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/** Every `.ts` file under `src` (recursively), excluding nothing. */
|
|
81
|
+
function allTsFiles(dir) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const entry of readdirSync(dir)) {
|
|
84
|
+
const p = join(dir, entry);
|
|
85
|
+
if (statSync(p).isDirectory())
|
|
86
|
+
out.push(...allTsFiles(p));
|
|
87
|
+
else if (entry.endsWith('.ts'))
|
|
88
|
+
out.push(p);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** Files (basename) sanctioned to import the driver, per §2.1. */
|
|
93
|
+
const ALLOWED_IMPORTERS = new Set(['tmux.ts', 'placement.ts', 'tmux-chrome.ts']);
|
|
94
|
+
function importsDriver(file) {
|
|
95
|
+
const src = readFileSync(file, 'utf8');
|
|
96
|
+
// A specifier whose basename is exactly `tmux.js` (so `tmux-chrome.js` and
|
|
97
|
+
// `tmux-spread.js` are NOT matched). Covers `from '...'` and `import('...')`.
|
|
98
|
+
return [...src.matchAll(/(?:from|import\s*\()\s*'([^']+)'/g)].some((m) => basename(m[1]) === 'tmux.js');
|
|
99
|
+
}
|
|
100
|
+
test('§5.1 lint: only placement.ts / tmux-chrome.ts import the tmux driver', { skip: 'WARN-ONLY in Step 2 — placement.ts does not exist yet and many modules still import tmux.ts directly. Step 8 flips this to a hard error.' }, () => {
|
|
101
|
+
const offenders = allTsFiles(SRC_ROOT)
|
|
102
|
+
.filter((f) => !ALLOWED_IMPORTERS.has(basename(f)))
|
|
103
|
+
.filter(importsDriver)
|
|
104
|
+
.map((f) => f.slice(SRC_ROOT.length + 1));
|
|
105
|
+
assert.deepEqual(offenders, [], `modules importing tmux.ts directly: ${offenders.join(', ')}`);
|
|
106
|
+
});
|
|
@@ -10,17 +10,23 @@ import assert from 'node:assert/strict';
|
|
|
10
10
|
import { defineRoot, defineBranch, defineLeaf, walk, unknownPathError } from '../command.js';
|
|
11
11
|
const leaf = defineLeaf({
|
|
12
12
|
name: 'search',
|
|
13
|
+
description: 'search',
|
|
14
|
+
whenToUse: 'x',
|
|
13
15
|
help: { name: 'search', summary: 'search', output: [], outputKind: 'object', effects: ['None. Read-only.'] },
|
|
14
16
|
run: async () => ({}),
|
|
15
17
|
});
|
|
16
18
|
const findBranch = defineBranch({
|
|
17
19
|
name: 'find',
|
|
18
|
-
|
|
20
|
+
description: 'find',
|
|
21
|
+
whenToUse: 'x',
|
|
22
|
+
help: { name: 'find', summary: 'find' },
|
|
19
23
|
children: [leaf],
|
|
20
24
|
});
|
|
21
25
|
const skillBranch = defineBranch({
|
|
22
26
|
name: 'skill',
|
|
23
|
-
|
|
27
|
+
description: 'skill',
|
|
28
|
+
whenToUse: 'x',
|
|
29
|
+
help: { name: 'skill', summary: 'skill' },
|
|
24
30
|
rootEntry: { concept: 'skill', desc: 'skill', useWhen: 'x' },
|
|
25
31
|
children: [findBranch],
|
|
26
32
|
});
|
|
@@ -17,6 +17,16 @@ export declare function countAsks(nodeId: string): number;
|
|
|
17
17
|
* Returns only entries with count > 0.
|
|
18
18
|
*/
|
|
19
19
|
export declare function pendingAsksForView(rootId: string): AskEntry[];
|
|
20
|
+
/**
|
|
21
|
+
* Per-node pending ask counts for an explicit set of node ids — the batched
|
|
22
|
+
* counterpart to `countAsks`, used by the nav chrome to label every visible
|
|
23
|
+
* node in ONE pass. Groups ids by their cwd so each distinct interactions dir
|
|
24
|
+
* is scanned exactly once, then buckets the decks by the `source.nodeId` stamp
|
|
25
|
+
* (same attribution as `countForCwd(cwd, nodeId)`). Asks with no node stamp are
|
|
26
|
+
* not attributable to any node and are excluded. Every requested id appears in
|
|
27
|
+
* the result (0 when it has none). Never throws.
|
|
28
|
+
*/
|
|
29
|
+
export declare function asksForNodes(ids: string[]): Record<string, number>;
|
|
20
30
|
/**
|
|
21
31
|
* Pending asks across the entire canvas — every distinct cwd among all known
|
|
22
32
|
* nodes. Returns only entries with count > 0.
|
|
@@ -77,6 +77,46 @@ export function pendingAsksForView(rootId) {
|
|
|
77
77
|
}
|
|
78
78
|
return Array.from(seen.values()).filter((e) => e.count > 0);
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Per-node pending ask counts for an explicit set of node ids — the batched
|
|
82
|
+
* counterpart to `countAsks`, used by the nav chrome to label every visible
|
|
83
|
+
* node in ONE pass. Groups ids by their cwd so each distinct interactions dir
|
|
84
|
+
* is scanned exactly once, then buckets the decks by the `source.nodeId` stamp
|
|
85
|
+
* (same attribution as `countForCwd(cwd, nodeId)`). Asks with no node stamp are
|
|
86
|
+
* not attributable to any node and are excluded. Every requested id appears in
|
|
87
|
+
* the result (0 when it has none). Never throws.
|
|
88
|
+
*/
|
|
89
|
+
export function asksForNodes(ids) {
|
|
90
|
+
const counts = {};
|
|
91
|
+
for (const id of ids)
|
|
92
|
+
counts[id] = 0;
|
|
93
|
+
// Bucket the requested ids by cwd so we scan each inbox once, not per id.
|
|
94
|
+
const idsByCwd = new Map();
|
|
95
|
+
for (const id of ids) {
|
|
96
|
+
const node = getNode(id);
|
|
97
|
+
if (node === null)
|
|
98
|
+
continue;
|
|
99
|
+
const arr = idsByCwd.get(node.cwd) ?? [];
|
|
100
|
+
arr.push(id);
|
|
101
|
+
idsByCwd.set(node.cwd, arr);
|
|
102
|
+
}
|
|
103
|
+
for (const [cwd, cwdIds] of idsByCwd) {
|
|
104
|
+
let items;
|
|
105
|
+
try {
|
|
106
|
+
items = scanInbox([interactionsRoot(cwd)]);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
continue; // humanloop missing / no interactions dir — leave these at 0
|
|
110
|
+
}
|
|
111
|
+
const want = new Set(cwdIds);
|
|
112
|
+
for (const i of items) {
|
|
113
|
+
const nid = i.source?.nodeId;
|
|
114
|
+
if (nid !== undefined && want.has(nid))
|
|
115
|
+
counts[nid] = (counts[nid] ?? 0) + 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return counts;
|
|
119
|
+
}
|
|
80
120
|
/**
|
|
81
121
|
* Pending asks across the entire canvas — every distinct cwd among all known
|
|
82
122
|
* nodes. Returns only entries with count > 0.
|
|
@@ -1,14 +1,41 @@
|
|
|
1
|
-
import type { NodeMeta, NodeRow, NodeStatus, SubscriptionRef } from './types.js';
|
|
2
|
-
/** Create a node: scaffold its dirs,
|
|
1
|
+
import type { NodeMeta, NodeIdentity, NodeRow, NodeStatus, ExitIntent, SubscriptionRef } from './types.js';
|
|
2
|
+
/** Create a node: scaffold its dirs, persist identity to meta.json, and seed the
|
|
3
|
+
* row (identity + runtime from the incoming meta). Returns the hydrated view. */
|
|
3
4
|
export declare function createNode(meta: NodeMeta): NodeMeta;
|
|
4
|
-
/** The canonical node record (
|
|
5
|
+
/** The canonical node record: durable identity (meta.json) ∪ authoritative
|
|
6
|
+
* runtime (the row). Null if unknown. */
|
|
5
7
|
export declare function getNode(nodeId: string): NodeMeta | null;
|
|
6
8
|
/** The indexed row (from the db) — cheap for queries that don't need full meta. */
|
|
7
9
|
export declare function getRow(nodeId: string): NodeRow | null;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/** The node row whose durable LOCATION pane is `pane`, or null. Lets placement
|
|
11
|
+
* resolve "who sits in this pane" by the first-class `%pane_id` handle (e.g.
|
|
12
|
+
* to adopt a caller's pane as a focus). pane is not UNIQUE in the schema, but a
|
|
13
|
+
* live pane backs at most one node, so this returns the single match. */
|
|
14
|
+
export declare function getRowByPane(pane: string): NodeRow | null;
|
|
15
|
+
/** Merge an IDENTITY patch into a node's meta.json and re-index its identity
|
|
16
|
+
* columns. Identity has a single writer per node, so this read-modify-write is
|
|
17
|
+
* safe (the contended runtime fields were moved out — see the atomic setters
|
|
18
|
+
* below). Returns the hydrated view (runtime included). */
|
|
19
|
+
export declare function updateNode(nodeId: string, patch: Partial<NodeIdentity>): NodeMeta;
|
|
20
|
+
/** Set a node's status. Atomic single-column write. */
|
|
11
21
|
export declare function setStatus(nodeId: string, status: NodeStatus): void;
|
|
22
|
+
/** Set a node's exit intent. Atomic single-column write. */
|
|
23
|
+
export declare function setIntent(nodeId: string, intent: ExitIntent): void;
|
|
24
|
+
/** Set a node's tmux presence in one atomic write: the durable LOCATION anchor
|
|
25
|
+
* `pane` (the `%pane_id`) plus its derived cache (`tmux_session` + `window`).
|
|
26
|
+
* All three move together — `pane` joins the others inside the single UPDATE so
|
|
27
|
+
* a move never half-writes the location. `pane` is optional: a caller that does
|
|
28
|
+
* not yet track it (every caller, until the placement layer lands) writes null,
|
|
29
|
+
* which is harmless because nothing reads `pane` yet. */
|
|
30
|
+
export declare function setPresence(nodeId: string, presence: {
|
|
31
|
+
tmux_session?: string | null;
|
|
32
|
+
window?: string | null;
|
|
33
|
+
pane?: string | null;
|
|
34
|
+
}): void;
|
|
35
|
+
/** Record the live pi pid (daemon liveness signal). Atomic single-column write. */
|
|
36
|
+
export declare function recordPid(nodeId: string, pid: number): void;
|
|
37
|
+
/** Clear the pi pid (window-backed relaunch, before the fresh pi re-records it). */
|
|
38
|
+
export declare function clearPid(nodeId: string): void;
|
|
12
39
|
/** All rows, optionally filtered by status. */
|
|
13
40
|
export declare function listNodes(filter?: {
|
|
14
41
|
status?: NodeStatus | NodeStatus[];
|
|
@@ -35,6 +62,38 @@ export declare function view(root: string): string[];
|
|
|
35
62
|
* If so, stopping is a legitimate await; if not, it must finish or escalate. */
|
|
36
63
|
export declare function hasActiveLiveSubscription(nodeId: string): boolean;
|
|
37
64
|
/** Rebuild node rows from on-disk metas (the db node table is a derived index).
|
|
65
|
+
* Only the IDENTITY columns are rebuilt — they are a projection of meta. The
|
|
66
|
+
* runtime columns (status/intent/pi_pid/window/tmux_session/pane) are NOT in meta
|
|
67
|
+
* and NOT re-derivable from it: they describe live process/presence state, so
|
|
68
|
+
* an existing row keeps them and a freshly re-created row takes the schema's
|
|
69
|
+
* quiescent defaults (status='active', the rest null). The daemon reconciles
|
|
70
|
+
* liveness from tmux reality, not from a stale file.
|
|
38
71
|
* Edges are left intact — subscribes_to is db-authoritative; spawned_by is
|
|
39
|
-
* re-derived from each meta's `parent
|
|
72
|
+
* re-derived from each meta's `spawned_by` (fallback: `parent` for legacy metas). */
|
|
40
73
|
export declare function rebuildIndex(): void;
|
|
74
|
+
/** A node selected for pruning (or that would be, under --dry-run). */
|
|
75
|
+
export interface PrunedNode {
|
|
76
|
+
node_id: string;
|
|
77
|
+
status: NodeStatus;
|
|
78
|
+
created: string;
|
|
79
|
+
}
|
|
80
|
+
export interface PruneResult {
|
|
81
|
+
/** The nodes pruned (or, under dryRun, the nodes that WOULD be pruned). */
|
|
82
|
+
pruned: PrunedNode[];
|
|
83
|
+
dryRun: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
86
|
+
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
87
|
+
* node rows + dirs. The edges→nodes FK (`ON DELETE CASCADE`, migration v4) GCs
|
|
88
|
+
* each pruned node's edges automatically; the on-disk `nodes/<id>/` dir is
|
|
89
|
+
* removed too. Live nodes are NEVER touched: active | idle are the daemon's
|
|
90
|
+
* domain, a DISJOINT status set, so prune and supervision can't interfere.
|
|
91
|
+
*
|
|
92
|
+
* The row deletes run in ONE transaction (so the sweep is all-or-nothing); the
|
|
93
|
+
* dir removals follow after COMMIT — the fs isn't transactional, and by then the
|
|
94
|
+
* rows are gone, so a re-run never re-finds a half-deleted node. `dryRun`
|
|
95
|
+
* reports the candidate set and deletes NOTHING. */
|
|
96
|
+
export declare function pruneNodes(opts: {
|
|
97
|
+
ttlDays: number;
|
|
98
|
+
dryRun?: boolean;
|
|
99
|
+
}): PruneResult;
|