@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.
- package/assets/agents/oat-phase-implementer.md +2 -2
- package/assets/agents/oat-reviewer.md +10 -8
- package/assets/docs/cli-utilities/backlog-lifecycle.md +61 -0
- package/assets/docs/cli-utilities/config-and-local-state.md +47 -0
- package/assets/docs/cli-utilities/index.md +1 -0
- package/assets/docs/cli-utilities/workflow-gates.md +55 -0
- package/assets/docs/provider-sync/instruction-sync.md +1 -0
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/workflows/projects/artifacts.md +22 -0
- package/assets/docs/workflows/projects/hill-checkpoints.md +2 -0
- package/assets/docs/workflows/projects/implementation-execution.md +2 -1
- package/assets/docs/workflows/projects/reviews.md +17 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-pjm-review-backlog/SKILL.md +29 -7
- package/assets/skills/oat-pjm-update-repo-reference/SKILL.md +25 -5
- package/assets/skills/oat-project-implement/SKILL.md +76 -5
- package/assets/skills/oat-project-review-provide/SKILL.md +25 -13
- package/assets/skills/oat-project-review-receive/SKILL.md +20 -10
- package/assets/skills/oat-review-provide/SKILL.md +14 -12
- package/assets/skills/oat-review-provide/references/review-artifact-template.md +1 -1
- package/assets/templates/pjm-agents.md +80 -1
- package/assets/templates/pjm-handoffs-readme.md +21 -0
- package/assets/templates/reference-agents.md +13 -0
- package/assets/templates/repo-agents.md +4 -0
- package/assets/templates/repo-readme.md +35 -0
- package/dist/commands/backlog/archive.d.ts +33 -0
- package/dist/commands/backlog/archive.d.ts.map +1 -0
- package/dist/commands/backlog/archive.js +229 -0
- package/dist/commands/backlog/index.d.ts +2 -0
- package/dist/commands/backlog/index.d.ts.map +1 -1
- package/dist/commands/backlog/index.js +56 -2
- package/dist/commands/backlog/regenerate-index.d.ts +5 -1
- package/dist/commands/backlog/regenerate-index.d.ts.map +1 -1
- package/dist/commands/backlog/regenerate-index.js +7 -1
- package/dist/commands/backlog/shared/item-status.d.ts +13 -0
- package/dist/commands/backlog/shared/item-status.d.ts.map +1 -0
- package/dist/commands/backlog/shared/item-status.js +34 -0
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +15 -3
- package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.js +24 -0
- package/dist/commands/pjm/doctor.d.ts.map +1 -1
- package/dist/commands/pjm/doctor.js +161 -0
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +2 -1
- package/dist/commands/pjm/init.d.ts +2 -1
- package/dist/commands/pjm/init.d.ts.map +1 -1
- package/dist/commands/pjm/init.js +7 -0
- package/dist/commands/review/latest.d.ts.map +1 -1
- package/dist/commands/review/latest.js +5 -2
- package/dist/commands/shared/frontmatter.d.ts +11 -0
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +15 -0
- 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;
|
|
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
|
|
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":"
|
|
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;
|
|
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 =
|
|
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;
|
|
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":"
|
|
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"}
|