@open-agent-toolkit/cli 0.1.40 → 0.1.42

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 (54) hide show
  1. package/assets/agents/oat-phase-implementer.md +2 -2
  2. package/assets/agents/oat-reviewer.md +10 -8
  3. package/assets/docs/cli-utilities/backlog-lifecycle.md +61 -0
  4. package/assets/docs/cli-utilities/config-and-local-state.md +47 -0
  5. package/assets/docs/cli-utilities/index.md +1 -0
  6. package/assets/docs/cli-utilities/workflow-gates.md +55 -0
  7. package/assets/docs/provider-sync/instruction-sync.md +1 -0
  8. package/assets/docs/reference/cli-reference.md +1 -1
  9. package/assets/docs/workflows/projects/artifacts.md +22 -0
  10. package/assets/docs/workflows/projects/hill-checkpoints.md +2 -0
  11. package/assets/docs/workflows/projects/implementation-execution.md +2 -1
  12. package/assets/docs/workflows/projects/reviews.md +17 -0
  13. package/assets/public-package-versions.json +4 -4
  14. package/assets/skills/oat-pjm-review-backlog/SKILL.md +29 -7
  15. package/assets/skills/oat-pjm-update-repo-reference/SKILL.md +25 -5
  16. package/assets/skills/oat-project-implement/SKILL.md +76 -5
  17. package/assets/skills/oat-project-review-provide/SKILL.md +25 -13
  18. package/assets/skills/oat-project-review-receive/SKILL.md +20 -10
  19. package/assets/skills/oat-review-provide/SKILL.md +14 -12
  20. package/assets/skills/oat-review-provide/references/review-artifact-template.md +1 -1
  21. package/assets/templates/pjm-agents.md +80 -1
  22. package/assets/templates/pjm-handoffs-readme.md +21 -0
  23. package/assets/templates/reference-agents.md +13 -0
  24. package/assets/templates/repo-agents.md +4 -0
  25. package/assets/templates/repo-readme.md +35 -0
  26. package/dist/commands/backlog/archive.d.ts +33 -0
  27. package/dist/commands/backlog/archive.d.ts.map +1 -0
  28. package/dist/commands/backlog/archive.js +229 -0
  29. package/dist/commands/backlog/index.d.ts +2 -0
  30. package/dist/commands/backlog/index.d.ts.map +1 -1
  31. package/dist/commands/backlog/index.js +56 -2
  32. package/dist/commands/backlog/regenerate-index.d.ts +5 -1
  33. package/dist/commands/backlog/regenerate-index.d.ts.map +1 -1
  34. package/dist/commands/backlog/regenerate-index.js +7 -1
  35. package/dist/commands/backlog/shared/item-status.d.ts +13 -0
  36. package/dist/commands/backlog/shared/item-status.d.ts.map +1 -0
  37. package/dist/commands/backlog/shared/item-status.js +34 -0
  38. package/dist/commands/gate/index.d.ts.map +1 -1
  39. package/dist/commands/gate/index.js +15 -3
  40. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  41. package/dist/commands/instructions/instructions.utils.js +24 -0
  42. package/dist/commands/pjm/doctor.d.ts.map +1 -1
  43. package/dist/commands/pjm/doctor.js +161 -0
  44. package/dist/commands/pjm/index.d.ts.map +1 -1
  45. package/dist/commands/pjm/index.js +2 -1
  46. package/dist/commands/pjm/init.d.ts +2 -1
  47. package/dist/commands/pjm/init.d.ts.map +1 -1
  48. package/dist/commands/pjm/init.js +7 -0
  49. package/dist/commands/review/latest.d.ts.map +1 -1
  50. package/dist/commands/review/latest.js +5 -2
  51. package/dist/commands/shared/frontmatter.d.ts +11 -0
  52. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  53. package/dist/commands/shared/frontmatter.js +15 -0
  54. package/package.json +2 -2
@@ -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":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,EAQL,KAAK,UAAU,EAIf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,cAAc,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,UAAU,EAAE,CACV,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AA0CD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,OAAO,EAAE,gBAAgB,GAAG,cAAc,GAAG,SAAS,CAAC;IACvD,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC7B;AAED,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,kBAAkB,GAAG,cAAc,GAAG,MAAM,CAAC;AAMlD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAyZD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,EAC9C,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,GAAG,IAAI,CAE3B;AAo6BD,wBAAgB,iBAAiB,CAC/B,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAwKT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,EAQL,KAAK,UAAU,EAIf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,cAAc,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,UAAU,EAAE,CACV,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AA0CD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,OAAO,EAAE,gBAAgB,GAAG,cAAc,GAAG,SAAS,CAAC;IACvD,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC7B;AAED,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,kBAAkB,GAAG,cAAc,GAAG,MAAM,CAAC;AAMlD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAyZD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,EAC9C,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,GAAG,IAAI,CAE3B;AAu7BD,wBAAgB,iBAAiB,CAC/B,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAwKT"}
@@ -1,9 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { createHash } from 'node:crypto';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { readdir, readFile } from 'node:fs/promises';
4
4
  import { basename, isAbsolute, join, relative } from 'node:path';
