@open-agent-toolkit/cli 0.0.30 → 0.0.32

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.
Files changed (33) hide show
  1. package/README.md +5 -0
  2. package/assets/docs/cli-utilities/config-and-local-state.md +12 -4
  3. package/assets/docs/provider-sync/commands.md +13 -6
  4. package/assets/docs/provider-sync/index.md +2 -0
  5. package/assets/docs/provider-sync/instruction-sync.md +146 -0
  6. package/assets/docs/provider-sync/scope-and-surface.md +2 -2
  7. package/assets/docs/reference/troubleshooting.md +10 -5
  8. package/assets/public-package-versions.json +4 -4
  9. package/dist/app/create-program.d.ts.map +1 -1
  10. package/dist/app/create-program.js +2 -2
  11. package/dist/commands/docs/init/scaffold.d.ts.map +1 -1
  12. package/dist/commands/docs/init/scaffold.js +12 -11
  13. package/dist/commands/instructions/instructions.types.d.ts +17 -4
  14. package/dist/commands/instructions/instructions.types.d.ts.map +1 -1
  15. package/dist/commands/instructions/instructions.types.js +5 -1
  16. package/dist/commands/instructions/instructions.utils.d.ts +4 -2
  17. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  18. package/dist/commands/instructions/instructions.utils.js +238 -25
  19. package/dist/commands/instructions/sync/sync.d.ts +3 -1
  20. package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
  21. package/dist/commands/instructions/sync/sync.js +176 -16
  22. package/dist/commands/instructions/validate/validate.d.ts +1 -1
  23. package/dist/commands/instructions/validate/validate.d.ts.map +1 -1
  24. package/dist/commands/instructions/validate/validate.js +16 -6
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +12 -4
  28. package/dist/manifest/manager.d.ts.map +1 -1
  29. package/dist/manifest/manager.js +1 -1
  30. package/dist/shared/oat-version.d.ts +2 -0
  31. package/dist/shared/oat-version.d.ts.map +1 -0
  32. package/dist/shared/oat-version.js +3 -0
  33. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
- import { readdir, readFile, stat } from 'node:fs/promises';
2
- import { dirname, join, relative } from 'node:path';
1
+ import { lstat, readdir, readFile, readlink, realpath, stat, } from 'node:fs/promises';
2
+ import { dirname, join, relative, resolve } from 'node:path';
3
3
  export const EXPECTED_CLAUDE_CONTENT = '@AGENTS.md\n';
4
+ export const DEFAULT_INSTRUCTION_SYNC_STRATEGY = 'pointer';
4
5
  const ROOT_EXCLUDED_DIRECTORIES = new Set(['.git', '.oat', '.worktrees']);
5
6
  const GLOBAL_EXCLUDED_DIRECTORIES = new Set(['node_modules']);
