facult 2.13.9 → 2.15.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.
Files changed (36) hide show
  1. package/README.md +26 -8
  2. package/assets/packs/facult-operating-model/agents/evolution-planner/agent.toml +3 -0
  3. package/assets/packs/facult-operating-model/agents/integration-auditor/agent.toml +8 -1
  4. package/assets/packs/facult-operating-model/agents/scope-promoter/agent.toml +8 -1
  5. package/assets/packs/facult-operating-model/agents/writeback-curator/agent.toml +2 -0
  6. package/assets/packs/facult-operating-model/instructions/INTEGRATION.md +44 -0
  7. package/assets/packs/facult-operating-model/instructions/PROJECT_CAPABILITY.md +35 -0
  8. package/assets/packs/facult-operating-model/instructions/WORK_UNITS.md +48 -0
  9. package/assets/packs/facult-operating-model/skills/capability-evolution/SKILL.md +25 -6
  10. package/assets/packs/facult-operating-model/skills/project-operating-layer-design/SKILL.md +33 -0
  11. package/assets/packs/facult-operating-model/snippets/global/core/feedback-loops.md +1 -0
  12. package/assets/packs/facult-operating-model/snippets/global/core/work-units.md +2 -1
  13. package/assets/packs/facult-operating-model/{AGENTS.global.md → snippets/templates/agents-global.md} +6 -0
  14. package/docs/README.md +4 -0
  15. package/docs/assets/fclt-capability-loop.png +0 -0
  16. package/docs/built-in-pack.md +23 -2
  17. package/docs/codex-plugin.md +57 -0
  18. package/docs/concepts.md +4 -1
  19. package/docs/pack-upgrades.md +73 -0
  20. package/docs/reference.md +2 -2
  21. package/docs/roadmap.md +4 -3
  22. package/docs/work-units.md +96 -0
  23. package/package.json +5 -1
  24. package/plugins/fclt/.codex-plugin/plugin.json +31 -0
  25. package/plugins/fclt/.mcp.json +11 -0
  26. package/plugins/fclt/scripts/fclt-mcp.js +320 -0
  27. package/plugins/fclt/skills/fclt-capability-review/SKILL.md +51 -0
  28. package/plugins/fclt/skills/fclt-evolution/SKILL.md +65 -0
  29. package/plugins/fclt/skills/fclt-setup/SKILL.md +65 -0
  30. package/plugins/fclt/skills/fclt-writeback/SKILL.md +57 -0
  31. package/src/agents.ts +1 -0
  32. package/src/builtin-assets.ts +1 -1
  33. package/src/builtin.ts +22 -0
  34. package/src/doctor.ts +6 -2
  35. package/src/global-docs.ts +6 -2
  36. package/src/remote.ts +252 -10
