agentic-workflow-manager 2.1.1 → 3.0.1
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/dist/src/commands/init.js +9 -0
- package/dist/src/release/core.js +83 -0
- package/dist/src/release/index.js +94 -0
- package/dist/src/release/orchestrator.js +116 -0
- package/dist/src/utils/config.js +12 -1
- package/dist/tests/commands/init.test.js +20 -0
- package/dist/tests/release/core.test.js +106 -0
- package/dist/tests/release/orchestrator.test.js +188 -0
- package/dist/tests/utils/config.test.js +7 -2
- package/package.json +3 -1
- package/dist/tests/core/registry-versioned-sync.test.js +0 -113
- package/dist/tests/core/registry.test.js +0 -51
- package/dist/tests/registry/b3-ledger-wiring.test.js +0 -74
- package/dist/tests/registry/catalog-consistency.test.js +0 -47
- package/dist/tests/registry/design-skills.test.js +0 -146
- package/dist/tests/registry/prose-agnostic.test.js +0 -18
- package/dist/tests/registry/sensor-packs.test.js +0 -45
- package/dist/tests/registry/skill-versions.test.js +0 -26
- package/dist/tests/registry/using-awm.test.js +0 -39
|
@@ -50,6 +50,7 @@ const registries_1 = require("../core/registries");
|
|
|
50
50
|
const orchestrator_1 = require("../core/init/orchestrator");
|
|
51
51
|
const steps_1 = require("../core/init/steps");
|
|
52
52
|
const paths_1 = require("../core/paths");
|
|
53
|
+
const config_1 = require("../utils/config");
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
// Rendering
|
|
55
56
|
// ---------------------------------------------------------------------------
|
|
@@ -91,6 +92,14 @@ function renderInitOutcome(o) {
|
|
|
91
92
|
async function runInit(opts = {}) {
|
|
92
93
|
const cwd = opts.cwd ?? process.cwd();
|
|
93
94
|
const agent = opts.agent ?? 'claude-code';
|
|
95
|
+
// #7: make init the source of truth for the default agent. Persist the resolved
|
|
96
|
+
// agent so later `awm add`/`awm sync` (which read preferences.defaultAgent) target
|
|
97
|
+
// the right agent instead of stamping the static default. Do NOT clobber an existing
|
|
98
|
+
// explicit preference on a bare re-init: only write when an agent was passed via -a,
|
|
99
|
+
// or when no preferences file exists yet.
|
|
100
|
+
if (opts.agent != null || !(0, config_1.preferencesExist)()) {
|
|
101
|
+
(0, config_1.savePreferences)({ ...(0, config_1.getPreferences)(), defaultAgent: agent });
|
|
102
|
+
}
|
|
94
103
|
let outcome;
|
|
95
104
|
try {
|
|
96
105
|
const mergedActions = {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GIT_LOG_FORMAT = exports.US = exports.RS = exports.PKG_NAME = void 0;
|
|
4
|
+
exports.parseCommits = parseCommits;
|
|
5
|
+
exports.determineBump = determineBump;
|
|
6
|
+
exports.nextVersion = nextVersion;
|
|
7
|
+
exports.selectFloor = selectFloor;
|
|
8
|
+
exports.renderChangelog = renderChangelog;
|
|
9
|
+
const versioning_1 = require("../core/versioning");
|
|
10
|
+
exports.PKG_NAME = 'agentic-workflow-manager';
|
|
11
|
+
exports.RS = '\x1e';
|
|
12
|
+
exports.US = '\x1f';
|
|
13
|
+
exports.GIT_LOG_FORMAT = `%s${exports.US}%b${exports.RS}`;
|
|
14
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
15
|
+
const HEADER_RE = /^(\w+)(?:\(([^)]+)\))?(!)?:\s+(.+)$/;
|
|
16
|
+
function parseOne(record) {
|
|
17
|
+
const [header, ...rest] = record.split(exports.US);
|
|
18
|
+
const body = rest.join(exports.US);
|
|
19
|
+
const m = HEADER_RE.exec(header.trim());
|
|
20
|
+
if (!m)
|
|
21
|
+
return null;
|
|
22
|
+
return {
|
|
23
|
+
type: m[1],
|
|
24
|
+
scope: m[2] ?? null,
|
|
25
|
+
breaking: Boolean(m[3]) || /^BREAKING CHANGE:/m.test(body),
|
|
26
|
+
subject: m[4].trim(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function parseCommits(raw) {
|
|
30
|
+
return raw
|
|
31
|
+
.split(exports.RS)
|
|
32
|
+
.map((r) => r.trim())
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map(parseOne)
|
|
35
|
+
.filter((c) => c !== null);
|
|
36
|
+
}
|
|
37
|
+
function determineBump(commits) {
|
|
38
|
+
if (commits.some((c) => c.breaking))
|
|
39
|
+
return 'major';
|
|
40
|
+
if (commits.some((c) => c.type === 'feat'))
|
|
41
|
+
return 'minor';
|
|
42
|
+
if (commits.some((c) => c.type === 'fix' || c.type === 'perf'))
|
|
43
|
+
return 'patch';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
function nextVersion(base, bump) {
|
|
47
|
+
const m = SEMVER_RE.exec((base ?? '').trim());
|
|
48
|
+
if (!m)
|
|
49
|
+
throw new Error(`Invalid base version: "${base}"`);
|
|
50
|
+
const [maj, min, pat] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
51
|
+
if (bump === 'major')
|
|
52
|
+
return `${maj + 1}.0.0`;
|
|
53
|
+
if (bump === 'minor')
|
|
54
|
+
return `${maj}.${min + 1}.0`;
|
|
55
|
+
return `${maj}.${min}.${pat + 1}`;
|
|
56
|
+
}
|
|
57
|
+
function selectFloor(current, lastTagVersion) {
|
|
58
|
+
if (!SEMVER_RE.test((current ?? '').trim())) {
|
|
59
|
+
throw new Error(`Invalid current version: "${current}"`);
|
|
60
|
+
}
|
|
61
|
+
const cur = current.trim();
|
|
62
|
+
if (!lastTagVersion)
|
|
63
|
+
return cur;
|
|
64
|
+
return (0, versioning_1.compareSemver)(cur, lastTagVersion) >= 0 ? cur : lastTagVersion;
|
|
65
|
+
}
|
|
66
|
+
function line(c) {
|
|
67
|
+
return c.scope ? `- **${c.scope}:** ${c.subject}` : `- ${c.subject}`;
|
|
68
|
+
}
|
|
69
|
+
function renderChangelog(version, dateISO, commits) {
|
|
70
|
+
const sections = [`## v${version} - ${dateISO}`, ''];
|
|
71
|
+
const groups = [
|
|
72
|
+
['Breaking Changes', (c) => c.breaking],
|
|
73
|
+
['Features', (c) => !c.breaking && c.type === 'feat'],
|
|
74
|
+
['Fixes', (c) => !c.breaking && (c.type === 'fix' || c.type === 'perf')],
|
|
75
|
+
];
|
|
76
|
+
for (const [title, pred] of groups) {
|
|
77
|
+
const items = commits.filter(pred);
|
|
78
|
+
if (items.length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
sections.push(`### ${title}`, ...items.map(line), '');
|
|
81
|
+
}
|
|
82
|
+
return sections.join('\n').trimEnd() + '\n';
|
|
83
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseArgs = parseArgs;
|
|
7
|
+
exports.main = main;
|
|
8
|
+
// cli/src/release/index.ts
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const orchestrator_1 = require("./orchestrator");
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const opts = { dryRun: false, force: null, push: true, branch: 'main', cliDir: '' };
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
const a = argv[i];
|
|
17
|
+
if (a === '--dry-run')
|
|
18
|
+
opts.dryRun = true;
|
|
19
|
+
else if (a === '--no-push')
|
|
20
|
+
opts.push = false;
|
|
21
|
+
else if (a === '--branch') {
|
|
22
|
+
const val = argv[++i];
|
|
23
|
+
if (val === undefined || val.startsWith('--')) {
|
|
24
|
+
throw new Error('--branch requiere un valor (ej: --branch main)');
|
|
25
|
+
}
|
|
26
|
+
opts.branch = val;
|
|
27
|
+
}
|
|
28
|
+
else if (a === '--force') {
|
|
29
|
+
const lvl = argv[++i];
|
|
30
|
+
if (lvl !== 'major' && lvl !== 'minor' && lvl !== 'patch') {
|
|
31
|
+
throw new Error(`--force requiere major|minor|patch, recibió "${lvl}"`);
|
|
32
|
+
}
|
|
33
|
+
opts.force = lvl;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
throw new Error(`Flag desconocida: ${a}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return opts;
|
|
40
|
+
}
|
|
41
|
+
function realIO(repoRoot, cliDir) {
|
|
42
|
+
const pkgPath = path_1.default.join(cliDir, 'package.json');
|
|
43
|
+
const changelogPath = path_1.default.join(repoRoot, 'CHANGELOG.md');
|
|
44
|
+
const npmrcPath = path_1.default.join(cliDir, '.npmrc');
|
|
45
|
+
return {
|
|
46
|
+
run(cmd, args, o) {
|
|
47
|
+
return (0, child_process_1.execFileSync)(cmd, args, { cwd: o?.cwd ?? repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
48
|
+
},
|
|
49
|
+
readPackageVersion: () => JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8')).version,
|
|
50
|
+
writePackageVersion: (v) => {
|
|
51
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8'));
|
|
52
|
+
pkg.version = v;
|
|
53
|
+
fs_1.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
54
|
+
},
|
|
55
|
+
readChangelog: () => (fs_1.default.existsSync(changelogPath) ? fs_1.default.readFileSync(changelogPath, 'utf8') : ''),
|
|
56
|
+
writeChangelog: (c) => fs_1.default.writeFileSync(changelogPath, c),
|
|
57
|
+
writeNpmrc: (token) => fs_1.default.writeFileSync(npmrcPath, `//registry.npmjs.org/:_authToken=${token}\n`),
|
|
58
|
+
removeNpmrc: () => { if (fs_1.default.existsSync(npmrcPath))
|
|
59
|
+
fs_1.default.rmSync(npmrcPath); },
|
|
60
|
+
today: () => new Date().toISOString().slice(0, 10),
|
|
61
|
+
log: (m) => console.log(m),
|
|
62
|
+
env: process.env,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function main(argv) {
|
|
66
|
+
let opts;
|
|
67
|
+
try {
|
|
68
|
+
opts = parseArgs(argv);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.error(e.message);
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
// __dirname en dist/src/release/ → 4 niveles arriba = repoRoot
|
|
75
|
+
// dist/src/release → dist/src → dist → cli → repoRoot
|
|
76
|
+
const repoRoot = path_1.default.resolve(__dirname, '..', '..', '..', '..');
|
|
77
|
+
const cliDir = path_1.default.resolve(repoRoot, 'cli');
|
|
78
|
+
opts.cliDir = cliDir;
|
|
79
|
+
try {
|
|
80
|
+
const res = (0, orchestrator_1.release)(opts, realIO(repoRoot, cliDir));
|
|
81
|
+
if (res.released)
|
|
82
|
+
console.log(`✓ Publicado v${res.version}`);
|
|
83
|
+
else
|
|
84
|
+
console.log(`· Sin release: ${res.reason}${res.version ? ` (v${res.version})` : ''}`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
console.error(`✗ ${e.message}`);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (require.main === module) {
|
|
93
|
+
process.exit(main(process.argv.slice(2)));
|
|
94
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.release = release;
|
|
4
|
+
// cli/src/release/orchestrator.ts
|
|
5
|
+
const core_1 = require("./core");
|
|
6
|
+
const TAG_RE = /^v(\d+)\.(\d+)\.(\d+)$/;
|
|
7
|
+
function highestTag(io) {
|
|
8
|
+
const raw = io.run('git', ['tag', '--list', 'v*']).trim();
|
|
9
|
+
const tags = raw.split('\n').map((t) => t.trim()).filter((t) => TAG_RE.test(t));
|
|
10
|
+
if (tags.length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
tags.sort((a, b) => {
|
|
13
|
+
const [, a1, a2, a3] = TAG_RE.exec(a).map(Number);
|
|
14
|
+
const [, b1, b2, b3] = TAG_RE.exec(b).map(Number);
|
|
15
|
+
return a1 - b1 || a2 - b2 || a3 - b3;
|
|
16
|
+
});
|
|
17
|
+
return tags[tags.length - 1];
|
|
18
|
+
}
|
|
19
|
+
function release(opts, io) {
|
|
20
|
+
// --- preflight gates (contrato, antes de cualquier early-exit) ---
|
|
21
|
+
if (!opts.force) {
|
|
22
|
+
const branch = io.run('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
23
|
+
if (branch !== opts.branch) {
|
|
24
|
+
throw new Error(`Rama actual "${branch}" != esperada "${opts.branch}" (usá --force para relajar)`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!opts.dryRun) {
|
|
28
|
+
const dirty = io.run('git', ['status', '--porcelain']).trim();
|
|
29
|
+
if (dirty)
|
|
30
|
+
throw new Error('Working tree con cambios sin commitear — abortando');
|
|
31
|
+
if (!io.env.NPM_TOKEN && !io.env.NODE_AUTH_TOKEN && !io.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
|
|
32
|
+
throw new Error('Falta credencial de publicación: configurá NPM_TOKEN, NODE_AUTH_TOKEN, o usa OIDC (id-token: write en GitHub Actions)');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// baseline
|
|
36
|
+
const current = io.readPackageVersion();
|
|
37
|
+
const lastTag = highestTag(io); // "vX.Y.Z" | null
|
|
38
|
+
const lastTagVersion = lastTag ? lastTag.slice(1) : null;
|
|
39
|
+
const floor = (0, core_1.selectFloor)(current, lastTagVersion);
|
|
40
|
+
// rango de commits (solo cli/)
|
|
41
|
+
const range = lastTag ? [`${lastTag}..HEAD`] : [];
|
|
42
|
+
const logArgs = ['log', ...range, '--no-merges', `--format=${core_1.GIT_LOG_FORMAT}`, '--', 'cli/'];
|
|
43
|
+
const commits = (0, core_1.parseCommits)(io.run('git', logArgs));
|
|
44
|
+
// bump
|
|
45
|
+
const bump = opts.force ?? (0, core_1.determineBump)(commits);
|
|
46
|
+
if (!bump)
|
|
47
|
+
return { released: false, reason: 'nada que publicar (no releasable commits)' };
|
|
48
|
+
const version = (0, core_1.nextVersion)(floor, bump);
|
|
49
|
+
// GATE idempotencia
|
|
50
|
+
if (io.run('git', ['tag', '--list', `v${version}`]).trim()) {
|
|
51
|
+
throw new Error(`El tag v${version} ya existe — abortando`);
|
|
52
|
+
}
|
|
53
|
+
let published = '';
|
|
54
|
+
try {
|
|
55
|
+
published = io.run('npm', ['view', `${core_1.PKG_NAME}@${version}`, 'version']).trim();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
published = '';
|
|
59
|
+
}
|
|
60
|
+
if (published)
|
|
61
|
+
throw new Error(`${core_1.PKG_NAME}@${version} ya está publicado en npm — abortando`);
|
|
62
|
+
if (opts.dryRun) {
|
|
63
|
+
io.log(`[dry-run] publicaría v${version} (bump=${bump}, base=${floor})`);
|
|
64
|
+
return { released: false, version, reason: 'dry-run' };
|
|
65
|
+
}
|
|
66
|
+
// aplicar
|
|
67
|
+
io.writePackageVersion(version);
|
|
68
|
+
const section = (0, core_1.renderChangelog)(version, io.today(), commits);
|
|
69
|
+
io.writeChangelog(section + '\n' + io.readChangelog());
|
|
70
|
+
io.run('git', ['add', 'cli/package.json', 'CHANGELOG.md']);
|
|
71
|
+
io.run('git', ['commit', '-m', `chore(release): v${version} [skip ci]`]);
|
|
72
|
+
io.run('git', ['tag', '-a', `v${version}`, '-m', `v${version}`]);
|
|
73
|
+
// OIDC mode: GitHub inyecta ACTIONS_ID_TOKEN_REQUEST_URL con id-token:write
|
|
74
|
+
// o NODE_AUTH_TOKEN sin NPM_TOKEN (setup-node legacy con OIDC)
|
|
75
|
+
// Legacy mode (NPM_TOKEN): write .npmrc temporal, cleanup en finally
|
|
76
|
+
const useOidc = !io.env.NPM_TOKEN &&
|
|
77
|
+
(!!io.env.ACTIONS_ID_TOKEN_REQUEST_URL || !!io.env.NODE_AUTH_TOKEN);
|
|
78
|
+
let publishError;
|
|
79
|
+
if (useOidc) {
|
|
80
|
+
try {
|
|
81
|
+
io.run('npm', ['publish', '--provenance'], { cwd: opts.cliDir });
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
publishError = e;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const token = io.env.NPM_TOKEN;
|
|
89
|
+
try {
|
|
90
|
+
io.writeNpmrc(token);
|
|
91
|
+
io.run('npm', ['publish'], { cwd: opts.cliDir });
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
publishError = e;
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
io.removeNpmrc();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (publishError) {
|
|
101
|
+
try {
|
|
102
|
+
io.run('git', ['tag', '-d', `v${version}`]);
|
|
103
|
+
}
|
|
104
|
+
catch { /* best effort */ }
|
|
105
|
+
try {
|
|
106
|
+
io.run('git', ['reset', '--hard', 'HEAD~1']);
|
|
107
|
+
}
|
|
108
|
+
catch { /* best effort */ }
|
|
109
|
+
throw publishError;
|
|
110
|
+
}
|
|
111
|
+
if (opts.push) {
|
|
112
|
+
io.run('git', ['push', 'origin', opts.branch]);
|
|
113
|
+
io.run('git', ['push', 'origin', `v${version}`]);
|
|
114
|
+
}
|
|
115
|
+
return { released: true, version };
|
|
116
|
+
}
|
package/dist/src/utils/config.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.preferencesExist = preferencesExist;
|
|
6
7
|
exports.getPreferences = getPreferences;
|
|
7
8
|
exports.savePreferences = savePreferences;
|
|
8
9
|
// src/utils/config.ts
|
|
@@ -10,13 +11,23 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
11
12
|
const paths_1 = require("../core/paths");
|
|
12
13
|
const DEFAULT_PREFS = {
|
|
13
|
-
|
|
14
|
+
// claude-code matches `awm init`'s own documented default (see init.ts / `awm init --help`).
|
|
15
|
+
// Previously 'antigravity', which silently mis-installed bundles in claude-code
|
|
16
|
+
// environments when `awm add` (the first getPreferences caller) stamped it to disk (#7).
|
|
17
|
+
defaultAgent: 'claude-code',
|
|
14
18
|
installMethod: 'symlink',
|
|
15
19
|
defaultScope: 'local'
|
|
16
20
|
};
|
|
17
21
|
function prefsDir() {
|
|
18
22
|
return (0, paths_1.awmHome)();
|
|
19
23
|
}
|
|
24
|
+
function prefsFile() {
|
|
25
|
+
return path_1.default.join(prefsDir(), 'preferences.json');
|
|
26
|
+
}
|
|
27
|
+
/** True if preferences.json is already on disk (no side effect — does NOT create it). */
|
|
28
|
+
function preferencesExist() {
|
|
29
|
+
return fs_1.default.existsSync(prefsFile());
|
|
30
|
+
}
|
|
20
31
|
function getPreferences() {
|
|
21
32
|
const file = path_1.default.join(prefsDir(), 'preferences.json');
|
|
22
33
|
if (!fs_1.default.existsSync(file)) {
|
|
@@ -84,4 +84,24 @@ describe('runInit', () => {
|
|
|
84
84
|
expect(parsed.after.overall).toBe('degraded');
|
|
85
85
|
expect(code).toBe(1);
|
|
86
86
|
});
|
|
87
|
+
const prefsFile = () => path_1.default.join(process.env.AWM_HOME, 'preferences.json');
|
|
88
|
+
const readAgent = () => JSON.parse(fs_1.default.readFileSync(prefsFile(), 'utf-8')).defaultAgent;
|
|
89
|
+
it('#7: first init (no -a) persists claude-code as the default agent', async () => {
|
|
90
|
+
const { runInit } = require('../../src/commands/init');
|
|
91
|
+
expect(fs_1.default.existsSync(prefsFile())).toBe(false);
|
|
92
|
+
await runInit({ cwd: tmpHome, yes: true, actions: { syncCache: async () => { } } });
|
|
93
|
+
expect(readAgent()).toBe('claude-code');
|
|
94
|
+
});
|
|
95
|
+
it('#7: init -a opencode persists the explicit agent', async () => {
|
|
96
|
+
const { runInit } = require('../../src/commands/init');
|
|
97
|
+
await runInit({ cwd: tmpHome, yes: true, agent: 'opencode', actions: { syncCache: async () => { } } });
|
|
98
|
+
expect(readAgent()).toBe('opencode');
|
|
99
|
+
});
|
|
100
|
+
it('#7: re-init without -a does NOT clobber an existing explicit preference', async () => {
|
|
101
|
+
const { savePreferences } = require('../../src/utils/config');
|
|
102
|
+
savePreferences({ defaultAgent: 'opencode', installMethod: 'symlink', defaultScope: 'local' });
|
|
103
|
+
const { runInit } = require('../../src/commands/init');
|
|
104
|
+
await runInit({ cwd: tmpHome, yes: true, actions: { syncCache: async () => { } } });
|
|
105
|
+
expect(readAgent()).toBe('opencode');
|
|
106
|
+
});
|
|
87
107
|
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("../../src/release/core");
|
|
4
|
+
const rec = (header, body = '') => `${header}${core_1.US}${body}${core_1.RS}`;
|
|
5
|
+
const mk = (type, breaking = false) => ({ type, scope: null, breaking, subject: 's' });
|
|
6
|
+
describe('parseCommits', () => {
|
|
7
|
+
it('parsea type, scope, subject', () => {
|
|
8
|
+
const [c] = (0, core_1.parseCommits)(rec('feat(add): nueva flag'));
|
|
9
|
+
expect(c).toEqual({ type: 'feat', scope: 'add', breaking: false, subject: 'nueva flag' });
|
|
10
|
+
});
|
|
11
|
+
it('scope null cuando no hay paréntesis', () => {
|
|
12
|
+
expect((0, core_1.parseCommits)(rec('fix: corrige bug'))[0].scope).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
it('breaking por bang en el header', () => {
|
|
15
|
+
expect((0, core_1.parseCommits)(rec('feat!: rompe API'))[0].breaking).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it('breaking por footer BREAKING CHANGE en el body', () => {
|
|
18
|
+
expect((0, core_1.parseCommits)(rec('feat(x): y', 'cuerpo\nBREAKING CHANGE: cambia todo'))[0].breaking).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
it('descarta commits no convencionales sin romper', () => {
|
|
21
|
+
expect((0, core_1.parseCommits)(rec('merge branch foo') + rec('feat: ok'))).toEqual([
|
|
22
|
+
{ type: 'feat', scope: null, breaking: false, subject: 'ok' },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
it('entrada vacía → arreglo vacío', () => {
|
|
26
|
+
expect((0, core_1.parseCommits)('')).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('determineBump', () => {
|
|
30
|
+
it('breaking → major (gana sobre feat/fix)', () => {
|
|
31
|
+
expect((0, core_1.determineBump)([mk('fix'), mk('feat', true)])).toBe('major');
|
|
32
|
+
});
|
|
33
|
+
it('feat sin breaking → minor', () => {
|
|
34
|
+
expect((0, core_1.determineBump)([mk('fix'), mk('feat')])).toBe('minor');
|
|
35
|
+
});
|
|
36
|
+
it('fix o perf → patch', () => {
|
|
37
|
+
expect((0, core_1.determineBump)([mk('fix')])).toBe('patch');
|
|
38
|
+
expect((0, core_1.determineBump)([mk('perf')])).toBe('patch');
|
|
39
|
+
});
|
|
40
|
+
it('solo docs/chore/refactor → null (nada releasable)', () => {
|
|
41
|
+
expect((0, core_1.determineBump)([mk('docs'), mk('chore'), mk('refactor')])).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
it('lista vacía → null', () => {
|
|
44
|
+
expect((0, core_1.determineBump)([])).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('nextVersion', () => {
|
|
48
|
+
it('major resetea minor y patch', () => {
|
|
49
|
+
expect((0, core_1.nextVersion)('2.1.1', 'major')).toBe('3.0.0');
|
|
50
|
+
});
|
|
51
|
+
it('minor resetea patch', () => {
|
|
52
|
+
expect((0, core_1.nextVersion)('2.1.1', 'minor')).toBe('2.2.0');
|
|
53
|
+
});
|
|
54
|
+
it('patch incrementa patch', () => {
|
|
55
|
+
expect((0, core_1.nextVersion)('2.1.1', 'patch')).toBe('2.1.2');
|
|
56
|
+
});
|
|
57
|
+
it('tolera espacios alrededor', () => {
|
|
58
|
+
expect((0, core_1.nextVersion)(' 2.1.1 ', 'patch')).toBe('2.1.2');
|
|
59
|
+
});
|
|
60
|
+
it.each(['', '.', '..', 'x.y.z', '2.1', '2.1.1.0'])('rechaza base inválida %p', (bad) => {
|
|
61
|
+
expect(() => (0, core_1.nextVersion)(bad, 'patch')).toThrow(/invalid base version/i);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('selectFloor', () => {
|
|
65
|
+
it('sin tag → usa la versión del package.json', () => {
|
|
66
|
+
expect((0, core_1.selectFloor)('2.1.1', null)).toBe('2.1.1');
|
|
67
|
+
});
|
|
68
|
+
it('package.json mayor que el tag → gana package.json (caso drift real)', () => {
|
|
69
|
+
expect((0, core_1.selectFloor)('2.1.1', '1.0.0')).toBe('2.1.1');
|
|
70
|
+
});
|
|
71
|
+
it('tag mayor que package.json → gana el tag', () => {
|
|
72
|
+
expect((0, core_1.selectFloor)('2.1.1', '2.5.0')).toBe('2.5.0');
|
|
73
|
+
});
|
|
74
|
+
it('iguales → ese valor', () => {
|
|
75
|
+
expect((0, core_1.selectFloor)('2.1.1', '2.1.1')).toBe('2.1.1');
|
|
76
|
+
});
|
|
77
|
+
it('rechaza current inválido', () => {
|
|
78
|
+
expect(() => (0, core_1.selectFloor)('', null)).toThrow(/invalid/i);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe('renderChangelog', () => {
|
|
82
|
+
const commits = [
|
|
83
|
+
{ type: 'feat', scope: 'add', breaking: false, subject: 'flag --all' },
|
|
84
|
+
{ type: 'fix', scope: null, breaking: false, subject: 'corrige width' },
|
|
85
|
+
{ type: 'chore', scope: null, breaking: true, subject: 'sube node' },
|
|
86
|
+
];
|
|
87
|
+
it('encabeza con versión y fecha', () => {
|
|
88
|
+
expect((0, core_1.renderChangelog)('2.2.0', '2026-06-25', commits)).toContain('## v2.2.0 - 2026-06-25');
|
|
89
|
+
});
|
|
90
|
+
it('agrupa Features, Fixes y Breaking Changes', () => {
|
|
91
|
+
const md = (0, core_1.renderChangelog)('2.2.0', '2026-06-25', commits);
|
|
92
|
+
expect(md).toContain('### Features');
|
|
93
|
+
expect(md).toContain('- **add:** flag --all');
|
|
94
|
+
expect(md).toContain('### Fixes');
|
|
95
|
+
expect(md).toContain('- corrige width');
|
|
96
|
+
expect(md).toContain('### Breaking Changes');
|
|
97
|
+
expect(md).toContain('- sube node');
|
|
98
|
+
});
|
|
99
|
+
it('omite secciones vacías', () => {
|
|
100
|
+
const md = (0, core_1.renderChangelog)('2.2.1', '2026-06-25', [
|
|
101
|
+
{ type: 'fix', scope: null, breaking: false, subject: 'x' },
|
|
102
|
+
]);
|
|
103
|
+
expect(md).toContain('### Fixes');
|
|
104
|
+
expect(md).not.toContain('### Features');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// cli/tests/release/orchestrator.test.ts
|
|
4
|
+
const orchestrator_1 = require("../../src/release/orchestrator");
|
|
5
|
+
const core_1 = require("../../src/release/core");
|
|
6
|
+
function makeIO(over = {}) {
|
|
7
|
+
const calls = [];
|
|
8
|
+
let pkgVersion = '2.1.1';
|
|
9
|
+
const io = {
|
|
10
|
+
run(cmd, args) {
|
|
11
|
+
const full = `${cmd} ${args.join(' ')}`;
|
|
12
|
+
calls.push(full);
|
|
13
|
+
if (cmd === 'git' && args[0] === 'rev-parse' && args.includes('--abbrev-ref'))
|
|
14
|
+
return 'main';
|
|
15
|
+
if (cmd === 'git' && args[0] === 'status')
|
|
16
|
+
return '';
|
|
17
|
+
if (cmd === 'git' && args[0] === 'tag' && args[1] === '--list' && args[2] === 'v*')
|
|
18
|
+
return over.tags ?? '';
|
|
19
|
+
if (cmd === 'git' && args[0] === 'tag' && args[1] === '--list')
|
|
20
|
+
return ''; // gate: tag puntual no existe
|
|
21
|
+
if (cmd === 'git' && args[0] === 'log')
|
|
22
|
+
return over.commits ?? `feat: nueva${core_1.US}${core_1.RS}`;
|
|
23
|
+
if (cmd === 'npm' && args[0] === 'view')
|
|
24
|
+
return over.npmView ?? '';
|
|
25
|
+
return '';
|
|
26
|
+
},
|
|
27
|
+
readPackageVersion: () => pkgVersion,
|
|
28
|
+
writePackageVersion: (v) => { pkgVersion = v; calls.push(`WRITE_PKG ${v}`); },
|
|
29
|
+
readChangelog: () => '',
|
|
30
|
+
writeChangelog: (c) => calls.push(`WRITE_CHANGELOG ${c.split('\n')[0]}`),
|
|
31
|
+
writeNpmrc: () => calls.push('WRITE_NPMRC'),
|
|
32
|
+
removeNpmrc: () => calls.push('REMOVE_NPMRC'),
|
|
33
|
+
today: () => '2026-06-25',
|
|
34
|
+
log: () => { },
|
|
35
|
+
env: { NPM_TOKEN: 'tok' },
|
|
36
|
+
...over,
|
|
37
|
+
};
|
|
38
|
+
return { io, calls };
|
|
39
|
+
}
|
|
40
|
+
const opts = (o = {}) => ({ dryRun: false, force: null, push: true, branch: 'main', cliDir: '/cli', ...o });
|
|
41
|
+
describe('release — happy path', () => {
|
|
42
|
+
it('feat → minor: escribe versión, commitea, taggea, publica y pushea en orden', () => {
|
|
43
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
44
|
+
const res = (0, orchestrator_1.release)(opts(), io);
|
|
45
|
+
expect(res).toEqual({ released: true, version: '2.2.0' });
|
|
46
|
+
expect(calls).toContain('WRITE_PKG 2.2.0');
|
|
47
|
+
const joined = calls.join('\n');
|
|
48
|
+
expect(joined).toMatch(/git commit .*chore\(release\): v2.2.0/);
|
|
49
|
+
expect(joined).toMatch(/git tag -a v2.2.0/);
|
|
50
|
+
expect(joined).toMatch(/npm publish/);
|
|
51
|
+
expect(joined).toMatch(/git push origin main/);
|
|
52
|
+
expect(joined).toMatch(/git push origin v2.2.0/);
|
|
53
|
+
// npmrc creado y SIEMPRE removido
|
|
54
|
+
expect(calls).toContain('WRITE_NPMRC');
|
|
55
|
+
expect(calls).toContain('REMOVE_NPMRC');
|
|
56
|
+
});
|
|
57
|
+
it('sin commits releasables → no publica (exit 0 lógico)', () => {
|
|
58
|
+
const { io, calls } = makeIO({ commits: `docs: solo docs${core_1.US}${core_1.RS}` });
|
|
59
|
+
const res = (0, orchestrator_1.release)(opts(), io);
|
|
60
|
+
expect(res.released).toBe(false);
|
|
61
|
+
expect(res.reason).toMatch(/nada que publicar|no releasable/i);
|
|
62
|
+
expect(calls).not.toContain('WRITE_PKG 2.1.2');
|
|
63
|
+
expect(calls.join('\n')).not.toMatch(/npm publish/);
|
|
64
|
+
});
|
|
65
|
+
it('--force patch publica aunque no haya commits releasables', () => {
|
|
66
|
+
const { io } = makeIO({ commits: `docs: x${core_1.US}${core_1.RS}` });
|
|
67
|
+
expect((0, orchestrator_1.release)(opts({ force: 'patch' }), io)).toEqual({ released: true, version: '2.1.2' });
|
|
68
|
+
});
|
|
69
|
+
it('--dry-run calcula la versión pero NO escribe ni publica', () => {
|
|
70
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
71
|
+
const res = (0, orchestrator_1.release)(opts({ dryRun: true }), io);
|
|
72
|
+
expect(res).toEqual({ released: false, version: '2.2.0', reason: 'dry-run' });
|
|
73
|
+
expect(calls.join('\n')).not.toMatch(/npm publish|WRITE_PKG|git commit/);
|
|
74
|
+
});
|
|
75
|
+
it('OIDC via ACTIONS_ID_TOKEN_REQUEST_URL: usa --provenance y omite .npmrc', () => {
|
|
76
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
77
|
+
io.env = { ACTIONS_ID_TOKEN_REQUEST_URL: 'https://token.actions.githubusercontent.com/...' };
|
|
78
|
+
const res = (0, orchestrator_1.release)(opts(), io);
|
|
79
|
+
expect(res).toEqual({ released: true, version: '2.2.0' });
|
|
80
|
+
expect(calls.join('\n')).toMatch(/npm publish --provenance/);
|
|
81
|
+
expect(calls).not.toContain('WRITE_NPMRC');
|
|
82
|
+
expect(calls).not.toContain('REMOVE_NPMRC');
|
|
83
|
+
});
|
|
84
|
+
it('OIDC via NODE_AUTH_TOKEN sin NPM_TOKEN: usa --provenance y omite .npmrc', () => {
|
|
85
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
86
|
+
io.env = { NODE_AUTH_TOKEN: 'oidc-tok' };
|
|
87
|
+
const res = (0, orchestrator_1.release)(opts(), io);
|
|
88
|
+
expect(res).toEqual({ released: true, version: '2.2.0' });
|
|
89
|
+
expect(calls.join('\n')).toMatch(/npm publish --provenance/);
|
|
90
|
+
expect(calls).not.toContain('WRITE_NPMRC');
|
|
91
|
+
});
|
|
92
|
+
it('--no-push omite los push', () => {
|
|
93
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
94
|
+
(0, orchestrator_1.release)(opts({ push: false }), io);
|
|
95
|
+
expect(calls.join('\n')).not.toMatch(/git push/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('release — gates de preflight', () => {
|
|
99
|
+
it('aborta si la rama no es la esperada', () => {
|
|
100
|
+
const fake = makeIO().io;
|
|
101
|
+
const origRun = fake.run;
|
|
102
|
+
fake.run = (cmd, args, o) => cmd === 'git' && args[0] === 'rev-parse' ? 'feature/x' : origRun(cmd, args, o);
|
|
103
|
+
expect(() => (0, orchestrator_1.release)(opts(), fake)).toThrow(/rama|branch/i);
|
|
104
|
+
});
|
|
105
|
+
it('--force relaja el gate de rama', () => {
|
|
106
|
+
const fake = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` }).io;
|
|
107
|
+
const origRun = fake.run;
|
|
108
|
+
fake.run = (cmd, args, o) => cmd === 'git' && args[0] === 'rev-parse' ? 'feature/x' : origRun(cmd, args, o);
|
|
109
|
+
expect(() => (0, orchestrator_1.release)(opts({ force: 'patch' }), fake)).not.toThrow();
|
|
110
|
+
});
|
|
111
|
+
it('aborta si el working tree está sucio', () => {
|
|
112
|
+
const fake = makeIO().io;
|
|
113
|
+
const origRun = fake.run;
|
|
114
|
+
fake.run = (cmd, args, o) => cmd === 'git' && args[0] === 'status' ? ' M cli/src/x.ts' : origRun(cmd, args, o);
|
|
115
|
+
expect(() => (0, orchestrator_1.release)(opts(), fake)).toThrow(/working tree|sin commitear|dirty/i);
|
|
116
|
+
});
|
|
117
|
+
it('aborta si no hay credencial de publicación', () => {
|
|
118
|
+
const fake = makeIO().io;
|
|
119
|
+
fake.env = {};
|
|
120
|
+
expect(() => (0, orchestrator_1.release)(opts(), fake)).toThrow(/NPM_TOKEN|NODE_AUTH_TOKEN|OIDC/i);
|
|
121
|
+
});
|
|
122
|
+
it('el gate de credencial corre ANTES del early-exit de "nada que publicar"', () => {
|
|
123
|
+
const fake = makeIO({ commits: `docs: x${core_1.US}${core_1.RS}` }).io; // sin commits releasables
|
|
124
|
+
fake.env = {};
|
|
125
|
+
expect(() => (0, orchestrator_1.release)(opts(), fake)).toThrow(/NPM_TOKEN|NODE_AUTH_TOKEN|OIDC/i); // no devuelve {released:false}
|
|
126
|
+
});
|
|
127
|
+
it('--dry-run no exige NPM_TOKEN ni working tree limpio', () => {
|
|
128
|
+
const fake = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` }).io;
|
|
129
|
+
fake.env = {};
|
|
130
|
+
const origRun = fake.run;
|
|
131
|
+
fake.run = (cmd, args, o) => cmd === 'git' && args[0] === 'status' ? ' M x' : origRun(cmd, args, o);
|
|
132
|
+
expect((0, orchestrator_1.release)(opts({ dryRun: true }), fake).version).toBe('2.2.0');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('release — rollback', () => {
|
|
136
|
+
it('si npm publish falla, hace rollback del commit y del tag', () => {
|
|
137
|
+
const { io, calls } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
138
|
+
const origRun = io.run;
|
|
139
|
+
io.run = (cmd, args, o) => {
|
|
140
|
+
if (cmd === 'npm' && args[0] === 'publish')
|
|
141
|
+
throw new Error('network error');
|
|
142
|
+
return origRun(cmd, args, o);
|
|
143
|
+
};
|
|
144
|
+
expect(() => (0, orchestrator_1.release)(opts(), io)).toThrow(/network error/i);
|
|
145
|
+
const joined = calls.join('\n');
|
|
146
|
+
expect(joined).toMatch(/git tag -d v2.2.0/);
|
|
147
|
+
expect(joined).toMatch(/git reset --hard HEAD~1/);
|
|
148
|
+
expect(calls).toContain('REMOVE_NPMRC'); // .npmrc siempre se limpia
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('release — gates de idempotencia', () => {
|
|
152
|
+
it('aborta si el tag v2.2.0 ya existe en git', () => {
|
|
153
|
+
const { io } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
154
|
+
const origRun = io.run;
|
|
155
|
+
io.run = (cmd, args, o) => {
|
|
156
|
+
if (cmd === 'git' && args[0] === 'tag' && args[1] === '--list' && args[2] === 'v2.2.0')
|
|
157
|
+
return 'v2.2.0';
|
|
158
|
+
return origRun(cmd, args, o);
|
|
159
|
+
};
|
|
160
|
+
expect(() => (0, orchestrator_1.release)(opts(), io)).toThrow(/ya existe|v2\.2\.0/i);
|
|
161
|
+
});
|
|
162
|
+
it('aborta si agentic-workflow-manager@2.2.0 ya está publicado en npm', () => {
|
|
163
|
+
const { io } = makeIO({ commits: `feat: x${core_1.US}${core_1.RS}` });
|
|
164
|
+
const origRun = io.run;
|
|
165
|
+
io.run = (cmd, args, o) => {
|
|
166
|
+
if (cmd === 'npm' && args[0] === 'view' && args[1] === 'agentic-workflow-manager@2.2.0')
|
|
167
|
+
return '2.2.0';
|
|
168
|
+
return origRun(cmd, args, o);
|
|
169
|
+
};
|
|
170
|
+
expect(() => (0, orchestrator_1.release)(opts(), io)).toThrow(/ya está publicado|2\.2\.0/i);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
const index_1 = require("../../src/release/index");
|
|
174
|
+
describe('parseArgs', () => {
|
|
175
|
+
it('defaults', () => {
|
|
176
|
+
expect((0, index_1.parseArgs)([])).toMatchObject({ dryRun: false, force: null, push: true, branch: 'main' });
|
|
177
|
+
});
|
|
178
|
+
it('--dry-run, --no-push, --force minor, --branch release', () => {
|
|
179
|
+
expect((0, index_1.parseArgs)(['--dry-run', '--no-push', '--force', 'minor', '--branch', 'release']))
|
|
180
|
+
.toMatchObject({ dryRun: true, push: false, force: 'minor', branch: 'release' });
|
|
181
|
+
});
|
|
182
|
+
it('rechaza --force con nivel inválido', () => {
|
|
183
|
+
expect(() => (0, index_1.parseArgs)(['--force', 'huge'])).toThrow(/force/i);
|
|
184
|
+
});
|
|
185
|
+
it('--branch sin valor lanza error', () => {
|
|
186
|
+
expect(() => (0, index_1.parseArgs)(['--branch'])).toThrow(/--branch requiere/i);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -18,9 +18,14 @@ describe('Preferences Manager', () => {
|
|
|
18
18
|
delete process.env.AWM_HOME;
|
|
19
19
|
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
20
20
|
});
|
|
21
|
-
it('creates default preferences if none exist', () => {
|
|
21
|
+
it('creates default preferences if none exist (default agent is claude-code)', () => {
|
|
22
22
|
const prefs = (0, config_1.getPreferences)();
|
|
23
|
-
expect(prefs.defaultAgent).toBe('
|
|
23
|
+
expect(prefs.defaultAgent).toBe('claude-code');
|
|
24
|
+
});
|
|
25
|
+
it('preferencesExist reflects whether the file is on disk', () => {
|
|
26
|
+
expect((0, config_1.preferencesExist)()).toBe(false);
|
|
27
|
+
(0, config_1.getPreferences)(); // side-effect: persists defaults
|
|
28
|
+
expect((0, config_1.preferencesExist)()).toBe(true);
|
|
24
29
|
});
|
|
25
30
|
it('saves and loads preferences correctly', () => {
|
|
26
31
|
(0, config_1.savePreferences)({ defaultAgent: 'opencode', installMethod: 'copy', defaultScope: 'local' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"build": "tsc && chmod +x dist/src/index.js",
|
|
21
21
|
"test": "jest --runInBand",
|
|
22
22
|
"start": "ts-node src/index.ts",
|
|
23
|
+
"prerelease": "npm run build",
|
|
24
|
+
"release": "node dist/src/release/index.js",
|
|
23
25
|
"prepack": "npm run build",
|
|
24
26
|
"prepublishOnly": "npm run build"
|
|
25
27
|
},
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
// cli/tests/core/registry-versioned-sync.test.ts
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const os_1 = __importDefault(require("os"));
|
|
10
|
-
const child_process_1 = require("child_process");
|
|
11
|
-
const GIT = (cwd, cmd) => (0, child_process_1.execSync)(`git -c user.email=t@t.t -c user.name=t -c tag.gpgSign=false ${cmd}`, { cwd, stdio: 'pipe' });
|
|
12
|
-
function makeTaggedRepo(base, name, versions) {
|
|
13
|
-
const dir = path_1.default.join(base, name);
|
|
14
|
-
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
15
|
-
GIT(dir, 'init -q -b main');
|
|
16
|
-
fs_1.default.writeFileSync(path_1.default.join(dir, 'VERSION'), 'init');
|
|
17
|
-
GIT(dir, 'add -A');
|
|
18
|
-
GIT(dir, 'commit -qm init');
|
|
19
|
-
for (const v of versions) {
|
|
20
|
-
fs_1.default.writeFileSync(path_1.default.join(dir, 'VERSION'), v);
|
|
21
|
-
GIT(dir, 'add -A');
|
|
22
|
-
GIT(dir, `commit -qm ${v}`);
|
|
23
|
-
GIT(dir, `tag v${v}`);
|
|
24
|
-
}
|
|
25
|
-
return dir;
|
|
26
|
-
}
|
|
27
|
-
function addRelease(source, version) {
|
|
28
|
-
fs_1.default.writeFileSync(path_1.default.join(source, 'VERSION'), version);
|
|
29
|
-
GIT(source, 'add -A');
|
|
30
|
-
GIT(source, `commit -qm ${version}`);
|
|
31
|
-
GIT(source, `tag v${version}`);
|
|
32
|
-
}
|
|
33
|
-
describe('syncRegistry versionado (fixtures git locales)', () => {
|
|
34
|
-
let tmpHome;
|
|
35
|
-
let tmpWork;
|
|
36
|
-
let originalHome;
|
|
37
|
-
let originalAwmHome;
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-regver-home-'));
|
|
40
|
-
tmpWork = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-regver-work-'));
|
|
41
|
-
originalHome = process.env.HOME;
|
|
42
|
-
originalAwmHome = process.env.AWM_HOME;
|
|
43
|
-
process.env.HOME = tmpHome;
|
|
44
|
-
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
45
|
-
jest.resetModules();
|
|
46
|
-
});
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
49
|
-
fs_1.default.rmSync(tmpWork, { recursive: true, force: true });
|
|
50
|
-
if (originalHome === undefined)
|
|
51
|
-
delete process.env.HOME;
|
|
52
|
-
else
|
|
53
|
-
process.env.HOME = originalHome;
|
|
54
|
-
if (originalAwmHome === undefined)
|
|
55
|
-
delete process.env.AWM_HOME;
|
|
56
|
-
else
|
|
57
|
-
process.env.AWM_HOME = originalAwmHome;
|
|
58
|
-
});
|
|
59
|
-
const registryVersionFile = () => path_1.default.join(process.env.AWM_HOME, 'cli-source/VERSION');
|
|
60
|
-
it('clone fresco queda checkouteado en el último tag (no en HEAD)', async () => {
|
|
61
|
-
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
62
|
-
// commit post-tag: HEAD del remote va más allá del último release
|
|
63
|
-
fs_1.default.writeFileSync(path_1.default.join(source, 'VERSION'), 'unreleased');
|
|
64
|
-
GIT(source, 'add -A');
|
|
65
|
-
GIT(source, 'commit -qm unreleased');
|
|
66
|
-
const { syncRegistry } = require('../../src/core/registry');
|
|
67
|
-
const resolved = await syncRegistry(source, { channel: 'stable' });
|
|
68
|
-
expect(resolved).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
|
|
69
|
-
expect(fs_1.default.readFileSync(registryVersionFile(), 'utf-8')).toBe('1.0.0');
|
|
70
|
-
});
|
|
71
|
-
it('clone existente transiciona al tag nuevo tras un release en el remote', async () => {
|
|
72
|
-
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
73
|
-
const { syncRegistry } = require('../../src/core/registry');
|
|
74
|
-
await syncRegistry(source, { channel: 'stable' });
|
|
75
|
-
addRelease(source, '1.1.0');
|
|
76
|
-
const resolved = await syncRegistry(source, { channel: 'stable' });
|
|
77
|
-
expect(resolved).toEqual({ kind: 'tag', ref: 'v1.1.0', version: '1.1.0' });
|
|
78
|
-
expect(fs_1.default.readFileSync(registryVersionFile(), 'utf-8')).toBe('1.1.0');
|
|
79
|
-
});
|
|
80
|
-
it('rollback: pin a un tag anterior vuelve el contenido a esa versión', async () => {
|
|
81
|
-
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0', '1.1.0']);
|
|
82
|
-
const { syncRegistry } = require('../../src/core/registry');
|
|
83
|
-
await syncRegistry(source, { channel: 'stable' });
|
|
84
|
-
expect(fs_1.default.readFileSync(registryVersionFile(), 'utf-8')).toBe('1.1.0');
|
|
85
|
-
const resolved = await syncRegistry(source, { pin: '1.0.0', channel: 'stable' });
|
|
86
|
-
expect(resolved).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
|
|
87
|
-
expect(fs_1.default.readFileSync(registryVersionFile(), 'utf-8')).toBe('1.0.0');
|
|
88
|
-
});
|
|
89
|
-
it('canal dev sigue HEAD del branch y recibe commits nuevos', async () => {
|
|
90
|
-
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
91
|
-
const { syncRegistry } = require('../../src/core/registry');
|
|
92
|
-
const first = await syncRegistry(source, { channel: 'dev' });
|
|
93
|
-
expect(first).toEqual({ kind: 'head', ref: 'main' });
|
|
94
|
-
fs_1.default.writeFileSync(path_1.default.join(source, 'VERSION'), 'head-2');
|
|
95
|
-
GIT(source, 'add -A');
|
|
96
|
-
GIT(source, 'commit -qm head-2');
|
|
97
|
-
await syncRegistry(source, { channel: 'dev' });
|
|
98
|
-
expect(fs_1.default.readFileSync(registryVersionFile(), 'utf-8')).toBe('head-2');
|
|
99
|
-
});
|
|
100
|
-
it('repo sin tags en canal stable → head-fallback y sigue HEAD', async () => {
|
|
101
|
-
const source = makeTaggedRepo(tmpWork, 'src', []);
|
|
102
|
-
const { syncRegistry } = require('../../src/core/registry');
|
|
103
|
-
const resolved = await syncRegistry(source, { channel: 'stable' });
|
|
104
|
-
expect(resolved).toEqual({ kind: 'head-fallback', ref: 'main' });
|
|
105
|
-
expect(fs_1.default.readFileSync(registryVersionFile(), 'utf-8')).toBe('init');
|
|
106
|
-
});
|
|
107
|
-
it('sin opts (callers legacy) → comportamiento stable por default', async () => {
|
|
108
|
-
const source = makeTaggedRepo(tmpWork, 'src', ['2.0.0']);
|
|
109
|
-
const { syncRegistry } = require('../../src/core/registry');
|
|
110
|
-
const resolved = await syncRegistry(source);
|
|
111
|
-
expect(resolved).toEqual({ kind: 'tag', ref: 'v2.0.0', version: '2.0.0' });
|
|
112
|
-
});
|
|
113
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const child_process_1 = require("child_process");
|
|
4
|
-
jest.mock('child_process');
|
|
5
|
-
const mockSpawnSync = child_process_1.spawnSync;
|
|
6
|
-
describe('buildCli', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
jest.clearAllMocks();
|
|
9
|
-
});
|
|
10
|
-
it('returns success when npm run build exits 0', () => {
|
|
11
|
-
mockSpawnSync.mockReturnValue({ status: 0, stderr: Buffer.from(''), stdout: Buffer.from(''), pid: 1, output: [], signal: null });
|
|
12
|
-
const { buildCli } = require('../../src/core/registry');
|
|
13
|
-
const result = buildCli('/fake/cli');
|
|
14
|
-
expect(result).toEqual({ success: true });
|
|
15
|
-
expect(mockSpawnSync).toHaveBeenCalledWith('npm', ['run', 'build'], expect.objectContaining({ cwd: '/fake/cli', shell: true }));
|
|
16
|
-
});
|
|
17
|
-
it('returns failure with error message when build exits non-zero', () => {
|
|
18
|
-
mockSpawnSync.mockReturnValue({ status: 1, stderr: Buffer.from('tsc error: Type mismatch'), stdout: Buffer.from(''), pid: 1, output: [], signal: null });
|
|
19
|
-
const { buildCli } = require('../../src/core/registry');
|
|
20
|
-
const result = buildCli('/fake/cli');
|
|
21
|
-
expect(result.success).toBe(false);
|
|
22
|
-
expect(result.error).toContain('tsc error');
|
|
23
|
-
});
|
|
24
|
-
it('returns failure with stdout message when tsc writes errors to stdout', () => {
|
|
25
|
-
mockSpawnSync.mockReturnValue({ status: 2, stderr: Buffer.from(''), stdout: Buffer.from('error TS2322: Type mismatch'), pid: 1, output: [], signal: null });
|
|
26
|
-
const { buildCli } = require('../../src/core/registry');
|
|
27
|
-
const result = buildCli('/fake/cli');
|
|
28
|
-
expect(result.success).toBe(false);
|
|
29
|
-
expect(result.error).toContain('TS2322');
|
|
30
|
-
});
|
|
31
|
-
it('returns failure when spawnSync throws unexpectedly', () => {
|
|
32
|
-
mockSpawnSync.mockImplementation(() => { throw new Error('unexpected error'); });
|
|
33
|
-
const { buildCli } = require('../../src/core/registry');
|
|
34
|
-
const result = buildCli('/fake/cli');
|
|
35
|
-
expect(result.success).toBe(false);
|
|
36
|
-
expect(result.error).toBe('unexpected error');
|
|
37
|
-
});
|
|
38
|
-
it('returns failure when npm is not found (shell returns status 127)', () => {
|
|
39
|
-
mockSpawnSync.mockReturnValue({ status: 127, stderr: Buffer.from('/bin/sh: npm: not found'), stdout: Buffer.from(''), pid: 1, output: [], signal: null });
|
|
40
|
-
const { buildCli } = require('../../src/core/registry');
|
|
41
|
-
const result = buildCli('/fake/cli');
|
|
42
|
-
expect(result.success).toBe(false);
|
|
43
|
-
expect(result.error).toContain('not found');
|
|
44
|
-
});
|
|
45
|
-
it('uses REGISTRY_DIR/cli as default cwd', () => {
|
|
46
|
-
mockSpawnSync.mockReturnValue({ status: 0, stderr: Buffer.from(''), stdout: Buffer.from(''), pid: 1, output: [], signal: null });
|
|
47
|
-
const { buildCli, REGISTRY_DIR } = require('../../src/core/registry');
|
|
48
|
-
buildCli();
|
|
49
|
-
expect(mockSpawnSync).toHaveBeenCalledWith('npm', ['run', 'build'], expect.objectContaining({ cwd: `${REGISTRY_DIR}/cli` }));
|
|
50
|
-
});
|
|
51
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const fs_1 = __importDefault(require("fs"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const REG = path_1.default.join(__dirname, '../../../registry/skills');
|
|
9
|
-
const read = (p) => fs_1.default.readFileSync(path_1.default.join(REG, p), 'utf-8');
|
|
10
|
-
describe('B-3 harness-retro is ledger-driven', () => {
|
|
11
|
-
const skill = read('harness-retro/SKILL.md');
|
|
12
|
-
test('reads the ledger via awm ledger list + recurring', () => {
|
|
13
|
-
expect(skill).toMatch(/awm ledger list/);
|
|
14
|
-
expect(skill).toMatch(/awm ledger recurring/);
|
|
15
|
-
});
|
|
16
|
-
test('archives the ledger when done', () => {
|
|
17
|
-
expect(skill).toMatch(/awm ledger archive/);
|
|
18
|
-
});
|
|
19
|
-
test('no longer relies on the human "where did this fail before?" memory step', () => {
|
|
20
|
-
expect(skill).not.toMatch(/Where did this pattern fail before\?/);
|
|
21
|
-
});
|
|
22
|
-
test('cures into AGENTS.md (agnostic) for agent-style lessons + wins, not CLAUDE.md', () => {
|
|
23
|
-
expect(skill).toMatch(/AGENTS\.md/);
|
|
24
|
-
});
|
|
25
|
-
test('writes the awm-retro-complete marker', () => {
|
|
26
|
-
expect(skill).toMatch(/awm-retro-complete/);
|
|
27
|
-
});
|
|
28
|
-
test('treats recurrence as a signal, not a hard >=2 gate (interactive decision)', () => {
|
|
29
|
-
expect(skill).toMatch(/se(ñ|n)al|signal/i);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
describe('B-3 capture wiring — phases append to the ledger', () => {
|
|
33
|
-
test('SDD spec reviewer emits findings AND wins to the ledger', () => {
|
|
34
|
-
const p = read('subagent-driven-development/spec-reviewer-prompt.md');
|
|
35
|
-
expect(p).toMatch(/awm ledger add/);
|
|
36
|
-
expect(p).toMatch(/--polarity finding/);
|
|
37
|
-
expect(p).toMatch(/--polarity win/);
|
|
38
|
-
});
|
|
39
|
-
test('SDD code-quality reviewer emits findings AND wins to the ledger', () => {
|
|
40
|
-
const p = read('subagent-driven-development/code-quality-reviewer-prompt.md');
|
|
41
|
-
expect(p).toMatch(/awm ledger add/);
|
|
42
|
-
expect(p).toMatch(/--polarity win/);
|
|
43
|
-
});
|
|
44
|
-
test('post-qa deep-review emits findings AND wins to the ledger', () => {
|
|
45
|
-
const p = read('post-implementation-qa/deep-review-prompt.md');
|
|
46
|
-
expect(p).toMatch(/awm ledger add/);
|
|
47
|
-
expect(p).toMatch(/--polarity win/);
|
|
48
|
-
});
|
|
49
|
-
test('verification-before-completion logs recurring sensor failures', () => {
|
|
50
|
-
const p = read('verification-before-completion/SKILL.md');
|
|
51
|
-
expect(p).toMatch(/awm ledger add/);
|
|
52
|
-
});
|
|
53
|
-
test('systematic-debugging logs the confirmed root cause', () => {
|
|
54
|
-
const p = read('systematic-debugging/SKILL.md');
|
|
55
|
-
expect(p).toMatch(/awm ledger add/);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
describe('B-3 development-process routes harness-retro as a terminal phase', () => {
|
|
59
|
-
const skill = read('development-process/SKILL.md');
|
|
60
|
-
test('harness-retro appears as a pipeline phase between QA and finishing', () => {
|
|
61
|
-
const qaIdx = skill.indexOf('post-implementation-qa');
|
|
62
|
-
const retroIdx = skill.indexOf('harness-retro');
|
|
63
|
-
const finishIdx = skill.indexOf('finishing-a-development-branch');
|
|
64
|
-
expect(retroIdx).toBeGreaterThan(-1);
|
|
65
|
-
expect(qaIdx).toBeLessThan(retroIdx);
|
|
66
|
-
expect(retroIdx).toBeLessThan(finishIdx);
|
|
67
|
-
// Also assert the routing edge exists (not just node declarations)
|
|
68
|
-
expect(skill).toMatch(/post-implementation-qa.*harness-retro|harness-retro.*post-implementation-qa/s);
|
|
69
|
-
expect(skill).toMatch(/harness-retro.*finishing-a-development-branch|finishing-a-development-branch.*harness-retro/s);
|
|
70
|
-
});
|
|
71
|
-
test('routing keys on the awm-retro-complete marker', () => {
|
|
72
|
-
expect(skill).toMatch(/awm-retro-complete/);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const fs_1 = __importDefault(require("fs"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const CONTENT = path_1.default.join(__dirname, '../../../registry');
|
|
9
|
-
function readJson(p) { return JSON.parse(fs_1.default.readFileSync(p, 'utf-8')); }
|
|
10
|
-
describe('catalog/bundle consistency', () => {
|
|
11
|
-
const catalog = readJson(path_1.default.join(CONTENT, 'catalog.json'));
|
|
12
|
-
it('declares exactly the 4 bundles', () => {
|
|
13
|
-
expect(catalog.bundles.map((b) => b.name).sort())
|
|
14
|
-
.toEqual(['authoring', 'dev', 'docs', 'frontend']);
|
|
15
|
-
});
|
|
16
|
-
it('every catalog entry has a matching bundle.json whose mirrored fields agree', () => {
|
|
17
|
-
for (const entry of catalog.bundles) {
|
|
18
|
-
const manifest = readJson(path_1.default.join(CONTENT, entry.source, 'bundle.json'));
|
|
19
|
-
expect(manifest.name).toBe(entry.name);
|
|
20
|
-
expect(manifest.scope).toBe(entry.scope);
|
|
21
|
-
expect(manifest.version).toBe(entry.version);
|
|
22
|
-
expect(manifest.visibility ?? 'public').toBe(entry.visibility ?? 'public');
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
it('every referenced skill exists in registry/skills', () => {
|
|
26
|
-
for (const entry of catalog.bundles) {
|
|
27
|
-
const manifest = readJson(path_1.default.join(CONTENT, entry.source, 'bundle.json'));
|
|
28
|
-
for (const s of manifest.skills) {
|
|
29
|
-
const name = typeof s === 'string' ? s : s.name;
|
|
30
|
-
expect(fs_1.default.existsSync(path_1.default.join(CONTENT, 'skills', name, 'SKILL.md'))).toBe(true);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
it('bundle skills partition the 41 skills with no overlap', () => {
|
|
35
|
-
const all = [];
|
|
36
|
-
for (const entry of catalog.bundles) {
|
|
37
|
-
const manifest = readJson(path_1.default.join(CONTENT, entry.source, 'bundle.json'));
|
|
38
|
-
for (const s of manifest.skills)
|
|
39
|
-
all.push(typeof s === 'string' ? s : s.name);
|
|
40
|
-
}
|
|
41
|
-
expect(all.length).toBe(41);
|
|
42
|
-
expect(new Set(all).size).toBe(41);
|
|
43
|
-
});
|
|
44
|
-
it('processes.json has been removed', () => {
|
|
45
|
-
expect(fs_1.default.existsSync(path_1.default.join(CONTENT, 'processes.json'))).toBe(false);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const fs_1 = __importDefault(require("fs"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const REGISTRY = path_1.default.join(__dirname, '..', '..', '..', 'registry');
|
|
9
|
-
const SKILLS = path_1.default.join(REGISTRY, 'skills');
|
|
10
|
-
const LOCK_FILE = path_1.default.join(__dirname, '..', '..', '..', 'skills-lock.json');
|
|
11
|
-
function frontmatter(skill) {
|
|
12
|
-
const content = fs_1.default.readFileSync(path_1.default.join(SKILLS, skill, 'SKILL.md'), 'utf-8');
|
|
13
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
14
|
-
expect(match).not.toBeNull();
|
|
15
|
-
return match[1];
|
|
16
|
-
}
|
|
17
|
-
describe('frontend-craft skill', () => {
|
|
18
|
-
it('exists with valid frontmatter', () => {
|
|
19
|
-
const fm = frontmatter('frontend-craft');
|
|
20
|
-
expect(fm).toMatch(/^name:\s*frontend-craft\s*$/m);
|
|
21
|
-
expect(fm).toMatch(/^description:\s*.+$/m);
|
|
22
|
-
});
|
|
23
|
-
it('bundles emil and taste as internal references', () => {
|
|
24
|
-
const ref = path_1.default.join(SKILLS, 'frontend-craft', 'reference');
|
|
25
|
-
expect(fs_1.default.existsSync(path_1.default.join(ref, 'emil-design-eng.md'))).toBe(true);
|
|
26
|
-
expect(fs_1.default.existsSync(path_1.default.join(ref, 'design-taste-frontend.md'))).toBe(true);
|
|
27
|
-
});
|
|
28
|
-
it('SKILL.md points to its reference files', () => {
|
|
29
|
-
const content = fs_1.default.readFileSync(path_1.default.join(SKILLS, 'frontend-craft', 'SKILL.md'), 'utf-8');
|
|
30
|
-
expect(content).toMatch(/reference\/emil-design-eng\.md/);
|
|
31
|
-
expect(content).toMatch(/reference\/design-taste-frontend\.md/);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe('impeccable skill (non-live scope)', () => {
|
|
35
|
-
const base = path_1.default.join(SKILLS, 'impeccable');
|
|
36
|
-
it('exists with valid frontmatter', () => {
|
|
37
|
-
expect(fs_1.default.existsSync(path_1.default.join(base, 'SKILL.md'))).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
it('has no literal .agents/skills/impeccable paths in markdown', () => {
|
|
40
|
-
const mdFiles = [
|
|
41
|
-
path_1.default.join(base, 'SKILL.md'),
|
|
42
|
-
...fs_1.default.readdirSync(path_1.default.join(base, 'reference')).map((f) => path_1.default.join(base, 'reference', f)),
|
|
43
|
-
];
|
|
44
|
-
for (const f of mdFiles) {
|
|
45
|
-
const content = fs_1.default.readFileSync(f, 'utf-8');
|
|
46
|
-
expect(content).not.toMatch(/\.agents\/skills\/impeccable/);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
it('dropped the live/Codex layer', () => {
|
|
50
|
-
expect(fs_1.default.existsSync(path_1.default.join(base, 'agents'))).toBe(false);
|
|
51
|
-
expect(fs_1.default.existsSync(path_1.default.join(base, 'reference', 'live.md'))).toBe(false);
|
|
52
|
-
expect(fs_1.default.existsSync(path_1.default.join(base, 'reference', 'codex.md'))).toBe(false);
|
|
53
|
-
const liveScripts = fs_1.default.readdirSync(path_1.default.join(base, 'scripts')).filter((f) => /^live-/.test(f) || f === 'modern-screenshot.umd.js');
|
|
54
|
-
expect(liveScripts).toEqual([]);
|
|
55
|
-
});
|
|
56
|
-
it('kept the static detector and non-live support scripts', () => {
|
|
57
|
-
const scripts = path_1.default.join(base, 'scripts');
|
|
58
|
-
for (const keep of ['detect.mjs', 'context.mjs', 'critique-storage.mjs', 'impeccable-paths.mjs']) {
|
|
59
|
-
expect(fs_1.default.existsSync(path_1.default.join(scripts, keep))).toBe(true);
|
|
60
|
-
}
|
|
61
|
-
expect(fs_1.default.existsSync(path_1.default.join(scripts, 'detector'))).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
it('removed the live row from the commands table', () => {
|
|
64
|
-
const content = fs_1.default.readFileSync(path_1.default.join(base, 'SKILL.md'), 'utf-8');
|
|
65
|
-
expect(content).not.toMatch(/\|\s*`live`\s*\|/);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
describe('google stitch skills', () => {
|
|
69
|
-
for (const s of ['extract-design-md', 'code-to-design', 'react-components']) {
|
|
70
|
-
it(`${s} exists with SKILL.md`, () => {
|
|
71
|
-
expect(fs_1.default.existsSync(path_1.default.join(SKILLS, s, 'SKILL.md'))).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
describe('catalog/bundles (replaces processes.json)', () => {
|
|
76
|
-
const catalog = JSON.parse(fs_1.default.readFileSync(path_1.default.join(REGISTRY, 'catalog.json'), 'utf-8'));
|
|
77
|
-
function readBundle(source) {
|
|
78
|
-
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(REGISTRY, source, 'bundle.json'), 'utf-8'));
|
|
79
|
-
}
|
|
80
|
-
function skillNames(bundle) {
|
|
81
|
-
return bundle.skills.map((s) => (typeof s === 'string' ? s : s.name));
|
|
82
|
-
}
|
|
83
|
-
it('frontend bundle includes the heavy design skills', () => {
|
|
84
|
-
const entry = catalog.bundles.find((b) => b.name === 'frontend');
|
|
85
|
-
expect(entry).toBeDefined();
|
|
86
|
-
const bundle = readBundle(entry.source);
|
|
87
|
-
const names = skillNames(bundle);
|
|
88
|
-
for (const s of ['impeccable', 'ui-design', 'extract-design-md', 'code-to-design', 'react-components']) {
|
|
89
|
-
expect(names).toContain(s);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
it('frontend bundle includes frontend-craft', () => {
|
|
93
|
-
const entry = catalog.bundles.find((b) => b.name === 'frontend');
|
|
94
|
-
expect(entry).toBeDefined();
|
|
95
|
-
const bundle = readBundle(entry.source);
|
|
96
|
-
expect(skillNames(bundle)).toContain('frontend-craft');
|
|
97
|
-
});
|
|
98
|
-
it('every skill referenced by any bundle exists on disk', () => {
|
|
99
|
-
for (const entry of catalog.bundles) {
|
|
100
|
-
const bundle = readBundle(entry.source);
|
|
101
|
-
for (const s of skillNames(bundle)) {
|
|
102
|
-
expect(fs_1.default.existsSync(path_1.default.join(SKILLS, s, 'SKILL.md'))).toBe(true);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
describe('skills-lock.json', () => {
|
|
108
|
-
const lock = JSON.parse(fs_1.default.readFileSync(LOCK_FILE, 'utf-8'));
|
|
109
|
-
it('records provenance for the new external skills', () => {
|
|
110
|
-
for (const s of ['emil-design-eng', 'design-taste-frontend', 'impeccable', 'extract-design-md', 'code-to-design', 'react-components']) {
|
|
111
|
-
expect(lock.skills[s]).toBeDefined();
|
|
112
|
-
expect(lock.skills[s].source).toMatch(/.+\/.+/);
|
|
113
|
-
expect(lock.skills[s].sourceType).toBe('github');
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
describe('post-implementation-qa skill', () => {
|
|
118
|
-
const base = path_1.default.join(SKILLS, 'post-implementation-qa');
|
|
119
|
-
it('exists with valid frontmatter', () => {
|
|
120
|
-
const content = fs_1.default.readFileSync(path_1.default.join(base, 'SKILL.md'), 'utf-8');
|
|
121
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
122
|
-
expect(match).not.toBeNull();
|
|
123
|
-
const fm = match[1];
|
|
124
|
-
expect(fm).toMatch(/^name:\s*post-implementation-qa\s*$/m);
|
|
125
|
-
expect(fm).toMatch(/^description:\s*.+$/m);
|
|
126
|
-
});
|
|
127
|
-
it('includes deep-review-prompt.md', () => {
|
|
128
|
-
expect(fs_1.default.existsSync(path_1.default.join(base, 'deep-review-prompt.md'))).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
it('SKILL.md contains the awm-qa-complete marker instruction', () => {
|
|
131
|
-
const content = fs_1.default.readFileSync(path_1.default.join(base, 'SKILL.md'), 'utf-8');
|
|
132
|
-
expect(content).toMatch(/awm-qa-complete/);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe('development-process QA integration', () => {
|
|
136
|
-
it('SKILL.md references post-implementation-qa at least 6 times', () => {
|
|
137
|
-
const content = fs_1.default.readFileSync(path_1.default.join(SKILLS, 'development-process', 'SKILL.md'), 'utf-8');
|
|
138
|
-
const matches = content.match(/post-implementation-qa/g) || [];
|
|
139
|
-
expect(matches.length).toBeGreaterThanOrEqual(6);
|
|
140
|
-
});
|
|
141
|
-
it('SKILL.md references awm-qa-complete at least 2 times', () => {
|
|
142
|
-
const content = fs_1.default.readFileSync(path_1.default.join(SKILLS, 'development-process', 'SKILL.md'), 'utf-8');
|
|
143
|
-
const matches = content.match(/awm-qa-complete/g) || [];
|
|
144
|
-
expect(matches.length).toBeGreaterThanOrEqual(2);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const fs_1 = __importDefault(require("fs"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const REPO_ROOT = path_1.default.resolve(__dirname, '../../..');
|
|
9
|
-
describe('skill prose stays agent-agnostic (#5)', () => {
|
|
10
|
-
const files = ['writing-skills/SKILL.md', 'project-constitution/SKILL.md'];
|
|
11
|
-
for (const f of files) {
|
|
12
|
-
it(`${f} does not push the model to the ~/.claude/skills path`, () => {
|
|
13
|
-
const txt = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, 'registry/skills', f), 'utf-8');
|
|
14
|
-
expect(txt).not.toMatch(/~\/\.claude\/skills/);
|
|
15
|
-
expect(txt).not.toMatch(/\.claude\/settings\.json/);
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const fs_1 = __importDefault(require("fs"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const REGISTRY_ROOT = path_1.default.join(__dirname, '..', '..', '..', 'registry');
|
|
9
|
-
const PACKS_DIR = path_1.default.join(REGISTRY_ROOT, 'sensor-packs');
|
|
10
|
-
describe('sensor-packs registry', () => {
|
|
11
|
-
it('sensor-packs directory exists in registry', () => {
|
|
12
|
-
expect(fs_1.default.existsSync(PACKS_DIR)).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
for (const packName of ['js-ts', 'generic']) {
|
|
15
|
-
describe(`pack: ${packName}`, () => {
|
|
16
|
-
const packDir = path_1.default.join(PACKS_DIR, packName);
|
|
17
|
-
it('directory exists', () => {
|
|
18
|
-
expect(fs_1.default.existsSync(packDir)).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
it('has valid pack.json', () => {
|
|
21
|
-
const packJson = path_1.default.join(packDir, 'pack.json');
|
|
22
|
-
expect(fs_1.default.existsSync(packJson)).toBe(true);
|
|
23
|
-
const parsed = JSON.parse(fs_1.default.readFileSync(packJson, 'utf-8'));
|
|
24
|
-
expect(parsed.name).toBe(packName);
|
|
25
|
-
expect(typeof parsed.description).toBe('string');
|
|
26
|
-
expect(typeof parsed.sensors).toBe('object');
|
|
27
|
-
});
|
|
28
|
-
it('pack.json name matches directory name', () => {
|
|
29
|
-
const parsed = JSON.parse(fs_1.default.readFileSync(path_1.default.join(packDir, 'pack.json'), 'utf-8'));
|
|
30
|
-
expect(parsed.name).toBe(packName);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
it('js-ts pack has required sensor config files', () => {
|
|
35
|
-
const jstsDir = path_1.default.join(PACKS_DIR, 'js-ts');
|
|
36
|
-
expect(fs_1.default.existsSync(path_1.default.join(jstsDir, 'tsconfig.awm.json'))).toBe(true);
|
|
37
|
-
expect(fs_1.default.existsSync(path_1.default.join(jstsDir, 'eslint.config.awm.mjs'))).toBe(true);
|
|
38
|
-
expect(fs_1.default.existsSync(path_1.default.join(jstsDir, '.semgrep.awm.yml'))).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
it('tsconfig.awm.json extends ./tsconfig.json', () => {
|
|
41
|
-
const tsconfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(PACKS_DIR, 'js-ts', 'tsconfig.awm.json'), 'utf-8'));
|
|
42
|
-
expect(tsconfig.extends).toBe('./tsconfig.json');
|
|
43
|
-
expect(tsconfig.compilerOptions?.strict).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const fs_1 = __importDefault(require("fs"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const SKILLS_DIR = path_1.default.join(__dirname, '../../../registry/skills');
|
|
9
|
-
function frontmatter(file) {
|
|
10
|
-
const raw = fs_1.default.readFileSync(file, 'utf-8');
|
|
11
|
-
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
12
|
-
return m ? m[1] : '';
|
|
13
|
-
}
|
|
14
|
-
describe('skill frontmatter version', () => {
|
|
15
|
-
const dirs = fs_1.default.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
16
|
-
.filter((e) => e.isDirectory())
|
|
17
|
-
.filter((e) => fs_1.default.existsSync(path_1.default.join(SKILLS_DIR, e.name, 'SKILL.md')))
|
|
18
|
-
.map((e) => e.name);
|
|
19
|
-
it('finds the 41 skills', () => {
|
|
20
|
-
expect(dirs.length).toBe(41);
|
|
21
|
-
});
|
|
22
|
-
it.each(dirs)('skill "%s" declares a semver version', (name) => {
|
|
23
|
-
const fm = frontmatter(path_1.default.join(SKILLS_DIR, name, 'SKILL.md'));
|
|
24
|
-
expect(fm).toMatch(/^version:\s*["']?\d+\.\d+\.\d+["']?\s*$/m);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const fs_1 = __importDefault(require("fs"));
|
|
7
|
-
const path_1 = __importDefault(require("path"));
|
|
8
|
-
describe('using-awm skill', () => {
|
|
9
|
-
const skillPath = path_1.default.join(__dirname, '../../../registry/skills/using-awm/SKILL.md');
|
|
10
|
-
it('exists at the expected path', () => {
|
|
11
|
-
expect(fs_1.default.existsSync(skillPath)).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
it('has a valid frontmatter with required fields', () => {
|
|
14
|
-
const content = fs_1.default.readFileSync(skillPath, 'utf-8');
|
|
15
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
16
|
-
expect(match).not.toBeNull();
|
|
17
|
-
const frontmatter = match[1];
|
|
18
|
-
expect(frontmatter).toMatch(/^name:\s*using-awm\s*$/m);
|
|
19
|
-
expect(frontmatter).toMatch(/^description:\s*.+$/m);
|
|
20
|
-
});
|
|
21
|
-
it('does NOT contain a model: field (aligned with canon)', () => {
|
|
22
|
-
const content = fs_1.default.readFileSync(skillPath, 'utf-8');
|
|
23
|
-
expect(content).not.toMatch(/^model:\s*/m);
|
|
24
|
-
});
|
|
25
|
-
it('uses tiered triggering (no blanket 1% mandate)', () => {
|
|
26
|
-
const content = fs_1.default.readFileSync(skillPath, 'utf-8');
|
|
27
|
-
expect(content).not.toMatch(/1%/);
|
|
28
|
-
expect(content).toMatch(/always|siempre/i);
|
|
29
|
-
expect(content).toMatch(/signal|señal/i);
|
|
30
|
-
});
|
|
31
|
-
it('contains SUBAGENT-STOP block (prevents recursion)', () => {
|
|
32
|
-
const content = fs_1.default.readFileSync(skillPath, 'utf-8');
|
|
33
|
-
expect(content).toMatch(/<SUBAGENT-STOP>/);
|
|
34
|
-
});
|
|
35
|
-
it('points to development-process as default orchestrator', () => {
|
|
36
|
-
const content = fs_1.default.readFileSync(skillPath, 'utf-8');
|
|
37
|
-
expect(content).toMatch(/development-process/);
|
|
38
|
-
});
|
|
39
|
-
});
|