6
7
  function getErrorCode(error) {
@@ -11,12 +12,37 @@ function getErrorCode(error) {
11
12
  function normalizeLineEndings(content) {
12
13
  return content.replaceAll('\r\n', '\n');
13
14
  }
15
+ export function resolveInstructionSyncStrategy(strategy) {
16
+ return strategy ?? DEFAULT_INSTRUCTION_SYNC_STRATEGY;
17
+ }
18
+ function getValidInstructionDetail(strategy) {
19
+ switch (strategy) {
20
+ case 'symlink':
21
+ return 'symlink valid';
22
+ case 'copy':
23
+ return 'copy valid';
24
+ default:
25
+ return 'pointer valid';
26
+ }
27
+ }
28
+ function getInvalidInstructionDetail(strategy) {
29
+ switch (strategy) {
30
+ case 'symlink':
31
+ return 'expected symlink to AGENTS.md';
32
+ case 'copy':
33
+ return 'expected hard copy of AGENTS.md content';
34
+ default:
35
+ return `expected ${JSON.stringify(EXPECTED_CLAUDE_CONTENT)}`;
36
+ }
37
+ }
14
38
  function toPosixPath(pathValue) {
15
39
  return pathValue.replaceAll('\\', '/');
16
40
  }
17
41
  function normalizeEntries(entries) {
18
42
  return [...entries].sort((left, right) => {
19
- return (left.agentsPath.localeCompare(right.agentsPath) ||
43
+ const leftSortPath = left.agentsPath ?? left.claudePath;
44
+ const rightSortPath = right.agentsPath ?? right.claudePath;
45
+ return (leftSortPath.localeCompare(rightSortPath) ||
20
46
  left.claudePath.localeCompare(right.claudePath) ||
21
47
  left.status.localeCompare(right.status) ||
22
48
  left.detail.localeCompare(right.detail));
@@ -30,9 +56,23 @@ function normalizeActions(actions) {
30
56
  left.reason.localeCompare(right.reason));
31
57
  });
32
58
  }
33
- async function scanAgentsFiles(repoRoot, dependencies) {
59
+ function recordInstructionFile(directoryEntries, directoryPath, entryName, entryPath) {
60
+ const current = directoryEntries.get(directoryPath) ?? {};
61
+ if (entryName === 'AGENTS.md') {
62
+ current.agentsPath = entryPath;
63
+ current.brokenAgentsPath = undefined;
64
+ current.brokenAgentsErrorCode = undefined;
65
+ }
66
+ else {
67
+ current.claudePath = entryPath;
68
+ current.brokenClaudePath = undefined;
69
+ current.brokenClaudeErrorCode = undefined;
70
+ }
71
+ directoryEntries.set(directoryPath, current);
72
+ }
73
+ async function scanInstructionDirectories(repoRoot, dependencies, debug) {
34
74
  const queue = [repoRoot];
35
- const agentsFiles = [];
75
+ const directoryEntries = new Map();
36
76
  while (queue.length > 0) {
37
77
  const currentDirectory = queue.shift();
38
78
  if (!currentDirectory) {
@@ -46,7 +86,7 @@ async function scanAgentsFiles(repoRoot, dependencies) {
46
86
  }
47
87
  catch (error) {
48
88
  const errorCode = getErrorCode(error);
49
- dependencies.debug?.(`Skipping directory scan for ${toPosixPath(currentDirectory)} (${errorCode ?? 'unknown error'})`);
89
+ debug?.(`Skipping directory scan for ${toPosixPath(currentDirectory)} (${errorCode ?? 'unknown error'})`);
50
90
  continue;
51
91
  }
52
92
  for (const entry of entries) {
@@ -63,8 +103,8 @@ async function scanAgentsFiles(repoRoot, dependencies) {
63
103
  continue;
64
104
  }
65
105
  if (entry.isFile()) {
66
- if (entry.name === 'AGENTS.md') {
67
- agentsFiles.push(entryPath);
106
+ if (entry.name === 'AGENTS.md' || entry.name === 'CLAUDE.md') {
107
+ recordInstructionFile(directoryEntries, currentDirectory, entry.name, entryPath);
68
108
  }
69
109
  continue;
70
110
  }
@@ -77,38 +117,157 @@ async function scanAgentsFiles(repoRoot, dependencies) {
77
117
  }
78
118
  catch (error) {
79
119
  const errorCode = getErrorCode(error);
80
- dependencies.debug?.(`Skipping symlink target stat for ${toPosixPath(entryPath)} (${errorCode ?? 'unknown error'})`);
120
+ if (entry.name === 'CLAUDE.md') {
121
+ const current = directoryEntries.get(currentDirectory) ?? {};
122
+ current.claudePath = entryPath;
123
+ current.brokenClaudePath = entryPath;
124
+ current.brokenClaudeErrorCode = errorCode ?? 'unknown error';
125
+ directoryEntries.set(currentDirectory, current);
126
+ continue;
127
+ }
128
+ if (entry.name === 'AGENTS.md') {
129
+ const current = directoryEntries.get(currentDirectory) ?? {};
130
+ current.brokenAgentsPath = entryPath;
131
+ current.brokenAgentsErrorCode = errorCode ?? 'unknown error';
132
+ directoryEntries.set(currentDirectory, current);
133
+ continue;
134
+ }
135
+ debug?.(`Skipping symlink target stat for ${toPosixPath(entryPath)} (${errorCode ?? 'unknown error'})`);
81
136
  continue;
82
137
  }
83
138
  if (entryStats.isDirectory()) {
84
139
  continue;
85
140
  }
86
- if (entryStats.isFile() && entry.name === 'AGENTS.md') {
87
- agentsFiles.push(entryPath);
141
+ if (entryStats.isFile() &&
142
+ (entry.name === 'AGENTS.md' || entry.name === 'CLAUDE.md')) {
143
+ recordInstructionFile(directoryEntries, currentDirectory, entry.name, entryPath);
88
144
  }
89
145
  }
90
146
  }
91
- return agentsFiles.sort((left, right) => left.localeCompare(right));
147
+ return directoryEntries;
92
148
  }
93
- export async function scanInstructionFiles(repoRoot, overrides = {}) {
149
+ export async function scanInstructionFiles(repoRoot, options = {}, overrides = {}) {
150
+ const strategy = resolveInstructionSyncStrategy(options.strategy);
94
151
  const dependencies = {
152
+ lstat,
153
+ realpath,
95
154
  readdir,
96
155
  readFile,
156
+ readlink,
97
157
  stat,
98
158
  ...overrides,
99
159
  };
100
- const agentsFiles = await scanAgentsFiles(repoRoot, dependencies);
160
+ const instructionDirectories = await scanInstructionDirectories(repoRoot, dependencies, options.debug);
101
161
  const entries = [];
102
- for (const agentsPath of agentsFiles) {
103
- const claudePath = join(dirname(agentsPath), 'CLAUDE.md');
162
+ for (const [directoryPath, directoryEntry] of instructionDirectories) {
163
+ const agentsPath = directoryEntry.agentsPath ?? null;
164
+ const brokenAgentsPath = directoryEntry.brokenAgentsPath ?? null;
165
+ const brokenAgentsErrorCode = directoryEntry.brokenAgentsErrorCode ?? 'unknown error';
166
+ const brokenClaudePath = directoryEntry.brokenClaudePath ?? null;
167
+ const brokenClaudeErrorCode = directoryEntry.brokenClaudeErrorCode ?? 'unknown error';
168
+ const claudePath = directoryEntry.claudePath ?? join(directoryPath, 'CLAUDE.md');
169
+ if (!agentsPath && brokenAgentsPath) {
170
+ entries.push({
171
+ agentsPath: brokenAgentsPath,
172
+ claudePath,
173
+ status: 'content_mismatch',
174
+ detail: brokenAgentsErrorCode === 'ENOENT'
175
+ ? 'broken AGENTS.md symlink'
176
+ : `unreadable AGENTS.md symlink target (${brokenAgentsErrorCode})`,
177
+ });
178
+ continue;
179
+ }
180
+ if (brokenClaudePath) {
181
+ entries.push({
182
+ agentsPath,
183
+ claudePath,
184
+ status: 'content_mismatch',
185
+ detail: brokenClaudeErrorCode === 'ENOENT'
186
+ ? 'broken CLAUDE.md symlink'
187
+ : `unreadable CLAUDE.md symlink target (${brokenClaudeErrorCode})`,
188
+ });
189
+ continue;
190
+ }
191
+ if (!agentsPath) {
192
+ try {
193
+ await dependencies.readFile(claudePath, 'utf8');
194
+ }
195
+ catch (error) {
196
+ entries.push({
197
+ agentsPath: null,
198
+ claudePath,
199
+ status: 'content_mismatch',
200
+ detail: `unable to read CLAUDE.md (${getErrorCode(error) ?? 'unknown'})`,
201
+ });
202
+ continue;
203
+ }
204
+ entries.push({
205
+ agentsPath: null,
206
+ claudePath,
207
+ status: 'stray',
208
+ detail: 'CLAUDE.md found without AGENTS.md',
209
+ });
210
+ continue;
211
+ }
212
+ let claudeStats;
104
213
  try {
105
- const claudeContent = await dependencies.readFile(claudePath, 'utf8');
106
- if (normalizeLineEndings(claudeContent) === EXPECTED_CLAUDE_CONTENT) {
214
+ claudeStats = await dependencies.lstat(claudePath);
215
+ }
216
+ catch (error) {
217
+ const errorCode = getErrorCode(error);
218
+ if (errorCode === 'ENOENT') {
219
+ entries.push({
220
+ agentsPath,
221
+ claudePath,
222
+ status: 'missing',
223
+ detail: 'CLAUDE.md missing',
224
+ });
225
+ }
226
+ else {
227
+ entries.push({
228
+ agentsPath,
229
+ claudePath,
230
+ status: 'content_mismatch',
231
+ detail: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
232
+ });
233
+ }
234
+ continue;
235
+ }
236
+ if (strategy === 'symlink') {
237
+ if (!claudeStats.isSymbolicLink()) {
238
+ entries.push({
239
+ agentsPath,
240
+ claudePath,
241
+ status: 'content_mismatch',
242
+ detail: getInvalidInstructionDetail(strategy),
243
+ });
244
+ continue;
245
+ }
246
+ let claudeTarget;
247
+ try {
248
+ claudeTarget = await dependencies.readlink(claudePath);
249
+ }
250
+ catch (error) {
251
+ const errorCode = getErrorCode(error);
252
+ entries.push({
253
+ agentsPath,
254
+ claudePath,
255
+ status: 'content_mismatch',
256
+ detail: `unable to read CLAUDE.md symlink target (${errorCode ?? 'unknown error'})`,
257
+ });
258
+ continue;
259
+ }
260
+ const resolvedTarget = resolve(dirname(claudePath), claudeTarget);
261
+ const [canonicalTarget, canonicalAgentsPath] = await Promise.all([
262
+ dependencies.realpath(resolvedTarget).catch(() => resolvedTarget),
263
+ dependencies.realpath(agentsPath).catch(() => agentsPath),
264
+ ]);
265
+ if (canonicalTarget === canonicalAgentsPath) {
107
266
  entries.push({
108
267
  agentsPath,
109
268
  claudePath,
110
269
  status: 'ok',
111
- detail: 'pointer valid',
270
+ detail: getValidInstructionDetail(strategy),
112
271
  });
113
272
  }
114
273
  else {
@@ -116,9 +275,23 @@ export async function scanInstructionFiles(repoRoot, overrides = {}) {
116
275
  agentsPath,
117
276
  claudePath,
118
277
  status: 'content_mismatch',
119
- detail: `expected ${JSON.stringify(EXPECTED_CLAUDE_CONTENT)}`,
278
+ detail: getInvalidInstructionDetail(strategy),
120
279
  });
121
280
  }
281
+ continue;
282
+ }
283
+ if (claudeStats.isSymbolicLink()) {
284
+ entries.push({
285
+ agentsPath,
286
+ claudePath,
287
+ status: 'content_mismatch',
288
+ detail: getInvalidInstructionDetail(strategy),
289
+ });
290
+ continue;
291
+ }
292
+ let claudeContent;
293
+ try {
294
+ claudeContent = await dependencies.readFile(claudePath, 'utf8');
122
295
  }
123
296
  catch (error) {
124
297
  const errorCode = getErrorCode(error);
@@ -138,6 +311,44 @@ export async function scanInstructionFiles(repoRoot, overrides = {}) {
138
311
  detail: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
139
312
  });
140
313
  }
314
+ continue;
315
+ }
316
+ const expectedContent = strategy === 'copy'
317
+ ? await (async () => {
318
+ try {
319
+ return await dependencies.readFile(agentsPath, 'utf8');
320
+ }
321
+ catch (error) {
322
+ const errorCode = getErrorCode(error);
323
+ entries.push({
324
+ agentsPath,
325
+ claudePath,
326
+ status: 'content_mismatch',
327
+ detail: `unable to read AGENTS.md (${errorCode ?? 'unknown error'})`,
328
+ });
329
+ return null;
330
+ }
331
+ })()
332
+ : EXPECTED_CLAUDE_CONTENT;
333
+ if (expectedContent === null) {
334
+ continue;
335
+ }
336
+ if (normalizeLineEndings(claudeContent) ===
337
+ normalizeLineEndings(expectedContent)) {
338
+ entries.push({
339
+ agentsPath,
340
+ claudePath,
341
+ status: 'ok',
342
+ detail: getValidInstructionDetail(strategy),
343
+ });
344
+ }
345
+ else {
346
+ entries.push({
347
+ agentsPath,
348
+ claudePath,
349
+ status: 'content_mismatch',
350
+ detail: getInvalidInstructionDetail(strategy),
351
+ });
141
352
  }
142
353
  }
143
354
  return normalizeEntries(entries);
@@ -151,6 +362,7 @@ export function buildInstructionsSummary(entries, actions) {
151
362
  missing: normalizedEntries.filter((entry) => entry.status === 'missing')
152
363
  .length,
153
364
  contentMismatch: normalizedEntries.filter((entry) => entry.status === 'content_mismatch').length,
365
+ stray: normalizedEntries.filter((entry) => entry.status === 'stray').length,
154
366
  created: normalizedActions.filter((action) => action.type === 'create')
155
367
  .length,
156
368
  updated: normalizedActions.filter((action) => action.type === 'update')
@@ -181,7 +393,7 @@ export function formatInstructionsReport(payload, repoRoot) {
181
393
  const lines = [
182
394
  `instructions ${payload.mode}`,
183
395
  `status: ${payload.status}`,
184
- `summary: scanned=${payload.summary.scanned}, ok=${payload.summary.ok}, missing=${payload.summary.missing}, content_mismatch=${payload.summary.contentMismatch}, created=${payload.summary.created}, updated=${payload.summary.updated}, skipped=${payload.summary.skipped}`,
396
+ `summary: scanned=${payload.summary.scanned}, ok=${payload.summary.ok}, missing=${payload.summary.missing}, content_mismatch=${payload.summary.contentMismatch}, stray=${payload.summary.stray}, created=${payload.summary.created}, updated=${payload.summary.updated}, skipped=${payload.summary.skipped}`,
185
397
  ];
186
398
  if (payload.entries.length === 0) {
187
399
  lines.push('entries: none');
@@ -189,10 +401,11 @@ export function formatInstructionsReport(payload, repoRoot) {
189
401
  else {
190
402
  lines.push('entries:');
191
403
  for (const entry of payload.entries) {
192
- const agentsPath = repoRoot
193
- ? toPosixPath(relative(repoRoot, entry.agentsPath)) || '.'
194
- : toPosixPath(entry.agentsPath);
195
- lines.push(`- ${agentsPath} -> ${entry.status} (${entry.detail})`);
404
+ const displayPath = entry.agentsPath ?? entry.claudePath;
405
+ const relativePath = repoRoot
406
+ ? toPosixPath(relative(repoRoot, displayPath)) || '.'
407
+ : toPosixPath(displayPath);
408
+ lines.push(`- ${relativePath} -> ${entry.status} (${entry.detail})`);
196
409
  }
197
410
  }
198
411
  if (payload.actions.length === 0) {
@@ -1,4 +1,6 @@
1
- import type { InstructionsSyncCommandDependencies } from '../../instructions/instructions.types.js';
1
+ import { rm } from 'node:fs/promises';
2
+ import { type InstructionsSyncCommandDependencies } from '../../instructions/instructions.types.js';
2
3
  import { Command } from 'commander';
4
+ export declare function removeInstructionFile(path: string, remove?: typeof rm): Promise<void>;
3
5
  export declare function createInstructionsSyncCommand(overrides?: Partial<InstructionsSyncCommandDependencies>): Command;
4
6
  //# sourceMappingURL=sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/sync/sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,mCAAmC,EACpC,MAAM,2CAA2C,CAAC;AAUnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmHpC,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,mCAAmC,CAAM,GAC3D,OAAO,CA+DT"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/sync/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,EAAE,EAAsB,MAAM,kBAAkB,CAAC;AAI3E,OAAO,EAKL,KAAK,mCAAmC,EACzC,MAAM,2CAA2C,CAAC;AAYnD,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAQ5C,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,IAAI,CAAC,CAEf;AA+SD,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,mCAAmC,CAAM,GAC3D,OAAO,CA6FT"}
@@ -1,26 +1,96 @@
1
- import { writeFile } from 'node:fs/promises';
1
+ import { lstat, readFile, rm, symlink, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, relative } from 'node:path';
2
3
  import { buildCommandContext } from '../../../app/command-context.js';
3
- import { buildInstructionsPayload, EXPECTED_CLAUDE_CONTENT, formatInstructionsReport, scanInstructionFiles, } from '../../instructions/instructions.utils.js';
4
+ import { INSTRUCTION_SYNC_STRATEGIES, } from '../../instructions/instructions.types.js';
5
+ import { buildInstructionsPayload, DEFAULT_INSTRUCTION_SYNC_STRATEGY, EXPECTED_CLAUDE_CONTENT, formatInstructionsReport, resolveInstructionSyncStrategy, scanInstructionFiles, } from '../../instructions/instructions.utils.js';
4
6
  import { readGlobalOptions } from '../../shared/shared.utils.js';
5
7
  import { CliError } from '../../../errors/cli-error.js';
6
8
  import { resolveProjectRoot } from '../../../fs/paths.js';
7
- import { Command } from 'commander';
9
+ import { Command, Option } from 'commander';
10
+ export async function removeInstructionFile(path, remove = rm) {
11
+ await remove(path, { force: true });
12
+ }
8
13
  function defaultDependencies() {
9
14
  return {
10
15
  buildCommandContext,
16
+ lstat,
17
+ readFile,
18
+ removeFile: removeInstructionFile,
11
19
  resolveProjectRoot,
12
20
  scanInstructionFiles,
21
+ symlinkFile: async (target, path) => {
22
+ await symlink(target, path, 'file');
23
+ },
13
24
  writeFile,
14
25
  };
15
26
  }
16
- function planSyncActions({ entries, force, }) {
27
+ function getSyncReason(actionType, strategy) {
28
+ const label = strategy === 'symlink'
29
+ ? 'symlink'
30
+ : strategy === 'copy'
31
+ ? 'hard copy'
32
+ : 'pointer file';
33
+ return actionType === 'create'
34
+ ? `missing CLAUDE.md ${label}`
35
+ : `overwrite CLAUDE.md with canonical ${label}`;
36
+ }
37
+ function getSyncedDetail(strategy) {
38
+ switch (strategy) {
39
+ case 'symlink':
40
+ return 'symlink synced';
41
+ case 'copy':
42
+ return 'copy synced';
43
+ default:
44
+ return 'pointer synced';
45
+ }
46
+ }
47
+ function getAgentsPath(entry) {
48
+ return entry.agentsPath ?? join(dirname(entry.claudePath), 'AGENTS.md');
49
+ }
50
+ function getErrorCode(error) {
51
+ return error && typeof error === 'object' && 'code' in error
52
+ ? String(error.code)
53
+ : null;
54
+ }
55
+ function hasUnreadableCanonicalAgents(entry) {
56
+ return (entry.agentsPath !== null &&
57
+ (entry.detail === 'broken AGENTS.md symlink' ||
58
+ entry.detail.startsWith('unreadable AGENTS.md symlink target') ||
59
+ entry.detail.startsWith('unable to read AGENTS.md')));
60
+ }
61
+ function hasUnreadableClaudeSource(entry) {
62
+ return (entry.agentsPath === null &&
63
+ (entry.detail === 'broken CLAUDE.md symlink' ||
64
+ entry.detail.startsWith('unreadable CLAUDE.md symlink target') ||
65
+ entry.detail.startsWith('unable to read CLAUDE.md')));
66
+ }
67
+ function wrapStrayResyncError(error, agentsPath, claudePath) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ return new CliError(`Adopted stray instructions into ${agentsPath}, but failed to regenerate ${claudePath}: ${message}`, error instanceof CliError ? error.exitCode : 2);
70
+ }
71
+ function planSyncActions({ entries, force, strategy, }) {
17
72
  const actions = [];
18
73
  for (const entry of entries) {
74
+ if (entry.status === 'stray') {
75
+ actions.push({
76
+ type: 'create',
77
+ target: getAgentsPath(entry),
78
+ reason: 'adopt stray CLAUDE.md into canonical AGENTS.md',
79
+ result: 'planned',
80
+ });
81
+ actions.push({
82
+ type: 'update',
83
+ target: entry.claudePath,
84
+ reason: getSyncReason('update', strategy),
85
+ result: 'planned',
86
+ });
87
+ continue;
88
+ }
19
89
  if (entry.status === 'missing') {
20
90
  actions.push({
21
91
  type: 'create',
22
92
  target: entry.claudePath,
23
- reason: 'missing CLAUDE.md pointer file',
93
+ reason: getSyncReason('create', strategy),
24
94
  result: 'planned',
25
95
  });
26
96
  continue;
@@ -28,6 +98,28 @@ function planSyncActions({ entries, force, }) {
28
98
  if (entry.status !== 'content_mismatch') {
29
99
  continue;
30
100
  }
101
+ if (hasUnreadableClaudeSource(entry)) {
102
+ actions.push({
103
+ type: 'skip',
104
+ target: entry.claudePath,
105
+ reason: 'CLAUDE.md unreadable; repair manually',
106
+ result: 'skipped',
107
+ });
108
+ continue;
109
+ }
110
+ if (hasUnreadableCanonicalAgents(entry)) {
111
+ const unreadableAgentsPath = entry.agentsPath;
112
+ if (!unreadableAgentsPath) {
113
+ throw new CliError(`Unable to resolve unreadable AGENTS.md path for ${entry.claudePath}`, 2);
114
+ }
115
+ actions.push({
116
+ type: 'skip',
117
+ target: unreadableAgentsPath,
118
+ reason: 'canonical AGENTS.md unreadable; repair manually',
119
+ result: 'skipped',
120
+ });
121
+ continue;
122
+ }
31
123
  if (!force) {
32
124
  actions.push({
33
125
  type: 'skip',
@@ -40,20 +132,76 @@ function planSyncActions({ entries, force, }) {
40
132
  actions.push({
41
133
  type: 'update',
42
134
  target: entry.claudePath,
43
- reason: 'overwrite CLAUDE.md with canonical pointer',
135
+ reason: getSyncReason('update', strategy),
44
136
  result: 'planned',
45
137
  });
46
138
  }
47
139
  return actions;
48
140
  }
49
- async function applySyncActions(actions, dependencies) {
141
+ async function applySyncActions(actions, entries, dependencies, strategy) {
50
142
  const appliedActions = [];
143
+ const entriesByTarget = new Map();
144
+ for (const entry of entries) {
145
+ entriesByTarget.set(entry.claudePath, entry);
146
+ entriesByTarget.set(getAgentsPath(entry), entry);
147
+ }
51
148
  for (const action of actions) {
52
149
  if (action.result !== 'planned') {
53
150
  appliedActions.push(action);
54
151
  continue;
55
152
  }
56
- await dependencies.writeFile(action.target, EXPECTED_CLAUDE_CONTENT, 'utf8');
153
+ const entry = entriesByTarget.get(action.target);
154
+ if (!entry) {
155
+ throw new CliError(`Unable to resolve instruction entry for ${action.target}`, 2);
156
+ }
157
+ const agentsPath = getAgentsPath(entry);
158
+ const isAgentsAction = action.target === agentsPath;
159
+ if (isAgentsAction) {
160
+ try {
161
+ await dependencies.lstat(agentsPath);
162
+ throw new CliError(`Canonical AGENTS.md appeared during sync at ${agentsPath}; re-run to reclassify before adopting stray CLAUDE.md`, 2);
163
+ }
164
+ catch (error) {
165
+ if (error instanceof CliError) {
166
+ throw error;
167
+ }
168
+ if (getErrorCode(error) !== 'ENOENT') {
169
+ throw error;
170
+ }
171
+ }
172
+ const adoptedContent = await dependencies.readFile(entry.claudePath, 'utf8');
173
+ await dependencies.writeFile(agentsPath, adoptedContent, 'utf8');
174
+ appliedActions.push({
175
+ ...action,
176
+ result: 'applied',
177
+ });
178
+ continue;
179
+ }
180
+ if (!entry.agentsPath && action.type !== 'update') {
181
+ throw new CliError(`Unable to resolve AGENTS.md for ${action.target}`, 2);
182
+ }
183
+ try {
184
+ if (action.type === 'update') {
185
+ await dependencies.removeFile(action.target);
186
+ }
187
+ if (strategy === 'symlink') {
188
+ const symlinkTarget = relative(dirname(action.target), agentsPath);
189
+ await dependencies.symlinkFile(symlinkTarget, action.target);
190
+ }
191
+ else if (strategy === 'copy') {
192
+ const agentsContent = await dependencies.readFile(agentsPath, 'utf8');
193
+ await dependencies.writeFile(action.target, agentsContent, 'utf8');
194
+ }
195
+ else {
196
+ await dependencies.writeFile(action.target, EXPECTED_CLAUDE_CONTENT, 'utf8');
197
+ }
198
+ }
199
+ catch (error) {
200
+ if (entry.status === 'stray') {
201
+ throw wrapStrayResyncError(error, agentsPath, action.target);
202
+ }
203
+ throw error;
204
+ }
57
205
  appliedActions.push({
58
206
  ...action,
59
207
  result: 'applied',
@@ -61,18 +209,21 @@ async function applySyncActions(actions, dependencies) {
61
209
  }
62
210
  return appliedActions;
63
211
  }
64
- function getPostSyncEntries(entries, actions) {
212
+ function getPostSyncEntries(entries, actions, strategy) {
65
213
  const actionByTarget = new Map(actions.map((action) => [action.target, action]));
66
214
  return entries.map((entry) => {
67
215
  const action = actionByTarget.get(entry.claudePath);
68
- if (!action) {
216
+ const adoptedAction = actionByTarget.get(getAgentsPath(entry));
217
+ if (!action && !adoptedAction) {
69
218
  return entry;
70
219
  }
71
- if (action.result === 'applied' && action.type !== 'skip') {
220
+ if (action?.result === 'applied' &&
221
+ (entry.status !== 'stray' || adoptedAction?.result === 'applied')) {
72
222
  return {
73
223
  ...entry,
224
+ agentsPath: getAgentsPath(entry),
74
225
  status: 'ok',
75
- detail: 'pointer synced',
226
+ detail: getSyncedDetail(strategy),
76
227
  };
77
228
  }
78
229
  return entry;
@@ -87,25 +238,34 @@ export function createInstructionsSyncCommand(overrides = {}) {
87
238
  ...overrides,
88
239
  };
89
240
  return new Command('sync')
90
- .description('Repair AGENTS.md to CLAUDE.md pointer drift')
241
+ .description('Repair AGENTS.md/CLAUDE.md sync drift using the selected strategy')
91
242
  .option('--dry-run', 'Preview sync changes without applying')
92
243
  .option('--force', 'Overwrite mismatched CLAUDE.md files')
244
+ .addOption(new Option('--strategy <strategy>', 'Sync strategy')
245
+ .choices([...INSTRUCTION_SYNC_STRATEGIES])
246
+ .default(DEFAULT_INSTRUCTION_SYNC_STRATEGY))
93
247
  .action(async (options, command) => {
94
248
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
95
249
  try {
96
250
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
97
- const entries = await dependencies.scanInstructionFiles(repoRoot);
251
+ const strategy = resolveInstructionSyncStrategy(options.strategy);
252
+ const entries = await dependencies.scanInstructionFiles(repoRoot, {
253
+ strategy,
254
+ });
98
255
  const plannedActions = planSyncActions({
99
256
  entries,
100
257
  force: options.force ?? false,
258
+ strategy,
101
259
  });
102
260
  const dryRun = options.dryRun ?? false;
103
261
  const actions = dryRun
104
262
  ? plannedActions
105
- : await applySyncActions(plannedActions, dependencies);
263
+ : await applySyncActions(plannedActions, entries, dependencies, strategy);
106
264
  const payload = buildInstructionsPayload({
107
265
  mode: dryRun ? 'dry-run' : 'apply',
108
- entries: dryRun ? entries : getPostSyncEntries(entries, actions),
266
+ entries: dryRun
267
+ ? entries
268
+ : getPostSyncEntries(entries, actions, strategy),
109
269
  actions,
110
270
  });
111
271
  if (context.json) {
@@ -1,4 +1,4 @@
1
- import type { InstructionsValidateCommandDependencies } from '../../instructions/instructions.types.js';
1
+ import { type InstructionsValidateCommandDependencies } from '../../instructions/instructions.types.js';
2
2
  import { Command } from 'commander';
3
3
  export declare function createInstructionsValidateCommand(overrides?: Partial<InstructionsValidateCommandDependencies>): Command;
4
4
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/validate/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,2CAA2C,CAAC;AASzG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,uCAAuC,CAAM,GAC/D,OAAO,CA0CT"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/validate/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,uCAAuC,EAC7C,MAAM,2CAA2C,CAAC;AAWnD,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAU5C,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,uCAAuC,CAAM,GAC/D,OAAO,CA2DT"}