agentic-workflow-manager 9.2.0 → 9.3.0
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/preflight/checks.js +48 -3
- package/dist/src/commands/preflight/index.js +4 -5
- package/dist/src/commands/update.js +24 -2
- package/dist/src/core/context-kernel/inspect.js +221 -0
- package/dist/src/core/context-kernel/types.js +2 -0
- package/dist/src/core/registries.js +48 -3
- package/dist/src/core/versioning.js +2 -2
- package/dist/tests/commands/preflight/preflight.test.js +145 -0
- package/dist/tests/commands/update.test.js +31 -0
- package/dist/tests/core/context-kernel/inspect.test.js +144 -0
- package/dist/tests/core/registries-sync.test.js +29 -11
- package/dist/tests/core/registry-manifest.test.js +28 -0
- package/dist/tests/core/versioning.test.js +5 -5
- package/package.json +2 -2
|
@@ -13,6 +13,8 @@ const init_1 = require("../sensors/init");
|
|
|
13
13
|
const baseline_1 = require("../sensors/baseline");
|
|
14
14
|
const run_1 = require("../sensors/run");
|
|
15
15
|
const paths_1 = require("../../core/paths");
|
|
16
|
+
const registries_1 = require("../../core/registries");
|
|
17
|
+
const inspect_1 = require("../../core/context-kernel/inspect");
|
|
16
18
|
const MANIFEST = path_1.default.join('.awm', 'sensors.json');
|
|
17
19
|
/**
|
|
18
20
|
* The agent needs project context delivered every session. A repo with neither file
|
|
@@ -31,6 +33,44 @@ function checkContext(cwd) {
|
|
|
31
33
|
}
|
|
32
34
|
return { id: 'context', ok: true, detail: present.join(', ') };
|
|
33
35
|
}
|
|
36
|
+
function checkContextKernel(cwd) {
|
|
37
|
+
const resolution = (0, registries_1.resolveProjectContextSchema)();
|
|
38
|
+
if (resolution.diagnostics.length > 0) {
|
|
39
|
+
return {
|
|
40
|
+
id: 'context-kernel',
|
|
41
|
+
ok: false,
|
|
42
|
+
advisory: false,
|
|
43
|
+
detail: `Context Kernel registry declaration is invalid: ${resolution.diagnostics.join('; ')}`,
|
|
44
|
+
remedy: 'repair the registry manifest or use the safe full-context project files until the registry is valid',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!resolution.declaration)
|
|
48
|
+
return null;
|
|
49
|
+
const inspection = (0, inspect_1.inspectContextKernel)(cwd);
|
|
50
|
+
if (inspection.state === 'legacy') {
|
|
51
|
+
return {
|
|
52
|
+
id: 'context-kernel',
|
|
53
|
+
ok: false,
|
|
54
|
+
advisory: true,
|
|
55
|
+
detail: 'legacy full context — Context Kernel v1 migration available',
|
|
56
|
+
remedy: 'run project-context-init and review the generated rule trace; awm update never rewrites project files',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (inspection.state === 'valid') {
|
|
60
|
+
return {
|
|
61
|
+
id: 'context-kernel',
|
|
62
|
+
ok: true,
|
|
63
|
+
detail: `Context Kernel v${inspection.schema} valid (${inspection.fixedBytes} fixed bytes)`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
id: 'context-kernel',
|
|
68
|
+
ok: false,
|
|
69
|
+
advisory: false,
|
|
70
|
+
detail: inspection.detail,
|
|
71
|
+
remedy: inspection.remedy,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
34
74
|
function readManifest(cwd) {
|
|
35
75
|
try {
|
|
36
76
|
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(cwd, MANIFEST), 'utf-8'));
|
|
@@ -345,8 +385,10 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
345
385
|
}
|
|
346
386
|
const manifest = readManifest(cwd);
|
|
347
387
|
const manifestExists = fs_1.default.existsSync(path_1.default.join(cwd, MANIFEST));
|
|
388
|
+
const contextKernel = checkContextKernel(cwd);
|
|
348
389
|
const checks = [
|
|
349
390
|
checkContext(cwd),
|
|
391
|
+
...(contextKernel ? [contextKernel] : []),
|
|
350
392
|
checkManifest(cwd, manifest),
|
|
351
393
|
// Skipped when there is no manifest: reporting "tools broken" (or nudging toward
|
|
352
394
|
// a baseline that has nothing to snapshot) on a repo that was never set up
|
|
@@ -359,8 +401,11 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
359
401
|
// about PR/MR tooling, not sensors.
|
|
360
402
|
checkHost(cwd),
|
|
361
403
|
];
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
404
|
+
const blockingFailure = (check) => !check.ok && check.advisory !== true;
|
|
405
|
+
const invalidContextKernel = contextKernel !== null && blockingFailure(contextKernel);
|
|
406
|
+
const status = invalidContextKernel ? 'degraded'
|
|
407
|
+
: !manifestExists ? 'not_configured'
|
|
408
|
+
: checks.some(blockingFailure) ? 'degraded'
|
|
409
|
+
: 'ready';
|
|
365
410
|
return { status, checks };
|
|
366
411
|
}
|
|
@@ -25,19 +25,18 @@ function formatReport(report) {
|
|
|
25
25
|
// longer `PreflightCheck['id']` is added (confirmed: 'sensors-baseline', at 16
|
|
26
26
|
// chars, broke a hardcoded 9-char pad).
|
|
27
27
|
const idWidth = Math.max(0, ...report.checks.map(c => c.id.length));
|
|
28
|
-
const lines = report.checks.map(c => ` ${c.ok ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘')} ${c.id.padEnd(idWidth)} ${c.detail}`
|
|
28
|
+
const lines = report.checks.map(c => ` ${c.advisory === true ? picocolors_1.default.yellow('⚠') : c.ok ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘')} ${c.id.padEnd(idWidth)} ${c.detail}`
|
|
29
29
|
+ (c.remedy ? `\n ${picocolors_1.default.dim('→ ' + c.remedy)}` : ''));
|
|
30
30
|
if (report.status === 'ready') {
|
|
31
31
|
return `${picocolors_1.default.green('✔')} Harness ready — this project can be gated.\n${lines.join('\n')}\n`;
|
|
32
32
|
}
|
|
33
33
|
const headline = report.status === 'not_configured'
|
|
34
34
|
? `${picocolors_1.default.red('✘')} AWM is not configured in this project.`
|
|
35
|
-
: `${picocolors_1.default.red('✘')} Harness degraded — it
|
|
35
|
+
: `${picocolors_1.default.red('✘')} Harness degraded — it has blocking preflight failures.`;
|
|
36
36
|
return `${headline}\n${lines.join('\n')}\n\n`
|
|
37
37
|
+ ` ${picocolors_1.default.bold('Do not hand this off to an unattended run.')} Every quality phase downstream\n`
|
|
38
|
-
+ ` (implementer, reviewers, post-qa) consumes
|
|
39
|
-
+ `
|
|
40
|
-
+ ` bad change is already merged.\n`;
|
|
38
|
+
+ ` (implementer, reviewers, post-qa) consumes these gates. Resolve every blocking\n`
|
|
39
|
+
+ ` check before handoff, so a bad change is not merged.\n`;
|
|
41
40
|
}
|
|
42
41
|
function registerPreflightCommand(program) {
|
|
43
42
|
program
|
|
@@ -21,6 +21,7 @@ exports.runUpdateCore = runUpdateCore;
|
|
|
21
21
|
// instead (which already imports clack at the top and is never `require()`d
|
|
22
22
|
// by tests).
|
|
23
23
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
24
|
+
const versioning_1 = require("../core/versioning");
|
|
24
25
|
const paths_1 = require("../core/paths");
|
|
25
26
|
const config_1 = require("../utils/config");
|
|
26
27
|
const agent_targets_1 = require("../core/agent-targets");
|
|
@@ -89,8 +90,29 @@ async function runUpdateCore(options = {}, deps = {}) {
|
|
|
89
90
|
for (const r of registryResults) {
|
|
90
91
|
if (r.action === 'error')
|
|
91
92
|
console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
|
|
92
|
-
else
|
|
93
|
-
|
|
93
|
+
else if (r.disposition === 'regressed') {
|
|
94
|
+
const latest = r.availableVersion && r.availableVersion !== r.version
|
|
95
|
+
? ` Latest available: ${r.availableVersion}.`
|
|
96
|
+
: '';
|
|
97
|
+
console.warn(picocolors_1.default.yellow(` ⚠ Registry ${r.name} regressed ${r.previousVersion ?? '(previous version)'} → ${r.version} `
|
|
98
|
+
+ `because it is pinned to v${(0, versioning_1.normalizePin)(r.pin ?? '')}.${latest} Run \`awm unpin ${r.name}\` to move forward.`));
|
|
99
|
+
}
|
|
100
|
+
else if (r.pin && r.availableVersion !== r.version) {
|
|
101
|
+
console.log(picocolors_1.default.yellow(` ✓ Registry ${r.name} resolved @ ${r.version} (pinned to v${(0, versioning_1.normalizePin)(r.pin)}; `
|
|
102
|
+
+ `latest available: ${r.availableVersion ?? 'unknown'}). Run \`awm unpin ${r.name}\` to move forward.`));
|
|
103
|
+
}
|
|
104
|
+
else if (r.disposition === 'unchanged') {
|
|
105
|
+
console.log(picocolors_1.default.dim(` ✓ Registry ${r.name} already at ${r.version}`));
|
|
106
|
+
}
|
|
107
|
+
else if (r.disposition === 'advanced') {
|
|
108
|
+
console.log(picocolors_1.default.green(` ✓ Registry ${r.name} updated @ ${r.version}`));
|
|
109
|
+
}
|
|
110
|
+
else if (r.disposition === 'installed') {
|
|
111
|
+
console.log(picocolors_1.default.green(` ✓ Registry ${r.name} installed @ ${r.version}`));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(picocolors_1.default.green(` ✓ Registry ${r.name} resolved @ ${r.version}`));
|
|
115
|
+
}
|
|
94
116
|
}
|
|
95
117
|
const registries = {
|
|
96
118
|
configured: registryResults.length,
|
|
@@ -0,0 +1,221 @@
|
|
|
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.inspectContextKernel = inspectContextKernel;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const INDEX_PATH = ['.awm', 'context', 'index.json'];
|
|
10
|
+
const ROOT_CONTEXT_FILES = ['AGENTS.md', 'CONSTITUTION.md', 'CLAUDE.md'];
|
|
11
|
+
const START_MARKER = '<!-- AWM:CONTEXT-KERNEL:START v1 -->';
|
|
12
|
+
const END_MARKER = '<!-- AWM:CONTEXT-KERNEL:END v1 -->';
|
|
13
|
+
const ANCHOR_MARKER_PREFIX = '<!-- awm-context:';
|
|
14
|
+
const TOP_LEVEL_FIELDS = ['schema', 'kernelFiles', 'maxFixedBytes', 'entries'];
|
|
15
|
+
const ENTRY_FIELDS = ['id', 'tier', 'path', 'anchor', 'when'];
|
|
16
|
+
const REMEDY = 'run project-context-init and review the Context Kernel v1 artifacts';
|
|
17
|
+
function invalid(detail) {
|
|
18
|
+
return { state: 'invalid', detail, remedy: REMEDY };
|
|
19
|
+
}
|
|
20
|
+
function within(root, candidate) {
|
|
21
|
+
const relative = path_1.default.relative(root, candidate);
|
|
22
|
+
return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative));
|
|
23
|
+
}
|
|
24
|
+
function count(text, needle) {
|
|
25
|
+
return text.split(needle).length - 1;
|
|
26
|
+
}
|
|
27
|
+
function exactFields(raw, fields) {
|
|
28
|
+
const keys = Object.keys(raw);
|
|
29
|
+
return keys.length === fields.length && fields.every((field) => Object.prototype.hasOwnProperty.call(raw, field));
|
|
30
|
+
}
|
|
31
|
+
function isNormalizedRelative(value) {
|
|
32
|
+
if (typeof value !== 'string' || value.length === 0 || path_1.default.isAbsolute(value) || path_1.default.win32.isAbsolute(value))
|
|
33
|
+
return false;
|
|
34
|
+
if (value.includes('\\') || value.split('/').some((part) => part === '.' || part === '..' || part === ''))
|
|
35
|
+
return false;
|
|
36
|
+
return path_1.default.posix.normalize(value) === value;
|
|
37
|
+
}
|
|
38
|
+
function resolveRegularInside(root, relative) {
|
|
39
|
+
if (!isNormalizedRelative(relative))
|
|
40
|
+
return { detail: `invalid repository-relative path ${JSON.stringify(relative)}` };
|
|
41
|
+
const candidate = path_1.default.join(root, relative);
|
|
42
|
+
try {
|
|
43
|
+
const stat = fs_1.default.statSync(candidate);
|
|
44
|
+
if (!stat.isFile())
|
|
45
|
+
return { detail: `${relative} must resolve to a regular file` };
|
|
46
|
+
const real = fs_1.default.realpathSync(candidate);
|
|
47
|
+
if (!within(root, real))
|
|
48
|
+
return { detail: `${relative} resolves outside the project root` };
|
|
49
|
+
return { file: real };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { detail: `${relative} cannot be read: ${error instanceof Error ? error.message : String(error)}` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function parseEntry(raw) {
|
|
56
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw) || !exactFields(raw, ENTRY_FIELDS)) {
|
|
57
|
+
return { detail: 'each index entry must contain exactly id, tier, path, anchor, when' };
|
|
58
|
+
}
|
|
59
|
+
const value = raw;
|
|
60
|
+
if (typeof value.id !== 'string' || !/^CTX-[A-Z0-9]+(?:-[A-Z0-9]+)*$/.test(value.id) || Buffer.byteLength(value.id, 'utf8') > 80) {
|
|
61
|
+
return { detail: 'entry id must match CTX-[A-Z0-9-]+ and be at most 80 bytes' };
|
|
62
|
+
}
|
|
63
|
+
if (value.tier !== 'kernel' && value.tier !== 'selective')
|
|
64
|
+
return { detail: 'entry tier must be kernel or selective' };
|
|
65
|
+
if (!isNormalizedRelative(value.path))
|
|
66
|
+
return { detail: `invalid repository-relative path ${JSON.stringify(value.path)}` };
|
|
67
|
+
for (const field of ['anchor', 'when']) {
|
|
68
|
+
if (typeof value[field] !== 'string' || value[field].length === 0 || Buffer.byteLength(value[field], 'utf8') > 500) {
|
|
69
|
+
return { detail: `entry ${field} must be a non-empty string of at most 500 bytes` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { entry: { id: value.id, tier: value.tier, path: value.path, anchor: value.anchor, when: value.when } };
|
|
73
|
+
}
|
|
74
|
+
function parseIndex(contents) {
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = JSON.parse(contents);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return { detail: `index is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
81
|
+
}
|
|
82
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw) || !exactFields(raw, TOP_LEVEL_FIELDS)) {
|
|
83
|
+
return { detail: 'index must contain exactly schema, kernelFiles, maxFixedBytes, entries' };
|
|
84
|
+
}
|
|
85
|
+
const value = raw;
|
|
86
|
+
if (value.schema !== 1 || !Number.isSafeInteger(value.schema))
|
|
87
|
+
return { detail: 'index schema must be exactly 1' };
|
|
88
|
+
if (!Array.isArray(value.kernelFiles) || value.kernelFiles.length === 0 || value.kernelFiles.some((file) => !isNormalizedRelative(file)) || new Set(value.kernelFiles).size !== value.kernelFiles.length) {
|
|
89
|
+
return { detail: 'kernelFiles must be a non-empty unique normalized path array' };
|
|
90
|
+
}
|
|
91
|
+
if (value.kernelFiles.some((file) => file.includes('/')))
|
|
92
|
+
return { detail: 'kernelFiles must name root context files' };
|
|
93
|
+
if (!Number.isSafeInteger(value.maxFixedBytes) || value.maxFixedBytes < 1)
|
|
94
|
+
return { detail: 'maxFixedBytes must be a positive integer' };
|
|
95
|
+
if (!Array.isArray(value.entries) || value.entries.length === 0)
|
|
96
|
+
return { detail: 'entries must be a non-empty array' };
|
|
97
|
+
const entries = [];
|
|
98
|
+
for (const rawEntry of value.entries) {
|
|
99
|
+
const parsed = parseEntry(rawEntry);
|
|
100
|
+
if (!parsed.entry)
|
|
101
|
+
return { detail: parsed.detail };
|
|
102
|
+
entries.push(parsed.entry);
|
|
103
|
+
}
|
|
104
|
+
if (new Set(entries.map((entry) => entry.id)).size !== entries.length)
|
|
105
|
+
return { detail: 'entry ids must be unique' };
|
|
106
|
+
if (new Set(entries.map((entry) => entry.anchor)).size !== entries.length)
|
|
107
|
+
return { detail: 'entry anchors must be unique' };
|
|
108
|
+
return { index: { schema: 1, kernelFiles: value.kernelFiles, maxFixedBytes: value.maxFixedBytes, entries } };
|
|
109
|
+
}
|
|
110
|
+
function markerBounds(contents) {
|
|
111
|
+
if (count(contents, START_MARKER) !== 1 || count(contents, END_MARKER) !== 1)
|
|
112
|
+
return null;
|
|
113
|
+
const start = contents.indexOf(START_MARKER) + START_MARKER.length;
|
|
114
|
+
const end = contents.indexOf(END_MARKER);
|
|
115
|
+
return start < end ? { start, end } : null;
|
|
116
|
+
}
|
|
117
|
+
/** Inspects project-owned Context Kernel v1 artifacts. Invalid project artifacts are
|
|
118
|
+
* classified explicitly; only invalid public input throws. */
|
|
119
|
+
function inspectContextKernel(cwd) {
|
|
120
|
+
if (typeof cwd !== 'string' || cwd.length === 0)
|
|
121
|
+
throw new Error('cwd must be a non-empty directory path');
|
|
122
|
+
let root;
|
|
123
|
+
try {
|
|
124
|
+
root = fs_1.default.realpathSync(cwd);
|
|
125
|
+
if (!fs_1.default.statSync(root).isDirectory())
|
|
126
|
+
throw new Error('not a directory');
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
throw new Error(`cwd must resolve to an existing directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
+
}
|
|
131
|
+
const indexFile = path_1.default.join(root, ...INDEX_PATH);
|
|
132
|
+
let indexExists;
|
|
133
|
+
try {
|
|
134
|
+
fs_1.default.lstatSync(indexFile);
|
|
135
|
+
indexExists = true;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.code !== 'ENOENT')
|
|
139
|
+
return invalid(`${indexFile} cannot be inspected`);
|
|
140
|
+
indexExists = false;
|
|
141
|
+
}
|
|
142
|
+
if (!indexExists) {
|
|
143
|
+
for (const name of ROOT_CONTEXT_FILES) {
|
|
144
|
+
const candidate = path_1.default.join(root, name);
|
|
145
|
+
try {
|
|
146
|
+
if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile()) {
|
|
147
|
+
const contents = fs_1.default.readFileSync(candidate, 'utf8');
|
|
148
|
+
if (!contents.includes('AWM:CONTEXT-KERNEL:') && !contents.includes(ANCHOR_MARKER_PREFIX))
|
|
149
|
+
continue;
|
|
150
|
+
return invalid(`${name} contains Context Kernel markers but ${INDEX_PATH.join('/')} is missing`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return invalid(`${name} cannot be inspected: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { state: 'legacy' };
|
|
158
|
+
}
|
|
159
|
+
const indexTarget = resolveRegularInside(root, INDEX_PATH.join('/'));
|
|
160
|
+
if (!indexTarget.file)
|
|
161
|
+
return invalid(`${INDEX_PATH.join('/')}: ${indexTarget.detail}`);
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = parseIndex(fs_1.default.readFileSync(indexTarget.file, 'utf8'));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return invalid(`${INDEX_PATH.join('/')}: cannot be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
}
|
|
169
|
+
if (!parsed.index)
|
|
170
|
+
return invalid(`${INDEX_PATH.join('/')}: ${parsed.detail}`);
|
|
171
|
+
const index = parsed.index;
|
|
172
|
+
const files = new Map();
|
|
173
|
+
for (const kernelFile of index.kernelFiles) {
|
|
174
|
+
const target = resolveRegularInside(root, kernelFile);
|
|
175
|
+
if (!target.file)
|
|
176
|
+
return invalid(`${kernelFile}: ${target.detail}`);
|
|
177
|
+
const contents = fs_1.default.readFileSync(target.file, 'utf8');
|
|
178
|
+
if (!markerBounds(contents))
|
|
179
|
+
return invalid(`${kernelFile}: requires exactly one ordered Context Kernel v1 marker pair`);
|
|
180
|
+
files.set(kernelFile, contents);
|
|
181
|
+
}
|
|
182
|
+
for (const entry of index.entries) {
|
|
183
|
+
const target = resolveRegularInside(root, entry.path);
|
|
184
|
+
if (!target.file)
|
|
185
|
+
return invalid(`${entry.path}: ${target.detail}`);
|
|
186
|
+
let contents;
|
|
187
|
+
try {
|
|
188
|
+
contents = files.get(entry.path) ?? fs_1.default.readFileSync(target.file, 'utf8');
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
return invalid(`${entry.path}: cannot be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
192
|
+
}
|
|
193
|
+
const marker = `<!-- ${entry.anchor} -->`;
|
|
194
|
+
if (count(contents, marker) !== 1)
|
|
195
|
+
return invalid(`${entry.path}: anchor ${entry.anchor} must appear exactly once`);
|
|
196
|
+
const anchor = contents.indexOf(marker);
|
|
197
|
+
const bounds = files.has(entry.path) ? markerBounds(contents) : null;
|
|
198
|
+
if (entry.tier === 'kernel') {
|
|
199
|
+
if (!files.has(entry.path))
|
|
200
|
+
return invalid(`${entry.path}: kernel entry must point to a kernel file`);
|
|
201
|
+
if (!bounds || anchor < bounds.start || anchor >= bounds.end)
|
|
202
|
+
return invalid(`${entry.path}: kernel anchor ${entry.anchor} must be inside the protected region`);
|
|
203
|
+
}
|
|
204
|
+
else if (bounds && anchor >= bounds.start && anchor < bounds.end) {
|
|
205
|
+
return invalid(`${entry.path}: selective anchor ${entry.anchor} must be outside the protected region`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
let fixedBytes = 0;
|
|
209
|
+
for (const name of ROOT_CONTEXT_FILES) {
|
|
210
|
+
const candidate = path_1.default.join(root, name);
|
|
211
|
+
if (!fs_1.default.existsSync(candidate))
|
|
212
|
+
continue;
|
|
213
|
+
const target = resolveRegularInside(root, name);
|
|
214
|
+
if (!target.file)
|
|
215
|
+
return invalid(`${name}: ${target.detail}`);
|
|
216
|
+
fixedBytes += fs_1.default.statSync(target.file).size;
|
|
217
|
+
}
|
|
218
|
+
if (fixedBytes > index.maxFixedBytes)
|
|
219
|
+
return invalid(`fixed context bytes ${fixedBytes} exceed maxFixedBytes ${index.maxFixedBytes}`);
|
|
220
|
+
return { state: 'valid', schema: 1, index, fixedBytes };
|
|
221
|
+
}
|
|
@@ -23,6 +23,7 @@ exports.describeRegistrySyncErrors = describeRegistrySyncErrors;
|
|
|
23
23
|
exports.unusableSyncedRegistries = unusableSyncedRegistries;
|
|
24
24
|
exports.assertSyncedRegistriesUsable = assertSyncedRegistriesUsable;
|
|
25
25
|
exports.readRegistryManifest = readRegistryManifest;
|
|
26
|
+
exports.resolveProjectContextSchema = resolveProjectContextSchema;
|
|
26
27
|
exports.registryNameForPath = registryNameForPath;
|
|
27
28
|
exports.verifyMinCliVersions = verifyMinCliVersions;
|
|
28
29
|
exports.assertRegistryGates = assertRegistryGates;
|
|
@@ -251,9 +252,11 @@ async function syncRegistries() {
|
|
|
251
252
|
await (0, simple_git_1.default)(reg.contentRoot).reset(['--hard']);
|
|
252
253
|
}
|
|
253
254
|
const git = (0, simple_git_1.default)(reg.contentRoot);
|
|
255
|
+
const previousVersion = freshClone ? null : await (0, versioning_1.currentVersion)(reg.contentRoot);
|
|
256
|
+
const versionOpts = (0, versioning_1.machineVersionOpts)(reg.name);
|
|
254
257
|
let resolved;
|
|
255
258
|
try {
|
|
256
|
-
resolved = await (0, versioning_1.resolveTargetRef)(reg.contentRoot,
|
|
259
|
+
resolved = await (0, versioning_1.resolveTargetRef)(reg.contentRoot, versionOpts);
|
|
257
260
|
await git.checkout(resolved.ref);
|
|
258
261
|
if (resolved.kind !== 'tag')
|
|
259
262
|
await git.pull('origin', resolved.ref);
|
|
@@ -266,10 +269,24 @@ async function syncRegistries() {
|
|
|
266
269
|
fs_1.default.rmSync(reg.contentRoot, { recursive: true, force: true });
|
|
267
270
|
throw e;
|
|
268
271
|
}
|
|
272
|
+
const version = resolved.kind === 'tag' ? `v${resolved.version}` : 'HEAD';
|
|
273
|
+
let disposition;
|
|
274
|
+
if (freshClone)
|
|
275
|
+
disposition = 'installed';
|
|
276
|
+
else if (resolved.kind !== 'tag' || previousVersion === null)
|
|
277
|
+
disposition = 'resolved';
|
|
278
|
+
else {
|
|
279
|
+
const direction = (0, versioning_1.compareSemver)(resolved.version, previousVersion);
|
|
280
|
+
disposition = direction > 0 ? 'advanced' : direction < 0 ? 'regressed' : 'unchanged';
|
|
281
|
+
}
|
|
269
282
|
results.push({
|
|
270
283
|
name: reg.name,
|
|
271
284
|
action: freshClone ? 'recloned' : 'pulled',
|
|
272
|
-
version
|
|
285
|
+
version,
|
|
286
|
+
disposition,
|
|
287
|
+
...(versionOpts.pin ? { pin: (0, versioning_1.normalizePin)(versionOpts.pin) } : {}),
|
|
288
|
+
...(previousVersion ? { previousVersion: `v${previousVersion}` } : {}),
|
|
289
|
+
...(resolved.kind === 'tag' ? { availableVersion: `v${resolved.latestVersion}` } : {}),
|
|
273
290
|
});
|
|
274
291
|
}
|
|
275
292
|
catch (e) {
|
|
@@ -347,7 +364,35 @@ function readRegistryManifest(root) {
|
|
|
347
364
|
}
|
|
348
365
|
minCliVersion = rawMin.replace(/^v/, '');
|
|
349
366
|
}
|
|
350
|
-
|
|
367
|
+
const rawSchema = raw?.projectContextSchema;
|
|
368
|
+
let projectContextSchema;
|
|
369
|
+
if (rawSchema !== undefined) {
|
|
370
|
+
if (rawSchema !== 1) {
|
|
371
|
+
throw new Error(`Invalid registry manifest at ${file}: "projectContextSchema" must be exactly 1, got ${JSON.stringify(rawSchema)}`);
|
|
372
|
+
}
|
|
373
|
+
projectContextSchema = 1;
|
|
374
|
+
}
|
|
375
|
+
return { overrides: new Set(overrides), minCliVersion, projectContextSchema };
|
|
376
|
+
}
|
|
377
|
+
/** Finds the first usable registry that declares Context Kernel v1. Every active
|
|
378
|
+
* registry is inspected even after a declaration is found, so a malformed manifest
|
|
379
|
+
* cannot be masked by a later valid declaration. */
|
|
380
|
+
function resolveProjectContextSchema() {
|
|
381
|
+
const diagnostics = [];
|
|
382
|
+
let declaration;
|
|
383
|
+
for (const registry of listRegistries()) {
|
|
384
|
+
if (!validateRegistryLayout(registry.contentRoot))
|
|
385
|
+
continue;
|
|
386
|
+
try {
|
|
387
|
+
if (!declaration && readRegistryManifest(registry.contentRoot).projectContextSchema === 1) {
|
|
388
|
+
declaration = { registry, schema: 1 };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
diagnostics.push(`Registry "${registry.name}" manifest is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return declaration ? { declaration, diagnostics } : { diagnostics };
|
|
351
396
|
}
|
|
352
397
|
/** Nombre del registry dueño de un path: el nombre del clone bajo registriesDir(),
|
|
353
398
|
* o null si no pertenece a ninguno. */
|
|
@@ -69,7 +69,7 @@ async function resolveTargetRef(repoDir, opts) {
|
|
|
69
69
|
: 'the registry has no version tags';
|
|
70
70
|
throw new Error(`Pinned version ${want} not found in ${repoDir} — ${available}`);
|
|
71
71
|
}
|
|
72
|
-
return { kind: 'tag', ref: want, version: normalizePin(opts.pin) };
|
|
72
|
+
return { kind: 'tag', ref: want, version: normalizePin(opts.pin), latestVersion: tags[tags.length - 1].slice(1) };
|
|
73
73
|
}
|
|
74
74
|
const branch = await defaultBranch(git);
|
|
75
75
|
if (opts.channel === 'dev')
|
|
@@ -77,7 +77,7 @@ async function resolveTargetRef(repoDir, opts) {
|
|
|
77
77
|
if (tags.length === 0)
|
|
78
78
|
return { kind: 'head-fallback', ref: branch };
|
|
79
79
|
const latest = tags[tags.length - 1];
|
|
80
|
-
return { kind: 'tag', ref: latest, version: latest.slice(1) };
|
|
80
|
+
return { kind: 'tag', ref: latest, version: latest.slice(1), latestVersion: latest.slice(1) };
|
|
81
81
|
}
|
|
82
82
|
/** Versión checkouteada actual ("X.Y.Z") si HEAD coincide exactamente con un tag semver; null si sigue un branch. */
|
|
83
83
|
async function currentVersion(repoDir) {
|
|
@@ -51,9 +51,154 @@ function project(opts = {}) {
|
|
|
51
51
|
const dirs = [];
|
|
52
52
|
const make = (o) => { const d = project(o); dirs.push(d); return d; };
|
|
53
53
|
afterAll(() => dirs.forEach(d => fs_1.default.rmSync(d, { recursive: true, force: true })));
|
|
54
|
+
let previousAwmHome;
|
|
55
|
+
const awmHomes = [];
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
previousAwmHome = process.env.AWM_HOME;
|
|
58
|
+
const awmHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-home-'));
|
|
59
|
+
awmHomes.push(awmHome);
|
|
60
|
+
process.env.AWM_HOME = awmHome;
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
if (previousAwmHome === undefined)
|
|
64
|
+
delete process.env.AWM_HOME;
|
|
65
|
+
else
|
|
66
|
+
process.env.AWM_HOME = previousAwmHome;
|
|
67
|
+
});
|
|
68
|
+
afterAll(() => awmHomes.forEach(dir => fs_1.default.rmSync(dir, { recursive: true, force: true })));
|
|
69
|
+
function installRegistry(manifest, name = 'test-registry') {
|
|
70
|
+
const awmHome = process.env.AWM_HOME;
|
|
71
|
+
const root = path_1.default.join(awmHome, 'registries', name);
|
|
72
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills'), { recursive: true });
|
|
73
|
+
const config = fs_1.default.existsSync(path_1.default.join(awmHome, 'registries.json'))
|
|
74
|
+
? JSON.parse(fs_1.default.readFileSync(path_1.default.join(awmHome, 'registries.json'), 'utf8'))
|
|
75
|
+
: [];
|
|
76
|
+
config.push({ name, remote: `https://example.invalid/${name}.git` });
|
|
77
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify(config));
|
|
78
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify(manifest));
|
|
79
|
+
}
|
|
80
|
+
function writeValidKernel(dir) {
|
|
81
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\nagent rules\n<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
82
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'CONSTITUTION.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\n<!-- awm-context:CTX-PROCESS-001 -->\nprocess rules\n<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
83
|
+
const card = path_1.default.join(dir, 'docs', 'awm', 'context', 'releases.md');
|
|
84
|
+
fs_1.default.mkdirSync(path_1.default.dirname(card), { recursive: true });
|
|
85
|
+
fs_1.default.writeFileSync(card, '<!-- awm-context:CTX-RELEASE-001 -->\nrelease rules\n');
|
|
86
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, '.awm', 'context'), { recursive: true });
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'context', 'index.json'), JSON.stringify({
|
|
88
|
+
schema: 1,
|
|
89
|
+
kernelFiles: ['AGENTS.md', 'CONSTITUTION.md'],
|
|
90
|
+
maxFixedBytes: 33_740,
|
|
91
|
+
entries: [
|
|
92
|
+
{ id: 'CTX-PROCESS-001', tier: 'kernel', path: 'CONSTITUTION.md', anchor: 'awm-context:CTX-PROCESS-001', when: 'always' },
|
|
93
|
+
{ id: 'CTX-RELEASE-001', tier: 'selective', path: 'docs/awm/context/releases.md', anchor: 'awm-context:CTX-RELEASE-001', when: 'release automation' },
|
|
94
|
+
],
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
function writePartialKernel(dir) {
|
|
98
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\nagent rules\n');
|
|
99
|
+
}
|
|
54
100
|
const check = (r, id) => r.checks.find(c => c.id === id);
|
|
55
101
|
describe('preflight', () => {
|
|
56
102
|
afterEach(() => mockRunSensors.mockReset());
|
|
103
|
+
it('does not add a row when no registry declares the schema', async () => {
|
|
104
|
+
installRegistry({ minCliVersion: '9.2.1' });
|
|
105
|
+
const report = await (0, checks_1.preflight)(make({
|
|
106
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
107
|
+
}));
|
|
108
|
+
expect(report.checks.map(c => c.id)).not.toContain('context-kernel');
|
|
109
|
+
});
|
|
110
|
+
it('blocks preflight when an active registry declares an invalid context kernel schema', async () => {
|
|
111
|
+
installRegistry({ projectContextSchema: 2 });
|
|
112
|
+
const report = await (0, checks_1.preflight)(make({
|
|
113
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
114
|
+
}));
|
|
115
|
+
expect(report.status).toBe('degraded');
|
|
116
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
117
|
+
expect(check(report, 'context-kernel')).toMatchObject({
|
|
118
|
+
ok: false,
|
|
119
|
+
advisory: false,
|
|
120
|
+
detail: expect.stringMatching(/projectContextSchema.*exactly 1.*2/),
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
it('fails closed when an earlier active registry manifest is malformed before a valid declaration', async () => {
|
|
124
|
+
installRegistry({ projectContextSchema: 2 }, 'malformed');
|
|
125
|
+
installRegistry({ projectContextSchema: 1 }, 'valid');
|
|
126
|
+
const report = await (0, checks_1.preflight)(make({
|
|
127
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
128
|
+
}));
|
|
129
|
+
expect(report.status).toBe('degraded');
|
|
130
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
131
|
+
expect(check(report, 'context-kernel')).toMatchObject({
|
|
132
|
+
ok: false,
|
|
133
|
+
advisory: false,
|
|
134
|
+
detail: expect.stringMatching(/malformed.*awm-registry\.json.*projectContextSchema.*exactly 1/i),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
it('keeps legacy ready but renders a persistent warning and remedy', async () => {
|
|
138
|
+
installRegistry({ projectContextSchema: 1 });
|
|
139
|
+
const report = await (0, checks_1.preflight)(make({
|
|
140
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
141
|
+
}));
|
|
142
|
+
expect(report.status).toBe('ready');
|
|
143
|
+
expect(report.checks).toContainEqual(expect.objectContaining({
|
|
144
|
+
id: 'context-kernel', ok: false, advisory: true,
|
|
145
|
+
detail: expect.stringMatching(/legacy full context/),
|
|
146
|
+
remedy: expect.stringMatching(/project-context-init/),
|
|
147
|
+
}));
|
|
148
|
+
expect((0, preflight_1.formatReport)(report)).toMatch(/⚠.*context-kernel.*legacy full context/s);
|
|
149
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(0);
|
|
150
|
+
expect(JSON.parse(JSON.stringify(report))).toEqual(expect.objectContaining({
|
|
151
|
+
status: 'ready',
|
|
152
|
+
checks: expect.arrayContaining([expect.objectContaining({
|
|
153
|
+
id: 'context-kernel', ok: false, advisory: true,
|
|
154
|
+
detail: expect.any(String), remedy: expect.any(String),
|
|
155
|
+
})]),
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
it('keeps an unconfigured legacy project not_configured while the context kernel remains advisory', async () => {
|
|
159
|
+
installRegistry({ projectContextSchema: 1 });
|
|
160
|
+
const report = await (0, checks_1.preflight)(make());
|
|
161
|
+
expect(report.status).toBe('not_configured');
|
|
162
|
+
expect(check(report, 'manifest')).toMatchObject({ ok: false, detail: 'no .awm/sensors.json' });
|
|
163
|
+
expect(check(report, 'context-kernel')).toMatchObject({
|
|
164
|
+
ok: false,
|
|
165
|
+
advisory: true,
|
|
166
|
+
detail: expect.stringMatching(/legacy full context/),
|
|
167
|
+
});
|
|
168
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
169
|
+
});
|
|
170
|
+
it('passes a valid kernel', async () => {
|
|
171
|
+
installRegistry({ projectContextSchema: 1 });
|
|
172
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
173
|
+
writeValidKernel(dir);
|
|
174
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
175
|
+
expect(report).toEqual(expect.objectContaining({ status: 'ready' }));
|
|
176
|
+
expect(check(report, 'context-kernel')).toMatchObject({ ok: true });
|
|
177
|
+
expect(check(report, 'context-kernel')).not.toHaveProperty('advisory');
|
|
178
|
+
});
|
|
179
|
+
it('degrades partial or invalid migration', async () => {
|
|
180
|
+
installRegistry({ projectContextSchema: 1 });
|
|
181
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
182
|
+
writePartialKernel(dir);
|
|
183
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
184
|
+
expect(report.status).toBe('degraded');
|
|
185
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
186
|
+
expect(report.checks).toContainEqual(expect.objectContaining({
|
|
187
|
+
id: 'context-kernel', advisory: false, ok: false,
|
|
188
|
+
}));
|
|
189
|
+
expect((0, preflight_1.formatReport)(report)).toMatch(/Harness degraded — it has blocking preflight failures\./);
|
|
190
|
+
expect((0, preflight_1.formatReport)(report)).not.toMatch(/declares sensors it cannot run/);
|
|
191
|
+
});
|
|
192
|
+
it('degrades an invalid kernel even when no sensor manifest exists', async () => {
|
|
193
|
+
installRegistry({ projectContextSchema: 1 });
|
|
194
|
+
const dir = make();
|
|
195
|
+
writePartialKernel(dir);
|
|
196
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
197
|
+
expect(report.status).toBe('degraded');
|
|
198
|
+
expect(check(report, 'manifest')).toMatchObject({ ok: false, detail: 'no .awm/sensors.json' });
|
|
199
|
+
expect(check(report, 'context-kernel')).toMatchObject({ ok: false, advisory: false });
|
|
200
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
201
|
+
});
|
|
57
202
|
it('keeps default preflight static and does not dispatch sensors', async () => {
|
|
58
203
|
const dir = make({
|
|
59
204
|
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
@@ -125,6 +125,37 @@ describe('awm update — honest outcome', () => {
|
|
|
125
125
|
expect(result.registries).toEqual({ configured: 2, synced: 2, failed: [] });
|
|
126
126
|
expect(updateOutro(result)).toBe('✅ 2 registries, skills and hooks updated.');
|
|
127
127
|
});
|
|
128
|
+
it('una regresión causada por pin se advierte y explica cómo volver a avanzar', async () => {
|
|
129
|
+
const { runUpdateCore } = require('../../src/commands/update');
|
|
130
|
+
await runUpdateCore({}, deps({
|
|
131
|
+
syncRegistries: async () => [{
|
|
132
|
+
name: 'baseline',
|
|
133
|
+
action: 'pulled',
|
|
134
|
+
version: 'v1.0.0',
|
|
135
|
+
disposition: 'regressed',
|
|
136
|
+
pin: '1.0.0',
|
|
137
|
+
availableVersion: 'v1.1.0',
|
|
138
|
+
}],
|
|
139
|
+
}));
|
|
140
|
+
const output = [...logSpy.mock.calls, ...warnSpy.mock.calls].flat().join('\n');
|
|
141
|
+
expect(output).toMatch(/regress/i);
|
|
142
|
+
expect(output).toMatch(/v1\.0\.0/);
|
|
143
|
+
expect(output).toMatch(/v1\.1\.0/);
|
|
144
|
+
expect(output).toMatch(/awm unpin baseline/);
|
|
145
|
+
expect(output).not.toMatch(/Registry baseline updated/i);
|
|
146
|
+
});
|
|
147
|
+
it('un pin que ya coincide con el último release no recomienda unpin', async () => {
|
|
148
|
+
const { runUpdateCore } = require('../../src/commands/update');
|
|
149
|
+
await runUpdateCore({}, deps({
|
|
150
|
+
syncRegistries: async () => [{
|
|
151
|
+
name: 'baseline', action: 'pulled', version: 'v1.1.0', disposition: 'unchanged',
|
|
152
|
+
pin: 'v1.1.0', availableVersion: 'v1.1.0',
|
|
153
|
+
}],
|
|
154
|
+
}));
|
|
155
|
+
const output = [...logSpy.mock.calls, ...warnSpy.mock.calls].flat().join('\n');
|
|
156
|
+
expect(output).toMatch(/already at v1\.1\.0/i);
|
|
157
|
+
expect(output).not.toMatch(/awm unpin baseline/);
|
|
158
|
+
});
|
|
128
159
|
it('un registry que falló y NO dejó contenido en disco falla cerrado', async () => {
|
|
129
160
|
const { runUpdateCore } = require('../../src/commands/update');
|
|
130
161
|
const result = await runUpdateCore({}, deps({
|
|
@@ -0,0 +1,144 @@
|
|
|
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 os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
describe('Context Kernel v1 inspection', () => {
|
|
10
|
+
let root;
|
|
11
|
+
let previousAwmHome;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.resetModules();
|
|
14
|
+
previousAwmHome = process.env.AWM_HOME;
|
|
15
|
+
root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-context-kernel-')));
|
|
16
|
+
process.env.AWM_HOME = path_1.default.join(root, 'awm-home');
|
|
17
|
+
fs_1.default.mkdirSync(process.env.AWM_HOME, { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (previousAwmHome === undefined)
|
|
21
|
+
delete process.env.AWM_HOME;
|
|
22
|
+
else
|
|
23
|
+
process.env.AWM_HOME = previousAwmHome;
|
|
24
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
function inspect() {
|
|
27
|
+
return require('../../../src/core/context-kernel/inspect').inspectContextKernel;
|
|
28
|
+
}
|
|
29
|
+
function indexPath() {
|
|
30
|
+
return path_1.default.join(root, '.awm', 'context', 'index.json');
|
|
31
|
+
}
|
|
32
|
+
function writeIndex(index) {
|
|
33
|
+
fs_1.default.mkdirSync(path_1.default.dirname(indexPath()), { recursive: true });
|
|
34
|
+
fs_1.default.writeFileSync(indexPath(), JSON.stringify(index, null, 2));
|
|
35
|
+
}
|
|
36
|
+
function writeValidKernel() {
|
|
37
|
+
const index = {
|
|
38
|
+
schema: 1,
|
|
39
|
+
kernelFiles: ['AGENTS.md', 'CONSTITUTION.md'],
|
|
40
|
+
maxFixedBytes: 33_740,
|
|
41
|
+
entries: [
|
|
42
|
+
{ id: 'CTX-PROCESS-001', tier: 'kernel', path: 'CONSTITUTION.md', anchor: 'awm-context:CTX-PROCESS-001', when: 'always' },
|
|
43
|
+
{ id: 'CTX-RELEASE-001', tier: 'selective', path: 'docs/awm/context/releases.md', anchor: 'awm-context:CTX-RELEASE-001', when: 'release automation' },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'AGENTS.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\nagent rules\n<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
47
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'CONSTITUTION.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\n<!-- awm-context:CTX-PROCESS-001 -->\nprocess rules\n<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
48
|
+
const card = path_1.default.join(root, 'docs', 'awm', 'context', 'releases.md');
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.dirname(card), { recursive: true });
|
|
50
|
+
fs_1.default.writeFileSync(card, '<!-- awm-context:CTX-RELEASE-001 -->\nrelease rules\n');
|
|
51
|
+
writeIndex(index);
|
|
52
|
+
return index;
|
|
53
|
+
}
|
|
54
|
+
function mutateFixture(mutation) {
|
|
55
|
+
const constitution = path_1.default.join(root, 'CONSTITUTION.md');
|
|
56
|
+
if (mutation === 'start marker missing') {
|
|
57
|
+
fs_1.default.writeFileSync(constitution, fs_1.default.readFileSync(constitution, 'utf8').replace('<!-- AWM:CONTEXT-KERNEL:START v1 -->\n', ''));
|
|
58
|
+
}
|
|
59
|
+
else if (mutation === 'end marker duplicated') {
|
|
60
|
+
fs_1.default.appendFileSync(constitution, '<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
61
|
+
}
|
|
62
|
+
else if (mutation === 'anchor outside region') {
|
|
63
|
+
fs_1.default.writeFileSync(constitution, '<!-- awm-context:CTX-PROCESS-001 -->\n' + fs_1.default.readFileSync(constitution, 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
fs_1.default.writeFileSync(constitution, fs_1.default.readFileSync(constitution, 'utf8').replace('process rules', '<!-- awm-context:CTX-PROCESS-001 -->\nprocess rules'));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
it('classifies no R3 artifacts as legacy', () => {
|
|
70
|
+
expect(inspect()(root)).toEqual({ state: 'legacy' });
|
|
71
|
+
});
|
|
72
|
+
it('rejects a root context marker when the Context Kernel index is missing', () => {
|
|
73
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'AGENTS.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\nagent rules\n');
|
|
74
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({
|
|
75
|
+
state: 'invalid',
|
|
76
|
+
detail: expect.stringContaining('.awm/context/index.json is missing'),
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
it('rejects an orphaned context anchor when the Context Kernel index is missing', () => {
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'AGENTS.md'), '<!-- awm-context:CTX-PROCESS-001 -->\nagent rules\n');
|
|
81
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({
|
|
82
|
+
state: 'invalid',
|
|
83
|
+
detail: expect.stringContaining('.awm/context/index.json is missing'),
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
it('accepts the canonical fixture', () => {
|
|
87
|
+
writeValidKernel();
|
|
88
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({ state: 'valid', schema: 1 }));
|
|
89
|
+
});
|
|
90
|
+
it.each([
|
|
91
|
+
['unknown top-level field', (x) => { x.extra = true; }],
|
|
92
|
+
['future schema', (x) => { x.schema = 2; }],
|
|
93
|
+
['duplicate id', (x) => { x.entries[1].id = x.entries[0].id; }],
|
|
94
|
+
['duplicate anchor', (x) => { x.entries[1].anchor = x.entries[0].anchor; }],
|
|
95
|
+
['absolute path', (x) => { x.entries[1].path = '/tmp/outside.md'; }],
|
|
96
|
+
['traversal', (x) => { x.entries[1].path = '../outside.md'; }],
|
|
97
|
+
['empty when', (x) => { x.entries[1].when = ''; }],
|
|
98
|
+
['fixed byte cap', (x) => { x.maxFixedBytes = 1; }],
|
|
99
|
+
])('rejects %s', (_name, mutate) => {
|
|
100
|
+
const index = writeValidKernel();
|
|
101
|
+
mutate(index);
|
|
102
|
+
writeIndex(index);
|
|
103
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({ state: 'invalid' }));
|
|
104
|
+
});
|
|
105
|
+
it('rejects a project path that resolves through a symlink to an external regular file', () => {
|
|
106
|
+
const index = writeValidKernel();
|
|
107
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-outside-'));
|
|
108
|
+
try {
|
|
109
|
+
const card = path_1.default.join(root, index.entries[1].path);
|
|
110
|
+
const externalCard = path_1.default.join(outside, 'card.md');
|
|
111
|
+
fs_1.default.writeFileSync(externalCard, '<!-- awm-context:CTX-RELEASE-001 -->\nexternal release rules\n');
|
|
112
|
+
fs_1.default.rmSync(card);
|
|
113
|
+
fs_1.default.symlinkSync(externalCard, card);
|
|
114
|
+
expect(fs_1.default.statSync(card).isFile()).toBe(true);
|
|
115
|
+
expect(fs_1.default.realpathSync(card)).toBe(externalCard);
|
|
116
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({
|
|
117
|
+
state: 'invalid',
|
|
118
|
+
detail: expect.stringContaining('resolves outside the project root'),
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
it('rejects a dangling index symlink instead of treating it as legacy', () => {
|
|
126
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-outside-'));
|
|
127
|
+
try {
|
|
128
|
+
fs_1.default.mkdirSync(path_1.default.dirname(indexPath()), { recursive: true });
|
|
129
|
+
fs_1.default.symlinkSync(path_1.default.join(outside, 'index.json'), indexPath());
|
|
130
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({ state: 'invalid' }));
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
it.each(['start marker missing', 'end marker duplicated', 'anchor outside region', 'anchor duplicated'])('rejects %s', (mutation) => {
|
|
137
|
+
writeValidKernel();
|
|
138
|
+
mutateFixture(mutation);
|
|
139
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({ state: 'invalid' }));
|
|
140
|
+
});
|
|
141
|
+
it('throws for an invalid public cwd', () => {
|
|
142
|
+
expect(() => inspect()(path_1.default.join(root, 'missing'))).toThrow(/cwd.*directory/);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -79,7 +79,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
79
79
|
const source = makeSourceRepo(tmpWork, 'alpha');
|
|
80
80
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
81
81
|
const results = await m.syncRegistries();
|
|
82
|
-
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD' }]);
|
|
82
|
+
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD', disposition: 'installed' }]);
|
|
83
83
|
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'))).toBe(true);
|
|
84
84
|
});
|
|
85
85
|
it('pulls an existing clone and reports non-fatal errors per registry', async () => {
|
|
@@ -95,7 +95,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
95
95
|
GIT(source, 'add -A');
|
|
96
96
|
GIT(source, 'commit -qm v2');
|
|
97
97
|
const results = await m.syncRegistries();
|
|
98
|
-
expect(results[0]).toEqual({ name: 'personal', action: 'pulled', version: 'HEAD' });
|
|
98
|
+
expect(results[0]).toEqual({ name: 'personal', action: 'pulled', version: 'HEAD', disposition: 'resolved' });
|
|
99
99
|
expect(results[1].name).toBe('broken');
|
|
100
100
|
expect(results[1].action).toBe('error');
|
|
101
101
|
const synced = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
@@ -127,7 +127,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
127
127
|
GIT(source, 'commit -qm unreleased');
|
|
128
128
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
129
129
|
const results = await m.syncRegistries();
|
|
130
|
-
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
|
|
130
|
+
expect(results).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
|
|
131
131
|
const synced = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
132
132
|
expect(synced).toContain('test skill'); // contenido del tag, no del HEAD
|
|
133
133
|
});
|
|
@@ -157,7 +157,9 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
157
157
|
fs_1.default.mkdirSync(awmDir, { recursive: true });
|
|
158
158
|
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
|
|
159
159
|
const results = await m.syncRegistries();
|
|
160
|
-
expect(results).toEqual([{
|
|
160
|
+
expect(results).toEqual([{
|
|
161
|
+
name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', pin: '1.0.0', availableVersion: 'v1.1.0',
|
|
162
|
+
}]);
|
|
161
163
|
});
|
|
162
164
|
it('baseline sembrado se sincroniza por el mismo loop que cualquier registry', async () => {
|
|
163
165
|
const m = require('../../src/core/registries');
|
|
@@ -167,7 +169,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
167
169
|
m.seedBaselineRegistry();
|
|
168
170
|
delete process.env.AWM_BASE_REMOTE;
|
|
169
171
|
const results = await m.syncRegistries();
|
|
170
|
-
expect(results).toEqual([{ name: 'baseline', action: 'recloned', version: 'v1.0.0' }]);
|
|
172
|
+
expect(results).toEqual([{ name: 'baseline', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
|
|
171
173
|
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/baseline/skills/alpha/SKILL.md'))).toBe(true);
|
|
172
174
|
});
|
|
173
175
|
it('verifyMinCliVersions reporta registries que exigen CLI más nuevo', async () => {
|
|
@@ -193,7 +195,7 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
193
195
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
194
196
|
// primera sync: queda en v1.0.0
|
|
195
197
|
const r1 = await m.syncRegistries();
|
|
196
|
-
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0' }]);
|
|
198
|
+
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.0.0', disposition: 'installed', availableVersion: 'v1.0.0' }]);
|
|
197
199
|
// nueva release en el remote
|
|
198
200
|
fs_1.default.writeFileSync(path_1.default.join(source, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: v2\n---\n');
|
|
199
201
|
GIT(source, 'add -A');
|
|
@@ -201,9 +203,15 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
201
203
|
GIT(source, 'tag v1.1.0');
|
|
202
204
|
// segunda sync: avanza al nuevo tag
|
|
203
205
|
const r2 = await m.syncRegistries();
|
|
204
|
-
expect(r2).toEqual([{
|
|
206
|
+
expect(r2).toEqual([{
|
|
207
|
+
name: 'personal', action: 'pulled', version: 'v1.1.0', disposition: 'advanced', previousVersion: 'v1.0.0', availableVersion: 'v1.1.0',
|
|
208
|
+
}]);
|
|
205
209
|
const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
206
210
|
expect(content).toContain('v2');
|
|
211
|
+
const r3 = await m.syncRegistries();
|
|
212
|
+
expect(r3).toEqual([{
|
|
213
|
+
name: 'personal', action: 'pulled', version: 'v1.1.0', disposition: 'unchanged', previousVersion: 'v1.1.0', availableVersion: 'v1.1.0',
|
|
214
|
+
}]);
|
|
207
215
|
});
|
|
208
216
|
it('rollback tag→tag: clone en v1.1.0 retrocede a v1.0.0 al establecer pin', async () => {
|
|
209
217
|
const m = require('../../src/core/registries');
|
|
@@ -216,13 +224,21 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
216
224
|
m.writeRegistriesConfig([{ name: 'personal', remote: source }]);
|
|
217
225
|
// primera sync sin pin: queda en v1.1.0
|
|
218
226
|
const r1 = await m.syncRegistries();
|
|
219
|
-
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.1.0' }]);
|
|
227
|
+
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'v1.1.0', disposition: 'installed', availableVersion: 'v1.1.0' }]);
|
|
220
228
|
// establece pin a la versión anterior
|
|
221
229
|
const awmDir = path_1.default.join(tmpHome, '.awm');
|
|
222
230
|
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', pins: { personal: '1.0.0' } }));
|
|
223
231
|
// segunda sync con pin: retrocede a v1.0.0
|
|
224
232
|
const r2 = await m.syncRegistries();
|
|
225
|
-
expect(r2).toEqual([{
|
|
233
|
+
expect(r2).toEqual([{
|
|
234
|
+
name: 'personal',
|
|
235
|
+
action: 'pulled',
|
|
236
|
+
version: 'v1.0.0',
|
|
237
|
+
disposition: 'regressed',
|
|
238
|
+
pin: '1.0.0',
|
|
239
|
+
previousVersion: 'v1.1.0',
|
|
240
|
+
availableVersion: 'v1.1.0',
|
|
241
|
+
}]);
|
|
226
242
|
const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
227
243
|
expect(content).toContain('test skill'); // contenido original del tag v1.0.0
|
|
228
244
|
});
|
|
@@ -236,14 +252,16 @@ describe('syncRegistries (git fixtures locales)', () => {
|
|
|
236
252
|
fs_1.default.writeFileSync(path_1.default.join(awmDir, 'preferences.json'), JSON.stringify({ defaultAgent: 'claude-code', installMethod: 'symlink', defaultScope: 'local', channel: 'dev' }));
|
|
237
253
|
// primera sync en canal dev: queda en HEAD (no en el tag)
|
|
238
254
|
const r1 = await m.syncRegistries();
|
|
239
|
-
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD' }]);
|
|
255
|
+
expect(r1).toEqual([{ name: 'personal', action: 'recloned', version: 'HEAD', disposition: 'installed' }]);
|
|
240
256
|
// nuevo commit en el remote (post-tag, no release)
|
|
241
257
|
fs_1.default.writeFileSync(path_1.default.join(source, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: head-2\n---\n');
|
|
242
258
|
GIT(source, 'add -A');
|
|
243
259
|
GIT(source, 'commit -qm head-2');
|
|
244
260
|
// segunda sync: recibe el nuevo commit
|
|
245
261
|
const r2 = await m.syncRegistries();
|
|
246
|
-
expect(r2).toEqual([{
|
|
262
|
+
expect(r2).toEqual([{
|
|
263
|
+
name: 'personal', action: 'pulled', version: 'HEAD', disposition: 'resolved', previousVersion: 'v1.0.0',
|
|
264
|
+
}]);
|
|
247
265
|
const content = fs_1.default.readFileSync(path_1.default.join(tmpHome, '.awm/registries/personal/skills/alpha/SKILL.md'), 'utf-8');
|
|
248
266
|
expect(content).toContain('head-2');
|
|
249
267
|
});
|
|
@@ -99,4 +99,32 @@ describe('registry manifest (awm-registry.json)', () => {
|
|
|
99
99
|
const { readRegistryManifest } = load();
|
|
100
100
|
expect(() => readRegistryManifest(tmpWork)).toThrow(/minCliVersion/);
|
|
101
101
|
});
|
|
102
|
+
it('reads projectContextSchema 1 without changing legacy manifests', () => {
|
|
103
|
+
writeManifest({ minCliVersion: '9.2.1', projectContextSchema: 1 });
|
|
104
|
+
const { readRegistryManifest } = load();
|
|
105
|
+
expect(readRegistryManifest(tmpWork)).toEqual({
|
|
106
|
+
overrides: new Set(), minCliVersion: '9.2.1', projectContextSchema: 1,
|
|
107
|
+
});
|
|
108
|
+
writeManifest({ minCliVersion: '9.2.1' });
|
|
109
|
+
expect(readRegistryManifest(tmpWork).projectContextSchema).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
it.each([0, 2, -1, 1.5, '1', null])('rejects unsupported project context schema %p', (value) => {
|
|
112
|
+
writeManifest({ projectContextSchema: value });
|
|
113
|
+
const { readRegistryManifest } = load();
|
|
114
|
+
expect(() => readRegistryManifest(tmpWork)).toThrow(/projectContextSchema.*exactly 1/);
|
|
115
|
+
});
|
|
116
|
+
it('resolves the first usable Context Kernel declaration and retains malformed manifest diagnostics', () => {
|
|
117
|
+
const { registriesDir, writeRegistriesConfig, resolveProjectContextSchema } = load();
|
|
118
|
+
writeRegistriesConfig([
|
|
119
|
+
{ name: 'broken', remote: 'https://example.invalid/broken.git' },
|
|
120
|
+
{ name: 'active', remote: 'https://example.invalid/active.git' },
|
|
121
|
+
]);
|
|
122
|
+
for (const name of ['broken', 'active'])
|
|
123
|
+
fs_1.default.mkdirSync(path_1.default.join(registriesDir(), name, 'skills'), { recursive: true });
|
|
124
|
+
fs_1.default.writeFileSync(path_1.default.join(registriesDir(), 'broken', 'awm-registry.json'), JSON.stringify({ projectContextSchema: 2 }));
|
|
125
|
+
fs_1.default.writeFileSync(path_1.default.join(registriesDir(), 'active', 'awm-registry.json'), JSON.stringify({ projectContextSchema: 1 }));
|
|
126
|
+
const result = resolveProjectContextSchema();
|
|
127
|
+
expect(result.declaration).toEqual(expect.objectContaining({ registry: expect.objectContaining({ name: 'active' }), schema: 1 }));
|
|
128
|
+
expect(result.diagnostics).toEqual([expect.stringMatching(/broken.*projectContextSchema.*exactly 1/)]);
|
|
129
|
+
});
|
|
102
130
|
});
|
|
@@ -62,16 +62,16 @@ describe('versioning core', () => {
|
|
|
62
62
|
const clone = cloneOf(source, tmpWork, 'clone');
|
|
63
63
|
const { resolveTargetRef } = require('../../src/core/versioning');
|
|
64
64
|
const r = await resolveTargetRef(clone, { channel: 'stable' });
|
|
65
|
-
expect(r).toEqual({ kind: 'tag', ref: 'v1.10.0', version: '1.10.0' });
|
|
65
|
+
expect(r).toEqual({ kind: 'tag', ref: 'v1.10.0', version: '1.10.0', latestVersion: '1.10.0' });
|
|
66
66
|
});
|
|
67
67
|
it('pin exacto gana, con y sin prefijo v', async () => {
|
|
68
68
|
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0', '1.1.0']);
|
|
69
69
|
const clone = cloneOf(source, tmpWork, 'clone');
|
|
70
70
|
const { resolveTargetRef } = require('../../src/core/versioning');
|
|
71
71
|
expect(await resolveTargetRef(clone, { pin: '1.0.0', channel: 'stable' }))
|
|
72
|
-
.toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
|
|
72
|
+
.toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.1.0' });
|
|
73
73
|
expect(await resolveTargetRef(clone, { pin: 'v1.0.0', channel: 'stable' }))
|
|
74
|
-
.toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
|
|
74
|
+
.toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.1.0' });
|
|
75
75
|
});
|
|
76
76
|
it('pin inexistente → error que lista las versiones disponibles', async () => {
|
|
77
77
|
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
@@ -102,7 +102,7 @@ describe('versioning core', () => {
|
|
|
102
102
|
const clone = cloneOf(source, tmpWork, 'clone');
|
|
103
103
|
const { resolveTargetRef } = require('../../src/core/versioning');
|
|
104
104
|
const r = await resolveTargetRef(clone, { channel: 'stable' });
|
|
105
|
-
expect(r).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0' });
|
|
105
|
+
expect(r).toEqual({ kind: 'tag', ref: 'v1.0.0', version: '1.0.0', latestVersion: '1.0.0' });
|
|
106
106
|
});
|
|
107
107
|
it('hace fetch: ve tags creados en el remote después del clone', async () => {
|
|
108
108
|
const source = makeTaggedRepo(tmpWork, 'src', ['1.0.0']);
|
|
@@ -114,7 +114,7 @@ describe('versioning core', () => {
|
|
|
114
114
|
GIT(source, 'tag v1.1.0');
|
|
115
115
|
const { resolveTargetRef } = require('../../src/core/versioning');
|
|
116
116
|
const r = await resolveTargetRef(clone, { channel: 'stable' });
|
|
117
|
-
expect(r).toEqual({ kind: 'tag', ref: 'v1.1.0', version: '1.1.0' });
|
|
117
|
+
expect(r).toEqual({ kind: 'tag', ref: 'v1.1.0', version: '1.1.0', latestVersion: '1.1.0' });
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
120
|
describe('currentVersion', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.0",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@types/js-yaml": "4.0.9",
|
|
56
56
|
"@types/node": "^25.3.0",
|
|
57
57
|
"@types/semver": "^7.7.1",
|
|
58
|
-
"dependency-cruiser": "
|
|
58
|
+
"dependency-cruiser": "16.10.4",
|
|
59
59
|
"eslint": "10.8.1",
|
|
60
60
|
"jest": "^30.2.0",
|
|
61
61
|
"js-yaml": "4.1.0",
|