agentic-workflow-manager 9.7.0 → 9.8.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 (114) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/doctor.js +4 -2
  3. package/dist/src/commands/export.js +3 -0
  4. package/dist/src/commands/init.js +10 -2
  5. package/dist/src/commands/job/exec-wrapper.js +53 -3
  6. package/dist/src/commands/plan/index.js +124 -6
  7. package/dist/src/commands/registry/add.js +2 -2
  8. package/dist/src/commands/registry/index.js +7 -5
  9. package/dist/src/commands/registry/install-bundles.js +4 -4
  10. package/dist/src/commands/registry/status.js +4 -4
  11. package/dist/src/commands/sensors/bootstrap.js +36 -5
  12. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  13. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  14. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  15. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  16. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  17. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  18. package/dist/src/commands/sensors/coverage/index.js +11 -2
  19. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  20. package/dist/src/commands/sensors/exec.js +16 -1
  21. package/dist/src/commands/sensors/migrate.js +62 -2
  22. package/dist/src/commands/sensors/prepare.js +2 -0
  23. package/dist/src/commands/sensors/result.js +2 -0
  24. package/dist/src/commands/sensors/run.js +64 -1
  25. package/dist/src/commands/sensors/status.js +6 -4
  26. package/dist/src/commands/sync.js +2 -1
  27. package/dist/src/commands/update.js +3 -1
  28. package/dist/src/commands/watch/archive-unused.js +138 -0
  29. package/dist/src/commands/watch/generations.js +32 -9
  30. package/dist/src/commands/watch/index.js +108 -2
  31. package/dist/src/commands/watch/init.js +149 -18
  32. package/dist/src/commands/watch/supervisor.js +167 -13
  33. package/dist/src/commands/watch/teardown-driver.js +13 -13
  34. package/dist/src/commands/watch/track-state.js +59 -0
  35. package/dist/src/commands/watch/tracks.js +38 -102
  36. package/dist/src/core/admission/index.js +181 -0
  37. package/dist/src/core/admission/registry-contracts.js +290 -0
  38. package/dist/src/core/bundle-install.js +2 -2
  39. package/dist/src/core/bundles.js +92 -19
  40. package/dist/src/core/diagnostics/context.js +1 -1
  41. package/dist/src/core/export/index.js +1 -1
  42. package/dist/src/core/export/resolve.js +2 -2
  43. package/dist/src/core/journal/fingerprint.js +35 -1
  44. package/dist/src/core/journal/paths.js +11 -0
  45. package/dist/src/core/journal/store.js +146 -4
  46. package/dist/src/core/journal/types.js +35 -1
  47. package/dist/src/core/migration/historical-provenance.js +18 -0
  48. package/dist/src/core/migration/index.js +322 -0
  49. package/dist/src/core/plan/identity.js +75 -0
  50. package/dist/src/core/plan/validate.js +178 -64
  51. package/dist/src/core/reconciliation.js +1 -1
  52. package/dist/src/index.js +46 -39
  53. package/dist/src/utils/grouping.js +1 -1
  54. package/dist/src/utils/registry-view.js +1 -1
  55. package/dist/tests/commands/bundle-diagnostics.test.js +145 -0
  56. package/dist/tests/commands/export.test.js +28 -2
  57. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  58. package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
  59. package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
  60. package/dist/tests/commands/pin.test.js +23 -23
  61. package/dist/tests/commands/plan/index.test.js +630 -14
  62. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  63. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  64. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  65. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  66. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  67. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  68. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  69. package/dist/tests/commands/sensors/exec.test.js +112 -0
  70. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  71. package/dist/tests/commands/sensors/run.test.js +15 -2
  72. package/dist/tests/commands/sensors/status.test.js +3 -3
  73. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  74. package/dist/tests/commands/watch/generations.test.js +113 -4
  75. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  76. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  77. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  78. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  79. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  80. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  81. package/dist/tests/core/admission/index.test.js +87 -0
  82. package/dist/tests/core/bundle-install.test.js +36 -0
  83. package/dist/tests/core/bundles-multiroot.test.js +24 -2
  84. package/dist/tests/core/bundles-overrides.test.js +10 -7
  85. package/dist/tests/core/bundles.test.js +75 -6
  86. package/dist/tests/core/diagnostics/context.test.js +2 -2
  87. package/dist/tests/core/export/engine.test.js +1 -1
  88. package/dist/tests/core/export/resolve.test.js +1 -1
  89. package/dist/tests/core/init/mutation-targets.test.js +1 -1
  90. package/dist/tests/core/init/orchestrator.test.js +1 -1
  91. package/dist/tests/core/init/steps.test.js +1 -1
  92. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  93. package/dist/tests/core/journal/store.test.js +53 -0
  94. package/dist/tests/core/journal/types.test.js +7 -0
  95. package/dist/tests/core/migration/facts.test.js +289 -0
  96. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  97. package/dist/tests/core/plan/validate.test.js +320 -6
  98. package/dist/tests/core/reconciliation.test.js +1 -1
  99. package/dist/tests/core/sync-profile-atomicity.test.js +1 -1
  100. package/dist/tests/integration/pack-e2e.test.js +4 -1
  101. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  102. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  103. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  104. package/dist/tests/providers/injection-config.test.js +14 -6
  105. package/dist/tests/structural/bundle-skill-reference-contract.test.js +56 -0
  106. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  107. package/dist/tests/structural/native-release-artifacts.test.js +16 -0
  108. package/dist/tests/utils/grouping.test.js +2 -2
  109. package/dist/tests/utils/registry-view.test.js +7 -7
  110. package/package.json +2 -2
  111. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  112. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  113. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  114. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,290 @@
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.consumedRegistryContracts = consumedRegistryContracts;
7
+ exports.registryCompatibility = registryCompatibility;
8
+ exports.admitRegistryPlan = admitRegistryPlan;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const providers_1 = require("../../providers");
13
+ const registry_1 = require("../renderers/registry");
14
+ const registries_1 = require("../registries");
15
+ const native_bridge_1 = require("../secure-fs/native-bridge");
16
+ const json_1 = require("../plan/json");
17
+ const cli_version_1 = require("../cli-version");
18
+ const versioning_1 = require("../versioning");
19
+ const check_1 = require("../currentness/check");
20
+ const run_1 = require("../../commands/sensors/run");
21
+ const index_1 = require("./index");
22
+ const FRAMEWORK_CONTRACTS = ['using-awm', 'writing-plans', 'development-process', 'subagent-driven-development', 'executing-plans', 'post-implementation-qa', 'post-implementation-docs', 'harness-retro', 'finishing-a-development-branch'];
23
+ const MAX_CONTRACT_BYTES = 1024 * 1024;
24
+ const MAX_MANIFEST_BYTES = 256 * 1024;
25
+ const MAX_DIRECTORY_ENTRIES = 10000;
26
+ function unknown(message) {
27
+ return { provenance: 'unknown', consumedRegistryComponents: [], provenanceDiagnostic: { code: 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED', message: `${message} Repair the installed runtime artifact or registry provenance before dispatch.` } };
28
+ }
29
+ function within(root, candidate) {
30
+ const relative = path_1.default.relative(root, candidate);
31
+ return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative));
32
+ }
33
+ function physicalDirectory(directory) {
34
+ const physical = fs_1.default.realpathSync.native(directory);
35
+ if (!fs_1.default.lstatSync(physical).isDirectory())
36
+ throw new Error('path is not a directory');
37
+ const handle = fs_1.default.opendirSync(physical);
38
+ try {
39
+ let entries = 0;
40
+ while (handle.readSync() !== null)
41
+ if (++entries > MAX_DIRECTORY_ENTRIES)
42
+ throw new Error('directory entry limit exceeded');
43
+ }
44
+ finally {
45
+ handle.closeSync();
46
+ }
47
+ return physical;
48
+ }
49
+ function filesystemIdentity(file) {
50
+ const stat = fs_1.default.lstatSync(file, { bigint: true });
51
+ if (stat.isSymbolicLink() || stat.ino === 0n)
52
+ throw new Error('filesystem identity is unavailable');
53
+ return `${stat.dev.toString()}:${stat.ino.toString()}`;
54
+ }
55
+ /** Native descriptor bytes plus exact bigint leaf identity are observed only
56
+ * in memory. No source body or new journal snapshot is persisted. */
57
+ function contractFileIdentity(file) {
58
+ const before = fs_1.default.lstatSync(file, { bigint: true });
59
+ if (!before.isFile() || before.isSymbolicLink() || before.ino === 0n)
60
+ throw new Error('contract identity is unavailable');
61
+ const bytes = native_bridge_1.secureFs.readRegularFile(file, MAX_CONTRACT_BYTES).bytes;
62
+ const after = fs_1.default.lstatSync(file, { bigint: true });
63
+ if (!after.isFile() || after.isSymbolicLink() || before.dev !== after.dev || before.ino !== after.ino
64
+ || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs)
65
+ throw new Error('contract identity changed during inspection');
66
+ return crypto_1.default.createHash('sha256').update(`${before.dev.toString()}:${before.ino.toString()}\0`).update(bytes).digest('hex');
67
+ }
68
+ /** ENOENT proves absence only when the nearest existing parent is readable.
69
+ * A dangling/replaced ancestor never certifies an empty provider install. */
70
+ function directoryExists(directory) {
71
+ let current = directory;
72
+ while (true) {
73
+ try {
74
+ fs_1.default.lstatSync(current);
75
+ physicalDirectory(current);
76
+ return current === directory;
77
+ }
78
+ catch (error) {
79
+ if (error.code !== 'ENOENT')
80
+ throw error;
81
+ const parent = path_1.default.dirname(current);
82
+ if (parent === current)
83
+ throw error;
84
+ // An existing dangling symlink causes realpath ENOENT; do not
85
+ // continue above it as though the requested install were absent.
86
+ try {
87
+ if (fs_1.default.lstatSync(current).isSymbolicLink())
88
+ throw new Error('dangling directory ancestor');
89
+ }
90
+ catch (inspection) {
91
+ if (inspection.code !== 'ENOENT')
92
+ throw inspection;
93
+ }
94
+ current = parent;
95
+ }
96
+ }
97
+ }
98
+ /** Physical sources AND actual provider artifacts define consumption. Copied
99
+ * rendered artifacts have no owner proof; a basename is never provenance. */
100
+ function consumedRegistryContracts(report, cwd, registries, target) {
101
+ if (report.state !== 'valid')
102
+ return unknown('A validated compact plan is required.');
103
+ if (!Array.isArray(registries) || registries.length > 256)
104
+ return unknown('Registry inventory is invalid or unbounded.');
105
+ let root;
106
+ try {
107
+ root = physicalDirectory(path_1.default.resolve(cwd));
108
+ }
109
+ catch {
110
+ return unknown('Project root provenance is unavailable.');
111
+ }
112
+ const physicalRegistries = [];
113
+ for (const registry of registries) {
114
+ try {
115
+ if (!registry || typeof registry.name !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/.test(registry.name)
116
+ || typeof registry.contentRoot !== 'string' || registry.contentRoot.length > 4096
117
+ || typeof registry.remote !== 'string' || registry.remote.length > 4096)
118
+ throw new Error('invalid registry identity');
119
+ const physicalRoot = physicalDirectory(registry.contentRoot);
120
+ physicalRegistries.push({ name: registry.name, root: physicalRoot, identity: filesystemIdentity(physicalRoot), remote: registry.remote });
121
+ }
122
+ catch {
123
+ const name = typeof registry?.name === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/.test(registry.name) ? registry.name : 'unknown';
124
+ return unknown(`Registry ${name} root cannot be inspected.`);
125
+ }
126
+ }
127
+ const consumed = new Set();
128
+ const identities = [];
129
+ for (const source of report.manifest.sources) {
130
+ try {
131
+ const file = fs_1.default.realpathSync.native(path_1.default.resolve(root, source.path));
132
+ if (!within(root, file))
133
+ return unknown(`Plan source ${source.path} escapes its physical project root.`);
134
+ identities.push(JSON.stringify(['source', source.id, contractFileIdentity(file)]));
135
+ for (const registry of physicalRegistries)
136
+ if (within(registry.root, file))
137
+ consumed.add(`registry:${registry.name}`);
138
+ }
139
+ catch {
140
+ return unknown(`Plan source ${source.path} cannot be inspected as a bounded regular file.`);
141
+ }
142
+ }
143
+ const provider = (0, providers_1.providerFor)(target);
144
+ const directories = [provider.skill.global, path_1.default.resolve(root, provider.skill.local)].filter((directory) => directory !== null);
145
+ for (const directory of directories) {
146
+ try {
147
+ if (!directoryExists(directory))
148
+ continue;
149
+ }
150
+ catch {
151
+ return unknown(`Provider ${target} install directory ${directory} is unsafe or unreadable.`);
152
+ }
153
+ for (const contract of FRAMEWORK_CONTRACTS) {
154
+ const artifact = path_1.default.join(directory, (0, registry_1.renderedFilename)(contract, provider.skill.renderer));
155
+ try {
156
+ fs_1.default.lstatSync(artifact);
157
+ }
158
+ catch (error) {
159
+ if (error.code === 'ENOENT')
160
+ continue;
161
+ return unknown(`Runtime artifact ${artifact} cannot be inspected.`);
162
+ }
163
+ const declaredFile = provider.skill.renderer === 'link' ? path_1.default.join(artifact, 'SKILL.md') : artifact;
164
+ try {
165
+ const file = fs_1.default.realpathSync.native(declaredFile);
166
+ const identity = contractFileIdentity(file);
167
+ const owners = physicalRegistries.filter(registry => within(registry.root, file));
168
+ if (owners.length !== 1)
169
+ return unknown(`Runtime artifact ${artifact} has no unique physical registry owner (copied/rendered content is not ownership proof).`);
170
+ consumed.add(`registry:${owners[0].name}`);
171
+ identities.push(JSON.stringify(['artifact', target, contract, provider.skill.renderer, identity]));
172
+ }
173
+ catch {
174
+ return unknown(`Runtime artifact ${artifact} is dangling, nonregular, unreadable or unbounded.`);
175
+ }
176
+ }
177
+ }
178
+ for (const registry of physicalRegistries)
179
+ if (consumed.has(`registry:${registry.name}`))
180
+ identities.push(JSON.stringify(['registry', registry.name, registry.identity, registry.remote]));
181
+ return { provenance: 'proven', consumedRegistryComponents: [...consumed].sort(), identityDigest: crypto_1.default.createHash('sha256').update(identities.sort().join('\0')).digest('hex') };
182
+ }
183
+ /** Only the declared CLI floor is consumed here, not a new registry catalog.
184
+ * The native bounded/no-follow read prevents stat/read races and body leaks. */
185
+ function registryCompatibility(registries, consumed) {
186
+ const diagnostics = [];
187
+ const current = (0, cli_version_1.cliVersion)();
188
+ for (const registry of registries) {
189
+ const component = `registry:${registry.name}`;
190
+ if (!consumed.includes(component))
191
+ continue;
192
+ try {
193
+ const physicalRoot = physicalDirectory(registry.contentRoot);
194
+ const manifest = path_1.default.join(physicalRoot, registries_1.REGISTRY_MANIFEST_NAME);
195
+ try {
196
+ fs_1.default.lstatSync(manifest);
197
+ }
198
+ catch (error) {
199
+ if (error.code === 'ENOENT')
200
+ continue;
201
+ throw error;
202
+ }
203
+ const bytes = native_bridge_1.secureFs.readRegularFile(manifest, MAX_MANIFEST_BYTES).bytes;
204
+ const raw = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
205
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
206
+ throw new Error('manifest must be a JSON object');
207
+ const min = raw.minCliVersion;
208
+ if (min !== undefined && (typeof min !== 'string' || !/^v?\d+\.\d+\.\d+$/.test(min)
209
+ || min.replace(/^v/, '').split('.').some(part => !Number.isSafeInteger(Number(part)))))
210
+ throw new Error('invalid CLI floor');
211
+ if (typeof min === 'string' && (0, versioning_1.compareSemver)(current, min) < 0)
212
+ diagnostics.push({ code: 'ADMISSION_REGISTRY_CLI_INCOMPATIBLE', message: `${component} requires CLI >= ${min}; installed CLI is ${current}. Update the CLI before dispatch.` });
213
+ }
214
+ catch {
215
+ diagnostics.push({ code: 'ADMISSION_REGISTRY_COMPATIBILITY_UNVERIFIABLE', message: `${component} manifest compatibility could not be verified as bounded regular JSON. Repair the registry before dispatch.` });
216
+ }
217
+ }
218
+ return diagnostics;
219
+ }
220
+ /** One admission authority for the public command AND actual watch dispatch.
221
+ * No empirical execution occurs before provenance/currentness/CLI-floor gates. */
222
+ async function admitRegistryPlan(input, dependencies = {}) {
223
+ const admission = dependencies.admitPlan ?? index_1.admitPlan;
224
+ if (input.plan.state !== 'valid' || !(0, providers_1.isAgentTarget)(input.provider) || (input.enabledAgents && !input.enabledAgents.includes(input.provider))
225
+ || (input.executionMode !== undefined && input.executionMode !== 'interactivo' && input.executionMode !== 'desatendido'))
226
+ return admission(input);
227
+ let registries = [];
228
+ let scope;
229
+ const inventory = dependencies.listRegistries ?? registries_1.listRegistries;
230
+ const target = input.provider;
231
+ try {
232
+ registries = inventory();
233
+ scope = consumedRegistryContracts(input.plan, input.cwd, registries, target);
234
+ }
235
+ catch {
236
+ scope = unknown('Registry inventory cannot be read.');
237
+ }
238
+ const refresh = () => {
239
+ try {
240
+ const refreshedRegistries = inventory();
241
+ const refreshed = consumedRegistryContracts(input.plan, input.cwd, refreshedRegistries, target);
242
+ const previousComponents = scope.consumedRegistryComponents.join('\0');
243
+ const previousIdentity = scope.identityDigest;
244
+ registries = refreshedRegistries;
245
+ scope = refreshed.provenance === 'unknown' ? refreshed
246
+ : refreshed.consumedRegistryComponents.join('\0') !== previousComponents || refreshed.identityDigest !== previousIdentity
247
+ ? unknown('Consumed registry ownership changed during an asynchronous admission observation. Re-run actual currentness and read-only plan admission.') : refreshed;
248
+ if (scope.provenanceDiagnostic)
249
+ scope = { ...scope, provenanceDiagnostic: { ...scope.provenanceDiagnostic, message: `${scope.provenanceDiagnostic.message} Re-run actual currentness after repair.` } };
250
+ }
251
+ catch {
252
+ scope = unknown('Registry inventory changed or cannot be read after asynchronous admission. Re-run actual currentness.');
253
+ }
254
+ return scope.provenance === 'proven';
255
+ };
256
+ const compose = async (fields) => {
257
+ const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...fields });
258
+ if (scope.provenanceDiagnostic && report.diagnostics.some(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED')) {
259
+ return { ...report, diagnostics: report.diagnostics.map(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED' ? scope.provenanceDiagnostic : diagnostic) };
260
+ }
261
+ return report;
262
+ };
263
+ let currentness = input.currentness;
264
+ let compatibilityDiagnostics;
265
+ if (input.requireCurrent) {
266
+ if (scope.provenance !== 'proven')
267
+ return compose({});
268
+ currentness = currentness ?? await (dependencies.checkCurrentness ?? check_1.checkCurrentness)(input.cwd);
269
+ if (!refresh())
270
+ return compose({ currentness });
271
+ // A missing sensor input stages composition before capability resolution.
272
+ const currentnessGate = await compose({ currentness, verifySensors: true, sensors: undefined });
273
+ if (currentnessGate.currentness !== 'current')
274
+ return currentnessGate;
275
+ compatibilityDiagnostics = registryCompatibility(registries, scope.consumedRegistryComponents);
276
+ const compatibilityGate = await compose({ currentness, compatibilityDiagnostics, verifySensors: true, sensors: undefined });
277
+ if (compatibilityGate.currentness !== 'current')
278
+ return compatibilityGate;
279
+ }
280
+ const sensors = input.verifySensors ? input.sensors ?? await (dependencies.runSensors ?? run_1.runSensors)({ cwd: input.cwd, all: true, readOnly: true }) : undefined;
281
+ if (input.requireCurrent && input.verifySensors) {
282
+ if (!refresh())
283
+ return compose({ currentness, sensors });
284
+ const currentnessGate = await compose({ currentness, verifySensors: true, sensors: undefined });
285
+ if (currentnessGate.currentness !== 'current')
286
+ return currentnessGate;
287
+ compatibilityDiagnostics = registryCompatibility(registries, scope.consumedRegistryComponents);
288
+ }
289
+ return compose({ currentness, compatibilityDiagnostics, sensors });
290
+ }
@@ -28,8 +28,8 @@ function bundleArtifacts(b, contentDir) {
28
28
  // estructural sobre la ruta ya resuelta.
29
29
  const refs = [];
30
30
  for (const s of b.skills) {
31
- (0, artifact_name_1.assertSafeArtifactName)(s.name, 'skill');
32
- refs.push({ name: s.name, type: 'skill', installName: s.name, sourcePath: path_1.default.join(contentDir, 'skills', s.name) });
31
+ (0, artifact_name_1.assertSafeArtifactName)(s, 'skill');
32
+ refs.push({ name: s, type: 'skill', installName: s, sourcePath: path_1.default.join(contentDir, 'skills', s) });
33
33
  }
34
34
  for (const w of b.workflows) {
35
35
  (0, artifact_name_1.assertSafeArtifactName)(w, 'workflow');
@@ -4,15 +4,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.readCatalog = readCatalog;
7
+ exports.inspectBundles = inspectBundles;
8
+ exports.createBundleDiagnosticReporter = createBundleDiagnosticReporter;
7
9
  exports.discoverBundles = discoverBundles;
8
10
  exports.resolveBundleSkills = resolveBundleSkills;
9
11
  exports.resolveBundleAgents = resolveBundleAgents;
10
12
  exports.defaultScopeForBundle = defaultScopeForBundle;
13
+ exports.inspectAllBundles = inspectAllBundles;
11
14
  exports.discoverAllBundles = discoverAllBundles;
12
15
  exports.resolveBundleClosure = resolveBundleClosure;
13
16
  const fs_1 = __importDefault(require("fs"));
17
+ const crypto_1 = __importDefault(require("crypto"));
14
18
  const path_1 = __importDefault(require("path"));
15
19
  const registries_1 = require("./registries");
20
+ const text_1 = require("./text");
16
21
  function catalogPath(contentDir) {
17
22
  return path_1.default.join(contentDir, 'catalog.json');
18
23
  }
@@ -85,29 +90,63 @@ function readCatalog(contentDir) {
85
90
  // TypeError opaco mas adelante; se descarta aca, nombrando el archivo.
86
91
  return bundles.filter((entry) => entry !== null && typeof entry === 'object' && typeof entry.source === 'string');
87
92
  }
88
- /** Tolera `skills` ausente o mal formado: lo que no sea un array se trata como
89
- * vacio en vez de tirar `(raw ?? []).map is not a function`, y cada entrada se
90
- * valida individualmente. */
91
- function normalizeSkillRefs(raw) {
92
- if (!Array.isArray(raw))
93
- return [];
94
- return raw.flatMap((s) => {
95
- if (typeof s === 'string')
96
- return [{ name: s, onSignal: false }];
97
- if (s !== null && typeof s === 'object' && typeof s.name === 'string') {
98
- return [{ name: s.name, onSignal: s.onSignal === true }];
93
+ function safeManifestIdentity(manifestPath) {
94
+ const sanitized = (0, text_1.sanitizeDiagnosticText)(manifestPath);
95
+ if (sanitized.length <= 240)
96
+ return sanitized;
97
+ const suffix = crypto_1.default.createHash('sha256').update(sanitized).digest('hex').slice(0, 16);
98
+ return `${sanitized.slice(0, 220)}…#${suffix}`;
99
+ }
100
+ function boundedDiagnostic(diagnostic) {
101
+ return (0, text_1.sanitizeDiagnosticText)(diagnostic).slice(0, 512);
102
+ }
103
+ /** Normaliza la única forma de compatibilidad CLI 9 antes de que el manifest
104
+ * alcance consumidores operativos. Los objetos nunca salen de este límite. */
105
+ function normalizeSkillRefs(raw, manifestPath) {
106
+ const identity = safeManifestIdentity(manifestPath);
107
+ if (!Array.isArray(raw)) {
108
+ throw new Error(`${identity}: "skills" must be an array.`);
109
+ }
110
+ const skills = [];
111
+ let legacyCount = 0;
112
+ raw.forEach((ref, index) => {
113
+ const location = `${identity}: skills[${index}]`;
114
+ if (typeof ref === 'string') {
115
+ if (ref === '')
116
+ throw new Error(`${location} must be a non-empty string.`);
117
+ skills.push(ref);
118
+ return;
99
119
  }
100
- return [];
120
+ if (ref === null || typeof ref !== 'object' || Array.isArray(ref)) {
121
+ throw new Error(`${location} must be a non-empty string or a supported legacy object.`);
122
+ }
123
+ const legacy = ref;
124
+ const keys = Object.keys(legacy);
125
+ if (!keys.every((key) => key === 'name' || key === 'onSignal')) {
126
+ throw new Error(`${location} legacy object may contain only "name" and optional "onSignal".`);
127
+ }
128
+ if (typeof legacy.name !== 'string' || legacy.name === '') {
129
+ throw new Error(`${location} legacy object "name" must be a non-empty string.`);
130
+ }
131
+ if (legacy.onSignal !== undefined && typeof legacy.onSignal !== 'boolean') {
132
+ throw new Error(`${location} legacy object "onSignal" must be a boolean.`);
133
+ }
134
+ skills.push(legacy.name);
135
+ legacyCount++;
101
136
  });
137
+ const diagnostics = legacyCount === 0 ? [] : [boundedDiagnostic(`${identity}: ${legacyCount} legacy object skill reference(s) accepted for CLI 9; ` +
138
+ 'replace with canonical skills: ["skill-name"]. Legacy object support is removed in v10.')];
139
+ return { skills, diagnostics };
102
140
  }
103
141
  /** Array de strings, o vacio. Cualquier otra forma en el contenido del registry
104
142
  * se ignora en vez de propagarse hasta un crash aguas abajo. */
105
143
  function stringArray(raw) {
106
144
  return Array.isArray(raw) ? raw.filter((v) => typeof v === 'string') : [];
107
145
  }
108
- function discoverBundles(contentDir) {
146
+ function inspectBundles(contentDir) {
109
147
  const entries = readCatalog(contentDir);
110
148
  const bundles = [];
149
+ const diagnostics = [];
111
150
  for (const entry of entries) {
112
151
  const manifestPath = bundleManifestPath(contentDir, entry.source);
113
152
  if (!(0, registries_1.assertRegularRegistryFile)(manifestPath))
@@ -128,6 +167,8 @@ function discoverBundles(contentDir) {
128
167
  if (typeof m.name !== 'string' || m.name === '') {
129
168
  throw new Error(`${manifestPath}: "name" must be a non-empty string.`);
130
169
  }
170
+ const normalizedSkills = normalizeSkillRefs(m.skills, manifestPath);
171
+ diagnostics.push(...normalizedSkills.diagnostics);
131
172
  bundles.push({
132
173
  name: m.name,
133
174
  description: typeof m.description === 'string' ? m.description : '',
@@ -135,13 +176,36 @@ function discoverBundles(contentDir) {
135
176
  scope: m.scope ?? 'project',
136
177
  visibility: m.visibility ?? 'public',
137
178
  dependsOn: stringArray(m.dependsOn),
138
- skills: normalizeSkillRefs(m.skills),
179
+ skills: normalizedSkills.skills,
139
180
  workflows: stringArray(m.workflows),
140
181
  agents: stringArray(m.agents),
141
182
  contentRoot: contentDir,
142
183
  });
143
184
  }
144
- return bundles;
185
+ return { bundles, diagnostics };
186
+ }
187
+ /** Creates the command-scoped CLI reporter used by discovery facades. */
188
+ function createBundleDiagnosticReporter(write = (message) => console.error(message)) {
189
+ const emitted = new Set();
190
+ return (diagnostic) => {
191
+ const safeDiagnostic = boundedDiagnostic(diagnostic);
192
+ if (emitted.has(safeDiagnostic))
193
+ return;
194
+ emitted.add(safeDiagnostic);
195
+ write(`warning: ${safeDiagnostic}`);
196
+ };
197
+ }
198
+ const defaultBundleDiagnosticReporter = createBundleDiagnosticReporter();
199
+ function reportDiagnostics(diagnostics, reporter) {
200
+ const emit = reporter ?? defaultBundleDiagnosticReporter;
201
+ for (const diagnostic of new Set(diagnostics))
202
+ emit(diagnostic);
203
+ }
204
+ /** Compatibility facade for callers that only need bundle definitions. */
205
+ function discoverBundles(contentDir, reporter) {
206
+ const result = inspectBundles(contentDir);
207
+ reportDiagnostics(result.diagnostics, reporter);
208
+ return result.bundles;
145
209
  }
146
210
  function resolveBundleSkills(bundleName, bundles) {
147
211
  const byName = new Map(bundles.map((b) => [b.name, b]));
@@ -157,7 +221,7 @@ function resolveBundleSkills(bundleName, bundles) {
157
221
  for (const dep of b.dependsOn)
158
222
  visit(dep);
159
223
  for (const s of b.skills)
160
- skills.add(s.name);
224
+ skills.add(s);
161
225
  };
162
226
  visit(bundleName);
163
227
  return Array.from(skills);
@@ -192,11 +256,14 @@ function defaultScopeForBundle(scope) {
192
256
  /** Descubre bundles de TODOS los roots (base + registries adicionales).
193
257
  * Colisión de nombre entre roots: override declarado en awm-registry.json
194
258
  * del root posterior → reemplaza; no declarado → error nombrando ambas fuentes. */
195
- function discoverAllBundles(roots = (0, registries_1.contentRoots)()) {
259
+ function inspectAllBundles(roots = (0, registries_1.contentRoots)()) {
196
260
  const byName = new Map();
261
+ const diagnostics = [];
197
262
  for (const root of roots) {
198
263
  const overrides = (0, registries_1.readRegistryManifest)(root).overrides;
199
- for (const b of discoverBundles(root)) {
264
+ const result = inspectBundles(root);
265
+ diagnostics.push(...result.diagnostics);
266
+ for (const b of result.bundles) {
200
267
  const prev = byName.get(b.name);
201
268
  if (!prev) {
202
269
  byName.set(b.name, b);
@@ -210,7 +277,13 @@ function discoverAllBundles(roots = (0, registries_1.contentRoots)()) {
210
277
  `Remove or rename one of them, or declare "${b.name}" in "overrides" of the later registry's awm-registry.json.`);
211
278
  }
212
279
  }
213
- return Array.from(byName.values());
280
+ return { bundles: Array.from(byName.values()), diagnostics: Array.from(new Set(diagnostics)) };
281
+ }
282
+ /** Compatibility facade for callers that only need bundle definitions. */
283
+ function discoverAllBundles(roots = (0, registries_1.contentRoots)(), reporter) {
284
+ const result = inspectAllBundles(roots);
285
+ reportDiagnostics(result.diagnostics, reporter);
286
+ return result.bundles;
214
287
  }
215
288
  /**
216
289
  * Resolves the dependency closure of a bundle in deps-first order, deduped.
@@ -307,7 +307,7 @@ function gatherProject(root, bundles, agent = 'claude-code') {
307
307
  }
308
308
  function gatherContext(opts = {}) {
309
309
  const cwd = opts.cwd ?? process.cwd();
310
- const bundles = opts.bundles ?? (0, bundles_1.discoverAllBundles)();
310
+ const bundles = opts.bundles ?? (0, bundles_1.discoverAllBundles)(undefined, opts.reporter);
311
311
  const agent = opts.agent ?? 'claude-code';
312
312
  const root = (0, profile_1.findProjectRoot)(cwd);
313
313
  const agents = opts.agents ?? [agent];
@@ -23,7 +23,7 @@ function runExport(opts) {
23
23
  }
24
24
  const roots = opts.roots ?? (0, registries_1.contentRoots)();
25
25
  const outDir = path_1.default.join(opts.out ?? path_1.default.join(process.cwd(), 'awm-export'), target);
26
- const resolution = (0, resolve_1.resolveExport)(opts.name, roots);
26
+ const resolution = (0, resolve_1.resolveExport)(opts.name, roots, opts.reporter);
27
27
  fs_1.default.mkdirSync(outDir, { recursive: true });
28
28
  const exported = [];
29
29
  let zipAvailable = true;
@@ -44,8 +44,8 @@ function assertOverrideConsistency(s) {
44
44
  throw new Error(`Inconsistent metadata for skill "${s.name}": ${OVERRIDE_FILE} exists but SKILL.md does not declare portable: true.`);
45
45
  }
46
46
  }
47
- function resolveExport(requested, roots) {
48
- const bundles = (0, bundles_1.discoverAllBundles)(roots);
47
+ function resolveExport(requested, roots, reporter) {
48
+ const bundles = (0, bundles_1.discoverAllBundles)(roots, reporter);
49
49
  if (bundles.some((b) => b.name === requested)) {
50
50
  const skills = [];
51
51
  const skipped = [];
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.reconcileUnattendedRecovery = reconcileUnattendedRecovery;
6
7
  exports.resolveWorkingDirectory = resolveWorkingDirectory;
7
8
  exports.computeFingerprint = computeFingerprint;
8
9
  const crypto_1 = __importDefault(require("crypto"));
@@ -19,8 +20,41 @@ function sha(parts) {
19
20
  // el stderr de git hacia el stderr DEL SUPERVISOR — si ese fd es un pipe roto, el
20
21
  // relay dispara un EPIPE no catcheable que crashea el proceso ENTERO (este helper
21
22
  // backea computeFingerprint, invocado en CADA tick via FingerprintNow/computeGate).
23
+ /** Git's textual plumbing is hashed, never streamed to a terminal. Keep its
24
+ * memory bounded even for hostile repositories; an incomplete listing must
25
+ * fail recovery rather than certify a partial fingerprint. */
26
+ const MAX_GIT_OUTPUT_BYTES = 16 * 1024 * 1024;
22
27
  function git(cwd, args) {
23
- return (0, child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8', maxBuffer: Infinity, stdio: process_1.EXEC_STDIO });
28
+ try {
29
+ return (0, child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8', maxBuffer: MAX_GIT_OUTPUT_BYTES, stdio: process_1.EXEC_STDIO });
30
+ }
31
+ catch (error) {
32
+ const code = error.code;
33
+ if (code === 'ENOBUFS' || error.message.includes('maxBuffer')) {
34
+ throw new Error(`git fingerprint output exceeded ${MAX_GIT_OUTPUT_BYTES} bytes`);
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+ function reconcileUnattendedRecovery(input) {
40
+ const activeJobIds = [...new Set(input.activeJobIds)].sort();
41
+ const result = (state, nextAction, diagnostics) => ({ state, nextAction, activeJobIds, diagnostics });
42
+ if (input.journalCorrupt || input.journal === null)
43
+ return result('blocked', 'repair-journal', ['journal-corrupt-or-missing']);
44
+ const binding = input.journal.schema === 2 ? input.journal.planBinding : undefined;
45
+ if (!binding || binding.path !== input.plan.path || binding.digest !== input.plan.digest || binding.schema !== input.plan.schema || binding.executionMode !== 'desatendido')
46
+ return result('blocked', 'rebind-plan', ['journal-plan-binding-stale']);
47
+ if (input.git !== 'current')
48
+ return result('blocked', 'reconcile-git', ['git-fingerprint-changed']);
49
+ if (activeJobIds.length > 0)
50
+ return result('ready', 'reconcile-active-jobs', []);
51
+ if (input.verdicts !== 'current')
52
+ return result('blocked', 'repair-verdicts', [`verdicts-${input.verdicts}`]);
53
+ if (input.tests !== 'pass')
54
+ return result('blocked', 'run-tests', [`tests-${input.tests}`]);
55
+ if (input.sensors !== 'pass')
56
+ return result('blocked', 'run-sensors', [`sensors-${input.sensors}`]);
57
+ return result('ready', 'select-work', []);
24
58
  }
25
59
  function resolveWorkingDirectory(repoRoot, cwdRel) {
26
60
  if (typeof cwdRel !== 'string' || cwdRel.length === 0)
@@ -13,6 +13,7 @@ exports.acksDir = acksDir;
13
13
  exports.logsDir = logsDir;
14
14
  exports.eventsPath = eventsPath;
15
15
  exports.exportDir = exportDir;
16
+ exports.bindingPlanPath = bindingPlanPath;
16
17
  const fs_1 = __importDefault(require("fs"));
17
18
  const path_1 = __importDefault(require("path"));
18
19
  function branchSlug(branch) {
@@ -63,3 +64,13 @@ function eventsPath(repoRoot, branch) {
63
64
  function exportDir(repoRoot, branch) {
64
65
  return path_1.default.join(journalDir(repoRoot, branch), 'export');
65
66
  }
67
+ /** A persisted binding path is always a normalized repository-relative POSIX path. */
68
+ function bindingPlanPath(planPath) {
69
+ if (typeof planPath !== 'string' || planPath.length === 0 || planPath.length > 4096 || /[\u0000-\u001F\u007F-\u009F]/.test(planPath))
70
+ throw new Error('plan path inválido para binding');
71
+ const normalized = path_1.default.posix.normalize(planPath.replace(/\\/g, '/'));
72
+ if (path_1.default.posix.isAbsolute(normalized) || path_1.default.win32.isAbsolute(planPath) || normalized === '..' || normalized.startsWith('../') || normalized === '.') {
73
+ throw new Error('plan path fuera del repositorio para binding');
74
+ }
75
+ return normalized;
76
+ }