facult 2.17.3 → 2.17.6
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/package.json +1 -1
- package/src/ai.ts +18 -1
- package/src/doctor.ts +40 -16
- package/src/global-docs.ts +2 -2
- package/src/manage.ts +3 -2
- package/src/paths.ts +46 -1
- package/src/self-update.ts +54 -24
package/package.json
CHANGED
package/src/ai.ts
CHANGED
|
@@ -491,6 +491,18 @@ async function writeProposalReviewArtifact(args: {
|
|
|
491
491
|
await Bun.write(pathValue, `${body.trimEnd()}\n`);
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
+
async function currentDraftBodyForProposal(
|
|
495
|
+
proposal: AiProposalRecord
|
|
496
|
+
): Promise<string | null> {
|
|
497
|
+
const draftPath = proposal.draftRefs.find((pathValue) =>
|
|
498
|
+
pathValue.endsWith(".md")
|
|
499
|
+
);
|
|
500
|
+
if (!(draftPath && (await fileExists(draftPath)))) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return readFile(draftPath, "utf8");
|
|
504
|
+
}
|
|
505
|
+
|
|
494
506
|
function slugToTitle(value: string): string {
|
|
495
507
|
return value
|
|
496
508
|
.split(SLUG_SPLIT_RE)
|
|
@@ -988,7 +1000,11 @@ export async function proposeEvolution(args: {
|
|
|
988
1000
|
: null;
|
|
989
1001
|
|
|
990
1002
|
const candidates = writebacks.filter((entry) => {
|
|
991
|
-
if (
|
|
1003
|
+
if (
|
|
1004
|
+
entry.status === "dismissed" ||
|
|
1005
|
+
entry.status === "resolved" ||
|
|
1006
|
+
entry.status === "superseded"
|
|
1007
|
+
) {
|
|
992
1008
|
return false;
|
|
993
1009
|
}
|
|
994
1010
|
if (entry.evidence.length === 0) {
|
|
@@ -1371,6 +1387,7 @@ export async function refreshAiReviewArtifacts(args: {
|
|
|
1371
1387
|
rootDir: args.rootDir,
|
|
1372
1388
|
proposal,
|
|
1373
1389
|
writebacks: sourceWritebacks,
|
|
1390
|
+
draftBody: await currentDraftBodyForProposal(proposal),
|
|
1374
1391
|
});
|
|
1375
1392
|
}
|
|
1376
1393
|
|
package/src/doctor.ts
CHANGED
|
@@ -476,7 +476,12 @@ async function repairCanonicalGlobalDocs(args: {
|
|
|
476
476
|
home: string;
|
|
477
477
|
rootDir: string;
|
|
478
478
|
}): Promise<{ changed: boolean; backupPath?: string }> {
|
|
479
|
-
|
|
479
|
+
if (projectRootFromAiRoot(args.rootDir, args.home)) {
|
|
480
|
+
return { changed: false };
|
|
481
|
+
}
|
|
482
|
+
const inspected = await inspectCanonicalGlobalDocs(args.rootDir, {
|
|
483
|
+
projectRoot: null,
|
|
484
|
+
});
|
|
480
485
|
if (!(inspected.exists && !inspected.valid)) {
|
|
481
486
|
return { changed: false };
|
|
482
487
|
}
|
|
@@ -494,13 +499,13 @@ async function repairCanonicalGlobalDocs(args: {
|
|
|
494
499
|
await copyFile(targetPath, backupPath);
|
|
495
500
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
496
501
|
await writeFile(targetPath, await readFile(sourcePath, "utf8"), "utf8");
|
|
497
|
-
await
|
|
502
|
+
await repairBuiltinOperatingModelSnippets(args.rootDir);
|
|
498
503
|
await ensureAiIndexPath({ homeDir: args.home, rootDir: args.rootDir });
|
|
499
504
|
await ensureAiGraphPath({ homeDir: args.home, rootDir: args.rootDir });
|
|
500
505
|
return { changed: true, backupPath };
|
|
501
506
|
}
|
|
502
507
|
|
|
503
|
-
async function
|
|
508
|
+
async function repairBuiltinOperatingModelSnippets(
|
|
504
509
|
rootDir: string
|
|
505
510
|
): Promise<void> {
|
|
506
511
|
const relPaths = [
|
|
@@ -515,7 +520,10 @@ async function copyMissingBuiltinOperatingModelSnippets(
|
|
|
515
520
|
for (const relPath of relPaths) {
|
|
516
521
|
const sourcePath = join(sourceRoot, relPath);
|
|
517
522
|
const targetPath = join(rootDir, relPath);
|
|
518
|
-
|
|
523
|
+
const existing = (await pathExists(targetPath))
|
|
524
|
+
? await readFile(targetPath, "utf8")
|
|
525
|
+
: null;
|
|
526
|
+
if (existing !== null && existing.trim().length > 0) {
|
|
519
527
|
continue;
|
|
520
528
|
}
|
|
521
529
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
@@ -601,7 +609,10 @@ const UNRESOLVED_REFS_TEMPLATE_RE = /\$\{refs\.([A-Za-z0-9_.-]+)\}/g;
|
|
|
601
609
|
const FCLTY_BLOCK_RE =
|
|
602
610
|
/<!--\s*fclty:([^>]+?)\s*-->([\s\S]*?)<!--\s*\/fclty:\1\s*-->/g;
|
|
603
611
|
|
|
604
|
-
async function inspectCanonicalGlobalDocs(
|
|
612
|
+
async function inspectCanonicalGlobalDocs(
|
|
613
|
+
rootDir: string,
|
|
614
|
+
opts: { projectRoot?: string | null } = {}
|
|
615
|
+
): Promise<{
|
|
605
616
|
exists: boolean;
|
|
606
617
|
valid: boolean;
|
|
607
618
|
issues: DoctorIssue[];
|
|
@@ -613,6 +624,12 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
|
|
|
613
624
|
|
|
614
625
|
const text = await readFile(pathValue, "utf8");
|
|
615
626
|
const issues: DoctorIssue[] = [];
|
|
627
|
+
const refreshCommand = opts.projectRoot
|
|
628
|
+
? "fclt templates init project-ai --force"
|
|
629
|
+
: "fclt templates init operating-model --global --force";
|
|
630
|
+
const docLabel = opts.projectRoot
|
|
631
|
+
? "project AGENTS.global.md"
|
|
632
|
+
: "AGENTS.global.md";
|
|
616
633
|
const withSnippets = await renderSnippetText({
|
|
617
634
|
text,
|
|
618
635
|
filePath: pathValue,
|
|
@@ -623,7 +640,7 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
|
|
|
623
640
|
severity: "warning",
|
|
624
641
|
code: "canonical-global-docs-render-error",
|
|
625
642
|
message: error,
|
|
626
|
-
fix:
|
|
643
|
+
fix: `Review ${docLabel} snippet markers or refresh the selected capability root with \`${refreshCommand}\`.`,
|
|
627
644
|
});
|
|
628
645
|
}
|
|
629
646
|
const rendered = await renderCanonicalText(withSnippets.text, {
|
|
@@ -636,9 +653,8 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
|
|
|
636
653
|
issues.push({
|
|
637
654
|
severity: "warning",
|
|
638
655
|
code: "canonical-global-docs-unresolved-template",
|
|
639
|
-
message:
|
|
640
|
-
|
|
641
|
-
fix: "Review AGENTS.global.md refs or refresh the built-in operating model with `fclt templates init operating-model --global --force`.",
|
|
656
|
+
message: `Rendered ${docLabel} contains unresolved template references.`,
|
|
657
|
+
fix: `Review ${docLabel} refs or refresh the selected capability root with \`${refreshCommand}\`.`,
|
|
642
658
|
});
|
|
643
659
|
}
|
|
644
660
|
|
|
@@ -654,8 +670,8 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
|
|
|
654
670
|
issues.push({
|
|
655
671
|
severity: "warning",
|
|
656
672
|
code: "canonical-global-docs-empty-managed-sections",
|
|
657
|
-
message: `Rendered
|
|
658
|
-
fix:
|
|
673
|
+
message: `Rendered ${docLabel} has empty fclty managed sections: ${emptySections.join(", ")}.`,
|
|
674
|
+
fix: `Add the missing snippets or refresh the selected capability root with \`${refreshCommand}\`.`,
|
|
659
675
|
});
|
|
660
676
|
}
|
|
661
677
|
|
|
@@ -976,7 +992,7 @@ export async function buildDoctorReport(opts?: {
|
|
|
976
992
|
pathExists(generatedGraph),
|
|
977
993
|
pathExists(writebackReviewDir),
|
|
978
994
|
pathExists(evolutionReviewDir),
|
|
979
|
-
inspectCanonicalGlobalDocs(rootDir),
|
|
995
|
+
inspectCanonicalGlobalDocs(rootDir, { projectRoot }),
|
|
980
996
|
inspectCanonicalTemplateRefs(rootDir),
|
|
981
997
|
planProjectSyncPolicyRepair({ home, rootDir }),
|
|
982
998
|
]);
|
|
@@ -1019,9 +1035,15 @@ export async function buildDoctorReport(opts?: {
|
|
|
1019
1035
|
}
|
|
1020
1036
|
if (canonicalGlobalDocs.exists && !canonicalGlobalDocs.valid) {
|
|
1021
1037
|
actions.push({
|
|
1022
|
-
id:
|
|
1023
|
-
|
|
1024
|
-
|
|
1038
|
+
id: projectRoot
|
|
1039
|
+
? "refresh-project-operating-model"
|
|
1040
|
+
: "refresh-global-operating-model",
|
|
1041
|
+
label: projectRoot
|
|
1042
|
+
? "Refresh project operating model"
|
|
1043
|
+
: "Refresh global operating model",
|
|
1044
|
+
command: projectRoot
|
|
1045
|
+
? "fclt templates init project-ai --force"
|
|
1046
|
+
: "fclt templates init operating-model --global --force",
|
|
1025
1047
|
risk: "canonical_write",
|
|
1026
1048
|
});
|
|
1027
1049
|
}
|
|
@@ -1369,7 +1391,9 @@ export async function doctorCommand(argv: string[]) {
|
|
|
1369
1391
|
`Project sync is still implicit for managed tools (${projectSyncRepairTools.join(", ")}). Run \`fclt doctor --repair\` to write explicit [project_sync.<tool>] entries.`
|
|
1370
1392
|
);
|
|
1371
1393
|
}
|
|
1372
|
-
const canonicalGlobalDocs = await inspectCanonicalGlobalDocs(rootDir
|
|
1394
|
+
const canonicalGlobalDocs = await inspectCanonicalGlobalDocs(rootDir, {
|
|
1395
|
+
projectRoot: projectRootFromAiRoot(rootDir, home),
|
|
1396
|
+
});
|
|
1373
1397
|
if (canonicalGlobalDocs.exists && !canonicalGlobalDocs.valid) {
|
|
1374
1398
|
for (const issue of canonicalGlobalDocs.issues) {
|
|
1375
1399
|
console.log(`${issue.message} ${issue.fix ?? ""}`.trim());
|
package/src/global-docs.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
facultBuiltinAgentsGlobalSourcePath,
|
|
7
7
|
facultBuiltinPackRoot,
|
|
8
8
|
} from "./builtin";
|
|
9
|
-
import { projectRootFromAiRoot } from "./paths";
|
|
9
|
+
import { pathIsInsideOrEqual, projectRootFromAiRoot } from "./paths";
|
|
10
10
|
import { projectSyncAllowsToolSurface } from "./project-sync";
|
|
11
11
|
import { renderSnippetText } from "./snippets";
|
|
12
12
|
|
|
@@ -233,7 +233,7 @@ async function renderSourceTarget(args: {
|
|
|
233
233
|
}): Promise<string> {
|
|
234
234
|
const raw = await Bun.file(args.sourcePath).text();
|
|
235
235
|
const builtinRoot = facultBuiltinPackRoot();
|
|
236
|
-
const sourceRoot = args.sourcePath
|
|
236
|
+
const sourceRoot = pathIsInsideOrEqual(args.sourcePath, builtinRoot)
|
|
237
237
|
? builtinRoot
|
|
238
238
|
: args.rootDir;
|
|
239
239
|
const withSnippets = await renderSnippetText({
|
package/src/manage.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
facultMachineStateDir,
|
|
49
49
|
facultRootDir,
|
|
50
50
|
legacyFacultStateDirForRoot,
|
|
51
|
+
pathIsInsideOrEqual,
|
|
51
52
|
projectRootFromAiRoot,
|
|
52
53
|
} from "./paths";
|
|
53
54
|
import { loadProjectToolSyncPolicy } from "./project-sync";
|
|
@@ -289,8 +290,8 @@ async function isGeneratedOnlyProjectRoot(args: {
|
|
|
289
290
|
function renderedSourceKindForPath(
|
|
290
291
|
sourcePath: string
|
|
291
292
|
): ManagedRenderedTargetState["sourceKind"] {
|
|
292
|
-
return sourcePath
|
|
293
|
-
sourcePath
|
|
293
|
+
return pathIsInsideOrEqual(sourcePath, facultBuiltinPackRoot()) ||
|
|
294
|
+
pathIsInsideOrEqual(sourcePath, facultBuiltinCodexPluginRoot())
|
|
294
295
|
? "builtin"
|
|
295
296
|
: "canonical";
|
|
296
297
|
}
|
package/src/paths.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
basename,
|
|
6
|
+
dirname,
|
|
7
|
+
isAbsolute,
|
|
8
|
+
join,
|
|
9
|
+
relative,
|
|
10
|
+
resolve,
|
|
11
|
+
win32,
|
|
12
|
+
} from "node:path";
|
|
5
13
|
import { parseJsonLenient } from "./util/json";
|
|
6
14
|
|
|
15
|
+
const WINDOWS_ABSOLUTE_PATH_RE = /^[A-Za-z]:[\\/]/;
|
|
16
|
+
|
|
7
17
|
export interface FacultConfig {
|
|
8
18
|
/**
|
|
9
19
|
* Override the canonical root directory.
|
|
@@ -88,6 +98,41 @@ export function preferredGlobalAiRoot(home: string = defaultHomeDir()): string {
|
|
|
88
98
|
return join(home, ".ai");
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
function looksLikeWindowsAbsolutePath(pathValue: string): boolean {
|
|
102
|
+
return (
|
|
103
|
+
WINDOWS_ABSOLUTE_PATH_RE.test(pathValue) || pathValue.startsWith("\\\\")
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function relativePathIsInsideOrEqual(args: {
|
|
108
|
+
rel: string;
|
|
109
|
+
isAbsolutePath: (pathValue: string) => boolean;
|
|
110
|
+
}): boolean {
|
|
111
|
+
const { isAbsolutePath, rel } = args;
|
|
112
|
+
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolutePath(rel));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function pathIsInsideOrEqual(
|
|
116
|
+
pathValue: string,
|
|
117
|
+
rootDir: string
|
|
118
|
+
): boolean {
|
|
119
|
+
if (
|
|
120
|
+
looksLikeWindowsAbsolutePath(pathValue) &&
|
|
121
|
+
looksLikeWindowsAbsolutePath(rootDir)
|
|
122
|
+
) {
|
|
123
|
+
return relativePathIsInsideOrEqual({
|
|
124
|
+
isAbsolutePath: win32.isAbsolute,
|
|
125
|
+
rel: win32.relative(rootDir, pathValue),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rel = relative(rootDir, pathValue);
|
|
130
|
+
return relativePathIsInsideOrEqual({
|
|
131
|
+
isAbsolutePath: isAbsolute,
|
|
132
|
+
rel,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
export function preferredGlobalFacultStateDir(
|
|
92
137
|
home: string = defaultHomeDir()
|
|
93
138
|
): string {
|
package/src/self-update.ts
CHANGED
|
@@ -195,9 +195,6 @@ async function activeFcltUsesMiseNpmFacult(): Promise<boolean> {
|
|
|
195
195
|
if (looksLikeMiseNpmFacultExecutable(fcltPath ?? "")) {
|
|
196
196
|
return true;
|
|
197
197
|
}
|
|
198
|
-
if (await miseHasCurrentFacultTool()) {
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
198
|
if (!looksLikeMiseShim(fcltPath)) {
|
|
202
199
|
return false;
|
|
203
200
|
}
|
|
@@ -218,20 +215,21 @@ async function activeFcltUsesMiseNpmFacult(): Promise<boolean> {
|
|
|
218
215
|
return looksLikeMiseNpmFacultExecutable(stdout.trim());
|
|
219
216
|
}
|
|
220
217
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
return
|
|
218
|
+
export function buildCommandLookupFallback(
|
|
219
|
+
command: string,
|
|
220
|
+
platform: NodeJS.Platform = process.platform
|
|
221
|
+
): string[] {
|
|
222
|
+
if (platform === "win32") {
|
|
223
|
+
return ["where.exe", command];
|
|
224
|
+
}
|
|
225
|
+
const quoted = `'${command.replaceAll("'", "'\\''")}'`;
|
|
226
|
+
return ["sh", "-lc", `command -v ${quoted}`];
|
|
230
227
|
}
|
|
231
228
|
|
|
232
229
|
async function resolveCommandPath(command: string): Promise<string | null> {
|
|
230
|
+
const cmd = buildCommandLookupFallback(command);
|
|
233
231
|
const proc = Bun.spawn({
|
|
234
|
-
cmd
|
|
232
|
+
cmd,
|
|
235
233
|
stdin: "ignore",
|
|
236
234
|
stdout: "pipe",
|
|
237
235
|
stderr: "ignore",
|
|
@@ -247,6 +245,17 @@ async function resolveCommandPath(command: string): Promise<string | null> {
|
|
|
247
245
|
return stdout.trim() || null;
|
|
248
246
|
}
|
|
249
247
|
|
|
248
|
+
export function looksLikeMiseNpmFacultExecutableForVersion(
|
|
249
|
+
executablePath: string,
|
|
250
|
+
version: string
|
|
251
|
+
): boolean {
|
|
252
|
+
const normalized = executablePath.split("\\").join(sep);
|
|
253
|
+
return (
|
|
254
|
+
looksLikeMiseNpmFacultExecutable(normalized) &&
|
|
255
|
+
normalized.includes(`${sep}npm-facult${sep}${version}${sep}`)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
250
259
|
function resolvePlatformTarget(): {
|
|
251
260
|
platform: string;
|
|
252
261
|
arch: string;
|
|
@@ -516,17 +525,38 @@ async function assertActiveFcltVersion(
|
|
|
516
525
|
expectedVersion: string,
|
|
517
526
|
packageManager: PackageManager
|
|
518
527
|
): Promise<void> {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
528
|
+
let cmd = ["fclt", "--version"];
|
|
529
|
+
if (packageManager === "mise") {
|
|
530
|
+
const which = Bun.spawn({
|
|
531
|
+
cmd: ["mise", "which", "fclt"],
|
|
532
|
+
stdin: "ignore",
|
|
533
|
+
stdout: "pipe",
|
|
534
|
+
stderr: "pipe",
|
|
535
|
+
env: process.env,
|
|
536
|
+
});
|
|
537
|
+
const [whichStdout, whichStderr, whichCode] = await Promise.all([
|
|
538
|
+
new Response(which.stdout).text(),
|
|
539
|
+
new Response(which.stderr).text(),
|
|
540
|
+
which.exited,
|
|
541
|
+
]);
|
|
542
|
+
const resolvedPath = whichStdout.trim();
|
|
543
|
+
if (whichCode !== 0) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Updated package, but could not verify active mise fclt path: ${whichStderr.trim()}`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (
|
|
549
|
+
!looksLikeMiseNpmFacultExecutableForVersion(resolvedPath, expectedVersion)
|
|
550
|
+
) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
[
|
|
553
|
+
`Updated package to ${expectedVersion}, but mise resolves fclt to ${resolvedPath || "nothing"}.`,
|
|
554
|
+
"Run `mise which fclt` and `mise current npm:facult` to inspect the active tool selection.",
|
|
555
|
+
].join("\n")
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
cmd = [resolvedPath, "--version"];
|
|
559
|
+
}
|
|
530
560
|
const proc = Bun.spawn({
|
|
531
561
|
cmd,
|
|
532
562
|
stdin: "ignore",
|