@open-agent-toolkit/cli 0.3.8 → 0.3.10

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 (121) hide show
  1. package/README.md +3 -3
  2. package/assets/agents/oat-codebase-mapper.md +3 -2
  3. package/assets/agents/oat-reviewer.md +5 -13
  4. package/assets/agents/skeptical-evaluator.md +6 -2
  5. package/assets/bundle-metadata.json +1 -1
  6. package/assets/config/dispatch-matrix-recommendation.json +6 -6
  7. package/assets/docs/cli-utilities/backlog-lifecycle.md +3 -2
  8. package/assets/docs/cli-utilities/bootstrap.md +5 -2
  9. package/assets/docs/cli-utilities/config-and-local-state.md +13 -6
  10. package/assets/docs/cli-utilities/configuration.md +22 -17
  11. package/assets/docs/cli-utilities/tool-packs.md +60 -10
  12. package/assets/docs/cli-utilities/workflow-gates.md +1 -1
  13. package/assets/docs/contributing/code.md +9 -5
  14. package/assets/docs/contributing/updating-model-guidance.md +34 -6
  15. package/assets/docs/contributing/verifying-cursor-pins.md +38 -9
  16. package/assets/docs/provider-sync/commands.md +11 -8
  17. package/assets/docs/provider-sync/instruction-sync.md +241 -39
  18. package/assets/docs/provider-sync/providers.md +4 -4
  19. package/assets/docs/provider-sync/scope-and-surface.md +1 -1
  20. package/assets/docs/reference/cli-reference.md +2 -0
  21. package/assets/docs/reference/oat-directory-structure.md +12 -11
  22. package/assets/docs/reference/troubleshooting.md +19 -4
  23. package/assets/docs/workflows/projects/dispatch-ceiling.md +21 -14
  24. package/assets/docs/workflows/projects/implementation-execution.md +26 -9
  25. package/assets/docs/workflows/projects/lifecycle.md +4 -6
  26. package/assets/public-package-versions.json +4 -4
  27. package/assets/skills/oat-agent-instructions-analyze/SKILL.md +4 -4
  28. package/assets/skills/oat-agent-instructions-analyze/references/analysis-artifact-template.md +5 -5
  29. package/assets/skills/oat-agent-instructions-analyze/references/docs/rules-files.md +14 -11
  30. package/assets/skills/oat-agent-instructions-analyze/references/quality-checklist.md +2 -2
  31. package/assets/skills/oat-agent-instructions-analyze/scripts/resolve-instruction-files.sh +4 -3
  32. package/assets/skills/oat-agent-instructions-apply/SKILL.md +3 -3
  33. package/assets/skills/oat-agent-instructions-apply/references/docs/rules-files.md +14 -11
  34. package/assets/skills/oat-dispatch-subagents/SKILL.md +1 -1
  35. package/assets/skills/oat-dispatch-subagents/references/provider-cursor.md +5 -2
  36. package/assets/skills/oat-doctor/SKILL.md +17 -13
  37. package/assets/skills/oat-project-implement/SKILL.md +2 -2
  38. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +33 -11
  39. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +1 -1
  40. package/assets/skills/oat-project-implement/references/phase-execution.md +109 -8
  41. package/assets/skills/oat-project-lite/SKILL.md +23 -1
  42. package/assets/skills/oat-project-next/SKILL.md +3 -3
  43. package/assets/skills/oat-project-plan-writing/SKILL.md +3 -3
  44. package/assets/skills/oat-project-progress/SKILL.md +2 -2
  45. package/assets/skills/subagent-orchestration/SKILL.md +1 -1
  46. package/assets/skills/subagent-orchestration/references/evidence-and-refresh.md +20 -8
  47. package/assets/skills/subagent-orchestration/references/provider-claude.md +25 -12
  48. package/assets/skills/subagent-orchestration/references/provider-codex.md +27 -18
  49. package/assets/skills/subagent-orchestration/references/provider-cursor.md +40 -10
  50. package/assets/templates/docs-app-mkdocs/docs/contributing.md +1 -3
  51. package/assets/templates/pjm-agents.md +7 -3
  52. package/assets/templates/state.md +2 -2
  53. package/dist/commands/backlog/archive.d.ts +10 -2
  54. package/dist/commands/backlog/archive.d.ts.map +1 -1
  55. package/dist/commands/backlog/archive.js +22 -6
  56. package/dist/commands/backlog/index.d.ts.map +1 -1
  57. package/dist/commands/backlog/index.js +5 -1
  58. package/dist/commands/backlog/rewrite-references.d.ts +41 -0
  59. package/dist/commands/backlog/rewrite-references.d.ts.map +1 -0
  60. package/dist/commands/backlog/rewrite-references.js +516 -0
  61. package/dist/commands/config/index.d.ts +1 -0
  62. package/dist/commands/config/index.d.ts.map +1 -1
  63. package/dist/commands/config/index.js +69 -34
  64. package/dist/commands/init/index.d.ts +6 -0
  65. package/dist/commands/init/index.d.ts.map +1 -1
  66. package/dist/commands/init/index.js +27 -4
  67. package/dist/commands/init/tools/index.d.ts +18 -1
  68. package/dist/commands/init/tools/index.d.ts.map +1 -1
  69. package/dist/commands/init/tools/index.js +84 -17
  70. package/dist/commands/init/tools/project-guidance.d.ts +19 -4
  71. package/dist/commands/init/tools/project-guidance.d.ts.map +1 -1
  72. package/dist/commands/init/tools/project-guidance.js +58 -15
  73. package/dist/commands/init/tools/workflows/index.d.ts.map +1 -1
  74. package/dist/commands/init/tools/workflows/index.js +5 -1
  75. package/dist/commands/instructions/instructions.types.d.ts +105 -4
  76. package/dist/commands/instructions/instructions.types.d.ts.map +1 -1
  77. package/dist/commands/instructions/instructions.types.js +4 -5
  78. package/dist/commands/instructions/instructions.utils.d.ts +161 -6
  79. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  80. package/dist/commands/instructions/instructions.utils.js +650 -12
  81. package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
  82. package/dist/commands/instructions/sync/sync.js +256 -18
  83. package/dist/commands/instructions/validate/validate.d.ts.map +1 -1
  84. package/dist/commands/instructions/validate/validate.js +36 -8
  85. package/dist/commands/pjm/index.d.ts.map +1 -1
  86. package/dist/commands/pjm/index.js +19 -4
  87. package/dist/commands/pjm/init.d.ts +3 -0
  88. package/dist/commands/pjm/init.d.ts.map +1 -1
  89. package/dist/commands/pjm/init.js +23 -5
  90. package/dist/commands/shared/agents-md.d.ts +26 -5
  91. package/dist/commands/shared/agents-md.d.ts.map +1 -1
  92. package/dist/commands/shared/agents-md.js +182 -3
  93. package/dist/commands/tools/guidance/index.d.ts +15 -0
  94. package/dist/commands/tools/guidance/index.d.ts.map +1 -0
  95. package/dist/commands/tools/guidance/index.js +81 -0
  96. package/dist/commands/tools/index.d.ts.map +1 -1
  97. package/dist/commands/tools/index.js +2 -0
  98. package/dist/config/oat-config.d.ts +49 -3
  99. package/dist/config/oat-config.d.ts.map +1 -1
  100. package/dist/config/oat-config.js +107 -14
  101. package/dist/config/resolve.d.ts.map +1 -1
  102. package/dist/config/resolve.js +12 -2
  103. package/dist/providers/claude/dispatch-envelope.d.ts.map +1 -1
  104. package/dist/providers/claude/dispatch-envelope.js +1 -0
  105. package/dist/providers/claude/targets.d.ts +1 -0
  106. package/dist/providers/claude/targets.d.ts.map +1 -1
  107. package/dist/providers/claude/targets.js +3 -1
  108. package/dist/providers/codex/codec/shared.d.ts +3 -0
  109. package/dist/providers/codex/codec/shared.d.ts.map +1 -1
  110. package/dist/providers/codex/codec/shared.js +4 -0
  111. package/dist/providers/cursor/codec/catalog.d.ts +2 -2
  112. package/dist/providers/cursor/codec/catalog.d.ts.map +1 -1
  113. package/dist/providers/cursor/codec/catalog.js +45 -0
  114. package/dist/providers/cursor/codec/mapping-validation.d.ts +4 -0
  115. package/dist/providers/cursor/codec/mapping-validation.d.ts.map +1 -0
  116. package/dist/providers/cursor/codec/mapping-validation.js +25 -0
  117. package/dist/providers/cursor/codec/materialize.d.ts.map +1 -1
  118. package/dist/providers/cursor/codec/materialize.js +2 -11
  119. package/dist/providers/cursor/codec/sync-extension.d.ts.map +1 -1
  120. package/dist/providers/cursor/codec/sync-extension.js +2 -11
  121. package/package.json +2 -2
