From 39bb501ac246a17c867c2f599ea1d150d1e74bc9 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 15 Jul 2026 11:47:25 -0400 Subject: [PATCH 1/8] refactor(activity): share project scope identity --- src/activity.ts | 8 ++------ src/paths.ts | 4 ++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/activity.ts b/src/activity.ts index 57b56e7..34eba6a 100644 --- a/src/activity.ts +++ b/src/activity.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import type { Dirent } from "node:fs"; import { lstat, readdir, readFile } from "node:fs/promises"; import { basename, join } from "node:path"; @@ -18,6 +17,7 @@ import { facultAiEvolutionLoopReportDir, facultAiEvolutionLoopStatePath, facultLocalStateRoot, + machineStateProjectScopeId, withFacultRootScope, } from "./paths"; import { reconciliationReviewById } from "./reconciliation"; @@ -1306,10 +1306,6 @@ async function latestGlobalActivity(args: { ); } -function projectScopeId(machineKey: string): string { - return `project:${createHash("sha256").update(machineKey).digest("hex").slice(0, 16)}`; -} - async function mapConcurrent( values: T[], concurrency: number, @@ -1388,7 +1384,7 @@ async function configuredProjectActivity(args: { homeDir: string }): Promise<{ activity: omitted ? null : await latestProjectActivityFromLoopDir(project.loopDir), - id: projectScopeId(project.entry.name), + id: machineStateProjectScopeId(project.entry.name), omitted, }; } diff --git a/src/paths.ts b/src/paths.ts index 15ef10f..bf0e437 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -303,6 +303,10 @@ export function machineStateProjectKey( return `${slug || "project"}-${digest}`; } +export function machineStateProjectScopeId(machineKey: string): string { + return `project:${createHash("sha256").update(machineKey).digest("hex").slice(0, 16)}`; +} + export function facultMachineStateDir( home: string = defaultHomeDir(), rootDir?: string, From 0d4f05e1e8cd4e00cdd2e9d62876bfbd239771ee Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 15 Jul 2026 12:16:02 -0400 Subject: [PATCH 2/8] feat(activity): add safe action locators --- README.md | 6 + docs/README.md | 1 + docs/activity-action-locators.md | 109 ++++ docs/codex-plugin-capability-matrix.json | 17 + docs/codex-plugin.md | 3 +- docs/reference.md | 10 + docs/writeback-evolution.md | 4 + plugins/fclt/scripts/fclt-mcp.cjs | 134 +++-- src/activity-action-contract.ts | 249 ++++++++ src/activity-action.test.ts | 717 +++++++++++++++++++++++ src/activity-action.ts | 600 +++++++++++++++++++ src/activity.ts | 39 +- src/ai.ts | 37 +- src/builtin-assets.ts | 2 +- src/evolution-loop.ts | 84 ++- src/fclt-mcp-plugin.test.ts | 135 ++++- 16 files changed, 2092 insertions(+), 55 deletions(-) create mode 100644 docs/activity-action-locators.md create mode 100644 src/activity-action-contract.ts create mode 100644 src/activity-action.test.ts create mode 100644 src/activity-action.ts diff --git a/README.md b/README.md index 50222a8..3609892 100644 --- a/README.md +++ b/README.md @@ -207,12 +207,17 @@ After the loop runs, read the human activity view or consume its portable JSON: ```bash fclt ai loop activity --project fclt ai loop activity --project --json +fclt ai loop activity --all --json +fclt ai loop resolve --json ``` The activity snapshot explains what agents captured, what sources were checked, how repeated observations were correlated, the current disposition, linked implementation work, and the next action. It is embedded in the immutable loop report so later writeback edits cannot rewrite history. +Aggregate actionable items may carry an opaque locator. Resolution revalidates +the exact current scope and lifecycle revision and returns a plain-language +plan without guessing a root or performing a mutation. ### 3. Inspect existing AI state @@ -584,6 +589,7 @@ Start with: - [Built-in pack](./docs/built-in-pack.md): packaged work-unit, writeback, and evolution defaults - [Built-in pack upgrades](./docs/pack-upgrades.md): non-destructive refresh behavior for existing `.ai` roots - [Codex plugin](./docs/codex-plugin.md): installable Codex skills and MCP tools for fclt workflows +- [Activity action locators](./docs/activity-action-locators.md): read-only resolution from aggregate activity to one verified current target - [Writeback and evolution](./docs/writeback-evolution.md): the feedback-loop workflow and review surfaces - [Managed mode](./docs/managed-mode.md): when to let `fclt` write tool files - [Roadmap](./docs/roadmap.md): current gaps and planned work diff --git a/docs/README.md b/docs/README.md index 56c68a7..818b04f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,7 @@ Start with [Writeback and evolution](./writeback-evolution.md) for the product's - [Built-in pack](./built-in-pack.md): the packaged operating-model layer for writeback and evolution. - [Built-in pack upgrades](./pack-upgrades.md): non-destructive refresh behavior for existing `.ai` roots. - [Codex plugin](./codex-plugin.md): installable Codex skills and MCP tools for fclt workflows. +- [Activity action locators](./activity-action-locators.md): resolve one aggregate activity item to a verified current target without guessing roots or performing mutation. - [Managed mode](./managed-mode.md): when to let `fclt` write tool files, and how adoption works. - [Security and trust](./security-trust.md): source trust, audit, secrets, and commit hygiene. - [Automations](./automations.md): recurring Codex loops for learning review, evolution review, and tool-call audit. diff --git a/docs/activity-action-locators.md b/docs/activity-action-locators.md new file mode 100644 index 0000000..51719b9 --- /dev/null +++ b/docs/activity-action-locators.md @@ -0,0 +1,109 @@ +# Activity action locators + +Aggregate activity can contain identical proposal, writeback, or signal ids in +more than one scope. An `actionLocator` lets a consumer ask fclt which exact +current target an item refers to without guessing a canonical root. + +The CLI/JSON contract is authoritative: + +```bash +fclt ai loop activity --json +fclt ai loop resolve --json +``` + +The Codex plugin exposes the same read-only resolver as `fclt_registry` action +`activity_resolve`. Its input is exactly one `locator` string. It rejects +caller-supplied scope, cwd, root, path, argv, endpoint, token, token-env, +credential, approval, and mutation fields. + +## Version 1 contract + +`ActivityItem.actionLocator` is optional. Consumers must treat a missing +locator as read-only, handoff-only activity. This preserves compatibility with +older reports and with items for which fclt cannot issue a safe current action. + +Version 1 locators use this opaque form: + +```text +fclt-act-v1.. +``` + +Consumers must not decode, synthesize, alter, or route on either digest. The +identity digest binds the opaque aggregate scope id plus resource kind and +identity. The binding digest additionally binds the verified machine-local +runtime identity, latest activity run, queue revision, current resource +lifecycle revision, and one allowed action class. + +The runtime identity is an opaque UUID persisted in the machine-local loop +configuration. fclt pairs it with a hash of the verified canonical root's +realpath and filesystem instance identity. Replacing a checkout at the same +path, copying runtime state, or redirecting an ancestor through a symlink does +not preserve that binding. + +The locator contains no root, path, command arguments, URL, endpoint, token, +token environment variable, credential, or external-system authority. + +## Resolution and expiration + +Resolution is read-only. fclt searches its preferred machine-local Global and +project runtime registry, verifies the canonical root and runtime-state relationship, +and recomputes the locator from current state. A project state directory cannot +redirect a locator to another clone, worktree, moved root, or project merely by +reusing an internal proposal id. + +A locator is state-bound rather than time-bound. It expires when any bound +fact changes, including: + +- the latest activity run or queue revision +- proposal or resource lifecycle state +- the allowed action class +- project/runtime identity or canonical-root registration +- locator schema compatibility + +Alternate caller-configured Global roots are not discoverable without accepting +caller authority, so their items intentionally omit locators and remain +handoff-only. + +Always resolve immediately before presenting an action plan, and resolve again +before any later lifecycle command. Refresh aggregate activity after any +rejection; never repair a locator or guess a root. + +## Resolution result + +A successful version 1 response returns: + +- exact opaque scope id and safe Global/project context +- resource kind and id +- activity run id and queue revision +- the one currently allowed action class +- a plain-language plan +- an explicit statement that no mutation was performed +- an explicit `available: false` mutation state +- the requirements a future separate mutation command must satisfy: explicit + approval and an atomic expected-binding revision check + +The action classes are `review`, `decide`, `apply`, `verify`, and `handoff`. +They describe the safe next workflow class; they do not grant permission to +execute it. + +## Fail-closed errors + +| Code | Meaning | Recovery | +| --- | --- | --- | +| `invalid_locator` | The locator is malformed. | Refresh activity and use the returned locator unchanged. | +| `incompatible_locator` | The locator version is unsupported. | Update fclt or refresh with a compatible producer. | +| `locator_not_found` | No verified current scope/resource identity matches. This includes removed resources, missing state, moved or renamed roots, and cross-project replay. | Repair registration/state if appropriate, then refresh activity. | +| `stale_revision` | The activity run, queue revision, resource lifecycle, runtime identity, or allowed action class changed. | Refresh activity and resolve the new locator. | +| `duplicate_identity` | More than one verified current target matched. | Repair duplicate registration; fclt will not choose one. | +| `locator_not_issued` | Current state matches, but the current aggregate snapshot did not issue that locator. | Refresh activity and use only the returned locator. | + +## Mutation boundary + +Resolution never reviews, accepts, rejects, applies, verifies, edits canonical +capability, writes project or tool-home files, changes workflow state, or +mutates an external system. Those operations remain separate closed commands. +Locator-bound mutation is withheld in version 1 because existing lifecycle +commands do not accept an expected locator binding revision. A consumer must +not translate a plan into those commands. A future mutation contract must +atomically require explicit approval and the expected current binding; a +locator alone is never mutation authority. diff --git a/docs/codex-plugin-capability-matrix.json b/docs/codex-plugin-capability-matrix.json index 7dc4a27..c4e24ec 100644 --- a/docs/codex-plugin-capability-matrix.json +++ b/docs/codex-plugin-capability-matrix.json @@ -759,6 +759,23 @@ "rollback": "not_applicable", "verification": "typed schema, aggregate default plus explicit filters, immutable activity snapshots, bounded targets and HTTP(S) links, no absolute paths, machine state keys, or credential-bearing URLs in portable activity JSON, queue continuity, and zero writes during preview" }, + { + "id": "evolution_loop.activity_action_resolution", + "cli": "ai loop resolve --json", + "cliAvailability": "available", + "mcp": { + "disposition": "exposed", + "tool": "fclt_registry", + "action": "activity_resolve", + "reason": "the closed router accepts only one opaque versioned locator; caller-supplied scope, cwd, root, path, argv, endpoint, token, token env, credential, approval, and mutation fields are rejected" + }, + "risk": "read_only", + "scope": "verified machine-local Global or project runtime identity selected only by the locator", + "preview": "revalidates the latest activity run, queue revision, current resource lifecycle, project/runtime identity, and allowed action class before returning a plain-language plan", + "apply": "withheld; version 1 exposes no locator-bound mutation because existing lifecycle commands do not accept the expected locator binding revision", + "rollback": "not_applicable; resolution performs zero canonical, tool-home, project, workflow, or external mutation", + "verification": "no-write snapshots, duplicate-id and duplicate-worktree isolation, cross-scope replay refusal, moved-root and missing-state refusal, stale lifecycle/run rejection, incompatible-version rejection, raw-field exclusion, and published typed-tool discovery" + }, { "id": "autosync.mutation", "cli": "autosync status|cleanup|uninstall|run --once", diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index cead1cc..ccc57fd 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -34,7 +34,8 @@ The plugin exposes: - `fclt_workflow`: typed writeback and evolution read/review/lifecycle operations - `fclt_sync`: managed-state inspection and dry-run sync preview - `fclt_registry`: source search/verification, strict-trust install/update preview, - and bounded reconciliation status/review through a closed scope/window/source-id schema + bounded reconciliation status/review through a closed scope/window/source-id schema, + and read-only resolution of one opaque activity action locator - `fclt_audit`: structured, redacted, non-interactive security audit with zero report or index writes - `fclt_automation`: read-only autosync status plus scheduled evolution-loop status and preview - `fclt_status` diff --git a/docs/reference.md b/docs/reference.md index 20908a7..8a5ca56 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -122,6 +122,7 @@ fclt ai loop enable [--rrule ] [--source ] [--dry-run] [--json] fclt ai loop disable [--dry-run] [--json] fclt ai loop status [--json] fclt ai loop activity [--all|--global|--project] [--json] +fclt ai loop resolve [--json] fclt ai loop run [--since ] [--until ] [--source ] [--dry-run] [--scheduled] [--json] ``` @@ -153,6 +154,15 @@ opaque `scopeId`, and `truncation` reports any bounded omissions. Aggregate responses are capped before they reach CLI or plugin consumers; incomplete or truncated coverage is never presented as complete. +Actionable items may include an optional opaque `actionLocator`. Resolve it +with `loop resolve` to obtain a read-only, plain-language plan for the exact +verified current scope and resource. Resolution accepts no root or scope flag, +performs no mutation, and fails closed when the activity run, queue revision, +resource lifecycle, allowed action class, or project/runtime identity changed. +Older or non-actionable items without a locator remain handoff-only. See +[Activity action locators](./activity-action-locators.md) for versioning and +error semantics. + `loop enable` is an explicit opt-in that installs an fclt-owned Codex automation. The loop persists the full current queue, emits a delta for notifications, retries bounded reconciliation failures, reports scheduler diff --git a/docs/writeback-evolution.md b/docs/writeback-evolution.md index 720a61e..03877d0 100644 --- a/docs/writeback-evolution.md +++ b/docs/writeback-evolution.md @@ -241,6 +241,10 @@ approvals, verification state, and the next action. The aggregate is contract version 2 and joins each version 1 feed to a stable opaque scope id. Check its `truncation` object before treating the returned item list as exhaustive. +Actionable items may include an opaque `actionLocator`. Resolve it with +`fclt ai loop resolve --json` to obtain a read-only plan for the +verified current scope and resource. Missing locators remain handoff-only; +consumers must never infer roots from scope labels or internal ids. Each item also identifies its global or project context, typed capability targets such as an instruction, skill, prompt, or automation, the reason for the decision, and bounded HTTP(S) evidence links when the source supplied one. diff --git a/plugins/fclt/scripts/fclt-mcp.cjs b/plugins/fclt/scripts/fclt-mcp.cjs index 6f14522..1f69da4 100644 --- a/plugins/fclt/scripts/fclt-mcp.cjs +++ b/plugins/fclt/scripts/fclt-mcp.cjs @@ -182,48 +182,65 @@ const tools = [ { name: "fclt_registry", description: - "Search and verify remote capability, preview installs and updates, or run typed source reconciliation reviews. Registry mutation remains withheld.", + "Search and verify remote capability, preview installs and updates, run typed source reconciliation reviews, or resolve one opaque activity action locator without mutation. Registry mutation remains withheld.", inputSchema: { - type: "object", - properties: { - action: { - type: "string", - enum: [ - "search", - "verify_source", - "source_list", - "install_preview", - "update_check", - "reconcile_status", - "reconcile", - ], - }, - scope: { type: "string", enum: ["global", "project"] }, - cwd: { type: "string" }, - query: { type: "string" }, - source: { type: "string" }, - item: { type: "string" }, - as: { type: "string" }, - since: { - type: "string", - pattern: - "^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2}))?$", - }, - until: { - type: "string", - pattern: - "^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2}))?$", + oneOf: [ + { + type: "object", + properties: { + action: { const: "activity_resolve" }, + locator: { + type: "string", + pattern: "^fclt-act-v[0-9]+\\.[a-f0-9]{64}\\.[a-f0-9]{64}$", + }, + }, + required: ["action", "locator"], + additionalProperties: false, }, - sourceIds: { - type: "array", - items: { - type: "string", - pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$", + { + type: "object", + properties: { + action: { + type: "string", + enum: [ + "search", + "verify_source", + "source_list", + "install_preview", + "update_check", + "reconcile_status", + "reconcile", + ], + }, + scope: { type: "string", enum: ["global", "project"] }, + cwd: { type: "string" }, + query: { type: "string" }, + source: { type: "string" }, + item: { type: "string" }, + as: { type: "string" }, + since: { + type: "string", + pattern: + "^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2}))?$", + }, + until: { + type: "string", + pattern: + "^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2}))?$", + }, + sourceIds: { + type: "array", + items: { + type: "string", + pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$", + }, + }, + incremental: { type: "boolean" }, }, + required: ["action"], + additionalProperties: false, }, - incremental: { type: "boolean" }, - }, - required: ["action"], + ], }, }, { @@ -410,7 +427,9 @@ const tools = [ ]; for (const tool of tools) { - tool.inputSchema.additionalProperties = false; + if (!tool.inputSchema.oneOf || tool.inputSchema.properties) { + tool.inputSchema.additionalProperties = false; + } } function isPlainObject(value) { @@ -425,7 +444,19 @@ function validateToolArguments(name, args) { if (!tool) { throw new Error(`Unknown tool: ${name}`); } - const schema = tool.inputSchema; + const rootSchema = tool.inputSchema; + const schema = + rootSchema.oneOf && !rootSchema.properties + ? rootSchema.oneOf.find((branch) => { + const action = branch.properties?.action; + return ( + action?.const === args.action || action?.enum?.includes(args.action) + ); + }) + : rootSchema; + if (!schema) { + throw new Error(`${name}.action is not an allowed value`); + } const properties = schema.properties || {}; const unknown = Object.keys(args).filter((key) => !(key in properties)); if (unknown.length > 0) { @@ -440,6 +471,12 @@ function validateToolArguments(name, args) { } for (const [key, value] of Object.entries(args)) { const property = properties[key]; + if (property.const !== undefined && value !== property.const) { + throw new Error(`${name}.${key} is not an allowed value`); + } + if (!property.type) { + continue; + } const validType = property.type === "array" ? Array.isArray(value) @@ -845,6 +882,23 @@ function requireOnlyRegistryFields(args, fields) { } function registryCommand(args) { + if (args.action === "activity_resolve") { + const unexpected = Object.keys(args).filter( + (key) => key !== "action" && key !== "locator" + ); + if (unexpected.length > 0) { + throw new Error( + `activity_resolve received unsupported fields: ${unexpected.join(", ")}` + ); + } + return [ + "ai", + "loop", + "resolve", + requireString("locator", args.locator), + "--json", + ]; + } if ( args.scope === "project" && args.action !== "reconcile_status" && diff --git a/src/activity-action-contract.ts b/src/activity-action-contract.ts new file mode 100644 index 0000000..b1e6a8c --- /dev/null +++ b/src/activity-action-contract.ts @@ -0,0 +1,249 @@ +import { createHash } from "node:crypto"; +import { lstatSync, realpathSync } from "node:fs"; +import type { AiProposalRecord } from "./ai"; +import type { LoopQueueItem } from "./evolution-loop"; +import { machineStateProjectKey, machineStateProjectScopeId } from "./paths"; + +export const ACTIVITY_ACTION_LOCATOR_VERSION = 1 as const; +export const ACTIVITY_ACTION_LOCATOR_PATTERN = + /^fclt-act-v(\d+)\.([a-f0-9]{64})\.([a-f0-9]{64})$/; +const ACTIVITY_ACTION_LOCATOR_VERSION_PREFIX_PATTERN = /^fclt-act-v(\d+)[.:]/; + +export type ActivityActionClass = + | "review" + | "decide" + | "apply" + | "verify" + | "handoff"; + +export type ActivityActionResourceKind = "proposal" | "signal" | "coverage"; + +export interface ActivityActionScopeBinding { + scope: "global" | "project"; + scopeId: string; + runtimeIdentity: string; +} + +export function activityActionRootIdentity(rootDir: string): string | null { + try { + const lexical = lstatSync(rootDir); + if (!lexical.isDirectory() || lexical.isSymbolicLink()) { + return null; + } + const canonicalPath = realpathSync(rootDir); + const canonical = lstatSync(canonicalPath); + if (!canonical.isDirectory() || canonical.isSymbolicLink()) { + return null; + } + return createHash("sha256") + .update( + JSON.stringify({ + canonicalPath, + device: String(canonical.dev), + inode: String(canonical.ino), + }) + ) + .digest("hex"); + } catch { + return null; + } +} + +export interface ActivityActionLocatorCandidate { + actionClass: ActivityActionClass; + bindingDigest: string; + identityDigest: string; + locator: string; + queueRevision: number; + resourceId: string; + resourceKind: ActivityActionResourceKind; +} + +export function activityActionScopeBinding(args: { + homeDir: string; + rootDir: string; + runtimeId: string; + scope: "global" | "project"; +}): ActivityActionScopeBinding | null { + const rootIdentity = activityActionRootIdentity(args.rootDir); + if (!rootIdentity) { + return null; + } + const runtimeIdentity = createHash("sha256") + .update(`${args.runtimeId}:${rootIdentity}`) + .digest("hex"); + if (args.scope === "global") { + return { + scope: "global", + scopeId: "global", + runtimeIdentity, + }; + } + const machineKey = machineStateProjectKey(args.rootDir, args.homeDir); + return { + scope: "project", + scopeId: machineStateProjectScopeId(machineKey), + runtimeIdentity, + }; +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) { + return `[${value + .map((entry) => (entry === undefined ? "null" : canonicalJson(entry))) + .join(",")}]`; + } + if (value && typeof value === "object") { + const entries = Object.entries(value as Record) + .filter(([, entry]) => entry !== undefined) + .sort(([left], [right]) => left.localeCompare(right)); + return `{${entries + .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`) + .join(",")}}`; + } + return JSON.stringify(value) ?? "null"; +} + +function digest(value: unknown): string { + return createHash("sha256").update(canonicalJson(value)).digest("hex"); +} + +function resourceIdentity(item: LoopQueueItem): { + id: string; + kind: ActivityActionResourceKind; +} { + if (item.proposalId) { + return { id: item.proposalId, kind: "proposal" }; + } + if (item.kind === "coverage") { + return { id: item.id, kind: "coverage" }; + } + return { id: item.familyId ?? item.id, kind: "signal" }; +} + +export function activityActionClass(args: { + item: LoopQueueItem; + proposal?: AiProposalRecord | null; +}): ActivityActionClass | null { + if (args.item.state === "resolved" || args.item.state === "deferred") { + return null; + } + if (!args.item.proposalId) { + return "handoff"; + } + if (!args.proposal) { + return null; + } + if (args.proposal.status === "proposed") { + return "review"; + } + if ( + args.proposal.status === "drafted" || + args.proposal.status === "in_review" + ) { + return "decide"; + } + if (args.proposal.status === "accepted") { + return "apply"; + } + if (args.proposal.status === "applied") { + return "verify"; + } + return "handoff"; +} + +export function createActivityActionLocator(args: { + item: LoopQueueItem; + proposal?: AiProposalRecord | null; + runId: string; + scope: ActivityActionScopeBinding; +}): ActivityActionLocatorCandidate | null { + const actionClass = activityActionClass({ + item: args.item, + proposal: args.proposal, + }); + if (!actionClass) { + return null; + } + const resource = resourceIdentity(args.item); + const identityDigest = digest({ + version: ACTIVITY_ACTION_LOCATOR_VERSION, + scopeId: args.scope.scopeId, + resource, + }); + const resourceRevision = digest( + resource.kind === "proposal" + ? (args.proposal ?? { id: resource.id, missing: true }) + : args.item + ); + const bindingDigest = digest({ + version: ACTIVITY_ACTION_LOCATOR_VERSION, + identityDigest, + runtimeIdentity: args.scope.runtimeIdentity, + runId: args.runId, + queueRevision: args.item.revision, + resourceRevision, + actionClass, + }); + return { + actionClass, + bindingDigest, + identityDigest, + locator: `fclt-act-v${ACTIVITY_ACTION_LOCATOR_VERSION}.${identityDigest}.${bindingDigest}`, + queueRevision: args.item.revision, + resourceId: resource.id, + resourceKind: resource.kind, + }; +} + +export function parseActivityActionLocator(locator: string): + | { + ok: true; + version: 1; + identityDigest: string; + bindingDigest: string; + } + | { + ok: false; + code: "invalid_locator" | "incompatible_locator"; + message: string; + } { + const match = ACTIVITY_ACTION_LOCATOR_PATTERN.exec(locator); + if (!match) { + const versionMatch = + ACTIVITY_ACTION_LOCATOR_VERSION_PREFIX_PATTERN.exec(locator); + if (versionMatch && Number(versionMatch[1]) !== 1) { + return { + ok: false, + code: "incompatible_locator", + message: `Unsupported activity action locator version: ${versionMatch[1]}`, + }; + } + return { + ok: false, + code: "invalid_locator", + message: "The activity action locator is malformed.", + }; + } + const [, version, identityDigest, bindingDigest] = match; + if (version !== "1") { + return { + ok: false, + code: "incompatible_locator", + message: `Unsupported activity action locator version: ${version}`, + }; + } + if (!(identityDigest && bindingDigest)) { + return { + ok: false, + code: "invalid_locator", + message: "The activity action locator is malformed.", + }; + } + return { + ok: true, + version: 1, + identityDigest, + bindingDigest, + }; +} diff --git a/src/activity-action.test.ts b/src/activity-action.test.ts new file mode 100644 index 0000000..8b022d0 --- /dev/null +++ b/src/activity-action.test.ts @@ -0,0 +1,717 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { + mkdir, + mkdtemp, + readdir, + readFile, + rename, + rm, + symlink, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + type ActivityFeed, + buildActivityFeed, + latestActivitySet, +} from "./activity"; +import { + renderActivityActionResolution, + resolveActivityActionLocator, +} from "./activity-action"; +import { activityActionRootIdentity } from "./activity-action-contract"; +import type { AiProposalRecord } from "./ai"; +import { aiCommand } from "./ai"; +import type { EvolutionLoopReport, LoopQueueItem } from "./evolution-loop"; +import { + facultAiEvolutionLoopConfigPath, + facultAiEvolutionLoopReportDir, + facultAiEvolutionLoopStatePath, + facultAiProposalDir, + machineStateProjectKey, + preferredGlobalAiRoot, +} from "./paths"; + +const STAMP = "2026-07-15T12:00:00.000Z"; + +function proposal(overrides?: Partial): AiProposalRecord { + return { + id: "EV-00001", + ts: STAMP, + status: "drafted", + scope: "project", + kind: "update_asset", + targets: ["@project/instructions/SETUP.md"], + sourceWritebacks: [], + summary: "Improve scoped setup guidance", + rationale: "Repeated evidence supports one narrow change.", + confidence: "high", + reviewRequired: true, + policyClass: "project_review", + draftRefs: [], + review: { history: [] }, + ...overrides, + }; +} + +function queueItem(overrides?: Partial): LoopQueueItem { + return { + id: "proposal:EV-00001", + kind: "proposal", + title: "Improve scoped setup guidance", + state: "approval_needed", + revision: 3, + firstSeenAt: STAMP, + lastSeenAt: STAMP, + lastChangedAt: STAMP, + proposalStatus: "drafted", + proposalId: "EV-00001", + linkedWork: [], + approvalRequired: true, + sourceIds: [], + evidenceRefs: [], + ...overrides, + }; +} + +function report(args: { + scope: "global" | "project"; + projectRoot?: string; + item?: LoopQueueItem; + runId: string; +}): EvolutionLoopReport { + const item = args.item ?? queueItem(); + return { + version: 1, + runId: args.runId, + generatedAt: STAMP, + scope: args.scope, + projectRoot: args.projectRoot, + status: "complete", + trigger: "scheduled", + generationBefore: 2, + generationAfter: 3, + coverage: [], + coverageComplete: true, + queue: [item], + delta: { + new: [item.id], + changed: [], + resolved: [], + notifiable: [item.id], + unchangedSuppressed: 0, + }, + mutations: [], + attempts: [{ attempt: 1, ok: true }], + artifactPath: "", + auditPath: "", + }; +} + +async function persistScope(args: { + homeDir: string; + scope: "global" | "project"; + proposal?: AiProposalRecord; + item?: LoopQueueItem; + projectName?: string; + runId: string; +}): Promise<{ + locator: string; + report: EvolutionLoopReport; + reportPath: string; + rootDir: string; +}> { + const projectRoot = + args.scope === "project" + ? join(args.homeDir, "workspaces", args.projectName ?? "example-project") + : undefined; + const rootDir = + args.scope === "global" + ? preferredGlobalAiRoot(args.homeDir) + : join(projectRoot!, ".ai"); + await mkdir(rootDir, { recursive: true }); + if (args.scope === "project") { + await Bun.write(join(rootDir, "config.toml"), "version = 1\n"); + } + const currentReport = report({ + scope: args.scope, + projectRoot, + runId: args.runId, + item: args.item, + }); + const runtimeId = "00000000-0000-4000-8000-000000000001"; + const rootIdentity = activityActionRootIdentity(rootDir); + if (!rootIdentity) { + throw new Error("Expected a verified activity root identity"); + } + currentReport.activity = buildActivityFeed({ + report: currentReport, + review: null, + writebacks: [], + proposals: args.proposal ? [args.proposal] : [], + locatorContext: { homeDir: args.homeDir, rootDir, runtimeId }, + }); + const locator = currentReport.activity.items[0]?.actionLocator; + if (!locator) { + throw new Error("Expected an activity action locator"); + } + if (args.proposal) { + const proposalDir = facultAiProposalDir(args.homeDir, rootDir); + await mkdir(proposalDir, { recursive: true }); + await Bun.write( + join(proposalDir, `${args.proposal.id}.json`), + `${JSON.stringify(args.proposal, null, 2)}\n` + ); + } + const reportDir = facultAiEvolutionLoopReportDir(args.homeDir, rootDir); + await mkdir(reportDir, { recursive: true }); + const reportPath = join(reportDir, `${args.runId}.json`); + await Bun.write(reportPath, `${JSON.stringify(currentReport, null, 2)}\n`); + await Bun.write( + facultAiEvolutionLoopConfigPath(args.homeDir, rootDir), + JSON.stringify({ + version: 1, + scope: args.scope, + actionLocator: { version: 1, runtimeId, rootIdentity }, + }) + ); + await Bun.write( + facultAiEvolutionLoopStatePath(args.homeDir, rootDir), + JSON.stringify({ version: 1, lastReportPath: reportPath }) + ); + return { locator, report: currentReport, reportPath, rootDir }; +} + +async function directorySnapshot( + root: string +): Promise> { + const result: Record = {}; + async function visit(pathValue: string, prefix: string): Promise { + for (const entry of await readdir(pathValue, { withFileTypes: true })) { + const child = join(pathValue, entry.name); + const key = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + await visit(child, key); + } else if (entry.isFile()) { + result[key] = await readFile(child, "utf8"); + } + } + } + await visit(root, ""); + return result; +} + +function captureConsole( + operation: () => Promise +): Promise<{ errors: string[]; logs: string[] }> { + const logs: string[] = []; + const errors: string[] = []; + const originalLog = console.log; + const originalError = console.error; + console.log = (...args: unknown[]) => logs.push(args.join(" ")); + console.error = (...args: unknown[]) => errors.push(args.join(" ")); + return operation().then( + () => { + console.log = originalLog; + console.error = originalError; + return { errors, logs }; + }, + (error) => { + console.log = originalLog; + console.error = originalError; + throw error; + } + ); +} + +describe("activity action locators", () => { + let homeDir = ""; + let originalHome: string | undefined; + let originalCwd = ""; + + beforeEach(async () => { + homeDir = await mkdtemp(join(tmpdir(), "fclt-action-locator-")); + originalHome = process.env.HOME; + originalCwd = process.cwd(); + process.env.HOME = homeDir; + process.chdir(homeDir); + process.exitCode = 0; + }); + + afterEach(async () => { + process.chdir(originalCwd); + process.env.HOME = originalHome; + process.exitCode = 0; + await rm(homeDir, { recursive: true, force: true }); + }); + + it("resolves identical internal proposal ids to exact global and project targets", async () => { + const global = await persistScope({ + homeDir, + scope: "global", + proposal: proposal({ + scope: "global", + targets: ["@ai/instructions/SETUP.md"], + }), + runId: "LR-global", + }); + const project = await persistScope({ + homeDir, + scope: "project", + proposal: proposal(), + runId: "LR-project", + }); + + expect(global.locator).not.toBe(project.locator); + const aggregate = await latestActivitySet({ + homeDir, + globalRootDir: preferredGlobalAiRoot(homeDir), + }); + expect( + aggregate.feeds.flatMap((entry) => + entry.feed.items.map((item) => item.actionLocator) + ) + ).toEqual(expect.arrayContaining([global.locator, project.locator])); + const globalResolution = await resolveActivityActionLocator({ + homeDir, + locator: global.locator, + now: () => new Date(STAMP), + }); + const projectResolution = await resolveActivityActionLocator({ + homeDir, + locator: project.locator, + now: () => new Date(STAMP), + }); + expect(globalResolution).toMatchObject({ + status: "resolved", + target: { + scopeId: "global", + scope: "global", + resource: { kind: "proposal", id: "EV-00001" }, + activity: { runId: "LR-global", revision: 3 }, + allowedActionClass: "decide", + }, + plan: { + mutation: { + performed: false, + separateCommandRequired: true, + approvalRequired: true, + staleRevisionCheckRequired: true, + }, + }, + }); + expect(projectResolution).toMatchObject({ + status: "resolved", + target: { + scope: "project", + resource: { kind: "proposal", id: "EV-00001" }, + }, + }); + expect(JSON.stringify(projectResolution)).not.toContain(project.rootDir); + }); + + it("is read-only and emits no raw root, argv, endpoint, token, or credential fields", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + proposal: proposal(), + runId: "LR-no-write", + }); + const before = await directorySnapshot(homeDir); + const resolution = await resolveActivityActionLocator({ + homeDir, + locator: fixture.locator, + now: () => new Date(STAMP), + }); + const after = await directorySnapshot(homeDir); + expect(after).toEqual(before); + const serialized = JSON.stringify(resolution); + expect(serialized).not.toContain(homeDir); + for (const rawField of [ + '"argv"', + '"cwd"', + '"root"', + '"rootDir"', + '"path"', + '"endpoint"', + '"token"', + '"tokenEnv"', + '"credential"', + ]) { + expect(serialized).not.toContain(rawField); + } + }); + + it("fails closed for stale lifecycle and activity revisions", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + proposal: proposal(), + runId: "LR-stale", + }); + await Bun.write( + join(facultAiProposalDir(homeDir, fixture.rootDir), "EV-00001.json"), + `${JSON.stringify(proposal({ status: "accepted" }), null, 2)}\n` + ); + expect( + await resolveActivityActionLocator({ homeDir, locator: fixture.locator }) + ).toMatchObject({ + status: "rejected", + error: { code: "stale_revision" }, + }); + + await Bun.write( + join(facultAiProposalDir(homeDir, fixture.rootDir), "EV-00001.json"), + `${JSON.stringify(proposal(), null, 2)}\n` + ); + const nextReport = { + ...fixture.report, + queue: [{ ...fixture.report.queue[0]!, revision: 4 }], + }; + await Bun.write( + fixture.reportPath, + `${JSON.stringify(nextReport, null, 2)}\n` + ); + expect( + await resolveActivityActionLocator({ homeDir, locator: fixture.locator }) + ).toMatchObject({ + status: "rejected", + error: { code: "stale_revision" }, + }); + + const nextRun = { + ...fixture.report, + runId: "LR-new-run", + activity: { + ...fixture.report.activity!, + run: { ...fixture.report.activity!.run, id: "LR-new-run" }, + }, + }; + await Bun.write( + fixture.reportPath, + `${JSON.stringify(nextRun, null, 2)}\n` + ); + expect( + await resolveActivityActionLocator({ homeDir, locator: fixture.locator }) + ).toMatchObject({ + status: "rejected", + error: { code: "stale_revision" }, + }); + }); + + it("resolves persisted signal and coverage locators after JSON round trips", async () => { + for (const kind of ["signal", "coverage"] as const) { + const item = queueItem({ + id: `${kind}:portable-item`, + kind, + title: `Portable ${kind}`, + state: "open", + proposalId: undefined, + proposalStatus: undefined, + familyId: kind === "signal" ? "portable-family" : undefined, + approvalRequired: false, + }); + const fixture = await persistScope({ + homeDir, + scope: "project", + projectName: `persisted-${kind}`, + item, + runId: `LR-persisted-${kind}`, + }); + expect( + await resolveActivityActionLocator({ + homeDir, + locator: fixture.locator, + }) + ).toMatchObject({ + status: "resolved", + target: { + resource: { + kind, + id: kind === "signal" ? "portable-family" : item.id, + }, + allowedActionClass: "handoff", + }, + }); + } + }); + + it("fails closed for moved roots, missing state, and incompatible locators", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + proposal: proposal(), + runId: "LR-missing", + }); + await rename( + join(fixture.rootDir, ".."), + join(homeDir, "workspaces", "renamed-project") + ); + expect( + await resolveActivityActionLocator({ homeDir, locator: fixture.locator }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_found" }, + }); + + const missingState = await persistScope({ + homeDir, + scope: "project", + projectName: "missing-state", + proposal: proposal(), + runId: "LR-no-state", + }); + await rm(facultAiEvolutionLoopStatePath(homeDir, missingState.rootDir)); + expect( + await resolveActivityActionLocator({ + homeDir, + locator: missingState.locator, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_found" }, + }); + + expect( + await resolveActivityActionLocator({ + homeDir, + locator: fixture.locator.replace("fclt-act-v1", "fclt-act-v2"), + }) + ).toMatchObject({ + status: "rejected", + error: { code: "incompatible_locator" }, + }); + expect( + await resolveActivityActionLocator({ homeDir, locator: "not-a-locator" }) + ).toMatchObject({ + status: "rejected", + error: { code: "invalid_locator" }, + }); + }); + + it("leaves legacy, custom-global, and missing-proposal items handoff-only", () => { + const globalReport = report({ scope: "global", runId: "LR-custom" }); + const customGlobal = buildActivityFeed({ + report: globalReport, + review: null, + writebacks: [], + proposals: [proposal({ scope: "global" })], + locatorContext: { + homeDir, + rootDir: join(homeDir, "custom-global-root"), + }, + }); + const missingProposal = buildActivityFeed({ + report: report({ scope: "project", runId: "LR-missing-proposal" }), + review: null, + writebacks: [], + proposals: [], + locatorContext: { + homeDir, + rootDir: join(homeDir, "workspaces", "missing", ".ai"), + }, + }); + const legacy = buildActivityFeed({ + report: globalReport, + review: null, + writebacks: [], + proposals: [proposal({ scope: "global" })], + }); + expect(customGlobal.items[0]?.actionLocator).toBeUndefined(); + expect(missingProposal.items[0]?.actionLocator).toBeUndefined(); + expect(legacy.items[0]?.actionLocator).toBeUndefined(); + }); + + it("rejects malformed current proposal state without throwing", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + proposal: proposal(), + runId: "LR-malformed", + }); + await Bun.write( + join(facultAiProposalDir(homeDir, fixture.rootDir), "EV-00001.json"), + "{not-json" + ); + expect( + await resolveActivityActionLocator({ homeDir, locator: fixture.locator }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_found" }, + }); + }); + + it("rejects incompatible state schema and locators absent from the current snapshot", async () => { + const incompatible = await persistScope({ + homeDir, + scope: "project", + projectName: "incompatible-state", + proposal: proposal(), + runId: "LR-incompatible-state", + }); + await Bun.write( + facultAiEvolutionLoopStatePath(homeDir, incompatible.rootDir), + JSON.stringify({ + version: 2, + lastReportPath: incompatible.reportPath, + }) + ); + expect( + await resolveActivityActionLocator({ + homeDir, + locator: incompatible.locator, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_found" }, + }); + + const notIssued = await persistScope({ + homeDir, + scope: "project", + projectName: "not-issued", + proposal: proposal(), + runId: "LR-not-issued", + }); + notIssued.report.activity!.items[0]!.actionLocator = undefined; + await Bun.write( + notIssued.reportPath, + `${JSON.stringify(notIssued.report, null, 2)}\n` + ); + expect( + await resolveActivityActionLocator({ + homeDir, + locator: notIssued.locator, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_issued" }, + }); + }); + + it("does not trust persisted project or title fields for plan output", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + proposal: proposal(), + runId: "LR-tampered-display", + }); + const activity = fixture.report.activity!; + activity.project = { + key: "unsafe", + name: `${homeDir}/private-project`, + rootDir: homeDir, + token: "secret-value", + } as ActivityFeed["project"]; + activity.items[0]!.title = `${homeDir}/private-title secret-value`; + await Bun.write( + fixture.reportPath, + `${JSON.stringify(fixture.report, null, 2)}\n` + ); + const serialized = JSON.stringify( + await resolveActivityActionLocator({ + homeDir, + locator: fixture.locator, + }) + ); + expect(serialized).not.toContain(homeDir); + expect(serialized).not.toContain("secret-value"); + expect(serialized).not.toContain("rootDir"); + }); + + it("rejects same-path replacement and ancestor-symlink replay", async () => { + const replaced = await persistScope({ + homeDir, + scope: "project", + projectName: "replace-me", + proposal: proposal(), + runId: "LR-replaced", + }); + const replacedProject = join(replaced.rootDir, ".."); + await rename(replacedProject, `${replacedProject}-old`); + await mkdir(replaced.rootDir, { recursive: true }); + expect( + await resolveActivityActionLocator({ + homeDir, + locator: replaced.locator, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_found" }, + }); + + const linked = await persistScope({ + homeDir, + scope: "project", + projectName: "link-me", + proposal: proposal(), + runId: "LR-linked", + }); + const linkedProject = join(linked.rootDir, ".."); + const movedProject = `${linkedProject}-moved`; + await rename(linkedProject, movedProject); + await symlink(movedProject, linkedProject, "dir"); + expect( + await resolveActivityActionLocator({ + homeDir, + locator: linked.locator, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_found" }, + }); + }); + + it("keeps duplicate worktrees distinct and refuses caller authority or approval fields", async () => { + const first = await persistScope({ + homeDir, + scope: "project", + projectName: "clone-one", + proposal: proposal(), + runId: "LR-clone-one", + }); + const second = await persistScope({ + homeDir, + scope: "project", + projectName: "clone-two", + proposal: proposal(), + runId: "LR-clone-two", + }); + expect(machineStateProjectKey(first.rootDir, homeDir)).not.toBe( + machineStateProjectKey(second.rootDir, homeDir) + ); + expect(first.locator).not.toBe(second.locator); + + const resolved = await captureConsole(async () => { + await aiCommand(["loop", "resolve", first.locator, "--json"]); + }); + expect(resolved.errors).toEqual([]); + expect(JSON.parse(resolved.logs.join("\n"))).toMatchObject({ + status: "resolved", + target: { activity: { runId: "LR-clone-one" } }, + }); + expect( + renderActivityActionResolution( + await resolveActivityActionLocator({ homeDir, locator: first.locator }) + ) + ).toContain("No mutation was performed"); + + await expect( + aiCommand([ + "loop", + "resolve", + first.locator, + "--root", + first.rootDir, + "--json", + ]) + ).rejects.toThrow( + "does not accept caller-supplied root or scope authority" + ); + await expect( + aiCommand(["loop", "resolve", first.locator, "--approve", "--json"]) + ).rejects.toThrow( + "loop resolve accepts exactly one opaque locator and optional --json" + ); + }); +}); diff --git a/src/activity-action.ts b/src/activity-action.ts new file mode 100644 index 0000000..b6871b0 --- /dev/null +++ b/src/activity-action.ts @@ -0,0 +1,600 @@ +import type { Dirent } from "node:fs"; +import { lstat, readdir, readFile } from "node:fs/promises"; +import { basename, join, resolve } from "node:path"; +import { + type ActivityFeed, + type ActivityItem, + isActivityFeed, +} from "./activity"; +import { + type ActivityActionClass, + type ActivityActionLocatorCandidate, + activityActionRootIdentity, + activityActionScopeBinding, + createActivityActionLocator, + parseActivityActionLocator, +} from "./activity-action-contract"; +import type { AiProposalRecord } from "./ai"; +import type { EvolutionLoopReport, LoopQueueItem } from "./evolution-loop"; +import { + facultAiEvolutionLoopConfigPath, + facultAiEvolutionLoopReportDir, + facultAiEvolutionLoopStatePath, + facultAiProposalDir, + facultAiStateDir, + facultLocalStateRoot, + facultMachineStateDir, + legacyFacultAiStateDirs, + machineStateProjectKey, + preferredGlobalAiRoot, +} from "./paths"; + +const MAX_ACTIVITY_REPORT_BYTES = 2_000_000; +const MAX_ACTIVITY_STATE_BYTES = 100_000; +const MAX_PROPOSAL_BYTES = 2_000_000; +const MAX_PROJECT_SCOPE_DIRS = 1000; +const PROPOSAL_ID_PATTERN = /^EV-[0-9]{5,}$/; +const RUNTIME_ID_PATTERN = /^[0-9a-f-]{36}$/; +const ROOT_IDENTITY_PATTERN = /^[a-f0-9]{64}$/; + +export type ActivityActionResolutionErrorCode = + | "invalid_locator" + | "incompatible_locator" + | "locator_not_found" + | "stale_revision" + | "duplicate_identity" + | "locator_not_issued"; + +export type ActivityActionResolution = + | { + version: 1; + kind: "activity-action-resolution"; + status: "resolved"; + resolvedAt: string; + target: { + scopeId: string; + scope: "global" | "project"; + resource: { + kind: ActivityActionLocatorCandidate["resourceKind"]; + id: string; + }; + activity: { + runId: string; + revision: number; + }; + allowedActionClass: ActivityActionClass; + }; + plan: { + summary: string; + steps: string[]; + mutation: { + available: false; + performed: false; + separateCommandRequired: true; + approvalRequired: boolean; + staleRevisionCheckRequired: true; + }; + }; + } + | { + version: 1; + kind: "activity-action-resolution"; + status: "rejected"; + error: { + code: ActivityActionResolutionErrorCode; + message: string; + recoverable: true; + next: string; + }; + }; + +interface ResolvableScope { + feed: ActivityFeed; + report: EvolutionLoopReport; + rootDir: string; + scopeBinding: NonNullable>; +} + +interface MatchingCandidate { + candidate: ActivityActionLocatorCandidate; + item: LoopQueueItem; + issuedItem?: ActivityItem; + scope: ResolvableScope; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +async function readBoundedJson( + pathValue: string, + maxBytes: number +): Promise { + const info = await lstat(pathValue); + if (!info.isFile() || info.isSymbolicLink() || info.size > maxBytes) { + throw new Error("Activity state is not a bounded regular file"); + } + return JSON.parse(await readFile(pathValue, "utf8")) as unknown; +} + +function isQueueItem(value: unknown): value is LoopQueueItem { + return ( + isRecord(value) && + typeof value.id === "string" && + typeof value.title === "string" && + (value.kind === "signal" || + value.kind === "proposal" || + value.kind === "coverage") && + typeof value.state === "string" && + Number.isInteger(value.revision) && + Number(value.revision) > 0 && + typeof value.firstSeenAt === "string" && + typeof value.lastSeenAt === "string" && + typeof value.lastChangedAt === "string" && + Array.isArray(value.linkedWork) && + value.linkedWork.every((entry) => typeof entry === "string") && + typeof value.approvalRequired === "boolean" && + Array.isArray(value.sourceIds) && + value.sourceIds.every((entry) => typeof entry === "string") && + Array.isArray(value.evidenceRefs) && + value.evidenceRefs.every((entry) => typeof entry === "string") && + (value.proposalId === undefined || + (typeof value.proposalId === "string" && + PROPOSAL_ID_PATTERN.test(value.proposalId))) && + (value.familyId === undefined || typeof value.familyId === "string") && + (value.proposalStatus === undefined || + typeof value.proposalStatus === "string") + ); +} + +function isProposalRecord( + value: unknown, + id: string +): value is AiProposalRecord { + return ( + isRecord(value) && + value.id === id && + typeof value.ts === "string" && + [ + "proposed", + "drafted", + "in_review", + "accepted", + "rejected", + "applied", + "failed", + "superseded", + ].includes(String(value.status)) && + (value.scope === "global" || value.scope === "project") && + typeof value.kind === "string" && + Array.isArray(value.targets) && + value.targets.every((entry) => typeof entry === "string") && + Array.isArray(value.sourceWritebacks) && + value.sourceWritebacks.every((entry) => typeof entry === "string") && + typeof value.summary === "string" && + typeof value.rationale === "string" && + (value.confidence === "low" || + value.confidence === "medium" || + value.confidence === "high") && + typeof value.reviewRequired === "boolean" && + typeof value.policyClass === "string" && + Array.isArray(value.draftRefs) && + value.draftRefs.every((entry) => typeof entry === "string") + ); +} + +async function currentProposal(args: { + homeDir: string; + id: string; + rootDir: string; + scope: "global" | "project"; +}): Promise { + if (!PROPOSAL_ID_PATTERN.test(args.id)) { + return null; + } + const dirs = [ + facultAiProposalDir(args.homeDir, args.rootDir), + join( + facultAiStateDir(args.homeDir, args.rootDir), + args.scope, + "evolution", + "proposals" + ), + ...legacyFacultAiStateDirs(args.homeDir, args.rootDir).map((dir) => + join(dir, args.scope, "evolution", "proposals") + ), + ]; + for (const dir of new Set(dirs)) { + try { + const value = await readBoundedJson( + join(dir, `${args.id}.json`), + MAX_PROPOSAL_BYTES + ); + if (isProposalRecord(value, args.id)) { + return value; + } + } catch (error) { + if ( + !( + isRecord(error) && + (error.code === "ENOENT" || error.code === "ENOTDIR") + ) + ) { + return null; + } + } + } + return null; +} + +async function latestReport(args: { + configPath: string; + reportDir: string; + statePath: string; + scope: "global" | "project"; +}): Promise<{ + report: EvolutionLoopReport; + rootIdentity: string; + runtimeId: string; +} | null> { + try { + const config = await readBoundedJson( + args.configPath, + MAX_ACTIVITY_STATE_BYTES + ); + if ( + !isRecord(config) || + config.version !== 1 || + config.scope !== args.scope || + !isRecord(config.actionLocator) || + config.actionLocator.version !== 1 || + typeof config.actionLocator.runtimeId !== "string" || + !RUNTIME_ID_PATTERN.test(config.actionLocator.runtimeId) || + typeof config.actionLocator.rootIdentity !== "string" || + !ROOT_IDENTITY_PATTERN.test(config.actionLocator.rootIdentity) + ) { + return null; + } + const state = await readBoundedJson( + args.statePath, + MAX_ACTIVITY_STATE_BYTES + ); + if ( + !( + isRecord(state) && + state.version === 1 && + typeof state.lastReportPath === "string" + ) + ) { + return null; + } + const reportName = basename(state.lastReportPath); + if (!reportName.endsWith(".json")) { + return null; + } + const value = await readBoundedJson( + join(args.reportDir, reportName), + MAX_ACTIVITY_REPORT_BYTES + ); + if ( + !isRecord(value) || + value.version !== 1 || + value.scope !== args.scope || + typeof value.runId !== "string" || + !Array.isArray(value.queue) || + !value.queue.every(isQueueItem) || + !isActivityFeed(value.activity) || + value.activity.scope !== args.scope || + value.activity.run.id !== value.runId + ) { + return null; + } + return { + report: value as unknown as EvolutionLoopReport, + rootIdentity: config.actionLocator.rootIdentity, + runtimeId: config.actionLocator.runtimeId, + }; + } catch { + return null; + } +} + +async function isVerifiedCanonicalRoot(rootDir: string): Promise { + try { + const info = await lstat(rootDir); + return info.isDirectory() && !info.isSymbolicLink(); + } catch { + return false; + } +} + +async function globalScope(homeDir: string): Promise { + const rootDir = preferredGlobalAiRoot(homeDir); + if (!(await isVerifiedCanonicalRoot(rootDir))) { + return null; + } + const report = await latestReport({ + configPath: facultAiEvolutionLoopConfigPath(homeDir, rootDir), + reportDir: facultAiEvolutionLoopReportDir(homeDir, rootDir), + statePath: facultAiEvolutionLoopStatePath(homeDir, rootDir), + scope: "global", + }); + if (!report?.report.activity) { + return null; + } + if (report.rootIdentity !== activityActionRootIdentity(rootDir)) { + return null; + } + const scopeBinding = activityActionScopeBinding({ + homeDir, + rootDir, + runtimeId: report.runtimeId, + scope: "global", + }); + if (!scopeBinding) { + return null; + } + return { + feed: report.report.activity, + report: report.report, + rootDir, + scopeBinding, + }; +} + +async function projectScope(args: { + entry: Dirent; + homeDir: string; + projectsDir: string; +}): Promise { + const loopDir = join( + args.projectsDir, + args.entry.name, + "ai", + "project", + "evolution", + "loop" + ); + const report = await latestReport({ + configPath: join(loopDir, "config.json"), + reportDir: join(loopDir, "reports"), + statePath: join(loopDir, "state.json"), + scope: "project", + }); + if ( + !(report?.report.activity && typeof report.report.projectRoot === "string") + ) { + return null; + } + const rootDir = join(report.report.projectRoot, ".ai"); + if (!(await isVerifiedCanonicalRoot(rootDir))) { + return null; + } + if (report.rootIdentity !== activityActionRootIdentity(rootDir)) { + return null; + } + if (machineStateProjectKey(rootDir, args.homeDir) !== args.entry.name) { + return null; + } + if ( + resolve(facultMachineStateDir(args.homeDir, rootDir)) !== + resolve(join(args.projectsDir, args.entry.name)) + ) { + return null; + } + const scopeBinding = activityActionScopeBinding({ + homeDir: args.homeDir, + rootDir, + runtimeId: report.runtimeId, + scope: "project", + }); + if (!scopeBinding) { + return null; + } + return { + feed: report.report.activity, + report: report.report, + rootDir, + scopeBinding, + }; +} + +async function resolvableScopes(homeDir: string): Promise { + const scopes: ResolvableScope[] = []; + const global = await globalScope(homeDir); + if (global) { + scopes.push(global); + } + const projectsDir = join(facultLocalStateRoot(homeDir), "projects"); + let entries: Dirent[] = []; + try { + entries = (await readdir(projectsDir, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .sort((left, right) => left.name.localeCompare(right.name)); + } catch { + return scopes; + } + if (entries.length > MAX_PROJECT_SCOPE_DIRS) { + return []; + } + for (const entry of entries) { + const project = await projectScope({ entry, homeDir, projectsDir }); + if (project) { + scopes.push(project); + } + } + return scopes; +} + +function rejected(args: { + code: ActivityActionResolutionErrorCode; + message: string; + next: string; +}): ActivityActionResolution { + return { + version: 1, + kind: "activity-action-resolution", + status: "rejected", + error: { ...args, recoverable: true }, + }; +} + +function planFor( + actionClass: ActivityActionClass, + resource: { kind: string; id: string } +) { + const actionStep: Record = { + review: + "Review the current evidence and proposed direction in the verified scope.", + decide: + "Review the current proposal, then explicitly accept or reject it through the separate workflow.", + apply: + "Preview the accepted proposal against current canonical state before any separately approved apply.", + verify: + "Collect fresh outcome evidence, then record verification through the separate workflow.", + handoff: + "Hand off the verified scope and resource target; this locator authorizes no mutation.", + }; + return { + summary: `${actionStep[actionClass]} Target: ${resource.kind} ${resource.id}.`, + steps: [ + actionStep[actionClass], + "Revalidate this locator immediately before any later lifecycle action.", + "Do not invoke a mutation from this plan; locator-bound mutation is withheld until a separate command can atomically require approval and the expected binding revision.", + ], + mutation: { + available: false as const, + performed: false as const, + separateCommandRequired: true as const, + approvalRequired: actionClass !== "handoff", + staleRevisionCheckRequired: true as const, + }, + }; +} + +export async function resolveActivityActionLocator(args: { + homeDir: string; + locator: string; + now?: () => Date; +}): Promise { + const parsed = parseActivityActionLocator(args.locator); + if (!parsed.ok) { + return rejected({ + code: parsed.code, + message: parsed.message, + next: "Refresh the aggregate activity set and use a current version 1 locator.", + }); + } + + const matches: MatchingCandidate[] = []; + for (const scope of await resolvableScopes(args.homeDir)) { + for (const item of scope.report.queue) { + const proposal = item.proposalId + ? await currentProposal({ + homeDir: args.homeDir, + id: item.proposalId, + rootDir: scope.rootDir, + scope: scope.scopeBinding.scope, + }) + : null; + const candidate = createActivityActionLocator({ + item, + proposal, + runId: scope.report.runId, + scope: scope.scopeBinding, + }); + if (candidate?.identityDigest !== parsed.identityDigest) { + continue; + } + matches.push({ + candidate, + item, + issuedItem: scope.feed.items.find( + (activityItem) => + activityItem.actionLocator === args.locator && + activityItem.technical.queueId === item.id + ), + scope, + }); + } + } + + if (matches.length === 0) { + return rejected({ + code: "locator_not_found", + message: + "The locator no longer identifies a verified current scope and resource.", + next: "Refresh activity. Missing state, moved or renamed roots, cross-project replay, and removed resources are intentionally not guessed.", + }); + } + if (matches.length > 1) { + return rejected({ + code: "duplicate_identity", + message: + "More than one verified current target matched the locator identity, so resolution was refused.", + next: "Refresh project registration and activity state before retrying.", + }); + } + const match = matches[0]; + if (!match) { + throw new Error("Expected one activity action locator match"); + } + if (match.candidate.bindingDigest !== parsed.bindingDigest) { + return rejected({ + code: "stale_revision", + message: + "The scope, activity run, queue revision, resource lifecycle, or allowed action class changed.", + next: "Refresh the aggregate activity set and resolve its new locator.", + }); + } + if (!match.issuedItem) { + return rejected({ + code: "locator_not_issued", + message: + "The locator matches current state but was not issued by the current aggregate activity snapshot.", + next: "Refresh the aggregate activity set and use the locator it returns.", + }); + } + + return { + version: 1, + kind: "activity-action-resolution", + status: "resolved", + resolvedAt: (args.now?.() ?? new Date()).toISOString(), + target: { + scopeId: match.scope.scopeBinding.scopeId, + scope: match.scope.scopeBinding.scope, + resource: { + kind: match.candidate.resourceKind, + id: match.candidate.resourceId, + }, + activity: { + runId: match.scope.report.runId, + revision: match.candidate.queueRevision, + }, + allowedActionClass: match.candidate.actionClass, + }, + plan: planFor(match.candidate.actionClass, { + kind: match.candidate.resourceKind, + id: match.candidate.resourceId, + }), + }; +} + +export function renderActivityActionResolution( + resolution: ActivityActionResolution +): string { + if (resolution.status === "rejected") { + return [ + `Action locator rejected: ${resolution.error.message}`, + `Next: ${resolution.error.next}`, + ].join("\n"); + } + return [ + `Resolved ${resolution.target.resource.kind} ${resolution.target.resource.id}`, + `Scope: ${resolution.target.scopeId}`, + `Allowed action: ${resolution.target.allowedActionClass}`, + `Plan: ${resolution.plan.summary}`, + "No mutation was performed.", + ].join("\n"); +} diff --git a/src/activity.ts b/src/activity.ts index 34eba6a..b2ac667 100644 --- a/src/activity.ts +++ b/src/activity.ts @@ -1,6 +1,11 @@ import type { Dirent } from "node:fs"; import { lstat, readdir, readFile } from "node:fs/promises"; -import { basename, join } from "node:path"; +import { basename, join, resolve } from "node:path"; +import { + ACTIVITY_ACTION_LOCATOR_PATTERN, + activityActionScopeBinding, + createActivityActionLocator, +} from "./activity-action-contract"; import type { AiProposalRecord, AiWritebackRecord, @@ -18,6 +23,7 @@ import { facultAiEvolutionLoopStatePath, facultLocalStateRoot, machineStateProjectScopeId, + preferredGlobalAiRoot, withFacultRootScope, } from "./paths"; import { reconciliationReviewById } from "./reconciliation"; @@ -118,6 +124,7 @@ export interface ActivityContext { } export interface ActivityItem { + actionLocator?: string; id: string; kind: LoopQueueItem["kind"]; categories: ActivityCategory[]; @@ -731,8 +738,24 @@ export function buildActivityFeed(args: { writebacks: AiWritebackRecord[]; proposals: AiProposalRecord[]; snapshot?: ActivityFeed["snapshot"]; + locatorContext?: { + homeDir: string; + rootDir: string; + runtimeId?: string; + }; }): ActivityFeed { const project = activityProject(args.report.projectRoot); + const locatorScope = + args.locatorContext?.runtimeId && + (args.report.scope === "project" || + resolve(args.locatorContext.rootDir) === + resolve(preferredGlobalAiRoot(args.locatorContext.homeDir))) + ? activityActionScopeBinding({ + ...args.locatorContext, + runtimeId: args.locatorContext.runtimeId, + scope: args.report.scope, + }) + : null; const writebackById = new Map( args.writebacks.map((record) => [record.id, record]) ); @@ -787,7 +810,16 @@ export function buildActivityFeed(args: { writebacks, }), }; + const actionLocator = locatorScope + ? createActivityActionLocator({ + item, + proposal, + runId: args.report.runId, + scope: locatorScope, + }) + : null; return { + ...(actionLocator ? { actionLocator: actionLocator.locator } : {}), id: item.id, kind: item.kind, categories, @@ -1029,6 +1061,9 @@ function isActivityItem(value: unknown): value is ActivityItem { return false; } if ( + (value.actionLocator !== undefined && + (typeof value.actionLocator !== "string" || + !ACTIVITY_ACTION_LOCATOR_PATTERN.test(value.actionLocator))) || typeof value.id !== "string" || typeof value.kind !== "string" || !Array.isArray(value.categories) || @@ -1090,7 +1125,7 @@ function isActivityItem(value: unknown): value is ActivityItem { return true; } -function isActivityFeed(value: unknown): value is ActivityFeed { +export function isActivityFeed(value: unknown): value is ActivityFeed { return ( isRecord(value) && value.version === 1 && diff --git a/src/ai.ts b/src/ai.ts index a73197b..4dcf39e 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -2809,7 +2809,7 @@ Usage: fclt ai writeback [args...] fclt ai evolve [args...] fclt ai review [args...] - fclt ai loop [args...] + fclt ai loop [args...] `; } @@ -2822,6 +2822,7 @@ Usage: fclt ai loop status [--json] fclt ai loop report [--json] fclt ai loop activity [--all|--global|--project] [--json] + fclt ai loop resolve [--json] fclt ai loop run [--since ] [--until ] [--source ] [--dry-run] [--scheduled] [--json] The loop keeps a full machine-local review queue and emits a delta for @@ -2941,6 +2942,40 @@ async function loopCommand(argv: string[]) { console.log(loopHelp()); return; } + if (sub === "resolve") { + if (parsed.rootArg || parsed.scope !== "merged") { + throw new Error( + "Activity locator resolution does not accept caller-supplied root or scope authority" + ); + } + const locatorArgs = commandArgs.filter((arg) => arg !== "--json"); + const locator = locatorArgs[0]; + if ( + locatorArgs.length !== 1 || + !locator || + locator.startsWith("-") || + commandArgs.some((arg) => arg.startsWith("-") && arg !== "--json") + ) { + throw new Error( + "loop resolve accepts exactly one opaque locator and optional --json" + ); + } + const { renderActivityActionResolution, resolveActivityActionLocator } = + await import("./activity-action"); + const result = await resolveActivityActionLocator({ + homeDir: process.env.HOME ?? "", + locator, + }); + console.log( + commandArgs.includes("--json") + ? JSON.stringify(result, null, 2) + : renderActivityActionResolution(result) + ); + if (result.status === "rejected") { + process.exitCode = 1; + } + return; + } const rootDir = resolveCliContextRoot({ rootArg: parsed.rootArg, scope: parsed.scope, diff --git a/src/builtin-assets.ts b/src/builtin-assets.ts index dc6bf58..3c3c812 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.2\\",\\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 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 complete fclt writeback/evolution loop and return readiness JSON.\\",\\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, preview installs and updates, or run typed source reconciliation reviews. Registry mutation remains withheld.\\",\\n inputSchema: {\\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 },\\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 tool.inputSchema.additionalProperties = false;\\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 schema = tool.inputSchema;\\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 [key, value] of Object.entries(args)) {\\n const property = properties[key];\\n const validType =\\n property.type === \\"array\\"\\n ? Array.isArray(value)\\n : property.type === \\"object\\"\\n ? isPlainObject(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 (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 (\\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\\" ? [\\"--global-only\\"] : []),\\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 \\"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 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 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","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## 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`: ask before running the proposal command, then create the smallest target-specific proposal.\\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. Accept/apply only when scope, target, and evidence are correct:\\n\\n```bash\\nfclt ai evolve accept EV-00001\\nfclt ai evolve apply EV-00001\\n```\\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- Ask for approval before applying global instructions, global skills, plugin behavior, or other broad shared surfaces.\\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` or project `/.ai`\\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. Bootstrap the complete loop with one idempotent command:\\n\\n```bash\\nfclt setup\\n```\\n\\nThis initializes or safely updates global capability, initializes the current git repository\\nwhen present, prepares writeback/evolution review state, and installs the Codex plugin when Codex\\nis available.\\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`.\\n\\n4. For advanced manual recovery, initialize global capability when missing:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n5. If a repo needs local capability, initialize project AI:\\n\\n```bash\\nfclt templates init project-ai\\n```\\n\\n6. Refresh 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\\n7. 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- First install should seed from existing agent guidance when available.\\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## 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 and evidence 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"}' + '{".codex-plugin/plugin.json":"{\\n \\"name\\": \\"fclt\\",\\n \\"version\\": \\"0.1.2\\",\\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 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 complete fclt writeback/evolution loop and return readiness JSON.\\",\\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, preview installs and updates, run typed source reconciliation reviews, or resolve one opaque activity action locator without mutation. Registry mutation remains withheld.\\",\\n inputSchema: {\\n oneOf: [\\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 [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 : 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 (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_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\\" ? [\\"--global-only\\"] : []),\\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 \\"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 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 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","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## 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`: ask before running the proposal command, then create the smallest target-specific proposal.\\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. Accept/apply only when scope, target, and evidence are correct:\\n\\n```bash\\nfclt ai evolve accept EV-00001\\nfclt ai evolve apply EV-00001\\n```\\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- Ask for approval before applying global instructions, global skills, plugin behavior, or other broad shared surfaces.\\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` or project `/.ai`\\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. Bootstrap the complete loop with one idempotent command:\\n\\n```bash\\nfclt setup\\n```\\n\\nThis initializes or safely updates global capability, initializes the current git repository\\nwhen present, prepares writeback/evolution review state, and installs the Codex plugin when Codex\\nis available.\\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`.\\n\\n4. For advanced manual recovery, initialize global capability when missing:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n5. If a repo needs local capability, initialize project AI:\\n\\n```bash\\nfclt templates init project-ai\\n```\\n\\n6. Refresh 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\\n7. 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- First install should seed from existing agent guidance when available.\\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## 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 and evidence 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"}' ) as Record; export const BUILTIN_FCLT_CODEX_PLUGIN_BINARY_FILES = JSON.parse( diff --git a/src/evolution-loop.ts b/src/evolution-loop.ts index a07cbf9..d2fb7ac 100644 --- a/src/evolution-loop.ts +++ b/src/evolution-loop.ts @@ -12,6 +12,7 @@ import { utimes, } from "node:fs/promises"; import { dirname, join } from "node:path"; +import { activityActionRootIdentity } from "./activity-action-contract"; import { type AiProposalRecord, type AiWritebackRecord, @@ -53,6 +54,8 @@ const DEFAULT_VERIFICATION_GRACE_HOURS = 24; const DEFAULT_MAX_ATTEMPTS = 3; const DEFAULT_LEASE_MINUTES = 60; const LOOP_VERSION = 1; +const ACTION_LOCATOR_RUNTIME_ID_PATTERN = /^[0-9a-f-]{36}$/; +const ACTION_LOCATOR_ROOT_IDENTITY_PATTERN = /^[a-f0-9]{64}$/; export type LoopQueueState = | "open" @@ -82,6 +85,11 @@ export interface EvolutionLoopConfig { mode: "off" | "plan-only"; reason: string; }; + actionLocator?: { + version: 1; + runtimeId: string; + rootIdentity: string; + }; updatedAt: string; } @@ -345,6 +353,18 @@ function parseConfig(value: unknown): EvolutionLoopConfig { ) { throw new Error("Malformed evolution loop config"); } + const actionLocator = value.actionLocator; + if ( + actionLocator !== undefined && + (!isPlainObject(actionLocator) || + actionLocator.version !== 1 || + typeof actionLocator.runtimeId !== "string" || + !ACTION_LOCATOR_RUNTIME_ID_PATTERN.test(actionLocator.runtimeId) || + typeof actionLocator.rootIdentity !== "string" || + !ACTION_LOCATOR_ROOT_IDENTITY_PATTERN.test(actionLocator.rootIdentity)) + ) { + throw new Error("Malformed evolution loop action locator identity"); + } return { version: 1, generation: value.generation, @@ -368,10 +388,36 @@ function parseConfig(value: unknown): EvolutionLoopConfig { mode: value.autoApply.mode, reason: value.autoApply.reason, }, + ...(actionLocator + ? { + actionLocator: actionLocator as EvolutionLoopConfig["actionLocator"], + } + : {}), updatedAt: value.updatedAt, }; } +function withCurrentActionLocatorIdentity(args: { + config: EvolutionLoopConfig; + rootDir: string; +}): EvolutionLoopConfig { + const rootIdentity = activityActionRootIdentity(args.rootDir); + if (!rootIdentity) { + return { ...args.config, actionLocator: undefined }; + } + if (args.config.actionLocator?.rootIdentity === rootIdentity) { + return args.config; + } + return { + ...args.config, + actionLocator: { + version: 1, + runtimeId: randomUUID(), + rootIdentity, + }, + }; +} + function emptyState(): EvolutionLoopState { return { version: 1, @@ -552,7 +598,7 @@ async function enableEvolutionLoopScoped(args: { ? projectRootFromAiRoot(args.rootDir, args.homeDir) : null; const name = current?.automationName ?? automationName({ ...args, scope }); - const config: EvolutionLoopConfig = { + let config: EvolutionLoopConfig = { version: 1, generation: (current?.generation ?? 0) + (args.dryRun ? 0 : 1), enabled: true, @@ -580,6 +626,12 @@ async function enableEvolutionLoopScoped(args: { }, updatedAt: now, }; + if (!args.dryRun) { + config = withCurrentActionLocatorIdentity({ + config, + rootDir: args.rootDir, + }); + } const existingAutomation = await automationStatus({ homeDir: args.homeDir, name, @@ -1825,6 +1877,11 @@ async function persistFailedLoopRun(args: { review: args.review ?? null, writebacks: [], proposals: [], + locatorContext: { + homeDir: args.homeDir, + rootDir: args.rootDir, + runtimeId: args.config.actionLocator?.runtimeId, + }, }); await atomicWrite(reportPath, `${JSON.stringify(report, null, 2)}\n`); await atomicWrite(artifactPath, `${renderReport(report)}\n`); @@ -1889,7 +1946,7 @@ async function runEvolutionLoopScoped(args: { const now = args.now?.() ?? new Date(); const projectRoot = projectRootFromAiRoot(args.rootDir, args.homeDir); const scope = args.scope ?? (projectRoot ? "project" : "global"); - const config: EvolutionLoopConfig = loadedConfig ?? { + let config: EvolutionLoopConfig = loadedConfig ?? { version: 1, generation: 0, enabled: false, @@ -1908,6 +1965,19 @@ async function runEvolutionLoopScoped(args: { }, updatedAt: now.toISOString(), }; + if (!args.dryRun) { + const identifiedConfig = withCurrentActionLocatorIdentity({ + config, + rootDir: args.rootDir, + }); + if (identifiedConfig !== config) { + await atomicWrite( + facultAiEvolutionLoopConfigPath(args.homeDir, args.rootDir), + `${JSON.stringify(identifiedConfig, null, 2)}\n` + ); + } + config = identifiedConfig; + } const lockPath = `${facultAiEvolutionLoopStatePath(args.homeDir, args.rootDir)}.lock`; const execute = async (): Promise => { const prior = await loadState(args); @@ -2047,6 +2117,11 @@ async function runEvolutionLoopScoped(args: { review, writebacks, proposals, + locatorContext: { + homeDir: args.homeDir, + rootDir: args.rootDir, + runtimeId: config.actionLocator?.runtimeId, + }, }); if (!args.dryRun) { const reportPath = join(reportDir, `${runId}.json`); @@ -2124,6 +2199,11 @@ async function runEvolutionLoopScoped(args: { review, writebacks, proposals, + locatorContext: { + homeDir: args.homeDir, + rootDir: args.rootDir, + runtimeId: config.actionLocator?.runtimeId, + }, }); await atomicWrite( reportPath, diff --git a/src/fclt-mcp-plugin.test.ts b/src/fclt-mcp-plugin.test.ts index 9c6228e..1084402 100644 --- a/src/fclt-mcp-plugin.test.ts +++ b/src/fclt-mcp-plugin.test.ts @@ -92,6 +92,9 @@ function spawnConfiguredMcp(options?: { } ); } +const CLOSED_FIELD_ERROR_RE = + /unknown argument fields|received unsupported fields/; +const ACTION_LOCATOR = `fclt-act-v1.${"a".repeat(64)}.${"b".repeat(64)}`; function frame(message: unknown): string { const body = JSON.stringify(message); @@ -474,6 +477,32 @@ describe("bundled fclt MCP plugin", () => { }, }); + child.stdin.write( + frame({ + jsonrpc: "2.0", + id: 311, + method: "tools/call", + params: { + name: "fclt_registry", + arguments: { + action: "activity_resolve", + locator: ACTION_LOCATOR, + }, + }, + }) + ); + const resolveResponse = (await readFrame(child.stdout)) as { + result?: { content?: { text?: string }[]; isError?: boolean }; + }; + expect(toolPayload(resolveResponse)).toMatchObject({ + operation: { preview: false, risk: "read_only" }, + result: { + stdout: { + argv: ["ai", "loop", "resolve", ACTION_LOCATOR, "--json"], + }, + }, + }); + child.stdin.write( frame({ jsonrpc: "2.0", @@ -748,7 +777,10 @@ describe("bundled fclt MCP plugin", () => { const response = (await readFrame(child.stdout)) as { result?: { tools?: { - inputSchema?: { additionalProperties?: boolean }; + inputSchema?: { + additionalProperties?: boolean; + oneOf?: Array<{ additionalProperties?: boolean }>; + }; name?: string; }[]; }; @@ -765,7 +797,12 @@ describe("bundled fclt MCP plugin", () => { expect(names).toContain("fclt_automation"); expect( published.every( - (tool) => tool.inputSchema?.additionalProperties === false + (tool) => + tool.inputSchema?.additionalProperties === false || + tool.inputSchema?.oneOf?.every( + (branch: { additionalProperties?: boolean }) => + branch.additionalProperties === false + ) ) ).toBe(true); const workflow = published.find( @@ -788,18 +825,39 @@ describe("bundled fclt MCP plugin", () => { ) as | { inputSchema?: { - properties?: { - action?: { enum?: string[] }; - sourceIds?: { items?: { pattern?: string } }; - }; + oneOf?: Array<{ + additionalProperties?: boolean; + properties?: { + action?: { const?: string; enum?: string[] }; + locator?: { pattern?: string }; + sourceIds?: { items?: { pattern?: string } }; + }; + required?: string[]; + }>; }; } | undefined; - expect(registry?.inputSchema?.properties?.action?.enum).toEqual( + const resolverSchema = registry?.inputSchema?.oneOf?.find( + (branch) => branch.properties?.action?.const === "activity_resolve" + ); + const registrySchema = registry?.inputSchema?.oneOf?.find((branch) => + branch.properties?.action?.enum?.includes("reconcile") + ); + expect(registrySchema?.properties?.action?.enum).toEqual( expect.arrayContaining(["reconcile_status", "reconcile"]) ); + expect(resolverSchema).toMatchObject({ + additionalProperties: false, + required: ["action", "locator"], + properties: { action: { const: "activity_resolve" } }, + }); + expect(resolverSchema?.properties?.locator?.pattern).toBeDefined(); + expect(Object.keys(resolverSchema?.properties ?? {}).sort()).toEqual([ + "action", + "locator", + ]); expect( - registry?.inputSchema?.properties?.sourceIds?.items?.pattern + registrySchema?.properties?.sourceIds?.items?.pattern ).toBeDefined(); const automation = published.find( (tool) => tool.name === "fclt_automation" @@ -1183,6 +1241,55 @@ describe("bundled fclt MCP plugin", () => { expect(irrelevant.error?.message).toContain( "writeback_add received unsupported fields: expectedOutcome" ); + + for (const rawField of [ + { cwd: "/tmp/unsafe" }, + { root: "/tmp/unsafe" }, + { argv: ["ai", "evolve", "apply"] }, + { endpoint: "https://example.invalid" }, + { token: "secret" }, + { tokenEnv: "SECRET_TOKEN" }, + { credential: "secret" }, + { approve: true }, + ]) { + child.stdin.write( + frame({ + jsonrpc: "2.0", + id: 40, + method: "tools/call", + params: { + name: "fclt_registry", + arguments: { + action: "activity_resolve", + locator: ACTION_LOCATOR, + ...rawField, + }, + }, + }) + ); + const rejected = (await readFrame(child.stdout)) as { + error?: { message?: string }; + }; + expect(rejected.error?.message).toMatch(CLOSED_FIELD_ERROR_RE); + } + + child.stdin.write( + frame({ + jsonrpc: "2.0", + id: 41, + method: "tools/call", + params: { + name: "fclt_registry", + arguments: { action: "activity_resolve" }, + }, + }) + ); + const missingLocator = (await readFrame(child.stdout)) as { + error?: { message?: string }; + }; + expect(missingLocator.error?.message).toContain( + "fclt_registry requires locator" + ); } finally { child.kill(); } @@ -1520,6 +1627,10 @@ describe("Codex plugin capability matrix", () => { const evolutionLoop = matrix.capabilities.find( (capability) => capability.id === "evolution_loop.review" ); + const activityResolution = matrix.capabilities.find( + (capability) => + capability.id === "evolution_loop.activity_action_resolution" + ); expect(new Set(ids).size).toBe(ids.length); expect(matrix.generatedFrom.packageVersion).toBe(packageJson.version); @@ -1559,5 +1670,13 @@ describe("Codex plugin capability matrix", () => { tool: "fclt_automation", actions: ["loop_status", "loop_activity", "loop_preview"], }); + expect(activityResolution).toMatchObject({ + risk: "read_only", + mcp: { + disposition: "exposed", + tool: "fclt_registry", + action: "activity_resolve", + }, + }); }); }); From 2780303cac32c0d183f7376e2404e3cd501f11bb Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 15 Jul 2026 12:29:37 -0400 Subject: [PATCH 3/8] fix(activity): serialize locator issuance --- src/evolution-loop.test.ts | 88 ++++++++++++++++++++++++++++++++++++++ src/evolution-loop.ts | 44 +++++++++++-------- 2 files changed, 114 insertions(+), 18 deletions(-) diff --git a/src/evolution-loop.test.ts b/src/evolution-loop.test.ts index 2487d13..72a59c6 100644 --- a/src/evolution-loop.test.ts +++ b/src/evolution-loop.test.ts @@ -12,6 +12,7 @@ import { import { tmpdir } from "node:os"; import { basename, dirname, join } from "node:path"; import { latestActivityFeed } from "./activity"; +import { resolveActivityActionLocator } from "./activity-action"; import { type AiProposalRecord, acceptProposal, @@ -760,6 +761,9 @@ describe("evolution loop", () => { }); expect(preview.coverage[0]?.signalsDiscovered).toBeGreaterThan(0); expect(preview.generationAfter).toBe(preview.generationBefore); + expect( + preview.activity?.items.every((item) => item.actionLocator === undefined) + ).toBe(true); expect(await readFile(statePath, "utf8")).toBe(stateBefore); expect(await readFile(auditPath, "utf8")).toBe(auditBefore); expect(await readFile(reconciliationStatePath, "utf8")).toBe( @@ -1227,6 +1231,90 @@ describe("evolution loop", () => { expect((await first).status).toBe("complete"); }); + it("creates locator runtime identity only after acquiring the loop lock", async () => { + const project = await makeProject(); + const writeback = await addWriteback({ + homeDir: project.homeDir, + rootDir: project.rootDir, + kind: "missing_context", + summary: "A scoped review instruction needs a durable target.", + suggestedDestination: "@project/instructions/REVIEW.md", + evidence: [{ type: "test", ref: "locator-lock-order" }], + }); + const [proposal] = await proposeEvolution({ + homeDir: project.homeDir, + rootDir: project.rootDir, + writebackIds: [writeback.id], + }); + await draftProposal(proposal!.id, { + homeDir: project.homeDir, + rootDir: project.rootDir, + }); + await enableEvolutionLoop({ + ...project, + now: () => new Date("2026-01-03T00:00:00.000Z"), + }); + const configPath = facultAiEvolutionLoopConfigPath( + project.homeDir, + project.rootDir + ); + const configured = JSON.parse(await readFile(configPath, "utf8")) as Record< + string, + unknown + >; + const { actionLocator: _removed, ...legacyConfig } = configured; + await Bun.write(configPath, `${JSON.stringify(legacyConfig, null, 2)}\n`); + + let markEntered: (() => void) | undefined; + const entered = new Promise((resolveEntered) => { + markEntered = resolveEntered; + }); + let releaseFirst: (() => void) | undefined; + const holdFirst = new Promise((resolveRelease) => { + releaseFirst = resolveRelease; + }); + const first = runEvolutionLoop({ + ...project, + since: "2026-01-01", + until: "2026-01-03", + now: () => new Date("2026-01-03T00:00:00.000Z"), + onLockAcquired: async () => { + markEntered?.(); + await holdFirst; + }, + }); + await entered; + await expect( + runEvolutionLoop({ + ...project, + since: "2026-01-01", + until: "2026-01-03", + now: () => new Date("2026-01-03T00:00:00.000Z"), + }) + ).rejects.toThrow("Another evolution loop run holds"); + expect( + ( + JSON.parse(await readFile(configPath, "utf8")) as Record< + string, + unknown + > + ).actionLocator + ).toBeUndefined(); + + releaseFirst?.(); + const report = await first; + const locator = report.activity?.items.find( + (item) => item.actionLocator + )?.actionLocator; + expect(locator).toBeDefined(); + expect( + await resolveActivityActionLocator({ + homeDir: project.homeDir, + locator: locator!, + }) + ).toMatchObject({ status: "resolved" }); + }); + it("reports an exact manual recovery boundary for an orphaned takeover claim", async () => { const project = await makeProject(); await enableEvolutionLoop({ diff --git a/src/evolution-loop.ts b/src/evolution-loop.ts index d2fb7ac..214344e 100644 --- a/src/evolution-loop.ts +++ b/src/evolution-loop.ts @@ -1965,21 +1965,27 @@ async function runEvolutionLoopScoped(args: { }, updatedAt: now.toISOString(), }; - if (!args.dryRun) { - const identifiedConfig = withCurrentActionLocatorIdentity({ - config, - rootDir: args.rootDir, - }); - if (identifiedConfig !== config) { - await atomicWrite( - facultAiEvolutionLoopConfigPath(args.homeDir, args.rootDir), - `${JSON.stringify(identifiedConfig, null, 2)}\n` - ); - } - config = identifiedConfig; - } const lockPath = `${facultAiEvolutionLoopStatePath(args.homeDir, args.rootDir)}.lock`; const execute = async (): Promise => { + if (!args.dryRun) { + const lockedConfig = await loadConfig(args); + if (!lockedConfig?.enabled) { + throw new Error( + "Evolution loop is disabled. Run `fclt ai loop enable` first." + ); + } + const identifiedConfig = withCurrentActionLocatorIdentity({ + config: lockedConfig, + rootDir: args.rootDir, + }); + if (identifiedConfig !== lockedConfig) { + await atomicWrite( + facultAiEvolutionLoopConfigPath(args.homeDir, args.rootDir), + `${JSON.stringify(identifiedConfig, null, 2)}\n` + ); + } + config = identifiedConfig; + } const prior = await loadState(args); const generatedAt = now.toISOString(); const since = @@ -2117,11 +2123,13 @@ async function runEvolutionLoopScoped(args: { review, writebacks, proposals, - locatorContext: { - homeDir: args.homeDir, - rootDir: args.rootDir, - runtimeId: config.actionLocator?.runtimeId, - }, + locatorContext: args.dryRun + ? undefined + : { + homeDir: args.homeDir, + rootDir: args.rootDir, + runtimeId: config.actionLocator?.runtimeId, + }, }); if (!args.dryRun) { const reportPath = join(reportDir, `${runId}.json`); From 916e8618252afa7c13b03acef41dc166489a92b6 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 15 Jul 2026 16:07:08 -0400 Subject: [PATCH 4/8] fix(activity): preserve locator identity on reconfiguration --- src/evolution-loop.test.ts | 27 +++++++++++++++++++++++++-- src/evolution-loop.ts | 18 +++++++++++++----- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/evolution-loop.test.ts b/src/evolution-loop.test.ts index 72a59c6..ffa63d9 100644 --- a/src/evolution-loop.test.ts +++ b/src/evolution-loop.test.ts @@ -200,18 +200,41 @@ describe("evolution loop", () => { it("preserves configured source selection when cadence alone is updated", async () => { const project = await makeProject(); - await enableEvolutionLoop({ + const initial = await enableEvolutionLoop({ ...project, sourceIds: ["review-notes"], now: () => new Date("2026-01-03T00:00:00.000Z"), }); - await enableEvolutionLoop({ + const updated = await enableEvolutionLoop({ ...project, rrule: "RRULE:FREQ=WEEKLY;BYDAY=FR", now: () => new Date("2026-01-03T01:00:00.000Z"), }); const status = await evolutionLoopStatus(project); expect(status.config?.sourceIds).toEqual(["review-notes"]); + expect(updated.config.actionLocator).toEqual(initial.config.actionLocator); + }); + + it("rotates locator identity when configured sources change", async () => { + const project = await makeProject(); + const initial = await enableEvolutionLoop({ + ...project, + sourceIds: ["review-notes"], + now: () => new Date("2026-01-03T00:00:00.000Z"), + }); + const updated = await enableEvolutionLoop({ + ...project, + sourceIds: ["verification-notes"], + now: () => new Date("2026-01-03T01:00:00.000Z"), + }); + + expect(updated.config.sourceIds).toEqual(["verification-notes"]); + expect(updated.config.actionLocator?.runtimeId).not.toBe( + initial.config.actionLocator?.runtimeId + ); + expect(updated.config.actionLocator?.rootIdentity).toBe( + initial.config.actionLocator?.rootIdentity + ); }); it("requires a scheduled run from the current config generation", async () => { diff --git a/src/evolution-loop.ts b/src/evolution-loop.ts index 214344e..8af588b 100644 --- a/src/evolution-loop.ts +++ b/src/evolution-loop.ts @@ -598,6 +598,15 @@ async function enableEvolutionLoopScoped(args: { ? projectRootFromAiRoot(args.rootDir, args.homeDir) : null; const name = current?.automationName ?? automationName({ ...args, scope }); + const sourceIds = unique( + args.sourceIds && args.sourceIds.length > 0 + ? args.sourceIds + : (current?.sourceIds ?? []) + ); + const sourceIdsUnchanged = + current !== null && + current.sourceIds.length === sourceIds.length && + current.sourceIds.every((sourceId, index) => sourceId === sourceIds[index]); let config: EvolutionLoopConfig = { version: 1, generation: (current?.generation ?? 0) + (args.dryRun ? 0 : 1), @@ -607,11 +616,7 @@ async function enableEvolutionLoopScoped(args: { rrule: normalizeRrule( args.rrule?.trim() || current?.rrule || DEFAULT_RRULE ), - sourceIds: unique( - args.sourceIds && args.sourceIds.length > 0 - ? args.sourceIds - : (current?.sourceIds ?? []) - ), + sourceIds, lookbackHours: current?.lookbackHours ?? DEFAULT_LOOKBACK_HOURS, verificationDelayHours: current?.verificationDelayHours ?? DEFAULT_VERIFICATION_DELAY_HOURS, @@ -624,6 +629,9 @@ async function enableEvolutionLoopScoped(args: { reason: "Automatic canonical writes are withheld until a hash-bound transaction and rollback receipt are available.", }, + ...(current?.actionLocator && sourceIdsUnchanged + ? { actionLocator: current.actionLocator } + : {}), updatedAt: now, }; if (!args.dryRun) { From 2cfdaeb6c2d06870760d8931a02c947f99e50bbc Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 15 Jul 2026 16:25:34 -0400 Subject: [PATCH 5/8] fix(activity): preserve global resolution under project cap --- src/activity-action.test.ts | 35 +++++++++++++++++++++++++++++++++++ src/activity-action.ts | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/activity-action.test.ts b/src/activity-action.test.ts index 8b022d0..831dec7 100644 --- a/src/activity-action.test.ts +++ b/src/activity-action.test.ts @@ -28,6 +28,7 @@ import { facultAiEvolutionLoopReportDir, facultAiEvolutionLoopStatePath, facultAiProposalDir, + facultLocalStateRoot, machineStateProjectKey, preferredGlobalAiRoot, } from "./paths"; @@ -310,6 +311,40 @@ describe("activity action locators", () => { expect(JSON.stringify(projectResolution)).not.toContain(project.rootDir); }); + it("preserves global resolution when project discovery exceeds its cap", async () => { + const global = await persistScope({ + homeDir, + scope: "global", + proposal: proposal({ scope: "global" }), + runId: "LR-global-cap", + }); + const project = await persistScope({ + homeDir, + scope: "project", + proposal: proposal(), + runId: "LR-project-cap", + }); + const projectsDir = join(facultLocalStateRoot(homeDir), "projects"); + await Promise.all( + Array.from({ length: 1000 }, (_, index) => + mkdir(join(projectsDir, `overflow-${index}`), { recursive: true }) + ) + ); + + expect( + await resolveActivityActionLocator({ homeDir, locator: global.locator }) + ).toMatchObject({ + status: "resolved", + target: { scopeId: "global", scope: "global" }, + }); + expect( + await resolveActivityActionLocator({ homeDir, locator: project.locator }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_found" }, + }); + }); + it("is read-only and emits no raw root, argv, endpoint, token, or credential fields", async () => { const fixture = await persistScope({ homeDir, diff --git a/src/activity-action.ts b/src/activity-action.ts index b6871b0..2804552 100644 --- a/src/activity-action.ts +++ b/src/activity-action.ts @@ -415,7 +415,7 @@ async function resolvableScopes(homeDir: string): Promise { return scopes; } if (entries.length > MAX_PROJECT_SCOPE_DIRS) { - return []; + return scopes; } for (const entry of entries) { const project = await projectScope({ entry, homeDir, projectsDir }); From ae7098fe84bf9182562d91fdfa13c71dfd9d7735 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 15 Jul 2026 16:34:54 -0400 Subject: [PATCH 6/8] fix(activity): resolve legacy proposal locators --- src/activity-action.test.ts | 23 +++++++++++++++++++++++ src/activity-action.ts | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/activity-action.test.ts b/src/activity-action.test.ts index 831dec7..63ce8d0 100644 --- a/src/activity-action.test.ts +++ b/src/activity-action.test.ts @@ -311,6 +311,29 @@ describe("activity action locators", () => { expect(JSON.stringify(projectResolution)).not.toContain(project.rootDir); }); + it("resolves locators for path-safe legacy proposal ids", async () => { + const legacyId = "EV-LEGACY"; + const fixture = await persistScope({ + homeDir, + scope: "project", + proposal: proposal({ id: legacyId }), + item: queueItem({ + id: `proposal:${legacyId}`, + proposalId: legacyId, + }), + runId: "LR-legacy-id", + }); + + expect( + await resolveActivityActionLocator({ homeDir, locator: fixture.locator }) + ).toMatchObject({ + status: "resolved", + target: { + resource: { kind: "proposal", id: legacyId }, + }, + }); + }); + it("preserves global resolution when project discovery exceeds its cap", async () => { const global = await persistScope({ homeDir, diff --git a/src/activity-action.ts b/src/activity-action.ts index 2804552..17ea977 100644 --- a/src/activity-action.ts +++ b/src/activity-action.ts @@ -33,7 +33,7 @@ const MAX_ACTIVITY_REPORT_BYTES = 2_000_000; const MAX_ACTIVITY_STATE_BYTES = 100_000; const MAX_PROPOSAL_BYTES = 2_000_000; const MAX_PROJECT_SCOPE_DIRS = 1000; -const PROPOSAL_ID_PATTERN = /^EV-[0-9]{5,}$/; +const PROPOSAL_ID_PATTERN = /^EV-[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; const RUNTIME_ID_PATTERN = /^[0-9a-f-]{36}$/; const ROOT_IDENTITY_PATTERN = /^[a-f0-9]{64}$/; From 1373641363b7cc2013ec1231a6a316833143af6b Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 15 Jul 2026 16:48:06 -0400 Subject: [PATCH 7/8] fix(activity): close resolver compatibility gaps --- plugins/fclt/scripts/fclt-mcp.cjs | 24 ++++++++++++++ src/builtin-assets.ts | 2 +- src/evolution-loop.test.ts | 53 +++++++++++++++++++++++++++++++ src/evolution-loop.ts | 6 +++- src/fclt-mcp-plugin.test.ts | 45 +++++++++++++++++++++++++- src/protocol.test.ts | 1 + src/protocol.ts | 1 + 7 files changed, 129 insertions(+), 3 deletions(-) diff --git a/plugins/fclt/scripts/fclt-mcp.cjs b/plugins/fclt/scripts/fclt-mcp.cjs index 1f69da4..e3ed649 100644 --- a/plugins/fclt/scripts/fclt-mcp.cjs +++ b/plugins/fclt/scripts/fclt-mcp.cjs @@ -7,6 +7,7 @@ const path = require("node:path"); const runtime = require("./fclt-runtime.cjs"); const DEFAULT_TIMEOUT_MS = Number(process.env.FCLT_MCP_TIMEOUT_MS || 60_000); +const ACTIVITY_ACTION_RESOLVE_CAPABILITY = "activity-action-resolve-v1"; const AUDIT_READ_ONLY_CAPABILITY = "audit-read-only-v1"; const CONTENT_LENGTH_RE = /Content-Length:\s*(\d+)/i; const PLUGIN_ROOT = path.resolve(__dirname, ".."); @@ -1314,6 +1315,29 @@ async function runFclt(args, cwd, operation) { }; } + if ( + operation.action === "activity_resolve" && + !discovery.selected.capabilities?.includes( + ACTIVITY_ACTION_RESOLVE_CAPABILITY + ) + ) { + return { + code: 1, + text: JSON.stringify( + { + schemaVersion: 1, + operation, + error: "missing_runtime_capability", + message: + "The selected fclt runtime does not advertise activity-action-resolve-v1; typed activity resolution fails closed.", + runtime: discovery, + }, + null, + 2 + ), + }; + } + const result = await runtime.runCommand(discovery.selected.executable, args, { cwd: cwd || process.cwd(), env: process.env, diff --git a/src/builtin-assets.ts b/src/builtin-assets.ts index 3c3c812..0027308 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.2\\",\\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 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 complete fclt writeback/evolution loop and return readiness JSON.\\",\\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, preview installs and updates, run typed source reconciliation reviews, or resolve one opaque activity action locator without mutation. Registry mutation remains withheld.\\",\\n inputSchema: {\\n oneOf: [\\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 [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 : 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 (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_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\\" ? [\\"--global-only\\"] : []),\\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 \\"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 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 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","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## 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`: ask before running the proposal command, then create the smallest target-specific proposal.\\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. Accept/apply only when scope, target, and evidence are correct:\\n\\n```bash\\nfclt ai evolve accept EV-00001\\nfclt ai evolve apply EV-00001\\n```\\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- Ask for approval before applying global instructions, global skills, plugin behavior, or other broad shared surfaces.\\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` or project `/.ai`\\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. Bootstrap the complete loop with one idempotent command:\\n\\n```bash\\nfclt setup\\n```\\n\\nThis initializes or safely updates global capability, initializes the current git repository\\nwhen present, prepares writeback/evolution review state, and installs the Codex plugin when Codex\\nis available.\\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`.\\n\\n4. For advanced manual recovery, initialize global capability when missing:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n5. If a repo needs local capability, initialize project AI:\\n\\n```bash\\nfclt templates init project-ai\\n```\\n\\n6. Refresh 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\\n7. 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- First install should seed from existing agent guidance when available.\\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## 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 and evidence 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"}' + '{".codex-plugin/plugin.json":"{\\n \\"name\\": \\"fclt\\",\\n \\"version\\": \\"0.1.2\\",\\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_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 complete fclt writeback/evolution loop and return readiness JSON.\\",\\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, preview installs and updates, run typed source reconciliation reviews, or resolve one opaque activity action locator without mutation. Registry mutation remains withheld.\\",\\n inputSchema: {\\n oneOf: [\\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 [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 : 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 (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_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\\" ? [\\"--global-only\\"] : []),\\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 \\"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 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_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","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## 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`: ask before running the proposal command, then create the smallest target-specific proposal.\\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. Accept/apply only when scope, target, and evidence are correct:\\n\\n```bash\\nfclt ai evolve accept EV-00001\\nfclt ai evolve apply EV-00001\\n```\\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- Ask for approval before applying global instructions, global skills, plugin behavior, or other broad shared surfaces.\\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` or project `/.ai`\\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. Bootstrap the complete loop with one idempotent command:\\n\\n```bash\\nfclt setup\\n```\\n\\nThis initializes or safely updates global capability, initializes the current git repository\\nwhen present, prepares writeback/evolution review state, and installs the Codex plugin when Codex\\nis available.\\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`.\\n\\n4. For advanced manual recovery, initialize global capability when missing:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n5. If a repo needs local capability, initialize project AI:\\n\\n```bash\\nfclt templates init project-ai\\n```\\n\\n6. Refresh 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\\n7. 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- First install should seed from existing agent guidance when available.\\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## 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 and evidence 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"}' ) as Record; export const BUILTIN_FCLT_CODEX_PLUGIN_BINARY_FILES = JSON.parse( diff --git a/src/evolution-loop.test.ts b/src/evolution-loop.test.ts index ffa63d9..9fe77bd 100644 --- a/src/evolution-loop.test.ts +++ b/src/evolution-loop.test.ts @@ -1075,6 +1075,59 @@ describe("evolution loop", () => { expect(audit).toContain('"attempt":3'); }); + it("keeps proposal action locators in failed-run activity snapshots", async () => { + const project = await makeProject(); + await mkdir(join(project.rootDir, "instructions"), { recursive: true }); + await Bun.write( + join(project.rootDir, "instructions", "REVIEW.md"), + "# Review\n" + ); + await Bun.write( + join(project.projectRoot, "review.md"), + "## 2026-01-02 Capability review\n\nThe rule in @project/instructions/REVIEW.md needs a durable verification loop.\n" + ); + await enableEvolutionLoop({ + ...project, + now: () => new Date("2026-01-03T00:00:00.000Z"), + }); + const complete = await runEvolutionLoop({ + ...project, + since: "2026-01-01", + until: "2026-01-03", + now: () => new Date("2026-01-03T00:00:00.000Z"), + }); + const proposalId = complete.queue.find( + (item) => item.kind === "proposal" + )?.proposalId; + expect(proposalId).toBeDefined(); + + await enableEvolutionLoop({ + ...project, + sourceIds: ["missing-source"], + now: () => new Date("2026-01-03T01:00:00.000Z"), + }); + const failed = await runEvolutionLoop({ + ...project, + since: "2026-01-01", + until: "2026-01-03", + now: () => new Date("2026-01-03T01:00:00.000Z"), + }); + const proposalActivity = failed.activity?.items.find( + (item) => item.technical.proposalId === proposalId + ); + expect(failed.status).toBe("failed"); + expect(proposalActivity?.actionLocator).toBeDefined(); + expect( + await resolveActivityActionLocator({ + homeDir: project.homeDir, + locator: proposalActivity!.actionLocator!, + }) + ).toMatchObject({ + status: "resolved", + target: { resource: { kind: "proposal", id: proposalId } }, + }); + }); + it("records committed mutations when a later materialization step fails", async () => { const project = await makeProject(); await mkdir(join(project.rootDir, "instructions"), { recursive: true }); diff --git a/src/evolution-loop.ts b/src/evolution-loop.ts index 8af588b..4f8cddc 100644 --- a/src/evolution-loop.ts +++ b/src/evolution-loop.ts @@ -1880,11 +1880,15 @@ async function persistFailedLoopRun(args: { auditPath, }; const { buildActivityFeed } = await import("./activity"); + const proposals = await listProposals({ + homeDir: args.homeDir, + rootDir: args.rootDir, + }); report.activity = buildActivityFeed({ report, review: args.review ?? null, writebacks: [], - proposals: [], + proposals, locatorContext: { homeDir: args.homeDir, rootDir: args.rootDir, diff --git a/src/fclt-mcp-plugin.test.ts b/src/fclt-mcp-plugin.test.ts index 1084402..3aec9c6 100644 --- a/src/fclt-mcp-plugin.test.ts +++ b/src/fclt-mcp-plugin.test.ts @@ -109,7 +109,7 @@ function compatibleStubScript(): string { return [ `#!${process.execPath}`, "if (process.argv[2] === 'protocol') {", - " console.log(JSON.stringify({schemaVersion:1,packageVersion:'9.9.9',protocol:{version:1,minimumPluginVersion:1,maximumPluginVersion:1},runtime:{platform:process.platform,architecture:process.arch,executable:process.argv[1]},capabilities:['audit-read-only-v1']}));", + " console.log(JSON.stringify({schemaVersion:1,packageVersion:'9.9.9',protocol:{version:1,minimumPluginVersion:1,maximumPluginVersion:1},runtime:{platform:process.platform,architecture:process.arch,executable:process.argv[1]},capabilities:['activity-action-resolve-v1','audit-read-only-v1']}));", "} else {", " console.log(JSON.stringify({ cwd: process.cwd(), argv: process.argv.slice(2) }));", "}", @@ -999,6 +999,49 @@ describe("bundled fclt MCP plugin", () => { } }); + it("fails typed activity resolution closed for runtimes without the resolver capability", async () => { + const base = await mkdtemp(join(tmpdir(), "facult-mcp-legacy-resolver-")); + const stub = join(base, "fclt-legacy.cjs"); + const invoked = join(base, "invoked.txt"); + await Bun.write(stub, legacyProtocolStubScript("2.25.4")); + await chmod(stub, 0o755); + const pluginRoot = facultBuiltinCodexPluginRoot(); + const child = spawnConfiguredMcp({ + env: { + FCLT_BIN: stub, + FCLT_LEGACY_INVOKED: invoked, + HOME: base, + PWD: base, + }, + pluginRoot, + }); + try { + child.stdin.write( + frame({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "fclt_registry", + arguments: { + action: "activity_resolve", + locator: ACTION_LOCATOR, + }, + }, + }) + ); + const response = (await readFrame(child.stdout)) as { + result?: { content?: { text?: string }[]; isError?: boolean }; + }; + expect(response.result?.isError).toBe(true); + const payload = toolPayload(response) as unknown as { error: string }; + expect(payload.error).toBe("missing_runtime_capability"); + expect(await Bun.file(invoked).exists()).toBe(false); + } finally { + child.kill(); + } + }); + it("routes typed capability and workflow operations without shell passthrough", async () => { const home = await mkdtemp(join(tmpdir(), "facult-mcp-home-")); const workspace = await mkdtemp(join(tmpdir(), "facult-mcp-workspace-")); diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 8ca89f1..5d87bac 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -29,6 +29,7 @@ describe("CLI/plugin protocol report", () => { expect(report.runtime.architecture).toBe(process.arch); expect(report.runtime.executable).toBe(process.execPath); expect(report.capabilities).toContain("plugin-runtime-handshake-v1"); + expect(report.capabilities).toContain("activity-action-resolve-v1"); expect(report.capabilities).toContain("audit-read-only-v1"); }); }); diff --git a/src/protocol.ts b/src/protocol.ts index 53354a5..5eed59e 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -35,6 +35,7 @@ export async function protocolReport(): Promise { executable: process.execPath, }, capabilities: [ + "activity-action-resolve-v1", "audit-read-only-v1", "json-output-v1", "plugin-runtime-handshake-v1", From 27f5b9affcddb724ed9076f7d7fff038caff9659 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy Date: Wed, 15 Jul 2026 16:50:37 -0400 Subject: [PATCH 8/8] test(activity): select failed proposal item --- src/evolution-loop.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/evolution-loop.test.ts b/src/evolution-loop.test.ts index 9fe77bd..9dbc27e 100644 --- a/src/evolution-loop.test.ts +++ b/src/evolution-loop.test.ts @@ -1113,7 +1113,8 @@ describe("evolution loop", () => { now: () => new Date("2026-01-03T01:00:00.000Z"), }); const proposalActivity = failed.activity?.items.find( - (item) => item.technical.proposalId === proposalId + (item) => + item.kind === "proposal" && item.technical.proposalId === proposalId ); expect(failed.status).toBe("failed"); expect(proposalActivity?.actionLocator).toBeDefined();