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
@@ -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) {
@@ -21,7 +21,7 @@ const protocol_1 = require("../../core/tracks/protocol");
21
21
  const git_1 = require("../../core/tracks/git");
22
22
  const teardown_1 = require("../../core/tracks/teardown");
23
23
  const store_1 = require("../../core/journal/store");
24
- const tracks_1 = require("./tracks");
24
+ const track_state_1 = require("./track-state");
25
25
  /** Task 13 (R4.2/R4.3/R4.6/R4.10/C2/C9): gathering READ-ONLY del estado real
26
26
  * de UN track en teardown — jamás cuenta como el side effect del tick
27
27
  * (mismo criterio que `ownedWorktreeExists`/`foreignPathExists` en
@@ -94,7 +94,7 @@ function gatherTeardownObservation(planRoot, s, ref) {
94
94
  * haya funcionado, la relectura es la única fuente de verdad). */
95
95
  async function runBeginTeardown(planRoot, branch, s, protocol, effect, runtime) {
96
96
  const trackId = effect.trackId;
97
- const ref = (0, tracks_1.refOf)(s, trackId);
97
+ const ref = (0, track_state_1.refOf)(s, trackId);
98
98
  const observed = gatherTeardownObservation(planRoot, s, ref);
99
99
  const decision = (0, protocol_1.decideTeardown)(protocol.tracks[trackId], observed);
100
100
  if (decision === 'persist-intent') {
@@ -104,16 +104,16 @@ async function runBeginTeardown(planRoot, branch, s, protocol, effect, runtime)
104
104
  // dato del journal, no del protocolo puro, así que se aplica con
105
105
  // `withRef` en la MISMA persistencia (una sola frontera, no dos).
106
106
  const applied = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'effect-applied', effect });
107
- const withIntent = (0, tracks_1.withRef)((0, tracks_1.applyProtocolToState)(s, applied), trackId, {
107
+ const withIntent = (0, track_state_1.withRef)((0, track_state_1.applyProtocolToState)(s, applied), trackId, {
108
108
  teardownIntent: { worktreePath: ref.worktreePath, branch: ref.branch, supervisorNonce: ref.supervisorProcessRef?.spawnNonce },
109
109
  });
110
- const next = (0, tracks_1.persist)(planRoot, branch, withIntent);
110
+ const next = (0, track_state_1.persist)(planRoot, branch, withIntent);
111
111
  (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: 'TEARDOWN_INTENT' });
112
112
  return { state: next, stop: true, executed: effect.kind };
113
113
  }
114
114
  if (decision === 'block-foreign') {
115
115
  const blocked = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'teardown-observation', trackId, ...observed });
116
- const next = (0, tracks_1.persist)(planRoot, branch, (0, tracks_1.applyProtocolToState)(s, blocked));
116
+ const next = (0, track_state_1.persist)(planRoot, branch, (0, track_state_1.applyProtocolToState)(s, blocked));
117
117
  (0, store_1.appendEvent)(planRoot, branch, {
118
118
  kind: 'track-blocked', trackId,
119
119
  reason: observed.foreignSupervisor ? 'identidad de supervisor ajena' : 'worktree preexistente ajeno',
@@ -125,8 +125,8 @@ async function runBeginTeardown(planRoot, branch, s, protocol, effect, runtime)
125
125
  // `runtime` este tick (mismo criterio que 'accept-worktree' en
126
126
  // `runCreateWorktree`).
127
127
  const applied = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'teardown-observation', trackId, ...observed });
128
- const next = (0, tracks_1.persist)(planRoot, branch, (0, tracks_1.applyProtocolToState)(s, applied));
129
- (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: (0, tracks_1.refOf)(next, trackId).phase });
128
+ const next = (0, track_state_1.persist)(planRoot, branch, (0, track_state_1.applyProtocolToState)(s, applied));
129
+ (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: (0, track_state_1.refOf)(next, trackId).phase });
130
130
  return { state: next, stop: true, executed: effect.kind };
131
131
  }
132
132
  if (decision === 'stop-own-supervisor') {
@@ -135,7 +135,7 @@ async function runBeginTeardown(planRoot, branch, s, protocol, effect, runtime)
135
135
  const confirmed = await runtime.stopOwnSupervisor(ref);
136
136
  const after = { ownSupervisorAlive: !confirmed };
137
137
  const applied = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'teardown-observation', trackId, ...after });
138
- const next = (0, tracks_1.persist)(planRoot, branch, (0, tracks_1.applyProtocolToState)(s, applied));
138
+ const next = (0, track_state_1.persist)(planRoot, branch, (0, track_state_1.applyProtocolToState)(s, applied));
139
139
  (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: confirmed ? 'SUPERVISOR_STOPPED' : 'TEARDOWN_INTENT' });
140
140
  return { state: next, stop: true, executed: effect.kind };
141
141
  }
@@ -149,12 +149,12 @@ async function runBeginTeardown(planRoot, branch, s, protocol, effect, runtime)
149
149
  }
150
150
  catch (error) {
151
151
  const failed = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'effect-failed', trackId, effect: effect.kind, detail: error.message });
152
- const next = (0, tracks_1.persist)(planRoot, branch, (0, tracks_1.applyProtocolToState)(s, failed));
152
+ const next = (0, track_state_1.persist)(planRoot, branch, (0, track_state_1.applyProtocolToState)(s, failed));
153
153
  (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-effect-failed', trackId, effect: effect.kind, detail: error.message });
154
154
  return { state: next, stop: true, executed: effect.kind };
155
155
  }
