facult 2.5.0 → 2.5.2

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
@@ -282,6 +282,8 @@ bunx fclt index
282
282
 
283
283
  This seeds `<repo>/.ai` from the built-in Facult operating-model pack and writes a merged project index/graph under `<repo>/.ai/.facult/ai/`.
284
284
 
285
+ Wide learning-review automations should use this same bootstrap when they hit a local writable repo with durable project-local signal but no repo-local `.ai` yet.
286
+
285
287
  ### 4. Inspect what you have
286
288
 
287
289
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "Manage canonical AI capabilities, sync surfaces, and evolution state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,6 +16,11 @@ export interface ParsedCliContext {
16
16
  sourceKind?: AssetSourceKind;
17
17
  }
18
18
 
19
+ function missingProjectAiRootMessage(pathValue?: string): string {
20
+ const suffix = pathValue ? `: ${pathValue}` : "";
21
+ return `No project-local .ai root found${suffix}. Run "fclt templates init project-ai" in the repo first, or pass --root <repo>/.ai.`;
22
+ }
23
+
19
24
  function expandHomePath(pathValue: string, home: string): string {
20
25
  if (pathValue === "~") {
21
26
  return home;
@@ -173,9 +178,7 @@ export function resolveCliContextRoot(args?: {
173
178
  if (args?.rootArg) {
174
179
  const rootDir = coerceCanonicalRoot(args.rootArg, homeDir);
175
180
  if (scope === "project" && !projectRootFromAiRoot(rootDir, homeDir)) {
176
- throw new Error(
177
- `Project scope requires a repo-local .ai root: ${rootDir}`
178
- );
181
+ throw new Error(missingProjectAiRootMessage(rootDir));
179
182
  }
180
183
  return rootDir;
181
184
  }
@@ -187,9 +190,7 @@ export function resolveCliContextRoot(args?: {
187
190
  if (scope === "project") {
188
191
  const projectRoot = findNearestProjectAiRoot(cwd);
189
192
  if (!projectRoot) {
190
- throw new Error(
191
- "No project-local .ai root found from the current directory"
192
- );
193
+ throw new Error(missingProjectAiRootMessage(cwd));
193
194
  }
194
195
  return projectRoot;
195
196
  }
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({
package/src/remote.ts CHANGED
@@ -341,7 +341,7 @@ Use this memory for pattern continuity:
341
341
  - For wide reviews, partition evidence by cwd first; do not let one repo's evidence stand in for another.
342
342
  - Grounding: prefer evidence from session messages, tool calls, shell commands, diffs, tests, commits, and touched files.
343
343
  - Threshold: only encode signal when you can name what was learned, why it matters, and the most plausible destination.
344
- - Scope: default to project writeback unless the signal clearly belongs in global doctrine or a shared capability.
344
+ - Scope: default to project writeback only when the repo has a project-local \`.ai\` root. If a local writable repo is missing one, bootstrap baseline project AI state with \`fclt templates init project-ai\` before retrying project-scoped writeback. If bootstrap fails or the repo is not writable, treat that as the blocker instead of silently falling back to global runtime state.
345
345
  - Promote to global only when the same signal appears across multiple repos or clearly targets shared doctrine, shared agents, or shared skills.
346
346
  - Verification: distinguish one-off friction from a repeated pattern before escalating it.
347
347
  - If available, use [$feedback-loop-setup]({{feedbackLoopSkill}}) when the review needs stronger feedback loops or verification framing.
@@ -367,6 +367,8 @@ Grounding rules:
367
367
 
368
368
  Decision rules:
369
369
  - Use \`fclt ai writeback add\` when the signal, target asset, and scope are clear.
370
+ - Before attempting project-scoped writeback, verify the cwd has a repo-local \`.ai\` root. If it does not and the cwd is a local writable repo, run \`fclt templates init project-ai\` from that repo root, then continue. If bootstrap fails or the repo is not writable, report the writeback as blocked by missing project AI state rather than falling back to merged/global runtime state.
371
+ - Before passing \`--asset\`, verify the target resolves in the Facult graph. If the destination is a raw file path or otherwise not graph-backed, report that as a missing-asset blocker instead of retrying blind.
370
372
  - Use \`fclt ai evolve\` only when repeated signal is strong enough to justify a reviewable capability change.
371
373
  - Prefer project scope unless the learning clearly belongs in shared global doctrine, shared agents, shared skills, or other cross-project capability.
372
374
  - For wide automations, require repeated evidence across more than one cwd before recommending a global/shared capability change unless the target is obviously global.