@open-agent-toolkit/cli 0.2.66 → 0.2.68

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 (118) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +14 -1
  3. package/assets/docs/cli-utilities/project-log.md +63 -4
  4. package/assets/docs/contributing/code.md +8 -4
  5. package/assets/docs/contributing/skills.md +29 -17
  6. package/assets/docs/docs-tooling/commands.md +3 -1
  7. package/assets/docs/provider-sync/manifest-and-drift.md +2 -18
  8. package/assets/docs/provider-sync/scope-and-surface.md +3 -2
  9. package/assets/docs/reference/cli-reference.md +1 -1
  10. package/assets/docs/workflows/projects/evidence-layers.md +6 -3
  11. package/assets/docs/workflows/projects/implementation-execution.md +4 -3
  12. package/assets/docs/workflows/projects/lifecycle.md +17 -0
  13. package/assets/docs/workflows/projects/orchestration-model.md +6 -4
  14. package/assets/docs/workflows/projects/picking-up-projects.md +18 -0
  15. package/assets/docs/workflows/skills/repo-improve.md +7 -3
  16. package/assets/public-package-versions.json +4 -4
  17. package/assets/skills/create-agnostic-skill/SKILL.md +19 -37
  18. package/assets/skills/create-agnostic-skill/references/docs/skills-guide.md +58 -15
  19. package/assets/skills/create-agnostic-skill/references/skill-template.md +17 -15
  20. package/assets/skills/oat-agent-instructions-apply/SKILL.md +1 -1
  21. package/assets/skills/oat-agent-instructions-apply/references/instruction-file-templates/glob-scoped-rule.md +1 -4
  22. package/assets/skills/oat-dispatch-subagents/SKILL.md +4 -2
  23. package/assets/skills/oat-doctor/SKILL.md +1 -4
  24. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +3 -2
  25. package/assets/skills/oat-project-complete/SKILL.md +158 -30
  26. package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +26 -7
  27. package/assets/skills/oat-project-complete/scripts/validate-durable-archive-receipt.mjs +183 -0
  28. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +9 -7
  29. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +3 -2
  30. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  31. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +11 -10
  32. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +3 -2
  33. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +3 -2
  34. package/assets/skills/oat-project-plan-writing/SKILL.md +7 -6
  35. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +3 -2
  36. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +3 -2
  37. package/assets/skills/oat-project-retro/SKILL.md +1 -1
  38. package/assets/skills/oat-project-retro/references/apply-procedure.md +11 -0
  39. package/assets/skills/oat-project-review-provide/SKILL.md +14 -13
  40. package/assets/skills/oat-project-review-provide-remote/SKILL.md +7 -6
  41. package/assets/skills/oat-project-summary/SKILL.md +24 -2
  42. package/assets/skills/oat-repo-improve/SKILL.md +9 -2
  43. package/assets/skills/oat-repo-improve/references/plan-template.md +4 -6
  44. package/assets/skills/oat-repo-knowledge-index/SKILL.md +11 -9
  45. package/assets/skills/oat-wave-execute/SKILL.md +20 -6
  46. package/assets/skills/oat-wave-program/SKILL.md +9 -4
  47. package/dist/commands/config/index.d.ts +2 -1
  48. package/dist/commands/config/index.d.ts.map +1 -1
  49. package/dist/commands/config/index.js +133 -38
  50. package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
  51. package/dist/commands/docs/index-generate/index.js +68 -12
  52. package/dist/commands/docs/init/index.d.ts +11 -0
  53. package/dist/commands/docs/init/index.d.ts.map +1 -1
  54. package/dist/commands/docs/init/index.js +19 -1
  55. package/dist/commands/gate/index.d.ts.map +1 -1
  56. package/dist/commands/gate/index.js +10 -2
  57. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  58. package/dist/commands/instructions/instructions.utils.js +42 -9
  59. package/dist/commands/internal/validate-skill-version-bumps.d.ts.map +1 -1
  60. package/dist/commands/internal/validate-skill-version-bumps.js +5 -5
  61. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  62. package/dist/commands/project/dispatch-ceiling/index.js +32 -18
  63. package/dist/commands/project/log/append.d.ts +48 -0
  64. package/dist/commands/project/log/append.d.ts.map +1 -1
  65. package/dist/commands/project/log/append.js +207 -30
  66. package/dist/commands/project/log/check.d.ts +87 -1
  67. package/dist/commands/project/log/check.d.ts.map +1 -1
  68. package/dist/commands/project/log/check.js +139 -5
  69. package/dist/commands/project/log/grammar.d.ts +165 -0
  70. package/dist/commands/project/log/grammar.d.ts.map +1 -1
  71. package/dist/commands/project/log/grammar.js +219 -1
  72. package/dist/commands/project/log/rollup.d.ts +16 -1
  73. package/dist/commands/project/log/rollup.d.ts.map +1 -1
  74. package/dist/commands/project/log/rollup.js +22 -2
  75. package/dist/commands/project/log/synthesize.d.ts +16 -0
  76. package/dist/commands/project/log/synthesize.d.ts.map +1 -1
  77. package/dist/commands/project/log/synthesize.js +85 -9
  78. package/dist/commands/status/index.d.ts.map +1 -1
  79. package/dist/commands/status/index.js +24 -2
  80. package/dist/commands/sync/apply.d.ts.map +1 -1
  81. package/dist/commands/sync/apply.js +6 -4
  82. package/dist/config/dispatch-matrix.d.ts.map +1 -1
  83. package/dist/config/dispatch-matrix.js +13 -5
  84. package/dist/config/oat-config.d.ts +15 -0
  85. package/dist/config/oat-config.d.ts.map +1 -1
  86. package/dist/config/oat-config.js +61 -7
  87. package/dist/config/own-keys.d.ts +22 -0
  88. package/dist/config/own-keys.d.ts.map +1 -0
  89. package/dist/config/own-keys.js +32 -0
  90. package/dist/config/resolve.d.ts +11 -0
  91. package/dist/config/resolve.d.ts.map +1 -1
  92. package/dist/config/resolve.js +35 -11
  93. package/dist/config/sync-config.d.ts.map +1 -1
  94. package/dist/config/sync-config.js +16 -0
  95. package/dist/drift/detector.d.ts.map +1 -1
  96. package/dist/drift/detector.js +52 -1
  97. package/dist/drift/skill-view-diagnostic.d.ts.map +1 -1
  98. package/dist/drift/skill-view-diagnostic.js +2 -23
  99. package/dist/engine/compute-plan.d.ts.map +1 -1
  100. package/dist/engine/compute-plan.js +22 -72
  101. package/dist/engine/managed-copy-hash.d.ts +69 -0
  102. package/dist/engine/managed-copy-hash.d.ts.map +1 -0
  103. package/dist/engine/managed-copy-hash.js +175 -0
  104. package/dist/fs/assets.d.ts +11 -0
  105. package/dist/fs/assets.d.ts.map +1 -1
  106. package/dist/fs/assets.js +1 -1
  107. package/dist/manifest/hash.d.ts +110 -0
  108. package/dist/manifest/hash.d.ts.map +1 -1
  109. package/dist/manifest/hash.js +111 -13
  110. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  111. package/dist/providers/ceiling/registry.js +6 -1
  112. package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
  113. package/dist/providers/identity/dispatch-report.js +4 -1
  114. package/dist/release/public-package-contract.d.ts.map +1 -1
  115. package/dist/release/public-package-contract.js +21 -0
  116. package/dist/validation/skills.d.ts.map +1 -1
  117. package/dist/validation/skills.js +305 -40
  118. package/package.json +2 -2
