catherd-cli 1.5.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 +164 -208
- package/THIRD_PARTY_NOTICES.md +217 -35
- package/catalog/jev.json +27 -1
- package/catalog/models.json +56 -247
- 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 -421
- package/plugin/skills/catherd-setup/SKILL.md +20 -19
- package/src/adapters/all.ts +0 -4
- package/src/adapters/backend.ts +1 -2
- package/src/adapters/cursor/home.ts +1 -1
- package/src/adapters/cursor/index.ts +4 -4
- package/src/cli.ts +22 -27
- 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 +21 -6
- package/src/domain/catalog.ts +69 -324
- package/src/domain/cost.ts +15 -30
- package/src/domain/errors.ts +2 -2
- package/src/domain/fail-lines.ts +26 -0
- package/src/domain/failover.ts +37 -87
- package/src/domain/findings.ts +47 -0
- package/src/domain/gate-brief.ts +1 -0
- 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 +22 -3
- package/src/domain/milestone-memory.ts +53 -0
- package/src/domain/notice.ts +25 -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 +101 -160
- package/src/domain/profile.ts +116 -63
- package/src/domain/record.ts +53 -2
- package/src/domain/role-prompts.ts +131 -45
- package/src/domain/role-scope.ts +4 -34
- package/src/domain/role-tools.ts +2 -2
- package/src/domain/roles.ts +2 -0
- package/src/domain/route.ts +17 -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 +151 -146
- package/src/domain/slopalytics.ts +72 -0
- package/src/entry/answer-command.ts +39 -0
- package/src/entry/catalog-command.ts +25 -174
- package/src/entry/check-bin.ts +17 -0
- package/src/entry/cli-kit.ts +1 -1
- package/src/entry/daemon-bin.ts +22 -0
- package/src/entry/glyphs.ts +15 -2
- package/src/entry/init-command.ts +18 -68
- package/src/entry/knowledge-command.ts +45 -4
- package/src/entry/lock-command.ts +6 -7
- package/src/entry/mcp/role-server.ts +18 -0
- package/src/entry/mcp/server.ts +13 -40
- 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/profile-command.ts +10 -14
- package/src/entry/role-cli-command.ts +47 -3
- package/src/entry/runs-command.ts +3 -19
- package/src/entry/scorecard-command.ts +187 -0
- package/src/entry/supervise-bin.ts +1 -1
- package/src/infra/env.ts +0 -2
- package/src/infra/git.ts +64 -0
- package/src/infra/heavy-lock.ts +45 -7
- package/src/infra/launch.ts +63 -7
- 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 +2 -2
- package/src/infra/slopalytics.ts +30 -0
- package/src/infra/store.ts +6 -1
- package/src/services/admission.ts +24 -9
- package/src/services/backends.ts +4 -6
- package/src/services/capture.ts +1 -22
- package/src/services/catalog-service.ts +155 -262
- package/src/services/credentials.ts +4 -20
- package/src/services/daemon.ts +181 -0
- package/src/services/dispatch-service.ts +5 -1
- package/src/services/doctor-backends.ts +1 -1
- package/src/services/doctor-checks.ts +47 -0
- package/src/services/doctor.ts +4 -9
- package/src/services/failover-rungs.ts +109 -0
- package/src/services/finalize.ts +12 -0
- package/src/services/gate-env.ts +1 -2
- package/src/services/gate-service.ts +21 -6
- package/src/services/jev-service.ts +2 -4
- package/src/services/lane-service.ts +34 -3
- package/src/services/milestone-check.ts +433 -0
- package/src/services/milestone-tree.ts +170 -0
- package/src/services/milestones.ts +35 -8
- package/src/services/notifier.ts +69 -2
- package/src/services/notify-service.ts +26 -0
- package/src/services/panel-setup.ts +116 -0
- package/src/services/plan-input.ts +151 -0
- package/src/services/ports.ts +30 -4
- package/src/services/preflight.ts +82 -8
- package/src/services/profile-service.ts +30 -3
- package/src/services/profile-store.ts +8 -3
- package/src/services/protocol.ts +4 -0
- package/src/services/provenance.ts +31 -90
- package/src/services/questions.ts +9 -0
- package/src/services/reconcile.ts +11 -0
- package/src/services/routing-service.ts +26 -15
- 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-read.ts +221 -0
- package/src/services/run-report.ts +176 -0
- package/src/services/run-service.ts +55 -6
- package/src/services/run-store.ts +54 -7
- package/src/services/scorecard-metrics.ts +86 -0
- package/src/services/scorecard.ts +382 -0
- package/src/services/summary.ts +27 -3
- package/src/services/tree-path.ts +27 -0
- package/src/services/workspace-admission.ts +5 -13
- package/src/services/workspace-service.ts +5 -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 -44
- 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 -335
- package/src/adapters/antigravity/models.ts +0 -37
- package/src/adapters/grok/events.ts +0 -112
- package/src/adapters/grok/home.ts +0 -187
- package/src/adapters/grok/index.ts +0 -358
- package/src/adapters/grok/models.ts +0 -48
- package/src/domain/calibration.ts +0 -132
- package/src/domain/sources.ts +0 -144
- package/src/entry/mcp/dispatch-tools.ts +0 -52
- package/src/entry/mcp/lane-tools.ts +0 -131
- package/src/entry/mcp/protocol-tools.ts +0 -63
- package/src/entry/mcp/push-tools.ts +0 -16
- package/src/entry/mcp/run-tools.ts +0 -164
- package/src/entry/mcp/setup-tools.ts +0 -109
- package/src/entry/mcp/workspace-tools.ts +0 -142
- package/src/entry/tui/commands.ts +0 -563
- package/src/entry/tui/effects.ts +0 -385
- 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 -400
- 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 -619
- package/src/entry/tui/views/save-dialog.tsx +0 -344
- package/src/entry/tui/views/status.tsx +0 -213
- 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 -95
- package/src/infra/sources/artificial-analysis.ts +0 -119
- package/src/infra/sources/cache.ts +0 -126
- 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 -290
- package/src/services/source-derive.ts +0 -397
- package/src/services/source-sync.ts +0 -312
- package/src/services/standins.ts +0 -238
- package/src/services/treat-likes.ts +0 -113
|
@@ -97,6 +97,10 @@ export function partialReviewer(
|
|
|
97
97
|
return last ? { name: last.name, at: last.at, record: last.record } : null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** Plan 28: the milestone's full reviewer passes since its lanes started (a partial pass is not a round). */
|
|
101
|
+
export const reviewRounds = (run: Run, m: string): number =>
|
|
102
|
+
reviewerAttempts(run, m, milestoneStart(run, m)).filter((a) => !a.partial).length;
|
|
103
|
+
|
|
100
104
|
export const FINDING = /^\s*(?:[-*]\s*)?(BLOCKER|BUG|NIT)\b/;
|
|
101
105
|
|
|
102
106
|
/** A reviewer reply's findings by severity; null when there is no reply to read. */
|
|
@@ -185,6 +189,8 @@ export interface MilestoneVerifier {
|
|
|
185
189
|
verdict: string;
|
|
186
190
|
/** plan 23: the probe of a `VERDICT: BLOCKED: environment` attempt ("" when it named none); null otherwise */
|
|
187
191
|
blocked: string | null;
|
|
192
|
+
/** plan 30b: the commit the verdict says it checked (a headless reply's `HEAD:` line); null for a native row */
|
|
193
|
+
head: string | null;
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
/** The one name a milestone's verifier is counted under (spec 1.5 "The ledger"): verifier-<m>, exactly. */
|
|
@@ -216,6 +222,7 @@ export function milestoneVerifier(
|
|
|
216
222
|
passed: a.status === "ok",
|
|
217
223
|
verdict: blocked === null ? a.status : (a.verdict ?? a.status),
|
|
218
224
|
blocked,
|
|
225
|
+
head: null,
|
|
219
226
|
};
|
|
220
227
|
}),
|
|
221
228
|
...readRecords(run)
|
|
@@ -231,6 +238,7 @@ export function milestoneVerifier(
|
|
|
231
238
|
passed,
|
|
232
239
|
verdict: r.status === "ok" ? said : `${r.status}, ${said}`,
|
|
233
240
|
blocked: r.status === "ok" ? blockedByEnvironment(first) : null,
|
|
241
|
+
head: r.head ?? null,
|
|
234
242
|
};
|
|
235
243
|
}),
|
|
236
244
|
];
|
|
@@ -250,6 +258,16 @@ export function landedCommits(run: Run): string[] {
|
|
|
250
258
|
.filter((c) => /^[0-9a-f]{7,40}$/.test(c));
|
|
251
259
|
}
|
|
252
260
|
|
|
261
|
+
/** The commit the ledger holds for `m`, its latest landing; null when it never landed or the row has no commit. */
|
|
262
|
+
export function landedCommitOf(run: Run, m: string): string | null {
|
|
263
|
+
const rows = nonBlankLines(runPaths(run.dir).ledger)
|
|
264
|
+
.slice(1)
|
|
265
|
+
.map((row) => row.split(" | "))
|
|
266
|
+
.filter((cells) => cells[0]?.trim() === m);
|
|
267
|
+
const commit = rows.at(-1)?.[2]?.trim() ?? "";
|
|
268
|
+
return /^[0-9a-f]{7,40}$/.test(commit) ? commit : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
253
271
|
/** The milestones the ledger holds, in landing order. */
|
|
254
272
|
export function landedMilestones(run: Run): string[] {
|
|
255
273
|
return nonBlankLines(runPaths(run.dir).ledger)
|
|
@@ -258,20 +276,29 @@ export function landedMilestones(run: Run): string[] {
|
|
|
258
276
|
.filter(Boolean);
|
|
259
277
|
}
|
|
260
278
|
|
|
261
|
-
/**
|
|
262
|
-
|
|
263
|
-
* started on (meta.json `startHead`, when that commit is still there), else the commit's own parent, to
|
|
264
|
-
* `commit`. Throws E_IO_UNEXPECTED when git cannot say.
|
|
265
|
-
*/
|
|
266
|
-
export async function milestoneFiles(run: Run, commit: string): Promise<string[]> {
|
|
279
|
+
/** The 1.5 range: see milestoneFiles. */
|
|
280
|
+
async function oneFiveFiles(run: Run, commit: string): ReturnType<typeof git> {
|
|
267
281
|
const repo = run.meta.repo;
|
|
268
282
|
const start = run.meta.startHead;
|
|
269
283
|
const base =
|
|
270
284
|
landedCommits(run).at(-1) ??
|
|
271
285
|
(start && (await commitExists(repo, start).catch(() => false)) ? start : undefined);
|
|
272
|
-
|
|
286
|
+
return base
|
|
273
287
|
? await git(repo, ["diff", "--name-only", base, commit])
|
|
274
288
|
: await git(repo, ["show", "--name-only", "--format=", "--first-parent", commit]);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The files the milestone's commit range changed: from the previous landed commit, else the HEAD the run
|
|
293
|
+
* started on (meta.json `startHead`, when that commit is still there), else the commit's own parent, to
|
|
294
|
+
* `commit`. A base run's milestone is its branch since origin's base (plan 35). Throws E_IO_UNEXPECTED when git cannot say.
|
|
295
|
+
*/
|
|
296
|
+
export async function milestoneFiles(run: Run, commit: string): Promise<string[]> {
|
|
297
|
+
const repo = run.meta.repo;
|
|
298
|
+
const r = run.meta.base
|
|
299
|
+
? // plan 35: a base run's milestone is its branch since origin's base; three dots, since other milestones land on the base meanwhile
|
|
300
|
+
await git(repo, ["diff", "--name-only", `origin/${run.meta.base}...${commit}`])
|
|
301
|
+
: await oneFiveFiles(run, commit);
|
|
275
302
|
if (r.kind !== "ok")
|
|
276
303
|
throw new CatherdError("E_IO_UNEXPECTED", `git could not list the files ${commit} changed`, {
|
|
277
304
|
fix: `check that git works in ${repo}`,
|
|
@@ -287,7 +314,7 @@ const DIGEST_COMMITS = 20;
|
|
|
287
314
|
* (else the commit alone) to `commit`. Just the hash when git cannot say: a digest never fails a landing.
|
|
288
315
|
*/
|
|
289
316
|
export async function milestoneCommits(run: Run, commit: string): Promise<string[]> {
|
|
290
|
-
const base = landedCommits(run).at(-1);
|
|
317
|
+
const base = run.meta.base ? `origin/${run.meta.base}` : landedCommits(run).at(-1);
|
|
291
318
|
const range = base ? [`${base}..${commit}`] : ["-1", commit];
|
|
292
319
|
const r = await git(run.meta.repo, ["log", "--reverse", "--format=%h %s", ...range]);
|
|
293
320
|
if (r.kind !== "ok") return [commit.slice(0, 7)];
|
package/src/services/notifier.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { adapterFor } from "../adapters/registry.ts";
|
|
4
4
|
import "../adapters/all.ts";
|
|
@@ -14,11 +14,12 @@ import {
|
|
|
14
14
|
type DeliveryAttempt,
|
|
15
15
|
} from "../infra/delivery.ts";
|
|
16
16
|
import type { RunRecord } from "../domain/record.ts";
|
|
17
|
+
import type { RunEvent } from "../domain/run-events.ts";
|
|
17
18
|
import { awaitsCollect, dispatchPaths } from "../infra/dispatch-dir.ts";
|
|
18
19
|
import { tryLock } from "../infra/filelock.ts";
|
|
19
20
|
import { log } from "../infra/log.ts";
|
|
20
21
|
import { type SendResult, sendToInbox } from "../infra/peer-inbox.ts";
|
|
21
|
-
import { nonBlankLines, writeJsonAtomic } from "../infra/store.ts";
|
|
22
|
+
import { ensurePrivateDir, nonBlankLines, writeJsonAtomic } from "../infra/store.ts";
|
|
22
23
|
import {
|
|
23
24
|
readStallQuietMs,
|
|
24
25
|
type Settled,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
} from "./dispatch-service.ts";
|
|
29
30
|
import { type Dispatch, listDispatches, readFailover, roleThreadOf } from "./dispatches.ts";
|
|
30
31
|
import type { Deps } from "./ports.ts";
|
|
32
|
+
import { eventsFile, isDaemonRun, readEvents } from "./run-events-store.ts";
|
|
31
33
|
import { listRuns, readRecords, type Run } from "./run-store.ts";
|
|
32
34
|
import { currentSession, ownsRun, readSessionRows, runOwner } from "./sessions.ts";
|
|
33
35
|
|
|
@@ -35,6 +37,8 @@ export interface NotifierOptions {
|
|
|
35
37
|
/** notices this close to each other go as one message (spec: 3 s); tests shorten it */
|
|
36
38
|
coalesceMs?: number;
|
|
37
39
|
sendCodex?: (target: HostSessionRef, content: string) => Promise<QueueSendResult>;
|
|
40
|
+
/** how often the reports of the session's 2.0 runs are looked for between scans; 0 (the default) never */
|
|
41
|
+
reportPollMs?: number;
|
|
38
42
|
/** the sender; tests may replace it */
|
|
39
43
|
send?: (
|
|
40
44
|
target: { socketPath: string | null; token: string | null },
|
|
@@ -134,6 +138,28 @@ export function finishedNotice(run: Run, d: Dispatch, r: RunRecord): Notice {
|
|
|
134
138
|
};
|
|
135
139
|
}
|
|
136
140
|
|
|
141
|
+
/** One report of a 2.0 run, as the session is told of it: the unit, what kind it is, its text. */
|
|
142
|
+
export function reportNotice(run: Run, e: Extract<RunEvent, { type: "report.posted" }>): Notice {
|
|
143
|
+
return {
|
|
144
|
+
kind: "report",
|
|
145
|
+
eventId: JSON.stringify([run.id, `report-${e.seq}`, "report"]),
|
|
146
|
+
runId: run.id,
|
|
147
|
+
runTitle: run.meta.title,
|
|
148
|
+
dispatchId: `report-${e.seq}`,
|
|
149
|
+
name: e.unit,
|
|
150
|
+
role: "run",
|
|
151
|
+
lane: null,
|
|
152
|
+
thread: null,
|
|
153
|
+
rung: "",
|
|
154
|
+
status: e.kind,
|
|
155
|
+
replyStatus: null,
|
|
156
|
+
secs: 0,
|
|
157
|
+
changedOwned: 0,
|
|
158
|
+
reply: e.text,
|
|
159
|
+
priority: e.kind === "parked" || e.kind === "failed" ? "next" : "later",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
137
163
|
/** A live fresh role's thread, as its CLI named it early in its stream; null before it did. */
|
|
138
164
|
function streamThread(d: Dispatch): string | null {
|
|
139
165
|
const a = adapterFor(d.admit.backend);
|
|
@@ -483,7 +509,35 @@ function notifierFor(deps: Deps, o: NotifierOptions, retryEvent?: string): Notif
|
|
|
483
509
|
}
|
|
484
510
|
};
|
|
485
511
|
|
|
512
|
+
/** The reports of a 2.0 run this session owns: queued once each, under `<run>/reports/<seq>/`. */
|
|
513
|
+
const reportSizes = new Map<string, number>();
|
|
514
|
+
const enqueueReports = (run: Run, force: boolean): void => {
|
|
515
|
+
const size = statSync(eventsFile(run)).size;
|
|
516
|
+
if (!force && reportSizes.get(run.id) === size) return;
|
|
517
|
+
reportSizes.set(run.id, size);
|
|
518
|
+
for (const e of readEvents(run)) {
|
|
519
|
+
if (e.type !== "report.posted") continue;
|
|
520
|
+
const dir = join(run.dir, "reports", String(e.seq));
|
|
521
|
+
ensurePrivateDir(dir);
|
|
522
|
+
const notice = reportNotice(run, e);
|
|
523
|
+
queueNotice({ key: notice.eventId, run, dir, notice, mark: join(dir, "notified"), due: () => true });
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const scanReports = (force: boolean): void => {
|
|
528
|
+
if (stopped || !currentSession(deps)) return;
|
|
529
|
+
for (const run of listRuns().runs) {
|
|
530
|
+
try {
|
|
531
|
+
if (isDaemonRun(run) && owned(run)) enqueueReports(run, force);
|
|
532
|
+
} catch (e) {
|
|
533
|
+
log("warn", "notify", { run: run.id, error: errorMessage(e) });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
486
538
|
const onSettled = (s: Settled): void => {
|
|
539
|
+
// the run's reports tell the session of a 2.0 run, not each role's finish
|
|
540
|
+
if (isDaemonRun(s.run)) return;
|
|
487
541
|
try {
|
|
488
542
|
enqueue(s.run, s.d, s.record);
|
|
489
543
|
} catch (e) {
|
|
@@ -499,6 +553,10 @@ function notifierFor(deps: Deps, o: NotifierOptions, retryEvent?: string): Notif
|
|
|
499
553
|
for (const run of listRuns().runs) {
|
|
500
554
|
try {
|
|
501
555
|
if (!owned(run)) continue;
|
|
556
|
+
if (isDaemonRun(run)) {
|
|
557
|
+
enqueueReports(run, true);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
502
560
|
const records = new Map(readRecords(run).records.map((r) => [r.dispatchId, r]));
|
|
503
561
|
for (const d of listDispatches(run)) {
|
|
504
562
|
// one dispatch's damaged evidence stays ambiguous for it alone: the rest of the run is still scanned
|
|
@@ -552,6 +610,15 @@ function notifierFor(deps: Deps, o: NotifierOptions, retryEvent?: string): Notif
|
|
|
552
610
|
settledHooks.add(onSettled);
|
|
553
611
|
stallHooks.add(onStall);
|
|
554
612
|
}
|
|
613
|
+
if (!retryEvent && o.reportPollMs) {
|
|
614
|
+
const poll = setInterval(() => scanReports(false), o.reportPollMs);
|
|
615
|
+
poll.unref();
|
|
616
|
+
const stopRest = n.stop;
|
|
617
|
+
n.stop = () => {
|
|
618
|
+
clearInterval(poll);
|
|
619
|
+
stopRest();
|
|
620
|
+
};
|
|
621
|
+
}
|
|
555
622
|
return n;
|
|
556
623
|
}
|
|
557
624
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { notifyMessage } from "../domain/notify.ts";
|
|
2
|
+
import type { NotifyMoment } from "../domain/profile.ts";
|
|
3
|
+
import { postNotify } from "../infra/ntfy.ts";
|
|
4
|
+
import type { Deps } from "./ports.ts";
|
|
5
|
+
import { runProfile } from "./run-pin.ts";
|
|
6
|
+
import type { Run } from "./run-store.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tells the owner's phone about a moment of a run, when the run's profile lists that moment. False when it is
|
|
10
|
+
* not listed, or the message did not go out; it never throws.
|
|
11
|
+
*/
|
|
12
|
+
export async function sendRunNotice(
|
|
13
|
+
deps: Deps,
|
|
14
|
+
run: Run,
|
|
15
|
+
i: { unit: string; moment: NotifyMoment },
|
|
16
|
+
o: { fetch?: typeof fetch; timeoutMs?: number } = {},
|
|
17
|
+
): Promise<boolean> {
|
|
18
|
+
if (!runProfile(deps, run).notify.includes(i.moment)) return false;
|
|
19
|
+
// CATHERD_NO_END_PUSH=1 (the test preload sets it) keeps every real send off; a test's own fetch still goes
|
|
20
|
+
if (process.env.CATHERD_NO_END_PUSH === "1" && !o.fetch) return false;
|
|
21
|
+
return postNotify(
|
|
22
|
+
deps.profiles.notifyUrl(),
|
|
23
|
+
notifyMessage({ runId: run.id, runTitle: run.meta.title, unit: i.unit, moment: i.moment }),
|
|
24
|
+
o,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BILLING_MODES } from "../domain/cost.ts";
|
|
3
|
+
import { pairOf } from "../domain/catalog.ts";
|
|
4
|
+
import { ROLES, type Role } from "../domain/roles.ts";
|
|
5
|
+
import { catalogQuery, loadCatalog, type IndexedPair } from "./catalog-service.ts";
|
|
6
|
+
import { backendChecks, usedBackends } from "./doctor-backends.ts";
|
|
7
|
+
import { indexCheck } from "./doctor-checks.ts";
|
|
8
|
+
import type { Deps, ProfileSaved } from "./ports.ts";
|
|
9
|
+
import { profileFor } from "./profile-store.ts";
|
|
10
|
+
|
|
11
|
+
// The panel's Setup page (spec 2.0 decision 25): backends with their doctor status, and each role's rungs picked
|
|
12
|
+
// from the indexed pairs. Every write goes through the ProfilePort.
|
|
13
|
+
|
|
14
|
+
export const CheckSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
label: z.string(),
|
|
17
|
+
state: z.enum(["ok", "warn", "fail", "skip", "info"]),
|
|
18
|
+
word: z.string(),
|
|
19
|
+
detail: z.string(),
|
|
20
|
+
fix: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
export const IssueSchema = z.object({ path: z.string(), message: z.string(), fix: z.string().optional() });
|
|
23
|
+
export const IndexedPairSchema = z.object({
|
|
24
|
+
rung: z.string(),
|
|
25
|
+
intelligence: z.number(),
|
|
26
|
+
price: z.object({ input: z.number(), output: z.number() }).nullable(),
|
|
27
|
+
speed: z.number().nullable(),
|
|
28
|
+
context: z.number().nullable(),
|
|
29
|
+
cost: z.object({
|
|
30
|
+
tier: z.union([z.literal(0), z.literal(1)]),
|
|
31
|
+
value: z.number().nullable(),
|
|
32
|
+
mode: z.enum(BILLING_MODES),
|
|
33
|
+
}),
|
|
34
|
+
evidence: z.string().nullable(),
|
|
35
|
+
});
|
|
36
|
+
export const ProfileSavedSchema = z.object({
|
|
37
|
+
saved: z.boolean(),
|
|
38
|
+
errors: z.array(IssueSchema),
|
|
39
|
+
warnings: z.array(IssueSchema),
|
|
40
|
+
diff: z.array(z.object({ path: z.string(), before: z.unknown(), after: z.unknown() })),
|
|
41
|
+
linked: z.array(z.string()),
|
|
42
|
+
pruned: z.array(z.string()),
|
|
43
|
+
newSessionNeededFor: z.array(z.string()),
|
|
44
|
+
});
|
|
45
|
+
export const SetupViewSchema = z.object({
|
|
46
|
+
repo: z.string().nullable(),
|
|
47
|
+
profile: z.object({ name: z.string(), active: z.string(), profiles: z.array(z.string()) }),
|
|
48
|
+
catalog: CheckSchema,
|
|
49
|
+
backends: z.array(CheckSchema),
|
|
50
|
+
issues: z.object({ errors: z.array(IssueSchema), warnings: z.array(IssueSchema) }),
|
|
51
|
+
roles: z.array(
|
|
52
|
+
z.object({
|
|
53
|
+
role: z.enum(ROLES),
|
|
54
|
+
enabled: z.boolean(),
|
|
55
|
+
rungs: z.array(z.object({ rung: z.string(), intelligence: z.number().nullable() })),
|
|
56
|
+
offered: z.array(IndexedPairSchema),
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
});
|
|
60
|
+
export type SetupView = z.infer<typeof SetupViewSchema>;
|
|
61
|
+
|
|
62
|
+
/** What the Setup page shows: the profile `repo` runs on (the active one without a repo). */
|
|
63
|
+
export async function setupView(deps: Deps, repo: string | null): Promise<SetupView> {
|
|
64
|
+
const g = deps.profiles.get(undefined, repo);
|
|
65
|
+
const p = profileFor(repo, deps.host.host);
|
|
66
|
+
const issues = deps.profiles.validate(undefined, repo);
|
|
67
|
+
const catalog = loadCatalog({ repo: repo ?? undefined });
|
|
68
|
+
const intelligence = (rung: string) => pairOf(catalog, rung)?.intelligence ?? null;
|
|
69
|
+
return {
|
|
70
|
+
repo,
|
|
71
|
+
profile: { name: g.here, active: g.active, profiles: g.profiles },
|
|
72
|
+
catalog: indexCheck(),
|
|
73
|
+
backends: await backendChecks(usedBackends([p]), [p]),
|
|
74
|
+
issues: { errors: issues.errors, warnings: issues.warnings },
|
|
75
|
+
roles: ROLES.map((role) => {
|
|
76
|
+
const r = g.profile.roles[role];
|
|
77
|
+
const offered = new Map<string, IndexedPair>();
|
|
78
|
+
for (const m of catalogQuery({ role, limit: 1000, repo: repo ?? undefined }, g.profile.billing).models)
|
|
79
|
+
for (const x of m.rungs) offered.set(x.rung, x);
|
|
80
|
+
return {
|
|
81
|
+
role,
|
|
82
|
+
enabled: r?.enabled ?? false,
|
|
83
|
+
rungs: (r?.rungs ?? []).map((rung) => ({ rung, intelligence: intelligence(rung) })),
|
|
84
|
+
offered: [...offered.values()].sort(
|
|
85
|
+
(a, b) => b.intelligence - a.intelligence || a.rung.localeCompare(b.rung),
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Unique, rising intelligence with unindexed last; ties keep the previous order, then the pick order. */
|
|
93
|
+
export function orderRungs(
|
|
94
|
+
rungs: string[],
|
|
95
|
+
previous: string[],
|
|
96
|
+
intelligence: (r: string) => number | null,
|
|
97
|
+
): string[] {
|
|
98
|
+
const at = (list: string[], r: string) => {
|
|
99
|
+
const i = list.indexOf(r);
|
|
100
|
+
return i < 0 ? Number.MAX_SAFE_INTEGER : i;
|
|
101
|
+
};
|
|
102
|
+
return [...new Set(rungs)].sort(
|
|
103
|
+
(a, b) =>
|
|
104
|
+
(intelligence(a) ?? Infinity) - (intelligence(b) ?? Infinity) ||
|
|
105
|
+
at(previous, a) - at(previous, b) ||
|
|
106
|
+
at(rungs, a) - at(rungs, b),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Saves a role's rungs, in ladder order, through the profile service (its validation is the gate). */
|
|
111
|
+
export function setRoleRungs(deps: Deps, repo: string | null, role: Role, rungs: string[]): ProfileSaved {
|
|
112
|
+
const current = deps.profiles.get(undefined, repo).profile.roles[role]?.rungs ?? [];
|
|
113
|
+
const catalog = loadCatalog({ repo: repo ?? undefined });
|
|
114
|
+
const ordered = orderRungs(rungs, current, (r) => pairOf(catalog, r)?.intelligence ?? null);
|
|
115
|
+
return deps.profiles.set(undefined, { roles: { [role]: { rungs: ordered } } }, repo);
|
|
116
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { CatherdError } from "../domain/errors.ts";
|
|
4
|
+
import { parseLaneHeader } from "../domain/lane.ts";
|
|
5
|
+
import {
|
|
6
|
+
coverageProblems,
|
|
7
|
+
depthProblems,
|
|
8
|
+
type PlanDoc,
|
|
9
|
+
type PlanTask,
|
|
10
|
+
parsePlanDoc,
|
|
11
|
+
planInputNames,
|
|
12
|
+
resolveTask,
|
|
13
|
+
} from "../domain/plan-doc.ts";
|
|
14
|
+
import type { Role } from "../domain/roles.ts";
|
|
15
|
+
import { writeTextAtomic } from "../infra/store.ts";
|
|
16
|
+
import { isDaemonRun, replayRun } from "./run-events-store.ts";
|
|
17
|
+
import { type Run, runPaths } from "./run-store.ts";
|
|
18
|
+
|
|
19
|
+
/** The run's copies of the owner's plan files: isolated roles read these, never the originals (plan 32). */
|
|
20
|
+
export const PLAN_INPUT_DIR = "plan-input";
|
|
21
|
+
/** Past this many characters of preambles, a role's `<context>` names the copies instead of repeating them. */
|
|
22
|
+
export const PLAN_CONTEXT_MAX = 100_000;
|
|
23
|
+
|
|
24
|
+
const invalid = (message: string, fix: string) => new CatherdError("E_INPUT_INVALID", message, { fix });
|
|
25
|
+
|
|
26
|
+
/** The absolute, deduped plan paths of a `run_start`; a relative one resolves against the repo. */
|
|
27
|
+
export function resolvePlanInputs(repo: string, paths: string[]): string[] {
|
|
28
|
+
const out: string[] = [];
|
|
29
|
+
for (const p of paths) {
|
|
30
|
+
const abs = isAbsolute(p) ? p : resolve(repo, p);
|
|
31
|
+
if (out.includes(abs)) continue;
|
|
32
|
+
if (!existsSync(abs))
|
|
33
|
+
throw invalid(`plan file ${abs} does not exist`, "pass the path of the reviewed plan file");
|
|
34
|
+
if (!statSync(abs).isFile())
|
|
35
|
+
throw invalid(`plan ${abs} is not a file`, "pass the path of the reviewed plan file");
|
|
36
|
+
out.push(abs);
|
|
37
|
+
}
|
|
38
|
+
if (out.length > 0 && !out.some((f) => parsePlanDoc(f, readFileSync(f, "utf8")).tasks.length > 0))
|
|
39
|
+
throw invalid(
|
|
40
|
+
`none of the plan files has a "### Task <n>:" heading: ${out.join(", ")}`,
|
|
41
|
+
"pass the reviewed plan, or no plan and the architect writes one",
|
|
42
|
+
);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The paths of an A-line `plan: a, b` (the label in any case). */
|
|
47
|
+
export function planInputPaths(aLines: string[]): string[] {
|
|
48
|
+
return aLines.flatMap((l) =>
|
|
49
|
+
(/^\s*plan:\s*(.*?)\s*$/i.exec(l)?.[1] ?? "")
|
|
50
|
+
.split(",")
|
|
51
|
+
.map((p) => p.trim())
|
|
52
|
+
.filter(Boolean),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `<run>/plan-input/<name>`, 0600, for each path; a file that has vanished since keeps its earlier copy. */
|
|
57
|
+
export function copyPlanInputs(run: Run, paths: string[]): void {
|
|
58
|
+
const names = planInputNames(paths);
|
|
59
|
+
for (const [i, p] of paths.entries())
|
|
60
|
+
if (existsSync(p))
|
|
61
|
+
writeTextAtomic(join(run.dir, PLAN_INPUT_DIR, names[i] as string), readFileSync(p, "utf8"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const read = (file: string): string | null => {
|
|
65
|
+
try {
|
|
66
|
+
return readFileSync(file, "utf8");
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The plan files a role can read, by their path in the run folder: the copies, else plan.md. */
|
|
73
|
+
function planFiles(run: Run): { path: string; doc: PlanDoc }[] {
|
|
74
|
+
const plan = replayRun(run).plan;
|
|
75
|
+
const files = plan.length
|
|
76
|
+
? planInputNames(plan).map((n) => ({ path: `${PLAN_INPUT_DIR}/${n}`, name: n }))
|
|
77
|
+
: [{ path: "plan.md", name: "plan.md" }];
|
|
78
|
+
return files.flatMap((f) => {
|
|
79
|
+
const text = read(join(run.dir, f.path));
|
|
80
|
+
return text === null ? [] : [{ path: f.path, doc: parsePlanDoc(f.name, text) }];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The run's plan as documents: the copies in `run.started.plan` order; else plan.md; none when neither exists. */
|
|
85
|
+
export const planDocs = (run: Run): PlanDoc[] => planFiles(run).map((f) => f.doc);
|
|
86
|
+
|
|
87
|
+
/** Each plan file's preamble, which applies to every task, for every role but the architect and the judge. */
|
|
88
|
+
export function planContext(run: Run, role: Role): string | null {
|
|
89
|
+
if (role === "architect" || role === "judge" || !isDaemonRun(run)) return null;
|
|
90
|
+
const files = planFiles(run);
|
|
91
|
+
const parts = files.flatMap(({ doc }) => {
|
|
92
|
+
if (doc.tasks.length === 0)
|
|
93
|
+
return [`The plan file ${doc.name}, which every task follows:\n\n${doc.text.trim()}`];
|
|
94
|
+
const pre = doc.preamble.trim();
|
|
95
|
+
return pre ? [`The plan's preamble (${doc.name}), which applies to every task:\n\n${pre}`] : [];
|
|
96
|
+
});
|
|
97
|
+
if (parts.length === 0) return null;
|
|
98
|
+
const text = parts.join("\n\n");
|
|
99
|
+
return text.length > PLAN_CONTEXT_MAX
|
|
100
|
+
? `The run's plan files are too long to repeat here; read them before you start, with read_run_file: ${files.map((f) => f.path).join(", ")}`
|
|
101
|
+
: text;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const natural = (a: string, b: string): number => a.localeCompare(b, "en", { numeric: true });
|
|
105
|
+
|
|
106
|
+
/** Every lane file's id, in lane order. */
|
|
107
|
+
function laneIds(run: Run): string[] {
|
|
108
|
+
const dir = runPaths(run.dir).lanes;
|
|
109
|
+
return (existsSync(dir) ? readdirSync(dir) : [])
|
|
110
|
+
.filter((f) => f.endsWith(".md"))
|
|
111
|
+
.map((f) => f.slice(0, -3))
|
|
112
|
+
.sort(natural);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const laneRefs = (run: Run, lane: string): string[] =>
|
|
116
|
+
parseLaneHeader(read(join(runPaths(run.dir).lanes, `${lane}.md`)) ?? "").tasks;
|
|
117
|
+
|
|
118
|
+
/** The plan tasks a lane's `Tasks:` line names; a ref that resolves to none is left out (planProblems names it). */
|
|
119
|
+
export function laneTasks(run: Run, lane: string): PlanTask[] {
|
|
120
|
+
const docs = planDocs(run);
|
|
121
|
+
return laneRefs(run, lane).flatMap((ref) => {
|
|
122
|
+
const t = resolveTask(docs, ref);
|
|
123
|
+
return typeof t === "string" ? [] : [t];
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A milestone's tasks: those of its lanes in lane order, each once. */
|
|
128
|
+
export function milestoneTasks(run: Run, milestone: string): PlanTask[] {
|
|
129
|
+
const seen = new Map<string, PlanTask>();
|
|
130
|
+
for (const id of laneIds(run).filter((l) => l.startsWith(`${milestone}.`)))
|
|
131
|
+
for (const t of laneTasks(run, id)) seen.set(`${t.doc}#${t.id}`, t);
|
|
132
|
+
return [...seen.values()];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* What the architect can fix: a lane that names no task or a missing one, and with no plan input plan.md's depth.
|
|
137
|
+
* A task of the owner's plan with neither a signature nor a test is the owner's, and preflight refuses it.
|
|
138
|
+
*/
|
|
139
|
+
export function planProblems(run: Run): string[] {
|
|
140
|
+
const docs = planDocs(run);
|
|
141
|
+
const lanes = laneIds(run).map((id) => ({ id, tasks: laneRefs(run, id) }));
|
|
142
|
+
const out = coverageProblems(docs, lanes)
|
|
143
|
+
.filter((p) => p.lane !== "")
|
|
144
|
+
.map((p) => p.note);
|
|
145
|
+
if (replayRun(run).plan.length === 0) out.push(...depthProblems(docs[0] ?? parsePlanDoc("plan.md", "")));
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A plan task as a brief carries it: verbatim, in a tag naming its file and number. */
|
|
150
|
+
export const taskBlock = (t: PlanTask): string =>
|
|
151
|
+
`<plan-task file="${t.doc}" id="${t.id}">\n${t.text.trimEnd()}\n</plan-task>`;
|
package/src/services/ports.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { BlockedReason } from "../domain/blocked.ts";
|
|
1
2
|
import type { HostContext } from "../domain/host.ts";
|
|
2
3
|
import type { Provenance } from "./provenance.ts";
|
|
4
|
+
import type { Bars } from "../domain/bars.ts";
|
|
3
5
|
import type { Budget } from "../domain/budget.ts";
|
|
4
6
|
import type { SessionEnv } from "../infra/claude-session.ts";
|
|
5
7
|
import type { BillingMode } from "../domain/cost.ts";
|
|
@@ -11,7 +13,8 @@ import type { RoleScope } from "../domain/role-scope.ts";
|
|
|
11
13
|
import type { Change, ProfileDoc, ProfilePatch } from "../domain/profile.ts";
|
|
12
14
|
import type { Issue } from "../domain/profile-rules.ts";
|
|
13
15
|
import type { RouteJev, RouteSource } from "../domain/route.ts";
|
|
14
|
-
import type {
|
|
16
|
+
import type { IndexFile } from "../domain/slopalytics.ts";
|
|
17
|
+
import type { IndexStatus } from "./catalog-service.ts";
|
|
15
18
|
|
|
16
19
|
/** What the run lifecycle reads from a profile. Rungs are `backend:model#effort`; a role's in ladder order. */
|
|
17
20
|
export interface ProfileView {
|
|
@@ -29,6 +32,8 @@ export interface ProfileView {
|
|
|
29
32
|
network?: false;
|
|
30
33
|
/** plan 23: the role's own timeouts over the profile's */
|
|
31
34
|
timeouts?: { idleMin?: number; wallMin?: number };
|
|
35
|
+
/** plan 33: the role's own bar percentiles over the profile's */
|
|
36
|
+
bars?: Bars;
|
|
32
37
|
}
|
|
33
38
|
>
|
|
34
39
|
>;
|
|
@@ -39,11 +44,17 @@ export interface ProfileView {
|
|
|
39
44
|
isolated: Partial<Record<string, boolean>>;
|
|
40
45
|
/** rung → its stand-in on a usage limit (spec §4.5) */
|
|
41
46
|
failover: Record<string, string>;
|
|
47
|
+
/** `<backend>:<model>` → the highest effort routing starts it at (plan 33) */
|
|
48
|
+
maxEffort?: Record<string, string>;
|
|
49
|
+
/** plan 33: bar percentiles per difficulty (0 to 100) over the shipped ones */
|
|
50
|
+
bars?: Bars;
|
|
42
51
|
budget: Budget;
|
|
43
52
|
timeouts: { idleMin: number; wallMin: number };
|
|
44
53
|
preflight: { confirm: boolean };
|
|
45
54
|
heavy: number | "cpus/2";
|
|
46
55
|
notify: string[];
|
|
56
|
+
/** plan 35: the memory numbers that cap milestones side by side, defaults filled */
|
|
57
|
+
limits: { memory: { reserveMb: number; perMilestoneMb: number; floorMb: number } };
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/** What a profile write returns (spec §7.3): whether it saved, why not, what changed, and the agents it touched. */
|
|
@@ -82,6 +93,10 @@ export interface ProfilePort {
|
|
|
82
93
|
set(name: string | undefined, patch: ProfilePatch, repo?: string | null): ProfileSaved;
|
|
83
94
|
/** The native agent that runs `rung` for `role` under `repo`'s profile; null unless it is a `claude:` rung. */
|
|
84
95
|
agentFor(repo: string | null, role: Role, rung: string): string | null;
|
|
96
|
+
/** The ntfy topic url of the phone messages: config's `notify.url`, a random ntfy.sh topic made once. */
|
|
97
|
+
notifyUrl(): string;
|
|
98
|
+
/** Replaces it; an http(s) url only. */
|
|
99
|
+
setNotifyUrl(url: string): void;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
export interface RouteRequest {
|
|
@@ -125,7 +140,6 @@ export interface CatalogFilter {
|
|
|
125
140
|
role?: Role;
|
|
126
141
|
backend?: string;
|
|
127
142
|
text?: string;
|
|
128
|
-
scoredOnly: boolean;
|
|
129
143
|
limit: number;
|
|
130
144
|
/** the git toplevel whose per-repository listings (opencode's) to read; none reads the global ones */
|
|
131
145
|
repo?: string;
|
|
@@ -148,6 +162,18 @@ export interface RoutingPort {
|
|
|
148
162
|
after: string,
|
|
149
163
|
use: "auto" | "off",
|
|
150
164
|
): Promise<Verdict<"yes" | "no">>;
|
|
165
|
+
/** plan 30b: why a role stopped that gave no reason; `other` and an unsure answer are the rule's fallback */
|
|
166
|
+
stopCause(
|
|
167
|
+
runDir: string,
|
|
168
|
+
status: string,
|
|
169
|
+
reply: string,
|
|
170
|
+
use: "auto" | "off",
|
|
171
|
+
): Promise<Verdict<BlockedReason | "other">>;
|
|
172
|
+
/**
|
|
173
|
+
* A run's start check: refreshes the index if it is due, reads it once, and names the enabled roles of `view`
|
|
174
|
+
* that no indexed pair can fill; `index` is what the run pins (null when there is none).
|
|
175
|
+
*/
|
|
176
|
+
checkStart(view: ProfileView, repo: string): Promise<{ unusable: Role[]; index: IndexFile | null }>;
|
|
151
177
|
/** `billing` prices the rungs (spec §5.3); absent keys bill as DEFAULT_BILLING */
|
|
152
178
|
catalog(
|
|
153
179
|
filter: CatalogFilter,
|
|
@@ -166,8 +192,8 @@ export interface Deps {
|
|
|
166
192
|
/** Claude transport credentials and registry refresh; host identity lives in `host`. */
|
|
167
193
|
session: SessionEnv | null;
|
|
168
194
|
now: () => number;
|
|
169
|
-
/**
|
|
170
|
-
sync?: (o: { force: boolean
|
|
195
|
+
/** `catalog_sync`; default: the real refresh (tests inject one that never reaches the network) */
|
|
196
|
+
sync?: (o: { force: boolean }) => Promise<IndexStatus>;
|
|
171
197
|
/**
|
|
172
198
|
* Spec 1.5 plan 21: the role this process works for (CATHERD_ROLE, or a role's scratch TMPDIR); null or
|
|
173
199
|
* absent for the orchestrator's own server. A role never claims a run and never reaches a coordinator tool.
|