facult 2.13.5 → 2.14.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 +31 -10
- package/assets/packs/facult-operating-model/AGENTS.global.md +41 -41
- package/assets/packs/facult-operating-model/agents/evolution-planner/agent.toml +3 -0
- package/assets/packs/facult-operating-model/agents/integration-auditor/agent.toml +8 -1
- package/assets/packs/facult-operating-model/agents/scope-promoter/agent.toml +8 -1
- package/assets/packs/facult-operating-model/agents/writeback-curator/agent.toml +2 -0
- package/assets/packs/facult-operating-model/instructions/CAPABILITY_COMPOSITION.md +4 -4
- package/assets/packs/facult-operating-model/instructions/EVOLUTION.md +3 -3
- package/assets/packs/facult-operating-model/instructions/INTEGRATION.md +44 -0
- package/assets/packs/facult-operating-model/instructions/LEARNING_AND_WRITEBACK.md +1 -1
- package/assets/packs/facult-operating-model/instructions/PROJECT_CAPABILITY.md +35 -0
- package/assets/packs/facult-operating-model/instructions/WORK_UNITS.md +48 -0
- package/assets/packs/facult-operating-model/skills/capability-evolution/SKILL.md +25 -6
- package/assets/packs/facult-operating-model/skills/project-operating-layer-design/SKILL.md +33 -0
- package/assets/packs/facult-operating-model/snippets/global/baseline.md +3 -0
- package/assets/packs/facult-operating-model/snippets/global/core/feedback-loops.md +11 -0
- package/assets/packs/facult-operating-model/snippets/global/core/verification.md +6 -0
- package/assets/packs/facult-operating-model/snippets/global/core/work-units.md +7 -0
- package/assets/packs/facult-operating-model/snippets/global/core/writeback.md +9 -0
- package/docs/README.md +3 -0
- package/docs/assets/fclt-capability-loop.png +0 -0
- package/docs/built-in-pack.md +13 -0
- package/docs/composable-capability.md +21 -22
- package/docs/concepts.md +4 -1
- package/docs/pack-upgrades.md +67 -0
- package/docs/reference.md +10 -2
- package/docs/work-units.md +96 -0
- package/package.json +1 -1
- package/src/agents.ts +17 -0
- package/src/builtin-assets.ts +2 -1
- package/src/doctor.ts +238 -7
- package/src/global-docs.ts +10 -3
- package/src/remote.ts +149 -14
- package/src/snippets-cli.ts +2 -2
- package/src/snippets.ts +1 -1
package/src/doctor.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "node:fs/promises";
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
|
+
import { renderCanonicalText } from "./agents";
|
|
18
19
|
import {
|
|
19
20
|
ensureAiGraphPath,
|
|
20
21
|
ensureAiIndexPath,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
loadConfiguredProjectSyncTools,
|
|
42
43
|
writeProjectSyncPolicy,
|
|
43
44
|
} from "./project-sync";
|
|
45
|
+
import { renderSnippetText } from "./snippets";
|
|
44
46
|
import { packageVersion } from "./status";
|
|
45
47
|
|
|
46
48
|
const TOML_FILE_SUFFIX_RE = /\.toml$/;
|
|
@@ -104,6 +106,9 @@ export interface DoctorReport {
|
|
|
104
106
|
evolutionReviewDirExists: boolean;
|
|
105
107
|
canonicalGlobalDocsValid: boolean;
|
|
106
108
|
canonicalGlobalDocsIssueCodes: string[];
|
|
109
|
+
canonicalTemplateRefsValid: boolean;
|
|
110
|
+
canonicalTemplateRefsIssueCodes: string[];
|
|
111
|
+
canonicalTemplateRefsIssuePaths: string[];
|
|
107
112
|
projectSyncRepairNeeded: boolean;
|
|
108
113
|
projectSyncRepairTools: string[];
|
|
109
114
|
};
|
|
@@ -446,6 +451,24 @@ async function repairLegacyCodexAuthoringLayout(args: {
|
|
|
446
451
|
return { changed, conflicts };
|
|
447
452
|
}
|
|
448
453
|
|
|
454
|
+
interface CanonicalTemplateIssue {
|
|
455
|
+
path: string;
|
|
456
|
+
relPath: string;
|
|
457
|
+
code: string;
|
|
458
|
+
message: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
interface CanonicalTemplateRefsInspection {
|
|
462
|
+
valid: boolean;
|
|
463
|
+
issues: CanonicalTemplateIssue[];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
interface CanonicalTemplateRefsRepair {
|
|
467
|
+
changed: boolean;
|
|
468
|
+
repairedPaths: string[];
|
|
469
|
+
backupPaths: string[];
|
|
470
|
+
}
|
|
471
|
+
|
|
449
472
|
async function repairCanonicalGlobalDocs(args: {
|
|
450
473
|
home: string;
|
|
451
474
|
rootDir: string;
|
|
@@ -468,11 +491,35 @@ async function repairCanonicalGlobalDocs(args: {
|
|
|
468
491
|
await copyFile(targetPath, backupPath);
|
|
469
492
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
470
493
|
await writeFile(targetPath, await readFile(sourcePath, "utf8"), "utf8");
|
|
494
|
+
await copyMissingBuiltinOperatingModelSnippets(args.rootDir);
|
|
471
495
|
await ensureAiIndexPath({ homeDir: args.home, rootDir: args.rootDir });
|
|
472
496
|
await ensureAiGraphPath({ homeDir: args.home, rootDir: args.rootDir });
|
|
473
497
|
return { changed: true, backupPath };
|
|
474
498
|
}
|
|
475
499
|
|
|
500
|
+
async function copyMissingBuiltinOperatingModelSnippets(
|
|
501
|
+
rootDir: string
|
|
502
|
+
): Promise<void> {
|
|
503
|
+
const relPaths = [
|
|
504
|
+
"snippets/global/baseline.md",
|
|
505
|
+
"snippets/global/core/work-units.md",
|
|
506
|
+
"snippets/global/core/feedback-loops.md",
|
|
507
|
+
"snippets/global/core/verification.md",
|
|
508
|
+
"snippets/global/core/writeback.md",
|
|
509
|
+
];
|
|
510
|
+
const sourceRoot = facultBuiltinPackRoot();
|
|
511
|
+
|
|
512
|
+
for (const relPath of relPaths) {
|
|
513
|
+
const sourcePath = join(sourceRoot, relPath);
|
|
514
|
+
const targetPath = join(rootDir, relPath);
|
|
515
|
+
if (await pathExists(targetPath)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
519
|
+
await copyFile(sourcePath, targetPath);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
476
523
|
async function listProjectSkillNames(rootDir: string): Promise<string[]> {
|
|
477
524
|
const skillsDir = join(rootDir, "skills");
|
|
478
525
|
const entries = await readdir(skillsDir, { withFileTypes: true }).catch(
|
|
@@ -547,6 +594,7 @@ async function hasProjectGlobalDocs(rootDir: string): Promise<boolean> {
|
|
|
547
594
|
}
|
|
548
595
|
|
|
549
596
|
const UNRESOLVED_TEMPLATE_REF_RE = /\$\{[^}\n]+\}/g;
|
|
597
|
+
const UNRESOLVED_REFS_TEMPLATE_RE = /\$\{refs\.([A-Za-z0-9_.-]+)\}/g;
|
|
550
598
|
const FCLTY_BLOCK_RE =
|
|
551
599
|
/<!--\s*fclty:([^>]+?)\s*-->([\s\S]*?)<!--\s*\/fclty:\1\s*-->/g;
|
|
552
600
|
|
|
@@ -562,19 +610,37 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
|
|
|
562
610
|
|
|
563
611
|
const text = await readFile(pathValue, "utf8");
|
|
564
612
|
const issues: DoctorIssue[] = [];
|
|
565
|
-
const
|
|
613
|
+
const withSnippets = await renderSnippetText({
|
|
614
|
+
text,
|
|
615
|
+
filePath: pathValue,
|
|
616
|
+
rootDir,
|
|
617
|
+
});
|
|
618
|
+
for (const error of withSnippets.errors) {
|
|
619
|
+
issues.push({
|
|
620
|
+
severity: "warning",
|
|
621
|
+
code: "canonical-global-docs-render-error",
|
|
622
|
+
message: error,
|
|
623
|
+
fix: "Review AGENTS.global.md snippet markers or refresh the built-in operating model with `fclt templates init operating-model --global --force`.",
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
const rendered = await renderCanonicalText(withSnippets.text, {
|
|
627
|
+
rootDir,
|
|
628
|
+
});
|
|
629
|
+
const unresolvedRefs = new Set(
|
|
630
|
+
rendered.match(UNRESOLVED_TEMPLATE_REF_RE) ?? []
|
|
631
|
+
);
|
|
566
632
|
if (unresolvedRefs.size > 0) {
|
|
567
633
|
issues.push({
|
|
568
634
|
severity: "warning",
|
|
569
635
|
code: "canonical-global-docs-unresolved-template",
|
|
570
636
|
message:
|
|
571
|
-
"
|
|
572
|
-
fix: "Review
|
|
637
|
+
"Rendered AGENTS.global.md contains unresolved template references.",
|
|
638
|
+
fix: "Review AGENTS.global.md refs or refresh the built-in operating model with `fclt templates init operating-model --global --force`.",
|
|
573
639
|
});
|
|
574
640
|
}
|
|
575
641
|
|
|
576
642
|
const emptySections: string[] = [];
|
|
577
|
-
for (const match of
|
|
643
|
+
for (const match of rendered.matchAll(FCLTY_BLOCK_RE)) {
|
|
578
644
|
const key = match[1]?.trim();
|
|
579
645
|
const body = match[2]?.trim();
|
|
580
646
|
if (key && !body) {
|
|
@@ -585,8 +651,8 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
|
|
|
585
651
|
issues.push({
|
|
586
652
|
severity: "warning",
|
|
587
653
|
code: "canonical-global-docs-empty-managed-sections",
|
|
588
|
-
message: `
|
|
589
|
-
fix: "
|
|
654
|
+
message: `Rendered AGENTS.global.md has empty fclty managed sections: ${emptySections.join(", ")}.`,
|
|
655
|
+
fix: "Add the missing snippets or refresh the built-in operating model with `fclt templates init operating-model --global --force`.",
|
|
590
656
|
});
|
|
591
657
|
}
|
|
592
658
|
|
|
@@ -597,6 +663,125 @@ async function inspectCanonicalGlobalDocs(rootDir: string): Promise<{
|
|
|
597
663
|
};
|
|
598
664
|
}
|
|
599
665
|
|
|
666
|
+
const CANONICAL_TEMPLATE_REF_DIRS = ["instructions"] as const;
|
|
667
|
+
|
|
668
|
+
function canonicalRefValues(rootDir: string): Record<string, string> {
|
|
669
|
+
return {
|
|
670
|
+
evolution: join(rootDir, "instructions", "EVOLUTION.md"),
|
|
671
|
+
feedback_loops: join(rootDir, "instructions", "FEEDBACK_LOOPS.md"),
|
|
672
|
+
integration: join(rootDir, "instructions", "INTEGRATION.md"),
|
|
673
|
+
learning_writeback: join(
|
|
674
|
+
rootDir,
|
|
675
|
+
"instructions",
|
|
676
|
+
"LEARNING_AND_WRITEBACK.md"
|
|
677
|
+
),
|
|
678
|
+
project_capability: join(rootDir, "instructions", "PROJECT_CAPABILITY.md"),
|
|
679
|
+
verification: join(rootDir, "instructions", "VERIFICATION.md"),
|
|
680
|
+
work_units: join(rootDir, "instructions", "WORK_UNITS.md"),
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function resolveKnownCanonicalRefs(text: string, rootDir: string): string {
|
|
685
|
+
const refs = canonicalRefValues(rootDir);
|
|
686
|
+
return text.replace(UNRESOLVED_REFS_TEMPLATE_RE, (match, key: string) => {
|
|
687
|
+
return refs[key] ?? match;
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function listCanonicalMarkdownFiles(rootDir: string): Promise<string[]> {
|
|
692
|
+
const out: string[] = [];
|
|
693
|
+
|
|
694
|
+
async function visit(dir: string): Promise<void> {
|
|
695
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(
|
|
696
|
+
() => [] as import("node:fs").Dirent[]
|
|
697
|
+
);
|
|
698
|
+
for (const entry of entries) {
|
|
699
|
+
if (entry.name.startsWith(".")) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const pathValue = join(dir, entry.name);
|
|
703
|
+
if (entry.isDirectory()) {
|
|
704
|
+
await visit(pathValue);
|
|
705
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
706
|
+
out.push(pathValue);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
for (const relDir of CANONICAL_TEMPLATE_REF_DIRS) {
|
|
712
|
+
await visit(join(rootDir, relDir));
|
|
713
|
+
}
|
|
714
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function relPathFromRoot(rootDir: string, pathValue: string): string {
|
|
718
|
+
return pathValue.startsWith(`${rootDir}/`)
|
|
719
|
+
? pathValue.slice(rootDir.length + 1)
|
|
720
|
+
: pathValue;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function inspectCanonicalTemplateRefs(
|
|
724
|
+
rootDir: string
|
|
725
|
+
): Promise<CanonicalTemplateRefsInspection> {
|
|
726
|
+
const files = await listCanonicalMarkdownFiles(rootDir);
|
|
727
|
+
const issues: CanonicalTemplateIssue[] = [];
|
|
728
|
+
|
|
729
|
+
for (const pathValue of files) {
|
|
730
|
+
const text = await readFile(pathValue, "utf8");
|
|
731
|
+
const refs = new Set(text.match(UNRESOLVED_REFS_TEMPLATE_RE) ?? []);
|
|
732
|
+
if (refs.size === 0) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const relPath = relPathFromRoot(rootDir, pathValue);
|
|
736
|
+
issues.push({
|
|
737
|
+
path: pathValue,
|
|
738
|
+
relPath,
|
|
739
|
+
code: "canonical-source-unresolved-template-ref",
|
|
740
|
+
message: `${relPath} contains unresolved template refs: ${[...refs].join(", ")}.`,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
valid: issues.length === 0,
|
|
746
|
+
issues,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function repairCanonicalTemplateRefs(
|
|
751
|
+
rootDir: string
|
|
752
|
+
): Promise<CanonicalTemplateRefsRepair> {
|
|
753
|
+
const files = await listCanonicalMarkdownFiles(rootDir);
|
|
754
|
+
const repairedPaths: string[] = [];
|
|
755
|
+
const backupPaths: string[] = [];
|
|
756
|
+
|
|
757
|
+
for (const pathValue of files) {
|
|
758
|
+
const before = await readFile(pathValue, "utf8");
|
|
759
|
+
const after = resolveKnownCanonicalRefs(before, rootDir);
|
|
760
|
+
if (after === before) {
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
const relPath = relPathFromRoot(rootDir, pathValue);
|
|
764
|
+
const backupPath = join(
|
|
765
|
+
rootDir,
|
|
766
|
+
".facult",
|
|
767
|
+
"backups",
|
|
768
|
+
"doctor",
|
|
769
|
+
`${relPath.replaceAll("/", "__")}.${timestampForBackup()}`
|
|
770
|
+
);
|
|
771
|
+
await mkdir(dirname(backupPath), { recursive: true });
|
|
772
|
+
await copyFile(pathValue, backupPath);
|
|
773
|
+
await writeFile(pathValue, after, "utf8");
|
|
774
|
+
repairedPaths.push(relPath);
|
|
775
|
+
backupPaths.push(backupPath);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return {
|
|
779
|
+
changed: repairedPaths.length > 0,
|
|
780
|
+
repairedPaths,
|
|
781
|
+
backupPaths,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
600
785
|
async function hasProjectToolRules(
|
|
601
786
|
rootDir: string,
|
|
602
787
|
tool: string
|
|
@@ -778,6 +963,7 @@ export async function buildDoctorReport(opts?: {
|
|
|
778
963
|
writebackReviewDirExists,
|
|
779
964
|
evolutionReviewDirExists,
|
|
780
965
|
canonicalGlobalDocs,
|
|
966
|
+
canonicalTemplateRefs,
|
|
781
967
|
projectSyncPlan,
|
|
782
968
|
] = await Promise.all([
|
|
783
969
|
pathExists(rootDir),
|
|
@@ -788,6 +974,7 @@ export async function buildDoctorReport(opts?: {
|
|
|
788
974
|
pathExists(writebackReviewDir),
|
|
789
975
|
pathExists(evolutionReviewDir),
|
|
790
976
|
inspectCanonicalGlobalDocs(rootDir),
|
|
977
|
+
inspectCanonicalTemplateRefs(rootDir),
|
|
791
978
|
planProjectSyncPolicyRepair({ home, rootDir }),
|
|
792
979
|
]);
|
|
793
980
|
|
|
@@ -836,6 +1023,23 @@ export async function buildDoctorReport(opts?: {
|
|
|
836
1023
|
});
|
|
837
1024
|
}
|
|
838
1025
|
|
|
1026
|
+
for (const issue of canonicalTemplateRefs.issues) {
|
|
1027
|
+
issues.push({
|
|
1028
|
+
severity: "warning",
|
|
1029
|
+
code: issue.code,
|
|
1030
|
+
message: issue.message,
|
|
1031
|
+
fix: "Run fclt doctor --repair to resolve known refs into concrete paths, then review any remaining placeholders.",
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
if (!canonicalTemplateRefs.valid) {
|
|
1035
|
+
actions.push({
|
|
1036
|
+
id: "repair-canonical-template-refs",
|
|
1037
|
+
label: "Repair unresolved canonical template refs",
|
|
1038
|
+
command: "fclt doctor --repair",
|
|
1039
|
+
risk: "canonical_write",
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
839
1043
|
if (generatedOnlyProjectRoot) {
|
|
840
1044
|
issues.push({
|
|
841
1045
|
severity: "error",
|
|
@@ -927,7 +1131,7 @@ export async function buildDoctorReport(opts?: {
|
|
|
927
1131
|
state = "project_generated_only";
|
|
928
1132
|
} else if (!(rootExists && canonicalSourceExists)) {
|
|
929
1133
|
state = "uninitialized";
|
|
930
|
-
} else if (!canonicalGlobalDocs.valid) {
|
|
1134
|
+
} else if (!(canonicalGlobalDocs.valid && canonicalTemplateRefs.valid)) {
|
|
931
1135
|
state = "canonical_source_attention";
|
|
932
1136
|
} else if (!(writebackReviewDirExists && evolutionReviewDirExists)) {
|
|
933
1137
|
state = "partial_global_config";
|
|
@@ -971,6 +1175,13 @@ export async function buildDoctorReport(opts?: {
|
|
|
971
1175
|
canonicalGlobalDocsIssueCodes: canonicalGlobalDocs.issues.map(
|
|
972
1176
|
(issue) => issue.code
|
|
973
1177
|
),
|
|
1178
|
+
canonicalTemplateRefsValid: canonicalTemplateRefs.valid,
|
|
1179
|
+
canonicalTemplateRefsIssueCodes: canonicalTemplateRefs.issues.map(
|
|
1180
|
+
(issue) => issue.code
|
|
1181
|
+
),
|
|
1182
|
+
canonicalTemplateRefsIssuePaths: canonicalTemplateRefs.issues.map(
|
|
1183
|
+
(issue) => issue.relPath
|
|
1184
|
+
),
|
|
974
1185
|
projectSyncRepairNeeded: projectSyncPlan.needed,
|
|
975
1186
|
projectSyncRepairTools,
|
|
976
1187
|
},
|
|
@@ -1029,6 +1240,7 @@ export async function doctorCommand(argv: string[]) {
|
|
|
1029
1240
|
let codexAuthoringConflicts: string[] = [];
|
|
1030
1241
|
let canonicalGlobalDocsRepaired = false;
|
|
1031
1242
|
let canonicalGlobalDocsBackupPath: string | undefined;
|
|
1243
|
+
let canonicalTemplateRefsRepair: CanonicalTemplateRefsRepair | undefined;
|
|
1032
1244
|
let reviewArtifactsRefreshed:
|
|
1033
1245
|
| {
|
|
1034
1246
|
writebackCount: number;
|
|
@@ -1061,6 +1273,7 @@ export async function doctorCommand(argv: string[]) {
|
|
|
1061
1273
|
});
|
|
1062
1274
|
canonicalGlobalDocsRepaired = globalDocsRepair.changed;
|
|
1063
1275
|
canonicalGlobalDocsBackupPath = globalDocsRepair.backupPath;
|
|
1276
|
+
canonicalTemplateRefsRepair = await repairCanonicalTemplateRefs(rootDir);
|
|
1064
1277
|
const { refreshAiReviewArtifacts } = await import("./ai");
|
|
1065
1278
|
reviewArtifactsRefreshed = await refreshAiReviewArtifacts({
|
|
1066
1279
|
homeDir: home,
|
|
@@ -1132,6 +1345,12 @@ export async function doctorCommand(argv: string[]) {
|
|
|
1132
1345
|
`Repaired canonical AGENTS.global.md from the built-in operating model. Backup: ${canonicalGlobalDocsBackupPath}`
|
|
1133
1346
|
);
|
|
1134
1347
|
}
|
|
1348
|
+
if (canonicalTemplateRefsRepair?.changed) {
|
|
1349
|
+
console.log("Resolved canonical template refs in:");
|
|
1350
|
+
for (const repairedPath of canonicalTemplateRefsRepair.repairedPaths) {
|
|
1351
|
+
console.log(`- ${repairedPath}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1135
1354
|
if (reviewArtifactsRefreshed) {
|
|
1136
1355
|
console.log(
|
|
1137
1356
|
`Refreshed AI review artifacts: ${reviewArtifactsRefreshed.writebackCount} writebacks in ${reviewArtifactsRefreshed.writebackReviewDir}, ${reviewArtifactsRefreshed.proposalCount} proposals in ${reviewArtifactsRefreshed.evolutionReviewDir}.`
|
|
@@ -1157,6 +1376,18 @@ export async function doctorCommand(argv: string[]) {
|
|
|
1157
1376
|
return;
|
|
1158
1377
|
}
|
|
1159
1378
|
}
|
|
1379
|
+
const canonicalTemplateRefs = await inspectCanonicalTemplateRefs(rootDir);
|
|
1380
|
+
if (!canonicalTemplateRefs.valid) {
|
|
1381
|
+
for (const issue of canonicalTemplateRefs.issues) {
|
|
1382
|
+
console.log(
|
|
1383
|
+
`${issue.message} Run \`fclt doctor --repair\` to resolve known refs into concrete paths, then review any remaining placeholders.`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (!repair) {
|
|
1387
|
+
process.exitCode = 1;
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1160
1391
|
if (await isGeneratedOnlyProjectRoot({ home, rootDir })) {
|
|
1161
1392
|
console.log(
|
|
1162
1393
|
"Project .ai root contains generated state only. Canonical project source is missing, so managed project sync should be treated as unsafe until source is initialized, restored, or management is detached."
|
package/src/global-docs.ts
CHANGED
|
@@ -228,18 +228,25 @@ async function renderSourceTarget(args: {
|
|
|
228
228
|
tool: string;
|
|
229
229
|
}): Promise<string> {
|
|
230
230
|
const raw = await Bun.file(args.sourcePath).text();
|
|
231
|
+
const builtinRoot = facultBuiltinPackRoot();
|
|
232
|
+
const sourceRoot = args.sourcePath.startsWith(`${builtinRoot}/`)
|
|
233
|
+
? builtinRoot
|
|
234
|
+
: args.rootDir;
|
|
231
235
|
const withSnippets = await renderSnippetText({
|
|
232
236
|
text: raw,
|
|
233
237
|
filePath: args.sourcePath,
|
|
234
|
-
rootDir:
|
|
238
|
+
rootDir: sourceRoot,
|
|
235
239
|
});
|
|
236
240
|
if (withSnippets.errors.length) {
|
|
237
241
|
throw new Error(withSnippets.errors.join("\n"));
|
|
238
242
|
}
|
|
239
243
|
return await renderCanonicalText(withSnippets.text, {
|
|
240
244
|
homeDir: args.homeDir,
|
|
241
|
-
rootDir:
|
|
242
|
-
projectRoot:
|
|
245
|
+
rootDir: sourceRoot,
|
|
246
|
+
projectRoot:
|
|
247
|
+
sourceRoot === args.rootDir
|
|
248
|
+
? (projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined)
|
|
249
|
+
: undefined,
|
|
243
250
|
targetTool: args.tool,
|
|
244
251
|
targetPath: args.targetPath,
|
|
245
252
|
});
|
package/src/remote.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import {
|
|
@@ -130,6 +131,7 @@ interface InstallResult {
|
|
|
130
131
|
sourceTrustLevel: SourceTrustLevel;
|
|
131
132
|
dryRun: boolean;
|
|
132
133
|
changedPaths: string[];
|
|
134
|
+
skippedPaths?: string[];
|
|
133
135
|
}
|
|
134
136
|
|
|
135
137
|
interface UpdateCheckResult {
|
|
@@ -394,17 +396,12 @@ Ship reliable changes quickly while keeping behavior predictable.
|
|
|
394
396
|
type: "snippet",
|
|
395
397
|
title: "Snippet Template",
|
|
396
398
|
description:
|
|
397
|
-
"Reusable snippet block template for
|
|
399
|
+
"Reusable snippet block template for a compact quality checklist.",
|
|
398
400
|
version: "1.0.0",
|
|
399
401
|
tags: ["template", "dx", "snippet"],
|
|
400
402
|
snippet: {
|
|
401
|
-
marker: "team/
|
|
402
|
-
content: `##
|
|
403
|
-
- Prefer explicit, descriptive names over abbreviations.
|
|
404
|
-
- Keep functions focused and side-effect boundaries obvious.
|
|
405
|
-
- Add tests when behavior changes.
|
|
406
|
-
|
|
407
|
-
## Review Checklist
|
|
403
|
+
marker: "team/quality-checklist",
|
|
404
|
+
content: `## Quality Checklist
|
|
408
405
|
- Is behavior correct for edge cases?
|
|
409
406
|
- Are failure modes clear and actionable?
|
|
410
407
|
- Is the change minimal for the goal?
|
|
@@ -1303,17 +1300,64 @@ async function listFilesRecursive(rootDir: string): Promise<string[]> {
|
|
|
1303
1300
|
return out.sort();
|
|
1304
1301
|
}
|
|
1305
1302
|
|
|
1303
|
+
interface BuiltinPackManifest {
|
|
1304
|
+
version: 1;
|
|
1305
|
+
pack: string;
|
|
1306
|
+
updatedAt: string;
|
|
1307
|
+
files: Record<string, { sha256: string }>;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function sha256Text(value: string): string {
|
|
1311
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function builtinPackManifestPath(rootDir: string): string {
|
|
1315
|
+
return join(rootDir, ".facult", "packs", "facult-operating-model.json");
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
async function readBuiltinPackManifest(
|
|
1319
|
+
rootDir: string
|
|
1320
|
+
): Promise<BuiltinPackManifest | null> {
|
|
1321
|
+
const pathValue = builtinPackManifestPath(rootDir);
|
|
1322
|
+
if (!(await pathExists(pathValue))) {
|
|
1323
|
+
return null;
|
|
1324
|
+
}
|
|
1325
|
+
try {
|
|
1326
|
+
const parsed = JSON.parse(await Bun.file(pathValue).text());
|
|
1327
|
+
if (
|
|
1328
|
+
parsed?.version === 1 &&
|
|
1329
|
+
parsed.pack === "facult-operating-model" &&
|
|
1330
|
+
isPlainObject(parsed.files)
|
|
1331
|
+
) {
|
|
1332
|
+
return parsed as BuiltinPackManifest;
|
|
1333
|
+
}
|
|
1334
|
+
} catch {
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function serializeBuiltinPackManifest(manifest: BuiltinPackManifest): string {
|
|
1341
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1306
1344
|
async function scaffoldBuiltinOperatingModelPack(args: {
|
|
1307
1345
|
rootDir: string;
|
|
1308
1346
|
homeDir?: string;
|
|
1309
1347
|
dryRun?: boolean;
|
|
1310
1348
|
force?: boolean;
|
|
1349
|
+
update?: boolean;
|
|
1311
1350
|
installedAs?: string;
|
|
1312
1351
|
}): Promise<InstallResult> {
|
|
1313
1352
|
const rootDir = resolve(args.rootDir);
|
|
1314
1353
|
const packRoot = facultBuiltinPackRoot("facult-operating-model");
|
|
1315
1354
|
const files = await listFilesRecursive(packRoot);
|
|
1316
1355
|
const changedPaths: string[] = [];
|
|
1356
|
+
const skippedPaths: string[] = [];
|
|
1357
|
+
const existingManifest = await readBuiltinPackManifest(rootDir);
|
|
1358
|
+
const manifestFiles: BuiltinPackManifest["files"] = {
|
|
1359
|
+
...(existingManifest?.files ?? {}),
|
|
1360
|
+
};
|
|
1317
1361
|
|
|
1318
1362
|
for (const sourcePath of files) {
|
|
1319
1363
|
const relPath = relative(packRoot, sourcePath);
|
|
@@ -1321,23 +1365,90 @@ async function scaffoldBuiltinOperatingModelPack(args: {
|
|
|
1321
1365
|
continue;
|
|
1322
1366
|
}
|
|
1323
1367
|
const targetPath = join(rootDir, relPath);
|
|
1368
|
+
const sourceText = await Bun.file(sourcePath).text();
|
|
1369
|
+
const sourceHash = sha256Text(sourceText);
|
|
1324
1370
|
const exists = await pathExists(targetPath);
|
|
1325
|
-
|
|
1371
|
+
let shouldWrite = !exists || Boolean(args.force);
|
|
1372
|
+
|
|
1373
|
+
if (exists && !shouldWrite) {
|
|
1374
|
+
const targetText = await Bun.file(targetPath).text();
|
|
1375
|
+
const targetHash = sha256Text(targetText);
|
|
1376
|
+
if (targetHash === sourceHash) {
|
|
1377
|
+
manifestFiles[relPath] = { sha256: sourceHash };
|
|
1378
|
+
} else if (
|
|
1379
|
+
args.update &&
|
|
1380
|
+
existingManifest?.files[relPath]?.sha256 === targetHash
|
|
1381
|
+
) {
|
|
1382
|
+
shouldWrite = true;
|
|
1383
|
+
} else if (args.update) {
|
|
1384
|
+
skippedPaths.push(targetPath);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (!shouldWrite) {
|
|
1326
1389
|
continue;
|
|
1327
1390
|
}
|
|
1328
1391
|
changedPaths.push(targetPath);
|
|
1392
|
+
manifestFiles[relPath] = { sha256: sourceHash };
|
|
1329
1393
|
if (!args.dryRun) {
|
|
1330
1394
|
await mkdir(dirname(targetPath), { recursive: true });
|
|
1331
|
-
await Bun.write(targetPath,
|
|
1395
|
+
await Bun.write(targetPath, sourceText);
|
|
1332
1396
|
}
|
|
1333
1397
|
}
|
|
1334
1398
|
|
|
1335
1399
|
const configPath = join(rootDir, "config.toml");
|
|
1336
|
-
|
|
1400
|
+
const configRelPath = "config.toml";
|
|
1401
|
+
const configText = "version = 1\n";
|
|
1402
|
+
const configHash = sha256Text(configText);
|
|
1403
|
+
const configExists = await pathExists(configPath);
|
|
1404
|
+
let shouldWriteConfig = !configExists || Boolean(args.force);
|
|
1405
|
+
if (configExists && !shouldWriteConfig) {
|
|
1406
|
+
const targetText = await Bun.file(configPath).text();
|
|
1407
|
+
const targetHash = sha256Text(targetText);
|
|
1408
|
+
if (targetHash === configHash) {
|
|
1409
|
+
manifestFiles[configRelPath] = { sha256: configHash };
|
|
1410
|
+
} else if (
|
|
1411
|
+
args.update &&
|
|
1412
|
+
existingManifest?.files[configRelPath]?.sha256 === targetHash
|
|
1413
|
+
) {
|
|
1414
|
+
shouldWriteConfig = true;
|
|
1415
|
+
} else if (args.update) {
|
|
1416
|
+
skippedPaths.push(configPath);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (shouldWriteConfig) {
|
|
1337
1420
|
changedPaths.push(configPath);
|
|
1421
|
+
manifestFiles[configRelPath] = { sha256: configHash };
|
|
1338
1422
|
if (!args.dryRun) {
|
|
1339
1423
|
await mkdir(dirname(configPath), { recursive: true });
|
|
1340
|
-
await Bun.write(configPath,
|
|
1424
|
+
await Bun.write(configPath, configText);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const manifestPath = builtinPackManifestPath(rootDir);
|
|
1429
|
+
const sortedManifestFiles = Object.fromEntries(
|
|
1430
|
+
Object.entries(manifestFiles).sort(([a], [b]) => a.localeCompare(b))
|
|
1431
|
+
);
|
|
1432
|
+
const stableManifest = serializeBuiltinPackManifest({
|
|
1433
|
+
version: 1,
|
|
1434
|
+
pack: "facult-operating-model",
|
|
1435
|
+
updatedAt: existingManifest?.updatedAt ?? "",
|
|
1436
|
+
files: sortedManifestFiles,
|
|
1437
|
+
});
|
|
1438
|
+
const existingManifestText = (await pathExists(manifestPath))
|
|
1439
|
+
? await Bun.file(manifestPath).text()
|
|
1440
|
+
: null;
|
|
1441
|
+
if (existingManifestText !== stableManifest) {
|
|
1442
|
+
const nextManifest = serializeBuiltinPackManifest({
|
|
1443
|
+
version: 1,
|
|
1444
|
+
pack: "facult-operating-model",
|
|
1445
|
+
updatedAt: new Date().toISOString(),
|
|
1446
|
+
files: sortedManifestFiles,
|
|
1447
|
+
});
|
|
1448
|
+
changedPaths.push(manifestPath);
|
|
1449
|
+
if (!args.dryRun) {
|
|
1450
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
1451
|
+
await Bun.write(manifestPath, nextManifest);
|
|
1341
1452
|
}
|
|
1342
1453
|
}
|
|
1343
1454
|
|
|
@@ -1357,6 +1468,7 @@ async function scaffoldBuiltinOperatingModelPack(args: {
|
|
|
1357
1468
|
sourceTrustLevel: "trusted",
|
|
1358
1469
|
dryRun: Boolean(args.dryRun),
|
|
1359
1470
|
changedPaths: uniqueSorted(changedPaths),
|
|
1471
|
+
skippedPaths: uniqueSorted(skippedPaths),
|
|
1360
1472
|
};
|
|
1361
1473
|
}
|
|
1362
1474
|
|
|
@@ -1365,6 +1477,7 @@ async function scaffoldBuiltinProjectAiPack(args: {
|
|
|
1365
1477
|
homeDir?: string;
|
|
1366
1478
|
dryRun?: boolean;
|
|
1367
1479
|
force?: boolean;
|
|
1480
|
+
update?: boolean;
|
|
1368
1481
|
}): Promise<InstallResult> {
|
|
1369
1482
|
const cwd = resolve(args.cwd ?? process.cwd());
|
|
1370
1483
|
return await scaffoldBuiltinOperatingModelPack({
|
|
@@ -1372,6 +1485,7 @@ async function scaffoldBuiltinProjectAiPack(args: {
|
|
|
1372
1485
|
homeDir: args.homeDir,
|
|
1373
1486
|
dryRun: args.dryRun,
|
|
1374
1487
|
force: args.force,
|
|
1488
|
+
update: args.update,
|
|
1375
1489
|
installedAs: "project-ai",
|
|
1376
1490
|
});
|
|
1377
1491
|
}
|
|
@@ -2820,9 +2934,11 @@ function printTemplatesHelp() {
|
|
|
2820
2934
|
),
|
|
2821
2935
|
renderCode("fclt templates init agents [--force] [--dry-run]"),
|
|
2822
2936
|
renderCode(
|
|
2823
|
-
"fclt templates init operating-model [--global|--project|--root PATH] [--force] [--dry-run]"
|
|
2937
|
+
"fclt templates init operating-model [--global|--project|--root PATH] [--update] [--force] [--dry-run]"
|
|
2938
|
+
),
|
|
2939
|
+
renderCode(
|
|
2940
|
+
"fclt templates init project-ai [--update] [--force] [--dry-run]"
|
|
2824
2941
|
),
|
|
2825
|
-
renderCode("fclt templates init project-ai [--force] [--dry-run]"),
|
|
2826
2942
|
renderCode(
|
|
2827
2943
|
"fclt templates init automation <template-id> [--scope global|project|wide] [--name <name>] [--project-root <path>] [--cwds <path1,path2>] [--rrule <RRULE>] [--status PAUSED|ACTIVE] [--yes] [--dry-run]"
|
|
2828
2944
|
),
|
|
@@ -3327,6 +3443,7 @@ export async function templatesCommand(
|
|
|
3327
3443
|
}
|
|
3328
3444
|
const dryRun = args.includes("--dry-run");
|
|
3329
3445
|
const force = args.includes("--force");
|
|
3446
|
+
const update = args.includes("--update");
|
|
3330
3447
|
const json = args.includes("--json");
|
|
3331
3448
|
const parsedArgs = parseTemplateInitArgs(args);
|
|
3332
3449
|
const positional = parsedArgs.positional;
|
|
@@ -3338,6 +3455,7 @@ export async function templatesCommand(
|
|
|
3338
3455
|
homeDir: ctx.homeDir,
|
|
3339
3456
|
dryRun,
|
|
3340
3457
|
force,
|
|
3458
|
+
update,
|
|
3341
3459
|
});
|
|
3342
3460
|
if (json) {
|
|
3343
3461
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3353,6 +3471,14 @@ export async function templatesCommand(
|
|
|
3353
3471
|
title: "Changed Paths",
|
|
3354
3472
|
lines: renderBullets(result.changedPaths),
|
|
3355
3473
|
},
|
|
3474
|
+
...(result.skippedPaths?.length
|
|
3475
|
+
? [
|
|
3476
|
+
{
|
|
3477
|
+
title: "Skipped Local Edits",
|
|
3478
|
+
lines: renderBullets(result.skippedPaths),
|
|
3479
|
+
},
|
|
3480
|
+
]
|
|
3481
|
+
: []),
|
|
3356
3482
|
],
|
|
3357
3483
|
})
|
|
3358
3484
|
);
|
|
@@ -3383,6 +3509,7 @@ export async function templatesCommand(
|
|
|
3383
3509
|
homeDir: ctx.homeDir,
|
|
3384
3510
|
dryRun,
|
|
3385
3511
|
force,
|
|
3512
|
+
update,
|
|
3386
3513
|
});
|
|
3387
3514
|
if (json) {
|
|
3388
3515
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -3398,6 +3525,14 @@ export async function templatesCommand(
|
|
|
3398
3525
|
title: "Changed Paths",
|
|
3399
3526
|
lines: renderBullets(result.changedPaths),
|
|
3400
3527
|
},
|
|
3528
|
+
...(result.skippedPaths?.length
|
|
3529
|
+
? [
|
|
3530
|
+
{
|
|
3531
|
+
title: "Skipped Local Edits",
|
|
3532
|
+
lines: renderBullets(result.skippedPaths),
|
|
3533
|
+
},
|
|
3534
|
+
]
|
|
3535
|
+
: []),
|
|
3401
3536
|
],
|
|
3402
3537
|
})
|
|
3403
3538
|
);
|
package/src/snippets-cli.ts
CHANGED
|
@@ -20,8 +20,8 @@ Usage:
|
|
|
20
20
|
fclt snippets sync [--dry-run] [file...]
|
|
21
21
|
|
|
22
22
|
Notes:
|
|
23
|
-
- <name> is the snippet marker name (e.g.
|
|
24
|
-
- Unscoped names (e.g.
|
|
23
|
+
- <name> is the snippet marker name (e.g. qualitychecklist, global/qualitychecklist, myproject/context)
|
|
24
|
+
- Unscoped names (e.g. qualitychecklist) resolve to project snippet first (if in a git repo), then global.
|
|
25
25
|
`);
|
|
26
26
|
}
|
|
27
27
|
|