agent-trellis 0.15.1 → 0.16.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.
|
@@ -15,7 +15,7 @@ import { type WorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
|
15
15
|
* machine.
|
|
16
16
|
*/
|
|
17
17
|
export declare const DEFAULT_KNOWN_HOST_INJECTED: readonly string[];
|
|
18
|
-
export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "runtime-drift" | "oauth-expired" | "mcp-unreachable";
|
|
18
|
+
export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "runtime-drift" | "oauth-expired" | "mcp-unreachable" | "workspace-toml-invalid";
|
|
19
19
|
export interface Finding {
|
|
20
20
|
kind: FindingKind;
|
|
21
21
|
/** Omitted for a finding that names more than one agent (drift). */
|
package/dist/commands/doctor.js
CHANGED
|
@@ -44,6 +44,10 @@ const EXIT_NONZERO_KINDS = new Set([
|
|
|
44
44
|
// exists to catch *before* the session does (trellis-mcp-oauth-renew
|
|
45
45
|
// design.md D4).
|
|
46
46
|
"oauth-expired",
|
|
47
|
+
// A project-level Codex config that fails TOML parsing will fail the
|
|
48
|
+
// *agent*'s own startup regardless of ownership — catch it here before
|
|
49
|
+
// the next session does (trellis-doctor-workspace-toml-validation).
|
|
50
|
+
"workspace-toml-invalid",
|
|
47
51
|
]);
|
|
48
52
|
/** Exported so tests can assert on this resolution directly, without
|
|
49
53
|
* routing through real per-agent probes `runDoctor` also runs. */
|
|
@@ -120,7 +124,53 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
|
|
|
120
124
|
// canonical has not been initialized yet.
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
|
-
|
|
127
|
+
// Workspace attribution runs whenever a workspace root is in scope —
|
|
128
|
+
// even when `~/.trellis/` itself failed to load or has never been
|
|
129
|
+
// initialized. The spec's third scenario: a user managing only project
|
|
130
|
+
// skills in `.agents/skills/` without a global Trellis install still
|
|
131
|
+
// gets project-level config validation
|
|
132
|
+
// (trellis-doctor-workspace-toml-validation design.md D5). When the
|
|
133
|
+
// canonical source is absent we synthesize the minimal workspace layer
|
|
134
|
+
// with every map empty — provenance is moot without a global layer, but
|
|
135
|
+
// `collectWorkspaceDoctorSection`'s per-file inspection is what we want.
|
|
136
|
+
let workspace;
|
|
137
|
+
if (canonical?.workspace) {
|
|
138
|
+
workspace = collectWorkspaceDoctorSection(canonical);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const workspaceRoot = workspaceDir != null ? findWorkspaceRoot(workspaceDir) : null;
|
|
142
|
+
if (workspaceRoot) {
|
|
143
|
+
// `managedAgents` is driven by `~/.trellis/managed.yaml`, which is
|
|
144
|
+
// absent in this branch — sweep every known agent so codex's TOML
|
|
145
|
+
// file is still inspected (otherwise an empty list would skip the
|
|
146
|
+
// file entirely and the spec's scenario-3 finding would never fire).
|
|
147
|
+
const minimal = {
|
|
148
|
+
instructionsFile: "",
|
|
149
|
+
managedAgents: ALL_AGENTS,
|
|
150
|
+
skills: [],
|
|
151
|
+
agents: [],
|
|
152
|
+
memories: [],
|
|
153
|
+
mcp: { servers: {}, knownHostInjected: [] },
|
|
154
|
+
secretsPolicy: { allowedVars: [], rejectPatterns: [] },
|
|
155
|
+
diagnostics: [],
|
|
156
|
+
workspace: { root: workspaceRoot, mcpProvenance: {}, mcpGlobalServers: {}, skillProvenance: {}, globalSkills: [], shadowed: [] },
|
|
157
|
+
};
|
|
158
|
+
workspace = collectWorkspaceDoctorSection(minimal);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Workspace TOML findings become top-level findings so the exit code
|
|
162
|
+
// and `--json` consumers see them alongside cross-agent findings, not
|
|
163
|
+
// only inside the workspace section. They keep `agent: "codex"` — the
|
|
164
|
+
// file layout tells us which agent the file breaks.
|
|
165
|
+
if (workspace) {
|
|
166
|
+
for (const finding of workspace.tomlFindings) {
|
|
167
|
+
findings.push({
|
|
168
|
+
kind: "workspace-toml-invalid",
|
|
169
|
+
agent: finding.agent,
|
|
170
|
+
message: `${finding.path}${finding.line !== undefined ? `:${finding.line}${finding.column !== undefined ? `:${finding.column}` : ""}` : ""} — ${finding.reason} (not rendered by Trellis; fix by hand)`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
124
174
|
const oauth = canonical ? collectOAuthHealth(homeDir, canonical) : [];
|
|
125
175
|
findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots), ...oauth.flatMap((entry) => (entry.status === "valid" ? [] : [oauthFinding(entry)])));
|
|
126
176
|
return { snapshots, findings, ...(oauth.length > 0 ? { oauth } : {}), ...(workspace ? { workspace } : {}) };
|
|
@@ -395,6 +445,10 @@ export function printReport(report) {
|
|
|
395
445
|
for (const hint of ws.unmigrated) {
|
|
396
446
|
console.log(` ⚠ ${hint.path} holds ${hint.entries.length} entr${hint.entries.length === 1 ? "y" : "ies"} Trellis didn't render (${hint.entries.join(", ")}) — run \`trellis migrate --workspace\``);
|
|
397
447
|
}
|
|
448
|
+
for (const finding of ws.tomlFindings) {
|
|
449
|
+
const location = finding.line !== undefined ? `:${finding.line}${finding.column !== undefined ? `:${finding.column}` : ""}` : "";
|
|
450
|
+
console.log(` ⚠ ${finding.path}${location} — ${finding.reason} (not rendered by Trellis — fix by hand and re-run \`trellis doctor\`)`);
|
|
451
|
+
}
|
|
398
452
|
for (const warning of ws.precedenceWarnings) {
|
|
399
453
|
console.log(` ⚠ ${warning}`);
|
|
400
454
|
}
|
|
@@ -20,6 +20,19 @@ export interface WorkspaceUnmigratedHint {
|
|
|
20
20
|
/** Entry names present in the file with no Trellis manifest record. */
|
|
21
21
|
entries: string[];
|
|
22
22
|
}
|
|
23
|
+
/** One parse-level failure when inspecting an agent's project MCP file.
|
|
24
|
+
* Distinct from `parse-diagnostic` (agent-global probes). Even when Trellis
|
|
25
|
+
* has never rendered into this file, a project file that breaks the agent's
|
|
26
|
+
* own startup is exactly what doctor exists to surface
|
|
27
|
+
* (trellis-doctor-workspace-toml-validation). */
|
|
28
|
+
export interface WorkspaceTomlFinding {
|
|
29
|
+
agent: AgentId;
|
|
30
|
+
path: string;
|
|
31
|
+
/** Parser's 1-based line and column of the failure, when known. */
|
|
32
|
+
line?: number;
|
|
33
|
+
column?: number;
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
23
36
|
export interface WorkspaceDoctorSection {
|
|
24
37
|
root: string;
|
|
25
38
|
mcp: WorkspaceLayerEntry[];
|
|
@@ -38,6 +51,8 @@ export interface WorkspaceDoctorSection {
|
|
|
38
51
|
name: string;
|
|
39
52
|
}[];
|
|
40
53
|
unmigrated: WorkspaceUnmigratedHint[];
|
|
54
|
+
/** Parse-level failures on workspace agent MCP files (usually empty). */
|
|
55
|
+
tomlFindings: WorkspaceTomlFinding[];
|
|
41
56
|
/** zcode's user-level-first deviation notices (empty otherwise). */
|
|
42
57
|
precedenceWarnings: string[];
|
|
43
58
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* from the already-loaded canonical source and file reads only.
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { parse as parseToml } from "smol-toml";
|
|
13
14
|
import { resolveSecretEnv } from "./secretEnv.js";
|
|
14
15
|
import { loadWorkspaceRendered, workspaceMcpConfigPath, workspacePrecedenceWarnings } from "./workspaceMcp.js";
|
|
15
16
|
const VAR_REF_RE = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
@@ -36,15 +37,36 @@ function collectVarRefs(canonical) {
|
|
|
36
37
|
return refs;
|
|
37
38
|
}
|
|
38
39
|
/** Entry names a JSON `mcpServers` file / Codex TOML / zcode nested
|
|
39
|
-
* config holds beyond what the in-repo manifest records Trellis
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* config holds beyond what the in-repo manifest records Trellis rendering.
|
|
41
|
+
*
|
|
42
|
+
* The Codex branch TOML-parses the file first — syntactically invalid TOML
|
|
43
|
+
* is indistinguishable from valid TOML to the section-header regex, but is
|
|
44
|
+
* fatal to Codex's own startup, so it is surfaced as `invalid` and never
|
|
45
|
+
* turned into an unmigrated hint. The parsed AST is discarded on success:
|
|
46
|
+
* header extraction keeps using the existing regex to avoid changing edge
|
|
47
|
+
* semantics (dotted quoted keys, single-quoted keys) that the regex was
|
|
48
|
+
* already validated against — smol-toml is used parse-only, never
|
|
49
|
+
* round-tripped, respecting lib/tomlSection.ts's write-path constraint. */
|
|
42
50
|
function unmarkedEntries(agentId, configPath, recorded) {
|
|
43
51
|
if (!existsSync(configPath))
|
|
44
|
-
return [];
|
|
52
|
+
return { kind: "ok", names: [] };
|
|
45
53
|
let names = [];
|
|
46
54
|
if (agentId === "codex") {
|
|
47
55
|
const content = readFileSync(configPath, "utf-8");
|
|
56
|
+
try {
|
|
57
|
+
parseToml(content);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const tomLError = err;
|
|
61
|
+
const reason = tomLError.message?.split("\n")[0] ?? String(err);
|
|
62
|
+
return {
|
|
63
|
+
kind: "invalid",
|
|
64
|
+
path: configPath,
|
|
65
|
+
...(typeof tomLError.line === "number" ? { line: tomLError.line } : {}),
|
|
66
|
+
...(typeof tomLError.column === "number" ? { column: tomLError.column + 1 } : {}),
|
|
67
|
+
reason,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
48
70
|
const re = /^\[mcp_servers\.([^\]]+)\]\s*$/gm;
|
|
49
71
|
let match;
|
|
50
72
|
while ((match = re.exec(content)) !== null)
|
|
@@ -59,10 +81,10 @@ function unmarkedEntries(agentId, configPath, recorded) {
|
|
|
59
81
|
names = Object.keys(servers);
|
|
60
82
|
}
|
|
61
83
|
catch {
|
|
62
|
-
return []; // unparsable file is the sync guard's finding, not doctor's
|
|
84
|
+
return { kind: "ok", names: [] }; // unparsable file is the sync guard's finding, not doctor's
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
|
-
return names.filter((name) => recorded[name] === undefined);
|
|
87
|
+
return { kind: "ok", names: names.filter((name) => recorded[name] === undefined) };
|
|
66
88
|
}
|
|
67
89
|
export function collectWorkspaceDoctorSection(canonical) {
|
|
68
90
|
const workspace = canonical.workspace;
|
|
@@ -83,12 +105,17 @@ export function collectWorkspaceDoctorSection(canonical) {
|
|
|
83
105
|
const unresolvedVars = refs.filter((r) => resolved[r.name] === undefined);
|
|
84
106
|
const manifest = loadWorkspaceRendered(workspace.root);
|
|
85
107
|
const unmigrated = [];
|
|
108
|
+
const tomlFindings = [];
|
|
86
109
|
for (const agentId of canonical.managedAgents) {
|
|
87
110
|
const configPath = workspaceMcpConfigPath(agentId, workspace.root);
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
111
|
+
const result = unmarkedEntries(agentId, configPath, manifest.mcp?.[agentId] ?? {});
|
|
112
|
+
if (result.kind === "invalid") {
|
|
113
|
+
tomlFindings.push({ agent: agentId, path: result.path, ...(result.line !== undefined ? { line: result.line } : {}), ...(result.column !== undefined ? { column: result.column } : {}), reason: result.reason });
|
|
114
|
+
continue; // never report an unparseable file's entries as unmigrated — spec req 2
|
|
115
|
+
}
|
|
116
|
+
if (result.names.length > 0)
|
|
117
|
+
unmigrated.push({ agent: agentId, path: configPath, entries: result.names });
|
|
91
118
|
}
|
|
92
119
|
const precedenceWarnings = canonical.managedAgents.flatMap((id) => workspacePrecedenceWarnings(id, canonical));
|
|
93
|
-
return { root: workspace.root, mcp, skills, shadowed: workspace.shadowed, tombstoned, unresolvedVars, unmigrated, precedenceWarnings };
|
|
120
|
+
return { root: workspace.root, mcp, skills, shadowed: workspace.shadowed, tombstoned, unresolvedVars, unmigrated, tomlFindings, precedenceWarnings };
|
|
94
121
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-trellis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "A unified Code Agent Runtime for Skills, MCP, Memory, and shared instructions across Claude Code, Codex, Kiro, pi, Kimi Code, and ZCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Paul Leo",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
66
66
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
67
|
+
"smol-toml": "^1.9.1",
|
|
67
68
|
"yaml": "^2.9.1",
|
|
68
69
|
"zod": "^4.6.5"
|
|
69
70
|
}
|