@open-agent-toolkit/cli 0.1.39 → 0.1.41

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 (40) hide show
  1. package/assets/docs/cli-utilities/backlog-lifecycle.md +61 -0
  2. package/assets/docs/cli-utilities/config-and-local-state.md +47 -0
  3. package/assets/docs/cli-utilities/configuration.md +2 -0
  4. package/assets/docs/cli-utilities/index.md +1 -0
  5. package/assets/docs/provider-sync/instruction-sync.md +1 -0
  6. package/assets/docs/workflows/projects/dispatch-ceiling.md +14 -3
  7. package/assets/docs/workflows/projects/implementation-execution.md +16 -2
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/oat-pjm-review-backlog/SKILL.md +29 -7
  10. package/assets/skills/oat-pjm-update-repo-reference/SKILL.md +25 -5
  11. package/assets/skills/oat-project-implement/SKILL.md +41 -10
  12. package/assets/templates/pjm-agents.md +80 -1
  13. package/assets/templates/pjm-handoffs-readme.md +21 -0
  14. package/assets/templates/reference-agents.md +13 -0
  15. package/assets/templates/repo-agents.md +4 -0
  16. package/assets/templates/repo-readme.md +35 -0
  17. package/dist/commands/backlog/archive.d.ts +33 -0
  18. package/dist/commands/backlog/archive.d.ts.map +1 -0
  19. package/dist/commands/backlog/archive.js +229 -0
  20. package/dist/commands/backlog/index.d.ts +2 -0
  21. package/dist/commands/backlog/index.d.ts.map +1 -1
  22. package/dist/commands/backlog/index.js +56 -2
  23. package/dist/commands/backlog/regenerate-index.d.ts +5 -1
  24. package/dist/commands/backlog/regenerate-index.d.ts.map +1 -1
  25. package/dist/commands/backlog/regenerate-index.js +7 -1
  26. package/dist/commands/backlog/shared/item-status.d.ts +13 -0
  27. package/dist/commands/backlog/shared/item-status.d.ts.map +1 -0
  28. package/dist/commands/backlog/shared/item-status.js +34 -0
  29. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  30. package/dist/commands/instructions/instructions.utils.js +24 -0
  31. package/dist/commands/pjm/doctor.d.ts.map +1 -1
  32. package/dist/commands/pjm/doctor.js +161 -0
  33. package/dist/commands/pjm/index.d.ts.map +1 -1
  34. package/dist/commands/pjm/index.js +2 -1
  35. package/dist/commands/pjm/init.d.ts +2 -1
  36. package/dist/commands/pjm/init.d.ts.map +1 -1
  37. package/dist/commands/pjm/init.js +7 -0
  38. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  39. package/dist/commands/project/dispatch-ceiling/index.js +80 -4
  40. package/package.json +2 -2
