facult 2.5.1 → 2.6.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/README.md CHANGED
@@ -445,6 +445,7 @@ The canonical store can contain several distinct asset classes:
445
445
  - `skills/`: workflow-specific capability folders
446
446
  - `mcp/`: canonical MCP server definitions
447
447
  - `tools/<tool>/config.toml`: canonical tool config
448
+ - `tools/<tool>/config.local.toml`: machine-local tool config overlay
448
449
  - `tools/<tool>/rules/*.rules`: canonical tool rules
449
450
  - global docs such as `AGENTS.global.md` and `AGENTS.override.global.md`
450
451
 
@@ -479,6 +480,8 @@ Built-ins currently include:
479
480
  Recommended split:
480
481
  - `~/.ai/config.toml` or `<repo>/.ai/config.toml`: tracked, portable, non-secret refs/defaults
481
482
  - `~/.ai/config.local.toml` or `<repo>/.ai/config.local.toml`: ignored, machine-local paths and secrets
483
+ - `~/.ai/tools/<tool>/config.toml` or `<repo>/.ai/tools/<tool>/config.toml`: tracked tool defaults
484
+ - `~/.ai/tools/<tool>/config.local.toml` or `<repo>/.ai/tools/<tool>/config.local.toml`: ignored, machine-local tool overrides merged after tracked tool config during sync
482
485
  - `[builtin].sync_defaults = false`: disable builtin default sync/materialization for this root
483
486
  - `fclt sync --builtin-conflicts overwrite`: allow packaged builtin defaults to overwrite locally modified generated targets
484
487
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "2.5.1",
3
+ "version": "2.6.0",
4
4
  "description": "Manage canonical AI capabilities, sync surfaces, and evolution state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -426,7 +426,16 @@ export async function planToolConfigSync(args: {
426
426
  previouslyManaged?: boolean;