@@ -491,16 +491,81 @@ function unusableVersionFinding(file) {
491
491
  message: 'Frontmatter declares a version that cannot be read; use a quoted semver string (unquoted, version: 1.10 is the number 1.1)',
492
492
  };
493
493
  }
494
+ const MISSING_FRONTMATTER_BLOCK_MESSAGE = 'Missing frontmatter block (--- ... ---)';
495
+ /**
496
+ * The file carries no `--- … ---` block at all, so there is no position a
497
+ * version could be declared in.
498
+ *
499
+ * Reported rather than skipped: a changed file with no frontmatter used to
500
+ * fall through the bump gate's `continue`, which meant a canonical skill or
501
+ * agent role could drop its frontmatter and pass the version contract by
502
+ * omission. The structural pass reports the same state for every skill
503
+ * directory, so `pushUniqueFinding` keeps it to one finding per file.
504
+ */
505
+ function missingFrontmatterFinding(file) {
506
+ return {
507
+ file,
508
+ code: 'skill-frontmatter-missing',
509
+ severity: 'error',
510
+ message: `${MISSING_FRONTMATTER_BLOCK_MESSAGE}; a changed canonical skill or agent role must declare a version`,
511
+ };
512
+ }
513
+ /**
514
+ * Frontmatter is present and readable but declares no version anywhere — the
515
+ * `metadata:` map with no `version` child is the shape that made this state
516
+ * common once bundled skills moved off the top-level alias.
517
+ *
518
+ * Distinct from `skill-version-unusable`: there the author wrote a version
519
+ * OAT cannot read, here no version was written at all. Both disable version
520
+ * enforcement, so both are errors.
521
+ */
522
+ /**
523
+ * True when some pass already reported that this file has no frontmatter
524
+ * block.
525
+ *
526
+ * The `oat-*` structural loop reports the state with its own long-standing
527
+ * message and no `code`, so `pushUniqueFinding` cannot dedupe it against the
528
+ * coded finding. `validateOatSkills` runs the structural loop, the
529
+ * version-source pass, and (when a `baseRef` is given) the bump collector into
530
+ * one findings array, and all three can see the same file.
531
+ */
532
+ function hasMissingFrontmatterReport(findings, file) {
533
+ return findings.some((finding) => finding.file === file &&
534
+ (finding.code === 'skill-frontmatter-missing' ||
535
+ finding.message === MISSING_FRONTMATTER_BLOCK_MESSAGE));
536
+ }
537
+ function missingVersionFinding(file) {
538
+ return {
539
+ file,
540
+ code: 'skill-version-missing',
541
+ severity: 'error',
542
+ message: 'Frontmatter declares no resolvable version; declare metadata.version (a canonical agent role under .agents/agents may declare a top-level version: instead)',
543
+ };
544
+ }
545
+ const UNCOMPARABLE_BASE_CODES = {
546
+ unreadable: 'skill-frontmatter-unreadable',
547
+ unusable: 'skill-version-unusable',
548
+ 'missing-frontmatter': 'skill-frontmatter-missing',
549
+ 'missing-version': 'skill-version-missing',
550
+ };
551
+ const UNCOMPARABLE_BASE_REASONS = {
552
+ unreadable: 'the base frontmatter is not a valid YAML mapping with unique keys',
553
+ unusable: 'the base frontmatter declares a version that cannot be read',
554
+ 'missing-frontmatter': 'the base file has no frontmatter block (--- ... ---)',
555
+ 'missing-version': 'the base frontmatter declares no resolvable version',
556
+ };
557
+ /**
558
+ * The base side is deliberately asymmetric with the current side: a base that
559
+ * cannot be read is a comparison problem, not a defect in the file the author
560
+ * just changed, so the message says the base cannot be compared rather than
561
+ * accusing the current file.
562
+ */
494
563
  function uncomparableBaseFinding(file, baseRef, reason) {
495
564
  return {
496
565
  file,
497
- code: reason === 'unreadable'
498
- ? 'skill-frontmatter-unreadable'
499
- : 'skill-version-unusable',
566
+ code: UNCOMPARABLE_BASE_CODES[reason],
500
567
  severity: 'error',
501
- message: reason === 'unreadable'
502
- ? `Changed canonical skill cannot be version-checked against ${baseRef}: the base frontmatter is not a valid YAML mapping with unique keys`
503
- : `Changed canonical skill cannot be version-checked against ${baseRef}: the base frontmatter declares a version that cannot be read`,
568
+ message: `Changed canonical skill cannot be version-checked against ${baseRef}: ${UNCOMPARABLE_BASE_REASONS[reason]}`,
504
569
  };
505
570
  }
