agentic-workflow-manager 9.7.1 → 9.9.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 (107) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/job/exec-wrapper.js +53 -3
  3. package/dist/src/commands/job/gate.js +18 -0
  4. package/dist/src/commands/job/index.js +61 -0
  5. package/dist/src/commands/model-policy/index.js +73 -0
  6. package/dist/src/commands/plan/index.js +182 -7
  7. package/dist/src/commands/sensors/bootstrap.js +36 -5
  8. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  9. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  10. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  11. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  12. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  13. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  14. package/dist/src/commands/sensors/coverage/index.js +11 -2
  15. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  16. package/dist/src/commands/sensors/exec.js +16 -1
  17. package/dist/src/commands/sensors/migrate.js +62 -2
  18. package/dist/src/commands/sensors/prepare.js +2 -0
  19. package/dist/src/commands/sensors/result.js +2 -0
  20. package/dist/src/commands/sensors/run.js +64 -1
  21. package/dist/src/commands/sensors/status.js +6 -4
  22. package/dist/src/commands/watch/apply.js +32 -0
  23. package/dist/src/commands/watch/archive-unused.js +138 -0
  24. package/dist/src/commands/watch/generations.js +32 -9
  25. package/dist/src/commands/watch/index.js +108 -2
  26. package/dist/src/commands/watch/init.js +149 -18
  27. package/dist/src/commands/watch/supervisor.js +167 -13
  28. package/dist/src/commands/watch/teardown-driver.js +13 -13
  29. package/dist/src/commands/watch/track-state.js +59 -0
  30. package/dist/src/commands/watch/tracks.js +38 -102
  31. package/dist/src/core/admission/index.js +231 -0
  32. package/dist/src/core/admission/registry-contracts.js +290 -0
  33. package/dist/src/core/journal/fingerprint.js +35 -1
  34. package/dist/src/core/journal/paths.js +11 -0
  35. package/dist/src/core/journal/requests.js +1 -1
  36. package/dist/src/core/journal/store.js +146 -4
  37. package/dist/src/core/journal/types.js +57 -2
  38. package/dist/src/core/migration/historical-provenance.js +18 -0
  39. package/dist/src/core/migration/index.js +322 -0
  40. package/dist/src/core/model-policy/canonical.js +18 -0
  41. package/dist/src/core/model-policy/capabilities.js +197 -0
  42. package/dist/src/core/model-policy/capability-types.js +2 -0
  43. package/dist/src/core/model-policy/journal.js +124 -0
  44. package/dist/src/core/model-policy/paths.js +12 -0
  45. package/dist/src/core/model-policy/resolve.js +116 -0
  46. package/dist/src/core/model-policy/store.js +182 -0
  47. package/dist/src/core/model-policy/types.js +2 -0
  48. package/dist/src/core/model-policy/validate.js +112 -0
  49. package/dist/src/core/plan/identity.js +77 -0
  50. package/dist/src/core/plan/validate.js +191 -78
  51. package/dist/src/index.js +2 -0
  52. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  53. package/dist/tests/commands/job/gate-reconcile.test.js +40 -10
  54. package/dist/tests/commands/job/gate.test.js +9 -0
  55. package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
  56. package/dist/tests/commands/job/routing.test.js +11 -0
  57. package/dist/tests/commands/model-policy/index.test.js +57 -0
  58. package/dist/tests/commands/pin.test.js +23 -23
  59. package/dist/tests/commands/plan/index.test.js +682 -14
  60. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  61. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  62. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  63. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  64. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  65. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  66. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  67. package/dist/tests/commands/sensors/exec.test.js +112 -0
  68. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  69. package/dist/tests/commands/sensors/run.test.js +15 -2
  70. package/dist/tests/commands/sensors/status.test.js +3 -3
  71. package/dist/tests/commands/watch/apply.test.js +12 -0
  72. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  73. package/dist/tests/commands/watch/generations.test.js +113 -4
  74. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  75. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  76. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  77. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  78. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  79. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  80. package/dist/tests/core/admission/index.test.js +140 -0
  81. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  82. package/dist/tests/core/journal/store.test.js +53 -0
  83. package/dist/tests/core/journal/types.test.js +7 -0
  84. package/dist/tests/core/migration/facts.test.js +289 -0
  85. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  86. package/dist/tests/core/model-policy/capabilities.test.js +365 -0
  87. package/dist/tests/core/model-policy/journal.test.js +46 -0
  88. package/dist/tests/core/model-policy/policy.test.js +47 -0
  89. package/dist/tests/core/model-policy/resolve.test.js +111 -0
  90. package/dist/tests/core/model-policy/store.test.js +177 -0
  91. package/dist/tests/core/plan/validate.test.js +360 -8
  92. package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
  93. package/dist/tests/integration/pack-e2e.test.js +4 -1
  94. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  95. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  96. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  97. package/dist/tests/providers/injection-config.test.js +14 -6
  98. package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
  99. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  100. package/dist/tests/structural/native-release-artifacts.test.js +30 -0
  101. package/package.json +4 -3
  102. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  103. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  104. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  105. package/prebuilds/linux-x64/secure_fs.node +0 -0
  106. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  107. 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, ...(dependencies.readRouting ? { routingReader: dependencies.readRouting } : {}) });
