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 +1 -1
- package/plugins/fclt/.mcp.json +2 -1
- package/src/ai.ts +13 -0
- package/src/builtin-assets.ts +1 -1
- package/src/doctor.ts +40 -16
- package/src/global-docs.ts +2 -2
- package/src/manage.ts +3 -2
- package/src/paths.ts +46 -1
- package/src/self-update.ts +54 -24
package/package.json
CHANGED
package/plugins/fclt/.mcp.json
CHANGED
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
|
|
package/src/builtin-assets.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
|
658
|
-
fix:
|
|
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:
|
|
1023
|
-
|
|
1024
|
-
|
|
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());
|
package/src/global-docs.ts
CHANGED
|
@@ -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
|
|
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
|
|
293
|
-
sourcePath
|
|
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 {
|
|
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 {
|
package/src/self-update.ts
CHANGED
|
@@ -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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
return
|
|
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
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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",
|