facult 2.11.0 → 2.13.0

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/src/remote.ts CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  type RemoteAgentItem,
44
44
  type RemoteIndexItem,
45
45
  type RemoteIndexManifest,
46
+ type RemoteInstructionItem,
46
47
  type RemoteItemType,
47
48
  type RemoteMcpItem,
48
49
  type RemoteSkillItem,
@@ -60,6 +61,8 @@ const QUERY_SPLIT_RE = /\s+/;
60
61
  const MD_EXT_RE = /\.md$/i;
61
62
  const FILE_EXT_RE = /\.[A-Za-z0-9]+$/;
62
63
  const TRAILING_SLASH_RE = /\/+$/;
64
+ const LEADING_SLASH_RE = /^\/+/;
65
+ const INSTRUCTION_TITLE_SPLIT_RE = /[-_.\s]+/;
63
66
  const PROMPT_PATH_SPLIT_RE = /[,\n]/;
64
67
  const GIT_WORKTREE_LINE_RE = /\r?\n/;
65
68
 
@@ -222,6 +225,52 @@ Use this skill when the task repeatedly follows a known workflow and you want co
222
225
  },
223
226
  },
224
227
  },
228
+ {
229
+ id: "instruction-template",
230
+ type: "instruction",
231
+ title: "Instruction Template",
232
+ description:
233
+ "Reusable markdown instruction scaffold with ref and snippet composition examples.",
234
+ version: "1.0.0",
235
+ tags: ["template", "dx", "instruction"],
236
+ instruction: {
237
+ name: "WORKFLOW.md",
238
+ content: `---
239
+ description: "{{name}} reusable instruction"
240
+ tags: [instruction, workflow]
241
+ ---
242
+
243
+ # {{title}}
244
+
245
+ Use this instruction when the task needs repeatable guidance that should be discoverable, targetable by writeback, and composable into agent docs.
246
+
247
+ ## Scope
248
+
249
+ - Applies to:
250
+ - Does not apply to:
251
+ - Project-specific overrides:
252
+
253
+ ## Guidance
254
+
255
+ - Keep the rule concrete enough that another agent can follow it without chat context.
256
+ - Link deeper reusable guidance with canonical refs such as \`@ai/instructions/VERIFICATION.md\` or \`@project/instructions/TESTING.md\`.
257
+ - Reuse stable partials with snippet markers when the same block appears in more than one rendered doc.
258
+
259
+ ## Composition
260
+
261
+ <!-- fclty:global/team/example -->
262
+ <!-- /fclty:global/team/example -->
263
+
264
+ ## Writeback Targeting
265
+
266
+ Record durable friction against this instruction with:
267
+
268
+ \`\`\`bash
269
+ fclt ai writeback add --kind missing_context --summary "<what was missing>" --asset instruction:{{assetName}}
270
+ \`\`\`
271
+ `,
272
+ },
273
+ },
225
274
  {
226
275
  id: "mcp-stdio-template",
227
276
  type: "mcp",
@@ -1461,7 +1510,8 @@ function parseIndexItem(raw: unknown): RemoteIndexItem | null {
1461
1510
  type !== "skill" &&
1462
1511
  type !== "mcp" &&
1463
1512
  type !== "agent" &&
1464
- type !== "snippet"
1513
+ type !== "snippet" &&
1514
+ type !== "instruction"
1465
1515
  ) {
1466
1516
  return null;
1467
1517
  }
@@ -1560,6 +1610,30 @@ function parseIndexItem(raw: unknown): RemoteIndexItem | null {
1560
1610
  };
1561
1611
  }
1562
1612
 
1613
+ if (type === "instruction") {
1614
+ const instructionRaw = obj.instruction;
1615
+ if (!isPlainObject(instructionRaw)) {
1616
+ return null;
1617
+ }
1618
+ const name =
1619
+ typeof instructionRaw.name === "string" ? instructionRaw.name.trim() : "";
1620
+ const content =
1621
+ typeof instructionRaw.content === "string" ? instructionRaw.content : "";
1622
+ if (!(name && content)) {
1623
+ return null;
1624
+ }
1625
+ return {
1626
+ id,
1627
+ type,
1628
+ title,
1629
+ description,
1630
+ version,
1631
+ sourceUrl,
1632
+ tags,
1633
+ instruction: { name, content },
1634
+ };
1635
+ }
1636
+
1563
1637
  const snippetRaw = obj.snippet;
