fraim 2.0.248 → 2.0.249

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.
@@ -12,25 +12,44 @@ exports.managerCommand = new commander_1.Command('manager')
12
12
  .description('Manage your portable personal manager context');
13
13
  exports.managerCommand
14
14
  .command('publish')
15
- .description('Publish manager context/rules/learnings files to the configured manager backend')
16
- .argument('<files...>', 'Local files to publish: manager_context.md, manager_rules.md, and/or personal learning files')
17
- .action(async (files) => {
15
+ .description('Publish manager context, rules, learnings, capabilities, or employees to the configured manager backend')
16
+ .argument('<files...>', 'Files to publish: manager_context.md, manager_rules.md, personal learning files, or pack-relative capability paths such as jobs/<cat>/<name>.md and employees/<slug>.json')
17
+ .option('--remove <paths...>', 'Pack-relative paths to remove in the same change (the source side of a move)')
18
+ .action(async (files, options) => {
18
19
  try {
19
20
  const artifacts = files.map((filePath) => {
20
21
  if (!fs_1.default.existsSync(filePath))
21
22
  throw new Error(`File not found: ${filePath}`);
22
23
  return { relativePath: (0, manager_publish_1.managerPackRelativePathFor)(filePath), content: fs_1.default.readFileSync(filePath, 'utf8') };
23
24
  });
24
- const result = await (0, manager_publish_1.publishManagerArtifacts)(artifacts);
25
+ const deletions = (options.remove ?? []).map(manager_publish_1.managerPackRelativePathFor);
26
+ const result = await (0, manager_publish_1.publishManagerArtifacts)(artifacts, { deletions });
25
27
  if (result.backend === 'fraim-cloud') {
26
28
  console.log(chalk_1.default.green(`Published ${artifacts.length} manager artifact(s) to your FRAIM account (version ${result.version}).`));
27
29
  console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
30
+ return;
31
+ }
32
+ if (result.backend === 'local-folder') {
33
+ console.log(chalk_1.default.green(`Wrote ${artifacts.length} manager artifact(s) to your synced folder.`));
34
+ for (const p of result.writtenPaths ?? [])
35
+ console.log(chalk_1.default.gray(` ${p}`));
36
+ if (result.deletedPaths?.length) {
37
+ console.log(chalk_1.default.green(`Removed ${result.deletedPaths.length} source file(s).`));
38
+ }
39
+ console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
40
+ return;
41
+ }
42
+ if (result.credentialWarning)
43
+ console.log(chalk_1.default.yellow(result.credentialWarning));
44
+ console.log(chalk_1.default.green(`Pushed ${artifacts.length} manager artifact(s) to branch "${result.branch}".`));
45
+ if (result.prCreated) {
46
+ console.log(chalk_1.default.cyan(`Opened the pull request: ${result.prUrl}`));
47
+ console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
28
48
  }
29
49
  else {
30
- console.log(chalk_1.default.green(`Pushed ${artifacts.length} manager artifact(s) to branch "${result.branch}" in your manager repo.`));
50
+ console.log(chalk_1.default.yellow(`No pull request was opened. ${result.prBlockedReason ?? ''}`.trim()));
31
51
  if (result.prUrl)
32
- console.log(chalk_1.default.cyan(`Open the pull request: ${result.prUrl}`));
33
- console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
52
+ console.log(chalk_1.default.cyan(`Open one here: ${result.prUrl}`));
34
53
  }
35
54
  }
36
55
  catch (error) {
@@ -12,25 +12,44 @@ exports.orgCommand = new commander_1.Command('org')
12
12
  .description('Manage your shared organization context');
13
13
  exports.orgCommand
14
14
  .command('publish')
15
- .description('Publish org context/rules/learnings/brand files to the configured org backend')
16
- .argument('<files...>', 'Local files to publish: org_context.md, org_rules.md, org_brand.json, and/or org-*.md learnings')
17
- .action(async (files) => {
15
+ .description('Publish org context, rules, learnings, brand, capabilities, or employees to the configured org backend')
16
+ .argument('<files...>', 'Files to publish: org_context.md, org_rules.md, org_brand.json, org-*.md learnings, or pack-relative capability paths such as jobs/<cat>/<name>.md and employees/<slug>.json')
17
+ .option('--remove <paths...>', 'Pack-relative paths to remove in the same change (the source side of a move)')
18
+ .action(async (files, options) => {
18
19
  try {
19
20
  const artifacts = files.map((f) => {
20
21
  if (!fs_1.default.existsSync(f))
21
22
  throw new Error(`File not found: ${f}`);
22
23
  return { relativePath: (0, org_publish_1.packRelativePathFor)(f), content: fs_1.default.readFileSync(f, 'utf8') };
23
24
  });
24
- const result = await (0, org_publish_1.publishOrgArtifacts)(artifacts);
25
+ const deletions = (options.remove ?? []).map(org_publish_1.packRelativePathFor);
26
+ const result = await (0, org_publish_1.publishOrgArtifacts)(artifacts, { deletions });
25
27
  if (result.backend === 'fraim-cloud') {
26
28
  console.log(chalk_1.default.green(`Published ${artifacts.length} org artifact(s) to your FRAIM organization (version ${result.version}).`));
27
29
  console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
30
+ return;
31
+ }
32
+ if (result.backend === 'local-folder') {
33
+ console.log(chalk_1.default.green(`Wrote ${artifacts.length} org artifact(s) to your synced folder.`));
34
+ for (const p of result.writtenPaths ?? [])
35
+ console.log(chalk_1.default.gray(` ${p}`));
36
+ if (result.deletedPaths?.length) {
37
+ console.log(chalk_1.default.green(`Removed ${result.deletedPaths.length} source file(s).`));
38
+ }
39
+ console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
40
+ return;
41
+ }
42
+ if (result.credentialWarning)
43
+ console.log(chalk_1.default.yellow(result.credentialWarning));
44
+ console.log(chalk_1.default.green(`Pushed ${artifacts.length} org artifact(s) to branch "${result.branch}".`));
45
+ if (result.prCreated) {
46
+ console.log(chalk_1.default.cyan(`Opened the pull request: ${result.prUrl}`));
47
+ console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
28
48
  }
29
49
  else {
30
- console.log(chalk_1.default.green(`Pushed ${artifacts.length} org artifact(s) to branch "${result.branch}" in your organization repo.`));
50
+ console.log(chalk_1.default.yellow(`No pull request was opened. ${result.prBlockedReason ?? ''}`.trim()));
31
51
  if (result.prUrl)
32
- console.log(chalk_1.default.cyan(`Open the pull request: ${result.prUrl}`));
33
- console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
52
+ console.log(chalk_1.default.cyan(`Open one here: ${result.prUrl}`));
34
53
  }
35
54
  }
36
55
  catch (error) {
@@ -230,6 +230,23 @@ const runSync = async (options) => {
230
230
  else if (outcome.status === 'absent') {
231
231
  console.log(chalk_1.default.yellow(`Org context not synced: ${outcome.error}`));
232
232
  }
233
+ // Issue #1043 round 2: the home is the only read location, so anything
234
+ // still sitting at the old standard path is invisible to FRAIM. Say so;
235
+ // silently orphaning a manager's work is worse than a noisy sync.
236
+ try {
237
+ const { findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
238
+ const stranded = findStrandedLegacyContent('org');
239
+ if (stranded.length > 0) {
240
+ const home = resolvePackHome('org');
241
+ console.log(chalk_1.default.yellow(`${stranded.length} org file(s) are not in your org home and are no longer read:`));
242
+ for (const rel of stranded.slice(0, 10))
243
+ console.log(chalk_1.default.yellow(` ${rel}`));
244
+ if (stranded.length > 10)
245
+ console.log(chalk_1.default.yellow(` ...and ${stranded.length - 10} more`));
246
+ console.log(chalk_1.default.yellow(`Move them into ${home.root} to make them live again. Nothing was deleted.`));
247
+ }
248
+ }
249
+ catch { /* reporting must never fail a sync */ }
233
250
  // 'disabled' (no org configured) stays silent.
234
251
  // R8.1: one-time publish offer for legacy machine-local org files. The
235
252
  // publish itself runs through organization-onboarding (propose-and-approve);
@@ -257,6 +274,23 @@ const runSync = async (options) => {
257
274
  else if (outcome.status === 'absent') {
258
275
  console.log(chalk_1.default.yellow(`Manager context not synced: ${outcome.error}`));
259
276
  }
277
+ // Issue #1043 round 2: the home is the only read location, so anything
278
+ // still sitting at the old standard path is invisible to FRAIM. Say so;
279
+ // silently orphaning a manager's work is worse than a noisy sync.
280
+ try {
281
+ const { findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
282
+ const stranded = findStrandedLegacyContent('manager');
283
+ if (stranded.length > 0) {
284
+ const home = resolvePackHome('manager');
285
+ console.log(chalk_1.default.yellow(`${stranded.length} manager file(s) are not in your manager home and are no longer read:`));
286
+ for (const rel of stranded.slice(0, 10))
287
+ console.log(chalk_1.default.yellow(` ${rel}`));
288
+ if (stranded.length > 10)
289
+ console.log(chalk_1.default.yellow(` ...and ${stranded.length - 10} more`));
290
+ console.log(chalk_1.default.yellow(`Move them into ${home.root} to make them live again. Nothing was deleted.`));
291
+ }
292
+ }
293
+ catch { /* reporting must never fail a sync */ }
260
294
  };
261
295
  const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
262
296
  const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
@@ -37,42 +37,84 @@ exports.isConflictCopy = isConflictCopy;
37
37
  exports.countConflictCopies = countConflictCopies;
38
38
  exports.collectLocalFolderFiles = collectLocalFolderFiles;
39
39
  exports.writeLocalFolderFile = writeLocalFolderFile;
40
+ exports.removeLocalFolderFiles = removeLocalFolderFiles;
40
41
  exports.localFolderVersion = localFolderVersion;
41
42
  const fs_1 = __importDefault(require("fs"));
42
43
  const path_1 = __importDefault(require("path"));
43
44
  const crypto_1 = __importDefault(require("crypto"));
44
- /** Pack subdirectories expected in every local-folder backend. */
45
- exports.LOCAL_FOLDER_PACK_DIRS = ['context', 'rules', 'learnings'];
45
+ const capability_pack_1 = require("../../core/capability-pack");
46
+ /**
47
+ * Pack subdirectories expected in every local-folder backend.
48
+ *
49
+ * Issue #1043: the capability directories (and `employees`) were missing, so a
50
+ * job, skill, nested rule, or employee record published into a local-folder
51
+ * pack was never collected on the way back down. The pack was write-only for
52
+ * those types. `CAPABILITY_DIRS` is the single allowlist for the capability
53
+ * half; this list is context/rules/learnings plus that, deduplicated because
54
+ * `rules` appears in both.
55
+ */
56
+ exports.LOCAL_FOLDER_PACK_DIRS = [
57
+ ...new Set(['context', 'rules', 'learnings', ...capability_pack_1.CAPABILITY_DIRS]),
58
+ ];
46
59
  /**
47
60
  * Matches the conflict-copy filename patterns produced by OneDrive,
48
61
  * SharePoint, Google Drive for Desktop, and Dropbox.
49
62
  *
50
63
  * Deliberately does NOT match ordinary parenthesised names like
51
64
  * "sprint-retro (Q2 2026).md" — only the well-known suffix patterns.
65
+ *
66
+ * Issue #1043 widened the extension from `.md` to `.md|.json`: employee
67
+ * records are JSON pack members and get conflict copies like any other file.
52
68
  */
53
- exports.CONFLICT_COPY_RE = /[\s-](\([^)]*'s conflicted copy \d{4}-\d{2}-\d{2}\)|\(\d+\)|[\w-]+-PC)\.md$/i;
69
+ exports.CONFLICT_COPY_RE = /[\s-](\([^)]*'s conflicted copy \d{4}-\d{2}-\d{2}\)|\(\d+\)|[\w-]+-PC)\.(md|json)$/i;
54
70
  /** True when the filename looks like a sync-client conflict copy. */
55
71
  function isConflictCopy(fileName) {
56
72
  return exports.CONFLICT_COPY_RE.test(fileName);
57
73
  }
58
74
  /**
59
- * Count conflict-copy files directly under dirPath/learnings (non-recursive).
60
- * Used by the Hub UI to surface an amber "N conflicts" indicator.
75
+ * Walk every pack file under localPath, depth-first, yielding pack-relative
76
+ * paths with forward slashes.
77
+ *
78
+ * Issue #1043: this replaces three separate one-level `readdirSync` loops.
79
+ * Capability members live at `jobs/<category>/<name>.md`, so a one-level scan
80
+ * could never see them — which is why a published job neither synced back down
81
+ * nor moved the pack version.
61
82
  */
62
- function countConflictCopies(localPath) {
63
- let count = 0;
83
+ function walkPackFiles(localPath, visit) {
84
+ const descend = (absDir, relDir) => {
85
+ let entries;
86
+ try {
87
+ entries = fs_1.default.readdirSync(absDir, { withFileTypes: true });
88
+ }
89
+ catch {
90
+ return; // non-fatal — missing or unreadable subdirs are fine
91
+ }
92
+ for (const entry of entries) {
93
+ const abs = path_1.default.join(absDir, entry.name);
94
+ const rel = `${relDir}/${entry.name}`;
95
+ if (entry.isDirectory())
96
+ descend(abs, rel);
97
+ else if (entry.isFile())
98
+ visit(rel, abs);
99
+ }
100
+ };
64
101
  for (const dirName of exports.LOCAL_FOLDER_PACK_DIRS) {
65
102
  const dir = path_1.default.join(localPath, dirName);
66
103
  if (!fs_1.default.existsSync(dir))
67
104
  continue;
68
- try {
69
- for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
70
- if (entry.isFile() && isConflictCopy(entry.name))
71
- count++;
72
- }
73
- }
74
- catch { /* non-fatal */ }
105
+ descend(dir, dirName);
75
106
  }
107
+ }
108
+ /**
109
+ * Count conflict-copy files across the pack directories.
110
+ * Used by the Hub UI to surface an amber "N conflicts" indicator.
111
+ */
112
+ function countConflictCopies(localPath) {
113
+ let count = 0;
114
+ walkPackFiles(localPath, (rel) => {
115
+ if (isConflictCopy(path_1.default.basename(rel)))
116
+ count++;
117
+ });
76
118
  return count;
77
119
  }
78
120
  /**
@@ -84,25 +126,16 @@ function countConflictCopies(localPath) {
84
126
  */
85
127
  function collectLocalFolderFiles(localPath, pathGuard) {
86
128
  const files = [];
87
- for (const dirName of exports.LOCAL_FOLDER_PACK_DIRS) {
88
- const dir = path_1.default.join(localPath, dirName);
89
- if (!fs_1.default.existsSync(dir))
90
- continue;
129
+ walkPackFiles(localPath, (rel, abs) => {
130
+ if (isConflictCopy(path_1.default.basename(rel)))
131
+ return;
132
+ if (!pathGuard(rel))
133
+ return;
91
134
  try {
92
- for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
93
- if (!entry.isFile())
94
- continue;
95
- if (isConflictCopy(entry.name))
96
- continue;
97
- const rel = `${dirName}/${entry.name}`;
98
- if (!pathGuard(rel))
99
- continue;
100
- const content = fs_1.default.readFileSync(path_1.default.join(dir, entry.name), 'utf8');
101
- files.push({ relativePath: rel, content });
102
- }
135
+ files.push({ relativePath: rel, content: fs_1.default.readFileSync(abs, 'utf8') });
103
136
  }
104
- catch { /* non-fatal — missing subdirs are fine */ }
105
- }
137
+ catch { /* non-fatal — skip files we cannot read */ }
138
+ });
106
139
  return files;
107
140
  }
108
141
  /**
@@ -147,32 +180,51 @@ function writeLocalFolderFile(localPath, relativePath, content, lastSyncedAt) {
147
180
  throw err;
148
181
  }
149
182
  }
183
+ /**
184
+ * Remove the source side of a move from a local-folder pack.
185
+ *
186
+ * Call this only after every destination write has landed, so a partial failure
187
+ * never leaves the artifact at neither level. A path that is also a write target
188
+ * is skipped: a same-path move is a no-op, not a delete of what was just written.
189
+ *
190
+ * Returns the absolute paths actually removed.
191
+ */
192
+ function removeLocalFolderFiles(localPath, deletions, writtenRelativePaths) {
193
+ const written = new Set(writtenRelativePaths);
194
+ const removed = [];
195
+ for (const relativePath of deletions) {
196
+ if (written.has(relativePath))
197
+ continue;
198
+ const target = path_1.default.join(localPath, relativePath);
199
+ if (!fs_1.default.existsSync(target))
200
+ continue;
201
+ fs_1.default.rmSync(target, { force: true });
202
+ removed.push(target);
203
+ }
204
+ return removed;
205
+ }
150
206
  /**
151
207
  * A stable version string for the local-folder backend — a hex digest
152
208
  * of the most recent mtime across all pack files. Changes whenever any
153
209
  * file in the folder is written (by this machine or the sync client).
154
210
  */
155
211
  function localFolderVersion(localPath) {
156
- const mtimes = [];
157
- for (const dirName of exports.LOCAL_FOLDER_PACK_DIRS) {
158
- const dir = path_1.default.join(localPath, dirName);
159
- if (!fs_1.default.existsSync(dir))
160
- continue;
212
+ // Issue #1043: the digest mixes in the pack-relative path, not just mtimes.
213
+ // Two files written in the same filesystem-clock tick produce identical
214
+ // mtimes, so an mtime-only digest could leave the version unchanged after a
215
+ // publish and make sync skip the update.
216
+ const entries = [];
217
+ walkPackFiles(localPath, (rel, abs) => {
218
+ if (isConflictCopy(path_1.default.basename(rel)))
219
+ return;
161
220
  try {
162
- for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
163
- if (!entry.isFile() || isConflictCopy(entry.name))
164
- continue;
165
- try {
166
- mtimes.push(fs_1.default.statSync(path_1.default.join(dir, entry.name)).mtimeMs);
167
- }
168
- catch { /* skip files we can't stat */ }
169
- }
221
+ entries.push(`${rel}:${fs_1.default.statSync(abs).mtimeMs}`);
170
222
  }
171
- catch { /* non-fatal */ }
172
- }
173
- if (mtimes.length === 0)
223
+ catch { /* skip files we can't stat */ }
224
+ });
225
+ if (entries.length === 0)
174
226
  return '0';
175
227
  const hash = crypto_1.default.createHash('sha1');
176
- hash.update(mtimes.sort((a, b) => a - b).join(','));
228
+ hash.update(entries.sort().join(','));
177
229
  return hash.digest('hex').slice(0, 12);
178
230
  }
@@ -3,10 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MANAGER_CACHE_MANAGED_HEADER = exports.MANAGER_SYNC_METADATA_FILE = exports.MANAGER_CACHE_DIRNAME = void 0;
7
- exports.getManagerCacheDir = getManagerCacheDir;
8
- exports.readManagerCacheMetadata = readManagerCacheMetadata;
9
- exports.getManagerCacheAgeHours = getManagerCacheAgeHours;
6
+ exports.MANAGER_MANAGED_HEADER = exports.MANAGER_SYNC_METADATA_FILE = exports.MANAGER_HOME_DIRNAME = void 0;
7
+ exports.getManagerHomeDir = getManagerHomeDir;
8
+ exports.readManagerSyncMetadata = readManagerSyncMetadata;
9
+ exports.getManagerSyncAgeHours = getManagerSyncAgeHours;
10
10
  exports.syncManagerCache = syncManagerCache;
11
11
  /**
12
12
  * Manager cache sync (issue #580).
@@ -25,29 +25,60 @@ const path_1 = __importDefault(require("path"));
25
25
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
26
26
  const manager_pack_1 = require("../../core/manager-pack");
27
27
  const capability_pack_1 = require("../../core/capability-pack");
28
- const git_org_sync_1 = require("./git-org-sync");
28
+ const pack_home_1 = require("./pack-home");
29
29
  const local_folder_sync_1 = require("./local-folder-sync");
30
30
  const user_config_1 = require("./user-config");
31
- exports.MANAGER_CACHE_DIRNAME = 'manager';
31
+ exports.MANAGER_HOME_DIRNAME = 'manager';
32
32
  exports.MANAGER_SYNC_METADATA_FILE = '.manager-sync-metadata.json';
33
- exports.MANAGER_CACHE_MANAGED_HEADER = '<!-- FRAIM_MANAGER_SYNC_MANAGED_CONTENT -->';
34
- const MANAGER_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates', 'scripts'];
35
- function getManagerCacheDir() {
36
- return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.MANAGER_CACHE_DIRNAME);
33
+ exports.MANAGER_MANAGED_HEADER = '<!-- FRAIM_MANAGER_SYNC_MANAGED_CONTENT -->';
34
+ // Issue #1043 adds 'employees' so a published employee record is walked out of
35
+ // a git snapshot. isManagerPackRelativePath still decides what is admitted.
36
+ const MANAGER_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates', 'scripts', 'employees'];
37
+ /**
38
+ * The layer's single home. Named `getManagerHomeDir` for backward compatibility with
39
+ * existing callers; issue #1043 round 2 removed the separate cache it used to
40
+ * point at, so this now resolves the one place the layer lives.
41
+ */
42
+ function getManagerHomeDir() {
43
+ return (0, pack_home_1.resolvePackHome)('manager').root;
44
+ }
45
+ /**
46
+ * Where sync provenance is recorded.
47
+ *
48
+ * Deliberately NOT inside the home. The home is the user's own content: for a
49
+ * git backend it is a real working tree, so a metadata file there shows up as
50
+ * an untracked change in their pack repo, and for a synced folder it would
51
+ * replicate machine-local bookkeeping to every other machine. Provenance is
52
+ * local state, so it belongs in ~/.fraim beside the config.
53
+ */
54
+ function syncMetadataPath() {
55
+ return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.MANAGER_SYNC_METADATA_FILE);
37
56
  }
38
- function readManagerCacheMetadata() {
57
+ /** Record when and from where this machine last synced. Never touches the home. */
58
+ function writeHomeMetadata(metadata) {
39
59
  try {
40
- const metadataPath = path_1.default.join(getManagerCacheDir(), exports.MANAGER_SYNC_METADATA_FILE);
41
- if (!fs_1.default.existsSync(metadataPath))
60
+ fs_1.default.mkdirSync((0, project_fraim_paths_1.getUserFraimDirPath)(), { recursive: true });
61
+ fs_1.default.writeFileSync(syncMetadataPath(), JSON.stringify(metadata, null, 2));
62
+ }
63
+ catch { /* provenance is best-effort; never fail a sync over it */ }
64
+ }
65
+ function readManagerSyncMetadata() {
66
+ try {
67
+ const metadataPath = syncMetadataPath();
68
+ if (fs_1.default.existsSync(metadataPath))
69
+ return JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf8'));
70
+ // A machine that last synced before provenance moved out of the home.
71
+ const legacy = path_1.default.join(getManagerHomeDir(), exports.MANAGER_SYNC_METADATA_FILE);
72
+ if (!fs_1.default.existsSync(legacy))
42
73
  return null;
43
- return JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf8'));
74
+ return JSON.parse(fs_1.default.readFileSync(legacy, 'utf8'));
44
75
  }
45
76
  catch {
46
77
  return null;
47
78
  }
48
79
  }
49
- function getManagerCacheAgeHours() {
50
- const metadata = readManagerCacheMetadata();
80
+ function getManagerSyncAgeHours() {
81
+ const metadata = readManagerSyncMetadata();
51
82
  if (!metadata?.syncedAt)
52
83
  return null;
53
84
  const syncedAt = Date.parse(metadata.syncedAt);
@@ -57,7 +88,7 @@ function getManagerCacheAgeHours() {
57
88
  }
58
89
  function decorateManagedManagerFile(content, backend) {
59
90
  const normalized = content.replace(/^\uFEFF/, '');
60
- if (normalized.startsWith(exports.MANAGER_CACHE_MANAGED_HEADER))
91
+ if (normalized.startsWith(exports.MANAGER_MANAGED_HEADER))
61
92
  return normalized;
62
93
  const writePath = backend === 'git'
63
94
  ? 'open a pull request against your manager storage repo'
@@ -65,7 +96,7 @@ function decorateManagedManagerFile(content, backend) {
65
96
  ? 'edit the file in your synced folder and let the sync client upload it'
66
97
  : 'publish the change through your FRAIM manager storage flow';
67
98
  const marker = [
68
- exports.MANAGER_CACHE_MANAGED_HEADER,
99
+ exports.MANAGER_MANAGED_HEADER,
69
100
  '> [!IMPORTANT]',
70
101
  '> Synced from your personal manager storage. Local edits are overwritten on the next sync.',
71
102
  `> To change this content, ${writePath}.`,
@@ -106,8 +137,8 @@ async function fetchCloudPack(remoteUrl, apiKey) {
106
137
  version: String(response.data?.version ?? '0')
107
138
  };
108
139
  }
109
- function materializeCache(files, metadata) {
110
- const cacheDir = getManagerCacheDir();
140
+ function materializeCloudHome(files, metadata) {
141
+ const cacheDir = getManagerHomeDir();
111
142
  const stagingDir = `${cacheDir}.staging-${process.pid}`;
112
143
  fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
113
144
  fs_1.default.mkdirSync(stagingDir, { recursive: true });
@@ -125,7 +156,7 @@ function materializeCache(files, metadata) {
125
156
  : file.content;
126
157
  fs_1.default.writeFileSync(destination, content);
127
158
  }
128
- fs_1.default.writeFileSync(path_1.default.join(stagingDir, exports.MANAGER_SYNC_METADATA_FILE), JSON.stringify(metadata, null, 2));
159
+ // Provenance is written outside the home by writeHomeMetadata.
129
160
  fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
130
161
  fs_1.default.renameSync(stagingDir, cacheDir);
131
162
  }
@@ -147,12 +178,12 @@ async function discoverCloudManagerStorage(options) {
147
178
  }
148
179
  function failureOutcome(error) {
149
180
  const message = error instanceof Error ? error.message : String(error);
150
- const existing = readManagerCacheMetadata();
181
+ const existing = readManagerSyncMetadata();
151
182
  if (existing) {
152
183
  return {
153
184
  status: 'stale',
154
185
  metadata: existing,
155
- ageHours: getManagerCacheAgeHours() ?? 0,
186
+ ageHours: getManagerSyncAgeHours() ?? 0,
156
187
  error: message
157
188
  };
158
189
  }
@@ -172,32 +203,35 @@ async function syncManagerCache(options) {
172
203
  }
173
204
  try {
174
205
  if (managerStorage.backend === 'local-folder') {
206
+ // Issue #1043 round 2: the configured folder IS the home. There is
207
+ // no cache to materialize into; the OS sync client is the transport
208
+ // and the files are already the single place FRAIM reads.
175
209
  const localPath = managerStorage.localPath;
176
- const files = (0, local_folder_sync_1.collectLocalFolderFiles)(localPath, manager_pack_1.isManagerPackRelativePath);
177
210
  const metadata = {
178
211
  version: (0, local_folder_sync_1.localFolderVersion)(localPath),
179
212
  backend: 'local-folder',
180
213
  source: localPath,
181
214
  syncedAt: new Date().toISOString(),
182
215
  };
183
- materializeCache(files, metadata);
216
+ writeHomeMetadata(metadata);
184
217
  return { status: 'synced', metadata };
185
218
  }
186
219
  if (managerStorage.backend === 'git') {
187
- const snapshot = (0, git_org_sync_1.fetchOrgRepoSnapshot)(managerStorage.gitUrl);
188
- try {
189
- const metadata = {
190
- version: snapshot.sha,
191
- backend: 'git',
192
- source: managerStorage.gitUrl,
193
- syncedAt: new Date().toISOString()
194
- };
195
- materializeCache(collectGitPackFiles(snapshot.dir), metadata);
196
- return { status: 'synced', metadata };
197
- }
198
- finally {
199
- snapshot.cleanup();
200
- }
220
+ // Issue #1043 round 2: the durable clone IS the home. ensurePackClone
221
+ // already fast-forwarded it, so syncing is that pull, not a copy into
222
+ // a second location.
223
+ const home = (0, pack_home_1.resolvePackHome)('manager');
224
+ const clone = (0, pack_home_1.ensurePackClone)(managerStorage.gitUrl, home.root);
225
+ if (clone.sha === null)
226
+ throw new Error(clone.offlineReason ?? 'Pack clone unavailable.');
227
+ const metadata = {
228
+ version: clone.sha,
229
+ backend: 'git',
230
+ source: managerStorage.gitUrl,
231
+ syncedAt: new Date().toISOString()
232
+ };
233
+ writeHomeMetadata(metadata);
234
+ return { status: 'synced', metadata };
201
235
  }
202
236
  const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
203
237
  const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
@@ -210,7 +244,10 @@ async function syncManagerCache(options) {
210
244
  source: remoteUrl,
211
245
  syncedAt: new Date().toISOString()
212
246
  };
213
- materializeCache(pack.files, metadata);
247
+ // The cloud has no user-visible folder, so this managed directory is the
248
+ // home for that backend, not a cache beside one.
249
+ materializeCloudHome(pack.files, metadata);
250
+ writeHomeMetadata(metadata);
214
251
  return { status: 'synced', metadata };
215
252
  }
216
253
  catch (error) {