agent-trellis 0.1.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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/dist/adapters/claude-code.d.ts +23 -0
  4. package/dist/adapters/claude-code.js +86 -0
  5. package/dist/adapters/codex.d.ts +27 -0
  6. package/dist/adapters/codex.js +119 -0
  7. package/dist/adapters/jsonMcp.d.ts +24 -0
  8. package/dist/adapters/jsonMcp.js +84 -0
  9. package/dist/adapters/kiro.d.ts +34 -0
  10. package/dist/adapters/kiro.js +175 -0
  11. package/dist/adapters/mcpPlan.d.ts +28 -0
  12. package/dist/adapters/mcpPlan.js +83 -0
  13. package/dist/adapters/pi.d.ts +23 -0
  14. package/dist/adapters/pi.js +108 -0
  15. package/dist/adapters/symlinkPlan.d.ts +33 -0
  16. package/dist/adapters/symlinkPlan.js +120 -0
  17. package/dist/cli.d.ts +7 -0
  18. package/dist/cli.js +135 -0
  19. package/dist/commands/doctor.d.ts +88 -0
  20. package/dist/commands/doctor.js +269 -0
  21. package/dist/commands/init.d.ts +44 -0
  22. package/dist/commands/init.js +150 -0
  23. package/dist/commands/mcp.d.ts +28 -0
  24. package/dist/commands/mcp.js +70 -0
  25. package/dist/commands/migrate.d.ts +38 -0
  26. package/dist/commands/migrate.js +132 -0
  27. package/dist/commands/onboard.d.ts +50 -0
  28. package/dist/commands/onboard.js +155 -0
  29. package/dist/commands/secretsAudit.d.ts +35 -0
  30. package/dist/commands/secretsAudit.js +115 -0
  31. package/dist/commands/sync.d.ts +40 -0
  32. package/dist/commands/sync.js +91 -0
  33. package/dist/core/adapter.d.ts +133 -0
  34. package/dist/core/adapter.js +16 -0
  35. package/dist/core/canonical.d.ts +16 -0
  36. package/dist/core/canonical.js +148 -0
  37. package/dist/core/types.d.ts +201 -0
  38. package/dist/core/types.js +15 -0
  39. package/dist/lib/dirEquals.d.ts +7 -0
  40. package/dist/lib/dirEquals.js +39 -0
  41. package/dist/lib/envVarNames.d.ts +35 -0
  42. package/dist/lib/envVarNames.js +79 -0
  43. package/dist/lib/fsIdentity.d.ts +16 -0
  44. package/dist/lib/fsIdentity.js +53 -0
  45. package/dist/lib/mcpProbe.d.ts +14 -0
  46. package/dist/lib/mcpProbe.js +96 -0
  47. package/dist/lib/probeCommon.d.ts +24 -0
  48. package/dist/lib/probeCommon.js +108 -0
  49. package/dist/lib/secretEnv.d.ts +19 -0
  50. package/dist/lib/secretEnv.js +46 -0
  51. package/dist/lib/skillFile.d.ts +12 -0
  52. package/dist/lib/skillFile.js +26 -0
  53. package/dist/lib/syncArgs.d.ts +16 -0
  54. package/dist/lib/syncArgs.js +17 -0
  55. package/dist/lib/tomlSection.d.ts +57 -0
  56. package/dist/lib/tomlSection.js +162 -0
  57. package/dist/pi-bridge/bundle.js +32074 -0
  58. package/dist/pi-bridge/index.d.ts +48 -0
  59. package/dist/pi-bridge/index.js +188 -0
  60. package/dist/pi-bridge/schemaTranslate.d.ts +55 -0
  61. package/dist/pi-bridge/schemaTranslate.js +40 -0
  62. package/dist/probes/claude-code.d.ts +13 -0
  63. package/dist/probes/claude-code.js +48 -0
  64. package/dist/probes/codex.d.ts +24 -0
  65. package/dist/probes/codex.js +78 -0
  66. package/dist/probes/kiro.d.ts +12 -0
  67. package/dist/probes/kiro.js +48 -0
  68. package/dist/probes/pi.d.ts +14 -0
  69. package/dist/probes/pi.js +53 -0
  70. package/dist/sdk.d.ts +14 -0
  71. package/dist/sdk.js +13 -0
  72. package/docs/architecture.md +367 -0
  73. package/docs/getting-started.md +235 -0
  74. package/docs/implementation-plan.md +341 -0
  75. package/docs/research.md +175 -0
  76. package/docs/roadmap.md +484 -0
  77. package/package.json +59 -0
  78. package/schema/scope.example.yaml +33 -0
  79. package/schema/secrets.policy.example.yaml +43 -0
  80. package/schema/servers.example.yaml +87 -0
