facult 2.8.0 → 2.8.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
@@ -295,10 +295,11 @@ Typical layout:
295
295
 
296
296
  Important split:
297
297
  - `.ai/` is canonical source
298
- - `.ai/.facult/ai/` is generated AI state that belongs with the canonical root
299
- - machine-local Facult state such as managed-tool state, autosync runtime/config, install metadata, and launcher caches lives outside `.ai/`
298
+ - global `.ai/.facult/ai/` is generated AI state for the global canonical root
299
+ - project generated AI state lives in machine-local per-project Facult state, outside the repo
300
+ - machine-local Facult state such as project indexes, project graphs, managed-tool state, autosync runtime/config, install metadata, and launcher caches lives outside project `.ai/`
300
301
  - tool homes such as `.codex/` and `.claude/` are rendered outputs
301
- - the generated capability graph lives at `.ai/.facult/ai/graph.json`
302
+ - the generated capability graph lives under the active generated AI state directory
302
303
 
303
304
  ### Asset types
304
305
 
@@ -442,11 +443,11 @@ fclt ai evolve promote EV-00003 --to global --project
442
443
 
443
444
  Runtime state stays generated and local inside the active canonical root:
444
445
  - global writeback state: `~/.ai/.facult/ai/global/...`
445
- - project writeback state: `<repo>/.ai/.facult/ai/project/...`
446
+ - project writeback state: machine-local per-project Facult state under `.../projects/<slug-hash>/ai/project/...`
446
447
 
447
448
  That split is intentional:
448
449
  - canonical source remains in `~/.ai` or `<repo>/.ai`
449
- - writeback queues, journals, proposal records, trust state, autosync state, and other generated runtime/config state stay inside `.ai/.facult/`
450
+ - global generated state stays inside `~/.ai/.facult/`; project generated state stays outside the repo in machine-local state
450
451
  - those records let agents inspect what changed, why it changed, and how it was reviewed
451
452
 
452
453
  Use writeback when:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "Manage canonical AI capabilities, sync surfaces, and evolution state.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "facult": "bun run ./src/index.ts",
44
44
  "scan": "bun run ./src/index.ts scan",
45
45
  "build": "bun run scripts/build-binary.ts",
46
- "build:verify": "./dist/fclt --help",
46
+ "build:verify": "bun run scripts/verify-binary.ts",
47
47
  "install:dev": "bun run scripts/install-cli.ts --mode=dev --force",
48
48
  "install:bin": "bun run build && bun run scripts/install-cli.ts --mode=bin --force",
49
49
  "install:status": "bun run scripts/install-status.ts",
package/src/ai-state.ts CHANGED
@@ -5,6 +5,7 @@ import { buildIndex } from "./index-builder";
5
5
  import {
6
6
  facultAiGraphPath,
7
7
  facultAiIndexPath,
8
+ legacyFacultAiStateDirs,
8
9
  legacyFacultStateDirForRoot,
9
10
  preferredGlobalAiRoot,
10
11
  projectRootFromAiRoot,
@@ -123,6 +124,21 @@ function legacyGeneratedAiIndexPath(homeDir: string, rootDir: string): string {
123
124
  );
124
125
  }
125
126
 
