agentic-workflow-manager 9.7.1 → 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 (78) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/job/exec-wrapper.js +53 -3
  3. package/dist/src/commands/plan/index.js +124 -6
  4. package/dist/src/commands/sensors/bootstrap.js +36 -5
  5. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  6. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  7. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  8. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  9. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  10. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  11. package/dist/src/commands/sensors/coverage/index.js +11 -2
  12. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  13. package/dist/src/commands/sensors/exec.js +16 -1
  14. package/dist/src/commands/sensors/migrate.js +62 -2
  15. package/dist/src/commands/sensors/prepare.js +2 -0
  16. package/dist/src/commands/sensors/result.js +2 -0
  17. package/dist/src/commands/sensors/run.js +64 -1
  18. package/dist/src/commands/sensors/status.js +6 -4
  19. package/dist/src/commands/watch/archive-unused.js +138 -0
  20. package/dist/src/commands/watch/generations.js +32 -9
  21. package/dist/src/commands/watch/index.js +108 -2
  22. package/dist/src/commands/watch/init.js +149 -18
  23. package/dist/src/commands/watch/supervisor.js +167 -13
  24. package/dist/src/commands/watch/teardown-driver.js +13 -13
  25. package/dist/src/commands/watch/track-state.js +59 -0
  26. package/dist/src/commands/watch/tracks.js +38 -102
  27. package/dist/src/core/admission/index.js +181 -0
  28. package/dist/src/core/admission/registry-contracts.js +290 -0
  29. package/dist/src/core/journal/fingerprint.js +35 -1
  30. package/dist/src/core/journal/paths.js +11 -0
  31. package/dist/src/core/journal/store.js +146 -4
  32. package/dist/src/core/journal/types.js +35 -1
  33. package/dist/src/core/migration/historical-provenance.js +18 -0
  34. package/dist/src/core/migration/index.js +322 -0
  35. package/dist/src/core/plan/identity.js +75 -0
  36. package/dist/src/core/plan/validate.js +178 -64
  37. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  38. package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
  39. package/dist/tests/commands/pin.test.js +23 -23
  40. package/dist/tests/commands/plan/index.test.js +630 -14
  41. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  42. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  43. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  44. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  45. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  46. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  47. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  48. package/dist/tests/commands/sensors/exec.test.js +112 -0
  49. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  50. package/dist/tests/commands/sensors/run.test.js +15 -2
  51. package/dist/tests/commands/sensors/status.test.js +3 -3
  52. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  53. package/dist/tests/commands/watch/generations.test.js +113 -4
  54. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  55. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  56. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  57. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  58. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  59. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  60. package/dist/tests/core/admission/index.test.js +87 -0
  61. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  62. package/dist/tests/core/journal/store.test.js +53 -0
  63. package/dist/tests/core/journal/types.test.js +7 -0
  64. package/dist/tests/core/migration/facts.test.js +289 -0
  65. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  66. package/dist/tests/core/plan/validate.test.js +320 -6
  67. package/dist/tests/integration/pack-e2e.test.js +4 -1
  68. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  69. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  70. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  71. package/dist/tests/providers/injection-config.test.js +14 -6
  72. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  73. package/dist/tests/structural/native-release-artifacts.test.js +16 -0
  74. package/package.json +2 -2
  75. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  76. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  77. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  78. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.registerWatchCommand = registerWatchCommand;
4
7
  const child_process_1 = require("child_process");
@@ -8,6 +11,11 @@ const process_1 = require("../../core/journal/process");
8
11
  const adapter_1 = require("../../core/journal/adapter");
9
12
  const context_1 = require("../../core/tracks/context");
10
13
  const concurrency_1 = require("../../core/tracks/concurrency");
