facult 1.1.0 → 1.2.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.
@@ -17,6 +17,7 @@ function ensureIndexStructure(index: FacultIndex): FacultIndex {
17
17
  mcp: index.mcp ?? { servers: {} },
18
18
  agents: index.agents ?? {},
19
19
  snippets: index.snippets ?? {},
20
+ instructions: index.instructions ?? {},
20
21
  };
21
22
  }
22
23
 
package/src/autosync.ts CHANGED
@@ -2,8 +2,9 @@ import { watch as fsWatch } from "node:fs";
2
2
  import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { homedir, hostname } from "node:os";
4
4
  import { basename, dirname, join } from "node:path";
5
+ import { parseCliContextArgs, resolveCliContextRoot } from "./cli-context";
5
6
  import { syncManagedTools } from "./manage";
6
- import { facultRootDir, facultStateDir } from "./paths";
7
+ import { facultRootDir, facultStateDir, projectRootFromAiRoot } from "./paths";
7
8
 
8
9
  const AUTOSYNC_VERSION = 1 as const;
9
10
  const DEFAULT_DEBOUNCE_MS = 1500;
@@ -109,8 +110,30 @@ function autosyncLogsDir(home: string): string {
109
110
  return join(autosyncDir(home), "logs");
110
111
  }
111
112
 
