@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
@@ -1,5 +1,6 @@
1
1
  import { access, readdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { BACKLOG_ITEM_STATUSES, extractBacklogStatus, isTerminalBacklogStatus, isValidBacklogStatus, } from '../backlog/shared/item-status.js';
3
4
  import { getFrontmatterBlock } from '../shared/frontmatter.js';
4
5
  import { CANONICAL_REPO_REFERENCE_PATHS } from './init.js';
5
6
  const ALLOWED_TOP_LEVEL_DIRECTORIES = new Set([
@@ -108,6 +109,45 @@ function containsTemplateFrontmatter(content) {
108
109
  (/\boat_template\s*:/i.test(frontmatter) ||
109
110
  /\boat_template_name\s*:/i.test(frontmatter)));
110
111
  }
112
+ const BACKLOG_ITEMS_DIRECTORY = 'pjm/backlog/items';
113
+ const BACKLOG_ARCHIVED_DIRECTORY = 'pjm/backlog/archived';
114
+ const BACKLOG_COMPLETED_FILE = 'pjm/backlog/completed.md';
115
+ // Best-effort backlog id scan: `BL-YYMMDD-slug` (legacy lowercase `bl-`
116
+ // matched case-insensitively). The six-digit requirement keeps the literal
117
+ // `BL-YYMMDD-slug` format placeholder in `completed.md` from matching.
118
+ const BACKLOG_ID_PATTERN = /\bbl-\d{6}-[a-z0-9][a-z0-9-]*/gi;
119
+ /**
120
+ * Single frontmatter pass over both backlog directories. Every drift check
121
+ * derives from this scan so the filesystem is walked once. `index.md` is the
122
+ * managed index, not an item, so it is excluded.
123
+ */
124
+ async function collectBacklogItems(repoRoot) {
125
+ const records = [];
126
+ const directories = [
127
+ { directory: 'items', relativeDir: BACKLOG_ITEMS_DIRECTORY },
128
+ { directory: 'archived', relativeDir: BACKLOG_ARCHIVED_DIRECTORY },
129
+ ];
130
+ for (const { directory, relativeDir } of directories) {
131
+ const fileNames = await listMarkdownFiles(join(repoRoot, relativeDir));
132
+ for (const fileName of fileNames) {
133
+ if (fileName === 'index.md') {
134
+ continue;
135
+ }
136
+ const content = await readIfExists(join(repoRoot, relativeDir, fileName));
137
+ records.push({
138
+ directory,
139
+ relativePath: `${relativeDir}/${fileName}`,
140
+ id: fileName.replace(/\.md$/, ''),
141
+ status: content ? extractBacklogStatus(content) : null,
142
+ });
143
+ }
144
+ }
145
+ return records;
146
+ }
147
+ /** Best-effort scan of completed-log content for backlog item ids. */
148
+ function extractCompletedIds(content) {
149
+ return [...content.matchAll(BACKLOG_ID_PATTERN)].map((match) => match[0]);
150
+ }
111
151
  function checkStatus(missing) {
112
152
  return missing.length === 0 ? 'pass' : 'fail';
113
153
  }
@@ -246,5 +286,126 @@ export async function runPjmDoctorChecks(repoRoot, options = {}) {
246
286
  ? undefined
247
287
  : 'Move active operational docs to pjm/ and leave reference/ for durable append-mostly artifacts.',
248
288
  });
289
+ const backlogItems = await collectBacklogItems(repoRoot);
290
+ const terminalInItems = backlogItems
291
+ .filter((item) => item.directory === 'items' &&
292
+ item.status !== null &&
293
+ isTerminalBacklogStatus(item.status))
294
+ .map((item) => item.relativePath);
295
+ checks.push({
296
+ name: 'pjm:backlog_terminal_in_items',
297
+ description: 'Terminal-status backlog items awaiting archive',
298
+ status: checkStatus(terminalInItems),
299
+ message: terminalInItems.length === 0
300
+ ? 'No terminal-status backlog items remain under items/.'
301
+ : `Terminal-status backlog items still under items/: ${terminalInItems.join(', ')}`,
302
+ fix: terminalInItems.length === 0
303
+ ? undefined
304
+ : 'Run `oat backlog archive <id>` to move each closed/wont_do item into archived/.',
305
+ });
306
+ // A missing/empty `status:` is as much a drift as an out-of-enum value: the
307
+ // `oat backlog archive` command rejects both, so `pjm doctor` must surface
308
+ // both rather than leaving status-less items silently invisible.
309
+ const missingStatusItems = backlogItems
310
+ .filter((item) => item.status === null || item.status.trim() === '')
311
+ .map((item) => item.relativePath);
312
+ const outOfEnumStatusItems = backlogItems
313
+ .filter((item) => item.status !== null &&
314
+ item.status.trim() !== '' &&
315
+ !isValidBacklogStatus(item.status))
316
+ .map((item) => item.relativePath);
317
+ const invalidStatus = [...outOfEnumStatusItems, ...missingStatusItems];
318
+ const invalidStatusDetails = [];
319
+ if (outOfEnumStatusItems.length > 0) {
320
+ invalidStatusDetails.push(`out-of-enum status: ${outOfEnumStatusItems.join(', ')}`);
321
+ }
322
+ if (missingStatusItems.length > 0) {
323
+ invalidStatusDetails.push(`missing status: ${missingStatusItems.join(', ')}`);
324
+ }
325
+ checks.push({
326
+ name: 'pjm:backlog_invalid_status',
327
+ description: 'Backlog items with a missing or out-of-enum status',
328
+ status: checkStatus(invalidStatus),
329
+ message: invalidStatus.length === 0
330
+ ? 'All backlog item statuses are within the valid enum.'
331
+ : `Backlog items with an invalid status (${invalidStatusDetails.join('; ')}). Valid statuses: ${BACKLOG_ITEM_STATUSES.join(', ')}.`,
332
+ fix: invalidStatus.length === 0
333
+ ? undefined
334
+ : `Set a valid status (${BACKLOG_ITEM_STATUSES.join(', ')}) on each listed item.`,
335
+ });
336
+ const archivedOpen = backlogItems
337
+ .filter((item) => item.directory === 'archived' &&
338
+ item.status !== null &&
339
+ isValidBacklogStatus(item.status) &&
340
+ !isTerminalBacklogStatus(item.status))
341
+ .map((item) => item.relativePath);
342
+ checks.push({
343
+ name: 'pjm:backlog_archived_open',
344
+ description: 'Archived backlog items with a non-terminal status',
345
+ status: warnStatus(archivedOpen),
346
+ message: archivedOpen.length === 0
347
+ ? 'No archived backlog items carry an open status.'
348
+ : `Archived backlog items still marked open/in_progress: ${archivedOpen.join(', ')}`,
349
+ fix: archivedOpen.length === 0
350
+ ? undefined
351
+ : 'Set a terminal status (closed/wont_do) on each archived item, or move it back under items/.',
352
+ });
353
+ // A single id must never live in both `items/` and `archived/`: the live copy
354
+ // shadows the archived record and `oat backlog archive` refuses to reconcile
355
+ // it automatically (auto-archiving would clobber the archived file). Derive
356
+ // from the same single scan so the filesystem is not re-walked.
357
+ const backlogPathsByIdAndDir = new Map();
358
+ for (const item of backlogItems) {
359
+ const key = item.id.toLowerCase();
360
+ const entry = backlogPathsByIdAndDir.get(key) ?? {};
361
+ entry[item.directory] = item.relativePath;
362
+ backlogPathsByIdAndDir.set(key, entry);
363
+ }
364
+ const duplicateIdPairs = [];
365
+ for (const entry of backlogPathsByIdAndDir.values()) {
366
+ if (entry.items && entry.archived) {
367
+ duplicateIdPairs.push(`${entry.items} + ${entry.archived}`);
368
+ }
369
+ }
370
+ checks.push({
371
+ name: 'pjm:backlog_duplicate_id',
372
+ description: 'Backlog ids present in both items/ and archived/',
373
+ status: checkStatus(duplicateIdPairs),
374
+ message: duplicateIdPairs.length === 0
375
+ ? 'No backlog id is present in both items/ and archived/.'
376
+ : `Backlog ids present in both items/ and archived/: ${duplicateIdPairs.join('; ')}`,
377
+ fix: duplicateIdPairs.length === 0
378
+ ? undefined
379
+ : 'Reconcile each duplicate manually: decide whether the active (items/) or archived copy is authoritative, remove the other, then re-run `oat backlog archive <id>` if the item still needs archiving.',
380
+ });
381
+ const itemPathsById = new Map();
382
+ for (const item of backlogItems) {
383
+ if (item.directory === 'items') {
384
+ itemPathsById.set(item.id.toLowerCase(), item.relativePath);
385
+ }
386
+ }
387
+ const completedContent = await readIfExists(join(repoRoot, BACKLOG_COMPLETED_FILE));
388
+ const completedUnarchived = [];
389
+ const seenUnarchived = new Set();
390
+ if (completedContent) {
391
+ for (const completedId of extractCompletedIds(completedContent)) {
392
+ const relativePath = itemPathsById.get(completedId.toLowerCase());
393
+ if (relativePath && !seenUnarchived.has(relativePath)) {
394
+ seenUnarchived.add(relativePath);
395
+ completedUnarchived.push(relativePath);
396
+ }
397
+ }
398
+ }
399
+ checks.push({
400
+ name: 'pjm:backlog_completed_unarchived',
401
+ description: 'Completed-log entries whose item file is still under items/',
402
+ status: warnStatus(completedUnarchived),
403
+ message: completedUnarchived.length === 0
404
+ ? 'No completed backlog entries reference an item still under items/.'
405
+ : `Completed log references items still under items/: ${completedUnarchived.join(', ')}`,
406
+ fix: completedUnarchived.length === 0
407
+ ? undefined
408
+ : 'Run `oat backlog archive <id>` to archive each completed item still under items/.',
409
+ });
249
410
  return checks;
250
411
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAiBnE,UAAU,sBAAsB;IAC9B,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,uBAAuB,EAAE,OAAO,uBAAuB,CAAC;IACxD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,cAAc,EAAE,OAAO,cAAc,CAAC;IACtC,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;CACvD;AA6FD,wBAAgB,gBAAgB,CAC9B,SAAS,GAAE,OAAO,CAAC,sBAAsB,CAAM,GAC9C,OAAO,CAsJT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAA0B,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAiBnE,UAAU,sBAAsB;IAC9B,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,uBAAuB,EAAE,OAAO,uBAAuB,CAAC;IACxD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,cAAc,EAAE,OAAO,cAAc,CAAC;IACtC,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;CACvD;AA6FD,wBAAgB,gBAAgB,CAC9B,SAAS,GAAE,OAAO,CAAC,sBAAsB,CAAM,GAC9C,OAAO,CAuJT"}
@@ -7,7 +7,7 @@ import { resolveProjectRoot } from '../../fs/paths.js';
7
7
  import { formatDoctorResults } from '../../ui/output.js';
8
8
  import { Command } from 'commander';
9
9
  import { runPjmDoctorChecks } from './doctor.js';
10
- import { initializeRepoReference } from './init.js';
10
+ import { initializeRepoReference, INSTRUCTIONS_SYNC_HINT } from './init.js';
11
11
  import { migratePjmRepo, readPjmMigrationPrompt } from './migrate.js';
12
12
  const DEFAULT_DEPENDENCIES = {
13
13
  buildCommandContext,
@@ -111,6 +111,7 @@ export function createPjmCommand(overrides = {}) {
111
111
  if (result.skipped.length > 0) {
112
112
  context.logger.info(`Skipped existing: ${result.skipped.join(', ')}`);
113
113
  }
114
+ context.logger.info(INSTRUCTIONS_SYNC_HINT);
114
115
  }
115
116
  process.exitCode = 0;
116
117
  }
@@ -8,6 +8,7 @@ export interface RepoReferenceInitResult {
8
8
  created: string[];
9
9
  skipped: string[];
10
10
  }
11
- export declare const CANONICAL_REPO_REFERENCE_PATHS: readonly [...("AGENTS.md" | "pjm/AGENTS.md" | "pjm/current-state.md" | "pjm/roadmap.md" | "reference/AGENTS.md")[], "pjm/backlog/index.md", "pjm/backlog/completed.md", "pjm/backlog/items/.gitkeep", "pjm/backlog/archived/.gitkeep", "reference/decisions/index.md"];
11
+ export declare const INSTRUCTIONS_SYNC_HINT: string;
12
+ export declare const CANONICAL_REPO_REFERENCE_PATHS: readonly [...("AGENTS.md" | "pjm/AGENTS.md" | "pjm/current-state.md" | "pjm/roadmap.md" | "reference/AGENTS.md" | "README.md" | "pjm/handoffs/README.md")[], "pjm/backlog/index.md", "pjm/backlog/completed.md", "pjm/backlog/items/.gitkeep", "pjm/backlog/archived/.gitkeep", "reference/decisions/index.md"];
12
13
  export declare function initializeRepoReference(options: InitializeRepoReferenceOptions): Promise<RepoReferenceInitResult>;
13
14
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/init.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAwBD,eAAO,MAAM,8BAA8B,wQAIjC,CAAC;AAyEX,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,CAgElC"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../src/commands/pjm/init.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAoBD,eAAO,MAAM,sBAAsB,QAEiD,CAAC;AAWrF,eAAO,MAAM,8BAA8B,iTAIjC,CAAC;AAyEX,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,uBAAuB,CAAC,CAgElC"}
@@ -9,7 +9,14 @@ const TEMPLATE_TARGETS = [
9
9
  { template: 'current-state.md', target: 'pjm/current-state.md' },
10
10
  { template: 'roadmap.md', target: 'pjm/roadmap.md' },
11
11
  { template: 'reference-agents.md', target: 'reference/AGENTS.md' },
12
+ { template: 'repo-readme.md', target: 'README.md' },
13
+ { template: 'pjm-handoffs-readme.md', target: 'pjm/handoffs/README.md' },
12
14
  ];
