facult 1.0.3 → 1.1.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 CHANGED
@@ -1,12 +1,28 @@
1
1
  # facult
2
2
 
3
- `facult` is a CLI for managing coding-agent skills and MCP configs across tools.
3
+ <div align="center">
4
+ <a aria-label="NPM version" href="https://www.npmjs.com/package/facult">
5
+ <img alt="facult npm version" src="https://img.shields.io/npm/v/facult.svg?style=flat-square&logo=npm&labelColor=000000&label=facult">
6
+ </a>
7
+ <a aria-label="CI status" href="https://github.com/hack-dance/facult/actions/workflows/ci.yml">
8
+ <img alt="CI" src="https://img.shields.io/github/actions/workflow/status/hack-dance/facult/ci.yml?branch=main&style=flat-square&logo=github&label=ci&labelColor=000000">
9
+ </a>
10
+ <a aria-label="hack.dance" href="https://hack.dance">
11
+ <img alt="Made by hack.dance" src="https://img.shields.io/badge/MADE%20BY%20HACK.DANCE-000000.svg?style=flat-square&labelColor=000000">
12
+ </a>
13
+ <a aria-label="X" href="https://x.com/dimitrikennedy">
14
+ <img alt="Follow on X" src="https://img.shields.io/twitter/follow/dimitrikennedy?style=social">
15
+ </a>
16
+ </div>
17
+
18
+ `facult` is a CLI for managing coding-agent configuration across tools.
4
19
 
5
20
  It helps you:
6
21
  - discover what is installed on your machine
7
22
  - consolidate everything into one canonical store
8
23
  - review trust/security before installing remote content
9
- - enable a curated skill set across Codex, Cursor, and Claude
24
+ - sync managed outputs into Codex, Cursor, and Claude
25
+ - manage a git-backed personal AI store under `~/.ai`
10
26
 
11
27
  ## What facult Is
12
28
 
@@ -16,6 +32,7 @@ Think of it as:
16
32
  - inventory + auditing for agent assets
17
33
  - package manager interface for skill/MCP catalogs
18
34
  - sync layer that applies your chosen setup to each tool
35
+ - canonical source manager for global AI instructions, agents, snippets, tool configs, and rules
19
36
 
20
37
  ## Quick Start
21
38
 
@@ -60,7 +77,17 @@ Pin to a specific version:
60
77
  facult self-update --version 0.0.1
61
78
  ```
62
79
 
63
- ### 2. Import existing skills/configs
80
+ ### 2. Start with a read-only inventory (recommended first)
81
+
82
+ ```bash
83
+ facult scan --show-duplicates
84
+ # optional machine-readable output
85
+ facult scan --json
86
+ ```
87
+
88
+ `scan` is read-only. It inspects local configs and reports what `facult` found without changing files.
89
+
90
+ ### 3. Import existing skills/configs
64
91
 
65
92
  ```bash
66
93
  facult consolidate --auto keep-current --from ~/.codex/skills --from ~/.agents/skills
@@ -69,9 +96,9 @@ facult index
69
96
 
70
97
  Why `keep-current`: it is deterministic and non-interactive for duplicate sources.
71
98
 
72
- Default canonical store: `~/agents/.facult`. You can change it later with `FACULT_ROOT_DIR` or `~/.facult/config.json`.
99
+ Canonical source root: `~/.ai`. Generated state remains under `~/.facult`.
73
100
 
74
- ### 3. Inspect what you have
101
+ ### 4. Inspect what you have
75
102
 
76
103
  ```bash
77
104
  facult list skills
@@ -80,7 +107,7 @@ facult show requesting-code-review
80
107
  facult show mcp:github
81
108
  ```
82
109
 
83
- ### 4. Enable managed mode for your tools
110
+ ### 5. Enable managed mode for your tools
84
111
 
