agent-trellis 0.7.0 → 0.8.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 +48 -153
- package/dist/adapters/claude-code.js +3 -2
- package/dist/adapters/codex.js +3 -2
- package/dist/adapters/jsonMcp.d.ts +3 -1
- package/dist/adapters/jsonMcp.js +18 -4
- package/dist/adapters/kimi-code.d.ts +21 -0
- package/dist/adapters/kimi-code.js +107 -0
- package/dist/adapters/kiro.js +3 -2
- package/dist/adapters/mcpPlan.d.ts +6 -1
- package/dist/adapters/mcpPlan.js +106 -8
- package/dist/adapters/pi.js +22 -3
- package/dist/adapters/symlinkPlan.d.ts +7 -0
- package/dist/adapters/symlinkPlan.js +16 -7
- package/dist/adapters/zcode.d.ts +21 -0
- package/dist/adapters/zcode.js +234 -0
- package/dist/cli.js +145 -29
- package/dist/commands/doctor.d.ts +7 -2
- package/dist/commands/doctor.js +40 -2
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +29 -13
- package/dist/commands/kimi.d.ts +16 -0
- package/dist/commands/kimi.js +64 -0
- package/dist/commands/manage.d.ts +39 -0
- package/dist/commands/manage.js +126 -0
- package/dist/commands/mcp.d.ts +77 -14
- package/dist/commands/mcp.js +169 -29
- package/dist/commands/mcpGateway.d.ts +6 -0
- package/dist/commands/mcpGateway.js +35 -14
- package/dist/commands/mcpImport.d.ts +46 -0
- package/dist/commands/mcpImport.js +359 -0
- package/dist/commands/memory.d.ts +17 -3
- package/dist/commands/memory.js +27 -8
- package/dist/commands/migrate.d.ts +16 -5
- package/dist/commands/migrate.js +93 -17
- package/dist/commands/onboard.d.ts +74 -2
- package/dist/commands/onboard.js +469 -71
- package/dist/commands/rollback.js +28 -2
- package/dist/commands/secretsAudit.js +4 -1
- package/dist/commands/skill.d.ts +52 -6
- package/dist/commands/skill.js +114 -15
- package/dist/commands/sync.js +5 -1
- package/dist/core/adapter.d.ts +11 -1
- package/dist/core/canonical.d.ts +15 -7
- package/dist/core/canonical.js +101 -13
- package/dist/core/capabilitySelection.d.ts +45 -0
- package/dist/core/capabilitySelection.js +124 -0
- package/dist/core/types.d.ts +25 -2
- package/dist/core/types.js +8 -0
- package/dist/lib/agentBridge.d.ts +169 -0
- package/dist/lib/agentBridge.js +367 -0
- package/dist/lib/agentBridgeProvider.d.ts +14 -0
- package/dist/lib/agentBridgeProvider.js +95 -0
- package/dist/lib/backup.d.ts +32 -0
- package/dist/lib/backup.js +20 -1
- package/dist/lib/builtinSkills.d.ts +2 -0
- package/dist/lib/builtinSkills.js +4 -0
- package/dist/lib/chatAgents.d.ts +42 -0
- package/dist/lib/chatAgents.js +59 -0
- package/dist/lib/cliMetadata.d.ts +1 -0
- package/dist/lib/cliMetadata.js +14 -0
- package/dist/lib/envVarNames.d.ts +3 -0
- package/dist/lib/envVarNames.js +19 -0
- package/dist/lib/gatewayBackend.d.ts +28 -0
- package/dist/lib/gatewayBackend.js +72 -1
- package/dist/lib/installAgent.d.ts +7 -5
- package/dist/lib/installAgent.js +4 -3
- package/dist/lib/mcpConnect.js +5 -1
- package/dist/lib/mcpMigrateRead.d.ts +2 -0
- package/dist/lib/mcpMigrateRead.js +22 -1
- package/dist/lib/mcpRuntime.d.ts +46 -0
- package/dist/lib/mcpRuntime.js +185 -0
- package/dist/lib/mcpStatusProvider.d.ts +10 -0
- package/dist/lib/mcpStatusProvider.js +25 -0
- package/dist/lib/mcpToolRegistry.d.ts +18 -0
- package/dist/lib/mcpToolRegistry.js +107 -14
- package/dist/lib/memoryGraph.d.ts +1 -1
- package/dist/lib/memoryGraph.js +9 -3
- package/dist/lib/memoryProvider.d.ts +58 -0
- package/dist/lib/memoryProvider.js +175 -0
- package/dist/lib/memoryStatus.d.ts +17 -0
- package/dist/lib/memoryStatus.js +88 -0
- package/dist/lib/nativeMemory.d.ts +19 -0
- package/dist/lib/nativeMemory.js +129 -0
- package/dist/lib/realHomeSnapshot.js +5 -0
- package/dist/lib/runtimeControlProvider.d.ts +16 -0
- package/dist/lib/runtimeControlProvider.js +170 -0
- package/dist/lib/secretEnv.d.ts +2 -1
- package/dist/lib/secretEnv.js +6 -2
- package/dist/lib/skillProvider.d.ts +13 -0
- package/dist/lib/skillProvider.js +210 -0
- package/dist/lib/taskProvider.d.ts +7 -0
- package/dist/lib/taskProvider.js +43 -0
- package/dist/lib/taskStore.d.ts +46 -0
- package/dist/lib/taskStore.js +156 -0
- package/dist/lib/terminalPicker.d.ts +23 -49
- package/dist/lib/terminalPicker.js +80 -218
- package/dist/lib/zcodeOwnership.d.ts +17 -0
- package/dist/lib/zcodeOwnership.js +37 -0
- package/dist/pi-bridge/bundle.js +476 -37
- package/dist/pi-bridge/index.js +42 -21
- package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
- package/dist/pi-bridge/schemaTranslate.js +4 -8
- package/dist/probes/codex.d.ts +7 -0
- package/dist/probes/codex.js +37 -5
- package/dist/probes/kimi-code.d.ts +12 -0
- package/dist/probes/kimi-code.js +55 -0
- package/dist/probes/zcode.d.ts +32 -0
- package/dist/probes/zcode.js +140 -0
- package/dist/sdk.d.ts +1 -1
- package/docs/architecture.md +230 -28
- package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
- package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
- package/docs/assets/poster-series-v3/README.md +60 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
- package/docs/getting-started.md +496 -42
- package/docs/overview.md +134 -0
- package/docs/overview.zh-CN.md +102 -0
- package/docs/research.md +59 -0
- package/docs/roadmap.md +11 -6
- package/docs/video/README.md +76 -0
- package/docs/video/build/audio/01-opening.aiff +0 -0
- package/docs/video/build/audio/01-opening.mp3 +0 -0
- package/docs/video/build/audio/02-pain.aiff +0 -0
- package/docs/video/build/audio/02-pain.mp3 +0 -0
- package/docs/video/build/audio/03-solution.aiff +0 -0
- package/docs/video/build/audio/03-solution.mp3 +0 -0
- package/docs/video/build/audio/04-install.aiff +0 -0
- package/docs/video/build/audio/04-install.mp3 +0 -0
- package/docs/video/build/audio/04-setup.mp3 +0 -0
- package/docs/video/build/audio/05-dryrun.mp3 +0 -0
- package/docs/video/build/audio/05-init.aiff +0 -0
- package/docs/video/build/audio/05-init.mp3 +0 -0
- package/docs/video/build/audio/05-interactive.mp3 +0 -0
- package/docs/video/build/audio/06-dryrun.aiff +0 -0
- package/docs/video/build/audio/06-dryrun.mp3 +0 -0
- package/docs/video/build/audio/06-onboard.mp3 +0 -0
- package/docs/video/build/audio/06-run.mp3 +0 -0
- package/docs/video/build/audio/07-onboard.aiff +0 -0
- package/docs/video/build/audio/07-onboard.mp3 +0 -0
- package/docs/video/build/audio/07-verify.mp3 +0 -0
- package/docs/video/build/audio/08-closing.mp3 +0 -0
- package/docs/video/build/audio/08-doctor.aiff +0 -0
- package/docs/video/build/audio/08-doctor.mp3 +0 -0
- package/docs/video/build/audio/09-rollback.aiff +0 -0
- package/docs/video/build/audio/09-rollback.mp3 +0 -0
- package/docs/video/build/audio/10-principles.aiff +0 -0
- package/docs/video/build/audio/10-principles.mp3 +0 -0
- package/docs/video/build/audio/11-closing.aiff +0 -0
- package/docs/video/build/audio/11-closing.mp3 +0 -0
- package/docs/video/build/frames/01-opening-0.png +0 -0
- package/docs/video/build/frames/02-pain-1.png +0 -0
- package/docs/video/build/frames/02-pain-2.png +0 -0
- package/docs/video/build/frames/03-solution-3.png +0 -0
- package/docs/video/build/frames/04-install-4.png +0 -0
- package/docs/video/build/frames/04-setup-4.png +0 -0
- package/docs/video/build/frames/04-setup-5.png +0 -0
- package/docs/video/build/frames/05-dryrun-6.png +0 -0
- package/docs/video/build/frames/05-init-5.png +0 -0
- package/docs/video/build/frames/05-interactive-5.png +0 -0
- package/docs/video/build/frames/05-interactive-6.png +0 -0
- package/docs/video/build/frames/06-dryrun-6.png +0 -0
- package/docs/video/build/frames/06-dryrun-7.png +0 -0
- package/docs/video/build/frames/06-onboard-7.png +0 -0
- package/docs/video/build/frames/06-onboard-8.png +0 -0
- package/docs/video/build/frames/06-run-7.png +0 -0
- package/docs/video/build/frames/06-run-8.png +0 -0
- package/docs/video/build/frames/07-onboard-10.png +0 -0
- package/docs/video/build/frames/07-onboard-7.png +0 -0
- package/docs/video/build/frames/07-onboard-8.png +0 -0
- package/docs/video/build/frames/07-onboard-9.png +0 -0
- package/docs/video/build/frames/07-verify-10.png +0 -0
- package/docs/video/build/frames/07-verify-8.png +0 -0
- package/docs/video/build/frames/07-verify-9.png +0 -0
- package/docs/video/build/frames/08-closing-10.png +0 -0
- package/docs/video/build/frames/08-closing-11.png +0 -0
- package/docs/video/build/frames/08-doctor-11.png +0 -0
- package/docs/video/build/frames/08-doctor-9.png +0 -0
- package/docs/video/build/frames/09-rollback-10.png +0 -0
- package/docs/video/build/frames/09-rollback-12.png +0 -0
- package/docs/video/build/frames/10-principles-11.png +0 -0
- package/docs/video/build/frames/10-principles-13.png +0 -0
- package/docs/video/build/frames/11-closing-12.png +0 -0
- package/docs/video/build/frames/11-closing-14.png +0 -0
- package/docs/video/build/segments/01-opening.mp4 +0 -0
- package/docs/video/build/segments/01-opening.txt +3 -0
- package/docs/video/build/segments/02-pain.mp4 +0 -0
- package/docs/video/build/segments/02-pain.txt +5 -0
- package/docs/video/build/segments/03-solution.mp4 +0 -0
- package/docs/video/build/segments/03-solution.txt +3 -0
- package/docs/video/build/segments/04-install.mp4 +0 -0
- package/docs/video/build/segments/04-install.txt +3 -0
- package/docs/video/build/segments/04-setup.mp4 +0 -0
- package/docs/video/build/segments/04-setup.txt +3 -0
- package/docs/video/build/segments/05-dryrun.mp4 +0 -0
- package/docs/video/build/segments/05-dryrun.txt +3 -0
- package/docs/video/build/segments/05-init.mp4 +0 -0
- package/docs/video/build/segments/05-init.txt +3 -0
- package/docs/video/build/segments/05-interactive.mp4 +0 -0
- package/docs/video/build/segments/05-interactive.txt +5 -0
- package/docs/video/build/segments/06-dryrun.mp4 +0 -0
- package/docs/video/build/segments/06-dryrun.txt +3 -0
- package/docs/video/build/segments/06-onboard.mp4 +0 -0
- package/docs/video/build/segments/06-onboard.txt +5 -0
- package/docs/video/build/segments/06-run.mp4 +0 -0
- package/docs/video/build/segments/06-run.txt +3 -0
- package/docs/video/build/segments/07-onboard.mp4 +0 -0
- package/docs/video/build/segments/07-onboard.txt +5 -0
- package/docs/video/build/segments/07-verify.mp4 +0 -0
- package/docs/video/build/segments/07-verify.txt +5 -0
- package/docs/video/build/segments/08-closing.mp4 +0 -0
- package/docs/video/build/segments/08-closing.txt +3 -0
- package/docs/video/build/segments/08-doctor.mp4 +0 -0
- package/docs/video/build/segments/08-doctor.txt +3 -0
- package/docs/video/build/segments/09-rollback.mp4 +0 -0
- package/docs/video/build/segments/09-rollback.txt +3 -0
- package/docs/video/build/segments/10-principles.mp4 +0 -0
- package/docs/video/build/segments/10-principles.txt +3 -0
- package/docs/video/build/segments/11-closing.mp4 +0 -0
- package/docs/video/build/segments/11-closing.txt +3 -0
- package/docs/video/build/segments/final.txt +8 -0
- package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
- package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
- package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
- package/docs/video/build-ai/audio/04-how.mp3 +0 -0
- package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
- package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
- package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
- package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
- package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
- package/docs/video/build-ai/clips/04-how.mp4 +0 -0
- package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
- package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
- package/docs/video/build-ai/first-frame-02-pain.png +0 -0
- package/docs/video/build-ai/first-frame-03-solution.png +0 -0
- package/docs/video/build-ai/first-frame-04-how.png +0 -0
- package/docs/video/build-ai/first-frame-05-safe.png +0 -0
- package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
- package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
- package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
- package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
- package/docs/video/build-ai/segments/04-how.mp4 +0 -0
- package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
- package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
- package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
- package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
- package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
- package/docs/video/build-ai/segments/cap-04-how.png +0 -0
- package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
- package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
- package/docs/video/build-ai/segments/final.txt +6 -0
- package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
- package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
- package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
- package/docs/video/build-posters/audio/04-how.mp3 +0 -0
- package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
- package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
- package/docs/video/build-posters/cap-01-hook.png +0 -0
- package/docs/video/build-posters/cap-02-pain.png +0 -0
- package/docs/video/build-posters/cap-03-solution.png +0 -0
- package/docs/video/build-posters/cap-04-how.png +0 -0
- package/docs/video/build-posters/cap-05-safe.png +0 -0
- package/docs/video/build-posters/cap-06-closing.png +0 -0
- package/docs/video/build-posters/card-01-hook.png +0 -0
- package/docs/video/build-posters/card-06-closing.png +0 -0
- package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
- package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
- package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
- package/docs/video/build-posters/segments/04-how.mp4 +0 -0
- package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
- package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
- package/docs/video/build-posters/segments/final.txt +6 -0
- package/docs/video/generate-video-ai.mjs +334 -0
- package/docs/video/generate-video-posters.mjs +204 -0
- package/docs/video/generate-video.mjs +426 -0
- package/docs/video/outline.zh-CN.md +102 -0
- package/docs/video/trellis-demo-video-ai.mp4 +0 -0
- package/docs/video/trellis-demo-video.mp4 +0 -0
- package/docs/video/voiceover.zh-CN.md +49 -0
- package/package.json +6 -2
- package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
- package/schema/capability-selection.example.yaml +23 -0
- package/schema/scope.example.yaml +1 -1
- package/schema/servers.example.yaml +15 -1
package/dist/adapters/mcpPlan.js
CHANGED
|
@@ -17,21 +17,32 @@
|
|
|
17
17
|
* ever returns create/repair + refuse.
|
|
18
18
|
*/
|
|
19
19
|
import { isInScope } from "../core/adapter.js";
|
|
20
|
+
import { capabilityDeliveryForAgent } from "../core/types.js";
|
|
20
21
|
import { codexBearerTokenEnvVar } from "../lib/tomlSection.js";
|
|
21
22
|
import { resolveSecretEnv } from "../lib/secretEnv.js";
|
|
22
23
|
export const HUB_ENTRY_NAME = "trellis-hub";
|
|
23
|
-
export const GATEWAY_ENTRY_NAME = "trellis
|
|
24
|
+
export const GATEWAY_ENTRY_NAME = "trellis";
|
|
25
|
+
export const LEGACY_GATEWAY_ENTRY_NAME = "trellis-gateway";
|
|
26
|
+
export const RUNTIME_ENTRY_NAME = "trellis";
|
|
27
|
+
export const LEGACY_RUNTIME_ENTRY_NAME = "trellis-runtime";
|
|
24
28
|
/** The command an agent spawns in gateway mode. Bare `trellis` rather than
|
|
25
29
|
* an absolute path: the entry has to keep working across reinstalls and
|
|
26
30
|
* version bumps, and every agent resolves it through the same PATH the
|
|
27
31
|
* user installed the CLI onto. */
|
|
28
32
|
export const GATEWAY_COMMAND = "trellis";
|
|
33
|
+
export const RUNTIME_COMMAND = "trellis";
|
|
34
|
+
export function isOAuthMcpServer(def) {
|
|
35
|
+
return def.auth === "oauth";
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Gateway mode applies to every managed agent when `agents` is omitted —
|
|
31
39
|
* the expected normal case (design.md D14). An explicit list narrows it,
|
|
32
40
|
* letting some agents stay in direct or hub mode.
|
|
33
41
|
*/
|
|
34
42
|
export function isGatewayAgent(agentId, mcp, managedAgents) {
|
|
43
|
+
const route = mcp.routes?.[agentId];
|
|
44
|
+
if (route)
|
|
45
|
+
return route.mode === "gateway";
|
|
35
46
|
const gateway = mcp.gateway;
|
|
36
47
|
if (!gateway?.enabled)
|
|
37
48
|
return false;
|
|
@@ -39,6 +50,16 @@ export function isGatewayAgent(agentId, mcp, managedAgents) {
|
|
|
39
50
|
return gateway.agents.includes(agentId);
|
|
40
51
|
return managedAgents.includes(agentId);
|
|
41
52
|
}
|
|
53
|
+
function routeForAgent(agentId, mcp, managedAgents) {
|
|
54
|
+
const explicit = mcp.routes?.[agentId];
|
|
55
|
+
if (explicit)
|
|
56
|
+
return explicit;
|
|
57
|
+
if (isGatewayAgent(agentId, mcp, managedAgents))
|
|
58
|
+
return { mode: "gateway" };
|
|
59
|
+
if (mcp.hub)
|
|
60
|
+
return { mode: "hub" };
|
|
61
|
+
return { mode: "direct" };
|
|
62
|
+
}
|
|
42
63
|
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.';
|
|
43
64
|
function collisionMessage(name, agentId) {
|
|
44
65
|
return `refusing to write MCP server "${name}": also appears in known_host_injected.${agentId === "codex" ? CODEX_STDIO_URL_CRASH_NOTE : ""}`;
|
|
@@ -116,45 +137,122 @@ function findUnresolvedEnvName(def, policy) {
|
|
|
116
137
|
return names.find((name) => !resolved[name]);
|
|
117
138
|
}
|
|
118
139
|
export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
140
|
+
const route = routeForAgent(agentId, mcp, managedAgents);
|
|
141
|
+
const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
|
|
119
142
|
// Checked before `hub`: only one entry can be "the" MCP entry for an
|
|
120
143
|
// agent, and gateway is the newer, purpose-built path (design.md D6).
|
|
121
144
|
// `hub` stays available, unchanged, for anyone pointing an agent at an
|
|
122
145
|
// externally-operated endpoint — including a different agent in the
|
|
123
146
|
// same canonical source.
|
|
124
|
-
if (
|
|
125
|
-
|
|
147
|
+
if (route.mode === "gateway") {
|
|
148
|
+
const entryName = runtimeEnabled ? RUNTIME_ENTRY_NAME : GATEWAY_ENTRY_NAME;
|
|
149
|
+
if (mcp.knownHostInjected.includes(entryName)) {
|
|
126
150
|
return {
|
|
127
151
|
desired: [],
|
|
128
|
-
conflicts: [{ name:
|
|
152
|
+
conflicts: [{ name: entryName, message: collisionMessage(entryName, agentId), remediation: collisionRemediation(entryName) }],
|
|
129
153
|
};
|
|
130
154
|
}
|
|
155
|
+
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isOAuthMcpServer);
|
|
156
|
+
// Every ordinary per-server check (enabled, scope, literal secrets,
|
|
157
|
+
// unresolved env names, Codex's header shape) still runs at gateway
|
|
158
|
+
// startup. OAuth servers are the deliberate exception: they are written
|
|
159
|
+
// as direct entries so the gateway never owns their client connection.
|
|
131
160
|
// Every per-server check (enabled, scope, literal secrets, unresolved
|
|
132
161
|
// env names, Codex's header shape) still runs — but at gateway
|
|
133
162
|
// *startup*, inside the subcommand, since no adapter sees an
|
|
134
163
|
// individual server in this mode (design.md D6).
|
|
135
164
|
return {
|
|
136
|
-
desired: [{
|
|
137
|
-
|
|
165
|
+
desired: [{
|
|
166
|
+
name: entryName,
|
|
167
|
+
def: {
|
|
168
|
+
transport: "stdio",
|
|
169
|
+
command: runtimeEnabled ? RUNTIME_COMMAND : GATEWAY_COMMAND,
|
|
170
|
+
args: [runtimeEnabled ? "mcp-runtime" : "mcp-gateway", "--agent", agentId],
|
|
171
|
+
},
|
|
172
|
+
}, ...oauth.desired],
|
|
173
|
+
conflicts: oauth.conflicts,
|
|
138
174
|
};
|
|
139
175
|
}
|
|
140
|
-
if (
|
|
176
|
+
if (route.mode === "hub") {
|
|
177
|
+
if (!mcp.hub) {
|
|
178
|
+
return {
|
|
179
|
+
desired: [],
|
|
180
|
+
conflicts: [{
|
|
181
|
+
name: HUB_ENTRY_NAME,
|
|
182
|
+
message: `cannot resolve hub route "${agentId}": canonical mcp.hub.url is not configured`,
|
|
183
|
+
remediation: `configure mcp.hub.url or choose direct/gateway mode for ${agentId}`,
|
|
184
|
+
}],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
141
187
|
if (mcp.knownHostInjected.includes(HUB_ENTRY_NAME)) {
|
|
142
188
|
return {
|
|
143
189
|
desired: [],
|
|
144
190
|
conflicts: [{ name: HUB_ENTRY_NAME, message: collisionMessage(HUB_ENTRY_NAME, agentId), remediation: collisionRemediation(HUB_ENTRY_NAME) }],
|
|
145
191
|
};
|
|
146
192
|
}
|
|
147
|
-
|
|
193
|
+
if (route.servers) {
|
|
194
|
+
return {
|
|
195
|
+
desired: [],
|
|
196
|
+
conflicts: [{
|
|
197
|
+
name: HUB_ENTRY_NAME,
|
|
198
|
+
message: `cannot restrict external hub route "${agentId}" to individual MCP servers: the hub owns its upstream set`,
|
|
199
|
+
remediation: `remove the servers list from the ${agentId} hub route, or use direct/gateway mode for per-server selection`,
|
|
200
|
+
}],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const desired = [{ name: HUB_ENTRY_NAME, def: { transport: "http", url: mcp.hub.url } }];
|
|
204
|
+
if (runtimeEnabled) {
|
|
205
|
+
if (mcp.knownHostInjected.includes(RUNTIME_ENTRY_NAME)) {
|
|
206
|
+
return {
|
|
207
|
+
desired: [],
|
|
208
|
+
conflicts: [{ name: RUNTIME_ENTRY_NAME, message: collisionMessage(RUNTIME_ENTRY_NAME, agentId), remediation: collisionRemediation(RUNTIME_ENTRY_NAME) }],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// The runtime provides Trellis-owned capabilities here. The external
|
|
212
|
+
// hub remains the upstream MCP edge; the runtime does not connect to
|
|
213
|
+
// canonical upstreams a second time for this route.
|
|
214
|
+
desired.push({
|
|
215
|
+
name: RUNTIME_ENTRY_NAME,
|
|
216
|
+
def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return { desired, conflicts: [] };
|
|
220
|
+
}
|
|
221
|
+
if (runtimeEnabled) {
|
|
222
|
+
if (mcp.knownHostInjected.includes(RUNTIME_ENTRY_NAME)) {
|
|
223
|
+
return {
|
|
224
|
+
desired: [],
|
|
225
|
+
conflicts: [{ name: RUNTIME_ENTRY_NAME, message: collisionMessage(RUNTIME_ENTRY_NAME, agentId), remediation: collisionRemediation(RUNTIME_ENTRY_NAME) }],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Runtime mode owns the agent-facing MCP edge. It resolves the same
|
|
229
|
+
// direct/gateway-compatible upstream route at startup, so native
|
|
230
|
+
// per-server entries are not duplicated here.
|
|
231
|
+
return {
|
|
232
|
+
desired: [{
|
|
233
|
+
name: RUNTIME_ENTRY_NAME,
|
|
234
|
+
def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] },
|
|
235
|
+
}],
|
|
236
|
+
conflicts: [],
|
|
237
|
+
};
|
|
148
238
|
}
|
|
239
|
+
return planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
240
|
+
}
|
|
241
|
+
function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
149
242
|
const desired = [];
|
|
150
243
|
const conflicts = [];
|
|
151
244
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
152
245
|
if (def.enabled === false) {
|
|
153
246
|
continue;
|
|
154
247
|
}
|
|
248
|
+
if (filter && !filter(def))
|
|
249
|
+
continue;
|
|
155
250
|
if (!isInScope(agentId, def.agents, managedAgents)) {
|
|
156
251
|
continue;
|
|
157
252
|
}
|
|
253
|
+
if (route.servers && !route.servers.includes(name)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
158
256
|
if (mcp.knownHostInjected.includes(name)) {
|
|
159
257
|
conflicts.push({ name, message: collisionMessage(name, agentId), remediation: collisionRemediation(name) });
|
|
160
258
|
continue;
|
package/dist/adapters/pi.js
CHANGED
|
@@ -9,13 +9,31 @@
|
|
|
9
9
|
* mechanism from every other agent's config-generation adapter, but the
|
|
10
10
|
* same symlink create/repair/remove machinery as skills/instructions.
|
|
11
11
|
*/
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import { dirname, join, sep } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
15
16
|
import { isInScope } from "../core/adapter.js";
|
|
17
|
+
import { usesNativeCapabilityDelivery } from "../core/types.js";
|
|
16
18
|
import * as piProbe from "../probes/pi.js";
|
|
17
19
|
import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
18
20
|
const BRIDGE_SYMLINK_NAME = "trellis-mcp-bridge.js";
|
|
21
|
+
/**
|
|
22
|
+
* A source-checkout bridge can survive a switch to the packaged CLI. Adopt it
|
|
23
|
+
* only when it has the Trellis bridge artifact shape and is byte-identical
|
|
24
|
+
* to the bundle this installation would write. This is intentionally not a
|
|
25
|
+
* general symlink ownership rule.
|
|
26
|
+
*/
|
|
27
|
+
function isEquivalentTrellisBridge(existingTarget, desiredTarget) {
|
|
28
|
+
if (!existingTarget.endsWith(`${sep}${join("dist", "pi-bridge", "bundle.js")}`))
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
return readFileSync(existingTarget).equals(readFileSync(desiredTarget));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
/**
|
|
20
38
|
* The bundled bridge file's location — always `<repo-root>/dist/pi-bridge/
|
|
21
39
|
* bundle.js`, never `~/.trellis/` (it's Trellis's own packaged code, not a
|
|
@@ -56,7 +74,7 @@ export class PiAdapter {
|
|
|
56
74
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
57
75
|
const agentDir = join(this.homeDir, ".pi", "agent");
|
|
58
76
|
const desiredSkills = canonical.skills
|
|
59
|
-
.filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
77
|
+
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
60
78
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
61
79
|
const skillItems = planSymlinks({
|
|
62
80
|
rootDir: join(agentDir, "skills"),
|
|
@@ -79,6 +97,7 @@ export class PiAdapter {
|
|
|
79
97
|
// (design.md D2), same role `canonicalRoot` plays for skills.
|
|
80
98
|
canonicalRoot: dirname(bridgeFile),
|
|
81
99
|
kind: "extension",
|
|
100
|
+
adoptExistingSymlink: isEquivalentTrellisBridge,
|
|
82
101
|
});
|
|
83
102
|
return [...skillItems, ...instructionsItems, ...extensionItems];
|
|
84
103
|
}
|
|
@@ -91,7 +110,7 @@ export class PiAdapter {
|
|
|
91
110
|
return { ok: false, mismatches: ["pi is not present on this machine"] };
|
|
92
111
|
}
|
|
93
112
|
const mismatches = [];
|
|
94
|
-
const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
113
|
+
const desiredNames = new Set(canonical.skills.filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
95
114
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
96
115
|
for (const name of desiredNames) {
|
|
97
116
|
if (!actualSkills.has(name)) {
|
|
@@ -12,6 +12,12 @@ export interface DesiredSymlink {
|
|
|
12
12
|
/** Absolute path the symlink should point at (inside `canonicalRoot`). */
|
|
13
13
|
target: string;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* A narrowly-scoped escape hatch for artifacts that Trellis can identify
|
|
17
|
+
* across installation roots. The default ownership rule remains path-based;
|
|
18
|
+
* callers must opt in explicitly for a particular artifact kind.
|
|
19
|
+
*/
|
|
20
|
+
export type ExistingSymlinkAdoption = (existingTarget: string, desiredTarget: string) => boolean;
|
|
15
21
|
/**
|
|
16
22
|
* `rootDir` is the directory entries are named directly under (e.g.
|
|
17
23
|
* `~/.claude/skills`, or the instructions file's own parent directory when
|
|
@@ -28,6 +34,7 @@ export declare function planSymlinks(opts: {
|
|
|
28
34
|
desired: DesiredSymlink[];
|
|
29
35
|
canonicalRoot: string;
|
|
30
36
|
kind: "skill" | "instructions" | "extension";
|
|
37
|
+
adoptExistingSymlink?: ExistingSymlinkAdoption;
|
|
31
38
|
}): AdapterPlanItem[];
|
|
32
39
|
/** Executes a `planSymlinks` result through the run's backup session
|
|
33
40
|
* (trellis-backup-rollback) — every create/repair/remove is recorded
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* trellis-sync-p1/specs/skill-instructions-sync/spec.md) — built once,
|
|
5
5
|
* reused by every adapter rather than reimplemented per agent.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { lstatSync, readdirSync, readlinkSync } from "node:fs";
|
|
8
8
|
import { mkdir } from "node:fs/promises";
|
|
9
|
-
import { join, resolve, sep } from "node:path";
|
|
9
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
10
10
|
import { isSymlinkTo } from "../lib/fsIdentity.js";
|
|
11
11
|
import { currentLinkTarget } from "../lib/backup.js";
|
|
12
12
|
function isUnderRoot(path, root) {
|
|
@@ -29,7 +29,7 @@ function isUnderRoot(path, root) {
|
|
|
29
29
|
* case this removal path exists to catch.
|
|
30
30
|
*/
|
|
31
31
|
export function planSymlinks(opts) {
|
|
32
|
-
const { rootDir, desired, kind } = opts;
|
|
32
|
+
const { rootDir, desired, kind, adoptExistingSymlink } = opts;
|
|
33
33
|
const items = [];
|
|
34
34
|
const desiredNames = new Set(desired.map((d) => d.name));
|
|
35
35
|
const canonicalRootResolved = resolve(opts.canonicalRoot);
|
|
@@ -38,8 +38,15 @@ export function planSymlinks(opts) {
|
|
|
38
38
|
if (isSymlinkTo(path, target)) {
|
|
39
39
|
continue; // already correct — no-op
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// lstat sees dangling links too; existsSync would treat them as absent
|
|
42
|
+
// and bypass the foreign-link protection below.
|
|
43
|
+
const existing = lstatSync(path, { throwIfNoEntry: false });
|
|
44
|
+
if (existing) {
|
|
45
|
+
const isSymlink = existing.isSymbolicLink();
|
|
46
|
+
const rawExistingTarget = isSymlink ? readlinkSync(path) : undefined;
|
|
47
|
+
const resolvedExistingTarget = rawExistingTarget === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: resolve(dirname(path), rawExistingTarget);
|
|
43
50
|
// A symlink whose stored target resolves outside canonicalRoot is
|
|
44
51
|
// owned by something else (e.g. a user's own dotfile-management
|
|
45
52
|
// setup) — repairing it as if it were a stale Trellis entry would
|
|
@@ -47,14 +54,16 @@ export function planSymlinks(opts) {
|
|
|
47
54
|
// symlink Trellis itself left pointing at a since-removed canonical
|
|
48
55
|
// entry is broken by construction and must still be treated as
|
|
49
56
|
// ours to repair, not thrown out to a conflict by a realpath error.
|
|
50
|
-
const isForeign =
|
|
57
|
+
const isForeign = resolvedExistingTarget !== undefined
|
|
58
|
+
&& !isUnderRoot(resolvedExistingTarget, canonicalRootResolved)
|
|
59
|
+
&& adoptExistingSymlink?.(resolvedExistingTarget, target) !== true;
|
|
51
60
|
if (!isSymlink || isForeign) {
|
|
52
61
|
items.push({
|
|
53
62
|
action: "conflict",
|
|
54
63
|
kind,
|
|
55
64
|
target: path,
|
|
56
65
|
description: isForeign
|
|
57
|
-
? `${path} exists as a symlink to ${
|
|
66
|
+
? `${path} exists as a symlink to ${rawExistingTarget}, not owned by Trellis — left untouched`
|
|
58
67
|
: `${path} exists and is not a Trellis-managed symlink — left untouched`,
|
|
59
68
|
remediation: isForeign
|
|
60
69
|
? `remove the existing symlink at ${path} if you want Trellis to manage it, then re-run sync — otherwise leave it, Trellis will not touch it`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-first ZCode adapter.
|
|
3
|
+
*
|
|
4
|
+
* ZCode's native `~/.agents/skills` scan prevents scope-safe native delivery,
|
|
5
|
+
* so Runtime-only delivery uses one MCP Runtime edge and turns off the two
|
|
6
|
+
* native Skill switches through a ledger-backed, reversible config change.
|
|
7
|
+
*/
|
|
8
|
+
import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
|
|
9
|
+
import type { CanonicalSource } from "../core/types.js";
|
|
10
|
+
import type { BackupSession } from "../lib/backup.js";
|
|
11
|
+
export declare class ZcodeAdapter implements TrellisAdapter {
|
|
12
|
+
private readonly homeDir;
|
|
13
|
+
private readonly env;
|
|
14
|
+
readonly name = "ZCode";
|
|
15
|
+
readonly id: "zcode";
|
|
16
|
+
constructor(homeDir?: string, env?: NodeJS.ProcessEnv);
|
|
17
|
+
probe(): Promise<AdapterProbeResult>;
|
|
18
|
+
plan(canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
|
|
19
|
+
apply(plan: AdapterPlanItem[], backup: BackupSession): Promise<void>;
|
|
20
|
+
verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-first ZCode adapter.
|
|
3
|
+
*
|
|
4
|
+
* ZCode's native `~/.agents/skills` scan prevents scope-safe native delivery,
|
|
5
|
+
* so Runtime-only delivery uses one MCP Runtime edge and turns off the two
|
|
6
|
+
* native Skill switches through a ledger-backed, reversible config change.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { isInScope } from "../core/adapter.js";
|
|
12
|
+
import { capabilityDeliveryForAgent, usesNativeCapabilityDelivery } from "../core/types.js";
|
|
13
|
+
import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
14
|
+
import { renderJsonServerEntry } from "./jsonMcp.js";
|
|
15
|
+
import { LEGACY_GATEWAY_ENTRY_NAME, LEGACY_RUNTIME_ENTRY_NAME, resolveMcpPlan } from "./mcpPlan.js";
|
|
16
|
+
import { deepEqual } from "../lib/deepEqual.js";
|
|
17
|
+
import { forgetOwned, loadMcpOwnership, ownedByAgent, recordOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
18
|
+
import { forgetZcodeSettingsOwnership, loadZcodeSettingsOwnership, saveZcodeSettingsOwnership } from "../lib/zcodeOwnership.js";
|
|
19
|
+
import * as zcodeProbe from "../probes/zcode.js";
|
|
20
|
+
function readConfig(path) {
|
|
21
|
+
if (!existsSync(path))
|
|
22
|
+
return undefined;
|
|
23
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
function record(value) {
|
|
26
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
27
|
+
}
|
|
28
|
+
function zcodeServers(parsed) {
|
|
29
|
+
return record(record(parsed?.mcp).servers);
|
|
30
|
+
}
|
|
31
|
+
function planZcodeMcp(opts) {
|
|
32
|
+
const { desired, conflicts } = resolveMcpPlan("zcode", opts.mcp, opts.managedAgents, opts.policy);
|
|
33
|
+
const existing = zcodeServers(opts.parsed);
|
|
34
|
+
const items = [];
|
|
35
|
+
for (const { name, def } of desired) {
|
|
36
|
+
const rendered = renderJsonServerEntry(def);
|
|
37
|
+
if (deepEqual(existing[name], rendered))
|
|
38
|
+
continue;
|
|
39
|
+
items.push({
|
|
40
|
+
action: "create",
|
|
41
|
+
kind: "mcp",
|
|
42
|
+
target: opts.configPath,
|
|
43
|
+
mcpWrite: { name, def },
|
|
44
|
+
description: `MCP server "${name}" ${existing[name] === undefined ? "created" : "updated"} in ${opts.configPath}`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
for (const conflict of conflicts) {
|
|
48
|
+
items.push({ action: "conflict", kind: "mcp", target: opts.configPath, description: conflict.message, ...(conflict.remediation ? { remediation: conflict.remediation } : {}) });
|
|
49
|
+
}
|
|
50
|
+
const desiredNames = new Set(desired.map((entry) => entry.name));
|
|
51
|
+
for (const [name, expected] of Object.entries(opts.ownership)) {
|
|
52
|
+
if (desiredNames.has(name))
|
|
53
|
+
continue;
|
|
54
|
+
if ((name === LEGACY_GATEWAY_ENTRY_NAME || name === LEGACY_RUNTIME_ENTRY_NAME) && desiredNames.has("trellis"))
|
|
55
|
+
continue;
|
|
56
|
+
if (existing[name] === undefined || !deepEqual(existing[name], expected))
|
|
57
|
+
continue;
|
|
58
|
+
items.push({
|
|
59
|
+
action: "remove",
|
|
60
|
+
kind: "mcp",
|
|
61
|
+
target: opts.configPath,
|
|
62
|
+
mcpRemove: { name },
|
|
63
|
+
description: `MCP server "${name}" removed from ${opts.configPath} — no longer in canonical`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return items;
|
|
67
|
+
}
|
|
68
|
+
function applyZcodeMcp(parsed, items) {
|
|
69
|
+
const base = parsed ?? {};
|
|
70
|
+
const mcp = record(base.mcp);
|
|
71
|
+
const servers = { ...record(mcp.servers) };
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
if (item.kind !== "mcp")
|
|
74
|
+
continue;
|
|
75
|
+
if (item.action === "create" && item.mcpWrite)
|
|
76
|
+
servers[item.mcpWrite.name] = renderJsonServerEntry(item.mcpWrite.def);
|
|
77
|
+
if (item.action === "remove" && item.mcpRemove)
|
|
78
|
+
delete servers[item.mcpRemove.name];
|
|
79
|
+
}
|
|
80
|
+
return { ...base, mcp: { ...mcp, servers } };
|
|
81
|
+
}
|
|
82
|
+
function currentSkillControls(parsed) {
|
|
83
|
+
const features = record(parsed?.features);
|
|
84
|
+
const skills = record(parsed?.skills);
|
|
85
|
+
return {
|
|
86
|
+
featuresSkill: typeof features.skill === "boolean" ? features.skill : null,
|
|
87
|
+
skillsEnabled: typeof skills.enabled === "boolean" ? skills.enabled : null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function applySkillControls(parsed, controls) {
|
|
91
|
+
const features = { ...record(parsed.features) };
|
|
92
|
+
const skills = { ...record(parsed.skills) };
|
|
93
|
+
if (controls.featuresSkill === null)
|
|
94
|
+
delete features.skill;
|
|
95
|
+
else
|
|
96
|
+
features.skill = controls.featuresSkill;
|
|
97
|
+
if (controls.skillsEnabled === null)
|
|
98
|
+
delete skills.enabled;
|
|
99
|
+
else
|
|
100
|
+
skills.enabled = controls.skillsEnabled;
|
|
101
|
+
return { ...parsed, features, skills };
|
|
102
|
+
}
|
|
103
|
+
export class ZcodeAdapter {
|
|
104
|
+
homeDir;
|
|
105
|
+
env;
|
|
106
|
+
name = "ZCode";
|
|
107
|
+
id = "zcode";
|
|
108
|
+
constructor(homeDir = homedir(), env = process.env) {
|
|
109
|
+
this.homeDir = homeDir;
|
|
110
|
+
this.env = env;
|
|
111
|
+
}
|
|
112
|
+
async probe() {
|
|
113
|
+
const snapshot = await zcodeProbe.probe(this.homeDir, { env: this.env });
|
|
114
|
+
return { present: snapshot.present, version: snapshot.version, ...(snapshot.diagnostics.length ? { detail: snapshot.diagnostics.join("; ") } : {}) };
|
|
115
|
+
}
|
|
116
|
+
async plan(canonical) {
|
|
117
|
+
const profile = zcodeProbe.resolveZcodeProfile(this.homeDir, this.env);
|
|
118
|
+
if (!profile)
|
|
119
|
+
return [];
|
|
120
|
+
const parsed = readConfig(profile.configPath);
|
|
121
|
+
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
122
|
+
const runtimeOnly = capabilityDeliveryForAgent(this.id, canonical.mcp) === "mcp";
|
|
123
|
+
const items = [];
|
|
124
|
+
items.push(...planSymlinks({
|
|
125
|
+
rootDir: join(this.homeDir, ".zcode", "skills"),
|
|
126
|
+
desired: usesNativeCapabilityDelivery(this.id, canonical.mcp)
|
|
127
|
+
? canonical.skills.filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents)).map((skill) => ({ name: skill.name, target: skill.dir }))
|
|
128
|
+
: [],
|
|
129
|
+
canonicalRoot: join(canonicalRoot, "skills"),
|
|
130
|
+
kind: "skill",
|
|
131
|
+
}));
|
|
132
|
+
items.push(...planSymlinks({
|
|
133
|
+
rootDir: join(this.homeDir, ".zcode"),
|
|
134
|
+
desired: [{ name: "AGENTS.md", target: canonical.instructionsFile }],
|
|
135
|
+
canonicalRoot,
|
|
136
|
+
kind: "instructions",
|
|
137
|
+
}));
|
|
138
|
+
items.push(...planZcodeMcp({
|
|
139
|
+
configPath: profile.configPath,
|
|
140
|
+
parsed,
|
|
141
|
+
mcp: canonical.mcp,
|
|
142
|
+
managedAgents: canonical.managedAgents,
|
|
143
|
+
policy: canonical.secretsPolicy,
|
|
144
|
+
ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
|
|
145
|
+
}));
|
|
146
|
+
const current = currentSkillControls(parsed);
|
|
147
|
+
const ownership = loadZcodeSettingsOwnership(this.homeDir);
|
|
148
|
+
if (runtimeOnly && (current.featuresSkill !== false || current.skillsEnabled !== false)) {
|
|
149
|
+
items.push({
|
|
150
|
+
action: "create",
|
|
151
|
+
kind: "zcode-skill-control",
|
|
152
|
+
target: profile.configPath,
|
|
153
|
+
zcodeSkillControls: { featuresSkill: false, skillsEnabled: false, previous: current },
|
|
154
|
+
description: `native ZCode Skill discovery disabled in ${profile.configPath} for Runtime-only delivery`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else if (!runtimeOnly && ownership?.configPath === profile.configPath && current.featuresSkill === false && current.skillsEnabled === false) {
|
|
158
|
+
items.push({
|
|
159
|
+
action: "remove",
|
|
160
|
+
kind: "zcode-skill-control",
|
|
161
|
+
target: profile.configPath,
|
|
162
|
+
zcodeSkillControls: { ...ownership.previous },
|
|
163
|
+
description: `previous native ZCode Skill settings restored in ${profile.configPath}`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return items;
|
|
167
|
+
}
|
|
168
|
+
async apply(plan, backup) {
|
|
169
|
+
await applySymlinkPlan(plan.filter((item) => item.kind === "skill" || item.kind === "instructions"), backup);
|
|
170
|
+
const configItems = plan.filter((item) => (item.kind === "mcp" || item.kind === "zcode-skill-control") && (item.action === "create" || item.action === "remove"));
|
|
171
|
+
if (configItems.length === 0)
|
|
172
|
+
return;
|
|
173
|
+
const configPath = configItems[0].target;
|
|
174
|
+
let merged = applyZcodeMcp(readConfig(configPath), configItems);
|
|
175
|
+
const control = configItems.find((item) => item.kind === "zcode-skill-control")?.zcodeSkillControls;
|
|
176
|
+
if (control)
|
|
177
|
+
merged = applySkillControls(merged, control);
|
|
178
|
+
backup.writeFile(configPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
179
|
+
let ownership = loadMcpOwnership(this.homeDir);
|
|
180
|
+
for (const item of configItems) {
|
|
181
|
+
if (item.kind !== "mcp")
|
|
182
|
+
continue;
|
|
183
|
+
if (item.action === "create" && item.mcpWrite)
|
|
184
|
+
ownership = recordOwned(ownership, this.id, item.mcpWrite.name, renderJsonServerEntry(item.mcpWrite.def));
|
|
185
|
+
if (item.action === "remove" && item.mcpRemove)
|
|
186
|
+
ownership = forgetOwned(ownership, this.id, item.mcpRemove.name);
|
|
187
|
+
}
|
|
188
|
+
saveMcpOwnership(this.homeDir, ownership);
|
|
189
|
+
const controlItem = configItems.find((item) => item.kind === "zcode-skill-control");
|
|
190
|
+
if (controlItem?.action === "create" && controlItem.zcodeSkillControls?.previous) {
|
|
191
|
+
saveZcodeSettingsOwnership(this.homeDir, {
|
|
192
|
+
configPath,
|
|
193
|
+
managed: { featuresSkill: false, skillsEnabled: false },
|
|
194
|
+
previous: controlItem.zcodeSkillControls.previous,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else if (controlItem?.action === "remove") {
|
|
198
|
+
forgetZcodeSettingsOwnership(this.homeDir);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async verify(canonical) {
|
|
202
|
+
const snapshot = await zcodeProbe.probe(this.homeDir, { env: this.env });
|
|
203
|
+
if (!snapshot.present)
|
|
204
|
+
return { ok: false, mismatches: ["zcode is not present on this machine"] };
|
|
205
|
+
const profile = zcodeProbe.resolveZcodeProfile(this.homeDir, this.env);
|
|
206
|
+
if (!profile)
|
|
207
|
+
return { ok: false, mismatches: ["zcode profile cannot be selected"] };
|
|
208
|
+
const mismatches = [];
|
|
209
|
+
const desiredNativeSkills = canonical.skills
|
|
210
|
+
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
211
|
+
.map((skill) => skill.name);
|
|
212
|
+
const actualNativeSkills = new Set(snapshot.skillRoots.filter((root) => root.path === join(this.homeDir, ".zcode", "skills")).flatMap((root) => root.skills.map((skill) => skill.name)));
|
|
213
|
+
for (const name of desiredNativeSkills)
|
|
214
|
+
if (!actualNativeSkills.has(name))
|
|
215
|
+
mismatches.push(`skill "${name}" is missing on ZCode`);
|
|
216
|
+
const expectedMcp = planZcodeMcp({
|
|
217
|
+
configPath: profile.configPath,
|
|
218
|
+
parsed: readConfig(profile.configPath),
|
|
219
|
+
mcp: canonical.mcp,
|
|
220
|
+
managedAgents: canonical.managedAgents,
|
|
221
|
+
policy: canonical.secretsPolicy,
|
|
222
|
+
ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
|
|
223
|
+
}).filter((item) => item.action === "create" || item.action === "conflict");
|
|
224
|
+
mismatches.push(...expectedMcp.map((item) => item.description));
|
|
225
|
+
if (!snapshot.instructionsFile?.isSymlink || snapshot.instructionsFile.target !== canonical.instructionsFile)
|
|
226
|
+
mismatches.push("shared instructions are not linked into ~/.zcode/AGENTS.md");
|
|
227
|
+
if (capabilityDeliveryForAgent(this.id, canonical.mcp) === "mcp") {
|
|
228
|
+
const controls = currentSkillControls(readConfig(profile.configPath));
|
|
229
|
+
if (controls.featuresSkill !== false || controls.skillsEnabled !== false)
|
|
230
|
+
mismatches.push("native ZCode Skill discovery is still enabled during Runtime-only delivery");
|
|
231
|
+
}
|
|
232
|
+
return mismatches.length === 0 ? { ok: true } : { ok: false, mismatches };
|
|
233
|
+
}
|
|
234
|
+
}
|