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
|
@@ -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();
|