85
112
  ```bash
86
113
  facult manage codex
@@ -93,7 +120,21 @@ facult sync
93
120
 
94
121
  At this point, your selected skills are actively synced to all managed tools.
95
122
 
96
- ### 5. Turn on source trust and strict install flow
123
+ ### 6. Turn on background autosync
124
+
125
+ ```bash
126
+ facult autosync install --git-remote origin --git-branch main --git-interval-minutes 60
127
+ facult autosync status
128
+ ```
129
+
130
+ This installs a per-user macOS LaunchAgent that:
131
+ - watches `~/.ai` for local changes and syncs managed tool outputs automatically
132
+ - tracks dirty state for the canonical repo
133
+ - runs a slower git autosync loop that batches changes, auto-commits them, rebases on the configured remote branch, and pushes on success
134
+
135
+ If the repo hits a rebase conflict, remote autosync stops and reports the blocked state, but local tool sync continues.
136
+
137
+ ### 7. Turn on source trust and strict install flow
97
138
 
98
139
  ```bash
99
140
  facult sources list
@@ -134,6 +175,94 @@ facult sync
134
175
 
135
176
  Note: `templates init mcp ...` is a scaffold, not a running server by itself.
136
177
 
178
+ ## The `~/.ai` Model
179
+
180
+ `facult` now treats `~/.ai` as the canonical, git-backed source of truth for personal AI configuration.
181
+
182
+ Typical layout:
183
+
184
+ ```text
185
+ ~/.ai/
186
+ AGENTS.global.md
187
+ AGENTS.override.global.md
188
+ config.toml
189
+ config.local.toml
190
+ instructions/
191
+ snippets/
192
+ agents/
193
+ skills/
194
+ mcp/
195
+ templates/
196
+ tools/
197
+ codex/
198
+ config.toml
199
+ rules/
200
+ projects/
201
+ <slug>/
202
+ config.toml
203
+ config.local.toml
204
+ snippets/
205
+ instructions/
206
+ ```
207
+
208
+ Important split:
209
+ - `~/.ai` is canonical source
210
+ - `~/.facult` is generated state, trust state, managed tool state, autosync state, and caches
211
+ - tool homes such as `~/.codex` are rendered outputs
212
+
213
+ ### Canonical conventions
214
+
215
+ - Use `instructions/` for reusable markdown documents
216
+ - Use `snippets/` for composable partial blocks injected into markdown templates
217
+ - Use `tools/codex/rules/*.rules` for actual Codex approval-policy rules
218
+ - Use logical refs such as `@ai/instructions/WRITING.md` in tracked source
219
+ - Use config-backed refs in prompts where you want stable named references such as `${refs.writing_rule}`
220
+
221
+ ### Config and env layering
222
+
223
+ Canonical render context is layered explicitly:
224
+ 1. built-ins injected by `facult`
225
+ 2. `~/.ai/config.toml`
226
+ 3. `~/.ai/config.local.toml`
227
+ 4. `~/.ai/projects/<slug>/config.toml`
228
+ 5. `~/.ai/projects/<slug>/config.local.toml`
229
+ 6. explicit runtime overrides
230
+
231
+ Built-ins currently include:
232
+ - `AI_ROOT`
233
+ - `HOME`
234
+ - `PROJECT_ROOT`
235
+ - `PROJECT_SLUG`
236
+ - `TARGET_TOOL`
237
+ - `TARGET_PATH`
238
+
239
+ Recommended split:
240
+ - `config.toml`: tracked, portable, non-secret refs/defaults
241
+ - `config.local.toml`: ignored, machine-local paths and secrets
242
+
243
+ ### Snippets
244
+
245
+ Snippets use HTML comment markers:
246
+
247
+ ```md
248
+ <!-- fclty:global/codex/baseline -->
249
+ <!-- /fclty:global/codex/baseline -->
250
+ ```
251
+
252
+ Resolution rules:
253
+ - unscoped marker `codingstyle` prefers `snippets/projects/<project>/codingstyle.md`, then falls back to `snippets/global/codingstyle.md`
254
+ - explicit marker `global/codex/baseline` resolves directly to `snippets/global/codex/baseline.md`
255
+
256
+ Commands:
257
+
258
+ ```bash
259
+ facult snippets list
260
+ facult snippets show global/codex/baseline
261
+ facult snippets sync [--dry-run] [file...]
262
+ ```
263
+
264
+ Snippets are already used during global Codex `AGENTS.md` rendering.
265
+
137
266
  ## Security and Trust
138
267
 
139
268
  `facult` has two trust layers:
@@ -168,10 +297,11 @@ Recommended security flow:
168
297
  ### Capability categories
169
298
 