5
5
  import { buildCommandContext, } from '../../app/command-context.js';
6
- import { getFrontmatterBlock, getFrontmatterField, } from '../shared/frontmatter.js';
6
+ import { getFrontmatterBlock, getFrontmatterField, parseGeneratedTime, } from '../shared/frontmatter.js';
7
7
  import { readGlobalOptions } from '../shared/shared.utils.js';
8
8
  import { BUILTIN_EXEC_TARGETS, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
9
9
  import { resolveEffectiveConfig, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
@@ -502,7 +502,7 @@ async function readReviewGateArtifactCandidate(repoRoot, relativePath) {
502
502
  if (!generatedAt) {
503
503
  return null;
504
504
  }
505
- const generatedTime = Date.parse(generatedAt);
505
+ const generatedTime = parseGeneratedTime(generatedAt);
506
506
  if (Number.isNaN(generatedTime)) {
507
507
  return null;
508
508
  }
@@ -616,6 +616,7 @@ function writeReviewGateResult(context, payload) {
616
616
  else {
617
617
  context.logger.info('Review completed and gate passed.');
618
618
  }
619
+ context.logger.info(`Run: ${payload.runId} (generated ${payload.generatedAt})`);
619
620
  context.logger.info(`Review artifact: ${payload.artifactPath}`);
620
621
  if (payload.normalization) {
621
622
  context.logger.info(`Artifact normalized: inserted ${payload.normalization.insertedSeverities.map((severity) => severityDisplayName(severity)).join(', ')} empty Findings section(s).`);
@@ -629,6 +630,7 @@ function writeReviewGateExecutionFailure(context, payload) {
629
630
  context.logger.json({
630
631
  status: 'review_failed',
631
632
  outcome: 'review_did_not_complete',
633
+ runId: payload.runId,
632
634
  target: payload.target,
633
635
  project: payload.project,
634
636
  exitCode: payload.exitCode,
@@ -643,9 +645,11 @@ function writeReviewGateArtifactValidationFailure(context, payload) {
643
645
  context.logger.json({
644
646
  status: 'artifact_validation_failed',
645
647
  outcome: 'review_completed_artifact_validation_failed',
648
+ runId: payload.runId,
646
649
  target: payload.target,
647
650
  project: payload.project,
648
651
  artifactPath: payload.artifactPath,
652
+ generatedAt: payload.generatedAt,
649
653
  message: payload.message,
650
654
  recovery: payload.recovery,
651
655
  });
@@ -755,6 +759,7 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
755
759
  }
756
760
  }
757
761
  async function runReviewGate(prompt, options, context, dependencies) {
762
+ const runId = randomUUID();
758
763
  try {
759
764
  const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
760
765
  const userConfigDir = join(context.home, '.oat');
@@ -785,6 +790,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
785
790
  const childExitCode = await executeTarget(selected, [reviewPrompt], context, dependencies);
786
791
  if (childExitCode !== 0) {
787
792
  writeReviewGateExecutionFailure(context, {
793
+ runId,
788
794
  target: selected.id,
789
795
  project: projectPath,
790
796
  exitCode: childExitCode,
@@ -799,9 +805,11 @@ async function runReviewGate(prompt, options, context, dependencies) {
799
805
  const producedArtifact = findProducedReviewArtifact(before, after);
800
806
  if (!producedArtifact) {
801
807
  writeReviewGateArtifactValidationFailure(context, {
808
+ runId,
802
809
  target: selected.id,
803
810
  project: projectPath,
804
811
  artifactPath: null,
812
+ generatedAt: null,
805
813
  message: `No new review artifact was detected for project ${projectPath}.`,
806
814
  recovery: 'Ensure the review provider wrote a project review artifact. If it did, fix or attach that artifact and run oat-project-review-receive before treating the review as consumed.',
807
815
  });
@@ -817,9 +825,11 @@ async function runReviewGate(prompt, options, context, dependencies) {
817
825
  catch (error) {
818
826
  const detail = error instanceof Error ? error.message : String(error);
819
827
  writeReviewGateArtifactValidationFailure(context, {
828
+ runId,
820
829
  target: selected.id,
821
830
  project: projectPath,
822
831
  artifactPath: producedArtifact.path,
832
+ generatedAt: producedArtifact.generatedAt,
823
833
  message: detail,
824
834
  recovery: `The review artifact was created at ${producedArtifact.path} but could not be consumed. Fix the artifact format, then run oat-project-review-receive for ${producedArtifact.path}; if the only issue is a missing zero-count severity heading, rerun the gate to normalize the same artifact instead of creating a new review version.`,
825
835
  });
@@ -835,9 +845,11 @@ async function runReviewGate(prompt, options, context, dependencies) {
835
845
  });
836
846
  writeReviewGateResult(context, {
837
847
  status: blocking ? 'blocked' : 'ok',
848
+ runId,
838
849
  target: selected.id,
839
850
  project: projectPath,
840
851
  artifactPath: producedArtifact.path,
852
+ generatedAt: producedArtifact.generatedAt,
841
853
  threshold,
842
854
  blocking,
843
855
  counts: verdict.counts,
@@ -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"}