@north-light/crouter 0.3.198 → 0.3.199
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/api/client.d.ts +18 -18
- package/dist/api/client.js +29 -37
- package/dist/api/dto/broker-ops.d.ts +3 -1
- package/dist/api/dto/broker.d.ts +13 -2
- package/dist/api/dto/broker.js +4 -4
- package/dist/api/dto/human.d.ts +6 -5
- package/dist/api/dto/inbox.d.ts +81 -34
- package/dist/api/dto/nodes.d.ts +8 -2
- package/dist/api/dto/profiles.d.ts +23 -0
- package/dist/api/dto/profiles.js +2 -2
- package/dist/api/dto/worktree.d.ts +3 -5
- package/dist/api/routes.d.ts +3 -5
- package/dist/api/routes.js +4 -6
- package/dist/builtin-memory/00-runtime-base.md +6 -6
- package/dist/builtin-memory/01-spine/01-no-manager.md +1 -1
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -3
- package/dist/builtin-memory/04-orchestration-kernel.md +6 -6
- package/dist/builtin-memory/05-kinds/design/00-base.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +2 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +2 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -1
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -1
- package/dist/builtin-memory/insights/capture.md +2 -2
- package/dist/builtin-memory/insights/init.md +3 -3
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +1 -1
- package/dist/builtin-memory/internal/nodes-and-canvas.md +2 -2
- package/dist/builtin-memory/internal/plugins.md +59 -2
- package/dist/builtin-memory/internal/storage-tiers.md +3 -1
- package/dist/builtin-memory/spec/roadmap.md +2 -2
- package/dist/builtin-memory/testing.md +1 -1
- package/dist/clients/attach/chrome/bash-jobs.js +3 -3
- package/dist/clients/attach/render/chat-view.d.ts +26 -1
- package/dist/clients/attach/render/chat-view.js +61 -11
- package/dist/clients/attach/render/page-block.d.ts +34 -0
- package/dist/clients/attach/render/page-block.js +226 -0
- package/dist/clients/attach/session/whip.d.ts +2 -0
- package/dist/clients/attach/session/whip.js +2 -0
- package/dist/clients/attach/viewer.js +1419 -993
- package/dist/clients/conversation/projection.js +18 -3
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +9 -6
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +13 -21
- package/dist/clients/inbox/controller.js +10 -16
- package/dist/clients/inbox/page-adapter.d.ts +1 -3
- package/dist/clients/inbox/page-adapter.js +3 -4
- package/dist/clients/inbox/resolve.d.ts +1 -1
- package/dist/clients/inbox/resolve.js +3 -3
- package/dist/clients/inbox/review/document-surface.js +3 -1
- package/dist/clients/inbox/review/state.d.ts +2 -1
- package/dist/clients/inbox/review/state.js +5 -16
- package/dist/clients/inbox/tui/input.js +37 -19
- package/dist/clients/inbox/tui/panel.js +5 -31
- package/dist/clients/inbox/tui/render.d.ts +11 -0
- package/dist/clients/inbox/tui/render.js +71 -27
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +16 -12
- package/dist/clients/inbox/tui/types.d.ts +2 -7
- package/dist/clients/inbox/tui.js +2 -2
- package/dist/commands/__tests__/human.test.js +71 -114
- package/dist/commands/__tests__/node-message.test.js +14 -0
- package/dist/commands/attention.js +4 -4
- package/dist/commands/canvas-snapshot.js +1 -1
- package/dist/commands/canvas.js +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/human/{html.d.ts → components.d.ts} +1 -1
- package/dist/commands/human/components.js +77 -0
- package/dist/commands/human/feedback.d.ts +2 -0
- package/dist/commands/human/feedback.js +108 -0
- package/dist/commands/human/prompts.d.ts +13 -5
- package/dist/commands/human/prompts.js +160 -97
- package/dist/commands/human/queue.js +57 -88
- package/dist/commands/human/shared.d.ts +2 -12
- package/dist/commands/human/shared.js +17 -11
- package/dist/commands/human.js +29 -21
- package/dist/commands/memory/origin.js +2 -2
- package/dist/commands/node/bash.js +4 -4
- package/dist/commands/node/inspect.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node/message.js +2 -2
- package/dist/commands/node-worktree.js +8 -8
- package/dist/commands/pkg/browse.js +1 -1
- package/dist/commands/pkg/plugin-manage.js +129 -10
- package/dist/commands/profile/delete.js +41 -15
- package/dist/commands/profile.js +1 -1
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/focus.js +29 -14
- package/dist/commands/surface-edit.js +3 -3
- package/dist/commands/surface-inbox.d.ts +2 -0
- package/dist/commands/{human/inbox.js → surface-inbox.js} +14 -18
- package/dist/commands/surface.js +2 -1
- package/dist/commands/sys/doctor.js +60 -3
- package/dist/commands/sys/feedback.js +79 -25
- package/dist/commands/sys/settings.js +1 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +1 -1
- package/dist/core/__tests__/extension-abort.test.js +5 -2
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -2
- package/dist/core/__tests__/fixtures/fake-engine.js +47 -3
- package/dist/core/__tests__/human-cancel-guard.test.js +31 -10
- package/dist/core/__tests__/human-deliver.test.js +31 -26
- package/dist/core/__tests__/human-node-not-supervised.test.js +1 -1
- package/dist/core/__tests__/plugin-page-components.test.js +157 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +40 -0
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +2 -2
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +12 -6
- package/dist/core/__tests__/serial/live-mutation.test.js +1 -1
- package/dist/core/__tests__/serial/tmux-surface.test.js +4 -2
- package/dist/core/__tests__/serial/worktree.test.js +24 -198
- package/dist/core/__tests__/session-cycles.test.js +22 -0
- package/dist/core/__tests__/stop-guard.test.js +4 -4
- package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +19 -4
- package/dist/core/bash-jobs.d.ts +6 -0
- package/dist/core/bash-jobs.js +10 -0
- package/dist/core/canvas/__tests__/attention.test.js +8 -5
- package/dist/core/canvas/__tests__/render-remote.test.js +5 -5
- package/dist/core/canvas/attention.js +8 -6
- package/dist/core/canvas/browse/model.d.ts +8 -2
- package/dist/core/canvas/browse/model.js +11 -4
- package/dist/core/canvas/canvas.d.ts +7 -0
- package/dist/core/canvas/canvas.js +37 -0
- package/dist/core/canvas/crons.d.ts +9 -0
- package/dist/core/canvas/crons.js +51 -0
- package/dist/core/canvas/node-order.d.ts +22 -1
- package/dist/core/canvas/node-order.js +26 -1
- package/dist/core/canvas/render-source.d.ts +5 -0
- package/dist/core/canvas/render-source.js +1 -0
- package/dist/core/canvas/types.d.ts +9 -3
- package/dist/core/command-plugins/bundle.d.ts +6 -1
- package/dist/core/command-plugins/bundle.js +26 -13
- package/dist/core/command.js +2 -1
- package/dist/core/config.d.ts +47 -1
- package/dist/core/config.js +137 -4
- package/dist/core/help.js +1 -1
- package/dist/core/human/__tests__/page-catalog.test.js +4 -0
- package/dist/core/human/__tests__/page-tickets.test.js +65 -52
- package/dist/core/human/__tests__/page.test.js +56 -98
- package/dist/core/human/__tests__/serial/inbox-core.test.js +6 -6
- package/dist/core/human/answer-text.js +3 -3
- package/dist/core/human/answer.d.ts +6 -4
- package/dist/core/human/answer.js +4 -10
- package/dist/core/human/component-docs.d.ts +11 -7
- package/dist/core/human/component-docs.js +361 -88
- package/dist/core/human/convention.d.ts +17 -3
- package/dist/core/human/convention.js +36 -5
- package/dist/core/human/feedback-companion.d.ts +20 -0
- package/dist/core/human/feedback-companion.js +98 -0
- package/dist/core/human/feedback.d.ts +65 -0
- package/dist/core/human/feedback.js +116 -0
- package/dist/core/human/page-catalog.d.ts +40 -5
- package/dist/core/human/page-catalog.js +102 -30
- package/dist/core/human/page-errors.d.ts +4 -0
- package/dist/core/human/page-errors.js +7 -0
- package/dist/core/human/page-eval.d.ts +20 -0
- package/dist/core/human/page-eval.js +123 -0
- package/dist/core/human/page-schema.d.ts +23 -191
- package/dist/core/human/page-schema.js +60 -89
- package/dist/core/human/page-synth.d.ts +1 -1
- package/dist/core/human/page-synth.js +15 -18
- package/dist/core/human/page.d.ts +12 -27
- package/dist/core/human/page.js +142 -474
- package/dist/core/human/root.d.ts +6 -2
- package/dist/core/human/root.js +12 -4
- package/dist/core/human/scan.d.ts +3 -4
- package/dist/core/human/scan.js +16 -15
- package/dist/core/human/summary.d.ts +1 -2
- package/dist/core/human/summary.js +2 -2
- package/dist/core/human/tickets.d.ts +20 -14
- package/dist/core/human/tickets.js +91 -31
- package/dist/core/human/types.d.ts +10 -2
- package/dist/core/preview-registry.js +8 -10
- package/dist/core/profiles/default-binding.d.ts +4 -0
- package/dist/core/profiles/default-binding.js +24 -0
- package/dist/core/profiles/deletion-reservation.d.ts +8 -0
- package/dist/core/profiles/deletion-reservation.js +38 -0
- package/dist/core/profiles/manifest.d.ts +3 -0
- package/dist/core/profiles/manifest.js +12 -0
- package/dist/core/profiles/select.js +2 -2
- package/dist/core/review/realize.js +3 -3
- package/dist/core/review/store.d.ts +6 -0
- package/dist/core/review/store.js +31 -0
- package/dist/core/runtime/bearings.d.ts +0 -4
- package/dist/core/runtime/bearings.js +4 -62
- package/dist/core/runtime/bin-contributions.d.ts +44 -0
- package/dist/core/runtime/bin-contributions.js +186 -0
- package/dist/core/runtime/broker/client-registry.d.ts +4 -0
- package/dist/core/runtime/broker/client-registry.js +13 -1
- package/dist/core/runtime/broker/extension-abort.d.ts +1 -1
- package/dist/core/runtime/broker/extension-abort.js +4 -2
- package/dist/core/runtime/broker/fault-retry.js +4 -0
- package/dist/core/runtime/broker/frame-dispatch.d.ts +3 -1
- package/dist/core/runtime/broker/frame-dispatch.js +5 -6
- package/dist/core/runtime/broker/read-ops.d.ts +1 -1
- package/dist/core/runtime/broker/read-ops.js +2 -2
- package/dist/core/runtime/broker/rebind.d.ts +1 -0
- package/dist/core/runtime/broker/rebind.js +6 -1
- package/dist/core/runtime/broker-extension-render.js +19 -0
- package/dist/core/runtime/broker-protocol.d.ts +14 -0
- package/dist/core/runtime/broker.d.ts +1 -1
- package/dist/core/runtime/broker.js +17 -6
- package/dist/core/runtime/front-door-env.d.ts +5 -0
- package/dist/core/runtime/front-door-env.js +16 -0
- package/dist/core/runtime/front-door.d.ts +1 -5
- package/dist/core/runtime/front-door.js +2 -5
- package/dist/core/runtime/host.d.ts +1 -1
- package/dist/core/runtime/invocation.d.ts +6 -0
- package/dist/core/runtime/invocation.js +10 -0
- package/dist/core/runtime/kickoff.js +34 -0
- package/dist/core/runtime/launch.d.ts +2 -6
- package/dist/core/runtime/node-read.js +4 -1
- package/dist/core/runtime/nodes.js +2 -0
- package/dist/core/runtime/promote.d.ts +1 -0
- package/dist/core/runtime/promote.js +3 -1
- package/dist/core/runtime/session-cycles.d.ts +6 -0
- package/dist/core/runtime/session-cycles.js +15 -1
- package/dist/core/runtime/session-visibility.d.ts +17 -8
- package/dist/core/runtime/session-visibility.js +19 -10
- package/dist/core/runtime/spawn-env.d.ts +9 -1
- package/dist/core/runtime/spawn-env.js +62 -2
- package/dist/core/runtime/stop-guard.d.ts +2 -2
- package/dist/core/runtime/stop-guard.js +2 -2
- package/dist/core/runtime/stop-signals.d.ts +1 -1
- package/dist/core/runtime/stop-signals.js +2 -2
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/session-model/session-state.d.ts +4 -3
- package/dist/core/session-model/session-state.js +14 -1
- package/dist/core/user-settings.d.ts +21 -2
- package/dist/core/user-settings.js +32 -11
- package/dist/core/worktree.d.ts +15 -45
- package/dist/core/worktree.js +59 -261
- package/dist/daemon/api/__tests__/seam/profile-delete.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/seam/profile-delete.test.js +386 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +9 -5
- package/dist/daemon/api/handlers/feedback-comments.d.ts +2 -0
- package/dist/daemon/api/handlers/feedback-comments.js +207 -0
- package/dist/daemon/api/handlers/human.js +24 -20
- package/dist/daemon/api/handlers/inbox.d.ts +10 -0
- package/dist/daemon/api/handlers/inbox.js +39 -115
- package/dist/daemon/api/handlers/profiles.js +11 -29
- package/dist/daemon/api/map.d.ts +6 -1
- package/dist/daemon/api/map.js +23 -0
- package/dist/daemon/api/server.js +3 -1
- package/dist/daemon/companion-retire.d.ts +2 -0
- package/dist/daemon/companion-retire.js +33 -0
- package/dist/daemon/cron-run.js +14 -16
- package/dist/daemon/human/finish.d.ts +12 -7
- package/dist/daemon/human/finish.js +101 -38
- package/dist/daemon/human/sweep.js +5 -1
- package/dist/daemon/profile-delete.d.ts +7 -0
- package/dist/daemon/profile-delete.js +306 -0
- package/dist/daemon/reconcilers/broker-supervision.js +3 -1
- package/dist/daemon/review/deliver.js +3 -3
- package/dist/daemon/review/finish.d.ts +0 -2
- package/dist/daemon/review/finish.js +1 -29
- package/dist/pi-extensions/__tests__/canvas-bash-valve.test.js +56 -1
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.js +19 -5
- package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.d.ts +4 -0
- package/dist/pi-extensions/canvas-review-boundary.js +18 -9
- package/dist/pi-extensions/canvas-stophook.js +4 -4
- package/dist/shared/generated-context.js +2 -2
- package/dist/types.d.ts +44 -2
- package/dist/types.js +7 -2
- package/package.json +4 -4
- package/runtime.lock.json +5 -33
- package/dist/builtin-memory/init.md +0 -38
- package/dist/builtin-memory/plan.md +0 -19
- package/dist/builtin-memory/spec.md +0 -19
- package/dist/commands/human/doc.d.ts +0 -2
- package/dist/commands/human/doc.js +0 -91
- package/dist/commands/human/html.js +0 -81
- package/dist/commands/human/inbox.d.ts +0 -2
- package/dist/core/human/__tests__/html-markdown.test.js +0 -52
- package/dist/core/human/__tests__/page-render.test.js +0 -66
- package/dist/core/human/html-markdown.d.ts +0 -3
- package/dist/core/human/html-markdown.js +0 -288
- package/dist/core/human/page-render.d.ts +0 -4
- package/dist/core/human/page-render.js +0 -105
- package/dist/pages/bundle.css +0 -1
- package/dist/pages/bundle.js +0 -1778
- package/dist/pages/comments.d.ts +0 -72
- package/dist/pages/comments.js +0 -176
- package/dist/pages/controls.d.ts +0 -98
- package/dist/pages/controls.js +0 -261
- package/dist/pages/elements/cards.d.ts +0 -28
- package/dist/pages/elements/cards.js +0 -915
- package/dist/pages/elements/chart.d.ts +0 -23
- package/dist/pages/elements/chart.js +0 -664
- package/dist/pages/elements/options.d.ts +0 -30
- package/dist/pages/elements/options.js +0 -742
- package/dist/pages/elements/pages.d.ts +0 -30
- package/dist/pages/elements/pages.js +0 -377
- package/dist/pages/elements/slot.d.ts +0 -26
- package/dist/pages/elements/slot.js +0 -112
- package/dist/pages/elements/table.d.ts +0 -36
- package/dist/pages/elements/table.js +0 -1043
- package/dist/pages/elements/text.d.ts +0 -31
- package/dist/pages/elements/text.js +0 -1175
- package/dist/pages/entry.d.ts +0 -8
- package/dist/pages/entry.js +0 -8
- package/dist/pages/host.d.ts +0 -134
- package/dist/pages/host.js +0 -165
- package/dist/pages/readonly.d.ts +0 -24
- package/dist/pages/readonly.js +0 -31
- package/dist/pages/register.d.ts +0 -2
- package/dist/pages/register.js +0 -3
- package/dist/pages/responses.d.ts +0 -37
- package/dist/pages/responses.js +0 -56
- package/dist/pages/slot-config.d.ts +0 -36
- package/dist/pages/slot-config.js +0 -50
- package/dist/pages/types.d.ts +0 -98
- package/dist/pages/types.js +0 -23
- /package/dist/{core/human/__tests__/html-markdown.test.d.ts → commands/__tests__/node-message.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/page-render.test.d.ts → __tests__/plugin-page-components.test.d.ts} +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { cronIdsForProfileDeletion, cronIdsForProfileDetach, deleteCronsForProfile, deleteNodes, getNode, getRow, globalizeCronsForProfile, listNodesByProfile, nodesRoot, reviewDir, } from '../core/canvas/index.js';
|
|
4
|
+
import { usage } from '../core/errors.js';
|
|
5
|
+
import { emitEvent } from '../core/events/emit.js';
|
|
6
|
+
import { clearDefaultProfileId, defaultProfileDirs, } from '../core/profiles/default-binding.js';
|
|
7
|
+
import { reserveProfileDeletion } from '../core/profiles/deletion-reservation.js';
|
|
8
|
+
import { deleteProfile, loadProfileManifest, } from '../core/profiles/manifest.js';
|
|
9
|
+
import { deleteReviewsByOriginNodes, reviewIdsByOriginNodes, } from '../core/review/store.js';
|
|
10
|
+
import { boundFleet } from '../core/runtime/fleet.js';
|
|
11
|
+
import { headlessBrokerHost } from '../core/runtime/host.js';
|
|
12
|
+
import { transition } from '../core/runtime/lifecycle.js';
|
|
13
|
+
import { tearDownNode } from '../core/runtime/placement.js';
|
|
14
|
+
import { reshapeNode } from '../core/runtime/promote.js';
|
|
15
|
+
import { reviveNode } from '../core/runtime/revive.js';
|
|
16
|
+
const SELF_DELETE_GRACE_MS = 1_000;
|
|
17
|
+
const TICKET_FILES = new Set([
|
|
18
|
+
'page.json',
|
|
19
|
+
'page.tsx',
|
|
20
|
+
'page.html',
|
|
21
|
+
'page.js',
|
|
22
|
+
'review.json',
|
|
23
|
+
'response.json',
|
|
24
|
+
'progress.json',
|
|
25
|
+
'claim.json',
|
|
26
|
+
'reply-route.json',
|
|
27
|
+
'feedback-comments.json',
|
|
28
|
+
'feedback.json',
|
|
29
|
+
'.ticket-lock',
|
|
30
|
+
]);
|
|
31
|
+
const pending = new Map();
|
|
32
|
+
function requestObject(body) {
|
|
33
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
34
|
+
throw usage('profile deletion requires a JSON request body', {
|
|
35
|
+
error: 'force_required',
|
|
36
|
+
message: 'profile deletion requires --force',
|
|
37
|
+
next: 'Re-run `crtr profile delete <profile> --force` after reviewing its effects.',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
42
|
+
function validateRequest(body) {
|
|
43
|
+
const value = requestObject(body);
|
|
44
|
+
if (value['force'] !== true) {
|
|
45
|
+
throw usage('profile deletion requires --force', {
|
|
46
|
+
error: 'force_required',
|
|
47
|
+
message: 'profile deletion requires --force',
|
|
48
|
+
next: 'Re-run `crtr profile delete <profile> --force` after reviewing its effects.',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (value['detach'] !== undefined && typeof value['detach'] !== 'boolean') {
|
|
52
|
+
throw usage('detach must be a boolean', {
|
|
53
|
+
error: 'invalid_detach',
|
|
54
|
+
message: 'detach must be a boolean',
|
|
55
|
+
next: 'Use --detach as a presence-only flag, or omit it.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const caller = value['caller_node_id'];
|
|
59
|
+
if (caller !== undefined && caller !== null && (typeof caller !== 'string' || caller === '')) {
|
|
60
|
+
throw usage('caller_node_id must be a nonempty string or null');
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
force: true,
|
|
64
|
+
...(value['detach'] === true ? { detach: true } : {}),
|
|
65
|
+
...(caller === undefined ? {} : { caller_node_id: caller }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function manifestSourceNodeId(path) {
|
|
69
|
+
if (!existsSync(path))
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
73
|
+
return typeof parsed.source?.nodeId === 'string' && parsed.source.nodeId !== ''
|
|
74
|
+
? parsed.source.nodeId
|
|
75
|
+
: null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function ticketCleanupForNodes(nodeIds) {
|
|
82
|
+
const wanted = new Set(nodeIds);
|
|
83
|
+
const root = nodesRoot();
|
|
84
|
+
if (wanted.size === 0 || !existsSync(root))
|
|
85
|
+
return [];
|
|
86
|
+
const cleanup = [];
|
|
87
|
+
for (const dirent of readdirSync(root, { withFileTypes: true })) {
|
|
88
|
+
if (!dirent.isDirectory())
|
|
89
|
+
continue;
|
|
90
|
+
const dir = join(root, dirent.name);
|
|
91
|
+
const sourceIds = [
|
|
92
|
+
manifestSourceNodeId(join(dir, 'page.json')),
|
|
93
|
+
manifestSourceNodeId(join(dir, 'review.json')),
|
|
94
|
+
];
|
|
95
|
+
if (!sourceIds.some((sourceId) => sourceId !== null && wanted.has(sourceId)))
|
|
96
|
+
continue;
|
|
97
|
+
cleanup.push({
|
|
98
|
+
dir,
|
|
99
|
+
wholeDirectory: wanted.has(dirent.name) || getRow(dirent.name) === null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return cleanup.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
103
|
+
}
|
|
104
|
+
function isTicketOwnedResidue(name) {
|
|
105
|
+
if (TICKET_FILES.has(name))
|
|
106
|
+
return true;
|
|
107
|
+
for (const base of TICKET_FILES) {
|
|
108
|
+
if (name.startsWith(`${base}.`) && (name.endsWith('.tmp') || name.endsWith('.lock'))) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function cleanTickets(tickets) {
|
|
115
|
+
let cleaned = 0;
|
|
116
|
+
for (const ticket of tickets) {
|
|
117
|
+
if (!existsSync(ticket.dir))
|
|
118
|
+
continue;
|
|
119
|
+
if (ticket.wholeDirectory) {
|
|
120
|
+
rmSync(ticket.dir, { recursive: true, force: true });
|
|
121
|
+
cleaned += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const dirent of readdirSync(ticket.dir, { withFileTypes: true })) {
|
|
125
|
+
if (!isTicketOwnedResidue(dirent.name))
|
|
126
|
+
continue;
|
|
127
|
+
rmSync(join(ticket.dir, dirent.name), { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
cleaned += 1;
|
|
130
|
+
}
|
|
131
|
+
return cleaned;
|
|
132
|
+
}
|
|
133
|
+
function openWorktreeError(nodes) {
|
|
134
|
+
const blocked = nodes
|
|
135
|
+
.filter((node) => node.managed_worktree?.state === 'open')
|
|
136
|
+
.map((node) => ({ node_id: node.node_id, path: node.managed_worktree.path }));
|
|
137
|
+
if (blocked.length === 0)
|
|
138
|
+
return null;
|
|
139
|
+
const message = `profile deletion is blocked by ${blocked.length} open managed worktree${blocked.length === 1 ? '' : 's'}`;
|
|
140
|
+
return usage(message, {
|
|
141
|
+
error: 'open_managed_worktree',
|
|
142
|
+
message,
|
|
143
|
+
nodes: blocked,
|
|
144
|
+
next: `Close ${blocked.map(({ node_id }) => node_id).join(', ')} with \`crtr node worktree close <node>\`, then retry.`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function prepareDeletion(entry, request) {
|
|
148
|
+
const nodes = listNodesByProfile(entry.profileId);
|
|
149
|
+
const blocked = openWorktreeError(nodes);
|
|
150
|
+
if (blocked !== null)
|
|
151
|
+
throw blocked;
|
|
152
|
+
const nodeIds = nodes.map(({ node_id }) => node_id);
|
|
153
|
+
const mode = request.detach === true ? 'detached' : 'deleted';
|
|
154
|
+
return {
|
|
155
|
+
entry,
|
|
156
|
+
mode,
|
|
157
|
+
nodes,
|
|
158
|
+
nodeIds,
|
|
159
|
+
tickets: mode === 'deleted' ? ticketCleanupForNodes(nodeIds) : [],
|
|
160
|
+
cronIds: mode === 'deleted'
|
|
161
|
+
? cronIdsForProfileDeletion(entry.profileId, nodeIds)
|
|
162
|
+
: cronIdsForProfileDetach(entry.profileId),
|
|
163
|
+
reviewIds: mode === 'deleted' ? reviewIdsByOriginNodes(nodeIds) : [],
|
|
164
|
+
defaultDirs: defaultProfileDirs(entry.profileId),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function assertDeletionPlanPreconditions(plan) {
|
|
168
|
+
const currentNodes = plan.nodeIds.flatMap((nodeId) => {
|
|
169
|
+
const node = getNode(nodeId);
|
|
170
|
+
return node === null ? [] : [node];
|
|
171
|
+
});
|
|
172
|
+
const blocked = openWorktreeError(currentNodes);
|
|
173
|
+
if (blocked !== null)
|
|
174
|
+
throw blocked;
|
|
175
|
+
}
|
|
176
|
+
function resultForPlan(plan) {
|
|
177
|
+
return {
|
|
178
|
+
profile_id: plan.entry.profileId,
|
|
179
|
+
name: plan.entry.manifest.name,
|
|
180
|
+
mode: plan.mode,
|
|
181
|
+
nodes: plan.nodeIds.length,
|
|
182
|
+
crons: plan.cronIds.length,
|
|
183
|
+
tickets: plan.tickets.length,
|
|
184
|
+
reviews: plan.reviewIds.length,
|
|
185
|
+
default_pins: plan.defaultDirs.length,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function detachProfile(plan) {
|
|
189
|
+
assertDeletionPlanPreconditions(plan);
|
|
190
|
+
const existingNodeIds = plan.nodeIds.filter((nodeId) => getNode(nodeId) !== null);
|
|
191
|
+
const fleet = boundFleet();
|
|
192
|
+
const live = existingNodeIds.flatMap((nodeId) => {
|
|
193
|
+
const entry = fleet.get(nodeId);
|
|
194
|
+
return entry === undefined ? [] : [{ nodeId, exited: entry.handle.exited }];
|
|
195
|
+
});
|
|
196
|
+
const restartNodeIds = [];
|
|
197
|
+
for (const { nodeId } of live) {
|
|
198
|
+
const current = getNode(nodeId);
|
|
199
|
+
if (current?.status === 'active' || current?.status === 'idle') {
|
|
200
|
+
transition(nodeId, 'release');
|
|
201
|
+
restartNodeIds.push(nodeId);
|
|
202
|
+
}
|
|
203
|
+
headlessBrokerHost.teardown(nodeId);
|
|
204
|
+
}
|
|
205
|
+
await Promise.all(live.map(({ exited }) => exited));
|
|
206
|
+
const detachedNodeIds = [];
|
|
207
|
+
for (const nodeId of existingNodeIds) {
|
|
208
|
+
if (getNode(nodeId) === null)
|
|
209
|
+
continue;
|
|
210
|
+
reshapeNode(nodeId, { profileId: null });
|
|
211
|
+
detachedNodeIds.push(nodeId);
|
|
212
|
+
}
|
|
213
|
+
const cronIds = globalizeCronsForProfile(plan.entry.profileId);
|
|
214
|
+
const defaultDirs = clearDefaultProfileId(plan.entry.profileId);
|
|
215
|
+
deleteProfile(plan.entry.profileId);
|
|
216
|
+
const detached = new Set(detachedNodeIds);
|
|
217
|
+
for (const nodeId of restartNodeIds) {
|
|
218
|
+
if (detached.has(nodeId))
|
|
219
|
+
reviveNode(nodeId, { resume: true, recovery: true });
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
...resultForPlan(plan),
|
|
223
|
+
nodes: detachedNodeIds.length,
|
|
224
|
+
crons: cronIds.length,
|
|
225
|
+
default_pins: defaultDirs.length,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function hardDeleteProfile(plan) {
|
|
229
|
+
assertDeletionPlanPreconditions(plan);
|
|
230
|
+
const cronIds = deleteCronsForProfile(plan.entry.profileId, plan.nodeIds);
|
|
231
|
+
for (const nodeId of plan.nodeIds) {
|
|
232
|
+
headlessBrokerHost.teardown(nodeId);
|
|
233
|
+
tearDownNode(nodeId);
|
|
234
|
+
}
|
|
235
|
+
const reviewIds = deleteReviewsByOriginNodes(plan.nodeIds);
|
|
236
|
+
for (const reviewId of reviewIds)
|
|
237
|
+
rmSync(reviewDir(reviewId), { recursive: true, force: true });
|
|
238
|
+
const tickets = cleanTickets(plan.tickets);
|
|
239
|
+
const deletedNodeIds = deleteNodes(plan.nodeIds);
|
|
240
|
+
const defaultDirs = clearDefaultProfileId(plan.entry.profileId);
|
|
241
|
+
deleteProfile(plan.entry.profileId);
|
|
242
|
+
return {
|
|
243
|
+
...resultForPlan(plan),
|
|
244
|
+
nodes: deletedNodeIds.length,
|
|
245
|
+
crons: cronIds.length,
|
|
246
|
+
tickets,
|
|
247
|
+
reviews: reviewIds.length,
|
|
248
|
+
default_pins: defaultDirs.length,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async function executePlan(plan) {
|
|
252
|
+
return plan.mode === 'detached' ? detachProfile(plan) : hardDeleteProfile(plan);
|
|
253
|
+
}
|
|
254
|
+
function scheduleSelfDeletion(plan, releaseReservation) {
|
|
255
|
+
const result = {
|
|
256
|
+
...resultForPlan(plan),
|
|
257
|
+
accepted: true,
|
|
258
|
+
grace_ms: SELF_DELETE_GRACE_MS,
|
|
259
|
+
};
|
|
260
|
+
pending.set(plan.entry.profileId, { result });
|
|
261
|
+
setTimeout(() => {
|
|
262
|
+
void (async () => {
|
|
263
|
+
try {
|
|
264
|
+
await executePlan(plan);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
emitEvent({
|
|
268
|
+
level: 'error',
|
|
269
|
+
event: 'profile.delete.deferred_failed',
|
|
270
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
271
|
+
fields: { profile_id: plan.entry.profileId, mode: plan.mode },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
pending.delete(plan.entry.profileId);
|
|
276
|
+
releaseReservation();
|
|
277
|
+
}
|
|
278
|
+
})();
|
|
279
|
+
}, SELF_DELETE_GRACE_MS);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
/** Validate and enact one forced profile deletion. A request from a node that
|
|
283
|
+
* belongs to the target profile is acknowledged before its broker is stopped. */
|
|
284
|
+
export async function requestProfileDeletion(name, body) {
|
|
285
|
+
const request = validateRequest(body);
|
|
286
|
+
const entry = loadProfileManifest(name);
|
|
287
|
+
const existing = pending.get(entry.profileId);
|
|
288
|
+
if (existing !== undefined)
|
|
289
|
+
return { status: 202, body: existing.result };
|
|
290
|
+
const releaseReservation = reserveProfileDeletion(entry.profileId);
|
|
291
|
+
let deferred = false;
|
|
292
|
+
try {
|
|
293
|
+
const plan = prepareDeletion(entry, request);
|
|
294
|
+
const selfDeleting = request.caller_node_id != null && plan.nodeIds.includes(request.caller_node_id);
|
|
295
|
+
if (selfDeleting) {
|
|
296
|
+
const result = scheduleSelfDeletion(plan, releaseReservation);
|
|
297
|
+
deferred = true;
|
|
298
|
+
return { status: 202, body: result };
|
|
299
|
+
}
|
|
300
|
+
return { status: 200, body: await executePlan(plan) };
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
if (!deferred)
|
|
304
|
+
releaseReservation();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -20,6 +20,7 @@ import { operationIdContext } from '../../core/events/operation-id.js';
|
|
|
20
20
|
import { readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
21
21
|
import { activeBackgroundBashJobs } from '../../core/bash-jobs.js';
|
|
22
22
|
import { focusOf } from '../../core/runtime/placement.js';
|
|
23
|
+
import { hasAttachedBrokerViewers } from '../../core/runtime/broker/client-registry.js';
|
|
23
24
|
import { parkEvent } from './live-obligation.js';
|
|
24
25
|
const execFileAsync = promisify(execFile);
|
|
25
26
|
function configuredPositiveInteger(name, fallback) {
|
|
@@ -191,7 +192,8 @@ export class BrokerSupervisionReconciler {
|
|
|
191
192
|
|| row.intent !== null
|
|
192
193
|
|| isBusy(id)
|
|
193
194
|
|| activeBackgroundBashJobs(contextDir(id)).length > 0
|
|
194
|
-
|| focusOf(id) !== null
|
|
195
|
+
|| focusOf(id) !== null
|
|
196
|
+
|| hasAttachedBrokerViewers(id)) {
|
|
195
197
|
this.unattendedSince.delete(id);
|
|
196
198
|
return;
|
|
197
199
|
}
|
|
@@ -3,8 +3,8 @@ import { formatReviewApproval } from '../../shared/generated-context.js';
|
|
|
3
3
|
import { deliverNodeMessage } from '../messaging/node-message.js';
|
|
4
4
|
function approvalBody(review, resultPath) {
|
|
5
5
|
const summary = review.changed
|
|
6
|
-
? `The
|
|
7
|
-
: `The
|
|
6
|
+
? `The user reviewed and approved \`${review.file}\`. The document changed during review; re-read it before continuing. Review conversation: node \`${review.companion_node_id}\`.`
|
|
7
|
+
: `The user reviewed and approved \`${review.file}\`. The document was not modified.`;
|
|
8
8
|
return `${summary}\n\nDurable review result: ${resultPath}`;
|
|
9
9
|
}
|
|
10
10
|
/** Render the immutable approval fact without replaying comment text. */
|
|
@@ -25,7 +25,7 @@ export async function deliverApproval(review, resultPath) {
|
|
|
25
25
|
const delivery = await deliverNodeMessage({
|
|
26
26
|
node_id: review.origin_node_id,
|
|
27
27
|
body,
|
|
28
|
-
label: '
|
|
28
|
+
label: 'review approved by the user',
|
|
29
29
|
from: 'crtrd',
|
|
30
30
|
mode: 'interactive',
|
|
31
31
|
data: { review_id: review.review_id, result_path: resultPath },
|
|
@@ -18,8 +18,6 @@ export interface ReviewCancellationFinishOutcome {
|
|
|
18
18
|
ticket_result?: TicketResult;
|
|
19
19
|
}
|
|
20
20
|
export type ReviewFinishOutcome = ReviewApprovalFinishOutcome | ReviewCancellationFinishOutcome;
|
|
21
|
-
/** Retire a companion only through a lifecycle transition legal for its current status. */
|
|
22
|
-
export declare function retireCompanion(nodeId: string, outcome: 'approved' | 'canceled'): 'retired' | 'already' | 'missing';
|
|
23
21
|
/** Approve on the human's behalf, waiting first when the companion is still
|
|
24
22
|
* working. Waiting is the only new outcome: an idle companion approves in the
|
|
25
23
|
* same pass it always did, and the wait never freezes the review — comments
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { withCanvasWrite } from '../../core/canvas/db.js';
|
|
2
|
-
import { getNode } from '../../core/canvas/canvas.js';
|
|
3
2
|
import { responsePath } from '../../core/human/convention.js';
|
|
4
3
|
import { ticketDir } from '../../core/human/root.js';
|
|
5
4
|
import { readTicketResult } from '../../core/human/tickets.js';
|
|
@@ -10,9 +9,8 @@ import { readReviewSource } from '../../core/review/document.js';
|
|
|
10
9
|
import { signalReviewActivity } from '../../core/review/signal.js';
|
|
11
10
|
import { approveReviewLocked, cancelReviewRow, getReview, getReviewByCompanion, markDelivered, markDeliveryError, markProjected, markProjectionError, requestReviewSubmit, requireVisibleReview, } from '../../core/review/store.js';
|
|
12
11
|
import { ReviewOperationError } from '../../core/review/types.js';
|
|
13
|
-
import { transition } from '../../core/runtime/lifecycle.js';
|
|
14
|
-
import { headlessBrokerHost } from '../../core/runtime/host.js';
|
|
15
12
|
import { emitEvent } from '../../core/events/emit.js';
|
|
13
|
+
import { retireCompanion } from '../companion-retire.js';
|
|
16
14
|
import { deliverNodeMessage } from '../messaging/node-message.js';
|
|
17
15
|
import { cancelHumanTicket, deliverTerminalResult } from '../human/finish.js';
|
|
18
16
|
import { companionHasQueuedWork, companionMidTurn, companionStillWorking } from './companion.js';
|
|
@@ -69,32 +67,6 @@ async function serializedFinish(reviewId, work) {
|
|
|
69
67
|
finishChains.delete(reviewId);
|
|
70
68
|
}
|
|
71
69
|
}
|
|
72
|
-
/** Retire a companion only through a lifecycle transition legal for its current status. */
|
|
73
|
-
export function retireCompanion(nodeId, outcome) {
|
|
74
|
-
const node = getNode(nodeId);
|
|
75
|
-
if (node === null)
|
|
76
|
-
return 'missing';
|
|
77
|
-
if (node.status === 'done' || node.status === 'canceled')
|
|
78
|
-
return 'already';
|
|
79
|
-
if (outcome === 'approved' && (node.status === 'active' || node.status === 'idle')) {
|
|
80
|
-
transition(nodeId, 'finish');
|
|
81
|
-
try {
|
|
82
|
-
headlessBrokerHost.teardown(nodeId);
|
|
83
|
-
}
|
|
84
|
-
catch { /* the terminal transition is authoritative */ }
|
|
85
|
-
return 'retired';
|
|
86
|
-
}
|
|
87
|
-
// Cancellation is legal from every status. An already-dead companion cannot
|
|
88
|
-
// be finished, so it is retired as canceled even when approval won.
|
|
89
|
-
transition(nodeId, 'cancel');
|
|
90
|
-
if (node.status === 'active' || node.status === 'idle') {
|
|
91
|
-
try {
|
|
92
|
-
headlessBrokerHost.teardown(nodeId);
|
|
93
|
-
}
|
|
94
|
-
catch { /* the terminal transition is authoritative */ }
|
|
95
|
-
}
|
|
96
|
-
return 'retired';
|
|
97
|
-
}
|
|
98
70
|
async function finishApproved(review, result) {
|
|
99
71
|
let current = requireVisibleReview(review.review_id);
|
|
100
72
|
if (current.projected_at === null) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// still spawned commands with that deleted directory as cwd. child_process
|
|
5
5
|
// then failed before bash could run, leaving the node unable to use its shell.
|
|
6
6
|
import assert from 'node:assert/strict';
|
|
7
|
-
import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
7
|
+
import { chmodSync, existsSync, mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { homedir, tmpdir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { test } from 'node:test';
|
|
@@ -29,3 +29,58 @@ test('bash falls back to home after its worktree cwd was removed', { timeout: 5_
|
|
|
29
29
|
rmSync(root, { recursive: true, force: true });
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
|
+
test('bash valve resolves an admitted relative PATH entry from the execution cwd', { timeout: 5_000 }, async () => {
|
|
33
|
+
const root = mkdtempSync(join(tmpdir(), 'crtr-bash-valve-relative-host-path-'));
|
|
34
|
+
const contextDir = join(root, 'context');
|
|
35
|
+
const hostBin = join(root, 'host-bin');
|
|
36
|
+
const shim = join(root, 'shim');
|
|
37
|
+
const marker = join(root, 'contributed-bash-ran');
|
|
38
|
+
mkdirSync(contextDir);
|
|
39
|
+
mkdirSync(hostBin);
|
|
40
|
+
mkdirSync(shim);
|
|
41
|
+
symlinkSync('/bin/bash', join(hostBin, 'bash'));
|
|
42
|
+
writeFileSync(join(shim, 'bash'), `#!/bin/sh\nprintf contributed > ${JSON.stringify(marker)}\nexit 97\n`);
|
|
43
|
+
writeFileSync(join(shim, 'tool'), '#!/bin/sh\nprintf tool\n');
|
|
44
|
+
chmodSync(join(shim, 'bash'), 0o755);
|
|
45
|
+
chmodSync(join(shim, 'tool'), 0o755);
|
|
46
|
+
try {
|
|
47
|
+
const output = [];
|
|
48
|
+
const result = await createValveOperations('test-node', contextDir).exec('tool; command -v tool', root, {
|
|
49
|
+
env: { PATH: `${shim}:host-bin:/bin:/usr/bin`, CRTR_ADMITTED_HOST_PATH: 'host-bin:/bin:/usr/bin' },
|
|
50
|
+
onData: (data) => output.push(data),
|
|
51
|
+
});
|
|
52
|
+
assert.equal(result.exitCode, 0);
|
|
53
|
+
assert.equal(Buffer.concat(output).toString(), `tool${shim}/tool\n`);
|
|
54
|
+
assert.equal(existsSync(marker), false, 'the contributed bash must never be crouter\'s interpreter');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
rmSync(root, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
test('bash valve uses the admitted host shell while commands keep shim precedence', { timeout: 5_000 }, async () => {
|
|
61
|
+
const root = mkdtempSync(join(tmpdir(), 'crtr-bash-valve-shim-'));
|
|
62
|
+
const contextDir = join(root, 'context');
|
|
63
|
+
const shim = join(root, 'shim');
|
|
64
|
+
const marker = join(root, 'contributed-bash-ran');
|
|
65
|
+
mkdirSync(contextDir);
|
|
66
|
+
mkdirSync(shim);
|
|
67
|
+
writeFileSync(join(shim, 'bash'), `#!/bin/sh\nprintf contributed > ${JSON.stringify(marker)}\nexit 97\n`);
|
|
68
|
+
writeFileSync(join(shim, 'tool'), '#!/bin/sh\nprintf tool\n');
|
|
69
|
+
chmodSync(join(shim, 'bash'), 0o755);
|
|
70
|
+
chmodSync(join(shim, 'tool'), 0o755);
|
|
71
|
+
try {
|
|
72
|
+
const output = [];
|
|
73
|
+
const hostPath = process.env['PATH'];
|
|
74
|
+
assert.notEqual(hostPath, undefined);
|
|
75
|
+
const result = await createValveOperations('test-node', contextDir).exec('tool; command -v tool', root, {
|
|
76
|
+
env: { PATH: `${shim}:${hostPath}`, CRTR_ADMITTED_HOST_PATH: hostPath },
|
|
77
|
+
onData: (data) => output.push(data),
|
|
78
|
+
});
|
|
79
|
+
assert.equal(result.exitCode, 0);
|
|
80
|
+
assert.equal(Buffer.concat(output).toString(), `tool${shim}/tool\n`);
|
|
81
|
+
assert.equal(existsSync(marker), false, 'the contributed bash must never be crouter\'s interpreter');
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
rmSync(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
@@ -363,8 +363,8 @@ test('turn_end still clears a daemon→node wedge fault (existing behavior, unch
|
|
|
363
363
|
pi.fire('turn_end', turnEndEvent('turn completed'), {});
|
|
364
364
|
assert.equal(readFault('leaf'), null, 'the daemon→node wedge fault is still cleared at turn_end');
|
|
365
365
|
});
|
|
366
|
-
test('a successful final or human
|
|
367
|
-
for (const [signal, shutsDown] of [['pushedFinal', true], ['
|
|
366
|
+
test('a successful final or requested human reply suppresses the stop reprompt without a canvas read', async () => {
|
|
367
|
+
for (const [signal, shutsDown] of [['pushedFinal', true], ['requestedHumanReply', false]]) {
|
|
368
368
|
process.env['CRTR_NODE_ID'] = 'signal-only';
|
|
369
369
|
markStopSignal('signal-only', signal);
|
|
370
370
|
const pi = makeFakePi();
|
|
@@ -389,12 +389,12 @@ test('an in-run retry (agent_start with no new input) does not erase this run\'s
|
|
|
389
389
|
const ctx = { shutdown: () => { } };
|
|
390
390
|
pi.fire('input', { type: 'input' }, ctx);
|
|
391
391
|
pi.fire('agent_start', { type: 'agent_start' }, ctx);
|
|
392
|
-
markStopSignal('leaf', '
|
|
392
|
+
markStopSignal('leaf', 'requestedHumanReply'); // the node ran `crtr human send` mid-run
|
|
393
393
|
pi.fire('agent_start', { type: 'agent_start' }, ctx); // pi auto-retried after a provider error
|
|
394
394
|
await settle(pi, stopEvent('waiting on the human now'), ctx);
|
|
395
395
|
assert.equal(pi.injected.length, 0, 'the ask survives the in-run retry — no stall reprompt');
|
|
396
396
|
// ...and the NEXT user-directed run does start clean.
|
|
397
|
-
markStopSignal('leaf', '
|
|
397
|
+
markStopSignal('leaf', 'requestedHumanReply');
|
|
398
398
|
pi.fire('input', { type: 'input' }, ctx);
|
|
399
399
|
await settle(pi, stopEvent('I think I am basically done here'), ctx);
|
|
400
400
|
assert.equal(pi.injected.length, 1, 'a new input discards a leftover marker from a prior run');
|
|
@@ -29,6 +29,7 @@ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmS
|
|
|
29
29
|
import { homedir } from 'node:os';
|
|
30
30
|
import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose } from '../core/bash-jobs.js';
|
|
31
31
|
import { readConfig } from '../core/config.js';
|
|
32
|
+
import { resolveAdmittedHostCommand } from '../core/runtime/spawn-env.js';
|
|
32
33
|
import { Type } from 'typebox';
|
|
33
34
|
import { createBashToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
@@ -52,19 +53,31 @@ function resolveValveMs() {
|
|
|
52
53
|
// Positional argv (never interpolated into the script — the command text
|
|
53
54
|
// lives only in cmd.sh):
|
|
54
55
|
// $1 cmd.sh $2 job.log $3 job.exit $4 job.run
|
|
55
|
-
// $5 job.done $6 job.bg $7 jobId $8 nodeId
|
|
56
|
+
// $5 job.done $6 job.bg $7 jobId $8 nodeId $9 shell path
|
|
56
57
|
//
|
|
57
58
|
// `--` before the paths is a dummy $0 (a standard `bash -c script -- args`
|
|
58
59
|
// idiom) so the real payload lands at $1 onward.
|
|
59
60
|
// ---------------------------------------------------------------------------
|
|
61
|
+
// The elapsed rendering mirrors formatBashElapsed() in core/bash-jobs.ts
|
|
62
|
+
// (`12s` / `3m 40s` / `1h 04m`) so the completion message reads the same as the
|
|
63
|
+
// handoff notice; it measures TOTAL runtime, foreground portion included.
|
|
60
64
|
const SUPERVISOR_SCRIPT = [
|
|
61
|
-
'
|
|
65
|
+
'SECONDS=0',
|
|
66
|
+
'"${9}" "$1" > "$2" 2>&1',
|
|
62
67
|
'ec=$?',
|
|
68
|
+
'd=$SECONDS',
|
|
63
69
|
'echo "$ec" > "$3"',
|
|
70
|
+
'if [ "$d" -lt 60 ]; then',
|
|
71
|
+
' el="${d}s"',
|
|
72
|
+
'elif [ "$d" -lt 3600 ]; then',
|
|
73
|
+
' el="$((d / 60))m $(printf "%02d" "$((d % 60))")s"',
|
|
74
|
+
'else',
|
|
75
|
+
' el="$((d / 3600))h $(printf "%02d" "$((d / 60 % 60))")m"',
|
|
76
|
+
'fi',
|
|
64
77
|
'if mv "$4" "$5" 2>/dev/null; then',
|
|
65
78
|
' :',
|
|
66
79
|
'elif [ -f "$6" ]; then',
|
|
67
|
-
' crtr node message send "Background bash job $7 finished (exit $ec). Log: $2 Command: $1" --to "$8" --tier urgent || true',
|
|
80
|
+
' crtr node message send "Background bash job $7 finished (exit $ec) after $el. Log: $2 Command: $1" --to "$8" --tier urgent || true',
|
|
68
81
|
'fi',
|
|
69
82
|
].join('\n');
|
|
70
83
|
function makeJobPaths(contextDir) {
|
|
@@ -183,7 +196,7 @@ function longSleepRefusal(command) {
|
|
|
183
196
|
`Holding a live window on a clock is never how you wait here. Instead:\n` +
|
|
184
197
|
` - schedule the wake and end your turn: crtr cron -h (one-shot at a time, or recurring)\n` +
|
|
185
198
|
` - race an inbox message against a deadline: crtr node wait deadline -h\n` +
|
|
186
|
-
` - waiting on a child, a
|
|
199
|
+
` - waiting on a child, a reply from the user, or any spine event: just stop — the runtime wakes you.\n` +
|
|
187
200
|
`If you truly need a short pause inline, sleep ${MAX_INLINE_SLEEP_SECONDS}s or less.`);
|
|
188
201
|
}
|
|
189
202
|
/** A node can legitimately remove its own merged worktree. Never recreate that
|
|
@@ -296,7 +309,8 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
|
|
|
296
309
|
const execution = resolveExecutionCwd(cwd);
|
|
297
310
|
if (execution.warning)
|
|
298
311
|
onData(Buffer.from(execution.warning));
|
|
299
|
-
const
|
|
312
|
+
const shellPath = resolveAdmittedHostCommand('bash', env ?? process.env, execution.cwd);
|
|
313
|
+
const child = spawn(shellPath, ['-c', SUPERVISOR_SCRIPT, '--', paths.cmdSh, paths.jobLog, paths.jobExit, paths.jobRun, paths.jobDone, paths.jobBg, paths.jobId, nodeId, shellPath], {
|
|
300
314
|
cwd: execution.cwd,
|
|
301
315
|
detached: true,
|
|
302
316
|
stdio: 'ignore',
|
|
@@ -306,7 +306,7 @@ class CanvasInboxWatcher {
|
|
|
306
306
|
}
|
|
307
307
|
/** True when delivery must wait a tick for a report-node projection refresh.
|
|
308
308
|
* The projection captured at session_start omits every node created since —
|
|
309
|
-
* each `crtr human
|
|
309
|
+
* each reply-bearing `crtr human send` bridge and each child this node spawns — so their
|
|
310
310
|
* reports would render as bare refs instead of inlined bodies. One refresh
|
|
311
311
|
* per unknown node id keeps a genuinely missing node from stalling delivery. */
|
|
312
312
|
refreshForUnknownReportRefs() {
|
|
@@ -19,6 +19,10 @@ interface CustomMessageLike {
|
|
|
19
19
|
reviewId: string;
|
|
20
20
|
originNodeId: string;
|
|
21
21
|
targetFile: string;
|
|
22
|
+
/** Which binding flavor authored this marker — lets a display surface
|
|
23
|
+
* (e.g. Northlight's native boundary) pick its copy without parsing the
|
|
24
|
+
* agent-facing prose. Absent on markers written before this field. */
|
|
25
|
+
kind: 'review' | 'page_feedback';
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
interface RenderedMessageLike {
|
|
@@ -55,19 +55,28 @@ export function registerCanvasReviewBoundary(pi) {
|
|
|
55
55
|
const alreadyMarked = scan.some((entry) => isReviewBoundary(entry, node.review_binding?.review_id));
|
|
56
56
|
if (alreadyMarked)
|
|
57
57
|
return;
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
const binding = node.review_binding;
|
|
59
|
+
const { review_id: reviewId, origin_node_id: originNodeId, target_file: targetFile } = binding;
|
|
60
|
+
const content = binding.kind === 'page_feedback'
|
|
61
|
+
? [
|
|
62
|
+
`This conversation began from feedback on page ${reviewId}.`,
|
|
63
|
+
`Page source: ${targetFile}`,
|
|
64
|
+
`Origin node: ${originNodeId}`,
|
|
65
|
+
'',
|
|
66
|
+
"The person's visible transcript starts here. Everything above this marker is the origin node's work — inherited context they have not seen.",
|
|
67
|
+
].join('\n')
|
|
68
|
+
: [
|
|
69
|
+
`Reviewed file: ${targetFile}`,
|
|
70
|
+
`Review ID: ${reviewId}`,
|
|
71
|
+
`Origin node: ${originNodeId}`,
|
|
72
|
+
'',
|
|
73
|
+
"The person's visible transcript starts here. Everything above this marker is inherited context they have not seen.",
|
|
74
|
+
].join('\n');
|
|
66
75
|
pi.sendMessage({
|
|
67
76
|
customType: REVIEW_BOUNDARY_CUSTOM_TYPE,
|
|
68
77
|
content,
|
|
69
78
|
display: true,
|
|
70
|
-
details: { reviewId, originNodeId, targetFile },
|
|
79
|
+
details: { reviewId, originNodeId, targetFile, kind: binding.kind ?? 'review' },
|
|
71
80
|
});
|
|
72
81
|
});
|
|
73
82
|
}
|
|
@@ -407,7 +407,7 @@ export function registerCanvasStophook(pi) {
|
|
|
407
407
|
// input — a new user-directed run is beginning. This is the ONLY place stop
|
|
408
408
|
// signals are cleared: `agent_start` re-fires for pi's automatic retries and
|
|
409
409
|
// post-compaction continuations within the SAME run, so clearing there would
|
|
410
|
-
// erase the `crtr human
|
|
410
|
+
// erase the `crtr human send` / `crtr push final` marker written earlier in the
|
|
411
411
|
// run and make the eventual settlement fall back to a canvas read (exactly the
|
|
412
412
|
// read the marker exists to avoid). One run's markers survive until the one
|
|
413
413
|
// `agent_settled` consumes them; a leftover from an errored or interrupted run
|
|
@@ -508,7 +508,7 @@ export function registerCanvasStophook(pi) {
|
|
|
508
508
|
stopReason: 'quit',
|
|
509
509
|
backgroundJobsRunning: backgroundJobsRunning(),
|
|
510
510
|
pushedFinal: false,
|
|
511
|
-
|
|
511
|
+
requestedHumanReply: false,
|
|
512
512
|
});
|
|
513
513
|
enactSettleDirective(directive, ctx);
|
|
514
514
|
}
|
|
@@ -637,14 +637,14 @@ export function registerCanvasStophook(pi) {
|
|
|
637
637
|
const last = lastAssistantMessage(messages);
|
|
638
638
|
const stopReason = typeof last?.stopReason === 'string' ? last.stopReason : '';
|
|
639
639
|
const stopSignals = event === null
|
|
640
|
-
? { pushedFinal: false,
|
|
640
|
+
? { pushedFinal: false, requestedHumanReply: false }
|
|
641
641
|
: consumeStopSignals(nodeId);
|
|
642
642
|
try {
|
|
643
643
|
const directive = await settleBroker(nodeId, {
|
|
644
644
|
stopReason,
|
|
645
645
|
backgroundJobsRunning: backgroundJobsRunning(),
|
|
646
646
|
pushedFinal: stopSignals.pushedFinal,
|
|
647
|
-
|
|
647
|
+
requestedHumanReply: stopSignals.requestedHumanReply,
|
|
648
648
|
});
|
|
649
649
|
enactSettleDirective(directive, ctx);
|
|
650
650
|
}
|