diff --git a/README.md b/README.md index 0ab520da..7c249214 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,8 @@ fclt ai loop activity --project fclt ai loop activity --project --json fclt ai loop activity --all --json fclt ai loop resolve --json +fclt ai loop decide --decision accept \ + --expected-revision --actor --approval-ref --approve --json fclt ai loop history --project --since 2026-01-01T00:00:00Z --json ``` @@ -219,6 +221,10 @@ 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. +For signal-family items, `loop decide` records one explicitly approved, +revision-bound `accept`, `redirect`, `reject`, or `defer` receipt. It writes +only machine-local decision history; canonical capability, Git, trackers, +proposals, tasks, and external systems remain unchanged. Use `loop history` for a bounded multi-run timeline. It returns append-only event lineage, opaque cross-scope identities, cursor pagination, and explicit diff --git a/assets/packs/facult-operating-model/instructions/EVOLUTION.md b/assets/packs/facult-operating-model/instructions/EVOLUTION.md index 883293f6..e7b5f19f 100644 --- a/assets/packs/facult-operating-model/instructions/EVOLUTION.md +++ b/assets/packs/facult-operating-model/instructions/EVOLUTION.md @@ -169,3 +169,12 @@ mutate an external tracker or automatically apply canonical changes. Treat project auto-apply as plan-only until a hash-bound transaction, validation, rollback, and durable receipt exist; keep global and plugin changes proposal-only. + +Signal-family decisions are a separate machine-local lifecycle. Resolve the +current opaque activity locator, then record `accept`, `redirect`, `reject`, or +`defer` only with explicit approval and the exact queue revision. The append-only +receipt preserves bounded work-unit context but does not apply capability, +mutate Git or an external tracker, spawn a task, or grant authority beyond the +recorded decision. Current-source completion may close already-landed families; +hold/defer remains visible and unimplemented. Treat coverage completeness and +cursor freshness as independent review facts. diff --git a/assets/packs/facult-operating-model/skills/capability-evolution/SKILL.md b/assets/packs/facult-operating-model/skills/capability-evolution/SKILL.md index d328d856..fbbce1df 100644 --- a/assets/packs/facult-operating-model/skills/capability-evolution/SKILL.md +++ b/assets/packs/facult-operating-model/skills/capability-evolution/SKILL.md @@ -52,6 +52,11 @@ fclt ai evolve apply EV-00001 fclt ai writeback link WB-00001 --issue TEAM-123 fclt ai writeback disposition WB-00001 --type task --target TEAM-123 fclt ai evolve verify EV-00001 --effectiveness improved --evidence test:post-apply +fclt ai loop activity --json +fclt ai loop resolve --json +fclt ai loop decide --decision accept \ + --expected-revision --actor \ + --approval-ref --approve --json ``` For an explicitly enabled scheduled loop, use: @@ -69,6 +74,14 @@ check observed execution health. The loop may prepare writebacks and proposals, but canonical apply remains plan-only and external task mutation remains a separate approved integration. +For a signal-family activity item, resolve its opaque locator before recording +an operator decision. Record `accept`, `redirect`, `reject`, or `defer` only +with explicit source approval and the exact current queue revision. The receipt +is durable handoff evidence, not implementation authority: accepted work still +belongs in the external task/orchestration system, and fclt must not edit Git, +trackers, canonical capability, or spawn work on the receipt's behalf. Refresh +activity after any stale, replayed, moved-root, or duplicate-identity refusal. + The writeback queue alone cannot prove an empty window. Run configured source reconciliation first and preserve its coverage, exclusions, correlations, linked work, and dispositions. If any source is stale or unavailable, report a diff --git a/docs/README.md b/docs/README.md index 578f4aef..bde389b4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,7 +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. +- [Activity action locators](./activity-action-locators.md): resolve one aggregate activity item without guessing roots, then atomically record an explicitly approved signal-family decision without canonical or external 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 index 51719b9a..60e8a434 100644 --- a/docs/activity-action-locators.md +++ b/docs/activity-action-locators.md @@ -9,12 +9,18 @@ The CLI/JSON contract is authoritative: ```bash fclt ai loop activity --json fclt ai loop resolve --json +fclt ai loop decide \ + --decision accept \ + --expected-revision \ + --actor \ + --approval-ref \ + --approve --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. +The Codex plugin exposes the read-only resolver as `fclt_registry` action +`activity_resolve` and the signal decision command as `activity_decide`. Both +use closed schemas. Neither accepts caller-supplied scope, cwd, root, path, +argv, endpoint, token, token-env, credential, or external mutation fields. ## Version 1 contract @@ -79,13 +85,45 @@ A successful version 1 response returns: - 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 +- the requirements the separate signal decision 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. +## Signal decision lifecycle + +`loop decide` records one decision for one currently issued signal-family +activity revision. It supports `accept`, `redirect`, `reject`, and `defer`. +The command requires all of these in one call: + +- the unchanged opaque locator +- the exact current queue revision from resolution +- an explicit `--approve` +- a bounded actor identifier +- exactly one portable `--approval-ref` or bounded `--note` +- one `--redirect-target` only when the decision is `redirect` + +fclt revalidates the locator, root identity, runtime identity, activity run, +queue revision, resource identity, and issued activity item under the +evolution-loop lock. It then atomically appends one machine-local version 1 +receipt. Replaying the same binding is rejected. A later decision for the same +family requires a genuinely newer queue revision and advances the lifecycle +revision without rewriting prior history. + +The durable receipt contains the exact opaque scope and signal-family id, +decision, actor, approval reference or note, previous and new lifecycle +revisions, activity run and queue revision, binding revision, and timestamp. +It contains no root or absolute private path. Accepted output also preserves +the activity item's bounded targets, evidence summary, linked work, expected +outcome, verification state, and next action so an external orchestrator can +construct a work unit. + +Recording a decision does not edit canonical capability, update Git or a task +tracker, apply a proposal, spawn work, or grant implementation authority. In +particular, `accept` is a durable approval/handoff receipt, not execution. + ## Fail-closed errors | Code | Meaning | Recovery | @@ -96,14 +134,19 @@ execute it. | `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. | +| `approval_required` | Explicit approval was omitted. | Obtain approval for the exact current signal and retry with `--approve`. | +| `invalid_decision_input` | Decision fields are malformed, unsafe, incompatible, or unbounded. | Use the closed command shape and one portable approval source. | +| `not_signal_family` | The locator identifies a proposal or coverage item. | Use the proposal lifecycle or source reconciliation instead. | +| `replayed_decision` | This binding or an equal/older queue revision already has a receipt. | Read the existing receipt or wait for a newer signal revision. | +| `malformed_history` | The bounded machine-local decision journal is corrupt or incompatible. | Inspect and repair that journal before retrying. | +| `decision_conflict` | The loop lock or journal changed during commit. | Refresh activity after the competing operation completes. | ## 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. +mutates an external system. Signal decision recording is the one narrow +locator-bound review mutation: it writes only the append-only machine-local +decision journal after explicit approval and stale-binding checks. Proposal +lifecycle, canonical apply, cross-scope mutation, task creation, Git, and +external systems remain separate and are never inferred from a locator. diff --git a/docs/codex-plugin-capability-matrix.json b/docs/codex-plugin-capability-matrix.json index 5837ed67..b1870c6a 100644 --- a/docs/codex-plugin-capability-matrix.json +++ b/docs/codex-plugin-capability-matrix.json @@ -772,10 +772,27 @@ "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", + "apply": "resolution remains read-only; signal-family decisions use the separate activity_decide contract with explicit approval and expected 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": "evolution_loop.activity_decision_lifecycle", + "cli": "ai loop decide --decision --expected-revision --actor (--approval-ref |--note ) --approve --json", + "cliAvailability": "available", + "mcp": { + "disposition": "exposed", + "tool": "fclt_registry", + "action": "activity_decide", + "reason": "the closed router accepts one issued locator, one decision, the exact expected queue revision, explicit approval, a bounded actor, and exactly one portable approval source" + }, + "risk": "review_producing", + "scope": "verified machine-local Global or project runtime identity selected only by the locator", + "preview": "use activity_resolve and the current activity item before deciding; the decision command revalidates all bindings under the evolution-loop lock", + "apply": "atomically appends one machine-local signal decision receipt; canonical capability, Git, trackers, tasks, proposals, and external systems are unchanged", + "rollback": "append-only history is retained; a later decision requires a genuinely newer signal revision and cannot rewrite prior approval evidence", + "verification": "accept, redirect, reject, and defer receipts; stale and replay refusal; malformed history and unsafe input refusal; duplicate identity, moved-root, cross-scope, and non-signal refusal; bounded accepted work-unit output; no canonical or external mutation" + }, { "id": "autosync.mutation", "cli": "autosync status|cleanup|uninstall|run --once", diff --git a/docs/codex-plugin.md b/docs/codex-plugin.md index 2bed1250..ce407722 100644 --- a/docs/codex-plugin.md +++ b/docs/codex-plugin.md @@ -35,7 +35,8 @@ The plugin exposes: - `fclt_sync`: managed-state inspection and dry-run sync preview - `fclt_registry`: source search/verification, strict-trust install/update preview, bounded reconciliation status/review through a closed scope/window/source-id schema, - and read-only resolution of one opaque activity action locator + read-only resolution of one opaque activity action locator, and atomic + recording of one revision-bound signal-family decision - `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` @@ -53,6 +54,13 @@ trust-policy mutation, destructive migration, and background-service mutation remain deliberately withheld until their CLI APIs provide transaction-safe preview, precondition, verification, and rollback contracts. +`fclt_registry` action `activity_decide` is a narrow review-producing +exception to the otherwise read-only registry surface. It requires one issued +locator, `accept|redirect|reject|defer`, the exact expected queue revision, a +bounded actor, exactly one approval reference or note, and `approve: true`. +It records only a machine-local receipt. It cannot select a root, mutate +canonical capability or external systems, apply proposals, or spawn tasks. + The evolution-loop actions exposed through `fclt_automation` are `loop_status`, `loop_activity`, and `loop_preview`. `loop_activity` defaults to `all` and returns one portable activity set across Global and every configured diff --git a/docs/reference.md b/docs/reference.md index 5c139673..8515b9de 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -164,6 +164,7 @@ 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 decide --decision --expected-revision --actor (--approval-ref |--note ) [--redirect-target ] --approve [--json] fclt ai loop history [--all|--global|--project] [--since ] [--until ] [--item ] [--scope-id ] [--event ] [--limit <1-200>] [--cursor ] [--json] fclt ai loop run [--since ] [--until ] [--source ] [--dry-run] [--scheduled] [--json] ``` @@ -205,6 +206,15 @@ Older or non-actionable items without a locator remain handoff-only. See [Activity action locators](./activity-action-locators.md) for versioning and error semantics. +For a current signal-family item, `loop decide` atomically records an explicit +`accept`, `redirect`, `reject`, or `defer` receipt in machine-local history. It +accepts no root or scope flag and requires the exact expected queue revision, +explicit approval, a bounded actor, and exactly one approval reference or +note. The command revalidates the locator under the loop lock, refuses stale or +replayed bindings, and performs no canonical, Git, tracker, proposal-apply, or +task-spawn mutation. Accepted JSON preserves bounded work-unit context for a +separate orchestrator. + `loop history` is the bounded version 1 multi-run timeline and lineage contract. It stores immutable per-run event segments in machine-local runtime state and returns delta events rather than copying current activity items. diff --git a/docs/writeback-evolution.md b/docs/writeback-evolution.md index a53a45ae..49476876 100644 --- a/docs/writeback-evolution.md +++ b/docs/writeback-evolution.md @@ -264,6 +264,13 @@ 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. +After resolving a current signal family, record an explicitly approved +decision with `fclt ai loop decide --decision + --expected-revision --actor +(--approval-ref |--note ) --approve --json`. This appends one +revisioned machine-local receipt and does not implement the decision. Accepted +output carries bounded target, evidence, linked-work, expected-outcome, +verification, and next-action fields for an external work-unit orchestrator. 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. @@ -289,6 +296,13 @@ or unavailable sources remain visible. A complete empty run means configured coverage was checked; degraded or failed empty runs never claim that nothing is pending. +Coverage completeness and source cursor freshness remain independent. A +complete review can still report one or more stale cursors, and each stale +cursor remains visible as its own coverage activity item. Current-source +terminal work or exact default-branch containment resolves a previously open +family; an explicit hold/defer remains unresolved and is never materialized as +implementation. + ## Evolution Review accumulated signal: diff --git a/plugins/fclt/scripts/fclt-mcp.cjs b/plugins/fclt/scripts/fclt-mcp.cjs index 59490d06..67aaa2aa 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_DECIDE_CAPABILITY = "activity-action-decide-v1"; 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; @@ -183,9 +184,55 @@ const tools = [ { name: "fclt_registry", description: - "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.", + "Search and verify remote capability, run typed source reconciliation reviews, resolve one opaque activity action locator, or record one revision-bound signal decision. Canonical and external mutation remains withheld.", inputSchema: { oneOf: [ + { + type: "object", + properties: { + action: { const: "activity_decide" }, + locator: { + type: "string", + pattern: "^fclt-act-v[0-9]+\\.[a-f0-9]{64}\\.[a-f0-9]{64}$", + }, + decision: { + type: "string", + enum: ["accept", "redirect", "reject", "defer"], + }, + expectedRevision: { type: "integer", minimum: 1 }, + actor: { + type: "string", + pattern: "^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$", + }, + approvalReference: { type: "string", minLength: 1, maxLength: 500 }, + note: { type: "string", minLength: 1, maxLength: 1000 }, + redirectTarget: { type: "string", minLength: 1, maxLength: 500 }, + approve: { const: true }, + }, + required: [ + "action", + "locator", + "decision", + "expectedRevision", + "actor", + "approve", + ], + allOf: [ + { + oneOf: [ + { + required: ["approvalReference"], + not: { required: ["note"] }, + }, + { + required: ["note"], + not: { required: ["approvalReference"] }, + }, + ], + }, + ], + additionalProperties: false, + }, { type: "object", properties: { @@ -470,6 +517,22 @@ function validateToolArguments(name, args) { throw new Error(`${name} requires ${required}`); } } + for (const constraint of schema.allOf || []) { + if (Array.isArray(constraint.oneOf)) { + const matches = constraint.oneOf.filter((branch) => { + const hasRequired = (branch.required || []).every((key) => key in args); + const violatesNot = branch.not?.required + ? branch.not.required.every((key) => key in args) + : false; + return hasRequired && !violatesNot; + }); + if (matches.length !== 1) { + throw new Error( + `${name} arguments do not match exactly one allowed shape` + ); + } + } + } for (const [key, value] of Object.entries(args)) { const property = properties[key]; if (property.const !== undefined && value !== property.const) { @@ -483,13 +546,30 @@ function validateToolArguments(name, args) { ? Array.isArray(value) : property.type === "object" ? isPlainObject(value) - : typeof value === property.type; + : property.type === "integer" + ? Number.isSafeInteger(value) + : typeof value === property.type; if (!validType) { throw new Error(`${name}.${key} must be ${property.type}`); } if (property.enum && !property.enum.includes(value)) { throw new Error(`${name}.${key} is not an allowed value`); } + if ( + typeof value === "string" && + ((property.minLength !== undefined && + value.length < property.minLength) || + (property.maxLength !== undefined && value.length > property.maxLength)) + ) { + throw new Error(`${name}.${key} has an invalid length`); + } + if ( + typeof value === "number" && + property.minimum !== undefined && + value < property.minimum + ) { + throw new Error(`${name}.${key} is below the minimum`); + } if (property.pattern && !new RegExp(property.pattern).test(value)) { throw new Error(`${name}.${key} has an invalid format`); } @@ -883,6 +963,52 @@ function requireOnlyRegistryFields(args, fields) { } function registryCommand(args) { + if (args.action === "activity_decide") { + const expectedFields = new Set([ + "action", + "locator", + "decision", + "expectedRevision", + "actor", + "approvalReference", + "note", + "redirectTarget", + "approve", + ]); + const unexpected = Object.keys(args).filter( + (key) => !expectedFields.has(key) + ); + if (unexpected.length > 0) { + throw new Error( + `activity_decide received unsupported fields: ${unexpected.join(", ")}` + ); + } + if (args.approve !== true) { + throw new Error("activity_decide requires approve=true"); + } + if (!Number.isSafeInteger(args.expectedRevision)) { + throw new Error( + "activity_decide requires expectedRevision as an integer" + ); + } + return [ + "ai", + "loop", + "decide", + requireString("locator", args.locator), + "--decision", + requireString("decision", args.decision), + "--expected-revision", + String(args.expectedRevision), + "--actor", + requireString("actor", args.actor), + ...stringFlag("--approval-ref", args.approvalReference), + ...stringFlag("--note", args.note), + ...stringFlag("--redirect-target", args.redirectTarget), + "--approve", + "--json", + ]; + } if (args.action === "activity_resolve") { const unexpected = Object.keys(args).filter( (key) => key !== "action" && key !== "locator" @@ -1208,6 +1334,7 @@ function operationMetadata(name, args, command) { ? `evolve_${args.action || "list"}` : args.action || name; const reviewActions = new Set([ + "activity_decide", "writeback_add", "writeback_link", "writeback_disposition", @@ -1241,6 +1368,7 @@ function operationMetadata(name, args, command) { args.item || args.source || args.tool || + args.locator || null, preview, }; @@ -1315,6 +1443,29 @@ async function runFclt(args, cwd, operation) { }; } + if ( + operation.action === "activity_decide" && + !discovery.selected.capabilities?.includes( + ACTIVITY_ACTION_DECIDE_CAPABILITY + ) + ) { + return { + code: 1, + text: JSON.stringify( + { + schemaVersion: 1, + operation, + error: "missing_runtime_capability", + message: + "The selected fclt runtime does not advertise activity-action-decide-v1; typed activity decisions fail closed.", + runtime: discovery, + }, + null, + 2 + ), + }; + } + if ( operation.action === "activity_resolve" && !discovery.selected.capabilities?.includes( diff --git a/src/activity-action.test.ts b/src/activity-action.test.ts index 527c3e23..26b8739a 100644 --- a/src/activity-action.test.ts +++ b/src/activity-action.test.ts @@ -9,14 +9,16 @@ import { symlink, } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { type ActivityFeed, buildActivityFeed, latestActivitySet, } from "./activity"; import { + decideActivityAction, renderActivityActionResolution, + renderActivityDecisionResult, resolveActivityActionLocator, } from "./activity-action"; import { activityActionRootIdentity } from "./activity-action-contract"; @@ -25,6 +27,7 @@ import { aiCommand } from "./ai"; import type { EvolutionLoopReport, LoopQueueItem } from "./evolution-loop"; import { facultAiEvolutionLoopConfigPath, + facultAiEvolutionLoopDecisionJournalPath, facultAiEvolutionLoopReportDir, facultAiEvolutionLoopStatePath, facultAiProposalDir, @@ -75,6 +78,23 @@ function queueItem(overrides?: Partial): LoopQueueItem { }; } +function signalQueueItem(overrides?: Partial): LoopQueueItem { + return queueItem({ + id: "family:SF-approved", + kind: "signal", + title: "Preserve a bounded implementation handoff", + state: "open", + proposalStatus: undefined, + proposalId: undefined, + familyId: "SF-approved", + linkedWork: ["WORK-123"], + approvalRequired: true, + sourceIds: ["evidence"], + evidenceRefs: ["commit:abc123"], + ...overrides, + }); +} + function report(args: { scope: "global" | "project"; projectRoot?: string; @@ -500,6 +520,574 @@ describe("activity action locators", () => { } }); + it("atomically records each signal-family decision with a revision-bound receipt", async () => { + for (const decision of ["accept", "redirect", "reject", "defer"] as const) { + const fixture = await persistScope({ + homeDir, + scope: "project", + projectName: `decision-${decision}`, + item: signalQueueItem({ + familyId: `SF-${decision}`, + id: `family:SF-${decision}`, + }), + runId: `LR-decision-${decision}`, + }); + const reportBefore = await readFile(fixture.reportPath, "utf8"); + const canonicalBefore = await directorySnapshot(fixture.rootDir); + const result = await decideActivityAction({ + homeDir, + locator: fixture.locator, + decision, + expectedRevision: 3, + actor: "operator-1", + ...(decision === "defer" + ? { note: "Approved hold pending a newer signal revision." } + : { approvalReference: "linear-comment:approval-1" }), + redirectTarget: + decision === "redirect" ? "instruction:WORK_UNITS.md" : undefined, + approve: true, + now: () => new Date(STAMP), + }); + + expect(result).toMatchObject({ + version: 1, + kind: "activity-decision-receipt", + status: "recorded", + receipt: { + scope: "project", + resource: { kind: "signal", id: `SF-${decision}` }, + decision, + actor: "operator-1", + approval: + decision === "defer" + ? { note: "Approved hold pending a newer signal revision." } + : { reference: "linear-comment:approval-1" }, + previousLifecycleRevision: 0, + newLifecycleRevision: 1, + activity: { runId: `LR-decision-${decision}`, queueRevision: 3 }, + decidedAt: STAMP, + }, + }); + expect(JSON.stringify(result)).not.toContain(homeDir); + if (result.status === "recorded") { + expect(result.receipt.approval).toEqual( + decision === "defer" + ? { note: "Approved hold pending a newer signal revision." } + : { reference: "linear-comment:approval-1" } + ); + expect(result.receipt.redirectTarget).toBe( + decision === "redirect" ? "instruction:WORK_UNITS.md" : undefined + ); + } + expect(await readFile(fixture.reportPath, "utf8")).toBe(reportBefore); + expect(await directorySnapshot(fixture.rootDir)).toEqual(canonicalBefore); + const history = await readFile( + facultAiEvolutionLoopDecisionJournalPath(homeDir, fixture.rootDir), + "utf8" + ); + expect(history.trim().split("\n")).toHaveLength(1); + expect(JSON.parse(history)).toMatchObject( + result.status === "recorded" ? result.receipt : {} + ); + } + }); + + it("preserves bounded accepted-signal context for work-unit construction", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + projectName: "accepted-context", + item: signalQueueItem(), + runId: "LR-accepted-context", + }); + const issued = fixture.report.activity?.items[0]; + if (!issued) { + throw new Error("Expected issued signal activity"); + } + issued.context = { + scope: "project", + targets: [ + { + kind: "instruction", + scope: "project", + selector: "instruction:WORK_UNITS.md", + label: "Work units", + }, + ], + links: [], + }; + issued.evidence = { + count: 2, + types: ["commit", "issue"], + writebackIds: ["WB-00001"], + }; + issued.observations = [ + { + writebackId: "WB-00001", + category: "opportunity", + sensitivity: "internal", + summary: "The implementation is ready for a bounded handoff.", + contextOmitted: false, + desiredOutcome: "Dispatch one isolated implementation work unit.", + }, + ]; + issued.verification = { state: "pending", attempts: 0 }; + issued.nextAction = "Create the bounded implementation work unit."; + await Bun.write( + fixture.reportPath, + `${JSON.stringify(fixture.report, null, 2)}\n` + ); + + const result = await decideActivityAction({ + homeDir, + locator: fixture.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + now: () => new Date(STAMP), + }); + + expect(result).toMatchObject({ + status: "recorded", + workUnit: { + targets: [{ selector: "instruction:WORK_UNITS.md" }], + evidence: { + count: 2, + types: ["commit", "issue"], + writebackIds: ["WB-00001"], + }, + linkedWork: ["WORK-123"], + expectedOutcome: "Dispatch one isolated implementation work unit.", + verification: { state: "pending", attempts: 0 }, + nextAction: "Create the bounded implementation work unit.", + }, + }); + }); + + it("advances lifecycle revisions only for a newer current signal revision", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + projectName: "decision-lifecycle", + item: signalQueueItem(), + runId: "LR-decision-lifecycle-1", + }); + const first = await decideActivityAction({ + homeDir, + locator: fixture.locator, + decision: "defer", + expectedRevision: 3, + actor: "operator-1", + note: "Hold until the source produces a changed signal.", + approve: true, + now: () => new Date(STAMP), + }); + expect(first).toMatchObject({ + status: "recorded", + receipt: { previousLifecycleRevision: 0, newLifecycleRevision: 1 }, + }); + + const nextReport: EvolutionLoopReport = { + ...fixture.report, + runId: "LR-decision-lifecycle-2", + queue: [{ ...fixture.report.queue[0]!, revision: 4 }], + }; + nextReport.activity = buildActivityFeed({ + report: nextReport, + review: null, + writebacks: [], + proposals: [], + locatorContext: { + homeDir, + rootDir: fixture.rootDir, + runtimeId: "00000000-0000-4000-8000-000000000001", + }, + }); + const nextLocator = nextReport.activity.items[0]?.actionLocator; + if (!nextLocator) { + throw new Error("Expected a locator for the newer signal revision"); + } + await Bun.write( + fixture.reportPath, + `${JSON.stringify(nextReport, null, 2)}\n` + ); + + expect( + await decideActivityAction({ + homeDir, + locator: nextLocator, + decision: "accept", + expectedRevision: 4, + actor: "operator-2", + approvalReference: "linear-comment:approval-2", + approve: true, + now: () => new Date("2026-07-16T12:00:00.000Z"), + }) + ).toMatchObject({ + status: "recorded", + receipt: { + previousLifecycleRevision: 1, + newLifecycleRevision: 2, + activity: { queueRevision: 4 }, + }, + }); + }); + + it("rejects stale, replayed, malformed, incompatible, and unsafe decision inputs", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + projectName: "decision-adversarial", + item: signalQueueItem(), + runId: "LR-decision-adversarial", + }); + const base = { + homeDir, + locator: fixture.locator, + decision: "accept" as const, + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }; + + expect( + await decideActivityAction({ ...base, expectedRevision: 2 }) + ).toMatchObject({ status: "rejected", error: { code: "stale_revision" } }); + expect( + await decideActivityAction({ ...base, approve: false }) + ).toMatchObject({ + status: "rejected", + error: { code: "approval_required" }, + }); + expect( + await decideActivityAction({ + ...base, + approvalReference: "token=secret-value", + }) + ).toMatchObject({ + status: "rejected", + error: { code: "invalid_decision_input" }, + }); + expect( + await decideActivityAction({ ...base, locator: "not-a-locator" }) + ).toMatchObject({ status: "rejected", error: { code: "invalid_locator" } }); + expect( + await decideActivityAction({ + ...base, + locator: fixture.locator.replace("fclt-act-v1", "fclt-act-v2"), + }) + ).toMatchObject({ + status: "rejected", + error: { code: "incompatible_locator" }, + }); + expect( + await decideActivityAction({ + ...base, + decision: "redirect", + redirectTarget: undefined, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "invalid_decision_input" }, + }); + + expect(await decideActivityAction(base)).toMatchObject({ + status: "recorded", + }); + expect(await decideActivityAction(base)).toMatchObject({ + status: "rejected", + error: { code: "replayed_decision" }, + }); + }); + + it("fails closed for malformed history and concurrent decision attempts", async () => { + const malformed = await persistScope({ + homeDir, + scope: "project", + projectName: "decision-malformed-history", + item: signalQueueItem(), + runId: "LR-decision-malformed-history", + }); + const malformedJournal = facultAiEvolutionLoopDecisionJournalPath( + homeDir, + malformed.rootDir + ); + await mkdir(dirname(malformedJournal), { recursive: true }); + await Bun.write(malformedJournal, "{not-json}\n"); + expect( + await decideActivityAction({ + homeDir, + locator: malformed.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "malformed_history" }, + }); + + const sourceScope = await persistScope({ + homeDir, + scope: "project", + projectName: "decision-source-scope", + item: signalQueueItem(), + runId: "LR-decision-source-scope", + }); + expect( + await decideActivityAction({ + homeDir, + locator: sourceScope.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }) + ).toMatchObject({ status: "recorded" }); + const copiedHistory = await readFile( + facultAiEvolutionLoopDecisionJournalPath(homeDir, sourceScope.rootDir), + "utf8" + ); + const targetScope = await persistScope({ + homeDir, + scope: "project", + projectName: "decision-target-scope", + item: signalQueueItem(), + runId: "LR-decision-target-scope", + }); + const targetJournal = facultAiEvolutionLoopDecisionJournalPath( + homeDir, + targetScope.rootDir + ); + await mkdir(dirname(targetJournal), { recursive: true }); + await Bun.write(targetJournal, copiedHistory); + expect( + await decideActivityAction({ + homeDir, + locator: targetScope.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-2", + approvalReference: "linear-comment:approval-2", + approve: true, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "malformed_history" }, + }); + + const concurrent = await persistScope({ + homeDir, + scope: "project", + projectName: "decision-concurrent", + item: signalQueueItem(), + runId: "LR-decision-concurrent", + }); + const attempts = await Promise.all([ + decideActivityAction({ + homeDir, + locator: concurrent.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }), + decideActivityAction({ + homeDir, + locator: concurrent.locator, + decision: "reject", + expectedRevision: 3, + actor: "operator-2", + approvalReference: "linear-comment:approval-2", + approve: true, + }), + ]); + expect( + attempts.filter((result) => result.status === "recorded") + ).toHaveLength(1); + expect( + attempts.filter((result) => result.status === "rejected") + ).toHaveLength(1); + const concurrentHistory = await readFile( + facultAiEvolutionLoopDecisionJournalPath(homeDir, concurrent.rootDir), + "utf8" + ); + expect(concurrentHistory.trim().split("\n")).toHaveLength(1); + }); + + it("rejects non-signal resources and moved-root decision replay", async () => { + const proposalFixture = await persistScope({ + homeDir, + scope: "project", + projectName: "proposal-decision", + proposal: proposal(), + runId: "LR-proposal-decision", + }); + expect( + await decideActivityAction({ + homeDir, + locator: proposalFixture.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "not_signal_family" }, + }); + + const duplicate = await persistScope({ + homeDir, + scope: "project", + projectName: "duplicate-decision", + item: signalQueueItem(), + runId: "LR-duplicate-decision", + }); + await Bun.write( + duplicate.reportPath, + `${JSON.stringify( + { + ...duplicate.report, + queue: [duplicate.report.queue[0], duplicate.report.queue[0]], + }, + null, + 2 + )}\n` + ); + expect( + await decideActivityAction({ + homeDir, + locator: duplicate.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "duplicate_identity" }, + }); + + const unsafeIdentity = await persistScope({ + homeDir, + scope: "project", + projectName: "unsafe-identity-decision", + item: signalQueueItem({ + familyId: join(homeDir, "private-signal"), + id: "family:unsafe-identity", + }), + runId: "LR-unsafe-identity-decision", + }); + expect( + await decideActivityAction({ + homeDir, + locator: unsafeIdentity.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "invalid_decision_input" }, + }); + + const moved = await persistScope({ + homeDir, + scope: "project", + projectName: "moved-decision", + item: signalQueueItem(), + runId: "LR-moved-decision", + }); + await rename( + join(moved.rootDir, ".."), + join(homeDir, "workspaces", "moved-decision-renamed") + ); + expect( + await decideActivityAction({ + homeDir, + locator: moved.locator, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }) + ).toMatchObject({ + status: "rejected", + error: { code: "locator_not_found" }, + }); + }); + + it("exposes a closed CLI decision command and readable receipt", async () => { + const fixture = await persistScope({ + homeDir, + scope: "project", + projectName: "decision-cli", + item: signalQueueItem(), + runId: "LR-decision-cli", + }); + const output = await captureConsole(() => + aiCommand([ + "loop", + "decide", + fixture.locator, + "--decision", + "accept", + "--expected-revision", + "3", + "--actor", + "operator-1", + "--approval-ref", + "linear-comment:approval-1", + "--approve", + "--json", + ]) + ); + expect(JSON.parse(output.logs[0] ?? "{}")).toMatchObject({ + status: "recorded", + receipt: { decision: "accept", newLifecycleRevision: 1 }, + }); + expect( + renderActivityDecisionResult( + JSON.parse(output.logs[0] ?? "{}") as Awaited< + ReturnType + > + ) + ).toContain("Recorded accept decision"); + await expect( + aiCommand([ + "loop", + "decide", + fixture.locator, + "--decision", + "accept", + "--expected-revision", + "3", + "--actor", + "operator-1", + "--approval-ref", + "linear-comment:approval-1", + "--approve", + "--root", + fixture.rootDir, + ]) + ).rejects.toThrow( + "does not accept caller-supplied root or scope authority" + ); + }); + it("fails closed for moved roots, missing state, and incompatible locators", async () => { const fixture = await persistScope({ homeDir, diff --git a/src/activity-action.ts b/src/activity-action.ts index 17ea9777..92417371 100644 --- a/src/activity-action.ts +++ b/src/activity-action.ts @@ -1,10 +1,20 @@ +import { randomUUID } from "node:crypto"; import type { Dirent } from "node:fs"; -import { lstat, readdir, readFile } from "node:fs/promises"; -import { basename, join, resolve } from "node:path"; +import { + lstat, + mkdir, + open, + readdir, + readFile, + rename, + rm, +} from "node:fs/promises"; +import { basename, dirname, join, resolve } from "node:path"; import { type ActivityFeed, type ActivityItem, isActivityFeed, + redactPortableActivityText, } from "./activity"; import { type ActivityActionClass, @@ -15,9 +25,14 @@ import { parseActivityActionLocator, } from "./activity-action-contract"; import type { AiProposalRecord } from "./ai"; -import type { EvolutionLoopReport, LoopQueueItem } from "./evolution-loop"; +import { + type EvolutionLoopReport, + type LoopQueueItem, + withEvolutionLoopMutationLock, +} from "./evolution-loop"; import { facultAiEvolutionLoopConfigPath, + facultAiEvolutionLoopDecisionJournalPath, facultAiEvolutionLoopReportDir, facultAiEvolutionLoopStatePath, facultAiProposalDir, @@ -36,6 +51,17 @@ const MAX_PROJECT_SCOPE_DIRS = 1000; 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}$/; +const ACTIVITY_SCOPE_ID_PATTERN = /^(?:global|project:[a-f0-9]{16})$/; +const DECISION_ACTOR_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$/; +const DECISION_RECEIPT_ID_PATTERN = /^AD-[0-9a-f-]{36}$/; +const MAX_DECISION_APPROVAL_REFERENCE_LENGTH = 500; +const MAX_DECISION_NOTE_LENGTH = 1000; +const MAX_DECISION_REDIRECT_TARGET_LENGTH = 500; +const MAX_DECISION_RESOURCE_ID_LENGTH = 500; +const MAX_DECISION_RUN_ID_LENGTH = 500; +const MAX_DECISION_WORK_UNIT_TEXT_LENGTH = 2000; +const MAX_DECISION_JOURNAL_BYTES = 5_000_000; +const MAX_DECISION_JOURNAL_ENTRIES = 10_000; export type ActivityActionResolutionErrorCode = | "invalid_locator" @@ -88,6 +114,75 @@ export type ActivityActionResolution = }; }; +export type ActivityDecision = "accept" | "redirect" | "reject" | "defer"; + +export type ActivityDecisionErrorCode = + | ActivityActionResolutionErrorCode + | "approval_required" + | "invalid_decision_input" + | "not_signal_family" + | "replayed_decision" + | "malformed_history" + | "decision_conflict"; + +export interface ActivityDecisionWorkUnit { + targets: NonNullable["targets"]; + evidence: ActivityItem["evidence"]; + linkedWork: string[]; + expectedOutcome: string | null; + verification: ActivityItem["verification"] | null; + nextAction: string; +} + +export interface ActivityDecisionReceipt { + version: 1; + kind: "activity-decision"; + receiptId: string; + scopeId: string; + scope: "global" | "project"; + resource: { kind: "signal"; id: string }; + decision: ActivityDecision; + actor: string; + approval: { reference: string } | { note: string }; + redirectTarget?: string; + previousLifecycleRevision: number; + newLifecycleRevision: number; + activity: { + runId: string; + queueRevision: number; + bindingRevision: string; + }; + decidedAt: string; + workUnit: ActivityDecisionWorkUnit; +} + +export type ActivityDecisionResult = + | { + version: 1; + kind: "activity-decision-receipt"; + status: "recorded"; + receipt: ActivityDecisionReceipt; + workUnit: ActivityDecisionWorkUnit; + mutation: { + decisionHistoryRecorded: true; + canonicalCapabilityChanged: false; + externalSystemsChanged: false; + taskSpawned: false; + authorityGranted: false; + }; + } + | { + version: 1; + kind: "activity-decision-receipt"; + status: "rejected"; + error: { + code: ActivityDecisionErrorCode; + message: string; + recoverable: true; + next: string; + }; + }; + interface ResolvableScope { feed: ActivityFeed; report: EvolutionLoopReport; @@ -430,7 +525,7 @@ function rejected(args: { code: ActivityActionResolutionErrorCode; message: string; next: string; -}): ActivityActionResolution { +}): Extract { return { version: 1, kind: "activity-action-resolution", @@ -460,7 +555,9 @@ function planFor( 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.", + resource.kind === "signal" + ? "Record an approved signal decision only through loop decide with this locator and the exact expected queue revision; the receipt does not implement the decision." + : "Do not invoke a mutation from this plan; proposal and coverage lifecycle changes remain separate closed workflows.", ], mutation: { available: false as const, @@ -472,18 +569,26 @@ function planFor( }; } -export async function resolveActivityActionLocator(args: { +async function matchingCandidate(args: { homeDir: string; locator: string; - now?: () => Date; -}): Promise { +}): Promise< + | { status: "matched"; match: MatchingCandidate } + | { + status: "rejected"; + resolution: Extract; + } +> { 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.", - }); + return { + status: "rejected", + resolution: rejected({ + code: parsed.code, + message: parsed.message, + next: "Refresh the aggregate activity set and use a current version 1 locator.", + }), + }; } const matches: MatchingCandidate[] = []; @@ -520,41 +625,66 @@ export async function resolveActivityActionLocator(args: { } 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.", - }); + return { + status: "rejected", + resolution: 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.", - }); + return { + status: "rejected", + resolution: 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.", - }); + return { + status: "rejected", + resolution: 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 { + status: "rejected", + resolution: 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 { status: "matched", match }; +} + +export async function resolveActivityActionLocator(args: { + homeDir: string; + locator: string; + now?: () => Date; +}): Promise { + const resolved = await matchingCandidate(args); + if (resolved.status === "rejected") { + return resolved.resolution; + } + const { match } = resolved; return { version: 1, @@ -581,6 +711,660 @@ export async function resolveActivityActionLocator(args: { }; } +class DecisionJournalError extends Error { + readonly code: "malformed_history" | "decision_conflict"; + + constructor( + code: "malformed_history" | "decision_conflict", + message: string + ) { + super(message); + this.code = code; + this.name = "DecisionJournalError"; + } +} + +function decisionRejected(args: { + code: ActivityDecisionErrorCode; + message: string; + next: string; +}): ActivityDecisionResult { + return { + version: 1, + kind: "activity-decision-receipt", + status: "rejected", + error: { ...args, recoverable: true }, + }; +} + +function resolutionDecisionRejection( + resolution: Extract +): ActivityDecisionResult { + return decisionRejected(resolution.error); +} + +function isPortableDecisionText(value: string, maxLength: number): boolean { + return ( + value.length > 0 && + value.length <= maxLength && + value.trim() === value && + Array.from(value).every((character) => { + const codePoint = character.codePointAt(0) ?? 0; + return codePoint >= 32 && codePoint !== 127; + }) && + redactPortableActivityText(value) === value + ); +} + +function validateDecisionInput(args: { + decision: ActivityDecision; + expectedRevision: number; + actor: string; + approvalReference?: string; + note?: string; + redirectTarget?: string; + approve: boolean; +}): ActivityDecisionResult | null { + if (!args.approve) { + return decisionRejected({ + code: "approval_required", + message: "Recording an activity decision requires explicit approval.", + next: "Retry with approve=true only after the exact current signal decision is approved.", + }); + } + const validDecision = ["accept", "redirect", "reject", "defer"].includes( + args.decision + ); + if ( + !(validDecision && Number.isSafeInteger(args.expectedRevision)) || + args.expectedRevision < 1 || + !DECISION_ACTOR_PATTERN.test(args.actor) + ) { + return decisionRejected({ + code: "invalid_decision_input", + message: "The decision, expected revision, or actor is malformed.", + next: "Use a supported decision, a positive expected revision, and a bounded actor identifier.", + }); + } + const hasReference = args.approvalReference !== undefined; + const hasNote = args.note !== undefined; + if (hasReference === hasNote) { + return decisionRejected({ + code: "invalid_decision_input", + message: + "Provide exactly one approval reference or bounded approval note.", + next: "Retry with one portable approval source and no secret or local path content.", + }); + } + if ( + (args.approvalReference !== undefined && + !isPortableDecisionText( + args.approvalReference, + MAX_DECISION_APPROVAL_REFERENCE_LENGTH + )) || + (args.note !== undefined && + !isPortableDecisionText(args.note, MAX_DECISION_NOTE_LENGTH)) + ) { + return decisionRejected({ + code: "invalid_decision_input", + message: "The approval source is empty, unsafe, or exceeds its bound.", + next: "Use an opaque source reference or a short portable note without secrets or machine paths.", + }); + } + if ( + args.decision === "redirect" ? !args.redirectTarget : args.redirectTarget + ) { + return decisionRejected({ + code: "invalid_decision_input", + message: + "Redirect requires one bounded target; other decisions do not accept a redirect target.", + next: "Provide --redirect-target only with the redirect decision.", + }); + } + if ( + args.redirectTarget !== undefined && + !isPortableDecisionText( + args.redirectTarget, + MAX_DECISION_REDIRECT_TARGET_LENGTH + ) + ) { + return decisionRejected({ + code: "invalid_decision_input", + message: "The redirect target is unsafe or exceeds its bound.", + next: "Use one portable capability selector or opaque work reference.", + }); + } + return null; +} + +function isPortableTimestamp(value: unknown): value is string { + if (typeof value !== "string") { + return false; + } + try { + return new Date(value).toISOString() === value; + } catch { + return false; + } +} + +function isActivityDecisionWorkUnit( + value: unknown +): value is ActivityDecisionWorkUnit { + if (!(isRecord(value) && isRecord(value.evidence))) { + return false; + } + const verification = value.verification; + return ( + Array.isArray(value.targets) && + value.targets.every( + (target) => + isRecord(target) && + [ + "instruction", + "snippet", + "skill", + "agent", + "prompt", + "automation", + "mcp", + "tool", + "document", + "capability", + ].includes(String(target.kind)) && + (target.scope === "global" || + target.scope === "project" || + target.scope === "unknown") && + typeof target.selector === "string" && + isPortableDecisionText( + target.selector, + MAX_DECISION_WORK_UNIT_TEXT_LENGTH + ) && + typeof target.label === "string" && + isPortableDecisionText(target.label, MAX_DECISION_WORK_UNIT_TEXT_LENGTH) + ) && + Number.isSafeInteger(value.evidence.count) && + Number(value.evidence.count) >= 0 && + Array.isArray(value.evidence.types) && + value.evidence.types.every( + (entry) => + typeof entry === "string" && + isPortableDecisionText(entry, MAX_DECISION_WORK_UNIT_TEXT_LENGTH) + ) && + Array.isArray(value.evidence.writebackIds) && + value.evidence.writebackIds.every( + (entry) => + typeof entry === "string" && + isPortableDecisionText(entry, MAX_DECISION_WORK_UNIT_TEXT_LENGTH) + ) && + Array.isArray(value.linkedWork) && + value.linkedWork.every( + (entry) => + typeof entry === "string" && + isPortableDecisionText(entry, MAX_DECISION_WORK_UNIT_TEXT_LENGTH) + ) && + (value.expectedOutcome === null || + (typeof value.expectedOutcome === "string" && + isPortableDecisionText( + value.expectedOutcome, + MAX_DECISION_WORK_UNIT_TEXT_LENGTH + ))) && + (verification === null || + (isRecord(verification) && + [ + "unscheduled", + "pending", + "due", + "overdue", + "improved", + "unchanged", + "regressed", + "inconclusive", + ].includes(String(verification.state)) && + Number.isSafeInteger(verification.attempts) && + Number(verification.attempts) >= 0 && + (verification.opensAt === undefined || + isPortableTimestamp(verification.opensAt)) && + (verification.dueAt === undefined || + isPortableTimestamp(verification.dueAt)) && + (verification.overdueAt === undefined || + isPortableTimestamp(verification.overdueAt)))) && + typeof value.nextAction === "string" && + isPortableDecisionText(value.nextAction, MAX_DECISION_WORK_UNIT_TEXT_LENGTH) + ); +} + +function isActivityDecisionReceipt( + value: unknown +): value is ActivityDecisionReceipt { + if ( + !(isRecord(value) && isRecord(value.resource) && isRecord(value.activity)) + ) { + return false; + } + const approval = value.approval; + const workUnit = value.workUnit; + return ( + value.version === 1 && + value.kind === "activity-decision" && + typeof value.receiptId === "string" && + DECISION_RECEIPT_ID_PATTERN.test(value.receiptId) && + typeof value.scopeId === "string" && + ACTIVITY_SCOPE_ID_PATTERN.test(value.scopeId) && + (value.scope === "global" || value.scope === "project") && + (value.scope === "global" + ? value.scopeId === "global" + : value.scopeId.startsWith("project:")) && + value.resource.kind === "signal" && + typeof value.resource.id === "string" && + isPortableDecisionText( + value.resource.id, + MAX_DECISION_RESOURCE_ID_LENGTH + ) && + ["accept", "redirect", "reject", "defer"].includes( + String(value.decision) + ) && + typeof value.actor === "string" && + DECISION_ACTOR_PATTERN.test(value.actor) && + isRecord(approval) && + ((typeof approval.reference === "string" && + isPortableDecisionText( + approval.reference, + MAX_DECISION_APPROVAL_REFERENCE_LENGTH + ) && + approval.note === undefined) || + (typeof approval.note === "string" && + isPortableDecisionText(approval.note, MAX_DECISION_NOTE_LENGTH) && + approval.reference === undefined)) && + (value.decision === "redirect" + ? typeof value.redirectTarget === "string" && + isPortableDecisionText( + value.redirectTarget, + MAX_DECISION_REDIRECT_TARGET_LENGTH + ) + : value.redirectTarget === undefined) && + Number.isSafeInteger(value.previousLifecycleRevision) && + Number(value.previousLifecycleRevision) >= 0 && + Number.isSafeInteger(value.newLifecycleRevision) && + Number(value.newLifecycleRevision) === + Number(value.previousLifecycleRevision) + 1 && + typeof value.activity.runId === "string" && + isPortableDecisionText(value.activity.runId, MAX_DECISION_RUN_ID_LENGTH) && + Number.isSafeInteger(value.activity.queueRevision) && + Number(value.activity.queueRevision) > 0 && + typeof value.activity.bindingRevision === "string" && + ROOT_IDENTITY_PATTERN.test(value.activity.bindingRevision) && + isPortableTimestamp(value.decidedAt) && + isActivityDecisionWorkUnit(workUnit) + ); +} + +async function readDecisionJournal(pathValue: string): Promise<{ + body: string; + entries: ActivityDecisionReceipt[]; +}> { + let body: string; + try { + const info = await lstat(pathValue); + if ( + !info.isFile() || + info.isSymbolicLink() || + info.size > MAX_DECISION_JOURNAL_BYTES + ) { + throw new DecisionJournalError( + "malformed_history", + "The decision journal is not a bounded regular file." + ); + } + body = await readFile(pathValue, "utf8"); + } catch (error) { + if ( + error instanceof Error && + "code" in error && + (error as NodeJS.ErrnoException).code === "ENOENT" + ) { + return { body: "", entries: [] }; + } + throw error; + } + const lines = body.split("\n").filter((line) => line.length > 0); + if (lines.length > MAX_DECISION_JOURNAL_ENTRIES) { + throw new DecisionJournalError( + "malformed_history", + "The decision journal exceeds its entry bound." + ); + } + const entries: ActivityDecisionReceipt[] = []; + for (const line of lines) { + let parsed: unknown; + try { + parsed = JSON.parse(line) as unknown; + } catch { + throw new DecisionJournalError( + "malformed_history", + "The decision journal contains malformed JSON." + ); + } + if (!isActivityDecisionReceipt(parsed)) { + throw new DecisionJournalError( + "malformed_history", + "The decision journal contains an incompatible receipt." + ); + } + entries.push(parsed); + } + return { body, entries }; +} + +async function replaceDecisionJournal(args: { + path: string; + expectedBody: string; + receipt: ActivityDecisionReceipt; +}): Promise { + const nextBody = `${args.expectedBody}${ + args.expectedBody && !args.expectedBody.endsWith("\n") ? "\n" : "" + }${JSON.stringify(args.receipt)}\n`; + if (Buffer.byteLength(nextBody) > MAX_DECISION_JOURNAL_BYTES) { + throw new DecisionJournalError( + "malformed_history", + "The decision journal reached its size bound." + ); + } + await mkdir(dirname(args.path), { recursive: true, mode: 0o700 }); + const temporaryPath = `${args.path}.tmp-${process.pid}-${randomUUID()}`; + let committed = false; + try { + const handle = await open(temporaryPath, "wx", 0o600); + try { + await handle.writeFile(nextBody, "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + const current = await readDecisionJournal(args.path); + if (current.body !== args.expectedBody) { + throw new DecisionJournalError( + "decision_conflict", + "The decision journal changed before commit." + ); + } + await rename(temporaryPath, args.path); + committed = true; + } finally { + if (!committed) { + await rm(temporaryPath, { force: true }); + } + } +} + +function decisionWorkUnit(item: ActivityItem): ActivityDecisionWorkUnit { + const desiredOutcome = item.observations.find( + (entry) => typeof entry.desiredOutcome === "string" + )?.desiredOutcome; + return { + targets: (item.context?.targets ?? []).map((target) => ({ + kind: target.kind, + scope: target.scope, + selector: redactPortableActivityText(target.selector), + label: redactPortableActivityText(target.label), + })), + evidence: { + count: item.evidence.count, + types: item.evidence.types.map(redactPortableActivityText), + writebackIds: item.evidence.writebackIds.map(redactPortableActivityText), + }, + linkedWork: item.linkedWork.map(redactPortableActivityText), + expectedOutcome: + typeof desiredOutcome === "string" + ? redactPortableActivityText(desiredOutcome) + : null, + verification: item.verification ?? null, + nextAction: redactPortableActivityText(item.nextAction), + }; +} + +export async function decideActivityAction(args: { + homeDir: string; + locator: string; + decision: ActivityDecision; + expectedRevision: number; + actor: string; + approvalReference?: string; + note?: string; + redirectTarget?: string; + approve: boolean; + now?: () => Date; +}): Promise { + const inputError = validateDecisionInput(args); + if (inputError) { + return inputError; + } + const initial = await matchingCandidate(args); + if (initial.status === "rejected") { + return resolutionDecisionRejection(initial.resolution); + } + if ( + initial.match.item.kind !== "signal" || + initial.match.candidate.resourceKind !== "signal" + ) { + return decisionRejected({ + code: "not_signal_family", + message: "Activity decisions are limited to signal-family items.", + next: "Use the existing proposal lifecycle for proposals and refresh coverage separately.", + }); + } + const decidedAt = (args.now?.() ?? new Date()).toISOString(); + try { + return await withEvolutionLoopMutationLock({ + homeDir: args.homeDir, + rootDir: initial.match.scope.rootDir, + now: new Date(decidedAt), + fn: async () => { + const current = await matchingCandidate(args); + if (current.status === "rejected") { + return resolutionDecisionRejection(current.resolution); + } + const { match } = current; + if ( + match.item.kind !== "signal" || + match.candidate.resourceKind !== "signal" + ) { + return decisionRejected({ + code: "not_signal_family", + message: "Activity decisions are limited to signal-family items.", + next: "Refresh activity and use a current signal-family locator.", + }); + } + if (match.candidate.queueRevision !== args.expectedRevision) { + return decisionRejected({ + code: "stale_revision", + message: "The expected activity revision is no longer current.", + next: "Refresh activity and retry with its locator and exact queue revision.", + }); + } + if (!match.issuedItem) { + return decisionRejected({ + code: "locator_not_issued", + message: + "The current activity snapshot did not issue this locator.", + next: "Refresh activity and use the locator it returns.", + }); + } + if ( + !( + isPortableDecisionText( + match.candidate.resourceId, + MAX_DECISION_RESOURCE_ID_LENGTH + ) && + isPortableDecisionText( + match.scope.report.runId, + MAX_DECISION_RUN_ID_LENGTH + ) + ) + ) { + return decisionRejected({ + code: "invalid_decision_input", + message: + "The current signal binding contains a non-portable resource or run identifier.", + next: "Refresh or repair the current machine-local activity state before recording a decision.", + }); + } + const journalPath = facultAiEvolutionLoopDecisionJournalPath( + args.homeDir, + match.scope.rootDir + ); + const journal = await readDecisionJournal(journalPath); + if ( + journal.entries.some( + (entry) => + entry.scopeId !== match.scope.scopeBinding.scopeId || + entry.scope !== match.scope.scopeBinding.scope + ) + ) { + throw new DecisionJournalError( + "malformed_history", + "The decision journal contains a receipt for another scope." + ); + } + const resourceEntries = journal.entries.filter( + (entry) => + entry.scopeId === match.scope.scopeBinding.scopeId && + entry.resource.id === match.candidate.resourceId + ); + let previousLifecycleRevision = 0; + let priorQueueRevision = 0; + for (const entry of resourceEntries) { + if ( + entry.previousLifecycleRevision !== previousLifecycleRevision || + entry.newLifecycleRevision !== previousLifecycleRevision + 1 || + entry.activity.queueRevision <= priorQueueRevision + ) { + throw new DecisionJournalError( + "malformed_history", + "The signal decision lifecycle history is not monotonic." + ); + } + previousLifecycleRevision = entry.newLifecycleRevision; + priorQueueRevision = entry.activity.queueRevision; + } + if ( + resourceEntries.some( + (entry) => + entry.activity.bindingRevision === + match.candidate.bindingDigest || + (entry.activity.runId === match.scope.report.runId && + entry.activity.queueRevision === match.candidate.queueRevision) + ) + ) { + return decisionRejected({ + code: "replayed_decision", + message: + "This exact signal activity revision already has a recorded decision.", + next: "Read the existing receipt or wait for a genuinely changed signal revision before deciding again.", + }); + } + if (match.candidate.queueRevision <= priorQueueRevision) { + return decisionRejected({ + code: "replayed_decision", + message: + "The signal revision does not advance the recorded decision lifecycle.", + next: "Refresh activity and decide only a newer signal revision.", + }); + } + const workUnit = decisionWorkUnit(match.issuedItem); + if (!isActivityDecisionWorkUnit(workUnit)) { + return decisionRejected({ + code: "invalid_decision_input", + message: + "The current activity item does not contain bounded portable work-unit context.", + next: "Refresh or repair the current activity state before recording a decision.", + }); + } + const receipt: ActivityDecisionReceipt = { + version: 1, + kind: "activity-decision", + receiptId: `AD-${randomUUID()}`, + scopeId: match.scope.scopeBinding.scopeId, + scope: match.scope.scopeBinding.scope, + resource: { kind: "signal", id: match.candidate.resourceId }, + decision: args.decision, + actor: args.actor, + approval: + args.approvalReference !== undefined + ? { reference: args.approvalReference } + : { note: args.note! }, + ...(args.redirectTarget + ? { redirectTarget: args.redirectTarget } + : {}), + previousLifecycleRevision, + newLifecycleRevision: previousLifecycleRevision + 1, + activity: { + runId: match.scope.report.runId, + queueRevision: match.candidate.queueRevision, + bindingRevision: match.candidate.bindingDigest, + }, + decidedAt, + workUnit, + }; + await replaceDecisionJournal({ + path: journalPath, + expectedBody: journal.body, + receipt, + }); + return { + version: 1, + kind: "activity-decision-receipt", + status: "recorded", + receipt, + workUnit, + mutation: { + decisionHistoryRecorded: true, + canonicalCapabilityChanged: false, + externalSystemsChanged: false, + taskSpawned: false, + authorityGranted: false, + }, + }; + }, + }); + } catch (error) { + if (error instanceof DecisionJournalError) { + return decisionRejected({ + code: error.code, + message: error.message, + next: + error.code === "malformed_history" + ? "Inspect and repair the bounded machine-local decision journal before retrying." + : "Refresh activity after the competing operation completes.", + }); + } + return decisionRejected({ + code: "decision_conflict", + message: + "The signal decision could not acquire or commit under the evolution-loop lock.", + next: "Retry after the current loop operation completes, then refresh the locator and revision.", + }); + } +} + +export function renderActivityDecisionResult( + result: ActivityDecisionResult +): string { + if (result.status === "rejected") { + return [ + `Activity decision rejected: ${result.error.message}`, + `Next: ${result.error.next}`, + ].join("\n"); + } + return [ + `Recorded ${result.receipt.decision} decision for signal ${result.receipt.resource.id}`, + `Scope: ${result.receipt.scopeId}`, + `Lifecycle revision: ${result.receipt.previousLifecycleRevision} -> ${result.receipt.newLifecycleRevision}`, + `Next: ${result.workUnit.nextAction}`, + "No canonical capability, external system, or task was changed.", + ].join("\n"); +} + export function renderActivityActionResolution( resolution: ActivityActionResolution ): string { diff --git a/src/ai.ts b/src/ai.ts index 23cff63a..2cb3949b 100644 --- a/src/ai.ts +++ b/src/ai.ts @@ -2810,7 +2810,7 @@ Usage: fclt ai writeback [args...] fclt ai evolve [args...] fclt ai review [args...] - fclt ai loop [args...] + fclt ai loop [args...] `; } @@ -2824,6 +2824,7 @@ Usage: fclt ai loop report [--json] fclt ai loop activity [--all|--global|--project] [--json] fclt ai loop resolve [--json] + fclt ai loop decide --decision --expected-revision --actor (--approval-ref |--note ) [--redirect-target ] --approve [--json] fclt ai loop history [--all|--global|--project] [--since ] [--until ] [--item ] [--scope-id ] [--event ] [--limit <1-200>] [--cursor ] [--json] fclt ai loop run [--since ] [--until ] [--source ] [--dry-run] [--scheduled] [--json] @@ -2990,6 +2991,100 @@ async function loopCommand(argv: string[]) { } return; } + if (sub === "decide") { + if (parsed.rootArg || parsed.scope !== "merged") { + throw new Error( + "Activity decision recording does not accept caller-supplied root or scope authority" + ); + } + const valueFlags = new Set([ + "--decision", + "--expected-revision", + "--actor", + "--approval-ref", + "--note", + "--redirect-target", + ]); + const values = new Map(); + const booleans = new Set(); + const positional: string[] = []; + for (let index = 0; index < commandArgs.length; index += 1) { + const argument = commandArgs[index]; + if (!argument) { + continue; + } + if (argument === "--json" || argument === "--approve") { + if (booleans.has(argument)) { + throw new Error(`loop decide received duplicate ${argument}`); + } + booleans.add(argument); + continue; + } + const equalsIndex = argument.indexOf("="); + const flag = equalsIndex > 0 ? argument.slice(0, equalsIndex) : argument; + if (valueFlags.has(flag)) { + if (values.has(flag)) { + throw new Error(`loop decide received duplicate ${flag}`); + } + const value = + equalsIndex > 0 + ? argument.slice(equalsIndex + 1) + : commandArgs[++index]; + if (!value) { + throw new Error(`${flag} requires a value`); + } + values.set(flag, value); + continue; + } + if (argument.startsWith("-")) { + throw new Error(`loop decide received unsupported option ${argument}`); + } + positional.push(argument); + } + if (positional.length !== 1) { + throw new Error( + "loop decide accepts exactly one opaque locator and closed decision fields" + ); + } + const decision = values.get("--decision"); + const expectedRevision = Number(values.get("--expected-revision")); + const actor = values.get("--actor"); + if ( + !( + decision && + ["accept", "redirect", "reject", "defer"].includes(decision) && + Number.isSafeInteger(expectedRevision) && + actor + ) + ) { + throw new Error( + "loop decide requires --decision, --expected-revision, and --actor" + ); + } + const { decideActivityAction, renderActivityDecisionResult } = await import( + "./activity-action" + ); + const result = await decideActivityAction({ + homeDir: process.env.HOME ?? "", + locator: positional[0]!, + decision: decision as "accept" | "redirect" | "reject" | "defer", + expectedRevision, + actor, + approvalReference: values.get("--approval-ref"), + note: values.get("--note"), + redirectTarget: values.get("--redirect-target"), + approve: booleans.has("--approve"), + }); + console.log( + booleans.has("--json") + ? JSON.stringify(result, null, 2) + : renderActivityDecisionResult(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 95fa85e0..58604470 100644 --- a/src/builtin-assets.ts +++ b/src/builtin-assets.ts @@ -2,12 +2,12 @@ export const BUILTIN_OPERATING_MODEL_FILES = JSON.parse( // biome-ignore lint/suspicious/noTemplateCurlyInString: Built-in templates intentionally contain literal render placeholders. - '{"agents/evolution-planner/agent.toml":"name = \\"evolution-planner\\"\\ndescription = \\"Turn repeated writeback into concrete capability proposals.\\"\\n\\ndeveloper_instructions = \\"\\"\\"\\nYou plan capability evolution.\\n\\nPrioritize:\\n- smallest useful change\\n- correct target asset type\\n- correct target scope\\n- evidence that justifies the change\\n- repeated writeback clusters or clearly missing capabilities, not isolated preferences\\n\\nProposal kinds you should consider first:\\n- update_asset\\n- create_asset\\n- extract_snippet\\n- add_skill\\n- promote_asset\\n\\nDefault to project scope when the pattern is repo-local.\\nPromote to global only when reuse is demonstrated and pollution risk is low.\\n\\nReturn concise proposals ordered by expected leverage, including:\\n- proposal kind\\n- target asset\\n- target scope\\n- why this is the smallest durable change\\n- source writeback ids or evidence summary\\n- approval risk and verification path\\n\\nDo not escalate to evolution when a single writeback is enough.\\nDo not use evolution as a substitute for executable task tracking when the main need is owner, priority, state, or implementation follow-through.\\nDo not globalize private, repo-specific, or speculative guidance.\\n\\"\\"\\"\\n","agents/integration-auditor/agent.toml":"name = \\"integration-auditor\\"\\ndescription = \\"Find where local success can still fail system-wide.\\"\\n\\ndeveloper_instructions = \\"\\"\\"\\nYou audit integration risk.\\n\\nPrioritize:\\n- hidden dependencies\\n- rollout hazards\\n- operational constraints\\n- gaps between local verification and real system behavior\\n- packaged, installed, rendered, or synced paths that differ from source behavior\\n- parallel execution and state-location risks\\n- privacy boundaries between global, project, generated, and machine-local state\\n\\nReturn concise findings ordered by impact. For each finding include:\\n- boundary at risk\\n- why the current evidence is or is not enough\\n- strongest next verification step\\n- whether the fix belongs in code, docs, a task, or capability evolution\\n\\"\\"\\"\\n","agents/scope-promoter/agent.toml":"name = \\"scope-promoter\\"\\ndescription = \\"Decide whether learning belongs at project or global scope.\\"\\n\\ndeveloper_instructions = \\"\\"\\"\\nYou decide scope.\\n\\nPrioritize:\\n- project specificity\\n- cross-project reuse potential\\n- pollution risk from globalizing too early\\n- private or repo-specific details that must not move into global capability\\n- whether a smaller snippet, instruction, skill, or agent should be promoted instead of a broad doc\\n\\nWhen recommending promotion, make the standard path explicit:\\n- keep the source capability in project scope until promotion is approved\\n- create a reviewable global proposal\\n- do not treat promotion as implicit apply\\n\\nReturn concise decisions with:\\n- recommended scope\\n- target asset or smallest unit\\n- evidence for reuse\\n- privacy/pollution risk\\n- promotion path or no-op rationale\\n\\"\\"\\"\\n","agents/writeback-curator/agent.toml":"name = \\"writeback-curator\\"\\ndescription = \\"Turn noisy outcomes into high-signal writeback.\\"\\n\\ndeveloper_instructions = \\"\\"\\"\\nYou curate durable writeback.\\n\\nPrioritize:\\n- repeated failures\\n- repeated wins\\n- stale guidance\\n- missing capability edges\\n- tool, skill, MCP, plugin, automation, or instruction friction that repeatedly slows work down\\n\\nFor each recommendation, prefer returning:\\n- suggested writeback kind\\n- category: friction, opportunity, or reusable-success\\n- best target asset or destination\\n- best scope (`project` or `global`)\\n- the evidence that justifies recording it\\n- concise context, impact, attempted workaround, desired outcome, and sensitivity when known\\n- whether the signal is enough for writeback only, task tracking, or evolution\\n\\nDo not emit low-signal noise.\\nIf the learning is repo-specific, keep it project-scoped by default.\\nWhen the signal is already strong and the target is clear, prefer recommending direct writeback capture rather than abstract advice.\\nWhen the issue is executable tooling work, recommend task tracking for the fix and writeback only for the reusable operating-model learning.\\nWhen the issue contains private project details, preserve the general learning without copying private details into a global asset.\\nNever request or emit hidden chain-of-thought, raw transcripts, unbounded logs, secrets, tokens, or credential-bearing payloads. Use redacted evidence references.\\n\\"\\"\\"\\n","instructions/CAPABILITY_COMPOSITION.md":"---\\ndescription: \\"Compose small capability units across global and project roots, then evolve the smallest affected unit.\\"\\ntags: [\\"facult\\", \\"composition\\", \\"refs\\", \\"snippets\\", \\"instructions\\"]\\n---\\n\\n# Capability Composition\\n\\nUse `fclt` capability as small units that can be composed, inspected, rendered, and evolved independently.\\n\\nThe main units are:\\n\\n- instructions: standalone markdown doctrine such as language preferences, verification rules, or review standards\\n- snippets: small markdown partials inserted into one or more rendered docs\\n- skills: task-specific workflows with `SKILL.md`\\n- agents: focused role manifests\\n- MCP definitions: tool interfaces and their safe auth shape\\n- automations: scheduled review or maintenance loops\\n- tool rules/config: tool-specific defaults and policy\\n\\n## Composition Rules\\n\\n- Keep reusable doctrine in `instructions/`.\\n- Keep repeated paragraphs or policy blocks in `snippets/`.\\n- Keep workflow execution in `skills/`.\\n- Keep persona or delegation behavior in `agents/`.\\n- Keep tool wiring in `mcp/` and `tools//`.\\n- Compose broad agent docs from refs and snippets instead of copying text by hand.\\n- Prefer one narrow reusable unit over one large instruction file that mixes unrelated domains.\\n\\nExamples:\\n\\n- `@ai/instructions/LANGUAGE.md` for a user-owned language/tooling preference.\\n- `@ai/instructions/REVIEW.md` for a user-owned review standard.\\n- `@project/instructions/TESTING.md` for repo-specific test policy.\\n- `` for a shared rendered block.\\n\\n## Scope\\n\\nUse global scope for capability that should follow the user across projects.\\n\\nUse project scope for capability that belongs to a repo, team workflow, architecture, or local test harness.\\n\\nPromote project capability to global only when repeated evidence shows reuse across projects. Do not globalize a project quirk just because it worked once.\\n\\n## Writeback and Evolution\\n\\nTarget the smallest affected unit.\\n\\n- If a paragraph is reused in several rendered docs, target the snippet.\\n- If a domain rule is wrong, target the instruction.\\n- If a workflow is incomplete, target the skill.\\n- If a delegated role is unclear, target the agent.\\n- If a tool interface is missing or unsafe, target the MCP or tool config.\\n- If a scheduled review loop is noisy or missing context, target the automation.\\n\\nGood writeback targets are graph-backed selectors when possible:\\n\\n```bash\\nfclt ai writeback add --kind missing_context --summary \\"Language guidance did not cover test runner selection.\\" --asset instruction:LANGUAGE\\nfclt ai writeback add --kind reusable_pattern --summary \\"Project test policy should become a shared verification snippet.\\" --asset @project/instructions/TESTING.md\\nfclt ai writeback add --kind bad_default --summary \\"The review automation escalated one-off preferences.\\" --asset automation:evolution-review\\n```\\n\\nUse `fclt ai evolve ...` only after repeated signal, a clearly missing capability, or a stale canonical asset points at a concrete change. Prefer the smallest valid proposal kind: `update_asset`, `create_asset`, `extract_snippet`, `add_skill`, or `promote_asset`.\\n\\n## Agent Defaults\\n\\nWhen an agent sees a repeated language, framework, or test preference, it should not bury that in chat. It should identify whether the durable unit is:\\n\\n- a global instruction\\n- a project instruction\\n- a snippet reused by rendered docs\\n- a skill workflow\\n- a project-to-global promotion candidate\\n\\nThen it should record writeback against that unit, or draft a proposal when the evidence is already strong enough.\\n","instructions/EVOLUTION.md":"---\\ndescription: Turn repeated signal into concrete capability changes.\\ntags: [facult, evolution, writeback]\\n---\\n\\n# Evolution\\n\\nUse writeback and evolution to improve the AI operating layer itself.\\n\\nEvolution is the synthesis and change side of the feedback loop. It turns accumulated writebacks, repeated tool friction, stale canonical assets, or clearly missing capability into small reviewable changes to instructions, skills, snippets, agents, or other markdown canonical assets.\\n\\nUse capability composition when choosing the target. Instructions, snippets, skills, agents, MCP/tool config, and automations are separate units. Target the smallest unit that actually needs to change instead of rewriting a broad agent doc.\\n\\n## When To Record Writeback\\n\\nRecord writeback when one of these is true:\\n\\n- the same failure repeats\\n- the same success pattern repeats\\n- guidance is stale or missing\\n- a prompt or loop has to be restated often\\n- a project-specific pattern looks reusable\\n\\nDo not record low-signal noise:\\n\\n- one-off annoyance with no reuse value\\n- generic \\"could be better\\" commentary\\n- duplicate observations with no new evidence\\n\\nThe intended default is that agents record strong writebacks themselves when the signal is clear enough, rather than only recommending that a user do it manually later.\\n\\nDo not wait for a weekly review to preserve high-signal evidence. Do wait for repeated evidence or a clearly missing capability before drafting a proposal.\\n\\n## Scope\\n\\nChoose `project` scope when the learning depends on:\\n\\n- repo architecture\\n- team workflow\\n- project tooling\\n- local testing or verification behavior\\n\\nChoose `global` scope when the learning is reusable across projects.\\n\\nPromote from project to global only after repeated reuse or strong evidence.\\n\\n## Writeback Kinds\\n\\nCommon kinds:\\n\\n- `weak_verification`\\n- `false_positive`\\n- `missing_context`\\n- `reusable_pattern`\\n- `capability_gap`\\n- `bad_default`\\n\\nEvery good writeback should try to include:\\n\\n- a concrete summary\\n- the best target asset if known\\n- the right scope\\n- domain or tags when useful\\n\\nGood target examples:\\n\\n- `instruction:LANGUAGE` when shared language/tooling guidance is stale or missing\\n- `@project/instructions/TESTING.md` when repo test policy needs project-scoped evolution\\n- `snippet:global/policy/review` when a repeated rendered block should be fixed or extracted\\n- `skill:capability-evolution` when a workflow skill is missing steps or examples\\n- `automation:evolution-review` when the scheduled review loop is noisy or incomplete\\n\\n## Operator Flow\\n\\nTypical workflow:\\n\\n```bash\\nfclt ai writeback add --kind weak_verification --summary \\"Checks were too shallow\\" --asset instruction:VERIFICATION\\nfclt ai writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai evolve assess --asset instruction:VERIFICATION --json\\nfclt ai evolve propose\\nfclt ai evolve draft EV-00001\\nfclt ai evolve accept EV-00001\\nfclt ai evolve apply EV-00001\\n```\\n\\nUse `fclt ai evolve draft --append \\"...\\"` to revise a draft while preserving draft history.\\n\\nReview surfaces:\\n\\n- open `~/.ai/writebacks/` and `~/.ai/evolution/` in a Markdown editor for frontmatter-rich global and project-scoped review artifacts\\n- `fclt status --json` for queue/proposal paths, review artifact paths, counts, and active scope\\n- `fclt ai writeback list|show|group|summarize` for raw and clustered signal\\n- `fclt ai evolve assess` for read-only proposal readiness and the safest next action\\n- `fclt ai evolve list|show|review` for proposal state without applying changes\\n- `fclt templates init automation learning-review` for recurring capture/review\\n- `fclt templates init automation evolution-review` for recurring proposal review\\n- `fclt templates init automation tool-call-audit` for repeated tool-friction review\\n\\nAn operator may explicitly enable the coordinated closed loop with `fclt ai\\nloop enable --project` or `--global`. Use `fclt ai loop status --json` to check\\nboth scheduler registration and observed successful execution. The durable\\nqueue must retain unchanged and temporarily unobserved items; only the\\nnotification delta should suppress noise.\\n\\nEvolution proposal metadata, markdown drafts, patch artifacts, writeback queues,\\nand journals are runtime state. `fclt` stores JSON queues, proposal records,\\ndraft refs, patches, and journals in machine-local `fclt` state. It mirrors\\nhuman-readable review artifacts into global `~/.ai/writebacks/...` and\\n`~/.ai/evolution/...`, including project-scoped artifacts under\\n`projects//` with cwd/project metadata in frontmatter. Canonical\\nassets in `~/.ai` or `/.ai` should only change when a proposal is applied.\\n\\n## Default Agent Behavior\\n\\nUse the smallest action that fits the signal:\\n\\n1. record one strong writeback when there is a clear durable learning\\n2. use `writeback-curator` when the target, kind, or scope is ambiguous\\n3. run `fclt ai evolve assess --asset --json` before proposing when a target is known\\n4. use `capability-evolution` or `evolution-planner` when repeated signal should become a proposal\\n5. do not draft or apply proposals just because a writeback exists; require repeated evidence or a clearly missing capability\\n\\nWhen assessment recommends no mutation or more writeback, agents should still produce a useful review: state the current target, evidence grade, missing signal, exact recurrence that would justify evolution, and any read-only follow-up. Do not end with only \\"no proposal\\".\\n\\nAvoid creating writeback/evolution noise for one-off nits, vague preferences, or speculative ideas without evidence.\\n\\nWhen the friction is executable product/tooling work that needs ownership,\\npriority, state, or implementation follow-through, create or update a real task\\nsystem item instead of forcing it into capability evolution. Use evolution for\\nthe reusable operating-layer change.\\n\\n## Proposal Kinds\\n\\nCurrent supported proposal kinds:\\n\\n- `update_asset`\\n- `create_asset`\\n- `extract_snippet`\\n- `add_skill`\\n- `promote_asset`\\n\\nUse the smallest durable change that fits the evidence.\\n\\nExamples:\\n\\n- `update_asset`: fix a stale instruction, snippet, agent, or automation markdown asset.\\n- `create_asset`: add a missing instruction such as `LANGUAGE.md` or `REVIEW.md`.\\n- `extract_snippet`: move repeated guidance out of several docs into one snippet.\\n- `add_skill`: create a workflow when instructions are not enough.\\n- `promote_asset`: move a proven project instruction/snippet/skill toward global reuse.\\n\\n## Review And Apply Rules\\n\\n- draft before apply\\n- accept before apply\\n- prefer the smallest safe change\\n- keep reviewable evidence tied to source writebacks\\n- do not globalize project behavior too early\\n- do not apply high-risk global instruction, skill, plugin, or shared-tool changes without explicit review/approval\\n\\nApply is for markdown canonical assets only. If the target is wrong, revise the proposal rather than forcing it through.\\n\\nScheduled review does not weaken the approval boundary. It may reconcile\\nread-only sources, record targeted writebacks, draft proposals, and report a\\nvendor-neutral request to reopen linked implementation work. It must not\\nmutate an external tracker or automatically apply canonical changes. Treat\\nproject auto-apply as plan-only until a hash-bound transaction, validation,\\nrollback, and durable receipt exist; keep global and plugin changes\\nproposal-only.\\n","instructions/INTEGRATION.md":"---\\ndescription: Detect where local success can still fail at integration boundaries.\\ntags: [facult, integration, verification]\\n---\\n\\n# Integration\\n\\nDistinguish local correctness from system correctness. Check hidden dependencies, rollout order, and operational constraints before calling work done.\\n\\n## When To Use\\n\\nUse this when a local green signal may still fail at a boundary:\\n\\n- code passes focused tests but has not been checked against the real workflow\\n- docs are correct in isolation but may send agents to a stale command or path\\n- a tool command works locally but may fail under packaged, sandboxed, or parallel execution\\n- a capability change renders into one agent tool but not another\\n- a project-local improvement may collide with global defaults or managed output\\n- a migration, release, or rollout has ordering constraints\\n\\n## Integration Questions\\n\\nAsk the smallest set that matches the risk:\\n\\n- What consumes this output?\\n- What state does this depend on?\\n- What happens if two agents or commands run this at the same time?\\n- Does the packaged/released path behave like the source checkout?\\n- Does the project-scoped path avoid leaking into global or public surfaces?\\n- Does the global path avoid overwriting tool-native or user-edited state?\\n- Is rollback or recovery clear if the integration fails?\\n\\n## Evidence\\n\\nPrefer evidence that crosses the boundary that could fail:\\n\\n- run the installed CLI, packaged binary, or generated artifact when source tests are not enough\\n- inspect rendered output when changing snippets, refs, or agent docs\\n- use temp roots and clean homes for setup, upgrade, and sync behavior\\n- verify review artifacts land in global `~/.ai/writebacks` or `~/.ai/evolution`, not repo-local private state\\n- check release, package, or plugin surfaces when the change affects users outside the repo\\n\\n## Output\\n\\nReturn concise findings ordered by risk:\\n\\n- boundary checked\\n- evidence used\\n- remaining assumption\\n- fix or follow-up if local correctness does not prove system correctness\\n\\nRecord writeback when the same integration boundary repeatedly fails, the verification loop is too weak, or a missing skill/tool would make the boundary easier to check next time.\\n","instructions/LEARNING_AND_WRITEBACK.md":"---\\ndescription: Preserve durable signal and record writeback when the operating layer should learn.\\ntags: [facult, learning, writeback]\\n---\\n\\n# Learning And Writeback\\n\\nUse this when work produces a durable decision, failure, success pattern, or missing guardrail that should outlive the current task.\\n\\nThis is the capture side of the feedback loop. The goal is to let normal agent work produce reusable signal without requiring a human to manually restate every friction point later.\\n\\n## Default Behavior\\n\\nThe normal path should be agent-driven.\\n\\nIf you can clearly answer:\\n\\n- what was learned\\n- why it matters\\n- where it should land\\n- whether it belongs in `project` or `global`\\n\\nthen record the writeback instead of only suggesting that someone should do it later.\\n\\nUse:\\n\\n```bash\\nfclt ai writeback add \\\\\\n --kind \\\\\\n --category \\\\\\n --summary \\"\\" \\\\\\n --details \\"\\" \\\\\\n --impact \\"\\" \\\\\\n --attempted-workaround \\"\\" \\\\\\n --desired-outcome \\"\\" \\\\\\n --sensitivity \\\\\\n --evidence \\\\\\n --asset \\n```\\n\\nKeep the capture concise and factual. Preserve decision rationale as a short\\nexplanation of the observed evidence and selected disposition. Never record\\nhidden chain-of-thought, raw transcripts, unbounded logs, secrets, tokens, or\\ncredential-bearing payloads. Store logs in their existing system and reference\\nthe smallest redacted evidence identifier that lets a reviewer retrieve them.\\n\\nSensitivity controls supplemental context in portable review surfaces:\\n\\n- `public`: generic context that is safe to show broadly\\n- `internal`: context for the exact local/project review surface; this is the default\\n- `private`: keep the actionable summary, but omit supplemental context from portable artifacts and tools\\n\\nSensitivity is not a secret store. Secret-shaped values are redacted at the\\nwriteback boundary regardless of the selected sensitivity.\\n\\nThe writeback queue is runtime state, not canonical source. `fclt` stores JSON\\nqueue state in machine-local `fclt` state so sandboxed agents can record durable\\nfriction without mutating canonical assets unless an evolution proposal is later\\nreviewed and applied.\\n\\nEvery writeback also refreshes a Markdown review artifact under the global\\n`~/.ai/writebacks/...` tree. Global signal lands in `~/.ai/writebacks/global/`;\\nproject-scoped signal lands in `~/.ai/writebacks/projects//` with\\nfrontmatter for scope, project root, cwd, target asset, status, tags, evidence,\\nand timestamps. Do not write writeback review artifacts into a repo-local `.ai`;\\nrepo-local state should contribute project metadata and evidence, not bundled\\nprivate review files.\\n\\nProject-scoped writebacks should usually be recorded from the repo that produced\\nthe evidence. Global writebacks should be reserved for shared doctrine, shared\\nskills, shared agents, tool behavior, or cross-project capability gaps.\\n\\nTarget the smallest composable unit that explains the friction:\\n\\n- instruction: domain guidance, preferences, verification rules, or review doctrine\\n- snippet: repeated markdown block used by more than one rendered doc\\n- skill: workflow execution steps or examples\\n- agent: delegated role behavior\\n- MCP/tool config: tool interface, auth shape, or rendered integration\\n- automation: scheduled review loop, cadence, prompt, or memory\\n\\n## Record Writeback When\\n\\n- the same failure or weak loop appears again\\n- a reusable success pattern shows up\\n- guidance is clearly stale or missing\\n- a repo-local behavior probably belongs in project capability\\n- a cross-project behavior probably belongs in global capability\\n- a skill, tool, MCP, plugin, automation, or instruction gap repeatedly slows work down\\n- an agent has to restate the same workaround, verification rule, or review rule\\n- a repeated preference should become an atomic user-owned instruction or project-specific testing policy\\n\\n## Do Not Record Writeback For\\n\\n- one-off annoyance with no durable value\\n- weak commentary with no target\\n- speculative ideas without evidence\\n- duplicate noise with no new signal\\n\\n## Follow Through\\n\\n- prefer one strong writeback over many weak ones\\n- mention the writeback id when summarizing what changed\\n- escalate to `capability-evolution` or `fclt ai evolve ...` only when the signal is repeated or clearly points at a durable capability change\\n- use `fclt ai writeback group --by asset` or `fclt ai writeback summarize --by domain` to review accumulated signal before proposing broad changes\\n- use `fclt ai loop activity --project` (or `--global`) for the readable latest activity snapshot; use `--json` for downstream UI\\n- use scheduled `learning-review`, `evolution-review`, or `tool-call-audit` automations when the signal should be reviewed in the background\\n","instructions/PROJECT_CAPABILITY.md":"---\\ndescription: Decide what belongs in repo-local .ai versus the global store.\\ntags: [facult, project, scope]\\n---\\n\\n# Project Capability\\n\\nPrefer project scope when the guidance depends on repo architecture, team workflow, or colocated tooling. Promote to global only after repeated cross-project reuse.\\n\\n## Project First\\n\\nDefault to `/.ai` when the capability is about:\\n\\n- local architecture\\n- repo-specific testing or verification\\n- team conventions\\n- project tools and workflows\\n- product, customer, deployment, or operational context tied to one repo\\n- examples that would leak private or irrelevant detail if copied globally\\n\\nProject capability should travel with the repo when it is safe to commit. Generated state, machine-local runtime state, secrets, and review queues should not travel with it.\\n\\n## Global Scope\\n\\nUse `~/.ai` when the capability should follow the user across projects:\\n\\n- general verification standards\\n- reusable work-unit, feedback-loop, or writeback doctrine\\n- user-owned language/tool preferences that are safe to share across repos\\n- cross-project skills or agents\\n- MCP/tool integration patterns that are not tied to one repo\\n\\nGlobal capability should be broadly useful and low-noise. A global rule that only helps one project is usually a project rule.\\n\\n## Review Artifacts\\n\\nProject-scoped writebacks and evolution proposals use the project as evidence, but their Markdown review artifacts are mirrored under global `~/.ai/writebacks/projects//` and `~/.ai/evolution/projects//`.\\n\\nDo not create repo-local `writebacks/` or `evolution/` review trees inside `/.ai`. Keep private review state out of the repo while preserving project metadata in the global review artifact frontmatter.\\n\\n## Promote Carefully\\n\\nPromote to `~/.ai` only when:\\n\\n- the same pattern succeeds in more than one repo\\n- the capability is not coupled to local architecture\\n- the global version will not create noise for unrelated projects\\n- private examples can be removed or generalized without losing the rule\\n- the target global unit is smaller than a broad rewrite\\n\\nUse:\\n\\n```bash\\nfclt ai evolve promote EV-00001 --to global --project\\n```\\n\\nThat creates a new global proposal for review. It does not auto-apply the promotion.\\n\\n## Decision Checklist\\n\\nChoose project when the answer depends on \\"this repo\\". Choose global when the answer would still be correct after removing the repo name.\\n\\nIf unsure:\\n\\n1. keep the asset project-scoped\\n2. record writeback with the reason it might generalize\\n3. wait for another project or repeated evidence\\n4. promote through a reviewable proposal, not by copying files by hand\\n","instructions/WORK_UNITS.md":"---\\ndescription: \\"Define work units so agent tasks have a clear goal, evidence path, artifact, and writeback target.\\"\\ntags: [\\"work-units\\", \\"planning\\", \\"verification\\", \\"writeback\\"]\\n---\\n\\n# Work Units\\n\\nA work unit is the smallest coherent unit of agent work that can be understood, verified, and preserved.\\n\\nIt is not just the user\'s latest sentence. It is the operational shape around that sentence: what is being changed, why it matters, what evidence is needed, what artifact should remain, and how future agents should benefit from the result.\\n\\nUse work units for ordinary work, not only for capability updates. Coding changes, research answers, documentation edits, operational triage, setup repair, design reviews, and capability evolution all benefit from the same shape when the task has real uncertainty or risk.\\n\\n## Minimum Contract\\n\\nA well-formed work unit names:\\n\\n- goal: the outcome the user needs\\n- acceptance criteria: what must be true when the work is done\\n- required context: source files, docs, systems, messages, or prior decisions needed for correctness\\n- constraints: permissions, privacy, compatibility, deadlines, ownership, or scope limits\\n- signals or evidence: checks that can confirm progress or falsify assumptions\\n- output artifact: code, docs, proposal, issue, note, draft, or report\\n- verification path: commands, review surfaces, manual checks, or source-of-truth reads\\n- writeback target: where durable learning belongs if the work teaches something reusable\\n\\nIf one of these is missing and the gap blocks correctness, surface the gap early and recover it before moving faster.\\n\\nFor low-risk one-step work, keep the contract implicit. For ambiguous, high-impact, cross-tool, stateful, or multi-step work, make the contract explicit before executing.\\n\\n## Why It Exists\\n\\nWork-unit framing prevents shallow completion. It helps agents avoid:\\n\\n- changing files before understanding the target\\n- treating a weak green signal as proof\\n- losing reusable learning in chat\\n- creating duplicate tasks or proposals\\n- turning one-off preferences into global rules\\n- pushing project-specific details into global capability\\n- producing output faster than the system can review, integrate, or learn from it\\n\\nThe point is not paperwork. The point is to attach machine work to intent, context, evidence, and memory so that useful learning can change future work instead of disappearing into chat history.\\n\\n## How To Use It\\n\\nFor simple tasks, keep the work unit implicit but still verify the result.\\n\\nFor ambiguous, high-impact, or multi-step tasks, make the work unit explicit before executing. A compact form is enough:\\n\\n```text\\nGoal:\\nAcceptance:\\nContext:\\nConstraints:\\nEvidence:\\nArtifact:\\nVerification:\\nWriteback:\\n```\\n\\nUse the smallest framing that makes the task correct. Do not turn every request into paperwork.\\n\\n## Examples\\n\\nCoding:\\n\\n```text\\nGoal: fix the failing login test\\nAcceptance: test passes and no auth regression is introduced\\nContext: failing test output, auth middleware, recent commits\\nConstraints: preserve public API\\nEvidence: focused test, relevant integration test\\nArtifact: code diff and concise summary\\nVerification: command output and changed behavior\\nWriteback: only if the failure exposes stale test or auth guidance\\n```\\n\\nResearch:\\n\\n```text\\nGoal: answer a source-backed product question\\nAcceptance: answer cites current primary sources\\nContext: user question, relevant docs, dates\\nConstraints: distinguish verified facts from inference\\nEvidence: source links and quotes within fair-use limits\\nArtifact: answer or research note\\nVerification: source freshness and consistency check\\nWriteback: durable note if the finding will recur\\n```\\n\\nCapability evolution:\\n\\n```text\\nGoal: decide whether repeated writebacks justify a proposal\\nAcceptance: proposal exists only if evidence repeats or a capability is clearly missing\\nContext: grouped writebacks, target asset, current canonical guidance\\nConstraints: avoid global noise and private leakage\\nEvidence: writeback IDs and affected work units\\nArtifact: accepted proposal, rejected proposal, or no-op note\\nVerification: proposal kind, scope, target, and review artifact\\nWriteback: only for new meta-learning about the evolution process\\n```\\n\\n## Writeback\\n\\nWhen the work reveals durable friction, missing capability, stale guidance, or a repeatable workflow, prefer one strong writeback over many weak ones.\\n\\nUse `fclt ai writeback add ...` when the target asset, scope, and evidence are clear. Use `fclt ai evolve ...` only when repeated signal supports a concrete proposal.\\n","skills/capability-evolution/SKILL.md":"---\\ndescription: Convert repeated writeback into concrete fclt capability proposals.\\ntags: [facult, evolution, writeback]\\n---\\n\\n# capability-evolution\\n\\n## When To Use\\nUse this skill when the same missing guidance, weak loop, or recurring win appears often enough that the AI system itself should probably change.\\n\\nDo not wait for a human operator by default if the signal is already clear and the environment permits local AI runtime state to be updated.\\n\\nUse writeback first when the signal is useful but not yet repeated. Use evolution when accumulated writebacks, repeated tool friction, or a clearly missing capability point at a specific target asset or new capability.\\n\\nThe goal is a governed feedback loop: work creates evidence, evidence produces writeback, repeated writeback becomes a small reviewed proposal, and accepted proposals change future agent behavior.\\n\\n## Scope Decision\\n\\nChoose `project` when the behavior depends on repo-local architecture or workflow.\\n\\nChoose `global` when the behavior is broadly reusable.\\n\\nIf unsure, start at project scope and promote later with evidence.\\n\\nReject global scope when the proposal depends on private examples, one repo\'s architecture, a single user\'s temporary preference, or a workflow that has not repeated.\\n\\n## Working Flow\\n\\n1. Harvest the review window from work logs, commits, runbooks, tool notes, and task-system changes.\\n2. Reconcile each source signal to a writeback, implementation task, local asset, or explicit exclusion.\\n3. Group or summarize repeated signal by asset, kind, and scope.\\n4. Run a read-only evolution assessment for the target when possible.\\n5. Check the current target asset before proposing a change.\\n6. Choose the smallest valid proposal kind and draft it.\\n7. Accept only after the target and scope are correct.\\n8. Apply only when the markdown target is the intended canonical asset.\\n9. Verify the producing loop and grade effectiveness before resolving source writebacks.\\n\\nUse:\\n\\n```bash\\nfclt ai review reconcile --since --until --json\\nfclt ai writeback add ...\\nfclt ai writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai evolve assess --asset --json\\nfclt ai evolve propose\\nfclt ai evolve draft EV-00001\\nfclt ai evolve draft EV-00001 --append \\"tighten the rule with a concrete verification step\\"\\nfclt ai evolve accept EV-00001\\nfclt ai evolve apply EV-00001\\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\\nFor an explicitly enabled scheduled loop, use:\\n\\n```bash\\nfclt ai loop enable --project\\nfclt ai loop status --project --json\\nfclt ai loop run --project --json\\nfclt ai loop disable --project\\n```\\n\\nTreat the durable full queue as the source of truth and the notification delta\\nas a noise-control view. Scheduler registration alone is not proof of a run;\\ncheck observed execution health. The loop may prepare writebacks and proposals,\\nbut canonical apply remains plan-only and external task mutation remains a\\nseparate approved integration.\\n\\nThe writeback queue alone cannot prove an empty window. Run configured source\\nreconciliation first and preserve its coverage, exclusions, correlations,\\nlinked work, and dispositions. If any source is stale or unavailable, report a\\ndegraded review rather than \\"nothing pending.\\" Implementation tickets remain\\nevidence and task targets; do not turn each ticket into a capability proposal.\\n\\nApplying is not completion. Preserve baseline friction, expected behavior, the producing loop,\\nimplementation tickets, verification evidence, and an effectiveness grade. A recurrence after\\napply is `unchanged` or `regressed` evidence for the same evolution, not a new singleton.\\n\\nFor manually scaffolded background review templates, use:\\n\\n```bash\\nfclt templates init automation learning-review\\nfclt templates init automation evolution-review\\nfclt templates init automation tool-call-audit\\n```\\n\\nIf there is not yet enough repeated signal for evolution, do not stop at a bare \\"no\\". Explain what evidence would change the decision, whether another writeback should be recorded, and the smallest future target if the pattern repeats.\\n\\nDo not create a proposal only to preserve an idea. Preserve the idea as writeback, notes, or task tracking unless it has enough evidence to change capability.\\n\\n## Proposal Kind Selection\\n\\n- `update_asset` for tightening existing guidance\\n- `create_asset` for missing instructions or docs\\n- `extract_snippet` for reusable partial guidance\\n- `add_skill` for reusable workflow instruction\\n- `promote_asset` for project-to-global promotion\\n\\nUse task tracking instead of evolution when the main work is an executable tool or product fix that needs an owner, priority, state, or delivery plan. Use evolution for the reusable instruction, skill, or operating-model change that should survive that fix.\\n\\n## Review Criteria\\n\\nBefore accept/apply, verify:\\n\\n- evidence is repeated or the missing capability is obvious\\n- the proposal targets the smallest affected unit\\n- project/global scope is correct\\n- private or project-specific examples are not leaking into global assets\\n- the patch changes canonical markdown assets, not generated runtime state\\n- the resulting behavior can be verified by reading, rendering, indexing, or running the relevant command\\n\\n## Output Contract\\n- repeated signal\\n- proposed asset change\\n- target scope\\n- evidence\\n- smallest useful next step\\n- approval or no-op rationale\\n- next evidence to collect when no proposal is justified\\n- exact read-only and mutating commands, with approval boundaries\\n","skills/fclt-writeback/SKILL.md":"---\\ndescription: Record and review durable writeback from real agent work.\\ntags: [facult, writeback, learning, feedback-loop]\\n---\\n\\n# fclt-writeback\\n\\n## When To Use\\n\\nUse this skill when work reveals durable friction, missing context, weak verification,\\nstale guidance, repeated success, or a capability gap.\\n\\nWriteback is for preserving reusable signal. It is not for every preference or one-off\\nannoyance.\\n\\n## Workflow\\n\\n1. Choose project scope when the learning depends on a repo, architecture, test harness,\\n or team workflow. Choose global scope for broadly reusable learning.\\n2. Target the smallest affected instruction, snippet, skill, agent, MCP/tool config, or\\n automation.\\n3. Record concrete evidence:\\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- Capture a concise observation, impact, attempted workaround, desired outcome,\\n evidence reference, and sensitivity when those fields add real context.\\n- Never capture hidden chain-of-thought, raw transcripts, unbounded logs,\\n secrets, tokens, or credential-bearing payloads. Reference redacted external\\n evidence instead.\\n- Do not copy private project detail into global writebacks.\\n- Use task tracking for executable product work; use writeback for reusable operating-layer\\n learning.\\n- Hand repeated, target-specific signal to the capability-evolution workflow.\\n\\n## Output\\n\\n- writeback id or no-op rationale\\n- scope and target asset\\n- evidence summary\\n- whether the signal is ready for evolution\\n","skills/project-operating-layer-design/SKILL.md":"---\\ndescription: Design or improve a repo-local .ai operating layer.\\ntags: [facult, project, design]\\n---\\n\\n# project-operating-layer-design\\n\\n## When To Use\\nUse this skill when a project needs its own `.ai/` structure, repo-specific instructions, or local bootstrap guidance.\\n\\nUse it when:\\n\\n- a repo has recurring agent friction that should not become global doctrine\\n- setup or verification steps are repeatedly rediscovered\\n- project skills, agents, MCP definitions, or snippets need a stable source of truth\\n- a repo needs policy for what may be rendered into tool homes\\n- a project should contribute writeback/evolution evidence without committing private review artifacts\\n\\nDo not use it to copy a user\'s private global preferences into a public repo.\\n\\n## Design Rules\\n\\n- Start from the repo\'s real workflows, commands, and risk boundaries.\\n- Keep project-specific guidance in `/.ai`.\\n- Keep generated state, queues, review artifacts, and local machine config out of the repo.\\n- Prefer a few high-leverage instructions or skills over a large generic dump.\\n- Use snippets only for blocks that are reused or independently evolvable.\\n- Make verification and integration paths explicit enough for future agents to run.\\n- Add sync policy only for assets that should render into repo-local tool outputs.\\n\\n## Working Flow\\n\\n1. Inventory existing repo guidance and tool files.\\n2. Identify repeated friction from recent work, issues, reviews, or writebacks.\\n3. Separate project-specific behavior from global/user-owned behavior.\\n4. Propose a minimal `.ai` layout.\\n5. Add or update the smallest useful assets.\\n6. Verify the graph/index and any rendered output.\\n7. Record writeback for reusable learnings that should evolve later.\\n\\n## Output Contract\\n- recommended `.ai/` layout\\n- what stays project-local\\n- what stays global\\n- what should remain generated runtime output only\\n- sync/rendering policy\\n- verification path\\n- privacy or commit-safety risks\\n","snippets/global/baseline.md":"- Preserve existing user changes unless asked to rewrite them.\\n- Prefer small, reviewable diffs and verify meaningful changes before claiming success.\\n- State constraints, risks, and follow-up steps directly.\\n","snippets/global/core/feedback-loops.md":"- For any task, identify the highest-signal feedback loops available.\\n- Prefer loops that can verify progress, falsify weak assumptions, and expose failure early.\\n- Do not rely on a single shallow positive signal if stronger verification exists.\\n- If the available loop is stale, weak, noisy, or easy to game, improve it or say what is missing.\\n- When useful, leave behind a stronger loop than the one you started with.\\n- Treat verification, evaluation, and writeback as part of the work, not cleanup after it.\\n- For work-unit clarification, read ${refs.work_units}.\\n- For verification guidance, read ${refs.verification}.\\n- For integration risk, read ${refs.integration}.\\n- For learning and writeback, read ${refs.learning_writeback}.\\n- For deeper guidance, read ${refs.feedback_loops}.\\n","snippets/global/core/verification.md":"- Treat verification as part of the work, not a final checkbox.\\n- Prefer the strongest available proof that matches the real risk.\\n- Make clear what has actually been verified and what remains assumed.\\n- Distrust shallow green signals when stronger checks are available.\\n- If the current harness is stale, weak, or misleading, say so and improve it where possible.\\n- For deeper guidance, read ${refs.verification}.\\n","snippets/global/core/work-units.md":"- Treat every task as a work unit, not just a request.\\n- A work unit should have a goal, acceptance criteria, required context, constraints, signals or evidence, an output artifact, a verification path, and a writeback target when the work teaches something reusable.\\n- If any of those are missing and the gap blocks correctness, surface it early and try to recover it.\\n- Prefer making the work unit more explicit before increasing execution speed.\\n- If the task is vague, ambiguous, or overloaded, narrow it before acting.\\n- Treat work-unit framing as generally applicable to coding, research, writing, operations, setup, debugging, and capability evolution.\\n- For deeper guidance, read ${refs.work_units}.\\n","snippets/global/core/writeback.md":"- Do not end at output if something important was learned.\\n- Preserve decisions, failures, successes, and reusable signal when they will improve future work.\\n- Prefer writing to a real destination over leaving knowledge in chat.\\n- When useful, leave behind better docs, tests, evals, prompts, notes, or follow-up tasks.\\n- When a high-signal learning clearly points at a canonical asset or durable destination, record a writeback before ending the task.\\n- Prefer one strong writeback over many weak ones.\\n- If you can name the target asset, the expected scope, and the actual signal, use `fclt ai writeback add ...` instead of merely mentioning that writeback would be useful.\\n- If repeated signal is already accumulating, use the `capability-evolution` skill or `fclt ai evolve ...` flow to turn it into a reviewable proposal.\\n- For deeper guidance, read ${refs.learning_writeback}.\\n","snippets/templates/agents-global.md":"# Global Agent Instructions\\n\\nThis template materializes as `AGENTS.global.md` when the operating-model pack is\\ninstalled. It should stay small and composed from snippets. Put detailed\\ndoctrine in instructions, workflow execution in skills, and local/private\\npreferences in user-owned or project-owned assets outside the public pack.\\n\\n## Working mode\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n## Shared instruction sources\\n\\n- For work-unit definition and scope clarification, read ${refs.work_units}.\\n- For identifying, improving, and validating feedback loops, read ${refs.feedback_loops}.\\n- For verification and anti-false-positive checks, read ${refs.verification}.\\n- For checking integration boundaries, read ${refs.integration}.\\n- For learning, decisions, and writeback, read ${refs.learning_writeback}.\\n- For capability evolution, proposal kinds, and `facult ai` workflow, read ${refs.evolution}.\\n- For deciding whether something belongs in global or project scope, read ${refs.project_capability}.\\n- Add private language, coding, or writing refs in local config only when they belong to the user\'s own operating layer.\\n\\n## Layering\\n\\n- Treat this file as the global baseline.\\n- Treat repo-level `AGENTS.md` files as more specific additions layered after this file.\\n- Repo-level files may add or refine project-specific behavior, but they should not weaken global defaults for rigor, verification, or writeback discipline.\\n- If a closer `AGENTS.override.md` exists, follow it as the most specific instructions file in that directory while still preserving the global baseline unless the closer file explicitly tightens it.\\n"}' + '{"agents/evolution-planner/agent.toml":"name = \\"evolution-planner\\"\\ndescription = \\"Turn repeated writeback into concrete capability proposals.\\"\\n\\ndeveloper_instructions = \\"\\"\\"\\nYou plan capability evolution.\\n\\nPrioritize:\\n- smallest useful change\\n- correct target asset type\\n- correct target scope\\n- evidence that justifies the change\\n- repeated writeback clusters or clearly missing capabilities, not isolated preferences\\n\\nProposal kinds you should consider first:\\n- update_asset\\n- create_asset\\n- extract_snippet\\n- add_skill\\n- promote_asset\\n\\nDefault to project scope when the pattern is repo-local.\\nPromote to global only when reuse is demonstrated and pollution risk is low.\\n\\nReturn concise proposals ordered by expected leverage, including:\\n- proposal kind\\n- target asset\\n- target scope\\n- why this is the smallest durable change\\n- source writeback ids or evidence summary\\n- approval risk and verification path\\n\\nDo not escalate to evolution when a single writeback is enough.\\nDo not use evolution as a substitute for executable task tracking when the main need is owner, priority, state, or implementation follow-through.\\nDo not globalize private, repo-specific, or speculative guidance.\\n\\"\\"\\"\\n","agents/integration-auditor/agent.toml":"name = \\"integration-auditor\\"\\ndescription = \\"Find where local success can still fail system-wide.\\"\\n\\ndeveloper_instructions = \\"\\"\\"\\nYou audit integration risk.\\n\\nPrioritize:\\n- hidden dependencies\\n- rollout hazards\\n- operational constraints\\n- gaps between local verification and real system behavior\\n- packaged, installed, rendered, or synced paths that differ from source behavior\\n- parallel execution and state-location risks\\n- privacy boundaries between global, project, generated, and machine-local state\\n\\nReturn concise findings ordered by impact. For each finding include:\\n- boundary at risk\\n- why the current evidence is or is not enough\\n- strongest next verification step\\n- whether the fix belongs in code, docs, a task, or capability evolution\\n\\"\\"\\"\\n","agents/scope-promoter/agent.toml":"name = \\"scope-promoter\\"\\ndescription = \\"Decide whether learning belongs at project or global scope.\\"\\n\\ndeveloper_instructions = \\"\\"\\"\\nYou decide scope.\\n\\nPrioritize:\\n- project specificity\\n- cross-project reuse potential\\n- pollution risk from globalizing too early\\n- private or repo-specific details that must not move into global capability\\n- whether a smaller snippet, instruction, skill, or agent should be promoted instead of a broad doc\\n\\nWhen recommending promotion, make the standard path explicit:\\n- keep the source capability in project scope until promotion is approved\\n- create a reviewable global proposal\\n- do not treat promotion as implicit apply\\n\\nReturn concise decisions with:\\n- recommended scope\\n- target asset or smallest unit\\n- evidence for reuse\\n- privacy/pollution risk\\n- promotion path or no-op rationale\\n\\"\\"\\"\\n","agents/writeback-curator/agent.toml":"name = \\"writeback-curator\\"\\ndescription = \\"Turn noisy outcomes into high-signal writeback.\\"\\n\\ndeveloper_instructions = \\"\\"\\"\\nYou curate durable writeback.\\n\\nPrioritize:\\n- repeated failures\\n- repeated wins\\n- stale guidance\\n- missing capability edges\\n- tool, skill, MCP, plugin, automation, or instruction friction that repeatedly slows work down\\n\\nFor each recommendation, prefer returning:\\n- suggested writeback kind\\n- category: friction, opportunity, or reusable-success\\n- best target asset or destination\\n- best scope (`project` or `global`)\\n- the evidence that justifies recording it\\n- concise context, impact, attempted workaround, desired outcome, and sensitivity when known\\n- whether the signal is enough for writeback only, task tracking, or evolution\\n\\nDo not emit low-signal noise.\\nIf the learning is repo-specific, keep it project-scoped by default.\\nWhen the signal is already strong and the target is clear, prefer recommending direct writeback capture rather than abstract advice.\\nWhen the issue is executable tooling work, recommend task tracking for the fix and writeback only for the reusable operating-model learning.\\nWhen the issue contains private project details, preserve the general learning without copying private details into a global asset.\\nNever request or emit hidden chain-of-thought, raw transcripts, unbounded logs, secrets, tokens, or credential-bearing payloads. Use redacted evidence references.\\n\\"\\"\\"\\n","instructions/CAPABILITY_COMPOSITION.md":"---\\ndescription: \\"Compose small capability units across global and project roots, then evolve the smallest affected unit.\\"\\ntags: [\\"facult\\", \\"composition\\", \\"refs\\", \\"snippets\\", \\"instructions\\"]\\n---\\n\\n# Capability Composition\\n\\nUse `fclt` capability as small units that can be composed, inspected, rendered, and evolved independently.\\n\\nThe main units are:\\n\\n- instructions: standalone markdown doctrine such as language preferences, verification rules, or review standards\\n- snippets: small markdown partials inserted into one or more rendered docs\\n- skills: task-specific workflows with `SKILL.md`\\n- agents: focused role manifests\\n- MCP definitions: tool interfaces and their safe auth shape\\n- automations: scheduled review or maintenance loops\\n- tool rules/config: tool-specific defaults and policy\\n\\n## Composition Rules\\n\\n- Keep reusable doctrine in `instructions/`.\\n- Keep repeated paragraphs or policy blocks in `snippets/`.\\n- Keep workflow execution in `skills/`.\\n- Keep persona or delegation behavior in `agents/`.\\n- Keep tool wiring in `mcp/` and `tools//`.\\n- Compose broad agent docs from refs and snippets instead of copying text by hand.\\n- Prefer one narrow reusable unit over one large instruction file that mixes unrelated domains.\\n\\nExamples:\\n\\n- `@ai/instructions/LANGUAGE.md` for a user-owned language/tooling preference.\\n- `@ai/instructions/REVIEW.md` for a user-owned review standard.\\n- `@project/instructions/TESTING.md` for repo-specific test policy.\\n- `` for a shared rendered block.\\n\\n## Scope\\n\\nUse global scope for capability that should follow the user across projects.\\n\\nUse project scope for capability that belongs to a repo, team workflow, architecture, or local test harness.\\n\\nPromote project capability to global only when repeated evidence shows reuse across projects. Do not globalize a project quirk just because it worked once.\\n\\n## Writeback and Evolution\\n\\nTarget the smallest affected unit.\\n\\n- If a paragraph is reused in several rendered docs, target the snippet.\\n- If a domain rule is wrong, target the instruction.\\n- If a workflow is incomplete, target the skill.\\n- If a delegated role is unclear, target the agent.\\n- If a tool interface is missing or unsafe, target the MCP or tool config.\\n- If a scheduled review loop is noisy or missing context, target the automation.\\n\\nGood writeback targets are graph-backed selectors when possible:\\n\\n```bash\\nfclt ai writeback add --kind missing_context --summary \\"Language guidance did not cover test runner selection.\\" --asset instruction:LANGUAGE\\nfclt ai writeback add --kind reusable_pattern --summary \\"Project test policy should become a shared verification snippet.\\" --asset @project/instructions/TESTING.md\\nfclt ai writeback add --kind bad_default --summary \\"The review automation escalated one-off preferences.\\" --asset automation:evolution-review\\n```\\n\\nUse `fclt ai evolve ...` only after repeated signal, a clearly missing capability, or a stale canonical asset points at a concrete change. Prefer the smallest valid proposal kind: `update_asset`, `create_asset`, `extract_snippet`, `add_skill`, or `promote_asset`.\\n\\n## Agent Defaults\\n\\nWhen an agent sees a repeated language, framework, or test preference, it should not bury that in chat. It should identify whether the durable unit is:\\n\\n- a global instruction\\n- a project instruction\\n- a snippet reused by rendered docs\\n- a skill workflow\\n- a project-to-global promotion candidate\\n\\nThen it should record writeback against that unit, or draft a proposal when the evidence is already strong enough.\\n","instructions/EVOLUTION.md":"---\\ndescription: Turn repeated signal into concrete capability changes.\\ntags: [facult, evolution, writeback]\\n---\\n\\n# Evolution\\n\\nUse writeback and evolution to improve the AI operating layer itself.\\n\\nEvolution is the synthesis and change side of the feedback loop. It turns accumulated writebacks, repeated tool friction, stale canonical assets, or clearly missing capability into small reviewable changes to instructions, skills, snippets, agents, or other markdown canonical assets.\\n\\nUse capability composition when choosing the target. Instructions, snippets, skills, agents, MCP/tool config, and automations are separate units. Target the smallest unit that actually needs to change instead of rewriting a broad agent doc.\\n\\n## When To Record Writeback\\n\\nRecord writeback when one of these is true:\\n\\n- the same failure repeats\\n- the same success pattern repeats\\n- guidance is stale or missing\\n- a prompt or loop has to be restated often\\n- a project-specific pattern looks reusable\\n\\nDo not record low-signal noise:\\n\\n- one-off annoyance with no reuse value\\n- generic \\"could be better\\" commentary\\n- duplicate observations with no new evidence\\n\\nThe intended default is that agents record strong writebacks themselves when the signal is clear enough, rather than only recommending that a user do it manually later.\\n\\nDo not wait for a weekly review to preserve high-signal evidence. Do wait for repeated evidence or a clearly missing capability before drafting a proposal.\\n\\n## Scope\\n\\nChoose `project` scope when the learning depends on:\\n\\n- repo architecture\\n- team workflow\\n- project tooling\\n- local testing or verification behavior\\n\\nChoose `global` scope when the learning is reusable across projects.\\n\\nPromote from project to global only after repeated reuse or strong evidence.\\n\\n## Writeback Kinds\\n\\nCommon kinds:\\n\\n- `weak_verification`\\n- `false_positive`\\n- `missing_context`\\n- `reusable_pattern`\\n- `capability_gap`\\n- `bad_default`\\n\\nEvery good writeback should try to include:\\n\\n- a concrete summary\\n- the best target asset if known\\n- the right scope\\n- domain or tags when useful\\n\\nGood target examples:\\n\\n- `instruction:LANGUAGE` when shared language/tooling guidance is stale or missing\\n- `@project/instructions/TESTING.md` when repo test policy needs project-scoped evolution\\n- `snippet:global/policy/review` when a repeated rendered block should be fixed or extracted\\n- `skill:capability-evolution` when a workflow skill is missing steps or examples\\n- `automation:evolution-review` when the scheduled review loop is noisy or incomplete\\n\\n## Operator Flow\\n\\nTypical workflow:\\n\\n```bash\\nfclt ai writeback add --kind weak_verification --summary \\"Checks were too shallow\\" --asset instruction:VERIFICATION\\nfclt ai writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai evolve assess --asset instruction:VERIFICATION --json\\nfclt ai evolve propose\\nfclt ai evolve draft EV-00001\\nfclt ai evolve accept EV-00001\\nfclt ai evolve apply EV-00001\\n```\\n\\nUse `fclt ai evolve draft --append \\"...\\"` to revise a draft while preserving draft history.\\n\\nReview surfaces:\\n\\n- open `~/.ai/writebacks/` and `~/.ai/evolution/` in a Markdown editor for frontmatter-rich global and project-scoped review artifacts\\n- `fclt status --json` for queue/proposal paths, review artifact paths, counts, and active scope\\n- `fclt ai writeback list|show|group|summarize` for raw and clustered signal\\n- `fclt ai evolve assess` for read-only proposal readiness and the safest next action\\n- `fclt ai evolve list|show|review` for proposal state without applying changes\\n- `fclt templates init automation learning-review` for recurring capture/review\\n- `fclt templates init automation evolution-review` for recurring proposal review\\n- `fclt templates init automation tool-call-audit` for repeated tool-friction review\\n\\nAn operator may explicitly enable the coordinated closed loop with `fclt ai\\nloop enable --project` or `--global`. Use `fclt ai loop status --json` to check\\nboth scheduler registration and observed successful execution. The durable\\nqueue must retain unchanged and temporarily unobserved items; only the\\nnotification delta should suppress noise.\\n\\nEvolution proposal metadata, markdown drafts, patch artifacts, writeback queues,\\nand journals are runtime state. `fclt` stores JSON queues, proposal records,\\ndraft refs, patches, and journals in machine-local `fclt` state. It mirrors\\nhuman-readable review artifacts into global `~/.ai/writebacks/...` and\\n`~/.ai/evolution/...`, including project-scoped artifacts under\\n`projects//` with cwd/project metadata in frontmatter. Canonical\\nassets in `~/.ai` or `/.ai` should only change when a proposal is applied.\\n\\n## Default Agent Behavior\\n\\nUse the smallest action that fits the signal:\\n\\n1. record one strong writeback when there is a clear durable learning\\n2. use `writeback-curator` when the target, kind, or scope is ambiguous\\n3. run `fclt ai evolve assess --asset --json` before proposing when a target is known\\n4. use `capability-evolution` or `evolution-planner` when repeated signal should become a proposal\\n5. do not draft or apply proposals just because a writeback exists; require repeated evidence or a clearly missing capability\\n\\nWhen assessment recommends no mutation or more writeback, agents should still produce a useful review: state the current target, evidence grade, missing signal, exact recurrence that would justify evolution, and any read-only follow-up. Do not end with only \\"no proposal\\".\\n\\nAvoid creating writeback/evolution noise for one-off nits, vague preferences, or speculative ideas without evidence.\\n\\nWhen the friction is executable product/tooling work that needs ownership,\\npriority, state, or implementation follow-through, create or update a real task\\nsystem item instead of forcing it into capability evolution. Use evolution for\\nthe reusable operating-layer change.\\n\\n## Proposal Kinds\\n\\nCurrent supported proposal kinds:\\n\\n- `update_asset`\\n- `create_asset`\\n- `extract_snippet`\\n- `add_skill`\\n- `promote_asset`\\n\\nUse the smallest durable change that fits the evidence.\\n\\nExamples:\\n\\n- `update_asset`: fix a stale instruction, snippet, agent, or automation markdown asset.\\n- `create_asset`: add a missing instruction such as `LANGUAGE.md` or `REVIEW.md`.\\n- `extract_snippet`: move repeated guidance out of several docs into one snippet.\\n- `add_skill`: create a workflow when instructions are not enough.\\n- `promote_asset`: move a proven project instruction/snippet/skill toward global reuse.\\n\\n## Review And Apply Rules\\n\\n- draft before apply\\n- accept before apply\\n- prefer the smallest safe change\\n- keep reviewable evidence tied to source writebacks\\n- do not globalize project behavior too early\\n- do not apply high-risk global instruction, skill, plugin, or shared-tool changes without explicit review/approval\\n\\nApply is for markdown canonical assets only. If the target is wrong, revise the proposal rather than forcing it through.\\n\\nScheduled review does not weaken the approval boundary. It may reconcile\\nread-only sources, record targeted writebacks, draft proposals, and report a\\nvendor-neutral request to reopen linked implementation work. It must not\\nmutate an external tracker or automatically apply canonical changes. Treat\\nproject auto-apply as plan-only until a hash-bound transaction, validation,\\nrollback, and durable receipt exist; keep global and plugin changes\\nproposal-only.\\n\\nSignal-family decisions are a separate machine-local lifecycle. Resolve the\\ncurrent opaque activity locator, then record `accept`, `redirect`, `reject`, or\\n`defer` only with explicit approval and the exact queue revision. The append-only\\nreceipt preserves bounded work-unit context but does not apply capability,\\nmutate Git or an external tracker, spawn a task, or grant authority beyond the\\nrecorded decision. Current-source completion may close already-landed families;\\nhold/defer remains visible and unimplemented. Treat coverage completeness and\\ncursor freshness as independent review facts.\\n","instructions/INTEGRATION.md":"---\\ndescription: Detect where local success can still fail at integration boundaries.\\ntags: [facult, integration, verification]\\n---\\n\\n# Integration\\n\\nDistinguish local correctness from system correctness. Check hidden dependencies, rollout order, and operational constraints before calling work done.\\n\\n## When To Use\\n\\nUse this when a local green signal may still fail at a boundary:\\n\\n- code passes focused tests but has not been checked against the real workflow\\n- docs are correct in isolation but may send agents to a stale command or path\\n- a tool command works locally but may fail under packaged, sandboxed, or parallel execution\\n- a capability change renders into one agent tool but not another\\n- a project-local improvement may collide with global defaults or managed output\\n- a migration, release, or rollout has ordering constraints\\n\\n## Integration Questions\\n\\nAsk the smallest set that matches the risk:\\n\\n- What consumes this output?\\n- What state does this depend on?\\n- What happens if two agents or commands run this at the same time?\\n- Does the packaged/released path behave like the source checkout?\\n- Does the project-scoped path avoid leaking into global or public surfaces?\\n- Does the global path avoid overwriting tool-native or user-edited state?\\n- Is rollback or recovery clear if the integration fails?\\n\\n## Evidence\\n\\nPrefer evidence that crosses the boundary that could fail:\\n\\n- run the installed CLI, packaged binary, or generated artifact when source tests are not enough\\n- inspect rendered output when changing snippets, refs, or agent docs\\n- use temp roots and clean homes for setup, upgrade, and sync behavior\\n- verify review artifacts land in global `~/.ai/writebacks` or `~/.ai/evolution`, not repo-local private state\\n- check release, package, or plugin surfaces when the change affects users outside the repo\\n\\n## Output\\n\\nReturn concise findings ordered by risk:\\n\\n- boundary checked\\n- evidence used\\n- remaining assumption\\n- fix or follow-up if local correctness does not prove system correctness\\n\\nRecord writeback when the same integration boundary repeatedly fails, the verification loop is too weak, or a missing skill/tool would make the boundary easier to check next time.\\n","instructions/LEARNING_AND_WRITEBACK.md":"---\\ndescription: Preserve durable signal and record writeback when the operating layer should learn.\\ntags: [facult, learning, writeback]\\n---\\n\\n# Learning And Writeback\\n\\nUse this when work produces a durable decision, failure, success pattern, or missing guardrail that should outlive the current task.\\n\\nThis is the capture side of the feedback loop. The goal is to let normal agent work produce reusable signal without requiring a human to manually restate every friction point later.\\n\\n## Default Behavior\\n\\nThe normal path should be agent-driven.\\n\\nIf you can clearly answer:\\n\\n- what was learned\\n- why it matters\\n- where it should land\\n- whether it belongs in `project` or `global`\\n\\nthen record the writeback instead of only suggesting that someone should do it later.\\n\\nUse:\\n\\n```bash\\nfclt ai writeback add \\\\\\n --kind \\\\\\n --category \\\\\\n --summary \\"\\" \\\\\\n --details \\"\\" \\\\\\n --impact \\"\\" \\\\\\n --attempted-workaround \\"\\" \\\\\\n --desired-outcome \\"\\" \\\\\\n --sensitivity \\\\\\n --evidence \\\\\\n --asset \\n```\\n\\nKeep the capture concise and factual. Preserve decision rationale as a short\\nexplanation of the observed evidence and selected disposition. Never record\\nhidden chain-of-thought, raw transcripts, unbounded logs, secrets, tokens, or\\ncredential-bearing payloads. Store logs in their existing system and reference\\nthe smallest redacted evidence identifier that lets a reviewer retrieve them.\\n\\nSensitivity controls supplemental context in portable review surfaces:\\n\\n- `public`: generic context that is safe to show broadly\\n- `internal`: context for the exact local/project review surface; this is the default\\n- `private`: keep the actionable summary, but omit supplemental context from portable artifacts and tools\\n\\nSensitivity is not a secret store. Secret-shaped values are redacted at the\\nwriteback boundary regardless of the selected sensitivity.\\n\\nThe writeback queue is runtime state, not canonical source. `fclt` stores JSON\\nqueue state in machine-local `fclt` state so sandboxed agents can record durable\\nfriction without mutating canonical assets unless an evolution proposal is later\\nreviewed and applied.\\n\\nEvery writeback also refreshes a Markdown review artifact under the global\\n`~/.ai/writebacks/...` tree. Global signal lands in `~/.ai/writebacks/global/`;\\nproject-scoped signal lands in `~/.ai/writebacks/projects//` with\\nfrontmatter for scope, project root, cwd, target asset, status, tags, evidence,\\nand timestamps. Do not write writeback review artifacts into a repo-local `.ai`;\\nrepo-local state should contribute project metadata and evidence, not bundled\\nprivate review files.\\n\\nProject-scoped writebacks should usually be recorded from the repo that produced\\nthe evidence. Global writebacks should be reserved for shared doctrine, shared\\nskills, shared agents, tool behavior, or cross-project capability gaps.\\n\\nTarget the smallest composable unit that explains the friction:\\n\\n- instruction: domain guidance, preferences, verification rules, or review doctrine\\n- snippet: repeated markdown block used by more than one rendered doc\\n- skill: workflow execution steps or examples\\n- agent: delegated role behavior\\n- MCP/tool config: tool interface, auth shape, or rendered integration\\n- automation: scheduled review loop, cadence, prompt, or memory\\n\\n## Record Writeback When\\n\\n- the same failure or weak loop appears again\\n- a reusable success pattern shows up\\n- guidance is clearly stale or missing\\n- a repo-local behavior probably belongs in project capability\\n- a cross-project behavior probably belongs in global capability\\n- a skill, tool, MCP, plugin, automation, or instruction gap repeatedly slows work down\\n- an agent has to restate the same workaround, verification rule, or review rule\\n- a repeated preference should become an atomic user-owned instruction or project-specific testing policy\\n\\n## Do Not Record Writeback For\\n\\n- one-off annoyance with no durable value\\n- weak commentary with no target\\n- speculative ideas without evidence\\n- duplicate noise with no new signal\\n\\n## Follow Through\\n\\n- prefer one strong writeback over many weak ones\\n- mention the writeback id when summarizing what changed\\n- escalate to `capability-evolution` or `fclt ai evolve ...` only when the signal is repeated or clearly points at a durable capability change\\n- use `fclt ai writeback group --by asset` or `fclt ai writeback summarize --by domain` to review accumulated signal before proposing broad changes\\n- use `fclt ai loop activity --project` (or `--global`) for the readable latest activity snapshot; use `--json` for downstream UI\\n- use scheduled `learning-review`, `evolution-review`, or `tool-call-audit` automations when the signal should be reviewed in the background\\n","instructions/PROJECT_CAPABILITY.md":"---\\ndescription: Decide what belongs in repo-local .ai versus the global store.\\ntags: [facult, project, scope]\\n---\\n\\n# Project Capability\\n\\nPrefer project scope when the guidance depends on repo architecture, team workflow, or colocated tooling. Promote to global only after repeated cross-project reuse.\\n\\n## Project First\\n\\nDefault to `/.ai` when the capability is about:\\n\\n- local architecture\\n- repo-specific testing or verification\\n- team conventions\\n- project tools and workflows\\n- product, customer, deployment, or operational context tied to one repo\\n- examples that would leak private or irrelevant detail if copied globally\\n\\nProject capability should travel with the repo when it is safe to commit. Generated state, machine-local runtime state, secrets, and review queues should not travel with it.\\n\\n## Global Scope\\n\\nUse `~/.ai` when the capability should follow the user across projects:\\n\\n- general verification standards\\n- reusable work-unit, feedback-loop, or writeback doctrine\\n- user-owned language/tool preferences that are safe to share across repos\\n- cross-project skills or agents\\n- MCP/tool integration patterns that are not tied to one repo\\n\\nGlobal capability should be broadly useful and low-noise. A global rule that only helps one project is usually a project rule.\\n\\n## Review Artifacts\\n\\nProject-scoped writebacks and evolution proposals use the project as evidence, but their Markdown review artifacts are mirrored under global `~/.ai/writebacks/projects//` and `~/.ai/evolution/projects//`.\\n\\nDo not create repo-local `writebacks/` or `evolution/` review trees inside `/.ai`. Keep private review state out of the repo while preserving project metadata in the global review artifact frontmatter.\\n\\n## Promote Carefully\\n\\nPromote to `~/.ai` only when:\\n\\n- the same pattern succeeds in more than one repo\\n- the capability is not coupled to local architecture\\n- the global version will not create noise for unrelated projects\\n- private examples can be removed or generalized without losing the rule\\n- the target global unit is smaller than a broad rewrite\\n\\nUse:\\n\\n```bash\\nfclt ai evolve promote EV-00001 --to global --project\\n```\\n\\nThat creates a new global proposal for review. It does not auto-apply the promotion.\\n\\n## Decision Checklist\\n\\nChoose project when the answer depends on \\"this repo\\". Choose global when the answer would still be correct after removing the repo name.\\n\\nIf unsure:\\n\\n1. keep the asset project-scoped\\n2. record writeback with the reason it might generalize\\n3. wait for another project or repeated evidence\\n4. promote through a reviewable proposal, not by copying files by hand\\n","instructions/WORK_UNITS.md":"---\\ndescription: \\"Define work units so agent tasks have a clear goal, evidence path, artifact, and writeback target.\\"\\ntags: [\\"work-units\\", \\"planning\\", \\"verification\\", \\"writeback\\"]\\n---\\n\\n# Work Units\\n\\nA work unit is the smallest coherent unit of agent work that can be understood, verified, and preserved.\\n\\nIt is not just the user\'s latest sentence. It is the operational shape around that sentence: what is being changed, why it matters, what evidence is needed, what artifact should remain, and how future agents should benefit from the result.\\n\\nUse work units for ordinary work, not only for capability updates. Coding changes, research answers, documentation edits, operational triage, setup repair, design reviews, and capability evolution all benefit from the same shape when the task has real uncertainty or risk.\\n\\n## Minimum Contract\\n\\nA well-formed work unit names:\\n\\n- goal: the outcome the user needs\\n- acceptance criteria: what must be true when the work is done\\n- required context: source files, docs, systems, messages, or prior decisions needed for correctness\\n- constraints: permissions, privacy, compatibility, deadlines, ownership, or scope limits\\n- signals or evidence: checks that can confirm progress or falsify assumptions\\n- output artifact: code, docs, proposal, issue, note, draft, or report\\n- verification path: commands, review surfaces, manual checks, or source-of-truth reads\\n- writeback target: where durable learning belongs if the work teaches something reusable\\n\\nIf one of these is missing and the gap blocks correctness, surface the gap early and recover it before moving faster.\\n\\nFor low-risk one-step work, keep the contract implicit. For ambiguous, high-impact, cross-tool, stateful, or multi-step work, make the contract explicit before executing.\\n\\n## Why It Exists\\n\\nWork-unit framing prevents shallow completion. It helps agents avoid:\\n\\n- changing files before understanding the target\\n- treating a weak green signal as proof\\n- losing reusable learning in chat\\n- creating duplicate tasks or proposals\\n- turning one-off preferences into global rules\\n- pushing project-specific details into global capability\\n- producing output faster than the system can review, integrate, or learn from it\\n\\nThe point is not paperwork. The point is to attach machine work to intent, context, evidence, and memory so that useful learning can change future work instead of disappearing into chat history.\\n\\n## How To Use It\\n\\nFor simple tasks, keep the work unit implicit but still verify the result.\\n\\nFor ambiguous, high-impact, or multi-step tasks, make the work unit explicit before executing. A compact form is enough:\\n\\n```text\\nGoal:\\nAcceptance:\\nContext:\\nConstraints:\\nEvidence:\\nArtifact:\\nVerification:\\nWriteback:\\n```\\n\\nUse the smallest framing that makes the task correct. Do not turn every request into paperwork.\\n\\n## Examples\\n\\nCoding:\\n\\n```text\\nGoal: fix the failing login test\\nAcceptance: test passes and no auth regression is introduced\\nContext: failing test output, auth middleware, recent commits\\nConstraints: preserve public API\\nEvidence: focused test, relevant integration test\\nArtifact: code diff and concise summary\\nVerification: command output and changed behavior\\nWriteback: only if the failure exposes stale test or auth guidance\\n```\\n\\nResearch:\\n\\n```text\\nGoal: answer a source-backed product question\\nAcceptance: answer cites current primary sources\\nContext: user question, relevant docs, dates\\nConstraints: distinguish verified facts from inference\\nEvidence: source links and quotes within fair-use limits\\nArtifact: answer or research note\\nVerification: source freshness and consistency check\\nWriteback: durable note if the finding will recur\\n```\\n\\nCapability evolution:\\n\\n```text\\nGoal: decide whether repeated writebacks justify a proposal\\nAcceptance: proposal exists only if evidence repeats or a capability is clearly missing\\nContext: grouped writebacks, target asset, current canonical guidance\\nConstraints: avoid global noise and private leakage\\nEvidence: writeback IDs and affected work units\\nArtifact: accepted proposal, rejected proposal, or no-op note\\nVerification: proposal kind, scope, target, and review artifact\\nWriteback: only for new meta-learning about the evolution process\\n```\\n\\n## Writeback\\n\\nWhen the work reveals durable friction, missing capability, stale guidance, or a repeatable workflow, prefer one strong writeback over many weak ones.\\n\\nUse `fclt ai writeback add ...` when the target asset, scope, and evidence are clear. Use `fclt ai evolve ...` only when repeated signal supports a concrete proposal.\\n","skills/capability-evolution/SKILL.md":"---\\ndescription: Convert repeated writeback into concrete fclt capability proposals.\\ntags: [facult, evolution, writeback]\\n---\\n\\n# capability-evolution\\n\\n## When To Use\\nUse this skill when the same missing guidance, weak loop, or recurring win appears often enough that the AI system itself should probably change.\\n\\nDo not wait for a human operator by default if the signal is already clear and the environment permits local AI runtime state to be updated.\\n\\nUse writeback first when the signal is useful but not yet repeated. Use evolution when accumulated writebacks, repeated tool friction, or a clearly missing capability point at a specific target asset or new capability.\\n\\nThe goal is a governed feedback loop: work creates evidence, evidence produces writeback, repeated writeback becomes a small reviewed proposal, and accepted proposals change future agent behavior.\\n\\n## Scope Decision\\n\\nChoose `project` when the behavior depends on repo-local architecture or workflow.\\n\\nChoose `global` when the behavior is broadly reusable.\\n\\nIf unsure, start at project scope and promote later with evidence.\\n\\nReject global scope when the proposal depends on private examples, one repo\'s architecture, a single user\'s temporary preference, or a workflow that has not repeated.\\n\\n## Working Flow\\n\\n1. Harvest the review window from work logs, commits, runbooks, tool notes, and task-system changes.\\n2. Reconcile each source signal to a writeback, implementation task, local asset, or explicit exclusion.\\n3. Group or summarize repeated signal by asset, kind, and scope.\\n4. Run a read-only evolution assessment for the target when possible.\\n5. Check the current target asset before proposing a change.\\n6. Choose the smallest valid proposal kind and draft it.\\n7. Accept only after the target and scope are correct.\\n8. Apply only when the markdown target is the intended canonical asset.\\n9. Verify the producing loop and grade effectiveness before resolving source writebacks.\\n\\nUse:\\n\\n```bash\\nfclt ai review reconcile --since --until --json\\nfclt ai writeback add ...\\nfclt ai writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai evolve assess --asset --json\\nfclt ai evolve propose\\nfclt ai evolve draft EV-00001\\nfclt ai evolve draft EV-00001 --append \\"tighten the rule with a concrete verification step\\"\\nfclt ai evolve accept EV-00001\\nfclt ai evolve apply EV-00001\\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\\nfclt ai loop activity --json\\nfclt ai loop resolve --json\\nfclt ai loop decide --decision accept \\\\\\n --expected-revision --actor \\\\\\n --approval-ref --approve --json\\n```\\n\\nFor an explicitly enabled scheduled loop, use:\\n\\n```bash\\nfclt ai loop enable --project\\nfclt ai loop status --project --json\\nfclt ai loop run --project --json\\nfclt ai loop disable --project\\n```\\n\\nTreat the durable full queue as the source of truth and the notification delta\\nas a noise-control view. Scheduler registration alone is not proof of a run;\\ncheck observed execution health. The loop may prepare writebacks and proposals,\\nbut canonical apply remains plan-only and external task mutation remains a\\nseparate approved integration.\\n\\nFor a signal-family activity item, resolve its opaque locator before recording\\nan operator decision. Record `accept`, `redirect`, `reject`, or `defer` only\\nwith explicit source approval and the exact current queue revision. The receipt\\nis durable handoff evidence, not implementation authority: accepted work still\\nbelongs in the external task/orchestration system, and fclt must not edit Git,\\ntrackers, canonical capability, or spawn work on the receipt\'s behalf. Refresh\\nactivity after any stale, replayed, moved-root, or duplicate-identity refusal.\\n\\nThe writeback queue alone cannot prove an empty window. Run configured source\\nreconciliation first and preserve its coverage, exclusions, correlations,\\nlinked work, and dispositions. If any source is stale or unavailable, report a\\ndegraded review rather than \\"nothing pending.\\" Implementation tickets remain\\nevidence and task targets; do not turn each ticket into a capability proposal.\\n\\nApplying is not completion. Preserve baseline friction, expected behavior, the producing loop,\\nimplementation tickets, verification evidence, and an effectiveness grade. A recurrence after\\napply is `unchanged` or `regressed` evidence for the same evolution, not a new singleton.\\n\\nFor manually scaffolded background review templates, use:\\n\\n```bash\\nfclt templates init automation learning-review\\nfclt templates init automation evolution-review\\nfclt templates init automation tool-call-audit\\n```\\n\\nIf there is not yet enough repeated signal for evolution, do not stop at a bare \\"no\\". Explain what evidence would change the decision, whether another writeback should be recorded, and the smallest future target if the pattern repeats.\\n\\nDo not create a proposal only to preserve an idea. Preserve the idea as writeback, notes, or task tracking unless it has enough evidence to change capability.\\n\\n## Proposal Kind Selection\\n\\n- `update_asset` for tightening existing guidance\\n- `create_asset` for missing instructions or docs\\n- `extract_snippet` for reusable partial guidance\\n- `add_skill` for reusable workflow instruction\\n- `promote_asset` for project-to-global promotion\\n\\nUse task tracking instead of evolution when the main work is an executable tool or product fix that needs an owner, priority, state, or delivery plan. Use evolution for the reusable instruction, skill, or operating-model change that should survive that fix.\\n\\n## Review Criteria\\n\\nBefore accept/apply, verify:\\n\\n- evidence is repeated or the missing capability is obvious\\n- the proposal targets the smallest affected unit\\n- project/global scope is correct\\n- private or project-specific examples are not leaking into global assets\\n- the patch changes canonical markdown assets, not generated runtime state\\n- the resulting behavior can be verified by reading, rendering, indexing, or running the relevant command\\n\\n## Output Contract\\n- repeated signal\\n- proposed asset change\\n- target scope\\n- evidence\\n- smallest useful next step\\n- approval or no-op rationale\\n- next evidence to collect when no proposal is justified\\n- exact read-only and mutating commands, with approval boundaries\\n","skills/fclt-writeback/SKILL.md":"---\\ndescription: Record and review durable writeback from real agent work.\\ntags: [facult, writeback, learning, feedback-loop]\\n---\\n\\n# fclt-writeback\\n\\n## When To Use\\n\\nUse this skill when work reveals durable friction, missing context, weak verification,\\nstale guidance, repeated success, or a capability gap.\\n\\nWriteback is for preserving reusable signal. It is not for every preference or one-off\\nannoyance.\\n\\n## Workflow\\n\\n1. Choose project scope when the learning depends on a repo, architecture, test harness,\\n or team workflow. Choose global scope for broadly reusable learning.\\n2. Target the smallest affected instruction, snippet, skill, agent, MCP/tool config, or\\n automation.\\n3. Record concrete evidence:\\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- Capture a concise observation, impact, attempted workaround, desired outcome,\\n evidence reference, and sensitivity when those fields add real context.\\n- Never capture hidden chain-of-thought, raw transcripts, unbounded logs,\\n secrets, tokens, or credential-bearing payloads. Reference redacted external\\n evidence instead.\\n- Do not copy private project detail into global writebacks.\\n- Use task tracking for executable product work; use writeback for reusable operating-layer\\n learning.\\n- Hand repeated, target-specific signal to the capability-evolution workflow.\\n\\n## Output\\n\\n- writeback id or no-op rationale\\n- scope and target asset\\n- evidence summary\\n- whether the signal is ready for evolution\\n","skills/project-operating-layer-design/SKILL.md":"---\\ndescription: Design or improve a repo-local .ai operating layer.\\ntags: [facult, project, design]\\n---\\n\\n# project-operating-layer-design\\n\\n## When To Use\\nUse this skill when a project needs its own `.ai/` structure, repo-specific instructions, or local bootstrap guidance.\\n\\nUse it when:\\n\\n- a repo has recurring agent friction that should not become global doctrine\\n- setup or verification steps are repeatedly rediscovered\\n- project skills, agents, MCP definitions, or snippets need a stable source of truth\\n- a repo needs policy for what may be rendered into tool homes\\n- a project should contribute writeback/evolution evidence without committing private review artifacts\\n\\nDo not use it to copy a user\'s private global preferences into a public repo.\\n\\n## Design Rules\\n\\n- Start from the repo\'s real workflows, commands, and risk boundaries.\\n- Keep project-specific guidance in `/.ai`.\\n- Keep generated state, queues, review artifacts, and local machine config out of the repo.\\n- Prefer a few high-leverage instructions or skills over a large generic dump.\\n- Use snippets only for blocks that are reused or independently evolvable.\\n- Make verification and integration paths explicit enough for future agents to run.\\n- Add sync policy only for assets that should render into repo-local tool outputs.\\n\\n## Working Flow\\n\\n1. Inventory existing repo guidance and tool files.\\n2. Identify repeated friction from recent work, issues, reviews, or writebacks.\\n3. Separate project-specific behavior from global/user-owned behavior.\\n4. Propose a minimal `.ai` layout.\\n5. Add or update the smallest useful assets.\\n6. Verify the graph/index and any rendered output.\\n7. Record writeback for reusable learnings that should evolve later.\\n\\n## Output Contract\\n- recommended `.ai/` layout\\n- what stays project-local\\n- what stays global\\n- what should remain generated runtime output only\\n- sync/rendering policy\\n- verification path\\n- privacy or commit-safety risks\\n","snippets/global/baseline.md":"- Preserve existing user changes unless asked to rewrite them.\\n- Prefer small, reviewable diffs and verify meaningful changes before claiming success.\\n- State constraints, risks, and follow-up steps directly.\\n","snippets/global/core/feedback-loops.md":"- For any task, identify the highest-signal feedback loops available.\\n- Prefer loops that can verify progress, falsify weak assumptions, and expose failure early.\\n- Do not rely on a single shallow positive signal if stronger verification exists.\\n- If the available loop is stale, weak, noisy, or easy to game, improve it or say what is missing.\\n- When useful, leave behind a stronger loop than the one you started with.\\n- Treat verification, evaluation, and writeback as part of the work, not cleanup after it.\\n- For work-unit clarification, read ${refs.work_units}.\\n- For verification guidance, read ${refs.verification}.\\n- For integration risk, read ${refs.integration}.\\n- For learning and writeback, read ${refs.learning_writeback}.\\n- For deeper guidance, read ${refs.feedback_loops}.\\n","snippets/global/core/verification.md":"- Treat verification as part of the work, not a final checkbox.\\n- Prefer the strongest available proof that matches the real risk.\\n- Make clear what has actually been verified and what remains assumed.\\n- Distrust shallow green signals when stronger checks are available.\\n- If the current harness is stale, weak, or misleading, say so and improve it where possible.\\n- For deeper guidance, read ${refs.verification}.\\n","snippets/global/core/work-units.md":"- Treat every task as a work unit, not just a request.\\n- A work unit should have a goal, acceptance criteria, required context, constraints, signals or evidence, an output artifact, a verification path, and a writeback target when the work teaches something reusable.\\n- If any of those are missing and the gap blocks correctness, surface it early and try to recover it.\\n- Prefer making the work unit more explicit before increasing execution speed.\\n- If the task is vague, ambiguous, or overloaded, narrow it before acting.\\n- Treat work-unit framing as generally applicable to coding, research, writing, operations, setup, debugging, and capability evolution.\\n- For deeper guidance, read ${refs.work_units}.\\n","snippets/global/core/writeback.md":"- Do not end at output if something important was learned.\\n- Preserve decisions, failures, successes, and reusable signal when they will improve future work.\\n- Prefer writing to a real destination over leaving knowledge in chat.\\n- When useful, leave behind better docs, tests, evals, prompts, notes, or follow-up tasks.\\n- When a high-signal learning clearly points at a canonical asset or durable destination, record a writeback before ending the task.\\n- Prefer one strong writeback over many weak ones.\\n- If you can name the target asset, the expected scope, and the actual signal, use `fclt ai writeback add ...` instead of merely mentioning that writeback would be useful.\\n- If repeated signal is already accumulating, use the `capability-evolution` skill or `fclt ai evolve ...` flow to turn it into a reviewable proposal.\\n- For deeper guidance, read ${refs.learning_writeback}.\\n","snippets/templates/agents-global.md":"# Global Agent Instructions\\n\\nThis template materializes as `AGENTS.global.md` when the operating-model pack is\\ninstalled. It should stay small and composed from snippets. Put detailed\\ndoctrine in instructions, workflow execution in skills, and local/private\\npreferences in user-owned or project-owned assets outside the public pack.\\n\\n## Working mode\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n## Shared instruction sources\\n\\n- For work-unit definition and scope clarification, read ${refs.work_units}.\\n- For identifying, improving, and validating feedback loops, read ${refs.feedback_loops}.\\n- For verification and anti-false-positive checks, read ${refs.verification}.\\n- For checking integration boundaries, read ${refs.integration}.\\n- For learning, decisions, and writeback, read ${refs.learning_writeback}.\\n- For capability evolution, proposal kinds, and `facult ai` workflow, read ${refs.evolution}.\\n- For deciding whether something belongs in global or project scope, read ${refs.project_capability}.\\n- Add private language, coding, or writing refs in local config only when they belong to the user\'s own operating layer.\\n\\n## Layering\\n\\n- Treat this file as the global baseline.\\n- Treat repo-level `AGENTS.md` files as more specific additions layered after this file.\\n- Repo-level files may add or refine project-specific behavior, but they should not weaken global defaults for rigor, verification, or writeback discipline.\\n- If a closer `AGENTS.override.md` exists, follow it as the most specific instructions file in that directory while still preserving the global baseline unless the closer file explicitly tightens it.\\n"}' ) as Record; 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.3\\",\\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 global fclt loop and optionally return an exact no-write project enrollment plan.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: {\\n type: \\"string\\",\\n enum: [\\"global\\", \\"global_and_project\\"],\\n },\\n cwd: { type: \\"string\\" },\\n dryRun: { type: \\"boolean\\" },\\n installCodexPlugin: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_runtime\\",\\n description:\\n \\"Discover, bootstrap, update, or roll back the verified fclt runtime used by this plugin.\\",\\n inputSchema: {\\n type: \\"object\\",\\n additionalProperties: false,\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\"status\\", \\"check\\", \\"policy\\", \\"stage\\", \\"apply\\", \\"rollback\\"],\\n },\\n version: { type: \\"string\\" },\\n expectedSha256: { type: \\"string\\", pattern: \\"^[a-f0-9]{64}$\\" },\\n expectedActiveVersion: { type: \\"string\\" },\\n pinnedVersion: { type: \\"string\\" },\\n clearPin: { type: \\"boolean\\" },\\n updateChecksEnabled: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_capability\\",\\n description:\\n \\"Inspect fclt capability, provenance, templates, snippets, adapters, and managed status without exposing secrets.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"scan\\",\\n \\"inventory\\",\\n \\"list\\",\\n \\"show\\",\\n \\"find\\",\\n \\"graph\\",\\n \\"adapters\\",\\n \\"managed_status\\",\\n \\"templates_list\\",\\n \\"snippet_list\\",\\n \\"snippet_show\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: {\\n type: \\"string\\",\\n enum: [\\n \\"skills\\",\\n \\"mcp\\",\\n \\"agents\\",\\n \\"automations\\",\\n \\"snippets\\",\\n \\"instructions\\",\\n ],\\n },\\n query: { type: \\"string\\" },\\n selector: { type: \\"string\\" },\\n graphMode: { type: \\"string\\", enum: [\\"show\\", \\"deps\\", \\"dependents\\"] },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_workflow\\",\\n description:\\n \\"Run typed writeback and evolution review operations. Canonical apply and cross-scope promotion are deliberately withheld.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"writeback_list\\",\\n \\"writeback_show\\",\\n \\"writeback_group\\",\\n \\"writeback_summarize\\",\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"evolve_assess\\",\\n \\"evolve_list\\",\\n \\"evolve_show\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_verify\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n id: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n category: {\\n type: \\"string\\",\\n enum: [\\"friction\\", \\"opportunity\\", \\"reusable-success\\"],\\n },\\n summary: { type: \\"string\\" },\\n details: { type: \\"string\\", maxLength: 2000 },\\n impact: { type: \\"string\\", maxLength: 1000 },\\n attemptedWorkaround: { type: \\"string\\", maxLength: 1000 },\\n desiredOutcome: { type: \\"string\\", maxLength: 1000 },\\n sensitivity: {\\n type: \\"string\\",\\n enum: [\\"public\\", \\"internal\\", \\"private\\"],\\n },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"array\\", items: { type: \\"string\\" } },\\n confidence: { type: \\"string\\", enum: [\\"low\\", \\"medium\\", \\"high\\"] },\\n by: { type: \\"string\\", enum: [\\"asset\\", \\"kind\\", \\"domain\\"] },\\n issue: { type: \\"string\\" },\\n disposition: {\\n type: \\"string\\",\\n enum: [\\"propose\\", \\"apply-local\\", \\"task\\", \\"resolve-watch\\", \\"defer\\"],\\n },\\n target: { type: \\"string\\" },\\n nextTrigger: { type: \\"string\\" },\\n expectedOutcome: { type: \\"string\\" },\\n append: { type: \\"string\\" },\\n reason: { type: \\"string\\" },\\n byProposal: { type: \\"string\\" },\\n effectiveness: {\\n type: \\"string\\",\\n enum: [\\"improved\\", \\"unchanged\\", \\"regressed\\", \\"inconclusive\\"],\\n },\\n note: { type: \\"string\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_sync\\",\\n description:\\n \\"Inspect managed state or preview a scoped tool sync. Apply and live adoption remain withheld pending transaction-safe APIs.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: { type: \\"string\\", enum: [\\"status\\", \\"preview\\"] },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n tool: { type: \\"string\\" },\\n },\\n required: [\\"action\\", \\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_registry\\",\\n description:\\n \\"Search and verify remote capability, 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_and_project\\" ? [\\"--include-project\\"] : []),\\n ...(apply ? [] : [\\"--dry-run\\"]),\\n ...(args.installCodexPlugin === false ? [\\"--no-codex-plugin\\"] : []),\\n ];\\n }\\n case \\"fclt_capability\\":\\n return capabilityCommand(args);\\n case \\"fclt_workflow\\":\\n return workflowCommand(args);\\n case \\"fclt_sync\\":\\n return syncCommand(args);\\n case \\"fclt_registry\\":\\n return registryCommand(args);\\n case \\"fclt_audit\\":\\n return [\\n \\"audit\\",\\n \\"--non-interactive\\",\\n ...(args.target ? [args.target] : []),\\n ...stringFlag(\\"--severity\\", args.severity),\\n \\"--json\\",\\n ];\\n case \\"fclt_automation\\":\\n if (\\n args.action !== \\"loop_activity\\" &&\\n args.scope !== \\"global\\" &&\\n args.scope !== \\"project\\"\\n ) {\\n throw new Error(`${args.action} requires global or project scope`);\\n }\\n if (args.action === \\"autosync_status\\") {\\n return [\\n \\"autosync\\",\\n \\"status\\",\\n ...(args.tool ? [args.tool] : []),\\n ...scopeArgs(args.scope),\\n ];\\n }\\n if (args.tool) {\\n throw new Error(`${args.action} does not accept tool`);\\n }\\n if (args.action === \\"loop_status\\") {\\n return [\\"ai\\", \\"loop\\", ...scopeArgs(args.scope), \\"status\\", \\"--json\\"];\\n }\\n if (args.action === \\"loop_activity\\") {\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n ...scopeArgs(args.scope),\\n \\"activity\\",\\n ...(args.scope === \\"global\\" || args.scope === \\"project\\"\\n ? []\\n : [\\"--all\\"]),\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"loop_preview\\") {\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n ...scopeArgs(args.scope),\\n \\"run\\",\\n \\"--dry-run\\",\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported automation action: ${args.action}`);\\n case \\"fclt_status\\":\\n return [\\"status\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_doctor\\":\\n return [\\"doctor\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_paths\\":\\n return [\\"paths\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_init_operating_model\\":\\n if (args.dryRun === false && args.approve !== true) {\\n throw new Error(\\n \\"fclt_init_operating_model apply requires approve=true\\"\\n );\\n }\\n if (args.force === true && args.approve !== true) {\\n throw new Error(\\n \\"fclt_init_operating_model force requires approve=true\\"\\n );\\n }\\n return [\\n \\"templates\\",\\n \\"init\\",\\n \\"operating-model\\",\\n ...scopeArgs(args.scope),\\n ...boolFlag(\\"--update\\", args.update),\\n ...(args.dryRun === false ? [] : [\\"--dry-run\\"]),\\n ...boolFlag(\\"--force\\", args.force),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_add\\":\\n requireMutationApproval(name, args);\\n requireString(\\"evidence\\", args.evidence);\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scopeArgs(args.scope),\\n \\"add\\",\\n \\"--kind\\",\\n args.kind,\\n \\"--summary\\",\\n args.summary,\\n ...stringFlag(\\"--asset\\", args.asset),\\n ...stringFlag(\\"--category\\", args.category),\\n ...stringFlag(\\"--details\\", args.details),\\n ...stringFlag(\\"--impact\\", args.impact),\\n ...stringFlag(\\"--attempted-workaround\\", args.attemptedWorkaround),\\n ...stringFlag(\\"--desired-outcome\\", args.desiredOutcome),\\n ...stringFlag(\\"--sensitivity\\", args.sensitivity),\\n ...stringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_review\\": {\\n const mode = args.mode || \\"list\\";\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scopeArgs(args.scope),\\n mode,\\n ...stringFlag(\\"--by\\", args.by),\\n ];\\n }\\n case \\"fclt_evolve\\": {\\n const action = args.action || \\"list\\";\\n if ([\\"propose\\", \\"draft\\", \\"review\\"].includes(action)) {\\n requireMutationApproval(`fclt_evolve ${action}`, args);\\n }\\n if (action === \\"propose\\") {\\n requireString(\\"asset\\", args.asset);\\n }\\n if ([\\"draft\\", \\"review\\", \\"show\\"].includes(action)) {\\n requireString(\\"id\\", args.id);\\n }\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scopeArgs(args.scope),\\n action,\\n ...(action === \\"assess\\" || action === \\"propose\\"\\n ? stringFlag(\\"--asset\\", args.asset)\\n : []),\\n ...(args.id ? [args.id] : []),\\n ...(action === \\"assess\\" ? [\\"--json\\"] : []),\\n ];\\n }\\n default:\\n throw new Error(`Unknown tool: ${name}`);\\n }\\n}\\n\\nfunction operationMetadata(name, args, command) {\\n const action =\\n name === \\"fclt_writeback_add\\"\\n ? \\"writeback_add\\"\\n : name === \\"fclt_evolve\\"\\n ? `evolve_${args.action || \\"list\\"}`\\n : args.action || name;\\n const reviewActions = new Set([\\n \\"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`\\n- discovering repositories and enrolling a reviewed minimal project layer\\n- installing or refreshing the built-in operating-model pack\\n- checking setup health with `doctor`\\n- finding canonical, generated, runtime, and review paths\\n\\n## Workflow\\n\\n1. Bootstrap the global loop with one idempotent command:\\n\\n```bash\\nfclt setup\\n```\\n\\nThis initializes or safely updates global capability, prepares writeback/evolution review state,\\nand installs the Codex plugin when Codex is available. It does not initialize the current\\nrepository.\\n\\n2. Inspect runtime selection and compatibility with `fclt_runtime` action\\n `status`. Report the selected executable, version, source, protocol\\n compatibility, and fresh-session state.\\n\\nIf no compatible runtime is available, use the staged lifecycle:\\n\\n- `check` is read-only\\n- `stage` requires an explicit version and approval, but does not activate it\\n- `apply` requires approval plus the staged checksum precondition\\n- `rollback` verifies and restores the retained prior runtime\\n\\nNever curl-pipe code, use an unverified mutable URL, or replace an existing\\nglobal installation silently.\\n\\n3. Check current setup state and exact repair actions:\\n\\n```bash\\nfclt --version\\nfclt paths --json\\nfclt doctor --json\\n```\\n\\nThrough MCP, call `fclt_setup` with an explicit `global` or\\n`global_and_project` scope. Project setup also requires the exact `cwd`.\\nPreview is the default; apply requires both `dryRun: false` and\\n`approve: true`. `global_and_project` returns a no-write project enrollment\\nplan; project application still uses the typed CLI plan-hash contract below.\\n\\n4. For advanced manual recovery, initialize global capability when missing:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n5. Discover candidate repositories only beneath explicit roots:\\n\\n```bash\\nfclt projects discover --root ~/dev --since 30d --json\\n```\\n\\nDiscovery is bounded and read-only. Review duplicate clone/worktree groups,\\ndirty state, existing guidance, stable portfolio identity, and the separate\\ncheckout/worktree execution identity before selecting a project. Never\\nbulk-enroll the discovery result.\\n\\n6. Preview the exact minimal project enrollment plan:\\n\\n```bash\\nfclt project init --project-root /path/to/repo --json\\n```\\n\\nThe minimal layer is `.ai/.gitignore` plus `.ai/config.toml`. It does not\\ninstall the operating pack, enable managed rendering, schedule a loop, or copy\\n`AGENTS.md`/`CLAUDE.md`. Review every canonical, generated, and machine-local\\nwrite plus the rollback command.\\n\\nIf project guidance should be adopted, name each canonical file explicitly:\\n\\n```bash\\nfclt project init --project-root /path/to/repo \\\\\\n --guidance AGENTS.md --json\\n```\\n\\nGuidance adoption is reference-only. fclt previews the full content and hash,\\nand refuses untracked, modified, `assume-unchanged`, `skip-worktree`,\\nsecret-shaped, or machine-path-bearing input. Cleanliness requires identical\\nworktree, index, and `HEAD` blobs.\\n\\n7. Apply only the unchanged reviewed plan:\\n\\n```bash\\nfclt project init --project-root /path/to/repo \\\\\\n --apply --plan-sha --json\\n```\\n\\nIf options, source files, or preconditions change, discard the old hash and\\npreview again. Apply serializes portfolio registry mutations, isolates\\nlocation-bearing generated state per checkout/worktree, refuses symlinked\\ngenerated targets, and publishes the receipt only after the registry commit.\\n\\n8. Inspect health, coverage, and lifecycle:\\n\\n```bash\\nfclt projects status --root /path/to/repo --json\\nfclt project disable --project-root /path/to/repo --json\\nfclt project rollback --receipt --json\\n```\\n\\nDisable and remove decisions preserve canonical files, receipts, and review\\nhistory. Rollback previews by default and refuses drift.\\n\\n9. Install the full operating pack only when explicitly requested:\\n\\n```bash\\nfclt templates init operating-model --project --dry-run\\nfclt templates init operating-model --project\\n```\\n\\nThis is distinct from minimal enrollment and does not seed project\\n`AGENTS.global.md` from repository guidance.\\n\\n10. Refresh global pack defaults non-destructively:\\n\\n```bash\\nfclt templates init operating-model --global --update --dry-run\\nfclt templates init operating-model --global --update\\n```\\n\\n11. Use `--force` only when the user explicitly wants to replace local edits.\\n\\n## Rules\\n\\n- Preserve existing `AGENTS.md`, `CLAUDE.md`, and `AGENTS.global.md` guidance.\\n- Never infer guidance adoption from filenames or copy repository guidance into\\n `.ai/AGENTS.global.md`.\\n- Treat root `AGENTS.md` or `CLAUDE.md` as canonical repository guidance unless\\n the user explicitly chooses another tracked, clean source.\\n- Write the protective `.ai/.gitignore` before generated state.\\n- Keep canonical project files separate from machine-local generated indexes,\\n registries, receipts, and scheduling state.\\n- Keep minimal enrollment separate from the full operating pack and managed\\n rendering.\\n- Treat `doctor --json` issues as setup facts, not user-facing blame.\\n- Treat Codex plugin registration as weaker evidence than fresh-session tool discovery.\\n- Treat external trackers as separate integrations. Core readiness depends only on configured local evidence coverage, not a vendor plugin or token.\\n- Prefer temp-root smoke tests for install/update behavior.\\n- Do not enable managed rendering unless the user wants fclt to write tool homes.\\n- Preview before mutation and state the exact global/project/plugin target.\\n- Do not report a staged runtime or installed plugin as active until the active\\n handshake and fresh-session discovery have been verified.\\n\\n## Output\\n\\n- current installed version\\n- setup health\\n- paths that matter\\n- commands run\\n- what changed\\n- problem, evidence, reason, target, risk, and expected outcome\\n- verification performed and its actual result\\n- assumptions and fresh-session state\\n- exact undo or rollback path\\n- what still needs approval\\n","skills/fclt-writeback/SKILL.md":"---\\nname: fclt-writeback\\ndescription: Record and review fclt writebacks from real agent work.\\ntags: [fclt, writeback, learning, feedback-loop]\\n---\\n\\n# fclt-writeback\\n\\n## When To Use\\nUse this skill when work reveals durable friction, missing context, weak verification, stale guidance, repeated success, or a capability gap.\\n\\nWriteback is for preserving signal. It is not for every preference or one-off annoyance.\\n\\n## 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.3\\",\\n \\"description\\": \\"Codex workflows and MCP tools for evidence reconciliation, writeback, evolution, and outcome review.\\",\\n \\"author\\": {\\n \\"name\\": \\"Hack Dance\\",\\n \\"url\\": \\"https://hack.dance\\"\\n },\\n \\"license\\": \\"MIT\\",\\n \\"keywords\\": [\\n \\"fclt\\",\\n \\"facult\\",\\n \\"codex\\",\\n \\"skills\\",\\n \\"mcp\\",\\n \\"writeback\\",\\n \\"evolution\\"\\n ],\\n \\"skills\\": \\"./skills/\\",\\n \\"mcpServers\\": \\"./.mcp.json\\",\\n \\"interface\\": {\\n \\"displayName\\": \\"fclt\\",\\n \\"shortDescription\\": \\"Feedback loops for AI work\\",\\n \\"longDescription\\": \\"Capture signal from real agent work, reconcile configured evidence, turn repeated findings into reviewed capability changes, and verify whether those changes improved the work that produced them.\\",\\n \\"developerName\\": \\"Hack Dance\\",\\n \\"category\\": \\"Productivity\\",\\n \\"capabilities\\": [\\"Read\\", \\"Write\\", \\"MCP\\"],\\n \\"defaultPrompt\\": [\\n \\"Use fclt to check this repo\'s AI capability setup.\\",\\n \\"Reconcile the configured evidence window and explain its coverage.\\",\\n \\"Record useful writeback from this work and review any repeated signal.\\"\\n ],\\n \\"brandColor\\": \\"#166534\\",\\n \\"composerIcon\\": \\"./assets/fclt-mark.png\\",\\n \\"logo\\": \\"./assets/fclt-mark.png\\"\\n }\\n}\\n",".mcp.json":"{\\n \\"mcpServers\\": {\\n \\"fclt\\": {\\n \\"command\\": \\"node\\",\\n \\"args\\": [\\"./scripts/fclt-mcp.cjs\\"],\\n \\"env\\": {\\n \\"FCLT_BIN\\": \\"fclt\\"\\n },\\n \\"cwd\\": \\".\\"\\n }\\n }\\n}\\n","scripts/fclt-mcp.cjs":"#!/usr/bin/env node\\n\\"use strict\\";\\n\\nconst fs = require(\\"node:fs\\");\\nconst os = require(\\"node:os\\");\\nconst path = require(\\"node:path\\");\\nconst runtime = require(\\"./fclt-runtime.cjs\\");\\n\\nconst DEFAULT_TIMEOUT_MS = Number(process.env.FCLT_MCP_TIMEOUT_MS || 60_000);\\nconst ACTIVITY_ACTION_DECIDE_CAPABILITY = \\"activity-action-decide-v1\\";\\nconst ACTIVITY_ACTION_RESOLVE_CAPABILITY = \\"activity-action-resolve-v1\\";\\nconst AUDIT_READ_ONLY_CAPABILITY = \\"audit-read-only-v1\\";\\nconst CONTENT_LENGTH_RE = /Content-Length:\\\\s*(\\\\d+)/i;\\nconst PLUGIN_ROOT = path.resolve(__dirname, \\"..\\");\\n\\nconst tools = [\\n {\\n name: \\"fclt_setup\\",\\n description:\\n \\"Bootstrap or repair the global fclt loop and optionally return an exact no-write project enrollment plan.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: {\\n type: \\"string\\",\\n enum: [\\"global\\", \\"global_and_project\\"],\\n },\\n cwd: { type: \\"string\\" },\\n dryRun: { type: \\"boolean\\" },\\n installCodexPlugin: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_runtime\\",\\n description:\\n \\"Discover, bootstrap, update, or roll back the verified fclt runtime used by this plugin.\\",\\n inputSchema: {\\n type: \\"object\\",\\n additionalProperties: false,\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\"status\\", \\"check\\", \\"policy\\", \\"stage\\", \\"apply\\", \\"rollback\\"],\\n },\\n version: { type: \\"string\\" },\\n expectedSha256: { type: \\"string\\", pattern: \\"^[a-f0-9]{64}$\\" },\\n expectedActiveVersion: { type: \\"string\\" },\\n pinnedVersion: { type: \\"string\\" },\\n clearPin: { type: \\"boolean\\" },\\n updateChecksEnabled: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_capability\\",\\n description:\\n \\"Inspect fclt capability, provenance, templates, snippets, adapters, and managed status without exposing secrets.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"scan\\",\\n \\"inventory\\",\\n \\"list\\",\\n \\"show\\",\\n \\"find\\",\\n \\"graph\\",\\n \\"adapters\\",\\n \\"managed_status\\",\\n \\"templates_list\\",\\n \\"snippet_list\\",\\n \\"snippet_show\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: {\\n type: \\"string\\",\\n enum: [\\n \\"skills\\",\\n \\"mcp\\",\\n \\"agents\\",\\n \\"automations\\",\\n \\"snippets\\",\\n \\"instructions\\",\\n ],\\n },\\n query: { type: \\"string\\" },\\n selector: { type: \\"string\\" },\\n graphMode: { type: \\"string\\", enum: [\\"show\\", \\"deps\\", \\"dependents\\"] },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_workflow\\",\\n description:\\n \\"Run typed writeback and evolution review operations. Canonical apply and cross-scope promotion are deliberately withheld.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"writeback_list\\",\\n \\"writeback_show\\",\\n \\"writeback_group\\",\\n \\"writeback_summarize\\",\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"evolve_assess\\",\\n \\"evolve_list\\",\\n \\"evolve_show\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_verify\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n id: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n category: {\\n type: \\"string\\",\\n enum: [\\"friction\\", \\"opportunity\\", \\"reusable-success\\"],\\n },\\n summary: { type: \\"string\\" },\\n details: { type: \\"string\\", maxLength: 2000 },\\n impact: { type: \\"string\\", maxLength: 1000 },\\n attemptedWorkaround: { type: \\"string\\", maxLength: 1000 },\\n desiredOutcome: { type: \\"string\\", maxLength: 1000 },\\n sensitivity: {\\n type: \\"string\\",\\n enum: [\\"public\\", \\"internal\\", \\"private\\"],\\n },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"array\\", items: { type: \\"string\\" } },\\n confidence: { type: \\"string\\", enum: [\\"low\\", \\"medium\\", \\"high\\"] },\\n by: { type: \\"string\\", enum: [\\"asset\\", \\"kind\\", \\"domain\\"] },\\n issue: { type: \\"string\\" },\\n disposition: {\\n type: \\"string\\",\\n enum: [\\"propose\\", \\"apply-local\\", \\"task\\", \\"resolve-watch\\", \\"defer\\"],\\n },\\n target: { type: \\"string\\" },\\n nextTrigger: { type: \\"string\\" },\\n expectedOutcome: { type: \\"string\\" },\\n append: { type: \\"string\\" },\\n reason: { type: \\"string\\" },\\n byProposal: { type: \\"string\\" },\\n effectiveness: {\\n type: \\"string\\",\\n enum: [\\"improved\\", \\"unchanged\\", \\"regressed\\", \\"inconclusive\\"],\\n },\\n note: { type: \\"string\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_sync\\",\\n description:\\n \\"Inspect managed state or preview a scoped tool sync. Apply and live adoption remain withheld pending transaction-safe APIs.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: { type: \\"string\\", enum: [\\"status\\", \\"preview\\"] },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n tool: { type: \\"string\\" },\\n },\\n required: [\\"action\\", \\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_registry\\",\\n description:\\n \\"Search and verify remote capability, run typed source reconciliation reviews, resolve one opaque activity action locator, or record one revision-bound signal decision. Canonical and external mutation remains withheld.\\",\\n inputSchema: {\\n oneOf: [\\n {\\n type: \\"object\\",\\n properties: {\\n action: { const: \\"activity_decide\\" },\\n locator: {\\n type: \\"string\\",\\n pattern: \\"^fclt-act-v[0-9]+\\\\\\\\.[a-f0-9]{64}\\\\\\\\.[a-f0-9]{64}$\\",\\n },\\n decision: {\\n type: \\"string\\",\\n enum: [\\"accept\\", \\"redirect\\", \\"reject\\", \\"defer\\"],\\n },\\n expectedRevision: { type: \\"integer\\", minimum: 1 },\\n actor: {\\n type: \\"string\\",\\n pattern: \\"^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$\\",\\n },\\n approvalReference: { type: \\"string\\", minLength: 1, maxLength: 500 },\\n note: { type: \\"string\\", minLength: 1, maxLength: 1000 },\\n redirectTarget: { type: \\"string\\", minLength: 1, maxLength: 500 },\\n approve: { const: true },\\n },\\n required: [\\n \\"action\\",\\n \\"locator\\",\\n \\"decision\\",\\n \\"expectedRevision\\",\\n \\"actor\\",\\n \\"approve\\",\\n ],\\n allOf: [\\n {\\n oneOf: [\\n {\\n required: [\\"approvalReference\\"],\\n not: { required: [\\"note\\"] },\\n },\\n {\\n required: [\\"note\\"],\\n not: { required: [\\"approvalReference\\"] },\\n },\\n ],\\n },\\n ],\\n additionalProperties: false,\\n },\\n {\\n type: \\"object\\",\\n properties: {\\n action: { const: \\"activity_resolve\\" },\\n locator: {\\n type: \\"string\\",\\n pattern: \\"^fclt-act-v[0-9]+\\\\\\\\.[a-f0-9]{64}\\\\\\\\.[a-f0-9]{64}$\\",\\n },\\n },\\n required: [\\"action\\", \\"locator\\"],\\n additionalProperties: false,\\n },\\n {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"search\\",\\n \\"verify_source\\",\\n \\"source_list\\",\\n \\"install_preview\\",\\n \\"update_check\\",\\n \\"reconcile_status\\",\\n \\"reconcile\\",\\n ],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n query: { type: \\"string\\" },\\n source: { type: \\"string\\" },\\n item: { type: \\"string\\" },\\n as: { type: \\"string\\" },\\n since: {\\n type: \\"string\\",\\n pattern:\\n \\"^\\\\\\\\d{4}-\\\\\\\\d{2}-\\\\\\\\d{2}(?:T\\\\\\\\d{2}:\\\\\\\\d{2}:\\\\\\\\d{2}(?:\\\\\\\\.\\\\\\\\d+)?(?:Z|[+-]\\\\\\\\d{2}:\\\\\\\\d{2}))?$\\",\\n },\\n until: {\\n type: \\"string\\",\\n pattern:\\n \\"^\\\\\\\\d{4}-\\\\\\\\d{2}-\\\\\\\\d{2}(?:T\\\\\\\\d{2}:\\\\\\\\d{2}:\\\\\\\\d{2}(?:\\\\\\\\.\\\\\\\\d+)?(?:Z|[+-]\\\\\\\\d{2}:\\\\\\\\d{2}))?$\\",\\n },\\n sourceIds: {\\n type: \\"array\\",\\n items: {\\n type: \\"string\\",\\n pattern: \\"^[A-Za-z0-9][A-Za-z0-9._-]*$\\",\\n },\\n },\\n incremental: { type: \\"boolean\\" },\\n },\\n required: [\\"action\\"],\\n additionalProperties: false,\\n },\\n ],\\n },\\n },\\n {\\n name: \\"fclt_audit\\",\\n description:\\n \\"Run a structured, redacted, non-interactive fclt security audit with no report or index writes.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: { type: \\"string\\", enum: [\\"scan\\"] },\\n cwd: { type: \\"string\\" },\\n target: { type: \\"string\\" },\\n severity: {\\n type: \\"string\\",\\n enum: [\\"critical\\", \\"high\\", \\"medium\\", \\"low\\"],\\n },\\n },\\n required: [\\"action\\"],\\n },\\n },\\n {\\n name: \\"fclt_automation\\",\\n description:\\n \\"Read one aggregate activity set across all configured loops by default, or inspect one explicit loop scope. Schedule and canonical mutation remain CLI-only.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n action: {\\n type: \\"string\\",\\n enum: [\\n \\"autosync_status\\",\\n \\"loop_status\\",\\n \\"loop_activity\\",\\n \\"loop_preview\\",\\n ],\\n },\\n scope: {\\n type: \\"string\\",\\n enum: [\\"all\\", \\"global\\", \\"project\\"],\\n },\\n cwd: { type: \\"string\\" },\\n tool: { type: \\"string\\" },\\n },\\n required: [\\"action\\"],\\n oneOf: [\\n {\\n properties: {\\n action: { const: \\"loop_activity\\" },\\n scope: {\\n type: \\"string\\",\\n enum: [\\"all\\", \\"global\\", \\"project\\"],\\n default: \\"all\\",\\n },\\n },\\n required: [\\"action\\"],\\n },\\n {\\n properties: {\\n action: {\\n enum: [\\"autosync_status\\", \\"loop_status\\", \\"loop_preview\\"],\\n },\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n },\\n required: [\\"action\\", \\"scope\\"],\\n },\\n ],\\n },\\n },\\n {\\n name: \\"fclt_status\\",\\n description:\\n \\"Return fclt status for the current, global, or project scope.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_doctor\\",\\n description: \\"Run read-only fclt doctor checks and return JSON output.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_paths\\",\\n description: \\"Return canonical, generated, review, and runtime fclt paths.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_init_operating_model\\",\\n description: \\"Install or update the built-in operating-model pack.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n update: { type: \\"boolean\\" },\\n dryRun: { type: \\"boolean\\" },\\n force: { type: \\"boolean\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\"],\\n },\\n },\\n {\\n name: \\"fclt_writeback_add\\",\\n description: \\"Record a durable fclt writeback with evidence.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n category: {\\n type: \\"string\\",\\n enum: [\\"friction\\", \\"opportunity\\", \\"reusable-success\\"],\\n },\\n summary: { type: \\"string\\" },\\n details: { type: \\"string\\", maxLength: 2000 },\\n impact: { type: \\"string\\", maxLength: 1000 },\\n attemptedWorkaround: { type: \\"string\\", maxLength: 1000 },\\n desiredOutcome: { type: \\"string\\", maxLength: 1000 },\\n sensitivity: {\\n type: \\"string\\",\\n enum: [\\"public\\", \\"internal\\", \\"private\\"],\\n },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"string\\" },\\n confidence: {\\n type: \\"string\\",\\n enum: [\\"low\\", \\"medium\\", \\"high\\"],\\n },\\n approve: { type: \\"boolean\\" },\\n },\\n required: [\\"scope\\", \\"kind\\", \\"summary\\", \\"evidence\\", \\"approve\\"],\\n },\\n },\\n {\\n name: \\"fclt_writeback_review\\",\\n description: \\"List, group, or summarize current fclt writebacks.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n mode: { type: \\"string\\", enum: [\\"list\\", \\"group\\", \\"summarize\\"] },\\n by: { type: \\"string\\" },\\n },\\n },\\n },\\n {\\n name: \\"fclt_evolve\\",\\n description:\\n \\"Assess, list, propose, draft, or review fclt evolution proposals.\\",\\n inputSchema: {\\n type: \\"object\\",\\n properties: {\\n scope: { type: \\"string\\", enum: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n action: {\\n type: \\"string\\",\\n enum: [\\"assess\\", \\"list\\", \\"propose\\", \\"draft\\", \\"review\\", \\"show\\"],\\n },\\n id: { type: \\"string\\" },\\n asset: { type: \\"string\\" },\\n approve: { type: \\"boolean\\" },\\n },\\n },\\n },\\n];\\n\\nfor (const tool of tools) {\\n if (!tool.inputSchema.oneOf || tool.inputSchema.properties) {\\n tool.inputSchema.additionalProperties = false;\\n }\\n}\\n\\nfunction isPlainObject(value) {\\n return Boolean(value) && typeof value === \\"object\\" && !Array.isArray(value);\\n}\\n\\nfunction validateToolArguments(name, args) {\\n if (!isPlainObject(args)) {\\n throw new Error(`${name} arguments must be an object`);\\n }\\n const tool = tools.find((entry) => entry.name === name);\\n if (!tool) {\\n throw new Error(`Unknown tool: ${name}`);\\n }\\n const rootSchema = tool.inputSchema;\\n const schema =\\n rootSchema.oneOf && !rootSchema.properties\\n ? rootSchema.oneOf.find((branch) => {\\n const action = branch.properties?.action;\\n return (\\n action?.const === args.action || action?.enum?.includes(args.action)\\n );\\n })\\n : rootSchema;\\n if (!schema) {\\n throw new Error(`${name}.action is not an allowed value`);\\n }\\n const properties = schema.properties || {};\\n const unknown = Object.keys(args).filter((key) => !(key in properties));\\n if (unknown.length > 0) {\\n throw new Error(\\n `${name} received unknown argument fields: ${unknown.join(\\", \\")}`\\n );\\n }\\n for (const required of schema.required || []) {\\n if (!(required in args)) {\\n throw new Error(`${name} requires ${required}`);\\n }\\n }\\n for (const constraint of schema.allOf || []) {\\n if (Array.isArray(constraint.oneOf)) {\\n const matches = constraint.oneOf.filter((branch) => {\\n const hasRequired = (branch.required || []).every((key) => key in args);\\n const violatesNot = branch.not?.required\\n ? branch.not.required.every((key) => key in args)\\n : false;\\n return hasRequired && !violatesNot;\\n });\\n if (matches.length !== 1) {\\n throw new Error(\\n `${name} arguments do not match exactly one allowed shape`\\n );\\n }\\n }\\n }\\n for (const [key, value] of Object.entries(args)) {\\n const property = properties[key];\\n if (property.const !== undefined && value !== property.const) {\\n throw new Error(`${name}.${key} is not an allowed value`);\\n }\\n if (!property.type) {\\n continue;\\n }\\n const validType =\\n property.type === \\"array\\"\\n ? Array.isArray(value)\\n : property.type === \\"object\\"\\n ? isPlainObject(value)\\n : property.type === \\"integer\\"\\n ? Number.isSafeInteger(value)\\n : typeof value === property.type;\\n if (!validType) {\\n throw new Error(`${name}.${key} must be ${property.type}`);\\n }\\n if (property.enum && !property.enum.includes(value)) {\\n throw new Error(`${name}.${key} is not an allowed value`);\\n }\\n if (\\n typeof value === \\"string\\" &&\\n ((property.minLength !== undefined &&\\n value.length < property.minLength) ||\\n (property.maxLength !== undefined && value.length > property.maxLength))\\n ) {\\n throw new Error(`${name}.${key} has an invalid length`);\\n }\\n if (\\n typeof value === \\"number\\" &&\\n property.minimum !== undefined &&\\n value < property.minimum\\n ) {\\n throw new Error(`${name}.${key} is below the minimum`);\\n }\\n if (property.pattern && !new RegExp(property.pattern).test(value)) {\\n throw new Error(`${name}.${key} has an invalid format`);\\n }\\n if (\\n property.type === \\"array\\" &&\\n property.items?.type &&\\n value.some((item) => typeof item !== property.items.type)\\n ) {\\n throw new Error(`${name}.${key} contains an invalid item`);\\n }\\n if (\\n property.type === \\"array\\" &&\\n property.items?.pattern &&\\n value.some((item) => !new RegExp(property.items.pattern).test(item))\\n ) {\\n throw new Error(`${name}.${key} contains an invalid item`);\\n }\\n }\\n}\\n\\nfunction scopeArgs(scope) {\\n if (scope === \\"global\\") {\\n return [\\"--global\\"];\\n }\\n if (scope === \\"project\\") {\\n return [\\"--project\\"];\\n }\\n return [];\\n}\\n\\nfunction boolFlag(name, value) {\\n return value ? [name] : [];\\n}\\n\\nfunction stringFlag(name, value) {\\n return typeof value === \\"string\\" && value.trim() ? [name, value] : [];\\n}\\n\\nfunction repeatedStringFlag(name, values) {\\n return Array.isArray(values)\\n ? values.flatMap((value) => stringFlag(name, value))\\n : [];\\n}\\n\\nfunction requireString(name, value) {\\n if (typeof value !== \\"string\\" || !value.trim()) {\\n throw new Error(`${name} is required`);\\n }\\n return value;\\n}\\n\\nfunction requireMutationApproval(name, args) {\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(`${name} requires an explicit global or project scope`);\\n }\\n if (args.approve !== true) {\\n throw new Error(`${name} requires approve=true`);\\n }\\n}\\n\\nfunction capabilityCommand(args) {\\n const action = args.action;\\n if (action === \\"scan\\") {\\n return [\\"scan\\", \\"--json\\"];\\n }\\n if (action === \\"inventory\\") {\\n return [\\"inventory\\", ...scopeArgs(args.scope), \\"--json\\"];\\n }\\n if (action === \\"list\\") {\\n return [\\"list\\", args.kind || \\"skills\\", ...scopeArgs(args.scope), \\"--json\\"];\\n }\\n if (action === \\"show\\") {\\n return [\\n \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n ...scopeArgs(args.scope),\\n ];\\n }\\n if (action === \\"find\\") {\\n return [\\n \\"find\\",\\n requireString(\\"query\\", args.query),\\n ...scopeArgs(args.scope),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"graph\\") {\\n return [\\n \\"graph\\",\\n args.graphMode || \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n ...scopeArgs(args.scope),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"adapters\\") {\\n return [\\"adapters\\", \\"--json\\"];\\n }\\n if (action === \\"managed_status\\") {\\n return [\\"managed\\", ...scopeArgs(args.scope)];\\n }\\n if (action === \\"templates_list\\") {\\n return [\\"templates\\", \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"snippet_list\\") {\\n return [\\"snippets\\", \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"snippet_show\\") {\\n return [\\n \\"snippets\\",\\n \\"show\\",\\n requireString(\\"selector\\", args.selector),\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported capability action: ${action}`);\\n}\\n\\nconst WORKFLOW_MUTATIONS = new Set([\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"writeback_dismiss\\",\\n \\"writeback_promote\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_accept\\",\\n \\"evolve_reject\\",\\n \\"evolve_supersede\\",\\n \\"evolve_verify\\",\\n]);\\n\\nconst WORKFLOW_ACTION_FIELDS = {\\n writeback_list: [],\\n writeback_show: [\\"id\\"],\\n writeback_group: [\\"by\\"],\\n writeback_summarize: [\\"by\\"],\\n writeback_add: [\\n \\"kind\\",\\n \\"category\\",\\n \\"summary\\",\\n \\"details\\",\\n \\"impact\\",\\n \\"attemptedWorkaround\\",\\n \\"desiredOutcome\\",\\n \\"sensitivity\\",\\n \\"asset\\",\\n \\"evidence\\",\\n \\"confidence\\",\\n \\"approve\\",\\n ],\\n writeback_link: [\\"id\\", \\"issue\\", \\"approve\\"],\\n writeback_disposition: [\\n \\"id\\",\\n \\"disposition\\",\\n \\"target\\",\\n \\"nextTrigger\\",\\n \\"expectedOutcome\\",\\n \\"approve\\",\\n ],\\n evolve_assess: [\\"asset\\"],\\n evolve_list: [],\\n evolve_show: [\\"id\\"],\\n evolve_propose: [\\"asset\\", \\"approve\\"],\\n evolve_draft: [\\"id\\", \\"append\\", \\"approve\\"],\\n evolve_review: [\\"id\\", \\"approve\\"],\\n evolve_verify: [\\"id\\", \\"effectiveness\\", \\"evidence\\", \\"note\\", \\"approve\\"],\\n};\\n\\nfunction requireOnlyWorkflowFields(args) {\\n const fields = WORKFLOW_ACTION_FIELDS[args.action];\\n if (!fields) {\\n return;\\n }\\n const allowed = new Set([\\"action\\", \\"scope\\", \\"cwd\\", ...fields]);\\n const unexpected = Object.keys(args).filter((key) => !allowed.has(key));\\n if (unexpected.length > 0) {\\n throw new Error(\\n `${args.action} received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n}\\n\\nfunction workflowCommand(args) {\\n const action = args.action;\\n requireOnlyWorkflowFields(args);\\n if (WORKFLOW_MUTATIONS.has(action)) {\\n requireMutationApproval(action, args);\\n }\\n const scope = scopeArgs(args.scope);\\n if (action === \\"writeback_list\\") {\\n return [\\"ai\\", \\"writeback\\", ...scope, \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"writeback_show\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"show\\",\\n requireString(\\"id\\", args.id),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_group\\" || action === \\"writeback_summarize\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n action === \\"writeback_group\\" ? \\"group\\" : \\"summarize\\",\\n ...stringFlag(\\"--by\\", args.by),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_add\\") {\\n if (!Array.isArray(args.evidence) || args.evidence.length === 0) {\\n throw new Error(\\"writeback_add requires at least one evidence reference\\");\\n }\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"add\\",\\n \\"--kind\\",\\n requireString(\\"kind\\", args.kind),\\n \\"--summary\\",\\n requireString(\\"summary\\", args.summary),\\n ...stringFlag(\\"--asset\\", args.asset),\\n ...stringFlag(\\"--category\\", args.category),\\n ...stringFlag(\\"--details\\", args.details),\\n ...stringFlag(\\"--impact\\", args.impact),\\n ...stringFlag(\\"--attempted-workaround\\", args.attemptedWorkaround),\\n ...stringFlag(\\"--desired-outcome\\", args.desiredOutcome),\\n ...stringFlag(\\"--sensitivity\\", args.sensitivity),\\n ...repeatedStringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"writeback_link\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"link\\",\\n requireString(\\"id\\", args.id),\\n \\"--issue\\",\\n requireString(\\"issue\\", args.issue),\\n ];\\n }\\n if (action === \\"writeback_disposition\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n \\"disposition\\",\\n requireString(\\"id\\", args.id),\\n \\"--type\\",\\n requireString(\\"disposition\\", args.disposition),\\n ...stringFlag(\\"--target\\", args.target),\\n ...stringFlag(\\"--next-trigger\\", args.nextTrigger),\\n ...stringFlag(\\"--expected-outcome\\", args.expectedOutcome),\\n ];\\n }\\n if (action === \\"writeback_dismiss\\" || action === \\"writeback_promote\\") {\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scope,\\n action === \\"writeback_dismiss\\" ? \\"dismiss\\" : \\"promote\\",\\n requireString(\\"id\\", args.id),\\n ];\\n }\\n if (action === \\"evolve_assess\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"assess\\",\\n ...stringFlag(\\"--asset\\", args.asset),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_list\\") {\\n return [\\"ai\\", \\"evolve\\", ...scope, \\"list\\", \\"--json\\"];\\n }\\n if (action === \\"evolve_show\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"show\\",\\n requireString(\\"id\\", args.id),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_propose\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"propose\\",\\n \\"--asset\\",\\n requireString(\\"asset\\", args.asset),\\n \\"--json\\",\\n ];\\n }\\n if (action === \\"evolve_draft\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"draft\\",\\n requireString(\\"id\\", args.id),\\n ...stringFlag(\\"--append\\", args.append),\\n ];\\n }\\n if (action === \\"evolve_review\\" || action === \\"evolve_accept\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n action === \\"evolve_review\\" ? \\"review\\" : \\"accept\\",\\n requireString(\\"id\\", args.id),\\n ];\\n }\\n if (action === \\"evolve_reject\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"reject\\",\\n requireString(\\"id\\", args.id),\\n \\"--reason\\",\\n requireString(\\"reason\\", args.reason),\\n ];\\n }\\n if (action === \\"evolve_supersede\\") {\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"supersede\\",\\n requireString(\\"id\\", args.id),\\n \\"--by\\",\\n requireString(\\"byProposal\\", args.byProposal),\\n ];\\n }\\n if (action === \\"evolve_verify\\") {\\n if (!Array.isArray(args.evidence) || args.evidence.length === 0) {\\n throw new Error(\\"evolve_verify requires at least one evidence reference\\");\\n }\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scope,\\n \\"verify\\",\\n requireString(\\"id\\", args.id),\\n \\"--effectiveness\\",\\n requireString(\\"effectiveness\\", args.effectiveness),\\n ...repeatedStringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--note\\", args.note),\\n ];\\n }\\n throw new Error(`Unsupported workflow action: ${action}`);\\n}\\n\\nfunction syncCommand(args) {\\n if (args.action === \\"status\\") {\\n return [\\"managed\\", ...scopeArgs(args.scope)];\\n }\\n if (args.action === \\"preview\\") {\\n return [\\n \\"sync\\",\\n ...(args.tool ? [args.tool] : []),\\n \\"--dry-run\\",\\n ...scopeArgs(args.scope),\\n ];\\n }\\n throw new Error(`Unsupported sync action: ${args.action}`);\\n}\\n\\nfunction requireOnlyRegistryFields(args, fields) {\\n const allowed = new Set([\\"action\\", \\"scope\\", \\"cwd\\", ...fields]);\\n const unexpected = Object.keys(args).filter((key) => !allowed.has(key));\\n if (unexpected.length > 0) {\\n throw new Error(\\n `${args.action} received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n}\\n\\nfunction registryCommand(args) {\\n if (args.action === \\"activity_decide\\") {\\n const expectedFields = new Set([\\n \\"action\\",\\n \\"locator\\",\\n \\"decision\\",\\n \\"expectedRevision\\",\\n \\"actor\\",\\n \\"approvalReference\\",\\n \\"note\\",\\n \\"redirectTarget\\",\\n \\"approve\\",\\n ]);\\n const unexpected = Object.keys(args).filter(\\n (key) => !expectedFields.has(key)\\n );\\n if (unexpected.length > 0) {\\n throw new Error(\\n `activity_decide received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n if (args.approve !== true) {\\n throw new Error(\\"activity_decide requires approve=true\\");\\n }\\n if (!Number.isSafeInteger(args.expectedRevision)) {\\n throw new Error(\\n \\"activity_decide requires expectedRevision as an integer\\"\\n );\\n }\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n \\"decide\\",\\n requireString(\\"locator\\", args.locator),\\n \\"--decision\\",\\n requireString(\\"decision\\", args.decision),\\n \\"--expected-revision\\",\\n String(args.expectedRevision),\\n \\"--actor\\",\\n requireString(\\"actor\\", args.actor),\\n ...stringFlag(\\"--approval-ref\\", args.approvalReference),\\n ...stringFlag(\\"--note\\", args.note),\\n ...stringFlag(\\"--redirect-target\\", args.redirectTarget),\\n \\"--approve\\",\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"activity_resolve\\") {\\n const unexpected = Object.keys(args).filter(\\n (key) => key !== \\"action\\" && key !== \\"locator\\"\\n );\\n if (unexpected.length > 0) {\\n throw new Error(\\n `activity_resolve received unsupported fields: ${unexpected.join(\\", \\")}`\\n );\\n }\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n \\"resolve\\",\\n requireString(\\"locator\\", args.locator),\\n \\"--json\\",\\n ];\\n }\\n if (\\n args.scope === \\"project\\" &&\\n args.action !== \\"reconcile_status\\" &&\\n args.action !== \\"reconcile\\"\\n ) {\\n throw new Error(`${args.action} only supports global scope`);\\n }\\n if (args.action === \\"search\\") {\\n requireOnlyRegistryFields(args, [\\"query\\"]);\\n return [\\"search\\", requireString(\\"query\\", args.query), \\"--json\\"];\\n }\\n if (args.action === \\"verify_source\\") {\\n requireOnlyRegistryFields(args, [\\"source\\"]);\\n return [\\"verify-source\\", requireString(\\"source\\", args.source), \\"--json\\"];\\n }\\n if (args.action === \\"source_list\\") {\\n requireOnlyRegistryFields(args, []);\\n return [\\"sources\\", \\"list\\", \\"--json\\"];\\n }\\n if (args.action === \\"install_preview\\") {\\n requireOnlyRegistryFields(args, [\\"item\\", \\"as\\"]);\\n return [\\n \\"install\\",\\n requireString(\\"item\\", args.item),\\n ...stringFlag(\\"--as\\", args.as),\\n \\"--dry-run\\",\\n \\"--strict-source-trust\\",\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"update_check\\") {\\n requireOnlyRegistryFields(args, []);\\n return [\\"update\\", \\"--strict-source-trust\\", \\"--json\\"];\\n }\\n if (args.action === \\"reconcile_status\\") {\\n requireOnlyRegistryFields(args, []);\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(\\n \\"reconcile_status requires an explicit global or project scope\\"\\n );\\n }\\n return [\\"ai\\", \\"review\\", ...scopeArgs(args.scope), \\"status\\", \\"--json\\"];\\n }\\n if (args.action === \\"reconcile\\") {\\n requireOnlyRegistryFields(args, [\\n \\"since\\",\\n \\"until\\",\\n \\"sourceIds\\",\\n \\"incremental\\",\\n ]);\\n if (args.scope !== \\"global\\" && args.scope !== \\"project\\") {\\n throw new Error(\\"reconcile requires an explicit global or project scope\\");\\n }\\n return [\\n \\"ai\\",\\n \\"review\\",\\n ...scopeArgs(args.scope),\\n \\"reconcile\\",\\n \\"--since\\",\\n requireString(\\"since\\", args.since),\\n ...stringFlag(\\"--until\\", args.until),\\n ...repeatedStringFlag(\\"--source\\", args.sourceIds),\\n ...boolFlag(\\"--incremental\\", args.incremental),\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported registry action: ${args.action}`);\\n}\\n\\nfunction isSubpath(child, parent) {\\n const relative = path.relative(parent, child);\\n return (\\n relative === \\"\\" || !(relative.startsWith(\\"..\\") || path.isAbsolute(relative))\\n );\\n}\\n\\nfunction isDirectory(candidate) {\\n try {\\n return fs.statSync(candidate).isDirectory();\\n } catch {\\n return false;\\n }\\n}\\n\\nfunction resolveWorkspaceCwd({ allowHomeFallback = true } = {}) {\\n const candidates = [\\n process.env.FCLT_MCP_WORKSPACE_CWD,\\n process.env.INIT_CWD,\\n process.env.PWD,\\n ];\\n for (const candidate of candidates) {\\n if (typeof candidate !== \\"string\\" || !candidate.trim()) {\\n continue;\\n }\\n const resolved = path.resolve(candidate);\\n const isHome = resolved === path.resolve(os.homedir());\\n if (\\n (allowHomeFallback || !isHome) &&\\n !isSubpath(resolved, PLUGIN_ROOT) &&\\n isDirectory(resolved)\\n ) {\\n return resolved;\\n }\\n }\\n if (allowHomeFallback && isDirectory(os.homedir())) {\\n return os.homedir();\\n }\\n return undefined;\\n}\\n\\nfunction resolveToolCwd(name, args = {}) {\\n if (typeof args.cwd === \\"string\\" && args.cwd.trim()) {\\n return args.cwd;\\n }\\n const inferred = resolveWorkspaceCwd({\\n allowHomeFallback: args.scope !== \\"project\\",\\n });\\n if (inferred) {\\n return inferred;\\n }\\n if (args.scope === \\"project\\") {\\n throw new Error(\\n `${name} with project scope requires a cwd for the target workspace`\\n );\\n }\\n return process.cwd();\\n}\\n\\nfunction commandForTool(name, args = {}) {\\n switch (name) {\\n case \\"fclt_setup\\": {\\n const apply = args.dryRun === false;\\n if (apply && args.approve !== true) {\\n throw new Error(\\"fclt_setup apply requires approve=true\\");\\n }\\n if (\\n args.scope === \\"global_and_project\\" &&\\n (typeof args.cwd !== \\"string\\" || !args.cwd.trim())\\n ) {\\n throw new Error(\\n \\"fclt_setup global_and_project scope requires an explicit cwd\\"\\n );\\n }\\n return [\\n \\"setup\\",\\n \\"--json\\",\\n ...(args.scope === \\"global_and_project\\" ? [\\"--include-project\\"] : []),\\n ...(apply ? [] : [\\"--dry-run\\"]),\\n ...(args.installCodexPlugin === false ? [\\"--no-codex-plugin\\"] : []),\\n ];\\n }\\n case \\"fclt_capability\\":\\n return capabilityCommand(args);\\n case \\"fclt_workflow\\":\\n return workflowCommand(args);\\n case \\"fclt_sync\\":\\n return syncCommand(args);\\n case \\"fclt_registry\\":\\n return registryCommand(args);\\n case \\"fclt_audit\\":\\n return [\\n \\"audit\\",\\n \\"--non-interactive\\",\\n ...(args.target ? [args.target] : []),\\n ...stringFlag(\\"--severity\\", args.severity),\\n \\"--json\\",\\n ];\\n case \\"fclt_automation\\":\\n if (\\n args.action !== \\"loop_activity\\" &&\\n args.scope !== \\"global\\" &&\\n args.scope !== \\"project\\"\\n ) {\\n throw new Error(`${args.action} requires global or project scope`);\\n }\\n if (args.action === \\"autosync_status\\") {\\n return [\\n \\"autosync\\",\\n \\"status\\",\\n ...(args.tool ? [args.tool] : []),\\n ...scopeArgs(args.scope),\\n ];\\n }\\n if (args.tool) {\\n throw new Error(`${args.action} does not accept tool`);\\n }\\n if (args.action === \\"loop_status\\") {\\n return [\\"ai\\", \\"loop\\", ...scopeArgs(args.scope), \\"status\\", \\"--json\\"];\\n }\\n if (args.action === \\"loop_activity\\") {\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n ...scopeArgs(args.scope),\\n \\"activity\\",\\n ...(args.scope === \\"global\\" || args.scope === \\"project\\"\\n ? []\\n : [\\"--all\\"]),\\n \\"--json\\",\\n ];\\n }\\n if (args.action === \\"loop_preview\\") {\\n return [\\n \\"ai\\",\\n \\"loop\\",\\n ...scopeArgs(args.scope),\\n \\"run\\",\\n \\"--dry-run\\",\\n \\"--json\\",\\n ];\\n }\\n throw new Error(`Unsupported automation action: ${args.action}`);\\n case \\"fclt_status\\":\\n return [\\"status\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_doctor\\":\\n return [\\"doctor\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_paths\\":\\n return [\\"paths\\", ...scopeArgs(args.scope), \\"--json\\"];\\n case \\"fclt_init_operating_model\\":\\n if (args.dryRun === false && args.approve !== true) {\\n throw new Error(\\n \\"fclt_init_operating_model apply requires approve=true\\"\\n );\\n }\\n if (args.force === true && args.approve !== true) {\\n throw new Error(\\n \\"fclt_init_operating_model force requires approve=true\\"\\n );\\n }\\n return [\\n \\"templates\\",\\n \\"init\\",\\n \\"operating-model\\",\\n ...scopeArgs(args.scope),\\n ...boolFlag(\\"--update\\", args.update),\\n ...(args.dryRun === false ? [] : [\\"--dry-run\\"]),\\n ...boolFlag(\\"--force\\", args.force),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_add\\":\\n requireMutationApproval(name, args);\\n requireString(\\"evidence\\", args.evidence);\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scopeArgs(args.scope),\\n \\"add\\",\\n \\"--kind\\",\\n args.kind,\\n \\"--summary\\",\\n args.summary,\\n ...stringFlag(\\"--asset\\", args.asset),\\n ...stringFlag(\\"--category\\", args.category),\\n ...stringFlag(\\"--details\\", args.details),\\n ...stringFlag(\\"--impact\\", args.impact),\\n ...stringFlag(\\"--attempted-workaround\\", args.attemptedWorkaround),\\n ...stringFlag(\\"--desired-outcome\\", args.desiredOutcome),\\n ...stringFlag(\\"--sensitivity\\", args.sensitivity),\\n ...stringFlag(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_review\\": {\\n const mode = args.mode || \\"list\\";\\n return [\\n \\"ai\\",\\n \\"writeback\\",\\n ...scopeArgs(args.scope),\\n mode,\\n ...stringFlag(\\"--by\\", args.by),\\n ];\\n }\\n case \\"fclt_evolve\\": {\\n const action = args.action || \\"list\\";\\n if ([\\"propose\\", \\"draft\\", \\"review\\"].includes(action)) {\\n requireMutationApproval(`fclt_evolve ${action}`, args);\\n }\\n if (action === \\"propose\\") {\\n requireString(\\"asset\\", args.asset);\\n }\\n if ([\\"draft\\", \\"review\\", \\"show\\"].includes(action)) {\\n requireString(\\"id\\", args.id);\\n }\\n return [\\n \\"ai\\",\\n \\"evolve\\",\\n ...scopeArgs(args.scope),\\n action,\\n ...(action === \\"assess\\" || action === \\"propose\\"\\n ? stringFlag(\\"--asset\\", args.asset)\\n : []),\\n ...(args.id ? [args.id] : []),\\n ...(action === \\"assess\\" ? [\\"--json\\"] : []),\\n ];\\n }\\n default:\\n throw new Error(`Unknown tool: ${name}`);\\n }\\n}\\n\\nfunction operationMetadata(name, args, command) {\\n const action =\\n name === \\"fclt_writeback_add\\"\\n ? \\"writeback_add\\"\\n : name === \\"fclt_evolve\\"\\n ? `evolve_${args.action || \\"list\\"}`\\n : args.action || name;\\n const reviewActions = new Set([\\n \\"activity_decide\\",\\n \\"writeback_add\\",\\n \\"writeback_link\\",\\n \\"writeback_disposition\\",\\n \\"evolve_propose\\",\\n \\"evolve_draft\\",\\n \\"evolve_review\\",\\n \\"evolve_verify\\",\\n \\"reconcile\\",\\n ]);\\n const preview = command.includes(\\"--dry-run\\");\\n const risk = reviewActions.has(action)\\n ? \\"review_producing\\"\\n : !preview && name === \\"fclt_setup\\"\\n ? \\"reversible_mutation\\"\\n : !preview && name === \\"fclt_init_operating_model\\"\\n ? \\"high_risk_destructive\\"\\n : \\"read_only\\";\\n return {\\n tool: name,\\n action,\\n risk,\\n scope:\\n args.scope ||\\n (name === \\"fclt_automation\\" && action === \\"loop_activity\\"\\n ? \\"all\\"\\n : \\"auto\\"),\\n target:\\n args.id ||\\n args.selector ||\\n args.asset ||\\n args.item ||\\n args.source ||\\n args.tool ||\\n args.locator ||\\n null,\\n preview,\\n };\\n}\\n\\nfunction recoveryForOperation(operation, stdout) {\\n if (operation.risk === \\"review_producing\\") {\\n return {\\n canonicalCapabilityChanged: false,\\n audit: \\"native fclt review artifacts and append-only journal\\",\\n };\\n }\\n if (\\n operation.risk !== \\"reversible_mutation\\" &&\\n operation.risk !== \\"high_risk_destructive\\"\\n ) {\\n return null;\\n }\\n const report = isPlainObject(stdout) ? stdout : {};\\n return {\\n rollbackAvailable: false,\\n changedPaths: Array.isArray(report.changedPaths) ? report.changedPaths : [],\\n skippedPaths: Array.isArray(report.skippedPaths) ? report.skippedPaths : [],\\n repairActions: Array.isArray(report.repairActions)\\n ? report.repairActions\\n : [],\\n verification:\\n operation.tool === \\"fclt_setup\\"\\n ? \\"rerun fclt_setup in preview mode and verify doctor readiness\\"\\n : \\"verify doctor, generated state, authored-file hashes, and exact target paths\\",\\n };\\n}\\n\\nasync function runFclt(args, cwd, operation) {\\n const discovery = await runtime.discoverRuntime();\\n if (!discovery.selected) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"no_compatible_runtime\\",\\n message:\\n \\"No compatible fclt runtime is available. Check, stage, and apply an explicit verified version with fclt_runtime.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.tool === \\"fclt_audit\\" &&\\n !discovery.selected.capabilities?.includes(AUDIT_READ_ONLY_CAPABILITY)\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise audit-read-only-v1; typed audit fails closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.action === \\"activity_decide\\" &&\\n !discovery.selected.capabilities?.includes(\\n ACTIVITY_ACTION_DECIDE_CAPABILITY\\n )\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise activity-action-decide-v1; typed activity decisions fail closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n if (\\n operation.action === \\"activity_resolve\\" &&\\n !discovery.selected.capabilities?.includes(\\n ACTIVITY_ACTION_RESOLVE_CAPABILITY\\n )\\n ) {\\n return {\\n code: 1,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n error: \\"missing_runtime_capability\\",\\n message:\\n \\"The selected fclt runtime does not advertise activity-action-resolve-v1; typed activity resolution fails closed.\\",\\n runtime: discovery,\\n },\\n null,\\n 2\\n ),\\n };\\n }\\n\\n const result = await runtime.runCommand(discovery.selected.executable, args, {\\n cwd: cwd || process.cwd(),\\n env: process.env,\\n timeoutMs: DEFAULT_TIMEOUT_MS,\\n });\\n const parsedStdout = parseJsonOrText(result.stdout.trim());\\n return {\\n code: result.code,\\n text: JSON.stringify(\\n {\\n schemaVersion: 1,\\n operation,\\n runtime: discovery.selected,\\n result: {\\n exitCode: result.code,\\n stdout: parsedStdout,\\n stderr: result.stderr,\\n },\\n verification: {\\n status: result.code === 0 ? \\"passed\\" : \\"failed\\",\\n exitCode: result.code,\\n },\\n recovery: recoveryForOperation(operation, parsedStdout),\\n },\\n null,\\n 2\\n ),\\n };\\n}\\n\\nfunction parseJsonOrText(value) {\\n if (!value) {\\n return \\"\\";\\n }\\n try {\\n return JSON.parse(value);\\n } catch {\\n return value;\\n }\\n}\\n\\nasync function handleRuntimeTool(args = {}) {\\n const action = args.action || \\"status\\";\\n if (action === \\"status\\") {\\n return await runtime.discoverRuntime();\\n }\\n if (action === \\"check\\") {\\n return await runtime.checkRuntimeUpdate();\\n }\\n if (action === \\"policy\\") {\\n return await runtime.setRuntimePolicy({\\n approve: args.approve,\\n pinnedVersion: args.pinnedVersion,\\n clearPin: args.clearPin,\\n updateChecksEnabled: args.updateChecksEnabled,\\n });\\n }\\n if (action === \\"stage\\") {\\n return await runtime.stageRuntime({\\n approve: args.approve,\\n version: args.version,\\n });\\n }\\n if (action === \\"apply\\") {\\n return await runtime.applyStagedRuntime({\\n approve: args.approve,\\n expectedSha256: args.expectedSha256,\\n version: args.version,\\n });\\n }\\n if (action === \\"rollback\\") {\\n return await runtime.rollbackRuntime({\\n approve: args.approve,\\n expectedActiveVersion: args.expectedActiveVersion,\\n });\\n }\\n throw new Error(`Unknown runtime action: ${action}`);\\n}\\n\\nfunction runtimeOperationMetadata(args, result) {\\n const action = args.action || \\"status\\";\\n const risk =\\n action === \\"status\\" || action === \\"check\\"\\n ? \\"read_only\\"\\n : action === \\"stage\\"\\n ? \\"review_producing\\"\\n : \\"high_risk_destructive\\";\\n return {\\n operation: {\\n tool: \\"fclt_runtime\\",\\n action,\\n risk,\\n scope: \\"plugin_runtime\\",\\n target:\\n args.version ||\\n args.pinnedVersion ||\\n args.expectedActiveVersion ||\\n null,\\n approved: args.approve === true,\\n },\\n verification: {\\n status: \\"passed\\",\\n activeVersion:\\n result.active?.packageVersion ||\\n result.selected?.packageVersion ||\\n null,\\n },\\n recovery:\\n action === \\"apply\\" || action === \\"rollback\\"\\n ? {\\n rollbackAvailable: result.rollbackAvailable === true,\\n previous: result.previous || null,\\n }\\n : action === \\"policy\\"\\n ? { previousPolicy: result.previous || null }\\n : null,\\n };\\n}\\n\\nlet transportFraming = \\"content-length\\";\\n\\nfunction send(message) {\\n const body = JSON.stringify(message);\\n if (transportFraming === \\"newline\\") {\\n process.stdout.write(`${body}\\\\n`);\\n return;\\n }\\n process.stdout.write(\\n `Content-Length: ${Buffer.byteLength(body)}\\\\r\\\\n\\\\r\\\\n${body}`\\n );\\n}\\n\\nasync function handle(message) {\\n if (!message || message.id == null) {\\n return;\\n }\\n\\n try {\\n if (message.method === \\"initialize\\") {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n protocolVersion: \\"2025-06-18\\",\\n capabilities: { tools: {} },\\n serverInfo: { name: \\"fclt\\", version: runtime.pluginVersion() },\\n },\\n });\\n return;\\n }\\n if (message.method === \\"tools/list\\") {\\n send({ jsonrpc: \\"2.0\\", id: message.id, result: { tools } });\\n return;\\n }\\n if (message.method === \\"tools/call\\") {\\n const { name, arguments: args = {} } = message.params || {};\\n validateToolArguments(name, args);\\n if (name === \\"fclt_runtime\\") {\\n const result = await handleRuntimeTool(args);\\n const metadata = runtimeOperationMetadata(args, result);\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n isError: false,\\n content: [\\n {\\n type: \\"text\\",\\n text: JSON.stringify({ ...result, ...metadata }, null, 2),\\n },\\n ],\\n },\\n });\\n return;\\n }\\n const command = commandForTool(name, args);\\n const result = await runFclt(\\n command,\\n resolveToolCwd(name, args),\\n operationMetadata(name, args, command)\\n );\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n result: {\\n isError: result.code !== 0,\\n content: [{ type: \\"text\\", text: result.text }],\\n },\\n });\\n return;\\n }\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n error: { code: -32_601, message: `Method not found: ${message.method}` },\\n });\\n } catch (error) {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: message.id,\\n error: {\\n code: -32_000,\\n message: error instanceof Error ? error.message : String(error),\\n },\\n });\\n }\\n}\\n\\nlet buffer = Buffer.alloc(0);\\n\\nfunction dispatch(body, framing) {\\n transportFraming = framing;\\n handle(JSON.parse(body)).catch((error) => {\\n send({\\n jsonrpc: \\"2.0\\",\\n id: null,\\n error: {\\n code: -32_000,\\n message: error instanceof Error ? error.message : String(error),\\n },\\n });\\n });\\n}\\n\\nprocess.stdin.on(\\"data\\", (chunk) => {\\n buffer = Buffer.concat([buffer, chunk]);\\n while (true) {\\n while (buffer[0] === 10 || buffer[0] === 13) {\\n buffer = buffer.subarray(1);\\n }\\n if (buffer.length === 0) {\\n return;\\n }\\n if (buffer[0] === 123 || buffer[0] === 91) {\\n const lineEnd = buffer.indexOf(\\"\\\\n\\");\\n if (lineEnd === -1) {\\n return;\\n }\\n const body = buffer.subarray(0, lineEnd).toString(\\"utf8\\").trim();\\n buffer = buffer.subarray(lineEnd + 1);\\n if (body) {\\n dispatch(body, \\"newline\\");\\n }\\n continue;\\n }\\n const headerEnd = buffer.indexOf(\\"\\\\r\\\\n\\\\r\\\\n\\");\\n if (headerEnd === -1) {\\n return;\\n }\\n const header = buffer.slice(0, headerEnd).toString(\\"utf8\\");\\n const match = CONTENT_LENGTH_RE.exec(header);\\n if (!match) {\\n buffer = Buffer.alloc(0);\\n return;\\n }\\n const length = Number(match[1]);\\n const frameEnd = headerEnd + 4 + length;\\n if (buffer.length < frameEnd) {\\n return;\\n }\\n const body = buffer.slice(headerEnd + 4, frameEnd).toString(\\"utf8\\");\\n buffer = buffer.slice(frameEnd);\\n dispatch(body, \\"content-length\\");\\n }\\n});\\n\\nif (process.argv.includes(\\"--self-test\\")) {\\n console.log(\\n JSON.stringify(\\n {\\n pluginVersion: runtime.pluginVersion(),\\n protocolVersion: runtime.PLUGIN_PROTOCOL_VERSION,\\n tools: tools.map((tool) => tool.name),\\n },\\n null,\\n 2\\n )\\n );\\n process.exit(0);\\n}\\n","scripts/fclt-runtime.cjs":"\\"use strict\\";\\n\\nconst { spawn } = require(\\"node:child_process\\");\\nconst crypto = require(\\"node:crypto\\");\\nconst fs = require(\\"node:fs\\");\\nconst fsp = require(\\"node:fs/promises\\");\\nconst https = require(\\"node:https\\");\\nconst os = require(\\"node:os\\");\\nconst path = require(\\"node:path\\");\\n\\nconst PLUGIN_PROTOCOL_VERSION = 1;\\nconst STATE_SCHEMA_VERSION = 1;\\nconst REPOSITORY = \\"hack-dance/fclt\\";\\nconst MAX_BINARY_BYTES = 256 * 1024 * 1024;\\nconst MAX_METADATA_BYTES = 2 * 1024 * 1024;\\nconst DOWNLOAD_TIMEOUT_MS = 30_000;\\nconst COMMAND_TIMEOUT_MS = 15_000;\\nconst ALLOWED_DOWNLOAD_HOSTS = new Set([\\n \\"api.github.com\\",\\n \\"github.com\\",\\n \\"objects.githubusercontent.com\\",\\n \\"release-assets.githubusercontent.com\\",\\n]);\\nconst SEMVER_RE = /^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:[-+][0-9A-Za-z.-]+)?$/;\\nconst SHA256_RE = /^[a-f0-9]{64}$/;\\nconst NEWLINE_RE = /\\\\r?\\\\n/;\\nconst CHECKSUM_LINE_RE = /^([a-fA-F0-9]{64})\\\\s+\\\\*?(.+)$/;\\nconst WINDOWS_SHIM_RE = /\\\\.(?:bat|cmd)$/i;\\n\\nfunction isPlainObject(value) {\\n return Boolean(value) && typeof value === \\"object\\" && !Array.isArray(value);\\n}\\n\\nfunction pluginVersion() {\\n try {\\n const manifest = JSON.parse(\\n fs.readFileSync(\\n path.resolve(__dirname, \\"..\\", \\".codex-plugin\\", \\"plugin.json\\"),\\n \\"utf8\\"\\n )\\n );\\n return typeof manifest.version === \\"string\\" ? manifest.version : \\"unknown\\";\\n } catch {\\n return \\"unknown\\";\\n }\\n}\\n\\nfunction runtimeStateRoot(env = process.env, platform = process.platform) {\\n if (env.FCLT_PLUGIN_RUNTIME_DIR) {\\n return path.resolve(env.FCLT_PLUGIN_RUNTIME_DIR);\\n }\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n if (platform === \\"darwin\\") {\\n return path.join(\\n home,\\n \\"Library\\",\\n \\"Application Support\\",\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n }\\n if (platform === \\"win32\\") {\\n return path.join(\\n env.LOCALAPPDATA || path.join(home, \\"AppData\\", \\"Local\\"),\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n }\\n return path.join(\\n env.XDG_STATE_HOME || path.join(home, \\".local\\", \\"state\\"),\\n \\"fclt\\",\\n \\"plugin-runtime\\"\\n );\\n}\\n\\nfunction installStatePaths(env = process.env, platform = process.platform) {\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n const override = env.FACULT_LOCAL_STATE_DIR?.trim();\\n const portableRoot = override\\n ? path.resolve(override)\\n : platform === \\"darwin\\"\\n ? path.join(home, \\"Library\\", \\"Application Support\\", \\"fclt\\")\\n : path.join(\\n env.XDG_STATE_HOME\\n ? path.resolve(env.XDG_STATE_HOME)\\n : path.join(home, \\".local\\", \\"state\\"),\\n \\"fclt\\"\\n );\\n const candidates = [\\n path.join(portableRoot, \\"install.json\\"),\\n ...(platform === \\"win32\\"\\n ? [\\n path.join(\\n env.LOCALAPPDATA || path.join(home, \\"AppData\\", \\"Local\\"),\\n \\"fclt\\",\\n \\"install.json\\"\\n ),\\n ]\\n : []),\\n path.join(home, \\".ai\\", \\".facult\\", \\"install.json\\"),\\n path.join(home, \\".facult\\", \\"install.json\\"),\\n path.join(home, \\".local\\", \\"share\\", \\"fclt\\", \\"install.json\\"),\\n ];\\n return [...new Set(candidates.map((candidate) => path.resolve(candidate)))];\\n}\\n\\nfunction isSubpath(child, parent) {\\n const relative = path.relative(parent, child);\\n return (\\n relative === \\"\\" || !(relative.startsWith(\\"..\\") || path.isAbsolute(relative))\\n );\\n}\\n\\nasync function assertManagedPath(target, root) {\\n const resolvedRoot = path.resolve(root);\\n const resolvedTarget = path.resolve(target);\\n if (!isSubpath(resolvedTarget, resolvedRoot)) {\\n throw new Error(\\"Runtime path escapes the managed runtime root.\\");\\n }\\n\\n const relative = path.relative(resolvedRoot, path.dirname(resolvedTarget));\\n const segments = relative ? relative.split(path.sep) : [];\\n let cursor = resolvedRoot;\\n for (const segment of segments) {\\n cursor = path.join(cursor, segment);\\n try {\\n if ((await fsp.lstat(cursor)).isSymbolicLink()) {\\n throw new Error(\\"Runtime path traverses a symbolic link.\\");\\n }\\n } catch (error) {\\n if (error && error.code === \\"ENOENT\\") {\\n continue;\\n }\\n throw error;\\n }\\n }\\n return resolvedTarget;\\n}\\n\\nasync function readJson(pathValue) {\\n try {\\n const value = JSON.parse(await fsp.readFile(pathValue, \\"utf8\\"));\\n return isPlainObject(value) ? value : null;\\n } catch {\\n return null;\\n }\\n}\\n\\nasync function runtimePolicy(options = {}) {\\n const root = runtimeStateRoot(options.env, options.platform);\\n const persisted = await readJson(path.join(root, \\"policy.json\\"));\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n updateChecksEnabled: persisted?.updateChecksEnabled !== false,\\n pinnedVersion:\\n typeof persisted?.pinnedVersion === \\"string\\" &&\\n persisted.pinnedVersion.trim()\\n ? normalizeVersion(persisted.pinnedVersion)\\n : null,\\n };\\n}\\n\\nfunction commandNames(platform = process.platform) {\\n return platform === \\"win32\\"\\n ? [\\"fclt.exe\\", \\"fclt.cmd\\", \\"facult.exe\\", \\"facult.cmd\\"]\\n : [\\"fclt\\", \\"facult\\"];\\n}\\n\\nfunction pathCandidates(env = process.env, platform = process.platform) {\\n const values = [];\\n for (const directory of (env.PATH || \\"\\").split(path.delimiter)) {\\n if (!directory) {\\n continue;\\n }\\n for (const name of commandNames(platform)) {\\n values.push(path.join(directory, name));\\n }\\n }\\n return values;\\n}\\n\\nfunction systemPathCandidates(env = process.env, platform = process.platform) {\\n if (Object.hasOwn(env, \\"FCLT_SYSTEM_PATHS\\")) {\\n return (env.FCLT_SYSTEM_PATHS || \\"\\").split(path.delimiter).filter(Boolean);\\n }\\n if (platform === \\"darwin\\") {\\n return [\\"/opt/homebrew/bin/fclt\\", \\"/usr/local/bin/fclt\\"];\\n }\\n if (platform === \\"win32\\") {\\n return [];\\n }\\n return [\\"/usr/local/bin/fclt\\", \\"/usr/bin/fclt\\"];\\n}\\n\\nfunction candidateSource(candidate) {\\n const normalized = candidate.split(\\"\\\\\\\\\\").join(\\"/\\");\\n if (normalized.includes(\\"/plugin-runtime/versions/\\")) {\\n return \\"plugin_runtime\\";\\n }\\n if (normalized.includes(\\"/mise/\\") || normalized.includes(\\"/mise/installs/\\")) {\\n return \\"mise\\";\\n }\\n if (\\n normalized.includes(\\"/Cellar/\\") ||\\n normalized.startsWith(\\"/opt/homebrew/\\")\\n ) {\\n return \\"homebrew\\";\\n }\\n if (normalized.includes(\\"/node_modules/\\") || normalized.includes(\\"/npm/\\")) {\\n return \\"npm\\";\\n }\\n if (normalized.includes(\\"/.ai/.facult/bin/\\")) {\\n return \\"canonical_install\\";\\n }\\n return \\"path\\";\\n}\\n\\nasync function activeRuntimeCandidate(root) {\\n const active = await readJson(path.join(root, \\"active.json\\"));\\n if (typeof active?.executable !== \\"string\\" || !active.executable.trim()) {\\n return null;\\n }\\n const executable = path.resolve(active.executable);\\n if (!isSubpath(executable, path.join(root, \\"versions\\"))) {\\n return null;\\n }\\n return {\\n executable,\\n source: \\"plugin_runtime\\",\\n expectedSha256: active.sha256,\\n active,\\n };\\n}\\n\\nasync function persistedInstallCandidates(\\n env = process.env,\\n platform = process.platform\\n) {\\n const candidates = [];\\n for (const statePath of installStatePaths(env, platform)) {\\n const state = await readJson(statePath);\\n if (typeof state?.binaryPath === \\"string\\" && state.binaryPath.trim()) {\\n candidates.push({\\n executable: path.resolve(state.binaryPath),\\n source:\\n typeof state.source === \\"string\\" ? state.source : \\"install_metadata\\",\\n installStatePath: statePath,\\n });\\n }\\n }\\n return candidates;\\n}\\n\\nasync function runtimeCandidates(options = {}) {\\n const env = options.env || process.env;\\n const platform = options.platform || process.platform;\\n const home = env.HOME || env.USERPROFILE || os.homedir();\\n const root = runtimeStateRoot(env, platform);\\n const candidates = [];\\n let configuredPathCandidate = null;\\n\\n if (env.FCLT_BIN?.trim()) {\\n const explicit = env.FCLT_BIN.trim();\\n if (path.isAbsolute(explicit) || explicit.includes(path.sep)) {\\n candidates.push({\\n executable: path.resolve(explicit),\\n source: \\"explicit\\",\\n });\\n } else {\\n const resolved = pathCandidates(env, platform).find(\\n (candidate) =>\\n path.basename(candidate) === explicit && fs.existsSync(candidate)\\n );\\n configuredPathCandidate = {\\n executable: resolved || explicit,\\n source: \\"configured_path\\",\\n };\\n }\\n }\\n\\n const active = await activeRuntimeCandidate(root);\\n if (active) {\\n candidates.push(active);\\n }\\n if (configuredPathCandidate) {\\n candidates.push(configuredPathCandidate);\\n }\\n candidates.push(...(await persistedInstallCandidates(env, platform)));\\n candidates.push(\\n ...pathCandidates(env, platform).map((executable) => ({\\n executable,\\n source: candidateSource(executable),\\n }))\\n );\\n\\n for (const executable of [\\n path.join(home, \\".ai\\", \\".facult\\", \\"bin\\", commandNames(platform)[0]),\\n ...systemPathCandidates(env, platform),\\n ]) {\\n candidates.push({ executable, source: candidateSource(executable) });\\n }\\n\\n const unique = [];\\n const seen = new Set();\\n for (const candidate of candidates) {\\n const key = path.resolve(candidate.executable);\\n if (!seen.has(key)) {\\n seen.add(key);\\n unique.push(candidate);\\n }\\n }\\n return unique;\\n}\\n\\nfunction runCommand(executable, args, options = {}) {\\n return new Promise((resolve) => {\\n let child;\\n try {\\n const platform = options.platform || process.platform;\\n const windowsShim =\\n platform === \\"win32\\" && WINDOWS_SHIM_RE.test(executable);\\n const command = windowsShim\\n ? options.env?.ComSpec || process.env.ComSpec || \\"cmd.exe\\"\\n : executable;\\n const commandArgs = windowsShim\\n ? [\\n \\"/d\\",\\n \\"/v:off\\",\\n \\"/s\\",\\n \\"/c\\",\\n [executable, ...args]\\n .map(\\n (value) =>\\n `\\"${String(value)\\n .replaceAll(\\"%\\", \\"%%\\")\\n .replace(/[\\\\^&|<>()!\\"]/g, \\"^$&\\")}\\"`\\n )\\n .join(\\" \\"),\\n ]\\n : args;\\n child = spawn(command, commandArgs, {\\n cwd: options.cwd || process.cwd(),\\n env: options.env || process.env,\\n stdio: [\\"ignore\\", \\"pipe\\", \\"pipe\\"],\\n });\\n } catch (error) {\\n resolve({ code: 1, stdout: \\"\\", stderr: error.message });\\n return;\\n }\\n let stdout = \\"\\";\\n let stderr = \\"\\";\\n let settled = false;\\n const timer = setTimeout(\\n () => child.kill(\\"SIGTERM\\"),\\n options.timeoutMs || COMMAND_TIMEOUT_MS\\n );\\n const finish = (code, error) => {\\n if (settled) {\\n return;\\n }\\n settled = true;\\n clearTimeout(timer);\\n resolve({\\n code,\\n stdout,\\n stderr: [stderr.trim(), error].filter(Boolean).join(\\"\\\\n\\"),\\n });\\n };\\n child.stdout.on(\\"data\\", (chunk) => {\\n stdout += chunk.toString();\\n if (stdout.length > MAX_METADATA_BYTES) {\\n child.kill(\\"SIGTERM\\");\\n }\\n });\\n child.stderr.on(\\"data\\", (chunk) => {\\n stderr += chunk.toString();\\n if (stderr.length > MAX_METADATA_BYTES) {\\n child.kill(\\"SIGTERM\\");\\n }\\n });\\n child.on(\\"error\\", (error) => finish(1, error.message));\\n child.on(\\"close\\", (code) => finish(code ?? 1));\\n });\\n}\\n\\nfunction parseProtocolReport(raw) {\\n try {\\n const report = JSON.parse(raw);\\n if (\\n !isPlainObject(report) ||\\n report.schemaVersion !== 1 ||\\n typeof report.packageVersion !== \\"string\\" ||\\n !isPlainObject(report.protocol) ||\\n !Number.isInteger(report.protocol.version) ||\\n !Number.isInteger(report.protocol.minimumPluginVersion) ||\\n !Number.isInteger(report.protocol.maximumPluginVersion) ||\\n (report.capabilities !== undefined &&\\n (!Array.isArray(report.capabilities) ||\\n report.capabilities.some((value) => typeof value !== \\"string\\")))\\n ) {\\n return null;\\n }\\n return report;\\n } catch {\\n return null;\\n }\\n}\\n\\nfunction protocolCompatibility(report) {\\n if (!report) {\\n return { compatible: false, reason: \\"missing_protocol_handshake\\" };\\n }\\n const compatible =\\n report.protocol.minimumPluginVersion <= PLUGIN_PROTOCOL_VERSION &&\\n report.protocol.maximumPluginVersion >= PLUGIN_PROTOCOL_VERSION;\\n return {\\n compatible,\\n reason: compatible ? \\"compatible\\" : \\"protocol_version_skew\\",\\n };\\n}\\n\\nasync function inspectCandidate(candidate, options = {}) {\\n const executable = path.resolve(candidate.executable);\\n try {\\n const stat = await fsp.stat(executable);\\n if (!stat.isFile()) {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"not_a_file\\",\\n };\\n }\\n } catch {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"not_found\\",\\n };\\n }\\n\\n if (candidate.source === \\"plugin_runtime\\") {\\n if (\\n typeof candidate.expectedSha256 !== \\"string\\" ||\\n !SHA256_RE.test(candidate.expectedSha256)\\n ) {\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: false,\\n reason: \\"missing_checksum\\",\\n };\\n }\\n try {\\n const actualSha256 = sha256(await fsp.readFile(executable));\\n if (actualSha256 !== candidate.expectedSha256) {\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: false,\\n reason: \\"checksum_mismatch\\",\\n };\\n }\\n } catch {\\n return {\\n ...candidate,\\n executable,\\n available: false,\\n compatible: false,\\n reason: \\"checksum_unreadable\\",\\n };\\n }\\n }\\n\\n const result = await runCommand(executable, [\\"protocol\\", \\"--json\\"], options);\\n const report =\\n result.code === 0 ? parseProtocolReport(result.stdout.trim()) : null;\\n const compatibility = protocolCompatibility(report);\\n return {\\n ...candidate,\\n executable,\\n available: true,\\n compatible: compatibility.compatible,\\n reason: compatibility.reason,\\n packageVersion: report?.packageVersion,\\n protocol: report?.protocol,\\n platform: report?.runtime?.platform,\\n architecture: report?.runtime?.architecture,\\n capabilities: report?.capabilities || [],\\n };\\n}\\n\\nasync function discoverRuntime(options = {}) {\\n const policy = await runtimePolicy(options);\\n const inspected = [];\\n for (const candidate of await runtimeCandidates(options)) {\\n const result = await inspectCandidate(candidate, options);\\n inspected.push(result);\\n if (result.compatible) {\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n plugin: {\\n version: pluginVersion(),\\n protocolVersion: PLUGIN_PROTOCOL_VERSION,\\n },\\n policy,\\n selected: result,\\n compatible: true,\\n requiresFreshSession: false,\\n candidates: inspected,\\n };\\n }\\n }\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n plugin: {\\n version: pluginVersion(),\\n protocolVersion: PLUGIN_PROTOCOL_VERSION,\\n },\\n policy,\\n selected: null,\\n compatible: false,\\n requiresFreshSession: false,\\n candidates: inspected,\\n };\\n}\\n\\nfunction releaseTarget(\\n platform = process.platform,\\n architecture = process.arch\\n) {\\n if (\\n platform === \\"darwin\\" &&\\n (architecture === \\"arm64\\" || architecture === \\"x64\\")\\n ) {\\n return { platform: \\"darwin\\", architecture, extension: \\"\\" };\\n }\\n if (platform === \\"linux\\" && architecture === \\"x64\\") {\\n return { platform: \\"linux\\", architecture, extension: \\"\\" };\\n }\\n if (platform === \\"win32\\" && architecture === \\"x64\\") {\\n return { platform: \\"windows\\", architecture, extension: \\".exe\\" };\\n }\\n throw new Error(\\n `Unsupported plugin runtime target: ${platform}/${architecture}`\\n );\\n}\\n\\nfunction normalizeVersion(version) {\\n const normalized = version?.startsWith(\\"v\\") ? version.slice(1) : version;\\n if (!(normalized && SEMVER_RE.test(normalized))) {\\n throw new Error(\\"Runtime version must be an explicit semantic version.\\");\\n }\\n return normalized;\\n}\\n\\nfunction assertAllowedUrl(urlValue) {\\n const url = new URL(urlValue);\\n if (url.protocol !== \\"https:\\" || !ALLOWED_DOWNLOAD_HOSTS.has(url.hostname)) {\\n throw new Error(\\n \\"Runtime downloads are restricted to approved HTTPS release hosts.\\"\\n );\\n }\\n return url;\\n}\\n\\nfunction downloadBuffer(urlValue, options = {}) {\\n const maxBytes = options.maxBytes || MAX_METADATA_BYTES;\\n const redirectsRemaining = options.redirectsRemaining ?? 5;\\n const url = assertAllowedUrl(urlValue);\\n return new Promise((resolve, reject) => {\\n const request = https.get(\\n url,\\n {\\n headers: {\\n accept: options.accept || \\"application/octet-stream\\",\\n \\"user-agent\\": \\"fclt-codex-plugin\\",\\n },\\n },\\n (response) => {\\n if (\\n response.statusCode &&\\n response.statusCode >= 300 &&\\n response.statusCode < 400 &&\\n response.headers.location\\n ) {\\n response.resume();\\n if (redirectsRemaining <= 0) {\\n reject(new Error(\\"Runtime download exceeded the redirect limit.\\"));\\n return;\\n }\\n const redirected = new URL(response.headers.location, url).toString();\\n downloadBuffer(redirected, {\\n ...options,\\n redirectsRemaining: redirectsRemaining - 1,\\n }).then(resolve, reject);\\n return;\\n }\\n if (response.statusCode !== 200) {\\n response.resume();\\n reject(\\n new Error(\\n `Runtime download failed with HTTP ${response.statusCode}.`\\n )\\n );\\n return;\\n }\\n const declaredLength = Number(response.headers[\\"content-length\\"] || 0);\\n if (declaredLength > maxBytes) {\\n response.resume();\\n reject(new Error(\\"Runtime download exceeds the allowed size.\\"));\\n return;\\n }\\n const chunks = [];\\n let total = 0;\\n response.on(\\"data\\", (chunk) => {\\n total += chunk.length;\\n if (total > maxBytes) {\\n request.destroy(\\n new Error(\\"Runtime download exceeds the allowed size.\\")\\n );\\n return;\\n }\\n chunks.push(chunk);\\n });\\n response.on(\\"end\\", () => resolve(Buffer.concat(chunks)));\\n }\\n );\\n request.setTimeout(options.timeoutMs || DOWNLOAD_TIMEOUT_MS, () => {\\n request.destroy(new Error(\\"Runtime download timed out.\\"));\\n });\\n request.on(\\"error\\", reject);\\n });\\n}\\n\\nfunction sha256(bytes) {\\n return crypto.createHash(\\"sha256\\").update(bytes).digest(\\"hex\\");\\n}\\n\\nfunction checksumForAsset(checksums, assetName) {\\n for (const line of checksums.split(NEWLINE_RE)) {\\n const match = CHECKSUM_LINE_RE.exec(line.trim());\\n if (match?.[2] === assetName) {\\n return match[1].toLowerCase();\\n }\\n }\\n throw new Error(`Published checksums do not include ${assetName}.`);\\n}\\n\\nasync function withMutationLock(root, action) {\\n await fsp.mkdir(root, { recursive: true, mode: 0o700 });\\n const lockPath = await assertManagedPath(\\n path.join(root, \\"mutation.lock\\"),\\n root\\n );\\n let handle;\\n try {\\n handle = await fsp.open(lockPath, \\"wx\\", 0o600);\\n } catch (error) {\\n if (error?.code === \\"EEXIST\\") {\\n throw new Error(\\n \\"Another fclt plugin runtime mutation is already in progress.\\"\\n );\\n }\\n throw error;\\n }\\n try {\\n return await action();\\n } finally {\\n await handle.close();\\n await fsp.rm(lockPath, { force: true });\\n }\\n}\\n\\nasync function writeJsonAtomic(pathValue, value, root) {\\n const target = await assertManagedPath(pathValue, root);\\n await fsp.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });\\n const temporary = `${target}.tmp-${crypto.randomUUID()}`;\\n await fsp.writeFile(temporary, `${JSON.stringify(value, null, 2)}\\\\n`, {\\n mode: 0o600,\\n });\\n await fsp.rename(temporary, target);\\n}\\n\\nfunction releaseUrls(version, target) {\\n const tag = `v${version}`;\\n const assetName = `fclt-${version}-${target.platform}-${target.architecture}${target.extension}`;\\n const base = `https://github.com/${REPOSITORY}/releases/download/${tag}`;\\n return {\\n tag,\\n assetName,\\n binaryUrl: `${base}/${assetName}`,\\n checksumUrl: `${base}/SHA256SUMS`,\\n };\\n}\\n\\nfunction releaseMetadataUrl(version) {\\n return `https://api.github.com/repos/${REPOSITORY}/releases/tags/v${version}`;\\n}\\n\\nfunction releaseAssets(metadata, version, target) {\\n if (\\n !isPlainObject(metadata) ||\\n metadata.tag_name !== `v${version}` ||\\n !Array.isArray(metadata.assets)\\n ) {\\n throw new Error(\\n \\"Release metadata does not match the requested immutable tag.\\"\\n );\\n }\\n const expected = releaseUrls(version, target);\\n const findAsset = (name) =>\\n metadata.assets.find(\\n (asset) =>\\n isPlainObject(asset) &&\\n asset.name === name &&\\n typeof asset.browser_download_url === \\"string\\"\\n );\\n const binary = findAsset(expected.assetName);\\n const checksums = findAsset(\\"SHA256SUMS\\");\\n if (!(binary && checksums)) {\\n throw new Error(\\n \\"Release metadata is missing the required runtime or checksum asset.\\"\\n );\\n }\\n assertAllowedUrl(binary.browser_download_url);\\n assertAllowedUrl(checksums.browser_download_url);\\n return { binary, checksums, expected };\\n}\\n\\nfunction verifyPublishedDigest(asset, bytes) {\\n if (typeof asset.digest !== \\"string\\" || !asset.digest.trim()) {\\n return null;\\n }\\n const [algorithm, expected] = asset.digest.toLowerCase().split(\\":\\");\\n if (algorithm !== \\"sha256\\" || !SHA256_RE.test(expected || \\"\\")) {\\n throw new Error(`Release asset ${asset.name} has an unsupported digest.`);\\n }\\n const actual = sha256(bytes);\\n if (actual !== expected) {\\n throw new Error(\\n `Release asset ${asset.name} does not match its published digest.`\\n );\\n }\\n return asset.digest.toLowerCase();\\n}\\n\\nasync function resolveLatestVersion(fetchBuffer = downloadBuffer) {\\n const bytes = await fetchBuffer(\\n `https://api.github.com/repos/${REPOSITORY}/releases/latest`,\\n {\\n maxBytes: MAX_METADATA_BYTES,\\n accept: \\"application/vnd.github+json\\",\\n }\\n );\\n const metadata = JSON.parse(bytes.toString(\\"utf8\\"));\\n if (!isPlainObject(metadata) || typeof metadata.tag_name !== \\"string\\") {\\n throw new Error(\\"Latest release metadata did not include a tag.\\");\\n }\\n return normalizeVersion(metadata.tag_name);\\n}\\n\\nasync function checkRuntimeUpdate(options = {}) {\\n const discovery = await discoverRuntime(options);\\n if (!discovery.policy.updateChecksEnabled) {\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n action: \\"check\\",\\n skipped: true,\\n reason: \\"update_checks_disabled\\",\\n currentVersion: discovery.selected?.packageVersion || null,\\n pinnedVersion: discovery.policy.pinnedVersion,\\n mutates: false,\\n };\\n }\\n const latestVersion =\\n discovery.policy.pinnedVersion ||\\n (await resolveLatestVersion(options.fetchBuffer || downloadBuffer));\\n return {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n action: \\"check\\",\\n currentVersion: discovery.selected?.packageVersion || null,\\n latestVersion,\\n channel: discovery.policy.pinnedVersion ? \\"pinned\\" : \\"latest\\",\\n updateAvailable: discovery.selected?.packageVersion !== latestVersion,\\n selected: discovery.selected,\\n mutates: false,\\n };\\n}\\n\\nasync function stageRuntime(options) {\\n if (options.approve !== true) {\\n throw new Error(\\"Staging a runtime download requires approve=true.\\");\\n }\\n const version = normalizeVersion(options.version);\\n const target = releaseTarget(options.platform, options.architecture);\\n const root = runtimeStateRoot(options.env, options.platform);\\n const fetchBuffer = options.fetchBuffer || downloadBuffer;\\n const urls = releaseUrls(version, target);\\n const policy = await runtimePolicy(options);\\n if (policy.pinnedVersion && policy.pinnedVersion !== version) {\\n throw new Error(`Runtime policy is pinned to ${policy.pinnedVersion}.`);\\n }\\n\\n return await withMutationLock(root, async () => {\\n const metadataBytes = await fetchBuffer(releaseMetadataUrl(version), {\\n maxBytes: MAX_METADATA_BYTES,\\n accept: \\"application/vnd.github+json\\",\\n });\\n const metadata = JSON.parse(metadataBytes.toString(\\"utf8\\"));\\n const assets = releaseAssets(metadata, version, target);\\n const [checksumBytes, binaryBytes] = await Promise.all([\\n fetchBuffer(assets.checksums.browser_download_url, {\\n maxBytes: MAX_METADATA_BYTES,\\n }),\\n fetchBuffer(assets.binary.browser_download_url, {\\n maxBytes: MAX_BINARY_BYTES,\\n }),\\n ]);\\n const checksumDigest = verifyPublishedDigest(\\n assets.checksums,\\n checksumBytes\\n );\\n const binaryDigest = verifyPublishedDigest(assets.binary, binaryBytes);\\n const expectedSha256 = checksumForAsset(\\n checksumBytes.toString(\\"utf8\\"),\\n urls.assetName\\n );\\n const actualSha256 = sha256(binaryBytes);\\n if (expectedSha256 !== actualSha256) {\\n throw new Error(\\n \\"Downloaded runtime checksum does not match the published SHA256SUMS entry.\\"\\n );\\n }\\n\\n const stageDir = await assertManagedPath(\\n path.join(root, \\"staged\\", version),\\n root\\n );\\n await fsp.rm(stageDir, { recursive: true, force: true });\\n await fsp.mkdir(stageDir, { recursive: true, mode: 0o700 });\\n const executable = await assertManagedPath(\\n path.join(stageDir, target.platform === \\"windows\\" ? \\"fclt.exe\\" : \\"fclt\\"),\\n root\\n );\\n await fsp.writeFile(executable, binaryBytes, { mode: 0o700 });\\n if (target.platform !== \\"windows\\") {\\n await fsp.chmod(executable, 0o700);\\n }\\n\\n const inspected = await inspectCandidate(\\n { executable, source: \\"staged_plugin_runtime\\" },\\n { env: options.env, timeoutMs: options.timeoutMs }\\n );\\n if (!inspected.compatible || inspected.packageVersion !== version) {\\n await fsp.rm(stageDir, { recursive: true, force: true });\\n throw new Error(\\n \\"Staged runtime failed version or protocol verification.\\"\\n );\\n }\\n\\n const manifest = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n version,\\n tag: urls.tag,\\n assetName: urls.assetName,\\n executable,\\n sha256: actualSha256,\\n source: {\\n repository: REPOSITORY,\\n releaseMetadataUrl: releaseMetadataUrl(version),\\n binaryUrl: assets.binary.browser_download_url,\\n binaryAssetId: assets.binary.id ?? null,\\n binaryDigest,\\n checksumUrl: assets.checksums.browser_download_url,\\n checksumAssetId: assets.checksums.id ?? null,\\n checksumDigest,\\n },\\n protocol: inspected.protocol,\\n platform: target.platform,\\n architecture: target.architecture,\\n stagedAt: new Date().toISOString(),\\n };\\n await writeJsonAtomic(path.join(stageDir, \\"manifest.json\\"), manifest, root);\\n return { action: \\"stage\\", mutatesActiveRuntime: false, manifest };\\n });\\n}\\n\\nasync function verifyManifestExecutable(manifest, root, expectedParent) {\\n if (\\n !isPlainObject(manifest) ||\\n manifest.schemaVersion !== STATE_SCHEMA_VERSION ||\\n typeof manifest.version !== \\"string\\" ||\\n typeof manifest.executable !== \\"string\\" ||\\n typeof manifest.sha256 !== \\"string\\" ||\\n !SHA256_RE.test(manifest.sha256)\\n ) {\\n throw new Error(\\"Runtime manifest is missing required verification data.\\");\\n }\\n const executable = path.resolve(manifest.executable);\\n if (!isSubpath(executable, expectedParent)) {\\n throw new Error(\\n \\"Runtime manifest executable escapes its expected directory.\\"\\n );\\n }\\n await assertManagedPath(executable, root);\\n const bytes = await fsp.readFile(executable);\\n if (sha256(bytes) !== manifest.sha256) {\\n throw new Error(\\"Runtime manifest checksum does not match its executable.\\");\\n }\\n const inspected = await inspectCandidate({\\n executable,\\n source: \\"plugin_runtime\\",\\n expectedSha256: manifest.sha256,\\n });\\n if (!inspected.compatible || inspected.packageVersion !== manifest.version) {\\n throw new Error(\\n \\"Runtime manifest executable failed protocol verification.\\"\\n );\\n }\\n return inspected;\\n}\\n\\nasync function setRuntimePolicy(options = {}) {\\n if (options.approve !== true) {\\n throw new Error(\\"Changing runtime update policy requires approve=true.\\");\\n }\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const current = await runtimePolicy(options);\\n const next = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n updateChecksEnabled:\\n typeof options.updateChecksEnabled === \\"boolean\\"\\n ? options.updateChecksEnabled\\n : current.updateChecksEnabled,\\n pinnedVersion: options.clearPin\\n ? null\\n : options.pinnedVersion\\n ? normalizeVersion(options.pinnedVersion)\\n : current.pinnedVersion,\\n updatedAt: new Date().toISOString(),\\n };\\n await writeJsonAtomic(path.join(root, \\"policy.json\\"), next, root);\\n return { action: \\"policy\\", previous: current, policy: next };\\n });\\n}\\n\\nasync function applyStagedRuntime(options) {\\n if (options.approve !== true) {\\n throw new Error(\\"Applying a runtime requires approve=true.\\");\\n }\\n const version = normalizeVersion(options.version);\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const stageDir = path.join(root, \\"staged\\", version);\\n const manifest = await readJson(path.join(stageDir, \\"manifest.json\\"));\\n if (!manifest) {\\n throw new Error(`No staged runtime exists for ${version}.`);\\n }\\n if (options.expectedSha256 !== manifest.sha256) {\\n throw new Error(\\n \\"Staged runtime precondition failed: expected checksum changed.\\"\\n );\\n }\\n await verifyManifestExecutable(manifest, root, stageDir);\\n\\n const activePath = path.join(root, \\"active.json\\");\\n const previous = await readJson(activePath);\\n const versionDir = await assertManagedPath(\\n path.join(root, \\"versions\\", version),\\n root\\n );\\n await fsp.mkdir(versionDir, { recursive: true, mode: 0o700 });\\n const executable = await assertManagedPath(\\n path.join(versionDir, path.basename(manifest.executable)),\\n root\\n );\\n const temporary = `${executable}.tmp-${crypto.randomUUID()}`;\\n await fsp.copyFile(manifest.executable, temporary);\\n if (process.platform !== \\"win32\\") {\\n await fsp.chmod(temporary, 0o700);\\n }\\n await fsp.rename(temporary, executable);\\n const activeManifest = {\\n ...manifest,\\n executable,\\n activatedAt: new Date().toISOString(),\\n previous:\\n typeof previous?.version === \\"string\\" &&\\n typeof previous?.executable === \\"string\\"\\n ? {\\n version: previous.version,\\n executable: previous.executable,\\n sha256: previous.sha256,\\n }\\n : null,\\n };\\n await writeJsonAtomic(activePath, activeManifest, root);\\n const inspected = await verifyManifestExecutable(\\n activeManifest,\\n root,\\n versionDir\\n );\\n return {\\n action: \\"apply\\",\\n active: inspected,\\n previous: activeManifest.previous,\\n rollbackAvailable: Boolean(activeManifest.previous),\\n requiresFreshSession: false,\\n };\\n });\\n}\\n\\nasync function rollbackRuntime(options = {}) {\\n if (options.approve !== true) {\\n throw new Error(\\"Rolling back a runtime requires approve=true.\\");\\n }\\n const root = runtimeStateRoot(options.env, options.platform);\\n return await withMutationLock(root, async () => {\\n const activePath = path.join(root, \\"active.json\\");\\n const active = await readJson(activePath);\\n if (!isPlainObject(active?.previous)) {\\n throw new Error(\\n \\"The active plugin runtime does not have a retained rollback target.\\"\\n );\\n }\\n if (\\n options.expectedActiveVersion &&\\n options.expectedActiveVersion !== active.version\\n ) {\\n throw new Error(\\n \\"Runtime rollback precondition failed: active version changed.\\"\\n );\\n }\\n const previous = {\\n schemaVersion: STATE_SCHEMA_VERSION,\\n version: active.previous.version,\\n executable: active.previous.executable,\\n sha256: active.previous.sha256,\\n previous: {\\n version: active.version,\\n executable: active.executable,\\n sha256: active.sha256,\\n },\\n rolledBackAt: new Date().toISOString(),\\n };\\n const previousDir = path.dirname(path.resolve(previous.executable));\\n const inspected = await verifyManifestExecutable(\\n previous,\\n root,\\n previousDir\\n );\\n await writeJsonAtomic(activePath, previous, root);\\n return {\\n action: \\"rollback\\",\\n active: inspected,\\n rolledBackFrom: active.version,\\n rollbackAvailable: true,\\n requiresFreshSession: false,\\n };\\n });\\n}\\n\\nmodule.exports = {\\n PLUGIN_PROTOCOL_VERSION,\\n applyStagedRuntime,\\n assertManagedPath,\\n checkRuntimeUpdate,\\n checksumForAsset,\\n discoverRuntime,\\n downloadBuffer,\\n normalizeVersion,\\n parseProtocolReport,\\n pluginVersion,\\n protocolCompatibility,\\n releaseTarget,\\n rollbackRuntime,\\n runCommand,\\n runtimeCandidates,\\n runtimePolicy,\\n runtimeStateRoot,\\n setRuntimePolicy,\\n sha256,\\n stageRuntime,\\n};\\n","skills/fclt-capability-review/SKILL.md":"---\\nname: fclt-capability-review\\ndescription: Inspect fclt capability roots, docs, snippets, skills, agents, MCP, and automations.\\ntags: [fclt, capability, review, inventory]\\n---\\n\\n# fclt-capability-review\\n\\n## When To Use\\nUse this skill when Codex needs to understand what capability exists before changing it.\\n\\nUse it for:\\n\\n- checking global and project `.ai` roots\\n- finding relevant skills, snippets, instructions, agents, MCP servers, or automations\\n- deciding whether a change belongs in global or project scope\\n- checking whether managed rendering is enabled or needed\\n- reviewing public/private boundaries before publishing docs or pack assets\\n\\n## Workflow\\n\\n```bash\\nfclt status --json\\nfclt inventory --json\\nfclt list skills\\nfclt list instructions\\nfclt list snippets\\nfclt graph AGENTS.global.md\\n```\\n\\nFor project work:\\n\\n```bash\\nfclt status --project --json\\nfclt inventory --project --json\\n```\\n\\n## Rules\\n\\n- Read existing repo guidance before proposing project capability.\\n- Use project scope for repo-specific commands, tests, architecture, or team workflow.\\n- Use global scope only for broadly reusable behavior.\\n- Keep generated state and review artifacts out of repo-local `.ai`.\\n- Prefer adding or updating the smallest unit: instruction, snippet, skill, agent, MCP config, or automation.\\n- Treat engineering implementation as task work. Do not force product changes\\n through capability evolution merely because evolution tools exist.\\n- Before recommending mutation, state the observed problem, evidence, why the\\n proposed capability is the correct target, risk class, expected outcome,\\n verification plan, assumptions, and recovery route.\\n- Never silently overwrite authored capability or confuse generated/rendered\\n output with canonical source.\\n\\n## Output\\n\\n- capability roots found\\n- relevant assets\\n- scope recommendation\\n- missing or stale capability\\n- safe next command\\n- deliberately withheld operations or missing safer API\\n- verification and undo path for any proposed mutation\\n","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`\\n- discovering repositories and enrolling a reviewed minimal project layer\\n- installing or refreshing the built-in operating-model pack\\n- checking setup health with `doctor`\\n- finding canonical, generated, runtime, and review paths\\n\\n## Workflow\\n\\n1. Bootstrap the global loop with one idempotent command:\\n\\n```bash\\nfclt setup\\n```\\n\\nThis initializes or safely updates global capability, prepares writeback/evolution review state,\\nand installs the Codex plugin when Codex is available. It does not initialize the current\\nrepository.\\n\\n2. Inspect runtime selection and compatibility with `fclt_runtime` action\\n `status`. Report the selected executable, version, source, protocol\\n compatibility, and fresh-session state.\\n\\nIf no compatible runtime is available, use the staged lifecycle:\\n\\n- `check` is read-only\\n- `stage` requires an explicit version and approval, but does not activate it\\n- `apply` requires approval plus the staged checksum precondition\\n- `rollback` verifies and restores the retained prior runtime\\n\\nNever curl-pipe code, use an unverified mutable URL, or replace an existing\\nglobal installation silently.\\n\\n3. Check current setup state and exact repair actions:\\n\\n```bash\\nfclt --version\\nfclt paths --json\\nfclt doctor --json\\n```\\n\\nThrough MCP, call `fclt_setup` with an explicit `global` or\\n`global_and_project` scope. Project setup also requires the exact `cwd`.\\nPreview is the default; apply requires both `dryRun: false` and\\n`approve: true`. `global_and_project` returns a no-write project enrollment\\nplan; project application still uses the typed CLI plan-hash contract below.\\n\\n4. For advanced manual recovery, initialize global capability when missing:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n5. Discover candidate repositories only beneath explicit roots:\\n\\n```bash\\nfclt projects discover --root ~/dev --since 30d --json\\n```\\n\\nDiscovery is bounded and read-only. Review duplicate clone/worktree groups,\\ndirty state, existing guidance, stable portfolio identity, and the separate\\ncheckout/worktree execution identity before selecting a project. Never\\nbulk-enroll the discovery result.\\n\\n6. Preview the exact minimal project enrollment plan:\\n\\n```bash\\nfclt project init --project-root /path/to/repo --json\\n```\\n\\nThe minimal layer is `.ai/.gitignore` plus `.ai/config.toml`. It does not\\ninstall the operating pack, enable managed rendering, schedule a loop, or copy\\n`AGENTS.md`/`CLAUDE.md`. Review every canonical, generated, and machine-local\\nwrite plus the rollback command.\\n\\nIf project guidance should be adopted, name each canonical file explicitly:\\n\\n```bash\\nfclt project init --project-root /path/to/repo \\\\\\n --guidance AGENTS.md --json\\n```\\n\\nGuidance adoption is reference-only. fclt previews the full content and hash,\\nand refuses untracked, modified, `assume-unchanged`, `skip-worktree`,\\nsecret-shaped, or machine-path-bearing input. Cleanliness requires identical\\nworktree, index, and `HEAD` blobs.\\n\\n7. Apply only the unchanged reviewed plan:\\n\\n```bash\\nfclt project init --project-root /path/to/repo \\\\\\n --apply --plan-sha --json\\n```\\n\\nIf options, source files, or preconditions change, discard the old hash and\\npreview again. Apply serializes portfolio registry mutations, isolates\\nlocation-bearing generated state per checkout/worktree, refuses symlinked\\ngenerated targets, and publishes the receipt only after the registry commit.\\n\\n8. Inspect health, coverage, and lifecycle:\\n\\n```bash\\nfclt projects status --root /path/to/repo --json\\nfclt project disable --project-root /path/to/repo --json\\nfclt project rollback --receipt --json\\n```\\n\\nDisable and remove decisions preserve canonical files, receipts, and review\\nhistory. Rollback previews by default and refuses drift.\\n\\n9. Install the full operating pack only when explicitly requested:\\n\\n```bash\\nfclt templates init operating-model --project --dry-run\\nfclt templates init operating-model --project\\n```\\n\\nThis is distinct from minimal enrollment and does not seed project\\n`AGENTS.global.md` from repository guidance.\\n\\n10. Refresh global pack defaults non-destructively:\\n\\n```bash\\nfclt templates init operating-model --global --update --dry-run\\nfclt templates init operating-model --global --update\\n```\\n\\n11. Use `--force` only when the user explicitly wants to replace local edits.\\n\\n## Rules\\n\\n- Preserve existing `AGENTS.md`, `CLAUDE.md`, and `AGENTS.global.md` guidance.\\n- Never infer guidance adoption from filenames or copy repository guidance into\\n `.ai/AGENTS.global.md`.\\n- Treat root `AGENTS.md` or `CLAUDE.md` as canonical repository guidance unless\\n the user explicitly chooses another tracked, clean source.\\n- Write the protective `.ai/.gitignore` before generated state.\\n- Keep canonical project files separate from machine-local generated indexes,\\n registries, receipts, and scheduling state.\\n- Keep minimal enrollment separate from the full operating pack and managed\\n rendering.\\n- Treat `doctor --json` issues as setup facts, not user-facing blame.\\n- Treat Codex plugin registration as weaker evidence than fresh-session tool discovery.\\n- Treat external trackers as separate integrations. Core readiness depends only on configured local evidence coverage, not a vendor plugin or token.\\n- Prefer temp-root smoke tests for install/update behavior.\\n- Do not enable managed rendering unless the user wants fclt to write tool homes.\\n- Preview before mutation and state the exact global/project/plugin target.\\n- Do not report a staged runtime or installed plugin as active until the active\\n handshake and fresh-session discovery have been verified.\\n\\n## Output\\n\\n- current installed version\\n- setup health\\n- paths that matter\\n- commands run\\n- what changed\\n- problem, evidence, reason, target, risk, and expected outcome\\n- verification performed and its actual result\\n- assumptions and fresh-session state\\n- exact undo or rollback path\\n- what still needs approval\\n","skills/fclt-writeback/SKILL.md":"---\\nname: fclt-writeback\\ndescription: Record and review fclt writebacks from real agent work.\\ntags: [fclt, writeback, learning, feedback-loop]\\n---\\n\\n# fclt-writeback\\n\\n## When To Use\\nUse this skill when work reveals durable friction, missing context, weak verification, stale guidance, repeated success, or a capability gap.\\n\\nWriteback is for preserving signal. It is not for every preference or one-off annoyance.\\n\\n## 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/cli.test.ts b/src/cli.test.ts index 5826923a..c9a859e1 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -233,11 +233,15 @@ describe("CLI output contracts", () => { version: number; contextRoot: string; canonical: { globalRoot: string }; + runtime: { evolutionLoopDecisionJournalPath: string }; review: { writebackDir: string; evolutionDir: string }; }; expect(parsed.version).toBe(1); expect(parsed.contextRoot).toBe(join(dir, ".ai")); expect(parsed.canonical.globalRoot).toBe(join(dir, ".ai")); + expect(parsed.runtime.evolutionLoopDecisionJournalPath).toContain( + join("evolution", "loop", "decisions.jsonl") + ); expect(parsed.review.writebackDir).toBe( join(dir, ".ai", "writebacks", "global") ); diff --git a/src/evolution-loop.ts b/src/evolution-loop.ts index b61835fe..6666a180 100644 --- a/src/evolution-loop.ts +++ b/src/evolution-loop.ts @@ -1665,6 +1665,20 @@ async function withLoopLock(args: { } } +export async function withEvolutionLoopMutationLock(args: { + homeDir: string; + rootDir: string; + now?: Date; + fn: () => Promise; +}): Promise { + return await withLoopLock({ + path: facultAiEvolutionLoopLockPath(args.homeDir, args.rootDir), + leaseMinutes: DEFAULT_LEASE_MINUTES, + now: args.now ?? new Date(), + fn: args.fn, + }); +} + async function evolutionLoopStatusScoped(args: { homeDir: string; rootDir: string; diff --git a/src/fclt-mcp-plugin.test.ts b/src/fclt-mcp-plugin.test.ts index 316cb799..8148d312 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:['activity-action-resolve-v1','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-decide-v1','activity-action-resolve-v1','audit-read-only-v1']}));", "} else {", " console.log(JSON.stringify({ cwd: process.cwd(), argv: process.argv.slice(2) }));", "}", @@ -503,6 +503,56 @@ describe("bundled fclt MCP plugin", () => { }, }); + child.stdin.write( + frame({ + jsonrpc: "2.0", + id: 312, + method: "tools/call", + params: { + name: "fclt_registry", + arguments: { + action: "activity_decide", + locator: ACTION_LOCATOR, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }, + }, + }) + ); + const decideResponse = (await readFrame(child.stdout)) as { + result?: { content?: { text?: string }[]; isError?: boolean }; + }; + expect(toolPayload(decideResponse)).toMatchObject({ + operation: { + action: "activity_decide", + preview: false, + risk: "review_producing", + }, + result: { + stdout: { + argv: [ + "ai", + "loop", + "decide", + ACTION_LOCATOR, + "--decision", + "accept", + "--expected-revision", + "3", + "--actor", + "operator-1", + "--approval-ref", + "linear-comment:approval-1", + "--approve", + "--json", + ], + }, + }, + }); + child.stdin.write( frame({ jsonrpc: "2.0", @@ -829,6 +879,10 @@ describe("bundled fclt MCP plugin", () => { additionalProperties?: boolean; properties?: { action?: { const?: string; enum?: string[] }; + actor?: { pattern?: string }; + approve?: { const?: boolean }; + decision?: { enum?: string[] }; + expectedRevision?: { minimum?: number; type?: string }; locator?: { pattern?: string }; sourceIds?: { items?: { pattern?: string } }; }; @@ -840,6 +894,9 @@ describe("bundled fclt MCP plugin", () => { const resolverSchema = registry?.inputSchema?.oneOf?.find( (branch) => branch.properties?.action?.const === "activity_resolve" ); + const decisionSchema = registry?.inputSchema?.oneOf?.find( + (branch) => branch.properties?.action?.const === "activity_decide" + ); const registrySchema = registry?.inputSchema?.oneOf?.find((branch) => branch.properties?.action?.enum?.includes("reconcile") ); @@ -856,6 +913,29 @@ describe("bundled fclt MCP plugin", () => { "action", "locator", ]); + expect(decisionSchema).toMatchObject({ + additionalProperties: false, + required: [ + "action", + "locator", + "decision", + "expectedRevision", + "actor", + "approve", + ], + properties: { + action: { const: "activity_decide" }, + approve: { const: true }, + expectedRevision: { type: "integer", minimum: 1 }, + }, + }); + expect(decisionSchema?.properties?.actor?.pattern).toBeDefined(); + expect(decisionSchema?.properties?.decision?.enum).toEqual([ + "accept", + "redirect", + "reject", + "defer", + ]); expect( registrySchema?.properties?.sourceIds?.items?.pattern ).toBeDefined(); @@ -1042,6 +1122,53 @@ describe("bundled fclt MCP plugin", () => { } }); + it("fails typed activity decisions closed for runtimes without the decision capability", async () => { + const base = await mkdtemp(join(tmpdir(), "facult-mcp-legacy-decision-")); + const stub = join(base, "fclt-legacy.cjs"); + const invoked = join(base, "invoked.txt"); + await Bun.write(stub, legacyProtocolStubScript("2.29.2")); + await chmod(stub, 0o755); + const child = spawnConfiguredMcp({ + env: { + FCLT_BIN: stub, + FCLT_LEGACY_INVOKED: invoked, + HOME: base, + PWD: base, + }, + pluginRoot: facultBuiltinCodexPluginRoot(), + }); + try { + child.stdin.write( + frame({ + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "fclt_registry", + arguments: { + action: "activity_decide", + locator: ACTION_LOCATOR, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + }, + }, + }) + ); + 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-")); @@ -1333,6 +1460,31 @@ describe("bundled fclt MCP plugin", () => { expect(missingLocator.error?.message).toContain( "fclt_registry requires locator" ); + + child.stdin.write( + frame({ + jsonrpc: "2.0", + id: 42, + method: "tools/call", + params: { + name: "fclt_registry", + arguments: { + action: "activity_decide", + locator: ACTION_LOCATOR, + decision: "accept", + expectedRevision: 3, + actor: "operator-1", + approvalReference: "linear-comment:approval-1", + approve: true, + root: "/tmp/unsafe", + }, + }, + }) + ); + const unsafeDecision = (await readFrame(child.stdout)) as { + error?: { message?: string }; + }; + expect(unsafeDecision.error?.message).toMatch(CLOSED_FIELD_ERROR_RE); } finally { child.kill(); } @@ -1674,6 +1826,10 @@ describe("Codex plugin capability matrix", () => { (capability) => capability.id === "evolution_loop.activity_action_resolution" ); + const activityDecision = matrix.capabilities.find( + (capability) => + capability.id === "evolution_loop.activity_decision_lifecycle" + ); expect(new Set(ids).size).toBe(ids.length); expect(matrix.generatedFrom.packageVersion).toBe(packageJson.version); @@ -1721,5 +1877,13 @@ describe("Codex plugin capability matrix", () => { action: "activity_resolve", }, }); + expect(activityDecision).toMatchObject({ + risk: "review_producing", + mcp: { + disposition: "exposed", + tool: "fclt_registry", + action: "activity_decide", + }, + }); }); }); diff --git a/src/paths-command.ts b/src/paths-command.ts index 7fefe1a0..d744f720 100644 --- a/src/paths-command.ts +++ b/src/paths-command.ts @@ -11,6 +11,7 @@ import { facultAiDraftDir, facultAiEvolutionLoopAuditPath, facultAiEvolutionLoopConfigPath, + facultAiEvolutionLoopDecisionJournalPath, facultAiEvolutionLoopReportDir, facultAiEvolutionLoopStatePath, facultAiEvolutionReviewDir, @@ -72,6 +73,7 @@ export interface FacultPaths { draftDir: string; reconciliationStatePath: string; evolutionLoopConfigPath: string; + evolutionLoopDecisionJournalPath: string; evolutionLoopStatePath: string; evolutionLoopAuditPath: string; evolutionLoopReportDir: string; @@ -181,6 +183,8 @@ async function buildPathsInScope(opts?: PathsOptions): Promise { homeDir, contextRoot ), + evolutionLoopDecisionJournalPath: + facultAiEvolutionLoopDecisionJournalPath(homeDir, contextRoot), evolutionLoopStatePath: facultAiEvolutionLoopStatePath( homeDir, contextRoot @@ -255,6 +259,10 @@ function printPaths(paths: FacultPaths) { ["draft dir", paths.runtime.draftDir], ["reconciliation state", paths.runtime.reconciliationStatePath], ["evolution loop config", paths.runtime.evolutionLoopConfigPath], + [ + "evolution decision journal", + paths.runtime.evolutionLoopDecisionJournalPath, + ], ["evolution loop state", paths.runtime.evolutionLoopStatePath], ["evolution loop audit", paths.runtime.evolutionLoopAuditPath], ["evolution loop reports", paths.runtime.evolutionLoopReportDir], diff --git a/src/paths.ts b/src/paths.ts index 3ee37a4c..4132fd6d 100644 --- a/src/paths.ts +++ b/src/paths.ts @@ -764,6 +764,18 @@ export function facultAiEvolutionLoopAuditPath( ); } +export function facultAiEvolutionLoopDecisionJournalPath( + home: string = defaultHomeDir(), + rootDir?: string +): string { + return join( + facultAiRuntimeScopeDir(home, rootDir), + "evolution", + "loop", + "decisions.jsonl" + ); +} + export function facultAiEvolutionLoopReportDir( home: string = defaultHomeDir(), rootDir?: string diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 5d87baca..9c77240d 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-decide-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 5eed59e5..07061e14 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -35,6 +35,7 @@ export async function protocolReport(): Promise { executable: process.execPath, }, capabilities: [ + "activity-action-decide-v1", "activity-action-resolve-v1", "audit-read-only-v1", "json-output-v1", diff --git a/src/reconciliation.test.ts b/src/reconciliation.test.ts index 5831aabe..939f4809 100644 --- a/src/reconciliation.test.ts +++ b/src/reconciliation.test.ts @@ -1555,6 +1555,95 @@ describe("source reconciliation", () => { expect((await run()).resolvedSignalFamilies).not.toContain(familyId); }); + it("reconciles the approved landed families while preserving the explicit hold", async () => { + const fixture = await makeFixture(); + const exportPath = join(fixture.projectRoot, "approved-statuses.json"); + const acceptedFamilyIds = [ + "SF-16812faa55aab6e1", + "SF-9674c0d7f55bf507", + "SF-195578a83ef94eaf", + "SF-73c1628e618c15ed", + ]; + const heldFamilyId = "SF-6dcf56dd1c6dd06e"; + const allFamilyIds = [...acceptedFamilyIds, heldFamilyId]; + await Bun.write( + join(fixture.rootDir, "reconciliation.json"), + JSON.stringify({ + version: 1, + sources: [ + { + id: "approved-statuses", + type: "evidence-export", + path: "approved-statuses.json", + }, + ], + }) + ); + await Bun.write( + exportPath, + JSON.stringify( + evidenceExport( + allFamilyIds.map((familyId, index) => ({ + id: `status-${index + 1}`, + kind: "status-change", + observedAt: "2026-07-05T00:00:00.000Z", + refs: [`WORK-${index + 1}`], + status: familyId === heldFamilyId ? "hold" : "done", + terminal: familyId !== heldFamilyId, + })) + ) + ) + ); + await Bun.write( + facultAiReconciliationStatePath(fixture.homeDir, fixture.rootDir), + `${JSON.stringify( + { + version: 1, + sources: {}, + evidence: {}, + decisions: {}, + families: Object.fromEntries( + allFamilyIds.map((familyId, index) => [ + familyId, + { + firstSeenAt: "2026-07-03T00:00:00.000Z", + lastSeenAt: "2026-07-04T00:00:00.000Z", + subjectKeys: [`issue:WORK-${index + 1}`], + evidenceKeys: [`evidence:${familyId}`], + reviewIds: ["RR-approved"], + signalIds: [`RS-${index + 1}`], + }, + ]) + ), + resolutionProofs: {}, + linkedWorkStatuses: {}, + reviews: {}, + }, + null, + 2 + )}\n` + ); + + const review = await reconcileSources({ + ...fixture, + since: "2026-07-03", + until: "2026-07-10", + persist: false, + }); + + expect(review.resolvedSignalFamilies).toEqual(acceptedFamilyIds.sort()); + expect(review.resolvedSignalFamilies).not.toContain(heldFamilyId); + expect(review.coverageComplete).toBe(true); + expect( + review.signals.find((signal) => signal.issueRefs.includes("WORK-5")) + ).toMatchObject({ unresolved: true }); + expect( + review.signals + .filter((signal) => signal.issueRefs[0] !== "WORK-5") + .every((signal) => signal.unresolved === false) + ).toBe(true); + }); + it("orders linked-work observations by instant across timezone offsets", async () => { const fixture = await makeFixture(); const exportPath = join(fixture.projectRoot, "issues.json"); @@ -2247,6 +2336,89 @@ describe("source reconciliation", () => { expect(await readFile(statePath, "utf8")).toBe(stateBefore); }); + it("reports six stale Git cursors independently from aggregate coverage", async () => { + const fixture = await makeFixture(); + for (const argv of [ + ["init", "--quiet", "--initial-branch=main"], + ["config", "user.email", "fixture@example.invalid"], + ["config", "user.name", "Fixture"], + ]) { + await runFixtureGit({ projectRoot: fixture.projectRoot, argv }); + } + await mkdir(join(fixture.projectRoot, "docs"), { recursive: true }); + await Bun.write( + join(fixture.projectRoot, "docs", "review.md"), + "Approved cursor repair baseline.\n" + ); + await runFixtureGit({ + projectRoot: fixture.projectRoot, + argv: ["add", "docs"], + }); + await runFixtureGit({ + projectRoot: fixture.projectRoot, + argv: ["commit", "--quiet", "-m", "docs: cursor baseline"], + date: "2026-07-23T18:12:45-04:00", + }); + const sourceIds = Array.from( + { length: 6 }, + (_, index) => `git-cursor-${index + 1}` + ); + await Bun.write( + join(fixture.rootDir, "reconciliation.json"), + JSON.stringify({ + version: 1, + sources: sourceIds.map((id) => ({ + id, + type: "git", + paths: ["docs"], + defaultBranch: "main", + freshnessThresholdHours: 168, + })), + }) + ); + await reconcileSources({ + ...fixture, + since: "2026-07-23T00:00:00-04:00", + until: "2026-07-23T18:15:00-04:00", + incremental: true, + }); + + await Bun.write(join(fixture.projectRoot, "outside.txt"), "new activity\n"); + await runFixtureGit({ + projectRoot: fixture.projectRoot, + argv: ["add", "outside.txt"], + }); + await runFixtureGit({ + projectRoot: fixture.projectRoot, + argv: ["commit", "--quiet", "-m", "fix: newer activity"], + date: "2026-07-23T18:28:50-04:00", + }); + const review = await reconcileSources({ + ...fixture, + since: "2026-07-23T00:00:00-04:00", + until: "2026-07-27T23:04:10Z", + incremental: true, + persist: false, + }); + + expect(review.coverageComplete).toBe(true); + expect(review.degraded).toBe(false); + expect(review.coverage).toHaveLength(6); + expect( + review.coverage.every( + (entry) => entry.state === "checked" || entry.state === "changed" + ) + ).toBe(true); + expect( + review.coverage.every((entry) => entry.freshness.state === "stale") + ).toBe(true); + expect(review.freshness).toMatchObject({ + state: "stale", + staleSourceIds: sourceIds, + alertSourceIds: sourceIds, + }); + }); + it("bounds repository freshness activity to the review window", async () => { const fixture = await makeFixture(); for (const argv of [