156
156
  const applied = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'teardown-observation', trackId, ownedWorktreeExists: false });
157
- const next = (0, tracks_1.persist)(planRoot, branch, (0, tracks_1.applyProtocolToState)(s, applied));
157
+ const next = (0, track_state_1.persist)(planRoot, branch, (0, track_state_1.applyProtocolToState)(s, applied));
158
158
  (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: 'WORKTREE_REMOVED' });
159
159
  return { state: next, stop: true, executed: effect.kind };
160
160
  }
@@ -168,7 +168,7 @@ async function runBeginTeardown(planRoot, branch, s, protocol, effect, runtime)
168
168
  // próximo tick, ya en esa fase, decide la branch de nuevo con una
169
169
  // observación fresca.
170
170
  const applied = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'teardown-observation', trackId, ownedWorktreeExists: false });
171
- const next = (0, tracks_1.persist)(planRoot, branch, (0, tracks_1.applyProtocolToState)(s, applied));
171
+ const next = (0, track_state_1.persist)(planRoot, branch, (0, track_state_1.applyProtocolToState)(s, applied));
172
172
  (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: 'WORKTREE_REMOVED' });
173
173
  return { state: next, stop: true, executed: effect.kind };
174
174
  }
@@ -178,12 +178,12 @@ async function runBeginTeardown(planRoot, branch, s, protocol, effect, runtime)
178
178
  }
179
179
  catch (error) {
180
180
  const failed = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'effect-failed', trackId, effect: effect.kind, detail: error.message });
181
- const next = (0, tracks_1.persist)(planRoot, branch, (0, tracks_1.applyProtocolToState)(s, failed));
181
+ const next = (0, track_state_1.persist)(planRoot, branch, (0, track_state_1.applyProtocolToState)(s, failed));
182
182
  (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-effect-failed', trackId, effect: effect.kind, detail: error.message });
183
183
  return { state: next, stop: true, executed: effect.kind };
184
184
  }
185
185
  const applied = (0, protocol_1.observeProtocolEffect)(protocol, effect, { kind: 'teardown-observation', trackId, ownedBranchExists: false });
186
- const next = (0, tracks_1.persist)(planRoot, branch, (0, tracks_1.applyProtocolToState)(s, applied));
186
+ const next = (0, track_state_1.persist)(planRoot, branch, (0, track_state_1.applyProtocolToState)(s, applied));
187
187
  (0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: 'BRANCH_REMOVED' });
188
188
  return { state: next, stop: true, executed: effect.kind };
189
189
  }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyProtocolToState = applyProtocolToState;
4
+ exports.persist = persist;
5
+ exports.refOf = refOf;
6
+ exports.withRef = withRef;
7
+ const store_1 = require("../../core/journal/store");
8
+ const types_1 = require("../../core/tracks/types");
9
+ function applyProtocolToState(state, protocol) {
10
+ const next = structuredClone(state);
11
+ next.cohortPhase = protocol.cohortPhase;
12
+ if (protocol.planHeadSha !== undefined)
13
+ next.cohortPlanHeadSha = protocol.planHeadSha;
14
+ if (protocol.cohortPhase === 'FALLBACK_PENDING') {
15
+ next.globalQaHeadSha = undefined;
16
+ next.finalIntegrationJobId = undefined;
17
+ next.qaFinalizeRequested = undefined;
18
+ }
19
+ else {
20
+ if (protocol.globalQaHeadSha !== undefined)
21
+ next.globalQaHeadSha = protocol.globalQaHeadSha;
22
+ if (protocol.finalIntegrationJobId !== undefined)
23
+ next.finalIntegrationJobId = protocol.finalIntegrationJobId;
24
+ }
25
+ if (protocol.fallbackReason !== undefined)
26
+ next.cohortFallbackReason = protocol.fallbackReason;
27
+ next.tracks = (next.tracks ?? []).map((ref) => {
28
+ const track = protocol.tracks[ref.trackId];
29
+ if (track === undefined)
30
+ return ref;
31
+ return {
32
+ ...ref,
33
+ phase: track.phase,
34
+ frozenHeadSha: track.frozenHeadSha,
35
+ blockedReason: track.blockedReason,
36
+ joinedCommitSha: track.joinedCommitSha,
37
+ joinIntent: track.expectedPlanHeadSha !== undefined && track.expectedTrackHeadSha !== undefined
38
+ ? { expectedPlanHeadSha: track.expectedPlanHeadSha, expectedTrackHeadSha: track.expectedTrackHeadSha, strategy: types_1.JOIN_STRATEGY_NO_FF }
39
+ : ref.joinIntent,
40
+ };
41
+ });
42
+ return next;
43
+ }
44
+ function persist(planRoot, branch, state) {
45
+ (0, store_1.writeJournal)(planRoot, branch, state);
46
+ const result = (0, store_1.readJournal)(planRoot, branch);
47
+ if (result.corrupt || result.state === null)
48
+ throw new Error('journal corrupto tras persistir tracks (R1.6)');
49
+ return result.state;
50
+ }
51
+ function refOf(state, trackId) {
52
+ const ref = state.tracks?.find((track) => track.trackId === trackId);
53
+ if (ref === undefined)
54
+ throw new Error(`invariante rota: TrackRef ausente para ${trackId}`);
55
+ return ref;
56
+ }
57
+ function withRef(state, trackId, patch) {
58
+ return { ...state, tracks: (state.tracks ?? []).map((track) => (track.trackId === trackId ? { ...track, ...patch } : track)) };
59
+ }