catherd-cli 1.2.1 → 1.4.0
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 +146 -36
- package/catalog/models.json +148 -5
- package/catalog/scores.json +24 -0
- package/catalog/sources.json +12 -2
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +11 -0
- package/plugin/.mcp-codex.json +10 -0
- package/plugin/bin/catherd-mcp +1 -1
- package/plugin/skills/catherd/SKILL.md +45 -39
- package/plugin/skills/catherd-setup/SKILL.md +24 -8
- package/src/adapters/access.ts +61 -18
- package/src/adapters/all.ts +6 -0
- package/src/adapters/antigravity/events.ts +131 -0
- package/src/adapters/antigravity/home.ts +46 -0
- package/src/adapters/antigravity/index.ts +320 -0
- package/src/adapters/antigravity/models.ts +37 -0
- package/src/adapters/backend.ts +34 -1
- package/src/adapters/claude-code/index.ts +2 -0
- package/src/adapters/cli.ts +1 -64
- package/src/adapters/codex/index.ts +2 -0
- package/src/adapters/cursor/events.ts +120 -0
- package/src/adapters/cursor/home.ts +79 -0
- package/src/adapters/cursor/index.ts +331 -0
- package/src/adapters/cursor/models.ts +30 -0
- package/src/adapters/discovery.ts +21 -0
- package/src/adapters/grok/events.ts +112 -0
- package/src/adapters/grok/home.ts +132 -0
- package/src/adapters/grok/index.ts +339 -0
- package/src/adapters/grok/models.ts +48 -0
- package/src/adapters/opencode/index.ts +2 -0
- package/src/cli.ts +49 -19
- package/src/domain/catalog.ts +10 -1
- package/src/domain/cost.ts +1 -0
- package/src/domain/errors.ts +1 -0
- package/src/domain/failover.ts +5 -2
- package/src/domain/host.ts +17 -0
- package/src/domain/ids.ts +2 -2
- package/src/domain/notice.ts +5 -2
- package/src/domain/profile-rules.ts +17 -0
- package/src/domain/profile.ts +89 -5
- package/src/domain/record.ts +3 -0
- package/src/domain/util.ts +3 -0
- package/src/entry/catalog-command.ts +4 -2
- package/src/entry/deps.ts +6 -3
- package/src/entry/doctor-command.ts +11 -2
- package/src/entry/host-arg.ts +28 -0
- package/src/entry/init-command.ts +18 -7
- package/src/entry/knowledge-command.ts +50 -0
- package/src/entry/lock-command.ts +7 -2
- package/src/entry/mcp/handshake.ts +3 -1
- package/src/entry/mcp/run-tools.ts +5 -2
- package/src/entry/mcp/server.ts +129 -40
- package/src/entry/mcp/setup-tools.ts +7 -2
- package/src/entry/profile-command.ts +108 -12
- package/src/entry/runs-command.ts +81 -5
- package/src/entry/tui/effects.ts +30 -10
- package/src/entry/tui/fixtures.ts +34 -10
- package/src/entry/tui/profile-tree.ts +10 -1
- package/src/entry/tui/run.tsx +10 -4
- package/src/entry/tui/state.ts +19 -6
- package/src/entry/tui/stories.ts +11 -4
- package/src/entry/tui/views/profile-actions.ts +4 -2
- package/src/entry/tui/views/profiles.tsx +16 -3
- package/src/entry/tui/views/save-dialog.tsx +2 -2
- package/src/infra/cli.ts +65 -0
- package/src/infra/codex-queue.ts +165 -0
- package/src/infra/delivery.ts +83 -0
- package/src/infra/dispatch-dir.ts +1 -0
- package/src/infra/env.ts +4 -1
- package/src/infra/host-context.ts +58 -0
- package/src/infra/paths.ts +3 -1
- package/src/infra/peer-inbox.ts +4 -1
- package/src/infra/supervisor.ts +3 -2
- package/src/services/admission.ts +34 -7
- package/src/services/agent-links.ts +30 -5
- package/src/services/backends.ts +49 -4
- package/src/services/capture.ts +112 -54
- package/src/services/catalog-refresh.ts +2 -1
- package/src/services/dispatch-service.ts +17 -7
- package/src/services/dispatches.ts +8 -1
- package/src/services/doctor-access.ts +14 -7
- package/src/services/doctor-backends.ts +39 -7
- package/src/services/doctor-checks.ts +43 -3
- package/src/services/doctor-push.ts +69 -90
- package/src/services/doctor.ts +107 -32
- package/src/services/finalize.ts +2 -1
- package/src/services/lane-service.ts +4 -10
- package/src/services/notifier.ts +322 -70
- package/src/services/peek.ts +46 -9
- package/src/services/ports.ts +8 -2
- package/src/services/profile-service.ts +117 -31
- package/src/services/profile-store.ts +126 -5
- package/src/services/routing-service.ts +6 -1
- package/src/services/run-debug.ts +98 -4
- package/src/services/run-service.ts +39 -4
- package/src/services/run-store.ts +21 -5
- package/src/services/runs-page.ts +3 -2
- package/src/services/session-view.ts +18 -13
- package/src/services/sessions.ts +32 -16
- package/src/services/setup.ts +31 -10
- package/src/services/source-derive.ts +32 -7
- package/src/services/summary.ts +40 -5
- package/src/services/treat-likes.ts +22 -9
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { OrchestrationHost } from "../domain/host.ts";
|
|
1
2
|
import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { errorMessage, isCatherdError } from "../domain/errors.ts";
|
|
5
|
+
import type { Profile } from "../domain/profile.ts";
|
|
4
6
|
import { claudeHome, locksDir } from "../infra/paths.ts";
|
|
5
7
|
import { ensurePrivateDir, PRIVATE_FILE } from "../infra/store.ts";
|
|
6
8
|
import { agentLinkState } from "./agent-links.ts";
|
|
@@ -130,9 +132,13 @@ export function locksCheck(): Check {
|
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
export function agentsCheck(
|
|
135
|
+
export function agentsCheck(
|
|
136
|
+
host: OrchestrationHost,
|
|
137
|
+
selected?: string[],
|
|
138
|
+
relink = `catherd profile use ${activeName()}`,
|
|
139
|
+
): Check {
|
|
134
140
|
const base = { id: "agents", label: "Claude agents" };
|
|
135
|
-
const links = agentLinkState();
|
|
141
|
+
const links = agentLinkState(host, selected);
|
|
136
142
|
const broken = [...links.missing, ...links.stale];
|
|
137
143
|
if (broken.length)
|
|
138
144
|
return {
|
|
@@ -140,9 +146,43 @@ export function agentsCheck(): Check {
|
|
|
140
146
|
state: "fail",
|
|
141
147
|
word: links.missing.length ? "missing" : "stale",
|
|
142
148
|
detail: broken.join(", "),
|
|
143
|
-
fix:
|
|
149
|
+
fix: relink,
|
|
144
150
|
};
|
|
145
151
|
return links.ok.length
|
|
146
152
|
? { ...base, state: "ok", word: "ready", detail: `${links.ok.length} linked` }
|
|
147
153
|
: { ...base, state: "skip", word: "none", detail: "no profile uses a native Claude rung" };
|
|
148
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* ` --host <host>` for a fix doctor prints, when it judged on a known host: run from a plain terminal, the fix
|
|
158
|
+
* must judge the profile the same way (omitted architect/verifier rungs resolve per host).
|
|
159
|
+
*/
|
|
160
|
+
export const hostFlag = (host: OrchestrationHost): string => (host === "unknown" ? "" : ` --host ${host}`);
|
|
161
|
+
|
|
162
|
+
/** The browser CLI the ui-reviewer's prompt takes its screenshots with. */
|
|
163
|
+
const UI_BROWSER = "agent-browser";
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The ui-reviewer takes screenshots with agent-browser: a profile that turns it on, on a machine without it,
|
|
167
|
+
* dispatches a role that cannot do its job. Nothing to say when it is on PATH or no profile turns the role on.
|
|
168
|
+
*/
|
|
169
|
+
export function uiBrowserCheck(
|
|
170
|
+
profiles: Profile[],
|
|
171
|
+
onPath: (bin: string) => string | null,
|
|
172
|
+
host: OrchestrationHost = "unknown",
|
|
173
|
+
): Check | null {
|
|
174
|
+
const on = profiles.filter((p) => p.roles["ui-reviewer"].enabled).map((p) => p.name);
|
|
175
|
+
if (!on.length || onPath(UI_BROWSER)) return null;
|
|
176
|
+
return {
|
|
177
|
+
id: "ui-browser",
|
|
178
|
+
label: UI_BROWSER,
|
|
179
|
+
state: "warn",
|
|
180
|
+
word: "missing",
|
|
181
|
+
detail: `${UI_BROWSER} is not on PATH, so ui-reviewer (${on.join(", ")}) cannot take screenshots`,
|
|
182
|
+
fix: [
|
|
183
|
+
`npm i -g ${UI_BROWSER}`,
|
|
184
|
+
"or turn the role off:",
|
|
185
|
+
...on.map((n) => `catherd profile set roles.ui-reviewer.enabled false --profile ${n}${hostFlag(host)}`),
|
|
186
|
+
].join("\n"),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -1,109 +1,88 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import type { HostContext } from "../domain/host.ts";
|
|
3
2
|
import { envelope } from "../domain/notice.ts";
|
|
4
3
|
import { readSessionEnv, sessionFileFor } from "../infra/claude-session.ts";
|
|
5
|
-
import {
|
|
4
|
+
import { sendToCodexQueue } from "../infra/codex-queue.ts";
|
|
6
5
|
import { sendToInbox } from "../infra/peer-inbox.ts";
|
|
7
6
|
import type { Check } from "./doctor-checks.ts";
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
* Spec §3.9 `doctor`'s `push` row: run from inside a Claude Code session (its Bash tool), send that session one
|
|
11
|
-
* `later` message and look for the enqueue line its transcript gets when the message is accepted
|
|
12
|
-
* (docs/research/2026-09-28-cross-session-messaging.md §7).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export type PushOutcome = "ok" | "no-session" | "held" | "unconfirmed" | "failed";
|
|
16
|
-
|
|
8
|
+
export type PushOutcome = "ok" | "no-session" | "unconfirmed" | "failed";
|
|
17
9
|
export interface PushProbe {
|
|
18
10
|
outcome: PushOutcome;
|
|
19
11
|
detail: string;
|
|
12
|
+
enqueue: "accepted" | "ambiguous" | "not-submitted" | "no-session";
|
|
13
|
+
processing: "observed" | "unconfirmed";
|
|
14
|
+
msgId: string | null;
|
|
20
15
|
}
|
|
21
16
|
|
|
22
|
-
/**
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
for (const d of readdirSync(projects)) {
|
|
30
|
-
const f = join(projects, d, `${sessionId}.jsonl`);
|
|
31
|
-
if (existsSync(f)) return f;
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const enqueued = (file: string, nonce: string): boolean => {
|
|
37
|
-
try {
|
|
38
|
-
return readFileSync(file, "utf8")
|
|
39
|
-
.split("\n")
|
|
40
|
-
.some((l) => l.includes('"operation":"enqueue"') && l.includes(nonce));
|
|
41
|
-
} catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export async function probePush(env: Record<string, string | undefined> = process.env): Promise<PushProbe> {
|
|
47
|
-
const s = readSessionEnv(env);
|
|
48
|
-
if (!s?.socketPath)
|
|
49
|
-
return { outcome: "no-session", detail: "run catherd doctor from a Claude Code session to test push" };
|
|
50
|
-
const sessionId = sessionFileFor(s)?.sessionId ?? s.sessionId;
|
|
51
|
-
const nonce = crypto.randomUUID().slice(0, 8);
|
|
52
|
-
const r = await sendToInbox(s, envelope(`catherd doctor: push test ${nonce}, no action needed`), "later");
|
|
53
|
-
// a socket that is gone or refuses is a shell that outlived its session, not a protocol change
|
|
54
|
-
if (r.outcome === "no-session" || r.outcome === "refused")
|
|
17
|
+
/** Explicit smoke only: native receipts prove acceptance, never model processing or result collection. */
|
|
18
|
+
export async function probePush(
|
|
19
|
+
host: HostContext,
|
|
20
|
+
env: Record<string, string | undefined>,
|
|
21
|
+
): Promise<PushProbe> {
|
|
22
|
+
const target = host.session;
|
|
23
|
+
if (host.conflict || !target || host.host === "unknown" || target.host !== host.host)
|
|
55
24
|
return {
|
|
56
25
|
outcome: "no-session",
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
detail: `catherd cannot notify this Claude Code version; peek still works (${r.reason ?? r.outcome})`,
|
|
26
|
+
enqueue: "no-session",
|
|
27
|
+
processing: "unconfirmed",
|
|
28
|
+
msgId: null,
|
|
29
|
+
detail:
|
|
30
|
+
"No validated live host session; run doctor --test-push from the original conversation. Terminal --host selects checks only. Use peek/result.",
|
|
63
31
|
};
|
|
64
|
-
const
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
outcome: "held",
|
|
76
|
-
detail: `the test message was sent but not accepted within ${pushLimits.waitMs / 1000} s: a crossSessionInbound setting holds or refuses it`,
|
|
77
|
-
}
|
|
78
|
-
: {
|
|
79
|
-
outcome: "unconfirmed",
|
|
80
|
-
detail: "sent; no transcript of this session was found to confirm it arrived",
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The probe as a `doctor` row. */
|
|
85
|
-
export function pushCheck(p: PushProbe): Check {
|
|
86
|
-
const base = { id: "push", label: "push to Claude Code", detail: p.detail };
|
|
87
|
-
switch (p.outcome) {
|
|
88
|
-
case "ok":
|
|
89
|
-
return { ...base, state: "ok", word: "ready" };
|
|
90
|
-
case "no-session":
|
|
91
|
-
return { ...base, state: "skip", word: "no session" };
|
|
92
|
-
case "held":
|
|
32
|
+
const content = envelope(`catherd doctor: push test ${crypto.randomUUID().slice(0, 8)}, no action needed`);
|
|
33
|
+
let enqueue: PushProbe["enqueue"];
|
|
34
|
+
let msgId: string | null = null;
|
|
35
|
+
if (host.host === "codex") {
|
|
36
|
+
const sent = await sendToCodexQueue(target, content, env);
|
|
37
|
+
enqueue = sent.outcome;
|
|
38
|
+
if (sent.outcome === "accepted") msgId = sent.msgId;
|
|
39
|
+
} else {
|
|
40
|
+
const s = readSessionEnv(env);
|
|
41
|
+
const liveId = s ? (sessionFileFor(s)?.sessionId ?? s.sessionId) : null;
|
|
42
|
+
if (!s || liveId !== target.sessionId)
|
|
93
43
|
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
44
|
+
outcome: "no-session",
|
|
45
|
+
enqueue: "no-session",
|
|
46
|
+
processing: "unconfirmed",
|
|
47
|
+
msgId: null,
|
|
48
|
+
detail: "Claude inbox identity does not match the validated live session; use peek/result.",
|
|
98
49
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
case "failed":
|
|
50
|
+
const sent = await sendToInbox(s, content, "later");
|
|
51
|
+
if (sent.outcome === "no-session" || sent.outcome === "refused")
|
|
102
52
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
53
|
+
outcome: "no-session",
|
|
54
|
+
enqueue: "not-submitted",
|
|
55
|
+
processing: "unconfirmed",
|
|
56
|
+
msgId: null,
|
|
57
|
+
detail:
|
|
58
|
+
"This session's inbox is not reachable; run doctor --test-push from a live Claude Code session. Use peek/result.",
|
|
107
59
|
};
|
|
60
|
+
enqueue = sent.outcome === "sent" && sent.msgId ? "accepted" : "ambiguous";
|
|
61
|
+
if (enqueue === "accepted") msgId = sent.msgId ?? null;
|
|
108
62
|
}
|
|
63
|
+
return {
|
|
64
|
+
outcome: enqueue === "accepted" ? "ok" : enqueue === "ambiguous" ? "unconfirmed" : "failed",
|
|
65
|
+
enqueue,
|
|
66
|
+
processing: "unconfirmed",
|
|
67
|
+
msgId,
|
|
68
|
+
detail:
|
|
69
|
+
enqueue === "accepted"
|
|
70
|
+
? "Smoke enqueue accepted; processing unconfirmed. Unloaded, interrupted or restarted hosts may retain queued input. Use peek/result for durable unread results."
|
|
71
|
+
: enqueue === "ambiguous"
|
|
72
|
+
? "Smoke has no verified queue receipt; processing unconfirmed. Retrying may duplicate input. Use peek/result."
|
|
73
|
+
: "Smoke was not submitted; check native queue support and the existing endpoint. Use peek/result.",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function pushCheck(p: PushProbe): Check {
|
|
78
|
+
const base = { id: "push", label: "host completion push", detail: p.detail };
|
|
79
|
+
if (p.enqueue === "accepted") return { ...base, state: "ok", word: "enqueue accepted" };
|
|
80
|
+
if (p.outcome === "no-session") return { ...base, state: "skip", word: "no session" };
|
|
81
|
+
if (p.enqueue === "ambiguous") return { ...base, state: "warn", word: "ambiguous" };
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
state: "fail",
|
|
85
|
+
word: "not submitted",
|
|
86
|
+
fix: "Check native host transport; peek/result keeps the unread record available.",
|
|
87
|
+
};
|
|
109
88
|
}
|
package/src/services/doctor.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { queueCapability, type QueueCapability } from "../infra/codex-queue.ts";
|
|
2
|
+
import type { HostContext } from "../domain/host.ts";
|
|
1
3
|
import { existsSync, statSync } from "node:fs";
|
|
2
4
|
import { adapterFor } from "../adapters/registry.ts";
|
|
3
5
|
import "../adapters/all.ts";
|
|
@@ -8,10 +10,11 @@ import type { JevTransport } from "../infra/jev-client.ts";
|
|
|
8
10
|
import { linkedProfiles } from "./agent-links.ts";
|
|
9
11
|
import { accessChecks } from "./doctor-access.ts";
|
|
10
12
|
import { backendChecks, usedBackends } from "./doctor-backends.ts";
|
|
11
|
-
import { type PushProbe, pushCheck } from "./doctor-push.ts";
|
|
13
|
+
import { type PushProbe, pushCheck, probePush } from "./doctor-push.ts";
|
|
12
14
|
import { sourcesCheck, standInsToConfirmIn } from "./doctor-sources.ts";
|
|
13
15
|
import {
|
|
14
16
|
agentsCheck,
|
|
17
|
+
hostFlag,
|
|
15
18
|
type Check,
|
|
16
19
|
errText,
|
|
17
20
|
fixOf,
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
locksCheck,
|
|
20
23
|
mcpCheck,
|
|
21
24
|
pluginCheck,
|
|
25
|
+
uiBrowserCheck,
|
|
22
26
|
} from "./doctor-checks.ts";
|
|
23
27
|
import { credentialsPath, jevKey, savedJevKey, testJevKey } from "./jev-service.ts";
|
|
24
28
|
import {
|
|
@@ -30,9 +34,13 @@ import {
|
|
|
30
34
|
readProjects,
|
|
31
35
|
validateNamed,
|
|
32
36
|
} from "./profile-store.ts";
|
|
37
|
+
import { inspectionHost } from "./run-debug.ts";
|
|
33
38
|
import { listRuns } from "./run-store.ts";
|
|
34
39
|
|
|
35
40
|
export interface DoctorReport {
|
|
41
|
+
host: HostContext;
|
|
42
|
+
queue: QueueCapability | null;
|
|
43
|
+
push: PushProbe | null;
|
|
36
44
|
/** false when any check failed: `catherd doctor` exits 3 */
|
|
37
45
|
ready: boolean;
|
|
38
46
|
version: string;
|
|
@@ -48,13 +56,17 @@ export interface Handshake {
|
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
export interface DoctorDeps {
|
|
59
|
+
host: HostContext;
|
|
60
|
+
repo: string | null;
|
|
51
61
|
bunVersion: string;
|
|
52
62
|
/** the package version, which the Claude Code plugin must pin */
|
|
53
63
|
version: string;
|
|
54
64
|
/** starts the MCP server as the plugin does (its launcher) over stdio and asks it for tools/list */
|
|
55
65
|
handshake: () => Promise<Handshake>;
|
|
56
|
-
/**
|
|
66
|
+
/** Explicit smoke test injection; default doctor never invokes it. */
|
|
57
67
|
push?: () => Promise<PushProbe>;
|
|
68
|
+
testPush?: boolean;
|
|
69
|
+
env?: Record<string, string | undefined>;
|
|
58
70
|
jev?: JevTransport;
|
|
59
71
|
}
|
|
60
72
|
|
|
@@ -77,13 +89,15 @@ export async function doctor(d: DoctorDeps): Promise<DoctorReport> {
|
|
|
77
89
|
: { id: "bun", label: "Bun", state: "ok", word: "ready", detail: d.bunVersion },
|
|
78
90
|
);
|
|
79
91
|
|
|
92
|
+
// every fix names the host doctor judged with: run elsewhere (a plain terminal), it must judge the same way
|
|
93
|
+
const onHost = hostFlag(d.host.host);
|
|
80
94
|
let profiles: Profile[] = [];
|
|
81
95
|
let linked: string[] = [];
|
|
82
96
|
let active: Profile | null = null;
|
|
83
97
|
try {
|
|
84
98
|
readConfig();
|
|
85
99
|
readProjects();
|
|
86
|
-
active = getProfile(activeName());
|
|
100
|
+
active = getProfile(activeName(d.repo), d.host.host);
|
|
87
101
|
linked = linkedProfiles();
|
|
88
102
|
checks.push({
|
|
89
103
|
id: "config",
|
|
@@ -105,7 +119,7 @@ export async function doctor(d: DoctorDeps): Promise<DoctorReport> {
|
|
|
105
119
|
// one unreadable linked profile must not hide the others' backends: its own row below reports it
|
|
106
120
|
profiles = linked.flatMap((n) => {
|
|
107
121
|
try {
|
|
108
|
-
return [getProfile(n)];
|
|
122
|
+
return [getProfile(n, d.host.host)];
|
|
109
123
|
} catch {
|
|
110
124
|
return [];
|
|
111
125
|
}
|
|
@@ -117,7 +131,7 @@ export async function doctor(d: DoctorDeps): Promise<DoctorReport> {
|
|
|
117
131
|
checks.push({
|
|
118
132
|
id: `binding:${repo}`,
|
|
119
133
|
label: `binding ${repo}`,
|
|
120
|
-
state: "fail",
|
|
134
|
+
state: repo === d.repo ? "fail" : "warn",
|
|
121
135
|
word: "missing",
|
|
122
136
|
detail: `bound to profile ${name}, which does not exist`,
|
|
123
137
|
fix: `cd ${repo} && catherd profile use --repo --clear`,
|
|
@@ -130,30 +144,32 @@ export async function doctor(d: DoctorDeps): Promise<DoctorReport> {
|
|
|
130
144
|
const names = active ? [active.name, ...linked.filter((n) => n !== active?.name)] : [];
|
|
131
145
|
for (const name of names) {
|
|
132
146
|
const id = name === active?.name ? "profile" : `profile:${name}`;
|
|
133
|
-
const validate = `catherd profile validate ${name}`;
|
|
147
|
+
const validate = `catherd profile validate ${name}${onHost}`;
|
|
134
148
|
const named = (fix: string) =>
|
|
135
|
-
fix.replaceAll("catherd profile set ", `catherd profile set --profile ${name} `);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
);
|
|
149
|
+
fix.replaceAll("catherd profile set ", `catherd profile set --profile ${name}${onHost} `);
|
|
150
|
+
const row = guarded(id, `profile ${name}`, validate, () => {
|
|
151
|
+
const v = validateNamed(name, d.repo, d.host.host);
|
|
152
|
+
const first = v.errors[0] ?? v.warnings[0];
|
|
153
|
+
return {
|
|
154
|
+
id,
|
|
155
|
+
label: `profile ${name}`,
|
|
156
|
+
state: v.errors.length ? "fail" : v.warnings.length ? "warn" : "ok",
|
|
157
|
+
word: v.errors.length ? "invalid" : v.warnings.length ? "warning" : "ready",
|
|
158
|
+
detail: first
|
|
159
|
+
? `${first.path}: ${first.message}${v.errors.length + v.warnings.length > 1 ? " (and more)" : ""}`
|
|
160
|
+
: "valid",
|
|
161
|
+
...(first ? { fix: first.fix ? named(first.fix) : validate } : {}),
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
if (name !== active?.name && row.state === "fail") row.state = "warn";
|
|
165
|
+
checks.push(row);
|
|
152
166
|
}
|
|
153
167
|
|
|
168
|
+
const diagnostics = profiles;
|
|
169
|
+
profiles = active ? [active] : [];
|
|
154
170
|
const used = usedBackends(profiles);
|
|
155
171
|
const installed = new Set<string>();
|
|
156
|
-
checks.push(...(await backendChecks(used, profiles, installed)));
|
|
172
|
+
checks.push(...(await backendChecks(used, profiles, installed, d.host.host)));
|
|
157
173
|
|
|
158
174
|
if (active && [active, ...profiles].every((p) => p.jev.use === "off"))
|
|
159
175
|
checks.push({
|
|
@@ -193,18 +209,44 @@ export async function doctor(d: DoctorDeps): Promise<DoctorReport> {
|
|
|
193
209
|
|
|
194
210
|
// spec 1.2 §9: the public sources' ages and errors, the Artificial Analysis key, and the active and linked
|
|
195
211
|
// profiles' stand-ins to confirm
|
|
196
|
-
const mine = [...(active ? [active] : []), ...
|
|
212
|
+
const mine = [...(active ? [active] : []), ...diagnostics.filter((p) => p.name !== active?.name)];
|
|
197
213
|
checks.push(
|
|
198
214
|
guarded("sources", "sources", "catherd catalog sync --force", () =>
|
|
199
215
|
sourcesCheck(Date.now(), standInsToConfirmIn(mine)),
|
|
200
216
|
),
|
|
201
217
|
);
|
|
202
218
|
|
|
203
|
-
|
|
219
|
+
const native = used.get("claude");
|
|
220
|
+
const plugin = pluginCheck(d.version);
|
|
221
|
+
if (d.host.host !== "claude-code" && native !== "role") {
|
|
222
|
+
plugin.state = native === "failover" ? "warn" : "skip";
|
|
223
|
+
plugin.word = native === "failover" ? "optional" : "not required";
|
|
224
|
+
}
|
|
225
|
+
checks.push(plugin);
|
|
204
226
|
|
|
227
|
+
// relinks the profile doctor checked: this repo's binding when it has one, else the active profile
|
|
228
|
+
let bound = false;
|
|
229
|
+
try {
|
|
230
|
+
bound = d.repo !== null && readProjects().bindings[d.repo] === active?.name;
|
|
231
|
+
} catch {
|
|
232
|
+
// the config row above already reports an unreadable projects.json
|
|
233
|
+
}
|
|
234
|
+
const relink = active
|
|
235
|
+
? `${bound ? `cd ${d.repo} && ` : ""}catherd profile use ${active.name}${bound ? " --repo" : ""}${onHost}`
|
|
236
|
+
: "";
|
|
205
237
|
checks.push(
|
|
206
238
|
active
|
|
207
|
-
? guarded("agents", "Claude agents",
|
|
239
|
+
? guarded("agents", "Claude agents", relink, () =>
|
|
240
|
+
native
|
|
241
|
+
? agentsCheck(d.host.host, [active!.name], relink)
|
|
242
|
+
: {
|
|
243
|
+
id: "agents",
|
|
244
|
+
label: "Claude agents",
|
|
245
|
+
state: "skip",
|
|
246
|
+
word: "not required",
|
|
247
|
+
detail: "selected profile has no native Claude roles",
|
|
248
|
+
},
|
|
249
|
+
)
|
|
208
250
|
: {
|
|
209
251
|
id: "agents",
|
|
210
252
|
label: "Claude agents",
|
|
@@ -214,15 +256,39 @@ export async function doctor(d: DoctorDeps): Promise<DoctorReport> {
|
|
|
214
256
|
},
|
|
215
257
|
);
|
|
216
258
|
|
|
259
|
+
const agents = checks.find((c) => c.id === "agents");
|
|
260
|
+
if (native === "failover" && agents?.state === "fail") agents.state = "warn";
|
|
261
|
+
|
|
217
262
|
const h = await d
|
|
218
263
|
.handshake()
|
|
219
264
|
.catch((e: unknown): Handshake => ({ ok: false, tools: [], error: errText(e) }));
|
|
220
265
|
checks.push(mcpCheck(h, d.version));
|
|
221
266
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
267
|
+
const queue =
|
|
268
|
+
d.host.host === "codex" && !d.host.conflict ? await queueCapability(d.env ?? process.env) : null;
|
|
269
|
+
if (queue)
|
|
270
|
+
checks.push({
|
|
271
|
+
id: "push-capability",
|
|
272
|
+
label: "native Codex queue",
|
|
273
|
+
state:
|
|
274
|
+
!queue.cli || queue.server === "unsupported" ? "fail" : queue.server === "unverified" ? "warn" : "ok",
|
|
275
|
+
word: !queue.cli ? "unavailable" : queue.server,
|
|
276
|
+
detail: queue.reason ?? "Native server advertises queue support",
|
|
277
|
+
...(!queue.cli || queue.server === "unsupported"
|
|
278
|
+
? { fix: "Update native Codex/server; use peek/result while push is unavailable." }
|
|
279
|
+
: {}),
|
|
280
|
+
});
|
|
281
|
+
let push: PushProbe | null = null;
|
|
282
|
+
if (d.testPush) {
|
|
283
|
+
push = await (d.push ?? (() => probePush(d.host, d.env ?? process.env)))().catch((): PushProbe => ({
|
|
284
|
+
outcome: "failed",
|
|
285
|
+
enqueue: "ambiguous",
|
|
286
|
+
processing: "unconfirmed",
|
|
287
|
+
msgId: null,
|
|
288
|
+
detail: "Smoke has no verified receipt; retry may duplicate input. Use peek/result.",
|
|
289
|
+
}));
|
|
290
|
+
checks.push(pushCheck(push));
|
|
291
|
+
}
|
|
226
292
|
|
|
227
293
|
checks.push(locksCheck());
|
|
228
294
|
// spec §5 and §12: which codex sandbox form runs, and the five access probes per workspace-write backend
|
|
@@ -321,6 +387,8 @@ export async function doctor(d: DoctorDeps): Promise<DoctorReport> {
|
|
|
321
387
|
detail: "the plugin's MCP launcher runs bunx when no global catherd is installed",
|
|
322
388
|
fix: "put Bun's bin folder (~/.bun/bin) on PATH",
|
|
323
389
|
});
|
|
390
|
+
const browser = uiBrowserCheck(mine, onPath, d.host.host);
|
|
391
|
+
if (browser) checks.push(browser);
|
|
324
392
|
let corrupt: ReturnType<typeof listRuns>["corrupt"] = [];
|
|
325
393
|
try {
|
|
326
394
|
corrupt = listRuns().corrupt;
|
|
@@ -344,5 +412,12 @@ export async function doctor(d: DoctorDeps): Promise<DoctorReport> {
|
|
|
344
412
|
fix: `fix or delete ${corrupt[0]?.dir}`,
|
|
345
413
|
});
|
|
346
414
|
|
|
347
|
-
return {
|
|
415
|
+
return {
|
|
416
|
+
host: inspectionHost(d.host),
|
|
417
|
+
queue,
|
|
418
|
+
push,
|
|
419
|
+
ready: !checks.some((c) => c.state === "fail"),
|
|
420
|
+
version: d.version,
|
|
421
|
+
checks,
|
|
422
|
+
};
|
|
348
423
|
}
|
package/src/services/finalize.ts
CHANGED
|
@@ -222,10 +222,11 @@ async function compute(run: Run, d: Dispatch): Promise<RunRecord> {
|
|
|
222
222
|
threadHeavy: o.tokens.input >= THREAD_HEAVY_INPUT,
|
|
223
223
|
access: a.access,
|
|
224
224
|
isolated: a.isolated,
|
|
225
|
+
...(a.network === undefined ? {} : { network: a.network }),
|
|
225
226
|
images: o.images,
|
|
226
227
|
error: o.error,
|
|
227
228
|
replyPath: relative(run.dir, p.reply),
|
|
228
|
-
...(a.sessionId ? { sessionId: a.sessionId } : {}),
|
|
229
|
+
...(a.sessionId ? { sessionId: a.sessionId, host: a.host ?? "claude-code" } : {}),
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -4,6 +4,7 @@ import { CatherdError } from "../domain/errors.ts";
|
|
|
4
4
|
import { assertId, ID_PATTERN, parseRung } from "../domain/ids.ts";
|
|
5
5
|
import { assertLaneHeader, type Difficulty, type Kind } from "../domain/lane.ts";
|
|
6
6
|
import type { Role } from "../domain/roles.ts";
|
|
7
|
+
import { cell } from "../domain/util.ts";
|
|
7
8
|
import {
|
|
8
9
|
type ClimbReason,
|
|
9
10
|
currentRoute,
|
|
@@ -28,11 +29,11 @@ import {
|
|
|
28
29
|
} from "./milestones.ts";
|
|
29
30
|
import type { Deps, Verdict } from "./ports.ts";
|
|
30
31
|
import {
|
|
32
|
+
appendKnowledge,
|
|
31
33
|
appendLedger,
|
|
32
34
|
appendOutcome,
|
|
33
35
|
appendRoute,
|
|
34
36
|
findRun,
|
|
35
|
-
knowledgeFile,
|
|
36
37
|
readRoutes,
|
|
37
38
|
type Run,
|
|
38
39
|
runFile,
|
|
@@ -41,7 +42,6 @@ import {
|
|
|
41
42
|
import { writeDigest } from "./protocol.ts";
|
|
42
43
|
import { openQuestions } from "./questions.ts";
|
|
43
44
|
import { type Notes, type NotesPatch, refreshState } from "./state.ts";
|
|
44
|
-
import { appendPrivate } from "../infra/store.ts";
|
|
45
45
|
|
|
46
46
|
const withHints = (hints: string[]) => (hints.length ? { hints } : {});
|
|
47
47
|
|
|
@@ -88,6 +88,7 @@ export async function route(
|
|
|
88
88
|
const lane = i.laneFile === undefined ? null : readLaneFile(run, i.laneFile);
|
|
89
89
|
const profile = deps.profiles.forRepo(run.meta.repo);
|
|
90
90
|
const a = await deps.routing.route({
|
|
91
|
+
host: deps.host.host,
|
|
91
92
|
runDir: run.dir,
|
|
92
93
|
repo: run.meta.repo,
|
|
93
94
|
profile,
|
|
@@ -212,8 +213,6 @@ export async function climb(
|
|
|
212
213
|
};
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
const cell = (s: string) => s.replace(/[|\n]/g, "/").replace(/\s+/g, " ").trim();
|
|
216
|
-
|
|
217
216
|
export const LAND_SKIPS = ["docs-only", "no-code"] as const;
|
|
218
217
|
export type LandSkip = (typeof LAND_SKIPS)[number];
|
|
219
218
|
|
|
@@ -348,12 +347,7 @@ export async function land(
|
|
|
348
347
|
hints.push(
|
|
349
348
|
`land: no routed lane is in milestone "${i.milestone}" (routed: ${routed.slice(0, 5).join(", ")}${routed.length > 5 ? ", …" : ""}); check its name: no lane outcome was recorded`,
|
|
350
349
|
);
|
|
351
|
-
if (i.learned) {
|
|
352
|
-
appendPrivate(
|
|
353
|
-
knowledgeFile(run.meta.repo),
|
|
354
|
-
`- ${now.toISOString().slice(0, 10)} ${run.meta.title} ${i.milestone}: ${cell(i.learned)}\n`,
|
|
355
|
-
);
|
|
356
|
-
}
|
|
350
|
+
if (i.learned) appendKnowledge(run.meta.repo, now, `${run.meta.title} ${i.milestone}`, i.learned);
|
|
357
351
|
// spec 1.1 §10: the milestone's digest, which the milestone push links
|
|
358
352
|
const digest = writeDigest(run, {
|
|
359
353
|
milestone: i.milestone,
|