@crouton-kit/crouter 0.3.39 → 0.3.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-root.js +1 -0
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +472 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +284 -76
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/strip-skills-docs.ts +31 -24
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +52 -15
- package/dist/clients/attach/attach-cmd.js +706 -733
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/web/server.js +5 -28
- package/dist/clients/web/web-cmd.js +1 -1
- package/dist/commands/__tests__/revive-now-gate.test.js +60 -0
- package/dist/commands/canvas-rebuild-index.js +5 -5
- package/dist/commands/node-snapshot.js +9 -1
- package/dist/commands/node.js +37 -17
- package/dist/commands/push.js +8 -0
- package/dist/commands/revive.d.ts +10 -0
- package/dist/commands/revive.js +28 -10
- package/dist/commands/sys/__tests__/setup-core.test.js +19 -0
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/setup-core.js +3 -2
- package/dist/commands/sys/setup.js +1 -1
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +94 -0
- package/dist/core/__tests__/boot.test.js +4 -4
- package/dist/core/__tests__/canvas.test.js +19 -7
- package/dist/core/__tests__/child-followup.test.js +15 -5
- package/dist/core/__tests__/daemon-boot.test.js +6 -1
- package/dist/core/__tests__/daemon-wedge.test.js +18 -1
- package/dist/core/__tests__/fault-classifier.test.js +30 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +13 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +20 -0
- package/dist/core/__tests__/full/spike-harness.test.js +8 -5
- package/dist/core/__tests__/grace-clock.test.js +18 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +465 -0
- package/dist/core/__tests__/prune-to-limit.test.js +14 -0
- package/dist/core/__tests__/review-model-floor.test.js +32 -0
- package/dist/core/__tests__/revive.test.js +18 -4
- package/dist/core/__tests__/session-cycles.test.js +77 -0
- package/dist/core/__tests__/worktree.test.js +85 -0
- package/dist/core/canvas/boot.js +12 -7
- package/dist/core/canvas/canvas.d.ts +27 -8
- package/dist/core/canvas/canvas.js +54 -26
- package/dist/core/canvas/db.js +14 -0
- package/dist/core/canvas/history.js +1 -0
- package/dist/core/canvas/paths.d.ts +10 -9
- package/dist/core/canvas/paths.js +10 -9
- package/dist/core/canvas/pid.d.ts +155 -1
- package/dist/core/canvas/pid.js +306 -1
- package/dist/core/canvas/status-glyph.d.ts +7 -0
- package/dist/core/canvas/status-glyph.js +10 -1
- package/dist/core/canvas/types.d.ts +34 -0
- package/dist/core/fault-classifier.js +5 -1
- package/dist/core/runtime/bearings.js +4 -0
- package/dist/core/runtime/branded-host.d.ts +7 -0
- package/dist/core/runtime/branded-host.js +44 -17
- package/dist/core/runtime/broker-sdk.js +28 -68
- package/dist/core/runtime/broker.js +35 -4
- package/dist/core/runtime/host.d.ts +3 -3
- package/dist/core/runtime/host.js +148 -33
- package/dist/core/runtime/launch.d.ts +15 -15
- package/dist/core/runtime/launch.js +58 -4
- package/dist/core/runtime/naming.js +3 -2
- package/dist/core/runtime/nodes.d.ts +3 -1
- package/dist/core/runtime/nodes.js +1 -0
- package/dist/core/runtime/pi-cli.d.ts +2 -0
- package/dist/core/runtime/pi-cli.js +51 -0
- package/dist/core/runtime/placement.d.ts +1 -1
- package/dist/core/runtime/placement.js +5 -2
- package/dist/core/runtime/recap.js +2 -1
- package/dist/core/runtime/revive.d.ts +21 -15
- package/dist/core/runtime/revive.js +87 -52
- package/dist/core/runtime/session-cycles.d.ts +30 -0
- package/dist/core/runtime/session-cycles.js +77 -0
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +152 -100
- package/dist/core/runtime/tmux.d.ts +7 -10
- package/dist/core/runtime/tmux.js +9 -11
- package/dist/core/worktree.d.ts +35 -0
- package/dist/core/worktree.js +158 -0
- package/dist/daemon/crtrd.d.ts +17 -8
- package/dist/daemon/crtrd.js +191 -40
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +1 -0
- package/dist/pi-extensions/canvas-context-intro.js +34 -23
- package/dist/pi-extensions/canvas-stophook.js +11 -5
- package/dist/web-client/assets/{index-CbO8L0mN.js → index-B00YpRQ1.js} +20 -20
- package/dist/web-client/assets/index-DrkcvANq.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/docs/compat/hearth-crtr-v1.md +191 -0
- package/docs/public-api.md +75 -0
- package/package.json +3 -4
- package/dist/core/__tests__/hearth-bootstrap.test.js +0 -136
- package/dist/core/hearth/__tests__/model-auth-guest.test.js +0 -151
- package/dist/core/hearth/config.d.ts +0 -3
- package/dist/core/hearth/config.js +0 -108
- package/dist/core/hearth/guest-env.d.ts +0 -9
- package/dist/core/hearth/guest-env.js +0 -27
- package/dist/core/hearth/index.d.ts +0 -4
- package/dist/core/hearth/index.js +0 -4
- package/dist/core/hearth/model-auth-guest.d.ts +0 -8
- package/dist/core/hearth/model-auth-guest.js +0 -430
- package/dist/core/hearth/provider.d.ts +0 -36
- package/dist/core/hearth/provider.js +0 -10
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +0 -254
- package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +0 -12
- package/dist/core/hearth/providers/blaxel-bootstrap.js +0 -147
- package/dist/core/hearth/providers/blaxel-home.d.ts +0 -54
- package/dist/core/hearth/providers/blaxel-home.js +0 -386
- package/dist/core/hearth/providers/blaxel.d.ts +0 -36
- package/dist/core/hearth/providers/blaxel.js +0 -364
- package/dist/core/hearth/providers/types.d.ts +0 -93
- package/dist/core/hearth/types.d.ts +0 -155
- package/dist/hearth/control-plane/__tests__/error-serialization.test.js +0 -29
- package/dist/hearth/control-plane/__tests__/node-message.test.js +0 -60
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +0 -44
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/rate-limit-recurrence.test.js +0 -49
- package/dist/hearth/control-plane/__tests__/relay-security.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/relay-security.test.js +0 -314
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/scheduler-scan-loop.test.js +0 -133
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/trigger-delivery.test.js +0 -170
- package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/wake-roll.test.js +0 -230
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.d.ts +0 -1
- package/dist/hearth/control-plane/__tests__/webhook-ingress.test.js +0 -167
- package/dist/hearth/control-plane/config.d.ts +0 -21
- package/dist/hearth/control-plane/config.js +0 -77
- package/dist/hearth/control-plane/db.d.ts +0 -30
- package/dist/hearth/control-plane/db.js +0 -561
- package/dist/hearth/control-plane/hearth-target.d.ts +0 -23
- package/dist/hearth/control-plane/hearth-target.js +0 -68
- package/dist/hearth/control-plane/ingress/rate-limit.d.ts +0 -24
- package/dist/hearth/control-plane/ingress/rate-limit.js +0 -100
- package/dist/hearth/control-plane/ingress/route-store.d.ts +0 -31
- package/dist/hearth/control-plane/ingress/route-store.js +0 -61
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.d.ts +0 -41
- package/dist/hearth/control-plane/ingress/webhook-delivery-store.js +0 -69
- package/dist/hearth/control-plane/ingress/webhook-route.d.ts +0 -55
- package/dist/hearth/control-plane/ingress/webhook-route.js +0 -285
- package/dist/hearth/control-plane/main.d.ts +0 -1
- package/dist/hearth/control-plane/main.js +0 -88
- package/dist/hearth/control-plane/node-message.d.ts +0 -31
- package/dist/hearth/control-plane/node-message.js +0 -98
- package/dist/hearth/control-plane/register.d.ts +0 -15
- package/dist/hearth/control-plane/register.js +0 -34
- package/dist/hearth/control-plane/registry.d.ts +0 -22
- package/dist/hearth/control-plane/registry.js +0 -168
- package/dist/hearth/control-plane/relay.d.ts +0 -44
- package/dist/hearth/control-plane/relay.js +0 -711
- package/dist/hearth/control-plane/scheduler/fire-store.d.ts +0 -36
- package/dist/hearth/control-plane/scheduler/fire-store.js +0 -73
- package/dist/hearth/control-plane/scheduler/recurrence.d.ts +0 -7
- package/dist/hearth/control-plane/scheduler/recurrence.js +0 -58
- package/dist/hearth/control-plane/scheduler/scan-loop.d.ts +0 -38
- package/dist/hearth/control-plane/scheduler/scan-loop.js +0 -138
- package/dist/hearth/control-plane/scheduler/schedule-store.d.ts +0 -32
- package/dist/hearth/control-plane/scheduler/schedule-store.js +0 -66
- package/dist/hearth/control-plane/secrets.d.ts +0 -31
- package/dist/hearth/control-plane/secrets.js +0 -134
- package/dist/hearth/control-plane/server.d.ts +0 -27
- package/dist/hearth/control-plane/server.js +0 -482
- package/dist/hearth/control-plane/serving.d.ts +0 -15
- package/dist/hearth/control-plane/serving.js +0 -106
- package/dist/hearth/control-plane/session.d.ts +0 -68
- package/dist/hearth/control-plane/session.js +0 -273
- package/dist/hearth/control-plane/triggers/acl.d.ts +0 -14
- package/dist/hearth/control-plane/triggers/acl.js +0 -52
- package/dist/hearth/control-plane/triggers/audit-store.d.ts +0 -38
- package/dist/hearth/control-plane/triggers/audit-store.js +0 -79
- package/dist/hearth/control-plane/triggers/deliver.d.ts +0 -43
- package/dist/hearth/control-plane/triggers/deliver.js +0 -76
- package/dist/hearth/control-plane/triggers/envelope.d.ts +0 -29
- package/dist/hearth/control-plane/triggers/envelope.js +0 -38
- package/dist/hearth/control-plane/types.d.ts +0 -86
- package/dist/hearth/control-plane/types.js +0 -1
- package/dist/hearth/control-plane/wake.d.ts +0 -86
- package/dist/hearth/control-plane/wake.js +0 -550
- package/dist/web-client/assets/index-DwO46Cs5.css +0 -2
- /package/dist/{core/__tests__/hearth-bootstrap.test.d.ts → commands/__tests__/revive-now-gate.test.d.ts} +0 -0
- /package/dist/{core/hearth/__tests__/model-auth-guest.test.d.ts → commands/sys/__tests__/setup-core.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/__tests__/sweep-and-release.test.d.ts → __tests__/fault-classifier.test.d.ts} +0 -0
- /package/dist/core/{hearth/providers/types.js → __tests__/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/{hearth/types.js → __tests__/review-model-floor.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/error-serialization.test.d.ts → core/__tests__/session-cycles.test.d.ts} +0 -0
- /package/dist/{hearth/control-plane/__tests__/node-message.test.d.ts → core/__tests__/worktree.test.d.ts} +0 -0
|
@@ -24,6 +24,7 @@ import { Container, Loader, Spacer, Text } from '@earendil-works/pi-tui';
|
|
|
24
24
|
import { AssistantMessageComponent, BashExecutionComponent, BranchSummaryMessageComponent, CompactionSummaryMessageComponent, CustomMessageComponent, getMarkdownTheme, parseSkillBlock, SkillInvocationMessageComponent, ToolExecutionComponent, UserMessageComponent, } from '@earendil-works/pi-coding-agent';
|
|
25
25
|
import { ContextMessageComponent } from './context-message.js';
|
|
26
26
|
import { CRTR_OUTPUT_CUSTOM_TYPE, CrtrOutputMessageComponent, createCrtrBashToolDefinition, } from './crtr-output-render.js';
|
|
27
|
+
import { CRTR_CYCLE_DIVIDER_CUSTOM_TYPE } from '../../core/runtime/session-cycles.js';
|
|
27
28
|
/** customType stamped on the injected `<crtr-context>` bearings block (mirrors
|
|
28
29
|
* CONTEXT_INTRO_CUSTOM_TYPE in canvas-context-intro.ts — kept as a local literal
|
|
29
30
|
* so the viewer never imports the in-process extension). */
|
|
@@ -438,6 +439,16 @@ export class ChatView {
|
|
|
438
439
|
this.append(new CrtrOutputMessageComponent(this.userMessageText(message), this.toolOutputExpanded, this.crtrOutputTheme()));
|
|
439
440
|
break;
|
|
440
441
|
}
|
|
442
|
+
// A refresh-yield cycle boundary (session-cycles.ts, synthetic — never
|
|
443
|
+
// persisted): render as a dim rule rather than the generic custom-
|
|
444
|
+
// message card, so scrolling through a multi-cycle node's full history
|
|
445
|
+
// reads as one conversation with clear seams between cycles.
|
|
446
|
+
if (message.customType === CRTR_CYCLE_DIVIDER_CUSTOM_TYPE) {
|
|
447
|
+
this.append(new Spacer(1));
|
|
448
|
+
this.append(new Text(this.dimStyle(`── ${this.userMessageText(message)} ──`), 1, 0));
|
|
449
|
+
this.append(new Spacer(1));
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
441
452
|
const component = new CustomMessageComponent(message, undefined, getMarkdownTheme());
|
|
442
453
|
component.setExpanded(this.toolOutputExpanded);
|
|
443
454
|
this.append(component);
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// • POST /__crtr/source → the source + command bridge: decode a SourceRequest
|
|
8
8
|
// and run it through the LOCAL transport (exec/file/
|
|
9
9
|
// http in this cwd). This serves views' READS and the
|
|
10
|
-
// command WRITES (`crtr …` subprocesses
|
|
11
|
-
//
|
|
10
|
+
// command WRITES (`crtr …` subprocesses via shelled-out
|
|
11
|
+
// CLI). The bridge is the ONLY sanctioned writer to the
|
|
12
|
+
// canvas; relay clients NEVER launch pi directly.
|
|
12
13
|
// • GET /__crtr/events → the SSE change-invalidation lane (events.ts).
|
|
13
14
|
// • WS /node/<id> → open that node's ALREADY-running broker `view.sock`
|
|
14
15
|
// and relay frames VERBATIM both directions. The relay
|
|
@@ -43,12 +44,6 @@ import { runSourceRequest } from '../../core/view/bridge.js';
|
|
|
43
44
|
import { startEventHub } from './events.js';
|
|
44
45
|
import { createDevServer } from './dev-server.js';
|
|
45
46
|
import { SOURCE_CACHE_TTL_MS, isCoalescableSourceRequest } from './source-cache.js';
|
|
46
|
-
// ── Hearth product seam: model-auth lane (P3.3b, Silas-approved exception) ──
|
|
47
|
-
// This is the ONLY Hearth-aware import in this file. All OAuth/status/start/
|
|
48
|
-
// finish product logic lives in core/hearth/model-auth-guest.ts; this module
|
|
49
|
-
// provides only routing + relay-token enforcement for the lane (see the route
|
|
50
|
-
// block in startWebServer below).
|
|
51
|
-
import { handleHearthModelAuthRequest } from '../../core/hearth/model-auth-guest.js';
|
|
52
47
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
53
48
|
/** Candidate dirs to serve the shell SPA bundle from, in priority order: an
|
|
54
49
|
* explicit override, the compiled-module-relative `dist/web-client/` (what
|
|
@@ -381,8 +376,8 @@ export async function startWebServer(opts) {
|
|
|
381
376
|
});
|
|
382
377
|
// The SSE change lane — one hub, fanned to every connected EventSource.
|
|
383
378
|
const eventHub = startEventHub();
|
|
384
|
-
// Same-origin allowlist: tokenless localhost pages
|
|
385
|
-
// while a valid relay token can authorize remote
|
|
379
|
+
// Same-origin allowlist: tokenless localhost pages are implicitly allowed,
|
|
380
|
+
// while a valid relay token can authorize remote/guest route origins that would
|
|
386
381
|
// otherwise be rejected by originAllowed().
|
|
387
382
|
let allowedOrigins = new Set();
|
|
388
383
|
const originAllowed = (origin) => {
|
|
@@ -447,24 +442,6 @@ export async function startWebServer(opts) {
|
|
|
447
442
|
eventHub.addClient(res);
|
|
448
443
|
return;
|
|
449
444
|
}
|
|
450
|
-
// ── Hearth product seam: model-auth lane ──
|
|
451
|
-
// Isolated per the P3.3a/P3.3b approved exception. ALL Hearth/OAuth logic
|
|
452
|
-
// lives in core/hearth/model-auth-guest.ts; this block is ONLY routing +
|
|
453
|
-
// the relay-token gate. CRITICAL: this lane uses the STRICT token-only
|
|
454
|
-
// predicate requestTokenMatches(req, token), never requestAllowed() —
|
|
455
|
-
// requestAllowed() also accepts a same-origin loopback request with NO
|
|
456
|
-
// token at all, which would make this lane tokenless on local/dev binds.
|
|
457
|
-
// Every /__hearth/model-auth/* request fails closed (401) without an
|
|
458
|
-
// exact relay-token match, even on loopback.
|
|
459
|
-
if (pathname.startsWith('/__hearth/model-auth/anthropic/')) {
|
|
460
|
-
if (!requestTokenMatches(req, token)) {
|
|
461
|
-
res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8' });
|
|
462
|
-
res.end('relay token required\n');
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
void handleHearthModelAuthRequest(req, res);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
445
|
// --dev: Vite middleware owns all remaining asset/HTML serving (incl. its
|
|
469
446
|
// own SPA fallback). It is mounted AFTER the bridge + SSE checks above, so
|
|
470
447
|
// those never fall through to Vite.
|
|
@@ -86,7 +86,7 @@ const webServeLeaf = defineLeaf({
|
|
|
86
86
|
outputKind: 'object',
|
|
87
87
|
effects: [
|
|
88
88
|
'Binds a long-running HTTP+WebSocket server on the requested host and port and keeps serving until interrupted (ctrl+c / SIGTERM). Safe to run non-interactively (daemon-like): it prints the URL and serves.',
|
|
89
|
-
'Serves the shell SPA (static dist/web-client/, or Vite middleware under --dev), runs the POST /__crtr/source bridge (exec/file/http in this cwd), streams the GET /__crtr/events SSE change lane, and on a browser ws://HOST:<port>/node/<id> connection opens that node\'s running broker view.sock and relays frames VERBATIM both directions. Non-loopback binds require a token so the remote
|
|
89
|
+
'Serves the shell SPA (static dist/web-client/, or Vite middleware under --dev), runs the POST /__crtr/source bridge (exec/file/http in this cwd), streams the GET /__crtr/events SSE change lane, and on a browser ws://HOST:<port>/node/<id> connection opens that node\'s running broker view.sock and relays frames VERBATIM both directions. Non-loopback binds require a token so the remote route is not an unauthenticated command surface.',
|
|
90
90
|
'The relay NEVER spawns pi and NEVER writes any session — it only opens a socket to an already-running broker; if the broker is not running, the browser connection is closed with a clear reason. All graph mutations go through `crtr` subprocesses the bridge runs.',
|
|
91
91
|
],
|
|
92
92
|
},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Regression for issue #115: `crtr canvas revive --now` rejected exactly the
|
|
2
|
+
// provider-fault nodes the canvas dashboard flags as ⚠ "needs you"
|
|
3
|
+
// (status-glyph.ts `faultDispositionText`), because the old gate required
|
|
4
|
+
// `retry.disposition === 'auto'` — the one disposition "needs you" NEVER is.
|
|
5
|
+
// Run with: node --import tsx/esm --test 'src/commands/__tests__/revive-now-gate.test.ts'
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { isNowEligibleFault } from '../revive.js';
|
|
9
|
+
import { faultNeedsYou } from '../../core/canvas/status-glyph.js';
|
|
10
|
+
function fault(over) {
|
|
11
|
+
return {
|
|
12
|
+
link: 'pi→provider',
|
|
13
|
+
op: 'generation turn',
|
|
14
|
+
kind: 'overloaded',
|
|
15
|
+
retry: { disposition: 'auto', by: 'daemon' },
|
|
16
|
+
message: 'boom',
|
|
17
|
+
since: new Date().toISOString(),
|
|
18
|
+
logPath: '/tmp/fault.log',
|
|
19
|
+
...over,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
test('null fault is never --now eligible', () => {
|
|
23
|
+
assert.equal(isNowEligibleFault(null), false);
|
|
24
|
+
});
|
|
25
|
+
test('daemon-owned auto retry is eligible (kick it early)', () => {
|
|
26
|
+
assert.equal(isNowEligibleFault(fault({ retry: { disposition: 'auto', by: 'daemon' } })), true);
|
|
27
|
+
});
|
|
28
|
+
test('client-owned auto retry (redialing) is refused — already being handled', () => {
|
|
29
|
+
assert.equal(isNowEligibleFault(fault({ retry: { disposition: 'auto', by: 'client' } })), false);
|
|
30
|
+
});
|
|
31
|
+
// The bug: exhausted-retry / immediate-fatal pi→provider faults render "needs
|
|
32
|
+
// you" on the dashboard (faultNeedsYou === true) but the old gate rejected
|
|
33
|
+
// them outright — no sanctioned recovery verb existed for exactly the nodes
|
|
34
|
+
// the dashboard told the user to attend to.
|
|
35
|
+
test('fatal pi→provider fault ("needs you") is now eligible', () => {
|
|
36
|
+
const f = fault({ retry: { disposition: 'fatal' } });
|
|
37
|
+
assert.equal(faultNeedsYou(f), true);
|
|
38
|
+
assert.equal(isNowEligibleFault(f), true);
|
|
39
|
+
});
|
|
40
|
+
test('manual pi→provider fault ("needs you") is eligible', () => {
|
|
41
|
+
const f = fault({ retry: { disposition: 'manual' } });
|
|
42
|
+
assert.equal(faultNeedsYou(f), true);
|
|
43
|
+
assert.equal(isNowEligibleFault(f), true);
|
|
44
|
+
});
|
|
45
|
+
test('a non-pi→provider fault (e.g. a daemon→node wedge) is refused — --now cannot clear it', () => {
|
|
46
|
+
const f = fault({ link: 'daemon→node', kind: 'wedged', retry: { disposition: 'manual' } });
|
|
47
|
+
assert.equal(isNowEligibleFault(f), false);
|
|
48
|
+
});
|
|
49
|
+
test('faultNeedsYou matches the gate for every disposition (no drift between dashboard and --now)', () => {
|
|
50
|
+
for (const disposition of ['auto', 'manual', 'fatal']) {
|
|
51
|
+
const f = fault({ retry: { disposition, by: 'daemon' } });
|
|
52
|
+
if (disposition === 'auto') {
|
|
53
|
+
assert.equal(faultNeedsYou(f), false);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
assert.equal(faultNeedsYou(f), true);
|
|
57
|
+
assert.equal(isNowEligibleFault(f), true);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// `crtr canvas rebuild-index` — re-derive the node-table index from on-disk metas.
|
|
2
2
|
//
|
|
3
3
|
// canvas.db's node rows are a derived index over `nodes/<id>/meta.json`. When the
|
|
4
|
-
// db is absent or stale relative to the metas — most concretely a
|
|
5
|
-
// recreate-from-template, where node data lives on
|
|
6
|
-
// canvas.db lives on local disk that the recreate wipes — this verb rebuilds
|
|
7
|
-
// rows (identity columns) from the surviving metas via `rebuildIndex()`, so a
|
|
4
|
+
// db is absent or stale relative to the metas — most concretely a remote/guest
|
|
5
|
+
// deployment's recreate-from-template, where node data lives on a durable Volume
|
|
6
|
+
// but canvas.db lives on local disk that the recreate wipes — this verb rebuilds
|
|
7
|
+
// the rows (identity columns) from the surviving metas via `rebuildIndex()`, so a
|
|
8
8
|
// subsequent `canvas revive` finds its node again.
|
|
9
9
|
//
|
|
10
10
|
// Fails loud if metas are present but the rebuild yields zero rows: a silent
|
|
@@ -29,7 +29,7 @@ function countOnDiskMetas() {
|
|
|
29
29
|
export const canvasRebuildIndexLeaf = defineLeaf({
|
|
30
30
|
name: 'rebuild-index',
|
|
31
31
|
description: 're-derive node rows in canvas.db from on-disk nodes/<id>/meta.json',
|
|
32
|
-
whenToUse: 'canvas.db is missing or stale relative to the on-disk node metas and you need its node rows re-derived — most concretely after a
|
|
32
|
+
whenToUse: 'canvas.db is missing or stale relative to the on-disk node metas and you need its node rows re-derived — most concretely after a remote/guest deployment\'s recreate-from-template, where node data survives on a durable Volume but canvas.db (local disk) was wiped, so a `canvas revive` would otherwise find no row. Identity columns are rebuilt from each meta; runtime columns (status/intent/pi_pid/window/tmux) take their quiescent defaults, and subscribes_to edges are left intact',
|
|
33
33
|
help: {
|
|
34
34
|
name: 'canvas rebuild-index',
|
|
35
35
|
summary: 're-derive canvas.db node rows from on-disk metas; fails loud if metas exist but yield no rows',
|
|
@@ -7,6 +7,7 @@ import { getNode } from '../core/canvas/index.js';
|
|
|
7
7
|
import { sessionPtrPath } from '../core/canvas/paths.js';
|
|
8
8
|
import { SessionManager } from '../core/runtime/broker-sdk.js';
|
|
9
9
|
import { BUILTIN_SLASH_COMMANDS } from '../core/runtime/pi-vendored.js';
|
|
10
|
+
import { cycleAwareMessages } from '../core/runtime/session-cycles.js';
|
|
10
11
|
function resolveSessionFile(nodeId, metaFile) {
|
|
11
12
|
if (typeof metaFile === 'string' && metaFile !== '' && existsSync(metaFile))
|
|
12
13
|
return metaFile;
|
|
@@ -95,7 +96,14 @@ export const nodeSnapshotLeaf = defineLeaf({
|
|
|
95
96
|
copyFileSync(sessionFile, copy);
|
|
96
97
|
const manager = SessionManager.open(copy);
|
|
97
98
|
const context = manager.buildSessionContext();
|
|
98
|
-
|
|
99
|
+
// Every prior refresh-yield cycle lives in this SAME `.jsonl` as a sibling
|
|
100
|
+
// branch (session-cycles.ts) — replay them ahead of the live cycle's
|
|
101
|
+
// context so a dormant node's snapshot reads as one continuous
|
|
102
|
+
// conversation, matching the broker's live welcome snapshot.
|
|
103
|
+
// Cast: the real SDK's `SessionEntry.data` is `unknown`, narrower than
|
|
104
|
+
// `CycleSessionManagerLike`'s structural read of the `crtr-cycle` marker's
|
|
105
|
+
// `{cycle, fromLeaf}` shape (see the matching cast in broker.ts).
|
|
106
|
+
const messages = cycleAwareMessages(manager);
|
|
99
107
|
const sessionId = manager.getSessionId();
|
|
100
108
|
const sessionName = manager.getSessionName();
|
|
101
109
|
const snapshot = {
|
package/dist/commands/node.js
CHANGED
|
@@ -23,6 +23,7 @@ import { readFaultAsync } from '../core/runtime/fault.js';
|
|
|
23
23
|
import { activeFaultForDisplay } from '../core/canvas/render.js';
|
|
24
24
|
import { detachToBackground, focus as placementFocus, windowAlive, windowOfPane, currentTmux, getPaneOption, renameWindow } from '../core/runtime/placement.js';
|
|
25
25
|
import { buildLaunchSpec, normalizeModel } from '../core/runtime/launch.js';
|
|
26
|
+
import { WorktreeError } from '../core/worktree.js';
|
|
26
27
|
import { closeNode } from '../core/runtime/close.js';
|
|
27
28
|
import { isBrokerLive, setModelLive, persistDormantModel } from '../core/runtime/model-swap.js';
|
|
28
29
|
import { appendInbox } from '../core/feed/inbox.js';
|
|
@@ -36,7 +37,7 @@ import { getNode, updateNode, listNodes, subscribe, unsubscribe, subscriptionsOf
|
|
|
36
37
|
// the result instead of this bloated one. Below the steering bands (130k+) on
|
|
37
38
|
// purpose — catch it at spawn, before the window is critically full.
|
|
38
39
|
const YIELD_NUDGE_THRESHOLD = 100_000;
|
|
39
|
-
const STD_CHILD_FOLLOW_UP = "Do not wait or
|
|
40
|
+
const STD_CHILD_FOLLOW_UP = "Do not wait, poll, or duplicate this child's assignment in your own context — there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn — the wake brings you back.";
|
|
40
41
|
/** Decision road sign for a managed (non-root) child. Normally STD_CHILD_FOLLOW_UP,
|
|
41
42
|
* but when the SPAWNER is an orchestrator whose context has already grown past
|
|
42
43
|
* YIELD_NUDGE_THRESHOLD, steer it to yield now and let its fresh revive handle
|
|
@@ -51,7 +52,12 @@ export function childFollowUp(spawnerId) {
|
|
|
51
52
|
if (ctxTokens === null || ctxTokens < YIELD_NUDGE_THRESHOLD)
|
|
52
53
|
return STD_CHILD_FOLLOW_UP;
|
|
53
54
|
const k = Math.round(ctxTokens / 1000);
|
|
54
|
-
return `Child spawned — you're auto-subscribed, so its finish wakes you on its own; never wait or
|
|
55
|
+
return `Child spawned — you're auto-subscribed, so its finish wakes you on its own; never wait, poll, or duplicate the child's assignment in your own context. But you're an orchestrator already carrying ~${k}k of context: rather than hold this window open for the result, checkpoint context/roadmap.md and \`crtr node yield\` now. Yielding ends this turn, and the child's completion then revives you fresh against your roadmap — let that clean revive absorb the result instead of this bloated context.`;
|
|
56
|
+
}
|
|
57
|
+
function mapWorktreeError(err) {
|
|
58
|
+
if (!(err instanceof WorktreeError))
|
|
59
|
+
return null;
|
|
60
|
+
return new InputError({ error: err.code, message: err.message, next: err.next });
|
|
55
61
|
}
|
|
56
62
|
/** Validate a `--kind` against the installed personas; throws a listing InputError. */
|
|
57
63
|
function assertKind(kind) {
|
|
@@ -120,7 +126,8 @@ const nodeNew = defineLeaf({
|
|
|
120
126
|
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: 'Dir the node is pinned to. Defaults to the caller cwd, resolved now — for a triggered spawn it must still exist at fire time or the spawn fails loud.' },
|
|
121
127
|
{ kind: 'flag', name: 'name', type: 'string', required: false, constraint: 'Display name (tmux window + resume picker). Defaults to the kind.' },
|
|
122
128
|
{ kind: 'flag', name: 'parent', type: 'string', required: false, constraint: 'Parent node id. Defaults to the calling node (CRTR_NODE_ID).' },
|
|
123
|
-
{ kind: 'flag', name: 'root', type: 'bool', required: false, constraint: 'Spawn an INDEPENDENT root instead of a managed child: no parent (top-level on the canvas), NO subscription back to you (you are NOT woken by it), resident lifecycle. It records spawned_by=you for provenance and is brought forefront so it can be driven directly. Use for a node you hand off and do not manage (e.g. a sub-orchestrator a human will discuss with).' },
|
|
129
|
+
{ kind: 'flag', name: 'root', type: 'bool', required: false, constraint: 'Spawn an INDEPENDENT root instead of a managed child: no parent (top-level on the canvas), NO subscription back to you (you are NOT woken by it), resident lifecycle. It records spawned_by=you for provenance and is brought forefront so it can be driven directly. Use for a node you hand off and do not manage (e.g. a sub-orchestrator a human will discuss with). Mutually exclusive with --worktree.' },
|
|
130
|
+
{ kind: 'flag', name: 'worktree', type: 'bool', required: false, constraint: 'Use when the child will COMMIT to the repo while you or other agents may also be editing it — the worktree isolates its work so nothing collides; skip it for read-only work or when the child is the sole writer. Creates a crouter-managed git worktree for the spawned child, keyed by the child node id, at `~/.crouter/canvas/worktrees/<node-id>/` on branch `crtr/<node-id>` based on `origin/main`; the child\'s cwd is pinned there, and it lands the work serially onto origin/main later with `crtr worktree close`. Mutually exclusive with --root.' },
|
|
124
131
|
{ kind: 'flag', name: 'fork-from', type: 'string', required: false, constraint: 'FORK the new node from an existing pi conversation instead of starting it fresh: pass a node id (forks from that node\'s session), an absolute session `.jsonl` path, or a partial pi session uuid. pi copies that whole history into a NEW session for the child (the source is untouched), then the prompt is delivered as the next message — i.e. the child wakes up as a continuation of that conversation. Use to branch exploratory work off a node that already built up the context you need, instead of re-deriving it. One-shot at birth: the fork resumes its own session thereafter.' },
|
|
125
132
|
{ kind: 'flag', name: 'model', type: 'enum', choices: ['ultra', 'strong', 'medium', 'light'], required: false, constraint: 'Override the model the node runs on, by capability TIER: ultra (frontier), strong (opus), medium (sonnet), light (haiku). Omit to use the kind\'s persona default (advisor=ultra, explore=light, most other builtins=strong). The override is durable — it survives revives and polymorphs (promote/demote).' },
|
|
126
133
|
{ kind: 'flag', name: 'profile', type: 'string', required: false, constraint: 'Select the profile this node runs under — an exact profile id or a unique manifest name (see the <profiles> list below, or `crtr profile list`). Omit to INHERIT the calling node\'s current profile (root has none, so an uninherited child has none either). --root does NOT reset this — it only means top-level, still inherits/uses the selected profile unless this overrides it.' },
|
|
@@ -143,12 +150,12 @@ const nodeNew = defineLeaf({
|
|
|
143
150
|
{ name: 'next_eval_at', type: 'string', required: false, constraint: 'The next predicate-evaluation instant (always "now" at arm time), present only for an armed --when trigger.' },
|
|
144
151
|
{ name: 'timeout_at', type: 'string', required: false, constraint: 'Absolute UTC wall-clock bound, present only for an armed --when trigger.' },
|
|
145
152
|
{ name: 'recur', type: 'string', required: false, constraint: 'Cadence display ("every 6h", cron, or eval cadence for --when), present only when --every was given.' },
|
|
146
|
-
{ name: 'follow_up', type: 'string', required: true, constraint: 'Decision road sign for the caller. Spawned now: the child runs independently and its finish wakes you on its own, so never wait or
|
|
153
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Decision road sign for the caller. Spawned now: the child runs independently and its finish wakes you on its own, so never wait, poll, or duplicate its assignment — either pick up other independent work now or end your turn (an orchestrator already deep in context is instead steered to `crtr node yield`). Armed: no node exists yet — inspect/cancel via `crtr node triggers list`/`cancel <id>`, then pick up other work or end your turn.' },
|
|
147
154
|
],
|
|
148
155
|
dynamicState: () => [kindsStateBlock(), profilesStateBlock()].join('\n'),
|
|
149
156
|
outputKind: 'object',
|
|
150
157
|
effects: [
|
|
151
|
-
'Spawning now: creates a node under ~/.crouter/canvas/nodes/<id>/ and indexes it in canvas.db. Default (managed child): parent auto-subscribes (active) and is woken on the child\'s pushes. With --root: no subscription — records a spawned_by edge for provenance only. Launches the node\'s engine as a detached broker (the only host); a managed child opens NO viewer, a --root opens one in your current tmux session.',
|
|
158
|
+
'Spawning now: creates a node under ~/.crouter/canvas/nodes/<id>/ and indexes it in canvas.db. Default (managed child): parent auto-subscribes (active) and is woken on the child\'s pushes. With --root: no subscription — records a spawned_by edge for provenance only. With --worktree: also creates a managed git worktree at ~/.crouter/canvas/worktrees/<node-id>/ on branch crtr/<node-id> based on origin/main; the child closes it later with `crtr worktree close` before finishing. Launches the node\'s engine as a detached broker (the only host); a managed child opens NO viewer, a --root opens one in your current tmux session.',
|
|
152
159
|
'Armed (--at/--every/--when): inserts one detached `new`/`node_birth` triggers row (node_id NULL, owner=you, parent/cwd resolved now); NO node and NO window exist until fire time. The daemon spawns from the stored recipe at fire time, re-deriving the launch spec live.',
|
|
153
160
|
],
|
|
154
161
|
},
|
|
@@ -163,6 +170,7 @@ const nodeNew = defineLeaf({
|
|
|
163
170
|
const name = input['name'];
|
|
164
171
|
const parent = input['parent'];
|
|
165
172
|
const root = input['root'] === true;
|
|
173
|
+
const worktree = input['worktree'] === true;
|
|
166
174
|
const forkFrom = input['forkFrom'];
|
|
167
175
|
const model = input['model'];
|
|
168
176
|
const profile = input['profile'];
|
|
@@ -183,19 +191,30 @@ const nodeNew = defineLeaf({
|
|
|
183
191
|
if (hasTz && !hasAt && !hasEvery) {
|
|
184
192
|
throw new InputError({ error: 'tz_needs_clock', message: '--tz has no effect without --at or --every to parse it against', field: 'tz', next: 'Drop --tz, or pass --at/--every.' });
|
|
185
193
|
}
|
|
194
|
+
if (root && worktree) {
|
|
195
|
+
throw new InputError({ error: 'worktree_root_conflict', message: '--worktree is mutually exclusive with --root', field: 'worktree', next: 'Drop either --root or --worktree.' });
|
|
196
|
+
}
|
|
186
197
|
if (!triggered) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
try {
|
|
199
|
+
const res = await spawnChild({ kind, mode, cwd, name, prompt, parent, root, worktree, forkFrom, model, profile });
|
|
200
|
+
return {
|
|
201
|
+
node_id: res.node.node_id,
|
|
202
|
+
name: res.node.name,
|
|
203
|
+
window: res.window ?? undefined,
|
|
204
|
+
session: res.session ?? undefined,
|
|
205
|
+
status: res.node.status,
|
|
206
|
+
armed: false,
|
|
207
|
+
follow_up: root
|
|
208
|
+
? "Independent root spawned — it is NOT under you. You are not subscribed, so its finish will NOT wake you and it does not hold you alive; it carries spawned_by=you for lineage only. Inside tmux it opened a viewer in your current session (the client switched to it) so it can be driven directly. Hand it off and move on — do not duplicate its assignment; you will not be notified of its progress."
|
|
209
|
+
: childFollowUp(parent ?? process.env['CRTR_NODE_ID']),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
const mapped = mapWorktreeError(err);
|
|
214
|
+
if (mapped !== null)
|
|
215
|
+
throw mapped;
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
199
218
|
}
|
|
200
219
|
if (hasWhen && hasAt) {
|
|
201
220
|
throw new InputError({ error: 'bad_when_combo', message: '--when cannot combine with --at', next: 'A trigger is either clock-driven (--at/--every) or predicate-driven (--when).' });
|
|
@@ -219,6 +238,7 @@ const nodeNew = defineLeaf({
|
|
|
219
238
|
parent: recipeParent,
|
|
220
239
|
...(name !== undefined ? { name } : {}),
|
|
221
240
|
...(root ? { root: true } : {}),
|
|
241
|
+
...(worktree ? { worktree: true } : {}),
|
|
222
242
|
...(forkFrom !== undefined ? { forkFrom } : {}),
|
|
223
243
|
...(model !== undefined ? { model } : {}),
|
|
224
244
|
...(profile !== undefined ? { profile } : {}),
|
package/dist/commands/push.js
CHANGED
|
@@ -12,6 +12,7 @@ import { defineBranch, defineLeaf } from '../core/command.js';
|
|
|
12
12
|
import { InputError, readStdinRaw } from '../core/io.js';
|
|
13
13
|
import { push } from '../core/feed/feed.js';
|
|
14
14
|
import { getNode, subscribersOf } from '../core/canvas/index.js';
|
|
15
|
+
import { hasOpenManagedWorktree } from '../core/worktree.js';
|
|
15
16
|
function requireCallerNode() {
|
|
16
17
|
const id = process.env['CRTR_NODE_ID'];
|
|
17
18
|
if (id === undefined || id.trim() === '') {
|
|
@@ -96,6 +97,13 @@ function makeTierLeaf(tier) {
|
|
|
96
97
|
if (node !== null && node.status !== 'active' && node.status !== 'idle') {
|
|
97
98
|
throw alreadyFinalizedError(node.status);
|
|
98
99
|
}
|
|
100
|
+
if (node !== null && hasOpenManagedWorktree(node)) {
|
|
101
|
+
throw new InputError({
|
|
102
|
+
error: 'open_managed_worktree',
|
|
103
|
+
message: 'This node still owns an open managed worktree — `push final` is blocked until it is closed.',
|
|
104
|
+
next: 'Commit the remaining work in the worktree, run `crtr worktree close`, then rerun `crtr push final`.',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
99
107
|
// A RESIDENT node with no subscribers is human-driven and has no one to
|
|
100
108
|
// submit a canonical result to: `push final` fans to subscribers, and a
|
|
101
109
|
// resident root conversation has none. Finishing it would shut its
|
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import type { LeafDef } from '../core/command.js';
|
|
2
|
+
import type { Fault } from '../core/runtime/fault.js';
|
|
3
|
+
/** --now eligibility for a live, non-busy fault: a daemon-owned AUTO retry
|
|
4
|
+
* (kick it early instead of waiting out the schedule), or any fault the
|
|
5
|
+
* canvas dashboard flags "needs you" (`faultNeedsYou`, status-glyph.ts) — the
|
|
6
|
+
* disposition that otherwise has NO sanctioned recovery verb (issue #115).
|
|
7
|
+
* Both are scoped to `pi→provider`: refuses a client-owned auto retry
|
|
8
|
+
* (redialing) already in flight, and refuses any OTHER link — notably a
|
|
9
|
+
* `daemon→node` wedge fault, which --now can't clear (SIGTERM doesn't touch
|
|
10
|
+
* the runaway subprocess; see `wedged-child-on-runaway-bash`). */
|
|
11
|
+
export declare function isNowEligibleFault(fault: Fault | null): boolean;
|
|
2
12
|
export declare const reviveLeaf: LeafDef;
|
package/dist/commands/revive.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// Bypasses the daemon: directly relaunches the broker engine for a node that is
|
|
4
4
|
// done, idle, or dead. Default behavior resumes the saved pi conversation by its
|
|
5
5
|
// absolute session-file path (--session <path>) when that .jsonl still exists,
|
|
6
|
-
// else falls through to a fresh launch
|
|
7
|
-
//
|
|
6
|
+
// else falls through to a fresh launch. `--fresh` skips the true resume: if the
|
|
7
|
+
// `.jsonl` already exists it CYCLES in place instead (same file, a fresh
|
|
8
|
+
// `crtr-cycle` branch — `resumed:false`), and only starts a genuinely clean
|
|
9
|
+
// session when no `.jsonl` exists yet (also `resumed:false`).
|
|
8
10
|
//
|
|
9
11
|
// `--all` sweeps EVERY disconnected node (engine not running, resumable session)
|
|
10
12
|
// in one shot — the recovery for a reboot / mass-crash / daemon-down-a-while
|
|
@@ -19,6 +21,22 @@ import { getNode, fullName } from '../core/canvas/index.js';
|
|
|
19
21
|
import { isPidAlive } from '../core/canvas/pid.js';
|
|
20
22
|
import { readFault } from '../core/runtime/fault.js';
|
|
21
23
|
import { isBusy } from '../core/runtime/busy.js';
|
|
24
|
+
import { faultNeedsYou } from '../core/canvas/status-glyph.js';
|
|
25
|
+
/** --now eligibility for a live, non-busy fault: a daemon-owned AUTO retry
|
|
26
|
+
* (kick it early instead of waiting out the schedule), or any fault the
|
|
27
|
+
* canvas dashboard flags "needs you" (`faultNeedsYou`, status-glyph.ts) — the
|
|
28
|
+
* disposition that otherwise has NO sanctioned recovery verb (issue #115).
|
|
29
|
+
* Both are scoped to `pi→provider`: refuses a client-owned auto retry
|
|
30
|
+
* (redialing) already in flight, and refuses any OTHER link — notably a
|
|
31
|
+
* `daemon→node` wedge fault, which --now can't clear (SIGTERM doesn't touch
|
|
32
|
+
* the runaway subprocess; see `wedged-child-on-runaway-bash`). */
|
|
33
|
+
export function isNowEligibleFault(fault) {
|
|
34
|
+
if (fault === null || fault.link !== 'pi→provider')
|
|
35
|
+
return false;
|
|
36
|
+
if (faultNeedsYou(fault))
|
|
37
|
+
return true;
|
|
38
|
+
return fault.retry.by === 'daemon';
|
|
39
|
+
}
|
|
22
40
|
// ---------------------------------------------------------------------------
|
|
23
41
|
// revive node
|
|
24
42
|
// ---------------------------------------------------------------------------
|
|
@@ -50,7 +68,7 @@ export const reviveLeaf = defineLeaf({
|
|
|
50
68
|
type: 'bool',
|
|
51
69
|
required: false,
|
|
52
70
|
default: false,
|
|
53
|
-
constraint: 'When set,
|
|
71
|
+
constraint: 'When set, do NOT do a true resume. If the node already has a session `.jsonl`, this instead CYCLES it in place — reuses that SAME file, resets the tree leaf, and roots a fresh `crtr-cycle` branch (a clean-context restart that still carries the file\'s history, output `resumed:false`). Only when no `.jsonl` exists yet does this start a genuinely brand-new session (also `resumed:false`). Default (no --fresh): a true resume — replays the existing `.jsonl` as-is (`resumed:true`). Ignored with --all (which always resumes).',
|
|
54
72
|
},
|
|
55
73
|
{
|
|
56
74
|
kind: 'flag',
|
|
@@ -58,7 +76,7 @@ export const reviveLeaf = defineLeaf({
|
|
|
58
76
|
type: 'bool',
|
|
59
77
|
required: false,
|
|
60
78
|
default: false,
|
|
61
|
-
constraint: 'On-demand kick of an active-fault node (parked on a provider
|
|
79
|
+
constraint: 'On-demand kick of an active-fault node (parked on a `pi→provider` fault, broker still alive). Its broker pid is alive, so an ordinary revive no-ops (double-launch guard) — --now SIGTERMs the live broker instead, so the daemon\'s crash→grace→resume path brings it back in ~20s. Valid for a live `pi→provider` fault that is either a daemon-owned auto retry (kicks it early), or ANY fault the canvas dashboard flags "needs you" (e.g. exhausted retries, an auth fault you just fixed) — that disposition otherwise has no recovery verb. Mutually exclusive with --all/--fresh.',
|
|
62
80
|
},
|
|
63
81
|
// --force is a DELIBERATELY UNDOCUMENTED confirmation gate for --all (hidden
|
|
64
82
|
// from -h; see FlagParam.hidden). Silas's intent (2026-06-13): a mass revive
|
|
@@ -80,7 +98,7 @@ export const reviveLeaf = defineLeaf({
|
|
|
80
98
|
output: [
|
|
81
99
|
{ name: 'window', type: 'string', required: false, constraint: 'Always null — the revived broker is headless and opens no tmux window. Kept for caller back-compat.' },
|
|
82
100
|
{ name: 'session', type: 'string', required: false, constraint: 'The node\'s last live location session, or null — the headless broker has no tmux session of its own.' },
|
|
83
|
-
{ name: 'resumed', type: 'boolean', required: false, constraint: 'True
|
|
101
|
+
{ name: 'resumed', type: 'boolean', required: false, constraint: 'True ONLY for a true resume (the existing `.jsonl` replayed as-is). False for a --fresh revive, whether that landed as an in-place CYCLE (an existing `.jsonl` reused with a fresh `crtr-cycle` branch — pi is still told --session, but this is not a resume) or a genuinely fresh launch (no `.jsonl` existed). Single-node revive only.' },
|
|
84
102
|
{ name: 'ready', type: 'boolean', required: false, constraint: 'True when the revived broker\'s view.sock accepted a connection before return — the node is immediately attachable/drivable. Single-node revive only.' },
|
|
85
103
|
{ name: 'mode', type: 'string', required: false, constraint: '"preview" (the --all candidate list, nothing launched), "revived" (the --all sweep ran), or absent for a single-node revive.' },
|
|
86
104
|
{ name: 'candidates', type: 'string', required: false, constraint: '--all preview: the node ids that WOULD be revived (newline-joined). Empty when none are disconnected.' },
|
|
@@ -145,8 +163,8 @@ export const reviveLeaf = defineLeaf({
|
|
|
145
163
|
// reviveNode would no-op the double-launch guard) — SIGTERM it so the daemon's
|
|
146
164
|
// ordinary crash→grace→resume path recovers it on the saved session, the same
|
|
147
165
|
// thing the daemon does at the auto-recovery grace, just on demand. Gated on
|
|
148
|
-
// a live pid and
|
|
149
|
-
// healthy node.
|
|
166
|
+
// a live pid and an eligible pi→provider fault (`isNowEligibleFault` above) so
|
|
167
|
+
// it can't be used to nuke a healthy node.
|
|
150
168
|
if (now) {
|
|
151
169
|
const pid = meta.pi_pid;
|
|
152
170
|
const stall = readFault(nodeId);
|
|
@@ -157,11 +175,11 @@ export const reviveLeaf = defineLeaf({
|
|
|
157
175
|
next: 'Revive a dormant node with `crtr canvas revive ' + nodeId + '` (no --now).',
|
|
158
176
|
});
|
|
159
177
|
}
|
|
160
|
-
if (stall
|
|
178
|
+
if (!isNowEligibleFault(stall)) {
|
|
161
179
|
throw new InputError({
|
|
162
180
|
error: 'not_hanging',
|
|
163
|
-
message: `${nodeId} is live but
|
|
164
|
-
next: 'Use --now only on a node the canvas shows as hanging (⚠). For a routine relaunch, omit --now.',
|
|
181
|
+
message: `${nodeId} is live but has no pi→provider fault eligible for --now (either healthy, or a client-owned retry / non-provider fault already being handled elsewhere) — --now refuses to SIGTERM a node that isn't stuck on a kickable provider fault.`,
|
|
182
|
+
next: 'Use --now only on a node the canvas shows as hanging (⚠) with a `pi→provider` fault. For a routine relaunch, omit --now.',
|
|
165
183
|
});
|
|
166
184
|
}
|
|
167
185
|
// The daemon never kills a BUSY engine (its verdict paths all return 'leave'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { resolveBundledPiCliPath } from '../../../core/runtime/pi-cli.js';
|
|
5
|
+
test('resolveBundledPiCliPath uses crouter bundled pi instead of a standalone pi on PATH', () => {
|
|
6
|
+
const prevPath = process.env.PATH;
|
|
7
|
+
process.env.PATH = '';
|
|
8
|
+
try {
|
|
9
|
+
const cliPath = resolveBundledPiCliPath();
|
|
10
|
+
assert.equal(existsSync(cliPath), true);
|
|
11
|
+
assert.match(cliPath, /@earendil-works[\/]pi-coding-agent[\/]dist[\/]cli\.js$/);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
if (prevPath === undefined)
|
|
15
|
+
delete process.env.PATH;
|
|
16
|
+
else
|
|
17
|
+
process.env.PATH = prevPath;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
@@ -71,7 +71,7 @@ function faultRemediation(fault) {
|
|
|
71
71
|
auth: 're-authenticate',
|
|
72
72
|
protocol: 'fix the protocol mismatch',
|
|
73
73
|
other: 'fix the underlying issue',
|
|
74
|
-
wedged: 'read `crtr memory read wedged-child-on-runaway-bash`
|
|
74
|
+
wedged: 'read `crtr memory read wedged-child-on-runaway-bash` — if a runaway subprocess is to blame, kill it (not the node); if the broker had no subprocess, the daemon already SIGTERM-kicked it to resume on its own (recheck node status; nudge it if it came back idle)',
|
|
75
75
|
};
|
|
76
76
|
const progress = fault.retry.disposition === 'auto'
|
|
77
77
|
? fault.retry.by === 'client'
|
|
@@ -4,6 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { builtinPiPackageDir } from '../../core/scope.js';
|
|
6
6
|
import { readJsonIfExists } from '../../core/fs-utils.js';
|
|
7
|
+
import { resolveBundledPiCliPath } from '../../core/runtime/pi-cli.js';
|
|
7
8
|
const PACKAGE_COPY = {
|
|
8
9
|
'crtr-extensions': {
|
|
9
10
|
name: 'pi-crtr-extensions',
|
|
@@ -285,8 +286,8 @@ function runShellCommand(command) {
|
|
|
285
286
|
throw new Error(`command failed: ${command}`);
|
|
286
287
|
}
|
|
287
288
|
}
|
|
288
|
-
function installPiPackage(source) {
|
|
289
|
-
const result = spawnSync(
|
|
289
|
+
function installPiPackage(source, piCliPath = resolveBundledPiCliPath()) {
|
|
290
|
+
const result = spawnSync(process.execPath, [piCliPath, 'install', source], { stdio: 'inherit' });
|
|
290
291
|
if (result.status !== 0) {
|
|
291
292
|
throw new Error(`pi install failed: ${source}`);
|
|
292
293
|
}
|
|
@@ -506,7 +506,7 @@ export const sysSetupLeaf = defineLeaf({
|
|
|
506
506
|
outputKind: 'object',
|
|
507
507
|
effects: [
|
|
508
508
|
'Opens a pi-tui wizard that collects companion package installs, system dependency installs, and an Exa API key.',
|
|
509
|
-
'Runs pi
|
|
509
|
+
'Runs the bundled pi CLI to install the selected companion packages, writes EXA_API_KEY to ~/.crouter/exa.key when provided, and prints a summary.',
|
|
510
510
|
],
|
|
511
511
|
},
|
|
512
512
|
run: async () => {
|