@crouton-kit/crouter 0.3.11 → 0.3.12
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 +354 -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 +38 -0
- package/dist/core/runtime/presence.js +152 -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 +89 -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 +33 -0
- package/dist/core/runtime/spawn.js +118 -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 +88 -0
- package/dist/core/runtime/tmux.js +198 -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 +373 -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/jobs.d.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
type TerminalStatus = 'done' | 'failed' | 'canceled' | 'closed';
|
|
2
|
-
type JobState = 'live' | TerminalStatus;
|
|
3
|
-
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
4
|
-
/**
|
|
5
|
-
* Allocate a new job directory and write meta.json atomically.
|
|
6
|
-
* Returns the job_id and the absolute directory path.
|
|
7
|
-
*/
|
|
8
|
-
export declare function createJob(kind: string, opts: {
|
|
9
|
-
cwd: string;
|
|
10
|
-
pid?: number;
|
|
11
|
-
}): {
|
|
12
|
-
jobId: string;
|
|
13
|
-
dir: string;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Record the tmux pane hosting a detached worker so `cancelJob` can kill it.
|
|
17
|
-
*/
|
|
18
|
-
export declare function recordJobPane(jobId: string, paneId: string): void;
|
|
19
|
-
/**
|
|
20
|
-
* Record the pid of a detached worker (e.g. a headless background agent) so
|
|
21
|
-
* jobStatus can mark the job failed if the process dies without a result.
|
|
22
|
-
*/
|
|
23
|
-
export declare function recordJobPid(jobId: string, pid: number): void;
|
|
24
|
-
/**
|
|
25
|
-
* Append one event line to log.jsonl. Does NOT throw if jobId doesn't exist —
|
|
26
|
-
* a crashed writer should not further corrupt state; use a guard at the call site.
|
|
27
|
-
*/
|
|
28
|
-
export declare function appendEvent(jobId: string, event: {
|
|
29
|
-
level: LogLevel;
|
|
30
|
-
event: string;
|
|
31
|
-
message: string;
|
|
32
|
-
data?: object;
|
|
33
|
-
}): void;
|
|
34
|
-
/**
|
|
35
|
-
* Atomically write result.json (structured object) and update meta.json status.
|
|
36
|
-
* Used by programmatic callers (human, sys) that produce object results.
|
|
37
|
-
* The result file's appearance is the completion signal — never inferred from log content.
|
|
38
|
-
*/
|
|
39
|
-
export declare function writeResult(jobId: string, result: object, terminalStatus: TerminalStatus): void;
|
|
40
|
-
/**
|
|
41
|
-
* Atomically write result.md (YAML frontmatter + markdown body) and update meta.json status.
|
|
42
|
-
* Used by `crtr job submit` for agent-driven markdown results.
|
|
43
|
-
*/
|
|
44
|
-
export declare function writeMarkdownResult(jobId: string, body: string, terminalStatus: TerminalStatus, reason?: string): void;
|
|
45
|
-
/**
|
|
46
|
-
* Read whichever result file exists (result.md or result.json). If neither
|
|
47
|
-
* exists and waitMs is given, block via fs.watch until one appears or the
|
|
48
|
-
* timeout elapses.
|
|
49
|
-
*
|
|
50
|
-
* Race safety: registers the watcher THEN re-stats. If a result file appeared
|
|
51
|
-
* between the first stat and the watch registration, the re-stat catches it
|
|
52
|
-
* before the watcher has a chance to miss it.
|
|
53
|
-
*
|
|
54
|
-
* Returns shape:
|
|
55
|
-
* - JSON path: { status, result: object }
|
|
56
|
-
* - Markdown path: { status, result_md: string, reason?: string }
|
|
57
|
-
* - Timeout: { status: 'timeout' }
|
|
58
|
-
* - Closed: pane vanished with no result → status 'closed'
|
|
59
|
-
*/
|
|
60
|
-
export interface ReadResultResponse {
|
|
61
|
-
status: 'done' | 'failed' | 'canceled' | 'closed' | 'timeout';
|
|
62
|
-
result?: object;
|
|
63
|
-
result_md?: string;
|
|
64
|
-
reason?: string;
|
|
65
|
-
}
|
|
66
|
-
export declare function readResult(jobId: string, opts?: {
|
|
67
|
-
waitMs?: number;
|
|
68
|
-
}): Promise<ReadResultResponse>;
|
|
69
|
-
/**
|
|
70
|
-
* Derive job state from meta.json, the result file, and the tail of log.jsonl.
|
|
71
|
-
* If a pid is recorded, is not alive, and no result file exists → 'failed'.
|
|
72
|
-
*/
|
|
73
|
-
export declare function jobStatus(jobId: string): {
|
|
74
|
-
state: JobState;
|
|
75
|
-
age_s: number;
|
|
76
|
-
last_event: {
|
|
77
|
-
event: string;
|
|
78
|
-
ts: string;
|
|
79
|
-
} | null;
|
|
80
|
-
};
|
|
81
|
-
/**
|
|
82
|
-
* List all jobs sorted by created_at ascending. Pagination is applied by the
|
|
83
|
-
* caller, not here.
|
|
84
|
-
*/
|
|
85
|
-
export declare function listJobs(): {
|
|
86
|
-
job_id: string;
|
|
87
|
-
kind: string;
|
|
88
|
-
state: JobState;
|
|
89
|
-
created_at: string;
|
|
90
|
-
}[];
|
|
91
|
-
/**
|
|
92
|
-
* Read and filter log events. Ordering preserved. sinceTs/untilTs are ISO8601
|
|
93
|
-
* strings; minLevel filters by severity rank (inclusive).
|
|
94
|
-
*/
|
|
95
|
-
export declare function readLog(jobId: string, opts?: {
|
|
96
|
-
sinceTs?: string;
|
|
97
|
-
untilTs?: string;
|
|
98
|
-
minLevel?: LogLevel;
|
|
99
|
-
}): object[];
|
|
100
|
-
/**
|
|
101
|
-
* Best-effort cancel: send SIGTERM to the recorded pid (if any), mark meta
|
|
102
|
-
* canceled. Success means the signal was delivered, not that execution stopped.
|
|
103
|
-
*/
|
|
104
|
-
export declare function cancelJob(jobId: string): {
|
|
105
|
-
canceled: boolean;
|
|
106
|
-
};
|
|
107
|
-
export {};
|
package/dist/core/jobs.js
DELETED
|
@@ -1,565 +0,0 @@
|
|
|
1
|
-
// Job / long-running-operation infrastructure.
|
|
2
|
-
//
|
|
3
|
-
// Files are the single source of truth. No in-memory registry. An agent picks
|
|
4
|
-
// up a job by id across processes. Crashes recover by reading files.
|
|
5
|
-
//
|
|
6
|
-
// Layout: ${XDG_STATE_HOME or ~/.local/state}/crtr/jobs/<job_id>/
|
|
7
|
-
// meta.json — written atomically on create; updated atomically on terminal transition.
|
|
8
|
-
// log.jsonl — append-only event log.
|
|
9
|
-
// result.md — agent submissions (markdown body + YAML frontmatter). Written atomically.
|
|
10
|
-
// result.json — programmatic submissions (structured object). Written atomically.
|
|
11
|
-
// Either result file's APPEARANCE is the completion signal. Exactly one is written per job.
|
|
12
|
-
//
|
|
13
|
-
// A worker is not required to submit. Besides an explicit submit, a job becomes
|
|
14
|
-
// terminal when (a) the wrapper shell's `crtr job _fail` runs on a clean exit,
|
|
15
|
-
// or (b) the hosting tmux pane is closed — which sends SIGHUP so (a) never runs.
|
|
16
|
-
// Case (b) is reaped here: when a live job's recorded pane is gone and no result
|
|
17
|
-
// exists, we write a `closed` result (terminal, but distinct from `failed`) so
|
|
18
|
-
// the job stops being a zombie without claiming an outcome we can't know.
|
|
19
|
-
import { existsSync, mkdirSync, appendFileSync, readFileSync, writeFileSync, renameSync, readdirSync, statSync, } from 'node:fs';
|
|
20
|
-
import { watch } from 'node:fs';
|
|
21
|
-
import { spawnSync } from 'node:child_process';
|
|
22
|
-
import { join } from 'node:path';
|
|
23
|
-
import { homedir } from 'node:os';
|
|
24
|
-
import { randomBytes } from 'node:crypto';
|
|
25
|
-
import { notFound, general } from './errors.js';
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Paths
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
function jobsRoot() {
|
|
30
|
-
const xdg = process.env['XDG_STATE_HOME'];
|
|
31
|
-
const base = (xdg !== undefined && xdg !== '') ? xdg : join(homedir(), '.local', 'state');
|
|
32
|
-
return join(base, 'crtr', 'jobs');
|
|
33
|
-
}
|
|
34
|
-
function jobDir(jobId) {
|
|
35
|
-
return join(jobsRoot(), jobId);
|
|
36
|
-
}
|
|
37
|
-
function metaPath(jobId) {
|
|
38
|
-
return join(jobDir(jobId), 'meta.json');
|
|
39
|
-
}
|
|
40
|
-
function logPath(jobId) {
|
|
41
|
-
return join(jobDir(jobId), 'log.jsonl');
|
|
42
|
-
}
|
|
43
|
-
function resultJsonPath(jobId) {
|
|
44
|
-
return join(jobDir(jobId), 'result.json');
|
|
45
|
-
}
|
|
46
|
-
function resultMdPath(jobId) {
|
|
47
|
-
return join(jobDir(jobId), 'result.md');
|
|
48
|
-
}
|
|
49
|
-
/** Path of whichever result file currently exists, or null if neither does. */
|
|
50
|
-
function existingResultPath(jobId) {
|
|
51
|
-
const md = resultMdPath(jobId);
|
|
52
|
-
if (existsSync(md))
|
|
53
|
-
return md;
|
|
54
|
-
const js = resultJsonPath(jobId);
|
|
55
|
-
if (existsSync(js))
|
|
56
|
-
return js;
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Internal helpers
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
function generateJobId() {
|
|
63
|
-
const ts = Date.now().toString(36);
|
|
64
|
-
const rnd = randomBytes(4).toString('hex');
|
|
65
|
-
return `${ts}-${rnd}`;
|
|
66
|
-
}
|
|
67
|
-
function ensureJobsRoot() {
|
|
68
|
-
mkdirSync(jobsRoot(), { recursive: true });
|
|
69
|
-
}
|
|
70
|
-
function readMeta(jobId) {
|
|
71
|
-
const p = metaPath(jobId);
|
|
72
|
-
if (!existsSync(p)) {
|
|
73
|
-
throw notFound(`job not found: ${jobId}`, { job_id: jobId });
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
return JSON.parse(readFileSync(p, 'utf8'));
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
throw general(`failed to parse meta.json for job ${jobId}`, { job_id: jobId });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
function writeMeta(jobId, meta) {
|
|
83
|
-
const dir = jobDir(jobId);
|
|
84
|
-
const tmp = join(dir, '.meta.tmp');
|
|
85
|
-
writeFileSync(tmp, JSON.stringify(meta, null, 2), 'utf8');
|
|
86
|
-
renameSync(tmp, metaPath(jobId));
|
|
87
|
-
}
|
|
88
|
-
function pidAlive(pid) {
|
|
89
|
-
try {
|
|
90
|
-
process.kill(pid, 0);
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Set of every tmux pane id across all sessions on the running server. Empty
|
|
99
|
-
* when no server is running (→ every recorded pane is treated as gone).
|
|
100
|
-
*
|
|
101
|
-
* This bridges tmux's pane lifecycle to the job registry. A worker whose pane
|
|
102
|
-
* is closed/killed receives SIGHUP, so the wrapper shell's `crtr job _fail`
|
|
103
|
-
* never runs and the job would otherwise stay `live` forever (a zombie). We
|
|
104
|
-
* detect the vanished pane and reap the job instead.
|
|
105
|
-
*/
|
|
106
|
-
function allTmuxPaneIds() {
|
|
107
|
-
const set = new Set();
|
|
108
|
-
const r = spawnSync('tmux', ['list-panes', '-a', '-F', '#{pane_id}'], { encoding: 'utf8' });
|
|
109
|
-
if (r.status !== 0 || typeof r.stdout !== 'string')
|
|
110
|
-
return set;
|
|
111
|
-
for (const line of r.stdout.split('\n')) {
|
|
112
|
-
const t = line.trim();
|
|
113
|
-
if (t !== '')
|
|
114
|
-
set.add(t);
|
|
115
|
-
}
|
|
116
|
-
return set;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Reap a job whose hosting tmux pane has disappeared. Acts only when the job is
|
|
120
|
-
* still `live`, has a recorded pane, and has produced no result file. Writes a
|
|
121
|
-
* terminal `closed` result so the job stops being a zombie and every reader
|
|
122
|
-
* (status, list, result --wait) agrees. `closed` is distinct from `failed`: we
|
|
123
|
-
* don't know the outcome, only that the pane is gone. Returns true if it reaped.
|
|
124
|
-
*
|
|
125
|
-
* `panes` lets a caller reuse a single tmux query across many jobs (listJobs).
|
|
126
|
-
*/
|
|
127
|
-
function reapIfPaneDead(meta, panes) {
|
|
128
|
-
if (meta.status !== 'live')
|
|
129
|
-
return false;
|
|
130
|
-
if (meta.pane_id === undefined || meta.pane_id === '')
|
|
131
|
-
return false;
|
|
132
|
-
if (existingResultPath(meta.job_id) !== null)
|
|
133
|
-
return false;
|
|
134
|
-
const set = panes ?? allTmuxPaneIds();
|
|
135
|
-
if (set.has(meta.pane_id))
|
|
136
|
-
return false;
|
|
137
|
-
try {
|
|
138
|
-
writeMarkdownResult(meta.job_id, '', 'closed', 'worker pane closed before submitting a result');
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
/** Poll cadence (ms) for detecting a closed worker pane during result --wait. */
|
|
146
|
-
const PANE_POLL_MS = 2000;
|
|
147
|
-
const LEVEL_RANK = {
|
|
148
|
-
debug: 0,
|
|
149
|
-
info: 1,
|
|
150
|
-
warn: 2,
|
|
151
|
-
error: 3,
|
|
152
|
-
};
|
|
153
|
-
// ---------------------------------------------------------------------------
|
|
154
|
-
// Exported API
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
/**
|
|
157
|
-
* Allocate a new job directory and write meta.json atomically.
|
|
158
|
-
* Returns the job_id and the absolute directory path.
|
|
159
|
-
*/
|
|
160
|
-
export function createJob(kind, opts) {
|
|
161
|
-
ensureJobsRoot();
|
|
162
|
-
const jobId = generateJobId();
|
|
163
|
-
const dir = jobDir(jobId);
|
|
164
|
-
mkdirSync(dir, { recursive: true });
|
|
165
|
-
const meta = {
|
|
166
|
-
job_id: jobId,
|
|
167
|
-
kind,
|
|
168
|
-
created_at: new Date().toISOString(),
|
|
169
|
-
cwd: opts.cwd,
|
|
170
|
-
status: 'live',
|
|
171
|
-
};
|
|
172
|
-
if (opts.pid !== undefined) {
|
|
173
|
-
meta.pid = opts.pid;
|
|
174
|
-
}
|
|
175
|
-
writeMeta(jobId, meta);
|
|
176
|
-
return { jobId, dir };
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Record the tmux pane hosting a detached worker so `cancelJob` can kill it.
|
|
180
|
-
*/
|
|
181
|
-
export function recordJobPane(jobId, paneId) {
|
|
182
|
-
const meta = readMeta(jobId);
|
|
183
|
-
meta.pane_id = paneId;
|
|
184
|
-
writeMeta(jobId, meta);
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Record the pid of a detached worker (e.g. a headless background agent) so
|
|
188
|
-
* jobStatus can mark the job failed if the process dies without a result.
|
|
189
|
-
*/
|
|
190
|
-
export function recordJobPid(jobId, pid) {
|
|
191
|
-
const meta = readMeta(jobId);
|
|
192
|
-
meta.pid = pid;
|
|
193
|
-
writeMeta(jobId, meta);
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Append one event line to log.jsonl. Does NOT throw if jobId doesn't exist —
|
|
197
|
-
* a crashed writer should not further corrupt state; use a guard at the call site.
|
|
198
|
-
*/
|
|
199
|
-
export function appendEvent(jobId, event) {
|
|
200
|
-
const p = logPath(jobId);
|
|
201
|
-
const line = {
|
|
202
|
-
ts: new Date().toISOString(),
|
|
203
|
-
level: event.level,
|
|
204
|
-
event: event.event,
|
|
205
|
-
message: event.message,
|
|
206
|
-
};
|
|
207
|
-
if (event.data !== undefined) {
|
|
208
|
-
line.data = event.data;
|
|
209
|
-
}
|
|
210
|
-
appendFileSync(p, JSON.stringify(line) + '\n', 'utf8');
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Atomically write result.json (structured object) and update meta.json status.
|
|
214
|
-
* Used by programmatic callers (human, sys) that produce object results.
|
|
215
|
-
* The result file's appearance is the completion signal — never inferred from log content.
|
|
216
|
-
*/
|
|
217
|
-
export function writeResult(jobId, result, terminalStatus) {
|
|
218
|
-
const dir = jobDir(jobId);
|
|
219
|
-
if (!existsSync(dir)) {
|
|
220
|
-
throw notFound(`job not found: ${jobId}`, { job_id: jobId });
|
|
221
|
-
}
|
|
222
|
-
const payload = {
|
|
223
|
-
status: terminalStatus,
|
|
224
|
-
result,
|
|
225
|
-
written_at: new Date().toISOString(),
|
|
226
|
-
};
|
|
227
|
-
const tmp = join(dir, '.result.tmp');
|
|
228
|
-
writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
229
|
-
renameSync(tmp, resultJsonPath(jobId));
|
|
230
|
-
const meta = readMeta(jobId);
|
|
231
|
-
meta.status = terminalStatus;
|
|
232
|
-
writeMeta(jobId, meta);
|
|
233
|
-
}
|
|
234
|
-
/**
|
|
235
|
-
* Atomically write result.md (YAML frontmatter + markdown body) and update meta.json status.
|
|
236
|
-
* Used by `crtr job submit` for agent-driven markdown results.
|
|
237
|
-
*/
|
|
238
|
-
export function writeMarkdownResult(jobId, body, terminalStatus, reason) {
|
|
239
|
-
const dir = jobDir(jobId);
|
|
240
|
-
if (!existsSync(dir)) {
|
|
241
|
-
throw notFound(`job not found: ${jobId}`, { job_id: jobId });
|
|
242
|
-
}
|
|
243
|
-
const fm = {
|
|
244
|
-
status: terminalStatus,
|
|
245
|
-
written_at: new Date().toISOString(),
|
|
246
|
-
};
|
|
247
|
-
if (reason !== undefined && reason !== '') {
|
|
248
|
-
fm.reason = reason;
|
|
249
|
-
}
|
|
250
|
-
const content = `${renderFrontmatter(fm)}${body}`;
|
|
251
|
-
const tmp = join(dir, '.result.tmp');
|
|
252
|
-
writeFileSync(tmp, content, 'utf8');
|
|
253
|
-
renameSync(tmp, resultMdPath(jobId));
|
|
254
|
-
const meta = readMeta(jobId);
|
|
255
|
-
meta.status = terminalStatus;
|
|
256
|
-
writeMeta(jobId, meta);
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Render a small fixed-shape frontmatter block. We control writer and reader,
|
|
260
|
-
* so a 3-key hand-rolled emitter is plenty — no YAML dep, no escaping surprises.
|
|
261
|
-
* Values are plain strings; we double-quote `reason` to survive newlines/colons.
|
|
262
|
-
*/
|
|
263
|
-
function renderFrontmatter(fm) {
|
|
264
|
-
const lines = ['---', `status: ${fm.status}`, `written_at: ${fm.written_at}`];
|
|
265
|
-
if (fm.reason !== undefined) {
|
|
266
|
-
const escaped = fm.reason.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
267
|
-
lines.push(`reason: "${escaped}"`);
|
|
268
|
-
}
|
|
269
|
-
lines.push('---', '');
|
|
270
|
-
return lines.join('\n');
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Parse the small fixed-shape frontmatter we emit. Tolerant of trailing
|
|
274
|
-
* whitespace; returns `{ frontmatter, body }`. Throws if the document does not
|
|
275
|
-
* start with `---\n` or no closing `---` is found.
|
|
276
|
-
*/
|
|
277
|
-
function parseMarkdownResult(raw) {
|
|
278
|
-
if (!raw.startsWith('---\n') && !raw.startsWith('---\r\n')) {
|
|
279
|
-
throw new Error('result.md missing opening --- delimiter');
|
|
280
|
-
}
|
|
281
|
-
const afterOpen = raw.indexOf('\n') + 1;
|
|
282
|
-
const closeIdx = raw.indexOf('\n---', afterOpen);
|
|
283
|
-
if (closeIdx === -1) {
|
|
284
|
-
throw new Error('result.md missing closing --- delimiter');
|
|
285
|
-
}
|
|
286
|
-
const fmBlock = raw.slice(afterOpen, closeIdx);
|
|
287
|
-
// Body starts after the closing `---` line.
|
|
288
|
-
const afterCloseLine = raw.indexOf('\n', closeIdx + 1);
|
|
289
|
-
const body = afterCloseLine === -1 ? '' : raw.slice(afterCloseLine + 1);
|
|
290
|
-
const fm = {};
|
|
291
|
-
for (const line of fmBlock.split('\n')) {
|
|
292
|
-
const m = line.match(/^([a-z_]+):\s*(.*)$/);
|
|
293
|
-
if (m === null)
|
|
294
|
-
continue;
|
|
295
|
-
const key = m[1];
|
|
296
|
-
if (m[2] === undefined)
|
|
297
|
-
continue;
|
|
298
|
-
let val = m[2];
|
|
299
|
-
if (val.startsWith('"') && val.endsWith('"') && val.length >= 2) {
|
|
300
|
-
val = val.slice(1, -1).replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
301
|
-
}
|
|
302
|
-
if (key === 'status') {
|
|
303
|
-
fm.status = val;
|
|
304
|
-
}
|
|
305
|
-
else if (key === 'written_at') {
|
|
306
|
-
fm.written_at = val;
|
|
307
|
-
}
|
|
308
|
-
else if (key === 'reason') {
|
|
309
|
-
fm.reason = val;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (fm.status === undefined || fm.written_at === undefined) {
|
|
313
|
-
throw new Error('result.md frontmatter missing status or written_at');
|
|
314
|
-
}
|
|
315
|
-
return { frontmatter: fm, body };
|
|
316
|
-
}
|
|
317
|
-
export function readResult(jobId, opts = {}) {
|
|
318
|
-
const dir = jobDir(jobId);
|
|
319
|
-
if (!existsSync(dir)) {
|
|
320
|
-
throw notFound(`job not found: ${jobId}`, { job_id: jobId });
|
|
321
|
-
}
|
|
322
|
-
function parseAt(path) {
|
|
323
|
-
const raw = readFileSync(path, 'utf8');
|
|
324
|
-
if (path.endsWith('.md')) {
|
|
325
|
-
const { frontmatter, body } = parseMarkdownResult(raw);
|
|
326
|
-
const out = { status: frontmatter.status, result_md: body };
|
|
327
|
-
if (frontmatter.reason !== undefined) {
|
|
328
|
-
out.reason = frontmatter.reason;
|
|
329
|
-
}
|
|
330
|
-
return out;
|
|
331
|
-
}
|
|
332
|
-
const parsed = JSON.parse(raw);
|
|
333
|
-
return { status: parsed.status, result: parsed.result };
|
|
334
|
-
}
|
|
335
|
-
const existing = existingResultPath(jobId);
|
|
336
|
-
if (existing !== null) {
|
|
337
|
-
return Promise.resolve(parseAt(existing));
|
|
338
|
-
}
|
|
339
|
-
if (opts.waitMs === undefined || opts.waitMs <= 0) {
|
|
340
|
-
return Promise.resolve({ status: 'timeout' });
|
|
341
|
-
}
|
|
342
|
-
return new Promise((resolve) => {
|
|
343
|
-
let settled = false;
|
|
344
|
-
let timer;
|
|
345
|
-
let poll;
|
|
346
|
-
const finish = (response) => {
|
|
347
|
-
if (settled)
|
|
348
|
-
return;
|
|
349
|
-
settled = true;
|
|
350
|
-
if (timer !== undefined)
|
|
351
|
-
clearTimeout(timer);
|
|
352
|
-
if (poll !== undefined)
|
|
353
|
-
clearInterval(poll);
|
|
354
|
-
try {
|
|
355
|
-
watcher.close();
|
|
356
|
-
}
|
|
357
|
-
catch { /* noop */ }
|
|
358
|
-
resolve(response);
|
|
359
|
-
};
|
|
360
|
-
const watcher = watch(dir, (_event, name) => {
|
|
361
|
-
if (name !== 'result.md' && name !== 'result.json')
|
|
362
|
-
return;
|
|
363
|
-
const path = existingResultPath(jobId);
|
|
364
|
-
if (path !== null) {
|
|
365
|
-
finish(parseAt(path));
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
const path = existingResultPath(jobId);
|
|
369
|
-
if (path !== null) {
|
|
370
|
-
finish(parseAt(path));
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
// fs.watch only fires on result files. A pane that closes without a submit
|
|
374
|
-
// produces no such event, so poll to reap it instead of hanging until the
|
|
375
|
-
// full timeout budget elapses.
|
|
376
|
-
poll = setInterval(() => {
|
|
377
|
-
const found = existingResultPath(jobId);
|
|
378
|
-
if (found !== null) {
|
|
379
|
-
finish(parseAt(found));
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
try {
|
|
383
|
-
if (reapIfPaneDead(readMeta(jobId))) {
|
|
384
|
-
const reaped = existingResultPath(jobId);
|
|
385
|
-
if (reaped !== null)
|
|
386
|
-
finish(parseAt(reaped));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
catch { /* noop */ }
|
|
390
|
-
}, PANE_POLL_MS);
|
|
391
|
-
// A non-finite budget (Infinity) means block until a result appears or the
|
|
392
|
-
// worker pane dies — used by `human review`, where the human may take an
|
|
393
|
-
// unbounded amount of time. The poll above still reaps a dead pane, so this
|
|
394
|
-
// never hangs forever on a closed pane.
|
|
395
|
-
if (Number.isFinite(opts.waitMs)) {
|
|
396
|
-
timer = setTimeout(() => {
|
|
397
|
-
finish({ status: 'timeout' });
|
|
398
|
-
}, opts.waitMs);
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Derive job state from meta.json, the result file, and the tail of log.jsonl.
|
|
404
|
-
* If a pid is recorded, is not alive, and no result file exists → 'failed'.
|
|
405
|
-
*/
|
|
406
|
-
export function jobStatus(jobId) {
|
|
407
|
-
let meta = readMeta(jobId);
|
|
408
|
-
if (reapIfPaneDead(meta)) {
|
|
409
|
-
meta = readMeta(jobId);
|
|
410
|
-
}
|
|
411
|
-
const age_s = (Date.now() - new Date(meta.created_at).getTime()) / 1000;
|
|
412
|
-
let state = meta.status;
|
|
413
|
-
if (state === 'live') {
|
|
414
|
-
const existing = existingResultPath(jobId);
|
|
415
|
-
if (existing !== null) {
|
|
416
|
-
// Result file present but meta not yet updated (rare); trust the file.
|
|
417
|
-
try {
|
|
418
|
-
if (existing.endsWith('.md')) {
|
|
419
|
-
const { frontmatter } = parseMarkdownResult(readFileSync(existing, 'utf8'));
|
|
420
|
-
state = frontmatter.status;
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
const r = JSON.parse(readFileSync(existing, 'utf8'));
|
|
424
|
-
state = r.status;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
catch { /* leave as live */ }
|
|
428
|
-
}
|
|
429
|
-
else if (meta.pid !== undefined && !pidAlive(meta.pid)) {
|
|
430
|
-
state = 'failed';
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
// Tail of log for last_event.
|
|
434
|
-
let last_event = null;
|
|
435
|
-
const lp = logPath(jobId);
|
|
436
|
-
if (existsSync(lp)) {
|
|
437
|
-
const lines = readFileSync(lp, 'utf8').trimEnd().split('\n');
|
|
438
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
439
|
-
const line = lines[i];
|
|
440
|
-
if (line === undefined || line.trim() === '')
|
|
441
|
-
continue;
|
|
442
|
-
try {
|
|
443
|
-
const ev = JSON.parse(line);
|
|
444
|
-
last_event = { event: ev.event, ts: ev.ts };
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return { state, age_s, last_event };
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* List all jobs sorted by created_at ascending. Pagination is applied by the
|
|
456
|
-
* caller, not here.
|
|
457
|
-
*/
|
|
458
|
-
export function listJobs() {
|
|
459
|
-
const root = jobsRoot();
|
|
460
|
-
if (!existsSync(root))
|
|
461
|
-
return [];
|
|
462
|
-
const entries = readdirSync(root);
|
|
463
|
-
const jobs = [];
|
|
464
|
-
// One tmux query, reused to reap every job whose pane has vanished.
|
|
465
|
-
const panes = allTmuxPaneIds();
|
|
466
|
-
for (const entry of entries) {
|
|
467
|
-
const dir = join(root, entry);
|
|
468
|
-
try {
|
|
469
|
-
if (!statSync(dir).isDirectory())
|
|
470
|
-
continue;
|
|
471
|
-
const mp = join(dir, 'meta.json');
|
|
472
|
-
if (!existsSync(mp))
|
|
473
|
-
continue;
|
|
474
|
-
let meta = JSON.parse(readFileSync(mp, 'utf8'));
|
|
475
|
-
if (reapIfPaneDead(meta, panes)) {
|
|
476
|
-
meta = JSON.parse(readFileSync(mp, 'utf8'));
|
|
477
|
-
}
|
|
478
|
-
// Derive effective state (result file beats meta.status for live jobs).
|
|
479
|
-
let state = meta.status;
|
|
480
|
-
if (state === 'live') {
|
|
481
|
-
const mdP = join(dir, 'result.md');
|
|
482
|
-
const jsP = join(dir, 'result.json');
|
|
483
|
-
try {
|
|
484
|
-
if (existsSync(mdP)) {
|
|
485
|
-
const { frontmatter } = parseMarkdownResult(readFileSync(mdP, 'utf8'));
|
|
486
|
-
state = frontmatter.status;
|
|
487
|
-
}
|
|
488
|
-
else if (existsSync(jsP)) {
|
|
489
|
-
const r = JSON.parse(readFileSync(jsP, 'utf8'));
|
|
490
|
-
state = r.status;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
catch { /* leave as live */ }
|
|
494
|
-
}
|
|
495
|
-
jobs.push({ job_id: meta.job_id, kind: meta.kind, state, created_at: meta.created_at });
|
|
496
|
-
}
|
|
497
|
-
catch {
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
jobs.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
502
|
-
return jobs;
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Read and filter log events. Ordering preserved. sinceTs/untilTs are ISO8601
|
|
506
|
-
* strings; minLevel filters by severity rank (inclusive).
|
|
507
|
-
*/
|
|
508
|
-
export function readLog(jobId, opts = {}) {
|
|
509
|
-
const dir = jobDir(jobId);
|
|
510
|
-
if (!existsSync(dir)) {
|
|
511
|
-
throw notFound(`job not found: ${jobId}`, { job_id: jobId });
|
|
512
|
-
}
|
|
513
|
-
const lp = logPath(jobId);
|
|
514
|
-
if (!existsSync(lp))
|
|
515
|
-
return [];
|
|
516
|
-
const raw = readFileSync(lp, 'utf8');
|
|
517
|
-
const results = [];
|
|
518
|
-
const minRank = opts.minLevel !== undefined ? LEVEL_RANK[opts.minLevel] : 0;
|
|
519
|
-
for (const line of raw.split('\n')) {
|
|
520
|
-
if (line.trim() === '')
|
|
521
|
-
continue;
|
|
522
|
-
let ev;
|
|
523
|
-
try {
|
|
524
|
-
ev = JSON.parse(line);
|
|
525
|
-
}
|
|
526
|
-
catch {
|
|
527
|
-
continue;
|
|
528
|
-
}
|
|
529
|
-
if (opts.sinceTs !== undefined && ev.ts < opts.sinceTs)
|
|
530
|
-
continue;
|
|
531
|
-
if (opts.untilTs !== undefined && ev.ts >= opts.untilTs)
|
|
532
|
-
continue;
|
|
533
|
-
if (LEVEL_RANK[ev.level] < minRank)
|
|
534
|
-
continue;
|
|
535
|
-
results.push(ev);
|
|
536
|
-
}
|
|
537
|
-
return results;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Best-effort cancel: send SIGTERM to the recorded pid (if any), mark meta
|
|
541
|
-
* canceled. Success means the signal was delivered, not that execution stopped.
|
|
542
|
-
*/
|
|
543
|
-
export function cancelJob(jobId) {
|
|
544
|
-
const meta = readMeta(jobId);
|
|
545
|
-
if (meta.status !== 'live') {
|
|
546
|
-
// Already terminal — nothing to cancel.
|
|
547
|
-
return { canceled: false };
|
|
548
|
-
}
|
|
549
|
-
let signaled = false;
|
|
550
|
-
if (meta.pid !== undefined) {
|
|
551
|
-
try {
|
|
552
|
-
process.kill(meta.pid, 'SIGTERM');
|
|
553
|
-
signaled = true;
|
|
554
|
-
}
|
|
555
|
-
catch { /* pid gone or unpermitted */ }
|
|
556
|
-
}
|
|
557
|
-
if (meta.pane_id !== undefined && meta.pane_id !== '') {
|
|
558
|
-
const k = spawnSync('tmux', ['kill-pane', '-t', meta.pane_id], { encoding: 'utf8' });
|
|
559
|
-
if (k.status === 0)
|
|
560
|
-
signaled = true;
|
|
561
|
-
}
|
|
562
|
-
meta.status = 'canceled';
|
|
563
|
-
writeMeta(jobId, meta);
|
|
564
|
-
return { canceled: signaled };
|
|
565
|
-
}
|
package/dist/core/subagents.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { Scope, Subagent } from '../types.js';
|
|
2
|
-
/** `<scope-root>/agents` for a given scope, or null when the scope has no root. */
|
|
3
|
-
export declare function scopeAgentsDir(scope: Scope): string | null;
|
|
4
|
-
/** Scope-root agents under `<scope-root>/agents/*.md`. */
|
|
5
|
-
export declare function listScopeRootSubagents(scope: Scope): Subagent[];
|
|
6
|
-
/** All subagents: scope-root agents (project, user) plus enabled plugins. */
|
|
7
|
-
export declare function listSubagents(scopeFilter?: Scope): Subagent[];
|
|
8
|
-
/** Canonical, unambiguous identifier: `<plugin>/<name>`, or bare `<name>` for
|
|
9
|
-
* scope-root agents. */
|
|
10
|
-
export declare function subagentId(a: Subagent): string;
|
|
11
|
-
export interface SubagentResolutionOpts {
|
|
12
|
-
scope?: Scope;
|
|
13
|
-
plugin?: string;
|
|
14
|
-
}
|
|
15
|
-
/** Resolve a subagent by name. Accepts a bare `<name>` or a `<plugin>/<name>`
|
|
16
|
-
* qualifier. Project precedes user precedes builtin; scope-root precedes
|
|
17
|
-
* plugin. Throws notFound / ambiguous as the skill resolver does. */
|
|
18
|
-
export declare function resolveSubagent(rawName: string, opts?: SubagentResolutionOpts): Subagent;
|