agentic-workflow-manager 9.17.0 → 9.19.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.
Files changed (36) hide show
  1. package/dist/src/commands/env/index.js +192 -0
  2. package/dist/src/commands/plan/index.js +1 -1
  3. package/dist/src/commands/registry/doctor.js +428 -0
  4. package/dist/src/commands/registry/index.js +52 -0
  5. package/dist/src/commands/registry/scaffold.js +140 -0
  6. package/dist/src/commands/sensors/read-only-observation.js +305 -0
  7. package/dist/src/commands/sensors/run.js +23 -60
  8. package/dist/src/commands/watch/index.js +3 -0
  9. package/dist/src/core/admission/index.js +11 -1
  10. package/dist/src/core/currentness/check.js +5 -2
  11. package/dist/src/core/environments/resolve.js +122 -0
  12. package/dist/src/core/environments/store.js +323 -0
  13. package/dist/src/core/facts/catalog.js +138 -0
  14. package/dist/src/core/facts/needs.js +57 -0
  15. package/dist/src/core/facts/types.js +21 -0
  16. package/dist/src/index.js +2 -0
  17. package/dist/tests/commands/env/index.test.js +261 -0
  18. package/dist/tests/commands/registry/doctor.test.js +283 -0
  19. package/dist/tests/commands/registry/scaffold.test.js +186 -0
  20. package/dist/tests/commands/sensors/read-only-observation.test.js +537 -0
  21. package/dist/tests/commands/sensors/run-read-only.test.js +151 -0
  22. package/dist/tests/commands/watch/reopened-196.test.js +9 -2
  23. package/dist/tests/core/admission/index.test.js +60 -0
  24. package/dist/tests/core/currentness/check-insteadof.test.js +120 -0
  25. package/dist/tests/core/currentness/check.test.js +75 -14
  26. package/dist/tests/core/environments/resolve.test.js +347 -0
  27. package/dist/tests/core/environments/store.test.js +248 -0
  28. package/dist/tests/core/facts/catalog.test.js +179 -0
  29. package/dist/tests/core/facts/needs.test.js +88 -0
  30. package/dist/tests/structural/cloud-admission-docs-contract.test.js +54 -0
  31. package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
  32. package/package.json +1 -1
  33. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  34. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  35. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  36. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,192 @@
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.mergeFactDefinitions = mergeFactDefinitions;
7
+ exports.registerEnvCommand = registerEnvCommand;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const readline_1 = __importDefault(require("readline"));
11
+ const catalog_1 = require("../../core/facts/catalog");
12
+ const registries_1 = require("../../core/registries");
13
+ const store_1 = require("../../core/environments/store");
14
+ const resolve_1 = require("../../core/environments/resolve");
15
+ /**
16
+ * Merge facts catalogs from installed registry roots.
17
+ * Later roots may refine a fact, but must not weaken durability: volatile wins
18
+ * over durable so an expired secret cannot be revived by a durable override.
19
+ */
20
+ function mergeFactDefinitions(into, from) {
21
+ for (const [id, next] of Object.entries(from)) {
22
+ const prev = Object.hasOwn(into, id) ? into[id] : undefined;
23
+ if (!prev) {
24
+ into[id] = next;
25
+ continue;
26
+ }
27
+ if (prev.durability === 'volatile' && next.durability === 'durable') {
28
+ const kept = { ...next, durability: 'volatile' };
29
+ if (prev.ttl !== undefined)
30
+ kept.ttl = prev.ttl;
31
+ into[id] = kept;
32
+ continue;
33
+ }
34
+ into[id] = next;
35
+ }
36
+ }
37
+ function loadCatalogFromInstalledRoots() {
38
+ const merged = { schemaVersion: 1, facts: {} };
39
+ for (const root of (0, registries_1.contentRoots)()) {
40
+ const factsPath = path_1.default.join(root, 'facts.json');
41
+ if (!fs_1.default.existsSync(factsPath))
42
+ continue;
43
+ const catalog = (0, catalog_1.parseFactsCatalog)(factsPath);
44
+ mergeFactDefinitions(merged.facts, catalog.facts);
45
+ }
46
+ return merged;
47
+ }
48
+ function defaultConsultedRoots() {
49
+ return (0, registries_1.contentRoots)();
50
+ }
51
+ function requireValue(value, flagHint) {
52
+ if (value === undefined) {
53
+ throw new store_1.EnvironmentStoreError(`${flagHint} requires --value <text> in non-interactive mode (prompt / editable store / --value only; RF-3.3)`);
54
+ }
55
+ return value;
56
+ }
57
+ async function defaultPromptValue(question) {
58
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
59
+ try {
60
+ return await new Promise((resolve) => {
61
+ rl.question(question, (answer) => resolve(answer));
62
+ });
63
+ }
64
+ finally {
65
+ rl.close();
66
+ }
67
+ }
68
+ async function resolveValue(optsValue, flagHint, factId, deps) {
69
+ if (optsValue !== undefined)
70
+ return optsValue;
71
+ if (!deps.stdinIsTTY()) {
72
+ return requireValue(undefined, flagHint);
73
+ }
74
+ return Promise.resolve(deps.promptValue(`Value for ${factId}: `));
75
+ }
76
+ function setFact(factId, rawValue, deps) {
77
+ const catalog = deps.loadCatalog();
78
+ const name = (0, store_1.getActiveEnvironmentName)();
79
+ (0, store_1.writeFact)(name, factId, {
80
+ value: rawValue,
81
+ status: 'fresh',
82
+ updatedAt: new Date().toISOString(),
83
+ }, {
84
+ catalog,
85
+ consultedRoots: deps.consultedRoots(),
86
+ });
87
+ }
88
+ function registerEnvCommand(program, deps = {}) {
89
+ const loadCatalog = deps.loadCatalog ?? loadCatalogFromInstalledRoots;
90
+ const consultedRoots = deps.consultedRoots ?? defaultConsultedRoots;
91
+ const now = deps.now ?? (() => new Date());
92
+ const stdinIsTTY = deps.stdinIsTTY ?? (() => Boolean(process.stdin.isTTY));
93
+ const promptValue = deps.promptValue ?? defaultPromptValue;
94
+ const env = program.command('env').description('manage machine-local operator environments (facts store)');
95
+ env.command('status')
96
+ .description('show active environment and fact statuses vs catalog')
97
+ .action(() => {
98
+ const name = (0, store_1.getActiveEnvironmentName)();
99
+ const catalog = loadCatalog();
100
+ const store = (0, store_1.readValues)(name);
101
+ const statuses = (0, resolve_1.statusFor)(catalog, store, now());
102
+ const lines = [
103
+ `Active environment: ${name}`,
104
+ `Store: ${(0, store_1.valuesPath)(name)}`,
105
+ '',
106
+ 'Facts:',
107
+ ];
108
+ const ids = Object.keys(catalog.facts).sort();
109
+ if (ids.length === 0) {
110
+ lines.push(' (no facts in consulted catalogs)');
111
+ }
112
+ else {
113
+ for (const id of ids) {
114
+ lines.push(` ${id}: ${statuses[id]}`);
115
+ }
116
+ }
117
+ process.stdout.write(lines.join('\n') + '\n');
118
+ });
119
+ env.command('set <factId>')
120
+ .description('set a fact value in the active environment (--value for non-interactive)')
121
+ .option('--value <text>', 'fact value (required non-interactive; never runs registry commands)')
122
+ .action(async (factId, opts) => {
123
+ const value = await resolveValue(opts.value, 'awm env set', factId, {
124
+ stdinIsTTY,
125
+ promptValue,
126
+ });
127
+ setFact(factId, value, { loadCatalog, consultedRoots });
128
+ process.stdout.write(`Set ${factId} in environment ${(0, store_1.getActiveEnvironmentName)()}\n`);
129
+ });
130
+ env.command('edit')
131
+ .description('print the absolute path of the active environment values.json')
132
+ .action(() => {
133
+ const name = (0, store_1.getActiveEnvironmentName)();
134
+ const file = (0, store_1.valuesPath)(name);
135
+ process.stdout.write(`${file}\n`);
136
+ });
137
+ env.command('refresh [factId]')
138
+ .description('re-set a fact value (prompt / --value only; no registry commands)')
139
+ .option('--value <text>', 'new value (required non-interactive)')
140
+ .option('--all', 'refresh every fact present in the store (interactive prompts, or pass factId)')
141
+ .action(async (factId, opts) => {
142
+ if (opts.all && !factId) {
143
+ if (!stdinIsTTY()) {
144
+ throw new store_1.EnvironmentStoreError('awm env refresh --all requires a TTY (or pass <factId> with --value in non-interactive mode)');
145
+ }
146
+ const name = (0, store_1.getActiveEnvironmentName)();
147
+ const ids = Object.keys((0, store_1.readValues)(name));
148
+ if (ids.length === 0) {
149
+ throw new store_1.EnvironmentStoreError('awm env refresh --all: active store has no facts');
150
+ }
151
+ for (const id of ids) {
152
+ const value = await Promise.resolve(promptValue(`Value for ${id}: `));
153
+ setFact(id, value, { loadCatalog, consultedRoots });
154
+ process.stdout.write(`Refreshed ${id} in environment ${(0, store_1.getActiveEnvironmentName)()}\n`);
155
+ }
156
+ return;
157
+ }
158
+ if (!factId) {
159
+ throw new store_1.EnvironmentStoreError('awm env refresh requires <factId> (or interactive --all)');
160
+ }
161
+ const value = await resolveValue(opts.value, 'awm env refresh', factId, {
162
+ stdinIsTTY,
163
+ promptValue,
164
+ });
165
+ setFact(factId, value, { loadCatalog, consultedRoots });
166
+ process.stdout.write(`Refreshed ${factId} in environment ${(0, store_1.getActiveEnvironmentName)()}\n`);
167
+ });
168
+ env.command('use <name>')
169
+ .description('switch the active environment (one active per machine)')
170
+ .action((name) => {
171
+ (0, store_1.setActiveEnvironmentName)(name);
172
+ process.stdout.write(`Active environment: ${name}\n`);
173
+ });
174
+ env.command('inject')
175
+ .description('print JSON injection payload for the active environment (default mode: reference)')
176
+ .option('--mode <mode>', 'injection mode: reference|context', 'reference')
177
+ .action((opts) => {
178
+ const rawMode = opts.mode ?? 'reference';
179
+ const mode = rawMode === 'reference' || rawMode === 'context' ? rawMode : 'reference';
180
+ const name = (0, store_1.getActiveEnvironmentName)();
181
+ const catalog = loadCatalog();
182
+ const store = (0, store_1.readValues)(name);
183
+ const result = (0, resolve_1.buildInjection)({
184
+ catalog,
185
+ store,
186
+ storePath: (0, store_1.valuesPath)(name),
187
+ inject: mode,
188
+ now: now(),
189
+ });
190
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
191
+ });
192
+ }
@@ -283,7 +283,7 @@ function registerPlanCommand(program, deps = { validatePlanFile: validate_1.vali
283
283
  .requiredOption('--cwd <path>', 'repository root for plan containment and source resolution')
284
284
  .option('--execution-mode <mode>', 'explicit override: interactivo or desatendido')
285
285
  .option('--require-current', 'require authoritative consumed-contract currentness')
286
- .option('--verify-sensors', 'require an empirical sensor pass')
286
+ .option('--verify-sensors', 'require an empirical sensor pass; read-only observation skips git-ignored build output (see .awm/admission.json)')
287
287
  .option('--runtime-kind <kind>', 'routing runtime kind for compact v2')
288
288
  .option('--runtime-version <version>', 'routing runtime version for compact v2')
289
289
  .option('--account-scope-digest <sha>', 'routing account scope digest for compact v2')
@@ -0,0 +1,428 @@
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.doctorExitCode = doctorExitCode;
7
+ exports.doctorRegistry = doctorRegistry;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const catalog_1 = require("../../core/facts/catalog");
11
+ const needs_1 = require("../../core/facts/needs");
12
+ const types_1 = require("../../core/facts/types");
13
+ const bundles_1 = require("../../core/bundles");
14
+ const registries_1 = require("../../core/registries");
15
+ const versioning_1 = require("../../core/versioning");
16
+ /** Exit code for CLI: structural failures → 1; advisories alone → 0. */
17
+ function doctorExitCode(report) {
18
+ return report.structural.length ? 1 : 0;
19
+ }
20
+ /** Capability ids the doctor understands in v1; others are advisory (forward-compatible). */
21
+ const KNOWN_CAPABILITY_IDS = new Set(['facts']);
22
+ const CAPABILITIES_HEADER_RE = /^\|\s*id\s*\|\s*status\s*\|\s*notes\s*\|$/i;
23
+ const HOME_PATH_RE = /(^|[^\w])~(\/|\\)/;
24
+ const USERS_PATH_RE = /\/Users\/[^/\s]+/;
25
+ const ADVISORY_CAP = 50;
26
+ const STORE_BASENAMES = new Set(['values.json', 'meta.json']);
27
+ function isMissing(error) {
28
+ return error?.code === 'ENOENT';
29
+ }
30
+ function walkRegularFiles(root, visit) {
31
+ const stack = [root];
32
+ while (stack.length > 0) {
33
+ const dir = stack.pop();
34
+ let entries;
35
+ try {
36
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
37
+ }
38
+ catch {
39
+ continue;
40
+ }
41
+ for (const entry of entries) {
42
+ if (entry.name === '.git' || entry.name === 'node_modules')
43
+ continue;
44
+ const abs = path_1.default.join(dir, entry.name);
45
+ let stat;
46
+ try {
47
+ stat = fs_1.default.lstatSync(abs);
48
+ }
49
+ catch {
50
+ continue;
51
+ }
52
+ if (stat.isSymbolicLink())
53
+ continue;
54
+ if (stat.isDirectory()) {
55
+ stack.push(abs);
56
+ continue;
57
+ }
58
+ if (stat.isFile()) {
59
+ visit(abs, path_1.default.relative(root, abs));
60
+ }
61
+ }
62
+ }
63
+ }
64
+ function parseCapabilitiesIndex(markdown) {
65
+ const lines = markdown.split(/\r?\n/);
66
+ const headingIdx = lines.findIndex((l) => /^##\s+Capabilities\s*$/i.test(l.trim()));
67
+ if (headingIdx === -1) {
68
+ return { rows: [], errors: ['REGISTRY.md missing ## Capabilities section'] };
69
+ }
70
+ let headerIdx = -1;
71
+ for (let i = headingIdx + 1; i < lines.length; i++) {
72
+ const line = lines[i].trim();
73
+ if (/^##\s+/.test(line))
74
+ break;
75
+ if (CAPABILITIES_HEADER_RE.test(line)) {
76
+ headerIdx = i;
77
+ break;
78
+ }
79
+ }
80
+ if (headerIdx === -1) {
81
+ return {
82
+ rows: [],
83
+ errors: ['REGISTRY.md Capabilities index missing | id | status | notes | header'],
84
+ };
85
+ }
86
+ const rows = [];
87
+ const errors = [];
88
+ const seen = new Set();
89
+ for (let i = headerIdx + 1; i < lines.length; i++) {
90
+ const line = lines[i].trim();
91
+ if (line === '')
92
+ continue;
93
+ if (/^##\s+/.test(line))
94
+ break;
95
+ if (!line.startsWith('|'))
96
+ continue;
97
+ if (/^\|\s*-+/.test(line))
98
+ continue; // separator row
99
+ const cells = line
100
+ .replace(/^\|/, '')
101
+ .replace(/\|$/, '')
102
+ .split('|')
103
+ .map((c) => c.trim());
104
+ if (cells.length < 3) {
105
+ errors.push(`Capabilities row malformed: ${line}`);
106
+ continue;
107
+ }
108
+ const id = cells[0];
109
+ if (id === '') {
110
+ errors.push('Capabilities table has empty id');
111
+ continue;
112
+ }
113
+ if (seen.has(id)) {
114
+ errors.push(`Capabilities table duplicate id "${id}"`);
115
+ continue;
116
+ }
117
+ seen.add(id);
118
+ rows.push({ id, status: cells[1] ?? '', notes: cells[2] ?? '' });
119
+ }
120
+ return { rows, errors };
121
+ }
122
+ /** Collect readable regular SKILL.md paths; symlink / non-regular / unreadable → structural. */
123
+ function collectSkillMdPaths(root, structural) {
124
+ const skillsRoot = path_1.default.join(root, 'skills');
125
+ const out = [];
126
+ if (!fs_1.default.existsSync(skillsRoot))
127
+ return out;
128
+ let entries;
129
+ try {
130
+ entries = fs_1.default.readdirSync(skillsRoot, { withFileTypes: true });
131
+ }
132
+ catch (e) {
133
+ structural.push(`Cannot read skills/: ${e instanceof Error ? e.message : String(e)}`);
134
+ return out;
135
+ }
136
+ for (const entry of entries) {
137
+ const skillDir = path_1.default.join(skillsRoot, entry.name);
138
+ const relDir = path_1.default.relative(root, skillDir);
139
+ let dirStat;
140
+ try {
141
+ dirStat = fs_1.default.lstatSync(skillDir);
142
+ }
143
+ catch (e) {
144
+ structural.push(`Cannot inspect skill path ${relDir}: ${e instanceof Error ? e.message : String(e)}`);
145
+ continue;
146
+ }
147
+ if (dirStat.isSymbolicLink()) {
148
+ structural.push(`Skill path ${relDir} must not be a symbolic link (fail closed)`);
149
+ continue;
150
+ }
151
+ if (!dirStat.isDirectory()) {
152
+ // Scaffold tracks empty skills/ with .gitkeep so git clone keeps the
153
+ // content dir (RF-4.1). Other loose files remain structural.
154
+ if (entry.name === '.gitkeep' && dirStat.isFile())
155
+ continue;
156
+ structural.push(`Skill path ${relDir} must be a regular directory (fail closed)`);
157
+ continue;
158
+ }
159
+ const skillMd = path_1.default.join(skillDir, 'SKILL.md');
160
+ const relMd = path_1.default.relative(root, skillMd);
161
+ let mdStat;
162
+ try {
163
+ mdStat = fs_1.default.lstatSync(skillMd);
164
+ }
165
+ catch (error) {
166
+ if (isMissing(error))
167
+ continue;
168
+ structural.push(`Cannot inspect ${relMd}: ${error instanceof Error ? error.message : String(error)}`);
169
+ continue;
170
+ }
171
+ if (mdStat.isSymbolicLink()) {
172
+ structural.push(`${relMd} must not be a symbolic link (fail closed)`);
173
+ continue;
174
+ }
175
+ if (!mdStat.isFile()) {
176
+ structural.push(`${relMd} must be a regular file (fail closed)`);
177
+ continue;
178
+ }
179
+ try {
180
+ fs_1.default.accessSync(skillMd, fs_1.default.constants.R_OK);
181
+ }
182
+ catch (e) {
183
+ structural.push(`Unreadable ${relMd}: ${e instanceof Error ? e.message : String(e)} (fail closed)`);
184
+ continue;
185
+ }
186
+ out.push(skillMd);
187
+ }
188
+ return out;
189
+ }
190
+ /**
191
+ * RF-4.5: store files under the registry tree are structural.
192
+ * Symlink entries named `environments`, or basename `values.json`/`meta.json`, are structural
193
+ * even when the walk does not follow the link.
194
+ */
195
+ function scanEnvStoreInTree(root, structural) {
196
+ const stack = [root];
197
+ while (stack.length > 0) {
198
+ const dir = stack.pop();
199
+ let entries;
200
+ try {
201
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
202
+ }
203
+ catch {
204
+ continue;
205
+ }
206
+ for (const entry of entries) {
207
+ if (entry.name === '.git' || entry.name === 'node_modules')
208
+ continue;
209
+ const abs = path_1.default.join(dir, entry.name);
210
+ const rel = path_1.default.relative(root, abs);
211
+ let stat;
212
+ try {
213
+ stat = fs_1.default.lstatSync(abs);
214
+ }
215
+ catch {
216
+ continue;
217
+ }
218
+ if (stat.isSymbolicLink()) {
219
+ if (entry.name === 'environments' || STORE_BASENAMES.has(entry.name)) {
220
+ structural.push(`Environment store symlink must not live in the registry tree: ${rel} (RF-4.5)`);
221
+ }
222
+ continue;
223
+ }
224
+ if (stat.isDirectory()) {
225
+ stack.push(abs);
226
+ continue;
227
+ }
228
+ if (stat.isFile()) {
229
+ const parts = rel.split(/[/\\]/);
230
+ if (parts.includes('environments') && STORE_BASENAMES.has(entry.name)) {
231
+ structural.push(`Environment store file must not live in the registry tree: ${rel} (RF-4.5)`);
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ function scanProsePathHints(root, advisories) {
238
+ const skillsRoot = path_1.default.join(root, 'skills');
239
+ if (!fs_1.default.existsSync(skillsRoot))
240
+ return;
241
+ walkRegularFiles(skillsRoot, (abs, rel) => {
242
+ if (!abs.endsWith('.md'))
243
+ return;
244
+ if (advisories.length >= ADVISORY_CAP)
245
+ return;
246
+ let text;
247
+ try {
248
+ text = fs_1.default.readFileSync(abs, 'utf-8');
249
+ }
250
+ catch {
251
+ return;
252
+ }
253
+ for (const line of text.split(/\r?\n/)) {
254
+ if (advisories.length >= ADVISORY_CAP)
255
+ return;
256
+ if (HOME_PATH_RE.test(line) || USERS_PATH_RE.test(line)) {
257
+ advisories.push(`Possible machine path in ${rel}: prefer a fact (e.g. ~/Developments or /Users/…)`);
258
+ break;
259
+ }
260
+ }
261
+ });
262
+ }
263
+ function checkVersionCoherence(root, structural) {
264
+ let entries;
265
+ try {
266
+ entries = (0, bundles_1.readCatalog)(root);
267
+ }
268
+ catch (e) {
269
+ structural.push(`catalog.json unreadable: ${e instanceof Error ? e.message : String(e)}`);
270
+ return;
271
+ }
272
+ for (const entry of entries) {
273
+ if (typeof entry.version !== 'string')
274
+ continue;
275
+ const bundlePath = path_1.default.join(root, entry.source, 'bundle.json');
276
+ let stat;
277
+ try {
278
+ stat = fs_1.default.lstatSync(bundlePath);
279
+ }
280
+ catch (error) {
281
+ if (isMissing(error))
282
+ continue;
283
+ structural.push(`Cannot inspect ${path_1.default.relative(root, bundlePath)}`);
284
+ continue;
285
+ }
286
+ if (!stat.isFile() || stat.isSymbolicLink())
287
+ continue;
288
+ let raw;
289
+ try {
290
+ raw = JSON.parse(fs_1.default.readFileSync(bundlePath, 'utf-8'));
291
+ }
292
+ catch (e) {
293
+ structural.push(`bundle.json invalid at ${path_1.default.relative(root, bundlePath)}: ${e instanceof Error ? e.message : String(e)}`);
294
+ continue;
295
+ }
296
+ const bundleVersion = raw !== null && typeof raw === 'object' && typeof raw.version === 'string'
297
+ ? raw.version
298
+ : undefined;
299
+ if (bundleVersion !== undefined && bundleVersion !== entry.version) {
300
+ structural.push(`Bundle "${entry.name}" version skew: catalog=${entry.version} bundle.json=${bundleVersion} (RF-4.7)`);
301
+ }
302
+ }
303
+ }
304
+ /**
305
+ * Structural + advisory audit of a registry tree. Path-only API (RF-5.1) — schema/doctor are the gates.
306
+ */
307
+ function doctorRegistry(root) {
308
+ const structural = [];
309
+ const advisories = [];
310
+ if (typeof root !== 'string' || root.trim() === '') {
311
+ return {
312
+ structural: ['Registry root must be a non-empty string'],
313
+ advisories,
314
+ };
315
+ }
316
+ const resolvedRoot = path_1.default.resolve(root);
317
+ if (!fs_1.default.existsSync(resolvedRoot) || !fs_1.default.statSync(resolvedRoot).isDirectory()) {
318
+ return { structural: [`Registry root does not exist or is not a directory: ${resolvedRoot}`], advisories };
319
+ }
320
+ let manifestMin;
321
+ try {
322
+ const manifest = (0, registries_1.readRegistryManifest)(resolvedRoot);
323
+ manifestMin = manifest.minCliVersion;
324
+ }
325
+ catch (e) {
326
+ structural.push(`awm-registry.json invalid: ${e instanceof Error ? e.message : String(e)}`);
327
+ }
328
+ const handbookPath = path_1.default.join(resolvedRoot, 'REGISTRY.md');
329
+ if (!fs_1.default.existsSync(handbookPath)) {
330
+ structural.push('missing REGISTRY.md');
331
+ }
332
+ else {
333
+ let handbook;
334
+ try {
335
+ handbook = fs_1.default.readFileSync(handbookPath, 'utf-8');
336
+ }
337
+ catch (e) {
338
+ structural.push(`Cannot read REGISTRY.md: ${e instanceof Error ? e.message : String(e)}`);
339
+ handbook = '';
340
+ }
341
+ if (handbook) {
342
+ const caps = parseCapabilitiesIndex(handbook);
343
+ structural.push(...caps.errors);
344
+ for (const row of caps.rows) {
345
+ if (!KNOWN_CAPABILITY_IDS.has(row.id)) {
346
+ if (advisories.length < ADVISORY_CAP) {
347
+ advisories.push(`Unknown capability id "${row.id}" in REGISTRY.md (advisory; RF-4.6)`);
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ checkVersionCoherence(resolvedRoot, structural);
354
+ scanEnvStoreInTree(resolvedRoot, structural);
355
+ const factsPath = path_1.default.join(resolvedRoot, 'facts.json');
356
+ let catalog = null;
357
+ let factsPresent = false;
358
+ try {
359
+ const factsStat = fs_1.default.lstatSync(factsPath);
360
+ factsPresent = true;
361
+ if (factsStat.isSymbolicLink()) {
362
+ structural.push('facts.json must not be a symbolic link (regular-file guard)');
363
+ }
364
+ else if (!factsStat.isFile()) {
365
+ structural.push('facts.json must be a regular file');
366
+ }
367
+ else {
368
+ try {
369
+ catalog = (0, catalog_1.parseFactsCatalog)(factsPath);
370
+ }
371
+ catch (e) {
372
+ structural.push(`facts.json invalid: ${e instanceof Error ? e.message : String(e)}`);
373
+ }
374
+ }
375
+ }
376
+ catch (error) {
377
+ if (!isMissing(error)) {
378
+ structural.push(`Cannot inspect facts.json: ${error instanceof Error ? error.message : String(error)}`);
379
+ }
380
+ }
381
+ const skillPaths = collectSkillMdPaths(resolvedRoot, structural);
382
+ let anyNeeds = false;
383
+ for (const skillMd of skillPaths) {
384
+ let needs;
385
+ try {
386
+ needs = (0, needs_1.readSkillNeeds)(skillMd);
387
+ }
388
+ catch (e) {
389
+ if (e instanceof types_1.SkillNeedsError) {
390
+ structural.push(e.message);
391
+ continue;
392
+ }
393
+ structural.push(`Cannot read needs from ${path_1.default.relative(resolvedRoot, skillMd)}`);
394
+ continue;
395
+ }
396
+ if (needs.length === 0)
397
+ continue;
398
+ anyNeeds = true;
399
+ if (!catalog) {
400
+ const reason = factsPresent
401
+ ? 'facts.json is invalid/unreadable'
402
+ : 'facts.json is missing';
403
+ structural.push(`Skill ${path_1.default.relative(resolvedRoot, skillMd)} declares needs but ${reason} (fail closed)`);
404
+ continue;
405
+ }
406
+ const orphans = (0, needs_1.findOrphanNeeds)(needs, catalog);
407
+ for (const id of orphans) {
408
+ structural.push(`Orphan need "${id}" in ${path_1.default.relative(resolvedRoot, skillMd)} — not in facts.json (RF-1.3)`);
409
+ }
410
+ }
411
+ if (factsPresent || anyNeeds) {
412
+ if (!manifestMin) {
413
+ structural.push(`minCliVersion required when facts/needs are present (must be >= ${types_1.FACTS_MIN_CLI_VERSION}) (RF-4.8)`);
414
+ }
415
+ else {
416
+ try {
417
+ if ((0, versioning_1.compareSemver)(manifestMin, types_1.FACTS_MIN_CLI_VERSION) < 0) {
418
+ structural.push(`minCliVersion ${manifestMin} is below FACTS_MIN_CLI_VERSION ${types_1.FACTS_MIN_CLI_VERSION} (RF-4.8)`);
419
+ }
420
+ }
421
+ catch (e) {
422
+ structural.push(`minCliVersion unreadable for facts gate: ${e instanceof Error ? e.message : String(e)}`);
423
+ }
424
+ }
425
+ }
426
+ scanProsePathHints(resolvedRoot, advisories);
427
+ return { structural, advisories };
428
+ }