agentic-workflow-manager 9.16.1 → 9.18.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 (54) hide show
  1. package/dist/src/commands/env/index.js +192 -0
  2. package/dist/src/commands/preflight/checks.js +63 -13
  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/bootstrap.js +2 -1
  7. package/dist/src/commands/sensors/compatibility/contract.js +30 -26
  8. package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
  9. package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
  10. package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
  11. package/dist/src/commands/sensors/compatibility/source.js +3 -2
  12. package/dist/src/commands/sensors/coverage/render.js +8 -2
  13. package/dist/src/commands/sensors/coverage/resolve.js +7 -0
  14. package/dist/src/commands/sensors/init.js +15 -2
  15. package/dist/src/commands/sensors/migrate.js +13 -10
  16. package/dist/src/commands/sensors/prepare.js +53 -6
  17. package/dist/src/commands/sensors/project.js +60 -6
  18. package/dist/src/commands/sensors/result.js +18 -3
  19. package/dist/src/commands/sensors/run.js +124 -16
  20. package/dist/src/commands/sensors/status.js +163 -18
  21. package/dist/src/commands/watch/index.js +3 -0
  22. package/dist/src/core/environments/resolve.js +122 -0
  23. package/dist/src/core/environments/store.js +323 -0
  24. package/dist/src/core/facts/catalog.js +138 -0
  25. package/dist/src/core/facts/needs.js +57 -0
  26. package/dist/src/core/facts/types.js +21 -0
  27. package/dist/src/core/init/steps.js +5 -1
  28. package/dist/src/index.js +2 -0
  29. package/dist/tests/commands/env/index.test.js +261 -0
  30. package/dist/tests/commands/preflight/preflight.test.js +121 -0
  31. package/dist/tests/commands/registry/doctor.test.js +283 -0
  32. package/dist/tests/commands/registry/scaffold.test.js +186 -0
  33. package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
  34. package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
  35. package/dist/tests/commands/sensors/init.test.js +107 -0
  36. package/dist/tests/commands/sensors/migrate.test.js +4 -3
  37. package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
  38. package/dist/tests/commands/sensors/prepare.test.js +62 -0
  39. package/dist/tests/commands/sensors/result.test.js +66 -0
  40. package/dist/tests/commands/sensors/run.test.js +331 -0
  41. package/dist/tests/commands/sensors/status.test.js +204 -0
  42. package/dist/tests/commands/watch/reopened-196.test.js +9 -2
  43. package/dist/tests/core/environments/resolve.test.js +347 -0
  44. package/dist/tests/core/environments/store.test.js +248 -0
  45. package/dist/tests/core/facts/catalog.test.js +179 -0
  46. package/dist/tests/core/facts/needs.test.js +88 -0
  47. package/dist/tests/structural/native-release-artifacts.test.js +4 -2
  48. package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
  49. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -0
  50. package/package.json +1 -1
  51. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  52. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  53. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  54. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,323 @@
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.EnvironmentStoreError = void 0;
7
+ exports.environmentsRoot = environmentsRoot;
8
+ exports.envDir = envDir;
9
+ exports.valuesPath = valuesPath;
10
+ exports.getActiveEnvironmentName = getActiveEnvironmentName;
11
+ exports.setActiveEnvironmentName = setActiveEnvironmentName;
12
+ exports.readValues = readValues;
13
+ exports.isInsideRegistryTree = isInsideRegistryTree;
14
+ exports.assertNotInsideRegistryTree = assertNotInsideRegistryTree;
15
+ exports.writeEnvValue = writeEnvValue;
16
+ exports.writeFact = writeFact;
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const atomic_file_1 = require("../atomic-file");
20
+ const paths_1 = require("../paths");
21
+ const registries_1 = require("../registries");
22
+ class EnvironmentStoreError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = 'EnvironmentStoreError';
26
+ }
27
+ }
28
+ exports.EnvironmentStoreError = EnvironmentStoreError;
29
+ const DEFAULT_ENV_NAME = 'default';
30
+ const META_FILE = 'meta.json';
31
+ const VALUES_FILE = 'values.json';
32
+ function isRecord(value) {
33
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
34
+ }
35
+ /** Root of machine-local environment stores: `AWM_HOME/environments`. */
36
+ function environmentsRoot() {
37
+ return path_1.default.join((0, paths_1.awmHome)(), 'environments');
38
+ }
39
+ function envDir(name) {
40
+ assertEnvName(name);
41
+ return path_1.default.join(environmentsRoot(), name);
42
+ }
43
+ function valuesPath(name) {
44
+ return path_1.default.join(envDir(name), VALUES_FILE);
45
+ }
46
+ function metaPath() {
47
+ return path_1.default.join(environmentsRoot(), META_FILE);
48
+ }
49
+ function assertEnvName(name) {
50
+ if (typeof name !== 'string' || name.trim() === '') {
51
+ throw new EnvironmentStoreError('environment name must be a non-empty string');
52
+ }
53
+ if (name.includes('\0')) {
54
+ throw new EnvironmentStoreError(`Invalid environment name ${JSON.stringify(name)}: NUL byte`);
55
+ }
56
+ const lower = name.toLowerCase();
57
+ if (lower === META_FILE.toLowerCase() || lower === VALUES_FILE.toLowerCase()) {
58
+ throw new EnvironmentStoreError(`Invalid environment name ${JSON.stringify(name)}: reserved store filename`);
59
+ }
60
+ if (name.includes('/') || name.includes('\\') || name.includes('..') || name === '.' || name === '..') {
61
+ throw new EnvironmentStoreError(`Invalid environment name ${JSON.stringify(name)}`);
62
+ }
63
+ }
64
+ function readMeta() {
65
+ const file = metaPath();
66
+ if (!fs_1.default.existsSync(file))
67
+ return null;
68
+ let raw;
69
+ try {
70
+ raw = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
71
+ }
72
+ catch (e) {
73
+ throw new EnvironmentStoreError(`Invalid environments meta at ${file}: ${e instanceof Error ? e.message : String(e)}`);
74
+ }
75
+ if (!isRecord(raw) || typeof raw.active !== 'string' || raw.active.trim() === '') {
76
+ throw new EnvironmentStoreError(`Invalid environments meta at ${file}: expected { active: string }`);
77
+ }
78
+ return { active: raw.active };
79
+ }
80
+ function writeMeta(meta) {
81
+ const file = metaPath();
82
+ assertNotInsideRegistryTree(file);
83
+ (0, atomic_file_1.writeFileAtomic)(file, JSON.stringify(meta, null, 2) + '\n');
84
+ }
85
+ /** Exactly one active environment per machine; default name `default` (RF-2.2). */
86
+ function getActiveEnvironmentName() {
87
+ const meta = readMeta();
88
+ if (!meta)
89
+ return DEFAULT_ENV_NAME;
90
+ assertEnvName(meta.active);
91
+ return meta.active;
92
+ }
93
+ function setActiveEnvironmentName(name) {
94
+ assertEnvName(name);
95
+ writeMeta({ active: name });
96
+ }
97
+ function parseStoredEntry(id, raw) {
98
+ if (!isRecord(raw)) {
99
+ throw new EnvironmentStoreError(`values.${id} must be an object`);
100
+ }
101
+ if (typeof raw.value !== 'string') {
102
+ throw new EnvironmentStoreError(`values.${id}.value must be a string`);
103
+ }
104
+ if (raw.status !== 'fresh' && raw.status !== 'stale' && raw.status !== 'missing') {
105
+ throw new EnvironmentStoreError(`values.${id}.status must be fresh|stale|missing`);
106
+ }
107
+ if (typeof raw.updatedAt !== 'string') {
108
+ throw new EnvironmentStoreError(`values.${id}.updatedAt must be a string`);
109
+ }
110
+ const entry = {
111
+ value: raw.value,
112
+ status: raw.status,
113
+ updatedAt: raw.updatedAt,
114
+ };
115
+ if (raw.expiresAt !== undefined) {
116
+ if (typeof raw.expiresAt !== 'string') {
117
+ throw new EnvironmentStoreError(`values.${id}.expiresAt must be a string`);
118
+ }
119
+ entry.expiresAt = raw.expiresAt;
120
+ }
121
+ return entry;
122
+ }
123
+ function readValues(name) {
124
+ const file = valuesPath(name);
125
+ if (!fs_1.default.existsSync(file))
126
+ return {};
127
+ let raw;
128
+ try {
129
+ raw = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
130
+ }
131
+ catch (e) {
132
+ throw new EnvironmentStoreError(`Invalid values.json at ${file}: ${e instanceof Error ? e.message : String(e)}`);
133
+ }
134
+ if (!isRecord(raw)) {
135
+ throw new EnvironmentStoreError(`values.json at ${file} must be a JSON object`);
136
+ }
137
+ const out = Object.create(null);
138
+ for (const [id, entry] of Object.entries(raw)) {
139
+ if (id === '__proto__' || id === 'constructor' || id === 'prototype') {
140
+ throw new EnvironmentStoreError(`values.json at ${file}: forbidden fact id ${JSON.stringify(id)}`);
141
+ }
142
+ out[id] = parseStoredEntry(id, entry);
143
+ }
144
+ return out;
145
+ }
146
+ /**
147
+ * Resolve a path through any intermediate symlinks (including when the leaf does
148
+ * not exist yet). Walks up to the nearest existing ancestor, realpaths it, then
149
+ * re-appends missing segments — same spirit as sensors/ledger realpath containment.
150
+ */
151
+ function resolvePathForContainment(targetPath) {
152
+ const absolute = path_1.default.resolve(targetPath);
153
+ const missing = [];
154
+ let cursor = absolute;
155
+ while (!fs_1.default.existsSync(cursor)) {
156
+ const parent = path_1.default.dirname(cursor);
157
+ if (parent === cursor)
158
+ break;
159
+ missing.unshift(path_1.default.basename(cursor));
160
+ cursor = parent;
161
+ }
162
+ let realBase;
163
+ try {
164
+ realBase = fs_1.default.realpathSync.native(cursor);
165
+ }
166
+ catch {
167
+ realBase = cursor;
168
+ }
169
+ return missing.length === 0 ? realBase : path_1.default.join(realBase, ...missing);
170
+ }
171
+ function isPathInsideRoot(candidate, root) {
172
+ if ((0, paths_1.sameExistingPath)(candidate, root))
173
+ return true;
174
+ const rel = path_1.default.relative(root, candidate);
175
+ if (rel === '')
176
+ return true;
177
+ if (rel === '..')
178
+ return false;
179
+ if (!rel.startsWith(`..${path_1.default.sep}`) && !rel.startsWith('..') && !path_1.default.isAbsolute(rel))
180
+ return true;
181
+ const prefix = root.endsWith(path_1.default.sep) ? root : root + path_1.default.sep;
182
+ return candidate === root || candidate.startsWith(prefix);
183
+ }
184
+ /**
185
+ * True when `targetPath` (after resolving symlinks) is the same as or nested under
186
+ * any registry content root. Catches AWM_HOME/environments → registry symlink (RNF-1).
187
+ *
188
+ * Always unions `contentRoots()` with any explicit `registryRoots`. An empty
189
+ * explicit array no longer disables checks — callers that need extra roots pass
190
+ * them; they cannot opt out of installed content roots.
191
+ */
192
+ function isInsideRegistryTree(targetPath, registryRoots) {
193
+ if (typeof targetPath !== 'string' || targetPath.trim() === '') {
194
+ throw new EnvironmentStoreError('target path must be a non-empty string');
195
+ }
196
+ const lexicalTarget = path_1.default.resolve(targetPath);
197
+ const realTarget = resolvePathForContainment(targetPath);
198
+ const roots = [...(0, registries_1.contentRoots)(), ...(registryRoots ?? [])];
199
+ for (const root of roots) {
200
+ if (typeof root !== 'string' || root.trim() === '')
201
+ continue;
202
+ const lexicalRoot = path_1.default.resolve(root);
203
+ const realRoot = resolvePathForContainment(root);
204
+ if (isPathInsideRoot(realTarget, realRoot))
205
+ return true;
206
+ // Also check lexical forms so non-existent trees still refuse obvious nesting.
207
+ if (isPathInsideRoot(lexicalTarget, lexicalRoot))
208
+ return true;
209
+ }
210
+ return false;
211
+ }
212
+ function assertNotInsideRegistryTree(targetPath, registryRoots) {
213
+ if (isInsideRegistryTree(targetPath, registryRoots)) {
214
+ throw new EnvironmentStoreError(`Refusing to write environment store inside a registry tree: ${targetPath} (RNF-1)`);
215
+ }
216
+ }
217
+ /**
218
+ * Atomic write of a values.json document. Refuses targets under registry roots (RNF-1).
219
+ */
220
+ function writeEnvValue(targetPath, values, opts = {}) {
221
+ if (typeof targetPath !== 'string' || targetPath.trim() === '') {
222
+ throw new EnvironmentStoreError('target path must be a non-empty string');
223
+ }
224
+ if (!isRecord(values)) {
225
+ throw new EnvironmentStoreError('values must be an object');
226
+ }
227
+ for (const [id, entry] of Object.entries(values)) {
228
+ parseStoredEntry(id, entry);
229
+ }
230
+ // RNF-1: refuse registry-tree targets. Callers that need environmentsRoot placement
231
+ // (writeFact) pass valuesPath(name); this helper does not re-check environmentsRoot.
232
+ assertNotInsideRegistryTree(targetPath, opts.registryRoots);
233
+ (0, atomic_file_1.writeFileAtomic)(targetPath, JSON.stringify(values, null, 2) + '\n');
234
+ }
235
+ function validateValue(definition, value) {
236
+ if (definition.valueType === 'path') {
237
+ if (value.trim() === '') {
238
+ throw new EnvironmentStoreError('path value must be a non-empty string');
239
+ }
240
+ }
241
+ if (definition.valueType === 'enum') {
242
+ const allowed = definition.values ?? [];
243
+ if (!allowed.includes(value)) {
244
+ throw new EnvironmentStoreError(`enum value ${JSON.stringify(value)} is not one of: ${allowed.join(', ')}`);
245
+ }
246
+ }
247
+ }
248
+ function withExpiresAt(entry, definition) {
249
+ if (!definition || definition.durability !== 'volatile') {
250
+ return {
251
+ value: entry.value,
252
+ status: entry.status,
253
+ updatedAt: entry.updatedAt,
254
+ };
255
+ }
256
+ if (typeof definition.ttl !== 'number' || !Number.isFinite(definition.ttl) || definition.ttl <= 0) {
257
+ throw new EnvironmentStoreError('volatile fact definition requires a finite ttl > 0');
258
+ }
259
+ const ttlSeconds = definition.ttl;
260
+ const updatedMs = Date.parse(entry.updatedAt);
261
+ if (!Number.isFinite(updatedMs)) {
262
+ throw new EnvironmentStoreError(`updatedAt must be a valid ISO timestamp, got ${JSON.stringify(entry.updatedAt)}`);
263
+ }
264
+ let expiresAt;
265
+ try {
266
+ expiresAt = new Date(updatedMs + ttlSeconds * 1000).toISOString();
267
+ }
268
+ catch (e) {
269
+ if (e instanceof RangeError) {
270
+ throw new EnvironmentStoreError(`volatile expiresAt out of range for updatedAt=${JSON.stringify(entry.updatedAt)} ttl=${ttlSeconds}`);
271
+ }
272
+ throw e;
273
+ }
274
+ return { ...entry, expiresAt };
275
+ }
276
+ /**
277
+ * Write one fact into `environments/<name>/values.json` (RF-2.1). Atomic rename.
278
+ * When `catalog` is provided, unknown ids error and list `consultedRoots`.
279
+ */
280
+ function writeFact(name, id, entry, opts = {}) {
281
+ assertEnvName(name);
282
+ if (typeof id !== 'string' || id.trim() === '') {
283
+ throw new EnvironmentStoreError('fact id must be a non-empty string');
284
+ }
285
+ if (!isRecord(entry) || typeof entry.value !== 'string') {
286
+ throw new EnvironmentStoreError('fact entry.value must be a string');
287
+ }
288
+ if (entry.status !== 'fresh' && entry.status !== 'stale' && entry.status !== 'missing') {
289
+ throw new EnvironmentStoreError('fact entry.status must be fresh|stale|missing');
290
+ }
291
+ if (typeof entry.updatedAt !== 'string') {
292
+ throw new EnvironmentStoreError('fact entry.updatedAt must be a string');
293
+ }
294
+ let definition = opts.definition;
295
+ if (opts.catalog) {
296
+ // Own keys only — prototype members (toString, …) are not catalog facts.
297
+ if (!Object.hasOwn(opts.catalog.facts, id)) {
298
+ const roots = opts.consultedRoots?.length
299
+ ? opts.consultedRoots.join(', ')
300
+ : '(no catalog roots)';
301
+ throw new EnvironmentStoreError(`Unknown fact id ${JSON.stringify(id)}; consulted: ${roots}`);
302
+ }
303
+ definition = definition ?? opts.catalog.facts[id];
304
+ }
305
+ if (definition) {
306
+ validateValue(definition, entry.value);
307
+ }
308
+ const nextEntry = withExpiresAt({
309
+ value: entry.value,
310
+ status: entry.status,
311
+ updatedAt: entry.updatedAt,
312
+ ...(entry.expiresAt !== undefined ? { expiresAt: entry.expiresAt } : {}),
313
+ }, definition);
314
+ const file = valuesPath(name);
315
+ assertNotInsideRegistryTree(file, opts.registryRoots);
316
+ const current = readValues(name);
317
+ const next = { ...current, [id]: nextEntry };
318
+ writeEnvValue(file, next, { registryRoots: opts.registryRoots });
319
+ // First write materializes the single active env as the env being written (RF-2.2).
320
+ if (!readMeta()) {
321
+ writeMeta({ active: name });
322
+ }
323
+ }
@@ -0,0 +1,138 @@
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.MAX_VOLATILE_TTL_SECONDS = void 0;
7
+ exports.parseFactsCatalog = parseFactsCatalog;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const registries_1 = require("../registries");
10
+ const types_1 = require("./types");
11
+ const VALUE_TYPES = ['string', 'path', 'enum'];
12
+ const RESOLUTIONS = ['prompt'];
13
+ const DURABILITIES = ['durable', 'volatile'];
14
+ const INJECTS = ['reference', 'context'];
15
+ /** Max volatile ttl (seconds) so updatedMs + ttl*1000 stays in a safe Date range. */
16
+ exports.MAX_VOLATILE_TTL_SECONDS = 365 * 24 * 3600;
17
+ function isRecord(value) {
18
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
19
+ }
20
+ function parseFactEntry(id, raw) {
21
+ if (!types_1.FACT_ID_RE.test(id)) {
22
+ throw new types_1.FactsCatalogError(`Invalid fact id ${JSON.stringify(id)}: must match ${types_1.FACT_ID_RE}`);
23
+ }
24
+ if (!isRecord(raw)) {
25
+ throw new types_1.FactsCatalogError(`facts.${id} must be an object`);
26
+ }
27
+ if (typeof raw.valueType !== 'string' || !VALUE_TYPES.includes(raw.valueType)) {
28
+ throw new types_1.FactsCatalogError(`facts.${id}.valueType must be one of ${VALUE_TYPES.join(', ')}`);
29
+ }
30
+ const valueType = raw.valueType;
31
+ if (raw.resolution === 'command') {
32
+ throw new types_1.FactsCatalogError(`facts.${id}: resolution: command is not supported in v1 (use prompt / --value / editable store)`);
33
+ }
34
+ if (raw.resolution !== undefined) {
35
+ if (typeof raw.resolution !== 'string' || !RESOLUTIONS.includes(raw.resolution)) {
36
+ throw new types_1.FactsCatalogError(`facts.${id}.resolution must be "prompt" in v1`);
37
+ }
38
+ }
39
+ if (raw.durability !== undefined) {
40
+ if (typeof raw.durability !== 'string' || !DURABILITIES.includes(raw.durability)) {
41
+ throw new types_1.FactsCatalogError(`facts.${id}.durability must be durable|volatile`);
42
+ }
43
+ }
44
+ if (raw.inject !== undefined) {
45
+ if (typeof raw.inject !== 'string' || !INJECTS.includes(raw.inject)) {
46
+ throw new types_1.FactsCatalogError(`facts.${id}.inject must be reference|context`);
47
+ }
48
+ }
49
+ const def = {
50
+ valueType,
51
+ resolution: raw.resolution ?? 'prompt',
52
+ durability: raw.durability ?? 'durable',
53
+ inject: raw.inject ?? 'reference',
54
+ };
55
+ if (raw.description !== undefined) {
56
+ if (typeof raw.description !== 'string') {
57
+ throw new types_1.FactsCatalogError(`facts.${id}.description must be a string`);
58
+ }
59
+ def.description = raw.description;
60
+ }
61
+ if (valueType === 'enum') {
62
+ if (!Array.isArray(raw.values) || raw.values.length === 0 || raw.values.some((v) => typeof v !== 'string')) {
63
+ throw new types_1.FactsCatalogError(`facts.${id}.values must be a non-empty string array when valueType is enum`);
64
+ }
65
+ def.values = raw.values;
66
+ }
67
+ else if (raw.values !== undefined) {
68
+ throw new types_1.FactsCatalogError(`facts.${id}.values is only allowed when valueType is enum`);
69
+ }
70
+ if (raw.ttl !== undefined) {
71
+ if (typeof raw.ttl !== 'number' || !Number.isFinite(raw.ttl) || raw.ttl < 0) {
72
+ throw new types_1.FactsCatalogError(`facts.${id}.ttl must be a non-negative number`);
73
+ }
74
+ def.ttl = raw.ttl;
75
+ }
76
+ if (def.durability === 'volatile') {
77
+ if (typeof def.ttl !== 'number' ||
78
+ !Number.isFinite(def.ttl) ||
79
+ def.ttl <= 0 ||
80
+ def.ttl > exports.MAX_VOLATILE_TTL_SECONDS) {
81
+ throw new types_1.FactsCatalogError(`facts.${id}: durability volatile requires finite ttl in (0, ${exports.MAX_VOLATILE_TTL_SECONDS}] seconds`);
82
+ }
83
+ }
84
+ return def;
85
+ }
86
+ function parseObject(raw) {
87
+ if (!isRecord(raw)) {
88
+ throw new types_1.FactsCatalogError('facts catalog must be a JSON object');
89
+ }
90
+ if (raw.schemaVersion !== 1) {
91
+ throw new types_1.FactsCatalogError(`facts catalog schemaVersion must be 1, got ${JSON.stringify(raw.schemaVersion)}`);
92
+ }
93
+ if (!isRecord(raw.facts)) {
94
+ throw new types_1.FactsCatalogError('facts catalog must include a facts object');
95
+ }
96
+ const facts = {};
97
+ for (const [id, entry] of Object.entries(raw.facts)) {
98
+ if (id === '') {
99
+ throw new types_1.FactsCatalogError('fact id must be non-empty');
100
+ }
101
+ facts[id] = parseFactEntry(id, entry);
102
+ }
103
+ return { schemaVersion: 1, facts };
104
+ }
105
+ /**
106
+ * Parse a facts catalog from an in-memory object or from a file path string.
107
+ * Named errors: {@link FactsCatalogError}.
108
+ */
109
+ function parseFactsCatalog(rawOrPath) {
110
+ if (typeof rawOrPath === 'string') {
111
+ try {
112
+ if (!(0, registries_1.assertRegularRegistryFile)(rawOrPath)) {
113
+ throw new types_1.FactsCatalogError(`facts catalog not found at ${rawOrPath}`);
114
+ }
115
+ }
116
+ catch (e) {
117
+ if (e instanceof types_1.FactsCatalogError)
118
+ throw e;
119
+ throw new types_1.FactsCatalogError(e instanceof Error ? e.message : `Cannot inspect facts catalog at ${rawOrPath}: ${String(e)}`);
120
+ }
121
+ let text;
122
+ try {
123
+ text = fs_1.default.readFileSync(rawOrPath, 'utf-8');
124
+ }
125
+ catch (e) {
126
+ throw new types_1.FactsCatalogError(`Cannot read facts catalog at ${rawOrPath}: ${e instanceof Error ? e.message : String(e)}`);
127
+ }
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(text);
131
+ }
132
+ catch (e) {
133
+ throw new types_1.FactsCatalogError(`Invalid JSON in facts catalog at ${rawOrPath}: ${e instanceof Error ? e.message : String(e)}`);
134
+ }
135
+ return parseObject(parsed);
136
+ }
137
+ return parseObject(rawOrPath);
138
+ }
@@ -0,0 +1,57 @@
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.readSkillNeeds = readSkillNeeds;
7
+ exports.findOrphanNeeds = findOrphanNeeds;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ const frontmatter_1 = require("../frontmatter");
11
+ const types_1 = require("./types");
12
+ /**
13
+ * Read the `needs` list from a skill SKILL.md frontmatter.
14
+ * Absent → `[]`. Invalid shape → {@link SkillNeedsError}.
15
+ */
16
+ function readSkillNeeds(skillMdPath) {
17
+ let raw;
18
+ try {
19
+ const stat = fs_1.default.lstatSync(skillMdPath);
20
+ if (stat.isSymbolicLink() || !stat.isFile()) {
21
+ throw new types_1.SkillNeedsError(`Skill path ${skillMdPath} must be a regular file`);
22
+ }
23
+ raw = fs_1.default.readFileSync(skillMdPath, 'utf-8');
24
+ }
25
+ catch (e) {
26
+ if (e instanceof types_1.SkillNeedsError)
27
+ throw e;
28
+ throw new types_1.SkillNeedsError(`Cannot read skill at ${skillMdPath}: ${e instanceof Error ? e.message : String(e)}`);
29
+ }
30
+ const block = (0, frontmatter_1.matchFrontmatterBlock)(raw);
31
+ if (block === null)
32
+ return [];
33
+ let parsed;
34
+ try {
35
+ parsed = js_yaml_1.default.load(block);
36
+ }
37
+ catch (e) {
38
+ throw new types_1.SkillNeedsError(`Invalid skill frontmatter at ${skillMdPath}: ${e instanceof Error ? e.message : String(e)}`);
39
+ }
40
+ if (parsed === null || parsed === undefined)
41
+ return [];
42
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
43
+ throw new types_1.SkillNeedsError(`Invalid skill frontmatter at ${skillMdPath}: expected a mapping`);
44
+ }
45
+ const needs = parsed.needs;
46
+ if (needs === undefined)
47
+ return [];
48
+ if (!Array.isArray(needs) || needs.some((n) => typeof n !== 'string' || n === '')) {
49
+ throw new types_1.SkillNeedsError(`Invalid needs at ${skillMdPath}: needs must be a YAML list of non-empty strings`);
50
+ }
51
+ return needs;
52
+ }
53
+ /** Ids in `needs` that are absent from the catalog (fail-closed; never invent). */
54
+ function findOrphanNeeds(needs, catalog) {
55
+ // Own keys only — `in` would treat Object.prototype (toString, …) as catalog facts.
56
+ return needs.filter((id) => !Object.hasOwn(catalog.facts, id));
57
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SkillNeedsError = exports.FactsCatalogError = exports.FACT_ID_RE = exports.FACTS_MIN_CLI_VERSION = void 0;
4
+ /** First CLI version that understands registry facts / needs. Bump only with intentional release. */
5
+ exports.FACTS_MIN_CLI_VERSION = '9.18.0';
6
+ /** Dotted fact ids: `repos.root`, `aws.profile.dev`. */
7
+ exports.FACT_ID_RE = /^[a-z][a-z0-9]*(\.[a-z0-9]+)+$/;
8
+ class FactsCatalogError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'FactsCatalogError';
12
+ }
13
+ }
14
+ exports.FactsCatalogError = FactsCatalogError;
15
+ class SkillNeedsError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = 'SkillNeedsError';
19
+ }
20
+ }
21
+ exports.SkillNeedsError = SkillNeedsError;
@@ -75,7 +75,11 @@ exports.defaultActions = {
75
75
  throw new Error('sensor init does not migrate an existing v2 manifest; run awm sensors bootstrap');
76
76
  if (plan.kind === 'create')
77
77
  (0, bootstrap_1.applySensorBootstrap)(plan);
78
- const pack = plan.kind === 'create' && plan.manifest.mode === 'project-sensors' ? plan.manifest.pack : (0, detection_1.detectStack)(o.cwd).pack;
78
+ const detected = (0, detection_1.detectStack)(o.cwd).pack;
79
+ const declared = plan.kind === 'create' && plan.manifest.mode === 'project-sensors' ? plan.manifest.pack : undefined;
80
+ const pack = typeof declared === 'string' && declared.length > 0 ? declared : detected;
81
+ if (typeof pack !== 'string' || pack.length === 0)
82
+ throw new Error('sensor init could not resolve a pack id');
79
83
  return { detection: { pack }, manifest: { pack } };
80
84
  },
81
85
  addExtension: (root, name) => { (0, profile_1.addExtension)(root, name); },
package/dist/src/index.js CHANGED
@@ -37,6 +37,7 @@ const doctor_1 = require("./commands/doctor");
37
37
  const backup_1 = require("./commands/backup");
38
38
  const init_1 = require("./commands/init");
39
39
  const registry_1 = require("./commands/registry");
40
+ const env_1 = require("./commands/env");
40
41
  const pin_1 = require("./commands/pin");
41
42
  const export_1 = require("./commands/export");
42
43
  const agent_1 = require("./commands/agent");
@@ -743,6 +744,7 @@ miroCmd.command('sync <storyMapPath>')
743
744
  (0, backup_1.registerBackupCommand)(exports.program);
744
745
  (0, init_1.registerInitCommand)(exports.program);
745
746
  (0, registry_1.registerRegistryCommand)(exports.program);
747
+ (0, env_1.registerEnvCommand)(exports.program);
746
748
  (0, pin_1.registerPinCommands)(exports.program);
747
749
  (0, export_1.registerExportCommand)(exports.program);
748
750
  (0, agent_1.registerAgentCommand)(exports.program);