From 06d42b0c6e36de062e8278b1aa0dbe8e84107308 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 7 Oct 2026 10:41:15 -0400 Subject: [PATCH] feat: add versioned capability ownership and narrow deployment --- README.md | 3 + docs/capability-registry.md | 117 +++++ .../skills/fclt-capability-review/SKILL.md | 2 + plugins/fclt/skills/fclt-setup/SKILL.md | 6 + src/builtin-assets.ts | 2 +- src/capability-apply.ts | 153 +++++++ src/capability-files.ts | 219 +++++++++ src/capability-plan.ts | 410 +++++++++++++++++ src/capability-registry.ts | 274 ++++++++++++ src/capability.test.ts | 418 ++++++++++++++++++ src/capability.ts | 156 +++++++ src/index.ts | 9 + 12 files changed, 1768 insertions(+), 1 deletion(-) create mode 100644 docs/capability-registry.md create mode 100644 src/capability-apply.ts create mode 100644 src/capability-files.ts create mode 100644 src/capability-plan.ts create mode 100644 src/capability-registry.ts create mode 100644 src/capability.test.ts create mode 100644 src/capability.ts diff --git a/README.md b/README.md index dfed2ae8..aab4d1cc 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,8 @@ fclt deploy plan \ --json ``` +Use [`fclt capability`](./docs/capability-registry.md) for a versioned skills/plugin/MCP registry, host overlays, drift inspection, and one explicitly owned copy deployment. Native plugin files remain independently owned. + `deploy plan` reads canonical, target, and ownership state directly and never writes them. The content-addressed JSON plan fails closed on stale hashes, unsupported adapters, unresolved variables, lossy translation, corrupt ownership state, and path escape. Ownership is keyed by the @@ -626,6 +628,7 @@ fclt project render --root /.ai --project-root --check --json fclt project render --root /.ai --project-root --json fclt project render --root /.ai --project-root --rollback --json fclt deploy plan --help +fclt capability --help fclt setup codex-plugin [--dry-run] [--json] [--no-codex-install] fclt manage --dry-run fclt sync [tool] --dry-run diff --git a/docs/capability-registry.md b/docs/capability-registry.md new file mode 100644 index 00000000..d3f499e4 --- /dev/null +++ b/docs/capability-registry.md @@ -0,0 +1,117 @@ +# Versioned capability ownership + +`fclt capability` records where a skill, custom plugin, or MCP configuration artifact came from and deploys one explicitly owned copy at a time. A pinned digest detects source drift. An ownership receipt detects live edits before an update. An installer that replaces a skill symlink with a directory therefore produces a conflict instead of silently losing either version. + +The commands are separate from the read-only `deploy plan` text-file contract. Broad legacy `sync` and `manage` apply remain deprecated. + +## Registry and source snapshots + +Keep a registry in version control alongside reviewed source snapshots. This example uses `capabilities.json` under a canonical source root: + +```json +{ + "schemaVersion": 1, + "entries": [ + { + "id": "example-skill", + "kind": "skill", + "owner": "fclt", + "enabled": true, + "source": { + "path": "skills/example", + "origin": "https://example.org/skills", + "revision": "upstream-commit-or-version", + "sha256": "sha256:REPLACE_WITH_DIGEST" + }, + "target": ".agents/skills/example", + "platforms": ["darwin", "linux"], + "requires": [] + }, + { + "id": "example-native-plugin", + "kind": "plugin", + "owner": "native", + "source": { + "origin": "marketplace:example-plugin", + "revision": "1.0.0" + } + } + ] +} +``` + +`kind` is `skill`, `plugin`, or `mcp`. `owner` is `fclt`, `native`, or `external`. A native/external record describes registered intent; it does **not** prove installation, observed version, authentication, or readiness. Use its provider to verify those states. Its target is optional, and several independent entries may identify one shared provider config file. Any target overlap involving an fclt-owned entry is rejected. + +An fclt-owned source requires `path`, `origin`, `revision`, and `sha256`; it also requires a target. Source and target paths are relative to the explicit roots. A source can be a directory or one regular file. A skill requires a directory containing `SKILL.md`. An MCP entry deploys a complete standalone configuration artifact; it does not merge servers into shared provider settings. Native plugins retain ownership of their installed files. To distribute a custom plugin's authored package, register a separate fclt-owned staging target outside the native manager's directories, then install or update through that manager. + +The digest includes file bytes, paths, empty directories, and executable flags. Ownership, timestamps, and other permission bits are excluded. Copies use private permissions (directories/executables `0700`, other files `0600`). Nested symlinks, hardlinked files, special files, case-colliding paths, and snapshots over 32 MiB or 4096 nodes are rejected. Directories named `.git`, `node_modules`, `__pycache__`, `.local`, and files named `.env` are rejected rather than silently omitted. Prepare a clean snapshot; do not copy machine caches or secrets into canonical source. If a live target gains runtime caches, that entry reports a conflict and inventory continues for other entries. Caches are never silently excluded or deleted. Configure runtime-writing skills to store caches outside their managed directories, or review and archive the divergent target before an explicit redeployment. Compiled assets such as `dist` may be legitimate plugin inputs and require review before pinning. + +## Add, import, or update a capability + +1. Acquire the upstream version in an isolated staging directory. Keep download credentials in the acquisition tool's authorized runtime; never put values in the registry or source tree. +2. Inspect its files, license, source URL, and immutable revision. For an existing live installation, compare it with canonical source and explicitly choose the intended version. The CLI does not infer which divergent copy is newer or correct. +3. Place the reviewed snapshot under the canonical source root. Run `fclt capability digest --path /path/to/canonical/skills/example` and record the returned `sha256` with its upstream revision in the registry. Commit the source and registry together. +4. Inspect `inventory` and the selected `plan`, then apply its exact `planId`. Verify provider discovery or runtime behavior separately. + +Run upstream installers into staging, not into fclt-owned live directories. Replacing a live link or writing through a canonical link is detectable, but detection cannot reconstruct an unrecorded upstream version. Version control and acquisition provenance provide that history. The registry never downloads or executes upstream content. + +## Host, platform, and private overlays + +An entry may set `hosts`, `platforms`, and `overrides`. A matching platform override is applied first, then a matching host override. Each override has exactly one selector (`host` or `platform`) and may replace `enabled`, `source`, or `target`. `source` replacement must supply the complete pinned source object. Duplicate selectors are rejected. Host/platform allowlists are applied after these embedded overrides. + +```json +{ + "overrides": [ + { "platform": "linux", "enabled": false }, + { "host": "build-station", "enabled": true } + ] +} +``` + +For machine-owned or private choices, pass repeated `--overlay FILE` arguments. Each overlay has this shape: + +```json +{ + "schemaVersion": 1, + "entries": [ + { "id": "example-skill", "enabled": false } + ] +} +``` + +Overlay files apply in CLI order after embedded overrides and selection. They may explicitly enable an excluded entry. Patches may contain only `id`, `enabled`, `source`, `target`, and `requires`; unknown IDs, duplicate IDs, unknown fields, and ownership changes are rejected. Overlay paths stay relative to the same roots. Select which platform/machine/private files to pass in the machine configuration; the library implements their common merge and validation rules. Keep private files out of shared source control. Plan IDs include all manifest and overlay bytes. + +`requires` lists explicit **runtime** requirements, for example `env:API_TOKEN` or `op://vault/item/field`. Ordinary downloaded skill documents normally have none. Credentials used only to download or install a package belong to acquisition, not `requires`. An environment reference is available when that variable is nonempty; the command does not output or inject its value. A 1Password reference remains unresolved and blocks owned deployment in v1. Resolution belongs to an authorized provider integration; there is no override flag or secret store in this command. + +## Inspect and apply + +```sh +fclt capability inventory \ + --manifest /path/to/canonical/capabilities.json \ + --source-root /path/to/canonical \ + --target-root /path/to/tool-home-parent \ + --state-root /path/to/runtime/capabilities \ + --host build-station --platform linux \ + --overlay /path/to/machine/platform.json \ + --overlay /path/to/private/capabilities.json +``` + +Use the same arguments with `plan --id example-skill`, then `apply --id example-skill --expected-plan sha256:...`. All commands emit JSON; `--json` is accepted. The source root defaults to the manifest's directory; host and platform default to the current machine. Source, target, and runtime state must be separate at the actual asset paths. Runtime state may be elsewhere under the target home. No live home is inferred. + +| Status | Meaning and apply behavior | +| --- | --- | +| `create` | Target absent; copy the pinned source and record ownership. | +| `restore` | Previously owned target absent; explicitly restore it. | +| `adopt` | Target has identical bytes, or is an exact link to canonical source; record ownership and convert that link into a copy. | +| `update` | Target still matches its receipt; copy the new pinned source. | +| `current` | Source, target, provenance, and receipt match; apply is a no-op. | +| `conflict` | Source pin or live content diverged, binding changed, or a foreign/broken link exists; preserve it and refuse apply. | +| `blocked` | Source or a required runtime reference is unavailable; refuse apply. | +| `disabled` | Intentional exclusion; retain existing content and refuse apply. | +| `independent` | Native/external ownership; no reads of installed contents and no mutation. | + +A missing target never implies intentional disable; use `enabled: false` to express that choice. The `targetPresence` field reports `present`, `absent`, or `uninspected`; disabled fclt-owned entries inspect presence without reading content. Disabling stops management and retains existing files. It does not uninstall a capability or disable a native plugin in its provider. + +`plan` and `inventory` do not write state. `apply` rebuilds and compares the plan before writing. Stale plans fail. A per-target lock serializes cooperating applies; unexpected locks are not automatically removed. Updates stage and verify a copy in the runtime state directory, retain the previous target under `recovery-*/previous` there, publish, verify, and write the receipt. Runtime state and the target must share a filesystem; otherwise apply refuses before changing the target. Recovery content stays outside provider discovery. The apply result returns `recoveryPath`; save it with deployment evidence. The old content is never automatically deleted. If publication fails after moving the old target and the destination remains absent, the command restores it. A process kill may leave a transaction directory, lock, or missing target: inspect them, preserve recovery content, and plan recovery before clearing the stale lock. + +These checks protect against ordinary drift and cooperating deployments. Stop other installers while applying; the command does not provide an OS-level transaction against unrelated processes racing filesystem changes. A successful apply proves file content and provenance match. It does not prove provider discovery, plugin installation, MCP authentication, or tool execution. diff --git a/plugins/fclt/skills/fclt-capability-review/SKILL.md b/plugins/fclt/skills/fclt-capability-review/SKILL.md index db197456..8d735840 100644 --- a/plugins/fclt/skills/fclt-capability-review/SKILL.md +++ b/plugins/fclt/skills/fclt-capability-review/SKILL.md @@ -69,3 +69,5 @@ Drafting or recording does not authorize canonical apply, cross-scope promotion, tracker changes, publishing, or scheduler activation. Preserve those gates and native authorization. Do not infer effectiveness from apply: verify the producing workflow before resolving source writebacks. Keep unchanged, non-actionable scheduled results quiet unless periodic reports were requested. + +For a versioned capability registry, use `fclt capability inventory` with explicit source/target/state roots and host/platform overlays. Review source pins, ownership, drift, credential-reference availability, and retained disabled targets. Independent native records describe intent, not observed version or readiness. A successful file deployment does not prove provider discovery or execution. Preserve both sides of a conflict; do not repair it with an upstream installer in a managed output directory. diff --git a/plugins/fclt/skills/fclt-setup/SKILL.md b/plugins/fclt/skills/fclt-setup/SKILL.md index f6ca1376..021c8186 100644 --- a/plugins/fclt/skills/fclt-setup/SKILL.md +++ b/plugins/fclt/skills/fclt-setup/SKILL.md @@ -173,3 +173,9 @@ fclt templates init operating-model --global --update - assumptions and fresh-session state - exact undo or rollback path - what still needs approval + +## Versioned skill and plugin sources + +For registered skill, custom plugin, or MCP artifact updates, use `fclt capability inventory` and a per-entry `plan` before `apply --id ... --expected-plan ...`. Use the registry's explicit source, target, and runtime state roots; pass machine/private choices with ordered `--overlay` files. Acquire upstream updates into isolated staging, review the snapshot, and record its revision and `capability digest` before deploying. Do not run embedded installer or auto-update instructions against fclt-owned outputs, and do not use broad legacy sync to repair drift. Preserve divergent files for source selection. + +Native plugin managers own their installed files. Registered native versions are intended versions until verified with the provider. `enabled: false` stops fclt management but retains existing files; it is not a provider disable or uninstall operation. See the package's `docs/capability-registry.md` for the schema, conflict states, and recovery procedure. diff --git a/src/builtin-assets.ts b/src/builtin-assets.ts index 8b6d6177..6e7b2eb3 100644 --- a/src/builtin-assets.ts +++ b/src/builtin-assets.ts @@ -7,7 +7,7 @@ export const BUILTIN_OPERATING_MODEL_FILES = JSON.parse( export const BUILTIN_FCLT_CODEX_PLUGIN_FILES = JSON.parse( // biome-ignore lint/suspicious/noTemplateCurlyInString: Built-in plugin files intentionally contain literal render placeholders. - '{".codex-plugin/plugin.json":"{\\n \\"name\\": \\"fclt\\",\\n \\"version\\": \\"0.1.5\\",\\n \\"description\\": \\"Codex workflows and MCP tools for evidence reconciliation, writeback, evolution, and outcome review.\\",\\n \\"author\\": {\\n \\"name\\": \\"Hack Dance\\",\\n \\"url\\": \\"https://hack.dance\\"\\n },\\n \\"license\\": \\"MIT\\",\\n \\"keywords\\": [\\n \\"fclt\\",\\n \\"facult\\",\\n \\"codex\\",\\n \\"skills\\",\\n \\"mcp\\",\\n \\"writeback\\",\\n \\"evolution\\"\\n ],\\n \\"skills\\": \\"./skills/\\",\\n \\"mcpServers\\": \\"./.mcp.json\\",\\n \\"interface\\": {\\n \\"displayName\\": \\"fclt\\",\\n \\"shortDescription\\": \\"Feedback loops for AI work\\",\\n \\"longDescription\\": \\"Capture signal from real agent work, reconcile configured evidence, turn repeated findings into reviewed capability changes, and verify whether those changes improved the work that produced them.\\",\\n \\"developerName\\": \\"Hack Dance\\",\\n \\"category\\": \\"Productivity\\",\\n \\"capabilities\\": [\\"Read\\", \\"Write\\", \\"MCP\\"],\\n \\"defaultPrompt\\": [\\n \\"Use fclt to check this repo\'s AI capability setup.\\",\\n \\"Reconcile the configured evidence window and explain its coverage.\\",\\n \\"Record useful writeback from this work and review any repeated signal.\\"\\n ],\\n \\"brandColor\\": \\"#166534\\",\\n \\"composerIcon\\": \\"./assets/fclt-mark.png\\",\\n \\"logo\\": \\"./assets/fclt-mark.png\\"\\n }\\n}\\n",".mcp.json":"{\\n \\"mcpServers\\": {\\n \\"fclt\\": {\\n \\"command\\": \\"node\\",\\n \\"args\\": [\\"./scripts/fclt-mcp.cjs\\"],\\n \\"env\\": {\\n \\"FCLT_BIN\\": \\"fclt\\"\\n },\\n \\"cwd\\": \\".\\"\\n }\\n }\\n}\\n","scripts/fclt-mcp.cjs":"#!/usr/bin/env node\\n\\"use strict\\";\\n\\nconst fs = require(\\"node:fs\\");\\nconst os = require(\\"node:os\\");\\nconst path = require(\\"node:path\\");\\nconst runtime = require(\\"./fclt-runtime.cjs\\");\\n\\nconst DEFAULT_TIMEOUT_MS = Number(process.env.FCLT_MCP_TIMEOUT_MS || 60_000);\\nconst ACTIVITY_ACTION_DECIDE_CAPABILITY = \\"activity-action-decide-v1\\";\\nconst ACTIVITY_ACTION_RESOLVE_CAPABILITY = \\"activity-action-resolve-v1\\";\\nconst AUDIT_READ_ONLY_CAPABILITY = \\"audit-read-only-v1\\";\\nconst CONTENT_LENGTH_RE = /Content-Length:\\\\s*(\\\\d+)/i;\\nconst PLUGIN_ROOT = path.resolve(__dirname, \\"..\\");\\n\\nconst tools = [\\n {\\n name: \\"fclt_setup\\",\\n description:\\n \\"Bootstrap or repair the global fclt loop and optionally return an exact no-write project enrollment plan.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: {\\n type: \\"string\\",\\n enum: [\\"global\\", \\"global_and_project\\"],\\n },\\n cwd: { type: \\"string\\" },\\n dryRun: { type: \\"boolean\\" },\\n installCodexPlugin: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_runtime\\",\\n description:\\n \\"Discover, bootstrap, update, or roll back the verified fclt runtime used by this plugin.\\",\\n inputSchema: {\\n type: \\"object\\",\\n additionalProperties: false,\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\"status\\", \\"check\\", \\"policy\\", \\"stage\\", \\"apply\\", \\"rollback\\"],\\n },\\n version: { type: \\"string\\" },\\n expectedSha256: { type: \\"string\\", pattern: \\"^[a-f0-9]{64}$\\" },\\n expectedActiveVersion: { type: \\"string\\" },\\n pinnedVersion: { type: \\"string\\" },\\n clearPin: { type: \\"boolean\\" },\\n updateChecksEnabled: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_capability\\",\\n description:\\n \\"Inspect fclt capability, provenance, templates, snippets, adapters, and managed status without exposing secrets.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"scan\\",\\n \\"inventory\\",\\n \\"list\\",\\n \\"show\\",\\n \\"find\\",\\n \\"graph\\",\\n \\"adapters\\",\\n \\"managed_status\\",\\n \\"templates_list\\",\\n \\"snippet_list\\",\\n \\"snippet_show\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: {\\n type: \\"string\\",\\n enum: [\\n \\"skills\\",\\n \\"mcp\\",\\n \\"agents\\",\\n \\"automations\\",\\n \\"snippets\\",\\n \\"instructions\\",\\n ],\\n },\\n query: { type: \\"string\\" },\\n selector: { type: \\"string\\" },\\n graphMode: { type: \\"string\\", enum: [\\"show\\", \\"deps\\", \\"dependents\\"] },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_workflow\\",\\n description:\\n \\"Run typed writeback and evolution review operations. Canonical apply and cross-scope promotion are deliberately withheld.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"writeback_list\\",\\n \\"writeback_show\\",\\n \\"writeback_group\\",\\n \\"writeback_summarize\\",\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"evolve_assess\\",\\n \\"evolve_list\\",\\n \\"evolve_show\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_verify\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n id: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n category: {\\n type: \\"string\\",\\n enum: [\\"friction\\", \\"opportunity\\", \\"reusable-success\\"],\\n },\\n summary: { type: \\"string\\" },\\n details: { type: \\"string\\", maxLength: 2000 },\\n impact: { type: \\"string\\", maxLength: 1000 },\\n attemptedWorkaround: { type: \\"string\\", maxLength: 1000 },\\n desiredOutcome: { type: \\"string\\", maxLength: 1000 },\\n sensitivity: {\\n type: \\"string\\",\\n enum: [\\"public\\", \\"internal\\", \\"private\\"],\\n },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"array\\", items: { type: \\"string\\" } },\\n confidence: { type: \\"string\\", enum: [\\"low\\", \\"medium\\", \\"high\\"] },\\n by: { type: \\"string\\", enum: [\\"asset\\", \\"kind\\", \\"domain\\"] },\\n issue: { type: \\"string\\" },\\n disposition: {\\n type: \\"string\\",\\n enum: [\\"propose\\", \\"apply-local\\", \\"task\\", \\"resolve-watch\\", \\"defer\\"],\\n },\\n target: { type: \\"string\\" },\\n nextTrigger: { type: \\"string\\" },\\n expectedOutcome: { type: \\"string\\" },\\n append: { type: \\"string\\" },\\n reason: { type: \\"string\\" },\\n byProposal: { type: \\"string\\" },\\n effectiveness: {\\n type: \\"string\\",\\n enum: [\\"improved\\", \\"unchanged\\", \\"regressed\\", \\"inconclusive\\"],\\n },\\n note: { type: \\"string\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_sync\\",\\n description:\\n \\"Inspect managed state or preview a scoped tool sync. Apply and live adoption remain withheld pending transaction-safe APIs.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: { type: \\"string\\", enum: [\\"status\\", \\"preview\\"] },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n tool: { type: \\"string\\" },\\n },\\n required: [\\"action\\", \\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_registry\\",\\n description:\\n \\"Search and verify remote capability, run typed source reconciliation reviews, resolve one opaque activity action locator, or record one revision-bound signal decision. Canonical and external mutation remains withheld.\\",\\n inputSchema: {\\n oneOf: [\\n {\\n type: \\"object\\",\\n properties: {\\n action: { const: \\"activity_decide\\" },\\n locator: {\\n type: \\"string\\",\\n pattern: \\"^fclt-act-v[0-9]+\\\\\\\\.[a-f0-9]{64}\\\\\\\\.[a-f0-9]{64}$\\",\\n },\\n decision: {\\n type: \\"string\\",\\n enum: [\\"accept\\", \\"redirect\\", \\"reject\\", \\"defer\\"],\\n },\\n expectedRevision: { type: \\"integer\\", minimum: 1 },\\n actor: {\\n type: \\"string\\",\\n pattern: \\"^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$\\",\\n },\\n approvalReference: { type: \\"string\\", minLength: 1, maxLength: 500 },\\n note: { type: \\"string\\", minLength: 1, maxLength: 1000 },\\n redirectTarget: { type: \\"string\\", minLength: 1, maxLength: 500 },\\n approve: { const: true },\\n },\\n required: [\\n \\"action\\",\\n \\"locator\\",\\n \\"decision\\",\\n \\"expectedRevision\\",\\n \\"actor\\",\\n \\"approve\\",\\n ],\\n allOf: [\\n {\\n oneOf: [\\n {\\n required: [\\"approvalReference\\"],\\n not: { required: [\\"note\\"] },\\n },\\n {\\n required: [\\"note\\"],\\n not: { required: [\\"approvalReference\\"] },\\n },\\n ],\\n },\\n ],\\n additionalProperties: false,\\n },\\n {\\n type: \\"object\\",\\n properties: {\\n action: { const: \\"activity_resolve\\" },\\n locator: {\\n type: \\"string\\",\\n pattern: \\"^fclt-act-v[0-9]+\\\\\\\\.[a-f0-9]{64}\\\\\\\\.[a-f0-9]{64}$\\",\\n },\\n },\\n required: [\\"action\\", \\"locator\\"],\\n additionalProperties: false,\\n },\\n {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"search\\",\\n \\"verify_source\\",\\n \\"source_list\\",\\n \\"install_preview\\",\\n \\"update_check\\",\\n \\"reconcile_status\\",\\n \\"reconcile\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n query: { type: \\"string\\" },\\n source: { type: \\"string\\" },\\n item: { type: \\"string\\" },\\n as: { type: \\"string\\" },\\n since: {\\n type: \\"string\\",\\n pattern:\\n \\"^\\\\\\\\d{4}-\\\\\\\\d{2}-\\\\\\\\d{2}(?:T\\\\\\\\d{2}:\\\\\\\\d{2}:\\\\\\\\d{2}(?:\\\\\\\\.\\\\\\\\d+)?(?:Z|[+-]\\\\\\\\d{2}:\\\\\\\\d{2}))?$\\",\\n },\\n until: {\\n type: \\"string\\",\\n pattern:\\n \\"^\\\\\\\\d{4}-\\\\\\\\d{2}-\\\\\\\\d{2}(?:T\\\\\\\\d{2}:\\\\\\\\d{2}:\\\\\\\\d{2}(?:\\\\\\\\.\\\\\\\\d+)?(?:Z|[+-]\\\\\\\\d{2}:\\\\\\\\d{2}))?$\\",\\n },\\n sourceIds: {\\n type: \\"array\\",\\n items: {\\n type: \\"string\\",\\n pattern: \\"^[A-Za-z0-9][A-Za-z0-9._-]*$\\",\\n },\\n },\\n incremental: { type: \\"boolean\\" },\\n },\\n required: [\\"action\\"],\\n additionalProperties: false,\\n },\\n ],\\n },\\n },\\n {\\n name: \\"fclt_audit\\",\\n description:\\n \\"Run a structured, redacted, non-interactive fclt security audit with no report or index writes.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: { type: \\"string\\", enum: [\\"scan\\"] },\\n cwd: { type: \\"string\\" },\\n target: { type: \\"string\\" },\\n severity: {\\n type: \\"string\\",\\n enum: [\\"critical\\", \\"high\\", \\"medium\\", \\"low\\"],\\n },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_automation\\",\\n description:\\n \\"Read one aggregate activity set across all configured loops by default, or inspect one explicit loop scope. Schedule and canonical mutation remain CLI-only.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"autosync_status\\",\\n \\"loop_status\\",\\n \\"loop_activity\\",\\n \\"loop_preview\\",\\n ],\\n },\\n scope: {\\n type: \\"string\\",\\n enum: [\\"all\\", \\"global\\", \\"project\\"],\\n },\\n cwd: { type: \\"string\\" },\\n tool: { type: \\"string\\" },\\n },\\n required: [\\"action\\"],\\n oneOf: [\\n {\\n properties: {\\n action: { const: \\"loop_activity\\" },\\n scope: {\\n type: \\"string\\",\\n enum: [\\"all\\", \\"global\\", \\"project\\"],\\n default: \\"all\\",\\n },\\n },\\n required: [\\"action\\"],\\n },\\n {\\n properties: {\\n action: {\\n enum: [\\"autosync_status\\", \\"loop_status\\", \\"loop_preview\\"],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n },\\n required: [\\"action\\", \\"scope\\"],\\n },\\n ],\\n },\\n },\\n {\\n name: \\"fclt_status\\",\\n description:\\n \\"Return fclt status for the current, global, or project scope.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_doctor\\",\\n description: \\"Run read-only fclt doctor checks and return JSON output.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_paths\\",\\n description: \\"Return canonical, generated, review, and runtime fclt paths.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_init_operating_model\\",\\n description: \\"Install or update the built-in operating-model pack.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n update: { type: \\"boolean\\" },\\n dryRun: { type: \\"boolean\\" },\\n force: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_writeback_add\\",\\n description: \\"Record a durable fclt writeback with evidence.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n category: {\\n type: \\"string\\",\\n enum: [\\"friction\\", \\"opportunity\\", \\"reusable-success\\"],\\n },\\n summary: { type: \\"string\\" },\\n details: { type: \\"string\\", maxLength: 2000 },\\n impact: { type: \\"string\\", maxLength: 1000 },\\n attemptedWorkaround: { type: \\"string\\", maxLength: 1000 },\\n desiredOutcome: { type: \\"string\\", maxLength: 1000 },\\n sensitivity: {\\n type: \\"string\\",\\n enum: [\\"public\\", \\"internal\\", \\"private\\"],\\n },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"string\\" },\\n confidence: {\\n type: \\"string\\",\\n enum: [\\"low\\", \\"medium\\", \\"high\\"],\\n },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\", \\"kind\\", \\"summary\\", \\"evidence\\", \\"approve\\"],\\n },\\n },\\n {\\n name: \\"fclt_writeback_review\\",\\n description: \\"List, group, or summarize current fclt writebacks.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n mode: { type: \\"string\\", enum: [\\"list\\", \\"group\\", \\"summarize\\"] },\\n by: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_evolve\\",\\n description:\\n \\"Assess, list, propose, draft, or review fclt evolution proposals.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n action: {\\n type: \\"string\\",\\n enum: [\\"assess\\", \\"list\\", \\"propose\\", \\"draft\\", \\"review\\", \\"show\\"],\\n },\\n id: { type: \\"string\\" },\\n asset: { type: \\"string\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n },\\n },\\n];\\n\\nfor (const tool of tools) {\\n if (!tool.inputSchema.oneOf || tool.inputSchema.properties) {\\n tool.inputSchema.additionalProperties = false;\\n }\\n}\\n\\nfunction isPlainObject(value) {\\n return Boolean(value) && typeof value === \\"object\\" && !Array.isArray(value);\\n}\\n\\nfunction validateToolArguments(name, args) {\\n if (!isPlainObject(args)) {\\n throw new Error(`${name} arguments must be an object`);\\n }\\n const tool = tools.find((entry) => entry.name === name);\\n if (!tool) {\\n throw new Error(`Unknown tool: ${name}`);\\n }\\n const rootSchema = tool.inputSchema;\\n const schema =\\n rootSchema.oneOf && !rootSchema.properties\\n ? rootSchema.oneOf.find((branch) => {\\n const action = branch.properties?.action;\\n return (\\n action?.const === args.action || action?.enum?.includes(args.action)\\n );\\n })\\n : rootSchema;\\n if (!schema) {\\n throw new Error(`${name}.action is not an allowed value`);\\n }\\n const properties = schema.properties || {};\\n const unknown = Object.keys(args).filter((key) => !(key in properties));\\n if (unknown.length > 0) {\\n throw new Error(\\n `${name} received unknown argument fields: ${unknown.join(\\", \\")}`\\n );\\n }\\n for (const required of schema.required || []) {\\n if (!(required in args)) {\\n throw new Error(`${name} requires ${required}`);\\n }\\n }\\n for (const constraint of schema.allOf || []) {\\n if (Array.isArray(constraint.oneOf)) {\\n const matches = constraint.oneOf.filter((branch) => {\\n const hasRequired = (branch.required || []).every((key) => key in args);\\n const violatesNot = branch.not?.required\\n ? branch.not.required.every((key) => key in args)\\n : false;\\n return hasRequired && !violatesNot;\\n });\\n if (matches.length !== 1) {\\n throw new Error(\\n `${name} arguments do not match exactly one allowed shape`\\n );\\n }\\n }\\n }\\n for (const [key, value] of Object.entries(args)) {\\n const property = properties[key];\\n if (property.const !== undefined && value !== property.const) {\\n throw new Error(`${name}.${key} is not an allowed value`);\\n }\\n if (!property.type) {\\n continue;\\n }\\n const validType =\\n property.type === \\"array\\"\\n ? Array.isArray(value)\\n : property.type === \\"object\\"\\n ? isPlainObject(value)\\n : property.type === \\"integer\\"\\n ? Number.isSafeInteger(value)\\n : typeof value === property.type;\\n if (!validType) {\\n throw new Error(`${name}.${key} must be ${property.type}`);\\n }\\n if (property.enum && !property.enum.includes(value)) {\\n throw new Error(`${name}.${key} is not an allowed value`);\\n }\\n if (\\n typeof value === \\"string\\" &&\\n ((property.minLength !== undefined &&\\n value.length < property.minLength) ||\\n (property.maxLength !== undefined && value.length > property.maxLength))\\n ) {\\n throw new Error(`${name}.${key} has an invalid length`);\\n }\\n if (\\n typeof value === \\"number\\" &&\\n property.minimum !== undefined &&\\n value < property.minimum\\n ) {\\n throw new Error(`${name}.${key} is below the minimum`);\\n }\\n if (property.pattern && !new RegExp(property.pattern).test(value)) {\\n throw new Error(`${name}.${key} has an invalid format`);\\n }\\n if (\\n property.type === \\"array\\" &&\\n property.items?.type &&\\n value.some((item) => typeof item !== property.items.type)\\n ) {\\n throw new Error(`${name}.${key} contains an invalid item`);\\n }\\n if (\\n property.type === \\"array\\" &&\\n property.items?.pattern &&\\n value.some((item) => !new RegExp(property.items.pattern).test(item))\\n ) {\\n throw new Error(`${name}.${key} contains an invalid item`);\\n }\\n }\\n}\\n\\nfunction scopeArgs(scope) {\\n if (scope === \\"global\\") {\\n return [\\"--global\\"];\\n }\\n if (scope === \\"project\\") {\\n return [\\"--project\\"];\\n }\\n return [];\\n}\\n\\nfunction boolFlag(name, value) {\\n return value ? [name] : [];\\n}\\n\\nfunction stringFlag(name, value) {\\n return typeof value === \\"string\\" && value.trim() ? [name, value] : [];\\n}\\n\\nfunction repeatedStringFlag(name, values) {\\n return Array.isArray(values)\\n ? values.flatMap((value) => stringFlag(name, value))\\n : [];\\n}\\n\\nfunction requireString(name, value) {\\n if (typeof value !== \\"string\\" || !value.trim()) {\\n throw new Error(`${name} is required`);\\n }\\n return value;\\n}\\n\\nfunction requireMutationApproval(name, args) {\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(`${name} requires an explicit global or project scope`);\\n }\\n if (args.approve !== true) {\\n throw new Error(`${name} requires approve=true`);\\n }\\n}\\n\\nfunction capabilityCommand(args) {\\n const action = args.action;\\n if (action === \\"scan\\") {\\n return [\\"scan\\", \\"--json\\"];\\n }\\n if (action === \\"inventory\\") {\\n return [\\"inventory\\", ...scopeArgs(args.scope), \\"--json\\"];\\n }\\n if (action === \\"list\\") {\\n return [\\"list\\", args.kind || \\"skills\\", ...scopeArgs(args.scope), \\"--json\\"];\\n }\\n if (action === \\"show\\") {\\n return [\\n \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n ...scopeArgs(args.scope),\\n ];\\n }\\n if (action === \\"find\\") {\\n return [\\n \\"find\\",\\n requireString(\\"query\\", args.query),\\n ...scopeArgs(args.scope),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"graph\\") {\\n return [\\n \\"graph\\",\\n args.graphMode || \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n ...scopeArgs(args.scope),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"adapters\\") {\\n return [\\"adapters\\", \\"--json\\"];\\n }\\n if (action === \\"managed_status\\") {\\n return [\\"managed\\", ...scopeArgs(args.scope)];\\n }\\n if (action === \\"templates_list\\") {\\n return [\\"templates\\", \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"snippet_list\\") {\\n return [\\"snippets\\", \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"snippet_show\\") {\\n return [\\n \\"snippets\\",\\n \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported capability action: ${action}`);\\n}\\n\\nconst WORKFLOW_MUTATIONS = new Set([\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"writeback_dismiss\\",\\n \\"writeback_promote\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_accept\\",\\n \\"evolve_reject\\",\\n \\"evolve_supersede\\",\\n \\"evolve_verify\\",\\n]);\\n\\nconst WORKFLOW_ACTION_FIELDS = {\\n writeback_list: [],\\n writeback_show: [\\"id\\"],\\n writeback_group: [\\"by\\"],\\n writeback_summarize: [\\"by\\"],\\n writeback_add: [\\n \\"kind\\",\\n \\"category\\",\\n \\"summary\\",\\n \\"details\\",\\n \\"impact\\",\\n \\"attemptedWorkaround\\",\\n \\"desiredOutcome\\",\\n \\"sensitivity\\",\\n \\"asset\\",\\n \\"evidence\\",\\n \\"confidence\\",\\n \\"approve\\",\\n ],\\n writeback_link: [\\"id\\", \\"issue\\", \\"approve\\"],\\n writeback_disposition: [\\n \\"id\\",\\n \\"disposition\\",\\n \\"target\\",\\n \\"nextTrigger\\",\\n \\"expectedOutcome\\",\\n \\"approve\\",\\n ],\\n evolve_assess: [\\"asset\\"],\\n evolve_list: [],\\n evolve_show: [\\"id\\"],\\n evolve_propose: [\\"asset\\", \\"approve\\"],\\n evolve_draft: [\\"id\\", \\"append\\", \\"approve\\"],\\n evolve_review: [\\"id\\", \\"approve\\"],\\n evolve_verify: [\\"id\\", \\"effectiveness\\", \\"evidence\\", \\"note\\", \\"approve\\"],\\n};\\n\\nfunction requireOnlyWorkflowFields(args) {\\n const fields = WORKFLOW_ACTION_FIELDS[args.action];\\n if (!fields) {\\n return;\\n }\\n const allowed = new Set([\\"action\\", \\"scope\\", \\"cwd\\", ...fields]);\\n const unexpected = Object.keys(args).filter((key) => !allowed.has(key));\\n if (unexpected.length > 0) {\\n throw new Error(\\n `${args.action} received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n}\\n\\nfunction workflowCommand(args) {\\n const action = args.action;\\n requireOnlyWorkflowFields(args);\\n if (WORKFLOW_MUTATIONS.has(action)) {\\n requireMutationApproval(action, args);\\n }\\n const scope = scopeArgs(args.scope);\\n if (action === \\"writeback_list\\") {\\n return [\\"ai\\", \\"writeback\\", ...scope, \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"writeback_show\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"show\\",\\n requireString(\\"id\\", args.id),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_group\\" || action === \\"writeback_summarize\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n action === \\"writeback_group\\" ? \\"group\\" : \\"summarize\\",\\n ...stringFlag(\\"--by\\", args.by),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_add\\") {\\n if (!Array.isArray(args.evidence) || args.evidence.length === 0) {\\n throw new Error(\\"writeback_add requires at least one evidence reference\\");\\n }\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"add\\",\\n \\"--kind\\",\\n requireString(\\"kind\\", args.kind),\\n \\"--summary\\",\\n requireString(\\"summary\\", args.summary),\\n ...stringFlag(\\"--asset\\", args.asset),\\n ...stringFlag(\\"--category\\", args.category),\\n ...stringFlag(\\"--details\\", args.details),\\n ...stringFlag(\\"--impact\\", args.impact),\\n ...stringFlag(\\"--attempted-workaround\\", args.attemptedWorkaround),\\n ...stringFlag(\\"--desired-outcome\\", args.desiredOutcome),\\n ...stringFlag(\\"--sensitivity\\", args.sensitivity),\\n ...repeatedStringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_link\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"link\\",\\n requireString(\\"id\\", args.id),\\n \\"--issue\\",\\n requireString(\\"issue\\", args.issue),\\n ];\\n }\\n if (action === \\"writeback_disposition\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"disposition\\",\\n requireString(\\"id\\", args.id),\\n \\"--type\\",\\n requireString(\\"disposition\\", args.disposition),\\n ...stringFlag(\\"--target\\", args.target),\\n ...stringFlag(\\"--next-trigger\\", args.nextTrigger),\\n ...stringFlag(\\"--expected-outcome\\", args.expectedOutcome),\\n ];\\n }\\n if (action === \\"writeback_dismiss\\" || action === \\"writeback_promote\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n action === \\"writeback_dismiss\\" ? \\"dismiss\\" : \\"promote\\",\\n requireString(\\"id\\", args.id),\\n ];\\n }\\n if (action === \\"evolve_assess\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"assess\\",\\n ...stringFlag(\\"--asset\\", args.asset),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_list\\") {\\n return [\\"ai\\", \\"evolve\\", ...scope, \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"evolve_show\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"show\\",\\n requireString(\\"id\\", args.id),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_propose\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"propose\\",\\n \\"--asset\\",\\n requireString(\\"asset\\", args.asset),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_draft\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"draft\\",\\n requireString(\\"id\\", args.id),\\n ...stringFlag(\\"--append\\", args.append),\\n ];\\n }\\n if (action === \\"evolve_review\\" || action === \\"evolve_accept\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n action === \\"evolve_review\\" ? \\"review\\" : \\"accept\\",\\n requireString(\\"id\\", args.id),\\n ];\\n }\\n if (action === \\"evolve_reject\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"reject\\",\\n requireString(\\"id\\", args.id),\\n \\"--reason\\",\\n requireString(\\"reason\\", args.reason),\\n ];\\n }\\n if (action === \\"evolve_supersede\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"supersede\\",\\n requireString(\\"id\\", args.id),\\n \\"--by\\",\\n requireString(\\"byProposal\\", args.byProposal),\\n ];\\n }\\n if (action === \\"evolve_verify\\") {\\n if (!Array.isArray(args.evidence) || args.evidence.length === 0) {\\n throw new Error(\\"evolve_verify requires at least one evidence reference\\");\\n }\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"verify\\",\\n requireString(\\"id\\", args.id),\\n \\"--effectiveness\\",\\n requireString(\\"effectiveness\\", args.effectiveness),\\n ...repeatedStringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--note\\", args.note),\\n ];\\n }\\n throw new Error(`Unsupported workflow action: ${action}`);\\n}\\n\\nfunction syncCommand(args) {\\n if (args.action === \\"status\\") {\\n return [\\"managed\\", ...scopeArgs(args.scope)];\\n }\\n if (args.action === \\"preview\\") {\\n return [\\n \\"sync\\",\\n ...(args.tool ? [args.tool] : []),\\n \\"--dry-run\\",\\n ...scopeArgs(args.scope),\\n ];\\n }\\n throw new Error(`Unsupported sync action: ${args.action}`);\\n}\\n\\nfunction requireOnlyRegistryFields(args, fields) {\\n const allowed = new Set([\\"action\\", \\"scope\\", \\"cwd\\", ...fields]);\\n const unexpected = Object.keys(args).filter((key) => !allowed.has(key));\\n if (unexpected.length > 0) {\\n throw new Error(\\n `${args.action} received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n}\\n\\nfunction registryCommand(args) {\\n if (args.action === \\"activity_decide\\") {\\n const expectedFields = new Set([\\n \\"action\\",\\n \\"locator\\",\\n \\"decision\\",\\n \\"expectedRevision\\",\\n \\"actor\\",\\n \\"approvalReference\\",\\n \\"note\\",\\n \\"redirectTarget\\",\\n \\"approve\\",\\n ]);\\n const unexpected = Object.keys(args).filter(\\n (key) => !expectedFields.has(key)\\n );\\n if (unexpected.length > 0) {\\n throw new Error(\\n `activity_decide received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n if (args.approve !== true) {\\n throw new Error(\\"activity_decide requires approve=true\\");\\n }\\n if (!Number.isSafeInteger(args.expectedRevision)) {\\n throw new Error(\\n \\"activity_decide requires expectedRevision as an integer\\"\\n );\\n }\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n \\"decide\\",\\n requireString(\\"locator\\", args.locator),\\n \\"--decision\\",\\n requireString(\\"decision\\", args.decision),\\n \\"--expected-revision\\",\\n String(args.expectedRevision),\\n \\"--actor\\",\\n requireString(\\"actor\\", args.actor),\\n ...stringFlag(\\"--approval-ref\\", args.approvalReference),\\n ...stringFlag(\\"--note\\", args.note),\\n ...stringFlag(\\"--redirect-target\\", args.redirectTarget),\\n \\"--approve\\",\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"activity_resolve\\") {\\n const unexpected = Object.keys(args).filter(\\n (key) => key !== \\"action\\" && key !== \\"locator\\"\\n );\\n if (unexpected.length > 0) {\\n throw new Error(\\n `activity_resolve received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n \\"resolve\\",\\n requireString(\\"locator\\", args.locator),\\n \\"--json\\",\\n ];\\n }\\n if (\\n args.scope === \\"project\\" &&\\n args.action !== \\"reconcile_status\\" &&\\n args.action !== \\"reconcile\\"\\n ) {\\n throw new Error(`${args.action} only supports global scope`);\\n }\\n if (args.action === \\"search\\") {\\n requireOnlyRegistryFields(args, [\\"query\\"]);\\n return [\\"search\\", requireString(\\"query\\", args.query), \\"--json\\"];\\n }\\n if (args.action === \\"verify_source\\") {\\n requireOnlyRegistryFields(args, [\\"source\\"]);\\n return [\\"verify-source\\", requireString(\\"source\\", args.source), \\"--json\\"];\\n }\\n if (args.action === \\"source_list\\") {\\n requireOnlyRegistryFields(args, []);\\n return [\\"sources\\", \\"list\\", \\"--json\\"];\\n }\\n if (args.action === \\"install_preview\\") {\\n requireOnlyRegistryFields(args, [\\"item\\", \\"as\\"]);\\n return [\\n \\"install\\",\\n requireString(\\"item\\", args.item),\\n ...stringFlag(\\"--as\\", args.as),\\n \\"--dry-run\\",\\n \\"--strict-source-trust\\",\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"update_check\\") {\\n requireOnlyRegistryFields(args, []);\\n return [\\"update\\", \\"--strict-source-trust\\", \\"--json\\"];\\n }\\n if (args.action === \\"reconcile_status\\") {\\n requireOnlyRegistryFields(args, []);\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(\\n \\"reconcile_status requires an explicit global or project scope\\"\\n );\\n }\\n return [\\"ai\\", \\"review\\", ...scopeArgs(args.scope), \\"status\\", \\"--json\\"];\\n }\\n if (args.action === \\"reconcile\\") {\\n requireOnlyRegistryFields(args, [\\n \\"since\\",\\n \\"until\\",\\n \\"sourceIds\\",\\n \\"incremental\\",\\n ]);\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(\\"reconcile requires an explicit global or project scope\\");\\n }\\n return [\\n \\"ai\\",\\n \\"review\\",\\n ...scopeArgs(args.scope),\\n \\"reconcile\\",\\n \\"--since\\",\\n requireString(\\"since\\", args.since),\\n ...stringFlag(\\"--until\\", args.until),\\n ...repeatedStringFlag(\\"--source\\", args.sourceIds),\\n ...boolFlag(\\"--incremental\\", args.incremental),\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported registry action: ${args.action}`);\\n}\\n\\nfunction isSubpath(child, parent) {\\n const relative = path.relative(parent, child);\\n return (\\n relative === \\"\\" || !(relative.startsWith(\\"..\\") || path.isAbsolute(relative))\\n );\\n}\\n\\nfunction isDirectory(candidate) {\\n try {\\n return fs.statSync(candidate).isDirectory();\\n } catch {\\n return false;\\n }\\n}\\n\\nfunction resolveWorkspaceCwd({ allowHomeFallback = true } = {}) {\\n const candidates = [\\n process.env.FCLT_MCP_WORKSPACE_CWD,\\n process.env.INIT_CWD,\\n process.env.PWD,\\n ];\\n for (const candidate of candidates) {\\n if (typeof candidate !== \\"string\\" || !candidate.trim()) {\\n continue;\\n }\\n const resolved = path.resolve(candidate);\\n const isHome = resolved === path.resolve(os.homedir());\\n if (\\n (allowHomeFallback || !isHome) &&\\n !isSubpath(resolved, PLUGIN_ROOT) &&\\n isDirectory(resolved)\\n ) {\\n return resolved;\\n }\\n }\\n if (allowHomeFallback && isDirectory(os.homedir())) {\\n return os.homedir();\\n }\\n return undefined;\\n}\\n\\nfunction resolveToolCwd(name, args = {}) {\\n if (typeof args.cwd === \\"string\\" && args.cwd.trim()) {\\n return args.cwd;\\n }\\n const inferred = resolveWorkspaceCwd({\\n allowHomeFallback: args.scope !== \\"project\\",\\n });\\n if (inferred) {\\n return inferred;\\n }\\n if (args.scope === \\"project\\") {\\n throw new Error(\\n `${name} with project scope requires a cwd for the target workspace`\\n );\\n }\\n return process.cwd();\\n}\\n\\nfunction commandForTool(name, args = {}) {\\n switch (name) {\\n case \\"fclt_setup\\": {\\n const apply = args.dryRun === false;\\n if (apply && args.approve !== true) {\\n throw new Error(\\"fclt_setup apply requires approve=true\\");\\n }\\n if (\\n args.scope === \\"global_and_project\\" &&\\n (typeof args.cwd !== \\"string\\" || !args.cwd.trim())\\n ) {\\n throw new Error(\\n \\"fclt_setup global_and_project scope requires an explicit cwd\\"\\n );\\n }\\n return [\\n \\"setup\\",\\n \\"--json\\",\\n ...(args.scope === \\"global_and_project\\" ? [\\"--include-project\\"] : []),\\n ...(apply ? [] : [\\"--dry-run\\"]),\\n ...(args.installCodexPlugin === false ? [\\"--no-codex-plugin\\"] : []),\\n ];\\n }\\n case \\"fclt_capability\\":\\n return capabilityCommand(args);\\n case \\"fclt_workflow\\":\\n return workflowCommand(args);\\n case \\"fclt_sync\\":\\n return syncCommand(args);\\n case \\"fclt_registry\\":\\n return registryCommand(args);\\n case \\"fclt_audit\\":\\n return [\\n \\"audit\\",\\n \\"--non-interactive\\",\\n ...(args.target ? [args.target] : []),\\n ...stringFlag(\\"--severity\\", args.severity),\\n \\"--json\\",\\n ];\\n case \\"fclt_automation\\":\\n if (\\n args.action !== \\"loop_activity\\" &&\\n args.scope !== \\"global\\" &&\\n args.scope !== \\"project\\"\\n ) {\\n throw new Error(`${args.action} requires global or project scope`);\\n }\\n if (args.action === \\"autosync_status\\") {\\n return [\\n \\"autosync\\",\\n \\"status\\",\\n ...(args.tool ? [args.tool] : []),\\n ...scopeArgs(args.scope),\\n ];\\n }\\n if (args.tool) {\\n throw new Error(`${args.action} does not accept tool`);\\n }\\n if (args.action === \\"loop_status\\") {\\n return [\\"ai\\", \\"loop\\", ...scopeArgs(args.scope), \\"status\\", \\"--json\\"];\\n }\\n if (args.action === \\"loop_activity\\") {\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n ...scopeArgs(args.scope),\\n \\"activity\\",\\n ...(args.scope === \\"global\\" || args.scope === \\"project\\"\\n ? []\\n : [\\"--all\\"]),\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"loop_preview\\") {\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n ...scopeArgs(args.scope),\\n \\"run\\",\\n \\"--dry-run\\",\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported automation action: ${args.action}`);\\n case \\"fclt_status\\":\\n return [\\"status\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_doctor\\":\\n return [\\"doctor\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_paths\\":\\n return [\\"paths\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_init_operating_model\\":\\n if (args.dryRun === false && args.approve !== true) {\\n throw new Error(\\n \\"fclt_init_operating_model apply requires approve=true\\"\\n );\\n }\\n if (args.force === true && args.approve !== true) {\\n throw new Error(\\n \\"fclt_init_operating_model force requires approve=true\\"\\n );\\n }\\n return [\\n \\"templates\\",\\n \\"init\\",\\n \\"operating-model\\",\\n ...scopeArgs(args.scope),\\n ...boolFlag(\\"--update\\", args.update),\\n ...(args.dryRun === false ? [] : [\\"--dry-run\\"]),\\n ...boolFlag(\\"--force\\", args.force),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_add\\":\\n requireMutationApproval(name, args);\\n requireString(\\"evidence\\", args.evidence);\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scopeArgs(args.scope),\\n \\"add\\",\\n \\"--kind\\",\\n args.kind,\\n \\"--summary\\",\\n args.summary,\\n ...stringFlag(\\"--asset\\", args.asset),\\n ...stringFlag(\\"--category\\", args.category),\\n ...stringFlag(\\"--details\\", args.details),\\n ...stringFlag(\\"--impact\\", args.impact),\\n ...stringFlag(\\"--attempted-workaround\\", args.attemptedWorkaround),\\n ...stringFlag(\\"--desired-outcome\\", args.desiredOutcome),\\n ...stringFlag(\\"--sensitivity\\", args.sensitivity),\\n ...stringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_review\\": {\\n const mode = args.mode || \\"list\\";\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scopeArgs(args.scope),\\n mode,\\n ...stringFlag(\\"--by\\", args.by),\\n ];\\n }\\n case \\"fclt_evolve\\": {\\n const action = args.action || \\"list\\";\\n if ([\\"propose\\", \\"draft\\", \\"review\\"].includes(action)) {\\n requireMutationApproval(`fclt_evolve ${action}`, args);\\n }\\n if (action === \\"propose\\") {\\n requireString(\\"asset\\", args.asset);\\n }\\n if ([\\"draft\\", \\"review\\", \\"show\\"].includes(action)) {\\n requireString(\\"id\\", args.id);\\n }\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scopeArgs(args.scope),\\n action,\\n ...(action === \\"assess\\" || action === \\"propose\\"\\n ? stringFlag(\\"--asset\\", args.asset)\\n : []),\\n ...(args.id ? [args.id] : []),\\n ...(action === \\"assess\\" ? [\\"--json\\"] : []),\\n ];\\n }\\n default:\\n throw new Error(`Unknown tool: ${name}`);\\n }\\n}\\n\\nfunction operationMetadata(name, args, command) {\\n const action =\\n name === \\"fclt_writeback_add\\"\\n ? \\"writeback_add\\"\\n : name === \\"fclt_evolve\\"\\n ? `evolve_${args.action || \\"list\\"}`\\n : args.action || name;\\n const reviewActions = new Set([\\n \\"activity_decide\\",\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_verify\\",\\n \\"reconcile\\",\\n ]);\\n const preview = command.includes(\\"--dry-run\\");\\n const risk = reviewActions.has(action)\\n ? \\"review_producing\\"\\n : !preview && name === \\"fclt_setup\\"\\n ? \\"reversible_mutation\\"\\n : !preview && name === \\"fclt_init_operating_model\\"\\n ? \\"high_risk_destructive\\"\\n : \\"read_only\\";\\n return {\\n tool: name,\\n action,\\n risk,\\n scope:\\n args.scope ||\\n (name === \\"fclt_automation\\" && action === \\"loop_activity\\"\\n ? \\"all\\"\\n : \\"auto\\"),\\n target:\\n args.id ||\\n args.selector ||\\n args.asset ||\\n args.item ||\\n args.source ||\\n args.tool ||\\n args.locator ||\\n null,\\n preview,\\n };\\n}\\n\\nfunction recoveryForOperation(operation, stdout) {\\n if (operation.risk === \\"review_producing\\") {\\n return {\\n canonicalCapabilityChanged: false,\\n audit: \\"native fclt review artifacts and append-only journal\\",\\n };\\n }\\n if (\\n operation.risk !== \\"reversible_mutation\\" &&\\n operation.risk !== \\"high_risk_destructive\\"\\n ) {\\n return null;\\n }\\n const report = isPlainObject(stdout) ? stdout : {};\\n return {\\n rollbackAvailable: false,\\n changedPaths: Array.isArray(report.changedPaths) ? report.changedPaths : [],\\n skippedPaths: Array.isArray(report.skippedPaths) ? report.skippedPaths : [],\\n repairActions: Array.isArray(report.repairActions)\\n ? report.repairActions\\n : [],\\n verification:\\n operation.tool === \\"fclt_setup\\"\\n ? \\"rerun fclt_setup in preview mode and verify doctor readiness\\"\\n : \\"verify doctor, generated state, authored-file hashes, and exact target paths\\",\\n };\\n}\\n\\nasync function runFclt(args, cwd, operation) {\\n const discovery = await runtime.discoverRuntime();\\n if (!discovery.selected) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"no_compatible_runtime\\",\\n message:\\n \\"No compatible fclt runtime is available. Check, stage, and apply an explicit verified version with fclt_runtime.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.tool === \\"fclt_audit\\" &&\\n !discovery.selected.capabilities?.includes(AUDIT_READ_ONLY_CAPABILITY)\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise audit-read-only-v1; typed audit fails closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.action === \\"activity_decide\\" &&\\n !discovery.selected.capabilities?.includes(\\n ACTIVITY_ACTION_DECIDE_CAPABILITY\\n )\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise activity-action-decide-v1; typed activity decisions fail closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.action === \\"activity_resolve\\" &&\\n !discovery.selected.capabilities?.includes(\\n ACTIVITY_ACTION_RESOLVE_CAPABILITY\\n )\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise activity-action-resolve-v1; typed activity resolution fails closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n const result = await runtime.runCommand(discovery.selected.executable, args, {\\n cwd: cwd || process.cwd(),\\n env: process.env,\\n timeoutMs: DEFAULT_TIMEOUT_MS,\\n });\\n const parsedStdout = parseJsonOrText(result.stdout.trim());\\n return {\\n code: result.code,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n runtime: discovery.selected,\\n result: {\\n exitCode: result.code,\\n stdout: parsedStdout,\\n stderr: result.stderr,\\n },\\n verification: {\\n status: result.code === 0 ? \\"passed\\" : \\"failed\\",\\n exitCode: result.code,\\n },\\n recovery: recoveryForOperation(operation, parsedStdout),\\n },\\n null,\\n 2\\n ),\\n };\\n}\\n\\nfunction parseJsonOrText(value) {\\n if (!value) {\\n return \\"\\";\\n }\\n try {\\n return JSON.parse(value);\\n } catch {\\n return value;\\n }\\n}\\n\\nasync function handleRuntimeTool(args = {}) {\\n const action = args.action || \\"status\\";\\n if (action === \\"status\\") {\\n return await runtime.discoverRuntime();\\n }\\n if (action === \\"check\\") {\\n return await runtime.checkRuntimeUpdate();\\n }\\n if (action === \\"policy\\") {\\n return await runtime.setRuntimePolicy({\\n approve: args.approve,\\n pinnedVersion: args.pinnedVersion,\\n clearPin: args.clearPin,\\n updateChecksEnabled: args.updateChecksEnabled,\\n });\\n }\\n if (action === \\"stage\\") {\\n return await runtime.stageRuntime({\\n approve: args.approve,\\n version: args.version,\\n });\\n }\\n if (action === \\"apply\\") {\\n return await runtime.applyStagedRuntime({\\n approve: args.approve,\\n expectedSha256: args.expectedSha256,\\n version: args.version,\\n });\\n }\\n if (action === \\"rollback\\") {\\n return await runtime.rollbackRuntime({\\n approve: args.approve,\\n expectedActiveVersion: args.expectedActiveVersion,\\n });\\n }\\n throw new Error(`Unknown runtime action: ${action}`);\\n}\\n\\nfunction runtimeOperationMetadata(args, result) {\\n const action = args.action || \\"status\\";\\n const risk =\\n action === \\"status\\" || action === \\"check\\"\\n ? \\"read_only\\"\\n : action === \\"stage\\"\\n ? \\"review_producing\\"\\n : \\"high_risk_destructive\\";\\n return {\\n operation: {\\n tool: \\"fclt_runtime\\",\\n action,\\n risk,\\n scope: \\"plugin_runtime\\",\\n target:\\n args.version ||\\n args.pinnedVersion ||\\n args.expectedActiveVersion ||\\n null,\\n approved: args.approve === true,\\n },\\n verification: {\\n status: \\"passed\\",\\n activeVersion:\\n result.active?.packageVersion ||\\n result.selected?.packageVersion ||\\n null,\\n },\\n recovery:\\n action === \\"apply\\" || action === \\"rollback\\"\\n ? {\\n rollbackAvailable: result.rollbackAvailable === true,\\n previous: result.previous || null,\\n }\\n : action === \\"policy\\"\\n ? { previousPolicy: result.previous || null }\\n : null,\\n };\\n}\\n\\nlet transportFraming = \\"content-length\\";\\n\\nfunction send(message) {\\n const body = JSON.stringify(message);\\n if (transportFraming === \\"newline\\") {\\n process.stdout.write(`${body}\\\\n`);\\n return;\\n }\\n process.stdout.write(\\n `Content-Length: ${Buffer.byteLength(body)}\\\\r\\\\n\\\\r\\\\n${body}`\\n );\\n}\\n\\nasync function handle(message) {\\n if (!message || message.id == null) {\\n return;\\n }\\n\\n try {\\n if (message.method === \\"initialize\\") {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n protocolVersion: \\"2025-06-18\\",\\n capabilities: { tools: {} },\\n serverInfo: { name: \\"fclt\\", version: runtime.pluginVersion() },\\n },\\n });\\n return;\\n }\\n if (message.method === \\"tools/list\\") {\\n send({ jsonrpc: \\"2.0\\", id: message.id, result: { tools } });\\n return;\\n }\\n if (message.method === \\"tools/call\\") {\\n const { name, arguments: args = {} } = message.params || {};\\n validateToolArguments(name, args);\\n if (name === \\"fclt_runtime\\") {\\n const result = await handleRuntimeTool(args);\\n const metadata = runtimeOperationMetadata(args, result);\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n isError: false,\\n content: [\\n {\\n type: \\"text\\",\\n text: JSON.stringify({ ...result, ...metadata }, null, 2),\\n },\\n ],\\n },\\n });\\n return;\\n }\\n const command = commandForTool(name, args);\\n const result = await runFclt(\\n command,\\n resolveToolCwd(name, args),\\n operationMetadata(name, args, command)\\n );\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n isError: result.code !== 0,\\n content: [{ type: \\"text\\", text: result.text }],\\n },\\n });\\n return;\\n }\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n error: { code: -32_601, message: `Method not found: ${message.method}` },\\n });\\n } catch (error) {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n error: {\\n code: -32_000,\\n message: error instanceof Error ? error.message : String(error),\\n },\\n });\\n }\\n}\\n\\nlet buffer = Buffer.alloc(0);\\n\\nfunction dispatch(body, framing) {\\n transportFraming = framing;\\n handle(JSON.parse(body)).catch((error) => {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: null,\\n error: {\\n code: -32_000,\\n message: error instanceof Error ? error.message : String(error),\\n },\\n });\\n });\\n}\\n\\nprocess.stdin.on(\\"data\\", (chunk) => {\\n buffer = Buffer.concat([buffer, chunk]);\\n while (true) {\\n while (buffer[0] === 10 || buffer[0] === 13) {\\n buffer = buffer.subarray(1);\\n }\\n if (buffer.length === 0) {\\n return;\\n }\\n if (buffer[0] === 123 || buffer[0] === 91) {\\n const lineEnd = buffer.indexOf(\\"\\\\n\\");\\n if (lineEnd === -1) {\\n return;\\n }\\n const body = buffer.subarray(0, lineEnd).toString(\\"utf8\\").trim();\\n buffer = buffer.subarray(lineEnd + 1);\\n if (body) {\\n dispatch(body, \\"newline\\");\\n }\\n continue;\\n }\\n const headerEnd = buffer.indexOf(\\"\\\\r\\\\n\\\\r\\\\n\\");\\n if (headerEnd === -1) {\\n return;\\n }\\n const header = buffer.slice(0, headerEnd).toString(\\"utf8\\");\\n const match = CONTENT_LENGTH_RE.exec(header);\\n if (!match) {\\n buffer = Buffer.alloc(0);\\n return;\\n }\\n const length = Number(match[1]);\\n const frameEnd = headerEnd + 4 + length;\\n if (buffer.length < frameEnd) {\\n return;\\n }\\n const body = buffer.slice(headerEnd + 4, frameEnd).toString(\\"utf8\\");\\n buffer = buffer.slice(frameEnd);\\n dispatch(body, \\"content-length\\");\\n }\\n});\\n\\nif (process.argv.includes(\\"--self-test\\")) {\\n console.log(\\n JSON.stringify(\\n {\\n pluginVersion: runtime.pluginVersion(),\\n protocolVersion: runtime.PLUGIN_PROTOCOL_VERSION,\\n tools: tools.map((tool) => tool.name),\\n },\\n null,\\n 2\\n )\\n );\\n process.exit(0);\\n}\\n","scripts/fclt-runtime.cjs":"\\"use strict\\";\\n\\nconst { spawn } = require(\\"node:child_process\\");\\nconst crypto = require(\\"node:crypto\\");\\nconst fs = require(\\"node:fs\\");\\nconst fsp = require(\\"node:fs/promises\\");\\nconst https = require(\\"node:https\\");\\nconst os = require(\\"node:os\\");\\nconst path = require(\\"node:path\\");\\n\\nconst PLUGIN_PROTOCOL_VERSION = 1;\\nconst STATE_SCHEMA_VERSION = 1;\\nconst REPOSITORY = \\"hack-dance/fclt\\";\\nconst MAX_BINARY_BYTES = 256 * 1024 * 1024;\\nconst MAX_METADATA_BYTES = 2 * 1024 * 1024;\\nconst DOWNLOAD_TIMEOUT_MS = 30_000;\\nconst COMMAND_TIMEOUT_MS = 15_000;\\nconst ALLOWED_DOWNLOAD_HOSTS = new Set([\\n \\"api.github.com\\",\\n \\"github.com\\",\\n \\"objects.githubusercontent.com\\",\\n \\"release-assets.githubusercontent.com\\",\\n]);\\nconst SEMVER_RE = /^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:[-+][0-9A-Za-z.-]+)?$/;\\nconst SHA256_RE = /^[a-f0-9]{64}$/;\\nconst NEWLINE_RE = /\\\\r?\\\\n/;\\nconst CHECKSUM_LINE_RE = /^([a-fA-F0-9]{64})\\\\s+\\\\*?(.+)$/;\\nconst WINDOWS_SHIM_RE = /\\\\.(?:bat|cmd)$/i;\\n\\nfunction isPlainObject(value) {\\n return Boolean(value) && typeof value === \\"object\\" && !Array.isArray(value);\\n}\\n\\nfunction pluginVersion() {\\n try {\\n const manifest = JSON.parse(\\n fs.readFileSync(\\n path.resolve(__dirname, \\"..\\", \\".codex-plugin\\", \\"plugin.json\\"),\\n \\"utf8\\"\\n )\\n );\\n return typeof manifest.version === \\"string\\" ? manifest.version : \\"unknown\\";\\n } catch {\\n return \\"unknown\\";\\n }\\n}\\n\\nfunction runtimeStateRoot(env = process.env, platform = process.platform) {\\n if (env.FCLT_PLUGIN_RUNTIME_DIR) {\\n return path.resolve(env.FCLT_PLUGIN_RUNTIME_DIR);\\n }\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n if (platform === \\"darwin\\") {\\n return path.join(\\n home,\\n \\"Library\\",\\n \\"Application Support\\",\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n }\\n if (platform === \\"win32\\") {\\n return path.join(\\n env.LOCALAPPDATA || path.join(home, \\"AppData\\", \\"Local\\"),\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n }\\n return path.join(\\n env.XDG_STATE_HOME || path.join(home, \\".local\\", \\"state\\"),\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n}\\n\\nfunction installStatePaths(env = process.env, platform = process.platform) {\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n const override = env.FACULT_LOCAL_STATE_DIR?.trim();\\n const portableRoot = override\\n ? path.resolve(override)\\n : platform === \\"darwin\\"\\n ? path.join(home, \\"Library\\", \\"Application Support\\", \\"fclt\\")\\n : path.join(\\n env.XDG_STATE_HOME\\n ? path.resolve(env.XDG_STATE_HOME)\\n : path.join(home, \\".local\\", \\"state\\"),\\n \\"fclt\\"\\n );\\n const candidates = [\\n path.join(portableRoot, \\"install.json\\"),\\n ...(platform === \\"win32\\"\\n ? [\\n path.join(\\n env.LOCALAPPDATA || path.join(home, \\"AppData\\", \\"Local\\"),\\n \\"fclt\\",\\n \\"install.json\\"\\n ),\\n ]\\n : []),\\n path.join(home, \\".ai\\", \\".facult\\", \\"install.json\\"),\\n path.join(home, \\".facult\\", \\"install.json\\"),\\n path.join(home, \\".local\\", \\"share\\", \\"fclt\\", \\"install.json\\"),\\n ];\\n return [...new Set(candidates.map((candidate) => path.resolve(candidate)))];\\n}\\n\\nfunction isSubpath(child, parent) {\\n const relative = path.relative(parent, child);\\n return (\\n relative === \\"\\" || !(relative.startsWith(\\"..\\") || path.isAbsolute(relative))\\n );\\n}\\n\\nasync function assertManagedPath(target, root) {\\n const resolvedRoot = path.resolve(root);\\n const resolvedTarget = path.resolve(target);\\n if (!isSubpath(resolvedTarget, resolvedRoot)) {\\n throw new Error(\\"Runtime path escapes the managed runtime root.\\");\\n }\\n\\n const relative = path.relative(resolvedRoot, path.dirname(resolvedTarget));\\n const segments = relative ? relative.split(path.sep) : [];\\n let cursor = resolvedRoot;\\n for (const segment of segments) {\\n cursor = path.join(cursor, segment);\\n try {\\n if ((await fsp.lstat(cursor)).isSymbolicLink()) {\\n throw new Error(\\"Runtime path traverses a symbolic link.\\");\\n }\\n } catch (error) {\\n if (error && error.code === \\"ENOENT\\") {\\n continue;\\n }\\n throw error;\\n }\\n }\\n return resolvedTarget;\\n}\\n\\nasync function readJson(pathValue) {\\n try {\\n const value = JSON.parse(await fsp.readFile(pathValue, \\"utf8\\"));\\n return isPlainObject(value) ? value : null;\\n } catch {\\n return null;\\n }\\n}\\n\\nasync function runtimePolicy(options = {}) {\\n const root = runtimeStateRoot(options.env, options.platform);\\n const persisted = await readJson(path.join(root, \\"policy.json\\"));\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n updateChecksEnabled: persisted?.updateChecksEnabled !== false,\\n pinnedVersion:\\n typeof persisted?.pinnedVersion === \\"string\\" &&\\n persisted.pinnedVersion.trim()\\n ? normalizeVersion(persisted.pinnedVersion)\\n : null,\\n };\\n}\\n\\nfunction commandNames(platform = process.platform) {\\n return platform === \\"win32\\"\\n ? [\\"fclt.exe\\", \\"fclt.cmd\\", \\"facult.exe\\", \\"facult.cmd\\"]\\n : [\\"fclt\\", \\"facult\\"];\\n}\\n\\nfunction pathCandidates(env = process.env, platform = process.platform) {\\n const values = [];\\n for (const directory of (env.PATH || \\"\\").split(path.delimiter)) {\\n if (!directory) {\\n continue;\\n }\\n for (const name of commandNames(platform)) {\\n values.push(path.join(directory, name));\\n }\\n }\\n return values;\\n}\\n\\nfunction systemPathCandidates(env = process.env, platform = process.platform) {\\n if (Object.hasOwn(env, \\"FCLT_SYSTEM_PATHS\\")) {\\n return (env.FCLT_SYSTEM_PATHS || \\"\\").split(path.delimiter).filter(Boolean);\\n }\\n if (platform === \\"darwin\\") {\\n return [\\"/opt/homebrew/bin/fclt\\", \\"/usr/local/bin/fclt\\"];\\n }\\n if (platform === \\"win32\\") {\\n return [];\\n }\\n return [\\"/usr/local/bin/fclt\\", \\"/usr/bin/fclt\\"];\\n}\\n\\nfunction candidateSource(candidate) {\\n const normalized = candidate.split(\\"\\\\\\\\\\").join(\\"/\\");\\n if (normalized.includes(\\"/plugin-runtime/versions/\\")) {\\n return \\"plugin_runtime\\";\\n }\\n if (normalized.includes(\\"/mise/\\") || normalized.includes(\\"/mise/installs/\\")) {\\n return \\"mise\\";\\n }\\n if (\\n normalized.includes(\\"/Cellar/\\") ||\\n normalized.startsWith(\\"/opt/homebrew/\\")\\n ) {\\n return \\"homebrew\\";\\n }\\n if (normalized.includes(\\"/node_modules/\\") || normalized.includes(\\"/npm/\\")) {\\n return \\"npm\\";\\n }\\n if (normalized.includes(\\"/.ai/.facult/bin/\\")) {\\n return \\"canonical_install\\";\\n }\\n return \\"path\\";\\n}\\n\\nasync function activeRuntimeCandidate(root) {\\n const active = await readJson(path.join(root, \\"active.json\\"));\\n if (typeof active?.executable !== \\"string\\" || !active.executable.trim()) {\\n return null;\\n }\\n const executable = path.resolve(active.executable);\\n if (!isSubpath(executable, path.join(root, \\"versions\\"))) {\\n return null;\\n }\\n return {\\n executable,\\n source: \\"plugin_runtime\\",\\n expectedSha256: active.sha256,\\n active,\\n };\\n}\\n\\nasync function persistedInstallCandidates(\\n env = process.env,\\n platform = process.platform\\n) {\\n const candidates = [];\\n for (const statePath of installStatePaths(env, platform)) {\\n const state = await readJson(statePath);\\n if (typeof state?.binaryPath === \\"string\\" && state.binaryPath.trim()) {\\n candidates.push({\\n executable: path.resolve(state.binaryPath),\\n source:\\n typeof state.source === \\"string\\" ? state.source : \\"install_metadata\\",\\n installStatePath: statePath,\\n });\\n }\\n }\\n return candidates;\\n}\\n\\nasync function runtimeCandidates(options = {}) {\\n const env = options.env || process.env;\\n const platform = options.platform || process.platform;\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n const root = runtimeStateRoot(env, platform);\\n const candidates = [];\\n let configuredPathCandidate = null;\\n\\n if (env.FCLT_BIN?.trim()) {\\n const explicit = env.FCLT_BIN.trim();\\n if (path.isAbsolute(explicit) || explicit.includes(path.sep)) {\\n candidates.push({\\n executable: path.resolve(explicit),\\n source: \\"explicit\\",\\n });\\n } else {\\n const resolved = pathCandidates(env, platform).find(\\n (candidate) =>\\n path.basename(candidate) === explicit && fs.existsSync(candidate)\\n );\\n configuredPathCandidate = {\\n executable: resolved || explicit,\\n source: \\"configured_path\\",\\n };\\n }\\n }\\n\\n const active = await activeRuntimeCandidate(root);\\n if (active) {\\n candidates.push(active);\\n }\\n if (configuredPathCandidate) {\\n candidates.push(configuredPathCandidate);\\n }\\n candidates.push(...(await persistedInstallCandidates(env, platform)));\\n candidates.push(\\n ...pathCandidates(env, platform).map((executable) => ({\\n executable,\\n source: candidateSource(executable),\\n }))\\n );\\n\\n for (const executable of [\\n path.join(home, \\".ai\\", \\".facult\\", \\"bin\\", commandNames(platform)[0]),\\n ...systemPathCandidates(env, platform),\\n ]) {\\n candidates.push({ executable, source: candidateSource(executable) });\\n }\\n\\n const unique = [];\\n const seen = new Set();\\n for (const candidate of candidates) {\\n const key = path.resolve(candidate.executable);\\n if (!seen.has(key)) {\\n seen.add(key);\\n unique.push(candidate);\\n }\\n }\\n return unique;\\n}\\n\\nfunction runCommand(executable, args, options = {}) {\\n return new Promise((resolve) => {\\n let child;\\n try {\\n const platform = options.platform || process.platform;\\n const windowsShim =\\n platform === \\"win32\\" && WINDOWS_SHIM_RE.test(executable);\\n const command = windowsShim\\n ? options.env?.ComSpec || process.env.ComSpec || \\"cmd.exe\\"\\n : executable;\\n const commandArgs = windowsShim\\n ? [\\n \\"/d\\",\\n \\"/v:off\\",\\n \\"/s\\",\\n \\"/c\\",\\n [executable, ...args]\\n .map(\\n (value) =>\\n `\\"${String(value)\\n .replaceAll(\\"%\\", \\"%%\\")\\n .replace(/[\\\\^&|<>()!\\"]/g, \\"^$&\\")}\\"`\\n )\\n .join(\\" \\"),\\n ]\\n : args;\\n child = spawn(command, commandArgs, {\\n cwd: options.cwd || process.cwd(),\\n env: options.env || process.env,\\n stdio: [\\"ignore\\", \\"pipe\\", \\"pipe\\"],\\n });\\n } catch (error) {\\n resolve({ code: 1, stdout: \\"\\", stderr: error.message });\\n return;\\n }\\n let stdout = \\"\\";\\n let stderr = \\"\\";\\n let settled = false;\\n const timer = setTimeout(\\n () => child.kill(\\"SIGTERM\\"),\\n options.timeoutMs || COMMAND_TIMEOUT_MS\\n );\\n const finish = (code, error) => {\\n if (settled) {\\n return;\\n }\\n settled = true;\\n clearTimeout(timer);\\n resolve({\\n code,\\n stdout,\\n stderr: [stderr.trim(), error].filter(Boolean).join(\\"\\\\n\\"),\\n });\\n };\\n child.stdout.on(\\"data\\", (chunk) => {\\n stdout += chunk.toString();\\n if (stdout.length > MAX_METADATA_BYTES) {\\n child.kill(\\"SIGTERM\\");\\n }\\n });\\n child.stderr.on(\\"data\\", (chunk) => {\\n stderr += chunk.toString();\\n if (stderr.length > MAX_METADATA_BYTES) {\\n child.kill(\\"SIGTERM\\");\\n }\\n });\\n child.on(\\"error\\", (error) => finish(1, error.message));\\n child.on(\\"close\\", (code) => finish(code ?? 1));\\n });\\n}\\n\\nfunction parseProtocolReport(raw) {\\n try {\\n const report = JSON.parse(raw);\\n if (\\n !isPlainObject(report) ||\\n report.schemaVersion !== 1 ||\\n typeof report.packageVersion !== \\"string\\" ||\\n !isPlainObject(report.protocol) ||\\n !Number.isInteger(report.protocol.version) ||\\n !Number.isInteger(report.protocol.minimumPluginVersion) ||\\n !Number.isInteger(report.protocol.maximumPluginVersion) ||\\n (report.capabilities !== undefined &&\\n (!Array.isArray(report.capabilities) ||\\n report.capabilities.some((value) => typeof value !== \\"string\\")))\\n ) {\\n return null;\\n }\\n return report;\\n } catch {\\n return null;\\n }\\n}\\n\\nfunction protocolCompatibility(report) {\\n if (!report) {\\n return { compatible: false, reason: \\"missing_protocol_handshake\\" };\\n }\\n const compatible =\\n report.protocol.minimumPluginVersion <= PLUGIN_PROTOCOL_VERSION &&\\n report.protocol.maximumPluginVersion >= PLUGIN_PROTOCOL_VERSION;\\n return {\\n compatible,\\n reason: compatible ? \\"compatible\\" : \\"protocol_version_skew\\",\\n };\\n}\\n\\nasync function inspectCandidate(candidate, options = {}) {\\n const executable = path.resolve(candidate.executable);\\n try {\\n const stat = await fsp.stat(executable);\\n if (!stat.isFile()) {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"not_a_file\\",\\n };\\n }\\n } catch {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"not_found\\",\\n };\\n }\\n\\n if (candidate.source === \\"plugin_runtime\\") {\\n if (\\n typeof candidate.expectedSha256 !== \\"string\\" ||\\n !SHA256_RE.test(candidate.expectedSha256)\\n ) {\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: false,\\n reason: \\"missing_checksum\\",\\n };\\n }\\n try {\\n const actualSha256 = sha256(await fsp.readFile(executable));\\n if (actualSha256 !== candidate.expectedSha256) {\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: false,\\n reason: \\"checksum_mismatch\\",\\n };\\n }\\n } catch {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"checksum_unreadable\\",\\n };\\n }\\n }\\n\\n const result = await runCommand(executable, [\\"protocol\\", \\"--json\\"], options);\\n const report =\\n result.code === 0 ? parseProtocolReport(result.stdout.trim()) : null;\\n const compatibility = protocolCompatibility(report);\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: compatibility.compatible,\\n reason: compatibility.reason,\\n packageVersion: report?.packageVersion,\\n protocol: report?.protocol,\\n platform: report?.runtime?.platform,\\n architecture: report?.runtime?.architecture,\\n capabilities: report?.capabilities || [],\\n };\\n}\\n\\nasync function discoverRuntime(options = {}) {\\n const policy = await runtimePolicy(options);\\n const inspected = [];\\n for (const candidate of await runtimeCandidates(options)) {\\n const result = await inspectCandidate(candidate, options);\\n inspected.push(result);\\n if (result.compatible) {\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n plugin: {\\n version: pluginVersion(),\\n protocolVersion: PLUGIN_PROTOCOL_VERSION,\\n },\\n policy,\\n selected: result,\\n compatible: true,\\n requiresFreshSession: false,\\n candidates: inspected,\\n };\\n }\\n }\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n plugin: {\\n version: pluginVersion(),\\n protocolVersion: PLUGIN_PROTOCOL_VERSION,\\n },\\n policy,\\n selected: null,\\n compatible: false,\\n requiresFreshSession: false,\\n candidates: inspected,\\n };\\n}\\n\\nfunction releaseTarget(\\n platform = process.platform,\\n architecture = process.arch\\n) {\\n if (\\n platform === \\"darwin\\" &&\\n (architecture === \\"arm64\\" || architecture === \\"x64\\")\\n ) {\\n return { platform: \\"darwin\\", architecture, extension: \\"\\" };\\n }\\n if (platform === \\"linux\\" && architecture === \\"x64\\") {\\n return { platform: \\"linux\\", architecture, extension: \\"\\" };\\n }\\n if (platform === \\"win32\\" && architecture === \\"x64\\") {\\n return { platform: \\"windows\\", architecture, extension: \\".exe\\" };\\n }\\n throw new Error(\\n `Unsupported plugin runtime target: ${platform}/${architecture}`\\n );\\n}\\n\\nfunction normalizeVersion(version) {\\n const normalized = version?.startsWith(\\"v\\") ? version.slice(1) : version;\\n if (!(normalized && SEMVER_RE.test(normalized))) {\\n throw new Error(\\"Runtime version must be an explicit semantic version.\\");\\n }\\n return normalized;\\n}\\n\\nfunction assertAllowedUrl(urlValue) {\\n const url = new URL(urlValue);\\n if (url.protocol !== \\"https:\\" || !ALLOWED_DOWNLOAD_HOSTS.has(url.hostname)) {\\n throw new Error(\\n \\"Runtime downloads are restricted to approved HTTPS release hosts.\\"\\n );\\n }\\n return url;\\n}\\n\\nfunction downloadBuffer(urlValue, options = {}) {\\n const maxBytes = options.maxBytes || MAX_METADATA_BYTES;\\n const redirectsRemaining = options.redirectsRemaining ?? 5;\\n const url = assertAllowedUrl(urlValue);\\n return new Promise((resolve, reject) => {\\n const request = https.get(\\n url,\\n {\\n headers: {\\n accept: options.accept || \\"application/octet-stream\\",\\n \\"user-agent\\": \\"fclt-codex-plugin\\",\\n },\\n },\\n (response) => {\\n if (\\n response.statusCode &&\\n response.statusCode >= 300 &&\\n response.statusCode < 400 &&\\n response.headers.location\\n ) {\\n response.resume();\\n if (redirectsRemaining <= 0) {\\n reject(new Error(\\"Runtime download exceeded the redirect limit.\\"));\\n return;\\n }\\n const redirected = new URL(response.headers.location, url).toString();\\n downloadBuffer(redirected, {\\n ...options,\\n redirectsRemaining: redirectsRemaining - 1,\\n }).then(resolve, reject);\\n return;\\n }\\n if (response.statusCode !== 200) {\\n response.resume();\\n reject(\\n new Error(\\n `Runtime download failed with HTTP ${response.statusCode}.`\\n )\\n );\\n return;\\n }\\n const declaredLength = Number(response.headers[\\"content-length\\"] || 0);\\n if (declaredLength > maxBytes) {\\n response.resume();\\n reject(new Error(\\"Runtime download exceeds the allowed size.\\"));\\n return;\\n }\\n const chunks = [];\\n let total = 0;\\n response.on(\\"data\\", (chunk) => {\\n total += chunk.length;\\n if (total > maxBytes) {\\n request.destroy(\\n new Error(\\"Runtime download exceeds the allowed size.\\")\\n );\\n return;\\n }\\n chunks.push(chunk);\\n });\\n response.on(\\"end\\", () => resolve(Buffer.concat(chunks)));\\n }\\n );\\n request.setTimeout(options.timeoutMs || DOWNLOAD_TIMEOUT_MS, () => {\\n request.destroy(new Error(\\"Runtime download timed out.\\"));\\n });\\n request.on(\\"error\\", reject);\\n });\\n}\\n\\nfunction sha256(bytes) {\\n return crypto.createHash(\\"sha256\\").update(bytes).digest(\\"hex\\");\\n}\\n\\nfunction checksumForAsset(checksums, assetName) {\\n for (const line of checksums.split(NEWLINE_RE)) {\\n const match = CHECKSUM_LINE_RE.exec(line.trim());\\n if (match?.[2] === assetName) {\\n return match[1].toLowerCase();\\n }\\n }\\n throw new Error(`Published checksums do not include ${assetName}.`);\\n}\\n\\nasync function withMutationLock(root, action) {\\n await fsp.mkdir(root, { recursive: true, mode: 0o700 });\\n const lockPath = await assertManagedPath(\\n path.join(root, \\"mutation.lock\\"),\\n root\\n );\\n let handle;\\n try {\\n handle = await fsp.open(lockPath, \\"wx\\", 0o600);\\n } catch (error) {\\n if (error?.code === \\"EEXIST\\") {\\n throw new Error(\\n \\"Another fclt plugin runtime mutation is already in progress.\\"\\n );\\n }\\n throw error;\\n }\\n try {\\n return await action();\\n } finally {\\n await handle.close();\\n await fsp.rm(lockPath, { force: true });\\n }\\n}\\n\\nasync function writeJsonAtomic(pathValue, value, root) {\\n const target = await assertManagedPath(pathValue, root);\\n await fsp.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });\\n const temporary = `${target}.tmp-${crypto.randomUUID()}`;\\n await fsp.writeFile(temporary, `${JSON.stringify(value, null, 2)}\\\\n`, {\\n mode: 0o600,\\n });\\n await fsp.rename(temporary, target);\\n}\\n\\nfunction releaseUrls(version, target) {\\n const tag = `v${version}`;\\n const assetName = `fclt-${version}-${target.platform}-${target.architecture}${target.extension}`;\\n const base = `https://github.com/${REPOSITORY}/releases/download/${tag}`;\\n return {\\n tag,\\n assetName,\\n binaryUrl: `${base}/${assetName}`,\\n checksumUrl: `${base}/SHA256SUMS`,\\n };\\n}\\n\\nfunction releaseMetadataUrl(version) {\\n return `https://api.github.com/repos/${REPOSITORY}/releases/tags/v${version}`;\\n}\\n\\nfunction releaseAssets(metadata, version, target) {\\n if (\\n !isPlainObject(metadata) ||\\n metadata.tag_name !== `v${version}` ||\\n !Array.isArray(metadata.assets)\\n ) {\\n throw new Error(\\n \\"Release metadata does not match the requested immutable tag.\\"\\n );\\n }\\n const expected = releaseUrls(version, target);\\n const findAsset = (name) =>\\n metadata.assets.find(\\n (asset) =>\\n isPlainObject(asset) &&\\n asset.name === name &&\\n typeof asset.browser_download_url === \\"string\\"\\n );\\n const binary = findAsset(expected.assetName);\\n const checksums = findAsset(\\"SHA256SUMS\\");\\n if (!(binary && checksums)) {\\n throw new Error(\\n \\"Release metadata is missing the required runtime or checksum asset.\\"\\n );\\n }\\n assertAllowedUrl(binary.browser_download_url);\\n assertAllowedUrl(checksums.browser_download_url);\\n return { binary, checksums, expected };\\n}\\n\\nfunction verifyPublishedDigest(asset, bytes) {\\n if (typeof asset.digest !== \\"string\\" || !asset.digest.trim()) {\\n return null;\\n }\\n const [algorithm, expected] = asset.digest.toLowerCase().split(\\":\\");\\n if (algorithm !== \\"sha256\\" || !SHA256_RE.test(expected || \\"\\")) {\\n throw new Error(`Release asset ${asset.name} has an unsupported digest.`);\\n }\\n const actual = sha256(bytes);\\n if (actual !== expected) {\\n throw new Error(\\n `Release asset ${asset.name} does not match its published digest.`\\n );\\n }\\n return asset.digest.toLowerCase();\\n}\\n\\nasync function resolveLatestVersion(fetchBuffer = downloadBuffer) {\\n const bytes = await fetchBuffer(\\n `https://api.github.com/repos/${REPOSITORY}/releases/latest`,\\n {\\n maxBytes: MAX_METADATA_BYTES,\\n accept: \\"application/vnd.github+json\\",\\n }\\n );\\n const metadata = JSON.parse(bytes.toString(\\"utf8\\"));\\n if (!isPlainObject(metadata) || typeof metadata.tag_name !== \\"string\\") {\\n throw new Error(\\"Latest release metadata did not include a tag.\\");\\n }\\n return normalizeVersion(metadata.tag_name);\\n}\\n\\nasync function checkRuntimeUpdate(options = {}) {\\n const discovery = await discoverRuntime(options);\\n if (!discovery.policy.updateChecksEnabled) {\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n action: \\"check\\",\\n skipped: true,\\n reason: \\"update_checks_disabled\\",\\n currentVersion: discovery.selected?.packageVersion || null,\\n pinnedVersion: discovery.policy.pinnedVersion,\\n mutates: false,\\n };\\n }\\n const latestVersion =\\n discovery.policy.pinnedVersion ||\\n (await resolveLatestVersion(options.fetchBuffer || downloadBuffer));\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n action: \\"check\\",\\n currentVersion: discovery.selected?.packageVersion || null,\\n latestVersion,\\n channel: discovery.policy.pinnedVersion ? \\"pinned\\" : \\"latest\\",\\n updateAvailable: discovery.selected?.packageVersion !== latestVersion,\\n selected: discovery.selected,\\n mutates: false,\\n };\\n}\\n\\nasync function stageRuntime(options) {\\n if (options.approve !== true) {\\n throw new Error(\\"Staging a runtime download requires approve=true.\\");\\n }\\n const version = normalizeVersion(options.version);\\n const target = releaseTarget(options.platform, options.architecture);\\n const root = runtimeStateRoot(options.env, options.platform);\\n const fetchBuffer = options.fetchBuffer || downloadBuffer;\\n const urls = releaseUrls(version, target);\\n const policy = await runtimePolicy(options);\\n if (policy.pinnedVersion && policy.pinnedVersion !== version) {\\n throw new Error(`Runtime policy is pinned to ${policy.pinnedVersion}.`);\\n }\\n\\n return await withMutationLock(root, async () => {\\n const metadataBytes = await fetchBuffer(releaseMetadataUrl(version), {\\n maxBytes: MAX_METADATA_BYTES,\\n accept: \\"application/vnd.github+json\\",\\n });\\n const metadata = JSON.parse(metadataBytes.toString(\\"utf8\\"));\\n const assets = releaseAssets(metadata, version, target);\\n const [checksumBytes, binaryBytes] = await Promise.all([\\n fetchBuffer(assets.checksums.browser_download_url, {\\n maxBytes: MAX_METADATA_BYTES,\\n }),\\n fetchBuffer(assets.binary.browser_download_url, {\\n maxBytes: MAX_BINARY_BYTES,\\n }),\\n ]);\\n const checksumDigest = verifyPublishedDigest(\\n assets.checksums,\\n checksumBytes\\n );\\n const binaryDigest = verifyPublishedDigest(assets.binary, binaryBytes);\\n const expectedSha256 = checksumForAsset(\\n checksumBytes.toString(\\"utf8\\"),\\n urls.assetName\\n );\\n const actualSha256 = sha256(binaryBytes);\\n if (expectedSha256 !== actualSha256) {\\n throw new Error(\\n \\"Downloaded runtime checksum does not match the published SHA256SUMS entry.\\"\\n );\\n }\\n\\n const stageDir = await assertManagedPath(\\n path.join(root, \\"staged\\", version),\\n root\\n );\\n await fsp.rm(stageDir, { recursive: true, force: true });\\n await fsp.mkdir(stageDir, { recursive: true, mode: 0o700 });\\n const executable = await assertManagedPath(\\n path.join(stageDir, target.platform === \\"windows\\" ? \\"fclt.exe\\" : \\"fclt\\"),\\n root\\n );\\n await fsp.writeFile(executable, binaryBytes, { mode: 0o700 });\\n if (target.platform !== \\"windows\\") {\\n await fsp.chmod(executable, 0o700);\\n }\\n\\n const inspected = await inspectCandidate(\\n { executable, source: \\"staged_plugin_runtime\\" },\\n { env: options.env, timeoutMs: options.timeoutMs }\\n );\\n if (!inspected.compatible || inspected.packageVersion !== version) {\\n await fsp.rm(stageDir, { recursive: true, force: true });\\n throw new Error(\\n \\"Staged runtime failed version or protocol verification.\\"\\n );\\n }\\n\\n const manifest = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n version,\\n tag: urls.tag,\\n assetName: urls.assetName,\\n executable,\\n sha256: actualSha256,\\n source: {\\n repository: REPOSITORY,\\n releaseMetadataUrl: releaseMetadataUrl(version),\\n binaryUrl: assets.binary.browser_download_url,\\n binaryAssetId: assets.binary.id ?? null,\\n binaryDigest,\\n checksumUrl: assets.checksums.browser_download_url,\\n checksumAssetId: assets.checksums.id ?? null,\\n checksumDigest,\\n },\\n protocol: inspected.protocol,\\n platform: target.platform,\\n architecture: target.architecture,\\n stagedAt: new Date().toISOString(),\\n };\\n await writeJsonAtomic(path.join(stageDir, \\"manifest.json\\"), manifest, root);\\n return { action: \\"stage\\", mutatesActiveRuntime: false, manifest };\\n });\\n}\\n\\nasync function verifyManifestExecutable(manifest, root, expectedParent) {\\n if (\\n !isPlainObject(manifest) ||\\n manifest.schemaVersion !== STATE_SCHEMA_VERSION ||\\n typeof manifest.version !== \\"string\\" ||\\n typeof manifest.executable !== \\"string\\" ||\\n typeof manifest.sha256 !== \\"string\\" ||\\n !SHA256_RE.test(manifest.sha256)\\n ) {\\n throw new Error(\\"Runtime manifest is missing required verification data.\\");\\n }\\n const executable = path.resolve(manifest.executable);\\n if (!isSubpath(executable, expectedParent)) {\\n throw new Error(\\n \\"Runtime manifest executable escapes its expected directory.\\"\\n );\\n }\\n await assertManagedPath(executable, root);\\n const bytes = await fsp.readFile(executable);\\n if (sha256(bytes) !== manifest.sha256) {\\n throw new Error(\\"Runtime manifest checksum does not match its executable.\\");\\n }\\n const inspected = await inspectCandidate({\\n executable,\\n source: \\"plugin_runtime\\",\\n expectedSha256: manifest.sha256,\\n });\\n if (!inspected.compatible || inspected.packageVersion !== manifest.version) {\\n throw new Error(\\n \\"Runtime manifest executable failed protocol verification.\\"\\n );\\n }\\n return inspected;\\n}\\n\\nasync function setRuntimePolicy(options = {}) {\\n if (options.approve !== true) {\\n throw new Error(\\"Changing runtime update policy requires approve=true.\\");\\n }\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const current = await runtimePolicy(options);\\n const next = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n updateChecksEnabled:\\n typeof options.updateChecksEnabled === \\"boolean\\"\\n ? options.updateChecksEnabled\\n : current.updateChecksEnabled,\\n pinnedVersion: options.clearPin\\n ? null\\n : options.pinnedVersion\\n ? normalizeVersion(options.pinnedVersion)\\n : current.pinnedVersion,\\n updatedAt: new Date().toISOString(),\\n };\\n await writeJsonAtomic(path.join(root, \\"policy.json\\"), next, root);\\n return { action: \\"policy\\", previous: current, policy: next };\\n });\\n}\\n\\nasync function applyStagedRuntime(options) {\\n if (options.approve !== true) {\\n throw new Error(\\"Applying a runtime requires approve=true.\\");\\n }\\n const version = normalizeVersion(options.version);\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const stageDir = path.join(root, \\"staged\\", version);\\n const manifest = await readJson(path.join(stageDir, \\"manifest.json\\"));\\n if (!manifest) {\\n throw new Error(`No staged runtime exists for ${version}.`);\\n }\\n if (options.expectedSha256 !== manifest.sha256) {\\n throw new Error(\\n \\"Staged runtime precondition failed: expected checksum changed.\\"\\n );\\n }\\n await verifyManifestExecutable(manifest, root, stageDir);\\n\\n const activePath = path.join(root, \\"active.json\\");\\n const previous = await readJson(activePath);\\n const versionDir = await assertManagedPath(\\n path.join(root, \\"versions\\", version),\\n root\\n );\\n await fsp.mkdir(versionDir, { recursive: true, mode: 0o700 });\\n const executable = await assertManagedPath(\\n path.join(versionDir, path.basename(manifest.executable)),\\n root\\n );\\n const temporary = `${executable}.tmp-${crypto.randomUUID()}`;\\n await fsp.copyFile(manifest.executable, temporary);\\n if (process.platform !== \\"win32\\") {\\n await fsp.chmod(temporary, 0o700);\\n }\\n await fsp.rename(temporary, executable);\\n const activeManifest = {\\n ...manifest,\\n executable,\\n activatedAt: new Date().toISOString(),\\n previous:\\n typeof previous?.version === \\"string\\" &&\\n typeof previous?.executable === \\"string\\"\\n ? {\\n version: previous.version,\\n executable: previous.executable,\\n sha256: previous.sha256,\\n }\\n : null,\\n };\\n await writeJsonAtomic(activePath, activeManifest, root);\\n const inspected = await verifyManifestExecutable(\\n activeManifest,\\n root,\\n versionDir\\n );\\n return {\\n action: \\"apply\\",\\n active: inspected,\\n previous: activeManifest.previous,\\n rollbackAvailable: Boolean(activeManifest.previous),\\n requiresFreshSession: false,\\n };\\n });\\n}\\n\\nasync function rollbackRuntime(options = {}) {\\n if (options.approve !== true) {\\n throw new Error(\\"Rolling back a runtime requires approve=true.\\");\\n }\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const activePath = path.join(root, \\"active.json\\");\\n const active = await readJson(activePath);\\n if (!isPlainObject(active?.previous)) {\\n throw new Error(\\n \\"The active plugin runtime does not have a retained rollback target.\\"\\n );\\n }\\n if (\\n options.expectedActiveVersion &&\\n options.expectedActiveVersion !== active.version\\n ) {\\n throw new Error(\\n \\"Runtime rollback precondition failed: active version changed.\\"\\n );\\n }\\n const previous = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n version: active.previous.version,\\n executable: active.previous.executable,\\n sha256: active.previous.sha256,\\n previous: {\\n version: active.version,\\n executable: active.executable,\\n sha256: active.sha256,\\n },\\n rolledBackAt: new Date().toISOString(),\\n };\\n const previousDir = path.dirname(path.resolve(previous.executable));\\n const inspected = await verifyManifestExecutable(\\n previous,\\n root,\\n previousDir\\n );\\n await writeJsonAtomic(activePath, previous, root);\\n return {\\n action: \\"rollback\\",\\n active: inspected,\\n rolledBackFrom: active.version,\\n rollbackAvailable: true,\\n requiresFreshSession: false,\\n };\\n });\\n}\\n\\nmodule.exports = {\\n PLUGIN_PROTOCOL_VERSION,\\n applyStagedRuntime,\\n assertManagedPath,\\n checkRuntimeUpdate,\\n checksumForAsset,\\n discoverRuntime,\\n downloadBuffer,\\n normalizeVersion,\\n parseProtocolReport,\\n pluginVersion,\\n protocolCompatibility,\\n releaseTarget,\\n rollbackRuntime,\\n runCommand,\\n runtimeCandidates,\\n runtimePolicy,\\n runtimeStateRoot,\\n setRuntimePolicy,\\n sha256,\\n stageRuntime,\\n};\\n","skills/fclt-capability-review/SKILL.md":"---\\nname: fclt-capability-review\\ndescription: Inspect fclt capability roots, docs, snippets, skills, agents, MCP, and automations.\\ntags: [fclt, capability, review, inventory]\\n---\\n\\n# fclt-capability-review\\n\\n## When To Use\\nUse this skill when Codex needs to understand what capability exists before changing it.\\n\\nUse it for:\\n\\n- checking global and project `.ai` roots\\n- finding relevant skills, snippets, instructions, agents, MCP servers, or automations\\n- deciding whether a change belongs in global or project scope\\n- checking whether managed rendering is enabled or needed\\n- reviewing public/private boundaries before publishing docs or pack assets\\n\\n## Workflow\\n\\n```bash\\nfclt status --json\\nfclt inventory --json\\nfclt list skills\\nfclt list instructions\\nfclt list snippets\\nfclt graph AGENTS.global.md\\n```\\n\\nFor project work:\\n\\n```bash\\nfclt status --project --json\\nfclt inventory --project --json\\n```\\n\\n## Rules\\n\\n- Read existing repo guidance before proposing project capability.\\n- Use project scope for repo-specific commands, tests, architecture, or team workflow.\\n- Use global scope only for broadly reusable behavior.\\n- Keep generated state and review artifacts out of repo-local `.ai`.\\n- Prefer adding or updating the smallest unit: instruction, snippet, skill, agent, MCP config, or automation.\\n- Treat engineering implementation as task work. Do not force product changes\\n through capability evolution merely because evolution tools exist.\\n- Before recommending mutation, state the observed problem, evidence, why the\\n proposed capability is the correct target, risk class, expected outcome,\\n verification plan, assumptions, and recovery route.\\n- Never silently overwrite authored capability or confuse generated/rendered\\n output with canonical source.\\n\\n## Output\\n\\n- capability roots found\\n- relevant assets\\n- scope recommendation\\n- missing or stale capability\\n- safe next command\\n- deliberately withheld operations or missing safer API\\n- verification and undo path for any proposed mutation\\n\\n## Authorization and evidence boundaries\\n\\nUse existing authorization for the same action and scope; do not request the same approval again.\\nHonor read-only and proposal-only requests by returning findings or a proposed writeback without\\nrecording state. When review-state mutation is authorized, prepare the smallest evidence-backed draft.\\nDrafting or recording does not authorize canonical apply, cross-scope promotion, external messages,\\ntracker changes, publishing, or scheduler activation. Preserve those gates and native authorization.\\nDo not infer effectiveness from apply: verify the producing workflow before resolving source writebacks.\\nKeep unchanged, non-actionable scheduled results quiet unless periodic reports were requested.\\n","skills/fclt-evolution/SKILL.md":"---\\nname: fclt-evolution\\ndescription: Turn repeated fclt writebacks into reviewed capability changes.\\ntags: [fclt, evolution, proposals, capability]\\n---\\n\\n# fclt-evolution\\n\\n## When To Use\\nUse this skill when repeated writebacks, stale canonical assets, or a clearly missing capability should become a concrete proposal.\\n\\nDo not use it for a single weak preference or speculative idea.\\n\\n## Authorization\\n\\nProceed with recording or drafting when the current request authorizes that action and scope;\\ndo not request the same approval again. For read-only or proposal-only work, return findings or a\\nproposed writeback without changing state. Strong evidence alone does not grant write authority.\\n\\nRecording or drafting does not authorize canonical application, cross-scope promotion, external\\nmessages, tracker changes, publishing, or scheduler activation. Preserve those separate gates and\\nnative authorization. Keep unchanged, non-actionable scheduled results quiet unless periodic\\nreports were requested.\\n\\n## Workflow\\n\\n1. Review signal:\\n\\n```bash\\nfclt ai review reconcile --since --until --json\\nfclt ai writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai evolve list\\n```\\n\\nFor an enabled scheduled loop, inspect its durable queue and observed scheduler\\nhealth with `fclt ai loop status --json`. Use `fclt ai loop run --dry-run\\n--json` for a fresh incremental scan that does not advance cursors or write\\nreconciliation or loop state. The full queue is authoritative; the notification\\ndelta intentionally suppresses unchanged items.\\n\\n2. Assess proposal readiness before mutating state:\\n\\n```bash\\nfclt ai evolve assess --asset --json\\n```\\n\\nUse the assessment recommendation as the decision checkpoint:\\n\\n- `reconcile_sources`: run the bounded read-only source review; writebacks alone cannot prove the window is empty.\\n- `review_reconciled_signals`: review correlated dispositions and linked work without creating one proposal per ticket.\\n- `no_mutation`: do not change capability state; ask for a target or evidence.\\n- `record_more_writeback`: explain what recurrence would justify evolution and record a new writeback only if there is fresh concrete evidence.\\n- `propose`: create the smallest target-specific draft when review-state mutation is already authorized; otherwise present the proposed action. Retain canonical apply and promotion gates.\\n- `review_existing_proposal`: inspect or revise the existing proposal instead of creating a duplicate.\\n\\n3. Propose only when evidence is strong enough:\\n\\n```bash\\nfclt ai evolve propose\\n```\\n\\n4. Draft and inspect:\\n\\n```bash\\nfclt ai evolve draft EV-00001\\nfclt ai evolve review EV-00001\\n```\\n\\n5. Present the reviewed proposal with its exact target, scope, evidence, and required authority.\\nCanonical apply and cross-scope promotion are unavailable through this plugin. Stop at the\\nreviewed proposal; do not use shell commands to bypass the unavailable surface.\\n\\n6. Verify the outcome after the producing loop has had a real chance to run:\\n\\n```bash\\nfclt ai writeback link WB-00001 --issue TEAM-123\\nfclt ai writeback disposition WB-00001 --type task --target TEAM-123\\nfclt ai evolve verify EV-00001 --effectiveness improved --evidence test:post-apply\\n```\\n\\nApply is not completion. Do not resolve source writebacks until post-apply evidence shows the\\nintended behavior improved. Treat recurrence as unchanged or regressed evidence linked to the same\\nevolution, not as an unrelated singleton.\\n\\n## Proposal Kinds\\n\\n- `update_asset`\\n- `create_asset`\\n- `extract_snippet`\\n- `add_skill`\\n- `promote_asset`\\n\\n## Rules\\n\\n- Prefer the smallest valid proposal kind.\\n- Keep project-specific behavior project-scoped until reuse is proven.\\n- Require approval covering the exact scope before applying global instructions, global skills, plugin behavior, or other broad shared surfaces. Existing approval for that action and scope is sufficient; never bypass an unavailable or restricted apply surface.\\n- Reject or park proposals that are stale, duplicated, vague, or unsupported.\\n- Use the operator\'s task system for executable implementation work that needs owner, priority, or state.\\n- A no-op answer must still be useful: include the evidence grade, missing signal, next writeback target, and exact approval boundary.\\n- State the problem, source evidence, reason for the selected target, risk,\\n expected outcome, verification plan, assumptions, and undo path before any\\n review-producing or mutating action.\\n- Preview and verify the exact scope. Never overwrite authored capability\\n silently or treat generated/rendered output as the canonical target.\\n- Canonical apply and cross-scope promotion are unavailable through the plugin\\n until a transaction-safe API can return tested rollback data. Do not bypass\\n that boundary with shell or arbitrary CLI arguments.\\n- The plugin exposes only closed-schema loop status and preview actions.\\n Scheduler enable/disable/run and external tracker mutation remain outside the\\n MCP surface.\\n\\n## Output\\n\\n- proposals reviewed\\n- repeated signal\\n- assessment recommendation\\n- proposal created or updated\\n- approvals needed\\n- apply/reject/no-op rationale\\n- actual changed records/artifacts, verification result, and recovery route\\n","skills/fclt-setup/SKILL.md":"---\\nname: fclt-setup\\ndescription: Install, update, inspect, and initialize fclt from Codex.\\ntags: [fclt, setup, codex, onboarding]\\n---\\n\\n# fclt-setup\\n\\n## When To Use\\nUse this skill when a user wants Codex to install, update, configure, inspect, or repair fclt.\\n\\nUse it for:\\n\\n- checking whether `fclt` is installed and current\\n- initializing global `~/.ai`\\n- discovering repositories and enrolling a reviewed minimal project layer\\n- installing or refreshing the built-in operating-model pack\\n- checking setup health with `doctor`\\n- finding canonical, generated, runtime, and review paths\\n\\n## Workflow\\n\\n1. For inspection, start with `fclt --version`, `fclt paths --json`, and `fclt doctor --json`. Run setup only when initialization or repair is authorized; existing authorization for the same targets is sufficient. Bootstrap an authorized global setup with:\\n\\n```bash\\nfclt setup\\n```\\n\\nThis initializes or safely updates global capability, prepares writeback/evolution review state,\\nand installs the Codex plugin when Codex is available. It does not initialize the current\\nrepository.\\n\\n2. Inspect runtime selection and compatibility with `fclt_runtime` action\\n `status`. Report the selected executable, version, source, protocol\\n compatibility, and fresh-session state.\\n\\nIf no compatible runtime is available, use the staged lifecycle:\\n\\n- `check` is read-only\\n- `stage` requires an explicit version and approval, but does not activate it\\n- `apply` requires approval plus the staged checksum precondition\\n- `rollback` verifies and restores the retained prior runtime\\n\\nNever curl-pipe code, use an unverified mutable URL, or replace an existing\\nglobal installation silently.\\n\\n3. Check current setup state and exact repair actions:\\n\\n```bash\\nfclt --version\\nfclt paths --json\\nfclt doctor --json\\n```\\n\\nThrough MCP, call `fclt_setup` with an explicit `global` or\\n`global_and_project` scope. Project setup also requires the exact `cwd`.\\nPreview is the default; apply requires both `dryRun: false` and\\n`approve: true`. `global_and_project` returns a no-write project enrollment\\nplan; project application still uses the typed CLI plan-hash contract below.\\n\\n4. For advanced manual recovery, initialize global capability when missing:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n5. Discover candidate repositories only beneath explicit roots:\\n\\n```bash\\nfclt projects discover --root ~/dev --since 30d --json\\n```\\n\\nDiscovery is bounded and read-only. Review duplicate clone/worktree groups,\\ndirty state, existing guidance, stable portfolio identity, and the separate\\ncheckout/worktree execution identity before selecting a project. Never\\nbulk-enroll the discovery result.\\n\\n6. Preview the exact minimal project enrollment plan:\\n\\n```bash\\nfclt project init --project-root /path/to/repo --json\\n```\\n\\nThe minimal layer is `.ai/.gitignore` plus `.ai/config.toml`. It does not\\ninstall the operating pack, enable managed rendering, schedule a loop, or copy\\n`AGENTS.md`/`CLAUDE.md`. Review every canonical, generated, and machine-local\\nwrite plus the rollback command.\\n\\nIf project guidance should be adopted, name each canonical file explicitly:\\n\\n```bash\\nfclt project init --project-root /path/to/repo \\\\\\n --guidance AGENTS.md --json\\n```\\n\\nGuidance adoption is reference-only. fclt previews the full content and hash,\\nand refuses untracked, modified, `assume-unchanged`, `skip-worktree`,\\nsecret-shaped, or machine-path-bearing input. Cleanliness requires identical\\nworktree, index, and `HEAD` blobs.\\n\\n7. Apply only the unchanged reviewed plan:\\n\\n```bash\\nfclt project init --project-root /path/to/repo \\\\\\n --apply --plan-sha --json\\n```\\n\\nIf options, source files, or preconditions change, discard the old hash and\\npreview again. Apply serializes portfolio registry mutations, isolates\\nlocation-bearing generated state per checkout/worktree, refuses symlinked\\ngenerated targets, and publishes the receipt only after the registry commit.\\n\\n8. Inspect health, coverage, and lifecycle:\\n\\n```bash\\nfclt projects status --root /path/to/repo --json\\nfclt project disable --project-root /path/to/repo --json\\nfclt project rollback --receipt --json\\n```\\n\\nDisable and remove decisions preserve canonical files, receipts, and review\\nhistory. Rollback previews by default and refuses drift.\\n\\n9. Install the full operating pack only when explicitly requested:\\n\\n```bash\\nfclt templates init operating-model --project --dry-run\\nfclt templates init operating-model --project\\n```\\n\\nThis is distinct from minimal enrollment and does not seed project\\n`AGENTS.global.md` from repository guidance.\\n\\n10. Refresh global pack defaults non-destructively:\\n\\n```bash\\nfclt templates init operating-model --global --update --dry-run\\nfclt templates init operating-model --global --update\\n```\\n\\n11. Use `--force` only when the user explicitly wants to replace local edits.\\n\\n## Rules\\n\\n- Preserve existing `AGENTS.md`, `CLAUDE.md`, and `AGENTS.global.md` guidance.\\n- Never infer guidance adoption from filenames or copy repository guidance into\\n `.ai/AGENTS.global.md`.\\n- Treat root `AGENTS.md` or `CLAUDE.md` as canonical repository guidance unless\\n the user explicitly chooses another tracked, clean source.\\n- Write the protective `.ai/.gitignore` before generated state.\\n- Keep canonical project files separate from machine-local generated indexes,\\n registries, receipts, and scheduling state.\\n- Keep minimal enrollment separate from the full operating pack and managed\\n rendering.\\n- Treat `doctor --json` issues as setup facts, not user-facing blame.\\n- Treat Codex plugin registration as weaker evidence than fresh-session tool discovery.\\n- Treat external trackers as separate integrations. Core readiness depends only on configured local evidence coverage, not a vendor plugin or token.\\n- Prefer temp-root smoke tests for install/update behavior.\\n- Do not enable managed rendering unless the user wants fclt to write tool homes.\\n- Preview before mutation and state the exact global/project/plugin target.\\n- Do not report a staged runtime or installed plugin as active until the active\\n handshake and fresh-session discovery have been verified.\\n\\n## Output\\n\\n- current installed version\\n- setup health\\n- paths that matter\\n- commands run\\n- what changed\\n- problem, evidence, reason, target, risk, and expected outcome\\n- verification performed and its actual result\\n- assumptions and fresh-session state\\n- exact undo or rollback path\\n- what still needs approval\\n","skills/fclt-writeback/SKILL.md":"---\\nname: fclt-writeback\\ndescription: Record and review fclt writebacks from real agent work.\\ntags: [fclt, writeback, learning, feedback-loop]\\n---\\n\\n# fclt-writeback\\n\\n## When To Use\\nUse this skill when work reveals durable friction, missing context, weak verification, stale guidance, repeated success, or a capability gap.\\n\\nWriteback is for preserving signal. It is not for every preference or one-off annoyance.\\n\\n## Authorization\\n\\nProceed with recording or drafting when the current request authorizes that action and scope;\\ndo not request the same approval again. For read-only or proposal-only work, return findings or a\\nproposed writeback without changing state. Strong evidence alone does not grant write authority.\\n\\nRecording or drafting does not authorize canonical application, cross-scope promotion, external\\nmessages, tracker changes, publishing, or scheduler activation. Preserve those separate gates and\\nnative authorization. Keep unchanged, non-actionable scheduled results quiet unless periodic\\nreports were requested.\\n\\n## Workflow\\n\\n1. Decide scope:\\n\\n- `project` when the learning depends on a repo, test harness, architecture, or workflow.\\n- `global` when the learning applies across projects or shared tool behavior.\\n\\n2. Choose the smallest target:\\n\\n- instruction\\n- snippet\\n- skill\\n- agent\\n- MCP/tool config\\n- automation\\n\\n3. Record writeback when the target, evidence, and authorization are clear:\\n\\n```bash\\nfclt ai writeback add \\\\\\n --kind missing_context \\\\\\n --category opportunity \\\\\\n --summary \\"Project verification guidance was not discoverable\\" \\\\\\n --details \\"The task had to reconstruct the command from CI configuration\\" \\\\\\n --impact \\"Verification took longer and could have selected the wrong harness\\" \\\\\\n --attempted-workaround \\"Inspected package scripts and CI\\" \\\\\\n --desired-outcome \\"The supported verification command is available at task start\\" \\\\\\n --sensitivity internal \\\\\\n --evidence session: \\\\\\n --asset @project/instructions/TESTING.md\\n```\\n\\n4. Review current signal:\\n\\n```bash\\nfclt ai writeback list\\nfclt ai writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai loop activity --project\\n```\\n\\n## Rules\\n\\n- Prefer one high-signal writeback over several weak ones.\\n- Include concrete evidence when possible.\\n- Capture concise context, impact, attempted workaround, desired outcome, and\\n sensitivity when they improve review quality.\\n- Never capture hidden chain-of-thought, raw transcripts, unbounded logs,\\n secrets, tokens, or credential-bearing payloads. Reference the smallest\\n redacted external evidence identifier instead.\\n- Do not copy private project detail into global writebacks.\\n- Use task tracking for executable product/tooling work; use writeback for reusable operating-layer learning.\\n- If the same signal repeats and the target is clear, hand off to `fclt-evolution`.\\n- State the observed problem, evidence, target, reason, expected outcome, and\\n assumptions before recording.\\n- Do not capture secrets, private tokens, or raw sensitive payloads as evidence.\\n- For lifecycle mutations, use an explicit scope and expected prior state.\\n Report the journal/review evidence and the available undo transition.\\n\\n## Output\\n\\n- writeback id or no-op rationale\\n- scope\\n- target asset\\n- evidence summary\\n- whether this is ready for evolution\\n- risk class and approval boundary\\n- actual changed records/artifacts\\n- verification result and recovery route\\n\\n\\nVerify the producing workflow before resolving source writebacks; applying a change alone does not prove effectiveness.\\n"}' + '{".codex-plugin/plugin.json":"{\\n \\"name\\": \\"fclt\\",\\n \\"version\\": \\"0.1.5\\",\\n \\"description\\": \\"Codex workflows and MCP tools for evidence reconciliation, writeback, evolution, and outcome review.\\",\\n \\"author\\": {\\n \\"name\\": \\"Hack Dance\\",\\n \\"url\\": \\"https://hack.dance\\"\\n },\\n \\"license\\": \\"MIT\\",\\n \\"keywords\\": [\\n \\"fclt\\",\\n \\"facult\\",\\n \\"codex\\",\\n \\"skills\\",\\n \\"mcp\\",\\n \\"writeback\\",\\n \\"evolution\\"\\n ],\\n \\"skills\\": \\"./skills/\\",\\n \\"mcpServers\\": \\"./.mcp.json\\",\\n \\"interface\\": {\\n \\"displayName\\": \\"fclt\\",\\n \\"shortDescription\\": \\"Feedback loops for AI work\\",\\n \\"longDescription\\": \\"Capture signal from real agent work, reconcile configured evidence, turn repeated findings into reviewed capability changes, and verify whether those changes improved the work that produced them.\\",\\n \\"developerName\\": \\"Hack Dance\\",\\n \\"category\\": \\"Productivity\\",\\n \\"capabilities\\": [\\"Read\\", \\"Write\\", \\"MCP\\"],\\n \\"defaultPrompt\\": [\\n \\"Use fclt to check this repo\'s AI capability setup.\\",\\n \\"Reconcile the configured evidence window and explain its coverage.\\",\\n \\"Record useful writeback from this work and review any repeated signal.\\"\\n ],\\n \\"brandColor\\": \\"#166534\\",\\n \\"composerIcon\\": \\"./assets/fclt-mark.png\\",\\n \\"logo\\": \\"./assets/fclt-mark.png\\"\\n }\\n}\\n",".mcp.json":"{\\n \\"mcpServers\\": {\\n \\"fclt\\": {\\n \\"command\\": \\"node\\",\\n \\"args\\": [\\"./scripts/fclt-mcp.cjs\\"],\\n \\"env\\": {\\n \\"FCLT_BIN\\": \\"fclt\\"\\n },\\n \\"cwd\\": \\".\\"\\n }\\n }\\n}\\n","scripts/fclt-mcp.cjs":"#!/usr/bin/env node\\n\\"use strict\\";\\n\\nconst fs = require(\\"node:fs\\");\\nconst os = require(\\"node:os\\");\\nconst path = require(\\"node:path\\");\\nconst runtime = require(\\"./fclt-runtime.cjs\\");\\n\\nconst DEFAULT_TIMEOUT_MS = Number(process.env.FCLT_MCP_TIMEOUT_MS || 60_000);\\nconst ACTIVITY_ACTION_DECIDE_CAPABILITY = \\"activity-action-decide-v1\\";\\nconst ACTIVITY_ACTION_RESOLVE_CAPABILITY = \\"activity-action-resolve-v1\\";\\nconst AUDIT_READ_ONLY_CAPABILITY = \\"audit-read-only-v1\\";\\nconst CONTENT_LENGTH_RE = /Content-Length:\\\\s*(\\\\d+)/i;\\nconst PLUGIN_ROOT = path.resolve(__dirname, \\"..\\");\\n\\nconst tools = [\\n {\\n name: \\"fclt_setup\\",\\n description:\\n \\"Bootstrap or repair the global fclt loop and optionally return an exact no-write project enrollment plan.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: {\\n type: \\"string\\",\\n enum: [\\"global\\", \\"global_and_project\\"],\\n },\\n cwd: { type: \\"string\\" },\\n dryRun: { type: \\"boolean\\" },\\n installCodexPlugin: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_runtime\\",\\n description:\\n \\"Discover, bootstrap, update, or roll back the verified fclt runtime used by this plugin.\\",\\n inputSchema: {\\n type: \\"object\\",\\n additionalProperties: false,\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\"status\\", \\"check\\", \\"policy\\", \\"stage\\", \\"apply\\", \\"rollback\\"],\\n },\\n version: { type: \\"string\\" },\\n expectedSha256: { type: \\"string\\", pattern: \\"^[a-f0-9]{64}$\\" },\\n expectedActiveVersion: { type: \\"string\\" },\\n pinnedVersion: { type: \\"string\\" },\\n clearPin: { type: \\"boolean\\" },\\n updateChecksEnabled: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_capability\\",\\n description:\\n \\"Inspect fclt capability, provenance, templates, snippets, adapters, and managed status without exposing secrets.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"scan\\",\\n \\"inventory\\",\\n \\"list\\",\\n \\"show\\",\\n \\"find\\",\\n \\"graph\\",\\n \\"adapters\\",\\n \\"managed_status\\",\\n \\"templates_list\\",\\n \\"snippet_list\\",\\n \\"snippet_show\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: {\\n type: \\"string\\",\\n enum: [\\n \\"skills\\",\\n \\"mcp\\",\\n \\"agents\\",\\n \\"automations\\",\\n \\"snippets\\",\\n \\"instructions\\",\\n ],\\n },\\n query: { type: \\"string\\" },\\n selector: { type: \\"string\\" },\\n graphMode: { type: \\"string\\", enum: [\\"show\\", \\"deps\\", \\"dependents\\"] },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_workflow\\",\\n description:\\n \\"Run typed writeback and evolution review operations. Canonical apply and cross-scope promotion are deliberately withheld.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"writeback_list\\",\\n \\"writeback_show\\",\\n \\"writeback_group\\",\\n \\"writeback_summarize\\",\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"evolve_assess\\",\\n \\"evolve_list\\",\\n \\"evolve_show\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_verify\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n id: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n category: {\\n type: \\"string\\",\\n enum: [\\"friction\\", \\"opportunity\\", \\"reusable-success\\"],\\n },\\n summary: { type: \\"string\\" },\\n details: { type: \\"string\\", maxLength: 2000 },\\n impact: { type: \\"string\\", maxLength: 1000 },\\n attemptedWorkaround: { type: \\"string\\", maxLength: 1000 },\\n desiredOutcome: { type: \\"string\\", maxLength: 1000 },\\n sensitivity: {\\n type: \\"string\\",\\n enum: [\\"public\\", \\"internal\\", \\"private\\"],\\n },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"array\\", items: { type: \\"string\\" } },\\n confidence: { type: \\"string\\", enum: [\\"low\\", \\"medium\\", \\"high\\"] },\\n by: { type: \\"string\\", enum: [\\"asset\\", \\"kind\\", \\"domain\\"] },\\n issue: { type: \\"string\\" },\\n disposition: {\\n type: \\"string\\",\\n enum: [\\"propose\\", \\"apply-local\\", \\"task\\", \\"resolve-watch\\", \\"defer\\"],\\n },\\n target: { type: \\"string\\" },\\n nextTrigger: { type: \\"string\\" },\\n expectedOutcome: { type: \\"string\\" },\\n append: { type: \\"string\\" },\\n reason: { type: \\"string\\" },\\n byProposal: { type: \\"string\\" },\\n effectiveness: {\\n type: \\"string\\",\\n enum: [\\"improved\\", \\"unchanged\\", \\"regressed\\", \\"inconclusive\\"],\\n },\\n note: { type: \\"string\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_sync\\",\\n description:\\n \\"Inspect managed state or preview a scoped tool sync. Apply and live adoption remain withheld pending transaction-safe APIs.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: { type: \\"string\\", enum: [\\"status\\", \\"preview\\"] },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n tool: { type: \\"string\\" },\\n },\\n required: [\\"action\\", \\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_registry\\",\\n description:\\n \\"Search and verify remote capability, run typed source reconciliation reviews, resolve one opaque activity action locator, or record one revision-bound signal decision. Canonical and external mutation remains withheld.\\",\\n inputSchema: {\\n oneOf: [\\n {\\n type: \\"object\\",\\n properties: {\\n action: { const: \\"activity_decide\\" },\\n locator: {\\n type: \\"string\\",\\n pattern: \\"^fclt-act-v[0-9]+\\\\\\\\.[a-f0-9]{64}\\\\\\\\.[a-f0-9]{64}$\\",\\n },\\n decision: {\\n type: \\"string\\",\\n enum: [\\"accept\\", \\"redirect\\", \\"reject\\", \\"defer\\"],\\n },\\n expectedRevision: { type: \\"integer\\", minimum: 1 },\\n actor: {\\n type: \\"string\\",\\n pattern: \\"^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$\\",\\n },\\n approvalReference: { type: \\"string\\", minLength: 1, maxLength: 500 },\\n note: { type: \\"string\\", minLength: 1, maxLength: 1000 },\\n redirectTarget: { type: \\"string\\", minLength: 1, maxLength: 500 },\\n approve: { const: true },\\n },\\n required: [\\n \\"action\\",\\n \\"locator\\",\\n \\"decision\\",\\n \\"expectedRevision\\",\\n \\"actor\\",\\n \\"approve\\",\\n ],\\n allOf: [\\n {\\n oneOf: [\\n {\\n required: [\\"approvalReference\\"],\\n not: { required: [\\"note\\"] },\\n },\\n {\\n required: [\\"note\\"],\\n not: { required: [\\"approvalReference\\"] },\\n },\\n ],\\n },\\n ],\\n additionalProperties: false,\\n },\\n {\\n type: \\"object\\",\\n properties: {\\n action: { const: \\"activity_resolve\\" },\\n locator: {\\n type: \\"string\\",\\n pattern: \\"^fclt-act-v[0-9]+\\\\\\\\.[a-f0-9]{64}\\\\\\\\.[a-f0-9]{64}$\\",\\n },\\n },\\n required: [\\"action\\", \\"locator\\"],\\n additionalProperties: false,\\n },\\n {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"search\\",\\n \\"verify_source\\",\\n \\"source_list\\",\\n \\"install_preview\\",\\n \\"update_check\\",\\n \\"reconcile_status\\",\\n \\"reconcile\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n query: { type: \\"string\\" },\\n source: { type: \\"string\\" },\\n item: { type: \\"string\\" },\\n as: { type: \\"string\\" },\\n since: {\\n type: \\"string\\",\\n pattern:\\n \\"^\\\\\\\\d{4}-\\\\\\\\d{2}-\\\\\\\\d{2}(?:T\\\\\\\\d{2}:\\\\\\\\d{2}:\\\\\\\\d{2}(?:\\\\\\\\.\\\\\\\\d+)?(?:Z|[+-]\\\\\\\\d{2}:\\\\\\\\d{2}))?$\\",\\n },\\n until: {\\n type: \\"string\\",\\n pattern:\\n \\"^\\\\\\\\d{4}-\\\\\\\\d{2}-\\\\\\\\d{2}(?:T\\\\\\\\d{2}:\\\\\\\\d{2}:\\\\\\\\d{2}(?:\\\\\\\\.\\\\\\\\d+)?(?:Z|[+-]\\\\\\\\d{2}:\\\\\\\\d{2}))?$\\",\\n },\\n sourceIds: {\\n type: \\"array\\",\\n items: {\\n type: \\"string\\",\\n pattern: \\"^[A-Za-z0-9][A-Za-z0-9._-]*$\\",\\n },\\n },\\n incremental: { type: \\"boolean\\" },\\n },\\n required: [\\"action\\"],\\n additionalProperties: false,\\n },\\n ],\\n },\\n },\\n {\\n name: \\"fclt_audit\\",\\n description:\\n \\"Run a structured, redacted, non-interactive fclt security audit with no report or index writes.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: { type: \\"string\\", enum: [\\"scan\\"] },\\n cwd: { type: \\"string\\" },\\n target: { type: \\"string\\" },\\n severity: {\\n type: \\"string\\",\\n enum: [\\"critical\\", \\"high\\", \\"medium\\", \\"low\\"],\\n },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_automation\\",\\n description:\\n \\"Read one aggregate activity set across all configured loops by default, or inspect one explicit loop scope. Schedule and canonical mutation remain CLI-only.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"autosync_status\\",\\n \\"loop_status\\",\\n \\"loop_activity\\",\\n \\"loop_preview\\",\\n ],\\n },\\n scope: {\\n type: \\"string\\",\\n enum: [\\"all\\", \\"global\\", \\"project\\"],\\n },\\n cwd: { type: \\"string\\" },\\n tool: { type: \\"string\\" },\\n },\\n required: [\\"action\\"],\\n oneOf: [\\n {\\n properties: {\\n action: { const: \\"loop_activity\\" },\\n scope: {\\n type: \\"string\\",\\n enum: [\\"all\\", \\"global\\", \\"project\\"],\\n default: \\"all\\",\\n },\\n },\\n required: [\\"action\\"],\\n },\\n {\\n properties: {\\n action: {\\n enum: [\\"autosync_status\\", \\"loop_status\\", \\"loop_preview\\"],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n },\\n required: [\\"action\\", \\"scope\\"],\\n },\\n ],\\n },\\n },\\n {\\n name: \\"fclt_status\\",\\n description:\\n \\"Return fclt status for the current, global, or project scope.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_doctor\\",\\n description: \\"Run read-only fclt doctor checks and return JSON output.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_paths\\",\\n description: \\"Return canonical, generated, review, and runtime fclt paths.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_init_operating_model\\",\\n description: \\"Install or update the built-in operating-model pack.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n update: { type: \\"boolean\\" },\\n dryRun: { type: \\"boolean\\" },\\n force: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_writeback_add\\",\\n description: \\"Record a durable fclt writeback with evidence.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n category: {\\n type: \\"string\\",\\n enum: [\\"friction\\", \\"opportunity\\", \\"reusable-success\\"],\\n },\\n summary: { type: \\"string\\" },\\n details: { type: \\"string\\", maxLength: 2000 },\\n impact: { type: \\"string\\", maxLength: 1000 },\\n attemptedWorkaround: { type: \\"string\\", maxLength: 1000 },\\n desiredOutcome: { type: \\"string\\", maxLength: 1000 },\\n sensitivity: {\\n type: \\"string\\",\\n enum: [\\"public\\", \\"internal\\", \\"private\\"],\\n },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"string\\" },\\n confidence: {\\n type: \\"string\\",\\n enum: [\\"low\\", \\"medium\\", \\"high\\"],\\n },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\", \\"kind\\", \\"summary\\", \\"evidence\\", \\"approve\\"],\\n },\\n },\\n {\\n name: \\"fclt_writeback_review\\",\\n description: \\"List, group, or summarize current fclt writebacks.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n mode: { type: \\"string\\", enum: [\\"list\\", \\"group\\", \\"summarize\\"] },\\n by: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_evolve\\",\\n description:\\n \\"Assess, list, propose, draft, or review fclt evolution proposals.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n action: {\\n type: \\"string\\",\\n enum: [\\"assess\\", \\"list\\", \\"propose\\", \\"draft\\", \\"review\\", \\"show\\"],\\n },\\n id: { type: \\"string\\" },\\n asset: { type: \\"string\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n },\\n },\\n];\\n\\nfor (const tool of tools) {\\n if (!tool.inputSchema.oneOf || tool.inputSchema.properties) {\\n tool.inputSchema.additionalProperties = false;\\n }\\n}\\n\\nfunction isPlainObject(value) {\\n return Boolean(value) && typeof value === \\"object\\" && !Array.isArray(value);\\n}\\n\\nfunction validateToolArguments(name, args) {\\n if (!isPlainObject(args)) {\\n throw new Error(`${name} arguments must be an object`);\\n }\\n const tool = tools.find((entry) => entry.name === name);\\n if (!tool) {\\n throw new Error(`Unknown tool: ${name}`);\\n }\\n const rootSchema = tool.inputSchema;\\n const schema =\\n rootSchema.oneOf && !rootSchema.properties\\n ? rootSchema.oneOf.find((branch) => {\\n const action = branch.properties?.action;\\n return (\\n action?.const === args.action || action?.enum?.includes(args.action)\\n );\\n })\\n : rootSchema;\\n if (!schema) {\\n throw new Error(`${name}.action is not an allowed value`);\\n }\\n const properties = schema.properties || {};\\n const unknown = Object.keys(args).filter((key) => !(key in properties));\\n if (unknown.length > 0) {\\n throw new Error(\\n `${name} received unknown argument fields: ${unknown.join(\\", \\")}`\\n );\\n }\\n for (const required of schema.required || []) {\\n if (!(required in args)) {\\n throw new Error(`${name} requires ${required}`);\\n }\\n }\\n for (const constraint of schema.allOf || []) {\\n if (Array.isArray(constraint.oneOf)) {\\n const matches = constraint.oneOf.filter((branch) => {\\n const hasRequired = (branch.required || []).every((key) => key in args);\\n const violatesNot = branch.not?.required\\n ? branch.not.required.every((key) => key in args)\\n : false;\\n return hasRequired && !violatesNot;\\n });\\n if (matches.length !== 1) {\\n throw new Error(\\n `${name} arguments do not match exactly one allowed shape`\\n );\\n }\\n }\\n }\\n for (const [key, value] of Object.entries(args)) {\\n const property = properties[key];\\n if (property.const !== undefined && value !== property.const) {\\n throw new Error(`${name}.${key} is not an allowed value`);\\n }\\n if (!property.type) {\\n continue;\\n }\\n const validType =\\n property.type === \\"array\\"\\n ? Array.isArray(value)\\n : property.type === \\"object\\"\\n ? isPlainObject(value)\\n : property.type === \\"integer\\"\\n ? Number.isSafeInteger(value)\\n : typeof value === property.type;\\n if (!validType) {\\n throw new Error(`${name}.${key} must be ${property.type}`);\\n }\\n if (property.enum && !property.enum.includes(value)) {\\n throw new Error(`${name}.${key} is not an allowed value`);\\n }\\n if (\\n typeof value === \\"string\\" &&\\n ((property.minLength !== undefined &&\\n value.length < property.minLength) ||\\n (property.maxLength !== undefined && value.length > property.maxLength))\\n ) {\\n throw new Error(`${name}.${key} has an invalid length`);\\n }\\n if (\\n typeof value === \\"number\\" &&\\n property.minimum !== undefined &&\\n value < property.minimum\\n ) {\\n throw new Error(`${name}.${key} is below the minimum`);\\n }\\n if (property.pattern && !new RegExp(property.pattern).test(value)) {\\n throw new Error(`${name}.${key} has an invalid format`);\\n }\\n if (\\n property.type === \\"array\\" &&\\n property.items?.type &&\\n value.some((item) => typeof item !== property.items.type)\\n ) {\\n throw new Error(`${name}.${key} contains an invalid item`);\\n }\\n if (\\n property.type === \\"array\\" &&\\n property.items?.pattern &&\\n value.some((item) => !new RegExp(property.items.pattern).test(item))\\n ) {\\n throw new Error(`${name}.${key} contains an invalid item`);\\n }\\n }\\n}\\n\\nfunction scopeArgs(scope) {\\n if (scope === \\"global\\") {\\n return [\\"--global\\"];\\n }\\n if (scope === \\"project\\") {\\n return [\\"--project\\"];\\n }\\n return [];\\n}\\n\\nfunction boolFlag(name, value) {\\n return value ? [name] : [];\\n}\\n\\nfunction stringFlag(name, value) {\\n return typeof value === \\"string\\" && value.trim() ? [name, value] : [];\\n}\\n\\nfunction repeatedStringFlag(name, values) {\\n return Array.isArray(values)\\n ? values.flatMap((value) => stringFlag(name, value))\\n : [];\\n}\\n\\nfunction requireString(name, value) {\\n if (typeof value !== \\"string\\" || !value.trim()) {\\n throw new Error(`${name} is required`);\\n }\\n return value;\\n}\\n\\nfunction requireMutationApproval(name, args) {\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(`${name} requires an explicit global or project scope`);\\n }\\n if (args.approve !== true) {\\n throw new Error(`${name} requires approve=true`);\\n }\\n}\\n\\nfunction capabilityCommand(args) {\\n const action = args.action;\\n if (action === \\"scan\\") {\\n return [\\"scan\\", \\"--json\\"];\\n }\\n if (action === \\"inventory\\") {\\n return [\\"inventory\\", ...scopeArgs(args.scope), \\"--json\\"];\\n }\\n if (action === \\"list\\") {\\n return [\\"list\\", args.kind || \\"skills\\", ...scopeArgs(args.scope), \\"--json\\"];\\n }\\n if (action === \\"show\\") {\\n return [\\n \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n ...scopeArgs(args.scope),\\n ];\\n }\\n if (action === \\"find\\") {\\n return [\\n \\"find\\",\\n requireString(\\"query\\", args.query),\\n ...scopeArgs(args.scope),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"graph\\") {\\n return [\\n \\"graph\\",\\n args.graphMode || \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n ...scopeArgs(args.scope),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"adapters\\") {\\n return [\\"adapters\\", \\"--json\\"];\\n }\\n if (action === \\"managed_status\\") {\\n return [\\"managed\\", ...scopeArgs(args.scope)];\\n }\\n if (action === \\"templates_list\\") {\\n return [\\"templates\\", \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"snippet_list\\") {\\n return [\\"snippets\\", \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"snippet_show\\") {\\n return [\\n \\"snippets\\",\\n \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported capability action: ${action}`);\\n}\\n\\nconst WORKFLOW_MUTATIONS = new Set([\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"writeback_dismiss\\",\\n \\"writeback_promote\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_accept\\",\\n \\"evolve_reject\\",\\n \\"evolve_supersede\\",\\n \\"evolve_verify\\",\\n]);\\n\\nconst WORKFLOW_ACTION_FIELDS = {\\n writeback_list: [],\\n writeback_show: [\\"id\\"],\\n writeback_group: [\\"by\\"],\\n writeback_summarize: [\\"by\\"],\\n writeback_add: [\\n \\"kind\\",\\n \\"category\\",\\n \\"summary\\",\\n \\"details\\",\\n \\"impact\\",\\n \\"attemptedWorkaround\\",\\n \\"desiredOutcome\\",\\n \\"sensitivity\\",\\n \\"asset\\",\\n \\"evidence\\",\\n \\"confidence\\",\\n \\"approve\\",\\n ],\\n writeback_link: [\\"id\\", \\"issue\\", \\"approve\\"],\\n writeback_disposition: [\\n \\"id\\",\\n \\"disposition\\",\\n \\"target\\",\\n \\"nextTrigger\\",\\n \\"expectedOutcome\\",\\n \\"approve\\",\\n ],\\n evolve_assess: [\\"asset\\"],\\n evolve_list: [],\\n evolve_show: [\\"id\\"],\\n evolve_propose: [\\"asset\\", \\"approve\\"],\\n evolve_draft: [\\"id\\", \\"append\\", \\"approve\\"],\\n evolve_review: [\\"id\\", \\"approve\\"],\\n evolve_verify: [\\"id\\", \\"effectiveness\\", \\"evidence\\", \\"note\\", \\"approve\\"],\\n};\\n\\nfunction requireOnlyWorkflowFields(args) {\\n const fields = WORKFLOW_ACTION_FIELDS[args.action];\\n if (!fields) {\\n return;\\n }\\n const allowed = new Set([\\"action\\", \\"scope\\", \\"cwd\\", ...fields]);\\n const unexpected = Object.keys(args).filter((key) => !allowed.has(key));\\n if (unexpected.length > 0) {\\n throw new Error(\\n `${args.action} received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n}\\n\\nfunction workflowCommand(args) {\\n const action = args.action;\\n requireOnlyWorkflowFields(args);\\n if (WORKFLOW_MUTATIONS.has(action)) {\\n requireMutationApproval(action, args);\\n }\\n const scope = scopeArgs(args.scope);\\n if (action === \\"writeback_list\\") {\\n return [\\"ai\\", \\"writeback\\", ...scope, \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"writeback_show\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"show\\",\\n requireString(\\"id\\", args.id),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_group\\" || action === \\"writeback_summarize\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n action === \\"writeback_group\\" ? \\"group\\" : \\"summarize\\",\\n ...stringFlag(\\"--by\\", args.by),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_add\\") {\\n if (!Array.isArray(args.evidence) || args.evidence.length === 0) {\\n throw new Error(\\"writeback_add requires at least one evidence reference\\");\\n }\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"add\\",\\n \\"--kind\\",\\n requireString(\\"kind\\", args.kind),\\n \\"--summary\\",\\n requireString(\\"summary\\", args.summary),\\n ...stringFlag(\\"--asset\\", args.asset),\\n ...stringFlag(\\"--category\\", args.category),\\n ...stringFlag(\\"--details\\", args.details),\\n ...stringFlag(\\"--impact\\", args.impact),\\n ...stringFlag(\\"--attempted-workaround\\", args.attemptedWorkaround),\\n ...stringFlag(\\"--desired-outcome\\", args.desiredOutcome),\\n ...stringFlag(\\"--sensitivity\\", args.sensitivity),\\n ...repeatedStringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_link\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"link\\",\\n requireString(\\"id\\", args.id),\\n \\"--issue\\",\\n requireString(\\"issue\\", args.issue),\\n ];\\n }\\n if (action === \\"writeback_disposition\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"disposition\\",\\n requireString(\\"id\\", args.id),\\n \\"--type\\",\\n requireString(\\"disposition\\", args.disposition),\\n ...stringFlag(\\"--target\\", args.target),\\n ...stringFlag(\\"--next-trigger\\", args.nextTrigger),\\n ...stringFlag(\\"--expected-outcome\\", args.expectedOutcome),\\n ];\\n }\\n if (action === \\"writeback_dismiss\\" || action === \\"writeback_promote\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n action === \\"writeback_dismiss\\" ? \\"dismiss\\" : \\"promote\\",\\n requireString(\\"id\\", args.id),\\n ];\\n }\\n if (action === \\"evolve_assess\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"assess\\",\\n ...stringFlag(\\"--asset\\", args.asset),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_list\\") {\\n return [\\"ai\\", \\"evolve\\", ...scope, \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"evolve_show\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"show\\",\\n requireString(\\"id\\", args.id),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_propose\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"propose\\",\\n \\"--asset\\",\\n requireString(\\"asset\\", args.asset),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_draft\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"draft\\",\\n requireString(\\"id\\", args.id),\\n ...stringFlag(\\"--append\\", args.append),\\n ];\\n }\\n if (action === \\"evolve_review\\" || action === \\"evolve_accept\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n action === \\"evolve_review\\" ? \\"review\\" : \\"accept\\",\\n requireString(\\"id\\", args.id),\\n ];\\n }\\n if (action === \\"evolve_reject\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"reject\\",\\n requireString(\\"id\\", args.id),\\n \\"--reason\\",\\n requireString(\\"reason\\", args.reason),\\n ];\\n }\\n if (action === \\"evolve_supersede\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"supersede\\",\\n requireString(\\"id\\", args.id),\\n \\"--by\\",\\n requireString(\\"byProposal\\", args.byProposal),\\n ];\\n }\\n if (action === \\"evolve_verify\\") {\\n if (!Array.isArray(args.evidence) || args.evidence.length === 0) {\\n throw new Error(\\"evolve_verify requires at least one evidence reference\\");\\n }\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"verify\\",\\n requireString(\\"id\\", args.id),\\n \\"--effectiveness\\",\\n requireString(\\"effectiveness\\", args.effectiveness),\\n ...repeatedStringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--note\\", args.note),\\n ];\\n }\\n throw new Error(`Unsupported workflow action: ${action}`);\\n}\\n\\nfunction syncCommand(args) {\\n if (args.action === \\"status\\") {\\n return [\\"managed\\", ...scopeArgs(args.scope)];\\n }\\n if (args.action === \\"preview\\") {\\n return [\\n \\"sync\\",\\n ...(args.tool ? [args.tool] : []),\\n \\"--dry-run\\",\\n ...scopeArgs(args.scope),\\n ];\\n }\\n throw new Error(`Unsupported sync action: ${args.action}`);\\n}\\n\\nfunction requireOnlyRegistryFields(args, fields) {\\n const allowed = new Set([\\"action\\", \\"scope\\", \\"cwd\\", ...fields]);\\n const unexpected = Object.keys(args).filter((key) => !allowed.has(key));\\n if (unexpected.length > 0) {\\n throw new Error(\\n `${args.action} received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n}\\n\\nfunction registryCommand(args) {\\n if (args.action === \\"activity_decide\\") {\\n const expectedFields = new Set([\\n \\"action\\",\\n \\"locator\\",\\n \\"decision\\",\\n \\"expectedRevision\\",\\n \\"actor\\",\\n \\"approvalReference\\",\\n \\"note\\",\\n \\"redirectTarget\\",\\n \\"approve\\",\\n ]);\\n const unexpected = Object.keys(args).filter(\\n (key) => !expectedFields.has(key)\\n );\\n if (unexpected.length > 0) {\\n throw new Error(\\n `activity_decide received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n if (args.approve !== true) {\\n throw new Error(\\"activity_decide requires approve=true\\");\\n }\\n if (!Number.isSafeInteger(args.expectedRevision)) {\\n throw new Error(\\n \\"activity_decide requires expectedRevision as an integer\\"\\n );\\n }\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n \\"decide\\",\\n requireString(\\"locator\\", args.locator),\\n \\"--decision\\",\\n requireString(\\"decision\\", args.decision),\\n \\"--expected-revision\\",\\n String(args.expectedRevision),\\n \\"--actor\\",\\n requireString(\\"actor\\", args.actor),\\n ...stringFlag(\\"--approval-ref\\", args.approvalReference),\\n ...stringFlag(\\"--note\\", args.note),\\n ...stringFlag(\\"--redirect-target\\", args.redirectTarget),\\n \\"--approve\\",\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"activity_resolve\\") {\\n const unexpected = Object.keys(args).filter(\\n (key) => key !== \\"action\\" && key !== \\"locator\\"\\n );\\n if (unexpected.length > 0) {\\n throw new Error(\\n `activity_resolve received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n \\"resolve\\",\\n requireString(\\"locator\\", args.locator),\\n \\"--json\\",\\n ];\\n }\\n if (\\n args.scope === \\"project\\" &&\\n args.action !== \\"reconcile_status\\" &&\\n args.action !== \\"reconcile\\"\\n ) {\\n throw new Error(`${args.action} only supports global scope`);\\n }\\n if (args.action === \\"search\\") {\\n requireOnlyRegistryFields(args, [\\"query\\"]);\\n return [\\"search\\", requireString(\\"query\\", args.query), \\"--json\\"];\\n }\\n if (args.action === \\"verify_source\\") {\\n requireOnlyRegistryFields(args, [\\"source\\"]);\\n return [\\"verify-source\\", requireString(\\"source\\", args.source), \\"--json\\"];\\n }\\n if (args.action === \\"source_list\\") {\\n requireOnlyRegistryFields(args, []);\\n return [\\"sources\\", \\"list\\", \\"--json\\"];\\n }\\n if (args.action === \\"install_preview\\") {\\n requireOnlyRegistryFields(args, [\\"item\\", \\"as\\"]);\\n return [\\n \\"install\\",\\n requireString(\\"item\\", args.item),\\n ...stringFlag(\\"--as\\", args.as),\\n \\"--dry-run\\",\\n \\"--strict-source-trust\\",\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"update_check\\") {\\n requireOnlyRegistryFields(args, []);\\n return [\\"update\\", \\"--strict-source-trust\\", \\"--json\\"];\\n }\\n if (args.action === \\"reconcile_status\\") {\\n requireOnlyRegistryFields(args, []);\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(\\n \\"reconcile_status requires an explicit global or project scope\\"\\n );\\n }\\n return [\\"ai\\", \\"review\\", ...scopeArgs(args.scope), \\"status\\", \\"--json\\"];\\n }\\n if (args.action === \\"reconcile\\") {\\n requireOnlyRegistryFields(args, [\\n \\"since\\",\\n \\"until\\",\\n \\"sourceIds\\",\\n \\"incremental\\",\\n ]);\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(\\"reconcile requires an explicit global or project scope\\");\\n }\\n return [\\n \\"ai\\",\\n \\"review\\",\\n ...scopeArgs(args.scope),\\n \\"reconcile\\",\\n \\"--since\\",\\n requireString(\\"since\\", args.since),\\n ...stringFlag(\\"--until\\", args.until),\\n ...repeatedStringFlag(\\"--source\\", args.sourceIds),\\n ...boolFlag(\\"--incremental\\", args.incremental),\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported registry action: ${args.action}`);\\n}\\n\\nfunction isSubpath(child, parent) {\\n const relative = path.relative(parent, child);\\n return (\\n relative === \\"\\" || !(relative.startsWith(\\"..\\") || path.isAbsolute(relative))\\n );\\n}\\n\\nfunction isDirectory(candidate) {\\n try {\\n return fs.statSync(candidate).isDirectory();\\n } catch {\\n return false;\\n }\\n}\\n\\nfunction resolveWorkspaceCwd({ allowHomeFallback = true } = {}) {\\n const candidates = [\\n process.env.FCLT_MCP_WORKSPACE_CWD,\\n process.env.INIT_CWD,\\n process.env.PWD,\\n ];\\n for (const candidate of candidates) {\\n if (typeof candidate !== \\"string\\" || !candidate.trim()) {\\n continue;\\n }\\n const resolved = path.resolve(candidate);\\n const isHome = resolved === path.resolve(os.homedir());\\n if (\\n (allowHomeFallback || !isHome) &&\\n !isSubpath(resolved, PLUGIN_ROOT) &&\\n isDirectory(resolved)\\n ) {\\n return resolved;\\n }\\n }\\n if (allowHomeFallback && isDirectory(os.homedir())) {\\n return os.homedir();\\n }\\n return undefined;\\n}\\n\\nfunction resolveToolCwd(name, args = {}) {\\n if (typeof args.cwd === \\"string\\" && args.cwd.trim()) {\\n return args.cwd;\\n }\\n const inferred = resolveWorkspaceCwd({\\n allowHomeFallback: args.scope !== \\"project\\",\\n });\\n if (inferred) {\\n return inferred;\\n }\\n if (args.scope === \\"project\\") {\\n throw new Error(\\n `${name} with project scope requires a cwd for the target workspace`\\n );\\n }\\n return process.cwd();\\n}\\n\\nfunction commandForTool(name, args = {}) {\\n switch (name) {\\n case \\"fclt_setup\\": {\\n const apply = args.dryRun === false;\\n if (apply && args.approve !== true) {\\n throw new Error(\\"fclt_setup apply requires approve=true\\");\\n }\\n if (\\n args.scope === \\"global_and_project\\" &&\\n (typeof args.cwd !== \\"string\\" || !args.cwd.trim())\\n ) {\\n throw new Error(\\n \\"fclt_setup global_and_project scope requires an explicit cwd\\"\\n );\\n }\\n return [\\n \\"setup\\",\\n \\"--json\\",\\n ...(args.scope === \\"global_and_project\\" ? [\\"--include-project\\"] : []),\\n ...(apply ? [] : [\\"--dry-run\\"]),\\n ...(args.installCodexPlugin === false ? [\\"--no-codex-plugin\\"] : []),\\n ];\\n }\\n case \\"fclt_capability\\":\\n return capabilityCommand(args);\\n case \\"fclt_workflow\\":\\n return workflowCommand(args);\\n case \\"fclt_sync\\":\\n return syncCommand(args);\\n case \\"fclt_registry\\":\\n return registryCommand(args);\\n case \\"fclt_audit\\":\\n return [\\n \\"audit\\",\\n \\"--non-interactive\\",\\n ...(args.target ? [args.target] : []),\\n ...stringFlag(\\"--severity\\", args.severity),\\n \\"--json\\",\\n ];\\n case \\"fclt_automation\\":\\n if (\\n args.action !== \\"loop_activity\\" &&\\n args.scope !== \\"global\\" &&\\n args.scope !== \\"project\\"\\n ) {\\n throw new Error(`${args.action} requires global or project scope`);\\n }\\n if (args.action === \\"autosync_status\\") {\\n return [\\n \\"autosync\\",\\n \\"status\\",\\n ...(args.tool ? [args.tool] : []),\\n ...scopeArgs(args.scope),\\n ];\\n }\\n if (args.tool) {\\n throw new Error(`${args.action} does not accept tool`);\\n }\\n if (args.action === \\"loop_status\\") {\\n return [\\"ai\\", \\"loop\\", ...scopeArgs(args.scope), \\"status\\", \\"--json\\"];\\n }\\n if (args.action === \\"loop_activity\\") {\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n ...scopeArgs(args.scope),\\n \\"activity\\",\\n ...(args.scope === \\"global\\" || args.scope === \\"project\\"\\n ? []\\n : [\\"--all\\"]),\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"loop_preview\\") {\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n ...scopeArgs(args.scope),\\n \\"run\\",\\n \\"--dry-run\\",\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported automation action: ${args.action}`);\\n case \\"fclt_status\\":\\n return [\\"status\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_doctor\\":\\n return [\\"doctor\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_paths\\":\\n return [\\"paths\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_init_operating_model\\":\\n if (args.dryRun === false && args.approve !== true) {\\n throw new Error(\\n \\"fclt_init_operating_model apply requires approve=true\\"\\n );\\n }\\n if (args.force === true && args.approve !== true) {\\n throw new Error(\\n \\"fclt_init_operating_model force requires approve=true\\"\\n );\\n }\\n return [\\n \\"templates\\",\\n \\"init\\",\\n \\"operating-model\\",\\n ...scopeArgs(args.scope),\\n ...boolFlag(\\"--update\\", args.update),\\n ...(args.dryRun === false ? [] : [\\"--dry-run\\"]),\\n ...boolFlag(\\"--force\\", args.force),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_add\\":\\n requireMutationApproval(name, args);\\n requireString(\\"evidence\\", args.evidence);\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scopeArgs(args.scope),\\n \\"add\\",\\n \\"--kind\\",\\n args.kind,\\n \\"--summary\\",\\n args.summary,\\n ...stringFlag(\\"--asset\\", args.asset),\\n ...stringFlag(\\"--category\\", args.category),\\n ...stringFlag(\\"--details\\", args.details),\\n ...stringFlag(\\"--impact\\", args.impact),\\n ...stringFlag(\\"--attempted-workaround\\", args.attemptedWorkaround),\\n ...stringFlag(\\"--desired-outcome\\", args.desiredOutcome),\\n ...stringFlag(\\"--sensitivity\\", args.sensitivity),\\n ...stringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_review\\": {\\n const mode = args.mode || \\"list\\";\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scopeArgs(args.scope),\\n mode,\\n ...stringFlag(\\"--by\\", args.by),\\n ];\\n }\\n case \\"fclt_evolve\\": {\\n const action = args.action || \\"list\\";\\n if ([\\"propose\\", \\"draft\\", \\"review\\"].includes(action)) {\\n requireMutationApproval(`fclt_evolve ${action}`, args);\\n }\\n if (action === \\"propose\\") {\\n requireString(\\"asset\\", args.asset);\\n }\\n if ([\\"draft\\", \\"review\\", \\"show\\"].includes(action)) {\\n requireString(\\"id\\", args.id);\\n }\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scopeArgs(args.scope),\\n action,\\n ...(action === \\"assess\\" || action === \\"propose\\"\\n ? stringFlag(\\"--asset\\", args.asset)\\n : []),\\n ...(args.id ? [args.id] : []),\\n ...(action === \\"assess\\" ? [\\"--json\\"] : []),\\n ];\\n }\\n default:\\n throw new Error(`Unknown tool: ${name}`);\\n }\\n}\\n\\nfunction operationMetadata(name, args, command) {\\n const action =\\n name === \\"fclt_writeback_add\\"\\n ? \\"writeback_add\\"\\n : name === \\"fclt_evolve\\"\\n ? `evolve_${args.action || \\"list\\"}`\\n : args.action || name;\\n const reviewActions = new Set([\\n \\"activity_decide\\",\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_verify\\",\\n \\"reconcile\\",\\n ]);\\n const preview = command.includes(\\"--dry-run\\");\\n const risk = reviewActions.has(action)\\n ? \\"review_producing\\"\\n : !preview && name === \\"fclt_setup\\"\\n ? \\"reversible_mutation\\"\\n : !preview && name === \\"fclt_init_operating_model\\"\\n ? \\"high_risk_destructive\\"\\n : \\"read_only\\";\\n return {\\n tool: name,\\n action,\\n risk,\\n scope:\\n args.scope ||\\n (name === \\"fclt_automation\\" && action === \\"loop_activity\\"\\n ? \\"all\\"\\n : \\"auto\\"),\\n target:\\n args.id ||\\n args.selector ||\\n args.asset ||\\n args.item ||\\n args.source ||\\n args.tool ||\\n args.locator ||\\n null,\\n preview,\\n };\\n}\\n\\nfunction recoveryForOperation(operation, stdout) {\\n if (operation.risk === \\"review_producing\\") {\\n return {\\n canonicalCapabilityChanged: false,\\n audit: \\"native fclt review artifacts and append-only journal\\",\\n };\\n }\\n if (\\n operation.risk !== \\"reversible_mutation\\" &&\\n operation.risk !== \\"high_risk_destructive\\"\\n ) {\\n return null;\\n }\\n const report = isPlainObject(stdout) ? stdout : {};\\n return {\\n rollbackAvailable: false,\\n changedPaths: Array.isArray(report.changedPaths) ? report.changedPaths : [],\\n skippedPaths: Array.isArray(report.skippedPaths) ? report.skippedPaths : [],\\n repairActions: Array.isArray(report.repairActions)\\n ? report.repairActions\\n : [],\\n verification:\\n operation.tool === \\"fclt_setup\\"\\n ? \\"rerun fclt_setup in preview mode and verify doctor readiness\\"\\n : \\"verify doctor, generated state, authored-file hashes, and exact target paths\\",\\n };\\n}\\n\\nasync function runFclt(args, cwd, operation) {\\n const discovery = await runtime.discoverRuntime();\\n if (!discovery.selected) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"no_compatible_runtime\\",\\n message:\\n \\"No compatible fclt runtime is available. Check, stage, and apply an explicit verified version with fclt_runtime.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.tool === \\"fclt_audit\\" &&\\n !discovery.selected.capabilities?.includes(AUDIT_READ_ONLY_CAPABILITY)\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise audit-read-only-v1; typed audit fails closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.action === \\"activity_decide\\" &&\\n !discovery.selected.capabilities?.includes(\\n ACTIVITY_ACTION_DECIDE_CAPABILITY\\n )\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise activity-action-decide-v1; typed activity decisions fail closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.action === \\"activity_resolve\\" &&\\n !discovery.selected.capabilities?.includes(\\n ACTIVITY_ACTION_RESOLVE_CAPABILITY\\n )\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise activity-action-resolve-v1; typed activity resolution fails closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n const result = await runtime.runCommand(discovery.selected.executable, args, {\\n cwd: cwd || process.cwd(),\\n env: process.env,\\n timeoutMs: DEFAULT_TIMEOUT_MS,\\n });\\n const parsedStdout = parseJsonOrText(result.stdout.trim());\\n return {\\n code: result.code,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n runtime: discovery.selected,\\n result: {\\n exitCode: result.code,\\n stdout: parsedStdout,\\n stderr: result.stderr,\\n },\\n verification: {\\n status: result.code === 0 ? \\"passed\\" : \\"failed\\",\\n exitCode: result.code,\\n },\\n recovery: recoveryForOperation(operation, parsedStdout),\\n },\\n null,\\n 2\\n ),\\n };\\n}\\n\\nfunction parseJsonOrText(value) {\\n if (!value) {\\n return \\"\\";\\n }\\n try {\\n return JSON.parse(value);\\n } catch {\\n return value;\\n }\\n}\\n\\nasync function handleRuntimeTool(args = {}) {\\n const action = args.action || \\"status\\";\\n if (action === \\"status\\") {\\n return await runtime.discoverRuntime();\\n }\\n if (action === \\"check\\") {\\n return await runtime.checkRuntimeUpdate();\\n }\\n if (action === \\"policy\\") {\\n return await runtime.setRuntimePolicy({\\n approve: args.approve,\\n pinnedVersion: args.pinnedVersion,\\n clearPin: args.clearPin,\\n updateChecksEnabled: args.updateChecksEnabled,\\n });\\n }\\n if (action === \\"stage\\") {\\n return await runtime.stageRuntime({\\n approve: args.approve,\\n version: args.version,\\n });\\n }\\n if (action === \\"apply\\") {\\n return await runtime.applyStagedRuntime({\\n approve: args.approve,\\n expectedSha256: args.expectedSha256,\\n version: args.version,\\n });\\n }\\n if (action === \\"rollback\\") {\\n return await runtime.rollbackRuntime({\\n approve: args.approve,\\n expectedActiveVersion: args.expectedActiveVersion,\\n });\\n }\\n throw new Error(`Unknown runtime action: ${action}`);\\n}\\n\\nfunction runtimeOperationMetadata(args, result) {\\n const action = args.action || \\"status\\";\\n const risk =\\n action === \\"status\\" || action === \\"check\\"\\n ? \\"read_only\\"\\n : action === \\"stage\\"\\n ? \\"review_producing\\"\\n : \\"high_risk_destructive\\";\\n return {\\n operation: {\\n tool: \\"fclt_runtime\\",\\n action,\\n risk,\\n scope: \\"plugin_runtime\\",\\n target:\\n args.version ||\\n args.pinnedVersion ||\\n args.expectedActiveVersion ||\\n null,\\n approved: args.approve === true,\\n },\\n verification: {\\n status: \\"passed\\",\\n activeVersion:\\n result.active?.packageVersion ||\\n result.selected?.packageVersion ||\\n null,\\n },\\n recovery:\\n action === \\"apply\\" || action === \\"rollback\\"\\n ? {\\n rollbackAvailable: result.rollbackAvailable === true,\\n previous: result.previous || null,\\n }\\n : action === \\"policy\\"\\n ? { previousPolicy: result.previous || null }\\n : null,\\n };\\n}\\n\\nlet transportFraming = \\"content-length\\";\\n\\nfunction send(message) {\\n const body = JSON.stringify(message);\\n if (transportFraming === \\"newline\\") {\\n process.stdout.write(`${body}\\\\n`);\\n return;\\n }\\n process.stdout.write(\\n `Content-Length: ${Buffer.byteLength(body)}\\\\r\\\\n\\\\r\\\\n${body}`\\n );\\n}\\n\\nasync function handle(message) {\\n if (!message || message.id == null) {\\n return;\\n }\\n\\n try {\\n if (message.method === \\"initialize\\") {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n protocolVersion: \\"2025-06-18\\",\\n capabilities: { tools: {} },\\n serverInfo: { name: \\"fclt\\", version: runtime.pluginVersion() },\\n },\\n });\\n return;\\n }\\n if (message.method === \\"tools/list\\") {\\n send({ jsonrpc: \\"2.0\\", id: message.id, result: { tools } });\\n return;\\n }\\n if (message.method === \\"tools/call\\") {\\n const { name, arguments: args = {} } = message.params || {};\\n validateToolArguments(name, args);\\n if (name === \\"fclt_runtime\\") {\\n const result = await handleRuntimeTool(args);\\n const metadata = runtimeOperationMetadata(args, result);\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n isError: false,\\n content: [\\n {\\n type: \\"text\\",\\n text: JSON.stringify({ ...result, ...metadata }, null, 2),\\n },\\n ],\\n },\\n });\\n return;\\n }\\n const command = commandForTool(name, args);\\n const result = await runFclt(\\n command,\\n resolveToolCwd(name, args),\\n operationMetadata(name, args, command)\\n );\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n isError: result.code !== 0,\\n content: [{ type: \\"text\\", text: result.text }],\\n },\\n });\\n return;\\n }\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n error: { code: -32_601, message: `Method not found: ${message.method}` },\\n });\\n } catch (error) {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n error: {\\n code: -32_000,\\n message: error instanceof Error ? error.message : String(error),\\n },\\n });\\n }\\n}\\n\\nlet buffer = Buffer.alloc(0);\\n\\nfunction dispatch(body, framing) {\\n transportFraming = framing;\\n handle(JSON.parse(body)).catch((error) => {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: null,\\n error: {\\n code: -32_000,\\n message: error instanceof Error ? error.message : String(error),\\n },\\n });\\n });\\n}\\n\\nprocess.stdin.on(\\"data\\", (chunk) => {\\n buffer = Buffer.concat([buffer, chunk]);\\n while (true) {\\n while (buffer[0] === 10 || buffer[0] === 13) {\\n buffer = buffer.subarray(1);\\n }\\n if (buffer.length === 0) {\\n return;\\n }\\n if (buffer[0] === 123 || buffer[0] === 91) {\\n const lineEnd = buffer.indexOf(\\"\\\\n\\");\\n if (lineEnd === -1) {\\n return;\\n }\\n const body = buffer.subarray(0, lineEnd).toString(\\"utf8\\").trim();\\n buffer = buffer.subarray(lineEnd + 1);\\n if (body) {\\n dispatch(body, \\"newline\\");\\n }\\n continue;\\n }\\n const headerEnd = buffer.indexOf(\\"\\\\r\\\\n\\\\r\\\\n\\");\\n if (headerEnd === -1) {\\n return;\\n }\\n const header = buffer.slice(0, headerEnd).toString(\\"utf8\\");\\n const match = CONTENT_LENGTH_RE.exec(header);\\n if (!match) {\\n buffer = Buffer.alloc(0);\\n return;\\n }\\n const length = Number(match[1]);\\n const frameEnd = headerEnd + 4 + length;\\n if (buffer.length < frameEnd) {\\n return;\\n }\\n const body = buffer.slice(headerEnd + 4, frameEnd).toString(\\"utf8\\");\\n buffer = buffer.slice(frameEnd);\\n dispatch(body, \\"content-length\\");\\n }\\n});\\n\\nif (process.argv.includes(\\"--self-test\\")) {\\n console.log(\\n JSON.stringify(\\n {\\n pluginVersion: runtime.pluginVersion(),\\n protocolVersion: runtime.PLUGIN_PROTOCOL_VERSION,\\n tools: tools.map((tool) => tool.name),\\n },\\n null,\\n 2\\n )\\n );\\n process.exit(0);\\n}\\n","scripts/fclt-runtime.cjs":"\\"use strict\\";\\n\\nconst { spawn } = require(\\"node:child_process\\");\\nconst crypto = require(\\"node:crypto\\");\\nconst fs = require(\\"node:fs\\");\\nconst fsp = require(\\"node:fs/promises\\");\\nconst https = require(\\"node:https\\");\\nconst os = require(\\"node:os\\");\\nconst path = require(\\"node:path\\");\\n\\nconst PLUGIN_PROTOCOL_VERSION = 1;\\nconst STATE_SCHEMA_VERSION = 1;\\nconst REPOSITORY = \\"hack-dance/fclt\\";\\nconst MAX_BINARY_BYTES = 256 * 1024 * 1024;\\nconst MAX_METADATA_BYTES = 2 * 1024 * 1024;\\nconst DOWNLOAD_TIMEOUT_MS = 30_000;\\nconst COMMAND_TIMEOUT_MS = 15_000;\\nconst ALLOWED_DOWNLOAD_HOSTS = new Set([\\n \\"api.github.com\\",\\n \\"github.com\\",\\n \\"objects.githubusercontent.com\\",\\n \\"release-assets.githubusercontent.com\\",\\n]);\\nconst SEMVER_RE = /^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:[-+][0-9A-Za-z.-]+)?$/;\\nconst SHA256_RE = /^[a-f0-9]{64}$/;\\nconst NEWLINE_RE = /\\\\r?\\\\n/;\\nconst CHECKSUM_LINE_RE = /^([a-fA-F0-9]{64})\\\\s+\\\\*?(.+)$/;\\nconst WINDOWS_SHIM_RE = /\\\\.(?:bat|cmd)$/i;\\n\\nfunction isPlainObject(value) {\\n return Boolean(value) && typeof value === \\"object\\" && !Array.isArray(value);\\n}\\n\\nfunction pluginVersion() {\\n try {\\n const manifest = JSON.parse(\\n fs.readFileSync(\\n path.resolve(__dirname, \\"..\\", \\".codex-plugin\\", \\"plugin.json\\"),\\n \\"utf8\\"\\n )\\n );\\n return typeof manifest.version === \\"string\\" ? manifest.version : \\"unknown\\";\\n } catch {\\n return \\"unknown\\";\\n }\\n}\\n\\nfunction runtimeStateRoot(env = process.env, platform = process.platform) {\\n if (env.FCLT_PLUGIN_RUNTIME_DIR) {\\n return path.resolve(env.FCLT_PLUGIN_RUNTIME_DIR);\\n }\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n if (platform === \\"darwin\\") {\\n return path.join(\\n home,\\n \\"Library\\",\\n \\"Application Support\\",\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n }\\n if (platform === \\"win32\\") {\\n return path.join(\\n env.LOCALAPPDATA || path.join(home, \\"AppData\\", \\"Local\\"),\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n }\\n return path.join(\\n env.XDG_STATE_HOME || path.join(home, \\".local\\", \\"state\\"),\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n}\\n\\nfunction installStatePaths(env = process.env, platform = process.platform) {\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n const override = env.FACULT_LOCAL_STATE_DIR?.trim();\\n const portableRoot = override\\n ? path.resolve(override)\\n : platform === \\"darwin\\"\\n ? path.join(home, \\"Library\\", \\"Application Support\\", \\"fclt\\")\\n : path.join(\\n env.XDG_STATE_HOME\\n ? path.resolve(env.XDG_STATE_HOME)\\n : path.join(home, \\".local\\", \\"state\\"),\\n \\"fclt\\"\\n );\\n const candidates = [\\n path.join(portableRoot, \\"install.json\\"),\\n ...(platform === \\"win32\\"\\n ? [\\n path.join(\\n env.LOCALAPPDATA || path.join(home, \\"AppData\\", \\"Local\\"),\\n \\"fclt\\",\\n \\"install.json\\"\\n ),\\n ]\\n : []),\\n path.join(home, \\".ai\\", \\".facult\\", \\"install.json\\"),\\n path.join(home, \\".facult\\", \\"install.json\\"),\\n path.join(home, \\".local\\", \\"share\\", \\"fclt\\", \\"install.json\\"),\\n ];\\n return [...new Set(candidates.map((candidate) => path.resolve(candidate)))];\\n}\\n\\nfunction isSubpath(child, parent) {\\n const relative = path.relative(parent, child);\\n return (\\n relative === \\"\\" || !(relative.startsWith(\\"..\\") || path.isAbsolute(relative))\\n );\\n}\\n\\nasync function assertManagedPath(target, root) {\\n const resolvedRoot = path.resolve(root);\\n const resolvedTarget = path.resolve(target);\\n if (!isSubpath(resolvedTarget, resolvedRoot)) {\\n throw new Error(\\"Runtime path escapes the managed runtime root.\\");\\n }\\n\\n const relative = path.relative(resolvedRoot, path.dirname(resolvedTarget));\\n const segments = relative ? relative.split(path.sep) : [];\\n let cursor = resolvedRoot;\\n for (const segment of segments) {\\n cursor = path.join(cursor, segment);\\n try {\\n if ((await fsp.lstat(cursor)).isSymbolicLink()) {\\n throw new Error(\\"Runtime path traverses a symbolic link.\\");\\n }\\n } catch (error) {\\n if (error && error.code === \\"ENOENT\\") {\\n continue;\\n }\\n throw error;\\n }\\n }\\n return resolvedTarget;\\n}\\n\\nasync function readJson(pathValue) {\\n try {\\n const value = JSON.parse(await fsp.readFile(pathValue, \\"utf8\\"));\\n return isPlainObject(value) ? value : null;\\n } catch {\\n return null;\\n }\\n}\\n\\nasync function runtimePolicy(options = {}) {\\n const root = runtimeStateRoot(options.env, options.platform);\\n const persisted = await readJson(path.join(root, \\"policy.json\\"));\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n updateChecksEnabled: persisted?.updateChecksEnabled !== false,\\n pinnedVersion:\\n typeof persisted?.pinnedVersion === \\"string\\" &&\\n persisted.pinnedVersion.trim()\\n ? normalizeVersion(persisted.pinnedVersion)\\n : null,\\n };\\n}\\n\\nfunction commandNames(platform = process.platform) {\\n return platform === \\"win32\\"\\n ? [\\"fclt.exe\\", \\"fclt.cmd\\", \\"facult.exe\\", \\"facult.cmd\\"]\\n : [\\"fclt\\", \\"facult\\"];\\n}\\n\\nfunction pathCandidates(env = process.env, platform = process.platform) {\\n const values = [];\\n for (const directory of (env.PATH || \\"\\").split(path.delimiter)) {\\n if (!directory) {\\n continue;\\n }\\n for (const name of commandNames(platform)) {\\n values.push(path.join(directory, name));\\n }\\n }\\n return values;\\n}\\n\\nfunction systemPathCandidates(env = process.env, platform = process.platform) {\\n if (Object.hasOwn(env, \\"FCLT_SYSTEM_PATHS\\")) {\\n return (env.FCLT_SYSTEM_PATHS || \\"\\").split(path.delimiter).filter(Boolean);\\n }\\n if (platform === \\"darwin\\") {\\n return [\\"/opt/homebrew/bin/fclt\\", \\"/usr/local/bin/fclt\\"];\\n }\\n if (platform === \\"win32\\") {\\n return [];\\n }\\n return [\\"/usr/local/bin/fclt\\", \\"/usr/bin/fclt\\"];\\n}\\n\\nfunction candidateSource(candidate) {\\n const normalized = candidate.split(\\"\\\\\\\\\\").join(\\"/\\");\\n if (normalized.includes(\\"/plugin-runtime/versions/\\")) {\\n return \\"plugin_runtime\\";\\n }\\n if (normalized.includes(\\"/mise/\\") || normalized.includes(\\"/mise/installs/\\")) {\\n return \\"mise\\";\\n }\\n if (\\n normalized.includes(\\"/Cellar/\\") ||\\n normalized.startsWith(\\"/opt/homebrew/\\")\\n ) {\\n return \\"homebrew\\";\\n }\\n if (normalized.includes(\\"/node_modules/\\") || normalized.includes(\\"/npm/\\")) {\\n return \\"npm\\";\\n }\\n if (normalized.includes(\\"/.ai/.facult/bin/\\")) {\\n return \\"canonical_install\\";\\n }\\n return \\"path\\";\\n}\\n\\nasync function activeRuntimeCandidate(root) {\\n const active = await readJson(path.join(root, \\"active.json\\"));\\n if (typeof active?.executable !== \\"string\\" || !active.executable.trim()) {\\n return null;\\n }\\n const executable = path.resolve(active.executable);\\n if (!isSubpath(executable, path.join(root, \\"versions\\"))) {\\n return null;\\n }\\n return {\\n executable,\\n source: \\"plugin_runtime\\",\\n expectedSha256: active.sha256,\\n active,\\n };\\n}\\n\\nasync function persistedInstallCandidates(\\n env = process.env,\\n platform = process.platform\\n) {\\n const candidates = [];\\n for (const statePath of installStatePaths(env, platform)) {\\n const state = await readJson(statePath);\\n if (typeof state?.binaryPath === \\"string\\" && state.binaryPath.trim()) {\\n candidates.push({\\n executable: path.resolve(state.binaryPath),\\n source:\\n typeof state.source === \\"string\\" ? state.source : \\"install_metadata\\",\\n installStatePath: statePath,\\n });\\n }\\n }\\n return candidates;\\n}\\n\\nasync function runtimeCandidates(options = {}) {\\n const env = options.env || process.env;\\n const platform = options.platform || process.platform;\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n const root = runtimeStateRoot(env, platform);\\n const candidates = [];\\n let configuredPathCandidate = null;\\n\\n if (env.FCLT_BIN?.trim()) {\\n const explicit = env.FCLT_BIN.trim();\\n if (path.isAbsolute(explicit) || explicit.includes(path.sep)) {\\n candidates.push({\\n executable: path.resolve(explicit),\\n source: \\"explicit\\",\\n });\\n } else {\\n const resolved = pathCandidates(env, platform).find(\\n (candidate) =>\\n path.basename(candidate) === explicit && fs.existsSync(candidate)\\n );\\n configuredPathCandidate = {\\n executable: resolved || explicit,\\n source: \\"configured_path\\",\\n };\\n }\\n }\\n\\n const active = await activeRuntimeCandidate(root);\\n if (active) {\\n candidates.push(active);\\n }\\n if (configuredPathCandidate) {\\n candidates.push(configuredPathCandidate);\\n }\\n candidates.push(...(await persistedInstallCandidates(env, platform)));\\n candidates.push(\\n ...pathCandidates(env, platform).map((executable) => ({\\n executable,\\n source: candidateSource(executable),\\n }))\\n );\\n\\n for (const executable of [\\n path.join(home, \\".ai\\", \\".facult\\", \\"bin\\", commandNames(platform)[0]),\\n ...systemPathCandidates(env, platform),\\n ]) {\\n candidates.push({ executable, source: candidateSource(executable) });\\n }\\n\\n const unique = [];\\n const seen = new Set();\\n for (const candidate of candidates) {\\n const key = path.resolve(candidate.executable);\\n if (!seen.has(key)) {\\n seen.add(key);\\n unique.push(candidate);\\n }\\n }\\n return unique;\\n}\\n\\nfunction runCommand(executable, args, options = {}) {\\n return new Promise((resolve) => {\\n let child;\\n try {\\n const platform = options.platform || process.platform;\\n const windowsShim =\\n platform === \\"win32\\" && WINDOWS_SHIM_RE.test(executable);\\n const command = windowsShim\\n ? options.env?.ComSpec || process.env.ComSpec || \\"cmd.exe\\"\\n : executable;\\n const commandArgs = windowsShim\\n ? [\\n \\"/d\\",\\n \\"/v:off\\",\\n \\"/s\\",\\n \\"/c\\",\\n [executable, ...args]\\n .map(\\n (value) =>\\n `\\"${String(value)\\n .replaceAll(\\"%\\", \\"%%\\")\\n .replace(/[\\\\^&|<>()!\\"]/g, \\"^$&\\")}\\"`\\n )\\n .join(\\" \\"),\\n ]\\n : args;\\n child = spawn(command, commandArgs, {\\n cwd: options.cwd || process.cwd(),\\n env: options.env || process.env,\\n stdio: [\\"ignore\\", \\"pipe\\", \\"pipe\\"],\\n });\\n } catch (error) {\\n resolve({ code: 1, stdout: \\"\\", stderr: error.message });\\n return;\\n }\\n let stdout = \\"\\";\\n let stderr = \\"\\";\\n let settled = false;\\n const timer = setTimeout(\\n () => child.kill(\\"SIGTERM\\"),\\n options.timeoutMs || COMMAND_TIMEOUT_MS\\n );\\n const finish = (code, error) => {\\n if (settled) {\\n return;\\n }\\n settled = true;\\n clearTimeout(timer);\\n resolve({\\n code,\\n stdout,\\n stderr: [stderr.trim(), error].filter(Boolean).join(\\"\\\\n\\"),\\n });\\n };\\n child.stdout.on(\\"data\\", (chunk) => {\\n stdout += chunk.toString();\\n if (stdout.length > MAX_METADATA_BYTES) {\\n child.kill(\\"SIGTERM\\");\\n }\\n });\\n child.stderr.on(\\"data\\", (chunk) => {\\n stderr += chunk.toString();\\n if (stderr.length > MAX_METADATA_BYTES) {\\n child.kill(\\"SIGTERM\\");\\n }\\n });\\n child.on(\\"error\\", (error) => finish(1, error.message));\\n child.on(\\"close\\", (code) => finish(code ?? 1));\\n });\\n}\\n\\nfunction parseProtocolReport(raw) {\\n try {\\n const report = JSON.parse(raw);\\n if (\\n !isPlainObject(report) ||\\n report.schemaVersion !== 1 ||\\n typeof report.packageVersion !== \\"string\\" ||\\n !isPlainObject(report.protocol) ||\\n !Number.isInteger(report.protocol.version) ||\\n !Number.isInteger(report.protocol.minimumPluginVersion) ||\\n !Number.isInteger(report.protocol.maximumPluginVersion) ||\\n (report.capabilities !== undefined &&\\n (!Array.isArray(report.capabilities) ||\\n report.capabilities.some((value) => typeof value !== \\"string\\")))\\n ) {\\n return null;\\n }\\n return report;\\n } catch {\\n return null;\\n }\\n}\\n\\nfunction protocolCompatibility(report) {\\n if (!report) {\\n return { compatible: false, reason: \\"missing_protocol_handshake\\" };\\n }\\n const compatible =\\n report.protocol.minimumPluginVersion <= PLUGIN_PROTOCOL_VERSION &&\\n report.protocol.maximumPluginVersion >= PLUGIN_PROTOCOL_VERSION;\\n return {\\n compatible,\\n reason: compatible ? \\"compatible\\" : \\"protocol_version_skew\\",\\n };\\n}\\n\\nasync function inspectCandidate(candidate, options = {}) {\\n const executable = path.resolve(candidate.executable);\\n try {\\n const stat = await fsp.stat(executable);\\n if (!stat.isFile()) {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"not_a_file\\",\\n };\\n }\\n } catch {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"not_found\\",\\n };\\n }\\n\\n if (candidate.source === \\"plugin_runtime\\") {\\n if (\\n typeof candidate.expectedSha256 !== \\"string\\" ||\\n !SHA256_RE.test(candidate.expectedSha256)\\n ) {\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: false,\\n reason: \\"missing_checksum\\",\\n };\\n }\\n try {\\n const actualSha256 = sha256(await fsp.readFile(executable));\\n if (actualSha256 !== candidate.expectedSha256) {\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: false,\\n reason: \\"checksum_mismatch\\",\\n };\\n }\\n } catch {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"checksum_unreadable\\",\\n };\\n }\\n }\\n\\n const result = await runCommand(executable, [\\"protocol\\", \\"--json\\"], options);\\n const report =\\n result.code === 0 ? parseProtocolReport(result.stdout.trim()) : null;\\n const compatibility = protocolCompatibility(report);\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: compatibility.compatible,\\n reason: compatibility.reason,\\n packageVersion: report?.packageVersion,\\n protocol: report?.protocol,\\n platform: report?.runtime?.platform,\\n architecture: report?.runtime?.architecture,\\n capabilities: report?.capabilities || [],\\n };\\n}\\n\\nasync function discoverRuntime(options = {}) {\\n const policy = await runtimePolicy(options);\\n const inspected = [];\\n for (const candidate of await runtimeCandidates(options)) {\\n const result = await inspectCandidate(candidate, options);\\n inspected.push(result);\\n if (result.compatible) {\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n plugin: {\\n version: pluginVersion(),\\n protocolVersion: PLUGIN_PROTOCOL_VERSION,\\n },\\n policy,\\n selected: result,\\n compatible: true,\\n requiresFreshSession: false,\\n candidates: inspected,\\n };\\n }\\n }\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n plugin: {\\n version: pluginVersion(),\\n protocolVersion: PLUGIN_PROTOCOL_VERSION,\\n },\\n policy,\\n selected: null,\\n compatible: false,\\n requiresFreshSession: false,\\n candidates: inspected,\\n };\\n}\\n\\nfunction releaseTarget(\\n platform = process.platform,\\n architecture = process.arch\\n) {\\n if (\\n platform === \\"darwin\\" &&\\n (architecture === \\"arm64\\" || architecture === \\"x64\\")\\n ) {\\n return { platform: \\"darwin\\", architecture, extension: \\"\\" };\\n }\\n if (platform === \\"linux\\" && architecture === \\"x64\\") {\\n return { platform: \\"linux\\", architecture, extension: \\"\\" };\\n }\\n if (platform === \\"win32\\" && architecture === \\"x64\\") {\\n return { platform: \\"windows\\", architecture, extension: \\".exe\\" };\\n }\\n throw new Error(\\n `Unsupported plugin runtime target: ${platform}/${architecture}`\\n );\\n}\\n\\nfunction normalizeVersion(version) {\\n const normalized = version?.startsWith(\\"v\\") ? version.slice(1) : version;\\n if (!(normalized && SEMVER_RE.test(normalized))) {\\n throw new Error(\\"Runtime version must be an explicit semantic version.\\");\\n }\\n return normalized;\\n}\\n\\nfunction assertAllowedUrl(urlValue) {\\n const url = new URL(urlValue);\\n if (url.protocol !== \\"https:\\" || !ALLOWED_DOWNLOAD_HOSTS.has(url.hostname)) {\\n throw new Error(\\n \\"Runtime downloads are restricted to approved HTTPS release hosts.\\"\\n );\\n }\\n return url;\\n}\\n\\nfunction downloadBuffer(urlValue, options = {}) {\\n const maxBytes = options.maxBytes || MAX_METADATA_BYTES;\\n const redirectsRemaining = options.redirectsRemaining ?? 5;\\n const url = assertAllowedUrl(urlValue);\\n return new Promise((resolve, reject) => {\\n const request = https.get(\\n url,\\n {\\n headers: {\\n accept: options.accept || \\"application/octet-stream\\",\\n \\"user-agent\\": \\"fclt-codex-plugin\\",\\n },\\n },\\n (response) => {\\n if (\\n response.statusCode &&\\n response.statusCode >= 300 &&\\n response.statusCode < 400 &&\\n response.headers.location\\n ) {\\n response.resume();\\n if (redirectsRemaining <= 0) {\\n reject(new Error(\\"Runtime download exceeded the redirect limit.\\"));\\n return;\\n }\\n const redirected = new URL(response.headers.location, url).toString();\\n downloadBuffer(redirected, {\\n ...options,\\n redirectsRemaining: redirectsRemaining - 1,\\n }).then(resolve, reject);\\n return;\\n }\\n if (response.statusCode !== 200) {\\n response.resume();\\n reject(\\n new Error(\\n `Runtime download failed with HTTP ${response.statusCode}.`\\n )\\n );\\n return;\\n }\\n const declaredLength = Number(response.headers[\\"content-length\\"] || 0);\\n if (declaredLength > maxBytes) {\\n response.resume();\\n reject(new Error(\\"Runtime download exceeds the allowed size.\\"));\\n return;\\n }\\n const chunks = [];\\n let total = 0;\\n response.on(\\"data\\", (chunk) => {\\n total += chunk.length;\\n if (total > maxBytes) {\\n request.destroy(\\n new Error(\\"Runtime download exceeds the allowed size.\\")\\n );\\n return;\\n }\\n chunks.push(chunk);\\n });\\n response.on(\\"end\\", () => resolve(Buffer.concat(chunks)));\\n }\\n );\\n request.setTimeout(options.timeoutMs || DOWNLOAD_TIMEOUT_MS, () => {\\n request.destroy(new Error(\\"Runtime download timed out.\\"));\\n });\\n request.on(\\"error\\", reject);\\n });\\n}\\n\\nfunction sha256(bytes) {\\n return crypto.createHash(\\"sha256\\").update(bytes).digest(\\"hex\\");\\n}\\n\\nfunction checksumForAsset(checksums, assetName) {\\n for (const line of checksums.split(NEWLINE_RE)) {\\n const match = CHECKSUM_LINE_RE.exec(line.trim());\\n if (match?.[2] === assetName) {\\n return match[1].toLowerCase();\\n }\\n }\\n throw new Error(`Published checksums do not include ${assetName}.`);\\n}\\n\\nasync function withMutationLock(root, action) {\\n await fsp.mkdir(root, { recursive: true, mode: 0o700 });\\n const lockPath = await assertManagedPath(\\n path.join(root, \\"mutation.lock\\"),\\n root\\n );\\n let handle;\\n try {\\n handle = await fsp.open(lockPath, \\"wx\\", 0o600);\\n } catch (error) {\\n if (error?.code === \\"EEXIST\\") {\\n throw new Error(\\n \\"Another fclt plugin runtime mutation is already in progress.\\"\\n );\\n }\\n throw error;\\n }\\n try {\\n return await action();\\n } finally {\\n await handle.close();\\n await fsp.rm(lockPath, { force: true });\\n }\\n}\\n\\nasync function writeJsonAtomic(pathValue, value, root) {\\n const target = await assertManagedPath(pathValue, root);\\n await fsp.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });\\n const temporary = `${target}.tmp-${crypto.randomUUID()}`;\\n await fsp.writeFile(temporary, `${JSON.stringify(value, null, 2)}\\\\n`, {\\n mode: 0o600,\\n });\\n await fsp.rename(temporary, target);\\n}\\n\\nfunction releaseUrls(version, target) {\\n const tag = `v${version}`;\\n const assetName = `fclt-${version}-${target.platform}-${target.architecture}${target.extension}`;\\n const base = `https://github.com/${REPOSITORY}/releases/download/${tag}`;\\n return {\\n tag,\\n assetName,\\n binaryUrl: `${base}/${assetName}`,\\n checksumUrl: `${base}/SHA256SUMS`,\\n };\\n}\\n\\nfunction releaseMetadataUrl(version) {\\n return `https://api.github.com/repos/${REPOSITORY}/releases/tags/v${version}`;\\n}\\n\\nfunction releaseAssets(metadata, version, target) {\\n if (\\n !isPlainObject(metadata) ||\\n metadata.tag_name !== `v${version}` ||\\n !Array.isArray(metadata.assets)\\n ) {\\n throw new Error(\\n \\"Release metadata does not match the requested immutable tag.\\"\\n );\\n }\\n const expected = releaseUrls(version, target);\\n const findAsset = (name) =>\\n metadata.assets.find(\\n (asset) =>\\n isPlainObject(asset) &&\\n asset.name === name &&\\n typeof asset.browser_download_url === \\"string\\"\\n );\\n const binary = findAsset(expected.assetName);\\n const checksums = findAsset(\\"SHA256SUMS\\");\\n if (!(binary && checksums)) {\\n throw new Error(\\n \\"Release metadata is missing the required runtime or checksum asset.\\"\\n );\\n }\\n assertAllowedUrl(binary.browser_download_url);\\n assertAllowedUrl(checksums.browser_download_url);\\n return { binary, checksums, expected };\\n}\\n\\nfunction verifyPublishedDigest(asset, bytes) {\\n if (typeof asset.digest !== \\"string\\" || !asset.digest.trim()) {\\n return null;\\n }\\n const [algorithm, expected] = asset.digest.toLowerCase().split(\\":\\");\\n if (algorithm !== \\"sha256\\" || !SHA256_RE.test(expected || \\"\\")) {\\n throw new Error(`Release asset ${asset.name} has an unsupported digest.`);\\n }\\n const actual = sha256(bytes);\\n if (actual !== expected) {\\n throw new Error(\\n `Release asset ${asset.name} does not match its published digest.`\\n );\\n }\\n return asset.digest.toLowerCase();\\n}\\n\\nasync function resolveLatestVersion(fetchBuffer = downloadBuffer) {\\n const bytes = await fetchBuffer(\\n `https://api.github.com/repos/${REPOSITORY}/releases/latest`,\\n {\\n maxBytes: MAX_METADATA_BYTES,\\n accept: \\"application/vnd.github+json\\",\\n }\\n );\\n const metadata = JSON.parse(bytes.toString(\\"utf8\\"));\\n if (!isPlainObject(metadata) || typeof metadata.tag_name !== \\"string\\") {\\n throw new Error(\\"Latest release metadata did not include a tag.\\");\\n }\\n return normalizeVersion(metadata.tag_name);\\n}\\n\\nasync function checkRuntimeUpdate(options = {}) {\\n const discovery = await discoverRuntime(options);\\n if (!discovery.policy.updateChecksEnabled) {\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n action: \\"check\\",\\n skipped: true,\\n reason: \\"update_checks_disabled\\",\\n currentVersion: discovery.selected?.packageVersion || null,\\n pinnedVersion: discovery.policy.pinnedVersion,\\n mutates: false,\\n };\\n }\\n const latestVersion =\\n discovery.policy.pinnedVersion ||\\n (await resolveLatestVersion(options.fetchBuffer || downloadBuffer));\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n action: \\"check\\",\\n currentVersion: discovery.selected?.packageVersion || null,\\n latestVersion,\\n channel: discovery.policy.pinnedVersion ? \\"pinned\\" : \\"latest\\",\\n updateAvailable: discovery.selected?.packageVersion !== latestVersion,\\n selected: discovery.selected,\\n mutates: false,\\n };\\n}\\n\\nasync function stageRuntime(options) {\\n if (options.approve !== true) {\\n throw new Error(\\"Staging a runtime download requires approve=true.\\");\\n }\\n const version = normalizeVersion(options.version);\\n const target = releaseTarget(options.platform, options.architecture);\\n const root = runtimeStateRoot(options.env, options.platform);\\n const fetchBuffer = options.fetchBuffer || downloadBuffer;\\n const urls = releaseUrls(version, target);\\n const policy = await runtimePolicy(options);\\n if (policy.pinnedVersion && policy.pinnedVersion !== version) {\\n throw new Error(`Runtime policy is pinned to ${policy.pinnedVersion}.`);\\n }\\n\\n return await withMutationLock(root, async () => {\\n const metadataBytes = await fetchBuffer(releaseMetadataUrl(version), {\\n maxBytes: MAX_METADATA_BYTES,\\n accept: \\"application/vnd.github+json\\",\\n });\\n const metadata = JSON.parse(metadataBytes.toString(\\"utf8\\"));\\n const assets = releaseAssets(metadata, version, target);\\n const [checksumBytes, binaryBytes] = await Promise.all([\\n fetchBuffer(assets.checksums.browser_download_url, {\\n maxBytes: MAX_METADATA_BYTES,\\n }),\\n fetchBuffer(assets.binary.browser_download_url, {\\n maxBytes: MAX_BINARY_BYTES,\\n }),\\n ]);\\n const checksumDigest = verifyPublishedDigest(\\n assets.checksums,\\n checksumBytes\\n );\\n const binaryDigest = verifyPublishedDigest(assets.binary, binaryBytes);\\n const expectedSha256 = checksumForAsset(\\n checksumBytes.toString(\\"utf8\\"),\\n urls.assetName\\n );\\n const actualSha256 = sha256(binaryBytes);\\n if (expectedSha256 !== actualSha256) {\\n throw new Error(\\n \\"Downloaded runtime checksum does not match the published SHA256SUMS entry.\\"\\n );\\n }\\n\\n const stageDir = await assertManagedPath(\\n path.join(root, \\"staged\\", version),\\n root\\n );\\n await fsp.rm(stageDir, { recursive: true, force: true });\\n await fsp.mkdir(stageDir, { recursive: true, mode: 0o700 });\\n const executable = await assertManagedPath(\\n path.join(stageDir, target.platform === \\"windows\\" ? \\"fclt.exe\\" : \\"fclt\\"),\\n root\\n );\\n await fsp.writeFile(executable, binaryBytes, { mode: 0o700 });\\n if (target.platform !== \\"windows\\") {\\n await fsp.chmod(executable, 0o700);\\n }\\n\\n const inspected = await inspectCandidate(\\n { executable, source: \\"staged_plugin_runtime\\" },\\n { env: options.env, timeoutMs: options.timeoutMs }\\n );\\n if (!inspected.compatible || inspected.packageVersion !== version) {\\n await fsp.rm(stageDir, { recursive: true, force: true });\\n throw new Error(\\n \\"Staged runtime failed version or protocol verification.\\"\\n );\\n }\\n\\n const manifest = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n version,\\n tag: urls.tag,\\n assetName: urls.assetName,\\n executable,\\n sha256: actualSha256,\\n source: {\\n repository: REPOSITORY,\\n releaseMetadataUrl: releaseMetadataUrl(version),\\n binaryUrl: assets.binary.browser_download_url,\\n binaryAssetId: assets.binary.id ?? null,\\n binaryDigest,\\n checksumUrl: assets.checksums.browser_download_url,\\n checksumAssetId: assets.checksums.id ?? null,\\n checksumDigest,\\n },\\n protocol: inspected.protocol,\\n platform: target.platform,\\n architecture: target.architecture,\\n stagedAt: new Date().toISOString(),\\n };\\n await writeJsonAtomic(path.join(stageDir, \\"manifest.json\\"), manifest, root);\\n return { action: \\"stage\\", mutatesActiveRuntime: false, manifest };\\n });\\n}\\n\\nasync function verifyManifestExecutable(manifest, root, expectedParent) {\\n if (\\n !isPlainObject(manifest) ||\\n manifest.schemaVersion !== STATE_SCHEMA_VERSION ||\\n typeof manifest.version !== \\"string\\" ||\\n typeof manifest.executable !== \\"string\\" ||\\n typeof manifest.sha256 !== \\"string\\" ||\\n !SHA256_RE.test(manifest.sha256)\\n ) {\\n throw new Error(\\"Runtime manifest is missing required verification data.\\");\\n }\\n const executable = path.resolve(manifest.executable);\\n if (!isSubpath(executable, expectedParent)) {\\n throw new Error(\\n \\"Runtime manifest executable escapes its expected directory.\\"\\n );\\n }\\n await assertManagedPath(executable, root);\\n const bytes = await fsp.readFile(executable);\\n if (sha256(bytes) !== manifest.sha256) {\\n throw new Error(\\"Runtime manifest checksum does not match its executable.\\");\\n }\\n const inspected = await inspectCandidate({\\n executable,\\n source: \\"plugin_runtime\\",\\n expectedSha256: manifest.sha256,\\n });\\n if (!inspected.compatible || inspected.packageVersion !== manifest.version) {\\n throw new Error(\\n \\"Runtime manifest executable failed protocol verification.\\"\\n );\\n }\\n return inspected;\\n}\\n\\nasync function setRuntimePolicy(options = {}) {\\n if (options.approve !== true) {\\n throw new Error(\\"Changing runtime update policy requires approve=true.\\");\\n }\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const current = await runtimePolicy(options);\\n const next = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n updateChecksEnabled:\\n typeof options.updateChecksEnabled === \\"boolean\\"\\n ? options.updateChecksEnabled\\n : current.updateChecksEnabled,\\n pinnedVersion: options.clearPin\\n ? null\\n : options.pinnedVersion\\n ? normalizeVersion(options.pinnedVersion)\\n : current.pinnedVersion,\\n updatedAt: new Date().toISOString(),\\n };\\n await writeJsonAtomic(path.join(root, \\"policy.json\\"), next, root);\\n return { action: \\"policy\\", previous: current, policy: next };\\n });\\n}\\n\\nasync function applyStagedRuntime(options) {\\n if (options.approve !== true) {\\n throw new Error(\\"Applying a runtime requires approve=true.\\");\\n }\\n const version = normalizeVersion(options.version);\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const stageDir = path.join(root, \\"staged\\", version);\\n const manifest = await readJson(path.join(stageDir, \\"manifest.json\\"));\\n if (!manifest) {\\n throw new Error(`No staged runtime exists for ${version}.`);\\n }\\n if (options.expectedSha256 !== manifest.sha256) {\\n throw new Error(\\n \\"Staged runtime precondition failed: expected checksum changed.\\"\\n );\\n }\\n await verifyManifestExecutable(manifest, root, stageDir);\\n\\n const activePath = path.join(root, \\"active.json\\");\\n const previous = await readJson(activePath);\\n const versionDir = await assertManagedPath(\\n path.join(root, \\"versions\\", version),\\n root\\n );\\n await fsp.mkdir(versionDir, { recursive: true, mode: 0o700 });\\n const executable = await assertManagedPath(\\n path.join(versionDir, path.basename(manifest.executable)),\\n root\\n );\\n const temporary = `${executable}.tmp-${crypto.randomUUID()}`;\\n await fsp.copyFile(manifest.executable, temporary);\\n if (process.platform !== \\"win32\\") {\\n await fsp.chmod(temporary, 0o700);\\n }\\n await fsp.rename(temporary, executable);\\n const activeManifest = {\\n ...manifest,\\n executable,\\n activatedAt: new Date().toISOString(),\\n previous:\\n typeof previous?.version === \\"string\\" &&\\n typeof previous?.executable === \\"string\\"\\n ? {\\n version: previous.version,\\n executable: previous.executable,\\n sha256: previous.sha256,\\n }\\n : null,\\n };\\n await writeJsonAtomic(activePath, activeManifest, root);\\n const inspected = await verifyManifestExecutable(\\n activeManifest,\\n root,\\n versionDir\\n );\\n return {\\n action: \\"apply\\",\\n active: inspected,\\n previous: activeManifest.previous,\\n rollbackAvailable: Boolean(activeManifest.previous),\\n requiresFreshSession: false,\\n };\\n });\\n}\\n\\nasync function rollbackRuntime(options = {}) {\\n if (options.approve !== true) {\\n throw new Error(\\"Rolling back a runtime requires approve=true.\\");\\n }\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const activePath = path.join(root, \\"active.json\\");\\n const active = await readJson(activePath);\\n if (!isPlainObject(active?.previous)) {\\n throw new Error(\\n \\"The active plugin runtime does not have a retained rollback target.\\"\\n );\\n }\\n if (\\n options.expectedActiveVersion &&\\n options.expectedActiveVersion !== active.version\\n ) {\\n throw new Error(\\n \\"Runtime rollback precondition failed: active version changed.\\"\\n );\\n }\\n const previous = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n version: active.previous.version,\\n executable: active.previous.executable,\\n sha256: active.previous.sha256,\\n previous: {\\n version: active.version,\\n executable: active.executable,\\n sha256: active.sha256,\\n },\\n rolledBackAt: new Date().toISOString(),\\n };\\n const previousDir = path.dirname(path.resolve(previous.executable));\\n const inspected = await verifyManifestExecutable(\\n previous,\\n root,\\n previousDir\\n );\\n await writeJsonAtomic(activePath, previous, root);\\n return {\\n action: \\"rollback\\",\\n active: inspected,\\n rolledBackFrom: active.version,\\n rollbackAvailable: true,\\n requiresFreshSession: false,\\n };\\n });\\n}\\n\\nmodule.exports = {\\n PLUGIN_PROTOCOL_VERSION,\\n applyStagedRuntime,\\n assertManagedPath,\\n checkRuntimeUpdate,\\n checksumForAsset,\\n discoverRuntime,\\n downloadBuffer,\\n normalizeVersion,\\n parseProtocolReport,\\n pluginVersion,\\n protocolCompatibility,\\n releaseTarget,\\n rollbackRuntime,\\n runCommand,\\n runtimeCandidates,\\n runtimePolicy,\\n runtimeStateRoot,\\n setRuntimePolicy,\\n sha256,\\n stageRuntime,\\n};\\n","skills/fclt-capability-review/SKILL.md":"---\\nname: fclt-capability-review\\ndescription: Inspect fclt capability roots, docs, snippets, skills, agents, MCP, and automations.\\ntags: [fclt, capability, review, inventory]\\n---\\n\\n# fclt-capability-review\\n\\n## When To Use\\nUse this skill when Codex needs to understand what capability exists before changing it.\\n\\nUse it for:\\n\\n- checking global and project `.ai` roots\\n- finding relevant skills, snippets, instructions, agents, MCP servers, or automations\\n- deciding whether a change belongs in global or project scope\\n- checking whether managed rendering is enabled or needed\\n- reviewing public/private boundaries before publishing docs or pack assets\\n\\n## Workflow\\n\\n```bash\\nfclt status --json\\nfclt inventory --json\\nfclt list skills\\nfclt list instructions\\nfclt list snippets\\nfclt graph AGENTS.global.md\\n```\\n\\nFor project work:\\n\\n```bash\\nfclt status --project --json\\nfclt inventory --project --json\\n```\\n\\n## Rules\\n\\n- Read existing repo guidance before proposing project capability.\\n- Use project scope for repo-specific commands, tests, architecture, or team workflow.\\n- Use global scope only for broadly reusable behavior.\\n- Keep generated state and review artifacts out of repo-local `.ai`.\\n- Prefer adding or updating the smallest unit: instruction, snippet, skill, agent, MCP config, or automation.\\n- Treat engineering implementation as task work. Do not force product changes\\n through capability evolution merely because evolution tools exist.\\n- Before recommending mutation, state the observed problem, evidence, why the\\n proposed capability is the correct target, risk class, expected outcome,\\n verification plan, assumptions, and recovery route.\\n- Never silently overwrite authored capability or confuse generated/rendered\\n output with canonical source.\\n\\n## Output\\n\\n- capability roots found\\n- relevant assets\\n- scope recommendation\\n- missing or stale capability\\n- safe next command\\n- deliberately withheld operations or missing safer API\\n- verification and undo path for any proposed mutation\\n\\n## Authorization and evidence boundaries\\n\\nUse existing authorization for the same action and scope; do not request the same approval again.\\nHonor read-only and proposal-only requests by returning findings or a proposed writeback without\\nrecording state. When review-state mutation is authorized, prepare the smallest evidence-backed draft.\\nDrafting or recording does not authorize canonical apply, cross-scope promotion, external messages,\\ntracker changes, publishing, or scheduler activation. Preserve those gates and native authorization.\\nDo not infer effectiveness from apply: verify the producing workflow before resolving source writebacks.\\nKeep unchanged, non-actionable scheduled results quiet unless periodic reports were requested.\\n\\nFor a versioned capability registry, use `fclt capability inventory` with explicit source/target/state roots and host/platform overlays. Review source pins, ownership, drift, credential-reference availability, and retained disabled targets. Independent native records describe intent, not observed version or readiness. A successful file deployment does not prove provider discovery or execution. Preserve both sides of a conflict; do not repair it with an upstream installer in a managed output directory.\\n","skills/fclt-evolution/SKILL.md":"---\\nname: fclt-evolution\\ndescription: Turn repeated fclt writebacks into reviewed capability changes.\\ntags: [fclt, evolution, proposals, capability]\\n---\\n\\n# fclt-evolution\\n\\n## When To Use\\nUse this skill when repeated writebacks, stale canonical assets, or a clearly missing capability should become a concrete proposal.\\n\\nDo not use it for a single weak preference or speculative idea.\\n\\n## Authorization\\n\\nProceed with recording or drafting when the current request authorizes that action and scope;\\ndo not request the same approval again. For read-only or proposal-only work, return findings or a\\nproposed writeback without changing state. Strong evidence alone does not grant write authority.\\n\\nRecording or drafting does not authorize canonical application, cross-scope promotion, external\\nmessages, tracker changes, publishing, or scheduler activation. Preserve those separate gates and\\nnative authorization. Keep unchanged, non-actionable scheduled results quiet unless periodic\\nreports were requested.\\n\\n## Workflow\\n\\n1. Review signal:\\n\\n```bash\\nfclt ai review reconcile --since --until --json\\nfclt ai writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai evolve list\\n```\\n\\nFor an enabled scheduled loop, inspect its durable queue and observed scheduler\\nhealth with `fclt ai loop status --json`. Use `fclt ai loop run --dry-run\\n--json` for a fresh incremental scan that does not advance cursors or write\\nreconciliation or loop state. The full queue is authoritative; the notification\\ndelta intentionally suppresses unchanged items.\\n\\n2. Assess proposal readiness before mutating state:\\n\\n```bash\\nfclt ai evolve assess --asset --json\\n```\\n\\nUse the assessment recommendation as the decision checkpoint:\\n\\n- `reconcile_sources`: run the bounded read-only source review; writebacks alone cannot prove the window is empty.\\n- `review_reconciled_signals`: review correlated dispositions and linked work without creating one proposal per ticket.\\n- `no_mutation`: do not change capability state; ask for a target or evidence.\\n- `record_more_writeback`: explain what recurrence would justify evolution and record a new writeback only if there is fresh concrete evidence.\\n- `propose`: create the smallest target-specific draft when review-state mutation is already authorized; otherwise present the proposed action. Retain canonical apply and promotion gates.\\n- `review_existing_proposal`: inspect or revise the existing proposal instead of creating a duplicate.\\n\\n3. Propose only when evidence is strong enough:\\n\\n```bash\\nfclt ai evolve propose\\n```\\n\\n4. Draft and inspect:\\n\\n```bash\\nfclt ai evolve draft EV-00001\\nfclt ai evolve review EV-00001\\n```\\n\\n5. Present the reviewed proposal with its exact target, scope, evidence, and required authority.\\nCanonical apply and cross-scope promotion are unavailable through this plugin. Stop at the\\nreviewed proposal; do not use shell commands to bypass the unavailable surface.\\n\\n6. Verify the outcome after the producing loop has had a real chance to run:\\n\\n```bash\\nfclt ai writeback link WB-00001 --issue TEAM-123\\nfclt ai writeback disposition WB-00001 --type task --target TEAM-123\\nfclt ai evolve verify EV-00001 --effectiveness improved --evidence test:post-apply\\n```\\n\\nApply is not completion. Do not resolve source writebacks until post-apply evidence shows the\\nintended behavior improved. Treat recurrence as unchanged or regressed evidence linked to the same\\nevolution, not as an unrelated singleton.\\n\\n## Proposal Kinds\\n\\n- `update_asset`\\n- `create_asset`\\n- `extract_snippet`\\n- `add_skill`\\n- `promote_asset`\\n\\n## Rules\\n\\n- Prefer the smallest valid proposal kind.\\n- Keep project-specific behavior project-scoped until reuse is proven.\\n- Require approval covering the exact scope before applying global instructions, global skills, plugin behavior, or other broad shared surfaces. Existing approval for that action and scope is sufficient; never bypass an unavailable or restricted apply surface.\\n- Reject or park proposals that are stale, duplicated, vague, or unsupported.\\n- Use the operator\'s task system for executable implementation work that needs owner, priority, or state.\\n- A no-op answer must still be useful: include the evidence grade, missing signal, next writeback target, and exact approval boundary.\\n- State the problem, source evidence, reason for the selected target, risk,\\n expected outcome, verification plan, assumptions, and undo path before any\\n review-producing or mutating action.\\n- Preview and verify the exact scope. Never overwrite authored capability\\n silently or treat generated/rendered output as the canonical target.\\n- Canonical apply and cross-scope promotion are unavailable through the plugin\\n until a transaction-safe API can return tested rollback data. Do not bypass\\n that boundary with shell or arbitrary CLI arguments.\\n- The plugin exposes only closed-schema loop status and preview actions.\\n Scheduler enable/disable/run and external tracker mutation remain outside the\\n MCP surface.\\n\\n## Output\\n\\n- proposals reviewed\\n- repeated signal\\n- assessment recommendation\\n- proposal created or updated\\n- approvals needed\\n- apply/reject/no-op rationale\\n- actual changed records/artifacts, verification result, and recovery route\\n","skills/fclt-setup/SKILL.md":"---\\nname: fclt-setup\\ndescription: Install, update, inspect, and initialize fclt from Codex.\\ntags: [fclt, setup, codex, onboarding]\\n---\\n\\n# fclt-setup\\n\\n## When To Use\\nUse this skill when a user wants Codex to install, update, configure, inspect, or repair fclt.\\n\\nUse it for:\\n\\n- checking whether `fclt` is installed and current\\n- initializing global `~/.ai`\\n- discovering repositories and enrolling a reviewed minimal project layer\\n- installing or refreshing the built-in operating-model pack\\n- checking setup health with `doctor`\\n- finding canonical, generated, runtime, and review paths\\n\\n## Workflow\\n\\n1. For inspection, start with `fclt --version`, `fclt paths --json`, and `fclt doctor --json`. Run setup only when initialization or repair is authorized; existing authorization for the same targets is sufficient. Bootstrap an authorized global setup with:\\n\\n```bash\\nfclt setup\\n```\\n\\nThis initializes or safely updates global capability, prepares writeback/evolution review state,\\nand installs the Codex plugin when Codex is available. It does not initialize the current\\nrepository.\\n\\n2. Inspect runtime selection and compatibility with `fclt_runtime` action\\n `status`. Report the selected executable, version, source, protocol\\n compatibility, and fresh-session state.\\n\\nIf no compatible runtime is available, use the staged lifecycle:\\n\\n- `check` is read-only\\n- `stage` requires an explicit version and approval, but does not activate it\\n- `apply` requires approval plus the staged checksum precondition\\n- `rollback` verifies and restores the retained prior runtime\\n\\nNever curl-pipe code, use an unverified mutable URL, or replace an existing\\nglobal installation silently.\\n\\n3. Check current setup state and exact repair actions:\\n\\n```bash\\nfclt --version\\nfclt paths --json\\nfclt doctor --json\\n```\\n\\nThrough MCP, call `fclt_setup` with an explicit `global` or\\n`global_and_project` scope. Project setup also requires the exact `cwd`.\\nPreview is the default; apply requires both `dryRun: false` and\\n`approve: true`. `global_and_project` returns a no-write project enrollment\\nplan; project application still uses the typed CLI plan-hash contract below.\\n\\n4. For advanced manual recovery, initialize global capability when missing:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n5. Discover candidate repositories only beneath explicit roots:\\n\\n```bash\\nfclt projects discover --root ~/dev --since 30d --json\\n```\\n\\nDiscovery is bounded and read-only. Review duplicate clone/worktree groups,\\ndirty state, existing guidance, stable portfolio identity, and the separate\\ncheckout/worktree execution identity before selecting a project. Never\\nbulk-enroll the discovery result.\\n\\n6. Preview the exact minimal project enrollment plan:\\n\\n```bash\\nfclt project init --project-root /path/to/repo --json\\n```\\n\\nThe minimal layer is `.ai/.gitignore` plus `.ai/config.toml`. It does not\\ninstall the operating pack, enable managed rendering, schedule a loop, or copy\\n`AGENTS.md`/`CLAUDE.md`. Review every canonical, generated, and machine-local\\nwrite plus the rollback command.\\n\\nIf project guidance should be adopted, name each canonical file explicitly:\\n\\n```bash\\nfclt project init --project-root /path/to/repo \\\\\\n --guidance AGENTS.md --json\\n```\\n\\nGuidance adoption is reference-only. fclt previews the full content and hash,\\nand refuses untracked, modified, `assume-unchanged`, `skip-worktree`,\\nsecret-shaped, or machine-path-bearing input. Cleanliness requires identical\\nworktree, index, and `HEAD` blobs.\\n\\n7. Apply only the unchanged reviewed plan:\\n\\n```bash\\nfclt project init --project-root /path/to/repo \\\\\\n --apply --plan-sha --json\\n```\\n\\nIf options, source files, or preconditions change, discard the old hash and\\npreview again. Apply serializes portfolio registry mutations, isolates\\nlocation-bearing generated state per checkout/worktree, refuses symlinked\\ngenerated targets, and publishes the receipt only after the registry commit.\\n\\n8. Inspect health, coverage, and lifecycle:\\n\\n```bash\\nfclt projects status --root /path/to/repo --json\\nfclt project disable --project-root /path/to/repo --json\\nfclt project rollback --receipt --json\\n```\\n\\nDisable and remove decisions preserve canonical files, receipts, and review\\nhistory. Rollback previews by default and refuses drift.\\n\\n9. Install the full operating pack only when explicitly requested:\\n\\n```bash\\nfclt templates init operating-model --project --dry-run\\nfclt templates init operating-model --project\\n```\\n\\nThis is distinct from minimal enrollment and does not seed project\\n`AGENTS.global.md` from repository guidance.\\n\\n10. Refresh global pack defaults non-destructively:\\n\\n```bash\\nfclt templates init operating-model --global --update --dry-run\\nfclt templates init operating-model --global --update\\n```\\n\\n11. Use `--force` only when the user explicitly wants to replace local edits.\\n\\n## Rules\\n\\n- Preserve existing `AGENTS.md`, `CLAUDE.md`, and `AGENTS.global.md` guidance.\\n- Never infer guidance adoption from filenames or copy repository guidance into\\n `.ai/AGENTS.global.md`.\\n- Treat root `AGENTS.md` or `CLAUDE.md` as canonical repository guidance unless\\n the user explicitly chooses another tracked, clean source.\\n- Write the protective `.ai/.gitignore` before generated state.\\n- Keep canonical project files separate from machine-local generated indexes,\\n registries, receipts, and scheduling state.\\n- Keep minimal enrollment separate from the full operating pack and managed\\n rendering.\\n- Treat `doctor --json` issues as setup facts, not user-facing blame.\\n- Treat Codex plugin registration as weaker evidence than fresh-session tool discovery.\\n- Treat external trackers as separate integrations. Core readiness depends only on configured local evidence coverage, not a vendor plugin or token.\\n- Prefer temp-root smoke tests for install/update behavior.\\n- Do not enable managed rendering unless the user wants fclt to write tool homes.\\n- Preview before mutation and state the exact global/project/plugin target.\\n- Do not report a staged runtime or installed plugin as active until the active\\n handshake and fresh-session discovery have been verified.\\n\\n## Output\\n\\n- current installed version\\n- setup health\\n- paths that matter\\n- commands run\\n- what changed\\n- problem, evidence, reason, target, risk, and expected outcome\\n- verification performed and its actual result\\n- assumptions and fresh-session state\\n- exact undo or rollback path\\n- what still needs approval\\n\\n## Versioned skill and plugin sources\\n\\nFor registered skill, custom plugin, or MCP artifact updates, use `fclt capability inventory` and a per-entry `plan` before `apply --id ... --expected-plan ...`. Use the registry\'s explicit source, target, and runtime state roots; pass machine/private choices with ordered `--overlay` files. Acquire upstream updates into isolated staging, review the snapshot, and record its revision and `capability digest` before deploying. Do not run embedded installer or auto-update instructions against fclt-owned outputs, and do not use broad legacy sync to repair drift. Preserve divergent files for source selection.\\n\\nNative plugin managers own their installed files. Registered native versions are intended versions until verified with the provider. `enabled: false` stops fclt management but retains existing files; it is not a provider disable or uninstall operation. See the package\'s `docs/capability-registry.md` for the schema, conflict states, and recovery procedure.\\n","skills/fclt-writeback/SKILL.md":"---\\nname: fclt-writeback\\ndescription: Record and review fclt writebacks from real agent work.\\ntags: [fclt, writeback, learning, feedback-loop]\\n---\\n\\n# fclt-writeback\\n\\n## When To Use\\nUse this skill when work reveals durable friction, missing context, weak verification, stale guidance, repeated success, or a capability gap.\\n\\nWriteback is for preserving signal. It is not for every preference or one-off annoyance.\\n\\n## Authorization\\n\\nProceed with recording or drafting when the current request authorizes that action and scope;\\ndo not request the same approval again. For read-only or proposal-only work, return findings or a\\nproposed writeback without changing state. Strong evidence alone does not grant write authority.\\n\\nRecording or drafting does not authorize canonical application, cross-scope promotion, external\\nmessages, tracker changes, publishing, or scheduler activation. Preserve those separate gates and\\nnative authorization. Keep unchanged, non-actionable scheduled results quiet unless periodic\\nreports were requested.\\n\\n## Workflow\\n\\n1. Decide scope:\\n\\n- `project` when the learning depends on a repo, test harness, architecture, or workflow.\\n- `global` when the learning applies across projects or shared tool behavior.\\n\\n2. Choose the smallest target:\\n\\n- instruction\\n- snippet\\n- skill\\n- agent\\n- MCP/tool config\\n- automation\\n\\n3. Record writeback when the target, evidence, and authorization are clear:\\n\\n```bash\\nfclt ai writeback add \\\\\\n --kind missing_context \\\\\\n --category opportunity \\\\\\n --summary \\"Project verification guidance was not discoverable\\" \\\\\\n --details \\"The task had to reconstruct the command from CI configuration\\" \\\\\\n --impact \\"Verification took longer and could have selected the wrong harness\\" \\\\\\n --attempted-workaround \\"Inspected package scripts and CI\\" \\\\\\n --desired-outcome \\"The supported verification command is available at task start\\" \\\\\\n --sensitivity internal \\\\\\n --evidence session: \\\\\\n --asset @project/instructions/TESTING.md\\n```\\n\\n4. Review current signal:\\n\\n```bash\\nfclt ai writeback list\\nfclt ai writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai loop activity --project\\n```\\n\\n## Rules\\n\\n- Prefer one high-signal writeback over several weak ones.\\n- Include concrete evidence when possible.\\n- Capture concise context, impact, attempted workaround, desired outcome, and\\n sensitivity when they improve review quality.\\n- Never capture hidden chain-of-thought, raw transcripts, unbounded logs,\\n secrets, tokens, or credential-bearing payloads. Reference the smallest\\n redacted external evidence identifier instead.\\n- Do not copy private project detail into global writebacks.\\n- Use task tracking for executable product/tooling work; use writeback for reusable operating-layer learning.\\n- If the same signal repeats and the target is clear, hand off to `fclt-evolution`.\\n- State the observed problem, evidence, target, reason, expected outcome, and\\n assumptions before recording.\\n- Do not capture secrets, private tokens, or raw sensitive payloads as evidence.\\n- For lifecycle mutations, use an explicit scope and expected prior state.\\n Report the journal/review evidence and the available undo transition.\\n\\n## Output\\n\\n- writeback id or no-op rationale\\n- scope\\n- target asset\\n- evidence summary\\n- whether this is ready for evolution\\n- risk class and approval boundary\\n- actual changed records/artifacts\\n- verification result and recovery route\\n\\n\\nVerify the producing workflow before resolving source writebacks; applying a change alone does not prove effectiveness.\\n"}' ) as Record; export const BUILTIN_FCLT_CODEX_PLUGIN_BINARY_FILES = JSON.parse( diff --git a/src/capability-apply.ts b/src/capability-apply.ts new file mode 100644 index 00000000..ffc5aa33 --- /dev/null +++ b/src/capability-apply.ts @@ -0,0 +1,153 @@ +import { randomUUID } from "node:crypto"; +import { + lstat, + mkdir, + mkdtemp, + rename, + rm, + rmdir, + writeFile, +} from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { + capabilityCollisionPath, + optionalStat, + safeCapabilityPath, + snapshotCapability, + stageCapability, +} from "./capability-files"; +import { + buildCapabilityPlan, + type CapabilityContext, + type CapabilityPlan, + type CapabilityReceipt, +} from "./capability-plan"; +import { hashCapability } from "./capability-registry"; + +const APPLY_STATUSES = new Set([ + "create", + "restore", + "adopt", + "update", + "current", +]); +export async function applyCapability( + options: CapabilityContext, + id: string, + expectedPlan: string +) { + const initial = await buildCapabilityPlan(options, id); + assertApplicable(initial, expectedPlan); + if (initial.status === "current") { + return { ...initial, applied: false, recoveryPath: null }; + } + const parent = dirname(initial.targetPath!); + await safeCapabilityPath(parent); + await mkdir(parent, { recursive: true, mode: 0o700 }); + await safeCapabilityPath(parent); + const lock = join( + parent, + `.fclt-${hashCapability(capabilityCollisionPath(initial.targetPath!)).slice(7)}.lock` + ); + await mkdir(lock, { mode: 0o700 }); + let transaction: string | null = null; + let recoveryPath: string | null = null; + let installed = false; + try { + const plan = await buildCapabilityPlan(options, id); + assertApplicable(plan, expectedPlan); + const snapshot = await snapshotCapability(plan.sourcePath!); + if (snapshot?.hash !== plan.hashes.source) { + throw new Error("Capability source changed after planning"); + } + const stateParent = dirname(plan.statePath); + await safeCapabilityPath(stateParent); + await mkdir(stateParent, { recursive: true, mode: 0o700 }); + await safeCapabilityPath(stateParent); + const stateDirectory = await lstat(stateParent); + // biome-ignore lint/suspicious/noBitwiseOperators: ownership state must not be writable by other users + if ((stateDirectory.mode & 0o022) !== 0) { + throw new Error( + "Capability runtime state must not be writable by other users" + ); + } + if (stateDirectory.dev !== (await lstat(parent)).dev) { + throw new Error( + "Capability runtime state and target must share a filesystem for recoverable publication" + ); + } + transaction = await mkdtemp(join(stateParent, "recovery-")); + const staged = join(transaction, "next"); + await stageCapability(staged, snapshot); + if ((await snapshotCapability(staged))?.hash !== plan.hashes.source) { + throw new Error("Staged capability verification failed"); + } + assertApplicable(await buildCapabilityPlan(options, id), expectedPlan); + if (plan.hashes.current !== plan.hashes.source) { + if (plan.hashes.current !== null) { + recoveryPath = join(transaction, "previous"); + await rename(plan.targetPath!, recoveryPath); + } + await rename(staged, plan.targetPath!); + installed = true; + } + if ((await snapshotCapability(plan.targetPath!))?.hash !== snapshot.hash) { + throw new Error( + "Deployed capability verification failed; recovery content retained" + ); + } + const receipt: CapabilityReceipt = { + schemaVersion: 1, + id, + sourcePath: plan.sourcePath!, + targetPath: plan.targetPath!, + sourceHash: snapshot.hash, + targetHash: snapshot.hash, + revision: plan.source.revision, + origin: plan.source.origin, + }; + const temporaryReceipt = `${plan.statePath}.${randomUUID()}.tmp`; + await writeFile(temporaryReceipt, `${JSON.stringify(receipt, null, 2)}\n`, { + flag: "wx", + mode: 0o600, + }); + await safeCapabilityPath(plan.statePath); + await rename(temporaryReceipt, plan.statePath); + const verified = await buildCapabilityPlan(options, id); + if (verified.status !== "current") { + throw new Error( + "Capability changed during verification; recovery content retained" + ); + } + return { ...verified, applied: true, recoveryPath }; + } catch (error) { + // If publication fails after moving the old target, restore it only into an + // absent destination. A competing writer's content must remain untouched. + if ( + recoveryPath && + !installed && + !(await optionalStat(initial.targetPath!)) + ) { + await safeCapabilityPath(dirname(initial.targetPath!)); + await rename(recoveryPath, initial.targetPath!); + recoveryPath = null; + } + throw error; + } finally { + // Never delete the previous target, including after interruption or failed publication. + if (transaction && !recoveryPath) { + await rm(transaction, { recursive: true, force: true }); + } + await rmdir(lock); + } +} +function assertApplicable(plan: CapabilityPlan, expectedPlan: string): void { + if (plan.planId !== expectedPlan) { + throw new Error( + "Stale capability plan; inspect a fresh plan before applying" + ); + } + if (!APPLY_STATUSES.has(plan.status)) { + throw new Error(`Capability apply refused: ${plan.status}: ${plan.reason}`); + } +} diff --git a/src/capability-files.ts b/src/capability-files.ts new file mode 100644 index 00000000..8e0f38eb --- /dev/null +++ b/src/capability-files.ts @@ -0,0 +1,219 @@ +import { constants } from "node:fs"; +import { + chmod, + lstat, + mkdir, + open, + readdir, + readlink, + realpath, + writeFile, +} from "node:fs/promises"; +import { dirname, join, parse, relative, resolve, sep } from "node:path"; +import { caseFold } from "unicode-case-folding"; +import { hashCapability } from "./capability-registry"; + +const MAX_BYTES = 32 * 1024 * 1024; +const MAX_NODES = 4096; +export interface CapabilityNode { + path: string; + kind: "file" | "directory"; + executable: boolean; + content?: Buffer; +} +export interface CapabilitySnapshot { + hash: string; + nodes: CapabilityNode[]; +} +export function capabilityCollisionPath(path: string): string { + return caseFold(resolve(path).normalize("NFC")); +} +export function pathsOverlap(first: string, second: string): boolean { + const a = capabilityCollisionPath(first); + const b = capabilityCollisionPath(second); + return a === b || a.startsWith(`${b}${sep}`) || b.startsWith(`${a}${sep}`); +} +/** Reject symlink ancestors, including missing-tail paths, before reads or writes. */ +export async function safeCapabilityPath(path: string): Promise { + const absolute = resolve(path); + const root = parse(absolute).root; + let current = root; + for (const part of relative(root, absolute).split(sep).filter(Boolean)) { + current = join(current, part); + const stat = await optionalStat(current); + if (stat?.isSymbolicLink()) { + throw new Error("Capability path contains a symlink ancestor"); + } + if (stat && current !== absolute && !stat.isDirectory()) { + throw new Error("Capability ancestor is not a directory"); + } + } + return absolute; +} +export async function optionalStat(path: string) { + try { + return await lstat(path); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + return null; + } + throw error; + } +} +export async function capabilityRoot(path: string): Promise { + // The explicitly supplied root may itself be a platform alias (/tmp on macOS). + // Descendants are still checked without following links. + const absolute = resolve(path); + let existing = absolute; + while (!(await optionalStat(existing))) { + existing = dirname(existing); + } + const physical = await realpath(existing); + return safeCapabilityPath(resolve(physical, relative(existing, absolute))); +} +/** Snapshot regular files only; neither nested symlinks nor special files are copied. */ +export async function snapshotCapability( + path: string +): Promise { + await safeCapabilityPath(dirname(path)); + if (!(await optionalStat(path))) { + return null; + } + const nodes: CapabilityNode[] = []; + let totalBytes = 0; + async function visit(current: string, name: string): Promise { + const stat = await lstat(current); + if (nodes.length >= MAX_NODES) { + throw new Error("Capability exceeds node limit"); + } + if (stat.isDirectory()) { + nodes.push({ path: name, kind: "directory", executable: false }); + const children = (await readdir(current)).sort(); + if ( + children.some((child) => + ["node_modules", "__pycache__", ".git", ".local", ".env"].includes( + child + ) + ) + ) { + throw new Error( + "Capability contains runtime or private material; prepare a clean source snapshot" + ); + } + const folded = children.map((child) => caseFold(child.normalize("NFC"))); + if (new Set(folded).size !== children.length) { + throw new Error("Capability has case-colliding paths"); + } + for (const child of children) { + const normalizedChild = child.normalize("NFC"); + await visit( + join(current, child), + name ? `${name}/${normalizedChild}` : normalizedChild + ); + } + return; + } + if (!stat.isFile() || stat.nlink !== 1) { + throw new Error("Capability contains a link or special file"); + } + totalBytes += stat.size; + if (totalBytes > MAX_BYTES) { + throw new Error("Capability exceeds byte limit"); + } + const handle = await open( + current, + // biome-ignore lint/suspicious/noBitwiseOperators: combine filesystem open flags + constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK + ); + try { + const before = await handle.stat(); + if ( + !before.isFile() || + before.ino !== stat.ino || + before.dev !== stat.dev || + before.size !== stat.size + ) { + throw new Error("Capability changed during read"); + } + const buffer = Buffer.alloc(before.size + 1); + let bytesRead = 0; + while (bytesRead < buffer.length) { + const read = await handle.read( + buffer, + bytesRead, + buffer.length - bytesRead, + bytesRead + ); + if (read.bytesRead === 0) { + break; + } + bytesRead += read.bytesRead; + } + const content = buffer.subarray(0, bytesRead); + const after = await handle.stat(); + if ( + after.size !== before.size || + after.mtimeMs !== before.mtimeMs || + after.ctimeMs !== before.ctimeMs || + content.length !== before.size + ) { + throw new Error("Capability changed during read"); + } + nodes.push({ + path: name, + kind: "file", + // biome-ignore lint/suspicious/noBitwiseOperators: preserve executable file semantics + executable: Boolean(stat.mode & 0o111), + content, + }); + } finally { + await handle.close(); + } + } + await visit(path, ""); + nodes.sort((first, second) => + first.path < second.path ? -1 : first.path > second.path ? 1 : 0 + ); + const hash = hashCapability( + JSON.stringify( + nodes.map((node) => ({ + path: node.path, + kind: node.kind, + executable: node.executable, + ...(node.content ? { sha256: hashCapability(node.content) } : {}), + })) + ) + ); + return { hash, nodes }; +} +export async function currentCapability( + path: string +): Promise<{ hash: string | null; link: string | null }> { + await safeCapabilityPath(dirname(path)); + const stat = await optionalStat(path); + if (stat?.isSymbolicLink()) { + const link = await readlink(path); + return { + hash: hashCapability(`symlink:${link}`), + link: resolve(dirname(path), link), + }; + } + return { hash: (await snapshotCapability(path))?.hash ?? null, link: null }; +} +export async function stageCapability( + path: string, + snapshot: CapabilitySnapshot +): Promise { + for (const node of snapshot.nodes) { + const target = node.path ? join(path, node.path) : path; + if (node.kind === "directory") { + await mkdir(target, { mode: 0o700 }); + } else { + await writeFile(target, node.content!, { + flag: "wx", + mode: node.executable ? 0o700 : 0o600, + }); + await chmod(target, node.executable ? 0o700 : 0o600); + } + } +} diff --git a/src/capability-plan.ts b/src/capability-plan.ts new file mode 100644 index 00000000..dcc348d0 --- /dev/null +++ b/src/capability-plan.ts @@ -0,0 +1,410 @@ +import { readFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { + capabilityCollisionPath, + capabilityRoot, + currentCapability, + optionalStat, + pathsOverlap, + safeCapabilityPath, + snapshotCapability, +} from "./capability-files"; +import { + type CapabilityEntry, + capabilityObject, + hashCapability, + parseCapabilityRegistry, + selectCapability, +} from "./capability-registry"; + +const RECEIPT_HASH = /^sha256:[a-f0-9]{64}$/; + +export interface CapabilityContext { + manifest: string; + overlays?: string[]; + sourceRoot: string; + targetRoot: string; + stateRoot: string; + host: string; + platform: string; +} +export interface CapabilityReceipt { + schemaVersion: 1; + id: string; + sourcePath: string; + targetPath: string; + sourceHash: string; + targetHash: string; + revision: string; + origin: string; +} +export interface CapabilityPlan { + schemaVersion: 1; + planId: string; + id: string; + kind: CapabilityEntry["kind"]; + owner: CapabilityEntry["owner"]; + host: string; + platform: string; + source: CapabilityEntry["source"]; + sourcePath: string | null; + targetPath: string | null; + statePath: string; + hashes: { + manifest: string; + source: string | null; + current: string | null; + state: string | null; + }; + status: + | "disabled" + | "independent" + | "blocked" + | "conflict" + | "create" + | "restore" + | "adopt" + | "update" + | "current"; + reason: string; + missingReferences: string[]; + targetPresence: "uninspected" | "present" | "absent"; +} +export async function loadCapabilityContext(options: CapabilityContext) { + const sourceRoot = await capabilityRoot(options.sourceRoot); + const targetRoot = await capabilityRoot(options.targetRoot); + const stateRoot = await capabilityRoot(options.stateRoot); + const manifestPath = await capabilityRoot(options.manifest); + if (pathsOverlap(stateRoot, sourceRoot)) { + throw new Error( + "Capability state must be separate from the canonical source root" + ); + } + const manifest = await readFile(manifestPath, "utf8"); + const registry = parseCapabilityRegistry(JSON.parse(manifest)); + let entries = registry.entries.map((entry) => + selectCapability(entry, options.host, options.platform) + ); + const manifestInputs = [manifest]; + const registryPaths = [manifestPath]; + for (const overlayPath of options.overlays ?? []) { + const overlayFile = await capabilityRoot(overlayPath); + registryPaths.push(overlayFile); + const raw = await readFile(overlayFile, "utf8"); + manifestInputs.push(raw); + const overlay = capabilityObject(JSON.parse(raw), [ + "schemaVersion", + "entries", + ]); + if (overlay.schemaVersion !== 1 || !Array.isArray(overlay.entries)) { + throw new Error("Invalid capability overlay"); + } + const ids = new Set(); + for (const patchValue of overlay.entries) { + const patch = capabilityObject(patchValue, [ + "id", + "enabled", + "source", + "target", + "requires", + ]); + if ( + typeof patch.id !== "string" || + ids.has(patch.id) || + !entries.some((entry) => entry.id === patch.id) + ) { + throw new Error("Unknown or duplicate overlay id"); + } + ids.add(patch.id); + entries = entries.map((entry) => + entry.id === patch.id + ? parseCapabilityRegistry({ + schemaVersion: 1, + entries: [{ ...entry, ...patch }], + }).entries[0]! + : entry + ); + } + } + const targets = entries + .filter((entry) => entry.target !== undefined) + .map((entry) => ({ + path: join(targetRoot, entry.target!), + owner: entry.owner, + })); + for (const target of targets.filter((entry) => entry.owner === "fclt")) { + if ( + pathsOverlap(target.path, sourceRoot) || + registryPaths.some((path) => pathsOverlap(target.path, path)) || + pathsOverlap(target.path, stateRoot) + ) { + throw new Error("Capability target overlaps source, manifest, or state"); + } + if ( + targets.some( + (other) => other !== target && pathsOverlap(target.path, other.path) + ) + ) { + throw new Error("Capability targets overlap"); + } + } + return { + ...options, + sourceRoot, + targetRoot, + stateRoot, + entries, + manifestHash: hashCapability(JSON.stringify(manifestInputs)), + }; +} +export async function readCapabilityReceipt( + path: string +): Promise<{ receipt: CapabilityReceipt | null; hash: string | null }> { + await safeCapabilityPath(path); + const stat = await optionalStat(path); + if (!stat) { + return { receipt: null, hash: null }; + } + if ( + !stat.isFile() || + stat.nlink !== 1 || + stat.size > 256 * 1024 || + // biome-ignore lint/suspicious/noBitwiseOperators: reject writable ownership evidence + (stat.mode & 0o022) !== 0 + ) { + throw new Error("Capability ownership receipt is unsafe"); + } + const raw = await readFile(path, "utf8"); + const value = capabilityObject(JSON.parse(raw), [ + "schemaVersion", + "id", + "sourcePath", + "targetPath", + "sourceHash", + "targetHash", + "revision", + "origin", + ]); + if ( + value.schemaVersion !== 1 || + [ + "id", + "sourcePath", + "targetPath", + "sourceHash", + "targetHash", + "revision", + "origin", + ].some((key) => typeof value[key] !== "string") + ) { + throw new Error("Invalid capability receipt"); + } + if ( + !RECEIPT_HASH.test(String(value.sourceHash)) || + value.sourceHash !== value.targetHash + ) { + throw new Error("Capability ownership receipt has invalid hashes"); + } + return { + receipt: value as unknown as CapabilityReceipt, + hash: hashCapability(raw), + }; +} +function inspectionReason(error: unknown): string { + // Our inspection errors contain only fixed descriptions; do not echo JSON or + // arbitrary filesystem/parser diagnostics into inventory. + if (error instanceof Error && error.message.startsWith("Capability ")) { + return error.message; + } + return "Capability inspection failed or state is invalid"; +} +async function inspectCapability( + read: () => Promise +): Promise<{ value: T } | { reason: string }> { + try { + return { value: await read() }; + } catch (error) { + return { reason: inspectionReason(error) }; + } +} +export async function buildCapabilityPlan( + options: CapabilityContext, + id: string +): Promise { + const context = await loadCapabilityContext(options); + const entry = context.entries.find((item) => item.id === id); + if (!entry) { + throw new Error("Unknown capability id"); + } + const sourcePath = entry.source.path + ? join(context.sourceRoot, entry.source.path) + : null; + const targetPath = entry.target + ? join(context.targetRoot, entry.target) + : null; + const statePath = join( + context.stateRoot, + `${hashCapability(targetPath ? capabilityCollisionPath(targetPath) : `native:${entry.id}`).slice(7)}.json` + ); + const body: Omit = { + schemaVersion: 1, + id, + kind: entry.kind, + owner: entry.owner, + host: options.host, + platform: options.platform, + source: entry.source, + sourcePath, + targetPath, + statePath, + hashes: { + manifest: context.manifestHash, + source: null, + current: null, + state: null, + }, + status: "blocked", + reason: "", + missingReferences: [], + targetPresence: "uninspected", + }; + function finish( + status: CapabilityPlan["status"], + reason: string + ): CapabilityPlan { + const result = { ...body, status, reason }; + return { ...result, planId: hashCapability(JSON.stringify(result)) }; + } + body.missingReferences = entry.requires.filter( + (reference) => + !(reference.startsWith("env:") && process.env[reference.slice(4)]) + ); + if (!entry.enabled) { + if (targetPath && entry.owner === "fclt") { + const presence = await inspectCapability(async () => { + await safeCapabilityPath(dirname(targetPath)); + return (await optionalStat(targetPath)) + ? ("present" as const) + : ("absent" as const); + }); + if ("value" in presence) { + body.targetPresence = presence.value; + } + } + return finish( + "disabled", + body.targetPresence === "present" + ? "Management disabled; existing target retained and may remain discoverable" + : "Management disabled; provider activation is not verified" + ); + } + if (entry.owner !== "fclt") { + return finish( + "independent", + "Registered intent only; native/external manager owns files and observed version is unverified" + ); + } + const sourceRead = await inspectCapability(() => + snapshotCapability(sourcePath!) + ); + if ("reason" in sourceRead) { + return finish("blocked", `Source: ${sourceRead.reason}`); + } + const snapshot = sourceRead.value; + body.hashes.source = snapshot?.hash ?? null; + if (!snapshot) { + return finish("blocked", "Canonical source is missing"); + } + if (snapshot.hash !== entry.source.sha256) { + return finish( + "conflict", + "Canonical content differs from its pinned source digest" + ); + } + if ( + entry.kind === "skill" && + !snapshot.nodes.some( + (node) => node.path === "SKILL.md" && node.kind === "file" + ) + ) { + return finish("blocked", "Skill source requires SKILL.md"); + } + const targetRead = await inspectCapability(() => + currentCapability(targetPath!) + ); + if ("reason" in targetRead) { + return finish("conflict", `Target preserved: ${targetRead.reason}`); + } + const current = targetRead.value; + body.hashes.current = current.hash; + body.targetPresence = current.hash === null ? "absent" : "present"; + const stateRead = await inspectCapability(() => + readCapabilityReceipt(statePath) + ); + if ("reason" in stateRead) { + return finish("conflict", `Receipt: ${stateRead.reason}`); + } + const state = stateRead.value; + body.hashes.state = state.hash; + if ( + state.receipt && + (state.receipt.id !== id || state.receipt.targetPath !== targetPath) + ) { + return finish("conflict", "Ownership receipt belongs to another binding"); + } + if (body.missingReferences.length) { + return finish( + "blocked", + "Required credential references are unavailable; no values are resolved" + ); + } + if (current.link) { + if (current.link !== resolve(sourcePath!)) { + return finish( + "conflict", + "Target is a foreign or broken link; preserved" + ); + } + return finish( + "adopt", + "Convert the exact canonical link to an independently owned copy" + ); + } + if (current.hash === snapshot.hash) { + if ( + state.receipt?.sourceHash === snapshot.hash && + state.receipt.targetHash === snapshot.hash && + state.receipt.revision === entry.source.revision && + state.receipt.origin === entry.source.origin && + state.receipt.sourcePath === sourcePath + ) { + return finish("current", "Source, target, provenance, and receipt match"); + } + return finish( + "adopt", + "Record ownership of identical content and current provenance" + ); + } + if (!current.hash) { + return finish( + state.receipt ? "restore" : "create", + "Create a copy of the pinned canonical source" + ); + } + if (!state.receipt) { + return finish( + "conflict", + "Unowned target differs from canonical source; both preserved" + ); + } + if (state.receipt.targetHash !== current.hash) { + return finish( + "conflict", + "Owned target changed outside fclt; both preserved" + ); + } + return finish( + "update", + "Target still matches its ownership receipt; deploy pinned source update" + ); +} diff --git a/src/capability-registry.ts b/src/capability-registry.ts new file mode 100644 index 00000000..11275537 --- /dev/null +++ b/src/capability-registry.ts @@ -0,0 +1,274 @@ +import { createHash } from "node:crypto"; +import { isAbsolute, posix, win32 } from "node:path"; + +const ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/; +const HASH = /^sha256:[a-f0-9]{64}$/; +const ENV = /^env:[A-Z_][A-Z0-9_]*$/; +const OP = /^op:\/\/[A-Za-z0-9._~%/-]+$/; +export type CapabilityKind = "skill" | "plugin" | "mcp"; +export interface CapabilitySource { + origin: string; + revision: string; + path?: string; + sha256?: string; +} +export interface CapabilityOverride { + host?: string; + platform?: string; + enabled?: boolean; + source?: CapabilitySource; + target?: string; +} +export interface CapabilityEntry { + id: string; + kind: CapabilityKind; + owner: "fclt" | "external" | "native"; + enabled: boolean; + source: CapabilitySource; + target?: string; + platforms?: string[]; + hosts?: string[]; + requires: string[]; + overrides: CapabilityOverride[]; +} +export interface CapabilityRegistry { + schemaVersion: 1; + entries: CapabilityEntry[]; +} +export function hashCapability(value: string | Uint8Array): string { + return `sha256:${createHash("sha256").update(value).digest("hex")}`; +} +export function capabilityObject( + value: unknown, + keys: string[] +): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error("Expected a capability object"); + } + const result = value as Record; + if (Object.keys(result).some((key) => !keys.includes(key))) { + throw new Error("Unknown capability field"); + } + return result; +} +function text(value: unknown): string { + if ( + typeof value !== "string" || + !value.trim() || + [...value].some((character) => character.charCodeAt(0) < 32) + ) { + throw new Error("Expected a nonempty capability string"); + } + return value; +} +export function capabilityRelativePath(value: unknown): string { + const path = text(value); + if ( + isAbsolute(path) || + win32.isAbsolute(path) || + path.includes("\\") || + path.includes(":") || + path.split("/").some((part) => !part || part === "." || part === "..") || + posix.normalize(path) !== path + ) { + throw new Error("Capability paths must be normalized relative paths"); + } + return path; +} +function strings(value: unknown): string[] | undefined { + if (value === undefined) { + return undefined; + } + if (!Array.isArray(value) || value.length === 0) { + throw new Error("Expected a nonempty selector array"); + } + return value.map(text); +} +function boolean(value: unknown, fallback: boolean): boolean { + if (value === undefined) { + return fallback; + } + if (typeof value !== "boolean") { + throw new Error("enabled must be boolean"); + } + return value; +} +function origin(value: unknown): string { + const result = text(value); + if (result.includes("://")) { + let url: URL; + try { + url = new URL(result); + } catch { + throw new Error("Invalid capability origin URL"); + } + if ( + url.password || + (url.username && !(url.protocol === "ssh:" && url.username === "git")) || + url.search + ) { + throw new Error( + "Capability origin must not contain credentials or query parameters" + ); + } + } + return result; +} +function source(value: unknown, owned: boolean): CapabilitySource { + const object = capabilityObject(value, [ + "origin", + "revision", + "path", + "sha256", + ]); + const result: CapabilitySource = { + origin: origin(object.origin), + revision: text(object.revision), + }; + if (object.path !== undefined) { + result.path = capabilityRelativePath(object.path); + } + if (object.sha256 !== undefined) { + result.sha256 = text(object.sha256); + if (!HASH.test(result.sha256)) { + throw new Error("Invalid capability source digest"); + } + } + if (owned && !(result.path && result.sha256)) { + throw new Error("fclt ownership requires a pinned source path and sha256"); + } + return result; +} +function override(value: unknown, owned: boolean): CapabilityOverride { + const object = capabilityObject(value, [ + "host", + "platform", + "enabled", + "source", + "target", + ]); + if ((object.host === undefined) === (object.platform === undefined)) { + throw new Error( + "Each override requires exactly one host or platform selector" + ); + } + return { + ...(object.host === undefined ? {} : { host: text(object.host) }), + ...(object.platform === undefined + ? {} + : { platform: text(object.platform) }), + ...(object.enabled === undefined + ? {} + : { enabled: boolean(object.enabled, true) }), + ...(object.source === undefined + ? {} + : { source: source(object.source, owned) }), + ...(object.target === undefined + ? {} + : { target: capabilityRelativePath(object.target) }), + }; +} +function entry(value: unknown): CapabilityEntry { + const object = capabilityObject(value, [ + "id", + "kind", + "owner", + "enabled", + "source", + "target", + "platforms", + "hosts", + "requires", + "overrides", + ]); + const id = text(object.id); + if (!ID.test(id)) { + throw new Error("Invalid capability id"); + } + if ( + object.kind !== "skill" && + object.kind !== "plugin" && + object.kind !== "mcp" + ) { + throw new Error("Invalid capability kind"); + } + if ( + object.owner !== "fclt" && + object.owner !== "native" && + object.owner !== "external" + ) { + throw new Error("Invalid capability owner"); + } + if (object.overrides !== undefined && !Array.isArray(object.overrides)) { + throw new Error("overrides must be an array"); + } + const overrides = ((object.overrides ?? []) as unknown[]).map((item) => + override(item, object.owner === "fclt") + ); + const selectors = overrides.map((item) => + item.host ? `host:${item.host}` : `platform:${item.platform}` + ); + if (new Set(selectors).size !== selectors.length) { + throw new Error("Duplicate capability override"); + } + if (object.requires !== undefined && !Array.isArray(object.requires)) { + throw new Error("requires must be an array"); + } + const requires = ((object.requires ?? []) as unknown[]).map(text); + if (requires.some((item) => !(ENV.test(item) || OP.test(item)))) { + throw new Error("Credentials must be env:NAME or op:// references"); + } + return { + id, + kind: object.kind, + owner: object.owner, + enabled: boolean(object.enabled, true), + source: source(object.source, object.owner === "fclt"), + target: + object.target === undefined && object.owner !== "fclt" + ? undefined + : capabilityRelativePath(object.target), + platforms: strings(object.platforms), + hosts: strings(object.hosts), + requires, + overrides, + }; +} +export function parseCapabilityRegistry(value: unknown): CapabilityRegistry { + const object = capabilityObject(value, ["schemaVersion", "entries"]); + if (object.schemaVersion !== 1 || !Array.isArray(object.entries)) { + throw new Error("Expected capability registry schemaVersion 1 and entries"); + } + const entries = object.entries.map(entry); + if (new Set(entries.map((item) => item.id)).size !== entries.length) { + throw new Error("Duplicate capability id"); + } + return { schemaVersion: 1, entries }; +} +export function selectCapability( + entryValue: CapabilityEntry, + host: string, + platform: string +): CapabilityEntry { + let result = { ...entryValue }; + for (const selected of [ + entryValue.overrides.find((item) => item.platform === platform), + entryValue.overrides.find((item) => item.host === host), + ]) { + if (selected) { + result = { + ...result, + ...(selected.enabled === undefined + ? {} + : { enabled: selected.enabled }), + ...(selected.source ? { source: selected.source } : {}), + ...(selected.target ? { target: selected.target } : {}), + }; + } + } + result.enabled = + result.enabled && + (!result.hosts || result.hosts.includes(host)) && + (!result.platforms || result.platforms.includes(platform)); + return result; +} diff --git a/src/capability.test.ts b/src/capability.test.ts new file mode 100644 index 00000000..cf267581 --- /dev/null +++ b/src/capability.test.ts @@ -0,0 +1,418 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { + chmod, + lstat, + mkdir, + mkdtemp, + readFile, + readlink, + realpath, + rm, + symlink, + truncate, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { applyCapability } from "./capability-apply"; +import { snapshotCapability } from "./capability-files"; +import { + buildCapabilityPlan, + type CapabilityContext, + loadCapabilityContext, +} from "./capability-plan"; +import { + type CapabilityEntry, + parseCapabilityRegistry, +} from "./capability-registry"; + +const roots: string[] = []; +afterEach(async () => { + for (const root of roots.splice(0)) { + await rm(root, { recursive: true, force: true }); + } +}); +async function fixture() { + const root = await realpath( + await mkdtemp(join(tmpdir(), "fclt-capability-")) + ); + roots.push(root); + const sourceRoot = join(root, "canonical"); + const targetRoot = join(root, "home"); + const sourcePath = join(sourceRoot, "skills/example"); + const target = join(targetRoot, "skills/example"); + await mkdir(sourcePath, { recursive: true }); + await mkdir(join(targetRoot, "skills"), { recursive: true }); + await writeFile(join(sourcePath, "SKILL.md"), "# Example\nv1\n"); + const entry: CapabilityEntry = { + id: "example", + kind: "skill", + owner: "fclt", + enabled: true, + source: { + path: "skills/example", + origin: "https://example.org/skills", + revision: "v1", + sha256: (await snapshotCapability(sourcePath))!.hash, + }, + target: "skills/example", + requires: [], + overrides: [], + }; + const options: CapabilityContext = { + manifest: join(sourceRoot, "capabilities.json"), + sourceRoot, + targetRoot, + stateRoot: join(root, "state"), + host: "workstation", + platform: "darwin", + }; + async function save(entries: CapabilityEntry[] = [entry]) { + await writeFile( + options.manifest, + JSON.stringify({ schemaVersion: 1, entries }) + ); + } + await save(); + async function plan() { + return await buildCapabilityPlan(options, "example"); + } + async function apply() { + const current = await plan(); + return await applyCapability(options, "example", current.planId); + } + async function updateSource() { + await writeFile(join(sourcePath, "SKILL.md"), "# Example\nv2\n"); + entry.source.sha256 = (await snapshotCapability(sourcePath))!.hash; + entry.source.revision = "v2"; + await save(); + } + return { + root, + entry, + options, + sourcePath, + target, + save, + plan, + apply, + updateSource, + }; +} +describe("versioned capability ownership", () => { + it("creates a copy, verifies provenance, and repeats without mutation", async () => { + const f = await fixture(); + expect((await f.plan()).status).toBe("create"); + const applied = await f.apply(); + expect(applied.status).toBe("current"); + expect(applied.applied).toBe(true); + expect((await lstat(f.target)).isSymbolicLink()).toBe(false); + const before = await lstat(f.target); + expect((await f.apply()).applied).toBe(false); + expect((await lstat(f.target)).mtimeMs).toBe(before.mtimeMs); + }); + it("detects installer link replacement and preserves both divergent copies", async () => { + const f = await fixture(); + await symlink(f.sourcePath, f.target); + await rm(f.target, { recursive: true, force: true }); + await mkdir(f.target); + await writeFile(join(f.target, "SKILL.md"), "installed v2"); + expect((await f.plan()).status).toBe("conflict"); + await expect(f.apply()).rejects.toThrow("refused"); + expect(await readFile(join(f.sourcePath, "SKILL.md"), "utf8")).toContain( + "v1" + ); + expect(await readFile(join(f.target, "SKILL.md"), "utf8")).toBe( + "installed v2" + ); + }); + it("detects installer writes through canonical links against the pin", async () => { + const f = await fixture(); + await symlink(f.sourcePath, f.target); + await writeFile(join(f.target, "SKILL.md"), "through link"); + expect((await f.plan()).reason).toContain("pinned"); + await expect(f.apply()).rejects.toThrow("refused"); + }); + it("converts an exact canonical link without changing its source", async () => { + const f = await fixture(); + await symlink(f.sourcePath, f.target); + expect((await f.plan()).status).toBe("adopt"); + const result = await f.apply(); + expect(result.recoveryPath).not.toBeNull(); + expect(await readlink(result.recoveryPath!)).toBe(f.sourcePath); + expect((await lstat(f.target)).isDirectory()).toBe(true); + }); + it("preserves foreign and broken links", async () => { + const f = await fixture(); + await symlink(join(f.root, "missing"), f.target); + expect((await f.plan()).status).toBe("conflict"); + await expect(f.apply()).rejects.toThrow("refused"); + expect((await lstat(f.target)).isSymbolicLink()).toBe(true); + }); + it("restores a missing owned target", async () => { + const f = await fixture(); + await f.apply(); + await rm(f.target, { recursive: true }); + expect((await f.plan()).status).toBe("restore"); + expect((await f.apply()).status).toBe("current"); + }); + it("blocks a missing source even if a broken canonical link remains", async () => { + const f = await fixture(); + await symlink(f.sourcePath, f.target); + await rm(f.sourcePath, { recursive: true }); + expect((await f.plan()).status).toBe("blocked"); + expect((await lstat(f.target)).isSymbolicLink()).toBe(true); + }); + it("updates source-only changes and retains old target for recovery", async () => { + const f = await fixture(); + await f.apply(); + await f.updateSource(); + expect((await f.plan()).status).toBe("update"); + const applied = await f.apply(); + expect( + await readFile(join(applied.recoveryPath!, "SKILL.md"), "utf8") + ).toContain("v1"); + expect(await readFile(join(f.target, "SKILL.md"), "utf8")).toContain("v2"); + }); + it("preserves live changes when source also changed", async () => { + const f = await fixture(); + await f.apply(); + await f.updateSource(); + await writeFile(join(f.target, "SKILL.md"), "local edit"); + expect((await f.plan()).status).toBe("conflict"); + await expect(f.apply()).rejects.toThrow("refused"); + }); + it("retains intentional disable without restoring a missing target", async () => { + const f = await fixture(); + await f.apply(); + await rm(f.target, { recursive: true }); + f.entry.enabled = false; + await f.save(); + expect((await f.plan()).status).toBe("disabled"); + await expect(f.apply()).rejects.toThrow("refused"); + }); + it("reports retained disabled targets without claiming provider disablement", async () => { + const f = await fixture(); + await f.apply(); + f.entry.enabled = false; + await f.save(); + const plan = await f.plan(); + expect(plan.status).toBe("disabled"); + expect(plan.targetPresence).toBe("present"); + expect(plan.reason).toContain("discoverable"); + }); + it("blocks missing references without exposing credential values", async () => { + const f = await fixture(); + f.entry.requires = [ + "env:FCLT_TEST_MISSING_CREDENTIAL_7821", + "op://vault/item/field", + ]; + await f.save(); + const plan = await f.plan(); + expect(plan.status).toBe("blocked"); + expect(plan.missingReferences).toEqual(f.entry.requires); + await expect(f.apply()).rejects.toThrow("refused"); + }); + it("does not require download credentials for ordinary skill documents", async () => { + const f = await fixture(); + expect((await f.apply()).status).toBe("current"); + }); + it("inventories independent plugins without targets or readiness claims", async () => { + const f = await fixture(); + f.entry.owner = "native"; + f.entry.source = { origin: "marketplace:example", revision: "1.0.0" }; + f.entry.target = undefined; + await f.save(); + const plan = await f.plan(); + expect(plan.status).toBe("independent"); + expect(plan.targetPath).toBeNull(); + expect(plan.hashes.current).toBeNull(); + await expect(f.apply()).rejects.toThrow("refused"); + }); + it("allows independent MCP entries sharing a config but refuses owned overlap", async () => { + const f = await fixture(); + const a = { + ...f.entry, + id: "a", + kind: "mcp" as const, + owner: "native" as const, + target: "config.toml", + }; + const b = { ...a, id: "b" }; + await f.save([a, b]); + expect((await loadCapabilityContext(f.options)).entries).toHaveLength(2); + await f.save([{ ...a, owner: "fclt" }, b]); + await expect(loadCapabilityContext(f.options)).rejects.toThrow("overlap"); + }); + it("applies platform then host overrides, then ordered overlay files", async () => { + const f = await fixture(); + f.entry.overrides = [ + { platform: "darwin", enabled: false }, + { host: "workstation", enabled: true }, + ]; + await f.save(); + expect((await f.plan()).status).toBe("create"); + const overlay = join(f.root, "private.json"); + await writeFile( + overlay, + JSON.stringify({ + schemaVersion: 1, + entries: [{ id: "example", enabled: false }], + }) + ); + f.options.overlays = [overlay]; + expect((await f.plan()).status).toBe("disabled"); + }); + it("rejects duplicate, unknown, ownership-changing overlays", async () => { + const f = await fixture(); + const overlay = join(f.root, "private.json"); + f.options.overlays = [overlay]; + await writeFile( + overlay, + JSON.stringify({ + schemaVersion: 1, + entries: [{ id: "example", owner: "native" }], + }) + ); + await expect(f.plan()).rejects.toThrow("Unknown"); + }); + it("refuses stale plan after source, live, or overlay changes", async () => { + const f = await fixture(); + const plan = await f.plan(); + await f.updateSource(); + await expect( + applyCapability(f.options, "example", plan.planId) + ).rejects.toThrow("Stale"); + expect(await Bun.file(join(f.target, "SKILL.md")).exists()).toBe(false); + }); + it("rejects nested symlinks and source runtime material", async () => { + const f = await fixture(); + await symlink(f.root, join(f.sourcePath, "escape")); + expect((await f.plan()).reason).toContain("link"); + await rm(join(f.sourcePath, "escape")); + await mkdir(join(f.sourcePath, "node_modules")); + expect((await f.plan()).reason).toContain("runtime"); + }); + it("rejects symlink target ancestors", async () => { + const f = await fixture(); + await rm(join(f.options.targetRoot, "skills"), { recursive: true }); + await symlink(f.sourcePath, join(f.options.targetRoot, "skills")); + expect((await f.plan()).reason).toContain("symlink ancestor"); + }); + it("preserves target runtime caches and continues CLI inventory for other entries", async () => { + const f = await fixture(); + await f.apply(); + await mkdir(join(f.target, "__pycache__")); + await writeFile(join(f.target, "__pycache__/cache.pyc"), "runtime bytes"); + await f.save([ + f.entry, + { ...f.entry, id: "other", target: "skills/other" }, + ]); + const result = Bun.spawnSync([ + process.execPath, + resolve(import.meta.dir, "index.ts"), + "capability", + "inventory", + "--manifest", + f.options.manifest, + "--source-root", + f.options.sourceRoot, + "--target-root", + f.options.targetRoot, + "--state-root", + f.options.stateRoot, + ]); + expect(result.exitCode).toBe(0); + const inventory = JSON.parse(result.stdout.toString()); + expect(inventory.entries[0].status).toBe("conflict"); + expect(inventory.entries[0].reason).toContain("runtime"); + expect(inventory.entries[1].status).toBe("create"); + await expect(f.apply()).rejects.toThrow("refused"); + expect( + await readFile(join(f.target, "__pycache__/cache.pyc"), "utf8") + ).toBe("runtime bytes"); + }); + it("rejects traversal, unknown fields, unpinned sources and duplicate ids", async () => { + const f = await fixture(); + for (const entry of [ + { ...f.entry, target: "../escape" }, + { ...f.entry, target: "C:\\escape" }, + { ...f.entry, secret: "value" }, + { ...f.entry, source: { origin: "repo", revision: "v1" } }, + ]) { + expect(() => + parseCapabilityRegistry({ schemaVersion: 1, entries: [entry] }) + ).toThrow(); + } + expect(() => + parseCapabilityRegistry({ schemaVersion: 1, entries: [f.entry, f.entry] }) + ).toThrow("Duplicate"); + }); + it("preserves executable scripts in the digest and projection", async () => { + const f = await fixture(); + await writeFile(join(f.sourcePath, "run.sh"), "#!/bin/sh\nexit 0\n"); + await chmod(join(f.sourcePath, "run.sh"), 0o700); + f.entry.source.sha256 = (await snapshotCapability(f.sourcePath))!.hash; + await f.save(); + expect((await f.apply()).status).toBe("current"); + expect((await lstat(join(f.target, "run.sh"))).mode.toString(8)).toEndWith( + "700" + ); + }); + it("allows a runtime state root under the target home outside actual targets", async () => { + const f = await fixture(); + f.options.stateRoot = join(f.options.targetRoot, "runtime/fclt"); + expect((await f.apply()).status).toBe("current"); + }); + it("rejects oversized live files per entry without changing them", async () => { + const f = await fixture(); + await f.apply(); + const large = join(f.target, "large"); + await writeFile(large, ""); + await truncate(large, 33 * 1024 * 1024); + const plan = await f.plan(); + expect(plan.status).toBe("conflict"); + expect(plan.reason).toContain("byte limit"); + await expect(f.apply()).rejects.toThrow("refused"); + expect((await lstat(large)).size).toBe(33 * 1024 * 1024); + }); + it("normalizes Unicode filenames in pinned digests", async () => { + const f = await fixture(); + await writeFile(join(f.sourcePath, "e\u0301.txt"), "text"); + const snapshot = (await snapshotCapability(f.sourcePath))!; + expect(snapshot.nodes.some((node) => node.path === "é.txt")).toBe(true); + f.entry.source.sha256 = snapshot.hash; + await f.save(); + expect((await f.apply()).status).toBe("current"); + }); + it("refuses tampered ownership receipt hashes", async () => { + const f = await fixture(); + const result = await f.apply(); + const receipt = JSON.parse(await readFile(result.statePath, "utf8")); + receipt.sourceHash = "invalid"; + await writeFile(result.statePath, JSON.stringify(receipt)); + expect((await f.plan()).status).toBe("conflict"); + await expect(f.apply()).rejects.toThrow("refused"); + }); + it("rejects credential-bearing provenance without echoing it", async () => { + const f = await fixture(); + f.entry.source.origin = "https://example.org/skills?token=fixture-value"; + await f.save(); + await expect(f.plan()).rejects.toThrow("must not contain credentials"); + }); + it("exposes the CLI digest and help contract", async () => { + const f = await fixture(); + const result = Bun.spawnSync([ + process.execPath, + resolve(import.meta.dir, "index.ts"), + "capability", + "digest", + "--path", + f.sourcePath, + ]); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout.toString()).sha256).toBe( + f.entry.source.sha256 + ); + }); +}); diff --git a/src/capability.ts b/src/capability.ts new file mode 100644 index 00000000..8a64ccbd --- /dev/null +++ b/src/capability.ts @@ -0,0 +1,156 @@ +import { hostname } from "node:os"; +import { dirname, resolve } from "node:path"; +import { applyCapability } from "./capability-apply"; +import { capabilityRoot, snapshotCapability } from "./capability-files"; +import { + buildCapabilityPlan, + type CapabilityContext, + type CapabilityPlan, + loadCapabilityContext, +} from "./capability-plan"; + +const FLAGS = new Set([ + "--manifest", + "--source-root", + "--target-root", + "--state-root", + "--host", + "--platform", + "--id", + "--expected-plan", + "--path", +]); +function parse(argv: string[]) { + const result = new Map(); + const overlays: string[] = []; + for (let index = 0; index < argv.length; index++) { + const flag = argv[index]!; + if (flag === "--json") { + continue; + } + if (flag === "--overlay") { + const path = argv[++index]; + if (!path || path.startsWith("--")) { + throw new Error("--overlay requires a file"); + } + overlays.push(path); + continue; + } + if (!FLAGS.has(flag) || result.has(flag)) { + throw new Error("Unknown or repeated capability option"); + } + const value = argv[++index]; + if (!value || value.startsWith("--")) { + throw new Error(`${flag} requires a value`); + } + result.set(flag, value); + } + return { flags: result, overlays }; +} +function required(flags: Map, key: string): string { + const value = flags.get(key); + if (!value) { + throw new Error(`${key} is required`); + } + return value; +} +export async function capabilityCommand(argv: string[]): Promise { + if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") { + console.log(`fclt capability — versioned ownership and narrow copy deployment + + digest --path + inventory --manifest --source-root --target-root --state-root + plan --id + apply --id --expected-plan + +All commands emit JSON. Optional selectors: --host --platform . +Apply ordered platform/machine/private patches with repeated --overlay . +Source root defaults to the manifest directory; host/platform default to this machine. +Apply handles one fclt-owned entry. Conflicts, disabled entries, unavailable credentials, +and native/external ownership never mutate targets. See docs/capability-registry.md.`); + return; + } + try { + const command = argv[0]; + const { flags, overlays } = parse(argv.slice(1)); + for (const flag of flags.keys()) { + if ( + (command === "digest" && flag !== "--path") || + (command !== "digest" && flag === "--path") || + (command !== "apply" && flag === "--expected-plan") || + (command !== "apply" && command !== "plan" && flag === "--id") + ) { + throw new Error("Capability option is not valid for this command"); + } + } + if (command === "digest" && overlays.length) { + throw new Error("digest does not accept overlays"); + } + if (command === "digest") { + const path = await capabilityRoot(required(flags, "--path")); + const snapshot = await snapshotCapability(path); + if (!snapshot) { + throw new Error("Capability path is missing"); + } + console.log( + JSON.stringify( + { + schemaVersion: 1, + path, + sha256: snapshot.hash, + nodes: snapshot.nodes.length, + }, + null, + 2 + ) + ); + return; + } + if (command !== "inventory" && command !== "plan" && command !== "apply") { + throw new Error("Expected capability digest, inventory, plan, or apply"); + } + const manifest = resolve(required(flags, "--manifest")); + const options: CapabilityContext = { + manifest, + overlays, + sourceRoot: flags.get("--source-root") ?? dirname(manifest), + targetRoot: required(flags, "--target-root"), + stateRoot: required(flags, "--state-root"), + host: flags.get("--host") ?? hostname(), + platform: flags.get("--platform") ?? process.platform, + }; + let result: unknown; + if (command === "inventory") { + const context = await loadCapabilityContext(options); + const entries: CapabilityPlan[] = []; + for (const entry of context.entries) { + entries.push(await buildCapabilityPlan(options, entry.id)); + } + result = { + schemaVersion: 1, + host: options.host, + platform: options.platform, + entries, + }; + } else if (command === "plan") { + result = await buildCapabilityPlan(options, required(flags, "--id")); + } else { + result = await applyCapability( + options, + required(flags, "--id"), + required(flags, "--expected-plan") + ); + } + console.log(JSON.stringify(result, null, 2)); + } catch (error) { + // Do not echo parser input or filesystem errors that could contain credential values. + console.error( + error instanceof SyntaxError + ? "Invalid capability JSON" + : error instanceof Error + ? error.message + : "Capability operation failed" + ); + process.exitCode = 1; + } +} diff --git a/src/index.ts b/src/index.ts index 5e0d6814..c4bccc34 100755 --- a/src/index.ts +++ b/src/index.ts @@ -144,6 +144,10 @@ function printHelp() { ["search/install/update", "Work with remote capability indices"], ["manage/sync", "Preview deprecated broad managed-mode output"], ["deploy plan", "Build one immutable per-asset deployment plan"], + [ + "capability", + "Inspect versioned ownership and deploy one capability", + ], [ "setup", "Install narrow agent integrations without full managed mode", @@ -1364,6 +1368,11 @@ async function main(argv: string[]) { case "manage": await import("./manage").then(({ manageCommand }) => manageCommand(rest)); return; + case "capability": + await import("./capability").then(({ capabilityCommand }) => + capabilityCommand(rest) + ); + return; case "deploy": await import("./deployment-plan").then(({ deployCommand }) => deployCommand(rest)