1564
1638
  if (!isPlainObject(snippetRaw)) {
1565
1639
  return null;
@@ -1731,7 +1805,8 @@ async function loadInstalledState(
1731
1805
  type !== "skill" &&
1732
1806
  type !== "mcp" &&
1733
1807
  type !== "agent" &&
1734
- type !== "snippet"
1808
+ type !== "snippet" &&
1809
+ type !== "instruction"
1735
1810
  ) {
1736
1811
  continue;
1737
1812
  }
@@ -2035,6 +2110,69 @@ async function installSnippetItem(args: {
2035
2110
  };
2036
2111
  }
2037
2112
 
2113
+ function normalizeInstructionName(value: string): string {
2114
+ const normalized = value
2115
+ .trim()
2116
+ .replaceAll("\\", "/")
2117
+ .replace(LEADING_SLASH_RE, "");
2118
+ if (!normalized) {
2119
+ throw new Error("Instruction name is required");
2120
+ }
2121
+ const withExt = MD_EXT_RE.test(normalized) ? normalized : `${normalized}.md`;
2122
+ if (!isSafeRelativePath(withExt)) {
2123
+ throw new Error(`Invalid instruction name: ${value}`);
2124
+ }
2125
+ return withExt;
2126
+ }
2127
+
2128
+ function instructionTitleFromFileName(fileName: string): string {
2129
+ const base = basename(fileName).replace(MD_EXT_RE, "");
2130
+ return base
2131
+ .split(INSTRUCTION_TITLE_SPLIT_RE)
2132
+ .filter(Boolean)
2133
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
2134
+ .join(" ");
2135
+ }
2136
+
2137
+ function instructionAssetName(fileName: string): string {
2138
+ return basename(fileName).replace(MD_EXT_RE, "");
2139
+ }
2140
+
2141
+ async function installInstructionItem(args: {
2142
+ item: RemoteInstructionItem;
2143
+ installAs?: string;
2144
+ rootDir: string;
2145
+ force: boolean;
2146
+ dryRun: boolean;
2147
+ }): Promise<{ installedAs: string; path: string; changedPaths: string[] }> {
2148
+ const fileName = normalizeInstructionName(
2149
+ args.installAs ?? args.item.instruction.name
2150
+ );
2151
+ const instructionPath = join(args.rootDir, "instructions", fileName);
2152
+ assertInstallPath(instructionPath, join(args.rootDir, "instructions"));
2153
+ if ((await fileExists(instructionPath)) && !args.force) {
2154
+ throw new Error(
2155
+ `Instruction already exists: ${fileName} (use --force to overwrite)`
2156
+ );
2157
+ }
2158
+ if (!args.dryRun) {
2159
+ await mkdir(dirname(instructionPath), { recursive: true });
2160
+ await Bun.write(
2161
+ instructionPath,
2162
+ renderTemplate(args.item.instruction.content, {
2163
+ name: fileName,
2164
+ title: instructionTitleFromFileName(fileName),
2165
+ assetName: instructionAssetName(fileName),
2166
+ })
2167
+ );
2168
+ }
2169
+ return {
2170
+ installedAs: fileName,
2171
+ path: instructionPath,
2172
+ changedPaths: [instructionPath],
2173
+ };
2174
+ }
2175
+
2038
2176
  async function installParsedItem(args: {
2039
2177
  parsedRef: { index: string; itemId: string };
2040
2178
  item: RemoteIndexItem;
@@ -2076,7 +2214,7 @@ async function installParsedItem(args: {
2076
2214
  force: args.force,
2077
2215
  dryRun: args.dryRun,
2078
2216
  });
2079
- } else {
2217
+ } else if (args.item.type === "snippet") {
2080
2218
  writeResult = await installSnippetItem({
2081
2219
  item: args.item,
2082
2220
  installAs: args.installAs,
@@ -2084,6 +2222,14 @@ async function installParsedItem(args: {
2084
2222
  force: args.force,
2085
2223
  dryRun: args.dryRun,
2086
2224
  });
2225
+ } else {
2226
+ writeResult = await installInstructionItem({
2227
+ item: args.item,
2228
+ installAs: args.installAs,
2229
+ rootDir: args.rootDir,
2230
+ force: args.force,
2231
+ dryRun: args.dryRun,
2232
+ });
2087
2233
  }
2088
2234
 
2089
2235
  const result: InstallResult = {
@@ -2666,6 +2812,9 @@ function printTemplatesHelp() {
2666
2812
  renderCode(
2667
2813
  "fclt templates init agent <name> [--force] [--dry-run]"
2668
2814
  ),
2815
+ renderCode(
2816
+ "fclt templates init instruction <name> [--force] [--dry-run]"
2817
+ ),
2669
2818
  renderCode(
2670
2819
  "fclt templates init snippet <marker> [--force] [--dry-run]"
2671
2820
  ),
@@ -2734,6 +2883,53 @@ function parseLongFlag(argv: string[], flag: string): string | null {
2734
2883
  return null;
2735
2884
  }
2736
2885
 
2886
+ const TEMPLATE_INIT_VALUE_FLAGS = new Set([
2887
+ "--automation-status",
2888
+ "--cwds",
2889
+ "--name",
2890
+ "--project-root",
2891
+ "--root",
2892
+ "--rrule",
2893
+ "--scope",
2894
+ "--status",
2895
+ ]);
2896
+
2897
+ function parseTemplateInitArgs(argv: string[]): {
2898
+ positional: string[];
2899
+ rootArg?: string;
2900
+ } {
2901
+ const positional: string[] = [];
2902
+ let rootArg: string | undefined;
2903
+ for (let i = 0; i < argv.length; i += 1) {
2904
+ const arg = argv[i];
2905
+ if (!arg) {
2906
+ continue;
2907
+ }
2908
+ if (arg === "--root") {
2909
+ rootArg = argv[i + 1] ?? undefined;
2910
+ i += 1;
2911
+ continue;
2912
+ }
2913
+ if (arg.startsWith("--root=")) {
2914
+ rootArg = arg.slice("--root=".length);
2915
+ continue;
2916
+ }
2917
+ if (TEMPLATE_INIT_VALUE_FLAGS.has(arg)) {
2918
+ i += 1;
2919
+ continue;
2920
+ }
2921
+ const equalFlag = arg.startsWith("--") ? arg.split("=", 1)[0] : "";
2922
+ if (equalFlag && TEMPLATE_INIT_VALUE_FLAGS.has(equalFlag)) {
2923
+ continue;
2924
+ }
2925
+ if (arg.startsWith("-")) {
2926
+ continue;
2927
+ }
2928
+ positional.push(arg);
2929
+ }
2930
+ return { positional, rootArg };
2931
+ }
2932
+
2737
2933
  export async function sourcesCommand(
2738
2934
  argv: string[],
2739
2935
  ctx: RemoteCommandContext = {}
@@ -3124,7 +3320,7 @@ export async function templatesCommand(
3124
3320
  const [kind, ...args] = rest;
3125
3321
  if (!kind) {
3126
3322
  console.error(
3127
- "templates init requires a kind (skill|mcp|agent|snippet|agents|claude|operating-model|project-ai|automation)"
3323
+ "templates init requires a kind (skill|mcp|agent|instruction|snippet|agents|claude|operating-model|project-ai|automation)"
3128
3324
  );
3129
3325
  process.exitCode = 2;
3130
3326
  return;
@@ -3132,7 +3328,8 @@ export async function templatesCommand(
3132
3328
  const dryRun = args.includes("--dry-run");
3133
3329
  const force = args.includes("--force");
3134
3330
  const json = args.includes("--json");
3135
- const positional = args.filter((a) => a && !a.startsWith("-"));
3331
+ const parsedArgs = parseTemplateInitArgs(args);
3332
+ const positional = parsedArgs.positional;
3136
3333
 
3137
3334
  if (kind === "project-ai") {
3138
3335
  try {
@@ -3244,6 +3441,14 @@ export async function templatesCommand(
3244
3441
  as = normalizedName.endsWith(".toml")
3245
3442
  ? normalizedName
3246
3443
  : `${normalizedName}/agent.toml`;
3444
+ } else if (kind === "instruction") {
3445
+ ref = `${BUILTIN_INDEX_NAME}:instruction-template`;
3446
+ as = positional[0];
3447
+ if (!as) {
3448
+ console.error("templates init instruction requires a <name>");
3449
+ process.exitCode = 2;
3450
+ return;
3451
+ }
3247
3452
  } else if (kind === "snippet") {
3248
3453
  ref = `${BUILTIN_INDEX_NAME}:snippet-template`;
3249
3454
  as = positional[0];
@@ -3353,7 +3558,7 @@ export async function templatesCommand(
3353
3558
  dryRun,
3354
3559
  force,
3355
3560
  homeDir: ctx.homeDir,
3356
- rootDir: ctx.rootDir,
3561
+ rootDir: parsedArgs.rootArg ?? ctx.rootDir,
3357
3562
  cwd: ctx.cwd,
3358
3563
  fetchJson: ctx.fetchJson,
3359
3564
  fetchText: ctx.fetchText,