112
- function autosyncServiceName(tool?: string): string {
113
- return tool?.trim() ? tool.trim() : "all";
113
+ function serviceSuffix(
114
+ rootDir: string | undefined,
115
+ home: string
116
+ ): string | null {
117
+ if (!rootDir) {
118
+ return null;
119
+ }
120
+ const projectRoot = projectRootFromAiRoot(rootDir, home);
121
+ if (!projectRoot) {
122
+ return null;
123
+ }
124
+ const base = basename(projectRoot).trim().toLowerCase();
125
+ const slug = base.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
126
+ return slug || "project";
127
+ }
128
+
129
+ function autosyncServiceName(
130
+ tool?: string,
131
+ rootDir?: string,
132
+ home: string = homedir()
133
+ ): string {
134
+ const base = tool?.trim() ? tool.trim() : "all";
135
+ const suffix = serviceSuffix(rootDir, home);
136
+ return suffix ? `${base}-${suffix}` : base;
114
137
  }
115
138
 
116
139
  function autosyncLabel(serviceName: string): string {
@@ -318,6 +341,7 @@ function defaultAutosyncConfig(args: {
318
341
  serviceName: string;
319
342
  tool?: string;
320
343
  homeDir: string;
344
+ rootDir?: string;
321
345
  remote?: string;
322
346
  branch?: string;
323
347
  intervalMinutes?: number;
@@ -328,7 +352,7 @@ function defaultAutosyncConfig(args: {
328
352
  version: AUTOSYNC_VERSION,
329
353
  name: args.serviceName,
330
354
  tool: args.tool,
331
- rootDir: facultRootDir(args.homeDir),
355
+ rootDir: args.rootDir ?? facultRootDir(args.homeDir),
332
356
  debounceMs: DEFAULT_DEBOUNCE_MS,
333
357
  git: {
334
358
  enabled: args.gitEnabled ?? true,
@@ -715,6 +739,9 @@ Options:
715
739
  --git-branch <name> Git branch for canonical repo sync (default: main)
716
740
  --git-interval-minutes <n> Remote git sync interval in minutes (default: 60)
717
741
  --git-disable Disable remote git sync for this service
742
+ --root <path> Select a canonical .ai root explicitly
743
+ --global Force the global canonical root
744
+ --project Force the nearest repo-local .ai root
718
745
  --once Run one local+remote sync cycle and exit
719
746
  `;
720
747
  }
@@ -722,17 +749,22 @@ Options:
722
749
  export async function installAutosyncService(args: {
723
750
  tool?: string;
724
751
  homeDir?: string;
752
+ rootDir?: string;
725
753
  gitRemote?: string;
726
754
  gitBranch?: string;
727
755
  gitIntervalMinutes?: number;
728
756
  gitEnabled?: boolean;
729
757
  }): Promise<AutosyncServiceConfig> {
730
758
  const home = args.homeDir ?? homedir();
731
- const serviceName = autosyncServiceName(args.tool);
759
+ const rootDir =
760
+ args.rootDir ??
761
+ resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
762
+ const serviceName = autosyncServiceName(args.tool, rootDir, home);
732
763
  const config = defaultAutosyncConfig({
733
764
  serviceName,
734
765
  tool: args.tool,
735
766
  homeDir: home,
767
+ rootDir,
736
768
  remote: args.gitRemote,
737
769
  branch: args.gitBranch,
738
770
  intervalMinutes: args.gitIntervalMinutes,
@@ -760,9 +792,13 @@ export async function installAutosyncService(args: {
760
792
  export async function uninstallAutosyncService(args: {
761
793
  tool?: string;
762
794
  homeDir?: string;
795
+ rootDir?: string;
763
796
  }): Promise<void> {
764
797
  const home = args.homeDir ?? homedir();
765
- const serviceName = autosyncServiceName(args.tool);
798
+ const rootDir =
799
+ args.rootDir ??
800
+ resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
801
+ const serviceName = autosyncServiceName(args.tool, rootDir, home);
766
802
  const label = autosyncLabel(serviceName);
767
803
  const domain = launchdDomain();
768
804
 
@@ -787,7 +823,9 @@ export async function repairAutosyncServices(
787
823
  if (!config) {
788
824
  continue;
789
825
  }
790
- const desiredRoot = facultRootDir(homeDir);
826
+ const desiredRoot = projectRootFromAiRoot(config.rootDir, homeDir)
827
+ ? config.rootDir
828
+ : facultRootDir(homeDir);
791
829
  if (config.rootDir !== desiredRoot) {
792
830
  config.rootDir = desiredRoot;
793
831
  await saveAutosyncConfig(config, homeDir);
@@ -827,9 +865,13 @@ export async function repairAutosyncServices(
827
865
  export async function autosyncStatus(args: {
828
866
  tool?: string;
829
867
  homeDir?: string;
868
+ rootDir?: string;
830
869
  }): Promise<AutosyncStatus> {
831
870
  const home = args.homeDir ?? homedir();
832
- const serviceName = autosyncServiceName(args.tool);
871
+ const rootDir =
872
+ args.rootDir ??
873
+ resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
874
+ const serviceName = autosyncServiceName(args.tool, rootDir, home);
833
875
  const config = await loadAutosyncConfig(serviceName, home);
834
876
  const state = await loadAutosyncRuntimeState(serviceName, home);
835
877
  const plistPath = autosyncPlistPath(home, serviceName);
@@ -853,8 +895,13 @@ export async function autosyncStatus(args: {
853
895
 
854
896
  export async function restartAutosyncService(args: {
855
897
  tool?: string;
898
+ rootDir?: string;
856
899
  }): Promise<void> {
857
- const serviceName = autosyncServiceName(args.tool);
900
+ const home = homedir();
901
+ const rootDir =
902
+ args.rootDir ??
903
+ resolveCliContextRoot({ homeDir: home, cwd: process.cwd() });
904
+ const serviceName = autosyncServiceName(args.tool, rootDir, home);
858
905
  const label = autosyncLabel(serviceName);
859
906
  await runLaunchctl(["kickstart", "-k", `${launchdDomain()}/${label}`]);
860
907
  }
@@ -867,21 +914,37 @@ export async function autosyncCommand(argv: string[]) {
867
914
  }
868
915
 
869
916
  try {
917
+ const parsed = parseCliContextArgs(rest);
918
+ if (
919
+ parsed.argv.includes("--help") ||
920
+ parsed.argv.includes("-h") ||
921
+ parsed.argv[0] === "help"
922
+ ) {
923
+ console.log(autosyncHelp());
924
+ return;
925
+ }
926
+ const rootDir = resolveCliContextRoot({
927
+ rootArg: parsed.rootArg,
928
+ scope: parsed.scope,
929
+ cwd: process.cwd(),
930
+ });
931
+
870
932
  if (sub === "install") {
871
- const tool = parseAutosyncPositionals(rest, [
933
+ const tool = parseAutosyncPositionals(parsed.argv, [
872
934
  "--git-remote",
873
935
  "--git-branch",
874
936
  "--git-interval-minutes",
875
937
  ])[0];
876
- const gitRemote = parseAutosyncStringFlag(rest, "--git-remote");
877
- const gitBranch = parseAutosyncStringFlag(rest, "--git-branch");
938
+ const gitRemote = parseAutosyncStringFlag(parsed.argv, "--git-remote");
939
+ const gitBranch = parseAutosyncStringFlag(parsed.argv, "--git-branch");
878
940
  const gitIntervalMinutes = parseAutosyncIntFlag(
879
- rest,
941
+ parsed.argv,
880
942
  "--git-interval-minutes"
881
943
  );
882
- const gitEnabled = !rest.includes("--git-disable");
944
+ const gitEnabled = !parsed.argv.includes("--git-disable");
883
945
  const config = await installAutosyncService({
884
946
  tool,
947
+ rootDir,
885
948
  gitRemote,
886
949
  gitBranch,
887
950
  gitIntervalMinutes,
@@ -893,16 +956,18 @@ export async function autosyncCommand(argv: string[]) {
893
956
  }
894
957
 
895
958
  if (sub === "uninstall") {
896
- const tool = parseAutosyncPositionals(rest, [])[0];
897
- await uninstallAutosyncService({ tool });
898
- console.log(`Removed autosync service: ${autosyncServiceName(tool)}`);
959
+ const tool = parseAutosyncPositionals(parsed.argv, [])[0];
960
+ await uninstallAutosyncService({ tool, rootDir });
961
+ console.log(
962
+ `Removed autosync service: ${autosyncServiceName(tool, rootDir)}`
963
+ );
899
964
  return;
900
965
  }
901
966
 
902
967
  if (sub === "status") {
903
- const tool = parseAutosyncPositionals(rest, [])[0];
904
- const status = await autosyncStatus({ tool });
905
- console.log(`Service: ${autosyncServiceName(tool)}`);
968
+ const tool = parseAutosyncPositionals(parsed.argv, [])[0];
969
+ const status = await autosyncStatus({ tool, rootDir });
970
+ console.log(`Service: ${autosyncServiceName(tool, rootDir)}`);
906
971
  console.log(`Plist: ${status.plistPath}`);
907
972
  console.log(`Installed: ${status.plistExists ? "yes" : "no"}`);
908
973
  console.log(`Loaded: ${status.loaded ? "yes" : "no"}`);
@@ -933,21 +998,25 @@ export async function autosyncCommand(argv: string[]) {
933
998
  }
934
999
 
935
1000
  if (sub === "restart") {
936
- const tool = parseAutosyncPositionals(rest, [])[0];
937
- await restartAutosyncService({ tool });
938
- console.log(`Restarted autosync service: ${autosyncServiceName(tool)}`);
1001
+ const tool = parseAutosyncPositionals(parsed.argv, [])[0];
1002
+ await restartAutosyncService({ tool, rootDir });
1003
+ console.log(
1004
+ `Restarted autosync service: ${autosyncServiceName(tool, rootDir)}`
1005
+ );
939
1006
  return;
940
1007
  }
941
1008
 
942
1009
  if (sub === "run") {
943
- const service = parseAutosyncStringFlag(rest, "--service");
944
- const tool = parseAutosyncPositionals(rest, ["--service"])[0];
945
- const serviceName = service ?? autosyncServiceName(tool);
1010
+ const service = parseAutosyncStringFlag(parsed.argv, "--service");
1011
+ const tool = parseAutosyncPositionals(parsed.argv, ["--service"])[0];
1012
+ const serviceName = service ?? autosyncServiceName(tool, rootDir);
946
1013
  const config = await loadAutosyncConfig(serviceName);
947
1014
  if (!config) {
948
1015
  throw new Error(`Autosync service not configured: ${serviceName}`);
949
1016
  }
950
- await runAutosyncService(config, { once: rest.includes("--once") });
1017
+ await runAutosyncService(config, {
1018
+ once: parsed.argv.includes("--once"),
1019
+ });
951
1020
  return;
952
1021
  }
953
1022
 
package/src/builtin.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
5
+ return !!value && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+
8
+ export function facultBuiltinPackRoot(
9
+ packName = "facult-operating-model"
10
+ ): string {
11
+ const here = dirname(fileURLToPath(import.meta.url));
12
+ return join(here, "..", "assets", "packs", packName);
13
+ }
14
+
15
+ async function readTomlObject(
16
+ pathValue: string
17
+ ): Promise<Record<string, unknown> | null> {
18
+ const file = Bun.file(pathValue);
19
+ if (!(await file.exists())) {
20
+ return null;
21
+ }
22
+ const parsed = Bun.TOML.parse(await file.text());
23
+ return isPlainObject(parsed) ? parsed : null;
24
+ }
25
+
26
+ function readBooleanConfig(
27
+ data: Record<string, unknown> | null,
28
+ key: string
29
+ ): boolean | null {
30
+ if (!data) {
31
+ return null;
32
+ }
33
+ const builtin = data.builtin;
34
+ if (!isPlainObject(builtin)) {
35
+ return null;
36
+ }
37
+ const value = builtin[key];
38
+ return typeof value === "boolean" ? value : null;
39
+ }
40
+
41
+ export async function builtinSyncDefaultsEnabled(
42
+ rootDir: string
43
+ ): Promise<boolean> {
44
+ const [tracked, local] = await Promise.all([
45
+ readTomlObject(join(rootDir, "config.toml")),
46
+ readTomlObject(join(rootDir, "config.local.toml")),
47
+ ]);
48
+
49
+ for (const candidate of [tracked, local]) {
50
+ const direct = readBooleanConfig(candidate, "sync_defaults");
51
+ if (direct != null) {
52
+ return direct;
53
+ }
54
+ const legacy = readBooleanConfig(candidate, "sync_global_defaults");
55
+ if (legacy != null) {
56
+ return legacy;
57
+ }
58
+ }
59
+
60
+ return true;
61
+ }
@@ -0,0 +1,198 @@
1
+ import { resolve } from "node:path";
2
+ import type { AssetSourceKind } from "./graph";
3
+ import {
4
+ facultContextRootDir,
5
+ facultRootDir,
6
+ findNearestProjectAiRoot,
7
+ projectRootFromAiRoot,
8
+ } from "./paths";
9
+
10
+ export type CapabilityScopeMode = "merged" | "global" | "project";
11
+
12
+ export interface ParsedCliContext {
13
+ argv: string[];
14
+ rootArg?: string;
15
+ scope: CapabilityScopeMode;
16
+ sourceKind?: AssetSourceKind;
17
+ }
18
+
19
+ function expandHomePath(pathValue: string, home: string): string {
20
+ if (pathValue === "~") {
21
+ return home;
22
+ }
23
+ if (pathValue.startsWith("~/")) {
24
+ return `${home}/${pathValue.slice(2)}`;
25
+ }
26
+ return pathValue;
27
+ }
28
+
29
+ function resolveRootArgument(pathValue: string, homeDir: string): string {
30
+ return resolve(expandHomePath(pathValue, homeDir));
31
+ }
32
+
33
+ function parseStringFlagValue(
34
+ arg: string,
35
+ nextArg: string | undefined,
36
+ flag: string
37
+ ): { value: string; advance: number } | null {
38
+ if (arg === flag) {
39
+ if (!nextArg) {
40
+ throw new Error(`${flag} requires a value`);
41
+ }
42
+ return { value: nextArg, advance: 1 };
43
+ }
44
+ if (arg.startsWith(`${flag}=`)) {
45
+ const value = arg.slice(flag.length + 1);
46
+ if (!value) {
47
+ throw new Error(`${flag} requires a value`);
48
+ }
49
+ return { value, advance: 0 };
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function parseScopeValue(value: string): CapabilityScopeMode {
55
+ if (value === "merged" || value === "global" || value === "project") {
56
+ return value;
57
+ }
58
+ throw new Error(`Unknown scope: ${value}`);
59
+ }
60
+
61
+ function parseSourceValue(value: string): AssetSourceKind {
62
+ if (value === "builtin" || value === "global" || value === "project") {
63
+ return value;
64
+ }
65
+ throw new Error(`Unknown source: ${value}`);
66
+ }
67
+
68
+ export function parseCliContextArgs(
69
+ argv: string[],
70
+ opts?: { allowSource?: boolean; allowScope?: boolean }
71
+ ): ParsedCliContext {
72
+ const rest: string[] = [];
73
+ let rootArg: string | undefined;
74
+ let scope: CapabilityScopeMode = "merged";
75
+ let sourceKind: AssetSourceKind | undefined;
76
+ let explicitRoot = false;
77
+ let explicitScope = false;
78
+
79
+ for (let i = 0; i < argv.length; i++) {
80
+ const arg = argv[i];
81
+ if (!arg) {
82
+ continue;
83
+ }
84
+
85
+ const root = parseStringFlagValue(arg, argv[i + 1], "--root");
86
+ if (root) {
87
+ if (explicitRoot) {
88
+ throw new Error("--root may only be provided once");
89
+ }
90
+ rootArg = root.value;
91
+ explicitRoot = true;
92
+ i += root.advance;
93
+ continue;
94
+ }
95
+
96
+ if (arg === "--global") {
97
+ if (explicitScope && scope !== "global") {
98
+ throw new Error("Conflicting scope flags");
99
+ }
100
+ scope = "global";
101
+ explicitScope = true;
102
+ continue;
103
+ }
104
+
105
+ if (arg === "--project") {
106
+ if (explicitScope && scope !== "project") {
107
+ throw new Error("Conflicting scope flags");
108
+ }
109
+ scope = "project";
110
+ explicitScope = true;
111
+ continue;
112
+ }
113
+
114
+ if (opts?.allowScope !== false) {
115
+ const parsedScope = parseStringFlagValue(arg, argv[i + 1], "--scope");
116
+ if (parsedScope) {
117
+ const value = parseScopeValue(parsedScope.value);
118
+ if (explicitScope && scope !== value) {
119
+ throw new Error("Conflicting scope flags");
120
+ }
121
+ scope = value;
122
+ explicitScope = true;
123
+ i += parsedScope.advance;
124
+ continue;
125
+ }
126
+ }
127
+
128
+ if (opts?.allowSource) {
129
+ const parsedSource = parseStringFlagValue(arg, argv[i + 1], "--source");
130
+ if (parsedSource) {
131
+ sourceKind = parseSourceValue(parsedSource.value);
132
+ i += parsedSource.advance;
133
+ continue;
134
+ }
135
+ }
136
+
137
+ rest.push(arg);
138
+ }
139
+
140
+ return {
141
+ argv: rest,
142
+ rootArg,
143
+ scope,
144
+ sourceKind,
145
+ };
146
+ }
147
+
148
+ function coerceCanonicalRoot(pathValue: string, homeDir: string): string {
149
+ const resolved = resolveRootArgument(pathValue, homeDir);
150
+ const nearestProjectAi = findNearestProjectAiRoot(resolved);
151
+ if (nearestProjectAi) {
152
+ const projectRoot = projectRootFromAiRoot(nearestProjectAi, homeDir);
153
+ if (
154
+ resolved === nearestProjectAi ||
155
+ resolved === resolve(projectRoot ?? "")
156
+ ) {
157
+ return nearestProjectAi;
158
+ }
159
+ }
160
+ return resolved;
161
+ }
162
+
163
+ export function resolveCliContextRoot(args?: {
164
+ homeDir?: string;
165
+ cwd?: string;
166
+ rootArg?: string;
167
+ scope?: CapabilityScopeMode;
168
+ }): string {
169
+ const homeDir = args?.homeDir ?? process.env.HOME ?? "";
170
+ const cwd = args?.cwd ?? process.cwd();
171
+ const scope = args?.scope ?? "merged";
172
+
173
+ if (args?.rootArg) {
174
+ const rootDir = coerceCanonicalRoot(args.rootArg, homeDir);
175
+ if (scope === "project" && !projectRootFromAiRoot(rootDir, homeDir)) {
176
+ throw new Error(
177
+ `Project scope requires a repo-local .ai root: ${rootDir}`
178
+ );
179
+ }
180
+ return rootDir;
181
+ }
182
+
183
+ if (scope === "global") {
184
+ return facultRootDir(homeDir);
185
+ }
186
+
187
+ if (scope === "project") {
188
+ const projectRoot = findNearestProjectAiRoot(cwd);
189
+ if (!projectRoot) {
190
+ throw new Error(
191
+ "No project-local .ai root found from the current directory"
192
+ );
193
+ }
194
+ return projectRoot;
195
+ }
196
+
197
+ return facultContextRootDir({ home: homeDir, cwd });
198
+ }
@@ -37,6 +37,7 @@ function ensureIndexStructure(index: FacultIndex): FacultIndex {
37
37
  mcp: index.mcp ?? { servers: {} },
38
38
  agents: index.agents ?? {},
39
39
  snippets: index.snippets ?? {},
40
+ instructions: index.instructions ?? {},
40
41
  };
41
42
  }
42
43
 
@@ -1,12 +1,15 @@
1
1
  import { mkdir, readdir, rm } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { renderCanonicalText } from "./agents";
4
+ import { builtinSyncDefaultsEnabled, facultBuiltinPackRoot } from "./builtin";
5
+ import { projectRootFromAiRoot } from "./paths";
4
6
  import { renderSnippetText } from "./snippets";
5
7
 
6
8
  export interface GlobalDocPlan {
7
9
  write: string[];
8
10
  remove: string[];
9
11
  contents: Map<string, string>;
12
+ sources: Map<string, string>;
10
13
  managedTargets: string[];
11
14
  }
12
15
 
@@ -14,6 +17,7 @@ export interface RulesPlan {
14
17
  write: string[];
15
18
  remove: string[];
16
19
  contents: Map<string, string>;
20
+ sources: Map<string, string>;
17
21
  managedRulesDir: boolean;
18
22
  }
19
23
 
@@ -22,6 +26,7 @@ export interface ToolConfigPlan {
22
26
  write: boolean;
23
27
  remove: boolean;
24
28
  contents: string | null;
29
+ sourcePath?: string;
25
30
  managedConfig: boolean;
26
31
  }
27
32
 
@@ -30,6 +35,11 @@ interface SourceTarget {
30
35
  targetPath: string;
31
36
  }
32
37
 
38
+ interface GlobalDocTargetPaths {
39
+ primary: string;
40
+ override?: string;
41
+ }
42
+
33
43
  const TOML_BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
34
44
 
35
45
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -151,30 +161,53 @@ async function listGlobalDocSources(args: {
151
161
  toolHome: string;
152
162
  }): Promise<SourceTarget[]> {
153
163
  const { rootDir, tool, toolHome } = args;
154
- if (tool !== "codex") {
155
- return [];
156
- }
164
+ const targets = globalDocTargetPaths(tool, toolHome);
165
+ const useBuiltinDefaults = await builtinSyncDefaultsEnabled(rootDir);
157
166
 
158
167
  const candidates: SourceTarget[] = [];
159
168
  const base = join(rootDir, "AGENTS.global.md");
160
169
  if (await fileExists(base)) {
161
170
  candidates.push({
162
171
  sourcePath: base,
163
- targetPath: join(toolHome, "AGENTS.md"),
172
+ targetPath: targets.primary,
164
173
  });
174
+ } else if (useBuiltinDefaults) {
175
+ const builtinBase = join(facultBuiltinPackRoot(), "AGENTS.global.md");
176
+ if (await fileExists(builtinBase)) {
177
+ candidates.push({
178
+ sourcePath: builtinBase,
179
+ targetPath: targets.primary,
180
+ });
181
+ }
165
182
  }
166
183
 
167
184
  const override = join(rootDir, "AGENTS.override.global.md");
168
- if (await fileExists(override)) {
185
+ if (targets.override && (await fileExists(override))) {
169
186
  candidates.push({
170
187
  sourcePath: override,
171
- targetPath: join(toolHome, "AGENTS.override.md"),
188
+ targetPath: targets.override,
172
189
  });
173
190
  }
174
191
 
175
192
  return candidates;
176
193
  }
177
194
 
195
+ export function globalDocTargetPaths(
196
+ tool: string,
197
+ toolHome: string
198
+ ): GlobalDocTargetPaths {
199
+ if (tool === "claude") {
200
+ return {
201
+ primary: join(toolHome, "CLAUDE.md"),
202
+ };
203
+ }
204
+
205
+ return {
206
+ primary: join(toolHome, "AGENTS.md"),
207
+ override: join(toolHome, "AGENTS.override.md"),
208
+ };
209
+ }
210
+
178
211
  async function renderSourceTarget(args: {
179
212
  homeDir: string;
180
213
  rootDir: string;
@@ -194,6 +227,7 @@ async function renderSourceTarget(args: {
194
227
  return await renderCanonicalText(withSnippets.text, {
195
228
  homeDir: args.homeDir,
196
229
  rootDir: args.rootDir,
230
+ projectRoot: projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined,
197
231
  targetTool: args.tool,
198
232
  targetPath: args.targetPath,
199
233
  });
@@ -208,6 +242,7 @@ export async function planToolGlobalDocsSync(args: {
208
242
  }): Promise<GlobalDocPlan> {
209
243
  const docs = await listGlobalDocSources(args);
210
244
  const contents = new Map<string, string>();
245
+ const sources = new Map<string, string>();
211
246
  const managedTargets = docs.map((doc) => doc.targetPath).sort();
212
247
 
213
248
  for (const doc of docs) {
@@ -219,6 +254,7 @@ export async function planToolGlobalDocsSync(args: {
219
254
  tool: args.tool,
220
255
  });
221
256
  contents.set(doc.targetPath, rendered);
257
+ sources.set(doc.targetPath, doc.sourcePath);
222
258
  }
223
259
 
224
260
  const write: string[] = [];
@@ -238,6 +274,7 @@ export async function planToolGlobalDocsSync(args: {
238
274
  write: write.sort(),
239
275
  remove,
240
276
  contents,
277
+ sources,
241
278
  managedTargets,
242
279
  };
243
280
  }
@@ -301,6 +338,7 @@ export async function planToolRulesSync(args: {
301
338
  }): Promise<RulesPlan> {
302
339
  const rules = await listToolRules(args);
303
340
  const contents = new Map<string, string>();
341
+ const sources = new Map<string, string>();
304
342
 
305
343
  for (const rule of rules) {
306
344
  const targetPath = join(args.rulesDir, rule.targetPath);
@@ -308,10 +346,13 @@ export async function planToolRulesSync(args: {
308
346
  const rendered = await renderCanonicalText(raw, {
309
347
  homeDir: args.homeDir,
310
348
  rootDir: args.rootDir,
349
+ projectRoot:
350
+ projectRootFromAiRoot(args.rootDir, args.homeDir) ?? undefined,
311
351
  targetTool: args.tool,
312
352
  targetPath,
313
353
  });
314
354
  contents.set(targetPath, rendered);
355
+ sources.set(targetPath, rule.sourcePath);
315
356
  }
316
357
 
317
358
  const write: string[] = [];
@@ -342,6 +383,7 @@ export async function planToolRulesSync(args: {
342
383
  write: write.sort(),
343
384
  remove: remove.sort(),
344
385
  contents,
386
+ sources,
345
387
  managedRulesDir: rules.length > 0,
346
388
  };
347
389
  }
@@ -390,6 +432,7 @@ export async function planToolConfigSync(args: {
390
432
  write: false,
391
433
  remove: false,
392
434
  contents: null,
435
+ sourcePath,
393
436
  managedConfig: false,
394
437
  };
395
438
  }
@@ -419,6 +462,7 @@ export async function planToolConfigSync(args: {
419
462
  write: current !== `${nextContents}\n`,
420
463
  remove: false,
421
464
  contents: nextContents,
465
+ sourcePath,
422
466
  managedConfig: true,
423
467
  };
424
468
  }