127
+ function legacyGeneratedAiIndexPaths(
128
+ homeDir: string,
129
+ rootDir: string
130
+ ): string[] {
131
+ return [
132
+ ...new Set([
133
+ ...legacyFacultAiStateDirs(homeDir, rootDir).map((dir) =>
134
+ join(dir, "index.json")
135
+ ),
136
+ legacyGeneratedAiIndexPath(homeDir, rootDir),
137
+ legacyAiIndexPath(rootDir),
138
+ ]),
139
+ ];
140
+ }
141
+
126
142
  function legacyGeneratedAiGraphPath(homeDir: string, rootDir: string): string {
127
143
  return join(
128
144
  legacyFacultStateDirForRoot(rootDir, homeDir),
@@ -131,6 +147,20 @@ function legacyGeneratedAiGraphPath(homeDir: string, rootDir: string): string {
131
147
  );
132
148
  }
133
149
 
150
+ function legacyGeneratedAiGraphPaths(
151
+ homeDir: string,
152
+ rootDir: string
153
+ ): string[] {
154
+ return [
155
+ ...new Set([
156
+ ...legacyFacultAiStateDirs(homeDir, rootDir).map((dir) =>
157
+ join(dir, "graph.json")
158
+ ),
159
+ legacyGeneratedAiGraphPath(homeDir, rootDir),
160
+ ]),
161
+ ];
162
+ }
163
+
134
164
  export async function ensureAiIndexPath(args: {
135
165
  homeDir: string;
136
166
  rootDir: string;
@@ -160,33 +190,21 @@ export async function ensureAiIndexPath(args: {
160
190
  return { path: generatedPath, repaired: false, source: "generated" };
161
191
  }
162
192
 
163
- const legacyGeneratedPath = legacyGeneratedAiIndexPath(
193
+ for (const legacyPath of legacyGeneratedAiIndexPaths(
164
194
  args.homeDir,
165
195
  args.rootDir
166
- );
167
- if (await fileExists(legacyGeneratedPath)) {
168
- if (args.repair !== false) {
169
- await mkdir(dirname(generatedPath), { recursive: true });
170
- await copyFile(legacyGeneratedPath, generatedPath);
171
- }
172
- return {
173
- path: generatedPath,
174
- repaired: args.repair !== false,
175
- source: "legacy",
176
- };
177
- }
178
-
179
- const legacyPath = legacyAiIndexPath(args.rootDir);
180
- if (await fileExists(legacyPath)) {
181
- if (args.repair !== false) {
182
- await mkdir(dirname(generatedPath), { recursive: true });
183
- await copyFile(legacyPath, generatedPath);
196
+ )) {
197
+ if (await fileExists(legacyPath)) {
198
+ if (args.repair !== false) {
199
+ await mkdir(dirname(generatedPath), { recursive: true });
200
+ await copyFile(legacyPath, generatedPath);
201
+ }
202
+ return {
203
+ path: generatedPath,
204
+ repaired: args.repair !== false,
205
+ source: "legacy",
206
+ };
184
207
  }
185
- return {
186
- path: generatedPath,
187
- repaired: args.repair !== false,
188
- source: "legacy",
189
- };
190
208
  }
191
209
 
192
210
  if (args.repair !== false) {
@@ -233,16 +251,17 @@ export async function ensureAiGraphPath(args: {
233
251
  return { path: generatedPath, rebuilt: false };
234
252
  }
235
253
 
236
- const legacyGeneratedPath = legacyGeneratedAiGraphPath(
254
+ for (const legacyGeneratedPath of legacyGeneratedAiGraphPaths(
237
255
  args.homeDir,
238
256
  args.rootDir
239
- );
240
- if (await fileExists(legacyGeneratedPath)) {
241
- if (args.repair !== false) {
242
- await mkdir(dirname(generatedPath), { recursive: true });
243
- await copyFile(legacyGeneratedPath, generatedPath);
257
+ )) {
258
+ if (await fileExists(legacyGeneratedPath)) {
259
+ if (args.repair !== false) {
260
+ await mkdir(dirname(generatedPath), { recursive: true });
261
+ await copyFile(legacyGeneratedPath, generatedPath);
262
+ }
263
+ return { path: generatedPath, rebuilt: args.repair !== false };
244
264
  }
245
- return { path: generatedPath, rebuilt: args.repair !== false };
246
265
  }
247
266
 
248
267
  if (args.repair !== false) {
package/src/ai.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  facultAiProposalDir,
12
12
  facultAiWritebackQueuePath,
13
13
  facultRootDir,
14
+ legacyFacultAiStateDirs,
14
15
  projectRootFromAiRoot,
15
16
  projectSlugFromAiRoot,
16
17
  } from "./paths";
@@ -218,6 +219,43 @@ async function readJsonLines<T>(pathValue: string): Promise<T[]> {
218
219
  .map((line) => JSON.parse(line) as T);
219
220
  }
220
221
 
222
+ async function readJsonLinesFromPaths<T>(pathValues: string[]): Promise<T[]> {
223
+ const entries: T[] = [];
224
+ for (const pathValue of pathValues) {
225
+ entries.push(...(await readJsonLines<T>(pathValue)));
226
+ }
227
+ return entries;
228
+ }
229
+
230
+ function aiRuntimeScopeName(rootDir: string, homeDir: string): AssetScope {
231
+ return projectRootFromAiRoot(rootDir, homeDir) ? "project" : "global";
232
+ }
233
+
234
+ function legacyAiRuntimeScopeDirs(homeDir: string, rootDir: string): string[] {
235
+ const scope = aiRuntimeScopeName(rootDir, homeDir);
236
+ return legacyFacultAiStateDirs(homeDir, rootDir).map((dir) =>
237
+ join(dir, scope)
238
+ );
239
+ }
240
+
241
+ function aiWritebackQueueReadPaths(homeDir: string, rootDir: string): string[] {
242
+ return [
243
+ ...legacyAiRuntimeScopeDirs(homeDir, rootDir).map((dir) =>
244
+ join(dir, "writeback", "queue.jsonl")
245
+ ),
246
+ facultAiWritebackQueuePath(homeDir, rootDir),
247
+ ];
248
+ }
249
+
250
+ function aiJournalReadPaths(homeDir: string, rootDir: string): string[] {
251
+ return [
252
+ ...legacyAiRuntimeScopeDirs(homeDir, rootDir).map((dir) =>
253
+ join(dir, "journal", "events.jsonl")
254
+ ),
255
+ facultAiJournalPath(homeDir, rootDir),
256
+ ];
257
+ }
258
+
221
259
  function supportedDraftTarget(pathValue: string): boolean {
222
260
  return pathValue.toLowerCase().endsWith(".md");
223
261
  }
@@ -285,8 +323,8 @@ async function latestWritebackMap(args: {
285
323
  homeDir: string;
286
324
  rootDir: string;
287
325
  }): Promise<Map<string, AiWritebackRecord>> {
288
- const entries = await readJsonLines<AiWritebackRecord>(
289
- facultAiWritebackQueuePath(args.homeDir, args.rootDir)
326
+ const entries = await readJsonLinesFromPaths<AiWritebackRecord>(
327
+ aiWritebackQueueReadPaths(args.homeDir, args.rootDir)
290
328
  );
291
329
  const latest = new Map<string, AiWritebackRecord>();
292
330
  for (const entry of entries) {
@@ -301,7 +339,9 @@ async function appendEvent(
301
339
  event: AiJournalEvent
302
340
  ): Promise<void> {
303
341
  const pathValue = facultAiJournalPath(homeDir, rootDir);
304
- const existing = await readJsonLines<AiJournalEvent>(pathValue);
342
+ const existing = await readJsonLinesFromPaths<AiJournalEvent>(
343
+ aiJournalReadPaths(homeDir, rootDir)
344
+ );
305
345
  const next = {
306
346
  ...event,
307
347
  id: nextId(
package/src/doctor.ts CHANGED
@@ -727,6 +727,17 @@ export async function doctorCommand(argv: string[]) {
727
727
  return;
728
728
  }
729
729
 
730
+ if (
731
+ result.source === "legacy" &&
732
+ projectRootFromAiRoot(rootDir, home) &&
733
+ (await hasCanonicalSource(rootDir))
734
+ ) {
735
+ console.log(
736
+ "Legacy repo-local generated AI state detected. Run `fclt doctor --repair` or `fclt index` to migrate it into machine-local project state."
737
+ );
738
+ return;
739
+ }
740
+
730
741
  if (result.source === "legacy") {
731
742
  console.log(
732
743
  "Legacy root index detected. Run `fclt doctor --repair` to reconcile it."
package/src/paths.ts CHANGED
@@ -229,6 +229,9 @@ export function facultStateDir(
229
229
  rootDir?: string
230
230
  ): string {
231
231
  const resolvedRoot = rootDir ?? facultRootDir(home);
232
+ if (projectRootFromAiRoot(resolvedRoot, home)) {
233
+ return facultMachineStateDir(home, resolvedRoot);
234
+ }
232
235
  if (shouldUsePreferredGlobalStateDir(resolvedRoot, home)) {
233
236
  return preferredGlobalFacultStateDir(home);
234
237
  }
@@ -317,6 +320,36 @@ export function facultAiStateDir(
317
320
  return join(facultGeneratedStateDir({ home, rootDir }), "ai");
318
321
  }
319
322
 
323
+ export function legacyRepoLocalFacultAiStateDir(
324
+ home: string = defaultHomeDir(),
325
+ rootDir?: string
326
+ ): string | null {
327
+ const resolvedRoot = rootDir ?? facultRootDir(home);
328
+ return projectRootFromAiRoot(resolvedRoot, home)
329
+ ? join(resolvedRoot, ".facult", "ai")
330
+ : null;
331
+ }
332
+
333
+ export function legacyFacultAiStateDirs(
334
+ home: string = defaultHomeDir(),
335
+ rootDir?: string
336
+ ): string[] {
337
+ const resolvedRoot = rootDir ?? facultRootDir(home);
338
+ const current = resolve(facultAiStateDir(home, resolvedRoot));
339
+ const legacyDirs = [
340
+ legacyRepoLocalFacultAiStateDir(home, resolvedRoot),
341
+ join(legacyFacultStateDirForRoot(resolvedRoot, home), "ai"),
342
+ ];
343
+
344
+ return [
345
+ ...new Set(
346
+ legacyDirs
347
+ .filter((pathValue): pathValue is string => Boolean(pathValue))
348
+ .filter((pathValue) => resolve(pathValue) !== current)
349
+ ),
350
+ ];
351
+ }
352
+
320
353
  export function facultAiIndexPath(
321
354
  home: string = defaultHomeDir(),
322
355
  rootDir?: string
package/src/status.ts CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  } from "./paths";
14
14
  import { parseJsonLenient } from "./util/json";
15
15
 
16
+ declare const FCLT_COMPILED_VERSION: string | undefined;
17
+
16
18
  export interface StatusIssue {
17
19
  severity: "info" | "warning" | "error";
18
20
  code: string;
@@ -120,8 +122,25 @@ async function countActiveProposals(
120
122
  }
121
123
 
122
124
  export async function packageVersion(): Promise<string> {
125
+ const envVersion =
126
+ process.env.FACULT_NPM_PACKAGE_VERSION ?? process.env.npm_package_version;
127
+ if (envVersion?.trim()) {
128
+ return envVersion.trim();
129
+ }
130
+
131
+ if (
132
+ typeof FCLT_COMPILED_VERSION === "string" &&
133
+ FCLT_COMPILED_VERSION.trim()
134
+ ) {
135
+ return FCLT_COMPILED_VERSION.trim();
136
+ }
137
+
123
138
  const packagePath = join(dirname(import.meta.dir), "package.json");
124
- const parsed = parseJsonLenient(await Bun.file(packagePath).text());
139
+ const parsed = parseJsonLenient(
140
+ await Bun.file(packagePath)
141
+ .text()
142
+ .catch(() => "{}")
143
+ );
125
144
  if (
126
145
  parsed &&
127
146
  typeof parsed === "object" &&