@@ -0,0 +1,516 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { constants as fsConstants, lstatSync } from 'node:fs';
4
+ import { lstat, open as openFile, readdir, realpath, rename, unlink, } from 'node:fs/promises';
5
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ const execFileAsync = promisify(execFile);
8
+ /** Characters that end a path-like token when scanning Markdown text. */
9
+ const TOKEN_DELIMITERS = new Set([
10
+ ' ',
11
+ '\t',
12
+ '\n',
13
+ '\r',
14
+ '(',
15
+ ')',
16
+ '[',
17
+ ']',
18
+ '<',
19
+ '>',
20
+ '"',
21
+ "'",
22
+ '`',
23
+ '|',
24
+ ',',
25
+ '=',
26
+ ]);
27
+ const SKIPPED_DIRECTORIES = new Set(['.git', 'node_modules']);
28
+ function toPosix(path) {
29
+ return path.split(sep).join('/');
30
+ }
31
+ /**
32
+ * Derive the scan layout from the backlog root. The canonical backlog lives at
33
+ * `<repo>/.oat/repo/pjm/backlog`; a non-canonical `--backlog-root` scans only
34
+ * the backlog root itself.
35
+ */
36
+ function resolveLayout(backlogRoot) {
37
+ const backlog = resolve(backlogRoot);
38
+ const pjm = dirname(backlog);
39
+ if (basename(backlog) !== 'backlog' || basename(pjm) !== 'pjm') {
40
+ return { scanRoot: backlog, repoRoot: null };
41
+ }
42
+ const scanRoot = dirname(pjm);
43
+ const oatDir = dirname(scanRoot);
44
+ const repoRoot = basename(scanRoot) === 'repo' && basename(oatDir) === '.oat'
45
+ ? dirname(oatDir)
46
+ : null;
47
+ return { scanRoot, repoRoot };
48
+ }
49
+ async function isInsideGitWorkTree(cwd) {
50
+ try {
51
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd });
52
+ return stdout.trim() === 'true';
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ /**
59
+ * True only for a regular file (never a symlink) whose real path stays inside
60
+ * `realScanRoot`, so the scan can never read or write outside `.oat/repo`.
61
+ */
62
+ async function isRegularFileInside(path, realScanRoot) {
63
+ try {
64
+ if (!(await lstat(path)).isFile()) {
65
+ return false;
66
+ }
67
+ const real = await realpath(path);
68
+ return real === realScanRoot || real.startsWith(`${realScanRoot}${sep}`);
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ /** True when anything (file, directory, or link) exists at `path`. */
75
+ function pathExistsSync(path) {
76
+ try {
77
+ lstatSync(path);
78
+ return true;
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ /** `O_NOFOLLOW` where the platform has it: refuse to open through a symlink. */
85
+ const NO_FOLLOW = fsConstants.O_NOFOLLOW ?? 0;
86
+ async function readNoFollow(path) {
87
+ const handle = await openFile(path, fsConstants.O_RDONLY | NO_FOLLOW);
88
+ try {
89
+ const info = await handle.stat();
90
+ return {
91
+ content: await handle.readFile('utf8'),
92
+ identity: { dev: info.dev, ino: info.ino, mode: info.mode },
93
+ };
94
+ }
95
+ finally {
96
+ await handle.close();
97
+ }
98
+ }
99
+ /**
100
+ * Replace `path` with `content` without ever writing to its existing inode:
101
+ * write a temporary file in the same (already verified, in-tree) directory,
102
+ * then rename it over the original. Truncating in place would write through
103
+ * every other hard link to the same inode, including one outside `.oat/repo`.
104
+ * Immediately before the rename, the target must still be the regular,
105
+ * non-symlink file that was read (same device and inode); otherwise the
106
+ * replacement is abandoned. The original permission bits are preserved.
107
+ */
108
+ async function replaceAtomically(path, content, identity) {
109
+ const temporary = join(dirname(path), `.${basename(path)}.oat-rewrite-${process.pid}-${randomBytes(6).toString('hex')}.tmp`);
110
+ const permissions = identity.mode & 0o7777;
111
+ const handle = await openFile(temporary, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | NO_FOLLOW, permissions);
112
+ try {
113
+ try {
114
+ await handle.writeFile(content, 'utf8');
115
+ // The creation mode is masked by the umask; set the exact bits.
116
+ await handle.chmod(permissions);
117
+ }
118
+ finally {
119
+ await handle.close();
120
+ }
121
+ const current = await lstat(path);
122
+ if (!current.isFile() ||
123
+ current.dev !== identity.dev ||
124
+ current.ino !== identity.ino) {
125
+ throw new Error(`${path} changed while its references were being rewritten; it was left untouched. Re-run \`oat backlog archive\` to retry.`);
126
+ }
127
+ await rename(temporary, path);
128
+ }
129
+ catch (error) {
130
+ await unlink(temporary).catch(() => undefined);
131
+ throw error;
132
+ }
133
+ }
134
+ async function walkMarkdown(directory) {
135
+ const entries = await readdir(directory, { withFileTypes: true });
136
+ const files = [];
137
+ for (const entry of entries) {
138
+ const path = join(directory, entry.name);
139
+ if (entry.isDirectory()) {
140
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) {
141
+ files.push(...(await walkMarkdown(path)));
142
+ }
143
+ }
144
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
145
+ files.push(path);
146
+ }
147
+ }
148
+ return files;
149
+ }
150
+ /**
151
+ * List Markdown files under the scan root: tracked plus untracked-but-not-
152
+ * ignored files inside a Git work tree, or a filesystem walk outside Git.
153
+ */
154
+ async function listMarkdownFiles(scanRoot) {
155
+ const realScanRoot = await realpath(scanRoot);
156
+ if (await isInsideGitWorkTree(scanRoot)) {
157
+ const { stdout } = await execFileAsync('git', [
158
+ 'ls-files',
159
+ '-z',
160
+ '--cached',
161
+ '--others',
162
+ '--exclude-standard',
163
+ '--',
164
+ '*.md',
165
+ ], { cwd: scanRoot, maxBuffer: 64 * 1024 * 1024 });
166
+ const unique = [...new Set(stdout.split('\0').filter(Boolean))];
167
+ const files = [];
168
+ for (const relativePath of unique) {
169
+ const path = join(scanRoot, relativePath);
170
+ if (await isRegularFileInside(path, realScanRoot)) {
171
+ files.push(path);
172
+ }
173
+ }
174
+ return files;
175
+ }
176
+ const walked = [];
177
+ for (const path of await walkMarkdown(scanRoot)) {
178
+ if (await isRegularFileInside(path, realScanRoot)) {
179
+ walked.push(path);
180
+ }
181
+ }
182
+ return walked;
183
+ }
184
+ function isRelativeTarget(target) {
185
+ return (target.length > 0 &&
186
+ !target.startsWith('/') &&
187
+ !target.startsWith('#') &&
188
+ !/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(target));
189
+ }
190
+ /** Render `absolute` relative to `fromDirectory`, keeping a leading `./` style. */
191
+ function relativeFrom(fromDirectory, absolute, original) {
192
+ const next = toPosix(relative(fromDirectory, absolute));
193
+ return original.startsWith('./') && !next.startsWith('..')
194
+ ? `./${next}`
195
+ : next;
196
+ }
197
+ /**
198
+ * Rewrite one path token that ends in `<id>.md`. Returns the replacement, or
199
+ * `null` when the token does not refer to the moved item.
200
+ */
201
+ function rewriteItemToken(token, context) {
202
+ const { layout, itemsPath, archivedPath } = context;
203
+ const itemFile = basename(itemsPath);
204
+ if (isRelativeTarget(token)) {
205
+ const fromFile = resolve(context.baseDirectory, token);
206
+ if (fromFile === itemsPath) {
207
+ return relativeFrom(context.currentDirectory, archivedPath, token);
208
+ }
209
+ // A token that already resolves to a different, existing file is a
210
+ // working link: never repoint it through a fallback base.
211
+ if (pathExistsSync(fromFile)) {
212
+ return null;
213
+ }
214
+ for (const base of [layout.repoRoot, layout.scanRoot]) {
215
+ if (base !== null && resolve(base, token) === itemsPath) {
216
+ return toPosix(relative(base, archivedPath));
217
+ }
218
+ }
219
+ }
220
+ // Only a token that resolves to the former items/ path is rewritten. URLs
221
+ // (any scheme) are never touched or reported; an unresolved local form that
222
+ // still names `items/<id>.md` is reported for a manual fix. Only
223
+ // `/`-separated forms are recognized: the token scan requires `/` or a
224
+ // delimiter before the file name, so backslash paths never reach here.
225
+ const itemsSuffix = `items/${itemFile}`;
226
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(token)) {
227
+ return null;
228
+ }
229
+ if (token === itemsSuffix || token.endsWith(`/${itemsSuffix}`)) {
230
+ context.warnings.push(`${context.fileName} references \`${token}\`, which names archived backlog item ${basename(itemFile, '.md')} but does not resolve to its old items/ path; update it by hand.`);
231
+ }
232
+ return null;
233
+ }
234
+ /**
235
+ * Split Markdown into prose, inline code spans, and fenced code blocks.
236
+ * Fences (``` or ~~~) close on a same-character run at least as long, or run
237
+ * to the end of the file. Inline code spans pair a backtick run with the next
238
+ * run of the same length inside one paragraph (a blank line ends it), so a
239
+ * stray backtick never swallows later paragraphs.
240
+ */
241
+ export function splitMarkdownCode(content) {
242
+ const segments = [];
243
+ const push = (kind, text) => {
244
+ if (text.length === 0) {
245
+ return;
246
+ }
247
+ const last = segments.at(-1);
248
+ if (last && last.kind === kind && kind === 'prose') {
249
+ last.text += text;
250
+ }
251
+ else {
252
+ segments.push({ kind, text });
253
+ }
254
+ };
255
+ const pushParagraph = (text) => {
256
+ let cursor = 0;
257
+ const opener = /`+/gu;
258
+ let match;
259
+ while ((match = opener.exec(text)) !== null) {
260
+ const run = match[0];
261
+ const closer = new RegExp(`(?<!\`)${run}(?!\`)`, 'u');
262
+ const rest = text.slice(match.index + run.length);
263
+ const close = closer.exec(rest);
264
+ if (!close) {
265
+ continue;
266
+ }
267
+ const end = match.index + run.length + close.index + run.length;
268
+ push('prose', text.slice(cursor, match.index));
269
+ push('span', text.slice(match.index, end));
270
+ cursor = end;
271
+ opener.lastIndex = end;
272
+ }
273
+ push('prose', text.slice(cursor));
274
+ };
275
+ const pushProse = (text) => {
276
+ // Blank lines end a paragraph; the separators stay prose.
277
+ for (const part of text.split(/(\r?\n[ \t]*\r?\n)/u)) {
278
+ pushParagraph(part);
279
+ }
280
+ };
281
+ const lines = content.split(/(?<=\n)/u);
282
+ let prose = '';
283
+ let fence = null;
284
+ let code = '';
285
+ for (const line of lines) {
286
+ if (fence) {
287
+ code += line;
288
+ const close = /^ {0,3}(`{3,}|~{3,})[ \t]*\r?\n?$/u.exec(line);
289
+ if (close &&
290
+ close[1][0] === fence.char &&
291
+ close[1].length >= fence.length) {
292
+ push('fence', code);
293
+ code = '';
294
+ fence = null;
295
+ }
296
+ continue;
297
+ }
298
+ const open = /^ {0,3}(`{3,}|~{3,})/u.exec(line);
299
+ if (open) {
300
+ pushProse(prose);
301
+ prose = '';
302
+ fence = { char: open[1][0], length: open[1].length };
303
+ code = line;
304
+ continue;
305
+ }
306
+ prose += line;
307
+ }
308
+ pushProse(prose);
309
+ push('fence', code);
310
+ return segments;
311
+ }
312
+ /** Report every `items/<id>.md` occurrence left inside code as written. */
313
+ function warnCodeOccurrences(text, context) {
314
+ const itemFile = basename(context.itemsPath);
315
+ const needle = `items/${itemFile}`;
316
+ let index = text.indexOf(needle);
317
+ while (index !== -1) {
318
+ const after = text[index + needle.length] ?? '';
319
+ if (after === '' || !/[A-Za-z0-9_-]/u.test(after)) {
320
+ context.warnings.push(`${context.fileName} mentions \`${needle}\` inside code, which is left as written; update it by hand if it is a live reference to archived backlog item ${basename(itemFile, '.md')}.`);
321
+ }
322
+ index = text.indexOf(needle, index + needle.length);
323
+ }
324
+ }
325
+ /**
326
+ * Rewrite an inline code span whose whole content is one path (optionally
327
+ * with a `#anchor`) that resolves to the moved item — the external-plan
328
+ * "Source artifact or scope" citation form. Any other span naming the item is
329
+ * left as written with a warning, so recorded commands keep their meaning.
330
+ */
331
+ function rewriteCodeSpan(span, context) {
332
+ const run = /^`+/u.exec(span)[0];
333
+ const inner = span.slice(run.length, span.length - run.length);
334
+ const single = /^(\s*)([^\s`]+)(\s*)$/u.exec(inner);
335
+ if (single) {
336
+ const [, leading, token, trailing] = single;
337
+ const hash = token.indexOf('#');
338
+ const path = hash === -1 ? token : token.slice(0, hash);
339
+ const anchor = hash === -1 ? '' : token.slice(hash);
340
+ const itemFile = basename(context.itemsPath);
341
+ if (path === itemFile || path.endsWith(`/${itemFile}`)) {
342
+ const replacement = rewriteItemToken(path, context);
343
+ return replacement === null || replacement === path
344
+ ? span
345
+ : `${run}${leading}${replacement}${anchor}${trailing}${run}`;
346
+ }
347
+ }
348
+ warnCodeOccurrences(inner, context);
349
+ return span;
350
+ }
351
+ /** Rewrite every token in `content` that ends in the item's file name. */
352
+ function rewriteItemTokens(content, context) {
353
+ const itemFile = basename(context.itemsPath);
354
+ let output = '';
355
+ let cursor = 0;
356
+ let index = content.indexOf(itemFile);
357
+ while (index !== -1) {
358
+ const end = index + itemFile.length;
359
+ const before = index === 0 ? '' : content[index - 1];
360
+ const after = content[end] ?? '';
361
+ const boundedBefore = before === '' || before === '/' || TOKEN_DELIMITERS.has(before);
362
+ const boundedAfter = after === '' || !/[A-Za-z0-9_-]/u.test(after);
363
+ if (boundedBefore && boundedAfter) {
364
+ let start = index;
365
+ while (start > cursor && !TOKEN_DELIMITERS.has(content[start - 1])) {
366
+ start -= 1;
367
+ }
368
+ const token = content.slice(start, end);
369
+ const replacement = rewriteItemToken(token, context);
370
+ if (replacement !== null && replacement !== token) {
371
+ output += content.slice(cursor, start) + replacement;
372
+ cursor = end;
373
+ }
374
+ }
375
+ index = content.indexOf(itemFile, end);
376
+ }
377
+ return output + content.slice(cursor);
378
+ }
379
+ /**
380
+ * Rebase one relative link target of the moved item, or return it as-is.
381
+ * Only a target that existed relative to `items/` is rebased, so prose or an
382
+ * already-broken target is never rewritten into a new broken path.
383
+ */
384
+ function rebaseTarget(target, context) {
385
+ if (!isRelativeTarget(target)) {
386
+ return target;
387
+ }
388
+ const absolute = resolve(context.baseDirectory, target);
389
+ if (absolute === context.itemsPath ||
390
+ resolve(context.currentDirectory, target) === absolute ||
391
+ !pathExistsSync(absolute)) {
392
+ return target;
393
+ }
394
+ return relativeFrom(context.currentDirectory, absolute, target);
395
+ }
396
+ /** Rebase `path?query#anchor`, keeping the query and anchor as written. */
397
+ function rebaseWithAnchor(target, context) {
398
+ const split = target.search(/[?#]/u);
399
+ const path = split === -1 ? target : target.slice(0, split);
400
+ const suffix = split === -1 ? '' : target.slice(split);
401
+ return path.length === 0 ? target : `${rebaseTarget(path, context)}${suffix}`;
402
+ }
403
+ /** A CommonMark link title: `"…"`, `'…'`, or `(…)`. */
404
+ const LINK_TITLE = String.raw `(?:"[^"\n]*"|'[^'\n]*'|\([^)\n]*\))`;
405
+ const INLINE_LINK = new RegExp(String.raw `\]\((?:<([^<>\n]*)>|([^)\s<]+))((?:\s+${LINK_TITLE})?)\)`, 'gu');
406
+ const REFERENCE_DEFINITION = new RegExp(String.raw `^( {0,3}\[(?!\^)[^\]\n]+\]:[ \t]*)(?:<([^<>\n]*)>|([^\s<>]+))((?:[ \t]+${LINK_TITLE})?[ \t]*\r?)$`, 'gmu');
407
+ /**
408
+ * Rebase the moved item's inline `[text](target "title")` links (angle
409
+ * destinations and every title form) from `items/` to `archived/`.
410
+ */
411
+ function rebaseMovedItemInlineLinks(content, context) {
412
+ return content.replace(INLINE_LINK, (_match, angled, bare, title) => angled !== undefined
413
+ ? `](<${rebaseWithAnchor(angled, context)}>${title})`
414
+ : `](${rebaseWithAnchor(bare, context)}${title})`);
415
+ }
416
+ /**
417
+ * Rebase the moved item's reference definitions (`[label]: <dest> "title"`)
418
+ * outside fenced code. Footnotes (`[^n]:`) and `[label]: prose` lines — a
419
+ * destination followed by anything but a title — are not definitions and are
420
+ * left untouched.
421
+ */
422
+ function rebaseMovedItemDefinitions(content, context) {
423
+ const segments = splitMarkdownCode(content);
424
+ return segments
425
+ .map((segment, index) => {
426
+ // Code spans and fences stay exactly as written.
427
+ if (segment.kind !== 'prose') {
428
+ return segment.text;
429
+ }
430
+ // A prose segment that follows a code span mid-line does not start a
431
+ // line, so a match at its offset 0 is not a definition.
432
+ const previous = index === 0 ? null : segments[index - 1].text;
433
+ const startsLine = previous === null || previous.endsWith('\n');
434
+ return segment.text.replace(REFERENCE_DEFINITION, (match, prefix, angled, bare, rest, offset) => {
435
+ if (offset === 0 && !startsLine) {
436
+ return match;
437
+ }
438
+ return angled !== undefined
439
+ ? `${prefix}<${rebaseWithAnchor(angled, context)}>${rest}`
440
+ : `${prefix}${rebaseWithAnchor(bare, context)}${rest}`;
441
+ });
442
+ })
443
+ .join('');
444
+ }
445
+ /** Rewrite one file's content: prose tokens, citation spans, moved links. */
446
+ function rewriteContent(content, context, moved) {
447
+ const itemFile = basename(context.itemsPath);
448
+ let next = content;
449
+ if (content.includes(itemFile)) {
450
+ next = splitMarkdownCode(content)
451
+ .map((segment) => {
452
+ if (segment.kind === 'prose') {
453
+ return rewriteItemTokens(segment.text, context);
454
+ }
455
+ if (segment.kind === 'span') {
456
+ return rewriteCodeSpan(segment.text, context);
457
+ }
458
+ warnCodeOccurrences(segment.text, context);
459
+ return segment.text;
460
+ })
461
+ .join('');
462
+ }
463
+ if (moved) {
464
+ next = splitMarkdownCode(next)
465
+ .map((segment) => segment.kind === 'prose'
466
+ ? rebaseMovedItemInlineLinks(segment.text, context)
467
+ : segment.text)
468
+ .join('');
469
+ next = rebaseMovedItemDefinitions(next, context);
470
+ }
471
+ return next;
472
+ }
473
+ /**
474
+ * After `items/<id>.md` moved to `archived/<id>.md`, rewrite every reference
475
+ * to the old path in Markdown under `.oat/repo/**` (links and repository-root
476
+ * path strings such as `oat_external_plan_sources` frontmatter). An inline code
477
+ * span is rewritten only when its whole content is a path to the item (the
478
+ * external-plan citation form); other spans and fenced code are left as
479
+ * written with a warning, so a recorded command keeps its meaning. References that name the item's `items/` path
480
+ * but cannot be resolved are reported as warnings and left untouched. The
481
+ * rewrite is idempotent, so a re-run on an already-archived item is safe.
482
+ */
483
+ export async function rewriteInboundReferences(backlogRoot, itemsPath, archivedPath, options = {}) {
484
+ const rebaseMovedItem = options.rebaseMovedItem ?? true;
485
+ const layout = resolveLayout(backlogRoot);
486
+ const absoluteItemsPath = resolve(itemsPath);
487
+ const absoluteArchivedPath = resolve(archivedPath);
488
+ const displayRoot = layout.repoRoot ?? layout.scanRoot;
489
+ const rewritten = [];
490
+ const warnings = [];
491
+ // The moved item goes first, so a failure on a later file never leaves its
492
+ // own links half-rebased: a retry (which skips the rebase) stays correct.
493
+ const files = (await listMarkdownFiles(layout.scanRoot)).sort((left, right) => Number(resolve(right) === absoluteArchivedPath) -
494
+ Number(resolve(left) === absoluteArchivedPath) ||
495
+ left.localeCompare(right));
496
+ for (const file of files) {
497
+ const absoluteFile = resolve(file);
498
+ const moved = rebaseMovedItem && absoluteFile === absoluteArchivedPath;
499
+ const { content, identity } = await readNoFollow(absoluteFile);
500
+ const context = {
501
+ layout,
502
+ itemsPath: absoluteItemsPath,
503
+ archivedPath: absoluteArchivedPath,
504
+ fileName: toPosix(relative(displayRoot, absoluteFile)),
505
+ baseDirectory: moved ? dirname(absoluteItemsPath) : dirname(absoluteFile),
506
+ currentDirectory: dirname(absoluteFile),
507
+ warnings,
508
+ };
509
+ const next = rewriteContent(content, context, moved);
510
+ if (next !== content) {
511
+ await replaceAtomically(absoluteFile, next, identity);
512
+ rewritten.push(context.fileName);
513
+ }
514
+ }
515
+ return { rewritten, warnings };
516
+ }
@@ -12,6 +12,7 @@ interface ConfigCommandDependencies {
12
12
  readOatConfigForDefaultScopeRepair: (repoRoot: string) => Promise<OatConfig>;
13
13
  readOatConfigForDocumentationExcludesRepair: (repoRoot: string) => Promise<OatConfig>;
14
14
  readOatConfigForInstructionPointerExcludesRepair: (repoRoot: string) => Promise<OatConfig>;
15
+ readOatConfigForInstructionSyncStrategyRepair: (repoRoot: string) => Promise<OatConfig>;
15
16
  readOatConfigWithWarnings: (repoRoot: string) => Promise<OatConfigRead>;
16
17
  writeOatConfig: (repoRoot: string, config: OatConfig) => Promise<void>;
17
18
  readOatLocalConfig: (repoRoot: string) => Promise<OatLocalConfig>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/config/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/config/index.ts"],"names":[],"mappings":"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"}