@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.
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +14 -1
- package/assets/docs/cli-utilities/project-log.md +63 -4
- package/assets/docs/contributing/code.md +8 -4
- package/assets/docs/contributing/skills.md +29 -17
- package/assets/docs/docs-tooling/commands.md +3 -1
- package/assets/docs/provider-sync/manifest-and-drift.md +2 -18
- package/assets/docs/provider-sync/scope-and-surface.md +3 -2
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/workflows/projects/evidence-layers.md +6 -3
- package/assets/docs/workflows/projects/implementation-execution.md +4 -3
- package/assets/docs/workflows/projects/lifecycle.md +17 -0
- package/assets/docs/workflows/projects/orchestration-model.md +6 -4
- package/assets/docs/workflows/projects/picking-up-projects.md +18 -0
- package/assets/docs/workflows/skills/repo-improve.md +7 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/create-agnostic-skill/SKILL.md +19 -37
- package/assets/skills/create-agnostic-skill/references/docs/skills-guide.md +58 -15
- package/assets/skills/create-agnostic-skill/references/skill-template.md +17 -15
- package/assets/skills/oat-agent-instructions-apply/SKILL.md +1 -1
- package/assets/skills/oat-agent-instructions-apply/references/instruction-file-templates/glob-scoped-rule.md +1 -4
- package/assets/skills/oat-dispatch-subagents/SKILL.md +4 -2
- package/assets/skills/oat-doctor/SKILL.md +1 -4
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +3 -2
- package/assets/skills/oat-project-complete/SKILL.md +158 -30
- package/assets/skills/oat-project-complete/scripts/finalize-synced-archive.mjs +26 -7
- package/assets/skills/oat-project-complete/scripts/validate-durable-archive-receipt.mjs +183 -0
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +9 -7
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +3 -2
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +11 -10
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +3 -2
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +3 -2
- package/assets/skills/oat-project-plan-writing/SKILL.md +7 -6
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +3 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +3 -2
- package/assets/skills/oat-project-retro/SKILL.md +1 -1
- package/assets/skills/oat-project-retro/references/apply-procedure.md +11 -0
- package/assets/skills/oat-project-review-provide/SKILL.md +14 -13
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +7 -6
- package/assets/skills/oat-project-summary/SKILL.md +24 -2
- package/assets/skills/oat-repo-improve/SKILL.md +9 -2
- package/assets/skills/oat-repo-improve/references/plan-template.md +4 -6
- package/assets/skills/oat-repo-knowledge-index/SKILL.md +11 -9
- package/assets/skills/oat-wave-execute/SKILL.md +20 -6
- package/assets/skills/oat-wave-program/SKILL.md +9 -4
- package/dist/commands/config/index.d.ts +2 -1
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +133 -38
- package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
- package/dist/commands/docs/index-generate/index.js +68 -12
- package/dist/commands/docs/init/index.d.ts +11 -0
- package/dist/commands/docs/init/index.d.ts.map +1 -1
- package/dist/commands/docs/init/index.js +19 -1
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +10 -2
- package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.js +42 -9
- package/dist/commands/internal/validate-skill-version-bumps.d.ts.map +1 -1
- package/dist/commands/internal/validate-skill-version-bumps.js +5 -5
- package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch-ceiling/index.js +32 -18
- package/dist/commands/project/log/append.d.ts +48 -0
- package/dist/commands/project/log/append.d.ts.map +1 -1
- package/dist/commands/project/log/append.js +207 -30
- package/dist/commands/project/log/check.d.ts +87 -1
- package/dist/commands/project/log/check.d.ts.map +1 -1
- package/dist/commands/project/log/check.js +139 -5
- package/dist/commands/project/log/grammar.d.ts +165 -0
- package/dist/commands/project/log/grammar.d.ts.map +1 -1
- package/dist/commands/project/log/grammar.js +219 -1
- package/dist/commands/project/log/rollup.d.ts +16 -1
- package/dist/commands/project/log/rollup.d.ts.map +1 -1
- package/dist/commands/project/log/rollup.js +22 -2
- package/dist/commands/project/log/synthesize.d.ts +16 -0
- package/dist/commands/project/log/synthesize.d.ts.map +1 -1
- package/dist/commands/project/log/synthesize.js +85 -9
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +24 -2
- package/dist/commands/sync/apply.d.ts.map +1 -1
- package/dist/commands/sync/apply.js +6 -4
- package/dist/config/dispatch-matrix.d.ts.map +1 -1
- package/dist/config/dispatch-matrix.js +13 -5
- package/dist/config/oat-config.d.ts +15 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +61 -7
- package/dist/config/own-keys.d.ts +22 -0
- package/dist/config/own-keys.d.ts.map +1 -0
- package/dist/config/own-keys.js +32 -0
- package/dist/config/resolve.d.ts +11 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +35 -11
- package/dist/config/sync-config.d.ts.map +1 -1
- package/dist/config/sync-config.js +16 -0
- package/dist/drift/detector.d.ts.map +1 -1
- package/dist/drift/detector.js +52 -1
- package/dist/drift/skill-view-diagnostic.d.ts.map +1 -1
- package/dist/drift/skill-view-diagnostic.js +2 -23
- package/dist/engine/compute-plan.d.ts.map +1 -1
- package/dist/engine/compute-plan.js +22 -72
- package/dist/engine/managed-copy-hash.d.ts +69 -0
- package/dist/engine/managed-copy-hash.d.ts.map +1 -0
- package/dist/engine/managed-copy-hash.js +175 -0
- package/dist/fs/assets.d.ts +11 -0
- package/dist/fs/assets.d.ts.map +1 -1
- package/dist/fs/assets.js +1 -1
- package/dist/manifest/hash.d.ts +110 -0
- package/dist/manifest/hash.d.ts.map +1 -1
- package/dist/manifest/hash.js +111 -13
- package/dist/providers/ceiling/registry.d.ts.map +1 -1
- package/dist/providers/ceiling/registry.js +6 -1
- package/dist/providers/identity/dispatch-report.d.ts.map +1 -1
- package/dist/providers/identity/dispatch-report.js +4 -1
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +21 -0
- package/dist/validation/skills.d.ts.map +1 -1
- package/dist/validation/skills.js +305 -40
- 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
|
|
498
|
-
? 'skill-frontmatter-unreadable'
|
|
499
|
-
: 'skill-version-unusable',
|
|
566
|
+
code: UNCOMPARABLE_BASE_CODES[reason],
|
|
500
567
|
severity: 'error',
|
|
501
|
-
message:
|
|
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
|
-
|
|
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
|
|
781
|
+
'.agents/skills',
|
|
782
|
+
'.agents/agents/*.md',
|
|
630
783
|
], {
|
|
631
784
|
cwd: repoRoot,
|
|
632
785
|
env: dependencies.env ?? process.env,
|
|
633
786
|
});
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
653
|
-
|
|
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
|
|
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
|
|
865
|
+
const parsedCurrent = currentBlock
|
|
866
|
+
? parseSkillFrontmatter(currentBlock)
|
|
867
|
+
: null;
|
|
662
868
|
const baseBlock = getFrontmatterBlock(baseContent);
|
|
663
|
-
const parsedBase = baseBlock
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
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.
|
|
690
|
-
//
|
|
691
|
-
//
|
|
692
|
-
// in structural validation and
|
|
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
|
-
|
|
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
|
|
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
|
|
729
|
-
await collectChangedSkillVersionBumpFindings(repoRoot, options.baseRef,
|
|
962
|
+
const changedVersionedFiles = await listChangedVersionedFiles(repoRoot, options.baseRef, dependencies);
|
|
963
|
+
await collectChangedSkillVersionBumpFindings(repoRoot, options.baseRef, changedVersionedFiles, findings, dependencies);
|
|
730
964
|
return {
|
|
731
|
-
|
|
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.
|
|
742
|
-
*
|
|
743
|
-
* `skill-version-
|
|
744
|
-
*
|
|
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
|
|
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: '
|
|
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:
|
|
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
|
|
901
|
-
await collectChangedSkillVersionBumpFindings(repoRoot, options.baseRef,
|
|
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.
|
|
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.
|
|
37
|
+
"@open-agent-toolkit/control-plane": "0.2.68"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.0",
|