@@ -0,0 +1,35 @@
1
+ ---
2
+ oat_template: true
3
+ oat_template_name: repo-readme
4
+ ---
5
+
6
+ # OAT Repo Reference
7
+
8
+ Human-facing orientation for this OAT repo-reference root — the canonical
9
+ project-management and durable-reference surface for the repository.
10
+ Agent-facing rules live in the `AGENTS.md` files alongside each directory.
11
+
12
+ ## Layout
13
+
14
+ | Path | What it is | Maintained by |
15
+ | -------------------------- | -------------------------------------------------------- | ---------------------------------------------------------- |
16
+ | `pjm/current-state.md` | Present operating picture | Curated |
17
+ | `pjm/roadmap.md` | Now / Next / Later direction | Curated |
18
+ | `pjm/backlog/items/` | Active backlog items, one file each | Curated + `oat backlog` |
19
+ | `pjm/backlog/archived/` | Completed/abandoned item files | Moved here at close-out |
20
+ | `pjm/backlog/completed.md` | Newest-first completion summaries | Appended at close-out |
21
+ | `pjm/backlog/index.md` | Curated overview + generated item table | Overview curated; table via `oat backlog regenerate-index` |
22
+ | `pjm/handoffs/` | One-shot project-kickoff prompts (deleted when consumed) | Backlog walkthroughs; removed in the shipping PR |
23
+ | `reference/decisions/` | Durable decision records + generated index | `oat decision` |
24
+
25
+ ## Conventions
26
+
27
+ - **Generated vs. curated.** Generated tables live inside `<!-- OAT ... -->`
28
+ marker pairs; regenerate them with the owning `oat` command instead of
29
+ hand-editing. Everything outside those markers is curated by hand.
30
+ - **IDs.** Backlog items follow `BL-YYMMDD-slug` (`oat backlog generate-id`);
31
+ decisions follow `DR-YYMMDD-slug`. Pair every backlog item ID with its
32
+ human-readable title — no bare IDs in prose or handoffs.
33
+ - **Close-out.** When work ships that satisfies a backlog item's acceptance
34
+ criteria, the item is closed and archived in the same commit/PR — run
35
+ `oat backlog archive <id>`. See the Backlog Lifecycle in `pjm/AGENTS.md`.
@@ -0,0 +1,33 @@
1
+ import { type BacklogItemStatus } from './shared/item-status.js';
2
+ /** Result of an {@link archiveBacklogItem} run. */
3
+ export interface ArchiveBacklogItemResult {
4
+ id: string;
5
+ result: 'archived' | 'noop';
6
+ status: BacklogItemStatus | null;
7
+ completedEntry: 'written' | 'scaffolded' | 'skipped';
8
+ movedTo: string | null;
9
+ indexRegenerated: boolean;
10
+ warnings: string[];
11
+ }
12
+ export interface ArchiveBacklogItemOptions {
13
+ wontDo?: boolean;
14
+ summary?: string;
15
+ /** Injectable clock for deterministic tests; defaults to the current time. */
16
+ now?: Date;
17
+ }
18
+ /**
19
+ * Actionable close-out failure (unknown id, invalid current status). The
20
+ * command wrapper maps `exitCode` onto `process.exitCode` and logs `message`.
21
+ */
22
+ export declare class BacklogArchiveError extends Error {
23
+ readonly exitCode: 1 | 2;
24
+ constructor(message: string, exitCode?: 1 | 2);
25
+ }
26
+ /**
27
+ * Atomic backlog close-out. Validates the current status, sets the terminal
28
+ * status and `updated`, records a canonical `completed.md` entry, moves the
29
+ * item file into `archived/`, and regenerates the index. Idempotent when the
30
+ * item is already archived.
31
+ */
32
+ export declare function archiveBacklogItem(backlogRoot: string, id: string, options?: ArchiveBacklogItemOptions): Promise<ArchiveBacklogItemResult>;
33
+ //# sourceMappingURL=archive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/archive.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,iBAAiB,EAGvB,MAAM,sBAAsB,CAAC;AAoB9B,mDAAmD;AACnD,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,GAAG,MAAM,CAAC;IAC5B,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,cAAc,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;IACrD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,QAAQ,GAAE,CAAC,GAAG,CAAK;CAKjD;AAkID;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,yBAA8B,GACtC,OAAO,CAAC,wBAAwB,CAAC,CAkHnC"}
@@ -0,0 +1,229 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { access, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { getFrontmatterBlock } from '../shared/frontmatter.js';
6
+ import YAML from 'yaml';
7
+ import { regenerateBacklogIndex } from './regenerate-index.js';
8
+ import { BACKLOG_ITEM_STATUSES, extractBacklogStatus, isValidBacklogStatus, } from './shared/item-status.js';
9
+ const execFileAsync = promisify(execFile);
10
+ const COMPLETED_HEADING = '## Completed Items';
11
+ const TODO_SUMMARY = 'TODO: summarize outcome';
12
+ const STARTER_COMPLETED = [
13
+ '# OAT Backlog Completed',
14
+ '',
15
+ '> Summary archive for completed backlog work. Keep newest entries first. Use `backlog/archived/` for full file-per-item historical records when a completed item still needs rich context.',
16
+ '',
17
+ '## Entry Format',
18
+ '',
19
+ '- `YYYY-MM-DD — BL-YYMMDD-slug — Title — one-line outcome summary`',
20
+ '',
21
+ COMPLETED_HEADING,
22
+ '',
23
+ ].join('\n');
24
+ /**
25
+ * Actionable close-out failure (unknown id, invalid current status). The
26
+ * command wrapper maps `exitCode` onto `process.exitCode` and logs `message`.
27
+ */
28
+ export class BacklogArchiveError extends Error {
29
+ exitCode;
30
+ constructor(message, exitCode = 1) {
31
+ super(message);
32
+ this.name = 'BacklogArchiveError';
33
+ this.exitCode = exitCode;
34
+ }
35
+ }
36
+ async function pathExists(path) {
37
+ try {
38
+ await access(path);
39
+ return true;
40
+ }
41
+ catch (error) {
42
+ const code = error && typeof error === 'object' && 'code' in error
43
+ ? String(error.code)
44
+ : null;
45
+ if (code !== 'ENOENT') {
46
+ throw error;
47
+ }
48
+ return false;
49
+ }
50
+ }
51
+ async function isInsideGitWorkTree(cwd) {
52
+ try {
53
+ await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd });
54
+ return true;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * Read the item `title` from its frontmatter. Parses the YAML block (rather
62
+ * than a line regex) so a legitimate `#` inside a quoted title — e.g.
63
+ * `title: 'Fix #123 crash'` — is preserved instead of being stripped as an
64
+ * inline comment.
65
+ */
66
+ function readItemTitle(content) {
67
+ const block = getFrontmatterBlock(content);
68
+ if (!block) {
69
+ return '';
70
+ }
71
+ const parsed = YAML.parse(block);
72
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
73
+ return '';
74
+ }
75
+ return String(parsed.title ?? '').trim();
76
+ }
77
+ /**
78
+ * Minimal-diff frontmatter rewrite: replace only the `status:` value (keeping
79
+ * any inline enum comment) and the `updated:` value, leaving all surrounding
80
+ * formatting untouched.
81
+ */
82
+ function rewriteFrontmatter(content, status, updatedIso) {
83
+ let next = content.replace(/^(status:[ \t]*)(\S+)([ \t]*#.*)?$/m, (_match, prefix, _value, comment) => `${prefix}${status}${comment ?? ''}`);
84
+ next = next.replace(/^(updated:)[ \t]*.*$/m, (_match, prefix) => `${prefix} '${updatedIso}'`);
85
+ return next;
86
+ }
87
+ /**
88
+ * Insert `entryLine` as the first bullet beneath the `## Completed Items`
89
+ * heading. When the heading is absent it is scaffolded (with a warning). The
90
+ * returned flag distinguishes a plain append from one that had to scaffold the
91
+ * section.
92
+ */
93
+ function insertCompletedEntry(completed, entryLine) {
94
+ const lines = completed.split('\n');
95
+ const headingIndex = lines.findIndex((line) => line.trim() === COMPLETED_HEADING);
96
+ if (headingIndex === -1) {
97
+ const trimmed = completed.replace(/\s*$/, '');
98
+ const content = `${trimmed}\n\n${COMPLETED_HEADING}\n\n${entryLine}\n`;
99
+ return {
100
+ content,
101
+ scaffolded: true,
102
+ warning: `Completed log was missing a \`${COMPLETED_HEADING}\` heading; a new section was scaffolded and the entry appended.`,
103
+ };
104
+ }
105
+ let insertAt = headingIndex + 1;
106
+ while (insertAt < lines.length && lines[insertAt].trim() === '') {
107
+ insertAt += 1;
108
+ }
109
+ lines.splice(insertAt, 0, entryLine);
110
+ // Guarantee exactly one blank line between the heading and the first entry.
111
+ if (lines[headingIndex + 1] !== '') {
112
+ lines.splice(headingIndex + 1, 0, '');
113
+ }
114
+ let content = lines.join('\n');
115
+ if (!content.endsWith('\n')) {
116
+ content += '\n';
117
+ }
118
+ return { content, scaffolded: false, warning: null };
119
+ }
120
+ async function moveItemFile(backlogRoot, fromPath, toPath, warnings) {
121
+ if (await isInsideGitWorkTree(backlogRoot)) {
122
+ try {
123
+ await execFileAsync('git', ['mv', fromPath, toPath], {
124
+ cwd: backlogRoot,
125
+ });
126
+ return;
127
+ }
128
+ catch {
129
+ warnings.push(`\`git mv\` failed for ${fromPath}; falling back to a plain filesystem rename (the move is no longer staged).`);
130
+ }
131
+ }
132
+ await rename(fromPath, toPath);
133
+ }
134
+ /**
135
+ * Atomic backlog close-out. Validates the current status, sets the terminal
136
+ * status and `updated`, records a canonical `completed.md` entry, moves the
137
+ * item file into `archived/`, and regenerates the index. Idempotent when the
138
+ * item is already archived.
139
+ */
140
+ export async function archiveBacklogItem(backlogRoot, id, options = {}) {
141
+ const itemsPath = join(backlogRoot, 'items', `${id}.md`);
142
+ const archivedPath = join(backlogRoot, 'archived', `${id}.md`);
143
+ const warnings = [];
144
+ // Idempotent no-op: already archived.
145
+ if (await pathExists(archivedPath)) {
146
+ // Conflicting duplicate: the same id lives in BOTH `items/` and
147
+ // `archived/`. Treating this as a clean no-op would silently leave the live
148
+ // `items/` copy unarchived, while auto-archiving would clobber the existing
149
+ // archived record. Refuse and make the user reconcile the duplicate.
150
+ if (await pathExists(itemsPath)) {
151
+ throw new BacklogArchiveError(`Backlog item ${id} exists in both \`items/\` (${itemsPath}) and \`archived/\` (${archivedPath}). Auto-archiving would clobber the existing archived record, so this is left untouched. Fix: reconcile the duplicate manually — decide which of the two files is authoritative and remove the other — then re-run \`oat backlog archive ${id}\`.`);
152
+ }
153
+ let status = null;
154
+ try {
155
+ const extracted = extractBacklogStatus(await readFile(archivedPath, 'utf8'));
156
+ status = extracted && isValidBacklogStatus(extracted) ? extracted : null;
157
+ }
158
+ catch {
159
+ status = null;
160
+ }
161
+ warnings.push(`Backlog item ${id} is already archived at ${archivedPath}; nothing to do.`);
162
+ return {
163
+ id,
164
+ result: 'noop',
165
+ status,
166
+ completedEntry: 'skipped',
167
+ movedTo: archivedPath,
168
+ indexRegenerated: false,
169
+ warnings,
170
+ };
171
+ }
172
+ if (!(await pathExists(itemsPath))) {
173
+ throw new BacklogArchiveError(`Backlog item ${id} not found at ${itemsPath}. Confirm the id and that the file lives under \`items/\`, then re-run \`oat backlog archive ${id}\`.`);
174
+ }
175
+ const content = await readFile(itemsPath, 'utf8');
176
+ const currentStatus = extractBacklogStatus(content);
177
+ if (currentStatus === null || !isValidBacklogStatus(currentStatus)) {
178
+ throw new BacklogArchiveError(`Backlog item ${itemsPath} has invalid status "${currentStatus ?? ''}". Valid statuses: ${BACKLOG_ITEM_STATUSES.join(', ')}. Fix: correct the \`status\` field manually, then re-run \`oat backlog archive ${id}\`.`);
179
+ }
180
+ const targetStatus = options.wontDo ? 'wont_do' : 'closed';
181
+ const now = options.now ?? new Date();
182
+ const updatedIso = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
183
+ const entryDate = updatedIso.slice(0, 10);
184
+ const title = readItemTitle(content);
185
+ // 4. Rewrite frontmatter in place (before the move) so a crash leaves the
186
+ // detectable "terminal status still in items/" drift, never corruption.
187
+ await writeFile(itemsPath, rewriteFrontmatter(content, targetStatus, updatedIso), 'utf8');
188
+ // 5. completed.md entry — always for `closed`; only with a summary for
189
+ // `wont_do`.
190
+ let completedEntry = 'skipped';
191
+ const shouldWriteEntry = targetStatus === 'closed' || Boolean(options.summary);
192
+ if (shouldWriteEntry) {
193
+ const summary = options.summary?.trim()
194
+ ? options.summary.trim()
195
+ : TODO_SUMMARY;
196
+ const entryLine = `- ${entryDate} — ${id} — ${title} — ${summary}`;
197
+ const completedPath = join(backlogRoot, 'completed.md');
198
+ let scaffoldedFile = false;
199
+ let existing;
200
+ if (await pathExists(completedPath)) {
201
+ existing = await readFile(completedPath, 'utf8');
202
+ }
203
+ else {
204
+ existing = STARTER_COMPLETED;
205
+ scaffoldedFile = true;
206
+ }
207
+ const inserted = insertCompletedEntry(existing, entryLine);
208
+ if (inserted.warning) {
209
+ warnings.push(inserted.warning);
210
+ }
211
+ await writeFile(completedPath, inserted.content, 'utf8');
212
+ completedEntry =
213
+ scaffoldedFile || inserted.scaffolded ? 'scaffolded' : 'written';
214
+ }
215
+ // 6. Move items/<id>.md -> archived/<id>.md (git mv with rename fallback).
216
+ await moveItemFile(backlogRoot, itemsPath, archivedPath, warnings);
217
+ // 7. Regenerate the index via the exported core.
218
+ const regeneration = await regenerateBacklogIndex(backlogRoot);
219
+ warnings.push(...regeneration.warnings);
220
+ return {
221
+ id,
222
+ result: 'archived',
223
+ status: targetStatus,
224
+ completedEntry,
225
+ movedTo: archivedPath,
226
+ indexRegenerated: true,
227
+ warnings,
228
+ };
229
+ }
@@ -1,6 +1,7 @@
1
1
  import { buildCommandContext } from '../../app/command-context.js';
2
2
  import { resolveProjectRoot } from '../../fs/paths.js';
3
3
  import { Command } from 'commander';
4
+ import { archiveBacklogItem } from './archive.js';
4
5
  import { initializeBacklog } from './init.js';
5
6
  import { regenerateBacklogIndex } from './regenerate-index.js';
6
7
  interface BacklogCommandDependencies {
@@ -8,6 +9,7 @@ interface BacklogCommandDependencies {
8
9
  resolveProjectRoot: typeof resolveProjectRoot;
9
10
  initializeBacklog: typeof initializeBacklog;
10
11
  regenerateBacklogIndex: typeof regenerateBacklogIndex;
12
+ archiveBacklogItem: typeof archiveBacklogItem;
11
13
  pathExists: (path: string) => Promise<boolean>;
12
14
  }
13
15
  export declare function createBacklogCommand(overrides?: Partial<BacklogCommandDependencies>): Command;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAe5D,UAAU,0BAA0B;IAClC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAChD;AAyCD,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,OAAO,CAAC,0BAA0B,CAAM,GAClD,OAAO,CAgIT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAAuB,MAAM,WAAW,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAqB5D,UAAU,0BAA0B;IAClC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAChD;AA0CD,wBAAgB,oBAAoB,CAClC,SAAS,GAAE,OAAO,CAAC,0BAA0B,CAAM,GAClD,OAAO,CAiMT"}
@@ -4,6 +4,7 @@ import { buildCommandContext } from '../../app/command-context.js';
4
4
  import { readGlobalOptions } from '../shared/shared.utils.js';
5
5
  import { resolveProjectRoot } from '../../fs/paths.js';
6
6
  import { Command } from 'commander';
7
+ import { archiveBacklogItem, BacklogArchiveError } from './archive.js';
7
8
  import { initializeBacklog } from './init.js';
8
9
  import { regenerateBacklogIndex } from './regenerate-index.js';
9
10
  import { generateBacklogId } from './shared/generate-id.js';
@@ -27,6 +28,7 @@ const DEFAULT_DEPENDENCIES = {
27
28
  resolveProjectRoot,
28
29
  initializeBacklog,
29
30
  regenerateBacklogIndex,
31
+ archiveBacklogItem,
30
32
  pathExists: pathExistsDefault,
31
33
  };
32
34
  async function resolveBacklogRoot(context, configuredRoot, dependencies = DEFAULT_DEPENDENCIES) {
@@ -65,15 +67,67 @@ export function createBacklogCommand(overrides = {}) {
65
67
  .action(async (options, command) => {
66
68
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
67
69
  const backlogRoot = await resolveBacklogRoot(context, options.backlogRoot, dependencies);
68
- await dependencies.regenerateBacklogIndex(backlogRoot);
70
+ const { itemCount, warnings } = await dependencies.regenerateBacklogIndex(backlogRoot);
69
71
  if (context.json) {
70
- context.logger.json({ status: 'ok', backlogRoot });
72
+ context.logger.json({ status: 'ok', backlogRoot, itemCount, warnings });
71
73
  }
72
74
  else {
75
+ for (const warning of warnings) {
76
+ context.logger.warn(warning);
77
+ }
73
78
  context.logger.info(`Regenerated backlog index at ${backlogRoot}`);
74
79
  }
75
80
  process.exitCode = 0;
76
81
  });
82
+ cmd
83
+ .command('archive')
84
+ .description('Close out a backlog item: set a terminal status, record it in completed.md, move it to archived/, and regenerate the index')
85
+ .argument('<id>', 'Backlog item id (`BL-YYMMDD-slug`)')
86
+ .option('--wont-do', 'Close the item as `wont_do` instead of `closed`')
87
+ .option('--summary <text>', 'One-line outcome summary for completed.md')
88
+ .option('--backlog-root <path>', 'Backlog root directory (defaults to .oat/repo/pjm/backlog)')
89
+ .action(async (id, options, command) => {
90
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
91
+ const backlogRoot = await resolveBacklogRoot(context, options.backlogRoot, dependencies);
92
+ try {
93
+ const result = await dependencies.archiveBacklogItem(backlogRoot, id, {
94
+ wontDo: options.wontDo,
95
+ summary: options.summary,
96
+ });
97
+ if (context.json) {
98
+ context.logger.json(result);
99
+ }
100
+ else {
101
+ for (const warning of result.warnings) {
102
+ context.logger.warn(warning);
103
+ }
104
+ if (result.result === 'noop') {
105
+ context.logger.info(`Backlog item ${id} is already archived.`);
106
+ }
107
+ else {
108
+ context.logger.success(`Archived ${id} as ${result.status} (moved to ${result.movedTo}).`);
109
+ }
110
+ }
111
+ process.exitCode = 0;
112
+ }
113
+ catch (error) {
114
+ if (error instanceof BacklogArchiveError) {
115
+ if (context.json) {
116
+ context.logger.json({
117
+ result: 'error',
118
+ id,
119
+ message: error.message,
120
+ });
121
+ }
122
+ else {
123
+ context.logger.error(error.message);
124
+ }
125
+ process.exitCode = error.exitCode;
126
+ return;
127
+ }
128
+ throw error;
129
+ }
130
+ });
77
131
  cmd
78
132
  .command('generate-id')
79
133
  .description('Generate a backlog item identifier (`BL-YYMMDD-slug`) from a title or slug')
@@ -1,2 +1,6 @@
1
- export declare function regenerateBacklogIndex(backlogRoot: string): Promise<void>;
1
+ export interface RegenerateBacklogIndexResult {
2
+ itemCount: number;
3
+ warnings: string[];
4
+ }
5
+ export declare function regenerateBacklogIndex(backlogRoot: string): Promise<RegenerateBacklogIndexResult>;
2
6
  //# sourceMappingURL=regenerate-index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"regenerate-index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/regenerate-index.ts"],"names":[],"mappings":"AAmGA,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CA+Cf"}
1
+ {"version":3,"file":"regenerate-index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/regenerate-index.ts"],"names":[],"mappings":"AAwGA,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,4BAA4B,CAAC,CAsDvC"}
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { getFrontmatterBlock } from '../shared/frontmatter.js';
4
4
  import { computeManagedIndexUpdate } from '../shared/managed-index.js';
5
5
  import YAML from 'yaml';
6
+ import { BACKLOG_ITEM_STATUSES, isValidBacklogStatus, } from './shared/item-status.js';
6
7
  const INDEX_START = '<!-- OAT BACKLOG-INDEX -->';
7
8
  const INDEX_END = '<!-- END OAT BACKLOG-INDEX -->';
8
9
  const PRIORITY_ORDER = {
@@ -71,10 +72,14 @@ export async function regenerateBacklogIndex(backlogRoot) {
71
72
  .map((entry) => entry.name)
72
73
  .sort();
73
74
  const items = [];
75
+ const warnings = [];
74
76
  for (const entry of entries) {
75
77
  const filePath = join(itemsDir, entry);
76
78
  const item = parseItemFrontmatter(await readFile(filePath, 'utf8'), filePath);
77
79
  if (item) {
80
+ if (item.status !== '' && !isValidBacklogStatus(item.status)) {
81
+ warnings.push(`Backlog item ${filePath} has invalid status "${item.status}". Valid statuses: ${BACKLOG_ITEM_STATUSES.join(', ')}.`);
82
+ }
78
83
  items.push(item);
79
84
  }
80
85
  }
@@ -91,7 +96,8 @@ export async function regenerateBacklogIndex(backlogRoot) {
91
96
  // write. Only rewrite on a genuine content change.
92
97
  const { content: updated } = computeManagedIndexUpdate(content, startIndex, endIndex + INDEX_END.length, renderManagedSection(items));
93
98
  if (updated === null) {
94
- return;
99
+ return { itemCount: items.length, warnings };
95
100
  }
96
101
  await writeFile(indexPath, updated, 'utf8');
102
+ return { itemCount: items.length, warnings };
97
103
  }
@@ -0,0 +1,13 @@
1
+ export declare const BACKLOG_ITEM_STATUSES: readonly ["open", "in_progress", "closed", "wont_do"];
2
+ export type BacklogItemStatus = (typeof BACKLOG_ITEM_STATUSES)[number];
3
+ export declare const TERMINAL_BACKLOG_STATUSES: readonly BacklogItemStatus[];
4
+ export declare function isValidBacklogStatus(value: string): value is BacklogItemStatus;
5
+ export declare function isTerminalBacklogStatus(value: string): boolean;
6
+ /**
7
+ * Extract the `status` value from backlog item content. Accepts either full
8
+ * item content (with `---` fences) or a bare frontmatter block. Inline enum
9
+ * comments (`status: closed # ...`) are stripped. Returns `null` when no
10
+ * `status` field is present.
11
+ */
12
+ export declare function extractBacklogStatus(frontmatterContent: string): string | null;
13
+ //# sourceMappingURL=item-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"item-status.d.ts","sourceRoot":"","sources":["../../../../src/commands/backlog/shared/item-status.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,qBAAqB,uDAKxB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvE,eAAO,MAAM,yBAAyB,EAAE,SAAS,iBAAiB,EAGjE,CAAC;AAEF,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,iBAAiB,CAE5B;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,kBAAkB,EAAE,MAAM,GACzB,MAAM,GAAG,IAAI,CAUf"}
@@ -0,0 +1,34 @@
1
+ // Single source of truth for backlog item statuses. Kept dependency-free so
2
+ // cross-directory consumers (archive, regenerate-index, pjm doctor) can import
3
+ // it via the `@commands/...` alias without pulling in a dependency chain.
4
+ export const BACKLOG_ITEM_STATUSES = [
5
+ 'open',
6
+ 'in_progress',
7
+ 'closed',
8
+ 'wont_do',
9
+ ];
10
+ export const TERMINAL_BACKLOG_STATUSES = [
11
+ 'closed',
12
+ 'wont_do',
13
+ ];
14
+ export function isValidBacklogStatus(value) {
15
+ return BACKLOG_ITEM_STATUSES.includes(value);
16
+ }
17
+ export function isTerminalBacklogStatus(value) {
18
+ return TERMINAL_BACKLOG_STATUSES.includes(value);
19
+ }
20
+ /**
21
+ * Extract the `status` value from backlog item content. Accepts either full
22
+ * item content (with `---` fences) or a bare frontmatter block. Inline enum
23
+ * comments (`status: closed # ...`) are stripped. Returns `null` when no
24
+ * `status` field is present.
25
+ */
26
+ export function extractBacklogStatus(frontmatterContent) {
27
+ const fenced = frontmatterContent.match(/^---\n([\s\S]*?)\n---/);
28
+ const block = fenced ? fenced[1] : frontmatterContent;
29
+ const match = block.match(/^status:\s*(.+)$/m);
30
+ if (!match) {
31
+ return null;
32
+ }
33
+ return match[1].replace(/\s*#.*$/, '').trim();
34
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"instructions.utils.d.ts","sourceRoot":"","sources":["../../../src/commands/instructions/instructions.utils.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAE5B,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,iCAAiC,EAAE,uBACrC,CAAC;AAKZ,UAAU,4BAA4B;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAqBD,wBAAgB,8BAA8B,CAC5C,QAAQ,CAAC,EAAE,uBAAuB,GACjC,uBAAuB,CAEzB;AAoLD,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,uBAA4B,EACrC,SAAS,GAAE,OAAO,CAAC,4BAA4B,CAAM,GACpD,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiO7B;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,OAAO,EAAE,uBAAuB,EAAE,GACjC,mBAAmB,CAoBrB;AAgBD,wBAAgB,wBAAwB,CAAC,EACvC,IAAI,EACJ,OAAO,EACP,OAAO,GACR,EAAE,4BAA4B,GAAG,uBAAuB,CAWxD;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAmCR"}
1
+ {"version":3,"file":"instructions.utils.d.ts","sourceRoot":"","sources":["../../../src/commands/instructions/instructions.utils.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,gBAAgB,EAChB,uBAAuB,EACvB,4BAA4B,EAE5B,mBAAmB,EACpB,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AACtD,eAAO,MAAM,iCAAiC,EAAE,uBACrC,CAAC;AAcZ,UAAU,4BAA4B;IACpC,IAAI,EAAE,gBAAgB,CAAC;IACvB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE,uBAAuB,EAAE,CAAC;CACpC;AAiCD,wBAAgB,8BAA8B,CAC5C,QAAQ,CAAC,EAAE,uBAAuB,GACjC,uBAAuB,CAEzB;AA2LD,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,uBAA4B,EACrC,SAAS,GAAE,OAAO,CAAC,4BAA4B,CAAM,GACpD,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAiO7B;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,OAAO,EAAE,uBAAuB,EAAE,GACjC,mBAAmB,CAoBrB;AAgBD,wBAAgB,wBAAwB,CAAC,EACvC,IAAI,EACJ,OAAO,EACP,OAAO,GACR,EAAE,4BAA4B,GAAG,uBAAuB,CAWxD;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAmCR"}
@@ -4,6 +4,23 @@ export const EXPECTED_CLAUDE_CONTENT = '@AGENTS.md\n';
4
4
  export const DEFAULT_INSTRUCTION_SYNC_STRATEGY = 'pointer';
5
5
  const ROOT_EXCLUDED_DIRECTORIES = new Set(['.git', '.oat', '.worktrees']);
6
6
  const GLOBAL_EXCLUDED_DIRECTORIES = new Set(['node_modules']);
7
+ // The only excluded-subtree re-entry points: a root-level directory is excluded
8
+ // wholesale, but a named subdirectory holds tracked instruction files that
9
+ // sync/validate must manage, so it is carved back into the scan. `.oat` is
10
+ // excluded at the repo root, yet `.oat/repo/**` carries AGENTS.md/CLAUDE.md
11
+ // pairs; `.oat/templates`, `.oat/projects`, and `.oat/sync` stay unscanned.
12
+ const ROOT_EXCLUDED_DIRECTORY_CARVE_INS = new Map([
13
+ ['.oat', 'repo'],
14
+ ]);
15
+ async function directoryExists(dependencies, directoryPath) {
16
+ try {
17
+ const stats = await dependencies.stat(directoryPath);
18
+ return stats.isDirectory();
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
7
24
  function getErrorCode(error) {
8
25
  return error && typeof error === 'object' && 'code' in error
9
26
  ? String(error.code)
@@ -97,6 +114,13 @@ async function scanInstructionDirectories(repoRoot, dependencies, debug) {
97
114
  continue;
98
115
  }
99
116
  if (isRootLevel && ROOT_EXCLUDED_DIRECTORIES.has(entry.name)) {
117
+ const carveIn = ROOT_EXCLUDED_DIRECTORY_CARVE_INS.get(entry.name);
118
+ if (carveIn) {
119
+ const carveInPath = join(entryPath, carveIn);
120
+ if (await directoryExists(dependencies, carveInPath)) {
121
+ queue.push(carveInPath);
122
+ }
123
+ }
100
124
  continue;
101
125
  }
102
126
  queue.push(entryPath);
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/doctor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA0I9C,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,wBAAgB,sBAAsB,IAAI,WAAW,CAQpD;AAED,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,EAAE,CAAC,CAyJxB"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/doctor.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA6L9C,MAAM,WAAW,gBAAgB;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED,wBAAgB,sBAAsB,IAAI,WAAW,CAQpD;AAED,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,EAAE,CAAC,CAoTxB"}