15
+ // Next-step hint printed after init/backfill. `oat pjm init` never writes
16
+ // CLAUDE.md shims itself — strategy ownership stays with `oat instructions
17
+ // sync`, so we point the operator at it (with the `--dry-run` preview).
18
+ export const INSTRUCTIONS_SYNC_HINT = 'Next step: run `oat instructions sync` to create CLAUDE.md shims for the ' +
19
+ 'repo-reference AGENTS.md files (preview with `oat instructions sync --dry-run`).';
13
20
  const BACKLOG_PATHS = [
14
21
  'pjm/backlog/index.md',
15
22
  'pjm/backlog/completed.md',
@@ -1 +1 @@
1
- {"version":3,"file":"latest.d.ts","sourceRoot":"","sources":["../../../src/commands/review/latest.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;CACrB;AAQD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,UAAU,wBAAwB;IAChC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACnE;AAmID,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAqF9B;AA+CD,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,OAAO,CAAC,wBAAwB,CAAM,GAChD,OAAO,CAkBT"}
1
+ {"version":3,"file":"latest.d.ts","sourceRoot":"","sources":["../../../src/commands/review/latest.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;CACrB;AAQD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,UAAU,wBAAwB;IAChC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACnE;AAsID,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAqF9B;AA+CD,wBAAgB,yBAAyB,CACvC,SAAS,GAAE,OAAO,CAAC,wBAAwB,CAAM,GAChD,OAAO,CAkBT"}
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { isAbsolute, join, relative } from 'node:path';
3
3
  import { buildCommandContext, } from '../../app/command-context.js';
4
- import { getFrontmatterBlock, getFrontmatterField, } from '../shared/frontmatter.js';
4
+ import { getFrontmatterBlock, getFrontmatterField, parseGeneratedTime, } from '../shared/frontmatter.js';
5
5
  import { readGlobalOptions } from '../shared/shared.utils.js';
6
6
  import { readOatLocalConfig } from '../../config/oat-config.js';
7
7
  import { normalizeToPosixPath, resolveProjectRoot } from '../../fs/paths.js';
@@ -72,7 +72,7 @@ async function readReviewCandidate(repoRoot, relativePath, kind, priority, archi
72
72
  if (!generatedAt) {
73
73
  return null;
74
74
  }
75
- const generatedTime = Date.parse(generatedAt);
75
+ const generatedTime = parseGeneratedTime(generatedAt);
76
76
  if (Number.isNaN(generatedTime)) {
77
77
  return null;
78
78
  }
@@ -99,6 +99,9 @@ function sortReviewCandidates(a, b) {
99
99
  if (a.lifecycleRank !== b.lifecycleRank) {
100
100
  return b.lifecycleRank - a.lifecycleRank;
101
101
  }
102
+ // Same scope, same generated time: fall back to a stable path order. Review
103
+ // artifacts carry a seconds-precision `oat_generated_at`, so distinct re-gate
104
+ // rounds differ on generatedTime above and never reach this branch.
102
105
  return a.path.localeCompare(b.path);
103
106
  }
104
107
  export async function findLatestReview(options) {
@@ -9,6 +9,17 @@ export declare function isProjectStatePhase(value: string): value is ProjectStat
9
9
  export declare function isProjectStateFrontmatterField(value: string): value is ProjectStateFrontmatterField;
10
10
  export declare function getFrontmatterBlock(content: string): string | null;
11
11
  export declare function getFrontmatterField(frontmatter: string, field: string): string | null;
12
+ /**
13
+ * Parse an `oat_generated_at` value into a comparable epoch millisecond time.
14
+ *
15
+ * Review artifacts should carry a UTC, `Z`-suffixed timestamp
16
+ * (`YYYY-MM-DDTHH:MM:SSZ`). A bare date (`YYYY-MM-DD`) already parses as UTC,
17
+ * but a datetime with no timezone designator parses as *local* time, which
18
+ * mis-orders artifacts written by agents in different timezones. Treat such a
19
+ * value as UTC by appending `Z` so ordering is timezone-independent regardless
20
+ * of what the writer emitted. Returns `NaN` for unparseable input.
21
+ */
22
+ export declare function parseGeneratedTime(value: string): number;
12
23
  export declare function parseFrontmatterField(filePath: string, field: string): Promise<string>;
13
24
  export declare function getSkillVersion(skillDir: string): Promise<string | null>;
14
25
  export declare function getAgentVersion(agentPath: string): Promise<string | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/frontmatter.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,6CAA8C,CAAC;AAE/E,eAAO,MAAM,oBAAoB,gFAOvB,CAAC;AAEX,eAAO,MAAM,gCAAgC,sgBA2BnC,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACpE,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AACtE,MAAM,MAAM,4BAA4B,GACtC,CAAC,OAAO,gCAAgC,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAE3E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,4BAA4B,CAIvC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAKf;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQxB;AAED,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB"}
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../src/commands/shared/frontmatter.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,6CAA8C,CAAC;AAE/E,eAAO,MAAM,oBAAoB,gFAOvB,CAAC;AAEX,eAAO,MAAM,gCAAgC,sgBA2BnC,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACpE,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AACtE,MAAM,MAAM,4BAA4B,GACtC,CAAC,OAAO,gCAAgC,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,gBAAgB,CAE3E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,4BAA4B,CAIvC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAKf;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQxB;AAED,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB"}
@@ -57,6 +57,21 @@ export function getFrontmatterField(frontmatter, field) {
57
57
  return null;
58
58
  return match[1].replace(/\s*#.*$/, '').trim();
59
59
  }
60
+ /**
61
+ * Parse an `oat_generated_at` value into a comparable epoch millisecond time.
62
+ *
63
+ * Review artifacts should carry a UTC, `Z`-suffixed timestamp
64
+ * (`YYYY-MM-DDTHH:MM:SSZ`). A bare date (`YYYY-MM-DD`) already parses as UTC,
65
+ * but a datetime with no timezone designator parses as *local* time, which
66
+ * mis-orders artifacts written by agents in different timezones. Treat such a
67
+ * value as UTC by appending `Z` so ordering is timezone-independent regardless
68
+ * of what the writer emitted. Returns `NaN` for unparseable input.
69
+ */
70
+ export function parseGeneratedTime(value) {
71
+ const hasTime = value.includes('T');
72
+ const hasZone = /(?:[zZ]|[+-]\d{2}:?\d{2})$/.test(value);
73
+ return Date.parse(hasTime && !hasZone ? `${value}Z` : value);
74
+ }
60
75
  export async function parseFrontmatterField(filePath, field) {
61
76
  try {
62
77
  const content = await readFile(filePath, 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.0.0",
35
35
  "yaml": "2.8.2",
36
36
  "zod": "^3.25.76",
37
- "@open-agent-toolkit/control-plane": "0.1.40"
37
+ "@open-agent-toolkit/control-plane": "0.1.42"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",