agentic-workflow-manager 6.2.1 → 6.3.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 (69) hide show
  1. package/README.md +17 -0
  2. package/dist/src/commands/job/gate.js +37 -13
  3. package/dist/src/commands/job/index.js +48 -8
  4. package/dist/src/commands/job/request.js +44 -6
  5. package/dist/src/commands/track/emit.js +26 -0
  6. package/dist/src/commands/track/index.js +194 -0
  7. package/dist/src/commands/track/status.js +63 -0
  8. package/dist/src/commands/track/supervisor-wrapper.js +89 -0
  9. package/dist/src/commands/watch/apply.js +201 -13
  10. package/dist/src/commands/watch/index.js +14 -0
  11. package/dist/src/commands/watch/runner.js +20 -1
  12. package/dist/src/commands/watch/supervisor.js +251 -27
  13. package/dist/src/commands/watch/teardown-driver.js +189 -0
  14. package/dist/src/commands/watch/tracks.js +1100 -0
  15. package/dist/src/core/journal/adapter.js +46 -6
  16. package/dist/src/core/journal/paths.js +9 -0
  17. package/dist/src/core/journal/process.js +101 -1
  18. package/dist/src/core/journal/requests.js +5 -1
  19. package/dist/src/core/journal/store.js +34 -2
  20. package/dist/src/core/journal/types.js +93 -4
  21. package/dist/src/core/paths.js +51 -0
  22. package/dist/src/core/tracks/concurrency.js +85 -0
  23. package/dist/src/core/tracks/context.js +89 -0
  24. package/dist/src/core/tracks/descriptor.js +40 -0
  25. package/dist/src/core/tracks/git.js +318 -0
  26. package/dist/src/core/tracks/join.js +185 -0
  27. package/dist/src/core/tracks/ownership.js +100 -0
  28. package/dist/src/core/tracks/plan-parser.js +108 -0
  29. package/dist/src/core/tracks/protocol.js +466 -0
  30. package/dist/src/core/tracks/teardown.js +34 -0
  31. package/dist/src/core/tracks/types.js +14 -0
  32. package/dist/src/index.js +2 -0
  33. package/dist/tests/commands/job/gate-reconcile.test.js +267 -0
  34. package/dist/tests/commands/track/fixtures.js +13 -0
  35. package/dist/tests/commands/track/status.test.js +157 -0
  36. package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +129 -0
  37. package/dist/tests/commands/track/supervisor-wrapper.test.js +131 -0
  38. package/dist/tests/commands/track/verbs.test.js +326 -0
  39. package/dist/tests/commands/watch/apply.test.js +116 -4
  40. package/dist/tests/commands/watch/runner.test.js +22 -0
  41. package/dist/tests/commands/watch/supervisor-loop.test.js +150 -0
  42. package/dist/tests/commands/watch/track-bootstrap-crash.test.js +342 -0
  43. package/dist/tests/commands/watch/track-bootstrap.test.js +350 -0
  44. package/dist/tests/commands/watch/track-finalize.test.js +643 -0
  45. package/dist/tests/commands/watch/track-freeze.test.js +591 -0
  46. package/dist/tests/commands/watch/track-join-crash.test.js +439 -0
  47. package/dist/tests/commands/watch/track-runtime-git.test.js +119 -0
  48. package/dist/tests/commands/watch/track-teardown-crash.test.js +426 -0
  49. package/dist/tests/core/journal/adapter-override.test.js +51 -0
  50. package/dist/tests/core/journal/process.test.js +37 -0
  51. package/dist/tests/core/journal/requests.test.js +21 -0
  52. package/dist/tests/core/journal/store.test.js +28 -0
  53. package/dist/tests/core/journal/types.test.js +89 -0
  54. package/dist/tests/core/same-existing-path.test.js +48 -0
  55. package/dist/tests/core/tracks/concurrency.test.js +134 -0
  56. package/dist/tests/core/tracks/context.test.js +177 -0
  57. package/dist/tests/core/tracks/descriptor.test.js +76 -0
  58. package/dist/tests/core/tracks/git.test.js +132 -0
  59. package/dist/tests/core/tracks/join-reconcile.test.js +53 -0
  60. package/dist/tests/core/tracks/join.test.js +197 -0
  61. package/dist/tests/core/tracks/ownership.test.js +96 -0
  62. package/dist/tests/core/tracks/plan-parser.test.js +94 -0
  63. package/dist/tests/core/tracks/protocol.test.js +416 -0
  64. package/dist/tests/core/tracks/teardown.test.js +62 -0
  65. package/dist/tests/helpers/git-fixture.js +35 -0
  66. package/dist/tests/integration/parallel-tracks.e2e.test.js +343 -0
  67. package/dist/tests/integration/r5-provider-evidence.test.js +67 -0
  68. package/dist/tests/structural/path-identity-not-string-compare.test.js +51 -0
  69. package/package.json +1 -1
