facult 2.14.0 → 2.15.1

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/manage.ts CHANGED
@@ -15,7 +15,11 @@ import { basename, dirname, join } from "node:path";
15
15
  import { getAdapter } from "./adapters";
16
16
  import { renderCanonicalText } from "./agents";
17
17
  import { ensureAiIndexPath } from "./ai-state";
18
- import { builtinSyncDefaultsEnabled, facultBuiltinPackRoot } from "./builtin";
18
+ import {
19
+ builtinSyncDefaultsEnabled,
20
+ facultBuiltinCodexPluginRoot,
21
+ facultBuiltinPackRoot,
22
+ } from "./builtin";
19
23
  import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
20
24
  import { renderBullets, renderCode, renderPage } from "./cli-ui";
21
25
  import { contentHash, normalizeText } from "./conflicts";
@@ -86,6 +90,8 @@ export interface ManagedState {
86
90
 
87
91
  type SyncLedgerAction = "add" | "adopt" | "remove" | "skip" | "write";
88
92
 
93
+ const FCLT_CODEX_PLUGIN_NAME = "fclt";
94
+
89
95
  interface SyncLedgerEvent {
90
96
  version: 1;
91
97
  id: string;
@@ -259,7 +265,8 @@ async function isGeneratedOnlyProjectRoot(args: {
259
265
  function renderedSourceKindForPath(
260
266
  sourcePath: string
261
267
  ): ManagedRenderedTargetState["sourceKind"] {
262
- return sourcePath.startsWith(facultBuiltinPackRoot())
268
+ return sourcePath.startsWith(facultBuiltinPackRoot()) ||
269
+ sourcePath.startsWith(facultBuiltinCodexPluginRoot())
263
270
  ? "builtin"
264
271
  : "canonical";
265
272
  }
@@ -328,6 +335,45 @@ function normalizeCodexMarketplaceText(text: string): string {
328
335
  }
329
336
  }
330
337
 
338
+ function fcltCodexMarketplaceEntry(): Record<string, unknown> {
339
+ return {
340
+ name: FCLT_CODEX_PLUGIN_NAME,
341
+ source: {
342
+ source: "local",
343
+ path: `./plugins/${FCLT_CODEX_PLUGIN_NAME}`,
344
+ },
345
+ policy: {
346
+ installation: "AVAILABLE",
347
+ authentication: "NONE",
348
+ },
349
+ category: "Productivity",
350
+ };
351
+ }
352
+
353
+ function withBuiltinFcltCodexMarketplaceEntry(text: string | null): string {
354
+ const base =
355
+ text == null
356
+ ? {
357
+ name: "local",
358
+ interface: { displayName: "Local Plugins" },
359
+ plugins: [],
360
+ }
361
+ : (JSON.parse(text) as unknown);
362
+ if (!isPlainObject(base)) {
363
+ return text == null ? normalizeCodexMarketplaceText("{}") : text;
364
+ }
365
+ const plugins = Array.isArray(base.plugins) ? [...base.plugins] : [];
366
+ const hasFclt = plugins.some(
367
+ (entry) => isPlainObject(entry) && entry.name === FCLT_CODEX_PLUGIN_NAME
368
+ );
369
+ if (!hasFclt) {
370
+ plugins.push(fcltCodexMarketplaceEntry());
371
+ }
372
+ return normalizeCodexMarketplaceText(
373
+ JSON.stringify({ ...base, plugins }, null, 2)
374
+ );
375
+ }
376
+
331
377
  function isSafeCodexPluginName(name: string): boolean {
332
378
  const trimmed = name.trim();
333
379
  return (
@@ -899,7 +945,8 @@ async function listRelativeFilesWithDotfiles(root: string): Promise<string[]> {
899
945
  }
900
946
 
901
947
  async function loadCanonicalCodexPlugins(
902
- rootDir: string
948
+ rootDir: string,
949
+ homeDir?: string
903
950
  ): Promise<CanonicalPluginEntry[]> {
904
951
  const pluginsRoot = codexCanonicalPluginsRoot(rootDir);
905
952
  const entries = await readdir(pluginsRoot, { withFileTypes: true }).catch(
@@ -907,36 +954,81 @@ async function loadCanonicalCodexPlugins(
907
954
  );
908
955
  const out: CanonicalPluginEntry[] = [];
909
956
 
957
+ async function loadEntry(
958
+ name: string,
959
+ sourceDir: string
960
+ ): Promise<CanonicalPluginEntry | null> {
961
+ if (!(await fileExists(join(sourceDir, ".codex-plugin", "plugin.json")))) {
962
+ return null;
963
+ }
964
+ const files = new Map<string, Uint8Array>();
965
+ for (const relPath of await listRelativeFilesWithDotfiles(sourceDir)) {
966
+ files.set(relPath, await Bun.file(join(sourceDir, relPath)).bytes());
967
+ }
968
+ return { name, sourceDir, files };
969
+ }
970
+
910
971
  for (const entry of entries) {
911
972
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
912
973
  continue;
913
974
  }
914
975
  const sourceDir = join(pluginsRoot, entry.name);
915
- if (!(await fileExists(join(sourceDir, ".codex-plugin", "plugin.json")))) {
916
- continue;
976
+ const plugin = await loadEntry(entry.name, sourceDir);
977
+ if (plugin) {
978
+ out.push(plugin);
917
979
  }
918
- const files = new Map<string, Uint8Array>();
919
- for (const relPath of await listRelativeFilesWithDotfiles(sourceDir)) {
920
- files.set(relPath, await Bun.file(join(sourceDir, relPath)).bytes());
980
+ }
981
+
982
+ if (
983
+ (await builtinSyncDefaultsEnabled(rootDir, homeDir)) &&
984
+ !out.some((entry) => entry.name === FCLT_CODEX_PLUGIN_NAME)
985
+ ) {
986
+ const plugin = await loadEntry(
987
+ FCLT_CODEX_PLUGIN_NAME,
988
+ facultBuiltinCodexPluginRoot()
989
+ );
990
+ if (plugin) {
991
+ out.push(plugin);
921
992
  }
922
- out.push({ name: entry.name, sourceDir, files });
923
993
  }
924
994
 
925
995
  return out.sort((a, b) => a.name.localeCompare(b.name));
926
996
  }
927
997
 
928
- async function canonicalCodexPluginsExist(rootDir: string): Promise<boolean> {
998
+ async function canonicalCodexPluginsExist(
999
+ rootDir: string,
1000
+ homeDir?: string
1001
+ ): Promise<boolean> {
929
1002
  if (await fileExists(codexCanonicalPluginMarketplacePath(rootDir))) {
930
1003
  return true;
931
1004
  }
932
- return (await loadCanonicalCodexPlugins(rootDir)).length > 0;
1005
+ return (await loadCanonicalCodexPlugins(rootDir, homeDir)).length > 0;
933
1006
  }
934
1007
 
935
1008
  async function loadCanonicalCodexMarketplaceText(
936
- rootDir: string
1009
+ rootDir: string,
1010
+ homeDir?: string
937
1011
  ): Promise<{ text: string | null; sourcePath: string }> {
938
1012
  const sourcePath = codexCanonicalPluginMarketplacePath(rootDir);
939
1013
  const raw = await readTextOrNull(sourcePath);
1014
+ const builtinEnabled = await builtinSyncDefaultsEnabled(rootDir, homeDir);
1015
+ if (builtinEnabled) {
1016
+ try {
1017
+ return {
1018
+ text: withBuiltinFcltCodexMarketplaceEntry(raw),
1019
+ sourcePath:
1020
+ raw == null
1021
+ ? join(
1022
+ facultBuiltinCodexPluginRoot(),
1023
+ ".codex-plugin",
1024
+ "plugin.json"
1025
+ )
1026
+ : sourcePath,
1027
+ };
1028
+ } catch {
1029
+ // Fall back to the canonical marketplace text if it is not JSON.
1030
+ }
1031
+ }
940
1032
  return {
941
1033
  text: raw == null ? null : normalizeCodexMarketplaceText(raw),
942
1034
  sourcePath,
@@ -2919,6 +3011,7 @@ export async function manageTool(tool: string, opts: ManageOptions = {}) {
2919
3011
  const pluginPreview =
2920
3012
  tool === "codex" && toolPaths.pluginsDir && toolPaths.pluginMarketplacePath
2921
3013
  ? await planCodexPluginFileChanges({
3014
+ homeDir: home,
2922
3015
  rootDir,
2923
3016
  pluginsDir: toolPaths.pluginsDir,
2924
3017
  pluginMarketplacePath: toolPaths.pluginMarketplacePath,
@@ -3390,7 +3483,7 @@ async function repairManagedToolEntry(args: {
3390
3483
  !(next.pluginsDir && next.pluginMarketplacePath) &&
3391
3484
  toolPaths.pluginsDir &&
3392
3485
  toolPaths.pluginMarketplacePath &&
3393
- (await canonicalCodexPluginsExist(rootDir))
3486
+ (await canonicalCodexPluginsExist(rootDir, homeDir))
3394
3487
  ) {
3395
3488
  if (!next.pluginsDir) {
3396
3489
  next.pluginsBackup = await backupPath(toolPaths.pluginsDir);
@@ -4409,6 +4502,7 @@ async function adoptExistingCodexPlugins(args: {
4409
4502
  }
4410
4503
 
4411
4504
  async function planCodexPluginFileChanges(args: {
4505
+ homeDir?: string;
4412
4506
  rootDir: string;
4413
4507
  pluginsDir: string;
4414
4508
  pluginMarketplacePath?: string;
@@ -4423,14 +4517,20 @@ async function planCodexPluginFileChanges(args: {
4423
4517
  const sources = new Map<string, string>();
4424
4518
  const desiredPaths = new Set<string>();
4425
4519
 
4426
- const marketplace = await loadCanonicalCodexMarketplaceText(args.rootDir);
4520
+ const marketplace = await loadCanonicalCodexMarketplaceText(
4521
+ args.rootDir,
4522
+ args.homeDir
4523
+ );
4427
4524
  if (marketplace.text != null && args.pluginMarketplacePath) {
4428
4525
  desiredPaths.add(args.pluginMarketplacePath);
4429
4526
  contents.set(args.pluginMarketplacePath, marketplace.text);
4430
4527
  sources.set(args.pluginMarketplacePath, marketplace.sourcePath);
4431
4528
  }
4432
4529
 
4433
- for (const plugin of await loadCanonicalCodexPlugins(args.rootDir)) {
4530
+ for (const plugin of await loadCanonicalCodexPlugins(
4531
+ args.rootDir,
4532
+ args.homeDir
4533
+ )) {
4434
4534
  for (const [relPath, bytes] of plugin.files.entries()) {
4435
4535
  const targetPath = join(args.pluginsDir, plugin.name, relPath);
4436
4536
  desiredPaths.add(targetPath);
@@ -4641,6 +4741,7 @@ async function syncManagedToolEntry({
4641
4741
  const pluginPlan =
4642
4742
  tool === "codex" && entry.pluginsDir
4643
4743
  ? await planCodexPluginFileChanges({
4744
+ homeDir,
4644
4745
  rootDir,
4645
4746
  pluginsDir: entry.pluginsDir,
4646
4747
  pluginMarketplacePath: entry.pluginMarketplacePath,
package/src/remote.ts CHANGED
@@ -11,7 +11,10 @@ import {
11
11
  resolve,
12
12
  } from "node:path";
13
13
  import { isCancel, multiselect, select, text } from "@clack/prompts";
14
- import { facultBuiltinPackRoot } from "./builtin";
14
+ import {
15
+ builtinOperatingModelInstallRelPath,
16
+ facultBuiltinPackRoot,
17
+ } from "./builtin";
15
18
  import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
16
19
  import {
17
20
  renderBullets,
@@ -22,7 +25,11 @@ import {
22
25
  renderTable,
23
26
  } from "./cli-ui";
24
27
  import { buildIndex } from "./index-builder";
25
- import { facultRootDir, readFacultConfig } from "./paths";
28
+ import {
29
+ facultRootDir,
30
+ projectRootFromAiRoot,
31
+ readFacultConfig,
32
+ } from "./paths";
26
33
  import {
27
34
  assertManifestIntegrity,
28
35
  assertManifestSignature,
@@ -1341,6 +1348,84 @@ function serializeBuiltinPackManifest(manifest: BuiltinPackManifest): string {
1341
1348
  return `${JSON.stringify(manifest, null, 2)}\n`;
1342
1349
  }
1343
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
+
1344
1429
  async function scaffoldBuiltinOperatingModelPack(args: {
1345
1430
  rootDir: string;
1346
1431
  homeDir?: string;
@@ -1364,20 +1449,33 @@ async function scaffoldBuiltinOperatingModelPack(args: {
1364
1449
  if (!relPath || relPath.startsWith("..")) {
1365
1450
  continue;
1366
1451
  }
1367
- const targetPath = join(rootDir, relPath);
1368
- const sourceText = await Bun.file(sourcePath).text();
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;
1369
1466
  const sourceHash = sha256Text(sourceText);
1370
- const exists = await pathExists(targetPath);
1371
- let shouldWrite = !exists || Boolean(args.force);
1467
+ let shouldWrite = !targetExists || Boolean(args.force);
1372
1468
 
1373
- if (exists && !shouldWrite) {
1469
+ if (targetExists && !shouldWrite) {
1374
1470
  const targetText = await Bun.file(targetPath).text();
1375
1471
  const targetHash = sha256Text(targetText);
1376
1472
  if (targetHash === sourceHash) {
1377
- manifestFiles[relPath] = { sha256: sourceHash };
1473
+ if (trackInManifest) {
1474
+ manifestFiles[targetRelPath] = { sha256: sourceHash };
1475
+ }
1378
1476
  } else if (
1379
1477
  args.update &&
1380
- existingManifest?.files[relPath]?.sha256 === targetHash
1478
+ existingManifest?.files[targetRelPath]?.sha256 === targetHash
1381
1479
  ) {
1382
1480
  shouldWrite = true;
1383
1481
  } else if (args.update) {
@@ -1389,7 +1487,11 @@ async function scaffoldBuiltinOperatingModelPack(args: {
1389
1487
  continue;
1390
1488
  }
1391
1489
  changedPaths.push(targetPath);
1392
- manifestFiles[relPath] = { sha256: sourceHash };
1490
+ if (trackInManifest) {
1491
+ manifestFiles[targetRelPath] = { sha256: sourceHash };
1492
+ } else {
1493
+ delete manifestFiles[targetRelPath];
1494
+ }
1393
1495
  if (!args.dryRun) {
1394
1496
  await mkdir(dirname(targetPath), { recursive: true });
1395
1497
  await Bun.write(targetPath, sourceText);