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.
- package/README.md +5 -1
- package/dist/src/commands/job/exec-wrapper.js +53 -3
- package/dist/src/commands/plan/index.js +124 -6
- package/dist/src/commands/sensors/bootstrap.js +36 -5
- package/dist/src/commands/sensors/compatibility/contract.js +19 -4
- package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
- package/dist/src/commands/sensors/compatibility/live.js +18 -1
- package/dist/src/commands/sensors/compatibility/probe.js +8 -2
- package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
- package/dist/src/commands/sensors/coverage/evidence.js +19 -10
- package/dist/src/commands/sensors/coverage/index.js +11 -2
- package/dist/src/commands/sensors/coverage/resolve.js +10 -0
- package/dist/src/commands/sensors/exec.js +16 -1
- package/dist/src/commands/sensors/migrate.js +62 -2
- package/dist/src/commands/sensors/prepare.js +2 -0
- package/dist/src/commands/sensors/result.js +2 -0
- package/dist/src/commands/sensors/run.js +64 -1
- package/dist/src/commands/sensors/status.js +6 -4
- package/dist/src/commands/watch/archive-unused.js +138 -0
- package/dist/src/commands/watch/generations.js +32 -9
- package/dist/src/commands/watch/index.js +108 -2
- package/dist/src/commands/watch/init.js +149 -18
- package/dist/src/commands/watch/supervisor.js +167 -13
- package/dist/src/commands/watch/teardown-driver.js +13 -13
- package/dist/src/commands/watch/track-state.js +59 -0
- package/dist/src/commands/watch/tracks.js +38 -102
- package/dist/src/core/admission/index.js +181 -0
- package/dist/src/core/admission/registry-contracts.js +290 -0
- package/dist/src/core/journal/fingerprint.js +35 -1
- package/dist/src/core/journal/paths.js +11 -0
- package/dist/src/core/journal/store.js +146 -4
- package/dist/src/core/journal/types.js +35 -1
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- package/dist/src/core/plan/identity.js +75 -0
- package/dist/src/core/plan/validate.js +178 -64
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
- package/dist/tests/commands/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +630 -14
- package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
- package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
- package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
- package/dist/tests/commands/sensors/exec.test.js +112 -0
- package/dist/tests/commands/sensors/run-changed.test.js +11 -0
- package/dist/tests/commands/sensors/run.test.js +15 -2
- package/dist/tests/commands/sensors/status.test.js +3 -3
- package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
- package/dist/tests/commands/watch/generations.test.js +113 -4
- package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
- package/dist/tests/commands/watch/track-finalize.test.js +20 -3
- package/dist/tests/commands/watch/track-freeze.test.js +43 -30
- package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
- package/dist/tests/commands/watch/watch-init.test.js +244 -0
- package/dist/tests/core/admission/index.test.js +87 -0
- package/dist/tests/core/journal/fingerprint.test.js +22 -0
- package/dist/tests/core/journal/store.test.js +53 -0
- package/dist/tests/core/journal/types.test.js +7 -0
- package/dist/tests/core/migration/facts.test.js +289 -0
- package/dist/tests/core/migration/historical-provenance.test.js +25 -0
- package/dist/tests/core/plan/validate.test.js +320 -6
- package/dist/tests/integration/pack-e2e.test.js +4 -1
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
- package/dist/tests/providers/injection-config.test.js +14 -6
- package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +16 -0
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- 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
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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 (
|
|
29
|
-
|
|
30
|
-
if (entry.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
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,
|
|
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
|
|
185
|
-
if (this.ensureController(
|
|
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(
|
|
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
|
|
404
|
-
const launched = this.ensureController(
|
|
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) {
|