package/src/remote.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { mkdir, readdir, readFile, rm } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
5
  import {
@@ -10,7 +11,10 @@ import {
10
11
  resolve,
11
12
  } from "node:path";
12
13
  import { isCancel, multiselect, select, text } from "@clack/prompts";
13
- import { facultBuiltinPackRoot } from "./builtin";
14
+ import {
15
+ builtinOperatingModelInstallRelPath,
16
+ facultBuiltinPackRoot,
17
+ } from "./builtin";
14
18
  import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
15
19
  import {
16
20
  renderBullets,
@@ -21,7 +25,11 @@ import {
21
25
  renderTable,
22
26
  } from "./cli-ui";
23
27
  import { buildIndex } from "./index-builder";
24
- import { facultRootDir, readFacultConfig } from "./paths";
28
+ import {
29
+ facultRootDir,
30
+ projectRootFromAiRoot,
31
+ readFacultConfig,
32
+ } from "./paths";
25
33
  import {
26
34
  assertManifestIntegrity,
27
35
  assertManifestSignature,
@@ -130,6 +138,7 @@ interface InstallResult {
130
138
  sourceTrustLevel: SourceTrustLevel;
131
139
  dryRun: boolean;
132
140
  changedPaths: string[];
141
+ skippedPaths?: string[];
133
142
  }
134
143
 
135
144
  interface UpdateCheckResult {
@@ -1298,41 +1307,250 @@ async function listFilesRecursive(rootDir: string): Promise<string[]> {
1298
1307
  return out.sort();
1299
1308
  }
1300
1309
 
1310
+ interface BuiltinPackManifest {
1311
+ version: 1;
1312
+ pack: string;
1313
+ updatedAt: string;
1314
+ files: Record<string, { sha256: string }>;
1315
+ }
1316
+
1317
+ function sha256Text(value: string): string {
1318
+ return createHash("sha256").update(value).digest("hex");
1319
+ }
1320
+
1321
+ function builtinPackManifestPath(rootDir: string): string {
1322
+ return join(rootDir, ".facult", "packs", "facult-operating-model.json");
1323
+ }
1324
+
1325
+ async function readBuiltinPackManifest(
1326
+ rootDir: string
1327
+ ): Promise<BuiltinPackManifest | null> {
1328
+ const pathValue = builtinPackManifestPath(rootDir);
1329
+ if (!(await pathExists(pathValue))) {
1330
+ return null;
1331
+ }
1332
+ try {
1333
+ const parsed = JSON.parse(await Bun.file(pathValue).text());
1334
+ if (
1335
+ parsed?.version === 1 &&
1336
+ parsed.pack === "facult-operating-model" &&
1337
+ isPlainObject(parsed.files)
1338
+ ) {
1339
+ return parsed as BuiltinPackManifest;
1340
+ }
1341
+ } catch {
1342
+ return null;
1343
+ }
1344
+ return null;
1345
+ }
1346
+
1347
+ function serializeBuiltinPackManifest(manifest: BuiltinPackManifest): string {
1348
+ return `${JSON.stringify(manifest, null, 2)}\n`;
1349
+ }
1350
+
1351
+ const OPERATING_MODEL_SNIPPET_FRAME = `## Working mode
1352
+
1353
+ <!-- fclty:global/baseline -->
1354
+ <!-- /fclty:global/baseline -->
1355
+
1356
+ <!-- fclty:global/core/work-units -->
1357
+ <!-- /fclty:global/core/work-units -->
1358
+
1359
+ <!-- fclty:global/core/feedback-loops -->
1360
+ <!-- /fclty:global/core/feedback-loops -->
1361
+
1362
+ <!-- fclty:global/core/verification -->
1363
+ <!-- /fclty:global/core/verification -->
1364
+
1365
+ <!-- fclty:global/core/writeback -->
1366
+ <!-- /fclty:global/core/writeback -->
1367
+
1368
+ ## Shared instruction sources
1369
+
1370
+ - For work-unit definition and scope clarification, read \${refs.work_units}.
1371
+ - For identifying, improving, and validating feedback loops, read \${refs.feedback_loops}.
1372
+ - For verification and anti-false-positive checks, read \${refs.verification}.
1373
+ - For checking integration boundaries, read \${refs.integration}.
1374
+ - For learning, decisions, and writeback, read \${refs.learning_writeback}.
1375
+ - For capability evolution, proposal kinds, and \`facult ai\` workflow, read \${refs.evolution}.
1376
+ - For deciding whether something belongs in global or project scope, read \${refs.project_capability}.
1377
+ - Add private language, coding, or writing refs in local config only when they belong to the user's own operating layer.
1378
+ `;
1379
+
1380
+ function appendOperatingModelFrame(seedText: string): string {
1381
+ const normalized = seedText.trimEnd();
1382
+ if (normalized.includes("<!-- fclty:global/baseline -->")) {
1383
+ return `${normalized}\n`;
1384
+ }
1385
+ return `${normalized}\n\n## Facult Operating Model\n\n${OPERATING_MODEL_SNIPPET_FRAME}`;
1386
+ }
1387
+
1388
+ async function firstExistingFileText(
1389
+ candidates: string[]
1390
+ ): Promise<string | null> {
1391
+ for (const candidate of candidates) {
1392
+ if (await pathExists(candidate)) {
1393
+ return await Bun.file(candidate).text();
1394
+ }
1395
+ }
1396
+ return null;
1397
+ }
1398
+
1399
+ async function seedAgentsGlobalText(args: {
1400
+ rootDir: string;
1401
+ homeDir?: string;
1402
+ fallbackText: string;
1403
+ }): Promise<{ text: string; seededFromExisting: boolean }> {
1404
+ const home = args.homeDir ?? homedir();
1405
+ const projectRoot = projectRootFromAiRoot(args.rootDir, home);
1406
+ const seedText = await firstExistingFileText(
1407
+ projectRoot
1408
+ ? [
1409
+ join(projectRoot, "AGENTS.md"),
1410
+ join(projectRoot, "CLAUDE.md"),
1411
+ join(projectRoot, ".codex", "AGENTS.md"),
1412
+ join(projectRoot, ".claude", "CLAUDE.md"),
1413
+ ]
1414
+ : [
1415
+ join(home, ".codex", "AGENTS.md"),
1416
+ join(home, ".claude", "CLAUDE.md"),
1417
+ join(home, ".cursor", "AGENTS.md"),
1418
+ ]
1419
+ );
1420
+ if (!seedText?.trim()) {
1421
+ return { text: args.fallbackText, seededFromExisting: false };
1422
+ }
1423
+ return {
1424
+ text: appendOperatingModelFrame(seedText),
1425
+ seededFromExisting: true,
1426
+ };
1427
+ }
1428
+
1301
1429
  async function scaffoldBuiltinOperatingModelPack(args: {
1302
1430
  rootDir: string;
1303
1431
  homeDir?: string;
1304
1432
  dryRun?: boolean;
1305
1433
  force?: boolean;
1434
+ update?: boolean;
1306
1435
  installedAs?: string;
1307
1436
  }): Promise<InstallResult> {
1308
1437
  const rootDir = resolve(args.rootDir);
1309
1438
  const packRoot = facultBuiltinPackRoot("facult-operating-model");
1310
1439
  const files = await listFilesRecursive(packRoot);
1311
1440
  const changedPaths: string[] = [];
1441
+ const skippedPaths: string[] = [];
1442
+ const existingManifest = await readBuiltinPackManifest(rootDir);
1443
+ const manifestFiles: BuiltinPackManifest["files"] = {
1444
+ ...(existingManifest?.files ?? {}),
1445
+ };
1312
1446
 
1313
1447
  for (const sourcePath of files) {
1314
1448
  const relPath = relative(packRoot, sourcePath);
1315
1449
  if (!relPath || relPath.startsWith("..")) {
1316
1450
  continue;
1317
1451
  }
1318
- const targetPath = join(rootDir, relPath);
1319
- const exists = await pathExists(targetPath);
1320
- if (exists && !args.force) {
1452
+ const targetRelPath = builtinOperatingModelInstallRelPath(relPath);
1453
+ const targetPath = join(rootDir, targetRelPath);
1454
+ const rawSourceText = await Bun.file(sourcePath).text();
1455
+ const targetExists = await pathExists(targetPath);
1456
+ const seed =
1457
+ targetRelPath === "AGENTS.global.md" && !targetExists
1458
+ ? await seedAgentsGlobalText({
1459
+ rootDir,
1460
+ homeDir: args.homeDir,
1461
+ fallbackText: rawSourceText,
1462
+ })
1463
+ : null;
1464
+ const sourceText = seed?.text ?? rawSourceText;
1465
+ const trackInManifest = !seed?.seededFromExisting;
1466
+ const sourceHash = sha256Text(sourceText);
1467
+ let shouldWrite = !targetExists || Boolean(args.force);
1468
+
1469
+ if (targetExists && !shouldWrite) {
1470
+ const targetText = await Bun.file(targetPath).text();
1471
+ const targetHash = sha256Text(targetText);
1472
+ if (targetHash === sourceHash) {
1473
+ if (trackInManifest) {
1474
+ manifestFiles[targetRelPath] = { sha256: sourceHash };
1475
+ }
1476
+ } else if (
1477
+ args.update &&
1478
+ existingManifest?.files[targetRelPath]?.sha256 === targetHash
1479
+ ) {
1480
+ shouldWrite = true;
1481
+ } else if (args.update) {
1482
+ skippedPaths.push(targetPath);
1483
+ }
1484
+ }
1485
+
1486
+ if (!shouldWrite) {
1321
1487
  continue;
1322
1488
  }
1323
1489
  changedPaths.push(targetPath);
1490
+ if (trackInManifest) {
1491
+ manifestFiles[targetRelPath] = { sha256: sourceHash };
1492
+ } else {
1493
+ delete manifestFiles[targetRelPath];
1494
+ }
1324
1495
  if (!args.dryRun) {
1325
1496
  await mkdir(dirname(targetPath), { recursive: true });
1326
- await Bun.write(targetPath, await Bun.file(sourcePath).text());
1497
+ await Bun.write(targetPath, sourceText);
1327
1498
  }
1328
1499
  }
1329
1500
 
1330
1501
  const configPath = join(rootDir, "config.toml");
1331
- if (!(await pathExists(configPath)) || args.force) {
1502
+ const configRelPath = "config.toml";
1503
+ const configText = "version = 1\n";
1504
+ const configHash = sha256Text(configText);
1505
+ const configExists = await pathExists(configPath);
1506
+ let shouldWriteConfig = !configExists || Boolean(args.force);
1507
+ if (configExists && !shouldWriteConfig) {
1508
+ const targetText = await Bun.file(configPath).text();
1509
+ const targetHash = sha256Text(targetText);
1510
+ if (targetHash === configHash) {
1511
+ manifestFiles[configRelPath] = { sha256: configHash };
1512
+ } else if (
1513
+ args.update &&
1514
+ existingManifest?.files[configRelPath]?.sha256 === targetHash
1515
+ ) {
1516
+ shouldWriteConfig = true;
1517
+ } else if (args.update) {
1518
+ skippedPaths.push(configPath);
1519
+ }
1520
+ }
1521
+ if (shouldWriteConfig) {
1332
1522
  changedPaths.push(configPath);
1523
+ manifestFiles[configRelPath] = { sha256: configHash };
1333
1524
  if (!args.dryRun) {
1334
1525
  await mkdir(dirname(configPath), { recursive: true });
1335
- await Bun.write(configPath, "version = 1\n");
1526
+ await Bun.write(configPath, configText);
1527
+ }
1528
+ }
1529
+
1530
+ const manifestPath = builtinPackManifestPath(rootDir);
1531
+ const sortedManifestFiles = Object.fromEntries(
1532
+ Object.entries(manifestFiles).sort(([a], [b]) => a.localeCompare(b))
1533
+ );
1534
+ const stableManifest = serializeBuiltinPackManifest({
1535
+ version: 1,
1536
+ pack: "facult-operating-model",
1537
+ updatedAt: existingManifest?.updatedAt ?? "",
1538
+ files: sortedManifestFiles,
1539
+ });
1540
+ const existingManifestText = (await pathExists(manifestPath))
1541
+ ? await Bun.file(manifestPath).text()
1542
+ : null;
1543
+ if (existingManifestText !== stableManifest) {
1544
+ const nextManifest = serializeBuiltinPackManifest({
1545
+ version: 1,
1546
+ pack: "facult-operating-model",
1547
+ updatedAt: new Date().toISOString(),
1548
+ files: sortedManifestFiles,
1549
+ });
1550
+ changedPaths.push(manifestPath);
1551
+ if (!args.dryRun) {
1552
+ await mkdir(dirname(manifestPath), { recursive: true });
1553
+ await Bun.write(manifestPath, nextManifest);
1336
1554
  }
1337
1555
  }
1338
1556
 
@@ -1352,6 +1570,7 @@ async function scaffoldBuiltinOperatingModelPack(args: {
1352
1570
  sourceTrustLevel: "trusted",
1353
1571
  dryRun: Boolean(args.dryRun),
1354
1572
  changedPaths: uniqueSorted(changedPaths),
1573
+ skippedPaths: uniqueSorted(skippedPaths),
1355
1574
  };
1356
1575
  }
1357
1576
 
@@ -1360,6 +1579,7 @@ async function scaffoldBuiltinProjectAiPack(args: {
1360
1579
  homeDir?: string;
1361
1580
  dryRun?: boolean;
1362
1581
  force?: boolean;
1582
+ update?: boolean;
1363
1583
  }): Promise<InstallResult> {
1364
1584
  const cwd = resolve(args.cwd ?? process.cwd());
1365
1585
  return await scaffoldBuiltinOperatingModelPack({
@@ -1367,6 +1587,7 @@ async function scaffoldBuiltinProjectAiPack(args: {
1367
1587
  homeDir: args.homeDir,
1368
1588
  dryRun: args.dryRun,
1369
1589
  force: args.force,
1590
+ update: args.update,
1370
1591
  installedAs: "project-ai",
1371
1592
  });
1372
1593
  }
@@ -2815,9 +3036,11 @@ function printTemplatesHelp() {
2815
3036
  ),
2816
3037
  renderCode("fclt templates init agents [--force] [--dry-run]"),
2817
3038
  renderCode(
2818
- "fclt templates init operating-model [--global|--project|--root PATH] [--force] [--dry-run]"
3039
+ "fclt templates init operating-model [--global|--project|--root PATH] [--update] [--force] [--dry-run]"
3040
+ ),
3041
+ renderCode(
3042
+ "fclt templates init project-ai [--update] [--force] [--dry-run]"
2819
3043
  ),
2820
- renderCode("fclt templates init project-ai [--force] [--dry-run]"),
2821
3044
  renderCode(
2822
3045
  "fclt templates init automation <template-id> [--scope global|project|wide] [--name <name>] [--project-root <path>] [--cwds <path1,path2>] [--rrule <RRULE>] [--status PAUSED|ACTIVE] [--yes] [--dry-run]"
2823
3046
  ),
@@ -3322,6 +3545,7 @@ export async function templatesCommand(
3322
3545
  }
3323
3546
  const dryRun = args.includes("--dry-run");
3324
3547
  const force = args.includes("--force");
3548
+ const update = args.includes("--update");
3325
3549
  const json = args.includes("--json");
3326
3550
  const parsedArgs = parseTemplateInitArgs(args);
3327
3551
  const positional = parsedArgs.positional;
@@ -3333,6 +3557,7 @@ export async function templatesCommand(
3333
3557
  homeDir: ctx.homeDir,
3334
3558
  dryRun,
3335
3559
  force,
3560
+ update,
3336
3561
  });
3337
3562
  if (json) {
3338
3563
  console.log(JSON.stringify(result, null, 2));
@@ -3348,6 +3573,14 @@ export async function templatesCommand(
3348
3573
  title: "Changed Paths",
3349
3574
  lines: renderBullets(result.changedPaths),
3350
3575
  },
3576
+ ...(result.skippedPaths?.length
3577
+ ? [
3578
+ {
3579
+ title: "Skipped Local Edits",
3580
+ lines: renderBullets(result.skippedPaths),
3581
+ },
3582
+ ]
3583
+ : []),
3351
3584
  ],
3352
3585
  })
3353
3586
  );
@@ -3378,6 +3611,7 @@ export async function templatesCommand(
3378
3611
  homeDir: ctx.homeDir,
3379
3612
  dryRun,
3380
3613
  force,
3614
+ update,
3381
3615
  });
3382
3616
  if (json) {
3383
3617
  console.log(JSON.stringify(result, null, 2));
@@ -3393,6 +3627,14 @@ export async function templatesCommand(
3393
3627
  title: "Changed Paths",
3394
3628
  lines: renderBullets(result.changedPaths),
3395
3629
  },
3630
+ ...(result.skippedPaths?.length
3631
+ ? [
3632
+ {
3633
+ title: "Skipped Local Edits",
3634
+ lines: renderBullets(result.skippedPaths),
3635
+ },
3636
+ ]
3637
+ : []),
3396
3638
  ],
3397
3639
  })
3398
3640
  );