427
427
  }): Promise<ToolConfigPlan> {
428
428
  const sourcePath = join(args.rootDir, "tools", args.tool, "config.toml");
429
- if (!(await fileExists(sourcePath))) {
429
+ const localSourcePath = join(
430
+ args.rootDir,
431
+ "tools",
432
+ args.tool,
433
+ "config.local.toml"
434
+ );
435
+ const hasTrackedSource = await fileExists(sourcePath);
436
+ const hasLocalSource = await fileExists(localSourcePath);
437
+
438
+ if (!(hasTrackedSource || hasLocalSource)) {
430
439
  return {
431
440
  targetPath: args.toolConfigPath,
432
441
  write: false,
@@ -437,14 +446,28 @@ export async function planToolConfigSync(args: {
437
446
  };
438
447
  }
439
448
 
440
- const rendered = await renderSourceTarget({
441
- homeDir: args.homeDir,
442
- rootDir: args.rootDir,
443
- sourcePath,
444
- targetPath: args.toolConfigPath,
445
- tool: args.tool,
446
- });
447
- const canonicalConfig = Bun.TOML.parse(rendered);
449
+ const trackedRendered = hasTrackedSource
450
+ ? await renderSourceTarget({
451
+ homeDir: args.homeDir,
452
+ rootDir: args.rootDir,
453
+ sourcePath,
454
+ targetPath: args.toolConfigPath,
455
+ tool: args.tool,
456
+ })
457
+ : null;
458
+ const localRendered = hasLocalSource
459
+ ? await renderSourceTarget({
460
+ homeDir: args.homeDir,
461
+ rootDir: args.rootDir,
462
+ sourcePath: localSourcePath,
463
+ targetPath: args.toolConfigPath,
464
+ tool: args.tool,
465
+ })
466
+ : null;
467
+ const canonicalConfig = trackedRendered
468
+ ? Bun.TOML.parse(trackedRendered)
469
+ : {};
470
+ const localConfig = localRendered ? Bun.TOML.parse(localRendered) : {};
448
471
  const existingConfig =
449
472
  (await readTomlFile(args.toolConfigPath)) ??
450
473
  (args.existingConfigPath
@@ -452,8 +475,11 @@ export async function planToolConfigSync(args: {
452
475
  : null) ??
453
476
  ({} as Record<string, unknown>);
454
477
  const merged = mergeTomlObjects(
455
- existingConfig,
456
- isPlainObject(canonicalConfig) ? canonicalConfig : {}
478
+ mergeTomlObjects(
479
+ existingConfig,
480
+ isPlainObject(canonicalConfig) ? canonicalConfig : {}
481
+ ),
482
+ isPlainObject(localConfig) ? localConfig : {}
457
483
  );
458
484
  const nextContents = stringifyTomlObject(merged);
459
485
  const current = await readTextIfExists(args.toolConfigPath);
@@ -462,7 +488,7 @@ export async function planToolConfigSync(args: {
462
488
  write: current !== `${nextContents}\n`,
463
489
  remove: false,
464
490
  contents: nextContents,
465
- sourcePath,
491
+ sourcePath: hasLocalSource ? localSourcePath : sourcePath,
466
492
  managedConfig: true,
467
493
  };
468
494
  }
package/src/manage.ts CHANGED
@@ -586,15 +586,34 @@ async function loadCanonicalAutomations(
586
586
  return await loadAutomationEntries(join(rootDir, "automations"));
587
587
  }
588
588
 
589
+ function isAutomationRuntimeRelativePath(relPath: string): boolean {
590
+ return relPath === "memory.md";
591
+ }
592
+
593
+ function isAutomationRuntimeTargetPath(targetPath: string): boolean {
594
+ return basename(targetPath) === "memory.md";
595
+ }
596
+
589
597
  function automationEntriesEqual(
590
598
  left: AutomationEntry,
591
599
  right: AutomationEntry
592
600
  ): boolean {
593
- if (left.files.size !== right.files.size) {
601
+ const leftFiles = new Map(
602
+ [...left.files.entries()].filter(
603
+ ([relPath]) => !isAutomationRuntimeRelativePath(relPath)
604
+ )
605
+ );
606
+ const rightFiles = new Map(
607
+ [...right.files.entries()].filter(
608
+ ([relPath]) => !isAutomationRuntimeRelativePath(relPath)
609
+ )
610
+ );
611
+
612
+ if (leftFiles.size !== rightFiles.size) {
594
613
  return false;
595
614
  }
596
- for (const [relPath, leftRaw] of left.files.entries()) {
597
- if (right.files.get(relPath) !== leftRaw) {
615
+ for (const [relPath, leftRaw] of leftFiles.entries()) {
616
+ if (rightFiles.get(relPath) !== leftRaw) {
598
617
  return false;
599
618
  }
600
619
  }
@@ -816,23 +835,27 @@ async function planAutomationFileChanges(args: {
816
835
  const contents = new Map<string, string>();
817
836
  const sources = new Map<string, string>();
818
837
  const desiredPaths = new Set<string>();
838
+ const add = new Set<string>();
819
839
 
820
840
  for (const automation of automations) {
821
841
  for (const [relPath, raw] of automation.files.entries()) {
822
842
  const targetPath = join(args.automationDir, automation.name, relPath);
823
843
  const sourcePath = join(automation.sourceDir, relPath);
824
- desiredPaths.add(targetPath);
825
844
  contents.set(targetPath, raw);
826
- sources.set(targetPath, sourcePath);
827
- }
828
- }
829
845
 
830
- const add = new Set<string>();
831
- for (const targetPath of desiredPaths) {
832
- const current = await readTextIfExists(targetPath);
833
- const desired = contents.get(targetPath);
834
- if (desired != null && current !== desired) {
835
- add.add(targetPath);
846
+ if (isAutomationRuntimeRelativePath(relPath)) {
847
+ if ((await readTextIfExists(targetPath)) == null) {
848
+ add.add(targetPath);
849
+ }
850
+ continue;
851
+ }
852
+
853
+ desiredPaths.add(targetPath);
854
+ sources.set(targetPath, sourcePath);
855
+ const current = await readTextIfExists(targetPath);
856
+ if (current !== raw) {
857
+ add.add(targetPath);
858
+ }
836
859
  }
837
860
  }
838
861
 
@@ -841,6 +864,7 @@ async function planAutomationFileChanges(args: {
841
864
  (args.previouslyManagedTargets ?? []).filter(
842
865
  (targetPath) =>
843
866
  targetPath.startsWith(join(args.automationDir, "")) &&
867
+ !isAutomationRuntimeTargetPath(targetPath) &&
844
868
  !desiredPaths.has(targetPath)
845
869
  )
846
870
  )
@@ -2973,6 +2997,30 @@ function updateRenderedTargetState(args: {
2973
2997
  args.entry.renderedTargets = next;
2974
2998
  }
2975
2999
 
3000
+ function pruneAutomationRuntimeRenderedTargets(args: {
3001
+ entry: ManagedToolState;
3002
+ automationDir?: string;
3003
+ }) {
3004
+ if (!(args.automationDir && args.entry.renderedTargets)) {
3005
+ return;
3006
+ }
3007
+ const prefix = join(args.automationDir, "");
3008
+ const next = { ...args.entry.renderedTargets };
3009
+ let changed = false;
3010
+ for (const targetPath of Object.keys(next)) {
3011
+ if (
3012
+ targetPath.startsWith(prefix) &&
3013
+ isAutomationRuntimeTargetPath(targetPath)
3014
+ ) {
3015
+ delete next[targetPath];
3016
+ changed = true;
3017
+ }
3018
+ }
3019
+ if (changed) {
3020
+ args.entry.renderedTargets = next;
3021
+ }
3022
+ }
3023
+
2976
3024
  function logSyncDryRun({
2977
3025
  tool,
2978
3026
  entry,
@@ -3177,6 +3225,11 @@ async function syncManagedToolEntry({
3177
3225
  dryRun?: boolean;
3178
3226
  builtinConflictMode?: "warn" | "overwrite";
3179
3227
  }) {
3228
+ pruneAutomationRuntimeRenderedTargets({
3229
+ entry,
3230
+ automationDir: entry.automationDir,
3231
+ });
3232
+
3180
3233
  const adoptedSkills = dryRun
3181
3234
  ? []
3182
3235
  : await repairManagedCanonicalContent({