@north-light/crouter 0.3.176 → 0.3.177
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 +4 -4
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +1 -1
- package/dist/api/dto/reports.d.ts +1 -1
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/config.js +5 -5
- package/dist/clients/attach/input/clipboard-image.js +2 -2
- package/dist/clients/attach/input/ref-autocomplete.js +1 -1
- package/dist/clients/attach/input/titled-editor.d.ts +12 -13
- package/dist/clients/attach/input/titled-editor.js +14 -15
- package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
- package/dist/clients/attach/overlays/dialogs.js +2 -2
- package/dist/clients/attach/overlays/mcp.js +5 -12
- package/dist/clients/attach/render/chat-view.d.ts +1 -1
- package/dist/clients/attach/render/chat-view.js +2 -2
- package/dist/clients/attach/render/group-recap.js +7 -0
- package/dist/clients/attach/session/context.js +3 -3
- package/dist/clients/attach/session/editor-inventory.js +6 -6
- package/dist/clients/attach/session/input-wiring.js +1 -1
- package/dist/clients/attach/session/teardown.js +7 -9
- package/dist/clients/attach/slash/dispatch.d.ts +1 -2
- package/dist/clients/attach/slash/dispatch.js +4 -8
- package/dist/clients/attach/viewer.js +394 -394
- package/dist/clients/inbox/controller.js +3 -2
- package/dist/clients/inbox/deck-adapter.js +3 -2
- package/dist/clients/surfaces/host.d.ts +1 -1
- package/dist/commands/api-client.d.ts +12 -13
- package/dist/commands/api-client.js +15 -16
- package/dist/commands/chord.js +1 -2
- package/dist/commands/human/queue.js +6 -5
- package/dist/commands/memory/find.js +3 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node-lifecycle-revive.d.ts +1 -1
- package/dist/commands/node-lifecycle-revive.js +3 -3
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/placement.d.ts +1 -1
- package/dist/commands/surface/node/placement.js +1 -1
- package/dist/commands/surface-edit.js +1 -4
- package/dist/commands/sys/panels/panel.d.ts +3 -3
- package/dist/commands/sys/panels/panel.js +3 -3
- package/dist/commands/sys/setup-core.js +1 -7
- package/dist/commands/sys/sync-deps.js +1 -3
- package/dist/commands/sys/sync-project-guidance.js +2 -11
- package/dist/commands/sys/sysprompt.js +1 -3
- package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -1
- package/dist/core/__tests__/helpers/harness.js +1 -1
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +2 -2
- package/dist/core/canvas/canvas.d.ts +3 -3
- package/dist/core/canvas/canvas.js +3 -3
- package/dist/core/canvas/daemon-owner.js +1 -2
- package/dist/core/canvas/db.js +7 -8
- package/dist/core/canvas/index.js +2 -2
- package/dist/core/canvas/pid.d.ts +12 -14
- package/dist/core/canvas/pid.js +16 -20
- package/dist/core/canvas/remote-transport.js +2 -2
- package/dist/core/canvas/status-glyph.d.ts +1 -1
- package/dist/core/canvas/status-glyph.js +1 -1
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command-manifests/manifest.js +4 -6
- package/dist/core/command-manifests/schema.js +10 -12
- package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
- package/dist/core/command-plugins/transport/http-invoke.js +4 -6
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +2 -2
- package/dist/core/events/read.js +1 -3
- package/dist/core/fault-classifier.js +1 -3
- package/dist/core/fs-utils.d.ts +7 -0
- package/dist/core/fs-utils.js +29 -2
- package/dist/core/host-exports/export.d.ts +4 -5
- package/dist/core/host-exports/export.js +4 -5
- package/dist/core/human/claim.js +3 -2
- package/dist/core/human/convention.d.ts +0 -1
- package/dist/core/human/convention.js +1 -9
- package/dist/core/human/scan.js +5 -4
- package/dist/core/profiles/manifest.d.ts +1 -1
- package/dist/core/profiles/manifest.js +1 -1
- package/dist/core/review/types.d.ts +1 -1
- package/dist/core/review/types.js +1 -1
- package/dist/core/runtime/broker/frame-dispatch.js +4 -4
- package/dist/core/runtime/broker-cli.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -10
- package/dist/core/runtime/broker-protocol.js +9 -10
- package/dist/core/runtime/broker-sdk.d.ts +1 -1
- package/dist/core/runtime/broker.js +6 -6
- package/dist/core/runtime/host.js +14 -14
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/lifecycle.js +16 -21
- package/dist/core/runtime/node-read.js +2 -3
- package/dist/core/runtime/package-health.js +1 -8
- package/dist/core/runtime/persona.js +2 -2
- package/dist/core/runtime/pi-vendored.d.ts +4 -2
- package/dist/core/runtime/pi-vendored.js +5 -16
- package/dist/core/runtime/promote.js +1 -1
- package/dist/core/runtime/reopen.js +2 -2
- package/dist/core/runtime/revive-all.d.ts +3 -3
- package/dist/core/runtime/revive-all.js +6 -6
- package/dist/core/runtime/revive.js +1 -1
- package/dist/core/runtime/spawn.d.ts +1 -2
- package/dist/core/runtime/spawn.js +2 -3
- package/dist/core/runtime/structured-output.js +4 -6
- package/dist/core/runtime/tmux-driver.d.ts +2 -2
- package/dist/core/runtime/tmux-driver.js +3 -5
- package/dist/core/runtime/tool-group-summary.js +5 -4
- package/dist/core/runtime/warm-pool.js +1 -1
- package/dist/core/scope.js +2 -9
- package/dist/core/self-update.js +3 -3
- package/dist/core/spawn.d.ts +0 -1
- package/dist/core/spawn.js +1 -4
- package/dist/core/substrate/on-read.js +1 -9
- package/dist/core/wake.js +2 -2
- package/dist/core/worktree.d.ts +3 -3
- package/dist/core/worktree.js +5 -7
- package/dist/daemon/api/bridge.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +1 -2
- package/dist/daemon/api/handlers/messages.js +2 -2
- package/dist/daemon/api/handlers/nodes.js +1 -1
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/validate.d.ts +2 -1
- package/dist/daemon/api/handlers/validate.js +2 -3
- package/dist/daemon/cron-run.js +7 -7
- package/dist/daemon/human/finish.js +3 -2
- package/dist/daemon/manage.d.ts +6 -6
- package/dist/daemon/manage.js +15 -16
- package/dist/pi-extensions/canvas-context-intro.js +1 -14
- package/dist/pi-extensions/canvas-doc-substrate.js +1 -1
- package/dist/pi-extensions/canvas-review-boundary.js +1 -14
- package/dist/pi-extensions/truncate.d.ts +5 -0
- package/dist/pi-extensions/truncate.js +14 -0
- package/dist/shared/predicates.d.ts +2 -0
- package/dist/shared/predicates.js +4 -0
- package/dist/shared/shell-quote.d.ts +3 -0
- package/dist/shared/shell-quote.js +5 -0
- package/dist/shared/tool-groups.d.ts +4 -2
- package/dist/shared/tool-groups.js +3 -3
- package/dist/types.d.ts +4 -5
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -13,18 +13,18 @@ export declare const TERMINAL_BY_CHOICE: readonly NodeStatus[];
|
|
|
13
13
|
* inbox watcher's own hold-and-flush) ever brings such a node back on its
|
|
14
14
|
* own — only an explicit revive/reopen does. Shared by `isDisconnected`
|
|
15
15
|
* (mass-revive scope, below) and `node message send --tier deferred`'s terminal-edge
|
|
16
|
-
* rejection
|
|
16
|
+
* rejection — a deferred entry appended to such a target would
|
|
17
17
|
* never be delivered, so that doorway rejects instead of silently holding
|
|
18
18
|
* it forever. */
|
|
19
19
|
export declare function hasNoNaturalCycle(meta: Pick<NodeMeta, 'status' | 'final_report'>): boolean;
|
|
20
20
|
/** True when `meta`'s engine is NOT running but it has a resumable saved session
|
|
21
|
-
* — the precise "disconnected" predicate
|
|
21
|
+
* — the precise "disconnected" predicate.
|
|
22
22
|
*
|
|
23
23
|
* - engine not running: no live `pi_pid` (a dead/absent broker pid).
|
|
24
24
|
* - reconnectable: a captured pi session marker (`pi_session_file` or
|
|
25
25
|
* `pi_session_id`) — without one there is nothing to reopen. reviveNode
|
|
26
26
|
* resumes by the `.jsonl` path when it still exists, else relaunches fresh (a
|
|
27
|
-
* bare `pi_session_id` alone
|
|
27
|
+
* bare `pi_session_id` alone does not resume — the broker preflight rejects
|
|
28
28
|
* it — so such a node comes back fresh).
|
|
29
29
|
* - `human`-kind rows are the `crtr human` bridge, never a pi engine, so they
|
|
30
30
|
* are never "disconnected" (mirrors the daemon's superviseTick carve-out).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// revive-all — resume EVERY disconnected node in one shot
|
|
1
|
+
// revive-all — resume EVERY disconnected node in one shot.
|
|
2
2
|
//
|
|
3
3
|
// After a mass-disconnect event (a reboot, a killed login/tmux session, a mass
|
|
4
4
|
// crash, or the daemon being down a while) many nodes end up with their
|
|
@@ -30,20 +30,20 @@ export const TERMINAL_BY_CHOICE = ['done', 'canceled'];
|
|
|
30
30
|
* inbox watcher's own hold-and-flush) ever brings such a node back on its
|
|
31
31
|
* own — only an explicit revive/reopen does. Shared by `isDisconnected`
|
|
32
32
|
* (mass-revive scope, below) and `node message send --tier deferred`'s terminal-edge
|
|
33
|
-
* rejection
|
|
33
|
+
* rejection — a deferred entry appended to such a target would
|
|
34
34
|
* never be delivered, so that doorway rejects instead of silently holding
|
|
35
35
|
* it forever. */
|
|
36
36
|
export function hasNoNaturalCycle(meta) {
|
|
37
37
|
return meta.final_report != null || TERMINAL_BY_CHOICE.includes(meta.status);
|
|
38
38
|
}
|
|
39
39
|
/** True when `meta`'s engine is NOT running but it has a resumable saved session
|
|
40
|
-
* — the precise "disconnected" predicate
|
|
40
|
+
* — the precise "disconnected" predicate.
|
|
41
41
|
*
|
|
42
42
|
* - engine not running: no live `pi_pid` (a dead/absent broker pid).
|
|
43
43
|
* - reconnectable: a captured pi session marker (`pi_session_file` or
|
|
44
44
|
* `pi_session_id`) — without one there is nothing to reopen. reviveNode
|
|
45
45
|
* resumes by the `.jsonl` path when it still exists, else relaunches fresh (a
|
|
46
|
-
* bare `pi_session_id` alone
|
|
46
|
+
* bare `pi_session_id` alone does not resume — the broker preflight rejects
|
|
47
47
|
* it — so such a node comes back fresh).
|
|
48
48
|
* - `human`-kind rows are the `crtr human` bridge, never a pi engine, so they
|
|
49
49
|
* are never "disconnected" (mirrors the daemon's superviseTick carve-out).
|
|
@@ -78,8 +78,8 @@ export function listDisconnected() {
|
|
|
78
78
|
// IDENTITY-aware AND requires CONFIRMED alive: a bare isPidAlive would
|
|
79
79
|
// misread a recycled pi_pid as "engine running" and SKIP the node, so it
|
|
80
80
|
// never reaches reviveNode's own identity-aware double-launch guard
|
|
81
|
-
//
|
|
82
|
-
//
|
|
81
|
+
// guard. `dead` OR `indeterminate` both count as disconnected/eligible
|
|
82
|
+
// here — only a
|
|
83
83
|
// positively confirmed-alive read is treated as "still connected".
|
|
84
84
|
if (meta !== null && isDisconnected(meta, (pid) => recordedPidLiveness(pid, meta.pi_pid_identity) === 'alive'))
|
|
85
85
|
out.push(meta);
|
|
@@ -100,7 +100,7 @@ export function reviveNode(nodeId, opts) {
|
|
|
100
100
|
}
|
|
101
101
|
// Fresh durable snapshot immediately before the lifecycle guard.
|
|
102
102
|
meta = getNode(nodeId) ?? meta;
|
|
103
|
-
//
|
|
103
|
+
// Refuse to revive a LATCHED node — the reopen gate at every
|
|
104
104
|
// legitimate caller (node message send, node lifecycle revive) already checked this
|
|
105
105
|
// before calling in, but a target can finalize in the gap between that
|
|
106
106
|
// check and this call. This authoritative snapshot closes that race;
|
|
@@ -109,8 +109,7 @@ export interface SpawnChildResult {
|
|
|
109
109
|
* (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
|
|
110
110
|
* does not: it is top-level by definition and the spawner identity is
|
|
111
111
|
* provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
|
|
112
|
-
* spawner
|
|
113
|
-
* throw here). */
|
|
112
|
+
* spawner, so `crtr node new --root` from outside a node must not throw. */
|
|
114
113
|
export declare function resolveSpawner(parent: string | undefined, ctxNodeId: string | null, root: boolean): string | null;
|
|
115
114
|
/** Spawn a node from a live node. By default a managed terminal worker in a
|
|
116
115
|
* background window, with the spawner auto-subscribed (active) via spawnNode.
|
|
@@ -187,8 +187,7 @@ export async function assertLaunchModelRegistered(request, options, registry) {
|
|
|
187
187
|
* (explicit `--parent` or the calling node's CRTR_NODE_ID). A --root spawn
|
|
188
188
|
* does not: it is top-level by definition and the spawner identity is
|
|
189
189
|
* provenance only — a human shell with no CRTR_NODE_ID is a legitimate root
|
|
190
|
-
* spawner
|
|
191
|
-
* throw here). */
|
|
190
|
+
* spawner, so `crtr node new --root` from outside a node must not throw. */
|
|
192
191
|
export function resolveSpawner(parent, ctxNodeId, root) {
|
|
193
192
|
const spawner = parent ?? ctxNodeId ?? null;
|
|
194
193
|
if (!root && spawner === null) {
|
|
@@ -218,7 +217,7 @@ export async function spawnChildPrepared(opts, beforeBrokerLaunch) {
|
|
|
218
217
|
const attendedRoot = root && lifecycle === 'resident';
|
|
219
218
|
// Born WITHOUT a name — the canvas-goal-capture extension names it async from
|
|
220
219
|
// its first message (the kickoff task) inside its own pi process, so spawn
|
|
221
|
-
// never blocks on the LLM naming round-trip (
|
|
220
|
+
// never blocks on the LLM naming round-trip (a 2-3s freeze).
|
|
222
221
|
const wantsWorktree = opts.worktree === true;
|
|
223
222
|
const nodeId = wantsWorktree ? (opts.nodeId ?? newNodeId()) : opts.nodeId;
|
|
224
223
|
// Preflight: resolve every fallible input BEFORE creating the worktree or the
|
|
@@ -3,15 +3,13 @@ import { dirname, resolve } from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { nodeDir, contextDir } from '../canvas/paths.js';
|
|
5
5
|
import { InputError } from '../io.js';
|
|
6
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
6
7
|
export function outputSchemaPath(nodeId) {
|
|
7
8
|
return `${nodeDir(nodeId)}/output-schema.json`;
|
|
8
9
|
}
|
|
9
10
|
export function outputResultPath(nodeId) {
|
|
10
11
|
return `${contextDir(nodeId)}/result.json`;
|
|
11
12
|
}
|
|
12
|
-
function isObject(value) {
|
|
13
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
-
}
|
|
15
13
|
export function parseOutputSchemaValue(value) {
|
|
16
14
|
const raw = value?.trim();
|
|
17
15
|
if (raw === undefined || raw === '')
|
|
@@ -49,7 +47,7 @@ export function parseOutputSchemaValue(value) {
|
|
|
49
47
|
next: 'Pass valid JSON whose top-level value is an object.',
|
|
50
48
|
});
|
|
51
49
|
}
|
|
52
|
-
if (!
|
|
50
|
+
if (!isRecord(parsed)) {
|
|
53
51
|
throw new InputError({
|
|
54
52
|
error: 'bad_output_schema',
|
|
55
53
|
message: `output schema from ${source} must be a JSON object`,
|
|
@@ -91,7 +89,7 @@ export function readOutputRequest(nodeId) {
|
|
|
91
89
|
catch (err) {
|
|
92
90
|
return { state: 'invalid', error: `request file is not valid JSON: ${err.message}` };
|
|
93
91
|
}
|
|
94
|
-
if (!
|
|
92
|
+
if (!isRecord(parsed)) {
|
|
95
93
|
return { state: 'invalid', error: 'request file top-level value must be an object' };
|
|
96
94
|
}
|
|
97
95
|
const mode = parsed['mode'];
|
|
@@ -99,7 +97,7 @@ export function readOutputRequest(nodeId) {
|
|
|
99
97
|
if (mode !== 'terminal' && mode !== 'oneoff') {
|
|
100
98
|
return { state: 'invalid', error: `mode must be 'terminal' or 'oneoff', got ${JSON.stringify(mode)}` };
|
|
101
99
|
}
|
|
102
|
-
if (!
|
|
100
|
+
if (!isRecord(schema)) {
|
|
103
101
|
return { state: 'invalid', error: `schema must be an object, got ${typeof schema}` };
|
|
104
102
|
}
|
|
105
103
|
return { state: 'valid', request: { mode, schema } };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AttachBindingId } from '../keybindings/index.js';
|
|
2
|
-
|
|
3
|
-
export
|
|
2
|
+
import { shellQuote } from '../../shared/shell-quote.js';
|
|
3
|
+
export { shellQuote };
|
|
4
4
|
declare function tmux(args: string[]): {
|
|
5
5
|
ok: boolean;
|
|
6
6
|
stdout: string;
|
|
@@ -6,13 +6,11 @@
|
|
|
6
6
|
import { execFile, spawnSync } from 'node:child_process';
|
|
7
7
|
import { encodeAttachControlInput, } from '../keybindings/index.js';
|
|
8
8
|
import { bundledPiCommandForShell } from './pi-cli.js';
|
|
9
|
+
import { shellQuote } from '../../shared/shell-quote.js';
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
|
-
//
|
|
11
|
+
// tmux invocation
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
export function shellQuote(s) {
|
|
14
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
15
|
-
}
|
|
13
|
+
export { shellQuote };
|
|
16
14
|
function tmux(args) {
|
|
17
15
|
const r = spawnSync('tmux', args, { encoding: 'utf8' });
|
|
18
16
|
return {
|
|
@@ -49,9 +49,10 @@ const SUMMARY_SYSTEM_PROMPT = 'Create a structured recap of this coding-agent to
|
|
|
49
49
|
'Omit inbox receipt, delivery, and queueing; node-response and report bookkeeping; help lookups; status checks; and summary-generation activity. ' +
|
|
50
50
|
'A node response may support a substantive outcome, but its arrival or resummarization is never itself an outcome. ' +
|
|
51
51
|
'If no meaningful outcome remains after those omissions, submit the single bullet "No meaningful changes." ' +
|
|
52
|
-
'Count nodes created and distinct files
|
|
52
|
+
'Count nodes created, and separately count distinct files removed from disk, only when the supplied tool arguments or results evidence them; otherwise use zero, even when the corresponding mechanics are omitted from bullets. ' +
|
|
53
|
+
'A deleted file is one that no longer exists after the segment, never a file that was merely edited or overwritten, and a recursive or wildcard removal of an unknown number of files counts as one. ' +
|
|
53
54
|
'Do not invent success when a result shows failure. ' +
|
|
54
|
-
'Call submit_summary exactly once with bullets, nodesSpawned, and
|
|
55
|
+
'Call submit_summary exactly once with bullets, nodesSpawned, and filesDeleted, then stop.';
|
|
55
56
|
const SUBMIT_SUMMARY_SCHEMA = Type.Object({
|
|
56
57
|
bullets: Type.Array(Type.String({
|
|
57
58
|
minLength: 1,
|
|
@@ -62,9 +63,9 @@ const SUBMIT_SUMMARY_SCHEMA = Type.Object({
|
|
|
62
63
|
minimum: 0,
|
|
63
64
|
description: 'Count of nodes created during the supplied segment, or zero when none are evidenced.',
|
|
64
65
|
}),
|
|
65
|
-
|
|
66
|
+
filesDeleted: Type.Integer({
|
|
66
67
|
minimum: 0,
|
|
67
|
-
description: 'Count of distinct files
|
|
68
|
+
description: 'Count of distinct files removed from disk during the supplied segment, or zero when none are evidenced.',
|
|
68
69
|
}),
|
|
69
70
|
}, { additionalProperties: false });
|
|
70
71
|
/** Generate a structured summary object, or null on every failure. */
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
// and at daemon start for the most recently used profiles, so the FIRST node
|
|
48
48
|
// after sitting down is warm too.
|
|
49
49
|
//
|
|
50
|
-
// The pool
|
|
50
|
+
// The pool does not fan out per directory: a node runs in its profile's home
|
|
51
51
|
// (`resolveNodeCwd` in the create handler), so the cwd a spare freezes is a
|
|
52
52
|
// function of its profile, and "which spare serves this create" is in practice
|
|
53
53
|
// a question about the profile alone.
|
package/dist/core/scope.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import { existsSync, statSync
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { join, resolve, dirname } from 'node:path';
|
|
4
4
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
5
5
|
import { assetRoot } from './asset-root.js';
|
|
6
6
|
import { usage } from './errors.js';
|
|
7
7
|
import { loadProfileManifest } from './profiles/manifest.js';
|
|
8
|
+
import { realpathOrSelf } from './fs-utils.js';
|
|
8
9
|
// Cache of resolved project-scope-root STACKS, keyed by (resolved cwd, profile
|
|
9
10
|
// id). A profile widens the pointer set beyond the bare cwd (see
|
|
10
11
|
// findProjectScopeRoots below), so the cache key must carry both — the same
|
|
@@ -31,14 +32,6 @@ export function builtinPiPackageDir(name) {
|
|
|
31
32
|
export function userScopeRoot() {
|
|
32
33
|
return join(homedir(), CRTR_DIR_NAME);
|
|
33
34
|
}
|
|
34
|
-
function realpathOrSelf(p) {
|
|
35
|
-
try {
|
|
36
|
-
return realpathSync(p);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return p;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
35
|
function isProjectScopeDir(candidate, userRoot) {
|
|
43
36
|
if (!existsSync(candidate))
|
|
44
37
|
return false;
|
package/dist/core/self-update.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Self-update and content-check primitives
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Self-update and content-check primitives. They live in core/ so auto-update.ts
|
|
2
|
+
// (core) reaches them without importing commands/update.ts, which would invert
|
|
3
|
+
// the core→commands layering.
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { join, dirname } from 'node:path';
|
package/dist/core/spawn.d.ts
CHANGED
|
@@ -13,4 +13,3 @@ export declare function isInTmux(): boolean;
|
|
|
13
13
|
* no server is running — the true "no tmux to surface into" case that should
|
|
14
14
|
* degrade to the inbox-drain follow-up. */
|
|
15
15
|
export declare function tmuxServerReachable(): boolean;
|
|
16
|
-
export declare function shellQuote(s: string): string;
|
package/dist/core/spawn.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Tmux reachability
|
|
1
|
+
// Tmux reachability for the `human` command tree.
|
|
2
2
|
//
|
|
3
3
|
// The human surface gates on whether the canvas tmux server is reachable
|
|
4
4
|
// (tmuxServerReachable) so it can decide whether to surface a prompt or degrade
|
|
@@ -25,6 +25,3 @@ export function tmuxServerReachable() {
|
|
|
25
25
|
const r = spawnSync('tmux', ['list-clients', '-F', '#{client_name}'], { encoding: 'utf8' });
|
|
26
26
|
return r.status === 0;
|
|
27
27
|
}
|
|
28
|
-
export function shellQuote(s) {
|
|
29
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
30
|
-
}
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
// Every candidate renders at its own fileReadVisibility rung. Explicit routing
|
|
25
25
|
// is the whole on-read model, so directory INDEX ceilings remain a boot-catalog
|
|
26
26
|
// concern and do not cap a deliberately matched document.
|
|
27
|
-
import { realpathSync } from 'node:fs';
|
|
28
27
|
import { homedir } from 'node:os';
|
|
29
28
|
import { basename, dirname, matchesGlob, parse, relative, sep } from 'node:path';
|
|
30
29
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
@@ -37,15 +36,8 @@ import { displayName } from './ceiling.js';
|
|
|
37
36
|
import { gatePasses } from './gate.js';
|
|
38
37
|
import { normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName } from './schema.js';
|
|
39
38
|
import { cachedSubstrateDocs } from './session-cache.js';
|
|
39
|
+
import { realpathOrSelf } from '../fs-utils.js';
|
|
40
40
|
const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
|
|
41
|
-
function realpathOrSelf(p) {
|
|
42
|
-
try {
|
|
43
|
-
return realpathSync(p);
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return p;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
41
|
function attr(s) {
|
|
50
42
|
return s
|
|
51
43
|
.replace(/&/g, '&')
|
package/dist/core/wake.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// This is the ONE place that owns cron time logic. Both the CLI surface and the
|
|
4
4
|
// daemon's firing pass import from here, so there is no duplicated cron usage
|
|
5
5
|
// and no surface<->daemon inverted dependency:
|
|
6
|
-
// - parseWhen / parseCadence — arm-time grammar (surface
|
|
7
|
-
// - nextSlotAfter — per-tick advance / coalescing (daemon
|
|
6
|
+
// - parseWhen / parseCadence — arm-time grammar (surface)
|
|
7
|
+
// - nextSlotAfter — per-tick advance / coalescing (daemon)
|
|
8
8
|
//
|
|
9
9
|
// Design refs: surface-design §4 (grammar), §5 (timezone/DST ruling),
|
|
10
10
|
// design §5.3 + D7 (engine evaluates UTC; the surface bakes the IANA zone
|
package/dist/core/worktree.d.ts
CHANGED
|
@@ -95,11 +95,11 @@ export interface AutoDroppedWorktree {
|
|
|
95
95
|
* branch and the live checkout stay fully recoverable and the caller's
|
|
96
96
|
* `open_managed_worktree` guard fires. */
|
|
97
97
|
export declare function autoDropCleanManagedWorktreeLocked(nodeId: string, wt: ManagedWorktree, proof: CleanEmptyProof): AutoDroppedWorktree | null;
|
|
98
|
-
/** `push final`'s escape hatch
|
|
98
|
+
/** `push final`'s escape hatch: a managed worktree that has
|
|
99
99
|
* nothing to land is auto-dropped instead of blocking on an explicit
|
|
100
100
|
* `node worktree close` — one the caller may have been told NOT to run
|
|
101
|
-
* because a manager already landed the same change another way
|
|
102
|
-
* that never had anything to close in the first place (
|
|
101
|
+
* because a manager already landed the same change another way, or
|
|
102
|
+
* that never had anything to close in the first place (a read-only
|
|
103
103
|
* review node). Returns null and changes nothing when the worktree is not
|
|
104
104
|
* PROVABLY empty-and-clean by `proveCleanEmptyManagedWorktree`, OR when a
|
|
105
105
|
* concurrent mutation invalidates that proof before the destructive delete
|
package/dist/core/worktree.js
CHANGED
|
@@ -6,6 +6,7 @@ import { crtrHome } from './canvas/paths.js';
|
|
|
6
6
|
import { getNode, updateNode } from './canvas/index.js';
|
|
7
7
|
import { CONFIG_FILE, CRTR_DIR_NAME, STATE_FILE } from '../types.js';
|
|
8
8
|
import { withExclusiveDirectoryLock } from './exclusive-lock.js';
|
|
9
|
+
import { shellQuote } from '../shared/shell-quote.js';
|
|
9
10
|
export class WorktreeError extends Error {
|
|
10
11
|
code;
|
|
11
12
|
next;
|
|
@@ -137,7 +138,7 @@ function rollbackManagedWorktreeLocked(wt, pathWasAbsent) {
|
|
|
137
138
|
export function rollbackManagedWorktree(wt) {
|
|
138
139
|
withRepositoryWorktreeLock(wt.repo_root, () => rollbackManagedWorktreeLocked(wt, false));
|
|
139
140
|
}
|
|
140
|
-
//
|
|
141
|
+
// Every crtr process auto-initializes a project scope's
|
|
141
142
|
// `.crouter/config.json` (`ensureProjectScope` → `ensureScopeInitialized`)
|
|
142
143
|
// the first time it runs against a project root that has no ancestor
|
|
143
144
|
// `.crouter/` yet — which every fresh managed worktree checkout is, unless
|
|
@@ -357,9 +358,6 @@ let worktreeCleanupRunner = defaultWorktreeCleanupRunner;
|
|
|
357
358
|
export function setWorktreeCleanupRunnerForTest(runner) {
|
|
358
359
|
worktreeCleanupRunner = runner ?? defaultWorktreeCleanupRunner;
|
|
359
360
|
}
|
|
360
|
-
function shellQuote(value) {
|
|
361
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
362
|
-
}
|
|
363
361
|
function isValidRegisteredCheckout(output, wt) {
|
|
364
362
|
let path = null;
|
|
365
363
|
let prunable = false;
|
|
@@ -607,11 +605,11 @@ export function autoDropCleanManagedWorktreeLocked(nodeId, wt, proof) {
|
|
|
607
605
|
branch_deleted: true,
|
|
608
606
|
};
|
|
609
607
|
}
|
|
610
|
-
/** `push final`'s escape hatch
|
|
608
|
+
/** `push final`'s escape hatch: a managed worktree that has
|
|
611
609
|
* nothing to land is auto-dropped instead of blocking on an explicit
|
|
612
610
|
* `node worktree close` — one the caller may have been told NOT to run
|
|
613
|
-
* because a manager already landed the same change another way
|
|
614
|
-
* that never had anything to close in the first place (
|
|
611
|
+
* because a manager already landed the same change another way, or
|
|
612
|
+
* that never had anything to close in the first place (a read-only
|
|
615
613
|
* review node). Returns null and changes nothing when the worktree is not
|
|
616
614
|
* PROVABLY empty-and-clean by `proveCleanEmptyManagedWorktree`, OR when a
|
|
617
615
|
* concurrent mutation invalidates that proof before the destructive delete
|
|
@@ -49,7 +49,7 @@ function refuseUpgrade(socket, status, statusText, code, message) {
|
|
|
49
49
|
`connection: close\r\n\r\n` +
|
|
50
50
|
body;
|
|
51
51
|
// `end(payload)` flushes the full body THEN sends FIN — a bare
|
|
52
|
-
// `write`+`destroy` can truncate the body for a slow remote peer
|
|
52
|
+
// `write`+`destroy` can truncate the body for a slow remote peer.
|
|
53
53
|
try {
|
|
54
54
|
socket.end(payload);
|
|
55
55
|
}
|
|
@@ -254,8 +254,7 @@ function settleBroker(nodeId, request) {
|
|
|
254
254
|
return node.intent === 'refresh' ? { action: 'shutdown' } : { action: 'stay_dormant' };
|
|
255
255
|
}
|
|
256
256
|
const signals = { pushedFinal: request.pushedFinal, askedHuman: request.askedHuman };
|
|
257
|
-
//
|
|
258
|
-
// stop guard. They are terminal broker consequences, not Pi decisions.
|
|
257
|
+
// Two current-state outcomes: terminal broker consequences, not Pi decisions.
|
|
259
258
|
if (signals.pushedFinal || node.status === 'done') {
|
|
260
259
|
clearFault(nodeId);
|
|
261
260
|
if (focusOf(nodeId) !== null)
|
|
@@ -86,7 +86,7 @@ function parseSchema(raw) {
|
|
|
86
86
|
function label(text) {
|
|
87
87
|
return text.split('\n')[0].slice(0, 120);
|
|
88
88
|
}
|
|
89
|
-
/** The
|
|
89
|
+
/** The fail-loud rejection for a deferred immediate message aimed at a
|
|
90
90
|
* target with no natural cycle ahead of it (done/canceled/finalized). Thrown as
|
|
91
91
|
* an InputError so `toErrorBody` round-trips the FULL structured payload
|
|
92
92
|
* (code+message+next) into `ApiError.details`, and the CLI's shared
|
|
@@ -207,7 +207,7 @@ async function handleMessage(ctx) {
|
|
|
207
207
|
data: { situational: true, situationalOnly: true },
|
|
208
208
|
});
|
|
209
209
|
};
|
|
210
|
-
//
|
|
210
|
+
// Deferred guard: a done/canceled/finalized target has no natural
|
|
211
211
|
// cycle ahead of it, so a deferred entry would sit unseen forever — reject
|
|
212
212
|
// fail-loud BEFORE any side effect, atomically with the append (the read and
|
|
213
213
|
// the write share one canvas write-lock). Non-deferred tiers deliver directly.
|
|
@@ -357,7 +357,7 @@ function handleRevive(ctx) {
|
|
|
357
357
|
const body = ctx.body;
|
|
358
358
|
const resume = body?.resume ?? true;
|
|
359
359
|
const reopen = body?.reopen ?? false;
|
|
360
|
-
//
|
|
360
|
+
// Reopen gate, enforced server-side (the latch clear is a canvas write the CLI
|
|
361
361
|
// caller cannot reach): --reopen CAS-clears a spent `final_report`; the
|
|
362
362
|
// no-reopen path refuses a still-latched node (`node_finalized`). reviveNode
|
|
363
363
|
// keeps its own belt-and-suspenders latch guard under the launch lock.
|
|
@@ -42,7 +42,7 @@ async function handlePush(ctx) {
|
|
|
42
42
|
const before = req.tier === 'final' ? getNode(id)?.status : undefined;
|
|
43
43
|
// `final` with an OPEN managed worktree, BEFORE the push/transition (spec
|
|
44
44
|
// §6.2 — worktree fs ops are server-side). A provably-clean, zero-commit
|
|
45
|
-
// worktree is auto-dropped
|
|
45
|
+
// worktree is auto-dropped so a read-only or already-landed node
|
|
46
46
|
// can finish without an explicit `node worktree close`; any real unlanded
|
|
47
47
|
// work instead blocks with the parity `open_managed_worktree` error,
|
|
48
48
|
// round-tripped via CrtrError.details so the CLI renders the same InputError
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { isRecord } from '../../../shared/predicates.js';
|
|
1
2
|
import type { RequestContext } from '../router.js';
|
|
3
|
+
export { isRecord };
|
|
2
4
|
export type Violation = {
|
|
3
5
|
field: string;
|
|
4
6
|
problem: string;
|
|
@@ -7,7 +9,6 @@ export type Violation = {
|
|
|
7
9
|
/** Reject the request with every violation collected so far. `subject` names
|
|
8
10
|
* what was being parsed, e.g. "review comment request". */
|
|
9
11
|
export declare function invalidRequest(subject: string, violations: Violation[]): never;
|
|
10
|
-
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
11
12
|
/** Narrow a request body to an object and reject any field outside `allowed`.
|
|
12
13
|
* A non-object body yields `{}` so the caller's field reads all miss and
|
|
13
14
|
* report their own violations rather than throwing. */
|
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
// `invalidRequest` once so a caller sees every problem with its body in a
|
|
5
5
|
// single 400 rather than one per round trip.
|
|
6
6
|
import { ApiError } from '../../../api/index.js';
|
|
7
|
+
import { isRecord } from '../../../shared/predicates.js';
|
|
8
|
+
export { isRecord };
|
|
7
9
|
/** Reject the request with every violation collected so far. `subject` names
|
|
8
10
|
* what was being parsed, e.g. "review comment request". */
|
|
9
11
|
export function invalidRequest(subject, violations) {
|
|
10
12
|
throw new ApiError(400, 'invalid_request', `${subject} is invalid`, { violations });
|
|
11
13
|
}
|
|
12
|
-
export function isRecord(value) {
|
|
13
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
-
}
|
|
15
14
|
/** Narrow a request body to an object and reject any field outside `allowed`.
|
|
16
15
|
* A non-object body yields `{}` so the caller's field reads all miss and
|
|
17
16
|
* report their own violations rather than throwing. */
|
package/dist/daemon/cron-run.js
CHANGED
|
@@ -377,8 +377,7 @@ async function disposeSettledRun(c, ctx, outOfBand, selfCanceled = false) {
|
|
|
377
377
|
}
|
|
378
378
|
/** The shared delivering tail of `always` and a CHANGED `on-change`: push
|
|
379
379
|
* stdout to the armed sink, or take the failure path when the sink is
|
|
380
|
-
* undeliverable.
|
|
381
|
-
* switch fallthrough.) */
|
|
380
|
+
* undeliverable. */
|
|
382
381
|
async function deliverOrEscalate(c, ctx, outOfBand, prefix) {
|
|
383
382
|
if (ctx.stdoutHead.trim() === '') {
|
|
384
383
|
return { delivered: `${prefix}empty stdout — nothing delivered`, paused: false };
|
|
@@ -464,8 +463,9 @@ export function recoverStaleCronLeases(now, options = {}) {
|
|
|
464
463
|
}
|
|
465
464
|
/** Wait for a SIGTERMed stale-lease run to actually die, escalating to SIGKILL
|
|
466
465
|
* if it does not — the same bound `killWithEscalation` gives a live timeout.
|
|
467
|
-
* Recovery
|
|
468
|
-
* signal-trapping command
|
|
466
|
+
* Recovery must not record the run as finished the instant it signals: a
|
|
467
|
+
* signal-trapping command can still be running and still producing side
|
|
468
|
+
* effects. */
|
|
469
469
|
async function awaitLeasedRunDeath(c) {
|
|
470
470
|
const deadline = Date.now() + CRON_KILL_GRACE_MS;
|
|
471
471
|
while (Date.now() < deadline) {
|
|
@@ -477,7 +477,7 @@ async function awaitLeasedRunDeath(c) {
|
|
|
477
477
|
}
|
|
478
478
|
/** Settle a stale lease whose process can no longer be observed: the daemon
|
|
479
479
|
* life that owned this run ended (SIGKILL, host reboot) and nothing recorded
|
|
480
|
-
* the outcome. Releasing the lease
|
|
480
|
+
* the outcome. Releasing the lease and nothing else is wrong twice.
|
|
481
481
|
*
|
|
482
482
|
* 1. The run must leave a RECORD. The run log's whole justification is "it
|
|
483
483
|
* silently stopped working Tuesday"; a hole exactly where the crash
|
|
@@ -799,8 +799,8 @@ export function executeCron(c, opts) {
|
|
|
799
799
|
}
|
|
800
800
|
catch (err) {
|
|
801
801
|
// Unparseable recurrence. This row can never be scheduled again as
|
|
802
|
-
// written — but DELETING it
|
|
803
|
-
//
|
|
802
|
+
// written — but DELETING it would destroy the user's config AND its
|
|
803
|
+
// whole run history with no record and no escalation, the
|
|
804
804
|
// one outcome the spec has no vocabulary for. Take the failure path
|
|
805
805
|
// instead: kill the just-started run and hand `close` a process error, so
|
|
806
806
|
// the row PAUSES and an escalation node arrives holding the parse error.
|
|
@@ -3,7 +3,8 @@ import { ApiError } from '../../api/index.js';
|
|
|
3
3
|
import { getNode, subscribersOf } from '../../core/canvas/canvas.js';
|
|
4
4
|
import { ticketDir } from '../../core/human/root.js';
|
|
5
5
|
import { cancelTicket, projectReviewOutput, readTicketResult, takeoverAndCompleteDeck, } from '../../core/human/tickets.js';
|
|
6
|
-
import {
|
|
6
|
+
import { responsePath } from '../../core/human/convention.js';
|
|
7
|
+
import { readJsonOrNull } from '../../core/fs-utils.js';
|
|
7
8
|
import { appendInbox } from '../../core/feed/inbox.js';
|
|
8
9
|
import { FinalDeliveryError, pushFinal } from '../../core/feed/feed.js';
|
|
9
10
|
import { transition } from '../../core/runtime/lifecycle.js';
|
|
@@ -67,7 +68,7 @@ export async function cancelHumanTicket(nodeId, opts) {
|
|
|
67
68
|
*/
|
|
68
69
|
export async function deliverTerminalResult(nodeId) {
|
|
69
70
|
const dir = ticketDir(nodeId);
|
|
70
|
-
const run =
|
|
71
|
+
const run = readJsonOrNull(join(dir, 'run.json'));
|
|
71
72
|
if (run?.mode === 'notify')
|
|
72
73
|
return false;
|
|
73
74
|
const result = readTicketResult(dir);
|
package/dist/daemon/manage.d.ts
CHANGED
|
@@ -78,12 +78,12 @@ export declare function waitForDaemonExit(pid: number, timeoutMs?: number, deps?
|
|
|
78
78
|
/** Stop the RECORDED daemon: SIGTERM, and if it does not exit within the wait
|
|
79
79
|
* window, escalate to SIGKILL exactly as `sweepStrayDaemons` does for a stray.
|
|
80
80
|
*
|
|
81
|
-
* The escalation is the point.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* unstoppable from the CLI: every retry re-
|
|
86
|
-
* ignoring, and `start`
|
|
81
|
+
* The escalation is the point. Without it — SIGTERM, wait, throw on timeout,
|
|
82
|
+
* never reaching the stray sweep — the one daemon `stop` is actually aimed at
|
|
83
|
+
* would be the only crtrd process exempt from the SIGKILL backstop, and a
|
|
84
|
+
* daemon whose shutdown hangs (or outruns the window) becomes permanently
|
|
85
|
+
* unstoppable from the CLI: every retry re-sends a SIGTERM it is already
|
|
86
|
+
* ignoring, and `start` keeps refusing as "already running".
|
|
87
87
|
*
|
|
88
88
|
* A SIGKILL'd daemon runs no exit handler, so it leaves its pidfile behind
|
|
89
89
|
* pointing at a dead pid. That is a stale record, not an owner: `readPidfile`
|
package/dist/daemon/manage.js
CHANGED
|
@@ -193,15 +193,14 @@ export function sweepStrayDaemons(keepPid) {
|
|
|
193
193
|
// Daemon lifecycle transitions are bounded so callers never claim a state
|
|
194
194
|
// change before the pidfile and process liveness agree.
|
|
195
195
|
//
|
|
196
|
-
//
|
|
197
|
-
// generation, possible pending db migration) can take several seconds to
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// code) still fails immediately regardless of this window.
|
|
196
|
+
// A cold boot right after `install-runtime` (empty module cache, fresh
|
|
197
|
+
// generation, possible pending db migration) can take several seconds to reach
|
|
198
|
+
// writePidfile(), so the window is wide enough to tolerate that slow-but-valid
|
|
199
|
+
// cold start rather than report a false "did not become ready"; a process exit
|
|
200
|
+
// (signal/non-zero code) still fails immediately regardless of this window.
|
|
202
201
|
// Exported so `commands/api-client.ts` can align the CLI-side `CrtrClient`'s
|
|
203
202
|
// cold-start `/healthz` poll window with this authoritative valid-startup bound
|
|
204
|
-
//
|
|
203
|
+
// instead of the two windows drifting independently.
|
|
205
204
|
export const DAEMON_VERIFY_WINDOW_MS = 20_000;
|
|
206
205
|
const DAEMON_VERIFY_POLL_MS = 10;
|
|
207
206
|
function sleepMs(ms) {
|
|
@@ -272,12 +271,12 @@ const DAEMON_KILL_WINDOW_MS = 2_000;
|
|
|
272
271
|
/** Stop the RECORDED daemon: SIGTERM, and if it does not exit within the wait
|
|
273
272
|
* window, escalate to SIGKILL exactly as `sweepStrayDaemons` does for a stray.
|
|
274
273
|
*
|
|
275
|
-
* The escalation is the point.
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* unstoppable from the CLI: every retry re-
|
|
280
|
-
* ignoring, and `start`
|
|
274
|
+
* The escalation is the point. Without it — SIGTERM, wait, throw on timeout,
|
|
275
|
+
* never reaching the stray sweep — the one daemon `stop` is actually aimed at
|
|
276
|
+
* would be the only crtrd process exempt from the SIGKILL backstop, and a
|
|
277
|
+
* daemon whose shutdown hangs (or outruns the window) becomes permanently
|
|
278
|
+
* unstoppable from the CLI: every retry re-sends a SIGTERM it is already
|
|
279
|
+
* ignoring, and `start` keeps refusing as "already running".
|
|
281
280
|
*
|
|
282
281
|
* A SIGKILL'd daemon runs no exit handler, so it leaves its pidfile behind
|
|
283
282
|
* pointing at a dead pid. That is a stale record, not an owner: `readPidfile`
|
|
@@ -332,8 +331,8 @@ export async function spawnDaemon() {
|
|
|
332
331
|
// output the daemon doesn't control, plus the narrow set of authorized raw
|
|
333
332
|
// `[crtrd] …` bootstrap/fatal lines that by construction predate or escape
|
|
334
333
|
// canonical event emission (the pre-bind losing-claim line and a cleanup-
|
|
335
|
-
// fatal write in crtrd.ts)
|
|
336
|
-
// started daemon, the common case (only a launchd-owned daemon
|
|
334
|
+
// fatal write in crtrd.ts). Without this fd all of it is discarded for any
|
|
335
|
+
// manually-started daemon, the common case (only a launchd-owned daemon has a
|
|
337
336
|
// logging plist). Every other daemon diagnostic — including per-node
|
|
338
337
|
// supervise/relaunch failures — is a canonical `emitEvent` write, not raw
|
|
339
338
|
// stderr; this file is not where those live. Mirrors host.ts's broker.log
|
|
@@ -344,7 +343,7 @@ export async function spawnDaemon() {
|
|
|
344
343
|
bindDaemonEventSource();
|
|
345
344
|
// `--canvas-home` declares this daemon's canvas ownership in argv so
|
|
346
345
|
// findDaemonPids()/sweepStrayDaemons (above) can match its own daemon
|
|
347
|
-
// exactly instead of scanning the whole machine
|
|
346
|
+
// exactly instead of scanning the whole machine. crtrd-cli.ts's argv
|
|
348
347
|
// parsing only looks for `--tcp`, so this extra pair of tokens is inert
|
|
349
348
|
// there — it exists purely for `ps` visibility.
|
|
350
349
|
const child = spawn(hostExecPath(), [...launchArgs, '--canvas-home', crtrHome()], {
|