ennodia 0.5.0 → 0.5.2
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 +2 -2
- package/docs/getting-started.md +3 -0
- package/docs/install.md +3 -1
- package/docs/reference/mcp-tools.md +16 -4
- package/docs/reference/supported-harnesses.md +12 -8
- package/package.json +1 -1
- package/src/core.ts +5 -18
- package/src/installation.ts +4 -2
- package/src/readiness.ts +23 -18
- package/src/server.ts +9 -9
- package/src/setup-app.ts +10 -10
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ See [how to read these notes](https://ennodia.cherninlab.com/docs/guides/underst
|
|
|
85
85
|
|
|
86
86
|
Codex CLI, Claude Code, OpenCode, Kilo Code, Kiro CLI, Cline, Hermes Agent, and Antigravity.
|
|
87
87
|
Model availability and tool permissions depend on the installed agent and its provider configuration.
|
|
88
|
-
Discovery reports native login checks and recovery actions.
|
|
88
|
+
Discovery reports native login checks and confirmed recovery actions. The requested task verifies model access without a separate model call first.
|
|
89
89
|
Codex workers use a read-only sandbox by default.
|
|
90
90
|
|
|
91
91
|
Ennodia discovers native Agent Skills and can request them for specific tasks.
|
|
@@ -142,7 +142,7 @@ Savings are not yet validated.
|
|
|
142
142
|
Ennodia checks selected workers, Judge, and Advisor routes before execution.
|
|
143
143
|
Blocked routes return a native recovery action before reviewers start.
|
|
144
144
|
Ennodia says nothing about its own status unless an agent you asked for is blocked.
|
|
145
|
-
Rechecks
|
|
145
|
+
Rechecks and blocked starts return compact diagnostics. The full setup panel appears only for an explicit setup request.
|
|
146
146
|
|
|
147
147
|
Your agent calls `ennodia_setup` to show each agent's status and fix in your current session.
|
|
148
148
|
Hosts that render MCP Apps, such as Claude Desktop and Visual Studio Code, can show it as a card with check buttons.
|
package/docs/getting-started.md
CHANGED
|
@@ -34,6 +34,9 @@ Start with one worker and a small set of files.
|
|
|
34
34
|
Ennodia checks each selected agent before execution. Your agent hears nothing about Ennodia's status unless an agent you requested is blocked.
|
|
35
35
|
A blocked agent comes with its exact fix, such as a sign-in command.
|
|
36
36
|
|
|
37
|
+
The requested task verifies model access. Ennodia does not add a separate model call before it.
|
|
38
|
+
An optional verification timeout leaves access unverified and does not prevent a task from starting.
|
|
39
|
+
|
|
37
40
|
For setup or recovery, give your agent this request:
|
|
38
41
|
|
|
39
42
|
```text
|
package/docs/install.md
CHANGED
|
@@ -81,7 +81,9 @@ Tell the user which step is needed before trying to call Ennodia.
|
|
|
81
81
|
|
|
82
82
|
Ennodia includes an installation self-check in run results and rechecks. Its MCP startup instructions carry only installation problems. It uses the loaded code location to identify a source checkout, project-local dependency, global installation, package cache, or unknown scope. It checks the Bun version, package version, core files, bundled skills, and filesystem access for temporary workspaces, history, and harness update receipts. Development files are not required for a packaged install. The npm launcher stops before startup if Bun is too old.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
Report confirmed installation blockers briefly. A running process using an older version needs a reconnect. This does not mean the installation is broken. Do not repeat unchanged warnings.
|
|
85
|
+
|
|
86
|
+
Call `ennodia_check_harnesses` with `refresh: true` to refresh the installation report and native harness status after a repair. Filesystem access checks do not prove free disk space or future writes. They do not verify provider access or the latest published package version. A global install gives no extra permissions. The user's normal permissions still apply.
|
|
85
87
|
|
|
86
88
|
The report also identifies compiled code. A compiled executable does not need separate source entrypoints. Missing bundled skills remain a reported limitation. Compilation is currently an experimental build path.
|
|
87
89
|
|
|
@@ -732,6 +732,8 @@ After the worker settles, inspect its task receipt. If `canContinue` is true,
|
|
|
732
732
|
pass that task ID as `continueTaskId` in the next run. The follow-up keeps the
|
|
733
733
|
harness and directory. It inherits the model and reasoning effort unless you
|
|
734
734
|
supply new values. Each follow-up has a new task ID and execution allowance.
|
|
735
|
+
|
|
736
|
+
Native status checks use the inherited or explicitly supplied model before the follow-up starts.
|
|
735
737
|
An earlier turn cannot be used once a follow-up has started.
|
|
736
738
|
|
|
737
739
|
Continuation requires a task still held by the same running Ennodia server.
|
|
@@ -811,7 +813,7 @@ Sign-in and credential changes stay in each agent's own CLI.
|
|
|
811
813
|
|
|
812
814
|
#### Setup card
|
|
813
815
|
|
|
814
|
-
`ennodia_setup`
|
|
816
|
+
Only `ennodia_setup` names the `ui://ennodia/setup.html` resource in `_meta.ui.resourceUri`.
|
|
815
817
|
The resource uses the MCP Apps type `text/html;profile=mcp-app`.
|
|
816
818
|
Hosts that render MCP Apps show the card inside the conversation. Other hosts ignore it and keep the Markdown panel.
|
|
817
819
|
|
|
@@ -835,15 +837,25 @@ The result includes `installation`, a local self-check of Ennodia's loaded code
|
|
|
835
837
|
It accepts `harnessIds`, `model`, `cwd`, `refresh`, `verify`, and `upgrade`.
|
|
836
838
|
Native status checks are the default. `verify: true` makes a bounded real model request.
|
|
837
839
|
|
|
838
|
-
`unverified` means
|
|
840
|
+
`unverified` means model access is unknown. Normal runs use native status checks and let the task verify model access.
|
|
841
|
+
An inconclusive check does not block the task or mark the agent as needing setup.
|
|
839
842
|
Use targeted `harnessIds` for recovery checks. Unrelated blockers do not prevent work on usable requested routes.
|
|
840
843
|
|
|
841
|
-
|
|
844
|
+
Only `ennodia_setup` includes the embedded `ennodia://setup` resource with `text/markdown` content.
|
|
845
|
+
Rechecks and blocked preflight responses return compact JSON diagnostics without creating another setup card.
|
|
842
846
|
The JSON text response keeps its existing shape.
|
|
843
847
|
|
|
844
848
|
The panel is a table of agents, with agents that need the user first. Each row gives the status in plain words and the next step.
|
|
845
849
|
Sign-in commands follow the table, for login blockers only. Key expiry and quota blockers name their own recovery.
|
|
846
|
-
|
|
850
|
+
|
|
851
|
+
Show the full panel only for an explicit setup or status request.
|
|
852
|
+
For a blocked task, report its cause and native recovery action briefly.
|
|
853
|
+
Do not repeat unchanged warnings or automatically retry checks.
|
|
854
|
+
Recheck the same harness and model after recovery, then resume the original task.
|
|
855
|
+
|
|
856
|
+
When the installed package differs from the running process, `installation.restartRequired` is true.
|
|
857
|
+
This is a reload notice, not a broken installation. Reconnect the MCP connection to load the installed version.
|
|
858
|
+
Missing runtime files or an unsupported runtime still report a broken installation.
|
|
847
859
|
|
|
848
860
|
`upgrade: true` runs supported updaters before rediscovery and recheck.
|
|
849
861
|
This tool can change installed software and call providers, so it does not declare `readOnlyHint`.
|
|
@@ -45,12 +45,14 @@ It does not guarantee media support, authentication, or access to a requested mo
|
|
|
45
45
|
Plan Advisor receives this guidance in its harness inventory.
|
|
46
46
|
|
|
47
47
|
Discovery checks installation and native login status through supported commands.
|
|
48
|
-
Model access remains unverified until
|
|
49
|
-
A stalled check stops at its deadline and
|
|
48
|
+
Model access remains unverified until an explicit verification request or a successful task returns an answer.
|
|
49
|
+
A stalled check stops at its deadline and leaves access unverified.
|
|
50
50
|
Ennodia checks selected worker, Judge, and Advisor routes before execution.
|
|
51
51
|
Known blockers prevent requested work from starting.
|
|
52
52
|
Automatic routing excludes blocked routes and reports missing coverage.
|
|
53
53
|
|
|
54
|
+
Normal runs check native status without making a separate model request.
|
|
55
|
+
|
|
54
56
|
## Media inputs
|
|
55
57
|
|
|
56
58
|
Ennodia sends a text prompt through the selected CLI. Include local file paths and requested ranges in that prompt.
|
|
@@ -138,7 +140,7 @@ Call `ennodia_check_harnesses` after sign-in:
|
|
|
138
140
|
```
|
|
139
141
|
|
|
140
142
|
Pass the same `model` and `cwd` when rechecking an explicit route.
|
|
141
|
-
The check returns `actionRequired` for login, setup, model,
|
|
143
|
+
The check returns `actionRequired` for confirmed login, setup, model, and capacity blockers.
|
|
142
144
|
A missing CLI requires installation. A capacity failure requires capacity recovery.
|
|
143
145
|
Neither is fixed by repeated sign-in.
|
|
144
146
|
|
|
@@ -147,7 +149,9 @@ Replace the key through the native credential flow for the intended provider.
|
|
|
147
149
|
Signing in again does not renew an expired API key.
|
|
148
150
|
`quota-exhausted` includes depleted credits.
|
|
149
151
|
Restore credits or quota through the intended provider before rechecking.
|
|
152
|
+
|
|
150
153
|
Ennodia reads terminal-formatted native errors and returns the matching recovery action.
|
|
154
|
+
A worker's plain-text answer does not trigger a sign-in or capacity recovery action.
|
|
151
155
|
|
|
152
156
|
Hermes uses the full `hermes chat` CLI. The legacy `hermes-agent` executable is not compatible.
|
|
153
157
|
|
|
@@ -155,8 +159,8 @@ Native checks cache for 30 seconds. Verified routes cache for five minutes.
|
|
|
155
159
|
A native authentication failure invalidates the route immediately.
|
|
156
160
|
Concurrent checks of the same route share one request.
|
|
157
161
|
|
|
158
|
-
`unverified` means
|
|
159
|
-
`ennodia_run`
|
|
162
|
+
`unverified` means model access is unknown. It does not mean a failed harness.
|
|
163
|
+
`ennodia_run` lets the requested task verify model access after native status checks.
|
|
160
164
|
Checking every supported harness is not a prerequisite for a run.
|
|
161
165
|
|
|
162
166
|
Unrelated harness blockers do not prevent work on a usable requested route.
|
|
@@ -167,9 +171,9 @@ Ennodia does not require an additional approval step.
|
|
|
167
171
|
Model probes run in an empty temporary directory with normal native permissions.
|
|
168
172
|
A temporary directory is not a security sandbox.
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
Explicit verification uses provider capacity independently of a run's budget.
|
|
175
|
+
Normal runs add no model probe calls or input-token reserves.
|
|
176
|
+
A verification timeout after 25 seconds does not block the requested task or imply a setup problem.
|
|
173
177
|
|
|
174
178
|
Before ordinary execution, Ennodia attempts supported upgrades once per day while no Ennodia task is active.
|
|
175
179
|
Homebrew installations use their installed formula or cask.
|
package/package.json
CHANGED
package/src/core.ts
CHANGED
|
@@ -416,7 +416,7 @@ export class EnnodiaCore {
|
|
|
416
416
|
for (const h of harnesses) { const fresh = refreshed.find(f => f.id === h.id); if (fresh) Object.assign(h, fresh); }
|
|
417
417
|
}
|
|
418
418
|
const unique = [...new Map(routes.map(r => [JSON.stringify(r), r])).values()];
|
|
419
|
-
//
|
|
419
|
+
// Native checks catch known blockers; the requested task verifies model access.
|
|
420
420
|
const checked = await Promise.all(unique.map(async route => {
|
|
421
421
|
const h = harnesses.find(h => h.id === route.harnessId);
|
|
422
422
|
if (!h) throw new Error(`Unknown harness: ${route.harnessId}`);
|
|
@@ -425,23 +425,10 @@ export class EnnodiaCore {
|
|
|
425
425
|
const initialBlocked = checked.filter(c => readinessBlocked(c.readiness)).map(c => ({ harnessId: c.h.id, harnessName: c.h.name, readiness: c.readiness }));
|
|
426
426
|
if (initialBlocked.length) throw new HarnessReadinessError(initialBlocked);
|
|
427
427
|
for (const { h, readiness } of checked) {
|
|
428
|
-
if (readiness.state !== "ready") {
|
|
429
|
-
// Probes are real provider calls. Reserve them before starting, and
|
|
430
|
-
// expose their estimate separately rather than implying free checks.
|
|
431
|
-
report.modelProbes++; report.estimatedInputTokens += 256;
|
|
432
|
-
}
|
|
433
428
|
report.checks.push({ harnessId: h.id, harnessName: h.name, readiness });
|
|
434
|
-
|
|
435
|
-
if (budget?.maxEstimatedInputTokens !== undefined && report.estimatedInputTokens > budget.maxEstimatedInputTokens) throw new Error("Budget limit exceeded: readiness probe input reserve.");
|
|
436
|
-
}
|
|
437
|
-
const verified = await Promise.all(report.checks.map(async c => ({ ...c, readiness: await this.readinessManager!.check(harnesses.find(h => h.id === c.harnessId)!, this.findHarnessAdapter(c.harnessId), { model: c.readiness.model, cwd, verify: true }) })));
|
|
438
|
-
report.checks = verified;
|
|
439
|
-
const blocked = verified.filter(c => readinessBlocked(c.readiness));
|
|
440
|
-
if (blocked.length) throw new HarnessReadinessError(blocked);
|
|
441
|
-
for (const check of verified) {
|
|
442
|
-
const h = harnesses.find(h => h.id === check.harnessId);
|
|
443
|
-
if (h) h.readiness = check.readiness;
|
|
429
|
+
h.readiness = readiness;
|
|
444
430
|
}
|
|
431
|
+
if (budget?.maxChildTasks !== undefined && routes.length > budget.maxChildTasks) throw new Error("Budget limit exceeded: selected routes exceed maxChildTasks.");
|
|
445
432
|
return report;
|
|
446
433
|
}
|
|
447
434
|
|
|
@@ -882,9 +869,9 @@ export class EnnodiaCore {
|
|
|
882
869
|
}
|
|
883
870
|
|
|
884
871
|
async startRun(input: RunStartWithSkillIdsInput): Promise<RunView> {
|
|
885
|
-
if (input.continueTaskId
|
|
872
|
+
if (input.continueTaskId) {
|
|
886
873
|
const previous = this.taskManager.get(input.continueTaskId, { includeOutput: false, includeEvents: false });
|
|
887
|
-
if (previous) input = { ...input, cwd: previous.cwd };
|
|
874
|
+
if (previous) input = { ...input, cwd: input.cwd ?? previous.cwd, model: input.model ?? previous.model };
|
|
888
875
|
}
|
|
889
876
|
const { skillIds, ...runInput } = input;
|
|
890
877
|
const skills = runInput.skills ??
|
package/src/installation.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type InstallationReport = {
|
|
|
13
13
|
status: "healthy" | "limited" | "broken";
|
|
14
14
|
version: string;
|
|
15
15
|
packageVersion?: string;
|
|
16
|
+
restartRequired?: boolean;
|
|
16
17
|
distribution: "source" | "compiled";
|
|
17
18
|
scope: InstallationScope;
|
|
18
19
|
evidence: string;
|
|
@@ -59,7 +60,8 @@ export function inspectInstallation(options: InstallationOptions = {}): Installa
|
|
|
59
60
|
const jsr = readMetadata(join(codeRoot, "jsr.json"));
|
|
60
61
|
const metadata = npm?.name === "ennodia" ? npm : jsr?.name === "@cherninlab/ennodia" ? jsr : undefined;
|
|
61
62
|
const packageVersion = typeof metadata?.version === "string" ? metadata.version : undefined;
|
|
62
|
-
|
|
63
|
+
const restartRequired = Boolean(packageVersion && packageVersion !== ENNODIA_VERSION);
|
|
64
|
+
add("package-version", packageVersion === ENNODIA_VERSION ? "pass" : "warning", restartRequired ? `Installed ${packageVersion}; running ${ENNODIA_VERSION}. Reconnect the Ennodia MCP connection to load the installed version.` : packageVersion ? `Metadata ${packageVersion}; loaded code ${ENNODIA_VERSION}.` : "Package metadata is unavailable; only the loaded code version is known.");
|
|
63
65
|
|
|
64
66
|
// JSR does not ship the npm launcher. Development tooling is not required.
|
|
65
67
|
const declaredModules = Array.isArray(npm?.files) ? npm.files.filter((p): p is string => typeof p === "string" && /^src\/[^/*]+\.ts$/.test(p)) : [];
|
|
@@ -99,7 +101,7 @@ export function inspectInstallation(options: InstallationOptions = {}): Installa
|
|
|
99
101
|
const launcher = launcherPath && isAbsolute(launcherPath) && canonical(launcherPath) === canonical(join(codeRoot, "bin/ennodia")) ? launcherPath : undefined;
|
|
100
102
|
return {
|
|
101
103
|
checkedAt: new Date().toISOString(), status: checks.some(c => c.status === "fail") ? "broken" : limitations.length ? "limited" : "healthy",
|
|
102
|
-
version: ENNODIA_VERSION, packageVersion, distribution: compiled ? "compiled" : "source", scope, evidence, codeRoot, serverCwd, launcher,
|
|
104
|
+
version: ENNODIA_VERSION, packageVersion, ...(restartRequired ? { restartRequired } : {}), distribution: compiled ? "compiled" : "source", scope, evidence, codeRoot, serverCwd, launcher,
|
|
103
105
|
runtime: { name: bunVersion ? "bun" : "unsupported", version: bunVersion, executable: process.execPath, minimumVersion: MIN_BUN_VERSION, platform: process.platform, arch: process.arch },
|
|
104
106
|
checks, storage: { temporaryDirectory, history, harnessUpdates: updates }, limitations,
|
|
105
107
|
};
|
package/src/readiness.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type HarnessReadinessOptions = {
|
|
|
28
28
|
isBusy?: () => boolean;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const BLOCKED = new Set<ReadinessState>(["login-required", "credentials-expired", "setup-required", "model-unavailable", "quota-exhausted", "
|
|
31
|
+
const BLOCKED = new Set<ReadinessState>(["login-required", "credentials-expired", "setup-required", "model-unavailable", "quota-exhausted", "updating"]);
|
|
32
32
|
export function readinessBlocked(readiness?: HarnessReadiness): boolean { return Boolean(readiness && BLOCKED.has(readiness.state)); }
|
|
33
33
|
export function harnessAttention(harnesses: HarnessDiscovery[]): HarnessAttention[] {
|
|
34
34
|
return harnesses.filter(h => readinessBlocked(h.readiness)).map(h => ({ harnessId: h.id, harnessName: h.name, readiness: h.readiness! }));
|
|
@@ -41,20 +41,24 @@ export class HarnessReadinessError extends Error {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Recognize native failure envelopes, never quoted prose inside an answer. */
|
|
44
|
-
export function nativeReadinessFailure(stdout: string, stderr: string): ReadinessState | undefined {
|
|
45
|
-
const
|
|
46
|
-
|
|
44
|
+
export function nativeReadinessFailure(stdout: string, stderr: string, allowPlainStdout = false): ReadinessState | undefined {
|
|
45
|
+
const stderrLines = stripVTControlCharacters(stderr).split(/\r?\n/);
|
|
46
|
+
const lines = [...stderrLines, ...stripVTControlCharacters(stdout).split(/\r?\n/)];
|
|
47
|
+
for (const [index, line] of lines.entries()) {
|
|
47
48
|
let message = line.trim();
|
|
48
49
|
try {
|
|
49
50
|
const event = JSON.parse(line);
|
|
50
51
|
if (event.type === "error") message = typeof event.message === "string" ? event.message : event.error?.message ?? "";
|
|
51
52
|
else if (event.type === "turn.failed") message = event.error?.message ?? "";
|
|
52
53
|
else continue;
|
|
53
|
-
} catch {
|
|
54
|
+
} catch {
|
|
55
|
+
// Only status/nonce checks can distinguish plain stdout from worker prose.
|
|
56
|
+
if (index >= stderrLines.length && !allowPlainStdout) continue;
|
|
57
|
+
}
|
|
54
58
|
message = stripVTControlCharacters(message).trim();
|
|
55
59
|
if (/^(?:Error:\s*)?(?:This |The |Your )?API key (?:has expired|is expired)\b/i.test(message)) return "credentials-expired";
|
|
56
60
|
if (/^(?:(?:API )?Error:\s*)?(?:Failed to authenticate\b|OAuth session expired\b|Authentication failed\b|Not logged in\b|Unauthorized\b|401\b|Re-authentication required\b|No access token found\b|Token refresh failed\b|Failed to refresh (?:OAuth|token)\b|Your authentication token .*expired\b)/i.test(message)) return "login-required";
|
|
57
|
-
if (/^(?:Error:\s*)?(?:You've hit your usage limit\b|quota (?:exceeded|exhausted)\b|insufficient (?:credits|balance)\b|rate limit exceeded\b|Add credits to continue\b)/i.test(message)) return "quota-exhausted";
|
|
61
|
+
if (/^(?:Error:\s*)?(?:You've hit your (?:usage|\d+[ -]hour|weekly) limit\b|quota (?:exceeded|exhausted)\b|insufficient (?:credits|balance)\b|rate limit exceeded\b|Add credits to continue\b)/i.test(message)) return "quota-exhausted";
|
|
58
62
|
if (/^(?:Error:\s*)?(?:model .* (?:not found|not available|not supported)|unknown model\b|ModelNotFoundError\b)/i.test(message)) return "model-unavailable";
|
|
59
63
|
if (/^(?:Error:\s*)?(?:No (?:API key|provider|credentials)\b|Missing .*API.?KEY\b|unrecognized arguments\b|unknown (?:option|command)\b)/i.test(message)) return "setup-required";
|
|
60
64
|
}
|
|
@@ -94,7 +98,7 @@ export function harnessUpgradeCommand(h: HarnessDiscovery): string[] | undefined
|
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
const messages: Record<ReadinessState, string> = {
|
|
97
|
-
unverified: "Model access is unverified
|
|
101
|
+
unverified: "Model access is unverified. A task can run without a separate model check.",
|
|
98
102
|
ready: "The selected route returned a verified response.",
|
|
99
103
|
"login-required": "Sign-in is required before review.",
|
|
100
104
|
"credentials-expired": "The configured provider API key has expired. Replace the key in the native CLI/provider settings; signing in again will not renew it.",
|
|
@@ -133,10 +137,9 @@ export class HarnessReadinessManager {
|
|
|
133
137
|
private value(h: HarnessDiscovery, state: ReadinessState, model?: string): HarnessReadiness {
|
|
134
138
|
const r = recipe(h.id);
|
|
135
139
|
const result: HarnessReadiness = { state, checkedAt: new Date(this.now()).toISOString(), model, message: messages[state], upgrade: this.updates.get(h.id) };
|
|
136
|
-
if (state !== "ready") result.recovery = {
|
|
137
|
-
command: h.commandPath ? [h.commandPath, ...(state === "login-required" || state === "credentials-expired"
|
|
140
|
+
if (state !== "ready" && state !== "unverified") result.recovery = {
|
|
141
|
+
command: h.commandPath ? [h.commandPath, ...(state === "login-required" || state === "credentials-expired" ? r.login : ["--help"])] : [],
|
|
138
142
|
instruction: !h.commandPath ? "Install the supported native CLI, then recheck."
|
|
139
|
-
: state === "unverified" ? "Run the targeted recheck to verify model access. Sign in only when a login blocker is reported."
|
|
140
143
|
: state === "login-required" ? "Run the native command in an interactive terminal using the intended account/provider, then recheck."
|
|
141
144
|
: state === "credentials-expired" ? "Replace the expired API key through the native credential flow for the intended provider, then recheck the same route. Signing in again will not renew the key."
|
|
142
145
|
: state === "quota-exhausted" ? "Restore credits or quota through the intended provider, then recheck the same route. Do not repeat sign-in or switch to a different model silently."
|
|
@@ -177,7 +180,7 @@ export class HarnessReadinessManager {
|
|
|
177
180
|
const flightKey = `${key}:${Boolean(input.verify)}`;
|
|
178
181
|
const flight = this.flights.get(flightKey);
|
|
179
182
|
if (flight) return flight;
|
|
180
|
-
const pending = this.performCheck(h, adapter, input).catch(() => this.value(h, "
|
|
183
|
+
const pending = this.performCheck(h, adapter, input).catch(() => ({ ...this.value(h, "unverified", input.model), message: "The native check could not complete. Model access remains unverified; a task can still run." })).then(value => {
|
|
181
184
|
this.cache.set(key, { at: this.now(), value });
|
|
182
185
|
return value;
|
|
183
186
|
}).finally(() => this.flights.delete(flightKey));
|
|
@@ -192,13 +195,13 @@ export class HarnessReadinessManager {
|
|
|
192
195
|
const auth = recipe(h.id).auth;
|
|
193
196
|
if (auth) {
|
|
194
197
|
const result = await this.runner({ command: h.commandPath, args: auth }, 5_000);
|
|
195
|
-
const state = nativeReadinessFailure(result.stdout, result.stderr);
|
|
198
|
+
const state = nativeReadinessFailure(result.stdout, result.stderr, true);
|
|
196
199
|
if (state) return this.value(h, state, input.model);
|
|
197
200
|
if (h.id === "claude-code") {
|
|
198
201
|
try { if (JSON.parse(result.stdout).loggedIn === false) return this.value(h, "login-required", input.model); } catch { /* verification below */ }
|
|
199
202
|
}
|
|
200
|
-
if (result.timedOut) return this.value(h, "
|
|
201
|
-
if (result.exitCode !== 0) return this.value(h,
|
|
203
|
+
if (result.timedOut) return { ...this.value(h, "unverified", input.model), message: "The native sign-in check timed out. No sign-in or setup failure was confirmed." };
|
|
204
|
+
if (result.exitCode !== 0) return { ...this.value(h, "unverified", input.model), message: "The native check exited without a recognized blocker. A task can still run." };
|
|
202
205
|
}
|
|
203
206
|
if (!input.verify) return this.value(h, "unverified", input.model);
|
|
204
207
|
const dir = mkdtempSync(join(tmpdir(), "ennodia-readiness-"));
|
|
@@ -206,13 +209,14 @@ export class HarnessReadinessManager {
|
|
|
206
209
|
try {
|
|
207
210
|
const spec = adapter.buildCommand(h.commandPath, { prompt: `Reply with exactly ${nonce}. Do not use tools, read files, or perform any other work.`, cwd: dir, model: input.model, timeoutMs: 20_000, finalMessagePath: join(dir, "answer.txt"), nativeSandbox: adapter.supportsNativeSandbox ? "read-only" : undefined });
|
|
208
211
|
const result = await this.runner(spec, 25_000);
|
|
209
|
-
const failure = nativeReadinessFailure(result.stdout, result.stderr);
|
|
212
|
+
const failure = nativeReadinessFailure(result.stdout, result.stderr, true);
|
|
210
213
|
if (failure) return this.value(h, failure, input.model);
|
|
211
|
-
if (result.timedOut
|
|
214
|
+
if (result.timedOut) return { ...this.value(h, "unverified", input.model), message: "Model verification timed out after 25 seconds. A task can still run with its own time limit." };
|
|
215
|
+
if (result.exitCode !== 0 || adapter.failureReason?.(result.stdout, result.stderr)) return { ...this.value(h, "unverified", input.model), message: "Model verification did not complete. A task can still run." };
|
|
212
216
|
let answer = adapter.readinessAnswer ? adapter.readinessAnswer(result.stdout) ?? "" : result.stdout;
|
|
213
217
|
try { answer = readFileSync(join(dir, "answer.txt"), "utf8"); } catch { /* native text output */ }
|
|
214
218
|
// An echoed prompt is not success. Require a standalone returned nonce.
|
|
215
|
-
if (!stripVTControlCharacters(answer).split(/\r?\n/).some(line => line.trim() === nonce)) return this.value(h, "
|
|
219
|
+
if (!stripVTControlCharacters(answer).split(/\r?\n/).some(line => line.trim() === nonce)) return { ...this.value(h, "unverified", input.model), message: "Model verification did not return the expected response. A task can still run." };
|
|
216
220
|
return this.value(h, "ready", input.model);
|
|
217
221
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
218
222
|
}
|
|
@@ -238,7 +242,8 @@ export class HarnessReadinessManager {
|
|
|
238
242
|
const command = harnessUpgradeCommand(h);
|
|
239
243
|
const checkedAt = new Date(this.now()).toISOString();
|
|
240
244
|
const save = (status: NonNullable<HarnessReadiness["upgrade"]>["status"], message: string) => {
|
|
241
|
-
this.updates.set(h.id, { status, checkedAt, command, message });
|
|
245
|
+
this.updates.set(h.id, { status, checkedAt, command, message });
|
|
246
|
+
if (status === "succeeded" || status === "failed") this.invalidate(h.id);
|
|
242
247
|
};
|
|
243
248
|
if (!command) { save("unsupported", "No supported updater was found for this installation. Use its original installer."); return; }
|
|
244
249
|
if (this.options.isBusy?.() || this.flights.size > 0) { save("deferred", "Upgrade deferred while Ennodia work is active."); return; }
|
package/src/server.ts
CHANGED
|
@@ -108,7 +108,7 @@ export function createEnnodiaServer(
|
|
|
108
108
|
catch (error) {
|
|
109
109
|
if (error instanceof HarnessReadinessError) {
|
|
110
110
|
const installation = core.getInstallation?.();
|
|
111
|
-
return { ...
|
|
111
|
+
return { ...jsonResult({ error: error.message, installation, actionRequired: error.actionRequired, coverage: { started: 0 } }), isError: true };
|
|
112
112
|
}
|
|
113
113
|
throw error;
|
|
114
114
|
}
|
|
@@ -149,8 +149,7 @@ export function createEnnodiaServer(
|
|
|
149
149
|
|
|
150
150
|
server.registerTool("ennodia_check_harnesses", {
|
|
151
151
|
title: "Recheck or upgrade Ennodia harnesses",
|
|
152
|
-
description: "Recheck
|
|
153
|
-
_meta: SETUP_APP_TOOL_META,
|
|
152
|
+
description: "Recheck selected harnesses and return compact diagnostics. Target affected harnessIds; unrelated blockers do not prevent work. verify makes an optional real model call; a timeout leaves access unverified and does not block tasks. upgrade runs supported updaters before rechecking. Never switch account/provider/model silently. Show a brief changed result; use ennodia_setup only when the user asks for the full setup view.",
|
|
154
153
|
inputSchema: {
|
|
155
154
|
harnessIds: z.array(z.string().min(1)).min(1).optional().describe("Harnesses to recheck. Omit for all installed harnesses."),
|
|
156
155
|
model: z.string().min(1).optional().describe("Exact model to verify on the selected route."),
|
|
@@ -161,7 +160,7 @@ export function createEnnodiaServer(
|
|
|
161
160
|
},
|
|
162
161
|
}, async input => {
|
|
163
162
|
const result = await core.checkHarnesses(input);
|
|
164
|
-
return
|
|
163
|
+
return jsonResult(result);
|
|
165
164
|
});
|
|
166
165
|
|
|
167
166
|
// Hosts that render MCP Apps show this card for the two tools above.
|
|
@@ -1337,9 +1336,9 @@ export async function shutdownEnnodia(
|
|
|
1337
1336
|
export function serverInstructions(installation?: InstallationReport): string {
|
|
1338
1337
|
const lines = [
|
|
1339
1338
|
"Ennodia hands work to the user's other local agents. Do not open with Ennodia's status or list agents the user did not ask about: ennodia_run checks the agents it selects before starting.",
|
|
1340
|
-
"
|
|
1339
|
+
"For a confirmed blocker on a requested agent, state its cause and native recovery action in one short line. Do not repeat unchanged warnings or automatically retry checks. After recovery, recheck that harness and model. If agents were skipped (actionRequired), say so in one line. Unrelated blockers must not hold up usable agents.",
|
|
1341
1340
|
"For setup or status requests, call ennodia_setup and show its panel. Hosts that render MCP Apps show it as a card; then add one line at most.",
|
|
1342
|
-
"Unverified means
|
|
1341
|
+
"Unverified means model access is unknown, not failed; the requested task can verify it. Use a separate model probe only when needed. A reload notice after an update is not a broken installation; mention it once when relevant. Never collect credentials in chat, switch account/provider/model silently, or claim a check ran when it did not. Follow existing authorization; add no approval step of your own.",
|
|
1343
1342
|
];
|
|
1344
1343
|
if (installation?.status === "broken") lines.push(`Ennodia's installation is broken: ${installation.limitations.join(" ")} Tell the user before starting work and link ${INSTALL_GUIDE_URL}.`);
|
|
1345
1344
|
else if (installation?.status === "limited") lines.push(`Installation notes, to mention only when they affect the request: ${installation.limitations.join(" ")}`);
|
|
@@ -1357,16 +1356,17 @@ function setupResult(value: unknown, routes: (HarnessDiscovery | HarnessAttentio
|
|
|
1357
1356
|
const cell = (text: string) => text.replaceAll("|", "\\|").replace(/[\r\n]+/g, " ");
|
|
1358
1357
|
const lines = ["## Ennodia agents"];
|
|
1359
1358
|
if (installation && installation.status !== "healthy") {
|
|
1360
|
-
lines.push("", `Installation ${installation.status}:`, ...installation.limitations.map(limit => `- ${limit}`)
|
|
1359
|
+
lines.push("", installation.status !== "broken" && installation.restartRequired ? "Ennodia update ready:" : `Installation ${installation.status}:`, ...installation.limitations.map(limit => `- ${limit}`));
|
|
1360
|
+
if (!installation.restartRequired || installation.status === "broken") lines.push("", `[Installation guide](${INSTALL_GUIDE_URL})`);
|
|
1361
1361
|
}
|
|
1362
1362
|
const rows = routes.map(route => {
|
|
1363
1363
|
const key = accessKey(route);
|
|
1364
|
-
return { name: "id" in route ? route.name : route.harnessName, model: route.readiness?.model, key, command: key === "login-required" ? route.readiness?.recovery?.command ?? [] : [] };
|
|
1364
|
+
return { name: "id" in route ? route.name : route.harnessName, model: route.readiness?.model, message: route.readiness?.message, key, command: key === "login-required" ? route.readiness?.recovery?.command ?? [] : [] };
|
|
1365
1365
|
}).sort((a, b) => accessRank(a.key) - accessRank(b.key));
|
|
1366
1366
|
if (rows.length) lines.push("", "| Agent | Status | Next step |", "| --- | --- | --- |");
|
|
1367
1367
|
else lines.push("", `No supported agents were found. Install one with the [harness guide](${HARNESS_GUIDE_URL}).`);
|
|
1368
1368
|
for (const row of rows) {
|
|
1369
|
-
const next = row.key === "missing" ? `${ACCESS_TEXT.missing.next} [Install guide](${HARNESS_GUIDE_URL})` : ACCESS_TEXT[row.key].next;
|
|
1369
|
+
const next = row.key === "missing" ? `${ACCESS_TEXT.missing.next} [Install guide](${HARNESS_GUIDE_URL})` : row.key === "unverified" && row.message ? row.message : ACCESS_TEXT[row.key].next;
|
|
1370
1370
|
lines.push(`| ${cell(row.name + (row.model ? ` · ${row.model}` : ""))} | ${ACCESS_TEXT[row.key].label} | ${cell(next)} |`);
|
|
1371
1371
|
}
|
|
1372
1372
|
// Only a login blocker calls for a command. Other blockers need credits or a new key.
|
package/src/setup-app.ts
CHANGED
|
@@ -21,16 +21,16 @@ export const ACCESS_TEXT: Record<AccessKey, { label: string; next: string }> = {
|
|
|
21
21
|
"quota-exhausted": { label: "Out of quota", next: "Restore credits or quota with the provider, then check again. Signing in does not help." },
|
|
22
22
|
"model-unavailable": { label: "Model unavailable", next: "This agent cannot use this model. Pick another from its model list." },
|
|
23
23
|
"setup-required": { label: "Needs setup", next: "Finish the agent's own setup, then check again." },
|
|
24
|
-
unreachable: { label: "
|
|
24
|
+
unreachable: { label: "Could not verify", next: "Model access is unknown. This does not confirm a sign-in or setup problem." },
|
|
25
25
|
updating: { label: "Updating", next: "An update is running. Check again when it finishes." },
|
|
26
|
-
unverified: { label: "Not
|
|
26
|
+
unverified: { label: "Not verified", next: "A task can run without a separate model check." },
|
|
27
27
|
ready: { label: "Ready", next: "" },
|
|
28
28
|
missing: { label: "Not available", next: "Not installed, or it cannot run without a terminal." },
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
/** Agents that need the user come first, unavailable ones last. */
|
|
32
32
|
export function accessRank(key: AccessKey): number {
|
|
33
|
-
return key === "unverified" ? 1 : key === "ready" ? 2 : key === "missing" ? 3 : 0;
|
|
33
|
+
return key === "unverified" || key === "unreachable" ? 1 : key === "ready" ? 2 : key === "missing" ? 3 : 0;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export const HARNESS_GUIDE_URL = "https://ennodia.cherninlab.com/docs/reference/supported-harnesses/";
|
|
@@ -88,7 +88,7 @@ const SETUP_APP_SCRIPT = String.raw`
|
|
|
88
88
|
function normalize(h) {
|
|
89
89
|
var r = h.readiness || {};
|
|
90
90
|
var key = h.runnable === false ? "missing" : (ACCESS[r.state] ? r.state : "unverified");
|
|
91
|
-
return { id: h.id, name: h.name || h.id, model: r.model || "", key: key,
|
|
91
|
+
return { id: h.id, name: h.name || h.id, model: r.model || "", message: r.message || "", key: key,
|
|
92
92
|
command: key === "login-required" && r.recovery && Array.isArray(r.recovery.command) ? r.recovery.command : [] };
|
|
93
93
|
}
|
|
94
94
|
function find(id) { for (var i = 0; i < state.agents.length; i++) if (state.agents[i].id === id) return state.agents[i]; return null; }
|
|
@@ -125,7 +125,7 @@ const SETUP_APP_SCRIPT = String.raw`
|
|
|
125
125
|
if (data.installation) state.installation = data.installation;
|
|
126
126
|
merge(data.harnesses);
|
|
127
127
|
var now = find(agent.id);
|
|
128
|
-
state.notes[agent.id] = now && now.key === "ready" ? "Ready now." :
|
|
128
|
+
state.notes[agent.id] = now && now.key === "ready" ? "Ready now." : "";
|
|
129
129
|
if (now && now.key === "ready" && before !== "ready") state.newlyReady[agent.id] = true;
|
|
130
130
|
}, function (error) { state.notes[agent.id] = error.message || "The check failed."; })
|
|
131
131
|
.then(function () { delete state.busy[agent.id]; render(); });
|
|
@@ -178,12 +178,12 @@ const SETUP_APP_SCRIPT = String.raw`
|
|
|
178
178
|
var head = el("div", "head");
|
|
179
179
|
head.appendChild(el("span", "dot"));
|
|
180
180
|
var name = el("span", "name", agent.name);
|
|
181
|
-
if (agent.model) name.appendChild(el("span", "model", agent.model));
|
|
181
|
+
if (agent.model) name.appendChild(el("span", "model", " · " + agent.model));
|
|
182
182
|
head.appendChild(name);
|
|
183
183
|
head.appendChild(el("span", "status", ACCESS[agent.key].label));
|
|
184
184
|
item.appendChild(head);
|
|
185
|
-
|
|
186
|
-
if (
|
|
185
|
+
var next = agent.key === "unverified" && agent.message ? agent.message : ACCESS[agent.key].next;
|
|
186
|
+
if (next) item.appendChild(el("p", "next", next));
|
|
187
187
|
if (agent.command.length) {
|
|
188
188
|
var row = el("div", "command");
|
|
189
189
|
var code = el("code", "", shellQuote(agent.command));
|
|
@@ -216,11 +216,11 @@ const SETUP_APP_SCRIPT = String.raw`
|
|
|
216
216
|
install.textContent = "";
|
|
217
217
|
install.hidden = !report || report.status === "healthy";
|
|
218
218
|
if (!install.hidden) {
|
|
219
|
-
install.appendChild(el("p", "", "Ennodia installation: " + report.status + "."));
|
|
219
|
+
install.appendChild(el("p", "", report.status !== "broken" && report.restartRequired ? "Ennodia update ready." : "Ennodia installation: " + report.status + "."));
|
|
220
220
|
var notes = el("ul");
|
|
221
221
|
(report.limitations || []).forEach(function (text) { notes.appendChild(el("li", "", text)); });
|
|
222
222
|
install.appendChild(notes);
|
|
223
|
-
install.appendChild(button("Installation guide", "", function () { openLink(__INSTALL__); }));
|
|
223
|
+
if (!report.restartRequired || report.status === "broken") install.appendChild(button("Installation guide", "", function () { openLink(__INSTALL__); }));
|
|
224
224
|
}
|
|
225
225
|
document.getElementById("cost").hidden = !state.host.serverTools || !state.agents.some(function (a) { return a.key !== "ready" && a.key !== "missing"; });
|
|
226
226
|
resize();
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ENNODIA_VERSION = "0.5.
|
|
1
|
+
export const ENNODIA_VERSION = "0.5.2";
|