@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/commands/debug.js
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
// `crtr mode debug` leaf — reproduce-first root-cause workflow.
|
|
2
|
-
//
|
|
3
|
-
// Running it spawns a reproduction-only agent in a sibling tmux pane (the same
|
|
4
|
-
// spawn + job-handle shape as `crtr agent new`) and returns a job handle
|
|
5
|
-
// plus a follow_up. The orchestrator-side methodology lives in FLOW_DEBUG_GUIDE
|
|
6
|
-
// (the leaf's help.guide), loaded via `crtr mode debug -h` after the repro
|
|
7
|
-
// agent returns. Methodology stays in the CLI guide field, like PLAN_NEW_GUIDE;
|
|
8
|
-
// no builtin skill.
|
|
9
|
-
export const FLOW_DEBUG_GUIDE = `## Debug workflow — reproduce first
|
|
10
|
-
|
|
11
|
-
Audience: the agent that ran \`crtr mode debug\`. A reproduction agent is
|
|
12
|
-
already spawned in a sibling pane. It writes ONE failing integration test and
|
|
13
|
-
never fixes anything. You do everything after: gate on the repro, root-cause,
|
|
14
|
-
fix, verify against that same test.
|
|
15
|
-
|
|
16
|
-
### Phase 0: Await the repro agent
|
|
17
|
-
|
|
18
|
-
Run \`crtr job read result <job_id> --wait\` (10-min budget).
|
|
19
|
-
On status:"timeout": re-issue the wait, or run \`crtr job read logs <job_id> --follow\`
|
|
20
|
-
until the job is terminal.
|
|
21
|
-
|
|
22
|
-
### Phase 1: Gate on reproduction
|
|
23
|
-
|
|
24
|
-
\`reproduces:true\`: read \`test_path\`, run \`test_command\` YOURSELF, confirm
|
|
25
|
-
it fails for the stated reason. Do not trust the agent's claim — if it passes
|
|
26
|
-
or fails differently, treat repro as NOT achieved. This test is the regression
|
|
27
|
-
gate; it stays in the suite after the fix.
|
|
28
|
-
\`status:"failed"\` / \`reproduces:false\` / your run disproves it: no repro
|
|
29
|
-
harness. Continue, but record "no reproduction — fix unverified; do not claim
|
|
30
|
-
verified-fixed."
|
|
31
|
-
|
|
32
|
-
### Phase 2: Reconnaissance
|
|
33
|
-
|
|
34
|
-
Read the key files yourself — entry point, failure point, the data flow
|
|
35
|
-
between. \`git log\` / \`git blame\` near the failure: recent changes are
|
|
36
|
-
high-signal.
|
|
37
|
-
|
|
38
|
-
### Phase 3: Assess difficulty, scale investigators
|
|
39
|
-
|
|
40
|
-
Simple → solo (Explore subagents for tracing if the area is large).
|
|
41
|
-
Medium → 2–3 parallel \`devcore:senior-advisor\`: data-flow tracer, assumption
|
|
42
|
-
auditor, change investigator.
|
|
43
|
-
Hard (intermittent, races, "been stuck", many modules) → 3–5 parallel:
|
|
44
|
-
end-to-end tracer, assumption breaker, git archaeologist, boundary inspector.
|
|
45
|
-
Give investigators file paths, observed behavior, and concrete tasks — never
|
|
46
|
-
your hypotheses. Challenge theories against each other; the one that survives
|
|
47
|
-
disconfirmation wins.
|
|
48
|
-
|
|
49
|
-
### Phase 4: Fix
|
|
50
|
-
|
|
51
|
-
Minimal root-cause fix. No scope expansion, no drive-by refactor.
|
|
52
|
-
|
|
53
|
-
### Phase 5: Verify
|
|
54
|
-
|
|
55
|
-
Re-run \`test_command\`: it MUST now pass. Run the broader suite for
|
|
56
|
-
regressions. If there was no repro test, state the fix is unverified by
|
|
57
|
-
reproduction and recommend explicit manual verification.
|
|
58
|
-
|
|
59
|
-
### Phase 6: Report
|
|
60
|
-
|
|
61
|
-
Root cause (exact line + why), evidence, the now-passing repro test path,
|
|
62
|
-
confidence (High/Medium/Low; if not High, name what is uncertain).
|
|
63
|
-
|
|
64
|
-
### Constraints
|
|
65
|
-
|
|
66
|
-
The repro test is the regression guard — it stays; a fix-agent must never
|
|
67
|
-
weaken it. Investigators run in forked contexts; they return summaries, not
|
|
68
|
-
raw output. No code changes during Phases 2–3 except the repro test.`;
|
|
69
|
-
import { defineLeaf } from '../core/command.js';
|
|
70
|
-
import { InputError } from '../core/io.js';
|
|
71
|
-
import { createJob, appendEvent } from '../core/jobs.js';
|
|
72
|
-
import { spawnAgent, isInTmux } from '../core/spawn.js';
|
|
73
|
-
import { readConfig } from '../core/config.js';
|
|
74
|
-
import { reproHandoffPrompt } from '../prompts/debug.js';
|
|
75
|
-
// Inlined from job.ts (module-private there; not exported, per the no-shim
|
|
76
|
-
// convention). Same forms.
|
|
77
|
-
function resolveMaxPanes() {
|
|
78
|
-
const cfg = readConfig('user');
|
|
79
|
-
return cfg.max_panes_per_window;
|
|
80
|
-
}
|
|
81
|
-
function assertTmux() {
|
|
82
|
-
if (!isInTmux()) {
|
|
83
|
-
throw new InputError({
|
|
84
|
-
error: 'not_in_tmux',
|
|
85
|
-
message: 'crtr mode debug requires tmux (TMUX env var not set).',
|
|
86
|
-
next: 'Run inside a tmux session.',
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
export function registerDebug() {
|
|
91
|
-
return defineLeaf({
|
|
92
|
-
name: 'debug',
|
|
93
|
-
help: {
|
|
94
|
-
name: 'mode debug',
|
|
95
|
-
summary: 'reproduce-first root-cause workflow: spawns a reproduction agent, then you root-cause and fix',
|
|
96
|
-
guide: FLOW_DEBUG_GUIDE,
|
|
97
|
-
params: [
|
|
98
|
-
{
|
|
99
|
-
kind: 'stdin',
|
|
100
|
-
name: 'steps_to_reproduce',
|
|
101
|
-
required: true,
|
|
102
|
-
constraint: 'Prose describing how to reproduce the failure. Pipe on stdin.',
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
kind: 'flag',
|
|
106
|
-
name: 'summary',
|
|
107
|
-
type: 'string',
|
|
108
|
-
required: true,
|
|
109
|
-
constraint: 'One paragraph summary of the failure: symptom, where observed, expected vs actual.',
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
kind: 'flag',
|
|
113
|
-
name: 'cwd',
|
|
114
|
-
type: 'path',
|
|
115
|
-
required: false,
|
|
116
|
-
constraint: 'Working directory for the spawned agent. Defaults to process.cwd().',
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
output: [
|
|
120
|
-
{
|
|
121
|
-
name: 'job_id',
|
|
122
|
-
type: 'string',
|
|
123
|
-
required: true,
|
|
124
|
-
constraint: 'Use with `job read status`, `job read logs`, `job read result`, `job cancel`.',
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: 'follow_up',
|
|
128
|
-
type: 'string',
|
|
129
|
-
required: true,
|
|
130
|
-
constraint: 'Recommended next call.',
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
outputKind: 'object',
|
|
134
|
-
effects: [
|
|
135
|
-
'Spawns a reproduction agent in a sibling tmux pane.',
|
|
136
|
-
'Creates a job entry at $XDG_STATE_HOME/crtr/jobs/<job_id>/.',
|
|
137
|
-
'On completion, result writes atomically to result.json.',
|
|
138
|
-
],
|
|
139
|
-
},
|
|
140
|
-
slash: {
|
|
141
|
-
name: 'debug',
|
|
142
|
-
description: 'Debug mode — reproduce-first root-cause investigation.',
|
|
143
|
-
argumentHint: '<symptom or failing test>',
|
|
144
|
-
body: `You are entering **debug mode**: a reproduce-first, root-cause investigation.
|
|
145
|
-
|
|
146
|
-
1. Run \`crtr mode debug -h\` to load the debugging workflow and output schema.
|
|
147
|
-
2. Follow it — reproduce the failure first, then isolate the root cause before proposing a fix.
|
|
148
|
-
|
|
149
|
-
The issue: $ARGUMENTS
|
|
150
|
-
|
|
151
|
-
If no issue was given, ask the user for the symptom or failing test before starting.`,
|
|
152
|
-
},
|
|
153
|
-
run: async (input) => {
|
|
154
|
-
assertTmux();
|
|
155
|
-
const stepsToReproduce = input['steps_to_reproduce'];
|
|
156
|
-
const summary = input['summary'];
|
|
157
|
-
const cwd = input['cwd'] ?? process.cwd();
|
|
158
|
-
const issue = `${summary}\n\n${stepsToReproduce}`;
|
|
159
|
-
const { jobId } = createJob('debug-repro', { cwd });
|
|
160
|
-
const result = spawnAgent({
|
|
161
|
-
prompt: reproHandoffPrompt(issue, jobId),
|
|
162
|
-
cwd,
|
|
163
|
-
jobId,
|
|
164
|
-
maxPanesPerWindow: resolveMaxPanes(),
|
|
165
|
-
});
|
|
166
|
-
if (result.status === 'not-in-tmux') {
|
|
167
|
-
throw new InputError({
|
|
168
|
-
error: 'not_in_tmux',
|
|
169
|
-
message: result.message,
|
|
170
|
-
next: 'Run inside a tmux session.',
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
if (result.status === 'spawn-failed') {
|
|
174
|
-
throw new InputError({
|
|
175
|
-
error: 'spawn_failed',
|
|
176
|
-
message: result.message,
|
|
177
|
-
next: 'Check tmux is running and try again.',
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
const paneLabel = result.paneId !== undefined ? result.paneId : 'unknown';
|
|
181
|
-
appendEvent(jobId, {
|
|
182
|
-
level: 'info',
|
|
183
|
-
event: 'worker_started',
|
|
184
|
-
message: `repro pane ${paneLabel} spawned`,
|
|
185
|
-
});
|
|
186
|
-
return {
|
|
187
|
-
job_id: jobId,
|
|
188
|
-
follow_up: `Await the reproduction agent: crtr job read result ${jobId} --wait. Then run \`crtr mode debug -h\` and follow the workflow from Phase 1.`,
|
|
189
|
-
};
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
}
|
package/dist/commands/job.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { BranchDef } from '../core/command.js';
|
|
2
|
-
/** Count of jobs currently in the live state, or null when listing fails.
|
|
3
|
-
* Backs the always-on "Workers running" signal on root -h so an agent never
|
|
4
|
-
* forgets it has in-flight workers to collect. */
|
|
5
|
-
export declare function liveJobCount(): number | null;
|
|
6
|
-
/** The job subtree's root-level dynamic block. A bounded aggregate (running
|
|
7
|
-
* count + how to collect), never an enumeration: live jobs are volatile and
|
|
8
|
-
* unbounded, so listing them in root -h would balloon (cli-design rule 15).
|
|
9
|
-
* Omitted when nothing is running. */
|
|
10
|
-
export declare function buildJobRootBlock(): string | null;
|
|
11
|
-
export declare function registerJob(): BranchDef;
|
package/dist/commands/job.js
DELETED
|
@@ -1,384 +0,0 @@
|
|
|
1
|
-
// `crtr job` subtree — universal monitoring registry for any ongoing task.
|
|
2
|
-
//
|
|
3
|
-
// Producers (agent spawns, future task systems) register jobs and write
|
|
4
|
-
// results; this subtree is the read/cancel/submit surface shared across all
|
|
5
|
-
// producers. Sub-branches: read {list, status, logs, result}, submit, _fail,
|
|
6
|
-
// cancel.
|
|
7
|
-
//
|
|
8
|
-
// Terminal-write contract:
|
|
9
|
-
// Worker MAY call `crtr job submit` → writes result.md (done|failed).
|
|
10
|
-
// If claude exits without submitting, the wrapper shell's `crtr job _fail`
|
|
11
|
-
// marks it failed IF no result file exists yet.
|
|
12
|
-
// If the worker's tmux pane is closed, SIGHUP skips `_fail`; the jobs layer
|
|
13
|
-
// then reaps the job by detecting that its recorded pane has vanished.
|
|
14
|
-
// `job read result` watches result file appearance and polls for pane death.
|
|
15
|
-
//
|
|
16
|
-
// `job read logs` is the only JSONL leaf.
|
|
17
|
-
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
18
|
-
import { emitLine } from '../core/io.js';
|
|
19
|
-
import { InputError } from '../core/io.js';
|
|
20
|
-
import { writeMarkdownResult, readResult as jobsReadResult, jobStatus, listJobs, readLog, cancelJob, appendEvent, } from '../core/jobs.js';
|
|
21
|
-
import { scheduleKillCurrentPane } from '../core/spawn.js';
|
|
22
|
-
import { paginate } from '../core/pagination.js';
|
|
23
|
-
import { stateBlock } from '../core/help.js';
|
|
24
|
-
const WAIT_BUDGET_MS = 10 * 60 * 1000;
|
|
25
|
-
const FOLLOW_POLL_MS = 1000;
|
|
26
|
-
/** Count of jobs currently in the live state, or null when listing fails.
|
|
27
|
-
* Backs the always-on "Workers running" signal on root -h so an agent never
|
|
28
|
-
* forgets it has in-flight workers to collect. */
|
|
29
|
-
export function liveJobCount() {
|
|
30
|
-
try {
|
|
31
|
-
return listJobs().filter((j) => j.state === 'live').length;
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/** The job subtree's root-level dynamic block. A bounded aggregate (running
|
|
38
|
-
* count + how to collect), never an enumeration: live jobs are volatile and
|
|
39
|
-
* unbounded, so listing them in root -h would balloon (cli-design rule 15).
|
|
40
|
-
* Omitted when nothing is running. */
|
|
41
|
-
export function buildJobRootBlock() {
|
|
42
|
-
const n = liveJobCount();
|
|
43
|
-
if (n === null || n === 0)
|
|
44
|
-
return null;
|
|
45
|
-
return stateBlock('workers', { count: n }, '`crtr job read list` to see them; `crtr job read result ID` to collect');
|
|
46
|
-
}
|
|
47
|
-
const DEFAULT_KILL_SECS = 2;
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// read sub-branch
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
const readList = defineLeaf({
|
|
52
|
-
name: 'list',
|
|
53
|
-
help: {
|
|
54
|
-
name: 'job read list',
|
|
55
|
-
summary: 'paginated list of jobs, sorted by created_at ascending',
|
|
56
|
-
params: [
|
|
57
|
-
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 20, constraint: 'Default 20, max 100.' },
|
|
58
|
-
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
59
|
-
],
|
|
60
|
-
output: [
|
|
61
|
-
{ name: 'items', type: 'object[]', required: true, constraint: 'Each: {job_id, kind, state, created_at}. Sorted by created_at ascending.' },
|
|
62
|
-
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'null means no more items.' },
|
|
63
|
-
{ name: 'total', type: 'integer | null', required: true, constraint: 'Total count of all jobs.' },
|
|
64
|
-
],
|
|
65
|
-
outputKind: 'object',
|
|
66
|
-
effects: ['None. Read-only.'],
|
|
67
|
-
},
|
|
68
|
-
run: async (input) => {
|
|
69
|
-
const limit = typeof input['limit'] === 'number' ? input['limit'] : 20;
|
|
70
|
-
const cursor = typeof input['cursor'] === 'string' ? input['cursor'] : undefined;
|
|
71
|
-
const all = listJobs();
|
|
72
|
-
const page = paginate(all, { limit, cursor }, {
|
|
73
|
-
defaultLimit: 20,
|
|
74
|
-
maxLimit: 100,
|
|
75
|
-
keyOf: (item) => item.created_at,
|
|
76
|
-
total: 'count',
|
|
77
|
-
});
|
|
78
|
-
return {
|
|
79
|
-
items: page.items,
|
|
80
|
-
next_cursor: page.next_cursor,
|
|
81
|
-
total: page.total,
|
|
82
|
-
};
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
const readStatus = defineLeaf({
|
|
86
|
-
name: 'status',
|
|
87
|
-
help: {
|
|
88
|
-
name: 'job read status',
|
|
89
|
-
summary: 'read the current status of a job',
|
|
90
|
-
params: [
|
|
91
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a producer (e.g. `crtr agent new *`).' },
|
|
92
|
-
],
|
|
93
|
-
output: [
|
|
94
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Echo of input.' },
|
|
95
|
-
{ name: 'state', type: 'string', required: true, constraint: 'One of: live, done, failed, canceled, closed (worker pane closed with no submitted result).' },
|
|
96
|
-
{ name: 'age_s', type: 'number', required: true, constraint: 'Seconds since job creation.' },
|
|
97
|
-
{ name: 'last_event', type: 'object | null', required: true, constraint: 'Most recent log event {event, ts}, or null if no events yet.' },
|
|
98
|
-
],
|
|
99
|
-
outputKind: 'object',
|
|
100
|
-
effects: ['None. Read-only.'],
|
|
101
|
-
},
|
|
102
|
-
run: async (input) => {
|
|
103
|
-
const jobId = input['job_id'];
|
|
104
|
-
const status = jobStatus(jobId);
|
|
105
|
-
return {
|
|
106
|
-
job_id: jobId,
|
|
107
|
-
state: status.state,
|
|
108
|
-
age_s: status.age_s,
|
|
109
|
-
last_event: status.last_event,
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
const readLogs = defineLeaf({
|
|
114
|
-
name: 'logs',
|
|
115
|
-
help: {
|
|
116
|
-
name: 'job read logs',
|
|
117
|
-
summary: 'read log events from a job; emits JSONL — one event object per line',
|
|
118
|
-
params: [
|
|
119
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a producer (e.g. `crtr agent new *`).' },
|
|
120
|
-
{ kind: 'flag', name: 'since', type: 'string', required: false, constraint: 'ISO 8601 timestamp. Only emit events at or after this time.' },
|
|
121
|
-
{ kind: 'flag', name: 'until', type: 'string', required: false, constraint: 'ISO 8601 timestamp. Only emit events before this time.' },
|
|
122
|
-
{ kind: 'flag', name: 'level', type: 'enum', choices: ['debug', 'info', 'warn', 'error'], required: false, default: 'info', constraint: 'Minimum severity. Default: info.' },
|
|
123
|
-
{ kind: 'flag', name: 'follow', type: 'bool', required: false, constraint: 'When present, stream new events until the job reaches a terminal state, then stop.' },
|
|
124
|
-
],
|
|
125
|
-
output: [
|
|
126
|
-
{
|
|
127
|
-
name: '<event line>',
|
|
128
|
-
type: 'object',
|
|
129
|
-
required: true,
|
|
130
|
-
constraint: 'Each JSONL line is: {ts:string, level:"debug"|"info"|"warn"|"error", event:string, message:string, data?:object}. Emitted one per line.',
|
|
131
|
-
},
|
|
132
|
-
],
|
|
133
|
-
outputKind: 'jsonl',
|
|
134
|
-
effects: ['None. Read-only.'],
|
|
135
|
-
},
|
|
136
|
-
run: async (input) => {
|
|
137
|
-
const jobId = input['job_id'];
|
|
138
|
-
const since = typeof input['since'] === 'string' ? input['since'] : undefined;
|
|
139
|
-
const until = typeof input['until'] === 'string' ? input['until'] : undefined;
|
|
140
|
-
const level = (typeof input['level'] === 'string' ? input['level'] : 'info');
|
|
141
|
-
const follow = input['follow'] === true;
|
|
142
|
-
const minLevel = level;
|
|
143
|
-
// Emit all existing events.
|
|
144
|
-
const events = readLog(jobId, { sinceTs: since, untilTs: until, minLevel });
|
|
145
|
-
for (const ev of events) {
|
|
146
|
-
emitLine(ev);
|
|
147
|
-
}
|
|
148
|
-
if (!follow)
|
|
149
|
-
return;
|
|
150
|
-
// Follow: poll for new events until the job reaches a terminal state.
|
|
151
|
-
// Track the latest emitted timestamp to avoid re-emitting.
|
|
152
|
-
let lastTs = until !== undefined ? until : new Date(0).toISOString();
|
|
153
|
-
// Update lastTs from emitted events.
|
|
154
|
-
for (const ev of events) {
|
|
155
|
-
const e = ev;
|
|
156
|
-
if (typeof e['ts'] === 'string' && e['ts'] > lastTs) {
|
|
157
|
-
lastTs = e['ts'];
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const terminalStates = new Set(['done', 'failed', 'canceled', 'closed']);
|
|
161
|
-
await new Promise((resolve) => {
|
|
162
|
-
const poll = () => {
|
|
163
|
-
const status = jobStatus(jobId);
|
|
164
|
-
const newEvents = readLog(jobId, { sinceTs: lastTs !== new Date(0).toISOString() ? lastTs : undefined, minLevel });
|
|
165
|
-
for (const ev of newEvents) {
|
|
166
|
-
const e = ev;
|
|
167
|
-
if (typeof e['ts'] === 'string' && e['ts'] > lastTs) {
|
|
168
|
-
emitLine(e);
|
|
169
|
-
lastTs = e['ts'];
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (terminalStates.has(status.state)) {
|
|
173
|
-
resolve();
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
setTimeout(poll, FOLLOW_POLL_MS);
|
|
177
|
-
};
|
|
178
|
-
setTimeout(poll, FOLLOW_POLL_MS);
|
|
179
|
-
});
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
const readResult = defineLeaf({
|
|
183
|
-
name: 'result',
|
|
184
|
-
help: {
|
|
185
|
-
name: 'job read result',
|
|
186
|
-
summary: 'read the final result of a completed job',
|
|
187
|
-
params: [
|
|
188
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a producer (e.g. `crtr agent new *`).' },
|
|
189
|
-
{ kind: 'flag', name: 'wait', type: 'bool', required: false, constraint: 'When present, blocks until a result file appears (up to 10 min).' },
|
|
190
|
-
],
|
|
191
|
-
output: [
|
|
192
|
-
{ name: 'job_id', type: 'string', required: true, constraint: 'Echo of input.' },
|
|
193
|
-
{ name: 'status', type: 'string', required: true, constraint: 'One of: done, failed, canceled, closed, timeout. closed = the worker pane went away before submitting a result.' },
|
|
194
|
-
{ name: 'result_md', type: 'string', required: false, constraint: 'Markdown body submitted by an agent via `crtr job submit`. Present when the job used the agent submit path.' },
|
|
195
|
-
{ name: 'result', type: 'object', required: false, constraint: 'Structured object submitted by a programmatic caller (human/sys). Present when the job used the programmatic submit path.' },
|
|
196
|
-
{ name: 'reason', type: 'string', required: false, constraint: 'Short explanation from frontmatter. Present when status is failed (agent-reported error) or closed (worker pane closed before submitting).' },
|
|
197
|
-
],
|
|
198
|
-
outputKind: 'object',
|
|
199
|
-
effects: ['None. Read-only.'],
|
|
200
|
-
},
|
|
201
|
-
run: async (input) => {
|
|
202
|
-
const jobId = input['job_id'];
|
|
203
|
-
const wait = input['wait'] === true;
|
|
204
|
-
const r = await jobsReadResult(jobId, { waitMs: wait ? WAIT_BUDGET_MS : 0 });
|
|
205
|
-
const out = { job_id: jobId, status: r.status };
|
|
206
|
-
if (r.result !== undefined) {
|
|
207
|
-
out['result'] = r.result;
|
|
208
|
-
}
|
|
209
|
-
if (r.result_md !== undefined) {
|
|
210
|
-
out['result_md'] = r.result_md;
|
|
211
|
-
}
|
|
212
|
-
if (r.reason !== undefined) {
|
|
213
|
-
out['reason'] = r.reason;
|
|
214
|
-
}
|
|
215
|
-
return out;
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
const readBranch = defineBranch({
|
|
219
|
-
name: 'read',
|
|
220
|
-
help: {
|
|
221
|
-
name: 'job read',
|
|
222
|
-
summary: 'read job status, logs, or results',
|
|
223
|
-
children: [
|
|
224
|
-
{ name: 'list', desc: 'paginated job list', useWhen: 'enumerating jobs' },
|
|
225
|
-
{ name: 'status', desc: 'current state and age', useWhen: 'checking if a job is still live' },
|
|
226
|
-
{ name: 'logs', desc: 'stream JSONL log events', useWhen: 'monitoring progress or debugging a job' },
|
|
227
|
-
{ name: 'result', desc: 'read final result', useWhen: 'collecting the output of a completed job' },
|
|
228
|
-
],
|
|
229
|
-
},
|
|
230
|
-
children: [readList, readStatus, readLogs, readResult],
|
|
231
|
-
});
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
// submit — called by the worker inside its pane (or by any producer that
|
|
234
|
-
// writes a result programmatically)
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
|
-
const jobSubmit = defineLeaf({
|
|
237
|
-
name: 'submit',
|
|
238
|
-
help: {
|
|
239
|
-
name: 'job submit',
|
|
240
|
-
summary: 'deliver a markdown result back to a job record (called by workers, or any producer writing the terminal value)',
|
|
241
|
-
params: [
|
|
242
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id injected as $CRTR_JOB_ID in the spawned pane.' },
|
|
243
|
-
{ kind: 'stdin', name: 'body', required: false, constraint: 'Markdown body of the result, piped on stdin. Required when --status is done (the default). When --status failed, stdin is optional; --reason carries the explanation.' },
|
|
244
|
-
{ kind: 'flag', name: 'status', type: 'enum', choices: ['done', 'failed'], required: false, default: 'done', constraint: 'Terminal status to record. Default: done.' },
|
|
245
|
-
{ kind: 'flag', name: 'reason', type: 'string', required: false, constraint: 'Short failure reason. Required when --status failed; ignored otherwise.' },
|
|
246
|
-
{ kind: 'flag', name: 'kill-pane', type: 'bool', required: false, constraint: `When present, schedule the current tmux pane to close ${DEFAULT_KILL_SECS}s after submission so the spawned worker does not linger. Reviewer agents should pass this; planner/implementer handoffs already self-kill on spawn.` },
|
|
247
|
-
],
|
|
248
|
-
output: [
|
|
249
|
-
{ name: 'submitted', type: 'boolean', required: true, constraint: 'Always true on success.' },
|
|
250
|
-
{ name: 'pane_kill_scheduled', type: 'boolean', required: true, constraint: 'True when --kill-pane is set and a tmux pane kill was scheduled. False otherwise (--kill-pane not set, not in tmux, or TMUX_PANE unset).' },
|
|
251
|
-
],
|
|
252
|
-
outputKind: 'object',
|
|
253
|
-
effects: [
|
|
254
|
-
'Writes <jobdir>/result.md atomically (YAML frontmatter + body), marking the job done or failed.',
|
|
255
|
-
'Updates meta.json status to match.',
|
|
256
|
-
`When --kill-pane is set, schedules \`tmux kill-pane\` on $TMUX_PANE after ${DEFAULT_KILL_SECS}s (detached; submit still returns cleanly).`,
|
|
257
|
-
],
|
|
258
|
-
},
|
|
259
|
-
run: async (input) => {
|
|
260
|
-
const jobId = input['job_id'];
|
|
261
|
-
const status = (typeof input['status'] === 'string' ? input['status'] : 'done');
|
|
262
|
-
const body = typeof input['body'] === 'string' ? input['body'] : '';
|
|
263
|
-
const reason = typeof input['reason'] === 'string' ? input['reason'] : '';
|
|
264
|
-
const killPane = input['killPane'] === true;
|
|
265
|
-
if (status === 'done' && body.trim() === '') {
|
|
266
|
-
throw new InputError({
|
|
267
|
-
error: 'invalid_field',
|
|
268
|
-
message: '--status done requires a markdown body on stdin.',
|
|
269
|
-
field: 'body',
|
|
270
|
-
next: `Pipe the markdown result on stdin, e.g. \`crtr job submit ${jobId} <<'MD' ... MD\`. For failures, use \`--status failed --reason "<why>"\`.`,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
if (status === 'failed' && reason.trim() === '') {
|
|
274
|
-
throw new InputError({
|
|
275
|
-
error: 'invalid_field',
|
|
276
|
-
message: '--status failed requires --reason "<text>".',
|
|
277
|
-
field: 'reason',
|
|
278
|
-
next: 'Pass --reason explaining why the task could not complete.',
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
writeMarkdownResult(jobId, body, status, status === 'failed' ? reason : undefined);
|
|
282
|
-
appendEvent(jobId, {
|
|
283
|
-
level: status === 'failed' ? 'error' : 'info',
|
|
284
|
-
event: 'worker_finished',
|
|
285
|
-
message: status === 'failed' ? `worker failed: ${reason}` : 'worker submitted result',
|
|
286
|
-
});
|
|
287
|
-
const paneKillScheduled = killPane ? scheduleKillCurrentPane(DEFAULT_KILL_SECS) : false;
|
|
288
|
-
return { submitted: true, pane_kill_scheduled: paneKillScheduled };
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
|
-
// _fail — called by the wrapper shell if claude exits without submitting
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
const jobFail = defineLeaf({
|
|
295
|
-
name: '_fail',
|
|
296
|
-
help: {
|
|
297
|
-
name: 'job _fail',
|
|
298
|
-
summary: 'internal: mark a job failed if it has not already been submitted (called by wrapper shell)',
|
|
299
|
-
params: [
|
|
300
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id. If a result file already exists, this is a no-op.' },
|
|
301
|
-
],
|
|
302
|
-
output: [
|
|
303
|
-
{ name: 'recorded', type: 'boolean', required: true, constraint: 'True if failure was recorded; false if a result file already existed (no-op).' },
|
|
304
|
-
],
|
|
305
|
-
outputKind: 'object',
|
|
306
|
-
effects: [
|
|
307
|
-
'Writes result.md with status "failed" and a reason if no result file is present.',
|
|
308
|
-
'Updates meta.json status to failed.',
|
|
309
|
-
],
|
|
310
|
-
},
|
|
311
|
-
run: async (input) => {
|
|
312
|
-
const jobId = input['job_id'];
|
|
313
|
-
try {
|
|
314
|
-
const existing = await jobsReadResult(jobId, { waitMs: 0 });
|
|
315
|
-
if (existing.status !== 'timeout') {
|
|
316
|
-
return { recorded: false };
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
// job dir not found — still try to write to surface the failure
|
|
321
|
-
}
|
|
322
|
-
try {
|
|
323
|
-
writeMarkdownResult(jobId, '', 'failed', 'worker exited without submitting');
|
|
324
|
-
appendEvent(jobId, {
|
|
325
|
-
level: 'error',
|
|
326
|
-
event: 'worker_finished',
|
|
327
|
-
message: 'worker exited without submitting',
|
|
328
|
-
});
|
|
329
|
-
return { recorded: true };
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
return { recorded: false };
|
|
333
|
-
}
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
// ---------------------------------------------------------------------------
|
|
337
|
-
// cancel
|
|
338
|
-
// ---------------------------------------------------------------------------
|
|
339
|
-
const jobCancel = defineLeaf({
|
|
340
|
-
name: 'cancel',
|
|
341
|
-
help: {
|
|
342
|
-
name: 'job cancel',
|
|
343
|
-
summary: 'send a best-effort cancellation signal to a running job',
|
|
344
|
-
params: [
|
|
345
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: 'Job id from a producer (e.g. `crtr agent new *`).' },
|
|
346
|
-
],
|
|
347
|
-
output: [
|
|
348
|
-
{ name: 'canceled', type: 'boolean', required: true, constraint: 'True if a signal was delivered or the job was already terminal; false if the job was not live.' },
|
|
349
|
-
],
|
|
350
|
-
outputKind: 'object',
|
|
351
|
-
effects: ['Best-effort: delivers SIGTERM to the worker process and marks meta.json canceled.'],
|
|
352
|
-
},
|
|
353
|
-
run: async (input) => {
|
|
354
|
-
const jobId = input['job_id'];
|
|
355
|
-
const result = cancelJob(jobId);
|
|
356
|
-
return { canceled: result.canceled };
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
// ---------------------------------------------------------------------------
|
|
360
|
-
// root branch
|
|
361
|
-
// ---------------------------------------------------------------------------
|
|
362
|
-
export function registerJob() {
|
|
363
|
-
return defineBranch({
|
|
364
|
-
name: 'job',
|
|
365
|
-
rootEntry: {
|
|
366
|
-
concept: 'producer-agnostic record of any ongoing task — its logs and result',
|
|
367
|
-
desc: 'monitor and collect from any ongoing task',
|
|
368
|
-
useWhen: 'reading status, logs, or result of a job started by any producer',
|
|
369
|
-
dynamicState: buildJobRootBlock,
|
|
370
|
-
},
|
|
371
|
-
help: {
|
|
372
|
-
name: 'job',
|
|
373
|
-
summary: 'monitor and collect results from any ongoing task',
|
|
374
|
-
model: 'A job is a producer-agnostic record of an ongoing task: state, logs, terminal result. Producers (`crtr agent new *`, future task systems) create jobs; this subtree is the shared read/cancel/submit surface. States: live | done | failed | canceled | closed (worker pane closed before submitting a result).',
|
|
375
|
-
children: [
|
|
376
|
-
{ name: 'read', desc: 'read status, logs, or results', useWhen: 'monitoring or collecting from a running or completed job' },
|
|
377
|
-
{ name: 'submit', desc: 'deliver result from inside a worker pane or any producer', useWhen: 'a worker is ready to return its output' },
|
|
378
|
-
{ name: '_fail', desc: 'internal: mark job failed on unsubmitted exit', useWhen: 'called by the wrapper shell, not manually' },
|
|
379
|
-
{ name: 'cancel', desc: 'best-effort cancel a live job', useWhen: 'stopping a job that is no longer needed' },
|
|
380
|
-
],
|
|
381
|
-
},
|
|
382
|
-
children: [readBranch, jobSubmit, jobFail, jobCancel],
|
|
383
|
-
});
|
|
384
|
-
}
|