facult 2.17.4 → 2.17.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "2.17.4",
3
+ "version": "2.17.7",
4
4
  "description": "Manage canonical AI capabilities, sync surfaces, and evolution state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,7 +5,8 @@
5
5
  "args": ["./scripts/fclt-mcp.cjs"],
6
6
  "env": {
7
7
  "FCLT_BIN": "fclt"
8
- }
8
+ },
9
+ "cwd": "."
9
10
  }
10
11
  }
11
12
  }
package/src/ai.ts CHANGED
@@ -491,6 +491,18 @@ async function writeProposalReviewArtifact(args: {
491
491
  await Bun.write(pathValue, `${body.trimEnd()}\n`);
492
492
  }
493
493
 
494
+ async function currentDraftBodyForProposal(
495
+ proposal: AiProposalRecord
496
+ ): Promise<string | null> {
497
+ const draftPath = proposal.draftRefs.find((pathValue) =>
498
+ pathValue.endsWith(".md")
499
+ );
500
+ if (!(draftPath && (await fileExists(draftPath)))) {
501
+ return null;
502
+ }
503
+ return readFile(draftPath, "utf8");
504
+ }
505
+
494
506
  function slugToTitle(value: string): string {
495
507
  return value
496
508
  .split(SLUG_SPLIT_RE)
@@ -1375,6 +1387,7 @@ export async function refreshAiReviewArtifacts(args: {
1375
1387
  rootDir: args.rootDir,
1376
1388
  proposal,
1377
1389
  writebacks: sourceWritebacks,
1390
+ draftBody: await currentDraftBodyForProposal(proposal),
1378
1391
  });
1379
1392
  }
1380
1393
 
@@ -7,5 +7,5 @@ export const BUILTIN_OPERATING_MODEL_FILES = JSON.parse(
7
7
 
8
8
  export const BUILTIN_FCLT_CODEX_PLUGIN_FILES = JSON.parse(
9
9
  // biome-ignore lint/suspicious/noTemplateCurlyInString: Built-in plugin files intentionally contain literal render placeholders.
10
- '{".codex-plugin/plugin.json":"{\\n \\"name\\": \\"fclt\\",\\n \\"version\\": \\"0.1.0\\",\\n \\"description\\": \\"Codex workflows and MCP tools for fclt setup, writeback, evolution, and capability review.\\",\\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\\": \\"Capability loops for Codex\\",\\n \\"longDescription\\": \\"Install and operate fclt from Codex: initialize AI capability roots, inspect setup health, record writebacks, review evolution proposals, and keep skills, snippets, instructions, automations, and MCP surfaces improving over time.\\",\\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 \\"Record useful fclt writeback from this work.\\",\\n \\"Review fclt evolution proposals and next actions.\\"\\n ],\\n \\"brandColor\\": \\"#166534\\"\\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 }\\n }\\n}\\n","scripts/fclt-mcp.cjs":"#!/usr/bin/env node\\n\\"use strict\\";\\n\\nconst { spawn } = require(\\"node:child_process\\");\\n\\nconst FCLT_BIN = process.env.FCLT_BIN || \\"fclt\\";\\nconst DEFAULT_TIMEOUT_MS = Number(process.env.FCLT_MCP_TIMEOUT_MS || 60_000);\\nconst CONTENT_LENGTH_RE = /Content-Length:\\\\s*(\\\\d+)/i;\\n\\nconst tools = [\\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 },\\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: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n summary: { type: \\"string\\" },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"string\\" },\\n confidence: {\\n type: \\"string\\",\\n enum: [\\"low\\", \\"medium\\", \\"high\\"],\\n },\\n },\\n required: [\\"kind\\", \\"summary\\"],\\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: [\\"auto\\", \\"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 },\\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 commandForTool(name, args = {}) {\\n switch (name) {\\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 return [\\n \\"templates\\",\\n \\"init\\",\\n \\"operating-model\\",\\n ...scopeArgs(args.scope),\\n ...boolFlag(\\"--update\\", args.update),\\n ...boolFlag(\\"--dry-run\\", args.dryRun),\\n ...boolFlag(\\"--force\\", args.force),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_add\\":\\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(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\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 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 runFclt(args, cwd) {\\n return new Promise((resolve) => {\\n const child = spawn(FCLT_BIN, args, {\\n cwd: cwd || process.cwd(),\\n env: process.env,\\n stdio: [\\"ignore\\", \\"pipe\\", \\"pipe\\"],\\n });\\n let stdout = \\"\\";\\n let stderr = \\"\\";\\n const timer = setTimeout(() => {\\n child.kill(\\"SIGTERM\\");\\n }, DEFAULT_TIMEOUT_MS);\\n child.stdout.on(\\"data\\", (chunk) => {\\n stdout += chunk.toString();\\n });\\n child.stderr.on(\\"data\\", (chunk) => {\\n stderr += chunk.toString();\\n });\\n child.on(\\"close\\", (code) => {\\n clearTimeout(timer);\\n resolve({\\n code,\\n text: [\\n `$ ${FCLT_BIN} ${args.join(\\" \\")}`,\\n stdout.trim(),\\n stderr.trim() ? `stderr:\\\\n${stderr.trim()}` : \\"\\",\\n ]\\n .filter(Boolean)\\n .join(\\"\\\\n\\\\n\\"),\\n });\\n });\\n });\\n}\\n\\nfunction send(message) {\\n const body = JSON.stringify(message);\\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: \\"0.1.0\\" },\\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 const command = commandForTool(name, args);\\n const result = await runFclt(command, args.cwd);\\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\\nprocess.stdin.on(\\"data\\", (chunk) => {\\n buffer = Buffer.concat([buffer, chunk]);\\n while (true) {\\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 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});\\n\\nif (process.argv.includes(\\"--self-test\\")) {\\n console.log(\\n JSON.stringify({ tools: tools.map((tool) => tool.name) }, null, 2)\\n );\\n process.exit(0);\\n}\\n","skills/fclt-capability-review/SKILL.md":"---\\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\\n## Output\\n\\n- capability roots found\\n- relevant assets\\n- scope recommendation\\n- missing or stale capability\\n- safe next command\\n","skills/fclt-evolution/SKILL.md":"---\\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 writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai evolve list\\n```\\n\\n2. Assess proposal readiness before mutating state:\\n\\n```bash\\nfclt ai evolve assess --asset <selector> --json\\n```\\n\\nUse the assessment recommendation as the decision checkpoint:\\n\\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\\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 Linear or another 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\\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","skills/fclt-setup/SKILL.md":"---\\ndescription: Install, update, inspect, and initialize fclt from Codex.\\ntags: [fclt, setup, codex, onboarding]\\n---\\n\\n# fclt-setup\\n\\n## When To Use\\nUse this skill when a user wants Codex to install, update, configure, inspect, or repair fclt.\\n\\nUse it for:\\n\\n- checking whether `fclt` is installed and current\\n- initializing global `~/.ai` or project `<repo>/.ai`\\n- installing or refreshing the built-in operating-model pack\\n- checking setup health with `doctor`\\n- finding canonical, generated, runtime, and review paths\\n\\n## Workflow\\n\\n1. Check current state:\\n\\n```bash\\nfclt --version\\nfclt paths --json\\nfclt doctor --json\\n```\\n\\n2. If global capability is missing, initialize it:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n3. If a repo needs local capability, initialize project AI:\\n\\n```bash\\nfclt templates init project-ai\\n```\\n\\n4. Refresh pack defaults non-destructively:\\n\\n```bash\\nfclt templates init operating-model --global --update --dry-run\\nfclt templates init operating-model --global --update\\n```\\n\\n5. Use `--force` only when the user explicitly wants to replace local edits.\\n\\n## Rules\\n\\n- Preserve existing `AGENTS.md`, `CLAUDE.md`, and `AGENTS.global.md` guidance.\\n- First install should seed from existing agent guidance when available.\\n- Treat `doctor --json` issues as setup facts, not user-facing blame.\\n- 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\\n## Output\\n\\n- current installed version\\n- setup health\\n- paths that matter\\n- commands run\\n- what changed\\n- what still needs approval\\n","skills/fclt-writeback/SKILL.md":"---\\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 --kind missing_context --summary \\"...\\" --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\\n```\\n\\n## Rules\\n\\n- Prefer one high-signal writeback over several weak ones.\\n- Include concrete evidence when possible.\\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\\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"}'
10
+ '{".codex-plugin/plugin.json":"{\\n \\"name\\": \\"fclt\\",\\n \\"version\\": \\"0.1.0\\",\\n \\"description\\": \\"Codex workflows and MCP tools for fclt setup, writeback, evolution, and capability review.\\",\\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\\": \\"Capability loops for Codex\\",\\n \\"longDescription\\": \\"Install and operate fclt from Codex: initialize AI capability roots, inspect setup health, record writebacks, review evolution proposals, and keep skills, snippets, instructions, automations, and MCP surfaces improving over time.\\",\\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 \\"Record useful fclt writeback from this work.\\",\\n \\"Review fclt evolution proposals and next actions.\\"\\n ],\\n \\"brandColor\\": \\"#166534\\"\\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 { spawn } = require(\\"node:child_process\\");\\n\\nconst FCLT_BIN = process.env.FCLT_BIN || \\"fclt\\";\\nconst DEFAULT_TIMEOUT_MS = Number(process.env.FCLT_MCP_TIMEOUT_MS || 60_000);\\nconst CONTENT_LENGTH_RE = /Content-Length:\\\\s*(\\\\d+)/i;\\n\\nconst tools = [\\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 },\\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: [\\"auto\\", \\"global\\", \\"project\\"] },\\n cwd: { type: \\"string\\" },\\n kind: { type: \\"string\\" },\\n summary: { type: \\"string\\" },\\n asset: { type: \\"string\\" },\\n evidence: { type: \\"string\\" },\\n confidence: {\\n type: \\"string\\",\\n enum: [\\"low\\", \\"medium\\", \\"high\\"],\\n },\\n },\\n required: [\\"kind\\", \\"summary\\"],\\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: [\\"auto\\", \\"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 },\\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 commandForTool(name, args = {}) {\\n switch (name) {\\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 return [\\n \\"templates\\",\\n \\"init\\",\\n \\"operating-model\\",\\n ...scopeArgs(args.scope),\\n ...boolFlag(\\"--update\\", args.update),\\n ...boolFlag(\\"--dry-run\\", args.dryRun),\\n ...boolFlag(\\"--force\\", args.force),\\n \\"--json\\",\\n ];\\n case \\"fclt_writeback_add\\":\\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(\\"--evidence\\", args.evidence),\\n ...stringFlag(\\"--confidence\\", args.confidence),\\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 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 runFclt(args, cwd) {\\n return new Promise((resolve) => {\\n const child = spawn(FCLT_BIN, args, {\\n cwd: cwd || process.cwd(),\\n env: process.env,\\n stdio: [\\"ignore\\", \\"pipe\\", \\"pipe\\"],\\n });\\n let stdout = \\"\\";\\n let stderr = \\"\\";\\n const timer = setTimeout(() => {\\n child.kill(\\"SIGTERM\\");\\n }, DEFAULT_TIMEOUT_MS);\\n child.stdout.on(\\"data\\", (chunk) => {\\n stdout += chunk.toString();\\n });\\n child.stderr.on(\\"data\\", (chunk) => {\\n stderr += chunk.toString();\\n });\\n child.on(\\"close\\", (code) => {\\n clearTimeout(timer);\\n resolve({\\n code,\\n text: [\\n `$ ${FCLT_BIN} ${args.join(\\" \\")}`,\\n stdout.trim(),\\n stderr.trim() ? `stderr:\\\\n${stderr.trim()}` : \\"\\",\\n ]\\n .filter(Boolean)\\n .join(\\"\\\\n\\\\n\\"),\\n });\\n });\\n });\\n}\\n\\nfunction send(message) {\\n const body = JSON.stringify(message);\\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: \\"0.1.0\\" },\\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 const command = commandForTool(name, args);\\n const result = await runFclt(command, args.cwd);\\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\\nprocess.stdin.on(\\"data\\", (chunk) => {\\n buffer = Buffer.concat([buffer, chunk]);\\n while (true) {\\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 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});\\n\\nif (process.argv.includes(\\"--self-test\\")) {\\n console.log(\\n JSON.stringify({ tools: tools.map((tool) => tool.name) }, null, 2)\\n );\\n process.exit(0);\\n}\\n","skills/fclt-capability-review/SKILL.md":"---\\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\\n## Output\\n\\n- capability roots found\\n- relevant assets\\n- scope recommendation\\n- missing or stale capability\\n- safe next command\\n","skills/fclt-evolution/SKILL.md":"---\\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 writeback group --by asset\\nfclt ai writeback summarize --by domain\\nfclt ai evolve list\\n```\\n\\n2. Assess proposal readiness before mutating state:\\n\\n```bash\\nfclt ai evolve assess --asset <selector> --json\\n```\\n\\nUse the assessment recommendation as the decision checkpoint:\\n\\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\\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 Linear or another 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\\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","skills/fclt-setup/SKILL.md":"---\\ndescription: Install, update, inspect, and initialize fclt from Codex.\\ntags: [fclt, setup, codex, onboarding]\\n---\\n\\n# fclt-setup\\n\\n## When To Use\\nUse this skill when a user wants Codex to install, update, configure, inspect, or repair fclt.\\n\\nUse it for:\\n\\n- checking whether `fclt` is installed and current\\n- initializing global `~/.ai` or project `<repo>/.ai`\\n- installing or refreshing the built-in operating-model pack\\n- checking setup health with `doctor`\\n- finding canonical, generated, runtime, and review paths\\n\\n## Workflow\\n\\n1. Check current state:\\n\\n```bash\\nfclt --version\\nfclt paths --json\\nfclt doctor --json\\n```\\n\\n2. If global capability is missing, initialize it:\\n\\n```bash\\nfclt templates init operating-model --global\\n```\\n\\n3. If a repo needs local capability, initialize project AI:\\n\\n```bash\\nfclt templates init project-ai\\n```\\n\\n4. Refresh pack defaults non-destructively:\\n\\n```bash\\nfclt templates init operating-model --global --update --dry-run\\nfclt templates init operating-model --global --update\\n```\\n\\n5. Use `--force` only when the user explicitly wants to replace local edits.\\n\\n## Rules\\n\\n- Preserve existing `AGENTS.md`, `CLAUDE.md`, and `AGENTS.global.md` guidance.\\n- First install should seed from existing agent guidance when available.\\n- Treat `doctor --json` issues as setup facts, not user-facing blame.\\n- 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\\n## Output\\n\\n- current installed version\\n- setup health\\n- paths that matter\\n- commands run\\n- what changed\\n- what still needs approval\\n","skills/fclt-writeback/SKILL.md":"---\\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 --kind missing_context --summary \\"...\\" --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\\n```\\n\\n## Rules\\n\\n- Prefer one high-signal writeback over several weak ones.\\n- Include concrete evidence when possible.\\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\\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"}'
11
11
  ) as Record<string, string>;
package/src/doctor.ts CHANGED
@@ -476,7 +476,12 @@ async function repairCanonicalGlobalDocs(args: {
476
476
  home: string;
477
477
  rootDir: string;
478
478
  }): Promise<{ changed: boolean; backupPath?: string }> {
479
- const inspected = await inspectCanonicalGlobalDocs(args.rootDir);
479
+ if (projectRootFromAiRoot(args.rootDir, args.home)) {
480
+ return { changed: false };
481
+ }
482
+ const inspected = await inspectCanonicalGlobalDocs(args.rootDir, {
483
+ projectRoot: null,
484
+ });
480
485
  if (!(inspected.exists && !inspected.valid)) {
481
486
  return { changed: false };
482
487
  }
@@ -494,13 +499,13 @@ async function repairCanonicalGlobalDocs(args: {
494
499
  await copyFile(targetPath, backupPath);
495
500
  await mkdir(dirname(targetPath), { recursive: true });
496
501
  await writeFile(targetPath, await readFile(sourcePath, "utf8"), "utf8");
497
- await copyMissingBuiltinOperatingModelSnippets(args.rootDir);
502
+ await repairBuiltinOperatingModelSnippets(args.rootDir);
498
503
  await ensureAiIndexPath({ homeDir: args.home, rootDir: args.rootDir });
499
504
  await ensureAiGraphPath({ homeDir: args.home, rootDir: args.rootDir });
500
505
  return { changed: true, backupPath };
501
506
  }
502
507
 
503
- async function copyMissingBuiltinOperatingModelSnippets(
508
+ async function repairBuiltinOperatingModelSnippets(
504
509
  rootDir: string
505
510
  ): Promise<void> {
506
511
  const relPaths = [
@@ -515,7 +520,10 @@ async function copyMissingBuiltinOperatingModelSnippets(
515
520
  for (const relPath of relPaths) {
516
521
  const sourcePath = join(sourceRoot, relPath);
517
522
  const targetPath = join(rootDir, relPath);
518
- if (await pathExists(targetPath)) {
523
+ const existing = (await pathExists(targetPath))
524
+ ? await readFile(targetPath, "utf8")
525
+ : null;
526
+ if (existing !== null && existing.trim().length > 0) {
519
527
  continue;
520
528
  }
521
529
  await mkdir(dirname(targetPath), { recursive: true });
@@ -601,7 +609,10 @@ const UNRESOLVED_REFS_TEMPLATE_RE = /\$\{refs\.([A-Za-z0-9_.-]+)\}/g;
601
609
  const FCLTY_BLOCK_RE =
602
610
  /<!--\s*fclty:([^>]+?)\s*-->([\s\S]*?)<!--\s*\/fclty:\1\s*-->/g;
603
611
 
604
- async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
612
+ async function inspectCanonicalGlobalDocs(
613
+ rootDir: string,
614
+ opts: { projectRoot?: string | null } = {}
615
+ ): Promise<{
605
616
  exists: boolean;
606
617
  valid: boolean;
607
618
  issues: DoctorIssue[];
@@ -613,6 +624,12 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
613
624
 
614
625
  const text = await readFile(pathValue, "utf8");
615
626
  const issues: DoctorIssue[] = [];
627
+ const refreshCommand = opts.projectRoot
628
+ ? "fclt templates init project-ai --force"
629
+ : "fclt templates init operating-model --global --force";
630
+ const docLabel = opts.projectRoot
631
+ ? "project AGENTS.global.md"
632
+ : "AGENTS.global.md";
616
633
  const withSnippets = await renderSnippetText({
617
634
  text,
618
635
  filePath: pathValue,
@@ -623,7 +640,7 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
623
640
  severity: "warning",
624
641
  code: "canonical-global-docs-render-error",
625
642
  message: error,
626
- fix: "Review AGENTS.global.md snippet markers or refresh the built-in operating model with `fclt templates init operating-model --global --force`.",
643
+ fix: `Review ${docLabel} snippet markers or refresh the selected capability root with \`${refreshCommand}\`.`,
627
644
  });
628
645
  }
629
646
  const rendered = await renderCanonicalText(withSnippets.text, {
@@ -636,9 +653,8 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
636
653
  issues.push({
637
654
  severity: "warning",
638
655
  code: "canonical-global-docs-unresolved-template",
639
- message:
640
- "Rendered AGENTS.global.md contains unresolved template references.",
641
- fix: "Review AGENTS.global.md refs or refresh the built-in operating model with `fclt templates init operating-model --global --force`.",
656
+ message: `Rendered ${docLabel} contains unresolved template references.`,
657
+ fix: `Review ${docLabel} refs or refresh the selected capability root with \`${refreshCommand}\`.`,
642
658
  });
643
659
  }
644
660
 
@@ -654,8 +670,8 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
654
670
  issues.push({
655
671
  severity: "warning",
656
672
  code: "canonical-global-docs-empty-managed-sections",
657
- message: `Rendered AGENTS.global.md has empty fclty managed sections: ${emptySections.join(", ")}.`,
658
- fix: "Add the missing snippets or refresh the built-in operating model with `fclt templates init operating-model --global --force`.",
673
+ message: `Rendered ${docLabel} has empty fclty managed sections: ${emptySections.join(", ")}.`,
674
+ fix: `Add the missing snippets or refresh the selected capability root with \`${refreshCommand}\`.`,
659
675
  });
660
676
  }
661
677
 
@@ -976,7 +992,7 @@ export async function buildDoctorReport(opts?: {
976
992
  pathExists(generatedGraph),
977
993
  pathExists(writebackReviewDir),
978
994
  pathExists(evolutionReviewDir),
979
- inspectCanonicalGlobalDocs(rootDir),
995
+ inspectCanonicalGlobalDocs(rootDir, { projectRoot }),
980
996
  inspectCanonicalTemplateRefs(rootDir),
981
997
  planProjectSyncPolicyRepair({ home, rootDir }),
982
998
  ]);
@@ -1019,9 +1035,15 @@ export async function buildDoctorReport(opts?: {
1019
1035
  }
1020
1036
  if (canonicalGlobalDocs.exists && !canonicalGlobalDocs.valid) {
1021
1037
  actions.push({
1022
- id: "refresh-global-operating-model",
1023
- label: "Refresh global operating model",
1024
- command: "fclt templates init operating-model --global --force",
1038
+ id: projectRoot
1039
+ ? "refresh-project-operating-model"
1040
+ : "refresh-global-operating-model",
1041
+ label: projectRoot
1042
+ ? "Refresh project operating model"
1043
+ : "Refresh global operating model",
1044
+ command: projectRoot
1045
+ ? "fclt templates init project-ai --force"
1046
+ : "fclt templates init operating-model --global --force",
1025
1047
  risk: "canonical_write",
1026
1048
  });
1027
1049
  }
@@ -1369,7 +1391,9 @@ export async function doctorCommand(argv: string[]) {
1369
1391
  `Project sync is still implicit for managed tools (${projectSyncRepairTools.join(", ")}). Run \`fclt doctor --repair\` to write explicit [project_sync.<tool>] entries.`
1370
1392
  );
1371
1393
  }
1372
- const canonicalGlobalDocs = await inspectCanonicalGlobalDocs(rootDir);
1394
+ const canonicalGlobalDocs = await inspectCanonicalGlobalDocs(rootDir, {
1395
+ projectRoot: projectRootFromAiRoot(rootDir, home),
1396
+ });
1373
1397
  if (canonicalGlobalDocs.exists && !canonicalGlobalDocs.valid) {
1374
1398
  for (const issue of canonicalGlobalDocs.issues) {
1375
1399
  console.log(`${issue.message} ${issue.fix ?? ""}`.trim());
@@ -6,7 +6,7 @@ import {
6
6
  facultBuiltinAgentsGlobalSourcePath,
7
7
  facultBuiltinPackRoot,
8
8
  } from "./builtin";
9
- import { projectRootFromAiRoot } from "./paths";
9
+ import { pathIsInsideOrEqual, projectRootFromAiRoot } from "./paths";
10
10
  import { projectSyncAllowsToolSurface } from "./project-sync";
11
11
  import { renderSnippetText } from "./snippets";
12
12
 
@@ -233,7 +233,7 @@ async function renderSourceTarget(args: {
233
233
  }): Promise<string> {
234
234
  const raw = await Bun.file(args.sourcePath).text();
235
235
  const builtinRoot = facultBuiltinPackRoot();
236
- const sourceRoot = args.sourcePath.startsWith(`${builtinRoot}/`)
236
+ const sourceRoot = pathIsInsideOrEqual(args.sourcePath, builtinRoot)
237
237
  ? builtinRoot
238
238
  : args.rootDir;
239
239
  const withSnippets = await renderSnippetText({
package/src/manage.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  facultMachineStateDir,
49
49
  facultRootDir,
50
50
  legacyFacultStateDirForRoot,
51
+ pathIsInsideOrEqual,
51
52
  projectRootFromAiRoot,
52
53
  } from "./paths";
53
54
  import { loadProjectToolSyncPolicy } from "./project-sync";
@@ -289,8 +290,8 @@ async function isGeneratedOnlyProjectRoot(args: {
289
290
  function renderedSourceKindForPath(
290
291
  sourcePath: string
291
292
  ): ManagedRenderedTargetState["sourceKind"] {
292
- return sourcePath.startsWith(facultBuiltinPackRoot()) ||
293
- sourcePath.startsWith(facultBuiltinCodexPluginRoot())
293
+ return pathIsInsideOrEqual(sourcePath, facultBuiltinPackRoot()) ||
294
+ pathIsInsideOrEqual(sourcePath, facultBuiltinCodexPluginRoot())
294
295
  ? "builtin"
295
296
  : "canonical";
296
297
  }
package/src/paths.ts CHANGED
@@ -1,9 +1,19 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { basename, dirname, join, resolve } from "node:path";
4
+ import {
5
+ basename,
6
+ dirname,
7
+ isAbsolute,
8
+ join,
9
+ relative,
10
+ resolve,
11
+ win32,
12
+ } from "node:path";
5
13
  import { parseJsonLenient } from "./util/json";
6
14
 
15
+ const WINDOWS_ABSOLUTE_PATH_RE = /^[A-Za-z]:[\\/]/;
16
+
7
17
  export interface FacultConfig {
8
18
  /**
9
19
  * Override the canonical root directory.
@@ -88,6 +98,41 @@ export function preferredGlobalAiRoot(home: string = defaultHomeDir()): string {
88
98
  return join(home, ".ai");
89
99
  }
90
100
 
101
+ function looksLikeWindowsAbsolutePath(pathValue: string): boolean {
102
+ return (
103
+ WINDOWS_ABSOLUTE_PATH_RE.test(pathValue) || pathValue.startsWith("\\\\")
104
+ );
105
+ }
106
+
107
+ function relativePathIsInsideOrEqual(args: {
108
+ rel: string;
109
+ isAbsolutePath: (pathValue: string) => boolean;
110
+ }): boolean {
111
+ const { isAbsolutePath, rel } = args;
112
+ return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolutePath(rel));
113
+ }
114
+
115
+ export function pathIsInsideOrEqual(
116
+ pathValue: string,
117
+ rootDir: string
118
+ ): boolean {
119
+ if (
120
+ looksLikeWindowsAbsolutePath(pathValue) &&
121
+ looksLikeWindowsAbsolutePath(rootDir)
122
+ ) {
123
+ return relativePathIsInsideOrEqual({
124
+ isAbsolutePath: win32.isAbsolute,
125
+ rel: win32.relative(rootDir, pathValue),
126
+ });
127
+ }
128
+
129
+ const rel = relative(rootDir, pathValue);
130
+ return relativePathIsInsideOrEqual({
131
+ isAbsolutePath: isAbsolute,
132
+ rel,
133
+ });
134
+ }
135
+
91
136
  export function preferredGlobalFacultStateDir(
92
137
  home: string = defaultHomeDir()
93
138
  ): string {
@@ -195,9 +195,6 @@ async function activeFcltUsesMiseNpmFacult(): Promise<boolean> {
195
195
  if (looksLikeMiseNpmFacultExecutable(fcltPath ?? "")) {
196
196
  return true;
197
197
  }
198
- if (await miseHasCurrentFacultTool()) {
199
- return true;
200
- }
201
198
  if (!looksLikeMiseShim(fcltPath)) {
202
199
  return false;
203
200
  }
@@ -218,20 +215,21 @@ async function activeFcltUsesMiseNpmFacult(): Promise<boolean> {
218
215
  return looksLikeMiseNpmFacultExecutable(stdout.trim());
219
216
  }
220
217
 
221
- async function miseHasCurrentFacultTool(): Promise<boolean> {
222
- const proc = Bun.spawn({
223
- cmd: ["mise", "current", `npm:${PACKAGE_NAME}`],
224
- stdin: "ignore",
225
- stdout: "ignore",
226
- stderr: "ignore",
227
- env: process.env,
228
- });
229
- return (await proc.exited) === 0;
218
+ export function buildCommandLookupFallback(
219
+ command: string,
220
+ platform: NodeJS.Platform = process.platform
221
+ ): string[] {
222
+ if (platform === "win32") {
223
+ return ["where.exe", command];
224
+ }
225
+ const quoted = `'${command.replaceAll("'", "'\\''")}'`;
226
+ return ["sh", "-lc", `command -v ${quoted}`];
230
227
  }
231
228
 
232
229
  async function resolveCommandPath(command: string): Promise<string | null> {
230
+ const cmd = buildCommandLookupFallback(command);
233
231
  const proc = Bun.spawn({
234
- cmd: ["sh", "-lc", `command -v ${command}`],
232
+ cmd,
235
233
  stdin: "ignore",
236
234
  stdout: "pipe",
237
235
  stderr: "ignore",
@@ -247,6 +245,17 @@ async function resolveCommandPath(command: string): Promise<string | null> {
247
245
  return stdout.trim() || null;
248
246
  }
249
247
 
248
+ export function looksLikeMiseNpmFacultExecutableForVersion(
249
+ executablePath: string,
250
+ version: string
251
+ ): boolean {
252
+ const normalized = executablePath.split("\\").join(sep);
253
+ return (
254
+ looksLikeMiseNpmFacultExecutable(normalized) &&
255
+ normalized.includes(`${sep}npm-facult${sep}${version}${sep}`)
256
+ );
257
+ }
258
+
250
259
  function resolvePlatformTarget(): {
251
260
  platform: string;
252
261
  arch: string;
@@ -516,17 +525,38 @@ async function assertActiveFcltVersion(
516
525
  expectedVersion: string,
517
526
  packageManager: PackageManager
518
527
  ): Promise<void> {
519
- const cmd =
520
- packageManager === "mise"
521
- ? [
522
- "mise",
523
- "exec",
524
- `npm:${PACKAGE_NAME}@${expectedVersion}`,
525
- "--",
526
- "fclt",
527
- "--version",
528
- ]
529
- : ["fclt", "--version"];
528
+ let cmd = ["fclt", "--version"];
529
+ if (packageManager === "mise") {
530
+ const which = Bun.spawn({
531
+ cmd: ["mise", "which", "fclt"],
532
+ stdin: "ignore",
533
+ stdout: "pipe",
534
+ stderr: "pipe",
535
+ env: process.env,
536
+ });
537
+ const [whichStdout, whichStderr, whichCode] = await Promise.all([
538
+ new Response(which.stdout).text(),
539
+ new Response(which.stderr).text(),
540
+ which.exited,
541
+ ]);
542
+ const resolvedPath = whichStdout.trim();
543
+ if (whichCode !== 0) {
544
+ throw new Error(
545
+ `Updated package, but could not verify active mise fclt path: ${whichStderr.trim()}`
546
+ );
547
+ }
548
+ if (
549
+ !looksLikeMiseNpmFacultExecutableForVersion(resolvedPath, expectedVersion)
550
+ ) {
551
+ throw new Error(
552
+ [
553
+ `Updated package to ${expectedVersion}, but mise resolves fclt to ${resolvedPath || "nothing"}.`,
554
+ "Run `mise which fclt` and `mise current npm:facult` to inspect the active tool selection.",
555
+ ].join("\n")
556
+ );
557
+ }
558
+ cmd = [resolvedPath, "--version"];
559
+ }
530
560
  const proc = Bun.spawn({
531
561
  cmd,
532
562
  stdin: "ignore",