agentic-workflow-manager 9.2.1 → 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/core/context-kernel/inspect.js +221 -0
- package/dist/src/core/context-kernel/types.js +2 -0
- package/dist/src/core/registries.js +30 -1
- package/dist/tests/commands/preflight/preflight.test.js +145 -0
- package/dist/tests/core/context-kernel/inspect.test.js +144 -0
- package/dist/tests/core/registry-manifest.test.js +28 -0
- 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
|
|
@@ -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;
|
|
@@ -363,7 +364,35 @@ function readRegistryManifest(root) {
|
|
|
363
364
|
}
|
|
364
365
|
minCliVersion = rawMin.replace(/^v/, '');
|
|
365
366
|
}
|
|
366
|
-
|
|
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 };
|
|
367
396
|
}
|
|
368
397
|
/** Nombre del registry dueño de un path: el nombre del clone bajo registriesDir(),
|
|
369
398
|
* o null si no pertenece a ninguno. */
|
|
@@ -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 } } },
|
|
@@ -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
|
+
});
|
|
@@ -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
|
});
|
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",
|