@@ -0,0 +1,466 @@
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.decideJoinReconciliation = decideJoinReconciliation;
7
+ exports.decidePrepare = decidePrepare;
8
+ exports.decideTeardown = decideTeardown;
9
+ exports.initialCohort = initialCohort;
10
+ exports.assertProtocolInvariants = assertProtocolInvariants;
11
+ exports.nextProtocolEffect = nextProtocolEffect;
12
+ exports.reconcileProtocol = reconcileProtocol;
13
+ exports.observeProtocolEffect = observeProtocolEffect;
14
+ const crypto_1 = __importDefault(require("crypto"));
15
+ const types_1 = require("./types");
16
+ /** Fases en las que un track de una cohorte activa puede estar. */
17
+ const WORKING = [
18
+ 'ARMED', 'ACTIVE', 'FREEZE_REQUESTED', 'FROZEN',
19
+ 'JOIN_REQUESTED', 'JOIN_INTENT', 'MERGED_UNVERIFIED', 'JOINED',
20
+ ];
21
+ /** Fases que ya fijaron un SHA congelado y no pueden volver a mutar el worktree. */
22
+ const FROZEN_OR_LATER = ['FROZEN', 'JOIN_INTENT', 'MERGED_UNVERIFIED', 'JOINED'];
23
+ /** Efecto aplicado exitosamente → fase resultante del track, para las transiciones puramente mecánicas. */
24
+ const EFFECT_APPLIED_PHASE = {
25
+ 'persist-prepare-intent': 'PREPARE_INTENT', 'create-worktree': 'WORKTREE_CREATED',
26
+ 'create-track-journal': 'JOURNAL_CREATED', 'spawn-track-supervisor': 'SUPERVISOR_STARTING',
27
+ 'activate-track': 'ACTIVE',
28
+ 'freeze-track': 'FREEZE_REQUESTED', 'persist-join-intent': 'JOIN_INTENT',
29
+ 'begin-teardown': 'TEARDOWN_INTENT',
30
+ };
31
+ const token = (journalId, trackId, purpose) => crypto_1.default.createHash('sha256').update(`${journalId}\0${trackId}\0${purpose}`).digest('hex').slice(0, 32);
32
+ const required = (value, name) => {
33
+ if (value === undefined)
34
+ throw new Error(`invariante rota: falta ${name}`);
35
+ return value;
36
+ };
37
+ /** C2: un fallo demostrable no bloquea, entra al teardown probatorio de C9 —
38
+ * salvo que ya sea terminal o BLOCKED. T13 (R4.3/C9): marca TODOS los tracks
39
+ * vivos de la cohorte (no solo el que falló) — `decideTeardown` exige la
40
+ * fase `TEARDOWN_REQUESTED` literal como único punto de entrada al state
41
+ * machine de teardown (Step 3 del plan), así que cualquier track que
42
+ * `nextProtocolEffect` vaya a elegir para `begin-teardown` (uno a la vez,
43
+ * ver el branch `FALLBACK_PENDING`) debe encontrarse YA en esa fase, sin
44
+ * importar en qué punto de su propio trabajo estaba cuando la cohorte
45
+ * entera cayó a fallback. */
46
+ const markTeardownRequested = (tracks) => {
47
+ for (const t of Object.values(tracks)) {
48
+ if (!['DECLARED', 'REMOVED', 'BLOCKED'].includes(t.phase))
49
+ t.phase = 'TEARDOWN_REQUESTED';
50
+ }
51
+ };
52
+ /** Autoridad única de reconciliación de join; T11 agrega su matriz de tests sobre esta función. */
53
+ function decideJoinReconciliation(intent, o) {
54
+ if (o.mergeHead === null && o.planHead === intent.expectedPlanHeadSha && !o.trackIsAncestor)
55
+ return { action: 'retry-merge' };
56
+ if (o.mergeHead === intent.expectedTrackHeadSha && o.planHead === intent.expectedPlanHeadSha)
57
+ return { action: 'abort-own-merge' };
58
+ if (o.mergeHead === null && o.trackIsAncestor === true)
59
+ return { action: 'accept-merge', joinedCommitSha: o.planHead };
60
+ if (o.mergeHead !== null && o.mergeHead !== intent.expectedTrackHeadSha)
61
+ return { action: 'block', reason: 'MERGE_HEAD ajeno' };
62
+ return { action: 'block', reason: 'estado de join indemostrable' };
63
+ }
64
+ /**
65
+ * Autoridad única de recuperación de crash de P1 (Task 9, R4.2/R4.6/C11):
66
+ * dado el estado protocolar de UN track en curso durante `PREPARING` y lo
67
+ * que el driver pudo observar del mundo real (fs/git/wrapper, siempre
68
+ * read-only — la gathering nunca cuenta como el side effect del tick, mismo
69
+ * criterio que `foreignPathExists` en `runCreateWorktree`), decide si la
70
+ * próxima llamada mutante a `TrackRuntime` debe intentarse de nuevo, si el
71
+ * resultado de un intento previo ya está ahí (crash entre el efecto real y
72
+ * su persistencia), si hay que bloquear por ajeno, o si la fase es
73
+ * indemostrable y hay que abortar a fallback.
74
+ *
75
+ * NO se llama desde `nextProtocolEffect`: esa función solo decide QUÉ efecto
76
+ * sigue mirando la fase (eso no cambia con Task 9 — `create-worktree` /
77
+ * `create-track-journal` / `spawn-track-supervisor` son inambiguos por
78
+ * fase). `decidePrepare` decide, para ESE MISMO efecto ya elegido, si
79
+ * `tracks.ts` debe tocar `TrackRuntime` de verdad o si una observación ya
80
+ * resuelve el resultado — enhebrarla en `nextProtocolEffect` (devolviendo
81
+ * `null` para 'accept-worktree'/'block-foreign'/'begin-fallback') obligaría
82
+ * a `tracks.ts` a volver a invocar esta misma función para saber QUÉ hacer
83
+ * con ese `null`, sin ganar nada: el punto de entrada más simple, y el único
84
+ * que no sobrecarga el significado de "sin más trabajo" de `null`, es que el
85
+ * driver la llame directamente con la observación ya recolectada — exactamente
86
+ * el mismo patrón que ya usa con `observeProtocolEffect`/`reconcileProtocol`
87
+ * para las observaciones POST-efecto. Sigue siendo la única autoridad de
88
+ * decisión: `tracks.ts` únicamente traduce su resultado.
89
+ */
90
+ function decidePrepare(t, observed) {
91
+ if (t.phase === 'PREPARE_INTENT') {
92
+ if (observed.worktreeOwned === true)
93
+ return 'accept-worktree';
94
+ if (observed.worktreeForeignNonEmpty === true)
95
+ return 'block-foreign';
96
+ return 'retry-worktree';
97
+ }
98
+ if (t.phase === 'WORKTREE_CREATED')
99
+ return 'write-descriptor';
100
+ if (t.phase === 'SUPERVISOR_STARTING') {
101
+ if (observed.supervisorArtifact === undefined || observed.supervisorArtifact === 'absent') {
102
+ return 'retry-supervisor-same-intent';
103
+ }
104
+ if (observed.supervisorArtifact === 'foreign')
105
+ return 'block-foreign';
106
+ // 'claimed'/'ready': ya hay evidencia real en disco de un intento
107
+ // previo (el mismo `supervisorIntent`, posiblemente de una instancia
108
+ // que crasheó DESPUÉS de spawnear pero ANTES de persistir
109
+ // `supervisorProcessRef`) — jamás re-spawnear (C11); delegar al
110
+ // mismo camino de observación que ya usa el post-spawn normal (C8
111
+ // decide el nonce ahí, no acá).
112
+ return 'accept-readiness';
113
+ }
114
+ // Fase inesperada para un efecto de PREPARING (fail-closed, R4.6): nunca
115
+ // alcanzable desde `reconcileTracks` (que solo llama `decidePrepare` para
116
+ // las tres fases de arriba), pero si algún día lo fuera, abortar a
117
+ // fallback es la única opción segura.
118
+ return 'begin-fallback';
119
+ }
120
+ /**
121
+ * Autoridad única de teardown (Task 13, R4.2/R4.3/R4.6/R4.10/C2/C9): dado el
122
+ * estado protocolar de UN track y lo que el driver (`tracks.ts`/`teardown.ts`)
123
+ * pudo probar del mundo real (fs/git/proceso, siempre READ-ONLY — gathering
124
+ * nunca cuenta como el side effect del tick, mismo criterio que
125
+ * `decidePrepare`), decide el próximo paso durable. Ajeno gana SIEMPRE
126
+ * primero, sin importar la fase: nunca se avanza un teardown sobre una
127
+ * identidad de supervisor o un worktree cuyo ownership es indemostrable
128
+ * (R4.6/R4.10) — ni siquiera si esa fase normalmente ya habría progresado.
129
+ *
130
+ * `teardown.ts` reexporta esta función (mismo patrón que `join.ts` con
131
+ * `decideJoinReconciliation`) y solo aporta los efectos reales; `tracks.ts`
132
+ * (`runBeginTeardown`) la llama dos veces por transición real, igual que
133
+ * `runMergeTrack` llama `decideJoinReconciliation`: una vez ANTES de tocar
134
+ * `runtime` (para decidir CUÁL efecto —o ausencia de efecto— corresponde), y
135
+ * la fase final siempre se persiste a través de `reconcileProtocol`
136
+ * (`teardown-observation`), que vuelve a llamar a esta misma función sobre la
137
+ * observación fresca — nunca confía en el resultado de la llamada a
138
+ * `runtime` por sí solo.
139
+ *
140
+ * `BLOCKED` cae deliberadamente en el `throw` final: ninguna fase de entrada
141
+ * a esta función es alcanzable en `BLOCKED` (`nextProtocolEffect` ya filtra
142
+ * cualquier cohorte con un track `BLOCKED` ANTES de emitir `begin-teardown`
143
+ * para nadie, ver el branch `FALLBACK_PENDING`) — un track bloqueado nunca
144
+ * entra a teardown automático. La propiedad que lo bloqueó es indemostrable
145
+ * justamente ahí, y "arreglarlo" solo/a ciegas sería el error que C9 existe
146
+ * para evitar. Sale de `BLOCKED` únicamente con evidencia del operador, que
147
+ * lo devuelve a `TEARDOWN_REQUESTED` explícitamente — jamás automático.
148
+ */
149
+ function decideTeardown(ref, o) {
150
+ if (o.foreignSupervisor || o.foreignWorktree)
151
+ return 'block-foreign';
152
+ if (ref.phase === 'TEARDOWN_REQUESTED')
153
+ return 'persist-intent';
154
+ if (ref.phase === 'TEARDOWN_INTENT')
155
+ return o.ownSupervisorAlive ? 'stop-own-supervisor' : 'accept-supervisor-stopped';
156
+ if (ref.phase === 'SUPERVISOR_STOPPED')
157
+ return o.ownedWorktreeExists ? 'remove-owned-worktree' : 'remove-owned-branch';
158
+ if (ref.phase === 'WORKTREE_REMOVED')
159
+ return o.ownedBranchExists ? 'remove-owned-branch' : 'mark-removed';
160
+ if (ref.phase === 'BRANCH_REMOVED')
161
+ return 'mark-removed';
162
+ throw new Error(`teardown no válido desde ${ref.phase}`);
163
+ }
164
+ function initialCohort(planJournalId, trackIds, maxParallel = trackIds.length) {
165
+ if (planJournalId.length === 0 || trackIds.length < 2 || new Set(trackIds).size !== trackIds.length
166
+ || !Number.isInteger(maxParallel) || maxParallel < 1) {
167
+ throw new Error('initialCohort requiere journalId y al menos dos trackIds únicos');
168
+ }
169
+ return {
170
+ planJournalId, cohortPhase: 'PREPARING', maxParallel,
171
+ tracks: Object.fromEntries(trackIds.map((trackId) => [trackId, {
172
+ trackId, phase: 'DECLARED',
173
+ fencingToken: token(planJournalId, trackId, 'fence'),
174
+ readinessNonce: token(planJournalId, trackId, 'readiness'),
175
+ }])),
176
+ };
177
+ }
178
+ function assertProtocolInvariants(s) {
179
+ const tracks = Object.values(s.tracks);
180
+ if (tracks.length < 2)
181
+ throw new Error('cohorte paralela requiere al menos dos tracks');
182
+ if ((s.cohortPhase === 'ACTIVE' || s.cohortPhase === 'JOINING') && tracks.some((t) => !WORKING.includes(t.phase))) {
183
+ throw new Error('ACTIVE prohíbe tracks sin armar');
184
+ }
185
+ if (tracks.filter((t) => t.phase === 'ACTIVE').length > s.maxParallel)
186
+ throw new Error('tracks ACTIVE exceden maxParallel');
187
+ if (tracks.filter((t) => t.phase === 'JOIN_INTENT').length > 1) {
188
+ throw new Error('un solo JOIN_INTENT vivo por cohorte'); // C7: los merges son serie estricta
189
+ }
190
+ if (tracks.some((t) => FROZEN_OR_LATER.includes(t.phase) && t.frozenHeadSha === undefined)) {
191
+ throw new Error('un track congelado exige frozenHeadSha');
192
+ }
193
+ // C2: SERIAL solo con la cohorte demostrablemente desmantelada. BLOCKED nunca cuenta como limpio.
194
+ if (s.cohortPhase === 'SERIAL' && tracks.some((t) => !['REMOVED', 'DECLARED'].includes(t.phase))) {
195
+ throw new Error('SERIAL prohíbe recursos paralelos activos');
196
+ }
197
+ if (s.finalIntegrationJobId !== undefined && tracks.some((t) => !['MERGED_UNVERIFIED', 'JOINED'].includes(t.phase))) {
198
+ throw new Error('integración final exige todos los tracks mergeados');
199
+ }
200
+ if (s.cohortPhase === 'COMPLETE' && tracks.some((t) => t.phase !== 'JOINED')) {
201
+ throw new Error('COMPLETE exige todos los tracks JOINED');
202
+ }
203
+ }
204
+ function nextProtocolEffect(s) {
205
+ assertProtocolInvariants(s);
206
+ const tracks = Object.values(s.tracks).sort((a, b) => a.trackId.localeCompare(b.trackId));
207
+ if (s.cohortPhase === 'BLOCKED' || s.cohortPhase === 'SERIAL' || s.cohortPhase === 'COMPLETE')
208
+ return null;
209
+ if (s.cohortPhase === 'FALLBACK_PENDING') {
210
+ // C2: propiedad indemostrable detiene la cohorte; nunca se serializa con recursos posiblemente vivos.
211
+ if (tracks.some((t) => t.phase === 'BLOCKED'))
212
+ return null;
213
+ const owned = tracks.find((t) => !['DECLARED', 'REMOVED'].includes(t.phase));
214
+ return owned !== undefined
215
+ ? { kind: 'begin-teardown', trackId: owned.trackId }
216
+ : { kind: 'enter-serial', reason: s.fallbackReason ?? 'prepare-failed' };
217
+ }
218
+ if (s.cohortPhase === 'PREPARING') {
219
+ const t = tracks.find((x) => x.phase !== 'ARMED' && x.phase !== 'BLOCKED');
220
+ if (t === undefined)
221
+ return tracks.every((x) => x.phase === 'ARMED') ? { kind: 'activate-cohort' } : null;
222
+ if (t.phase === 'DECLARED')
223
+ return { kind: 'persist-prepare-intent', trackId: t.trackId };
224
+ if (t.phase === 'PREPARE_INTENT')
225
+ return { kind: 'create-worktree', trackId: t.trackId };
226
+ if (t.phase === 'WORKTREE_CREATED')
227
+ return { kind: 'create-track-journal', trackId: t.trackId };
228
+ if (t.phase === 'JOURNAL_CREATED' || t.phase === 'SUPERVISOR_STARTING') {
229
+ return { kind: 'spawn-track-supervisor', trackId: t.trackId, readinessNonce: t.readinessNonce };
230
+ }
231
+ }
232
+ if (s.cohortPhase === 'ACTIVE' || s.cohortPhase === 'JOINING') {
233
+ if (s.cohortPhase === 'ACTIVE') {
234
+ const active = tracks.filter((t) => t.phase === 'ACTIVE').length;
235
+ const waiting = tracks.find((t) => t.phase === 'ARMED');
236
+ if (waiting !== undefined && active < s.maxParallel)
237
+ return { kind: 'activate-track', trackId: waiting.trackId };
238
+ }
239
+ const requested = tracks.find((t) => t.phase === 'JOIN_REQUESTED');
240
+ if (requested !== undefined)
241
+ return { kind: 'freeze-track', trackId: requested.trackId };
242
+ // C3: ningún merge empieza hasta que la cohorte entera esté congelada.
243
+ if (tracks.every((t) => FROZEN_OR_LATER.includes(t.phase))) {
244
+ const open = tracks.find((t) => t.phase === 'JOIN_INTENT');
245
+ if (open !== undefined) {
246
+ return {
247
+ kind: 'merge-track', trackId: open.trackId,
248
+ expectedPlanHeadSha: required(open.expectedPlanHeadSha, 'expectedPlanHeadSha'),
249
+ expectedTrackHeadSha: required(open.expectedTrackHeadSha, 'expectedTrackHeadSha'),
250
+ };
251
+ }
252
+ const next = tracks.find((t) => t.phase === 'FROZEN');
253
+ if (next !== undefined) {
254
+ return {
255
+ kind: 'persist-join-intent', trackId: next.trackId,
256
+ expectedPlanHeadSha: required(s.planHeadSha, 'planHeadSha'),
257
+ expectedTrackHeadSha: required(next.frozenHeadSha, 'frozenHeadSha'),
258
+ };
259
+ }
260
+ }
261
+ }
262
+ if (tracks.every((t) => t.phase === 'MERGED_UNVERIFIED') && s.globalQaHeadSha === undefined)
263
+ return { kind: 'request-global-qa' };
264
+ if (s.globalQaHeadSha !== undefined && s.finalIntegrationJobId === undefined)
265
+ return { kind: 'request-final-integration' };
266
+ if (s.finalIntegrationJobId !== undefined && s.cohortPhase === 'FINAL_INTERLOCK')
267
+ return { kind: 'run-final-interlock' };
268
+ return null;
269
+ }
270
+ function reconcileProtocol(s, observation) {
271
+ const out = structuredClone(s);
272
+ if (observation.kind === 'prepare-failed') {
273
+ out.cohortPhase = 'FALLBACK_PENDING';
274
+ out.fallbackReason = `prepare-failed:${observation.trackId}`;
275
+ markTeardownRequested(out.tracks);
276
+ }
277
+ else if (observation.kind === 'effect-failed') {
278
+ if (out.cohortPhase === 'PREPARING') {
279
+ out.cohortPhase = 'FALLBACK_PENDING';
280
+ // Nombra el TRACK además del efecto: "cuál falló" es la primera pregunta
281
+ // frente a una cohorte degradada, y sin el trackId el motivo obliga a
282
+ // correlacionar hacia atrás con el evento `track-effect-failed`. El sufijo se
283
+ // omite (no se rellena con un placeholder) cuando la observación genuinamente
284
+ // no tiene track — un `undefined` impreso sería peor que no decir nada.
285
+ out.fallbackReason = observation.trackId === undefined
286
+ ? `effect-failed:${observation.effect}`
287
+ : `effect-failed:${observation.effect}:${observation.trackId}`;
288
+ markTeardownRequested(out.tracks);
289
+ }
290
+ else if (observation.effect === 'begin-teardown' && observation.trackId !== undefined) {
291
+ // T13 (C9): un paso de teardown que no pudo probarse seguro
292
+ // (worktree sucio, branch todavía checked-out, ownership
293
+ // indemostrable) bloquea ESE track — nunca se fuerza `--force`/
294
+ // `-D` para "resolver" el fallo (R4.10). Mismo dead-end que
295
+ // `decideTeardown` documenta para BLOCKED: solo el operador lo
296
+ // saca de acá.
297
+ const t = out.tracks[observation.trackId];
298
+ if (t !== undefined) {
299
+ t.phase = 'BLOCKED';
300
+ t.blockedReason = observation.detail;
301
+ }
302
+ }
303
+ // effect-failed fuera de PREPARING/begin-teardown: sin transición definida todavía (no ejercitado por T1).
304
+ }
305
+ else if (observation.kind === 'worktree-observed') {
306
+ const t = out.tracks[observation.trackId];
307
+ if (t === undefined)
308
+ throw new Error(`track desconocido: ${observation.trackId}`);
309
+ // R4.6: un worktree que no es demostrablemente nuestro nunca se adopta ni se borra.
310
+ if (observation.owned)
311
+ t.phase = 'WORKTREE_CREATED';
312
+ else {
313
+ t.phase = 'BLOCKED';
314
+ t.blockedReason = 'worktree preexistente ajeno';
315
+ }
316
+ }
317
+ else if (observation.kind === 'supervisor-observed') {
318
+ const t = out.tracks[observation.trackId];
319
+ if (t === undefined)
320
+ throw new Error(`track desconocido: ${observation.trackId}`);
321
+ if (observation.identity === 'other') {
322
+ t.phase = 'BLOCKED';
323
+ t.blockedReason = 'identidad de supervisor ajena';
324
+ }
325
+ else if (observation.identity === 'expected') {
326
+ // C8: el readiness solo se acredita con el nonce que emitió el supervisor del plan.
327
+ if (observation.readinessNonce === t.readinessNonce)
328
+ t.phase = 'ARMED';
329
+ else {
330
+ t.phase = 'BLOCKED';
331
+ t.blockedReason = 'readinessNonce no coincide';
332
+ }
333
+ }
334
+ // 'absent' no mueve fase: el mismo supervisorIntent se reintenta (C11).
335
+ }
336
+ else if (observation.kind === 'join-requested') {
337
+ const t = out.tracks[observation.trackId];
338
+ if (t === undefined)
339
+ throw new Error(`track desconocido: ${observation.trackId}`);
340
+ if (t.phase === 'ACTIVE')
341
+ t.phase = 'JOIN_REQUESTED';
342
+ }
343
+ else if (observation.kind === 'teardown-observation') {
344
+ const t = out.tracks[observation.trackId];
345
+ if (t === undefined)
346
+ throw new Error(`track desconocido: ${observation.trackId}`);
347
+ // Autoridad única: la misma función que T13 prueba con su matriz
348
+ // pura completa (mismo criterio que `join-observation` arriba con
349
+ // `decideJoinReconciliation`) — nunca se reimplementa el mapeo acá.
350
+ const decision = decideTeardown(t, observation);
351
+ if (decision === 'block-foreign') {
352
+ t.phase = 'BLOCKED';
353
+ t.blockedReason = observation.foreignSupervisor ? 'identidad de supervisor ajena' : 'worktree preexistente ajeno';
354
+ }
355
+ else if (decision === 'accept-supervisor-stopped') {
356
+ t.phase = 'SUPERVISOR_STOPPED';
357
+ }
358
+ else if (decision === 'remove-owned-branch' && t.phase === 'SUPERVISOR_STOPPED') {
359
+ // Sin worktree propio que remover (nunca existió, o ya se quitó
360
+ // en un intento previo que crasheó antes de persistir esto):
361
+ // mecánicamente ya se alcanzó WORKTREE_REMOVED, nada más que probar.
362
+ t.phase = 'WORKTREE_REMOVED';
363
+ }
364
+ else if (decision === 'mark-removed' && t.phase === 'WORKTREE_REMOVED') {
365
+ // Sin branch propia que remover: mecánicamente ya se alcanzó BRANCH_REMOVED.
366
+ t.phase = 'BRANCH_REMOVED';
367
+ }
368
+ else if (decision === 'mark-removed' && t.phase === 'BRANCH_REMOVED') {
369
+ t.phase = 'REMOVED';
370
+ }
371
+ // 'persist-intent' / 'stop-own-supervisor' / 'remove-owned-worktree'
372
+ // / 'remove-owned-branch' (todavía desde WORKTREE_REMOVED, con la
373
+ // branch real por remover): el driver TODAVÍA no completó el efecto
374
+ // real — nada que avanzar en ESTA observación; el próximo
375
+ // re-observe (tras el efecto) sí lo hace.
376
+ }
377
+ else if (observation.kind === 'freeze-observation') {
378
+ const t = out.tracks[observation.trackId];
379
+ if (t === undefined)
380
+ throw new Error(`track desconocido: ${observation.trackId}`);
381
+ t.frozenHeadSha = observation.frozenHeadSha;
382
+ t.phase = 'FROZEN';
383
+ out.cohortPhase = 'JOINING';
384
+ }
385
+ else if (observation.kind === 'join-observation') {
386
+ const t = out.tracks[observation.trackId];
387
+ if (t === undefined)
388
+ throw new Error(`track desconocido: ${observation.trackId}`);
389
+ // Autoridad única: la misma función que T11 prueba con su matriz completa.
390
+ const decision = decideJoinReconciliation({
391
+ expectedPlanHeadSha: required(t.expectedPlanHeadSha, 'expectedPlanHeadSha'),
392
+ expectedTrackHeadSha: required(t.expectedTrackHeadSha, 'expectedTrackHeadSha'),
393
+ strategy: types_1.JOIN_STRATEGY_NO_FF,
394
+ }, observation);
395
+ if (decision.action === 'accept-merge') {
396
+ t.phase = 'MERGED_UNVERIFIED';
397
+ t.joinedCommitSha = decision.joinedCommitSha;
398
+ out.planHeadSha = decision.joinedCommitSha;
399
+ }
400
+ else if (decision.action === 'block') {
401
+ t.phase = 'BLOCKED';
402
+ t.blockedReason = decision.reason;
403
+ out.cohortPhase = 'BLOCKED';
404
+ }
405
+ // 'retry-merge' y 'abort-own-merge' no mueven fase: el próximo tick reintenta el mismo intent.
406
+ }
407
+ else if (observation.kind === 'global-qa-pass') {
408
+ if (!observation.clean)
409
+ throw new Error('QA global no puede cerrar con worktree sucio');
410
+ out.globalQaHeadSha = observation.headSha;
411
+ out.cohortPhase = 'FINAL_INTEGRATION';
412
+ }
413
+ else if (observation.kind === 'integration-pass') {
414
+ if (out.globalQaHeadSha !== observation.headSha)
415
+ throw new Error('integración no corresponde al HEAD de QA');
416
+ out.finalIntegrationJobId = observation.jobId;
417
+ out.cohortPhase = 'FINAL_INTERLOCK';
418
+ }
419
+ else if (observation.kind === 'interlock-pass') {
420
+ if (out.globalQaHeadSha !== observation.headSha)
421
+ throw new Error('interlock stale');
422
+ for (const t of Object.values(out.tracks))
423
+ t.phase = 'JOINED';
424
+ out.cohortPhase = 'COMPLETE';
425
+ }
426
+ else if (observation.kind !== 'effect-applied') {
427
+ const unhandled = observation; // el compilador exige una rama por variante
428
+ throw new Error(`observación no manejada: ${JSON.stringify(unhandled)}`);
429
+ }
430
+ assertProtocolInvariants(out);
431
+ return out;
432
+ }
433
+ function observeProtocolEffect(s, effect, observation) {
434
+ if (observation.kind === 'effect-failed' || observation.kind === 'prepare-failed' || observation.kind === 'join-observation'
435
+ || observation.kind === 'freeze-observation' || observation.kind === 'join-requested'
436
+ || observation.kind === 'supervisor-observed' || observation.kind === 'worktree-observed'
437
+ || observation.kind === 'teardown-observation'
438
+ || observation.kind === 'global-qa-pass'
439
+ || observation.kind === 'integration-pass' || observation.kind === 'interlock-pass') {
440
+ // reconcileProtocol clona internamente: pasar `s` sin clonar acá evita un structuredClone redundante.
441
+ return reconcileProtocol(s, observation);
442
+ }
443
+ const out = structuredClone(s);
444
+ if (observation.kind !== 'effect-applied')
445
+ return out;
446
+ const e = effect;
447
+ if ('trackId' in e) {
448
+ const t = out.tracks[e.trackId];
449
+ if (t === undefined)
450
+ throw new Error(`track desconocido: ${e.trackId}`);
451
+ const phase = EFFECT_APPLIED_PHASE[e.kind];
452
+ if (phase !== undefined)
453
+ t.phase = phase;
454
+ if (e.kind === 'persist-join-intent') {
455
+ t.expectedPlanHeadSha = e.expectedPlanHeadSha;
456
+ t.expectedTrackHeadSha = e.expectedTrackHeadSha;
457
+ }
458
+ }
459
+ if (e.kind === 'activate-cohort') {
460
+ out.cohortPhase = 'ACTIVE';
461
+ }
462
+ if (e.kind === 'enter-serial')
463
+ out.cohortPhase = 'SERIAL';
464
+ assertProtocolInvariants(out);
465
+ return out;
466
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideTeardown = void 0;
4
+ exports.worktreeOwnershipProven = worktreeOwnershipProven;
5
+ const descriptor_1 = require("./descriptor");
6
+ const paths_1 = require("../paths");
7
+ const git_1 = require("./git");
8
+ var protocol_1 = require("./protocol");
9
+ Object.defineProperty(exports, "decideTeardown", { enumerable: true, get: function () { return protocol_1.decideTeardown; } });
10
+ function worktreeOwnershipProven(repo, ref, planJournalId) {
11
+ const intent = ref.teardownIntent;
12
+ if (intent === undefined)
13
+ return false; // sin snapshot durable: nada que comparar, no probado
14
+ // Identidad de filesystem, no de string (mismo motivo que en `ownedWorktreeExists`):
15
+ // el snapshot durable del intent y el `TrackRef` vivo pueden traer dos grafías del
16
+ // mismo path. `sameExistingPath` ya devuelve `false` si alguno no existe.
17
+ if (!(0, paths_1.sameExistingPath)(intent.worktreePath, ref.worktreePath))
18
+ return false;
19
+ if (intent.branch !== ref.branch)
20
+ return false;
21
+ let descriptor;
22
+ try {
23
+ descriptor = (0, descriptor_1.readDescriptor)(ref.worktreePath);
24
+ }
25
+ catch {
26
+ return false; // track.json presente pero corrupto: indemostrable (R1.6/R4.6)
27
+ }
28
+ if (descriptor === null)
29
+ return false;
30
+ if (descriptor.planJournalId !== planJournalId || descriptor.trackId !== ref.trackId
31
+ || descriptor.fencingToken !== ref.fencingToken)
32
+ return false;
33
+ return (0, git_1.ownedWorktreeExists)(repo, ref.worktreePath, ref.branch);
34
+ }
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JOIN_STRATEGY_NO_FF = exports.TRACK_PHASES = void 0;
4
+ exports.TRACK_PHASES = [
5
+ 'DECLARED', 'PREPARE_INTENT', 'WORKTREE_CREATED', 'JOURNAL_CREATED',
6
+ 'SUPERVISOR_STARTING', 'ARMED', 'ACTIVE', 'FREEZE_REQUESTED', 'FROZEN',
7
+ 'JOIN_REQUESTED', 'JOIN_INTENT', 'MERGED_UNVERIFIED', 'JOINED',
8
+ 'TEARDOWN_REQUESTED', 'TEARDOWN_INTENT', 'SUPERVISOR_STOPPED',
9
+ 'WORKTREE_REMOVED', 'BRANCH_REMOVED', 'REMOVED', 'BLOCKED',
10
+ ];
11
+ // Única estrategia de merge soportada hoy (R6.x) — constante compartida para
12
+ // que `protocol.ts` y `watch/tracks.ts` (el driver) nunca dupliquen el
13
+ // literal (post-review, minor finding Task 8).
14
+ exports.JOIN_STRATEGY_NO_FF = 'no-ff';
package/dist/src/index.js CHANGED
@@ -40,6 +40,7 @@ const export_1 = require("./commands/export");
40
40
  const agent_1 = require("./commands/agent");
41
41
  const job_1 = require("./commands/job");
42
42
  const watch_1 = require("./commands/watch");
43
+ const track_1 = require("./commands/track");
43
44
  const add_1 = require("./commands/add");
44
45
  const sync_1 = require("./commands/sync");
45
46
  const update_1 = require("./commands/update");
@@ -704,4 +705,5 @@ miroCmd.command('sync <storyMapPath>')
704
705
  (0, agent_1.registerAgentCommand)(program);
705
706
  (0, job_1.registerJobCommand)(program);
706
707
  (0, watch_1.registerWatchCommand)(program);
708
+ (0, track_1.registerTrackCommand)(program);
707
709
  program.parse();