506
571
  function uncomparableBaseConflictFinding(file, baseRef, conflict) {
@@ -618,23 +683,130 @@ async function collectGateabilityFindings(skillsRoot, gateSkillNames, findings)
618
683
  }
619
684
  }
620
685
  }
621
- async function listChangedSkillFiles(repoRoot, baseRef, dependencies) {
686
+ const SKILLS_PATH_PREFIX = '.agents/skills/';
687
+ const AGENTS_PATH_PREFIX = '.agents/agents/';
688
+ /**
689
+ * Map a diffed path to the versioned files that must bump for it, or an empty
690
+ * list when nothing owns it.
691
+ *
692
+ * Everything a skill directory ships is owned by that skill's `SKILL.md`,
693
+ * because `bundle-assets.sh` copies the directory with `cp -RL` and then
694
+ * removes `tests`. `tests` is therefore the one excluded entry: it does not
695
+ * reach consumers, and demanding a bump for it would train maintainers to bump
696
+ * reflexively. The exclusion matches a plain `tests` path as well as the
697
+ * `tests/` subtree because `rm -rf .../tests` removes either one.
698
+ *
699
+ * A path can map to two files. The previous pathspec was a `*` wildcard
700
+ * between `.agents/skills/` and `SKILL.md`, and a Git pathspec `*` crosses
701
+ * `/`, so it also matched a *nested* `SKILL.md` such as
702
+ * `.agents/skills/foo/references/example/SKILL.md` and validated that file's
703
+ * own version. Mapping such a path only to the outer skill would drop a
704
+ * finding this gate already emitted, which the weaker-anywhere rule forbids.
705
+ * A nested `SKILL.md` therefore maps to itself (preserving the old half
706
+ * exactly) *and* to its owning skill (adding the new coverage).
707
+ */
708
+ async function resolveOwningVersionedFiles(repoRoot, changedPath) {
709
+ if (changedPath.startsWith(SKILLS_PATH_PREFIX)) {
710
+ const rest = changedPath.slice(SKILLS_PATH_PREFIX.length);
711
+ const separator = rest.indexOf('/');
712
+ if (separator <= 0) {
713
+ // A file directly under `.agents/skills` belongs to no skill.
714
+ return [];
715
+ }
716
+ const withinSkill = rest.slice(separator + 1);
717
+ if (withinSkill === 'tests' || withinSkill.startsWith('tests/')) {
718
+ // Deliberate, including for a `SKILL.md` nested under `tests/`, which
719
+ // the old wildcard pathspec also matched. Excluding it narrows that
720
+ // pathspec by exactly one shape, and that is the intended trade: a
721
+ // fixture `SKILL.md` under `tests/` is test data, it never reaches a
722
+ // consumer, and demanding a version bump for it is the reflexive
723
+ // bumping the `tests/` boundary exists to prevent. No such file exists
724
+ // in the bundled tree; every `SKILL.md` sits at the skill root.
725
+ return [];
726
+ }
727
+ const candidates = [
728
+ `${SKILLS_PATH_PREFIX}${rest.slice(0, separator)}/SKILL.md`,
729
+ ];
730
+ if (changedPath.endsWith('/SKILL.md') &&
731
+ !candidates.includes(changedPath)) {
732
+ candidates.push(changedPath);
733
+ }
734
+ const owners = [];
735
+ for (const candidate of candidates) {
736
+ // A candidate that is not on disk was removed or renamed away, so there
737
+ // is nothing left to bump and nothing to read.
738
+ if (await isFile(join(repoRoot, candidate))) {
739
+ owners.push(candidate);
740
+ }
741
+ }
742
+ return owners;
743
+ }
744
+ if (changedPath.startsWith(AGENTS_PATH_PREFIX)) {
745
+ const rest = changedPath.slice(AGENTS_PATH_PREFIX.length);
746
+ // Git pathspec `*` crosses `/`, so the nested-directory bound the pathspec
747
+ // implies is enforced here rather than assumed of the pathspec.
748
+ if (rest.length === 0 || rest.includes('/') || !rest.endsWith('.md')) {
749
+ return [];
750
+ }
751
+ return [changedPath];
752
+ }
753
+ return [];
754
+ }
755
+ /**
756
+ * List the canonical files whose version must move, given everything that
757
+ * changed under `.agents/skills` and `.agents/agents`.
758
+ *
759
+ * The pathspec is deliberately wider than the files that carry a version: a
760
+ * skill's `scripts/` and `references/` ship to every `oat tools install`
761
+ * consumer, so a change to one is a change to that skill even when `SKILL.md`
762
+ * is untouched. Agent roles are diffed for the first time here; per
763
+ * `DR-260908-bundled-skills-declare` they still declare a top-level `version:`
764
+ * and are deliberately not migrated, so the gate accepts either declaration
765
+ * shape rather than demanding `metadata.version` of them.
766
+ */
767
+ async function listChangedVersionedFiles(repoRoot, baseRef, dependencies) {
622
768
  const execFile = dependencies.gitExecFile ?? execFileAsync;
623
769
  const { stdout } = await execFile('git', [
624
770
  'diff',
625
771
  '--name-only',
772
+ // NUL-delimited: without `-z`, Git C-quotes any path containing a tab,
773
+ // quote, backslash, or (under the default `core.quotePath`) a non-ASCII
774
+ // byte. The old pathspec only ever produced plain `SKILL.md` paths, but
775
+ // this one lists every shipped file, so a quoted name would no longer
776
+ // match the prefixes below and would silently escape the gate.
777
+ '-z',
626
778
  '--diff-filter=ACMR',
627
779
  `${baseRef}...HEAD`,
628
780
  '--',
629
- '.agents/skills/*/SKILL.md',
781
+ '.agents/skills',
782
+ '.agents/agents/*.md',
630
783
  ], {
631
784
  cwd: repoRoot,
632
785
  env: dependencies.env ?? process.env,
633
786
  });
634
- return stdout
635
- .split('\n')
636
- .map((line) => line.trim())
637
- .filter((line) => line.length > 0);
787
+ // No trimming: with `-z` each entry is the exact path, and a Git path may
788
+ // legitimately begin or end with a space. The trailing empty entry after the
789
+ // final NUL is what the length filter removes.
790
+ const changedPaths = stdout.split('\0').filter((entry) => entry.length > 0);
791
+ const grouped = new Map();
792
+ for (const changedPath of changedPaths) {
793
+ const owningFiles = await resolveOwningVersionedFiles(repoRoot, changedPath);
794
+ for (const owningFile of owningFiles) {
795
+ const owned = grouped.get(owningFile);
796
+ if (owned === undefined) {
797
+ grouped.set(owningFile, [changedPath]);
798
+ }
799
+ else if (!owned.includes(changedPath)) {
800
+ owned.push(changedPath);
801
+ }
802
+ }
803
+ }
804
+ // Plain code-unit ordering, not `localeCompare`: findings order is part of
805
+ // this command's observable output, and a locale-aware comparison would make
806
+ // it depend on the ICU data of whatever machine ran the gate.
807
+ return [...grouped.entries()]
808
+ .map(([file, owned]) => ({ file, changedPaths: [...owned].sort() }))
809
+ .sort((left, right) => left.file < right.file ? -1 : left.file > right.file ? 1 : 0);
638
810
  }
639
811
  async function readFileAtGitRef(repoRoot, ref, filePath, dependencies) {
640
812
  const execFile = dependencies.gitExecFile ?? execFileAsync;
@@ -649,22 +821,64 @@ async function readFileAtGitRef(repoRoot, ref, filePath, dependencies) {
649
821
  return null;
650
822
  }
651
823
  }
652
- async function collectChangedSkillVersionBumpFindings(repoRoot, baseRef, changedSkillFiles, findings, dependencies) {
653
- for (const relativeSkillPath of changedSkillFiles) {
824
+ /**
825
+ * Name the sibling paths that changed, relative to the owning file's
826
+ * directory, when the owning file itself is not one of them.
827
+ *
828
+ * Without this a scripts-only failure would point at a `SKILL.md` whose bytes
829
+ * the author never touched and say nothing about why it is being asked to
830
+ * bump.
831
+ */
832
+ function changedSiblingSuffix(entry) {
833
+ if (entry.changedPaths.includes(entry.file)) {
834
+ return '';
835
+ }
836
+ const directory = entry.file.slice(0, entry.file.lastIndexOf('/') + 1);
837
+ const relativePaths = entry.changedPaths.map((path) => path.startsWith(directory) ? path.slice(directory.length) : path);
838
+ return ` [changed: ${relativePaths.join(', ')}]`;
839
+ }
840
+ const VERSION_DECLARATION_HINT = 'declare metadata.version (a canonical agent role under .agents/agents may declare a top-level version: instead)';
841
+ async function collectChangedSkillVersionBumpFindings(repoRoot, baseRef, changedVersionedFiles, findings, dependencies) {
842
+ for (const entry of changedVersionedFiles) {
843
+ const relativeSkillPath = entry.file;
654
844
  const skillPath = join(repoRoot, relativeSkillPath);
655
845
  const currentContent = await readFile(skillPath, 'utf8');
656
846
  const baseContent = await readFileAtGitRef(repoRoot, baseRef, relativeSkillPath, dependencies);
657
- if (baseContent === null || baseContent === currentContent) {
847
+ if (baseContent === null) {
848
+ continue;
849
+ }
850
+ // Identical bytes are skipped only when the owning file is the *whole* of
851
+ // what changed — a rename or mode change, which the original skip was
852
+ // written for. When a sibling under the skill directory changed, identical
853
+ // `SKILL.md` bytes are exactly the failure to report: the bundled skill
854
+ // shipped new content under an unchanged version.
855
+ if (baseContent === currentContent &&
856
+ entry.changedPaths.length === 1 &&
857
+ entry.changedPaths[0] === relativeSkillPath) {
658
858
  continue;
659
859
  }
860
+ // Falsy, not `=== null`: `getFrontmatterBlock` returns `""` for an empty
861
+ // `---` / `---` pair and `null` only when there is no pair at all. Both
862
+ // declare nothing, and the pre-existing `oat-*` structural loop already
863
+ // treats them alike, so matching it here keeps one file to one diagnosis.
660
864
  const currentBlock = getFrontmatterBlock(currentContent);
661
- const parsedCurrent = currentBlock === null ? null : parseSkillFrontmatter(currentBlock);
865
+ const parsedCurrent = currentBlock
866
+ ? parseSkillFrontmatter(currentBlock)
867
+ : null;
662
868
  const baseBlock = getFrontmatterBlock(baseContent);
663
- const parsedBase = baseBlock === null ? null : parseSkillFrontmatter(baseBlock);
664
- // Frontmatter that carries no readable version must never fall through to
665
- // the `continue` below: a value the resolver cannot read would otherwise
666
- // disable bump enforcement for that skill entirely. Both sides are
667
- // guarded, because either one being uncomparable defeats the comparison.
869
+ const parsedBase = baseBlock ? parseSkillFrontmatter(baseBlock) : null;
870
+ // Every state that leaves this collector without a version comparison now
871
+ // reports. The closed set is: malformed frontmatter, an unusable version
872
+ // declaration, a version conflict, no frontmatter block at all, and
873
+ // frontmatter that declares no version anywhere — on either side. Nothing
874
+ // reaches a silent `continue`, because "no version found, so skip" is the
875
+ // state that let a changed file pass the version contract by omission.
876
+ //
877
+ // The guards below run in a fixed order and the four uncomparable-state
878
+ // guards and the two conflict guards come first, so their specific
879
+ // diagnoses are never masked by the generic missing-block or
880
+ // missing-version message added at the tail. Both sides are guarded,
881
+ // because either one being uncomparable defeats the comparison.
668
882
  // `validateOatSkills` runs the structural version-source pass before this
669
883
  // collector, so the same file can already carry the current-side finding.
670
884
  if (parsedCurrent?.malformed) {
@@ -686,10 +900,11 @@ async function collectChangedSkillVersionBumpFindings(repoRoot, baseRef, changed
686
900
  const resolvedCurrent = parsedCurrent === null ? null : resolveSkillVersion(parsedCurrent);
687
901
  const resolvedBase = parsedBase === null ? null : resolveSkillVersion(parsedBase);
688
902
  // A conflicting skill has no resolvable version, so it cannot be bump
689
- // checked. This is the only version-source finding the bump validator
690
- // emits: `validate-skill-version-bumps.ts` fails the gate on any finding
691
- // regardless of severity, so the top-level alias deprecation warning stays
692
- // in structural validation and never reaches this result.
903
+ // checked. `validate-skill-version-bumps.ts` fails the gate on any finding
904
+ // regardless of severity, so the one version-source finding deliberately
905
+ // kept out of this collector is the top-level alias — an error since step
906
+ // 1 of the retirement schedule — which stays in structural validation and
907
+ // never reaches this result.
693
908
  if (resolvedCurrent?.conflict) {
694
909
  findings.push(versionConflictFinding(skillPath, resolvedCurrent.conflict));
695
910
  continue;
@@ -703,13 +918,32 @@ async function collectChangedSkillVersionBumpFindings(repoRoot, baseRef, changed
703
918
  }
704
919
  const currentVersion = resolvedCurrent?.version ?? null;
705
920
  const baseVersion = resolvedBase?.version ?? null;
706
- if (!currentVersion || !baseVersion) {
921
+ // The current-state defects come before the base-side comparison problems,
922
+ // matching the order the guards above already use, and both come after
923
+ // every specific diagnosis so neither can mask one.
924
+ if (!currentBlock) {
925
+ if (!hasMissingFrontmatterReport(findings, skillPath)) {
926
+ pushUniqueFinding(findings, missingFrontmatterFinding(skillPath));
927
+ }
928
+ continue;
929
+ }
930
+ if (!currentVersion) {
931
+ pushUniqueFinding(findings, missingVersionFinding(skillPath));
707
932
  continue;
708
933
  }
934
+ if (!baseBlock) {
935
+ findings.push(uncomparableBaseFinding(skillPath, baseRef, 'missing-frontmatter'));
936
+ continue;
937
+ }
938
+ if (!baseVersion) {
939
+ findings.push(uncomparableBaseFinding(skillPath, baseRef, 'missing-version'));
940
+ continue;
941
+ }
942
+ const changedSuffix = changedSiblingSuffix(entry);
709
943
  if (currentVersion === baseVersion) {
710
944
  findings.push({
711
945
  file: skillPath,
712
- message: `Changed canonical skill must bump frontmatter version relative to ${baseRef} (still ${currentVersion})`,
946
+ message: `Changed canonical skill or agent role must bump its version relative to ${baseRef} (still ${currentVersion})${changedSuffix}; ${VERSION_DECLARATION_HINT}`,
713
947
  });
714
948
  continue;
715
949
  }
@@ -718,17 +952,19 @@ async function collectChangedSkillVersionBumpFindings(repoRoot, baseRef, changed
718
952
  compareSemver(currentVersion, baseVersion) <= 0) {
719
953
  findings.push({
720
954
  file: skillPath,
721
- message: `Changed canonical skill version must increase relative to ${baseRef} (base ${baseVersion}, current ${currentVersion})`,
955
+ message: `Changed canonical skill or agent role version must increase relative to ${baseRef} (base ${baseVersion}, current ${currentVersion})${changedSuffix}; ${VERSION_DECLARATION_HINT}`,
722
956
  });
723
957
  }
724
958
  }
725
959
  }
726
960
  export async function validateChangedSkillVersionBumps(repoRoot, options, dependencies = {}) {
727
961
  const findings = [];
728
- const changedSkillFiles = await listChangedSkillFiles(repoRoot, options.baseRef, dependencies);
729
- await collectChangedSkillVersionBumpFindings(repoRoot, options.baseRef, changedSkillFiles, findings, dependencies);
962
+ const changedVersionedFiles = await listChangedVersionedFiles(repoRoot, options.baseRef, dependencies);
963
+ await collectChangedSkillVersionBumpFindings(repoRoot, options.baseRef, changedVersionedFiles, findings, dependencies);
730
964
  return {
731
- validatedSkillCount: changedSkillFiles.length,
965
+ // The count is owning files, not diffed paths: a skill whose `scripts/`
966
+ // and `references/` both changed is one file that must bump.
967
+ validatedSkillCount: changedVersionedFiles.length,
732
968
  findings,
733
969
  };
734
970
  }
@@ -738,10 +974,28 @@ export async function validateChangedSkillVersionBumps(repoRoot, options, depend
738
974
  * This pass deliberately iterates every skill directory, not just `oat-*`:
739
975
  * the top-level `version` alias is deprecated across the whole bundle, so a
740
976
  * pass that inherited the `oat-*` filter used by the other structural checks
741
- * could never fire for the 18 non-`oat-*` skills. `skill-version-alias` is a
742
- * warning (`validate-oat-skills.ts` treats warnings as non-blocking) and
743
- * `skill-version-conflict` is an error. Neither finding is produced by the
744
- * bump validator, whose wrapper fails on any finding at all.
977
+ * could never fire for the 18 non-`oat-*` skills.
978
+ *
979
+ * `skill-version-alias` is an **error**. That is step 1 of the retirement
980
+ * schedule fixed by `DR-260908-bundled-skills-declare`: the deprecated
981
+ * top-level alias becomes blocking in the first validator-changing release
982
+ * after CLI 0.2.65 (0.2.66 shipped without one). The promotion was safe to
983
+ * make because the bundled tree was verified clean immediately before it —
984
+ * `oat internal validate-oat-skills` returned 65 skills and zero findings — so
985
+ * no bundled skill trips it. A corpus case in `skills.test.ts` keeps that
986
+ * true. Step 2, removing the top-level read from
987
+ * `resolveSkillVersion`, is owned by `BL-260908-remove-the-top-level-skill`,
988
+ * must land a release later, and must not be done here: the bump gate now
989
+ * enforces `.agents/agents/*.md`, whose roles still declare the top-level
990
+ * field by design.
991
+ *
992
+ * `skill-version-alias` is never produced by the bump validator, whose wrapper
993
+ * fails on any finding at all regardless of severity. That separation is what
994
+ * keeps a third-party skill that still declares the alias from failing
995
+ * `check:skill-bumps` for a reason unrelated to bumping, and the promotion
996
+ * above does not change it. `skill-version-conflict` is not isolated that way:
997
+ * the bump collector emits it for a current or base conflict, because a
998
+ * conflicting file has no single version to compare.
745
999
  */
746
1000
  async function collectSkillVersionSourceFindings(skillsRoot, skillDirs, findings) {
747
1001
  for (const dir of skillDirs) {
@@ -756,7 +1010,14 @@ async function collectSkillVersionSourceFindings(skillsRoot, skillDirs, findings
756
1010
  continue;
757
1011
  }
758
1012
  const block = getFrontmatterBlock(content);
759
- if (block === null) {
1013
+ if (!block) {
1014
+ // The `oat-*` structural loop above already reports a missing block with
1015
+ // its own wording, so only skills it does not cover are reported here.
1016
+ // Without this, a non-`oat-*` skill could drop its frontmatter and be
1017
+ // reported by neither validator.
1018
+ if (!hasMissingFrontmatterReport(findings, skillPath)) {
1019
+ pushUniqueFinding(findings, missingFrontmatterFinding(skillPath));
1020
+ }
760
1021
  continue;
761
1022
  }
762
1023
  const parsed = parseSkillFrontmatter(block);
@@ -777,6 +1038,10 @@ async function collectSkillVersionSourceFindings(skillsRoot, skillDirs, findings
777
1038
  }
778
1039
  const resolved = resolveSkillVersion(parsed);
779
1040
  if (!resolved) {
1041
+ // Readable frontmatter that declares no version at all — a `metadata:`
1042
+ // map with no `version` child is the common shape. Skipping it silently
1043
+ // disabled the semver check and bump enforcement for that skill.
1044
+ pushUniqueFinding(findings, missingVersionFinding(skillPath));
780
1045
  continue;
781
1046
  }
782
1047
  if (resolved.conflict) {
@@ -787,7 +1052,7 @@ async function collectSkillVersionSourceFindings(skillsRoot, skillDirs, findings
787
1052
  findings.push({
788
1053
  file: skillPath,
789
1054
  code: 'skill-version-alias',
790
- severity: 'warning',
1055
+ severity: 'error',
791
1056
  message: `Frontmatter version ${resolved.version} uses the deprecated top-level alias; move it to metadata.version (metadata.version wins when both are present)`,
792
1057
  });
793
1058
  }
@@ -819,7 +1084,7 @@ export async function validateOatSkills(repoRoot, options = {}, dependencies = {
819
1084
  if (!fm) {
820
1085
  findings.push({
821
1086
  file: skillPath,
822
- message: 'Missing frontmatter block (--- ... ---)',
1087
+ message: MISSING_FRONTMATTER_BLOCK_MESSAGE,
823
1088
  });
824
1089
  continue;
825
1090
  }
@@ -897,8 +1162,8 @@ export async function validateOatSkills(repoRoot, options = {}, dependencies = {
897
1162
  await collectSyncedSafetyFindings(repoRoot, oatSkillDirs, findings);
898
1163
  await collectGateabilityFindings(skillsRoot, options.gateSkillNames, findings);
899
1164
  if (options.baseRef) {
900
- const changedSkillFiles = await listChangedSkillFiles(repoRoot, options.baseRef, dependencies);
901
- await collectChangedSkillVersionBumpFindings(repoRoot, options.baseRef, changedSkillFiles, findings, dependencies);
1165
+ const changedVersionedFiles = await listChangedVersionedFiles(repoRoot, options.baseRef, dependencies);
1166
+ await collectChangedSkillVersionBumpFindings(repoRoot, options.baseRef, changedVersionedFiles, findings, dependencies);
902
1167
  }
903
1168
  return { validatedSkillCount: oatSkillDirs.length, findings };
904
1169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.2.66",
3
+ "version": "0.2.68",
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.2.66"
37
+ "@open-agent-toolkit/control-plane": "0.2.68"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",