facult 2.12.0 → 2.13.1

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/src/doctor.ts CHANGED
@@ -25,8 +25,10 @@ import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
25
25
  import { loadManagedState } from "./manage";
26
26
  import { extractServersObject } from "./mcp-config";
27
27
  import {
28
+ facultAiEvolutionReviewDir,
28
29
  facultAiGraphPath,
29
30
  facultAiIndexPath,
31
+ facultAiWritebackReviewDir,
30
32
  facultConfigPath,
31
33
  facultRootDir,
32
34
  facultStateDir,
@@ -38,9 +40,72 @@ import {
38
40
  loadConfiguredProjectSyncTools,
39
41
  writeProjectSyncPolicy,
40
42
  } from "./project-sync";
43
+ import { packageVersion } from "./status";
41
44
 
42
45
  const TOML_FILE_SUFFIX_RE = /\.toml$/;
43
46
 
47
+ type DoctorHealthState =
48
+ | "healthy"
49
+ | "uninitialized"
50
+ | "partial_global_config"
51
+ | "project_generated_only"
52
+ | "project_policy_attention"
53
+ | "stale_or_missing_generated_state"
54
+ | "legacy_state_attention";
55
+
56
+ interface DoctorIssue {
57
+ severity: "info" | "warning" | "error";
58
+ code: string;
59
+ message: string;
60
+ fix?: string;
61
+ }
62
+
63
+ interface DoctorAction {
64
+ id: string;
65
+ label: string;
66
+ command: string;
67
+ risk:
68
+ | "read_only"
69
+ | "generated_state_write"
70
+ | "canonical_write"
71
+ | "tool_home_write";
72
+ }
73
+
74
+ export interface DoctorReport {
75
+ version: 1;
76
+ packageVersion: string;
77
+ cwd: string;
78
+ homeDir: string;
79
+ rootDir: string;
80
+ projectRoot: string | null;
81
+ health: {
82
+ state: DoctorHealthState;
83
+ ok: boolean;
84
+ };
85
+ paths: {
86
+ configPath: string;
87
+ generatedIndex: string;
88
+ generatedGraph: string;
89
+ stateDir: string;
90
+ legacyIndex: string;
91
+ writebackReviewDir: string;
92
+ evolutionReviewDir: string;
93
+ };
94
+ checks: {
95
+ rootExists: boolean;
96
+ canonicalSourceExists: boolean;
97
+ generatedOnlyProjectRoot: boolean;
98
+ generatedIndexSource: "generated" | "legacy" | "rebuilt" | "missing";
99
+ generatedGraphExists: boolean;
100
+ writebackReviewDirExists: boolean;
101
+ evolutionReviewDirExists: boolean;
102
+ projectSyncRepairNeeded: boolean;
103
+ projectSyncRepairTools: string[];
104
+ };
105
+ issues: DoctorIssue[];
106
+ actions: DoctorAction[];
107
+ }
108
+
44
109
  function legacyDefaultRoot(home: string): string {
45
110
  return join(home, "agents", ".facult");
46
111
  }
@@ -573,29 +638,266 @@ function printHelp() {
573
638
  console.log(`fclt doctor — inspect and repair local fclt state
574
639
 
575
640
  Usage:
576
- fclt doctor [--repair] [--root <path> | --global | --project]
641
+ fclt doctor [--json] [--repair] [--root <path> | --global | --project]
577
642
 
578
643
  Options:
644
+ --json Print read-only setup health, issues, and recommended actions
579
645
  --repair Reconcile legacy Facult state, canonical root config, AI index/graph, and autosync service config when needed
580
646
  `);
581
647
  }
582
648
 
649
+ export async function buildDoctorReport(opts?: {
650
+ cwd?: string;
651
+ homeDir?: string;
652
+ rootArg?: string;
653
+ scope?: "merged" | "global" | "project";
654
+ }): Promise<DoctorReport> {
655
+ const home = opts?.homeDir ?? process.env.HOME?.trim() ?? homedir();
656
+ const cwd = opts?.cwd ?? process.cwd();
657
+ const rootDir =
658
+ opts?.rootArg || opts?.scope === "project"
659
+ ? resolveCliContextRoot({
660
+ rootArg: opts?.rootArg,
661
+ scope: opts?.scope ?? "merged",
662
+ cwd,
663
+ homeDir: home,
664
+ })
665
+ : facultRootDir(home);
666
+ const projectRoot = projectRootFromAiRoot(rootDir, home);
667
+ const generated = facultAiIndexPath(home, rootDir);
668
+ const generatedGraph = facultAiGraphPath(home, rootDir);
669
+ const legacy = legacyAiIndexPath(rootDir);
670
+ const writebackReviewDir = facultAiWritebackReviewDir(home, rootDir);
671
+ const evolutionReviewDir = facultAiEvolutionReviewDir(home, rootDir);
672
+
673
+ const [
674
+ rootExists,
675
+ canonicalSourceExists,
676
+ generatedOnlyProjectRoot,
677
+ result,
678
+ generatedGraphExists,
679
+ writebackReviewDirExists,
680
+ evolutionReviewDirExists,
681
+ projectSyncPlan,
682
+ ] = await Promise.all([
683
+ pathExists(rootDir),
684
+ hasCanonicalSource(rootDir),
685
+ isGeneratedOnlyProjectRoot({ home, rootDir }),
686
+ ensureAiIndexPath({ homeDir: home, rootDir, repair: false }),
687
+ pathExists(generatedGraph),
688
+ pathExists(writebackReviewDir),
689
+ pathExists(evolutionReviewDir),
690
+ planProjectSyncPolicyRepair({ home, rootDir }),
691
+ ]);
692
+
693
+ const projectSyncRepairTools = Object.keys(projectSyncPlan.toolPolicies).sort(
694
+ (a, b) => a.localeCompare(b)
695
+ );
696
+ const issues: DoctorIssue[] = [];
697
+ const actions: DoctorAction[] = [];
698
+
699
+ if (!rootExists) {
700
+ issues.push({
701
+ severity: "error",
702
+ code: "missing-root",
703
+ message: "Canonical .ai root does not exist.",
704
+ fix: "Initialize the global operating model or project .ai root.",
705
+ });
706
+ actions.push({
707
+ id: "init-global-operating-model",
708
+ label: "Initialize global operating model",
709
+ command: "fclt templates init operating-model --global",
710
+ risk: "canonical_write",
711
+ });
712
+ }
713
+
714
+ if (!canonicalSourceExists) {
715
+ issues.push({
716
+ severity: projectRoot ? "error" : "warning",
717
+ code: "missing-canonical-source",
718
+ message:
719
+ "No canonical capability source was found in the selected .ai root.",
720
+ fix: projectRoot
721
+ ? "Run fclt templates init project-ai from the repo or restore canonical project assets."
722
+ : "Run fclt templates init operating-model --global.",
723
+ });
724
+ }
725
+
726
+ if (generatedOnlyProjectRoot) {
727
+ issues.push({
728
+ severity: "error",
729
+ code: "project-generated-only",
730
+ message:
731
+ "Project .ai contains generated state but no canonical project source.",
732
+ fix: "Initialize, restore, or detach canonical project capability before managed project sync.",
733
+ });
734
+ actions.push({
735
+ id: "init-project-ai",
736
+ label: "Initialize project AI root",
737
+ command: "fclt templates init project-ai",
738
+ risk: "canonical_write",
739
+ });
740
+ }
741
+
742
+ if (result.source === "missing") {
743
+ issues.push({
744
+ severity: "warning",
745
+ code: "missing-generated-index",
746
+ message: "Generated AI index is missing.",
747
+ fix: "Run fclt index or fclt doctor --repair.",
748
+ });
749
+ actions.push({
750
+ id: "rebuild-index",
751
+ label: "Rebuild generated index",
752
+ command: "fclt index",
753
+ risk: "generated_state_write",
754
+ });
755
+ } else if (result.source === "legacy") {
756
+ issues.push({
757
+ severity: "warning",
758
+ code: "legacy-generated-index",
759
+ message: "Generated AI index is still in a legacy location.",
760
+ fix: "Run fclt doctor --repair.",
761
+ });
762
+ actions.push({
763
+ id: "repair-generated-state",
764
+ label: "Repair generated state",
765
+ command: "fclt doctor --repair",
766
+ risk: "generated_state_write",
767
+ });
768
+ }
769
+
770
+ if (!generatedGraphExists) {
771
+ issues.push({
772
+ severity: "info",
773
+ code: "missing-generated-graph",
774
+ message: "Generated capability graph is missing.",
775
+ fix: "Run fclt index or fclt doctor --repair.",
776
+ });
777
+ }
778
+
779
+ if (!writebackReviewDirExists) {
780
+ issues.push({
781
+ severity: "info",
782
+ code: "missing-writeback-review-dir",
783
+ message: "Global writeback review directory is not present yet.",
784
+ fix: "It will be created when writebacks are recorded or the operating model is initialized.",
785
+ });
786
+ }
787
+
788
+ if (!evolutionReviewDirExists) {
789
+ issues.push({
790
+ severity: "info",
791
+ code: "missing-evolution-review-dir",
792
+ message: "Global evolution review directory is not present yet.",
793
+ fix: "It will be created when proposals are drafted or the operating model is initialized.",
794
+ });
795
+ }
796
+
797
+ if (projectSyncPlan.needed) {
798
+ issues.push({
799
+ severity: "warning",
800
+ code: "implicit-project-sync-policy",
801
+ message: `Project sync is still implicit for managed tools: ${projectSyncRepairTools.join(", ")}.`,
802
+ fix: "Run fclt doctor --repair to materialize explicit project sync policy.",
803
+ });
804
+ actions.push({
805
+ id: "materialize-project-sync-policy",
806
+ label: "Materialize project sync policy",
807
+ command: "fclt doctor --repair",
808
+ risk: "canonical_write",
809
+ });
810
+ }
811
+
812
+ let state: DoctorHealthState = "healthy";
813
+ if (generatedOnlyProjectRoot) {
814
+ state = "project_generated_only";
815
+ } else if (!(rootExists && canonicalSourceExists)) {
816
+ state = "uninitialized";
817
+ } else if (!(writebackReviewDirExists && evolutionReviewDirExists)) {
818
+ state = "partial_global_config";
819
+ } else if (projectSyncPlan.needed) {
820
+ state = "project_policy_attention";
821
+ } else if (result.source === "missing") {
822
+ state = "stale_or_missing_generated_state";
823
+ } else if (result.source === "legacy") {
824
+ state = "legacy_state_attention";
825
+ }
826
+
827
+ return {
828
+ version: 1,
829
+ packageVersion: await packageVersion(),
830
+ cwd,
831
+ homeDir: home,
832
+ rootDir,
833
+ projectRoot,
834
+ health: {
835
+ state,
836
+ ok: state === "healthy" || state === "partial_global_config",
837
+ },
838
+ paths: {
839
+ configPath: facultConfigPath(home),
840
+ generatedIndex: generated,
841
+ generatedGraph,
842
+ stateDir: facultStateDir(home, rootDir),
843
+ legacyIndex: legacy,
844
+ writebackReviewDir,
845
+ evolutionReviewDir,
846
+ },
847
+ checks: {
848
+ rootExists,
849
+ canonicalSourceExists,
850
+ generatedOnlyProjectRoot,
851
+ generatedIndexSource: result.source,
852
+ generatedGraphExists,
853
+ writebackReviewDirExists,
854
+ evolutionReviewDirExists,
855
+ projectSyncRepairNeeded: projectSyncPlan.needed,
856
+ projectSyncRepairTools,
857
+ },
858
+ issues,
859
+ actions,
860
+ };
861
+ }
862
+
583
863
  export async function doctorCommand(argv: string[]) {
584
864
  if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
585
865
  printHelp();
586
866
  return;
587
867
  }
588
868
 
869
+ const json = argv.includes("--json");
589
870
  const repair = argv.includes("--repair");
590
871
  const home = process.env.HOME?.trim() || homedir();
591
872
 
592
873
  try {
593
- const parsed = parseCliContextArgs(argv);
874
+ const parsed = parseCliContextArgs(
875
+ argv.filter((arg) => arg !== "--json" && arg !== "--repair")
876
+ );
877
+ if (json) {
878
+ if (repair) {
879
+ console.error(
880
+ "doctor --json is read-only; run doctor --repair without --json to mutate state."
881
+ );
882
+ process.exitCode = 1;
883
+ return;
884
+ }
885
+ const report = await buildDoctorReport({
886
+ cwd: process.cwd(),
887
+ homeDir: home,
888
+ rootArg: parsed.rootArg,
889
+ scope: parsed.scope,
890
+ });
891
+ console.log(JSON.stringify(report, null, 2));
892
+ return;
893
+ }
894
+
895
+ const textParsed = parseCliContextArgs(argv);
594
896
  const rootDir =
595
- parsed.rootArg || parsed.scope === "project"
897
+ textParsed.rootArg || textParsed.scope === "project"
596
898
  ? resolveCliContextRoot({
597
- rootArg: parsed.rootArg,
598
- scope: parsed.scope,
899
+ rootArg: textParsed.rootArg,
900
+ scope: textParsed.scope,
599
901
  cwd: process.cwd(),
600
902
  homeDir: home,
601
903
  })
package/src/index.ts CHANGED
@@ -116,6 +116,7 @@ function printHelp() {
116
116
  "status",
117
117
  "Show active roots, managed tools, graph/index, and sync risks",
118
118
  ],
119
+ ["paths", "Show canonical, generated, runtime, and review paths"],
119
120
  [
120
121
  "audit",
121
122
  "Run security audits with interactive or scripted flows",
@@ -1286,6 +1287,11 @@ async function main(argv: string[]) {
1286
1287
  case "status":
1287
1288
  await import("./status").then(({ statusCommand }) => statusCommand(rest));
1288
1289
  return;
1290
+ case "paths":
1291
+ await import("./paths-command").then(({ pathsCommand }) =>
1292
+ pathsCommand(rest)
1293
+ );
1294
+ return;
1289
1295
  case "audit":
1290
1296
  await import("./audit").then(({ auditCommand }) => auditCommand(rest));
1291
1297
  return;
@@ -0,0 +1,223 @@
1
+ import { homedir } from "node:os";
2
+ import {
3
+ type CapabilityScopeMode,
4
+ parseCliContextArgs,
5
+ resolveCliContextRoot,
6
+ } from "./cli-context";
7
+ import { renderCode, renderKeyValue, renderPage } from "./cli-ui";
8
+ import { loadManagedState } from "./manage";
9
+ import {
10
+ facultAiDraftDir,
11
+ facultAiEvolutionReviewDir,
12
+ facultAiGraphPath,
13
+ facultAiIndexPath,
14
+ facultAiJournalPath,
15
+ facultAiProposalDir,
16
+ facultAiRuntimeScopeDir,
17
+ facultAiStateDir,
18
+ facultAiWritebackQueuePath,
19
+ facultAiWritebackReviewDir,
20
+ facultConfigPath,
21
+ facultGeneratedStateDir,
22
+ facultInstallStatePath,
23
+ facultLocalCacheRoot,
24
+ facultLocalStateRoot,
25
+ facultMachineStateDir,
26
+ facultRootDir,
27
+ facultRuntimeCacheDir,
28
+ machineStateProjectKey,
29
+ preferredGlobalAiRoot,
30
+ projectRootFromAiRoot,
31
+ } from "./paths";
32
+
33
+ export interface FacultPaths {
34
+ version: 1;
35
+ cwd: string;
36
+ homeDir: string;
37
+ scope: CapabilityScopeMode;
38
+ globalRoot: string;
39
+ contextRoot: string;
40
+ projectRoot: string | null;
41
+ projectKey: string | null;
42
+ canonical: {
43
+ globalRoot: string;
44
+ contextRoot: string;
45
+ configPath: string;
46
+ };
47
+ generated: {
48
+ stateDir: string;
49
+ aiStateDir: string;
50
+ indexPath: string;
51
+ graphPath: string;
52
+ };
53
+ runtime: {
54
+ localStateRoot: string;
55
+ localCacheRoot: string;
56
+ installStatePath: string;
57
+ runtimeCacheDir: string;
58
+ machineStateDir: string;
59
+ aiRuntimeScopeDir: string;
60
+ journalPath: string;
61
+ writebackQueuePath: string;
62
+ proposalDir: string;
63
+ draftDir: string;
64
+ };
65
+ review: {
66
+ writebackDir: string;
67
+ evolutionDir: string;
68
+ };
69
+ managedTools: string[];
70
+ }
71
+
72
+ export async function buildPaths(opts?: {
73
+ cwd?: string;
74
+ homeDir?: string;
75
+ rootArg?: string;
76
+ scope?: CapabilityScopeMode;
77
+ }): Promise<FacultPaths> {
78
+ const homeDir = opts?.homeDir ?? process.env.HOME?.trim() ?? homedir();
79
+ const cwd = opts?.cwd ?? process.cwd();
80
+ const scope = opts?.scope ?? "merged";
81
+ const globalRoot = facultRootDir(homeDir);
82
+ const contextRoot = resolveCliContextRoot({
83
+ homeDir,
84
+ cwd,
85
+ rootArg: opts?.rootArg,
86
+ scope,
87
+ });
88
+ const projectRoot = projectRootFromAiRoot(contextRoot, homeDir);
89
+ const managed = await loadManagedState(homeDir, contextRoot);
90
+
91
+ return {
92
+ version: 1,
93
+ cwd,
94
+ homeDir,
95
+ scope,
96
+ globalRoot,
97
+ contextRoot,
98
+ projectRoot,
99
+ projectKey: projectRoot
100
+ ? machineStateProjectKey(contextRoot, homeDir)
101
+ : null,
102
+ canonical: {
103
+ globalRoot: preferredGlobalAiRoot(homeDir),
104
+ contextRoot,
105
+ configPath: facultConfigPath(homeDir),
106
+ },
107
+ generated: {
108
+ stateDir: facultGeneratedStateDir({
109
+ home: homeDir,
110
+ rootDir: contextRoot,
111
+ }),
112
+ aiStateDir: facultAiStateDir(homeDir, contextRoot),
113
+ indexPath: facultAiIndexPath(homeDir, contextRoot),
114
+ graphPath: facultAiGraphPath(homeDir, contextRoot),
115
+ },
116
+ runtime: {
117
+ localStateRoot: facultLocalStateRoot(homeDir),
118
+ localCacheRoot: facultLocalCacheRoot(homeDir),
119
+ installStatePath: facultInstallStatePath(homeDir),
120
+ runtimeCacheDir: facultRuntimeCacheDir(homeDir),
121
+ machineStateDir: facultMachineStateDir(homeDir, contextRoot),
122
+ aiRuntimeScopeDir: facultAiRuntimeScopeDir(homeDir, contextRoot),
123
+ journalPath: facultAiJournalPath(homeDir, contextRoot),
124
+ writebackQueuePath: facultAiWritebackQueuePath(homeDir, contextRoot),
125
+ proposalDir: facultAiProposalDir(homeDir, contextRoot),
126
+ draftDir: facultAiDraftDir(homeDir, contextRoot),
127
+ },
128
+ review: {
129
+ writebackDir: facultAiWritebackReviewDir(homeDir, contextRoot),
130
+ evolutionDir: facultAiEvolutionReviewDir(homeDir, contextRoot),
131
+ },
132
+ managedTools: Object.keys(managed.tools).sort(),
133
+ };
134
+ }
135
+
136
+ function printHelp() {
137
+ console.log(
138
+ renderPage({
139
+ title: "fclt paths",
140
+ subtitle: "Show canonical, generated, runtime, and review paths.",
141
+ sections: [
142
+ {
143
+ title: "Usage",
144
+ lines: [
145
+ renderCode("fclt paths"),
146
+ renderCode("fclt paths --json"),
147
+ renderCode("fclt paths --project --json"),
148
+ ],
149
+ },
150
+ ],
151
+ })
152
+ );
153
+ }
154
+
155
+ function printPaths(paths: FacultPaths) {
156
+ console.log(
157
+ renderPage({
158
+ title: "fclt paths",
159
+ subtitle: paths.contextRoot,
160
+ sections: [
161
+ {
162
+ title: "Canonical",
163
+ lines: renderKeyValue([
164
+ ["global root", paths.globalRoot],
165
+ ["context root", paths.contextRoot],
166
+ ["project root", paths.projectRoot ?? "(none)"],
167
+ ["config", paths.canonical.configPath],
168
+ ]),
169
+ },
170
+ {
171
+ title: "Generated",
172
+ lines: renderKeyValue([
173
+ ["state", paths.generated.stateDir],
174
+ ["index", paths.generated.indexPath],
175
+ ["graph", paths.generated.graphPath],
176
+ ]),
177
+ },
178
+ {
179
+ title: "Runtime",
180
+ lines: renderKeyValue([
181
+ ["machine state", paths.runtime.machineStateDir],
182
+ ["writeback queue", paths.runtime.writebackQueuePath],
183
+ ["proposal dir", paths.runtime.proposalDir],
184
+ ["draft dir", paths.runtime.draftDir],
185
+ ]),
186
+ },
187
+ {
188
+ title: "Review",
189
+ lines: renderKeyValue([
190
+ ["writebacks", paths.review.writebackDir],
191
+ ["evolution", paths.review.evolutionDir],
192
+ ]),
193
+ },
194
+ ],
195
+ })
196
+ );
197
+ }
198
+
199
+ export async function pathsCommand(argv: string[]) {
200
+ if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
201
+ printHelp();
202
+ return;
203
+ }
204
+
205
+ const json = argv.includes("--json");
206
+ try {
207
+ const parsed = parseCliContextArgs(argv.filter((arg) => arg !== "--json"));
208
+ const paths = await buildPaths({
209
+ cwd: process.cwd(),
210
+ homeDir: process.env.HOME?.trim() || homedir(),
211
+ rootArg: parsed.rootArg,
212
+ scope: parsed.scope,
213
+ });
214
+ if (json) {
215
+ console.log(JSON.stringify(paths, null, 2));
216
+ return;
217
+ }
218
+ printPaths(paths);
219
+ } catch (err) {
220
+ console.error(err instanceof Error ? err.message : String(err));
221
+ process.exitCode = 1;
222
+ }
223
+ }