290
+ }
@@ -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
+ }
@@ -68,7 +68,7 @@ function emitRequest(repoRoot, branch, env) {
68
68
  const KNOWN_KINDS = [
69
69
  'job-request', 'register-entity', 'controller-heartbeat', 'verdict',
70
70
  'track-prepare-request', 'track-freeze-request', 'track-join-request',
71
- 'track-teardown-request', 'track-finalize-request',
71
+ 'track-teardown-request', 'track-finalize-request', 'routing-reserve', 'routing-observe',
72
72
  ];
73
73
  function isRecord(x) {
74
74
  return typeof x === 'object' && x !== null && !Array.isArray(x);
@@ -3,15 +3,21 @@ 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.assertJournalInitPaths = assertJournalInitPaths;
6
7
  exports.initJournal = initJournal;
8
+ exports.initBoundJournal = initBoundJournal;
7
9
  exports.readJournal = readJournal;
8
10
  exports.writeJournal = writeJournal;
11
+ exports.rebindJournalPlan = rebindJournalPlan;
9
12
  exports.appendEvent = appendEvent;
10
13
  const fs_1 = __importDefault(require("fs"));
11
14
  const crypto_1 = __importDefault(require("crypto"));
15
+ const path_1 = __importDefault(require("path"));
12
16
  const atomic_file_1 = require("../atomic-file");
13
17
  const types_1 = require("./types");
14
18
  const paths_1 = require("./paths");
19
+ const identity_1 = require("../plan/identity");
20
+ const validate_1 = require("../plan/validate");
15
21
  /** Schema 1 evoluciono de forma aditiva durante R1/R5. Normalizamos solamente
16
22
  * campos que antes no existian; evidencia legacy queda deliberadamente con
17
23
  * fingerprint vacio para que el gate la considere stale, nunca certificada.
@@ -79,22 +85,116 @@ function normalizeSchemaOne(value) {
79
85
  }
80
86
  return parsed;
81
87
  }
82
- function initJournal(repoRoot, branch) {
83
- for (const d of [(0, paths_1.journalDir)(repoRoot, branch), (0, paths_1.requestsDir)(repoRoot, branch), (0, paths_1.acksDir)(repoRoot, branch), (0, paths_1.logsDir)(repoRoot, branch), (0, paths_1.exportDir)(repoRoot, branch)]) {
84
- fs_1.default.mkdirSync(d, { recursive: true, mode: 0o700 });
88
+ function initializeDirectories(repoRoot, branch) {
89
+ for (const d of journalDirectories(repoRoot, branch)) {
90
+ assertControlledDirectory(d);
91
+ if (!fs_1.default.existsSync(d))
92
+ fs_1.default.mkdirSync(d, { recursive: false, mode: 0o700 });
93
+ assertControlledDirectory(d);
85
94
  fs_1.default.chmodSync(d, 0o700); // mkdirSync mode es umask-dependiente: fijar explicito (R1.2)
86
95
  }
96
+ }
97
+ /** Every journal segment is owned state. Validate it centrally before any
98
+ * directory operation or journal file I/O so a hostile `.awm`/`journal`/branch
99
+ * link cannot redirect writes or reads outside the repository. */
100
+ function journalDirectories(repoRoot, branch) {
101
+ const root = path_1.default.join(repoRoot, '.awm');
102
+ const journal = path_1.default.join(root, 'journal');
103
+ return [root, journal, (0, paths_1.journalDir)(repoRoot, branch), (0, paths_1.requestsDir)(repoRoot, branch), (0, paths_1.acksDir)(repoRoot, branch), (0, paths_1.logsDir)(repoRoot, branch), (0, paths_1.exportDir)(repoRoot, branch)];
104
+ }
105
+ function assertControlledDirectory(directory) {
106
+ try {
107
+ const stat = fs_1.default.lstatSync(directory);
108
+ if (stat.isSymbolicLink())
109
+ throw new Error(`journal path rejects symlink: ${directory}`);
110
+ if (!stat.isDirectory())
111
+ throw new Error(`journal path is not a directory: ${directory}`);
112
+ }
113
+ catch (error) {
114
+ if (error.code === 'ENOENT')
115
+ return;
116
+ throw error;
117
+ }
118
+ }
119
+ function assertJournalTree(repoRoot, branch) {
120
+ for (const directory of journalDirectories(repoRoot, branch).slice(0, 3))
121
+ assertControlledDirectory(directory);
122
+ }
123
+ /** Inspect every existing bootstrap segment before the caller writes anything. */
124
+ function assertJournalInitPaths(repoRoot, branch) {
125
+ for (const directory of journalDirectories(repoRoot, branch))
126
+ assertControlledDirectory(directory);
127
+ assertJournalFileNotSymlink((0, paths_1.statePath)(repoRoot, branch));
128
+ }
129
+ function assertJournalFileNotSymlink(file) {
130
+ try {
131
+ if (fs_1.default.lstatSync(file).isSymbolicLink())
132
+ throw new Error(`journal file rejects symlink: ${file}`);
133
+ }
134
+ catch (error) {
135
+ if (error.code === 'ENOENT')
136
+ return;
137
+ throw error;
138
+ }
139
+ }
140
+ function initJournal(repoRoot, branch) {
141
+ initializeDirectories(repoRoot, branch);
87
142
  const sp = (0, paths_1.statePath)(repoRoot, branch);
143
+ assertJournalFileNotSymlink(sp);
88
144
  if (!fs_1.default.existsSync(sp)) {
89
145
  (0, atomic_file_1.writeFileAtomicDurable)(sp, JSON.stringify((0, types_1.emptyState)(branch), null, 2) + '\n', 0o600);
90
146
  }
91
147
  }
148
+ /** Create, never replace, the schema-2 state. The final hard-link is an
149
+ * exclusive atomic publication: another initializer wins cleanly and the
150
+ * completed prior journal is left untouched. */
151
+ function initBoundJournal(repoRoot, branch, binding, report) {
152
+ if (binding.executionDigest !== undefined)
153
+ assertBindingReport(binding, report);
154
+ initializeDirectories(repoRoot, branch);
155
+ const sp = (0, paths_1.statePath)(repoRoot, branch);
156
+ assertJournalFileNotSymlink(sp);
157
+ const initial = { ...(0, types_1.emptyState)(branch), schema: 2, planBinding: binding };
158
+ if (!(0, types_1.isWellFormedState)(initial))
159
+ throw new Error('binding de plan invalido');
160
+ const temporary = path_1.default.join((0, paths_1.journalDir)(repoRoot, branch), `.state.${process.pid}.${crypto_1.default.randomUUID()}.tmp`);
161
+ let fd;
162
+ try {
163
+ fd = fs_1.default.openSync(temporary, 'wx', 0o600);
164
+ fs_1.default.writeFileSync(fd, JSON.stringify(initial, null, 2) + '\n', 'utf8');
165
+ fs_1.default.fsyncSync(fd);
166
+ fs_1.default.closeSync(fd);
167
+ fd = undefined;
168
+ try {
169
+ fs_1.default.linkSync(temporary, sp);
170
+ (0, atomic_file_1.fsyncDirSync)(path_1.default.dirname(sp));
171
+ }
172
+ catch (error) {
173
+ if (error.code === 'EEXIST')
174
+ throw new Error('refusing to overwrite existing journal');
175
+ throw error;
176
+ }
177
+ }
178
+ finally {
179
+ if (fd !== undefined)
180
+ try {
181
+ fs_1.default.closeSync(fd);
182
+ }
183
+ catch { /* preserve primary error */ }
184
+ try {
185
+ fs_1.default.rmSync(temporary, { force: true });
186
+ }
187
+ catch { /* preserve primary error */ }
188
+ }
189
+ }
92
190
  /** Lectura corrupt-aware (R1.6): sintaxis invalida O shape invalido => corrupt:true.
93
191
  * Los CONSUMIDORES deciden: consultas muestran 'corrupt'; gate/reconcile bloquean. */
94
192
  function readJournal(repoRoot, branch) {
95
193
  const sp = (0, paths_1.statePath)(repoRoot, branch);
96
194
  let raw;
97
195
  try {
196
+ assertJournalTree(repoRoot, branch);
197
+ assertJournalFileNotSymlink(sp);
98
198
  raw = fs_1.default.readFileSync(sp, 'utf8');
99
199
  }
100
200
  catch {
@@ -125,13 +225,55 @@ function writeJournal(repoRoot, branch, state) {
125
225
  const next = { ...state, revision: state.revision + 1 };
126
226
  if (!(0, types_1.isWellFormedState)(next))
127
227
  throw new Error('writeJournal: estado propuesto con forma invalida, no se persiste (R1.6)');
228
+ assertJournalTree(repoRoot, branch);
229
+ assertJournalFileNotSymlink((0, paths_1.statePath)(repoRoot, branch));
128
230
  (0, atomic_file_1.writeFileAtomicDurable)((0, paths_1.statePath)(repoRoot, branch), JSON.stringify(next, null, 2) + '\n', 0o600);
129
231
  }
232
+ /** Explicit, guarded schema-2 binding transition. It intentionally uses the
233
+ * normal journal write path: state and its audit history are published in one
234
+ * atomic durable replacement, while initBoundJournal remains create-only. */
235
+ function rebindJournalPlan(repoRoot, branch, binding, report) {
236
+ const current = readJournal(repoRoot, branch);
237
+ if (current.corrupt || current.state === null)
238
+ throw new Error('journal inexistente o corrupto: no se puede reconciliar binding');
239
+ if (current.state.schema !== 2 || !current.state.planBinding)
240
+ throw new Error('journal no tiene un binding desatendido reconciliable');
241
+ const previous = current.state.planBinding;
242
+ if (!previous.executionDigest || previous.executionDigest !== binding.executionDigest
243
+ || previous.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA || binding.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA
244
+ || previous.path !== binding.path || previous.schema !== binding.schema || previous.executionMode !== binding.executionMode) {
245
+ throw new Error('rebind requires proved unchanged execution identity');
246
+ }
247
+ assertBindingReport(binding, report);
248
+ const next = {
249
+ ...current.state,
250
+ planBinding: binding,
251
+ planBindingHistory: [...(current.state.planBindingHistory ?? []), current.state.planBinding],
252
+ };
253
+ writeJournal(repoRoot, branch, next);
254
+ return { ...next, revision: next.revision + 1 };
255
+ }
256
+ /** The old commitment was emitted at initialization/rebind from trusted core
257
+ * validation. Only a new immutable report can demonstrate the same execution
258
+ * identity. Snapshot bytes stay process-private and are never written/exported. */
259
+ function assertBindingReport(binding, report) {
260
+ if (!report || report.state !== 'valid')
261
+ throw new Error('binding execution identity requires a verified plan report');
262
+ const snapshot = (0, validate_1.verifiedPlanSnapshot)(report);
263
+ if (binding.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA || report.planDigest !== binding.digest
264
+ || report.executionDigest !== binding.executionDigest || report.schema !== binding.schema
265
+ || report.executionMode !== binding.executionMode || (0, identity_1.fullPlanDigest)(snapshot) !== binding.digest
266
+ || (0, identity_1.executionPlanDigest)(snapshot) !== binding.executionDigest)
267
+ throw new Error('verified plan snapshot mismatches binding commitment');
268
+ }
130
269
  /** Auditoria derivada best-effort (R4.6): la escribe SOLO el supervisor, un
131
270
  * fallo aqui jamas invalida el estado — state.json es la unica autoridad. */
132
271
  function appendEvent(repoRoot, branch, event) {
133
272
  try {
134
- fs_1.default.appendFileSync((0, paths_1.eventsPath)(repoRoot, branch), JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n', { mode: 0o600 });
273
+ assertJournalTree(repoRoot, branch);
274
+ const ep = (0, paths_1.eventsPath)(repoRoot, branch);
275
+ assertJournalFileNotSymlink(ep);
276
+ fs_1.default.appendFileSync(ep, JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n', { mode: 0o600 });
135
277
  }
136
278
  catch {
137
279
  // best-effort: un evento perdido no se reconstruye ni bloquea (R4.6)