package/dist/cli.js ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point. Stays a thin dispatcher; real logic lives in
4
+ * src/commands/*.ts so it stays testable without going through argv. See
5
+ * docs/roadmap.md for what's implemented.
6
+ */
7
+ import { runDoctor } from "./commands/doctor.js";
8
+ import { runInit } from "./commands/init.js";
9
+ import { runMigrate } from "./commands/migrate.js";
10
+ import { runOnboard } from "./commands/onboard.js";
11
+ import { runSync } from "./commands/sync.js";
12
+ import { runMcpSync } from "./commands/mcp.js";
13
+ import { runSecretsAudit } from "./commands/secretsAudit.js";
14
+ import { parseSyncArgs } from "./lib/syncArgs.js";
15
+ const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "secrets"];
16
+ function printUsage() {
17
+ console.log(`trellis - a single source of capability for every coding agent
18
+
19
+ Usage:
20
+ trellis <command>
21
+
22
+ Commands:
23
+ onboard Guided flow: init -> detect agents -> pick a base agent ->
24
+ migrate -> sync, in one command
25
+ --agent <agent> non-interactive base-agent choice
26
+ (required with 2+ agents present and
27
+ no terminal to prompt in, e.g. --json)
28
+ --dry-run preview the whole flow, write nothing
29
+ --json machine-readable output, no report text
30
+ init Create ~/.trellis/ with a minimal valid skeleton if missing
31
+ (never overwrites an existing file — fills in only what's
32
+ missing) and prints which agents are present
33
+ --json machine-readable output, no report text
34
+ migrate --from <agent>
35
+ Import an existing agent's real skills/instructions into
36
+ canonical source (claude-code | codex | kiro | pi). Never
37
+ overwrites differing content — reports a conflict instead.
38
+ --dry-run preview the plan, write nothing
39
+ --json machine-readable output, no report text
40
+ doctor Scan Claude Code / Codex / Kiro / pi for drift
41
+ --json machine-readable output, no table text
42
+ --probe-mcp also handshake every configured MCP server
43
+ (off by default — spawns real processes,
44
+ some reaching real external services)
45
+ sync [skills|instructions]
46
+ Distribute skills/instructions to each agent (omit target for both)
47
+ --dry-run preview the plan, write nothing
48
+ --json machine-readable output, no report text
49
+ mcp sync Distribute MCP servers to each agent's native config
50
+ (create/repair only — no automatic removal, see docs/roadmap.md)
51
+ --json machine-readable output, no report text
52
+ secrets audit
53
+ Scan each present agent's real MCP config for leaked
54
+ credentials and unexpected env var names
55
+ --json machine-readable output, no report text
56
+
57
+ See docs/roadmap.md for what's built vs. planned.`);
58
+ }
59
+ async function main(argv) {
60
+ const [command, ...rest] = argv;
61
+ if (!command || command === "--help" || command === "-h") {
62
+ printUsage();
63
+ return;
64
+ }
65
+ if (!KNOWN_COMMANDS.includes(command)) {
66
+ console.error(`Unknown command: ${command}\n`);
67
+ printUsage();
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+ if (command === "onboard") {
72
+ const agentIndex = rest.indexOf("--agent");
73
+ const agent = agentIndex >= 0 ? rest[agentIndex + 1] : undefined;
74
+ const { exitCode } = await runOnboard({ agent, dryRun: rest.includes("--dry-run"), json: rest.includes("--json") });
75
+ process.exitCode = exitCode;
76
+ return;
77
+ }
78
+ if (command === "init") {
79
+ const { exitCode } = await runInit({ json: rest.includes("--json") });
80
+ process.exitCode = exitCode;
81
+ return;
82
+ }
83
+ if (command === "migrate") {
84
+ const fromIndex = rest.indexOf("--from");
85
+ const from = fromIndex >= 0 ? rest[fromIndex + 1] : undefined;
86
+ const { exitCode } = await runMigrate({ from, dryRun: rest.includes("--dry-run"), json: rest.includes("--json") });
87
+ process.exitCode = exitCode;
88
+ return;
89
+ }
90
+ if (command === "doctor") {
91
+ const { exitCode } = await runDoctor({ json: rest.includes("--json"), probeMcp: rest.includes("--probe-mcp") });
92
+ process.exitCode = exitCode;
93
+ return;
94
+ }
95
+ if (command === "sync") {
96
+ const { target, unknownArg } = parseSyncArgs(rest);
97
+ if (unknownArg) {
98
+ console.error(`Unknown sync target: ${unknownArg}\n`);
99
+ printUsage();
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ const { exitCode } = await runSync({ target, json: rest.includes("--json"), dryRun: rest.includes("--dry-run") });
104
+ process.exitCode = exitCode;
105
+ return;
106
+ }
107
+ if (command === "mcp") {
108
+ const [subcommand] = rest;
109
+ if (subcommand !== "sync") {
110
+ console.error(`Unknown mcp subcommand: ${subcommand ?? "(none)"}\nUsage: trellis mcp sync\n`);
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+ const { exitCode } = await runMcpSync({ json: rest.includes("--json") });
115
+ process.exitCode = exitCode;
116
+ return;
117
+ }
118
+ if (command === "secrets") {
119
+ const [subcommand] = rest;
120
+ if (subcommand !== "audit") {
121
+ console.error(`Unknown secrets subcommand: ${subcommand ?? "(none)"}\nUsage: trellis secrets audit\n`);
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ const { exitCode } = await runSecretsAudit({ json: rest.includes("--json") });
126
+ process.exitCode = exitCode;
127
+ return;
128
+ }
129
+ console.error(`\`trellis ${command}\` is not implemented yet — this is a pre-alpha scaffold.\nSee docs/roadmap.md for status.`);
130
+ process.exitCode = 1;
131
+ }
132
+ main(process.argv.slice(2)).catch((err) => {
133
+ console.error(err instanceof Error ? err.stack : String(err));
134
+ process.exitCode = 1;
135
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `trellis doctor` — read-only cross-agent scan. No `.trellis/` canonical
3
+ * source required (docs/roadmap.md): this compares the four agents' own
4
+ * current state against each other, the same manual process used
5
+ * throughout docs/research.md's investigation, now formalized.
6
+ */
7
+ import type { AgentId, AgentSnapshot } from "../core/types.js";
8
+ /**
9
+ * Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
10
+ * real `known_host_injected` field (P0 has no canonical source at all —
11
+ * docs/roadmap.md). Matches `schema/servers.example.yaml`'s list, which
12
+ * documents mirasim's actual known connectors on this project's own
13
+ * machine.
14
+ */
15
+ export declare const DEFAULT_KNOWN_HOST_INJECTED: readonly string[];
16
+ export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "mcp-unreachable";
17
+ export interface Finding {
18
+ kind: FindingKind;
19
+ /** Omitted for a finding that names more than one agent (drift). */
20
+ agent?: AgentId;
21
+ message: string;
22
+ }
23
+ export interface RunDoctorOptions {
24
+ json?: boolean;
25
+ /**
26
+ * Explicit override, mainly for tests. Most callers should leave this
27
+ * unset and let `runDoctor` resolve it itself: canonical source's real
28
+ * `known_host_injected` when `~/.trellis/` exists, `DEFAULT_KNOWN_HOST_INJECTED`
29
+ * otherwise (trellis-cli-init) — the same list `mcp sync`'s own
30
+ * collision refusal already reads, so the two commands agree on one
31
+ * machine instead of each trusting a different source.
32
+ */
33
+ knownHostInjected?: readonly string[];
34
+ /**
35
+ * Off by default. Spawning every configured MCP server for a live
36
+ * handshake is not actually read-only in the sense that matters here:
37
+ * some servers reach real external services with real credentials
38
+ * (OAuth-backed connectors, chrome-devtools-mcp's `--autoConnect`,
39
+ * etc.), and probing all of them serially against a machine with a
40
+ * dozen-plus configured servers made a single run take minutes. Default
41
+ * `trellis doctor` sticks to structural checks (collision, duplication,
42
+ * drift, case) that need no process spawned at all; pass this to also
43
+ * attempt handshakes.
44
+ */
45
+ probeMcp?: boolean;
46
+ /** Defaults to the real `~`; overridable for tests only — same seam
47
+ * every other Trellis entry point uses. */
48
+ homeDir?: string;
49
+ }
50
+ export interface DoctorReport {
51
+ snapshots: AgentSnapshot[];
52
+ findings: Finding[];
53
+ }
54
+ /** Exported so tests can assert on this resolution directly, without
55
+ * routing through real per-agent probes `runDoctor` also runs. */
56
+ export declare function resolveKnownHostInjected(opts: RunDoctorOptions): readonly string[];
57
+ export declare function runDoctor(opts?: RunDoctorOptions): Promise<{
58
+ exitCode: number;
59
+ }>;
60
+ /** Exported separately from `runDoctor` so tests can assert on findings
61
+ * without capturing stdout. */
62
+ export declare function collectDoctorReport(knownHostInjected?: readonly string[], probeMcp?: boolean): Promise<DoctorReport>;
63
+ /**
64
+ * Within one agent, across all of its skill roots: any realpath reached by
65
+ * more than one entry, where at least one of those entries is NOT a
66
+ * symlink, is a physical duplicate — design.md D3 / the Codex `openspec-*`
67
+ * incident this check exists to catch again if it recurs. Two symlinks
68
+ * sharing a realpath is the expected, already-deduplicated case: no finding.
69
+ */
70
+ export declare function detectDuplication(snapshots: AgentSnapshot[]): Finding[];
71
+ /**
72
+ * A statically configured server name that also appears in
73
+ * `known_host_injected` — see docs/research.md "Codex — three hard
74
+ * constraints" #3 for why this is worth catching before it happens rather
75
+ * than after.
76
+ */
77
+ export declare function detectCollisions(snapshots: AgentSnapshot[], knownHostInjected: readonly string[]): Finding[];
78
+ /**
79
+ * Same skill name, different realpath, across two or more DISTINCT
80
+ * agents. An agent reporting the same name from two of its own roots is a
81
+ * separate case (already covered by `detectDuplication` when the
82
+ * realpaths match, and not yet a specified finding class when they don't)
83
+ * — deliberately not conflated with cross-agent drift here.
84
+ */
85
+ export declare function detectCrossAgentDrift(snapshots: AgentSnapshot[]): Finding[];
86
+ export declare function detectCaseMismatches(snapshots: AgentSnapshot[]): Finding[];
87
+ export declare function detectParseDiagnostics(snapshots: AgentSnapshot[]): Finding[];
88
+ export declare function detectMcpUnreachable(snapshots: AgentSnapshot[]): Finding[];
@@ -0,0 +1,269 @@
1
+ /**
2
+ * `trellis doctor` — read-only cross-agent scan. No `.trellis/` canonical
3
+ * source required (docs/roadmap.md): this compares the four agents' own
4
+ * current state against each other, the same manual process used
5
+ * throughout docs/research.md's investigation, now formalized.
6
+ */
7
+ import * as claudeCodeProbe from "../probes/claude-code.js";
8
+ import * as codexProbe from "../probes/codex.js";
9
+ import * as kiroProbe from "../probes/kiro.js";
10
+ import * as piProbe from "../probes/pi.js";
11
+ import { loadCanonicalSource } from "../core/canonical.js";
12
+ import { ALL_AGENTS } from "../core/types.js";
13
+ /**
14
+ * Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
15
+ * real `known_host_injected` field (P0 has no canonical source at all —
16
+ * docs/roadmap.md). Matches `schema/servers.example.yaml`'s list, which
17
+ * documents mirasim's actual known connectors on this project's own
18
+ * machine.
19
+ */
20
+ export const DEFAULT_KNOWN_HOST_INJECTED = ["atlassian", "sentry", "memory", "mirasim"];
21
+ /**
22
+ * Every kind except "mcp-unreachable" fails the run per spec (a specific
23
+ * server not answering its handshake is real information, worth showing,
24
+ * but isn't itself drift/duplication/collision/an incomplete probe — see
25
+ * specs/capability-drift-detection/spec.md's "Non-zero exit" requirement).
26
+ */
27
+ const EXIT_NONZERO_KINDS = new Set([
28
+ "probe-failed",
29
+ "duplication",
30
+ "collision",
31
+ "drift",
32
+ "case-mismatch",
33
+ "parse-diagnostic",
34
+ ]);
35
+ /** Exported so tests can assert on this resolution directly, without
36
+ * routing through real per-agent probes `runDoctor` also runs. */
37
+ export function resolveKnownHostInjected(opts) {
38
+ if (opts.knownHostInjected)
39
+ return opts.knownHostInjected;
40
+ try {
41
+ const canonical = loadCanonicalSource(opts.homeDir);
42
+ return canonical.mcp.knownHostInjected;
43
+ }
44
+ catch {
45
+ // No canonical source — doctor must still work standalone (P0).
46
+ return DEFAULT_KNOWN_HOST_INJECTED;
47
+ }
48
+ }
49
+ export async function runDoctor(opts = {}) {
50
+ const report = await collectDoctorReport(resolveKnownHostInjected(opts), opts.probeMcp);
51
+ const exitCode = report.findings.some((f) => EXIT_NONZERO_KINDS.has(f.kind)) ? 1 : 0;
52
+ if (opts.json) {
53
+ console.log(JSON.stringify(report, null, 2));
54
+ }
55
+ else {
56
+ printTable(report);
57
+ }
58
+ return { exitCode };
59
+ }
60
+ /** Exported separately from `runDoctor` so tests can assert on findings
61
+ * without capturing stdout. */
62
+ export async function collectDoctorReport(knownHostInjected = DEFAULT_KNOWN_HOST_INJECTED, probeMcp = false) {
63
+ const probeList = [
64
+ { agent: "claude-code", run: () => claudeCodeProbe.probe(undefined, { probeMcp }) },
65
+ { agent: "codex", run: () => codexProbe.probe(undefined, { probeMcp }) },
66
+ { agent: "kiro", run: () => kiroProbe.probe(undefined, { probeMcp }) },
67
+ { agent: "pi", run: piProbe.probe },
68
+ ];
69
+ // One agent erroring must not blank the other three's results.
70
+ const settled = await Promise.allSettled(probeList.map((p) => p.run()));
71
+ const snapshots = [];
72
+ const findings = [];
73
+ settled.forEach((result, index) => {
74
+ if (result.status === "fulfilled") {
75
+ snapshots.push(result.value);
76
+ }
77
+ else {
78
+ findings.push({
79
+ kind: "probe-failed",
80
+ agent: probeList[index].agent,
81
+ message: `probe failed: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`,
82
+ });
83
+ }
84
+ });
85
+ findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...detectMcpUnreachable(snapshots));
86
+ return { snapshots, findings };
87
+ }
88
+ /**
89
+ * Within one agent, across all of its skill roots: any realpath reached by
90
+ * more than one entry, where at least one of those entries is NOT a
91
+ * symlink, is a physical duplicate — design.md D3 / the Codex `openspec-*`
92
+ * incident this check exists to catch again if it recurs. Two symlinks
93
+ * sharing a realpath is the expected, already-deduplicated case: no finding.
94
+ */
95
+ export function detectDuplication(snapshots) {
96
+ const findings = [];
97
+ for (const snap of snapshots) {
98
+ if (!snap.present)
99
+ continue;
100
+ const byRealDir = new Map();
101
+ for (const root of snap.skillRoots) {
102
+ for (const skill of root.skills) {
103
+ const bucket = byRealDir.get(skill.realDir) ?? [];
104
+ bucket.push(skill);
105
+ byRealDir.set(skill.realDir, bucket);
106
+ }
107
+ }
108
+ for (const bucket of byRealDir.values()) {
109
+ if (bucket.length < 2)
110
+ continue;
111
+ const nonSymlinkEntries = bucket.filter((s) => !s.isSymlink);
112
+ for (const entry of nonSymlinkEntries) {
113
+ findings.push({
114
+ kind: "duplication",
115
+ agent: snap.agent,
116
+ message: `physical duplicate skill "${entry.name}" at ${entry.dir} — same content already reachable elsewhere; should be a symlink or removed`,
117
+ });
118
+ }
119
+ }
120
+ }
121
+ return findings;
122
+ }
123
+ const CODEX_STDIO_URL_CRASH_NOTE = ' On Codex specifically, this crashes the entire process at startup ("url is not supported for stdio"), not just this one server — see docs/research.md.';
124
+ /**
125
+ * A statically configured server name that also appears in
126
+ * `known_host_injected` — see docs/research.md "Codex — three hard
127
+ * constraints" #3 for why this is worth catching before it happens rather
128
+ * than after.
129
+ */
130
+ export function detectCollisions(snapshots, knownHostInjected) {
131
+ const findings = [];
132
+ const known = new Set(knownHostInjected);
133
+ for (const snap of snapshots) {
134
+ if (!snap.present)
135
+ continue;
136
+ for (const server of snap.mcpServers) {
137
+ if (known.has(server.name)) {
138
+ findings.push({
139
+ kind: "collision",
140
+ agent: snap.agent,
141
+ message: `MCP server "${server.name}" is statically configured but also appears in known_host_injected.${snap.agent === "codex" ? CODEX_STDIO_URL_CRASH_NOTE : ""}`,
142
+ });
143
+ }
144
+ }
145
+ }
146
+ return findings;
147
+ }
148
+ /**
149
+ * Same skill name, different realpath, across two or more DISTINCT
150
+ * agents. An agent reporting the same name from two of its own roots is a
151
+ * separate case (already covered by `detectDuplication` when the
152
+ * realpaths match, and not yet a specified finding class when they don't)
153
+ * — deliberately not conflated with cross-agent drift here.
154
+ */
155
+ export function detectCrossAgentDrift(snapshots) {
156
+ const findings = [];
157
+ const byName = new Map();
158
+ for (const snap of snapshots) {
159
+ if (!snap.present)
160
+ continue;
161
+ for (const root of snap.skillRoots) {
162
+ for (const skill of root.skills) {
163
+ let perAgent = byName.get(skill.name);
164
+ if (!perAgent) {
165
+ perAgent = new Map();
166
+ byName.set(skill.name, perAgent);
167
+ }
168
+ if (!perAgent.has(snap.agent)) {
169
+ perAgent.set(snap.agent, skill.realDir);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ for (const [name, perAgent] of byName) {
175
+ if (perAgent.size < 2)
176
+ continue;
177
+ if (new Set(perAgent.values()).size < 2)
178
+ continue;
179
+ const detail = [...perAgent.entries()].map(([agent, realDir]) => `${agent}=${realDir}`).join(", ");
180
+ findings.push({ kind: "drift", message: `skill "${name}" resolves to different sources across agents: ${detail}` });
181
+ }
182
+ return findings;
183
+ }
184
+ export function detectCaseMismatches(snapshots) {
185
+ const findings = [];
186
+ for (const snap of snapshots) {
187
+ if (!snap.present)
188
+ continue;
189
+ for (const root of snap.skillRoots) {
190
+ for (const skill of root.skills) {
191
+ if (!skill.caseCorrect) {
192
+ findings.push({
193
+ kind: "case-mismatch",
194
+ agent: snap.agent,
195
+ message: `skill "${skill.name}" at ${skill.dir} has a wrong-case entry file (expected exact "SKILL.md") — silently dropped from discovery on at least one target agent (Codex)`,
196
+ });
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return findings;
202
+ }
203
+ export function detectParseDiagnostics(snapshots) {
204
+ const findings = [];
205
+ for (const snap of snapshots) {
206
+ for (const message of snap.diagnostics) {
207
+ findings.push({ kind: "parse-diagnostic", agent: snap.agent, message });
208
+ }
209
+ }
210
+ return findings;
211
+ }
212
+ export function detectMcpUnreachable(snapshots) {
213
+ const findings = [];
214
+ for (const snap of snapshots) {
215
+ if (!snap.present)
216
+ continue;
217
+ for (const server of snap.mcpServers) {
218
+ if (server.probe && !server.probe.ok) {
219
+ findings.push({
220
+ kind: "mcp-unreachable",
221
+ agent: snap.agent,
222
+ message: `MCP server "${server.name}" did not respond to handshake: ${server.probe.error ?? "unknown error"}`,
223
+ });
224
+ }
225
+ }
226
+ }
227
+ return findings;
228
+ }
229
+ function printTable(report) {
230
+ const byAgent = new Map();
231
+ const crossAgent = [];
232
+ for (const finding of report.findings) {
233
+ if (finding.agent) {
234
+ const list = byAgent.get(finding.agent) ?? [];
235
+ list.push(finding);
236
+ byAgent.set(finding.agent, list);
237
+ }
238
+ else {
239
+ crossAgent.push(finding);
240
+ }
241
+ }
242
+ for (const agent of ALL_AGENTS) {
243
+ const snap = report.snapshots.find((s) => s.agent === agent);
244
+ if (!snap) {
245
+ console.log(`❌ ${agent} — probe did not complete`);
246
+ continue;
247
+ }
248
+ if (!snap.present) {
249
+ console.log(`— ${agent} (not installed)`);
250
+ continue;
251
+ }
252
+ const agentFindings = byAgent.get(agent) ?? [];
253
+ const label = `${agent}${snap.version ? ` (${snap.version})` : ""}`;
254
+ if (agentFindings.length === 0) {
255
+ console.log(`✅ ${label}`);
256
+ continue;
257
+ }
258
+ console.log(`⚠️ ${label} — ${agentFindings.length} finding(s)`);
259
+ for (const finding of agentFindings) {
260
+ console.log(` - ${finding.message}`);
261
+ }
262
+ }
263
+ if (crossAgent.length > 0) {
264
+ console.log("\nCross-agent:");
265
+ for (const finding of crossAgent) {
266
+ console.log(`⚠️ ${finding.message}`);
267
+ }
268
+ }
269
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `trellis init` — bootstraps a minimal, valid `~/.trellis/` if one
3
+ * doesn't already exist, per-file (never overwrites an existing file;
4
+ * fills in only what's missing — trellis-cli-init). `loadCanonicalSource`
5
+ * only actually requires the `.trellis/` directory itself to exist; this
6
+ * command exists because an empty directory gives a new user nothing to
7
+ * act on.
8
+ */
9
+ import type { AgentId } from "../core/types.js";
10
+ export interface RunInitOptions {
11
+ json?: boolean;
12
+ /** Defaults to the real `~`; overridable for tests only — same seam
13
+ * every other Trellis entry point uses. */
14
+ homeDir?: string;
15
+ }
16
+ export interface InitFileResult {
17
+ path: string;
18
+ action: "create" | "already-present";
19
+ }
20
+ export interface AgentPointer {
21
+ agent: AgentId;
22
+ present: boolean;
23
+ message: string;
24
+ }
25
+ export interface InitReport {
26
+ files: InitFileResult[];
27
+ agents: AgentPointer[];
28
+ }
29
+ /** Exported so `migrate` can byte-compare canonical `agents.md` against
30
+ * this exact string (design.md D3, trellis-cli-migrate) rather than
31
+ * retyping an equivalent one that could silently drift from this. */
32
+ export declare const AGENTS_MD_TEMPLATE = "# Shared instructions\n\nWrite what every agent should know here \u2014 communication preferences,\nproject conventions, anything you'd otherwise repeat per agent.\n\nAlready using Claude Code, Codex, Kiro, or pi? `trellis migrate --from\n<agent>` imports its real instructions and skills instead of starting\nfrom this placeholder.\n";
33
+ /**
34
+ * Trellis never spawns an installer itself (global package installs are
35
+ * exactly the kind of irreversible, system-wide action that needs the
36
+ * user's own explicit "yes", not a silent trellis-init side effect) —
37
+ * these are printed as a pointer only. Verified against each project's
38
+ * own current install docs, same discipline as everything else here.
39
+ */
40
+ export declare const INSTALL_HINTS: Record<AgentId, string>;
41
+ export declare function collectInitReport(homeDir?: string): Promise<InitReport>;
42
+ export declare function runInit(opts?: RunInitOptions): Promise<{
43
+ exitCode: number;
44
+ }>;