agentic-workflow-manager 9.2.1 → 9.4.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/plan/index.js +98 -0
- package/dist/src/commands/preflight/checks.js +111 -6
- package/dist/src/commands/preflight/index.js +9 -6
- package/dist/src/core/context-kernel/inspect.js +221 -0
- package/dist/src/core/context-kernel/types.js +2 -0
- package/dist/src/core/currentness/check.js +224 -0
- package/dist/src/core/currentness/types.js +2 -0
- package/dist/src/core/plan/json.js +111 -0
- package/dist/src/core/plan/types.js +2 -0
- package/dist/src/core/plan/validate.js +393 -0
- package/dist/src/core/registries.js +30 -1
- package/dist/src/index.js +7 -1
- package/dist/tests/commands/plan/index.test.js +154 -0
- package/dist/tests/commands/preflight/preflight.test.js +238 -2
- package/dist/tests/core/context-kernel/inspect.test.js +144 -0
- package/dist/tests/core/currentness/check.test.js +293 -0
- package/dist/tests/core/plan/validate.test.js +437 -0
- package/dist/tests/core/registry-manifest.test.js +28 -0
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +38 -0
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +37 -0
- package/package.json +2 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkCurrentness = checkCurrentness;
|
|
4
|
+
exports.sanitizeSource = sanitizeSource;
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const util_1 = require("util");
|
|
7
|
+
const cli_version_1 = require("../cli-version");
|
|
8
|
+
const registries_1 = require("../registries");
|
|
9
|
+
const config_1 = require("../../utils/config");
|
|
10
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
11
|
+
const TIMEOUT_MS = 2_000;
|
|
12
|
+
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
13
|
+
const NPM_SOURCE = `https://registry.npmjs.org/${cli_version_1.CLI_PACKAGE_NAME}`;
|
|
14
|
+
const STRICT_SEMVER = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
15
|
+
function deadline(operation, timeoutMs = TIMEOUT_MS) {
|
|
16
|
+
let timer;
|
|
17
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
18
|
+
timer = setTimeout(() => reject(new Error('currentness transport timed out')), timeoutMs);
|
|
19
|
+
});
|
|
20
|
+
return Promise.race([operation, timeout]).finally(() => {
|
|
21
|
+
if (timer)
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function bounded(value, limit = 512) {
|
|
26
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
27
|
+
}
|
|
28
|
+
function authoritativeSource(value) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(value);
|
|
31
|
+
// HTTPS userinfo is a credential-bearing transport URL. Reject it before
|
|
32
|
+
// constructing any Git argv, rather than merely redacting it for output.
|
|
33
|
+
if (url.protocol === 'https:' && url.hostname && !url.username && !url.password && !url.search && !url.hash) {
|
|
34
|
+
return bounded(`https://${url.host}${url.pathname}`);
|
|
35
|
+
}
|
|
36
|
+
if (url.protocol === 'ssh:' && url.hostname && !url.password && !url.search && !url.hash) {
|
|
37
|
+
return bounded(`ssh://${url.host}${url.pathname}`);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// SCP-style Git remotes have no URL parser representation. Permit only an
|
|
43
|
+
// intentionally narrow grammar and remove the optional userinfo; all other
|
|
44
|
+
// schemes fail closed.
|
|
45
|
+
const match = /^(?:[A-Za-z0-9._-]+@)?([A-Za-z0-9.-]+):([A-Za-z0-9._/-]+)$/.exec(value);
|
|
46
|
+
return match
|
|
47
|
+
? bounded(`${match[1]}:${match[2]}`)
|
|
48
|
+
: null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function sanitizeSource(value) {
|
|
52
|
+
return authoritativeSource(value) ?? '[configured remote]';
|
|
53
|
+
}
|
|
54
|
+
function parseVersion(value) {
|
|
55
|
+
const match = STRICT_SEMVER.exec(value.trim());
|
|
56
|
+
if (!match)
|
|
57
|
+
return null;
|
|
58
|
+
return [match[1], match[2], match[3]];
|
|
59
|
+
}
|
|
60
|
+
function compareNumericIdentifier(a, b) {
|
|
61
|
+
if (a.length !== b.length)
|
|
62
|
+
return a.length < b.length ? -1 : 1;
|
|
63
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
function compareStrict(a, b) {
|
|
66
|
+
const av = parseVersion(a);
|
|
67
|
+
const bv = parseVersion(b);
|
|
68
|
+
if (!av || !bv)
|
|
69
|
+
return null;
|
|
70
|
+
return compareNumericIdentifier(av[0], bv[0])
|
|
71
|
+
|| compareNumericIdentifier(av[1], bv[1])
|
|
72
|
+
|| compareNumericIdentifier(av[2], bv[2]);
|
|
73
|
+
}
|
|
74
|
+
function iso(now) {
|
|
75
|
+
const value = now();
|
|
76
|
+
if (!Number.isFinite(value))
|
|
77
|
+
throw new Error('currentness clock must return a finite timestamp');
|
|
78
|
+
return new Date(value).toISOString();
|
|
79
|
+
}
|
|
80
|
+
const defaultGit = async (cwd, args, options) => {
|
|
81
|
+
const result = await execFileAsync('git', args, {
|
|
82
|
+
cwd,
|
|
83
|
+
timeout: options.timeoutMs,
|
|
84
|
+
maxBuffer: options.maxOutputBytes,
|
|
85
|
+
windowsHide: true,
|
|
86
|
+
});
|
|
87
|
+
return result.stdout;
|
|
88
|
+
};
|
|
89
|
+
function unavailable(component, source, checkedAt, installed, pin) {
|
|
90
|
+
return {
|
|
91
|
+
component, installed, latest: null, channel: 'stable', source, ...(pin ? { pin } : {}), checkedAt,
|
|
92
|
+
status: 'unverifiable', detail: 'Authoritative currentness could not be verified.',
|
|
93
|
+
remedy: 'Restore source access and rerun strict preflight.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function latestCli(fetchImpl) {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
99
|
+
try {
|
|
100
|
+
const response = await deadline(fetchImpl(NPM_SOURCE, { signal: controller.signal }));
|
|
101
|
+
if (!response.ok)
|
|
102
|
+
return null;
|
|
103
|
+
const text = await deadline(response.text());
|
|
104
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_OUTPUT_BYTES)
|
|
105
|
+
return null;
|
|
106
|
+
const body = JSON.parse(text);
|
|
107
|
+
if (!body || typeof body !== 'object')
|
|
108
|
+
return null;
|
|
109
|
+
const tags = body['dist-tags'];
|
|
110
|
+
const latest = tags && typeof tags === 'object' ? tags.latest : undefined;
|
|
111
|
+
return typeof latest === 'string' && parseVersion(latest) ? latest : null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function latestStableTag(output) {
|
|
121
|
+
if (Buffer.byteLength(output, 'utf8') > MAX_OUTPUT_BYTES)
|
|
122
|
+
return null;
|
|
123
|
+
const tags = new Map();
|
|
124
|
+
for (const line of output.split(/\r?\n/)) {
|
|
125
|
+
const match = /^([0-9a-fA-F]{40,64})\trefs\/tags\/(v?\d+\.\d+\.\d+)(\^\{\})?$/.exec(line);
|
|
126
|
+
if (!match)
|
|
127
|
+
continue;
|
|
128
|
+
const tag = tags.get(match[2]) ?? { sha: '' };
|
|
129
|
+
if (match[3])
|
|
130
|
+
tag.peeled = match[1];
|
|
131
|
+
else
|
|
132
|
+
tag.sha = match[1];
|
|
133
|
+
tags.set(match[2], tag);
|
|
134
|
+
}
|
|
135
|
+
let latest = null;
|
|
136
|
+
for (const [tag, ref] of tags) {
|
|
137
|
+
if (!ref.sha)
|
|
138
|
+
continue;
|
|
139
|
+
if (!latest || (compareStrict(tag, latest.tag) ?? -1) > 0)
|
|
140
|
+
latest = { sha: ref.peeled ?? ref.sha, tag };
|
|
141
|
+
}
|
|
142
|
+
return latest;
|
|
143
|
+
}
|
|
144
|
+
async function registryComponent(registry, git, prefs, checkedAt) {
|
|
145
|
+
const authorizedSource = authoritativeSource(registry.remote);
|
|
146
|
+
const source = authorizedSource ?? '[configured remote]';
|
|
147
|
+
const pin = prefs.pins?.[registry.name];
|
|
148
|
+
if (!authorizedSource)
|
|
149
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, null, pin);
|
|
150
|
+
let origin;
|
|
151
|
+
let head;
|
|
152
|
+
let exactTag;
|
|
153
|
+
let remoteTags;
|
|
154
|
+
try {
|
|
155
|
+
[origin, head, exactTag, remoteTags] = await Promise.all([
|
|
156
|
+
deadline(git(registry.contentRoot, ['remote', 'get-url', 'origin'], { timeoutMs: TIMEOUT_MS, maxOutputBytes: MAX_OUTPUT_BYTES })),
|
|
157
|
+
deadline(git(registry.contentRoot, ['rev-parse', 'HEAD'], { timeoutMs: TIMEOUT_MS, maxOutputBytes: MAX_OUTPUT_BYTES })),
|
|
158
|
+
deadline(git(registry.contentRoot, ['describe', '--tags', '--exact-match', 'HEAD'], { timeoutMs: TIMEOUT_MS, maxOutputBytes: MAX_OUTPUT_BYTES })),
|
|
159
|
+
deadline(git(registry.contentRoot, ['ls-remote', '--tags', registry.remote], { timeoutMs: TIMEOUT_MS, maxOutputBytes: MAX_OUTPUT_BYTES })),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, null, pin);
|
|
164
|
+
}
|
|
165
|
+
const installed = exactTag.trim();
|
|
166
|
+
const latest = latestStableTag(remoteTags);
|
|
167
|
+
const localHead = head.trim();
|
|
168
|
+
const installedVersion = parseVersion(installed);
|
|
169
|
+
if (!installedVersion || !latest || !/^[0-9a-fA-F]{40,64}$/.test(localHead) || origin.trim() !== registry.remote) {
|
|
170
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, installedVersion ? installed : null, pin);
|
|
171
|
+
}
|
|
172
|
+
const relation = compareStrict(installed, latest.tag);
|
|
173
|
+
if (relation === null)
|
|
174
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, installed, pin);
|
|
175
|
+
if (relation === 0 && localHead === latest.sha) {
|
|
176
|
+
return { component: `registry:${registry.name}`, installed, latest: latest.tag, channel: 'stable', source, ...(pin ? { pin } : {}), checkedAt, status: 'current', detail: 'Exact stable tag and configured origin match the authoritative remote.', remedy: 'No action required.' };
|
|
177
|
+
}
|
|
178
|
+
if (relation < 0) {
|
|
179
|
+
return {
|
|
180
|
+
component: `registry:${registry.name}`, installed, latest: latest.tag, channel: 'stable', source, ...(pin ? { pin } : {}), checkedAt,
|
|
181
|
+
status: pin ? 'pinned-behind' : 'stale', detail: 'Installed stable tag is behind the authoritative remote.',
|
|
182
|
+
remedy: pin ? `awm unpin ${registry.name} && awm update --yes` : 'awm update --yes',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, installed, pin);
|
|
186
|
+
}
|
|
187
|
+
/** Strict, read-only remote currentness check. It intentionally never reads/writes update cache. */
|
|
188
|
+
async function checkCurrentness(cwd, deps = {}) {
|
|
189
|
+
if (!cwd || typeof cwd !== 'string')
|
|
190
|
+
throw new Error('currentness cwd must be a non-empty string');
|
|
191
|
+
const now = deps.now ?? Date.now;
|
|
192
|
+
const checkedAt = iso(now);
|
|
193
|
+
const getCliVersion = deps.cliVersion ?? cli_version_1.cliVersion;
|
|
194
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
195
|
+
const git = deps.git ?? defaultGit;
|
|
196
|
+
const preferences = (deps.readPreferences ?? config_1.readPreferences)();
|
|
197
|
+
const installedCli = getCliVersion();
|
|
198
|
+
const latest = await latestCli(fetchImpl);
|
|
199
|
+
const cli = !parseVersion(installedCli) || !latest || compareStrict(installedCli, latest) === null
|
|
200
|
+
? unavailable('cli', NPM_SOURCE, checkedAt, parseVersion(installedCli) ? installedCli : null)
|
|
201
|
+
: compareStrict(installedCli, latest) === 0
|
|
202
|
+
? { component: 'cli', installed: installedCli, latest, channel: 'stable', source: NPM_SOURCE, checkedAt, status: 'current', detail: 'Installed version equals npm dist-tags.latest.', remedy: 'No action required.' }
|
|
203
|
+
: { component: 'cli', installed: installedCli, latest, channel: 'stable', source: NPM_SOURCE, checkedAt, status: 'stale', detail: 'Installed version is behind npm dist-tags.latest.', remedy: `npm i -g ${cli_version_1.CLI_PACKAGE_NAME}@latest && rerun in a fresh process` };
|
|
204
|
+
const components = [cli];
|
|
205
|
+
let registries;
|
|
206
|
+
try {
|
|
207
|
+
registries = (deps.listRegistries ?? registries_1.listRegistries)();
|
|
208
|
+
if (!Array.isArray(registries))
|
|
209
|
+
throw new Error('currentness registry inventory must be an array');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
components.push({
|
|
213
|
+
...unavailable('registry:inventory', '[configured registry inventory]', checkedAt, null),
|
|
214
|
+
remedy: 'Repair the local registry inventory and rerun strict preflight.',
|
|
215
|
+
});
|
|
216
|
+
return { checkedAt, components, compatibility: { status: 'not-checked' } };
|
|
217
|
+
}
|
|
218
|
+
if (registries.length === 0)
|
|
219
|
+
components.push(unavailable('registry:none', '[configured remote]', checkedAt, null));
|
|
220
|
+
else
|
|
221
|
+
for (const registry of registries)
|
|
222
|
+
components.push(await registryComponent(registry, git, preferences, checkedAt));
|
|
223
|
+
return { checkedAt, components, compatibility: { status: 'not-checked' } };
|
|
224
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseJsonNoDuplicate = parseJsonNoDuplicate;
|
|
4
|
+
/** Parses JSON after rejecting duplicate object keys without runtime dependencies. */
|
|
5
|
+
function parseJsonNoDuplicate(text) {
|
|
6
|
+
if (typeof text !== 'string')
|
|
7
|
+
throw new Error('JSON input must be a string');
|
|
8
|
+
let index = 0;
|
|
9
|
+
const fail = (message) => { throw new Error(`invalid JSON: ${message}`); };
|
|
10
|
+
const whitespace = () => { while (/\s/.test(text[index] ?? ''))
|
|
11
|
+
index++; };
|
|
12
|
+
const string = () => {
|
|
13
|
+
if (text[index++] !== '"')
|
|
14
|
+
fail('expected string');
|
|
15
|
+
let out = '';
|
|
16
|
+
while (index < text.length) {
|
|
17
|
+
const char = text[index++];
|
|
18
|
+
if (char === '"')
|
|
19
|
+
return out;
|
|
20
|
+
if (char === '\\') {
|
|
21
|
+
const escape = text[index++];
|
|
22
|
+
if (!escape)
|
|
23
|
+
fail('unterminated escape');
|
|
24
|
+
if (escape === 'u') {
|
|
25
|
+
const hex = text.slice(index, index + 4);
|
|
26
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
27
|
+
fail('invalid unicode escape');
|
|
28
|
+
out += String.fromCharCode(Number.parseInt(hex, 16));
|
|
29
|
+
index += 4;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const decoded = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
|
|
33
|
+
if (!(escape in decoded))
|
|
34
|
+
fail('invalid escape');
|
|
35
|
+
out += decoded[escape];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
if (char < ' ')
|
|
40
|
+
fail('control character in string');
|
|
41
|
+
out += char;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return fail('unterminated string');
|
|
45
|
+
};
|
|
46
|
+
const value = () => {
|
|
47
|
+
whitespace();
|
|
48
|
+
const char = text[index];
|
|
49
|
+
if (char === '"') {
|
|
50
|
+
string();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (char === '{') {
|
|
54
|
+
object();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (char === '[') {
|
|
58
|
+
array();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (text.startsWith('true', index) || text.startsWith('false', index) || text.startsWith('null', index)) {
|
|
62
|
+
index += text.startsWith('true', index) ? 4 : text.startsWith('false', index) ? 5 : 4;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const match = text.slice(index).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/);
|
|
66
|
+
if (!match)
|
|
67
|
+
return fail('expected value');
|
|
68
|
+
index += match[0].length;
|
|
69
|
+
};
|
|
70
|
+
const array = () => { index++; whitespace(); if (text[index] === ']') {
|
|
71
|
+
index++;
|
|
72
|
+
return;
|
|
73
|
+
} while (true) {
|
|
74
|
+
value();
|
|
75
|
+
whitespace();
|
|
76
|
+
if (text[index] === ']') {
|
|
77
|
+
index++;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (text[index++] !== ',')
|
|
81
|
+
fail('expected comma');
|
|
82
|
+
} };
|
|
83
|
+
const object = () => { index++; const keys = new Set(); whitespace(); if (text[index] === '}') {
|
|
84
|
+
index++;
|
|
85
|
+
return;
|
|
86
|
+
} while (true) {
|
|
87
|
+
whitespace();
|
|
88
|
+
if (text[index] !== '"')
|
|
89
|
+
fail('expected object key');
|
|
90
|
+
const key = string();
|
|
91
|
+
if (keys.has(key))
|
|
92
|
+
fail(`duplicate key ${JSON.stringify(key)}`);
|
|
93
|
+
keys.add(key);
|
|
94
|
+
whitespace();
|
|
95
|
+
if (text[index++] !== ':')
|
|
96
|
+
fail('expected colon');
|
|
97
|
+
value();
|
|
98
|
+
whitespace();
|
|
99
|
+
if (text[index] === '}') {
|
|
100
|
+
index++;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (text[index++] !== ',')
|
|
104
|
+
fail('expected comma');
|
|
105
|
+
} };
|
|
106
|
+
value();
|
|
107
|
+
whitespace();
|
|
108
|
+
if (index !== text.length)
|
|
109
|
+
fail('trailing content');
|
|
110
|
+
return JSON.parse(text);
|
|
111
|
+
}
|