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 +6 -5
- package/package.json +2 -2
- package/src/ai-state.ts +50 -31
- package/src/ai.ts +43 -3
- package/src/doctor.ts +11 -0
- package/src/paths.ts +33 -0
- package/src/status.ts +20 -1
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
|
|
299
|
-
-
|
|
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
|
|
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:
|
|
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
|
-
-
|
|
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.
|
|
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": "
|
|
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
|
|
193
|
+
for (const legacyPath of legacyGeneratedAiIndexPaths(
|
|
164
194
|
args.homeDir,
|
|
165
195
|
args.rootDir
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
254
|
+
for (const legacyGeneratedPath of legacyGeneratedAiGraphPaths(
|
|
237
255
|
args.homeDir,
|
|
238
256
|
args.rootDir
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
289
|
-
|
|
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
|
|
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(
|
|
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" &&
|