@crouton-kit/crouter 0.3.11 → 0.3.13
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/bin/crtrd +2 -0
- package/dist/builtin-personas/design/base.md +9 -0
- package/dist/builtin-personas/design/orchestrator.md +10 -0
- package/dist/builtin-personas/developer/base.md +9 -0
- package/dist/builtin-personas/developer/orchestrator.md +12 -0
- package/dist/builtin-personas/explore/base.md +9 -0
- package/dist/builtin-personas/explore/orchestrator.md +9 -0
- package/dist/builtin-personas/general/base.md +5 -0
- package/dist/builtin-personas/general/orchestrator.md +7 -0
- package/dist/builtin-personas/orchestration-kernel.md +71 -0
- package/dist/builtin-personas/plan/base.md +7 -0
- package/dist/builtin-personas/plan/orchestrator.md +12 -0
- package/dist/builtin-personas/review/base.md +7 -0
- package/dist/builtin-personas/review/orchestrator.md +9 -0
- package/dist/builtin-personas/runtime-base.md +39 -0
- package/dist/builtin-personas/spec/base.md +7 -0
- package/dist/builtin-personas/spec/orchestrator.md +10 -0
- package/dist/builtin-skills/skills/design/SKILL.md +51 -0
- package/dist/builtin-skills/skills/development/SKILL.md +109 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
- package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
- package/dist/cli.js +14 -6
- package/dist/commands/{mode.d.ts → attention.d.ts} +1 -1
- package/dist/commands/attention.js +152 -0
- package/dist/commands/canvas.d.ts +2 -0
- package/dist/commands/canvas.js +35 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +111 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +65 -0
- package/dist/commands/human/prompts.d.ts +5 -0
- package/dist/commands/human/prompts.js +269 -0
- package/dist/commands/human/queue.d.ts +3 -0
- package/dist/commands/human/queue.js +133 -0
- package/dist/commands/human/shared.d.ts +43 -0
- package/dist/commands/human/shared.js +107 -0
- package/dist/commands/human.js +10 -454
- package/dist/commands/node.d.ts +2 -0
- package/dist/commands/node.js +407 -0
- package/dist/commands/pkg/market-inspect.d.ts +1 -0
- package/dist/commands/pkg/market-inspect.js +157 -0
- package/dist/commands/pkg/market-manage.d.ts +1 -0
- package/dist/commands/pkg/market-manage.js +316 -0
- package/dist/commands/pkg/market.d.ts +1 -0
- package/dist/commands/pkg/market.js +16 -0
- package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
- package/dist/commands/pkg/plugin-inspect.js +142 -0
- package/dist/commands/pkg/plugin-manage.d.ts +1 -0
- package/dist/commands/pkg/plugin-manage.js +294 -0
- package/dist/commands/pkg/plugin.d.ts +1 -0
- package/dist/commands/pkg/plugin.js +16 -0
- package/dist/commands/pkg/shared.d.ts +5 -0
- package/dist/commands/pkg/shared.js +61 -0
- package/dist/commands/pkg.js +3 -1004
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.js +159 -0
- package/dist/commands/revive.d.ts +2 -0
- package/dist/commands/revive.js +64 -0
- package/dist/commands/skill/author.d.ts +3 -0
- package/dist/commands/skill/author.js +147 -0
- package/dist/commands/skill/find.d.ts +4 -0
- package/dist/commands/skill/find.js +254 -0
- package/dist/commands/skill/read.d.ts +1 -0
- package/dist/commands/skill/read.js +89 -0
- package/dist/commands/skill/shared.d.ts +19 -0
- package/dist/commands/skill/shared.js +207 -0
- package/dist/commands/skill/state.d.ts +3 -0
- package/dist/commands/skill/state.js +69 -0
- package/dist/commands/skill.js +6 -691
- package/dist/commands/sys/config.d.ts +1 -0
- package/dist/commands/sys/config.js +186 -0
- package/dist/commands/sys/doctor.d.ts +1 -0
- package/dist/commands/sys/doctor.js +369 -0
- package/dist/commands/sys/shared.d.ts +3 -0
- package/dist/commands/sys/shared.js +24 -0
- package/dist/commands/sys/update.d.ts +2 -0
- package/dist/commands/sys/update.js +114 -0
- package/dist/commands/sys.js +4 -694
- package/dist/core/__tests__/argv-parser.test.js +19 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
- package/dist/core/__tests__/canvas.test.js +154 -0
- package/dist/core/__tests__/reset.test.js +105 -0
- package/dist/core/canvas/attention.d.ts +24 -0
- package/dist/core/canvas/attention.js +94 -0
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +210 -0
- package/dist/core/canvas/db.d.ts +7 -0
- package/dist/core/canvas/db.js +61 -0
- package/dist/core/canvas/index.d.ts +4 -0
- package/dist/core/canvas/index.js +6 -0
- package/dist/core/canvas/paths.d.ts +16 -0
- package/dist/core/canvas/paths.js +62 -0
- package/dist/core/canvas/render.d.ts +30 -0
- package/dist/core/canvas/render.js +186 -0
- package/dist/core/canvas/types.d.ts +87 -0
- package/dist/core/canvas/types.js +8 -0
- package/dist/core/command.d.ts +5 -0
- package/dist/core/command.js +35 -10
- package/dist/core/feed/feed.d.ts +43 -0
- package/dist/core/feed/feed.js +116 -0
- package/dist/core/feed/inbox.d.ts +50 -0
- package/dist/core/feed/inbox.js +124 -0
- package/dist/core/help.js +5 -3
- package/dist/core/io.d.ts +15 -1
- package/dist/core/io.js +56 -6
- package/dist/core/personas/index.d.ts +12 -0
- package/dist/core/personas/index.js +10 -0
- package/dist/core/personas/loader.d.ts +44 -0
- package/dist/core/personas/loader.js +157 -0
- package/dist/core/personas/resolve.d.ts +36 -0
- package/dist/core/personas/resolve.js +110 -0
- package/dist/core/render.d.ts +11 -0
- package/dist/core/render.js +126 -0
- package/dist/core/resolver.d.ts +10 -0
- package/dist/core/resolver.js +109 -1
- package/dist/core/runtime/front-door.d.ts +10 -0
- package/dist/core/runtime/front-door.js +97 -0
- package/dist/core/runtime/kickoff.d.ts +23 -0
- package/dist/core/runtime/kickoff.js +134 -0
- package/dist/core/runtime/launch.d.ts +34 -0
- package/dist/core/runtime/launch.js +85 -0
- package/dist/core/runtime/nodes.d.ts +38 -0
- package/dist/core/runtime/nodes.js +95 -0
- package/dist/core/runtime/presence.d.ts +55 -0
- package/dist/core/runtime/presence.js +198 -0
- package/dist/core/runtime/promote.d.ts +30 -0
- package/dist/core/runtime/promote.js +105 -0
- package/dist/core/runtime/reset.d.ts +13 -0
- package/dist/core/runtime/reset.js +97 -0
- package/dist/core/runtime/revive.d.ts +26 -0
- package/dist/core/runtime/revive.js +87 -0
- package/dist/core/runtime/roadmap.d.ts +12 -0
- package/dist/core/runtime/roadmap.js +52 -0
- package/dist/core/runtime/spawn.d.ts +31 -0
- package/dist/core/runtime/spawn.js +123 -0
- package/dist/core/runtime/stop-guard.d.ts +18 -0
- package/dist/core/runtime/stop-guard.js +33 -0
- package/dist/core/runtime/tmux.d.ts +107 -0
- package/dist/core/runtime/tmux.js +244 -0
- package/dist/core/spawn.d.ts +17 -197
- package/dist/core/spawn.js +16 -539
- package/dist/daemon/crtrd-cli.js +4 -0
- package/dist/daemon/crtrd.d.ts +20 -0
- package/dist/daemon/crtrd.js +200 -0
- package/dist/daemon/manage.d.ts +17 -0
- package/dist/daemon/manage.js +57 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
- package/dist/pi-extensions/canvas-nav.d.ts +32 -0
- package/dist/pi-extensions/canvas-nav.js +536 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
- package/dist/pi-extensions/canvas-stophook.js +396 -0
- package/package.json +6 -5
- package/dist/commands/agent.d.ts +0 -6
- package/dist/commands/agent.js +0 -585
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -192
- package/dist/commands/job.d.ts +0 -11
- package/dist/commands/job.js +0 -384
- package/dist/commands/mode.js +0 -231
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -322
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -299
- package/dist/core/__tests__/flow-leaves.test.js +0 -248
- package/dist/core/__tests__/job.test.js +0 -310
- package/dist/core/__tests__/jobs.test.js +0 -98
- package/dist/core/__tests__/spawn.test.js +0 -138
- package/dist/core/__tests__/subagents.test.d.ts +0 -1
- package/dist/core/__tests__/subagents.test.js +0 -75
- package/dist/core/jobs.d.ts +0 -107
- package/dist/core/jobs.js +0 -565
- package/dist/core/subagents.d.ts +0 -18
- package/dist/core/subagents.js +0 -163
- package/dist/prompts/agent.d.ts +0 -27
- package/dist/prompts/agent.js +0 -184
- package/dist/prompts/debug.d.ts +0 -8
- package/dist/prompts/debug.js +0 -44
- /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
- /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
- /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
- /package/dist/{core/__tests__/spawn.test.d.ts → daemon/crtrd-cli.d.ts} +0 -0
package/dist/core/spawn.js
CHANGED
|
@@ -1,247 +1,19 @@
|
|
|
1
|
-
// Tmux pane
|
|
1
|
+
// Tmux pane detach helpers.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
// Agent-CLI selection: crtr can be hosted by different coding agents (Claude
|
|
11
|
-
// Code, pi). `detectAgentKind()` inspects the environment crtr inherited from
|
|
12
|
-
// its host and `buildAgentCommand()` emits the matching invocation, so a spawn
|
|
13
|
-
// launches a sibling of whatever agent is driving it.
|
|
14
|
-
//
|
|
15
|
-
// Crash detection: the wrapper shell command is:
|
|
16
|
-
// `<agent invocation>; crtr job _fail <job_id>`
|
|
17
|
-
// If the worker calls `crtr job submit` before the agent exits, result.json is
|
|
18
|
-
// written and `_fail` is a no-op (writeResult is idempotent for done status).
|
|
19
|
-
// If the agent dies without a submit, `_fail` writes status 'failed'. Either way
|
|
20
|
-
// `job read result` sees a terminal result.json.
|
|
21
|
-
import { spawnSync, spawn } from 'node:child_process';
|
|
3
|
+
// A small set of tmux primitives used by the `human` command tree to put the
|
|
4
|
+
// humanloop TUI in a detached pane: spawnAndDetach (open a pane running a given
|
|
5
|
+
// command), countPanesInCurrentWindow (placement decision), plus shellQuote and
|
|
6
|
+
// isInTmux. The canvas runtime has its own one-window-per-node machinery in
|
|
7
|
+
// core/runtime/tmux.ts; this module is only the pane-split path the human TUI
|
|
8
|
+
// needs.
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
22
10
|
export function isInTmux() {
|
|
23
11
|
return Boolean(process.env.TMUX);
|
|
24
12
|
}
|
|
25
13
|
export function shellQuote(s) {
|
|
26
14
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
27
15
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Foreground `comm` names that mark a tmux pane as hosting an interactive
|
|
30
|
-
* agent. claude reports `claude`; pi sets process.title to `pi`. Used by
|
|
31
|
-
* findWindowWithSpace so panes of EITHER agent count as agent panes and a
|
|
32
|
-
* mixed window still qualifies for placement.
|
|
33
|
-
*/
|
|
34
|
-
const AGENT_COMMS = new Set(['claude', 'pi']);
|
|
35
|
-
/**
|
|
36
|
-
* Detect which coding-agent CLI is hosting the current crtr process so spawns
|
|
37
|
-
* launch a matching sibling. pi exports `PI_CODING_AGENT=true` into its tool
|
|
38
|
-
* subprocess environment; Claude Code exports `CLAUDECODE` /
|
|
39
|
-
* `CLAUDE_CODE_SESSION_ID`. Defaults to claude when no signal is present
|
|
40
|
-
* (preserves prior behavior).
|
|
41
|
-
*/
|
|
42
|
-
export function detectAgentKind() {
|
|
43
|
-
if (process.env.PI_CODING_AGENT === 'true')
|
|
44
|
-
return 'pi';
|
|
45
|
-
return 'claude';
|
|
46
|
-
}
|
|
47
|
-
/** Bare Claude-Code model aliases that subagent frontmatter uses. */
|
|
48
|
-
const CLAUDE_MODEL_ALIASES = new Set(['sonnet', 'opus', 'haiku']);
|
|
49
|
-
/**
|
|
50
|
-
* Normalize a `--model` value for the target agent CLI.
|
|
51
|
-
*
|
|
52
|
-
* Subagent frontmatter uses Claude Code's bare aliases (`sonnet`, `opus`,
|
|
53
|
-
* `haiku`, optionally with a `:thinking` suffix). The `claude` CLI resolves
|
|
54
|
-
* those natively, but `pi` maps a bare alias to its default provider —
|
|
55
|
-
* `amazon-bedrock` — which most users have not authenticated, so the spawn
|
|
56
|
-
* dies with "No API key found for amazon-bedrock". These aliases name Anthropic
|
|
57
|
-
* models, so under pi we pin them to the `anthropic/` provider (preserving any
|
|
58
|
-
* `:thinking` suffix). Values that already carry a `provider/` prefix or are
|
|
59
|
-
* concrete model ids are passed through untouched.
|
|
60
|
-
*/
|
|
61
|
-
export function normalizeModelForKind(model, kind) {
|
|
62
|
-
if (kind !== 'pi')
|
|
63
|
-
return model;
|
|
64
|
-
if (model.includes('/'))
|
|
65
|
-
return model;
|
|
66
|
-
const [base, ...rest] = model.split(':');
|
|
67
|
-
if (!CLAUDE_MODEL_ALIASES.has(base.toLowerCase()))
|
|
68
|
-
return model;
|
|
69
|
-
const suffix = rest.length > 0 ? `:${rest.join(':')}` : '';
|
|
70
|
-
return `anthropic/${base.toLowerCase()}${suffix}`;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Build the agent-CLI invocation (no job wrapper) for the given kind.
|
|
74
|
-
*
|
|
75
|
-
* claude: `claude [-n <name>] [--resume <id> --fork-session] \
|
|
76
|
-
* --dangerously-skip-permissions <prompt>`
|
|
77
|
-
* pi: `pi [-n <name>] [--fork <id>] <prompt>`
|
|
78
|
-
*
|
|
79
|
-
* pi has no permission popups, so it needs no skip-permissions flag.
|
|
80
|
-
*/
|
|
81
|
-
export function buildAgentCommand(opts, kind = detectAgentKind()) {
|
|
82
|
-
if (kind === 'pi') {
|
|
83
|
-
const parts = ['pi'];
|
|
84
|
-
if (opts.name !== undefined && opts.name !== '') {
|
|
85
|
-
parts.push('-n', shellQuote(opts.name));
|
|
86
|
-
}
|
|
87
|
-
if (opts.fork !== undefined) {
|
|
88
|
-
parts.push('--fork', shellQuote(opts.fork.sessionId));
|
|
89
|
-
}
|
|
90
|
-
if (opts.model !== undefined && opts.model !== '') {
|
|
91
|
-
parts.push('--model', shellQuote(normalizeModelForKind(opts.model, 'pi')));
|
|
92
|
-
}
|
|
93
|
-
if (opts.tools !== undefined && opts.tools.length > 0) {
|
|
94
|
-
parts.push('--tools', shellQuote(opts.tools.join(',')));
|
|
95
|
-
}
|
|
96
|
-
if (opts.systemPrompt !== undefined && opts.systemPrompt.trim() !== '') {
|
|
97
|
-
parts.push('--append-system-prompt', shellQuote(opts.systemPrompt));
|
|
98
|
-
}
|
|
99
|
-
parts.push(shellQuote(opts.prompt));
|
|
100
|
-
return parts.join(' ');
|
|
101
|
-
}
|
|
102
|
-
const parts = ['claude'];
|
|
103
|
-
if (opts.name !== undefined && opts.name !== '') {
|
|
104
|
-
parts.push('-n', shellQuote(opts.name));
|
|
105
|
-
}
|
|
106
|
-
if (opts.fork !== undefined) {
|
|
107
|
-
parts.push('--resume', shellQuote(opts.fork.sessionId), '--fork-session');
|
|
108
|
-
}
|
|
109
|
-
if (opts.model !== undefined && opts.model !== '') {
|
|
110
|
-
parts.push('--model', shellQuote(opts.model));
|
|
111
|
-
}
|
|
112
|
-
if (opts.systemPrompt !== undefined && opts.systemPrompt.trim() !== '') {
|
|
113
|
-
parts.push('--append-system-prompt', shellQuote(opts.systemPrompt));
|
|
114
|
-
}
|
|
115
|
-
parts.push('--dangerously-skip-permissions', shellQuote(opts.prompt));
|
|
116
|
-
return parts.join(' ');
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Argv for a non-interactive print-mode run.
|
|
120
|
-
*
|
|
121
|
-
* claude: `claude [-n <name>] [--resume <id> --fork-session] -p \
|
|
122
|
-
* --dangerously-skip-permissions <prompt>`
|
|
123
|
-
* pi: `pi [-n <name>] [--fork <id>] -p <prompt>`
|
|
124
|
-
*
|
|
125
|
-
* Returned as a cmd + args array so callers can spawn without a shell.
|
|
126
|
-
*/
|
|
127
|
-
export function buildAgentPrintArgv(opts, kind = detectAgentKind()) {
|
|
128
|
-
if (kind === 'pi') {
|
|
129
|
-
const args = [];
|
|
130
|
-
if (opts.name !== undefined && opts.name !== '')
|
|
131
|
-
args.push('-n', opts.name);
|
|
132
|
-
if (opts.fork !== undefined)
|
|
133
|
-
args.push('--fork', opts.fork.sessionId);
|
|
134
|
-
if (opts.model !== undefined && opts.model !== '')
|
|
135
|
-
args.push('--model', normalizeModelForKind(opts.model, 'pi'));
|
|
136
|
-
if (opts.tools !== undefined && opts.tools.length > 0)
|
|
137
|
-
args.push('--tools', opts.tools.join(','));
|
|
138
|
-
if (opts.systemPrompt !== undefined && opts.systemPrompt.trim() !== '') {
|
|
139
|
-
args.push('--append-system-prompt', opts.systemPrompt);
|
|
140
|
-
}
|
|
141
|
-
args.push('-p', opts.prompt);
|
|
142
|
-
return { cmd: 'pi', args };
|
|
143
|
-
}
|
|
144
|
-
const args = [];
|
|
145
|
-
if (opts.name !== undefined && opts.name !== '')
|
|
146
|
-
args.push('-n', opts.name);
|
|
147
|
-
if (opts.fork !== undefined)
|
|
148
|
-
args.push('--resume', opts.fork.sessionId, '--fork-session');
|
|
149
|
-
if (opts.model !== undefined && opts.model !== '')
|
|
150
|
-
args.push('--model', opts.model);
|
|
151
|
-
if (opts.systemPrompt !== undefined && opts.systemPrompt.trim() !== '') {
|
|
152
|
-
args.push('--append-system-prompt', opts.systemPrompt);
|
|
153
|
-
}
|
|
154
|
-
args.push('-p', '--dangerously-skip-permissions', opts.prompt);
|
|
155
|
-
return { cmd: 'claude', args };
|
|
156
|
-
}
|
|
157
|
-
/** Same as buildAgentPrintArgv but rendered as a single shell-quoted string. */
|
|
158
|
-
export function buildAgentPrintCommand(opts, kind = detectAgentKind()) {
|
|
159
|
-
const { cmd, args } = buildAgentPrintArgv(opts, kind);
|
|
160
|
-
return [cmd, ...args.map(shellQuote)].join(' ');
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Run the agent headlessly and resolve once it exits. A blocking caller awaits
|
|
164
|
-
* this. stdout is captured as the result; a non-zero exit yields status
|
|
165
|
-
* 'failed' with the combined output.
|
|
166
|
-
*/
|
|
167
|
-
export function runAgentHeadless(opts) {
|
|
168
|
-
const { cmd, args } = buildAgentPrintArgv({
|
|
169
|
-
prompt: opts.prompt,
|
|
170
|
-
name: opts.name,
|
|
171
|
-
systemPrompt: opts.systemPrompt,
|
|
172
|
-
model: opts.model,
|
|
173
|
-
tools: opts.tools,
|
|
174
|
-
});
|
|
175
|
-
return new Promise((resolve) => {
|
|
176
|
-
let out = '';
|
|
177
|
-
let err = '';
|
|
178
|
-
let child;
|
|
179
|
-
try {
|
|
180
|
-
// stdin 'ignore' so the agent never blocks waiting for stdin EOF (the
|
|
181
|
-
// prompt is passed as an argv arg, not piped).
|
|
182
|
-
child = spawn(cmd, args, { cwd: opts.cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
183
|
-
}
|
|
184
|
-
catch (e) {
|
|
185
|
-
resolve({ status: 'failed', output: `failed to launch ${cmd}: ${String(e)}`, exitCode: null });
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
child.stdout?.on('data', (d) => { out += d.toString(); });
|
|
189
|
-
child.stderr?.on('data', (d) => { err += d.toString(); });
|
|
190
|
-
child.on('error', (e) => {
|
|
191
|
-
resolve({ status: 'failed', output: `failed to launch ${cmd}: ${String(e)}`, exitCode: null });
|
|
192
|
-
});
|
|
193
|
-
child.on('close', (code) => {
|
|
194
|
-
if (code === 0) {
|
|
195
|
-
resolve({ status: 'done', output: out.trim() !== '' ? out : '(agent produced no output)', exitCode: 0 });
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
const combined = `${out}${err}`.trim();
|
|
199
|
-
resolve({
|
|
200
|
-
status: 'failed',
|
|
201
|
-
output: combined !== '' ? combined : `agent exited with code ${code ?? 'null'}`,
|
|
202
|
-
exitCode: code,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Launch a headless agent detached (background). Its print output is captured
|
|
210
|
-
* and delivered to the job via `crtr job submit`; a non-zero exit marks the job
|
|
211
|
-
* failed. Returns immediately with the wrapper pid (recorded for crash
|
|
212
|
-
* detection). No tmux required.
|
|
213
|
-
*/
|
|
214
|
-
export function spawnHeadlessDetached(opts) {
|
|
215
|
-
const agentCmd = buildAgentPrintCommand({
|
|
216
|
-
prompt: opts.prompt,
|
|
217
|
-
name: opts.name,
|
|
218
|
-
systemPrompt: opts.systemPrompt,
|
|
219
|
-
model: opts.model,
|
|
220
|
-
tools: opts.tools,
|
|
221
|
-
});
|
|
222
|
-
const jid = shellQuote(opts.jobId);
|
|
223
|
-
// On failure, forward the captured stdout+stderr as the result body so the
|
|
224
|
-
// real cause (e.g. an auth error) is visible via `crtr job read result`/`logs`
|
|
225
|
-
// instead of being discarded behind a generic exit-code reason.
|
|
226
|
-
const wrapper = `out="$(${agentCmd} 2>&1)"; ec=$?; ` +
|
|
227
|
-
`if [ "$ec" -eq 0 ]; then ` +
|
|
228
|
-
`if [ -z "$out" ]; then out='(agent produced no output)'; fi; ` +
|
|
229
|
-
`printf '%s' "$out" | crtr job submit ${jid}; ` +
|
|
230
|
-
`else printf '%s' "$out" | crtr job submit ${jid} --status failed --reason "agent exited with code $ec"; fi`;
|
|
231
|
-
try {
|
|
232
|
-
const child = spawn('sh', ['-c', wrapper], {
|
|
233
|
-
cwd: opts.cwd,
|
|
234
|
-
env: process.env,
|
|
235
|
-
detached: true,
|
|
236
|
-
stdio: 'ignore',
|
|
237
|
-
});
|
|
238
|
-
child.unref();
|
|
239
|
-
return { status: 'spawned', pid: child.pid, message: `headless agent started (pid ${child.pid ?? 'unknown'})` };
|
|
240
|
-
}
|
|
241
|
-
catch (e) {
|
|
242
|
-
return { status: 'spawn-failed', message: `failed to launch headless agent: ${String(e)}` };
|
|
243
|
-
}
|
|
244
|
-
}
|
|
16
|
+
/** Count panes in the current tmux window (0 outside tmux / on error). */
|
|
245
17
|
export function countPanesInCurrentWindow() {
|
|
246
18
|
const result = spawnSync('tmux', ['list-panes', '-F', '#{pane_id}'], {
|
|
247
19
|
encoding: 'utf8',
|
|
@@ -250,135 +22,10 @@ export function countPanesInCurrentWindow() {
|
|
|
250
22
|
return 0;
|
|
251
23
|
return result.stdout.split('\n').filter((line) => line.trim() !== '').length;
|
|
252
24
|
}
|
|
253
|
-
function listWindowsInCurrentSession() {
|
|
254
|
-
const result = spawnSync('tmux', ['list-windows', '-F', '#{window_id} #{window_panes} #{window_active}'], { encoding: 'utf8' });
|
|
255
|
-
if (result.status !== 0)
|
|
256
|
-
return [];
|
|
257
|
-
return result.stdout
|
|
258
|
-
.split('\n')
|
|
259
|
-
.filter((line) => line.trim() !== '')
|
|
260
|
-
.map((line) => {
|
|
261
|
-
const [id, count, active] = line.split(' ');
|
|
262
|
-
return {
|
|
263
|
-
windowId: id,
|
|
264
|
-
paneCount: Number.parseInt(count, 10),
|
|
265
|
-
isActive: active === '1',
|
|
266
|
-
};
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Map of window_id → list of pane TTYs (basename, e.g. `ttys008`) for every
|
|
271
|
-
* pane in the current tmux session. Used as the bridge between tmux's pane
|
|
272
|
-
* model and the system process table for foreground-command lookup.
|
|
273
|
-
*
|
|
274
|
-
* tmux's `#{pane_current_command}` is unreliable on macOS because the Claude
|
|
275
|
-
* Code CLI sets `process.title` to its version (e.g. `2.1.143`), which is what
|
|
276
|
-
* tmux then reports. Going through the TTY + `ps` gives us the real foreground
|
|
277
|
-
* `comm` (`claude`, or `pi` from its process.title) from the kernel.
|
|
278
|
-
*/
|
|
279
|
-
function paneTtysByWindow() {
|
|
280
|
-
const result = spawnSync('tmux', ['list-panes', '-s', '-F', '#{window_id} #{pane_tty}'], { encoding: 'utf8' });
|
|
281
|
-
const out = new Map();
|
|
282
|
-
if (result.status !== 0)
|
|
283
|
-
return out;
|
|
284
|
-
for (const line of result.stdout.split('\n')) {
|
|
285
|
-
if (line.trim() === '')
|
|
286
|
-
continue;
|
|
287
|
-
const idx = line.indexOf(' ');
|
|
288
|
-
if (idx === -1)
|
|
289
|
-
continue;
|
|
290
|
-
const windowId = line.slice(0, idx);
|
|
291
|
-
const tty = line.slice(idx + 1);
|
|
292
|
-
const ttyBase = tty.startsWith('/dev/') ? tty.slice(5) : tty;
|
|
293
|
-
const existing = out.get(windowId);
|
|
294
|
-
if (existing === undefined) {
|
|
295
|
-
out.set(windowId, [ttyBase]);
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
existing.push(ttyBase);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return out;
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Map of tty basename → set of foreground process `comm` names on that tty.
|
|
305
|
-
* A process is "foreground" if its STAT field includes `+` (member of the
|
|
306
|
-
* terminal's foreground process group). Built from one `ps -axo ...` call.
|
|
307
|
-
*/
|
|
308
|
-
function foregroundCommsByTty() {
|
|
309
|
-
const result = spawnSync('ps', ['-axo', 'stat=,comm=,tty='], { encoding: 'utf8' });
|
|
310
|
-
const out = new Map();
|
|
311
|
-
if (result.status !== 0)
|
|
312
|
-
return out;
|
|
313
|
-
for (const line of result.stdout.split('\n')) {
|
|
314
|
-
if (line.trim() === '')
|
|
315
|
-
continue;
|
|
316
|
-
const m = line.match(/^(\S+)\s+(.+?)\s+(\S+)\s*$/);
|
|
317
|
-
if (m === null)
|
|
318
|
-
continue;
|
|
319
|
-
const [, stat, comm, tty] = m;
|
|
320
|
-
if (!stat.includes('+'))
|
|
321
|
-
continue;
|
|
322
|
-
if (tty === '??' || tty === '?')
|
|
323
|
-
continue;
|
|
324
|
-
const existing = out.get(tty);
|
|
325
|
-
if (existing === undefined) {
|
|
326
|
-
out.set(tty, new Set([comm.trim()]));
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
existing.add(comm.trim());
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
return out;
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Find a window in the current tmux session with fewer than `maxPanesPerWindow`
|
|
336
|
-
* panes AND where every existing pane hosts an agent (claude or pi) as its
|
|
337
|
-
* foreground process. Prefers the active window so the spawned pane is visible
|
|
338
|
-
* to the user; otherwise falls back to the first other eligible window. Returns
|
|
339
|
-
* the tmux window id (e.g. `@5`) to pass via `-t`, or null if no window qualifies.
|
|
340
|
-
*
|
|
341
|
-
* Windows holding non-agent panes (dashboards, log tails, idle shells, editors,
|
|
342
|
-
* REPLs, etc.) are skipped so spawning never disrupts those workflows. A pane
|
|
343
|
-
* qualifies as long as an agent comm is among its foreground commands —
|
|
344
|
-
* co-resident helpers like `caffeinate` don't disqualify it.
|
|
345
|
-
*/
|
|
346
|
-
export function findWindowWithSpace(maxPanesPerWindow) {
|
|
347
|
-
const windows = listWindowsInCurrentSession();
|
|
348
|
-
const ttysByWindow = paneTtysByWindow();
|
|
349
|
-
const fgByTty = foregroundCommsByTty();
|
|
350
|
-
const isAgentOnly = (windowId) => {
|
|
351
|
-
const ttys = ttysByWindow.get(windowId);
|
|
352
|
-
if (ttys === undefined || ttys.length === 0)
|
|
353
|
-
return false;
|
|
354
|
-
return ttys.every((tty) => {
|
|
355
|
-
const comms = fgByTty.get(tty);
|
|
356
|
-
if (comms === undefined)
|
|
357
|
-
return false;
|
|
358
|
-
for (const c of comms) {
|
|
359
|
-
if (AGENT_COMMS.has(c))
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
return false;
|
|
363
|
-
});
|
|
364
|
-
};
|
|
365
|
-
const eligible = windows.filter((w) => w.paneCount < maxPanesPerWindow && isAgentOnly(w.windowId));
|
|
366
|
-
const active = eligible.find((w) => w.isActive);
|
|
367
|
-
if (active !== undefined)
|
|
368
|
-
return active.windowId;
|
|
369
|
-
const first = eligible[0];
|
|
370
|
-
if (first === undefined)
|
|
371
|
-
return null;
|
|
372
|
-
return first.windowId;
|
|
373
|
-
}
|
|
374
25
|
/**
|
|
375
26
|
* Schedule a kill-pane on the *current* tmux pane after `delaySeconds`, detached
|
|
376
|
-
* so the caller can return normally before the pane dies. No-op outside tmux
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
* Used by `crtr job submit` (kill_pane=true) so a reviewer agent can self-close
|
|
380
|
-
* its pane after delivering its verdict, and by `spawnAndDetach` for handoff
|
|
381
|
-
* self-kill.
|
|
27
|
+
* so the caller can return normally before the pane dies. No-op outside tmux,
|
|
28
|
+
* when TMUX_PANE is unset, or when delaySeconds <= 0.
|
|
382
29
|
*/
|
|
383
30
|
export function scheduleKillCurrentPane(delaySeconds) {
|
|
384
31
|
const currentPane = process.env.TMUX_PANE;
|
|
@@ -392,24 +39,9 @@ export function scheduleKillCurrentPane(delaySeconds) {
|
|
|
392
39
|
return true;
|
|
393
40
|
}
|
|
394
41
|
/**
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
* If the worker submits via `crtr job submit` before the agent exits,
|
|
400
|
-
* result.json is already written (`done`); `_fail` sees it and is a no-op.
|
|
401
|
-
* If the agent crashes/exits without submitting, `_fail` writes status `failed`
|
|
402
|
-
* so `job read result` can distinguish completion from crash.
|
|
403
|
-
*/
|
|
404
|
-
function wrapperCmd(agentCmd, jobId) {
|
|
405
|
-
return `${agentCmd}; crtr job _fail ${shellQuote(jobId)}`;
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* Fire-and-forget: launch an interactive agent in a new pane (or window),
|
|
409
|
-
* then schedule the originating pane to be killed after `killAfterSeconds`.
|
|
410
|
-
*
|
|
411
|
-
* No custom system prompt — the task is delivered as the first user message.
|
|
412
|
-
* Returns as soon as the new pane is up; does NOT wait for the agent to finish.
|
|
42
|
+
* Fire-and-forget: launch `opts.command` in a new pane (or window), then
|
|
43
|
+
* schedule the originating pane to be killed after `killAfterSeconds`. Returns
|
|
44
|
+
* as soon as the new pane is up; does NOT wait for the command to finish.
|
|
413
45
|
*/
|
|
414
46
|
export function spawnAndDetach(opts) {
|
|
415
47
|
if (!isInTmux()) {
|
|
@@ -418,11 +50,6 @@ export function spawnAndDetach(opts) {
|
|
|
418
50
|
message: 'handoff requires tmux (TMUX env var not set)',
|
|
419
51
|
};
|
|
420
52
|
}
|
|
421
|
-
const inner = opts.command !== undefined
|
|
422
|
-
? opts.command
|
|
423
|
-
: buildAgentCommand({ prompt: opts.prompt, name: opts.name });
|
|
424
|
-
const useFailGuard = opts.failGuard !== false;
|
|
425
|
-
const fullCmd = useFailGuard ? wrapperCmd(inner, opts.jobId) : inner;
|
|
426
53
|
const splitArgs = [];
|
|
427
54
|
if (opts.placement === 'new-window') {
|
|
428
55
|
splitArgs.push('new-window');
|
|
@@ -443,7 +70,7 @@ export function spawnAndDetach(opts) {
|
|
|
443
70
|
if (opts.jobId !== undefined) {
|
|
444
71
|
splitArgs.push('-e', `CRTR_JOB_ID=${opts.jobId}`);
|
|
445
72
|
}
|
|
446
|
-
splitArgs.push(
|
|
73
|
+
splitArgs.push(opts.command);
|
|
447
74
|
const split = spawnSync('tmux', splitArgs, { encoding: 'utf8' });
|
|
448
75
|
if (split.status !== 0) {
|
|
449
76
|
const stderrText = split.stderr.trim();
|
|
@@ -456,156 +83,6 @@ export function spawnAndDetach(opts) {
|
|
|
456
83
|
return {
|
|
457
84
|
status: 'spawned',
|
|
458
85
|
paneId,
|
|
459
|
-
message: `handed off to pane ${paneId}
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
// ---------------------------------------------------------------------------
|
|
463
|
-
// Dedicated subagent session
|
|
464
|
-
//
|
|
465
|
-
// Every subagent crtr spawns lands in a tmux session dedicated to the pi/claude
|
|
466
|
-
// session that launched it, instead of splitting the user's working window. The
|
|
467
|
-
// session is keyed on the originating pane ($TMUX_PANE) so it is reused across
|
|
468
|
-
// spawns for the life of that pane. Spawns are interactive (headed) panes but
|
|
469
|
-
// never steal focus — the user jumps to the session with Alt-o.
|
|
470
|
-
//
|
|
471
|
-
// Navigation state is written as tmux user-options so a keybinding can toggle
|
|
472
|
-
// between the two without crtr involvement:
|
|
473
|
-
// - origin session: @crtr_subagent_session = <subagent session name>
|
|
474
|
-
// - subagent session: @crtr_origin_session = <origin session id>
|
|
475
|
-
// @crtr_origin_pane = <origin pane id>
|
|
476
|
-
// ---------------------------------------------------------------------------
|
|
477
|
-
function tmuxQuery(args) {
|
|
478
|
-
const r = spawnSync('tmux', args, { encoding: 'utf8' });
|
|
479
|
-
if (r.status !== 0)
|
|
480
|
-
return null;
|
|
481
|
-
return r.stdout.trim();
|
|
482
|
-
}
|
|
483
|
-
/** Originating pane id + session id of the host (pi/claude) crtr runs under. */
|
|
484
|
-
export function originContext() {
|
|
485
|
-
const pane = process.env.TMUX_PANE;
|
|
486
|
-
if (pane === undefined || pane === '')
|
|
487
|
-
return null;
|
|
488
|
-
const sessionId = tmuxQuery(['display-message', '-p', '-t', pane, '#{session_id}']);
|
|
489
|
-
if (sessionId === null || sessionId === '')
|
|
490
|
-
return null;
|
|
491
|
-
return { pane, sessionId };
|
|
492
|
-
}
|
|
493
|
-
/** Deterministic subagent session name for an originating pane id (e.g. `%5`). */
|
|
494
|
-
export function subagentSessionName(pane) {
|
|
495
|
-
return `crtr-agents-${pane.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
496
|
-
}
|
|
497
|
-
/** A window in `session` with fewer than `maxPanes` panes (active preferred). */
|
|
498
|
-
function findWindowWithSpaceInSession(session, maxPanes) {
|
|
499
|
-
const r = spawnSync('tmux', ['list-windows', '-t', session, '-F', '#{window_id} #{window_panes} #{window_active}'], { encoding: 'utf8' });
|
|
500
|
-
if (r.status !== 0)
|
|
501
|
-
return null;
|
|
502
|
-
const wins = r.stdout
|
|
503
|
-
.split('\n')
|
|
504
|
-
.filter((l) => l.trim() !== '')
|
|
505
|
-
.map((l) => {
|
|
506
|
-
const [id, count, active] = l.split(' ');
|
|
507
|
-
return { id, count: Number.parseInt(count, 10), active: active === '1' };
|
|
508
|
-
});
|
|
509
|
-
const eligible = wins.filter((w) => w.count < maxPanes);
|
|
510
|
-
const active = eligible.find((w) => w.active);
|
|
511
|
-
if (active !== undefined)
|
|
512
|
-
return active.id;
|
|
513
|
-
return eligible[0]?.id ?? null;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
|
-
* Ensure the dedicated subagent session exists and launch `fullCmd` in it,
|
|
517
|
-
* filling windows up to `maxPanes` before opening a new window. Records the
|
|
518
|
-
* cross-session navigation options on both sides. Never switches focus — the
|
|
519
|
-
* user navigates to the subagent session themselves (Alt-o).
|
|
520
|
-
*/
|
|
521
|
-
function placeInSubagentSession(opts) {
|
|
522
|
-
if (!isInTmux()) {
|
|
523
|
-
return { status: 'not-in-tmux', message: 'requires tmux (TMUX env var not set)' };
|
|
524
|
-
}
|
|
525
|
-
const origin = originContext();
|
|
526
|
-
if (origin === null) {
|
|
527
|
-
return { status: 'not-in-tmux', message: 'requires tmux (TMUX_PANE not set)' };
|
|
528
|
-
}
|
|
529
|
-
const session = subagentSessionName(origin.pane);
|
|
530
|
-
const envArgs = opts.jobId !== undefined ? ['-e', `CRTR_JOB_ID=${opts.jobId}`] : [];
|
|
531
|
-
const exists = spawnSync('tmux', ['has-session', '-t', session], { encoding: 'utf8' }).status === 0;
|
|
532
|
-
let paneId;
|
|
533
|
-
let placement;
|
|
534
|
-
if (!exists) {
|
|
535
|
-
const r = spawnSync('tmux', ['new-session', '-d', '-s', session, '-c', opts.cwd, '-P', '-F', '#{pane_id}', ...envArgs, opts.fullCmd], { encoding: 'utf8' });
|
|
536
|
-
if (r.status !== 0) {
|
|
537
|
-
return { status: 'spawn-failed', message: r.stderr.trim() || 'tmux new-session failed' };
|
|
538
|
-
}
|
|
539
|
-
paneId = r.stdout.trim();
|
|
540
|
-
placement = 'new-session';
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
const targetWindow = findWindowWithSpaceInSession(session, opts.maxPanes);
|
|
544
|
-
if (targetWindow === null) {
|
|
545
|
-
const r = spawnSync('tmux', ['new-window', '-t', session, '-c', opts.cwd, '-P', '-F', '#{pane_id}', ...envArgs, opts.fullCmd], { encoding: 'utf8' });
|
|
546
|
-
if (r.status !== 0) {
|
|
547
|
-
return { status: 'spawn-failed', message: r.stderr.trim() || 'tmux new-window failed' };
|
|
548
|
-
}
|
|
549
|
-
paneId = r.stdout.trim();
|
|
550
|
-
placement = 'new-window';
|
|
551
|
-
}
|
|
552
|
-
else {
|
|
553
|
-
const r = spawnSync('tmux', ['split-window', '-h', '-t', targetWindow, '-c', opts.cwd, '-P', '-F', '#{pane_id}', ...envArgs, opts.fullCmd], { encoding: 'utf8' });
|
|
554
|
-
if (r.status !== 0) {
|
|
555
|
-
return { status: 'spawn-failed', message: r.stderr.trim() || 'tmux split-window failed' };
|
|
556
|
-
}
|
|
557
|
-
paneId = r.stdout.trim();
|
|
558
|
-
placement = 'split-window';
|
|
559
|
-
spawnSync('tmux', ['select-layout', '-t', paneId, 'even-horizontal'], { encoding: 'utf8' });
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
// Record navigation state for the M-o toggle keybinding.
|
|
563
|
-
spawnSync('tmux', ['set-option', '-t', origin.sessionId, '@crtr_subagent_session', session], { encoding: 'utf8' });
|
|
564
|
-
spawnSync('tmux', ['set-option', '-t', session, '@crtr_origin_session', origin.sessionId], { encoding: 'utf8' });
|
|
565
|
-
spawnSync('tmux', ['set-option', '-t', session, '@crtr_origin_pane', origin.pane], { encoding: 'utf8' });
|
|
566
|
-
return {
|
|
567
|
-
status: 'spawned',
|
|
568
|
-
paneId,
|
|
569
|
-
session,
|
|
570
|
-
placement,
|
|
571
|
-
message: `agent spawned in pane ${paneId} of session ${session} (${placement})`,
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Async sibling spawn. Launches an interactive agent (claude or pi, per
|
|
576
|
-
* detectAgentKind) in the dedicated subagent session, progressively filling
|
|
577
|
-
* windows up to `maxPanesPerWindow` before creating a new window. Returns
|
|
578
|
-
* immediately with the pane id; the parent stays alive. Focus is never
|
|
579
|
-
* switched — the user jumps to the subagent session with Alt-o.
|
|
580
|
-
*
|
|
581
|
-
* If `fork` is set, forks the host session into a fresh one.
|
|
582
|
-
*/
|
|
583
|
-
export function spawnAgent(opts) {
|
|
584
|
-
if (!isInTmux()) {
|
|
585
|
-
return {
|
|
586
|
-
status: 'not-in-tmux',
|
|
587
|
-
message: 'crtr job requires tmux (TMUX env var not set)',
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
const agentCmd = buildAgentCommand({
|
|
591
|
-
prompt: opts.prompt,
|
|
592
|
-
name: opts.name,
|
|
593
|
-
fork: opts.fork,
|
|
594
|
-
systemPrompt: opts.systemPrompt,
|
|
595
|
-
model: opts.model,
|
|
596
|
-
tools: opts.tools,
|
|
597
|
-
});
|
|
598
|
-
const fullCmd = wrapperCmd(agentCmd, opts.jobId);
|
|
599
|
-
const placed = placeInSubagentSession({
|
|
600
|
-
fullCmd,
|
|
601
|
-
jobId: opts.jobId,
|
|
602
|
-
cwd: opts.cwd,
|
|
603
|
-
maxPanes: opts.maxPanesPerWindow,
|
|
604
|
-
});
|
|
605
|
-
return {
|
|
606
|
-
status: placed.status,
|
|
607
|
-
paneId: placed.paneId,
|
|
608
|
-
placement: placed.placement === 'split-window' ? 'split-window' : 'new-window',
|
|
609
|
-
message: placed.message,
|
|
86
|
+
message: `handed off to pane ${paneId}`,
|
|
610
87
|
};
|
|
611
88
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Read the pid stored in the pidfile, or null if absent / malformed. */
|
|
2
|
+
export declare function readPidfile(): number | null;
|
|
3
|
+
/** True if a process with `pid` is currently alive (signal-0 probe). */
|
|
4
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
5
|
+
/** True when a crtrd process is already running (pidfile exists + pid alive). */
|
|
6
|
+
export declare function isDaemonRunning(): boolean;
|
|
7
|
+
export interface DaemonOpts {
|
|
8
|
+
/** Milliseconds between supervision polls. Default 2000. */
|
|
9
|
+
intervalMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Start the supervisor loop.
|
|
12
|
+
*
|
|
13
|
+
* If a live crtrd is already running (pidfile + pid alive), exits immediately
|
|
14
|
+
* (exit 0 — idempotent, not an error). Otherwise, writes the pidfile, sets up
|
|
15
|
+
* signal handlers, and enters the poll loop.
|
|
16
|
+
*
|
|
17
|
+
* Returns a teardown callback that stops the loop and removes the pidfile.
|
|
18
|
+
* (Mainly useful for tests; in production the daemon runs until signaled.) */
|
|
19
|
+
export declare function runDaemon(opts?: DaemonOpts): () => void;
|
|
20
|
+
export default runDaemon;
|