catherd-cli 1.4.0 → 2.0.0-beta.20261022113309
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/README.md +188 -169
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -239
- package/package.json +6 -8
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/commands/catherd.md +2 -2
- package/plugin/skills/catherd/SKILL.md +97 -356
- package/plugin/skills/catherd-setup/SKILL.md +21 -20
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +19 -4
- package/src/adapters/claude-code/index.ts +126 -14
- package/src/adapters/codex/events.ts +10 -0
- package/src/adapters/codex/index.ts +17 -5
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +10 -4
- package/src/adapters/opencode/index.ts +38 -1
- package/src/cli.ts +27 -27
- package/src/domain/agents.ts +3 -1
- package/src/domain/bars.ts +33 -107
- package/src/domain/blocked.ts +72 -0
- package/src/domain/branch.ts +51 -0
- package/src/domain/brief.ts +106 -0
- package/src/domain/catalog.ts +69 -313
- package/src/domain/changes.ts +23 -1
- package/src/domain/cost.ts +19 -29
- package/src/domain/errors.ts +6 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -75
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +53 -0
- package/src/domain/hints.ts +13 -2
- package/src/domain/ids.ts +13 -2
- package/src/domain/jev.ts +47 -2
- package/src/domain/judge.ts +124 -0
- package/src/domain/lane.ts +118 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +32 -8
- package/src/domain/notify.ts +32 -0
- package/src/domain/plan-doc.ts +198 -0
- package/src/domain/plan-lint.ts +141 -0
- package/src/domain/profile-rules.ts +175 -138
- package/src/domain/profile.ts +140 -63
- package/src/domain/record.ts +68 -3
- package/src/domain/reply.ts +29 -0
- package/src/domain/role-prompts.ts +182 -56
- package/src/domain/role-scope.ts +76 -0
- package/src/domain/role-tools.ts +34 -0
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +90 -0
- package/src/domain/rules.ts +33 -0
- package/src/domain/run-events.ts +298 -0
- package/src/domain/run-machine.ts +668 -0
- package/src/domain/run-state.ts +1310 -0
- package/src/domain/scorecard.ts +197 -0
- package/src/domain/select.ts +229 -72
- package/src/domain/slopalytics.ts +72 -0
- package/src/domain/state.ts +5 -0
- package/src/domain/util.ts +22 -0
- package/src/domain/workspace.ts +130 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +24 -165
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +18 -2
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/deps.ts +2 -0
- package/src/entry/doctor-command.ts +20 -2
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +20 -69
- package/src/entry/knowledge-command.ts +121 -5
- package/src/entry/lock-command.ts +103 -8
- package/src/entry/mcp/handshake.ts +6 -2
- package/src/entry/mcp/role-bin.ts +20 -0
- package/src/entry/mcp/role-server.ts +134 -0
- package/src/entry/mcp/server.ts +85 -21
- package/src/entry/mcp/session-tools.ts +149 -0
- package/src/entry/panel/openapi.ts +189 -0
- package/src/entry/panel/server.ts +410 -0
- package/src/entry/panel-command.ts +68 -0
- package/src/entry/pause-command.ts +67 -0
- package/src/entry/profile-command.ts +33 -18
- package/src/entry/role-cli-command.ts +159 -0
- package/src/entry/runs-command.ts +81 -27
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/entry/supervise-command.ts +30 -3
- package/src/entry/workspace-command.ts +44 -0
- package/src/infra/boot-lock.ts +25 -0
- package/src/infra/delivery.ts +3 -0
- package/src/infra/dispatch-dir.ts +2 -0
- package/src/infra/env.ts +11 -3
- package/src/infra/git.ts +70 -0
- package/src/infra/heavy-lock.ts +133 -6
- package/src/infra/host-probe.ts +16 -0
- package/src/infra/launch.ts +63 -7
- package/src/infra/lock-activity.ts +88 -0
- package/src/infra/login-env.ts +48 -0
- package/src/infra/memory.ts +45 -0
- package/src/infra/ntfy.ts +40 -0
- package/src/infra/open-url.ts +42 -0
- package/src/infra/paths.ts +37 -2
- package/src/infra/role-mcp.ts +69 -0
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/infra/supervisor.ts +107 -16
- package/src/services/admission.ts +133 -31
- package/src/services/backends.ts +4 -6
- package/src/services/budget.ts +26 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -254
- package/src/services/credentials.ts +15 -26
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +209 -17
- package/src/services/dispatches.ts +30 -2
- package/src/services/doctor-access.ts +8 -3
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor-docker.ts +221 -0
- package/src/services/doctor-role-mcp.ts +65 -0
- package/src/services/doctor.ts +72 -15
- package/src/services/end-push.ts +157 -0
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +24 -2
- package/src/services/gate-env.ts +217 -0
- package/src/services/gate-service.ts +231 -45
- package/src/services/global-install.ts +46 -13
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-edit.ts +154 -0
- package/src/services/lane-service.ts +291 -55
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +167 -29
- package/src/services/notifier.ts +124 -4
- package/src/services/notify-service.ts +26 -0
- package/src/services/orchestrator-wait.ts +82 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/pause.ts +161 -0
- package/src/services/peek.ts +79 -17
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +60 -5
- package/src/services/preflight.ts +232 -24
- package/src/services/profile-service.ts +31 -3
- package/src/services/profile-store.ts +18 -6
- package/src/services/protocol.ts +123 -25
- package/src/services/provenance.ts +34 -89
- package/src/services/questions.ts +24 -0
- package/src/services/reconcile.ts +33 -1
- package/src/services/reentry.ts +7 -6
- package/src/services/role-access.ts +27 -0
- package/src/services/routing-service.ts +161 -42
- package/src/services/run-briefs.ts +477 -0
- package/src/services/run-client.ts +217 -0
- package/src/services/run-effects.ts +837 -0
- package/src/services/run-events-store.ts +174 -0
- package/src/services/run-evidence.ts +1 -1
- package/src/services/run-pin.ts +125 -0
- package/src/services/run-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +204 -23
- package/src/services/run-store.ts +159 -18
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/scratch.ts +60 -0
- package/src/services/sessions.ts +3 -1
- package/src/services/state.ts +21 -0
- package/src/services/summary.ts +76 -12
- package/src/services/tree-path.ts +27 -0
- package/src/services/verifier-brief.ts +23 -0
- package/src/services/verifier-step.ts +48 -0
- package/src/services/workspace-admission.ts +259 -0
- package/src/services/workspace-service.ts +259 -0
- package/src/services/workspace-store.ts +95 -0
- package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
- package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js +1 -0
- package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
- package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
- package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
- package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
- package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
- package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js +1 -0
- package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
- package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-B6GiwDkh.js +11 -0
- package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
- package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
- package/web/dist/assets/index-C5IbylvM.css +2 -0
- package/web/dist/assets/index-C5IbylvM.css.br +0 -0
- package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
- package/web/dist/assets/label-DFSYBc62.js +1 -0
- package/web/dist/assets/label-DFSYBc62.js.br +0 -0
- package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
- package/web/dist/assets/link-D8EcKZUt.js +1 -0
- package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
- package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
- package/web/dist/assets/login-DaN5HPPW.js +1 -0
- package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
- package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
- package/web/dist/assets/query-Cu--ccT3.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js +1 -0
- package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
- package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
- package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
- package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js +1 -0
- package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
- package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
- package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
- package/web/dist/assets/table-BOxXhqS4.js +1 -0
- package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
- package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
- package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
- package/web/dist/assets/time--9kSQbbp.js +1 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
- package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
- package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
- package/web/dist/index.html +15 -0
- package/catalog/ATTRIBUTION.md +0 -36
- package/catalog/scores.json +0 -3202
- package/catalog/sources.json +0 -97
- package/src/adapters/antigravity/events.ts +0 -131
- package/src/adapters/antigravity/home.ts +0 -46
- package/src/adapters/antigravity/index.ts +0 -320
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -132
- package/src/adapters/grok/index.ts +0 -339
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -125
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -84
- package/src/entry/mcp/protocol-tools.ts +0 -57
- package/src/entry/mcp/run-tools.ts +0 -148
- package/src/entry/mcp/setup-tools.ts +0 -108
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -381
- package/src/entry/tui/fixtures.ts +0 -637
- package/src/entry/tui/profile-edits.ts +0 -146
- package/src/entry/tui/profile-tree.ts +0 -429
- package/src/entry/tui/providers/app.tsx +0 -162
- package/src/entry/tui/providers/boundary.tsx +0 -57
- package/src/entry/tui/providers/data.tsx +0 -160
- package/src/entry/tui/providers/keymap.tsx +0 -166
- package/src/entry/tui/providers/root.tsx +0 -45
- package/src/entry/tui/providers/theme.tsx +0 -17
- package/src/entry/tui/providers/toast.tsx +0 -77
- package/src/entry/tui/run.tsx +0 -114
- package/src/entry/tui/state.ts +0 -373
- package/src/entry/tui/stories.ts +0 -114
- package/src/entry/tui/text.ts +0 -130
- package/src/entry/tui/theme.ts +0 -140
- package/src/entry/tui/views/app.tsx +0 -208
- package/src/entry/tui/views/dialogs.tsx +0 -41
- package/src/entry/tui/views/profile-actions.ts +0 -331
- package/src/entry/tui/views/profiles.tsx +0 -520
- package/src/entry/tui/views/runs.tsx +0 -573
- package/src/entry/tui/views/save-dialog.tsx +0 -340
- package/src/entry/tui/views/status.tsx +0 -209
- package/src/entry/tui/widgets/chrome.tsx +0 -114
- package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
- package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
- package/src/entry/tui/widgets/dialog-select.tsx +0 -212
- package/src/entry/tui/widgets/dialog.tsx +0 -68
- package/src/entry/tui/widgets/line.tsx +0 -92
- package/src/entry/tui/widgets/list.tsx +0 -197
- package/src/entry/tui/widgets/spinner.tsx +0 -49
- package/src/entry/tui/widgets/toast.tsx +0 -62
- package/src/infra/sources/arena.ts +0 -87
- package/src/infra/sources/artificial-analysis.ts +0 -112
- package/src/infra/sources/cache.ts +0 -124
- package/src/infra/sources/csv.ts +0 -38
- package/src/infra/sources/epoch.ts +0 -82
- package/src/infra/sources/http.ts +0 -75
- package/src/infra/sources/litellm.ts +0 -58
- package/src/infra/sources/models-dev.ts +0 -59
- package/src/infra/sources/openrouter-endpoints.ts +0 -52
- package/src/infra/sources/openrouter-models.ts +0 -60
- package/src/infra/sources/rows.ts +0 -38
- package/src/infra/sources/vectara.ts +0 -69
- package/src/infra/sources/zip.ts +0 -48
- package/src/services/catalog-refresh.ts +0 -201
- package/src/services/doctor-sources.ts +0 -76
- package/src/services/runs-page.ts +0 -255
- package/src/services/source-derive.ts +0 -374
- package/src/services/source-sync.ts +0 -305
- package/src/services/standins.ts +0 -204
- package/src/services/treat-likes.ts +0 -97
|
@@ -155,6 +155,20 @@ function finalize(run: FinishedRun): Outcome {
|
|
|
155
155
|
const f = foldOpencodeEvents(run.eventLines);
|
|
156
156
|
const stopped = run.exit.reason;
|
|
157
157
|
const tooOld = f.tooOld || OPENCODE_TOO_OLD.some((r) => r.test(run.stderr));
|
|
158
|
+
// plan 23: the provider kept failing; failover takes it as a usage limit, onto the next backend
|
|
159
|
+
if (stopped === "provider-unavailable")
|
|
160
|
+
return {
|
|
161
|
+
status: "limit",
|
|
162
|
+
thread: f.thread ?? run.request.thread,
|
|
163
|
+
tokens: f.tokens,
|
|
164
|
+
costUsd: f.costUsd,
|
|
165
|
+
images: [],
|
|
166
|
+
error: {
|
|
167
|
+
code: "provider-unavailable",
|
|
168
|
+
message: f.error?.message ?? "the provider retried with no progress",
|
|
169
|
+
},
|
|
170
|
+
reply: f.reply,
|
|
171
|
+
};
|
|
158
172
|
const status: RunStatus =
|
|
159
173
|
stopped === "cancelled"
|
|
160
174
|
? "cancelled"
|
|
@@ -201,6 +215,8 @@ function parse(line: string): EventDelta {
|
|
|
201
215
|
const activity = opencodeActivity(e);
|
|
202
216
|
if (activity) d.activity = activity;
|
|
203
217
|
if (typeof e.sessionID === "string") d.thread = e.sessionID;
|
|
218
|
+
// plan 23: each provider retry starts a step again with nothing between; the supervisor counts them
|
|
219
|
+
if (e.type === "step_start") d.step = true;
|
|
204
220
|
if (e.type === "step_finish") {
|
|
205
221
|
d.tokens = opencodeTokens(e.part?.tokens);
|
|
206
222
|
d.requestInput = d.tokens.input;
|
|
@@ -220,6 +236,21 @@ function parse(line: string): EventDelta {
|
|
|
220
236
|
* older messages keep their errors, so only the newest assistant message counts, and with `sinceMs` only
|
|
221
237
|
* if this run wrote it.
|
|
222
238
|
*/
|
|
239
|
+
/**
|
|
240
|
+
* Plan 23: the provider retry the newest assistant message this run wrote is waiting out (a 503, an
|
|
241
|
+
* overloaded backend), by its attempt number; null when it is not retrying.
|
|
242
|
+
*/
|
|
243
|
+
function providerRetryOf(messages: unknown, sinceMs: number): number | null {
|
|
244
|
+
if (!Array.isArray(messages)) return null;
|
|
245
|
+
const m = messages.find((x) => x?.type === "assistant") as Record<string, any> | undefined;
|
|
246
|
+
if (!m?.retry || !(m.time?.created >= sinceMs)) return null;
|
|
247
|
+
return typeof m.retry.attempt === "number" ? m.retry.attempt : 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function providerRetry(thread: string, sinceMs: number): Promise<number | null> {
|
|
251
|
+
return providerRetryOf((await opencodeApi("GET", `/api/session/${thread}/message`))?.data, sinceMs);
|
|
252
|
+
}
|
|
253
|
+
|
|
223
254
|
function limitRetry(messages: unknown, sinceMs?: number): string | null {
|
|
224
255
|
if (!Array.isArray(messages)) return null;
|
|
225
256
|
const m = messages.find((x) => x?.type === "assistant") as Record<string, any> | undefined;
|
|
@@ -265,7 +296,12 @@ async function isBusy(thread: string, sinceMs?: number): Promise<boolean> {
|
|
|
265
296
|
const active = (await opencodeApi("GET", "/api/session/active"))?.data;
|
|
266
297
|
if (!active || typeof active !== "object" || !(thread in active)) return false;
|
|
267
298
|
const messages = (await opencodeApi("GET", `/api/session/${thread}/message`))?.data;
|
|
268
|
-
|
|
299
|
+
// plan 23: a session only retrying its provider is not busy, so the idle timeout still runs
|
|
300
|
+
return (
|
|
301
|
+
Array.isArray(messages) &&
|
|
302
|
+
limitRetry(messages, sinceMs) === null &&
|
|
303
|
+
providerRetryOf(messages, sinceMs ?? 0) === null
|
|
304
|
+
);
|
|
269
305
|
}
|
|
270
306
|
|
|
271
307
|
/** Killing the v2 client does not stop its session; the service must be told (research §2.4). */
|
|
@@ -327,6 +363,7 @@ export const opencodeAdapter: BackendAdapter = {
|
|
|
327
363
|
resume: { supported: true, sameAccessOnly: false, threadPattern: THREAD },
|
|
328
364
|
interrupt: (thread) => interrupt(thread),
|
|
329
365
|
isBusy: (thread, _cwd, sinceMs) => isBusy(thread, sinceMs),
|
|
366
|
+
providerRetry: (thread, _cwd, sinceMs) => providerRetry(thread, sinceMs),
|
|
330
367
|
failoverFor,
|
|
331
368
|
graceAfterFinalMs: null,
|
|
332
369
|
reportsCost: true,
|
package/src/cli.ts
CHANGED
|
@@ -20,29 +20,23 @@ const { VERSION } = await import("./infra/version.ts");
|
|
|
20
20
|
|
|
21
21
|
type Command = ReturnType<typeof defineCommand>;
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
const
|
|
25
|
-
...HOST_ARG,
|
|
26
|
-
plain: {
|
|
27
|
-
type: "boolean",
|
|
28
|
-
description: "the dashboard in ASCII without colour (NO_COLOR also drops colour)",
|
|
29
|
-
},
|
|
30
|
-
"reduced-motion": { type: "boolean", description: "the dashboard without animation" },
|
|
31
|
-
};
|
|
23
|
+
/** HOST_ARG typed as any command's args, so `main` stays a plain `Command`. */
|
|
24
|
+
const MAIN_ARGS: ArgsDef = HOST_ARG;
|
|
32
25
|
|
|
33
26
|
/**
|
|
34
|
-
* Spec §8. Every subcommand loads lazily, so `mcp`, `lock` and `_supervise` never load
|
|
35
|
-
*
|
|
27
|
+
* Spec §8. Every subcommand loads lazily, so `mcp`, `lock` and `_supervise` never load the panel (spec §3.1);
|
|
28
|
+
* a bare `catherd` runs `catherd panel`.
|
|
36
29
|
*/
|
|
37
30
|
export const main: Command = defineCommand({
|
|
38
31
|
meta: {
|
|
39
32
|
name: "catherd",
|
|
40
33
|
version: VERSION,
|
|
41
|
-
description: "Herds coding agents",
|
|
34
|
+
description: "Herds coding agents. catherd (or catherd panel) opens the panel in your browser.",
|
|
42
35
|
},
|
|
43
|
-
args:
|
|
36
|
+
args: MAIN_ARGS,
|
|
44
37
|
subCommands: {
|
|
45
38
|
init: () => import("./entry/init-command.ts").then((m) => m.initCommand),
|
|
39
|
+
panel: () => import("./entry/panel-command.ts").then((m) => m.panelCommand),
|
|
46
40
|
status: () => import("./entry/runs-command.ts").then((m) => m.statusCommand),
|
|
47
41
|
watch: () => import("./entry/runs-command.ts").then((m) => m.watchCommand),
|
|
48
42
|
runs: () => import("./entry/runs-command.ts").then((m) => m.runsCommand),
|
|
@@ -50,17 +44,19 @@ export const main: Command = defineCommand({
|
|
|
50
44
|
doctor: () => import("./entry/doctor-command.ts").then((m) => m.doctorCommand),
|
|
51
45
|
catalog: () => import("./entry/catalog-command.ts").then((m) => m.catalogCommand),
|
|
52
46
|
knowledge: () => import("./entry/knowledge-command.ts").then((m) => m.knowledgeCommand),
|
|
47
|
+
workspace: () => import("./entry/workspace-command.ts").then((m) => m.workspaceCommand),
|
|
53
48
|
lock: () => import("./entry/lock-command.ts").then((m) => m.lockCommand),
|
|
49
|
+
"run-file": () => import("./entry/role-cli-command.ts").then((m) => m.runFileCommand),
|
|
50
|
+
gate: () => import("./entry/role-cli-command.ts").then((m) => m.gateCommand),
|
|
51
|
+
answer: () => import("./entry/answer-command.ts").then((m) => m.answerCommand),
|
|
52
|
+
pause: () => import("./entry/pause-command.ts").then((m) => m.pauseCommand),
|
|
53
|
+
resume: () => import("./entry/pause-command.ts").then((m) => m.resumeCommand),
|
|
54
54
|
"capture-fixtures": () =>
|
|
55
55
|
import("./entry/capture-fixtures-command.ts").then((m) => m.captureFixturesCommand),
|
|
56
|
+
scorecard: () => import("./entry/scorecard-command.ts").then((m) => m.scorecardCommand),
|
|
56
57
|
mcp: () => import("./entry/mcp/command.ts").then((m) => m.mcpCommand),
|
|
57
58
|
_supervise: () => import("./entry/supervise-command.ts").then((m) => m.superviseCommand),
|
|
58
59
|
},
|
|
59
|
-
// citty runs this after any subcommand too; only a bare `catherd` loads and opens the dashboard
|
|
60
|
-
async run(ctx) {
|
|
61
|
-
if (ctx.args._.length > 0) return;
|
|
62
|
-
await (await import("./entry/tui/run.tsx")).tuiRun(ctx);
|
|
63
|
-
},
|
|
64
60
|
});
|
|
65
61
|
|
|
66
62
|
const resolve = async <T>(v: T | Promise<T> | (() => T | Promise<T>)): Promise<T> =>
|
|
@@ -136,13 +132,10 @@ export async function runCli(argv: string[]): Promise<number> {
|
|
|
136
132
|
return EXIT.ok;
|
|
137
133
|
}
|
|
138
134
|
try {
|
|
139
|
-
const command = parseArgs(own,
|
|
140
|
-
// a bare `catherd`
|
|
135
|
+
const command = parseArgs(own, HOST_ARG)._[0];
|
|
136
|
+
// a bare `catherd` takes only --host: a typo is a usage error
|
|
141
137
|
const flag = (a: string) => (a.startsWith("--") ? (a.slice(2).split("=")[0] ?? "") : "");
|
|
142
|
-
|
|
143
|
-
const known = (name: string) =>
|
|
144
|
-
Object.hasOwn(dashboardArgs, name) ||
|
|
145
|
-
(name.startsWith("no-") && dashboardArgs[name.slice(3)]?.type === "boolean");
|
|
138
|
+
const known = (name: string) => Object.hasOwn(HOST_ARG, name);
|
|
146
139
|
const unknown =
|
|
147
140
|
command === undefined
|
|
148
141
|
? own.find((a, i) => a.startsWith("-") && !known(flag(a)) && own[i - 1] !== "--host")
|
|
@@ -152,9 +145,11 @@ export async function runCli(argv: string[]): Promise<number> {
|
|
|
152
145
|
return EXIT.usage;
|
|
153
146
|
}
|
|
154
147
|
// `lock` forwards signals to its command itself; everything else stops at once on Ctrl-C. The command is
|
|
155
|
-
// the first word that is not a flag: `catherd --
|
|
156
|
-
|
|
157
|
-
|
|
148
|
+
// the first word that is not a flag: `catherd --host codex lock -- …` is `lock` too.
|
|
149
|
+
// the panel stops itself on Ctrl-C
|
|
150
|
+
if (command !== "lock" && command !== undefined)
|
|
151
|
+
process.once("SIGINT", () => process.exit(EXIT.interrupted));
|
|
152
|
+
const parsed = parseArgs(own, HOST_ARG);
|
|
158
153
|
if (parsed.host !== undefined) {
|
|
159
154
|
const forwarded = own.filter(
|
|
160
155
|
(a, i) => a !== "--host" && own[i - 1] !== "--host" && !a.startsWith("--host="),
|
|
@@ -168,6 +163,11 @@ export async function runCli(argv: string[]): Promise<number> {
|
|
|
168
163
|
...(dashdash < 0 ? [] : argv.slice(dashdash)),
|
|
169
164
|
);
|
|
170
165
|
}
|
|
166
|
+
if (command === undefined) {
|
|
167
|
+
const { runPanel } = await import("./entry/panel-command.ts");
|
|
168
|
+
await runPanel({ port: 4317, open: true, host: parsed.host });
|
|
169
|
+
return EXIT.ok;
|
|
170
|
+
}
|
|
171
171
|
await runCommand(main, { rawArgs });
|
|
172
172
|
return Number(process.exitCode ?? EXIT.ok);
|
|
173
173
|
} catch (e) {
|
package/src/domain/agents.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseRung, tryParseRung } from "./ids.ts";
|
|
2
2
|
import { agentName, type Profile } from "./profile.ts";
|
|
3
3
|
import type { Access } from "./record.ts";
|
|
4
|
-
import { nativeDisallowedTools, rolePrompt } from "./role-prompts.ts";
|
|
4
|
+
import { NOT_THE_ORCHESTRATOR, nativeDisallowedTools, rolePrompt } from "./role-prompts.ts";
|
|
5
5
|
import { ROLES, type Role } from "./roles.ts";
|
|
6
6
|
|
|
7
7
|
export interface AgentFile {
|
|
@@ -32,6 +32,8 @@ export function renderAgent(o: {
|
|
|
32
32
|
"",
|
|
33
33
|
rolePrompt(o.role, o.version),
|
|
34
34
|
"",
|
|
35
|
+
NOT_THE_ORCHESTRATOR,
|
|
36
|
+
"",
|
|
35
37
|
].join("\n");
|
|
36
38
|
}
|
|
37
39
|
|
package/src/domain/bars.ts
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
logic: "the 60th percentile",
|
|
32
|
-
hard: "the 75th percentile",
|
|
33
|
-
};
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Catalog } from "./catalog.ts";
|
|
3
|
+
import { DIFFICULTIES, type Difficulty } from "./lane.ts";
|
|
4
|
+
|
|
5
|
+
/** The percentile of the shipped models' intelligences each difficulty's bar sits at. */
|
|
6
|
+
export const BAR_PERCENTILE: Record<Difficulty, number> = { copy: 20, build: 45, logic: 60, hard: 65 };
|
|
7
|
+
|
|
8
|
+
/** A profile's or a role's own percentiles (0 to 100), one difficulty at a time; a difficulty left out keeps its shipped percentile. */
|
|
9
|
+
export const BarsSchema = z.partialRecord(z.enum(DIFFICULTIES), z.number().min(0).max(100));
|
|
10
|
+
export type Bars = z.infer<typeof BarsSchema>;
|
|
11
|
+
|
|
12
|
+
/** The percentiles in force as stored: the shipped ones, with `p.bars` laid over them; they may decrease. */
|
|
13
|
+
export const effectiveBars = (p: { bars?: Bars }): Record<Difficulty, number> => ({
|
|
14
|
+
...BAR_PERCENTILE,
|
|
15
|
+
...p.bars,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** Each pair `[easier, harder]` of neighbouring difficulties whose percentile goes down. */
|
|
19
|
+
export const barInversions = (at: Record<Difficulty, number>): [Difficulty, Difficulty][] =>
|
|
20
|
+
DIFFICULTIES.slice(1).flatMap((d, i): [Difficulty, Difficulty][] =>
|
|
21
|
+
at[d] < at[DIFFICULTIES[i] as Difficulty] ? [[DIFFICULTIES[i] as Difficulty, d]] : [],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
/** The percentiles a route reads: the effective ones made non-decreasing, so a harder lane never routes weaker. */
|
|
25
|
+
export function percentilesOf(p: { bars?: Bars }): Record<Difficulty, number> {
|
|
26
|
+
const at = effectiveBars(p);
|
|
27
|
+
let floor = 0;
|
|
28
|
+
for (const d of DIFFICULTIES) at[d] = floor = Math.max(at[d], floor);
|
|
29
|
+
return at;
|
|
30
|
+
}
|
|
34
31
|
|
|
35
32
|
/** The `p`th percentile of `xs`, interpolated linearly between the closest ranks; NaN for none. */
|
|
36
33
|
export function percentile(xs: readonly number[], p: number): number {
|
|
@@ -42,78 +39,7 @@ export function percentile(xs: readonly number[], p: number): number {
|
|
|
42
39
|
return (s[lo] as number) + ((s[hi] as number) - (s[lo] as number)) * (pos - lo);
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* published by someone other than the vendor, which is 1.2's `measured`). Plan 14 Ruling 1.
|
|
49
|
-
*/
|
|
50
|
-
export const measuredOrBetter = (s: Score): boolean =>
|
|
51
|
-
s.confidence === "verified" ||
|
|
52
|
-
s.confidence === "measured" ||
|
|
53
|
-
(s.confidence === "secondary" && s.source === undefined);
|
|
54
|
-
|
|
55
|
-
/** Each dimension's values that bars are derived from: one per canonical rung, measured or better. */
|
|
56
|
-
export function barPool(scores: readonly Score[]): Record<Dim, number[]> {
|
|
57
|
-
const out = Object.fromEntries(DIMS.map((d) => [d, [] as number[]])) as Record<Dim, number[]>;
|
|
58
|
-
const seen = new Set<string>();
|
|
59
|
-
for (const s of scores) {
|
|
60
|
-
const key = `${s.rung} ${s.dim}`;
|
|
61
|
-
if (!measuredOrBetter(s) || seen.has(key)) continue;
|
|
62
|
-
seen.add(key);
|
|
63
|
-
out[s.dim].push(s.value);
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** A bar value, kept to four significant digits (DeepSWE 60.95, an Arena rating 1668, agentic 0.08622). */
|
|
69
|
-
const tidy = (v: number): number => Number(v.toPrecision(4));
|
|
70
|
-
|
|
71
|
-
export type Bars = Record<Kind, Record<Difficulty, Partial<Record<Dim, number>>>>;
|
|
72
|
-
/** `scores.json` `barsWhy`: per dimension and difficulty, where its threshold came from. */
|
|
73
|
-
export type BarsWhy = Partial<Record<Dim, Partial<Record<Difficulty, string>>>>;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Spec 1.2 §5.1–§5.2: the default bars. Each dimension's threshold at a difficulty is that difficulty's
|
|
77
|
-
* percentile of the pool (`barPool`), the same in every kind whose bar spans the dimension. A dimension with
|
|
78
|
-
* no pooled value gets no threshold (and a `barsWhy` line saying so). `benchmark` names each dimension's unit
|
|
79
|
-
* and `date` the day the data was read.
|
|
80
|
-
*/
|
|
81
|
-
export function deriveBars(
|
|
82
|
-
pool: Record<Dim, number[]>,
|
|
83
|
-
benchmark: (d: Dim) => string,
|
|
84
|
-
date: string,
|
|
85
|
-
): { bars: Bars; barsWhy: BarsWhy } {
|
|
86
|
-
const value: Partial<Record<Dim, Partial<Record<Difficulty, number>>>> = {};
|
|
87
|
-
const barsWhy: BarsWhy = {};
|
|
88
|
-
for (const dim of BAR_DIMS)
|
|
89
|
-
for (const d of DIFFICULTIES) {
|
|
90
|
-
if (!KINDS.some((k) => barDimsOf(k, d).includes(dim))) continue;
|
|
91
|
-
const xs = pool[dim];
|
|
92
|
-
const why = (barsWhy[dim] ??= {});
|
|
93
|
-
if (xs.length === 0) {
|
|
94
|
-
why[d] = `no rung is measured or better on ${benchmark(dim)} (${date}): no threshold`;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const v = tidy(percentile(xs, BAR_PERCENTILE[d]));
|
|
98
|
-
(value[dim] ??= {})[d] = v;
|
|
99
|
-
why[d] =
|
|
100
|
-
`${PERCENTILE_WORD[d]} of ${xs.length} rungs measured or better on ${benchmark(dim)} (${date}): ${v}`;
|
|
101
|
-
}
|
|
102
|
-
const bars = Object.fromEntries(
|
|
103
|
-
KINDS.map((k) => [
|
|
104
|
-
k,
|
|
105
|
-
Object.fromEntries(
|
|
106
|
-
DIFFICULTIES.map((d) => [
|
|
107
|
-
d,
|
|
108
|
-
Object.fromEntries(
|
|
109
|
-
barDimsOf(k, d).flatMap((dim) => {
|
|
110
|
-
const v = value[dim]?.[d];
|
|
111
|
-
return v === undefined ? [] : [[dim, v]];
|
|
112
|
-
}),
|
|
113
|
-
),
|
|
114
|
-
]),
|
|
115
|
-
),
|
|
116
|
-
]),
|
|
117
|
-
) as Bars;
|
|
118
|
-
return { bars, barsWhy };
|
|
42
|
+
/** A difficulty's bar: an intelligence percentile of the catalog's pool, 0 when there is no index. */
|
|
43
|
+
export function barOf(c: Catalog, d: Difficulty, at: Record<Difficulty, number> = BAR_PERCENTILE): number {
|
|
44
|
+
return c.pool.length === 0 ? 0 : percentile(c.pool, at[d]);
|
|
119
45
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Why a role stopped without finishing (spec 2.0 decision 3); the machine maps each to a transition. */
|
|
2
|
+
export const BLOCKED_REASONS = [
|
|
3
|
+
"needs_user",
|
|
4
|
+
"too_hard",
|
|
5
|
+
"provider_error",
|
|
6
|
+
"no_access",
|
|
7
|
+
"check_broken",
|
|
8
|
+
"out_of_scope",
|
|
9
|
+
] as const;
|
|
10
|
+
export type BlockedReason = (typeof BLOCKED_REASONS)[number];
|
|
11
|
+
|
|
12
|
+
export type StopAction = "climb" | "failover" | "park" | "wait";
|
|
13
|
+
|
|
14
|
+
/** What the machine does with each reason (spec 2.0 decision 8): climb, retry then fail over, park, or wait. */
|
|
15
|
+
export const REASON_ACTION: Readonly<Record<BlockedReason, StopAction>> = {
|
|
16
|
+
needs_user: "park",
|
|
17
|
+
too_hard: "climb",
|
|
18
|
+
provider_error: "failover",
|
|
19
|
+
no_access: "park",
|
|
20
|
+
check_broken: "wait",
|
|
21
|
+
out_of_scope: "park",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const REASON_AT_START = /^[`*\s]*([a-z][a-z_-]*)[`*]*\s*(?:[:—–-]\s*)?([\s\S]*)$/i;
|
|
25
|
+
|
|
26
|
+
/** The reason a `STATUS: blocked — <reason>: <why>` line names, and its why; null when it names none. */
|
|
27
|
+
export function parseBlockedReason(why: string | null): { reason: BlockedReason; why: string } | null {
|
|
28
|
+
const m = why === null ? null : REASON_AT_START.exec(why.trim());
|
|
29
|
+
if (!m) return null;
|
|
30
|
+
const word = (m[1] as string).toLowerCase().replace(/-/g, "_");
|
|
31
|
+
const reason = BLOCKED_REASONS.find((r) => r === word);
|
|
32
|
+
return reason ? { reason, why: (m[2] ?? "").trim() } : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The reply contract's blocked line, ported from t3herd `prompts/_no_questions.md`. */
|
|
36
|
+
export const BLOCKED_LINE = `Nobody reads your thread until you finish: never ask a question and never wait for an answer. When you cannot finish, leave the finished work in place, say what is left, and end with STATUS: blocked — <reason>: <why>, where <reason> is one of: ${[
|
|
37
|
+
"needs_user (only the user can decide; <why> is the question)",
|
|
38
|
+
"too_hard (you tried and cannot solve it; a stronger model should)",
|
|
39
|
+
"provider_error (your model, tools or sandbox failed)",
|
|
40
|
+
"no_access (you cannot reach what you need: the running app, a service, credentials)",
|
|
41
|
+
"check_broken (the check cannot pass for a reason outside your task; name each failing file:line)",
|
|
42
|
+
"out_of_scope (the task needs changes outside the files you own)",
|
|
43
|
+
].join(", ")}.`;
|
|
44
|
+
|
|
45
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
46
|
+
|
|
47
|
+
/** An `Owns:` entry as a path: cut at the first glob, trailing slashes dropped. */
|
|
48
|
+
const pathOf = (entry: string): string => entry.replace(/[*?[{].*$/, "").replace(/\/+$/, "");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The sibling `Owns:` entries a failed check blames, or [] when it blames none, or also blames one of the lane's
|
|
52
|
+
* own. Output names a file by its tail (`pkg/x.go:3`, `src/a.tsx(4,2)`), so a sibling's entry counts from two
|
|
53
|
+
* path segments, and an own entry already from its base name (t3herd `foreignFailure`).
|
|
54
|
+
*/
|
|
55
|
+
export function foreignFailure(log: string, own: string[], siblings: string[]): string[] {
|
|
56
|
+
const lines = log
|
|
57
|
+
.replace(ANSI, "")
|
|
58
|
+
.split("\n")
|
|
59
|
+
.filter((l) => !/^\s*(✓|ok\s|PASS\b)/.test(l));
|
|
60
|
+
const named = (p: string, min: number): boolean => {
|
|
61
|
+
const parts = p.split("/");
|
|
62
|
+
for (let i = 0; i <= parts.length - min; i++) {
|
|
63
|
+
const tail = parts.slice(i).join("/");
|
|
64
|
+
if (lines.some((l) => l.includes(tail))) return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
const mine = own.map(pathOf).filter(Boolean);
|
|
69
|
+
if (mine.some((f) => named(f, 1))) return [];
|
|
70
|
+
const theirs = siblings.map(pathOf).filter((f) => f && !mine.includes(f));
|
|
71
|
+
return [...new Set(theirs.filter((f) => named(f, Math.min(2, f.split("/").length))))];
|
|
72
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { headerLine } from "./lane.ts";
|
|
2
|
+
import { slug } from "./util.ts";
|
|
3
|
+
import { BaseRefSchema } from "./workspace.ts";
|
|
4
|
+
|
|
5
|
+
/** The run title as a folder and branch stem: at most 40 characters, `run` when nothing is left. */
|
|
6
|
+
export const runSlug = (title: string): string => slug(title).slice(0, 40) || "run";
|
|
7
|
+
|
|
8
|
+
/** A milestone's branch: the run's slug and the milestone, lower case (`wallet-split-m1`). */
|
|
9
|
+
export const milestoneBranch = (title: string, m: string): string => `${runSlug(title)}-${m.toLowerCase()}`;
|
|
10
|
+
|
|
11
|
+
/** A branch on origin as `run_start`'s base: a plain name, never a remote ref (`origin/…`) or a full ref. */
|
|
12
|
+
export const BaseBranchSchema = BaseRefSchema.refine(
|
|
13
|
+
(s) => !s.startsWith("origin/") && !s.startsWith("refs/"),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The `Setup:` command in the lines right under plan.md's `# ` title, up to the first blank line or heading
|
|
18
|
+
* (backticks stripped); null without one. A blank line after the title ends the header, so body prose never counts.
|
|
19
|
+
*/
|
|
20
|
+
export function planSetup(planText: string): string | null {
|
|
21
|
+
const lines = planText.split("\n");
|
|
22
|
+
const start = lines.findIndex((l) => /^#\s+/.test(l)) + 1;
|
|
23
|
+
let end = start;
|
|
24
|
+
while (end < lines.length && (lines[end] ?? "").trim() !== "" && !(lines[end] ?? "").startsWith("#")) end++;
|
|
25
|
+
const re = headerLine("setup");
|
|
26
|
+
for (const line of lines.slice(start, end)) {
|
|
27
|
+
const m = re.exec(line);
|
|
28
|
+
if (m)
|
|
29
|
+
return (
|
|
30
|
+
(m[1] ?? "")
|
|
31
|
+
.trim()
|
|
32
|
+
.replace(/^`+|`+$/g, "")
|
|
33
|
+
.trim() || null
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Whether the owner's answer to a landed milestone's question is the word that has catherd push its branch. */
|
|
40
|
+
export const isPushAnswer = (answer: string): boolean => answer.trim().toLowerCase() === "push";
|
|
41
|
+
|
|
42
|
+
/** The command that opens a milestone's MR, for the origin's host: gh on github.com, else glab. */
|
|
43
|
+
export function mrCommand(o: { originUrl: string; base: string; branch: string; body: string }): string {
|
|
44
|
+
return /(^|[@/.])github\.com[:/]/.test(o.originUrl)
|
|
45
|
+
? `gh pr create --base ${o.base} --head ${o.branch} --body-file ${o.body}`
|
|
46
|
+
: `glab mr create --source-branch ${o.branch} --target-branch ${o.base} --description-file ${o.body}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The milestone a unit (`run`, `M1`, `M1.L2`) belongs to; null for the run itself. */
|
|
50
|
+
export const unitMilestone = (unit: string): string | null =>
|
|
51
|
+
unit === "run" ? null : (unit.split(".")[0] ?? null);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { replyContract, roleSections } from "./role-prompts.ts";
|
|
2
|
+
import { ROLE_MCP_SERVER, roleMcpTools } from "./role-tools.ts";
|
|
3
|
+
import type { Role } from "./roles.ts";
|
|
4
|
+
|
|
5
|
+
/** What admission adds to a role's brief, besides its reply contract (spec 1.5 plan 21). */
|
|
6
|
+
export interface BriefContext {
|
|
7
|
+
run: string;
|
|
8
|
+
name: string;
|
|
9
|
+
role: Role;
|
|
10
|
+
/** the lane file as it stands at admission, header and body */
|
|
11
|
+
lane: { id: string; text: string } | null;
|
|
12
|
+
/** the role's scratch folder, its TMPDIR, on a backend that grants it */
|
|
13
|
+
scratch: string | null;
|
|
14
|
+
/** whether the role's harness lists the catherd_role tools */
|
|
15
|
+
roleServer: boolean;
|
|
16
|
+
/** the catherd version whose `lock` a worker's text names */
|
|
17
|
+
version: string;
|
|
18
|
+
/** the plan's preamble, which applies to every task (plan 32); null when the run has none for this role */
|
|
19
|
+
context: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The CLI form of each role server tool, which any role can run from its shell. */
|
|
23
|
+
function cliForm(tool: string, run: string): string {
|
|
24
|
+
switch (tool) {
|
|
25
|
+
case "read_run_file":
|
|
26
|
+
return `catherd run-file read ${run} <path>`;
|
|
27
|
+
case "write_run_file":
|
|
28
|
+
return `catherd run-file write ${run} <path> (the content on stdin)`;
|
|
29
|
+
case "read_knowledge":
|
|
30
|
+
return "catherd knowledge show";
|
|
31
|
+
case "gate_check":
|
|
32
|
+
return `catherd gate check ${run} --milestone <M> --item <item> --command <command> --paths <path,…>`;
|
|
33
|
+
case "gate_pass":
|
|
34
|
+
return `catherd gate pass ${run} --item <item> --command <command> --paths <path,…> --evidence <evidence>`;
|
|
35
|
+
case "gate_run":
|
|
36
|
+
return `catherd gate run ${run} --milestone <M> --command <command> [--at <base checkout>]`;
|
|
37
|
+
default:
|
|
38
|
+
return tool;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const LANE_OPEN = (id: string) => `<lane-file path="lanes/${id}.md">`;
|
|
43
|
+
const LANE_CLOSE = "</lane-file>";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The lane file inlined (spec 1.5 plan 21): an isolated headless worker cannot read the run folder, so its Owns,
|
|
47
|
+
* fast check and body travel in the brief itself.
|
|
48
|
+
*/
|
|
49
|
+
export function laneBlock(id: string, text: string): string {
|
|
50
|
+
return [
|
|
51
|
+
`Your lane file, lanes/${id}.md, as it stood when you were dispatched:`,
|
|
52
|
+
LANE_OPEN(id),
|
|
53
|
+
text.trimEnd(),
|
|
54
|
+
LANE_CLOSE,
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Who the role is, where its temp files go, and how it reaches its run's files and gate. */
|
|
59
|
+
export function roleNotes(c: BriefContext): string {
|
|
60
|
+
const tools = roleMcpTools(c.role);
|
|
61
|
+
const lines = [`catherd: you are ${c.name} of run ${c.run}.`];
|
|
62
|
+
if (c.scratch)
|
|
63
|
+
lines.push(
|
|
64
|
+
`Your scratch folder is ${c.scratch}, which is your $TMPDIR: put temporary files, logs and builds there, never in /tmp. catherd removes it with the run, so anything the orchestrator must keep goes in your reply.`,
|
|
65
|
+
);
|
|
66
|
+
const cli = tools.map((t) => cliForm(t, c.run)).join("; ");
|
|
67
|
+
lines.push(
|
|
68
|
+
c.roleServer
|
|
69
|
+
? `Your catherd tools: ${tools.map((t) => `mcp__${ROLE_MCP_SERVER}__${t}`).join(", ")}. If they are not listed, run the same from your shell: ${cli}.`
|
|
70
|
+
: `Your catherd commands, from your shell: ${cli}.`,
|
|
71
|
+
);
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const TASK_OPEN = "<task>\n";
|
|
76
|
+
const TASK_CLOSE = "\n</task>";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The brief admission writes: the role's sections around its task (the daemon's text, the lane file and the
|
|
80
|
+
* role's notes), each once. A failover stand-in that reruns an earlier brief gets the lane file as it stands now and
|
|
81
|
+
* its own notes, never two copies.
|
|
82
|
+
*/
|
|
83
|
+
export function composeBrief(brief: string, c: BriefContext): string {
|
|
84
|
+
const contract = replyContract(c.role);
|
|
85
|
+
let body = brief.trimEnd();
|
|
86
|
+
const open = body.indexOf(TASK_OPEN);
|
|
87
|
+
const close = body.lastIndexOf(TASK_CLOSE);
|
|
88
|
+
if (open >= 0 && close > open) body = body.slice(open + TASK_OPEN.length, close).trim();
|
|
89
|
+
// a 1.5-era brief rerun ends with its contract
|
|
90
|
+
if (body.endsWith(contract)) body = body.slice(0, -contract.length).trimEnd();
|
|
91
|
+
if (c.lane) {
|
|
92
|
+
const open = LANE_OPEN(c.lane.id);
|
|
93
|
+
const start = body.lastIndexOf(open);
|
|
94
|
+
const end = body.indexOf(LANE_CLOSE, start);
|
|
95
|
+
if (start >= 0 && end >= 0) {
|
|
96
|
+
const lead = body.lastIndexOf("\n\n", start);
|
|
97
|
+
body = `${body.slice(0, lead >= 0 ? lead : start)}${body.slice(end + LANE_CLOSE.length)}`.trimEnd();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const notesStart = body.lastIndexOf("\n\ncatherd: you are ");
|
|
101
|
+
if (notesStart >= 0) body = body.slice(0, notesStart).trimEnd();
|
|
102
|
+
const task = [body, ...(c.lane ? [laneBlock(c.lane.id, c.lane.text)] : []), roleNotes(c)]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join("\n\n");
|
|
105
|
+
return `${roleSections(c.role, c.version, { context: c.context, task })}\n`;
|
|
106
|
+
}
|