@open-agent-toolkit/cli 0.1.31 → 0.1.33

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 (98) hide show
  1. package/assets/docs/cli-utilities/config-and-local-state.md +19 -6
  2. package/assets/docs/cli-utilities/tool-packs.md +13 -8
  3. package/assets/docs/reference/cli-reference.md +14 -13
  4. package/assets/docs/reference/file-locations.md +2 -1
  5. package/assets/docs/reference/oat-directory-structure.md +35 -22
  6. package/assets/migration/pjm-restructure.md +500 -0
  7. package/assets/public-package-versions.json +4 -4
  8. package/assets/scripts/resolve-tracking.sh +52 -0
  9. package/assets/skills/deep-research/SKILL.md +2 -2
  10. package/assets/skills/oat-brainstorm/SKILL.md +3 -3
  11. package/assets/skills/oat-brainstorm/references/destinations.md +10 -2
  12. package/assets/skills/oat-brainstorm/references/dogfood-results.md +4 -4
  13. package/assets/skills/oat-pjm-add-backlog-item/SKILL.md +17 -20
  14. package/assets/skills/oat-pjm-decision/SKILL.md +129 -0
  15. package/assets/skills/oat-pjm-review-backlog/SKILL.md +17 -17
  16. package/assets/skills/oat-pjm-review-backlog/references/backlog-review-template.md +2 -2
  17. package/assets/skills/oat-pjm-review-backlog/references/priority-alignment-template.md +13 -13
  18. package/assets/skills/oat-pjm-update-repo-reference/SKILL.md +34 -25
  19. package/assets/skills/oat-project-complete/SKILL.md +2 -2
  20. package/assets/skills/oat-project-document/SKILL.md +4 -3
  21. package/assets/skills/oat-project-import-plan/SKILL.md +2 -2
  22. package/assets/skills/oat-project-summary/SKILL.md +76 -8
  23. package/assets/templates/backlog-item.md +1 -1
  24. package/assets/templates/current-state.md +11 -2
  25. package/assets/templates/decision.md +18 -0
  26. package/assets/templates/pjm-agents.md +13 -0
  27. package/assets/templates/reference-agents.md +14 -0
  28. package/assets/templates/repo-agents.md +13 -0
  29. package/assets/templates/roadmap.md +8 -3
  30. package/dist/commands/backlog/index.d.ts +1 -0
  31. package/dist/commands/backlog/index.d.ts.map +1 -1
  32. package/dist/commands/backlog/index.js +48 -11
  33. package/dist/commands/backlog/init.js +1 -1
  34. package/dist/commands/backlog/regenerate-index.d.ts.map +1 -1
  35. package/dist/commands/backlog/regenerate-index.js +24 -5
  36. package/dist/commands/backlog/shared/generate-id.d.ts +1 -3
  37. package/dist/commands/backlog/shared/generate-id.d.ts.map +1 -1
  38. package/dist/commands/backlog/shared/generate-id.js +4 -64
  39. package/dist/commands/cleanup/artifacts/artifacts.d.ts.map +1 -1
  40. package/dist/commands/cleanup/artifacts/artifacts.js +4 -1
  41. package/dist/commands/decision/index.d.ts +20 -0
  42. package/dist/commands/decision/index.d.ts.map +1 -0
  43. package/dist/commands/decision/index.js +177 -0
  44. package/dist/commands/decision/init.d.ts +7 -0
  45. package/dist/commands/decision/init.d.ts.map +1 -0
  46. package/dist/commands/decision/init.js +53 -0
  47. package/dist/commands/decision/migrate.d.ts +24 -0
  48. package/dist/commands/decision/migrate.d.ts.map +1 -0
  49. package/dist/commands/decision/migrate.js +270 -0
  50. package/dist/commands/decision/new.d.ts +16 -0
  51. package/dist/commands/decision/new.d.ts.map +1 -0
  52. package/dist/commands/decision/new.js +118 -0
  53. package/dist/commands/decision/regenerate-index.d.ts +14 -0
  54. package/dist/commands/decision/regenerate-index.d.ts.map +1 -0
  55. package/dist/commands/decision/regenerate-index.js +122 -0
  56. package/dist/commands/decision/shared/generate-id.d.ts +2 -0
  57. package/dist/commands/decision/shared/generate-id.d.ts.map +1 -0
  58. package/dist/commands/decision/shared/generate-id.js +5 -0
  59. package/dist/commands/doctor/index.d.ts +5 -0
  60. package/dist/commands/doctor/index.d.ts.map +1 -1
  61. package/dist/commands/doctor/index.js +15 -0
  62. package/dist/commands/index.d.ts.map +1 -1
  63. package/dist/commands/index.js +2 -0
  64. package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
  65. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  66. package/dist/commands/init/tools/shared/skill-manifest.js +5 -1
  67. package/dist/commands/pjm/doctor.d.ts +7 -0
  68. package/dist/commands/pjm/doctor.d.ts.map +1 -0
  69. package/dist/commands/pjm/doctor.js +250 -0
  70. package/dist/commands/pjm/index.d.ts +7 -0
  71. package/dist/commands/pjm/index.d.ts.map +1 -1
  72. package/dist/commands/pjm/index.js +135 -10
  73. package/dist/commands/pjm/init.d.ts +3 -2
  74. package/dist/commands/pjm/init.d.ts.map +1 -1
  75. package/dist/commands/pjm/init.js +42 -33
  76. package/dist/commands/pjm/migrate.d.ts +36 -0
  77. package/dist/commands/pjm/migrate.d.ts.map +1 -0
  78. package/dist/commands/pjm/migrate.js +393 -0
  79. package/dist/commands/shared/date-id.d.ts +2 -0
  80. package/dist/commands/shared/date-id.d.ts.map +1 -0
  81. package/dist/commands/shared/date-id.js +10 -0
  82. package/dist/commands/shared/managed-index.d.ts +22 -0
  83. package/dist/commands/shared/managed-index.d.ts.map +1 -0
  84. package/dist/commands/shared/managed-index.js +92 -0
  85. package/dist/commands/shared/slug.d.ts +2 -0
  86. package/dist/commands/shared/slug.d.ts.map +1 -0
  87. package/dist/commands/shared/slug.js +57 -0
  88. package/dist/commands/shared/strip-template-frontmatter.d.ts +2 -0
  89. package/dist/commands/shared/strip-template-frontmatter.d.ts.map +1 -0
  90. package/dist/commands/shared/strip-template-frontmatter.js +16 -0
  91. package/dist/commands/sync/apply.d.ts.map +1 -1
  92. package/dist/commands/sync/apply.js +6 -2
  93. package/dist/manifest/manager.d.ts.map +1 -1
  94. package/dist/manifest/manager.js +4 -1
  95. package/dist/release/public-package-contract.d.ts.map +1 -1
  96. package/dist/release/public-package-contract.js +10 -1
  97. package/package.json +2 -2
  98. package/assets/templates/decision-record.md +0 -21