14
+ const validate_1 = require("../../core/plan/validate");
15
+ const path_1 = __importDefault(require("path"));
16
+ const archive_unused_1 = require("./archive-unused");
17
+ const store_1 = require("../../core/journal/store");
18
+ const fingerprint_1 = require("../../core/journal/fingerprint");
11
19
  function currentBranch(cwd) {
12
20
  // stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
13
21
  // default de execFileSync del stderr de git hacia el stderr del llamante,
@@ -23,11 +31,19 @@ function minutes(flag, raw) {
23
31
  throw new Error(`${flag} requiere un numero de minutos > 0`);
24
32
  return n * 60000;
25
33
  }
34
+ function validPlanPath(value) {
35
+ return typeof value === 'string' && value.length > 0 && value.length <= 4096 && !/[\u0000-\u001F\u007F-\u009F]/.test(value);
36
+ }
37
+ function planForBinding(repo, rawPath) {
38
+ const report = (0, validate_1.validatePlanFile)(rawPath, repo);
39
+ return { path: path_1.default.relative(repo, path_1.default.resolve(repo, rawPath)).replace(/\\/g, '/'), report };
40
+ }
26
41
  function registerWatchCommand(program) {
27
- program
42
+ const watch = program
28
43
  .command('watch')
29
44
  .description('supervisor durable: ejecuta jobs, releva controladores caidos, nunca mata trabajo vivo')
30
45
  .option('--init', 'bootstrap: crea el journal de la rama actual, detecta verificadores y sale')
46
+ .option('--plan <path>', 'plan compacto desatendido que se vincula al inicializar')
31
47
  .option('--provider <p>', adapter_1.WATCH_PROVIDERS.join(' | '), 'codex')
32
48
  .option('--heartbeat-timeout <min>', 'minutos de silencio de heartbeat', '5')
33
49
  .option('--activity-window <min>', 'minutos extra sin actividad de proceso', '10')
@@ -45,8 +61,24 @@ function registerWatchCommand(program) {
45
61
  process.stderr.write(`${e.message}\n`);
46
62
  process.exit(1);
47
63
  }
64
+ if (opts.plan !== undefined && !opts.init) {
65
+ process.stderr.write('--plan requiere --init\n');
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+ if (opts.init && opts.plan === undefined) {
70
+ process.stderr.write('watch --init requiere --plan con un plan compacto válido\n');
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+ if (opts.plan !== undefined && !validPlanPath(opts.plan)) {
75
+ process.stderr.write('--plan requiere un path sin caracteres de control\n');
76
+ process.exitCode = 1;
77
+ return;
78
+ }
48
79
  if (opts.init) {
49
- const out = (0, init_1.initWatch)(repo, branch);
80
+ const plan = opts.plan === undefined ? undefined : planForBinding(repo, opts.plan);
81
+ const out = (0, init_1.initWatch)(repo, branch, plan);
50
82
  process.stdout.write(`journal inicializado para ${branch}; verificadores requeridos: ${JSON.stringify(out.requiredVerifiers)}\n`);
51
83
  return;
52
84
  }
@@ -70,4 +102,78 @@ function registerWatchCommand(program) {
70
102
  await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
71
103
  process.stdout.write('gate verde: ciclo COMPLETE — drenado, lock liberado, apagando\n');
72
104
  });
105
+ watch
106
+ .command('rebind')
107
+ .description('reconcilia intencionalmente el binding desatendido tras un cambio válido del ciclo del plan')
108
+ // `watch` conserva su --plan histórico para `watch --init --plan`.
109
+ // Commander lo asocia al padre incluso después del subcomando, por lo
110
+ // que este option es documental y la acción toma el valor del padre
111
+ // cuando corresponde; marcarlo required acá rechazaría una invocación
112
+ // válida antes de llegar a esa reconciliación explícita.
113
+ .option('--plan <path>', 'mismo plan compacto previamente vinculado; valida y conserva la historia antes de actualizar su digest')
114
+ .action((opts, command) => {
115
+ const repo = process.cwd();
116
+ const branch = currentBranch(repo);
117
+ try {
118
+ (0, context_1.resolveCommandContext)(repo, branch);
119
+ }
120
+ catch (e) {
121
+ process.stderr.write(`${e.message}\n`);
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ const plan = opts.plan ?? command.parent?.opts().plan;
126
+ if (!validPlanPath(plan)) {
127
+ process.stderr.write('--plan requiere un path sin caracteres de control\n');
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+ try {
132
+ const binding = (0, init_1.rebindWatchPlan)(repo, branch, planForBinding(repo, plan));
133
+ process.stdout.write(`binding reconciliado para ${binding.path}; digest ${binding.digest}\n`);
134
+ const state = (0, store_1.readJournal)(repo, branch).state;
135
+ const staleJobs = Object.values(state.jobs).filter(job => job.verdict === 'pass' && (() => {
136
+ try {
137
+ return (0, fingerprint_1.computeFingerprint)(repo, job.argv, job.paths, job.cwd).fingerprint !== job.fingerprint;
138
+ }
139
+ catch {
140
+ return true;
141
+ }
142
+ })()).map(job => job.id);
143
+ if (staleJobs.length > 0) {
144
+ process.stdout.write(`stale-fingerprint: ${staleJobs.join(', ')}; el rebind no conserva PASS con fingerprint cambiado. Re-ejecutar las verificaciones afectadas mediante awm job request con generation, paths y satisfies originales; no repetir watch/rebind ni re-fingerprinting de evidencia anterior.\n`);
145
+ }
146
+ }
147
+ catch (e) {
148
+ process.stderr.write(`${e.message}\n`);
149
+ process.exitCode = 1;
150
+ }
151
+ });
152
+ watch.command('archive-unused')
153
+ .description('archiva recuperablemente solo un bootstrap no utilizado; nunca declara COMPLETE ni crea evidencia de ejecución')
154
+ .option('--plan <path>', 'mismo plan vinculado al bootstrap; no adopta evidencia de trabajo manual')
155
+ .action((opts, command) => {
156
+ const repo = process.cwd();
157
+ const branch = currentBranch(repo);
158
+ try {
159
+ (0, context_1.resolveCommandContext)(repo, branch);
160
+ const plan = opts.plan ?? command.parent?.opts().plan;
161
+ if (!validPlanPath(plan))
162
+ throw new Error('--plan requiere un path sin caracteres de control');
163
+ const relative = path_1.default.relative(repo, path_1.default.resolve(repo, plan)).replace(/\\/g, '/');
164
+ process.stdout.write(`${JSON.stringify((0, archive_unused_1.archiveUnusedWatch)(repo, branch, relative))}\n`);
165
+ }
166
+ catch (error) {
167
+ process.stderr.write(`${error.message}\n`);
168
+ process.exitCode = 1;
169
+ }
170
+ });
171
+ watch.command('journal-status')
172
+ .description('consulta read-only el journal de la rama actual: missing | corrupt | present; no exporta contenido')
173
+ .option('--json', 'estado estructural y metadata de binding, sin cuerpos ni evidencia inferida')
174
+ .action(() => {
175
+ const repo = process.cwd();
176
+ const branch = currentBranch(repo);
177
+ process.stdout.write(`${JSON.stringify((0, archive_unused_1.watchJournalStatus)(repo, branch))}\n`);
178
+ });
73
179
  }
@@ -6,40 +6,78 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.detectRequiredVerifiers = detectRequiredVerifiers;
7
7
  exports.ensureJournalGitignored = ensureJournalGitignored;
8
8
  exports.initWatch = initWatch;
9
+ exports.rebindWatchPlan = rebindWatchPlan;
9
10
  // Bootstrap unico writer (R4.1) + validacion MECANICA plan-vs-repo (R1.4b,
10
11
  // bloqueador 5): la config real del repo determina los verificadores exigidos.
11
12
  const fs_1 = __importDefault(require("fs"));
12
13
  const path_1 = __importDefault(require("path"));
13
14
  const store_1 = require("../../core/journal/store");
15
+ const paths_1 = require("../../core/journal/paths");
16
+ const lock_1 = require("./lock");
17
+ const validate_1 = require("../../core/plan/validate");
18
+ const identity_1 = require("../../core/plan/identity");
19
+ const native_bridge_1 = require("../../core/secure-fs/native-bridge");
20
+ const MAX_VERIFIER_SCAN_DEPTH = 64;
21
+ const MAX_VERIFIER_SCAN_ENTRIES = 10000;
22
+ const MAX_PACKAGE_JSON_BYTES = 256 * 1024;
23
+ const MAX_SENSORS_JSON_BYTES = 256 * 1024;
14
24
  function detectRequiredVerifiers(repoRoot) {
15
25
  const kinds = new Set();
16
- const visit = (dir) => {
26
+ let visitedEntries = 0;
27
+ const visit = (dir, depth) => {
28
+ if (depth > MAX_VERIFIER_SCAN_DEPTH)
29
+ throw new Error('verifier scan depth limit exceeded');
17
30
  const sensors = path_1.default.join(dir, '.awm', 'sensors.json');
18
- if (fs_1.default.existsSync(sensors))
19
- kinds.add('sensors');
31
+ let sensorsPresent = false;
32
+ try {
33
+ fs_1.default.lstatSync(sensors);
34
+ sensorsPresent = true;
35
+ }
36
+ catch (error) {
37
+ if (error.code !== 'ENOENT')
38
+ throw new Error('verifier scan rejected sensors.json: unreadable path');
39
+ }
40
+ if (sensorsPresent) {
41
+ try {
42
+ const bytes = native_bridge_1.secureFs.readRegularFile(path_1.default.resolve(sensors), MAX_SENSORS_JSON_BYTES).bytes;
43
+ const config = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
44
+ if (!config || typeof config !== 'object' || Array.isArray(config))
45
+ throw new Error('sensors config must be a JSON object');
46
+ kinds.add('sensors');
47
+ }
48
+ catch (error) {
49
+ throw new Error(`verifier scan rejected sensors.json: ${error.message}`);
50
+ }
51
+ }
20
52
  let entries;
21
53
  try {
22
54
  entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
23
55
  }
24
- catch {
25
- return;
56
+ catch (error) {
57
+ throw new Error(`verifier scan cannot read ${dir}: ${error.message}`);
26
58
  }
27
59
  for (const entry of entries) {
28
- if (entry.isSymbolicLink())
29
- continue;
30
- if (entry.isFile() && entry.name === 'package.json') {
60
+ if (++visitedEntries > MAX_VERIFIER_SCAN_ENTRIES)
61
+ throw new Error('verifier scan entry limit exceeded');
62
+ if (entry.name === 'package.json') {
31
63
  try {
32
- const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, entry.name), 'utf8'));
64
+ const packagePath = path_1.default.join(dir, entry.name);
65
+ const bytes = native_bridge_1.secureFs.readRegularFile(path_1.default.resolve(packagePath), MAX_PACKAGE_JSON_BYTES).bytes;
66
+ const pkg = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
33
67
  if (typeof pkg === 'object' && pkg !== null && typeof pkg.scripts === 'object' && pkg.scripts !== null && typeof pkg.scripts.test === 'string')
34
68
  kinds.add('test');
35
69
  }
36
- catch { /* package ilegible: no prueba disponibilidad */ }
70
+ catch (error) {
71
+ throw new Error(`verifier scan rejected package.json: ${error.message}`);
72
+ }
37
73
  }
74
+ if (entry.isSymbolicLink())
75
+ continue;
38
76
  if (entry.isDirectory() && !['node_modules', '.git', '.awm'].includes(entry.name))
39
- visit(path_1.default.join(dir, entry.name));
77
+ visit(path_1.default.join(dir, entry.name), depth + 1);
40
78
  }
41
79
  };
42
- visit(repoRoot);
80
+ visit(repoRoot, 0);
43
81
  return ['test', 'sensors'].filter((kind) => kinds.has(kind));
44
82
  }
45
83
  /** El journal es gitignoreado (R1.1): sus escrituras jamas alteran fingerprints. */
@@ -47,19 +85,65 @@ function ensureJournalGitignored(repoRoot) {
47
85
  const gi = path_1.default.join(repoRoot, '.gitignore');
48
86
  let current = '';
49
87
  try {
88
+ if (fs_1.default.lstatSync(gi).isSymbolicLink())
89
+ throw new Error('.gitignore must not be a symlink');
50
90
  current = fs_1.default.readFileSync(gi, 'utf8');
51
91
  }
52
- catch {
53
- current = '';
92
+ catch (error) {
93
+ if (error.code !== 'ENOENT')
94
+ throw error;
54
95
  }
55
96
  if (!current.split('\n').some((l) => l.trim() === '.awm/' || l.trim() === '.awm')) {
56
97
  fs_1.default.writeFileSync(gi, current.length > 0 && !current.endsWith('\n') ? `${current}\n.awm/\n` : `${current}.awm/\n`);
57
98
  }
58
99
  }
59
- function initWatch(repoRoot, branch) {
60
- ensureJournalGitignored(repoRoot);
61
- (0, store_1.initJournal)(repoRoot, branch);
100
+ const TERMINAL_JOB_STATES = new Set(['exited', 'cancelled']);
101
+ function rebindBlocker(state) {
102
+ if (state === null)
103
+ return 'journal inexistente o corrupto';
104
+ const nonterminal = Object.values(state.jobs).filter(job => !TERMINAL_JOB_STATES.has(job.executionState));
105
+ if (nonterminal.length > 0)
106
+ return `hay jobs no terminales: ${nonterminal.map(job => job.id).join(', ')}`;
107
+ if (state.requestProblems.length > 0)
108
+ return 'hay conflictos durables de requests';
109
+ if (state.verdicts.some(verdict => verdict.result !== 'pass'))
110
+ return 'hay evidencia adversa durable';
111
+ if (state.fixes.some(fix => !fix.closed))
112
+ return 'hay fixes abiertos';
113
+ if (state.cycle.status === 'BLOCKED')
114
+ return 'el ciclo está bloqueado';
115
+ if (state.tracks?.some(track => track.phase !== 'REMOVED'))
116
+ return 'hay tracks no terminales';
117
+ return undefined;
118
+ }
119
+ function initWatch(repoRoot, branch, plan) {
120
+ if (plan) {
121
+ if (plan.report.state !== 'valid')
122
+ throw new Error('watch --init --plan requiere un plan compacto válido');
123
+ if (plan.report.executionMode !== 'desatendido')
124
+ throw new Error('watch --init --plan requiere un plan compacto desatendido');
125
+ if (plan.report.executionDigest)
126
+ (0, validate_1.verifiedPlanSnapshot)(plan.report);
127
+ }
128
+ if (plan && fs_1.default.existsSync((0, paths_1.statePath)(repoRoot, branch))) {
129
+ throw new Error('refusing to overwrite existing journal');
130
+ }
131
+ let planBinding;
132
+ if (plan) {
133
+ const report = plan.report;
134
+ planBinding = {
135
+ path: (0, paths_1.bindingPlanPath)(plan.path), digest: report.planDigest, schema: report.schema,
136
+ ...(report.executionDigest ? { executionDigest: report.executionDigest, executionIdentitySchema: identity_1.EXECUTION_IDENTITY_SCHEMA } : {}),
137
+ executionMode: 'desatendido', boundAt: new Date().toISOString(),
138
+ };
139
+ }
62
140
  const required = detectRequiredVerifiers(repoRoot);
141
+ (0, store_1.assertJournalInitPaths)(repoRoot, branch);
142
+ ensureJournalGitignored(repoRoot);
143
+ if (planBinding)
144
+ (0, store_1.initBoundJournal)(repoRoot, branch, planBinding, plan.report);
145
+ else
146
+ (0, store_1.initJournal)(repoRoot, branch);
63
147
  const r = (0, store_1.readJournal)(repoRoot, branch);
64
148
  if (r.corrupt || r.state === null)
65
149
  throw new Error('journal corrupto tras init: no se continua (R1.6)');
@@ -68,5 +152,52 @@ function initWatch(repoRoot, branch) {
68
152
  s.requiredVerifiers = required;
69
153
  (0, store_1.writeJournal)(repoRoot, branch, s);
70
154
  }
71
- return { requiredVerifiers: required };
155
+ return { requiredVerifiers: required, ...(planBinding ? { planBinding } : {}) };
156
+ }
157
+ /**
158
+ * Intentional recovery route for a valid compact plan whose lifecycle update
159
+ * changed its digest. It cannot initialize or overwrite a journal: only an
160
+ * existing schema-2 unattended binding for the exact canonical plan path is
161
+ * eligible, and all outstanding work/evidence conflicts fail closed.
162
+ */
163
+ function rebindWatchPlan(repoRoot, branch, plan) {
164
+ if (plan.report.state !== 'valid')
165
+ throw new Error('watch rebind --plan requiere un plan compacto válido');
166
+ // The same exclusive lock used by the supervisor closes the interval
167
+ // between observing quiescence and publishing the recovered binding.
168
+ const lock = (0, lock_1.acquireLock)(repoRoot);
169
+ try {
170
+ const current = (0, store_1.readJournal)(repoRoot, branch);
171
+ if (current.corrupt || current.state === null || current.state.schema !== 2 || !current.state.planBinding) {
172
+ throw new Error('no existe un journal schema-2 desatendido para reconciliar');
173
+ }
174
+ const existing = current.state.planBinding;
175
+ const canonicalPath = (0, paths_1.bindingPlanPath)(plan.path);
176
+ if (existing.executionMode !== 'desatendido')
177
+ throw new Error('solo se reconcilia un binding desatendido');
178
+ if (existing.path !== canonicalPath)
179
+ throw new Error('rebind requiere la misma ruta canónica del plan');
180
+ if (existing.digest === plan.report.planDigest && existing.schema === plan.report.schema)
181
+ throw new Error('el binding ya está vigente');
182
+ const blocker = rebindBlocker(current.state);
183
+ if (blocker)
184
+ throw new Error(`rebind rechazado: ${blocker}`);
185
+ const report = plan.report;
186
+ if (!existing.executionDigest || existing.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA || !report.executionDigest)
187
+ throw new Error('rebind rechazado: ciclo en curso o histórico sin prueba de identidad de ejecución');
188
+ (0, validate_1.verifiedPlanSnapshot)(report);
189
+ if (report.executionDigest !== existing.executionDigest || report.executionMode !== existing.executionMode || report.schema !== existing.schema)
190
+ throw new Error('rebind rechazado: cambiaron requisitos o contenido de ejecución; la evidencia anterior no se conserva');
191
+ const binding = {
192
+ path: canonicalPath, digest: report.planDigest, schema: report.schema,
193
+ executionDigest: report.executionDigest,
194
+ executionIdentitySchema: identity_1.EXECUTION_IDENTITY_SCHEMA,
195
+ executionMode: 'desatendido', boundAt: new Date().toISOString(),
196
+ };
197
+ (0, store_1.rebindJournalPlan)(repoRoot, branch, binding, report);
198
+ return binding;
199
+ }
200
+ finally {
201
+ (0, lock_1.releaseLock)(repoRoot, lock);
202
+ }
72
203
  }
@@ -2,11 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Supervisor = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
4
4
  exports.runSupervisorLoop = runSupervisorLoop;
5
- // Loop foreground (R4.4/R4.5): tick = apply -> collect/spawn -> stall -> gate.
6
- // COMPLETE exige gate verde (que exige cero vivos): drenaje ANTES de declarar.
7
- // Custodia BLOCKED: el loop sigue, el lock NO se libera, nada se mata.
8
5
  const store_1 = require("../../core/journal/store");
9
6
  const fingerprint_1 = require("../../core/journal/fingerprint");
7
+ const validate_1 = require("../../core/plan/validate");
8
+ const registry_contracts_1 = require("../../core/admission/registry-contracts");
9
+ const config_1 = require("../../utils/config");
10
10
  const adapter_1 = require("../../core/journal/adapter");
11
11
  const process_1 = require("../../core/journal/process");
12
12
  const gate_1 = require("../job/gate");
@@ -16,6 +16,20 @@ const apply_1 = require("./apply");
16
16
  const runner_1 = require("./runner");
17
17
  const tracks_1 = require("./tracks");
18
18
  const generations_1 = require("./generations");
19
+ function defaultDispatchAdmission(repoRoot, branch, provider) {
20
+ return async () => {
21
+ const observed = (0, store_1.readJournal)(repoRoot, branch);
22
+ const binding = observed.state?.schema === 2 ? observed.state.planBinding : undefined;
23
+ if (!binding || binding.executionMode !== 'desatendido') {
24
+ return { state: 'blocked', planState: 'invalid', journal: observed.corrupt ? 'corrupt' : 'missing', currentness: 'not-checked', sensors: 'not-required', diagnostics: [{ code: 'ADMISSION_JOURNAL_BINDING_REQUIRED', message: 'Unattended dispatch requires an exact compact plan binding.' }] };
25
+ }
26
+ const plan = (0, validate_1.validatePlanFile)(binding.path, repoRoot);
27
+ const preferences = (0, config_1.readPreferences)();
28
+ return (0, registry_contracts_1.admitRegistryPlan)({ plan, provider, cwd: repoRoot, enabledAgents: preferences.enabledAgents,
29
+ executionMode: 'desatendido', requireCurrent: true, verifySensors: true,
30
+ journalState: observed.state, journalCorrupt: observed.corrupt, planPath: binding.path });
31
+ };
32
+ }
19
33
  /** R7/C3/C4 (Task 12) + fix post-review #2 (re-derivado desde cero tras
20
34
  * encontrar que la justificación original no probaba lo que decía — ver
21
35
  * finding 1 del segundo round de re-review): fases de `CohortPhase` en las
@@ -107,21 +121,43 @@ exports.DEFAULT_SUPERVISOR_CONFIG = {
107
121
  maxParallelTracks: 1, // overridden en tiempo de ejecución con loadDefaultParallelism() (watch/index.ts)
108
122
  };
109
123
  const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
124
+ function recoveryWhitelistBlocker(state) {
125
+ if (state.cycle.status === 'BLOCKED')
126
+ return 'el ciclo está bloqueado';
127
+ if (state.requestProblems.length > 0)
128
+ return 'hay conflictos durables de requests';
129
+ // Pending work is the normal reason to launch (or retry launching) the
130
+ // controller. It is not recovery evidence and therefore cannot turn a
131
+ // transient launch failure into permanent custody. Conversely, once a
132
+ // non-empty task set claims completion, missing required evidence is an
133
+ // unsafe recovery fact and must remain fail-closed.
134
+ if (state.tasks.length === 0 || state.tasks.some(task => task.status !== 'done'))
135
+ return undefined;
136
+ const verificationItems = [...state.cycleVerificationPlan, ...state.tasks.flatMap(task => task.verificationPlan)];
137
+ for (const required of state.requiredVerifiers) {
138
+ const requiredItems = verificationItems.filter(item => item.kind === required);
139
+ if (requiredItems.length === 0 || requiredItems.some(item => item.satisfiedBy === undefined))
140
+ return `falta evidencia del verificador requerido: ${required}`;
141
+ }
142
+ return undefined;
143
+ }
110
144
  class Supervisor {
111
145
  repoRoot;
112
146
  branch;
113
147
  cfg;
114
148
  spawner;
149
+ dispatchAdmission;
115
150
  backoff = new generations_1.Backoff();
116
151
  relaunchNotBefore = 0;
117
152
  lastActivity = null;
118
153
  lastGenerationToken = null;
119
154
  trackRuntime;
120
- constructor(repoRoot, branch, cfg, spawner, trackRuntime) {
155
+ constructor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission = defaultDispatchAdmission(repoRoot, branch, cfg.provider)) {
121
156
  this.repoRoot = repoRoot;
122
157
  this.branch = branch;
123
158
  this.cfg = cfg;
124
159
  this.spawner = spawner;
160
+ this.dispatchAdmission = dispatchAdmission;
125
161
  this.trackRuntime = trackRuntime ?? (0, tracks_1.defaultTrackRuntime)(repoRoot, branch, { termGraceMs: cfg.termGraceMs, killGraceMs: cfg.killGraceMs });
126
162
  }
127
163
  fingerprintNow = (argv, paths, cwd) => {
@@ -132,7 +168,7 @@ class Supervisor {
132
168
  return null;
133
169
  } // no recomputable => el gate NO certifica (fail-closed)
134
170
  };
135
- ensureController(resumePrompt) {
171
+ ensureController(action) {
136
172
  if (Date.now() < this.relaunchNotBefore)
137
173
  return 'deferred';
138
174
  if (this.backoff.exhausted()) {
@@ -140,7 +176,7 @@ class Supervisor {
140
176
  return 'custody';
141
177
  }
142
178
  try {
143
- (0, generations_1.ensureControllerGeneration)(this.repoRoot, this.branch, this.cfg.provider, resumePrompt, this.spawner, this.cfg.reconcileGraceMs);
179
+ (0, generations_1.ensureControllerGeneration)(this.repoRoot, this.branch, this.cfg.provider, action, this.spawner, this.cfg.reconcileGraceMs);
144
180
  return 'ok';
145
181
  }
146
182
  catch (error) {
@@ -161,6 +197,121 @@ class Supervisor {
161
197
  const before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
162
198
  if (before0.corrupt || before0.state === null)
163
199
  throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
200
+ // Historical/schema-1 journals are intentionally readable by migration
201
+ // commands, never executable. Do this before controller recovery,
202
+ // request consumption, tracks, or runner reconciliation: each can cause
203
+ // a dispatch directly or indirectly.
204
+ if (before0.state.schema !== 2 || !before0.state.planBinding)
205
+ return 'custody';
206
+ // COMPLETE is terminal, not a recovery attempt. It cannot dispatch and
207
+ // must retain its normal no-op result even if old work evidence is no
208
+ // longer reconstructible.
209
+ if (before0.state.cycle.status === 'COMPLETE')
210
+ return 'complete';
211
+ // A controller can create jobs and runnerTick can start them. Both are
212
+ // downstream of the same full compact unattended admission, so it must
213
+ // complete before any reconciliation path that could dispatch either.
214
+ if (before0.state.schema === 2 && before0.state.planBinding) {
215
+ let admission;
216
+ try {
217
+ admission = await this.dispatchAdmission();
218
+ }
219
+ catch (error) {
220
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión desatendida no verificable: ${error.message}`);
221
+ return 'custody';
222
+ }
223
+ if (admission.state !== 'admitted' || admission.executionMode !== 'desatendido'
224
+ || admission.currentness !== 'current' || admission.sensors !== 'pass' || admission.journal !== 'current') {
225
+ // A first empirical sensor observation can be inconclusive while
226
+ // the already-started controller is still settling. It never
227
+ // authorizes dispatch; retry the read-only admission next tick
228
+ // instead of permanently custodying a healthy cycle.
229
+ if (admission.planState === 'valid' && admission.currentness === 'current'
230
+ && admission.sensors === 'not-certified' && admission.journal === 'not-required')
231
+ return 'continue';
232
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, `admisión compacta desatendida bloqueada antes de dispatch: ${admission.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join('; ')}`);
233
+ return 'custody';
234
+ }
235
+ }
236
+ let recoveryResumeAction;
237
+ // Schema-2 custody is reconciled before any controller launch. This is
238
+ // read-only: existing active jobs are reused, never re-requested.
239
+ if (before0.state.schema === 2 && before0.state.planBinding) {
240
+ const whitelistBlocker = recoveryWhitelistBlocker(before0.state);
241
+ if (whitelistBlocker) {
242
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, `recovery no autorizado: ${whitelistBlocker}`);
243
+ return 'custody';
244
+ }
245
+ const activeJobIds = Object.values(before0.state.jobs)
246
+ .filter(job => LIVE.includes(job.executionState) || job.executionState === 'orphaned').map(job => job.id);
247
+ const staleJob = Object.values(before0.state.jobs).filter(job => LIVE.includes(job.executionState) || job.executionState === 'orphaned').some(job => {
248
+ try {
249
+ return (0, fingerprint_1.computeFingerprint)(this.repoRoot, job.argv, job.paths, job.cwd).fingerprint !== job.fingerprint;
250
+ }
251
+ catch {
252
+ return true;
253
+ }
254
+ });
255
+ const boundPlan = (0, validate_1.validatePlanFile)(before0.state.planBinding.path, this.repoRoot);
256
+ const planChanged = boundPlan.state !== 'valid' || boundPlan.planDigest !== before0.state.planBinding.digest || boundPlan.schema !== before0.state.planBinding.schema;
257
+ const passed = (id) => {
258
+ if (!id)
259
+ return false;
260
+ const job = before0.state.jobs[id];
261
+ return job !== undefined && job.verdict === 'pass' && !staleJob;
262
+ };
263
+ const verificationItems = [...before0.state.cycleVerificationPlan, ...before0.state.tasks.flatMap(task => task.verificationPlan)];
264
+ const tests = verificationItems.filter(item => item.kind === 'test');
265
+ const sensorItems = verificationItems.filter(item => item.kind === 'sensors');
266
+ // `computeGate` is the single authority for every verification
267
+ // kind (review, QA and interlock included). Reuse its evidence
268
+ // semantics instead of maintaining a weaker recovery subset.
269
+ const evidenceGate = (0, gate_1.computeGate)(before0.state, false, this.fingerprintNow);
270
+ const terminalTaskClaims = before0.state.tasks.length > 0 && before0.state.tasks.every(task => task.status === 'done');
271
+ const unresolvedVerification = terminalTaskClaims && evidenceGate.reasons.some(reason => [
272
+ 'dangling-reference', 'unsatisfied-plan', 'adverse-verdict',
273
+ 'stale-fingerprint', 'open-obligation', 'open-fix',
274
+ ].includes(reason.category));
275
+ const hasStaleReview = before0.state.verdicts.some(verdict => verdict.fingerprint === '' || (verdict.argv.length > 0 && (() => {
276
+ try {
277
+ return (0, fingerprint_1.computeFingerprint)(this.repoRoot, verdict.argv, verdict.paths, verdict.cwd).fingerprint !== verdict.fingerprint;
278
+ }
279
+ catch {
280
+ return true;
281
+ }
282
+ })()));
283
+ // Keep the same fail-closed review/fix semantics as job/gate.ts:
284
+ // missing required kinds, dangling verdict references, adverse
285
+ // verdicts, and an adverse verdict without a closed fix all stop
286
+ // recovery before a controller can create more work.
287
+ const verdictById = new Map(before0.state.verdicts.map(verdict => [verdict.id, verdict]));
288
+ const openReviewOrFix = before0.state.tasks.some(task => {
289
+ const obligations = task.reviewObligations;
290
+ return !['spec', 'quality'].every(kind => obligations.some(obligation => obligation.kind === kind))
291
+ || obligations.some(obligation => {
292
+ const verdict = obligation.verdictId === undefined ? undefined : verdictById.get(obligation.verdictId);
293
+ return verdict === undefined || verdict.result !== 'pass';
294
+ });
295
+ }) || before0.state.verdicts.some(verdict => verdict.result !== 'pass'
296
+ && !before0.state.fixes.some(fix => fix.verdictId === verdict.id && fix.closed));
297
+ const recovery = (0, fingerprint_1.reconcileUnattendedRecovery)({
298
+ journal: before0.state, journalCorrupt: false, plan: before0.state.planBinding,
299
+ git: staleJob || planChanged ? 'changed' : 'current', activeJobIds,
300
+ tests: !terminalTaskClaims || tests.length === 0 || tests.every(item => passed(item.satisfiedBy)) ? 'pass' : 'missing',
301
+ sensors: !terminalTaskClaims || sensorItems.length === 0 || sensorItems.every(item => passed(item.satisfiedBy)) ? 'pass' : 'missing',
302
+ verdicts: hasStaleReview ? 'stale' : (openReviewOrFix || unresolvedVerification) ? 'missing' : 'current',
303
+ });
304
+ (0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'unattended-recovery', nextAction: recovery.nextAction, activeJobIds: recovery.activeJobIds, diagnostics: recovery.diagnostics });
305
+ if (recovery.state !== 'ready') {
306
+ const staleReasons = terminalTaskClaims ? evidenceGate.reasons.filter(reason => reason.category === 'stale-fingerprint') : [];
307
+ const remedy = staleReasons.length > 0
308
+ ? `; stale-fingerprint: ${staleReasons.map(reason => reason.detail).join('; ')}. Re-ejecutar verificaciones mediante awm job request con generation, paths y satisfies originales, y repetir reviews independientes cuando corresponda; no repetir watch/rebind ni actualizar fingerprints de PASS históricos`
309
+ : '';
310
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, `recovery no autorizado: ${recovery.diagnostics.join(', ')}${remedy}`);
311
+ return 'custody';
312
+ }
313
+ recoveryResumeAction = { schema: 'controller-recovery/v1', kind: recovery.nextAction === 'reconcile-active-jobs' ? 'reconcile-active-jobs' : 'resume-next-action' };
314
+ }
164
315
  // R6.2/R6.8/C7 (Task 11): reconciliar un `MERGE_HEAD` abierto por un
165
316
  // crash a mitad de un merge ANTES de cualquier guard general — hoy
166
317
  // ningún guard existente (`verifyBranchInvariant` incluido) rechaza
@@ -181,8 +332,8 @@ class Supervisor {
181
332
  if (before.frozen !== undefined)
182
333
  return 'frozen';
183
334
  const pending = before.cycle.nextAction;
184
- const resumePrompt = pending !== undefined ? `el next_action ${pending.actionId} del journal` : 'el plan del ciclo desde el journal';
185
- if (this.ensureController(resumePrompt) === 'custody')
335
+ const resumeAction = recoveryResumeAction ?? { schema: 'controller-recovery/v1', kind: pending !== undefined ? 'resume-next-action' : 'resume-cycle' };
336
+ if (this.ensureController(resumeAction) === 'custody')
186
337
  return 'custody';
187
338
  const r0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
188
339
  if (r0.corrupt || r0.state === null)
@@ -221,7 +372,7 @@ class Supervisor {
221
372
  if (!finalizing && afterRequests.state !== null && (0, generations_1.activeGeneration)(afterRequests.state) === undefined
222
373
  && afterRequests.state.generations.length > 0 && afterRequests.state.cycle.status === 'IN_PROGRESS') {
223
374
  (0, generations_1.beginGeneration)(this.repoRoot, this.branch);
224
- if (this.ensureController(resumePrompt) === 'custody')
375
+ if (this.ensureController(resumeAction) === 'custody')
225
376
  return 'custody';
226
377
  }
227
378
  // R5.2/R6.3 (Task 10): recién leído tras `consumePendingRequests` —
@@ -400,8 +551,8 @@ class Supervisor {
400
551
  return false; // esperando backoff, auditando
401
552
  (0, generations_1.beginGeneration)(this.repoRoot, this.branch);
402
553
  const nextAction = (0, store_1.readJournal)(this.repoRoot, this.branch).state.cycle.nextAction;
403
- const prompt = nextAction !== undefined ? `el next_action ${nextAction.actionId} del journal` : 'el plan del ciclo desde el journal';
404
- const launched = this.ensureController(prompt);
554
+ const action = { schema: 'controller-recovery/v1', kind: nextAction !== undefined ? 'resume-next-action' : 'resume-cycle' };
555
+ const launched = this.ensureController(action);
405
556
  if (launched === 'custody')
406
557
  return true;
407
558
  if (launched === 'ok') {
@@ -415,10 +566,13 @@ exports.Supervisor = Supervisor;
415
566
  /** Foreground, visible, terminable (R2.4): sin daemons. SIGINT/SIGTERM libera
416
567
  * el lock y sale; COMPLETE => auto-exit liberando lock y terminando la
417
568
  * generacion propia (cero huerfanos). */
418
- async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)(), trackRuntime) {
569
+ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)(), trackRuntime, dispatchAdmission) {
419
570
  const r = (0, store_1.readJournal)(repoRoot, branch);
420
571
  if (r.corrupt || r.state === null)
421
572
  throw new Error('journal ausente o corrupto: corre `awm watch --init` primero');
573
+ if (r.state.schema !== 2 || !r.state.planBinding) {
574
+ throw new Error('journal legacy o sin binding compacto: `awm watch` no puede ejecutar ni despachar trabajo');
575
+ }
422
576
  (0, lock_1.verifyBranchInvariant)(repoRoot, r.state.branch);
423
577
  if (r.state.cycle.status === 'COMPLETE')
424
578
  return;
@@ -429,7 +583,7 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
429
583
  const onSignal = () => { shutdownRequested = true; wakeSleep?.(); };
430
584
  process.on('SIGINT', onSignal);
431
585
  process.on('SIGTERM', onSignal);
432
- const sup = new Supervisor(repoRoot, branch, cfg, spawner, trackRuntime);
586
+ const sup = new Supervisor(repoRoot, branch, cfg, spawner, trackRuntime, dispatchAdmission);
433
587
  try {
434
588
  const s0 = (0, store_1.readJournal)(repoRoot, branch).state;
435
589
  if ((0, generations_1.activeGeneration)(s0) === undefined) {