agentic-workflow-manager 6.2.1 → 6.4.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 +17 -0
- package/dist/src/commands/job/gate.js +37 -13
- package/dist/src/commands/job/index.js +48 -8
- package/dist/src/commands/job/request.js +44 -6
- package/dist/src/commands/track/emit.js +26 -0
- package/dist/src/commands/track/index.js +194 -0
- package/dist/src/commands/track/status.js +63 -0
- package/dist/src/commands/track/supervisor-wrapper.js +89 -0
- package/dist/src/commands/watch/apply.js +201 -13
- package/dist/src/commands/watch/index.js +14 -0
- package/dist/src/commands/watch/runner.js +20 -1
- package/dist/src/commands/watch/supervisor.js +251 -27
- package/dist/src/commands/watch/teardown-driver.js +189 -0
- package/dist/src/commands/watch/tracks.js +1100 -0
- package/dist/src/core/journal/adapter.js +46 -6
- package/dist/src/core/journal/paths.js +9 -0
- package/dist/src/core/journal/process.js +101 -1
- package/dist/src/core/journal/requests.js +5 -1
- package/dist/src/core/journal/store.js +34 -2
- package/dist/src/core/journal/types.js +93 -4
- package/dist/src/core/paths.js +51 -0
- package/dist/src/core/tracks/concurrency.js +85 -0
- package/dist/src/core/tracks/context.js +89 -0
- package/dist/src/core/tracks/descriptor.js +40 -0
- package/dist/src/core/tracks/git.js +318 -0
- package/dist/src/core/tracks/join.js +185 -0
- package/dist/src/core/tracks/ownership.js +100 -0
- package/dist/src/core/tracks/plan-parser.js +108 -0
- package/dist/src/core/tracks/protocol.js +466 -0
- package/dist/src/core/tracks/teardown.js +34 -0
- package/dist/src/core/tracks/types.js +14 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +267 -0
- package/dist/tests/commands/track/fixtures.js +13 -0
- package/dist/tests/commands/track/status.test.js +157 -0
- package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +129 -0
- package/dist/tests/commands/track/supervisor-wrapper.test.js +131 -0
- package/dist/tests/commands/track/verbs.test.js +326 -0
- package/dist/tests/commands/watch/apply.test.js +116 -4
- package/dist/tests/commands/watch/runner.test.js +22 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +150 -0
- package/dist/tests/commands/watch/track-bootstrap-crash.test.js +342 -0
- package/dist/tests/commands/watch/track-bootstrap.test.js +350 -0
- package/dist/tests/commands/watch/track-finalize.test.js +643 -0
- package/dist/tests/commands/watch/track-freeze.test.js +591 -0
- package/dist/tests/commands/watch/track-join-crash.test.js +439 -0
- package/dist/tests/commands/watch/track-runtime-git.test.js +119 -0
- package/dist/tests/commands/watch/track-teardown-crash.test.js +426 -0
- package/dist/tests/core/journal/adapter-override.test.js +51 -0
- package/dist/tests/core/journal/process.test.js +37 -0
- package/dist/tests/core/journal/requests.test.js +21 -0
- package/dist/tests/core/journal/store.test.js +28 -0
- package/dist/tests/core/journal/types.test.js +89 -0
- package/dist/tests/core/same-existing-path.test.js +48 -0
- package/dist/tests/core/tracks/concurrency.test.js +134 -0
- package/dist/tests/core/tracks/context.test.js +177 -0
- package/dist/tests/core/tracks/descriptor.test.js +76 -0
- package/dist/tests/core/tracks/git.test.js +132 -0
- package/dist/tests/core/tracks/join-reconcile.test.js +53 -0
- package/dist/tests/core/tracks/join.test.js +197 -0
- package/dist/tests/core/tracks/ownership.test.js +96 -0
- package/dist/tests/core/tracks/plan-parser.test.js +94 -0
- package/dist/tests/core/tracks/protocol.test.js +416 -0
- package/dist/tests/core/tracks/teardown.test.js +62 -0
- package/dist/tests/helpers/git-fixture.js +35 -0
- package/dist/tests/integration/parallel-tracks.e2e.test.js +343 -0
- package/dist/tests/integration/r5-provider-evidence.test.js +67 -0
- package/dist/tests/structural/path-identity-not-string-compare.test.js +51 -0
- package/dist/tests/structural/sensor-configs-are-present.test.js +49 -0
- package/package.json +1 -1
|
@@ -10,10 +10,91 @@ const fingerprint_1 = require("../../core/journal/fingerprint");
|
|
|
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");
|
|
13
|
+
const git_1 = require("../../core/tracks/git");
|
|
13
14
|
const lock_1 = require("./lock");
|
|
14
15
|
const apply_1 = require("./apply");
|
|
15
16
|
const runner_1 = require("./runner");
|
|
17
|
+
const tracks_1 = require("./tracks");
|
|
16
18
|
const generations_1 = require("./generations");
|
|
19
|
+
/** R7/C3/C4 (Task 12) + fix post-review #2 (re-derivado desde cero tras
|
|
20
|
+
* encontrar que la justificación original no probaba lo que decía — ver
|
|
21
|
+
* finding 1 del segundo round de re-review): fases de `CohortPhase` en las
|
|
22
|
+
* que una cohorte de tracks todavía puede "ganarle la carrera" a su propio
|
|
23
|
+
* `run-final-interlock`, es decir donde el `computeGate` GENÉRICO de este
|
|
24
|
+
* mismo `tick()` (línea de abajo, calculado DESPUÉS de `runnerTick`) puede
|
|
25
|
+
* certificar pass antes de que `reconcileTracks` (que corrió ANTES de
|
|
26
|
+
* `runnerTick`, arriba en este mismo `tick()`) haya tenido chance de
|
|
27
|
+
* observar ese mismo resultado y mover `cohortPhase` a `COMPLETE`.
|
|
28
|
+
*
|
|
29
|
+
* Cadena causal EXACTA, trazada contra `protocol.ts`/`tracks.ts`/`apply.ts`:
|
|
30
|
+
* 1. `nextProtocolEffect` (protocol.ts) solo emite `request-final-integration`
|
|
31
|
+
* cuando `s.globalQaHeadSha !== undefined` — y ese campo se asigna
|
|
32
|
+
* (`reconcileProtocol`, observación `global-qa-pass`) EN LA MISMA
|
|
33
|
+
* transición que pone `cohortPhase = 'FINAL_INTEGRATION'`. No existe
|
|
34
|
+
* ningún camino donde `request-final-integration` se emita con
|
|
35
|
+
* `cohortPhase` todavía en `'ACTIVE'`/`'JOINING'`.
|
|
36
|
+
* 2. `runRequestFinalIntegration` (tracks.ts) es quien llama a `requestJob`
|
|
37
|
+
* con `verificationKind: 'track-integration'` — el ÚNICO lugar que
|
|
38
|
+
* pide el job canónico. Por (1), esto solo puede ocurrir con
|
|
39
|
+
* `cohortPhase === 'FINAL_INTEGRATION'`.
|
|
40
|
+
* 3. `apply.ts::applyRequestToState` (rama `job-request`) es quien enlaza
|
|
41
|
+
* `VerificationItem.satisfiedBy` — y lo hace a REQUEST time (job recién
|
|
42
|
+
* creado en `executionState: 'received'`, o job "equivalente" ya en
|
|
43
|
+
* vuelo), nunca esperando a que el job termine. Por (2), el primer
|
|
44
|
+
* tick en que esto puede pasar ya tiene `cohortPhase === 'FINAL_INTEGRATION'`.
|
|
45
|
+
* 4. Cada tick corre COMO MÁXIMO una mutación de protocolo por invocación
|
|
46
|
+
* de `reconcileTracks` (invariante propio de esa función, ver su
|
|
47
|
+
* comment de cabecera) y esa invocación sucede ANTES de `runnerTick`
|
|
48
|
+
* (que despacha/recolecta jobs) y ANTES del `computeGate` genérico de
|
|
49
|
+
* abajo. Entonces: en el tick donde el job canónico recién enlazado
|
|
50
|
+
* pasa a `verdict: 'pass'` (recolectado por `runnerTick`, que corre
|
|
51
|
+
* DESPUÉS de que `reconcileTracks` ya intentó — y no pudo, porque el
|
|
52
|
+
* job seguía vivo — avanzar el protocolo este mismo tick), el
|
|
53
|
+
* `computeGate` genérico de abajo puede certificar `pass` con
|
|
54
|
+
* `liveJobs === 0` mientras `cohortPhase` sigue en
|
|
55
|
+
* `'FINAL_INTEGRATION'` (el job pasó, pero `finalIntegrationJobId`
|
|
56
|
+
* todavía no se asignó — eso requiere la observación `integration-pass`,
|
|
57
|
+
* que recién corre el PRÓXIMO tick). Esta es la primera fase en riesgo.
|
|
58
|
+
* 5. Una vez que ese próximo tick observa `integration-pass` y mueve
|
|
59
|
+
* `cohortPhase` a `'FINAL_INTERLOCK'` (dentro de `reconcileTracks`,
|
|
60
|
+
* otra vez ANTES del `computeGate` genérico de ESE tick), el gate
|
|
61
|
+
* genérico sigue viendo el mismo job ya-pasado satisfaciendo el mismo
|
|
62
|
+
* item — sigue en riesgo de certificar en ESE tick, con `cohortPhase`
|
|
63
|
+
* todavía en `'FINAL_INTERLOCK'` (la transición real a `COMPLETE` la
|
|
64
|
+
* hace `runRunFinalInterlock`, que corre recién el tick SIGUIENTE,
|
|
65
|
+
* otra vez antes que el gate genérico de ese tick). Segunda y última
|
|
66
|
+
* fase en riesgo.
|
|
67
|
+
* 6. Cuando `runRunFinalInterlock` sí corre y su propio `computeGate`
|
|
68
|
+
* interno pasa, mueve `cohortPhase` a `'COMPLETE'` SINCRÓNICAMENTE
|
|
69
|
+
* dentro de `reconcileTracks`, antes de que el gate genérico de ese
|
|
70
|
+
* mismo tick se calcule — por eso `'COMPLETE'` nunca necesita estar en
|
|
71
|
+
* este set (ya lo cubre `cohortDone` por igualdad directa).
|
|
72
|
+
*
|
|
73
|
+
* Por (1)-(3): `'ACTIVE'`/`'JOINING'` NO califican. Más fuerte todavía:
|
|
74
|
+
* `registerTrackIntegrationItems` (apply.ts) agrega cada `track-integration:*`
|
|
75
|
+
* al `cycleVerificationPlan` SIN `satisfiedBy` en cuanto el track se declara
|
|
76
|
+
* (mucho antes de `ACTIVE`/`JOINING`) — mientras al menos uno siga sin
|
|
77
|
+
* `satisfiedBy`, `computeGate` (gate.ts) nunca puede dar `pass`, así que el
|
|
78
|
+
* gate genérico es estructuralmente incapaz de certificar durante
|
|
79
|
+
* `ACTIVE`/`JOINING`, con o sin este guard.
|
|
80
|
+
* `'FINAL_QA'` tampoco califica — y no es solo "no está en riesgo", es
|
|
81
|
+
* DEAD: se rastreó cada asignación a `cohortPhase` en `protocol.ts`
|
|
82
|
+
* (`initialCohort`, `reconcileProtocol`, `observeProtocolEffect`) y ese
|
|
83
|
+
* valor nunca se produce; queda en el tipo `CohortPhase` pero ninguna
|
|
84
|
+
* transición real lo alcanza.
|
|
85
|
+
* Quedan fuera por lo ya documentado en la versión anterior de este
|
|
86
|
+
* comentario (verificado de nuevo, sigue siendo cierto): `'SERIAL'` (sin
|
|
87
|
+
* camino de vuelta a COMPLETE, `nextProtocolEffect` devuelve `null`
|
|
88
|
+
* incondicional), `'BLOCKED'` (lo maneja `enterCustody` aparte),
|
|
89
|
+
* `'PREPARING'`/`'FALLBACK_PENDING'` (todavía no existe job canónico
|
|
90
|
+
* enlazable) y `'COMPLETE'` (es el destino, no una fase "todavía corriendo").
|
|
91
|
+
*
|
|
92
|
+
* Las ÚNICAS dos fases donde el job canónico ya puede estar enlazado
|
|
93
|
+
* (`satisfiedBy` set) sin que la cohorte misma haya llegado a `COMPLETE`
|
|
94
|
+
* son `'FINAL_INTEGRATION'` y `'FINAL_INTERLOCK'`. */
|
|
95
|
+
const LIVE_COHORT_PHASES = new Set([
|
|
96
|
+
'FINAL_INTEGRATION', 'FINAL_INTERLOCK',
|
|
97
|
+
]);
|
|
17
98
|
exports.DEFAULT_SUPERVISOR_CONFIG = {
|
|
18
99
|
provider: 'codex',
|
|
19
100
|
heartbeatTimeoutMs: 5 * 60000, // R4.2 default 5 min
|
|
@@ -23,6 +104,7 @@ exports.DEFAULT_SUPERVISOR_CONFIG = {
|
|
|
23
104
|
killGraceMs: 5000,
|
|
24
105
|
reconcileGraceMs: 10000,
|
|
25
106
|
jobStallObservationMs: 5 * 60000, // R3.5 default: mismo orden de magnitud que heartbeatTimeoutMs, concern independiente
|
|
107
|
+
maxParallelTracks: 1, // overridden en tiempo de ejecución con loadDefaultParallelism() (watch/index.ts)
|
|
26
108
|
};
|
|
27
109
|
const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
|
|
28
110
|
class Supervisor {
|
|
@@ -34,11 +116,13 @@ class Supervisor {
|
|
|
34
116
|
relaunchNotBefore = 0;
|
|
35
117
|
lastActivity = null;
|
|
36
118
|
lastGenerationToken = null;
|
|
37
|
-
|
|
119
|
+
trackRuntime;
|
|
120
|
+
constructor(repoRoot, branch, cfg, spawner, trackRuntime) {
|
|
38
121
|
this.repoRoot = repoRoot;
|
|
39
122
|
this.branch = branch;
|
|
40
123
|
this.cfg = cfg;
|
|
41
124
|
this.spawner = spawner;
|
|
125
|
+
this.trackRuntime = trackRuntime ?? (0, tracks_1.defaultTrackRuntime)(repoRoot, branch, { termGraceMs: cfg.termGraceMs, killGraceMs: cfg.killGraceMs });
|
|
42
126
|
}
|
|
43
127
|
fingerprintNow = (argv, paths, cwd) => {
|
|
44
128
|
try {
|
|
@@ -74,13 +158,29 @@ class Supervisor {
|
|
|
74
158
|
}
|
|
75
159
|
}
|
|
76
160
|
async tick() {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
161
|
+
const before0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
162
|
+
if (before0.corrupt || before0.state === null)
|
|
79
163
|
throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
|
|
80
|
-
|
|
81
|
-
|
|
164
|
+
// R6.2/R6.8/C7 (Task 11): reconciliar un `MERGE_HEAD` abierto por un
|
|
165
|
+
// crash a mitad de un merge ANTES de cualquier guard general — hoy
|
|
166
|
+
// ningún guard existente (`verifyBranchInvariant` incluido) rechaza
|
|
167
|
+
// por `MERGE_HEAD`, pero esto corre primero de todos modos para que
|
|
168
|
+
// uno agregado en el futuro nunca pueda rechazar un estado ya
|
|
169
|
+
// reconciliable. `openJoin.handled` evita una SEGUNDA mutación real
|
|
170
|
+
// de tracks más abajo en este mismo tick (a lo sumo una por tick).
|
|
171
|
+
const openJoin = await (0, tracks_1.reconcileOpenJoin)(this.repoRoot, this.branch, before0.state, this.trackRuntime, this.cfg.maxParallelTracks);
|
|
172
|
+
const before = openJoin.state;
|
|
173
|
+
(0, lock_1.verifyBranchInvariant)(this.repoRoot, before.branch);
|
|
174
|
+
if (before.cycle.status === 'COMPLETE')
|
|
82
175
|
return 'complete';
|
|
83
|
-
|
|
176
|
+
// R5.2/R6.3 (Task 10): restart-safe — un track ya `frozen` (crash del
|
|
177
|
+
// loop DESPUÉS de persistir el paso 6 pero ANTES de que
|
|
178
|
+
// `runSupervisorLoop` liberara el lock/saliera) jamás debe relanzar
|
|
179
|
+
// un controller nuevo ni volver a despachar; simplemente reafirma el
|
|
180
|
+
// mismo resultado terminal.
|
|
181
|
+
if (before.frozen !== undefined)
|
|
182
|
+
return 'frozen';
|
|
183
|
+
const pending = before.cycle.nextAction;
|
|
84
184
|
const resumePrompt = pending !== undefined ? `el next_action ${pending.actionId} del journal` : 'el plan del ciclo desde el journal';
|
|
85
185
|
if (this.ensureController(resumePrompt) === 'custody')
|
|
86
186
|
return 'custody';
|
|
@@ -89,37 +189,99 @@ class Supervisor {
|
|
|
89
189
|
throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
|
|
90
190
|
const gen = (0, generations_1.activeGeneration)(r0.state);
|
|
91
191
|
(0, apply_1.consumePendingRequests)(this.repoRoot, this.branch, gen?.token ?? null);
|
|
192
|
+
// P1/P2 (R4.1-R4.10): a lo sumo un side effect de bootstrap de tracks
|
|
193
|
+
// por tick, ANTES de tocar jobs — mientras la cohorte está PREPARING,
|
|
194
|
+
// ningún job de track se despacha (eso lo maneja `runnerTick` con los
|
|
195
|
+
// `Job` ya existentes; el bootstrap de tracks es un canal separado).
|
|
196
|
+
// Task 11: si `reconcileOpenJoin` ya ejecutó una mutación real este
|
|
197
|
+
// mismo tick (reconcilió un `MERGE_HEAD` abierto), no se vuelve a
|
|
198
|
+
// invocar `reconcileTracks` acá — a lo sumo una mutación real de
|
|
199
|
+
// tracks por tick, mismo invariante que ya sostenía `reconcileTracks`
|
|
200
|
+
// por sí solo antes de que existiera esta ruta temprana.
|
|
201
|
+
if (!openJoin.handled) {
|
|
202
|
+
const preTracks = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
203
|
+
if (!preTracks.corrupt && preTracks.state !== null) {
|
|
204
|
+
// Task 9: `reconcileTracks` es `async` desde que `begin-teardown`
|
|
205
|
+
// puede terminar el grupo del supervisor de un track con
|
|
206
|
+
// `terminatePreviouslyOwnedGroup` (espera real de gracia,
|
|
207
|
+
// R4.8) — awaitear acá es obligatorio, nunca fire-and-forget.
|
|
208
|
+
await (0, tracks_1.reconcileTracks)(this.repoRoot, this.branch, preTracks.state, this.trackRuntime, this.cfg.maxParallelTracks);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
92
211
|
const afterRequests = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
93
|
-
|
|
212
|
+
// R7/C3 (Task 12): mientras la cohorte corre el job canónico de
|
|
213
|
+
// integración final o espera el interlock, NADIE debe relanzar el
|
|
214
|
+
// controller — `runRequestFinalIntegration`/`runRunFinalInterlock`
|
|
215
|
+
// (watch/tracks.ts) ya pausaron esa generación explícitamente
|
|
216
|
+
// (R7.3/C3) precisamente para que el árbol quede quieto durante esa
|
|
217
|
+
// ventana; sin este guard, esta misma rama la revivía en el mismo
|
|
218
|
+
// tick (o el siguiente) apenas la generación quedaba `terminated`,
|
|
219
|
+
// exactamente la mutación concurrente que la pausa buscaba evitar.
|
|
220
|
+
const finalizing = afterRequests.state?.cohortPhase === 'FINAL_INTEGRATION' || afterRequests.state?.cohortPhase === 'FINAL_INTERLOCK';
|
|
221
|
+
if (!finalizing && afterRequests.state !== null && (0, generations_1.activeGeneration)(afterRequests.state) === undefined
|
|
94
222
|
&& afterRequests.state.generations.length > 0 && afterRequests.state.cycle.status === 'IN_PROGRESS') {
|
|
95
223
|
(0, generations_1.beginGeneration)(this.repoRoot, this.branch);
|
|
96
224
|
if (this.ensureController(resumePrompt) === 'custody')
|
|
97
225
|
return 'custody';
|
|
98
226
|
}
|
|
99
|
-
|
|
227
|
+
// R5.2/R6.3 (Task 10): recién leído tras `consumePendingRequests` —
|
|
228
|
+
// si el request `track-freeze-request` llegó en ESTE tick, `apply.ts`
|
|
229
|
+
// ya lo tradujo a `freezeRequested`. `dispatch:false` corta SOLO el
|
|
230
|
+
// arranque de trabajo NUEVO; el drenaje de lo ya vivo sigue intacto
|
|
231
|
+
// (paso 2 del freeze — "consume/reconcilia jobs existentes").
|
|
232
|
+
const freezeCheck = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
233
|
+
const freezing = !freezeCheck.corrupt && freezeCheck.state !== null
|
|
234
|
+
&& freezeCheck.state.freezeRequested === true && freezeCheck.state.frozen === undefined;
|
|
235
|
+
(0, runner_1.runnerTick)(this.repoRoot, this.branch, this.spawner, {
|
|
236
|
+
reconcileGraceMs: this.cfg.reconcileGraceMs, stallObservationMs: this.cfg.jobStallObservationMs, dispatch: !freezing,
|
|
237
|
+
});
|
|
100
238
|
const custody = await this.superviseController();
|
|
101
239
|
if (custody)
|
|
102
240
|
return 'custody';
|
|
241
|
+
if (freezing)
|
|
242
|
+
return this.attemptFreeze();
|
|
103
243
|
const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
104
244
|
const gate = (0, gate_1.computeGate)(r.state, r.corrupt, this.fingerprintNow);
|
|
105
245
|
const liveJobs = r.state === null ? 1 : Object.values(r.state.jobs).filter((j) => LIVE.includes(j.executionState)).length;
|
|
106
|
-
|
|
246
|
+
// R7/C3/C4 (Task 12): un journal de PLAN con cohorte de tracks NUNCA
|
|
247
|
+
// declara `cycle.status = COMPLETE` por este camino genérico mientras
|
|
248
|
+
// la cohorte no llegó ELLA MISMA a `cohortPhase === 'COMPLETE'` — sin
|
|
249
|
+
// este guard, `computeGate` certifica en cuanto el job canónico de
|
|
250
|
+
// integración se REQUIERE (apply.ts enlaza `satisfiedBy` al crear el
|
|
251
|
+
// job, antes de que termine) y pasa, ganándole la carrera al propio
|
|
252
|
+
// `run-final-interlock` (watch/tracks.ts): el ciclo se declararía
|
|
253
|
+
// COMPLETE mientras `cohortPhase` sigue en FINAL_INTEGRATION/
|
|
254
|
+
// FINAL_INTERLOCK, los tracks nunca llegan a JOINED y
|
|
255
|
+
// `integration.lock` queda retenido para siempre (este mismo check,
|
|
256
|
+
// arriba en `tick()`, corta el loop apenas ve `cycle.status ===
|
|
257
|
+
// COMPLETE` y jamás vuelve a llamar `reconcileTracks`).
|
|
258
|
+
//
|
|
259
|
+
// Fix post-review: la primera versión de este guard usaba
|
|
260
|
+
// `(tracks?.length ?? 0) >= 2` como único criterio de "gobernada" —
|
|
261
|
+
// pero el array `tracks` NUNCA se vacía ni se acorta cuando la
|
|
262
|
+
// cohorte cae a fallback SERIAL (Task 9: los tracks quedan
|
|
263
|
+
// `REMOVED`/`DECLARED` en el array, ver `track-bootstrap-crash.test.ts`),
|
|
264
|
+
// y `protocol.ts` no tiene (ni debe inventarse acá — single-authority)
|
|
265
|
+
// ningún camino de SERIAL de vuelta a COMPLETE. Con el criterio viejo,
|
|
266
|
+
// CUALQUIER plan cuya cohorte degradara a SERIAL quedaba
|
|
267
|
+
// PERMANENTEMENTE incapaz de completar su ciclo por este único lugar
|
|
268
|
+
// del código que fija `cycle.status = 'COMPLETE'`. El criterio
|
|
269
|
+
// correcto no es "¿existe un array de tracks?" sino "¿está la
|
|
270
|
+
// cohorte, AHORA MISMO, en una fase viva del ciclo de vida paralelo
|
|
271
|
+
// que todavía podría adelantarse a su propio interlock?"
|
|
272
|
+
// (`LIVE_COHORT_PHASES`, arriba). Una cohorte en SERIAL, BLOCKED,
|
|
273
|
+
// PREPARING o FALLBACK_PENDING — o un journal sin cohorte real — sigue
|
|
274
|
+
// el camino de siempre, sin cambios, igual que si nunca hubiera
|
|
275
|
+
// tenido tracks.
|
|
276
|
+
const cohortGoverned = r.state !== null && (r.state.tracks?.length ?? 0) >= 2
|
|
277
|
+
&& r.state.cohortPhase !== undefined && LIVE_COHORT_PHASES.has(r.state.cohortPhase);
|
|
278
|
+
const cohortDone = !cohortGoverned || r.state.cohortPhase === 'COMPLETE';
|
|
279
|
+
if (gate.pass && liveJobs === 0 && cohortDone) { // gate verde YA implica cero vivos; doble cinturon (R4.5)
|
|
107
280
|
const s = r.state;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (ref?.pid === process.pid)
|
|
113
|
-
continue;
|
|
114
|
-
if (ref === undefined || (0, process_1.groupIsGone)(ref.processGroup))
|
|
115
|
-
continue;
|
|
116
|
-
const confirmed = await (0, process_1.terminateGroupConfirmed)(ref, { termGraceMs: this.cfg.termGraceMs, killGraceMs: this.cfg.killGraceMs });
|
|
117
|
-
if (!confirmed) {
|
|
118
|
-
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `no se pudo terminar con identidad confirmada la generacion ${generation.n} antes de COMPLETE`);
|
|
119
|
-
return 'custody';
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
generation.state = 'terminated';
|
|
281
|
+
const terminated = await this.terminateAllGenerationsConfirmed(s);
|
|
282
|
+
if (!terminated.ok) {
|
|
283
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `no se pudo terminar con identidad confirmada la generacion ${terminated.n} antes de COMPLETE`);
|
|
284
|
+
return 'custody';
|
|
123
285
|
}
|
|
124
286
|
s.cycle.status = 'COMPLETE';
|
|
125
287
|
s.cycle.completedAt = new Date().toISOString();
|
|
@@ -129,6 +291,65 @@ class Supervisor {
|
|
|
129
291
|
}
|
|
130
292
|
return 'continue';
|
|
131
293
|
}
|
|
294
|
+
/** Termina, con identidad CONFIRMADA (jamás un `kill(pid)` crudo), toda
|
|
295
|
+
* generación de `s` que todavía tenga un `processRef`/`wrapperRef` vivo
|
|
296
|
+
* — muta `s` en memoria (incluye marcar `state = 'terminated'`) pero
|
|
297
|
+
* JAMÁS persiste por sí sola: el caller combina esta mutación con la
|
|
298
|
+
* suya propia (`cycle.status = 'COMPLETE'` o `frozen = {...}`) en UN
|
|
299
|
+
* solo `writeJournal` (R1.3 — dos escrituras secuenciales sobre el
|
|
300
|
+
* mismo objeto violarían el CAS por revisión de `writeJournal`).
|
|
301
|
+
* Compartida entre el camino COMPLETE y el camino FROZEN (Task 10): el
|
|
302
|
+
* mismo requisito ("ningún controller administrado sigue vivo") aplica
|
|
303
|
+
* a ambos. */
|
|
304
|
+
async terminateAllGenerationsConfirmed(s) {
|
|
305
|
+
for (const generation of s.generations) {
|
|
306
|
+
for (const ref of [generation.processRef, generation.wrapperRef]) {
|
|
307
|
+
// Los tests pueden ejecutar el wrapper in-process; nunca
|
|
308
|
+
// enviar una senial al propio supervisor.
|
|
309
|
+
if (ref?.pid === process.pid)
|
|
310
|
+
continue;
|
|
311
|
+
if (ref === undefined || (0, process_1.groupIsGone)(ref.processGroup))
|
|
312
|
+
continue;
|
|
313
|
+
const confirmed = await (0, process_1.terminateGroupConfirmed)(ref, { termGraceMs: this.cfg.termGraceMs, killGraceMs: this.cfg.killGraceMs });
|
|
314
|
+
if (!confirmed)
|
|
315
|
+
return { ok: false, n: generation.n };
|
|
316
|
+
}
|
|
317
|
+
generation.state = 'terminated';
|
|
318
|
+
}
|
|
319
|
+
return { ok: true };
|
|
320
|
+
}
|
|
321
|
+
/** Paso 2-6 del freeze (R5.2/R6.3, Task 10 — paso 1 "deja de despachar"
|
|
322
|
+
* ya lo hizo `dispatch:false` en `runnerTick`, arriba en `tick()`):
|
|
323
|
+
* fail-closed en cada paso — cualquier hecho todavía no demostrable
|
|
324
|
+
* simplemente pospone (`'continue'`, el próximo tick reintenta), JAMÁS
|
|
325
|
+
* fuerza `frozen` sobre evidencia incompleta. Solo cuando los 4 hechos
|
|
326
|
+
* restantes (cero vivos, gate local verde, worktree/index limpios,
|
|
327
|
+
* generación propia terminada CONFIRMADA) son TODOS demostrables se
|
|
328
|
+
* persiste `frozenHeadSha` + el marcador `frozen` — en el MISMO
|
|
329
|
+
* `writeJournal` que la terminación de generación de arriba. */
|
|
330
|
+
async attemptFreeze() {
|
|
331
|
+
const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
332
|
+
if (r.corrupt || r.state === null)
|
|
333
|
+
throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
|
|
334
|
+
const s = r.state;
|
|
335
|
+
const liveJobs = Object.values(s.jobs).filter((j) => LIVE.includes(j.executionState)).length;
|
|
336
|
+
if (liveJobs > 0)
|
|
337
|
+
return 'continue'; // paso 2: drenando todavía
|
|
338
|
+
const gate = (0, gate_1.computeTrackGate)(s, false, this.fingerprintNow);
|
|
339
|
+
if (!gate.pass)
|
|
340
|
+
return 'continue'; // paso 3: gate local todavía rojo
|
|
341
|
+
if (!(0, git_1.isWorktreeClean)(this.repoRoot))
|
|
342
|
+
return 'continue'; // paso 4: worktree/index todavía sucios
|
|
343
|
+
const terminated = await this.terminateAllGenerationsConfirmed(s); // paso 5
|
|
344
|
+
if (!terminated.ok) {
|
|
345
|
+
(0, generations_1.enterCustody)(this.repoRoot, this.branch, `no se pudo terminar con identidad confirmada la generacion ${terminated.n} antes de FROZEN (R5.2/R6.3)`);
|
|
346
|
+
return 'custody';
|
|
347
|
+
}
|
|
348
|
+
s.frozen = { headSha: (0, git_1.headSha)(this.repoRoot), at: new Date().toISOString() }; // paso 4/6: SHA congelado, durable
|
|
349
|
+
(0, store_1.writeJournal)(this.repoRoot, this.branch, s);
|
|
350
|
+
(0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'track-frozen', headSha: s.frozen.headSha });
|
|
351
|
+
return 'frozen'; // paso 6 ("libera el lock y sale"): runSupervisorLoop trata 'frozen' igual que 'complete'
|
|
352
|
+
}
|
|
132
353
|
/** true => custodia (el caller NO libera lock ni sale). */
|
|
133
354
|
async superviseController() {
|
|
134
355
|
const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
|
|
@@ -194,7 +415,7 @@ exports.Supervisor = Supervisor;
|
|
|
194
415
|
/** Foreground, visible, terminable (R2.4): sin daemons. SIGINT/SIGTERM libera
|
|
195
416
|
* el lock y sale; COMPLETE => auto-exit liberando lock y terminando la
|
|
196
417
|
* generacion propia (cero huerfanos). */
|
|
197
|
-
async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)()) {
|
|
418
|
+
async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)(), trackRuntime) {
|
|
198
419
|
const r = (0, store_1.readJournal)(repoRoot, branch);
|
|
199
420
|
if (r.corrupt || r.state === null)
|
|
200
421
|
throw new Error('journal ausente o corrupto: corre `awm watch --init` primero');
|
|
@@ -208,7 +429,7 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
208
429
|
const onSignal = () => { shutdownRequested = true; wakeSleep?.(); };
|
|
209
430
|
process.on('SIGINT', onSignal);
|
|
210
431
|
process.on('SIGTERM', onSignal);
|
|
211
|
-
const sup = new Supervisor(repoRoot, branch, cfg, spawner);
|
|
432
|
+
const sup = new Supervisor(repoRoot, branch, cfg, spawner, trackRuntime);
|
|
212
433
|
try {
|
|
213
434
|
const s0 = (0, store_1.readJournal)(repoRoot, branch).state;
|
|
214
435
|
if ((0, generations_1.activeGeneration)(s0) === undefined) {
|
|
@@ -218,7 +439,10 @@ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.d
|
|
|
218
439
|
if (shutdownRequested)
|
|
219
440
|
break;
|
|
220
441
|
const out = await sup.tick();
|
|
221
|
-
|
|
442
|
+
// 'frozen' (Task 10): mismo camino de salida que 'complete' —
|
|
443
|
+
// drenar ownership y liberar el lock, el track ya cumplió su
|
|
444
|
+
// freeze y no debe seguir despachando ni corriendo su loop.
|
|
445
|
+
if (out === 'complete' || out === 'frozen')
|
|
222
446
|
break;
|
|
223
447
|
// 'custody': NO liberar lock, NO salir — seguir auditando (R4.5)
|
|
224
448
|
await new Promise((resolve) => {
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runBeginTeardown = runBeginTeardown;
|
|
7
|
+
// Task 13 (R4.2/R4.3/R4.6/R4.10/C2/C9): extraído de `tracks.ts` (post-review
|
|
8
|
+
// de Task 13 — el archivo venía creciendo dos ciclos consecutivos de review
|
|
9
|
+
// hacia "actively harming reviewability") — mismo criterio que `join.ts` con
|
|
10
|
+
// `decideJoinReconciliation`/`acquireIntegrationLock`: la lógica de
|
|
11
|
+
// gathering + driver de UN concern vive en su propio módulo, `tracks.ts`
|
|
12
|
+
// sigue siendo el ÚNICO lugar con la orquestación (`reconcileTracks`,
|
|
13
|
+
// `executeRuntimeEffect`'s dispatch table) y el wiring de producción
|
|
14
|
+
// (`defaultTrackRuntime`). Esto es una extracción puramente MECÁNICA: cero
|
|
15
|
+
// cambio de comportamiento, `decideTeardown` sigue viviendo exclusivamente en
|
|
16
|
+
// `protocol.ts` (autoridad única, sin tocar).
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const paths_1 = require("../../core/journal/paths");
|
|
19
|
+
const process_1 = require("../../core/journal/process");
|
|
20
|
+
const protocol_1 = require("../../core/tracks/protocol");
|
|
21
|
+
const git_1 = require("../../core/tracks/git");
|
|
22
|
+
const teardown_1 = require("../../core/tracks/teardown");
|
|
23
|
+
const store_1 = require("../../core/journal/store");
|
|
24
|
+
const tracks_1 = require("./tracks");
|
|
25
|
+
/** Task 13 (R4.2/R4.3/R4.6/R4.10/C2/C9): gathering READ-ONLY del estado real
|
|
26
|
+
* de UN track en teardown — jamás cuenta como el side effect del tick
|
|
27
|
+
* (mismo criterio que `ownedWorktreeExists`/`foreignPathExists` en
|
|
28
|
+
* `runCreateWorktree`). Solo reúne los campos que la fase ACTUAL necesita
|
|
29
|
+
* (`decideTeardown` ignora el resto): `ownSupervisorAlive` en
|
|
30
|
+
* `TEARDOWN_INTENT`, `ownedWorktreeExists`/`foreignWorktree` en
|
|
31
|
+
* `SUPERVISOR_STOPPED`, `ownedBranchExists` en `WORKTREE_REMOVED`.
|
|
32
|
+
* `foreignSupervisor` deliberadamente nunca se produce acá: `refIsAlive`
|
|
33
|
+
* compara identidad completa (pid+startTime+pgid+argsDigest), así que un
|
|
34
|
+
* PID reciclado por otro proceso ya se reporta como "nuestro está muerto"
|
|
35
|
+
* (`ownSupervisorAlive: false`), nunca como "ajeno" — el campo existe en
|
|
36
|
+
* `TeardownObservation`/`decideTeardown` para la matriz pura y como defensa
|
|
37
|
+
* fail-closed, mismo criterio que `begin-fallback` en `decidePrepare`
|
|
38
|
+
* (nunca seleccionado por el driver real, pero cubierto por la autoridad
|
|
39
|
+
* única igual). */
|
|
40
|
+
function gatherTeardownObservation(planRoot, s, ref) {
|
|
41
|
+
if (ref.phase === 'TEARDOWN_INTENT') {
|
|
42
|
+
// Step 4 del plan: "supervisor propio muerto confirmado Y lock
|
|
43
|
+
// ausente" — un proceso identity-verified muerto que dejó su lock
|
|
44
|
+
// advisory sin liberar (crash a mitad de su propia salida, mismo
|
|
45
|
+
// riesgo que `runFreezeTrack` ya trata con `supervisorAlive ||
|
|
46
|
+
// lockExists`) todavía no prueba que sea seguro tocar el worktree.
|
|
47
|
+
// `ownSupervisorAlive: true` acá simplemente reintenta
|
|
48
|
+
// `stop-own-supervisor` en el próximo tick — inofensivo si el
|
|
49
|
+
// proceso ya está muerto (`groupIsGone` ya es `true`, no-op real),
|
|
50
|
+
// fail-closed si el lock sigue de verdad retenido.
|
|
51
|
+
const processAlive = ref.supervisorProcessRef !== undefined && (0, process_1.refIsAlive)(ref.supervisorProcessRef);
|
|
52
|
+
// `supervisorLockPath` exige un directorio real (`fs.realpathSync`)
|
|
53
|
+
// — un track abandonado ANTES de que su worktree llegara a existir
|
|
54
|
+
// (ej. `create-worktree` falló para el track siguiente, R4.5) nunca
|
|
55
|
+
// tuvo dónde escribir un lock: ausencia de directorio = ausencia de
|
|
56
|
+
// lock, nunca "indemostrable".
|
|
57
|
+
const lockExists = fs_1.default.existsSync(ref.worktreePath) && fs_1.default.existsSync((0, paths_1.supervisorLockPath)(ref.worktreePath));
|
|
58
|
+
return { ownSupervisorAlive: processAlive || lockExists };
|
|
59
|
+
}
|
|
60
|
+
if (ref.phase === 'SUPERVISOR_STOPPED') {
|
|
61
|
+
if (!fs_1.default.existsSync(ref.worktreePath))
|
|
62
|
+
return { ownedWorktreeExists: false };
|
|
63
|
+
if ((0, teardown_1.worktreeOwnershipProven)(planRoot, ref, s.journalId))
|
|
64
|
+
return { ownedWorktreeExists: true };
|
|
65
|
+
// Existe, pero no se pudo probar que es nuestro (R4.6/R4.10): jamás
|
|
66
|
+
// se adopta ni se borra a ciegas — bloquea en vez de saltear.
|
|
67
|
+
return { foreignWorktree: true };
|
|
68
|
+
}
|
|
69
|
+
if (ref.phase === 'WORKTREE_REMOVED') {
|
|
70
|
+
return { ownedBranchExists: (0, git_1.branchExists)(planRoot, ref.branch) };
|
|
71
|
+
}
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
/** Task 13 (R4.2/R4.3/R4.6/R4.10/C2/C9): reemplaza el `teardownOwned(ref,
|
|
75
|
+
* step)` TEMPORAL de Task 9 por el state machine completo de
|
|
76
|
+
* `decideTeardown` (`protocol.ts`, autoridad única) — a lo sumo UN efecto
|
|
77
|
+
* real por invocación, siempre persistido a través de `observeProtocolEffect`
|
|
78
|
+
* (nunca un `withRef` directo con una fase inventada acá, salvo para el
|
|
79
|
+
* snapshot no-decisional de `teardownIntent`), mismo criterio "gather ->
|
|
80
|
+
* decide -> tocar el mundo real como mucho una vez -> persistir -> stop"
|
|
81
|
+
* que `runCreateWorktree`/`runFreezeTrack`/`runMergeTrack`.
|
|
82
|
+
*
|
|
83
|
+
* Cada llamada a `decideTeardown` ocurre sobre la observación YA reunida
|
|
84
|
+
* (`observed`, antes de cualquier efecto): las decisiones "ya resuelto"
|
|
85
|
+
* (`accept-supervisor-stopped`, `mark-removed`, `block-foreign`) no
|
|
86
|
+
* necesitan tocar `runtime` — la observación existente alcanza para
|
|
87
|
+
* persistir el avance. Las decisiones "falta trabajo real"
|
|
88
|
+
* (`stop-own-supervisor`, `remove-owned-worktree`, `remove-owned-branch`)
|
|
89
|
+
* ejecutan EXACTAMENTE un efecto y vuelven a observar el mundo real después,
|
|
90
|
+
* persistiendo esa observación fresca — es `reconcileProtocol` quien, sobre
|
|
91
|
+
* ESA observación, vuelve a llamar `decideTeardown` para fijar la fase
|
|
92
|
+
* resultante (mismo patrón exacto que `runMergeTrack` con
|
|
93
|
+
* `decideJoinReconciliation`, T11: nunca se confía en que el efecto en sí
|
|
94
|
+
* haya funcionado, la relectura es la única fuente de verdad). */
|
|
95
|
+
async function runBeginTeardown(planRoot, branch, s, protocol, effect, runtime) {
|
|
96
|
+
const trackId = effect.trackId;
|
|
97
|
+
const ref = (0, tracks_1.refOf)(s, trackId);
|
|
98
|
+
const observed = gatherTeardownObservation(planRoot, s, ref);
|
|
99
|
+
const decision = (0, protocol_1.decideTeardown)(protocol.tracks[trackId], observed);
|
|
100
|
+
if (decision === 'persist-intent') {
|
|
101
|
+
// Único paso que además de la fase (`EFFECT_APPLIED_PHASE` ya mapea
|
|
102
|
+
// `begin-teardown` -> `TEARDOWN_INTENT`) captura el snapshot durable
|
|
103
|
+
// que Step 4 exige para probar ownership del worktree más adelante —
|
|
104
|
+
// dato del journal, no del protocolo puro, así que se aplica con
|
|
105
|
+
// `withRef` en la MISMA persistencia (una sola frontera, no dos).
|
|
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, {
|
|
108
|
+
teardownIntent: { worktreePath: ref.worktreePath, branch: ref.branch, supervisorNonce: ref.supervisorProcessRef?.spawnNonce },
|
|
109
|
+
});
|
|
110
|
+
const next = (0, tracks_1.persist)(planRoot, branch, withIntent);
|
|
111
|
+
(0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: 'TEARDOWN_INTENT' });
|
|
112
|
+
return { state: next, stop: true, executed: effect.kind };
|
|
113
|
+
}
|
|
114
|
+
if (decision === 'block-foreign') {
|
|
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));
|
|
117
|
+
(0, store_1.appendEvent)(planRoot, branch, {
|
|
118
|
+
kind: 'track-blocked', trackId,
|
|
119
|
+
reason: observed.foreignSupervisor ? 'identidad de supervisor ajena' : 'worktree preexistente ajeno',
|
|
120
|
+
});
|
|
121
|
+
return { state: next, stop: true, executed: null };
|
|
122
|
+
}
|
|
123
|
+
if (decision === 'accept-supervisor-stopped' || decision === 'mark-removed') {
|
|
124
|
+
// Ya resuelto por la observación reunida — ningún touch nuevo de
|
|
125
|
+
// `runtime` este tick (mismo criterio que 'accept-worktree' en
|
|
126
|
+
// `runCreateWorktree`).
|
|
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 });
|
|
130
|
+
return { state: next, stop: true, executed: effect.kind };
|
|
131
|
+
}
|
|
132
|
+
if (decision === 'stop-own-supervisor') {
|
|
133
|
+
// R4.8: identity-verified — jamás un `kill(pid)` crudo. `true` <=>
|
|
134
|
+
// grupo confirmado ausente (recién ahora, o ya lo estaba).
|
|
135
|
+
const confirmed = await runtime.stopOwnSupervisor(ref);
|
|
136
|
+
const after = { ownSupervisorAlive: !confirmed };
|
|
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));
|
|
139
|
+
(0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: confirmed ? 'SUPERVISOR_STOPPED' : 'TEARDOWN_INTENT' });
|
|
140
|
+
return { state: next, stop: true, executed: effect.kind };
|
|
141
|
+
}
|
|
142
|
+
if (decision === 'remove-owned-worktree') {
|
|
143
|
+
// Ownership YA probado por `gatherTeardownObservation`
|
|
144
|
+
// (`worktreeOwnershipProven`) para que `decideTeardown` llegara acá —
|
|
145
|
+
// lo único que falta es el efecto real (`git.ts` aplica el guard de
|
|
146
|
+
// limpieza, bloqueando si está sucio, y nunca usa `--force`).
|
|
147
|
+
try {
|
|
148
|
+
runtime.removeOwnedWorktree(planRoot, ref);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
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));
|
|
153
|
+
(0, store_1.appendEvent)(planRoot, branch, { kind: 'track-effect-failed', trackId, effect: effect.kind, detail: error.message });
|
|
154
|
+
return { state: next, stop: true, executed: effect.kind };
|
|
155
|
+
}
|
|
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));
|
|
158
|
+
(0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: 'WORKTREE_REMOVED' });
|
|
159
|
+
return { state: next, stop: true, executed: effect.kind };
|
|
160
|
+
}
|
|
161
|
+
// decision === 'remove-owned-branch': dos fases de origen posibles
|
|
162
|
+
// (mismo valor de decisión, mismo criterio de dualidad que el resto de
|
|
163
|
+
// esta interfaz — ver el comentario de `decideTeardown`).
|
|
164
|
+
if (ref.phase === 'SUPERVISOR_STOPPED') {
|
|
165
|
+
// Sin worktree propio que remover (nunca existió, o ya se quitó en
|
|
166
|
+
// un intento previo que crasheó antes de persistir esto): avance
|
|
167
|
+
// puramente mecánico a `WORKTREE_REMOVED`, sin tocar `runtime` — el
|
|
168
|
+
// próximo tick, ya en esa fase, decide la branch de nuevo con una
|
|
169
|
+
// observación fresca.
|
|
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));
|
|
172
|
+
(0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: 'WORKTREE_REMOVED' });
|
|
173
|
+
return { state: next, stop: true, executed: effect.kind };
|
|
174
|
+
}
|
|
175
|
+
// ref.phase === 'WORKTREE_REMOVED': la branch existe de verdad, efecto real.
|
|
176
|
+
try {
|
|
177
|
+
runtime.removeOwnedBranch(planRoot, ref.branch);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
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));
|
|
182
|
+
(0, store_1.appendEvent)(planRoot, branch, { kind: 'track-effect-failed', trackId, effect: effect.kind, detail: error.message });
|
|
183
|
+
return { state: next, stop: true, executed: effect.kind };
|
|
184
|
+
}
|
|
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));
|
|
187
|
+
(0, store_1.appendEvent)(planRoot, branch, { kind: 'track-teardown-step', trackId, step: 'BRANCH_REMOVED' });
|
|
188
|
+
return { state: next, stop: true, executed: effect.kind };
|
|
189
|
+
}
|