@@ -0,0 +1,393 @@
1
+ import { access, mkdir, readdir, readFile, rename, stat, writeFile, } from 'node:fs/promises';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { initializeBacklog } from '../backlog/init.js';
4
+ import { regenerateBacklogIndex } from '../backlog/regenerate-index.js';
5
+ import { generateBacklogId } from '../backlog/shared/generate-id.js';
6
+ import { initializeDecisionRecords } from '../decision/init.js';
7
+ import { migrateDecisionRecords, } from '../decision/migrate.js';
8
+ import { stripTemplateFrontmatter } from '../shared/strip-template-frontmatter.js';
9
+ import YAML from 'yaml';
10
+ import { initializeRepoReference } from './init.js';
11
+ const ACTIVE_MOVES = [
12
+ { source: 'reference/current-state.md', target: 'pjm/current-state.md' },
13
+ { source: 'reference/roadmap.md', target: 'pjm/roadmap.md' },
14
+ { source: 'reference/backlog', target: 'pjm/backlog' },
15
+ ];
16
+ const JUDGMENT_TARGETS = [
17
+ 'reference/backlog.md',
18
+ 'reference/backlog-completed.md',
19
+ ];
20
+ async function pathExists(path) {
21
+ try {
22
+ await access(path);
23
+ return true;
24
+ }
25
+ catch (error) {
26
+ const code = error && typeof error === 'object' && 'code' in error
27
+ ? String(error.code)
28
+ : null;
29
+ if (code !== 'ENOENT') {
30
+ throw error;
31
+ }
32
+ return false;
33
+ }
34
+ }
35
+ async function listMarkdownFiles(path) {
36
+ try {
37
+ const entries = await readdir(path, { withFileTypes: true });
38
+ return entries
39
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
40
+ .map((entry) => join(path, entry.name))
41
+ .sort();
42
+ }
43
+ catch (error) {
44
+ const code = error && typeof error === 'object' && 'code' in error
45
+ ? String(error.code)
46
+ : null;
47
+ if (code !== 'ENOENT') {
48
+ throw error;
49
+ }
50
+ return [];
51
+ }
52
+ }
53
+ function stripMarkdownExtension(path) {
54
+ return basename(path, '.md');
55
+ }
56
+ function relativeToRepo(repoRoot, path) {
57
+ return path.startsWith(`${repoRoot}/`)
58
+ ? path.slice(repoRoot.length + 1)
59
+ : path;
60
+ }
61
+ function parseFrontmatter(content, filePath) {
62
+ const match = content.match(/^---\n([\s\S]*?)\n---(\r?\n[\s\S]*)?$/);
63
+ if (!match) {
64
+ return null;
65
+ }
66
+ const parsed = YAML.parse(match[1] ?? '');
67
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
68
+ throw new Error(`Frontmatter in ${filePath} must be a YAML object.`);
69
+ }
70
+ return {
71
+ frontmatter: parsed,
72
+ body: match[2] ?? '\n',
73
+ };
74
+ }
75
+ function renderFrontmatterRecord(frontmatter, body) {
76
+ return `---\n${YAML.stringify(frontmatter).trimEnd()}\n---${body}`;
77
+ }
78
+ async function createdAtForBacklogRecord(frontmatter, filePath) {
79
+ const created = frontmatter.created;
80
+ if (typeof created === 'string' && created.trim()) {
81
+ return created;
82
+ }
83
+ return (await stat(filePath)).mtime.toISOString();
84
+ }
85
+ function withNumericSuffix(id, suffix) {
86
+ return `${id}-${suffix}`;
87
+ }
88
+ async function allocateBacklogId(initialId, currentPath, targetDir, plannedTargets) {
89
+ let id = initialId;
90
+ let suffix = 2;
91
+ let targetPath = join(targetDir, `${id}.md`);
92
+ while (plannedTargets.has(targetPath) ||
93
+ (targetPath !== currentPath && (await pathExists(targetPath)))) {
94
+ id = withNumericSuffix(initialId, suffix);
95
+ targetPath = join(targetDir, `${id}.md`);
96
+ suffix += 1;
97
+ }
98
+ plannedTargets.add(targetPath);
99
+ return { id, targetPath };
100
+ }
101
+ async function prepareBacklogMigrations(backlogRoot) {
102
+ const prepared = [];
103
+ const plannedTargets = new Set();
104
+ for (const bucket of ['items', 'archived']) {
105
+ const bucketRoot = join(backlogRoot, bucket);
106
+ const files = await listMarkdownFiles(bucketRoot);
107
+ for (const sourcePath of files) {
108
+ const content = await readFile(sourcePath, 'utf8');
109
+ const parsed = parseFrontmatter(content, sourcePath);
110
+ if (!parsed) {
111
+ continue;
112
+ }
113
+ const legacyId = String(parsed.frontmatter.id ?? stripMarkdownExtension(sourcePath));
114
+ const title = String(parsed.frontmatter.title ?? legacyId);
115
+ const createdAt = await createdAtForBacklogRecord(parsed.frontmatter, sourcePath);
116
+ const initialId = generateBacklogId(title, createdAt);
117
+ const { id, targetPath } = await allocateBacklogId(initialId, sourcePath, bucketRoot, plannedTargets);
118
+ if (id === legacyId && targetPath === sourcePath) {
119
+ continue;
120
+ }
121
+ const nextFrontmatter = {
122
+ ...parsed.frontmatter,
123
+ id,
124
+ };
125
+ // Migrated items are instantiated records, never raw templates. Drop the
126
+ // template-marker keys a legacy item may still carry so `pjm doctor`
127
+ // (prev2-t05) does not flag the migrated record. The shared
128
+ // `stripTemplateFrontmatter` defines which markers identify a raw
129
+ // template; mirror that key set here rather than re-parsing.
130
+ delete nextFrontmatter.oat_template;
131
+ delete nextFrontmatter.oat_template_name;
132
+ if (nextFrontmatter.legacy_id === undefined) {
133
+ nextFrontmatter.legacy_id = legacyId;
134
+ }
135
+ // Defense-in-depth: run the rebuilt record through the shared helper so any
136
+ // residual template frontmatter is stripped with the same rules doctor and
137
+ // `decision new` use. It is a no-op once the marker keys above are removed.
138
+ const migratedContent = stripTemplateFrontmatter(renderFrontmatterRecord(nextFrontmatter, parsed.body));
139
+ prepared.push({
140
+ sourcePath,
141
+ targetPath,
142
+ content: migratedContent,
143
+ mapping: {
144
+ legacyId,
145
+ id,
146
+ title,
147
+ filePath: targetPath,
148
+ },
149
+ });
150
+ }
151
+ }
152
+ return prepared;
153
+ }
154
+ async function applyBacklogMigrations(preparedMigrations) {
155
+ const written = [];
156
+ for (const migration of preparedMigrations) {
157
+ await mkdir(dirname(migration.targetPath), { recursive: true });
158
+ await writeFile(migration.sourcePath, migration.content, 'utf8');
159
+ if (migration.sourcePath !== migration.targetPath) {
160
+ await rename(migration.sourcePath, migration.targetPath);
161
+ }
162
+ written.push(migration.targetPath);
163
+ }
164
+ return written;
165
+ }
166
+ async function planActiveMoves(repoRoot) {
167
+ const actions = [];
168
+ for (const move of ACTIVE_MOVES) {
169
+ const sourcePath = join(repoRoot, move.source);
170
+ const targetPath = join(repoRoot, move.target);
171
+ const sourceExists = await pathExists(sourcePath);
172
+ const targetExists = await pathExists(targetPath);
173
+ if (sourceExists && !targetExists) {
174
+ actions.push({ type: 'move', ...move, result: 'planned' });
175
+ }
176
+ else if (sourceExists && targetExists) {
177
+ actions.push({
178
+ type: 'move',
179
+ ...move,
180
+ result: 'skipped',
181
+ reason: 'target already exists; leaving source for manual review',
182
+ });
183
+ }
184
+ }
185
+ return actions;
186
+ }
187
+ async function applyActiveMoves(repoRoot, actions) {
188
+ const moved = [];
189
+ for (const action of actions) {
190
+ if (action.type !== 'move' ||
191
+ action.result !== 'planned' ||
192
+ !action.source ||
193
+ !action.target) {
194
+ continue;
195
+ }
196
+ const sourcePath = join(repoRoot, action.source);
197
+ const targetPath = join(repoRoot, action.target);
198
+ await mkdir(dirname(targetPath), { recursive: true });
199
+ await rename(sourcePath, targetPath);
200
+ action.result = 'applied';
201
+ moved.push(targetPath);
202
+ }
203
+ return moved;
204
+ }
205
+ async function collectJudgmentProposals(repoRoot) {
206
+ const actions = [];
207
+ for (const target of JUDGMENT_TARGETS) {
208
+ if (await pathExists(join(repoRoot, target))) {
209
+ actions.push({
210
+ type: 'propose',
211
+ source: target,
212
+ result: 'planned',
213
+ reason: 'legacy backlog file retirement requires explicit review',
214
+ });
215
+ }
216
+ }
217
+ const referenceRoot = join(repoRoot, 'reference');
218
+ for (const filePath of await listMarkdownFiles(referenceRoot)) {
219
+ const name = basename(filePath);
220
+ if (name !== 'roadmap.md' && name.toLowerCase().includes('roadmap')) {
221
+ actions.push({
222
+ type: 'propose',
223
+ source: relativeToRepo(repoRoot, filePath),
224
+ result: 'planned',
225
+ reason: 'second roadmap disposition requires explicit review',
226
+ });
227
+ }
228
+ }
229
+ return actions;
230
+ }
231
+ async function isAlreadyMigrated(repoRoot) {
232
+ const hasPjm = await pathExists(join(repoRoot, 'pjm'));
233
+ const hasDecisions = await pathExists(join(repoRoot, 'reference', 'decisions'));
234
+ if (!hasPjm || !hasDecisions) {
235
+ return false;
236
+ }
237
+ for (const legacyPath of [
238
+ 'reference/current-state.md',
239
+ 'reference/roadmap.md',
240
+ 'reference/backlog',
241
+ 'reference/decision-record.md',
242
+ ]) {
243
+ if (await pathExists(join(repoRoot, legacyPath))) {
244
+ return false;
245
+ }
246
+ }
247
+ return true;
248
+ }
249
+ async function collectDecisionMappings(repoRoot) {
250
+ const referenceRoot = join(repoRoot, 'reference');
251
+ const legacyPath = join(referenceRoot, 'decision-record.md');
252
+ if (!(await pathExists(legacyPath))) {
253
+ return [];
254
+ }
255
+ return (await migrateDecisionRecords({
256
+ referenceRoot,
257
+ dryRun: true,
258
+ })).mappings;
259
+ }
260
+ /**
261
+ * Preflights the decision-migration step without mutating anything.
262
+ *
263
+ * Runs the same guards the apply step will run, including the destructive
264
+ * delete-safety guard (`deleteLegacy: true`), but in dry-run mode so it writes
265
+ * and deletes nothing. This is the failure-prone precondition surfaced by
266
+ * dogfooding: an unparseable `decision-record.md` previously aborted only after
267
+ * mechanical moves had already happened. Validating it up front keeps
268
+ * `--apply` atomic — any decision-parse problem fails before the tree changes.
269
+ */
270
+ async function preflightDecisionMigration(repoRoot) {
271
+ const referenceRoot = join(repoRoot, 'reference');
272
+ const legacyPath = join(referenceRoot, 'decision-record.md');
273
+ if (!(await pathExists(legacyPath))) {
274
+ return;
275
+ }
276
+ await migrateDecisionRecords({
277
+ referenceRoot,
278
+ deleteLegacy: true,
279
+ dryRun: true,
280
+ });
281
+ }
282
+ async function migrateDecisions(repoRoot) {
283
+ const referenceRoot = join(repoRoot, 'reference');
284
+ const legacyPath = join(referenceRoot, 'decision-record.md');
285
+ if (!(await pathExists(legacyPath))) {
286
+ await initializeDecisionRecords(join(referenceRoot, 'decisions'));
287
+ return { mappings: [], written: [] };
288
+ }
289
+ const result = await migrateDecisionRecords({
290
+ referenceRoot,
291
+ deleteLegacy: true,
292
+ });
293
+ return { mappings: result.mappings, written: result.written };
294
+ }
295
+ function buildEmptyResult(options, status, reason) {
296
+ return {
297
+ repoRoot: options.repoRoot,
298
+ status,
299
+ reason,
300
+ dryRun: !options.apply,
301
+ actions: [],
302
+ backlogMappings: [],
303
+ decisionMappings: [],
304
+ written: [],
305
+ };
306
+ }
307
+ export async function migratePjmRepo(options) {
308
+ if (!options.projectManagementEnabled) {
309
+ return buildEmptyResult(options, 'skipped', 'project-management pack is disabled');
310
+ }
311
+ if (await isAlreadyMigrated(options.repoRoot)) {
312
+ return buildEmptyResult(options, 'already-migrated');
313
+ }
314
+ const apply = options.apply ?? false;
315
+ const activeMoves = await planActiveMoves(options.repoRoot);
316
+ const judgmentProposals = await collectJudgmentProposals(options.repoRoot);
317
+ const legacyBacklogRoot = join(options.repoRoot, 'reference', 'backlog');
318
+ const pjmBacklogRoot = join(options.repoRoot, 'pjm', 'backlog');
319
+ const backlogRoot = (await pathExists(legacyBacklogRoot))
320
+ ? legacyBacklogRoot
321
+ : pjmBacklogRoot;
322
+ const preparedBacklogMigrations = await prepareBacklogMigrations(backlogRoot);
323
+ const decisionMappings = await collectDecisionMappings(options.repoRoot);
324
+ const backlogMappings = preparedBacklogMigrations.map((migration) => migration.mapping);
325
+ const migrationActions = [];
326
+ if (backlogMappings.length > 0) {
327
+ migrationActions.push({
328
+ type: 'migrate-backlog',
329
+ source: relativeToRepo(options.repoRoot, backlogRoot),
330
+ target: 'pjm/backlog',
331
+ result: 'planned',
332
+ });
333
+ }
334
+ if (decisionMappings.length > 0) {
335
+ migrationActions.push({
336
+ type: 'migrate-decisions',
337
+ source: 'reference/decision-record.md',
338
+ target: 'reference/decisions',
339
+ result: 'planned',
340
+ });
341
+ }
342
+ const actions = [...activeMoves, ...migrationActions, ...judgmentProposals];
343
+ if (!apply) {
344
+ return {
345
+ repoRoot: options.repoRoot,
346
+ status: 'dry-run',
347
+ dryRun: true,
348
+ actions,
349
+ backlogMappings,
350
+ decisionMappings,
351
+ written: [],
352
+ };
353
+ }
354
+ // Preflight every failure-prone precondition BEFORE any mechanical mutation so
355
+ // `--apply` is atomic: if a step would fail, we abort here with the tree
356
+ // unchanged. The decision-record parse is the dogfood-surfaced failure mode —
357
+ // an unparseable source previously aborted only after files had already moved,
358
+ // leaving a half-migrated tree.
359
+ await preflightDecisionMigration(options.repoRoot);
360
+ const written = [];
361
+ written.push(...(await applyActiveMoves(options.repoRoot, activeMoves)));
362
+ await initializeRepoReference({
363
+ repoRoot: options.repoRoot,
364
+ assetsRoot: options.assetsRoot,
365
+ templatesRoot: options.templatesRoot,
366
+ });
367
+ const preparedAfterMove = await prepareBacklogMigrations(pjmBacklogRoot);
368
+ written.push(...(await applyBacklogMigrations(preparedAfterMove)));
369
+ if (await pathExists(join(pjmBacklogRoot, 'index.md'))) {
370
+ await regenerateBacklogIndex(pjmBacklogRoot);
371
+ written.push(join(pjmBacklogRoot, 'index.md'));
372
+ }
373
+ else {
374
+ await initializeBacklog(pjmBacklogRoot);
375
+ }
376
+ const decisionResult = await migrateDecisions(options.repoRoot);
377
+ written.push(...decisionResult.written);
378
+ for (const action of migrationActions) {
379
+ action.result = 'applied';
380
+ }
381
+ return {
382
+ repoRoot: options.repoRoot,
383
+ status: 'migrated',
384
+ dryRun: false,
385
+ actions: [...activeMoves, ...migrationActions, ...judgmentProposals],
386
+ backlogMappings: preparedAfterMove.map((migration) => migration.mapping),
387
+ decisionMappings: decisionResult.mappings,
388
+ written,
389
+ };
390
+ }
391
+ export async function readPjmMigrationPrompt(assetsRoot) {
392
+ return readFile(join(assetsRoot, 'migration', 'pjm-restructure.md'), 'utf8');
393
+ }
@@ -0,0 +1,2 @@
1
+ export declare function yymmdd(isoOrDate: string | Date): string;
2
+ //# sourceMappingURL=date-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"date-id.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/date-id.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAavD"}
@@ -0,0 +1,10 @@
1
+ export function yymmdd(isoOrDate) {
2
+ const date = isoOrDate instanceof Date ? new Date(isoOrDate) : new Date(isoOrDate);
3
+ if (Number.isNaN(date.getTime())) {
4
+ throw new Error(`Invalid date for OAT identifier: ${String(isoOrDate)}`);
5
+ }
6
+ const year = String(date.getUTCFullYear() % 100).padStart(2, '0');
7
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
8
+ const day = String(date.getUTCDate()).padStart(2, '0');
9
+ return `${year}${month}${day}`;
10
+ }
@@ -0,0 +1,22 @@
1
+ export interface ManagedIndexUpdate {
2
+ /**
3
+ * The full file content to write, or `null` when the existing managed block
4
+ * is already content-equal to the target (callers must skip the write to
5
+ * preserve the on-disk bytes, including any external formatter's padding).
6
+ */
7
+ content: string | null;
8
+ }
9
+ /**
10
+ * Compute the content-idempotent result of regenerating a managed index block.
11
+ *
12
+ * @param existingContent Current on-disk file content (already known to contain
13
+ * the marker pair; bounds are passed in to avoid re-scanning).
14
+ * @param startIndex Index of the start marker within `existingContent`.
15
+ * @param endMarkerEnd Index just past the end marker within `existingContent`.
16
+ * @param renderedBlock Freshly rendered managed block (start marker ... end
17
+ * marker) built from the record files.
18
+ * @returns `{ content: null }` when the logical rows match (skip the write), or
19
+ * `{ content: <full file text> }` when the block must be rewritten.
20
+ */
21
+ export declare function computeManagedIndexUpdate(existingContent: string, startIndex: number, endMarkerEnd: number, renderedBlock: string): ManagedIndexUpdate;
22
+ //# sourceMappingURL=managed-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managed-index.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/managed-index.ts"],"names":[],"mappings":"AA8EA,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,GACpB,kBAAkB,CAYpB"}
@@ -0,0 +1,92 @@
1
+ // Shared helper for content-idempotent managed-index regeneration.
2
+ //
3
+ // Managed index files (decision + backlog) contain a CLI-managed block bounded
4
+ // by a start/end marker pair. The block holds a markdown table whose rows are
5
+ // rebuilt from the per-record files on each `regenerate-index` run.
6
+ //
7
+ // External formatters (oxfmt/prettier/etc.) frequently re-pad the table cells
8
+ // to align columns. Without an idempotence gate, the next `regenerate-index`
9
+ // rewrites the formatter's padding back to CLI padding, producing churn and
10
+ // noise in the "resolve a merge conflict by re-running regenerate" story.
11
+ //
12
+ // This helper compares the EXISTING managed block against the freshly rendered
13
+ // target block by their LOGICAL content (each table cell trimmed of surrounding
14
+ // whitespace), ignoring column padding. When the logical content matches it
15
+ // preserves the on-disk bytes verbatim; only a genuine content change (added,
16
+ // removed, reordered, or changed record) triggers a rewrite. It makes no
17
+ // assumptions about any specific formatter.
18
+ // A markdown table delimiter cell (e.g. `---`, `:---`, `:---:`, `-----`).
19
+ // Formatters freely re-pad the dash count, so any delimiter cell normalizes to
20
+ // a single canonical token and dash-width differences never count as a change.
21
+ //
22
+ // Require at least three dashes so the single-`-` data placeholder (the empty
23
+ // cell value rendered by the index formatters, e.g. `| - |`) is NOT collapsed to
24
+ // the delimiter token. A real markdown delimiter cell always has >= 3 dashes
25
+ // (the existing formatter-repad test uses `---` and `-----`), and formatters
26
+ // only ever widen the dash count, never shrink it below three. Without the lower
27
+ // bound, a data cell whose literal value is `-`/`--`/`:-:` would be
28
+ // indistinguishable from a delimiter cell, so a genuine change between two
29
+ // dash-shaped values could be masked as a no-op and the rewrite skipped.
30
+ const DELIMITER_CELL = /^:?-{3,}:?$/;
31
+ function normalizeCell(cell) {
32
+ const trimmed = cell.trim();
33
+ return DELIMITER_CELL.test(trimmed) ? '---' : trimmed;
34
+ }
35
+ /**
36
+ * Parse a managed block's text into a normalized list of rows, where each row
37
+ * is a list of cells trimmed of surrounding whitespace. The marker lines and
38
+ * any blank lines are ignored so that formatter-introduced blank lines or
39
+ * column padding never count as a logical difference.
40
+ */
41
+ function parseManagedRows(blockText) {
42
+ return blockText
43
+ .split('\n')
44
+ .map((line) => line.trim())
45
+ .filter((line) => line.length > 0)
46
+ .filter((line) => !line.startsWith('<!--'))
47
+ .map((line) => {
48
+ // A markdown table row is `| cell | cell | ... |`. Drop the leading and
49
+ // trailing pipe, then split on unescaped pipes and normalize each cell.
50
+ const inner = line.replace(/^\|/, '').replace(/\|$/, '');
51
+ return inner.split('|').map((cell) => normalizeCell(cell));
52
+ });
53
+ }
54
+ function rowsEqual(a, b) {
55
+ if (a.length !== b.length) {
56
+ return false;
57
+ }
58
+ for (let rowIndex = 0; rowIndex < a.length; rowIndex += 1) {
59
+ const rowA = a[rowIndex];
60
+ const rowB = b[rowIndex];
61
+ if (rowA.length !== rowB.length) {
62
+ return false;
63
+ }
64
+ for (let cellIndex = 0; cellIndex < rowA.length; cellIndex += 1) {
65
+ if (rowA[cellIndex] !== rowB[cellIndex]) {
66
+ return false;
67
+ }
68
+ }
69
+ }
70
+ return true;
71
+ }
72
+ /**
73
+ * Compute the content-idempotent result of regenerating a managed index block.
74
+ *
75
+ * @param existingContent Current on-disk file content (already known to contain
76
+ * the marker pair; bounds are passed in to avoid re-scanning).
77
+ * @param startIndex Index of the start marker within `existingContent`.
78
+ * @param endMarkerEnd Index just past the end marker within `existingContent`.
79
+ * @param renderedBlock Freshly rendered managed block (start marker ... end
80
+ * marker) built from the record files.
81
+ * @returns `{ content: null }` when the logical rows match (skip the write), or
82
+ * `{ content: <full file text> }` when the block must be rewritten.
83
+ */
84
+ export function computeManagedIndexUpdate(existingContent, startIndex, endMarkerEnd, renderedBlock) {
85
+ const existingBlock = existingContent.slice(startIndex, endMarkerEnd);
86
+ if (rowsEqual(parseManagedRows(existingBlock), parseManagedRows(renderedBlock))) {
87
+ return { content: null };
88
+ }
89
+ const before = existingContent.slice(0, startIndex);
90
+ const after = existingContent.slice(endMarkerEnd);
91
+ return { content: `${before}${renderedBlock}${after}` };
92
+ }
@@ -0,0 +1,2 @@
1
+ export declare function slugify(input: string): string;
2
+ //# sourceMappingURL=slug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slug.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/slug.ts"],"names":[],"mappings":"AAiDA,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAmB7C"}
@@ -0,0 +1,57 @@
1
+ const MAX_SLUG_LENGTH = 30;
2
+ const STOP_WORDS = new Set([
3
+ 'a',
4
+ 'an',
5
+ 'the',
6
+ 'of',
7
+ 'for',
8
+ 'and',
9
+ 'to',
10
+ 'in',
11
+ 'on',
12
+ 'as',
13
+ 'with',
14
+ ]);
15
+ function truncateToWordBoundary(slug) {
16
+ if (slug.length <= MAX_SLUG_LENGTH) {
17
+ return slug;
18
+ }
19
+ const [first, ...rest] = slug.split('-');
20
+ const firstWord = first ?? '';
21
+ // Edge case: the first word alone exceeds the limit. Hard-truncate it.
22
+ if (firstWord.length > MAX_SLUG_LENGTH) {
23
+ return firstWord.slice(0, MAX_SLUG_LENGTH);
24
+ }
25
+ let result = firstWord;
26
+ for (const word of rest) {
27
+ const candidate = `${result}-${word}`;
28
+ if (candidate.length > MAX_SLUG_LENGTH) {
29
+ break;
30
+ }
31
+ result = candidate;
32
+ }
33
+ return result;
34
+ }
35
+ function stripTrailingStopWords(slug) {
36
+ const words = slug.split('-');
37
+ while (words.length > 0 && STOP_WORDS.has(words[words.length - 1] ?? '')) {
38
+ words.pop();
39
+ }
40
+ return words.join('-');
41
+ }
42
+ export function slugify(input) {
43
+ const normalized = input
44
+ .normalize('NFKD')
45
+ .replace(/[\u0300-\u036f]/g, '')
46
+ .toLowerCase();
47
+ const slug = normalized
48
+ .replace(/[^a-z0-9]+/g, '-')
49
+ .replace(/^-+|-+$/g, '')
50
+ .replace(/-+/g, '-');
51
+ if (slug.length === 0) {
52
+ return 'untitled';
53
+ }
54
+ const truncated = truncateToWordBoundary(slug);
55
+ const trimmed = stripTrailingStopWords(truncated);
56
+ return trimmed || 'untitled';
57
+ }
@@ -0,0 +1,2 @@
1
+ export declare function stripTemplateFrontmatter(content: string): string;
2
+ //# sourceMappingURL=strip-template-frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip-template-frontmatter.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/strip-template-frontmatter.ts"],"names":[],"mappings":"AAAA,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAoBhE"}
@@ -0,0 +1,16 @@
1
+ export function stripTemplateFrontmatter(content) {
2
+ if (!content.startsWith('---\n')) {
3
+ return content;
4
+ }
5
+ const end = content.indexOf('\n---', 4);
6
+ if (end === -1) {
7
+ return content;
8
+ }
9
+ const frontmatter = content.slice(4, end);
10
+ if (!/\boat_template\s*:/i.test(frontmatter) &&
11
+ !/\boat_template_name\s*:/i.test(frontmatter)) {
12
+ return content;
13
+ }
14
+ const afterFrontmatter = content.slice(end + '\n---'.length);
15
+ return afterFrontmatter.replace(/^\r?\n+/, '');
16
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,KAAK,EACV,aAAa,EACb,uBAAuB,EAExB,MAAM,cAAc,CAAC;AAgEtB,wBAAsB,YAAY,CAChC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,aAAa,EAAE,EAC3B,YAAY,EAAE,uBAAuB,GACpC,OAAO,CAAC,IAAI,CAAC,CAwEf"}
1
+ {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/apply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EACV,aAAa,EACb,uBAAuB,EAExB,MAAM,cAAc,CAAC;AAgEtB,wBAAsB,YAAY,CAChC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,aAAa,EAAE,EAC3B,YAAY,EAAE,uBAAuB,GACpC,OAAO,CAAC,IAAI,CAAC,CA8Ef"}
@@ -1,3 +1,4 @@
1
+ import { OAT_VERSION } from '../../shared/oat-version.js';
1
2
  import { countPlannedOperations } from './sync.utils.js';
2
3
  function countSkippedEntries(scopePlans) {
3
4
  return scopePlans.reduce((total, scopePlan) => {
@@ -45,10 +46,13 @@ export async function runSyncApply(context, scopePlans, dependencies) {
45
46
  for (const scopePlan of scopePlans) {
46
47
  const hasSyncEntries = scopePlan.plan.entries.length > 0 || scopePlan.plan.removals.length > 0;
47
48
  const hasCodexPlannedOperations = scopePlan.codexExtensionPlan?.operations.some((operation) => operation.action !== 'skip') ?? false;
48
- if (!hasSyncEntries && !hasCodexPlannedOperations) {
49
+ const shouldRefreshManifestVersion = scopePlan.manifest.oatVersion !== OAT_VERSION;
50
+ if (!hasSyncEntries &&
51
+ !hasCodexPlannedOperations &&
52
+ !shouldRefreshManifestVersion) {
49
53
  continue;
50
54
  }
51
- if (hasSyncEntries) {
55
+ if (hasSyncEntries || shouldRefreshManifestVersion) {
52
56
  const result = await dependencies.executeSyncPlan(scopePlan.plan, scopePlan.manifest, scopePlan.manifestPath);
53
57
  applied += result.applied;
54
58
  failed += result.failed;
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/manifest/manager.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,aAAa,EAEnB,MAAM,kBAAkB,CAAC;AAa1B,wBAAgB,mBAAmB,IAAI,QAAQ,CAO9C;AAED,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CA6C1E;AAED,wBAAsB,YAAY,CAChC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED,wBAAgB,SAAS,CACvB,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,aAAa,GAAG,SAAS,CAK3B;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAc3E;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,QAAQ,CAeV"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/manifest/manager.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,aAAa,EAEnB,MAAM,kBAAkB,CAAC;AAa1B,wBAAgB,mBAAmB,IAAI,QAAQ,CAO9C;AAED,wBAAsB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CA6C1E;AAED,wBAAsB,YAAY,CAChC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAgB,SAAS,CACvB,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,aAAa,GAAG,SAAS,CAK3B;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAc3E;AAED,wBAAgB,WAAW,CACzB,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,QAAQ,CAeV"}
@@ -54,7 +54,10 @@ export async function loadManifest(manifestPath) {
54
54
  }
55
55
  }
56
56
  export async function saveManifest(manifestPath, manifest) {
57
- const validated = ManifestSchema.parse(manifest);
57
+ const validated = ManifestSchema.parse({
58
+ ...manifest,
59
+ oatVersion: OAT_VERSION,
60
+ });
58
61
  const dir = dirname(manifestPath);
59
62
  const tempPath = `${manifestPath}.${randomUUID()}.tmp`;
60
63
  await mkdir(dir, { recursive: true });