170
299
  - Inventory: discover local skills, MCP configs, hooks, and instruction files
171
- - Management: consolidate, index, manage/unmanage tools, enable/disable entries
300
+ - Management: consolidate, index, manage/unmanage tools, enable/disable entries, manage canonical AI config
172
301
  - Security: static audit, agent audit, item trust, source trust, source verification
173
302
  - Distribution: search/install/update from catalogs and verified manifests
174
303
  - DX: scaffold templates and sync snippets into instruction/config files
304
+ - Automation: background autosync for local tool propagation and canonical repo git sync
175
305
 
176
306
  ### Command categories
177
307
 
@@ -199,6 +329,10 @@ facult enable <name> [--for <tool1,tool2,...>]
199
329
  facult enable mcp:<name> [--for <tool1,tool2,...>]
200
330
  facult disable <name> [--for <tool1,tool2,...>]
201
331
  facult sync [tool] [--dry-run]
332
+ facult autosync install [tool] [--git-remote <name>] [--git-branch <name>] [--git-interval-minutes <n>] [--git-disable]
333
+ facult autosync status [tool]
334
+ facult autosync restart [tool]
335
+ facult autosync uninstall [tool]
202
336
  ```
203
337
 
204
338
  - Remote catalogs and policies
@@ -241,7 +375,8 @@ facult <command> --help
241
375
  `facult` resolves the canonical root in this order:
242
376
  1. `FACULT_ROOT_DIR`
243
377
  2. `~/.facult/config.json` (`rootDir`)
244
- 3. `~/agents/.facult` (or a detected legacy store under `~/agents/`)
378
+ 3. `~/.ai`
379
+ 4. `~/agents/.facult` (or a detected legacy store under `~/agents/`)
245
380
 
246
381
  ### Runtime env vars
247
382
 
@@ -256,9 +391,13 @@ Under `~/.facult/`:
256
391
  - `sources.json` (latest inventory scan state)
257
392
  - `consolidated.json` (consolidation state)
258
393
  - `managed.json` (managed tool state)
394
+ - `ai/index.json` (generated canonical AI inventory)
259
395
  - `audit/static-latest.json` (latest static audit report)
260
396
  - `audit/agent-latest.json` (latest agent audit report)
261
397
  - `trust/sources.json` (source trust policy state)
398
+ - `autosync/services/*.json` (autosync service configs)
399
+ - `autosync/state/*.json` (autosync runtime state)
400
+ - `autosync/logs/*` (autosync service logs)
262
401
 
263
402
  ### Config reference
264
403
 
@@ -275,7 +414,7 @@ Under `~/.facult/`:
275
414
  Example:
276
415
  ```json
277
416
  {
278
- "rootDir": "~/agents/.facult",
417
+ "rootDir": "~/.ai",
279
418
  "scanFrom": ["~/dev", "~/work"],
280
419
  "scanFromIgnore": ["vendor", ".venv"],
281
420
  "scanFromNoDefaultIgnore": false,
@@ -307,6 +446,42 @@ bun run install:status
307
446
 
308
447
  Default install path is `~/.facult/bin/facult`. You can pass a custom target dir via `--dir=/path`.
309
448
 
449
+ ## Autosync
450
+
451
+ `facult autosync` is the background propagation layer for managed installs.
452
+
453
+ Current v1 behavior:
454
+ - macOS LaunchAgent-backed
455
+ - immediate local managed-tool sync on `~/.ai` file changes
456
+ - periodic git autosync for the canonical repo
457
+ - automatic autosync commits with source-tagged commit messages such as:
458
+ - `chore(facult-autosync): sync canonical ai changes from <host> [service:all]`
459
+
460
+ Recommended usage:
461
+
462
+ ```bash
463
+ facult autosync install
464
+ facult autosync status
465
+ ```
466
+
467
+ Tool-scoped service:
468
+
469
+ ```bash
470
+ facult autosync install codex
471
+ ```
472
+
473
+ One-shot runner for verification/debugging:
474
+
475
+ ```bash
476
+ facult autosync run --service all --once
477
+ ```
478
+
479
+ Remote git policy:
480
+ - do not sync on every file event
481
+ - mark the canonical repo dirty on local changes
482
+ - on the configured timer, fetch, auto-commit local canonical changes if needed, pull `--rebase`, then push
483
+ - if rebase conflicts occur, remote autosync is blocked and reported, but local managed-tool sync keeps running
484
+
310
485
  ## CI and Release Automation
311
486
 
312
487
  - CI workflow: `.github/workflows/ci.yml`
@@ -373,3 +548,18 @@ bun run release:dry-run
373
548
  Not as a first-party `facult mcp serve` runtime.
374
549
 
375
550
  `facult` currently focuses on inventory, trust/audit, install/update, and managed sync of skills/MCP configs.
551
+
552
+ ### Does facult now manage global AI config, not just skills and MCP?
553
+
554
+ Yes. The core model now includes:
555
+ - canonical personal AI source in `~/.ai`
556
+ - rendered managed outputs in tool homes such as `~/.codex`
557
+ - global instruction docs such as `AGENTS.global.md`
558
+ - tool-native configs such as `~/.codex/config.toml`
559
+ - tool-native rule files such as `~/.codex/rules/*.rules`
560
+
561
+ ### Do I still need to run `facult sync` manually?
562
+
563
+ If autosync is not installed, yes.
564
+
565
+ If autosync is installed, local changes under `~/.ai` propagate automatically to managed tools. Manual `facult sync` is still useful for explicit repair, dry-runs, and non-daemon workflows.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "facult",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Manage coding-agent skills and MCP configs across tools.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,6 +11,7 @@ export const codexAdapter: ToolAdapter = {
11
11
  getDefaultPaths: () => ({
12
12
  mcp: "~/.codex/mcp.json",
13
13
  skills: "~/.codex/skills",
14
+ agents: "~/.codex/agents",
14
15
  config: "~/.config/openai/codex.json",
15
16
  }),
16
17
  parseMcp: (config) => parseMcpConfig(config),
@@ -21,6 +21,7 @@ export interface CanonicalSkill {
21
21
  export interface AdapterDefaultPaths {
22
22
  mcp?: string;
23
23
  skills?: string | string[];
24
+ agents?: string | string[];
24
25
  config?: string;
25
26
  }
26
27
 
package/src/agents.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { join } from "node:path";
2
+
3
+ const AI_REF_RE = /(?<![\w@])@ai\/([^\s"'`<>]+)/g;
4
+ const INTERPOLATION_RE = /\$\{([^}]+)\}/g;
5
+ const TRAILING_PUNCTUATION_RE = /[.,;:!?)}\]]+$/;
6
+ const MAX_RENDER_PASSES = 10;
7
+
8
+ export interface RenderCanonicalTextOptions {
9
+ homeDir?: string;
10
+ rootDir: string;
11
+ projectSlug?: string;
12
+ projectRoot?: string;
13
+ targetTool?: string;
14
+ targetPath?: string;
15
+ overrides?: Record<string, unknown>;
16
+ }
17
+
18
+ type RenderContext = Record<string, unknown>;
19
+
20
+ function trimTrailingPunctuation(refPath: string): {
21
+ path: string;
22
+ suffix: string;
23
+ } {
24
+ const match = TRAILING_PUNCTUATION_RE.exec(refPath);
25
+ if (!match) {
26
+ return { path: refPath, suffix: "" };
27
+ }
28
+
29
+ const suffix = match[0];
30
+ return {
31
+ path: refPath.slice(0, -suffix.length),
32
+ suffix,
33
+ };
34
+ }
35
+
36
+ export function renderAiRefs(input: string, canonicalRoot: string): string {
37
+ return input.replace(AI_REF_RE, (_match, refPath: string) => {
38
+ const { path, suffix } = trimTrailingPunctuation(refPath);
39
+ return `${join(canonicalRoot, path)}${suffix}`;
40
+ });
41
+ }
42
+
43
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
44
+ return !!value && typeof value === "object" && !Array.isArray(value);
45
+ }
46
+
47
+ function mergeContexts(
48
+ base: Record<string, unknown>,
49
+ override: Record<string, unknown>
50
+ ): Record<string, unknown> {
51
+ const merged: Record<string, unknown> = { ...base };
52
+
53
+ for (const [key, value] of Object.entries(override)) {
54
+ const current = merged[key];
55
+ if (isPlainObject(current) && isPlainObject(value)) {
56
+ merged[key] = mergeContexts(current, value);
57
+ continue;
58
+ }
59
+ merged[key] = value;
60
+ }
61
+
62
+ return merged;
63
+ }
64
+
65
+ async function readTomlFile(
66
+ pathValue: string
67
+ ): Promise<Record<string, unknown> | null> {
68
+ const file = Bun.file(pathValue);
69
+ if (!(await file.exists())) {
70
+ return null;
71
+ }
72
+
73
+ const text = await file.text();
74
+ const parsed = Bun.TOML.parse(text);
75
+ return isPlainObject(parsed) ? parsed : null;
76
+ }
77
+
78
+ function getContextValue(
79
+ context: Record<string, unknown>,
80
+ dottedPath: string
81
+ ): unknown {
82
+ const segments = dottedPath
83
+ .split(".")
84
+ .map((segment) => segment.trim())
85
+ .filter(Boolean);
86
+ if (segments.length === 0) {
87
+ return undefined;
88
+ }
89
+
90
+ let current: unknown = context;
91
+ for (const segment of segments) {
92
+ if (!(isPlainObject(current) && segment in current)) {
93
+ return undefined;
94
+ }
95
+ current = current[segment];
96
+ }
97
+ return current;
98
+ }
99
+
100
+ function interpolateString(
101
+ input: string,
102
+ context: Record<string, unknown>
103
+ ): string {
104
+ return input.replace(INTERPOLATION_RE, (match, keyPath: string) => {
105
+ const value = getContextValue(context, keyPath.trim());
106
+ return typeof value === "string" ? value : match;
107
+ });
108
+ }
109
+
110
+ export async function loadRenderContext(
111
+ options: RenderCanonicalTextOptions
112
+ ): Promise<RenderContext> {
113
+ const {
114
+ homeDir,
115
+ overrides,
116
+ projectRoot,
117
+ projectSlug,
118
+ rootDir,
119
+ targetPath,
120
+ targetTool,
121
+ } = options;
122
+ const contextBase: RenderContext = {
123
+ AI_ROOT: rootDir,
124
+ HOME: homeDir ?? "",
125
+ PROJECT_ROOT: projectRoot ?? "",
126
+ PROJECT_SLUG: projectSlug ?? "",
127
+ TARGET_PATH: targetPath ?? "",
128
+ TARGET_TOOL: targetTool ?? "",
129
+ };
130
+
131
+ let context = contextBase;
132
+ const layers = [
133
+ await readTomlFile(join(rootDir, "config.toml")),
134
+ await readTomlFile(join(rootDir, "config.local.toml")),
135
+ projectSlug
136
+ ? await readTomlFile(
137
+ join(rootDir, "projects", projectSlug, "config.toml")
138
+ )
139
+ : null,
140
+ projectSlug
141
+ ? await readTomlFile(
142
+ join(rootDir, "projects", projectSlug, "config.local.toml")
143
+ )
144
+ : null,
145
+ overrides && isPlainObject(overrides) ? overrides : null,
146
+ ];
147
+
148
+ for (const layer of layers) {
149
+ if (layer) {
150
+ context = mergeContexts(context, layer);
151
+ }
152
+ }
153
+
154
+ return context;
155
+ }
156
+
157
+ export async function renderCanonicalText(
158
+ input: string,
159
+ options: RenderCanonicalTextOptions
160
+ ): Promise<string> {
161
+ const context = await loadRenderContext(options);
162
+ let rendered = input;
163
+ const seen = new Set<string>();
164
+
165
+ for (let pass = 0; pass < MAX_RENDER_PASSES; pass += 1) {
166
+ if (seen.has(rendered)) {
167
+ break;
168
+ }
169
+ seen.add(rendered);
170
+
171
+ const interpolated = interpolateString(rendered, context);
172
+ const withRefs = renderAiRefs(interpolated, options.rootDir);
173
+ if (withRefs === rendered) {
174
+ return withRefs;
175
+ }
176
+ rendered = withRefs;
177
+ }
178
+
179
+ return rendered;
180
+ }
@@ -0,0 +1,55 @@
1
+ import { copyFile, mkdir, stat } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { buildIndex } from "./index-builder";
4
+ import { facultAiIndexPath } from "./paths";
5
+
6
+ async function fileExists(path: string): Promise<boolean> {
7
+ try {
8
+ return (await stat(path)).isFile();
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ export function legacyAiIndexPath(rootDir: string): string {
15
+ return join(rootDir, "index.json");
16
+ }
17
+
18
+ export async function ensureAiIndexPath(args: {
19
+ homeDir: string;
20
+ rootDir: string;
21
+ repair?: boolean;
22
+ }): Promise<{
23
+ path: string;
24
+ repaired: boolean;
25
+ source: "generated" | "legacy" | "rebuilt" | "missing";
26
+ }> {
27
+ const generatedPath = facultAiIndexPath(args.homeDir);
28
+ if (await fileExists(generatedPath)) {
29
+ return { path: generatedPath, repaired: false, source: "generated" };
30
+ }
31
+
32
+ const legacyPath = legacyAiIndexPath(args.rootDir);
33
+ if (await fileExists(legacyPath)) {
34
+ if (args.repair !== false) {
35
+ await mkdir(dirname(generatedPath), { recursive: true });
36
+ await copyFile(legacyPath, generatedPath);
37
+ }
38
+ return {
39
+ path: generatedPath,
40
+ repaired: args.repair !== false,
41
+ source: "legacy",
42
+ };
43
+ }
44
+
45
+ if (args.repair !== false) {
46
+ const { outputPath } = await buildIndex({
47
+ rootDir: args.rootDir,
48
+ homeDir: args.homeDir,
49
+ force: false,
50
+ });
51
+ return { path: outputPath, repaired: true, source: "rebuilt" };
52
+ }
53
+
54
+ return { path: generatedPath, repaired: false, source: "missing" };
55
+ }
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
- import { join } from "node:path";
2
+ import { ensureAiIndexPath } from "../ai-state";
3
3
  import type { FacultIndex } from "../index-builder";
4
- import { facultRootDir } from "../paths";
4
+ import { facultAiIndexPath, facultRootDir } from "../paths";
5
5
  import type { AuditItemResult, Severity } from "./types";
6
6
  import { SEVERITY_ORDER } from "./types";
7
7
 
@@ -33,8 +33,12 @@ function computeAuditStatus(
33
33
  return worst >= SEVERITY_ORDER.high ? "flagged" : "passed";
34
34
  }
35
35
 
36
- async function loadIndex(rootDir: string): Promise<FacultIndex | null> {
37
- const indexPath = join(rootDir, "index.json");
36
+ async function loadIndex(homeDir: string): Promise<FacultIndex | null> {
37
+ const { path: indexPath } = await ensureAiIndexPath({
38
+ homeDir,
39
+ rootDir: facultRootDir(homeDir),
40
+ repair: true,
41
+ });
38
42
  const file = Bun.file(indexPath);
39
43
  if (!(await file.exists())) {
40
44
  return null;
@@ -46,8 +50,8 @@ async function loadIndex(rootDir: string): Promise<FacultIndex | null> {
46
50
  }
47
51
  }
48
52
 
49
- async function writeIndex(rootDir: string, index: FacultIndex) {
50
- const indexPath = join(rootDir, "index.json");
53
+ async function writeIndex(homeDir: string, index: FacultIndex) {
54
+ const indexPath = facultAiIndexPath(homeDir);
51
55
  await Bun.write(indexPath, `${JSON.stringify(index, null, 2)}\n`);
52
56
  }
53
57
 
@@ -57,9 +61,7 @@ export async function updateIndexFromAuditReport(opts: {
57
61
  results: AuditItemResult[];
58
62
  }): Promise<{ updated: boolean; reason?: string }> {
59
63
  const home = opts.homeDir ?? homedir();
60
- const rootDir = facultRootDir(home);
61
-
62
- const loaded = await loadIndex(rootDir);
64
+ const loaded = await loadIndex(home);
63
65
  if (!loaded) {
64
66
  return { updated: false, reason: "index-missing" };
65
67
  }
@@ -110,6 +112,6 @@ export async function updateIndexFromAuditReport(opts: {
110
112
  }
111
113
 
112
114
  index.updatedAt = new Date().toISOString();
113
- await writeIndex(rootDir, index);
115
+ await writeIndex(home, index);
114
116
  return { updated: true };
115
117
  }