@sentropic/h2a 0.98.5 → 0.98.7-rc.1
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/cli-command-map.d.ts.map +1 -1
- package/dist/cli-command-map.js +1 -0
- package/dist/cli-command-map.js.map +1 -1
- package/dist/cli-contract.js +2 -2
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +224 -16
- package/dist/cli.js.map +1 -1
- package/dist/hosts/agy.js +1 -1
- package/dist/hosts/agy.js.map +1 -1
- package/dist/hosts/installation-doctor.d.ts +1 -0
- package/dist/hosts/installation-doctor.d.ts.map +1 -1
- package/dist/hosts/installation-doctor.js +1 -1
- package/dist/hosts/installation-doctor.js.map +1 -1
- package/dist/hosts/muse.d.ts.map +1 -1
- package/dist/hosts/muse.js +2 -4
- package/dist/hosts/muse.js.map +1 -1
- package/dist/hosts/native-artifacts.d.ts +33 -0
- package/dist/hosts/native-artifacts.d.ts.map +1 -0
- package/dist/hosts/native-artifacts.js +104 -0
- package/dist/hosts/native-artifacts.js.map +1 -0
- package/dist/hosts/native-catalog.d.ts +19 -0
- package/dist/hosts/native-catalog.d.ts.map +1 -0
- package/dist/hosts/native-catalog.js +14 -0
- package/dist/hosts/native-catalog.js.map +1 -0
- package/dist/hosts/native-convergence.d.ts +132 -0
- package/dist/hosts/native-convergence.d.ts.map +1 -0
- package/dist/hosts/native-convergence.js +86 -0
- package/dist/hosts/native-convergence.js.map +1 -0
- package/dist/hosts/native-files.d.ts +40 -0
- package/dist/hosts/native-files.d.ts.map +1 -0
- package/dist/hosts/native-files.js +131 -0
- package/dist/hosts/native-files.js.map +1 -0
- package/dist/hosts/native-inspection.d.ts +31 -0
- package/dist/hosts/native-inspection.d.ts.map +1 -0
- package/dist/hosts/native-inspection.js +79 -0
- package/dist/hosts/native-inspection.js.map +1 -0
- package/dist/hosts/native-paths.d.ts +11 -0
- package/dist/hosts/native-paths.d.ts.map +1 -0
- package/dist/hosts/native-paths.js +21 -0
- package/dist/hosts/native-paths.js.map +1 -0
- package/dist/hosts/native-proof.d.ts +32 -0
- package/dist/hosts/native-proof.d.ts.map +1 -0
- package/dist/hosts/native-proof.js +31 -0
- package/dist/hosts/native-proof.js.map +1 -0
- package/dist/hosts/native-runtime-record.d.ts +3 -0
- package/dist/hosts/native-runtime-record.d.ts.map +1 -0
- package/dist/hosts/native-runtime-record.js +60 -0
- package/dist/hosts/native-runtime-record.js.map +1 -0
- package/dist/hosts/native-skills.d.ts +24 -0
- package/dist/hosts/native-skills.d.ts.map +1 -0
- package/dist/hosts/native-skills.js +56 -0
- package/dist/hosts/native-skills.js.map +1 -0
- package/dist/hosts/native-toml.d.ts +18 -0
- package/dist/hosts/native-toml.d.ts.map +1 -0
- package/dist/hosts/native-toml.js +91 -0
- package/dist/hosts/native-toml.js.map +1 -0
- package/dist/hosts/operator-notices.d.ts +3 -0
- package/dist/hosts/operator-notices.d.ts.map +1 -0
- package/dist/hosts/operator-notices.js +31 -0
- package/dist/hosts/operator-notices.js.map +1 -0
- package/dist/hosts/plugin.d.ts.map +1 -1
- package/dist/hosts/plugin.js +3 -2
- package/dist/hosts/plugin.js.map +1 -1
- package/dist/hosts/upgrade-equipment.d.ts +42 -0
- package/dist/hosts/upgrade-equipment.d.ts.map +1 -0
- package/dist/hosts/upgrade-equipment.js +26 -0
- package/dist/hosts/upgrade-equipment.js.map +1 -0
- package/dist/hosts/vibe-discovery.d.ts +12 -0
- package/dist/hosts/vibe-discovery.d.ts.map +1 -0
- package/dist/hosts/vibe-discovery.js +33 -0
- package/dist/hosts/vibe-discovery.js.map +1 -0
- package/dist/hosts/vibe-equipment.d.ts +71 -0
- package/dist/hosts/vibe-equipment.d.ts.map +1 -0
- package/dist/hosts/vibe-equipment.js +162 -0
- package/dist/hosts/vibe-equipment.js.map +1 -0
- package/dist/hosts/vibe-mcp.d.ts +32 -0
- package/dist/hosts/vibe-mcp.d.ts.map +1 -0
- package/dist/hosts/vibe-mcp.js +210 -0
- package/dist/hosts/vibe-mcp.js.map +1 -0
- package/dist/hosts/vibe.d.ts +12 -3
- package/dist/hosts/vibe.d.ts.map +1 -1
- package/dist/hosts/vibe.js +15 -7
- package/dist/hosts/vibe.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/drive/consent.d.ts +1 -1
- package/dist/runtime/drive/consent.d.ts.map +1 -1
- package/dist/runtime/drive/consent.js +2 -2
- package/dist/runtime/drive/consent.js.map +1 -1
- package/dist/runtime/drive/index.d.ts +1 -1
- package/dist/runtime/drive/index.d.ts.map +1 -1
- package/dist/runtime/drive/index.js +3 -1
- package/dist/runtime/drive/index.js.map +1 -1
- package/dist/runtime/drumbeat/relaunchers.d.ts +1 -1
- package/dist/runtime/drumbeat/relaunchers.d.ts.map +1 -1
- package/dist/runtime/drumbeat/relaunchers.js +2 -1
- package/dist/runtime/drumbeat/relaunchers.js.map +1 -1
- package/dist/runtime/identity/binding-commit.d.ts +31 -0
- package/dist/runtime/identity/binding-commit.d.ts.map +1 -0
- package/dist/runtime/identity/binding-commit.js +211 -0
- package/dist/runtime/identity/binding-commit.js.map +1 -0
- package/dist/runtime/identity/bindings.d.ts +6 -30
- package/dist/runtime/identity/bindings.d.ts.map +1 -1
- package/dist/runtime/identity/bindings.js +85 -96
- package/dist/runtime/identity/bindings.js.map +1 -1
- package/dist/runtime/identity/durable.d.ts +7 -0
- package/dist/runtime/identity/durable.d.ts.map +1 -0
- package/dist/runtime/identity/durable.js +68 -0
- package/dist/runtime/identity/durable.js.map +1 -0
- package/dist/runtime/identity/live.d.ts +4 -1
- package/dist/runtime/identity/live.d.ts.map +1 -1
- package/dist/runtime/identity/live.js +59 -6
- package/dist/runtime/identity/live.js.map +1 -1
- package/dist/runtime/identity/possession.d.ts +34 -0
- package/dist/runtime/identity/possession.d.ts.map +1 -0
- package/dist/runtime/identity/possession.js +162 -0
- package/dist/runtime/identity/possession.js.map +1 -0
- package/dist/runtime/identity/worker.js +16 -5
- package/dist/runtime/identity/worker.js.map +1 -1
- package/dist/runtime/live/contracts.d.ts +55 -0
- package/dist/runtime/live/contracts.d.ts.map +1 -0
- package/dist/runtime/live/contracts.js +59 -0
- package/dist/runtime/live/contracts.js.map +1 -0
- package/dist/runtime/local-files/identity-fence.d.ts +164 -0
- package/dist/runtime/local-files/identity-fence.d.ts.map +1 -0
- package/dist/runtime/local-files/identity-fence.js +1338 -0
- package/dist/runtime/local-files/identity-fence.js.map +1 -0
- package/dist/runtime/local-files/index.d.ts +1 -0
- package/dist/runtime/local-files/index.d.ts.map +1 -1
- package/dist/runtime/local-files/index.js +1 -0
- package/dist/runtime/local-files/index.js.map +1 -1
- package/dist/runtime/local-files/legacy-identity-evidence.d.ts +26 -0
- package/dist/runtime/local-files/legacy-identity-evidence.d.ts.map +1 -0
- package/dist/runtime/local-files/legacy-identity-evidence.js +234 -0
- package/dist/runtime/local-files/legacy-identity-evidence.js.map +1 -0
- package/dist/runtime/local-files/locks.d.ts +3 -0
- package/dist/runtime/local-files/locks.d.ts.map +1 -1
- package/dist/runtime/local-files/locks.js +6 -5
- package/dist/runtime/local-files/locks.js.map +1 -1
- package/dist/runtime/local-files/orphan-gc.d.ts +3 -0
- package/dist/runtime/local-files/orphan-gc.d.ts.map +1 -0
- package/dist/runtime/local-files/orphan-gc.js +69 -0
- package/dist/runtime/local-files/orphan-gc.js.map +1 -0
- package/dist/runtime/local-files/presence.d.ts +5 -2
- package/dist/runtime/local-files/presence.d.ts.map +1 -1
- package/dist/runtime/local-files/presence.js +32 -29
- package/dist/runtime/local-files/presence.js.map +1 -1
- package/dist/runtime/local-files/recipient-hints.d.ts +10 -0
- package/dist/runtime/local-files/recipient-hints.d.ts.map +1 -0
- package/dist/runtime/local-files/recipient-hints.js +156 -0
- package/dist/runtime/local-files/recipient-hints.js.map +1 -0
- package/dist/runtime/local-files/store.d.ts +1 -1
- package/dist/runtime/local-files/store.d.ts.map +1 -1
- package/dist/runtime/local-files/store.js +12 -1
- package/dist/runtime/local-files/store.js.map +1 -1
- package/dist/runtime/local-files/succession-lock.d.ts +71 -6
- package/dist/runtime/local-files/succession-lock.d.ts.map +1 -1
- package/dist/runtime/local-files/succession-lock.js +226 -31
- package/dist/runtime/local-files/succession-lock.js.map +1 -1
- package/dist/runtime/mcp/handlers.d.ts +1 -0
- package/dist/runtime/mcp/handlers.d.ts.map +1 -1
- package/dist/runtime/mcp/handlers.js +7 -4
- package/dist/runtime/mcp/handlers.js.map +1 -1
- package/dist/runtime/mcp/identity-blocker-snapshot.d.ts +2 -0
- package/dist/runtime/mcp/identity-blocker-snapshot.d.ts.map +1 -0
- package/dist/runtime/mcp/identity-blocker-snapshot.js +98 -0
- package/dist/runtime/mcp/identity-blocker-snapshot.js.map +1 -0
- package/dist/runtime/mcp/identity-state.d.ts +20 -3
- package/dist/runtime/mcp/identity-state.d.ts.map +1 -1
- package/dist/runtime/mcp/identity-state.js +98 -13
- package/dist/runtime/mcp/identity-state.js.map +1 -1
- package/dist/runtime/mcp/notifications.d.ts +2 -1
- package/dist/runtime/mcp/notifications.d.ts.map +1 -1
- package/dist/runtime/mcp/notifications.js +16 -2
- package/dist/runtime/mcp/notifications.js.map +1 -1
- package/dist/runtime/mcp/server.d.ts +3 -0
- package/dist/runtime/mcp/server.d.ts.map +1 -1
- package/dist/runtime/mcp/server.js +31 -15
- package/dist/runtime/mcp/server.js.map +1 -1
- package/dist/runtime/mcp/sessions.d.ts +6 -6
- package/dist/runtime/mcp/sessions.d.ts.map +1 -1
- package/dist/runtime/mcp/sessions.js +18 -6
- package/dist/runtime/mcp/sessions.js.map +1 -1
- package/dist/runtime/mcp/stdio.d.ts.map +1 -1
- package/dist/runtime/mcp/stdio.js +11 -0
- package/dist/runtime/mcp/stdio.js.map +1 -1
- package/dist/runtime/mcp/tools.js +1 -1
- package/dist/runtime/mcp/tools.js.map +1 -1
- package/dist/runtime/remote/client.d.ts +1 -1
- package/dist/runtime/remote/client.d.ts.map +1 -1
- package/dist/runtime/remote/client.js +1 -1
- package/dist/runtime/remote/client.js.map +1 -1
- package/dist/runtime/send.d.ts +10 -0
- package/dist/runtime/send.d.ts.map +1 -1
- package/dist/runtime/send.js +80 -5
- package/dist/runtime/send.js.map +1 -1
- package/dist/runtime/upgrade/index.d.ts +1 -0
- package/dist/runtime/upgrade/index.d.ts.map +1 -1
- package/dist/runtime/upgrade/index.js +1 -0
- package/dist/runtime/upgrade/index.js.map +1 -1
- package/dist/vendor/engram-live/contracts.d.ts +208 -0
- package/dist/vendor/engram-live/contracts.js +558 -0
- package/fixtures/native-host-catalog.json +1 -0
- package/fixtures/native-host-history.json +1261 -0
- package/fixtures/native-host-renderers.json +1 -0
- package/hooks/deny-manual-h2a-cli.mjs +38 -0
- package/hooks/hooks.json +17 -0
- package/hooks/vibe-post-agent.mjs +29 -0
- package/hooks/vibe-pre-tool.mjs +40 -0
- package/package.json +9 -6
- package/scripts/qualification-budget.mjs +56 -0
- package/scripts/qualification-cleanup.mjs +15 -0
- package/scripts/qualification-oracles.mjs +44 -0
- package/scripts/qualification-patch.mjs +21 -0
- package/scripts/qualification-poll-hosts.mjs +19 -0
- package/scripts/qualification-probe.mjs +63 -0
- package/scripts/qualification-rpc.mjs +59 -0
- package/scripts/qualification-vibe.mjs +159 -0
- package/scripts/qualify-host.mjs +143 -0
- package/skills/h2a/SKILL.md +6 -2
package/dist/cli.js
CHANGED
|
@@ -62,6 +62,13 @@ import { runHarnessCli, HARNESS_SKILLS } from "./vendor/harness/index.js";
|
|
|
62
62
|
import { buildLaunchIndex } from "./runtime/local-files/launch-index.js";
|
|
63
63
|
import { renderCommandMap } from "./cli-command-map.js";
|
|
64
64
|
import { resolveHostConfigRoot } from "./runtime/host-config-root.js";
|
|
65
|
+
import { isNativeHost, NATIVE_HOSTS, REGISTRY_WAITING, stateRoot, vibeHome } from "./hosts/native-paths.js";
|
|
66
|
+
import { convergeNativeHost } from "./hosts/native-convergence.js";
|
|
67
|
+
import { nativeSkillArtifacts } from "./hosts/native-skills.js";
|
|
68
|
+
import { assertNativePath, atomicNativeWrite, snapshot as snapshotNativeFile } from "./hosts/native-files.js";
|
|
69
|
+
import { clearVibeDirectRemoved, editVibeMcpServers, setVibeDirectRemoved, VIBE_MCP_NAME } from "./hosts/vibe-mcp.js";
|
|
70
|
+
import { convergeAfterExplicitUpgrade } from "./hosts/upgrade-equipment.js";
|
|
71
|
+
import { captureHostOperatorNotices } from "./hosts/operator-notices.js";
|
|
65
72
|
import { H2A_ATTESTER_COMPREHENSION_RIGHT, H2A_COMPREHENSION_ATTESTATION_BODY_KIND, H2A_DECLARATION_INTERET_BODY_KIND, H2A_ORG_MANIFEST_FILENAME, H2A_ORG_PROPOSAL_BODY_KIND, H2A_ORG_RATIFIED_BODY_KIND, H2A_ROLES, H2A_WORK_STATUSES, auditNhiPosture, buildComprehensionAttestation, canAttestComprehension, checkEnvelopeFreshness, computeHash, createEnvelope, deriveWorkspaceId, diffOrgManifest, effectiveOrgInstances, isComprehensionAttestation, nhiAttestationEnvelope, nhiInventory, deriveConnectionConfidence, nhiTrustBundle, orgAssignmentEnvelope, parseOrgManifest, signCanonical, signEnvelope, subagentAddress, validateOrgManifest, verifyComprehensionAttestation } from "@sentropic/h2a";
|
|
66
73
|
import { H2A_CLAUDE_HOST } from "./hosts/claude.js";
|
|
67
74
|
import { H2A_CODEX_HOST } from "./hosts/codex.js";
|
|
@@ -263,9 +270,9 @@ export function renderCliHelp() {
|
|
|
263
270
|
" h2a drumbeat escalations [--root <path>]",
|
|
264
271
|
" h2a drumbeat relance-inbox [--instance <id>] [--relauncher logging|local-tmux|headless|auto] [--root <path>]",
|
|
265
272
|
" h2a drumbeat watch [--interval-ms <n>] [--max-relances <n>] [--relauncher logging|local-tmux|remote|headless|auto] [--instance <signer> --private-key <pem>] [--decider logging|<command>] [--decider-after <k>] [--decider-enforce] [--root <path>]",
|
|
266
|
-
" h2a host setup --host <codex|claude|gemini|agy|hermes|opencode|muse|vibe> [--endpoint local|remote] [--url <https://…/mcp>] [--root <path>] [--print | --write <file>] [--force] [--no-wake] (selects
|
|
273
|
+
" h2a host setup --host <codex|claude|gemini|agy|hermes|opencode|muse|vibe> [--endpoint local|remote] [--url <https://…/mcp>] [--root <path>] [--print | --write <file>] [--force] [--no-wake] (selects one endpoint; Muse/agy render nonrecommended historical direct paths pending registry bootstrap; Vibe --write installs the interim direct entry pending registry bootstrap (--remove clears it for migration))",
|
|
267
274
|
" h2a host status [--host <name>]",
|
|
268
|
-
" h2a host plugin --host <codex|claude|gemini|agy|hermes|opencode|muse|vibe> --instance <id> [--status <work-status>] [--root <path>] [--write <settings.json> [--force]] [--scaffold <dir>] (--write installs the Stop hook for claude|gemini|codex|hermes|opencode; --scaffold writes codex's full local marketplace + trust step; vibe
|
|
275
|
+
" h2a host plugin --host <codex|claude|gemini|agy|hermes|opencode|muse|vibe> --instance <id> [--status <work-status>] [--root <path>] [--write <settings.json|hooks.toml> [--force]] [--scaffold <dir>] (--write installs the Stop hook for claude|gemini|codex|hermes|opencode; --scaffold writes codex's full local marketplace + trust step; vibe writes its explicit parent-only wrapper to VIBE_HOME/hooks.toml; agy and muse have no qualified native hooks)",
|
|
269
276
|
" h2a store migrate [--from <v>] [--to <v>] [--sanitize-paths] [--dry-run] [--root <path>]",
|
|
270
277
|
" h2a store index-launch --root <absolute-path> (derived indexes; original logs retained)",
|
|
271
278
|
"",
|
|
@@ -274,7 +281,7 @@ export function renderCliHelp() {
|
|
|
274
281
|
" h2a conductor [--workspace <id|path>] [--root <path>] (who is the live conductor/owner of a workspace — derived from presence; conductor=role CONDUCTOR if set, else null; candidates=in-workspace live agents)",
|
|
275
282
|
" h2a conductor-launch-check [--workspace <id|path>] [--root <path>] [--idle-ms <ms>] (DRY-RUN: polls track workspace-activity; recommends launching a conductor if work is stalled and none is live — h2a does NOT spawn; launch parked pending spawn policy + remote)",
|
|
276
283
|
" h2a conductor-launch --workspace <id|path> [--root <path>] [--idle-ms <ms>] [--confirm] [--remote <instance>] [--instance <self>] (D3 EMIT: if stalled+no conductor, emits a launch-REQUEST envelope to a live remote agent — gated by --confirm + 1/30min/workspace cap; h2a NEVER spawns; remote does the actual spawn)",
|
|
277
|
-
" h2a doctor [--root <path>] [--scan <dir>] [--prune] [--repair] [--dry-run] (--repair converges
|
|
284
|
+
" h2a doctor [--root <path>] [--scan <dir>] [--prune] [--repair] [--dry-run] [--host <name>] [--skip-hosts] [--guard on|off] [--lifecycle on|off] (--repair converges detected host components; --repair --dry-run reports host findings/actions without changing them; --prune deletes host-less/phantom/orphan inbox dirs + stray buses; dry-run by default)",
|
|
278
285
|
" h2a keepalive [--root <path>] [--interval <ms>] [--once] (external keepalive prober — refreshes presence for agents whose tmux pane is still alive)",
|
|
279
286
|
" h2a rename --instance <id> --name <name> [--root <path>] (set a live session's display name so peers can find it via discover --name)",
|
|
280
287
|
" h2a status [--root <path>] [--scope <s>] [--instance <i>]",
|
|
@@ -1486,19 +1493,20 @@ export function cmdUpgrade(flags, streams, runtime) {
|
|
|
1486
1493
|
// the boot auto-upgrade (bounded — no infinite terminal wait). No cachePath is
|
|
1487
1494
|
// passed, so it always checks fresh and is never throttled for an explicit run.
|
|
1488
1495
|
const result = performAutoUpgrade(current, { ...(runtime ? { runtime } : {}) });
|
|
1496
|
+
const hosts = convergeAfterExplicitUpgrade(result, flags);
|
|
1489
1497
|
if (result.outcome === "upgraded") {
|
|
1490
|
-
streams.stdout.write(`${JSON.stringify({ ok: true, current, latest: result.target, upgraded: true, verified: result.verifiedVersion, package: H2A_CLI_PACKAGE, ...(result.logPath ? { log: result.logPath } : {}) }, null, 2)}\n`);
|
|
1498
|
+
streams.stdout.write(`${JSON.stringify({ ok: true, current, latest: result.target, upgraded: true, verified: result.verifiedVersion, package: H2A_CLI_PACKAGE, hosts, ...(result.logPath ? { log: result.logPath } : {}) }, null, 2)}\n`);
|
|
1491
1499
|
return 0;
|
|
1492
1500
|
}
|
|
1493
1501
|
if (result.outcome === "already-current") {
|
|
1494
|
-
streams.stdout.write(`${JSON.stringify({ ok: true, current, latest: result.target ?? current, upgraded: false, reason: "already current or registry unreachable" }, null, 2)}\n`);
|
|
1502
|
+
streams.stdout.write(`${JSON.stringify({ ok: true, current, latest: result.target ?? current, upgraded: false, hosts, reason: "already current or registry unreachable" }, null, 2)}\n`);
|
|
1495
1503
|
return 0;
|
|
1496
1504
|
}
|
|
1497
1505
|
// The target is published but not yet installable (propagation delay). A full
|
|
1498
1506
|
// `npm i -g` would hit the same registry and fail too, so we do not force it;
|
|
1499
1507
|
// the staged path retries on a later run. Nothing was mutated.
|
|
1500
1508
|
if (result.outcome === "deferred-propagation") {
|
|
1501
|
-
streams.stdout.write(`${JSON.stringify({ ok: true, current, latest: result.target, upgraded: false, reason: "update published but not yet installable; retry later", ...(result.logPath ? { log: result.logPath } : {}) }, null, 2)}\n`);
|
|
1509
|
+
streams.stdout.write(`${JSON.stringify({ ok: true, current, latest: result.target, upgraded: false, hosts, reason: "update published but not yet installable; retry later", ...(result.logPath ? { log: result.logPath } : {}) }, null, 2)}\n`);
|
|
1502
1510
|
return 0;
|
|
1503
1511
|
}
|
|
1504
1512
|
// skipped-locked / failed.
|
|
@@ -3625,6 +3633,105 @@ function driveLineFromHookInput(raw) {
|
|
|
3625
3633
|
}
|
|
3626
3634
|
return driveLineFromText(raw);
|
|
3627
3635
|
}
|
|
3636
|
+
function resolveVibeSetupTarget(flags) {
|
|
3637
|
+
if (flags.write && flags.write !== "true")
|
|
3638
|
+
return flags.write;
|
|
3639
|
+
return join(vibeHome(), "config.toml");
|
|
3640
|
+
}
|
|
3641
|
+
function vibeWantedEntry(snippet) {
|
|
3642
|
+
const incoming = snippet.config.mcpServers.h2a;
|
|
3643
|
+
if (typeof incoming.url === "string")
|
|
3644
|
+
return { name: VIBE_MCP_NAME, transport: "streamable-http", url: incoming.url };
|
|
3645
|
+
return { name: VIBE_MCP_NAME, transport: "stdio", command: incoming.command ?? "h2a", args: [...(incoming.args ?? ["mcp-serve"])] };
|
|
3646
|
+
}
|
|
3647
|
+
function vibeStateForTarget(targetPath) {
|
|
3648
|
+
return stateRoot("vibe", dirname(resolvePath(targetPath)));
|
|
3649
|
+
}
|
|
3650
|
+
function cmdVibeSetupWrite(flags, streams, snippet, endpoint) {
|
|
3651
|
+
const targetPath = resolveVibeSetupTarget(flags);
|
|
3652
|
+
if (!targetPath.toLowerCase().endsWith(".toml")) {
|
|
3653
|
+
streams.stderr.write(`h2a host setup: --write for Vibe MCP supports TOML only and will not rewrite ${targetPath}. Use the default <VIBE_HOME>/config.toml.\n`);
|
|
3654
|
+
return 1;
|
|
3655
|
+
}
|
|
3656
|
+
const wanted = vibeWantedEntry(snippet);
|
|
3657
|
+
let original;
|
|
3658
|
+
try {
|
|
3659
|
+
if (!existsSync(targetPath))
|
|
3660
|
+
original = "";
|
|
3661
|
+
else
|
|
3662
|
+
original = readFileSync(targetPath, "utf8");
|
|
3663
|
+
}
|
|
3664
|
+
catch (error) {
|
|
3665
|
+
streams.stderr.write(`h2a host setup: cannot read ${targetPath} (${error.message})\n`);
|
|
3666
|
+
return 3;
|
|
3667
|
+
}
|
|
3668
|
+
let edited;
|
|
3669
|
+
try {
|
|
3670
|
+
edited = editVibeMcpServers(original, wanted);
|
|
3671
|
+
}
|
|
3672
|
+
catch (error) {
|
|
3673
|
+
streams.stderr.write(`h2a host setup: ${targetPath} refused (${error.message}); invalid or incompatible TOML is never overwritten, even with --force.\n`);
|
|
3674
|
+
return 2;
|
|
3675
|
+
}
|
|
3676
|
+
if (!edited.changed) {
|
|
3677
|
+
clearVibeDirectRemoved(vibeStateForTarget(targetPath));
|
|
3678
|
+
streams.stdout.write(`${JSON.stringify({ ok: true, host: "vibe", endpoint, path: targetPath, merged: false, replacedH2a: false, removedH2aMcpServers: [], migration: "voie directe — non migrée" }, null, 2)}\n`);
|
|
3679
|
+
return 0;
|
|
3680
|
+
}
|
|
3681
|
+
try {
|
|
3682
|
+
atomicNativeWrite(targetPath, edited.text, { state: vibeStateForTarget(targetPath), expected: snapshotNativeFile(targetPath) });
|
|
3683
|
+
}
|
|
3684
|
+
catch (error) {
|
|
3685
|
+
streams.stderr.write(`h2a host setup: cannot write ${targetPath} (${error.message})\n`);
|
|
3686
|
+
return 3;
|
|
3687
|
+
}
|
|
3688
|
+
clearVibeDirectRemoved(vibeStateForTarget(targetPath));
|
|
3689
|
+
streams.stdout.write(`${JSON.stringify({ ok: true, host: "vibe", endpoint, path: targetPath, merged: true, replacedH2a: edited.replacedH2a, removedH2aMcpServers: edited.removed, migration: "voie directe — non migrée" }, null, 2)}\n`);
|
|
3690
|
+
streams.stderr.write(`# wrote the interim direct mcp_servers.h2a endpoint for host=vibe to ${targetPath} (owner 16/10, derogation O7.2; migration R5/R7: h2a host setup --host vibe --remove)\n# disabled duplicate h2a MCP entries: ${edited.removed.join(", ") || "none"}\n# ${snippet.path.hint}\n`);
|
|
3691
|
+
return 0;
|
|
3692
|
+
}
|
|
3693
|
+
function cmdVibeSetupRemove(flags, streams) {
|
|
3694
|
+
const targetPath = resolveVibeSetupTarget(flags);
|
|
3695
|
+
if (!targetPath.toLowerCase().endsWith(".toml")) {
|
|
3696
|
+
streams.stderr.write(`h2a host setup: --remove for Vibe MCP supports TOML only and will not rewrite ${targetPath}. Use the default <VIBE_HOME>/config.toml.\n`);
|
|
3697
|
+
return 1;
|
|
3698
|
+
}
|
|
3699
|
+
let original;
|
|
3700
|
+
try {
|
|
3701
|
+
if (!existsSync(targetPath))
|
|
3702
|
+
original = "";
|
|
3703
|
+
else
|
|
3704
|
+
original = readFileSync(targetPath, "utf8");
|
|
3705
|
+
}
|
|
3706
|
+
catch (error) {
|
|
3707
|
+
streams.stderr.write(`h2a host setup: cannot read ${targetPath} (${error.message})\n`);
|
|
3708
|
+
return 3;
|
|
3709
|
+
}
|
|
3710
|
+
let edited;
|
|
3711
|
+
try {
|
|
3712
|
+
edited = editVibeMcpServers(original, null);
|
|
3713
|
+
}
|
|
3714
|
+
catch (error) {
|
|
3715
|
+
streams.stderr.write(`h2a host setup: ${targetPath} refused (${error.message}); invalid or incompatible TOML is never overwritten, even with --force.\n`);
|
|
3716
|
+
return 2;
|
|
3717
|
+
}
|
|
3718
|
+
if (!edited.changed) {
|
|
3719
|
+
setVibeDirectRemoved(vibeStateForTarget(targetPath));
|
|
3720
|
+
streams.stdout.write(`${JSON.stringify({ ok: true, host: "vibe", path: targetPath, removed: false, migration: REGISTRY_WAITING }, null, 2)}\n`);
|
|
3721
|
+
return 0;
|
|
3722
|
+
}
|
|
3723
|
+
try {
|
|
3724
|
+
atomicNativeWrite(targetPath, edited.text, { state: vibeStateForTarget(targetPath), expected: snapshotNativeFile(targetPath) });
|
|
3725
|
+
}
|
|
3726
|
+
catch (error) {
|
|
3727
|
+
streams.stderr.write(`h2a host setup: cannot write ${targetPath} (${error.message})\n`);
|
|
3728
|
+
return 3;
|
|
3729
|
+
}
|
|
3730
|
+
setVibeDirectRemoved(vibeStateForTarget(targetPath));
|
|
3731
|
+
streams.stdout.write(`${JSON.stringify({ ok: true, host: "vibe", path: targetPath, removed: true, removedH2aMcpServers: edited.removed, migration: REGISTRY_WAITING }, null, 2)}\n`);
|
|
3732
|
+
streams.stderr.write(`# removed the interim direct mcp_servers.h2a endpoint for host=vibe from ${targetPath}; registry bootstrap expected.\n`);
|
|
3733
|
+
return 0;
|
|
3734
|
+
}
|
|
3628
3735
|
function cmdHostSetup(flags, streams, options) {
|
|
3629
3736
|
const host = flags.host;
|
|
3630
3737
|
if (!host) {
|
|
@@ -3714,9 +3821,16 @@ function cmdHostSetup(flags, streams, options) {
|
|
|
3714
3821
|
}
|
|
3715
3822
|
const targetPath = flags.write;
|
|
3716
3823
|
const printMode = flags.print === "true" || !targetPath;
|
|
3824
|
+
if (host === "vibe" && flags.remove === "true") {
|
|
3825
|
+
return cmdVibeSetupRemove(flags, streams);
|
|
3826
|
+
}
|
|
3827
|
+
if (host === "vibe" && targetPath) {
|
|
3828
|
+
return cmdVibeSetupWrite(flags, streams, snippet, endpoint);
|
|
3829
|
+
}
|
|
3717
3830
|
if (printMode && !targetPath) {
|
|
3718
|
-
streams.stdout.write(`${JSON.stringify(snippet.config, null, 2)}\n`);
|
|
3719
|
-
streams.stderr.write(
|
|
3831
|
+
streams.stdout.write(`${JSON.stringify(isNativeHost(host) ? { ...snippet.config, migration: REGISTRY_WAITING, hint: snippet.path.hint } : snippet.config, null, 2)}\n`);
|
|
3832
|
+
streams.stderr.write(isNativeHost(host) ? `# ${host}: rendu historique non recommandé; ${REGISTRY_WAITING}.\n# ${snippet.path.hint}\n` :
|
|
3833
|
+
`# ${host} — selected ${endpoint} h2a endpoint; paste this snippet under \`mcpServers\` in:\n# ${snippet.path.hint}\n# example path: ${snippet.path.example}\n`);
|
|
3720
3834
|
return reportHostSetupCoherence(host, streams, options) ? 0 : 2;
|
|
3721
3835
|
}
|
|
3722
3836
|
if (isUnsupportedHostWritePath(host, targetPath)) {
|
|
@@ -3803,6 +3917,7 @@ function cmdHostSetup(flags, streams, options) {
|
|
|
3803
3917
|
replacedH2a,
|
|
3804
3918
|
removedH2aMcpServers,
|
|
3805
3919
|
removedTrackMcpServers,
|
|
3920
|
+
...(isNativeHost(host) ? { migration: "voie directe — non migrée" } : {}),
|
|
3806
3921
|
...(coherent ? {} : { next: "h2a doctor --repair" })
|
|
3807
3922
|
}, null, 2)}\n`);
|
|
3808
3923
|
streams.stderr.write(`# wrote the selected ${endpoint} mcpServers.h2a endpoint for host=${host} to ${targetPath}\n# disabled duplicate h2a MCP entries: ${removedH2aMcpServers.join(", ") || "none"}\n# disabled standalone Track MCP entries: ${removedTrackMcpServers.join(", ") || "none"}\n# ${snippet.path.hint}\n`);
|
|
@@ -3865,7 +3980,17 @@ function cmdHostStatus(flags, streams) {
|
|
|
3865
3980
|
mcpAdapterShipped,
|
|
3866
3981
|
hostSetupShipped,
|
|
3867
3982
|
hostScenarioShipped,
|
|
3868
|
-
summary
|
|
3983
|
+
summary: hostScenarioShipped ? summary.replace('scenario shipped', 'rendu testé') : summary,
|
|
3984
|
+
...(isNativeHost(descriptor.host) ? { native: (() => {
|
|
3985
|
+
try {
|
|
3986
|
+
return nativeHostReport(descriptor.host, flags, false, false, undefined, streams.cwd?.());
|
|
3987
|
+
}
|
|
3988
|
+
catch (error) {
|
|
3989
|
+
return { ok: false, installation: 'inspection impossible', error: error.message };
|
|
3990
|
+
}
|
|
3991
|
+
})() } : {
|
|
3992
|
+
convergence: ['claude', 'codex'].includes(descriptor.host) ? 'historical doctor convergence; native parity unverified; registry non migré' : 'non couvert'
|
|
3993
|
+
})
|
|
3869
3994
|
};
|
|
3870
3995
|
});
|
|
3871
3996
|
streams.stdout.write(`${JSON.stringify({ ok: true, hosts }, null, 2)}\n`);
|
|
@@ -3885,6 +4010,24 @@ function cmdHostPlugin(flags, streams) {
|
|
|
3885
4010
|
streams.stderr.write("h2a host plugin: --instance <id> is required\n");
|
|
3886
4011
|
return 1;
|
|
3887
4012
|
}
|
|
4013
|
+
if (flags.host === 'vibe') {
|
|
4014
|
+
try {
|
|
4015
|
+
if (flags.instance === 'auto')
|
|
4016
|
+
throw new Error('non disponible — interface B non livrée');
|
|
4017
|
+
if (flags.write && resolvePath(flags.write) !== join(vibeHome(), 'hooks.toml')) {
|
|
4018
|
+
streams.stderr.write('h2a host plugin: this target is not available for vibe; post_agent requires the effective <VIBE_HOME>/hooks.toml TOML file.\n');
|
|
4019
|
+
return 1;
|
|
4020
|
+
}
|
|
4021
|
+
const result = nativeHostReport('vibe', flags, !!flags.write, false, undefined, streams.cwd?.());
|
|
4022
|
+
streams.stdout.write(`${JSON.stringify({ ...result, push: true, mechanism: 'vibe-hooks', record: 'rendered' in result ? result.rendered : '',
|
|
4023
|
+
receive: 'N-A', hint: 'post_agent in <VIBE_HOME>/hooks.toml; ' + ('lifecycleScope' in result ? result.lifecycleScope : 'hôte absent') }, null, 2)}\n`);
|
|
4024
|
+
return result.ok ? 0 : 2;
|
|
4025
|
+
}
|
|
4026
|
+
catch (error) {
|
|
4027
|
+
streams.stderr.write(`h2a host plugin: ${error.message}\n`);
|
|
4028
|
+
return 2;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
3888
4031
|
const render = renderStopHook(flags.host, {
|
|
3889
4032
|
instance: flags.instance,
|
|
3890
4033
|
...(flags.root ? { root: flags.root } : {}),
|
|
@@ -4715,6 +4858,14 @@ export function cmdConductorLaunch(argv, streams, opts) {
|
|
|
4715
4858
|
return 0;
|
|
4716
4859
|
}
|
|
4717
4860
|
function cmdDoctor(flags, streams, options = {}) {
|
|
4861
|
+
if (flags.host && !isNativeHost(flags.host) && !['claude', 'codex'].includes(flags.host)) {
|
|
4862
|
+
streams.stderr.write('h2a doctor: --host must be claude|codex|vibe|muse|agy\n');
|
|
4863
|
+
return 1;
|
|
4864
|
+
}
|
|
4865
|
+
if ((flags.guard || flags.lifecycle) && flags.host !== 'vibe') {
|
|
4866
|
+
streams.stderr.write('h2a doctor: --guard/--lifecycle require --host vibe\n');
|
|
4867
|
+
return 1;
|
|
4868
|
+
}
|
|
4718
4869
|
const cwd = streams.cwd ?? (() => process.cwd());
|
|
4719
4870
|
const root = resolveRoot(flags, cwd);
|
|
4720
4871
|
const report = {
|
|
@@ -4793,9 +4944,10 @@ function cmdDoctor(flags, streams, options = {}) {
|
|
|
4793
4944
|
// is allowed to touch host configuration/caches and invoke native host CLIs.
|
|
4794
4945
|
// That keeps an ordinary isolated-bus probe independent of the operator's
|
|
4795
4946
|
// personal Claude/Codex setup while retaining a fail-closed repair surface.
|
|
4796
|
-
if (flags.repair === "true") {
|
|
4947
|
+
if (flags.repair === "true" && (!flags.host || ['claude', 'codex'].includes(flags.host))) {
|
|
4797
4948
|
const dryRun = flags["dry-run"] === "true";
|
|
4798
|
-
const hostInstallations = (options.doctorHostInstallations ?? doctorHostInstallations)({ repair: true, dryRun
|
|
4949
|
+
const hostInstallations = (options.doctorHostInstallations ?? doctorHostInstallations)({ repair: true, dryRun,
|
|
4950
|
+
...(flags.host ? { hosts: [flags.host] } : {}) });
|
|
4799
4951
|
checks.hostInstallations = hostInstallations;
|
|
4800
4952
|
if (!hostInstallations.ok) {
|
|
4801
4953
|
report.ok = false;
|
|
@@ -4844,6 +4996,21 @@ function cmdDoctor(flags, streams, options = {}) {
|
|
|
4844
4996
|
message: "pass --repair to detect and converge Claude/Codex host installation drift"
|
|
4845
4997
|
};
|
|
4846
4998
|
}
|
|
4999
|
+
if (flags['skip-hosts'] !== 'true') {
|
|
5000
|
+
const selection = flags.host ? isNativeHost(flags.host) ? [flags.host] : [] : NATIVE_HOSTS;
|
|
5001
|
+
const hosts = selection.map(host => {
|
|
5002
|
+
try {
|
|
5003
|
+
const { instance, ...nativeFlags } = flags;
|
|
5004
|
+
return nativeHostReport(host, nativeFlags, flags.repair === 'true', false, undefined, cwd());
|
|
5005
|
+
}
|
|
5006
|
+
catch (error) {
|
|
5007
|
+
return { host, ok: false, installation: 'inspection impossible', error: error.message };
|
|
5008
|
+
}
|
|
5009
|
+
});
|
|
5010
|
+
checks.nativeHostInstallations = { ok: hosts.every(host => host.ok), hosts };
|
|
5011
|
+
if (!hosts.every(host => host.ok))
|
|
5012
|
+
report.ok = false;
|
|
5013
|
+
}
|
|
4847
5014
|
// ── Warning checks (do NOT flip report.ok) ──────────────────────────────
|
|
4848
5015
|
// (a) rootSource: record where the root came from
|
|
4849
5016
|
const rootInfo = resolveRootInfo(flags, cwd);
|
|
@@ -5453,7 +5620,9 @@ function cmdConnect(flags, streams) {
|
|
|
5453
5620
|
const followUp = [
|
|
5454
5621
|
flags.host === "remote"
|
|
5455
5622
|
? "Use the bridge-provided environment in the remote session Pod"
|
|
5456
|
-
: `
|
|
5623
|
+
: flags.host === "vibe" ? `Entrée directe intérimaire (décision owner 16/10, dérogation O7.2) : h2a host setup --host vibe --write ; migration registre R5/R7 : h2a host setup --host vibe --remove puis bootstrap.`
|
|
5624
|
+
: isNativeHost(flags.host) ? `Rendu historique non recommandé — ${REGISTRY_WAITING}; installation et activation par l’intégrateur registre.`
|
|
5625
|
+
: `Merge the snippet above under \`mcpServers\` in ${steps[1].pathExample}`,
|
|
5457
5626
|
identity.privateKeyPath
|
|
5458
5627
|
? `keypair ready: ${identity.privateKeyPath}`
|
|
5459
5628
|
: `legacy override: run h2a keys generate --instance ${instance} --root ${root} if needed`,
|
|
@@ -5565,7 +5734,7 @@ function targetSpecFor(host, cwd) {
|
|
|
5565
5734
|
// installed skills are listed at session open.
|
|
5566
5735
|
return {
|
|
5567
5736
|
host: "vibe",
|
|
5568
|
-
userBase: join(
|
|
5737
|
+
userBase: join(vibeHome(), "skills"),
|
|
5569
5738
|
projectBase: join(cwd, ".vibe", "skills"),
|
|
5570
5739
|
extension: "SKILL.md",
|
|
5571
5740
|
write: (base, skillName, _parsed, raw) => {
|
|
@@ -5704,7 +5873,7 @@ function readSkillFrom(srcDir, dirName, source, renameTo) {
|
|
|
5704
5873
|
raw = raw.replace(/^(name:[ \t]*).*$/m, `$1${installName}`);
|
|
5705
5874
|
parsed = { ...parsed, name: installName };
|
|
5706
5875
|
}
|
|
5707
|
-
return { kind: "skill", skill: { installName, parsed, raw, source } };
|
|
5876
|
+
return { kind: "skill", skill: { installName, parsed, raw, source, resourceDir: join(srcDir, dirName) } };
|
|
5708
5877
|
}
|
|
5709
5878
|
/**
|
|
5710
5879
|
* Resolve the on-disk `skills/` directory of an installed dependency package
|
|
@@ -5812,6 +5981,15 @@ function collectInstallableSkills() {
|
|
|
5812
5981
|
return { skills, sources, error: harnessRes.error };
|
|
5813
5982
|
return { skills, sources };
|
|
5814
5983
|
}
|
|
5984
|
+
function nativeHostReport(host, flags, repair, explicitInstall = false, root, cwd) {
|
|
5985
|
+
const collection = collectInstallableSkills();
|
|
5986
|
+
if (collection.error)
|
|
5987
|
+
throw new Error(collection.error.message);
|
|
5988
|
+
return convergeNativeHost({ host, root, cwd, version: currentCliVersion(), skills: collection.skills, renderGemini: renderSkillAsGeminiToml,
|
|
5989
|
+
sourcesComplete: collection.sources.every(s => s.dir !== null && s.count > 0), repair, explicitInstall,
|
|
5990
|
+
dryRun: flags['dry-run'] === 'true', force: flags.force === 'true', guard: flags.guard, lifecycle: flags.lifecycle,
|
|
5991
|
+
instance: flags.instance, busRoot: flags.root, status: flags.status });
|
|
5992
|
+
}
|
|
5815
5993
|
function cmdInstallSkills(flags, streams) {
|
|
5816
5994
|
const host = flags.host;
|
|
5817
5995
|
if (!host) {
|
|
@@ -5841,8 +6019,23 @@ function cmdInstallSkills(flags, streams) {
|
|
|
5841
6019
|
return collection.error.exit;
|
|
5842
6020
|
}
|
|
5843
6021
|
const targetBase = scope === "user" ? spec.userBase : spec.projectBase;
|
|
6022
|
+
if (scope === 'user' && (isNativeHost(host) || host === 'gemini')) {
|
|
6023
|
+
try {
|
|
6024
|
+
const result = nativeHostReport(host, flags, true, true, dirname(targetBase), cwd());
|
|
6025
|
+
streams.stdout.write(`${JSON.stringify({ ...result, scope, targetBase, installed: 'changed' in result ? result.changed : [], sources: collection.sources,
|
|
6026
|
+
prunedLegacy: [], skipped: 'conflicts' in result ? result.conflicts.map(path => ({ path, reason: 'customized or unknown provenance; preserved' })) : [],
|
|
6027
|
+
...(host === 'muse' ? { verifyHint: 'muse skills list # project scope requires workspace trust' } : {}),
|
|
6028
|
+
...(host === 'vibe' ? { verifyHint: 'vibe # skills load at session open; nouvelle session ou reprise propre' } : {}),
|
|
6029
|
+
...(host === 'agy' ? { importHint: 'agy plugin import gemini # then: agy plugin enable h2a' } : {}) }, null, 2)}\n`);
|
|
6030
|
+
return result.ok ? 0 : 2;
|
|
6031
|
+
}
|
|
6032
|
+
catch (error) {
|
|
6033
|
+
streams.stderr.write(`h2a install-skills: ${error.message}\n`);
|
|
6034
|
+
return 3;
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
5844
6037
|
// DEC-057: prune the pre-consolidation skill names if they linger on disk.
|
|
5845
|
-
const prunedLegacy = pruneLegacy(spec, targetBase);
|
|
6038
|
+
const prunedLegacy = isNativeHost(host) || host === 'gemini' ? [] : pruneLegacy(spec, targetBase);
|
|
5846
6039
|
const installed = [];
|
|
5847
6040
|
const skipped = [];
|
|
5848
6041
|
for (const skill of collection.skills) {
|
|
@@ -5860,7 +6053,18 @@ function cmdInstallSkills(flags, streams) {
|
|
|
5860
6053
|
continue;
|
|
5861
6054
|
}
|
|
5862
6055
|
try {
|
|
5863
|
-
|
|
6056
|
+
let targetPath;
|
|
6057
|
+
if (scope === 'project' && (isNativeHost(host) || host === 'gemini')) {
|
|
6058
|
+
const artifacts = nativeSkillArtifacts({ host, root: dirname(targetBase), project: true, version: currentCliVersion(), skills: [skill], renderGemini: renderSkillAsGeminiToml, historicalHashes: () => [] });
|
|
6059
|
+
for (const artifact of artifacts) {
|
|
6060
|
+
assertNativePath(artifact.path);
|
|
6061
|
+
mkdirSync(dirname(artifact.path), { recursive: true });
|
|
6062
|
+
writeFileSync(artifact.path, artifact.bytes);
|
|
6063
|
+
}
|
|
6064
|
+
targetPath = probeTarget;
|
|
6065
|
+
}
|
|
6066
|
+
else
|
|
6067
|
+
targetPath = spec.write(targetBase, skillName, skill.parsed, skill.raw);
|
|
5864
6068
|
installed.push(targetPath);
|
|
5865
6069
|
}
|
|
5866
6070
|
catch (error) {
|
|
@@ -6268,6 +6472,10 @@ export function runCli(argv = process.argv.slice(2), streams = {
|
|
|
6268
6472
|
stderr: process.stderr
|
|
6269
6473
|
}, options = {}) {
|
|
6270
6474
|
const { command, flags } = parseFlags(argv);
|
|
6475
|
+
if (streams.stderr === process.stderr && process.stderr.isTTY) {
|
|
6476
|
+
for (const notice of captureHostOperatorNotices())
|
|
6477
|
+
streams.stderr.write(`${notice}\n`);
|
|
6478
|
+
}
|
|
6271
6479
|
if (command && TRACK_FACADE_VERBS.has(command)) {
|
|
6272
6480
|
// ④: read-only + sync-write verbs run in-process; `focus` (async) keeps
|
|
6273
6481
|
// shelling out. Same `.track`, same lock. A native WRITE never falls back to
|