agentic-workflow-manager 6.2.0 → 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 (75) 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/diagnostics/provider-checks.js +12 -9
  16. package/dist/src/core/journal/adapter.js +46 -6
  17. package/dist/src/core/journal/paths.js +9 -0
  18. package/dist/src/core/journal/process.js +101 -1
  19. package/dist/src/core/journal/requests.js +5 -1
  20. package/dist/src/core/journal/store.js +34 -2
  21. package/dist/src/core/journal/types.js +93 -4
  22. package/dist/src/core/paths.js +51 -0
  23. package/dist/src/core/profile.js +15 -1
  24. package/dist/src/core/tracks/concurrency.js +85 -0
  25. package/dist/src/core/tracks/context.js +89 -0
  26. package/dist/src/core/tracks/descriptor.js +40 -0
  27. package/dist/src/core/tracks/git.js +318 -0
  28. package/dist/src/core/tracks/join.js +185 -0
  29. package/dist/src/core/tracks/ownership.js +100 -0
  30. package/dist/src/core/tracks/plan-parser.js +108 -0
  31. package/dist/src/core/tracks/protocol.js +466 -0
  32. package/dist/src/core/tracks/teardown.js +34 -0
  33. package/dist/src/core/tracks/types.js +14 -0
  34. package/dist/src/core/update-check.js +5 -1
  35. package/dist/src/index.js +2 -0
  36. package/dist/tests/commands/job/gate-reconcile.test.js +267 -0
  37. package/dist/tests/commands/track/fixtures.js +13 -0
  38. package/dist/tests/commands/track/status.test.js +157 -0
  39. package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +129 -0
  40. package/dist/tests/commands/track/supervisor-wrapper.test.js +131 -0
  41. package/dist/tests/commands/track/verbs.test.js +326 -0
  42. package/dist/tests/commands/watch/apply.test.js +116 -4
  43. package/dist/tests/commands/watch/runner.test.js +22 -0
  44. package/dist/tests/commands/watch/supervisor-loop.test.js +150 -0
  45. package/dist/tests/commands/watch/track-bootstrap-crash.test.js +342 -0
  46. package/dist/tests/commands/watch/track-bootstrap.test.js +350 -0
  47. package/dist/tests/commands/watch/track-finalize.test.js +643 -0
  48. package/dist/tests/commands/watch/track-freeze.test.js +591 -0
  49. package/dist/tests/commands/watch/track-join-crash.test.js +439 -0
  50. package/dist/tests/commands/watch/track-runtime-git.test.js +119 -0
  51. package/dist/tests/commands/watch/track-teardown-crash.test.js +426 -0
  52. package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
  53. package/dist/tests/core/journal/adapter-override.test.js +51 -0
  54. package/dist/tests/core/journal/process.test.js +37 -0
  55. package/dist/tests/core/journal/requests.test.js +21 -0
  56. package/dist/tests/core/journal/store.test.js +28 -0
  57. package/dist/tests/core/journal/types.test.js +89 -0
  58. package/dist/tests/core/profile.test.js +9 -2
  59. package/dist/tests/core/same-existing-path.test.js +48 -0
  60. package/dist/tests/core/tracks/concurrency.test.js +134 -0
  61. package/dist/tests/core/tracks/context.test.js +177 -0
  62. package/dist/tests/core/tracks/descriptor.test.js +76 -0
  63. package/dist/tests/core/tracks/git.test.js +132 -0
  64. package/dist/tests/core/tracks/join-reconcile.test.js +53 -0
  65. package/dist/tests/core/tracks/join.test.js +197 -0
  66. package/dist/tests/core/tracks/ownership.test.js +96 -0
  67. package/dist/tests/core/tracks/plan-parser.test.js +94 -0
  68. package/dist/tests/core/tracks/protocol.test.js +416 -0
  69. package/dist/tests/core/tracks/teardown.test.js +62 -0
  70. package/dist/tests/core/update-check.test.js +34 -3
  71. package/dist/tests/helpers/git-fixture.js +35 -0
  72. package/dist/tests/integration/parallel-tracks.e2e.test.js +343 -0
  73. package/dist/tests/integration/r5-provider-evidence.test.js +67 -0
  74. package/dist/tests/structural/path-identity-not-string-compare.test.js +51 -0
  75. package/package.json +1 -1
@@ -0,0 +1,89 @@
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.runSupervisorWrapper = runSupervisorWrapper;
7
+ // Wrapper detached del supervisor de UN track (Step 5, R4.7-R4.10, C11):
8
+ // mismo contrato de durabilidad que `job/exec-wrapper.ts` (claim exclusivo ->
9
+ // identidad -> resultado), adaptado a "arrancar un supervisor" en vez de "un
10
+ // comando". Claim con `wx`: un segundo wrapper lanzado con el mismo intent
11
+ // jamás lanza un segundo supervisor — solo detecta el claim y sale.
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const child_process_1 = require("child_process");
15
+ const child_process_2 = require("child_process");
16
+ const process_1 = require("../../core/journal/process");
17
+ const atomic_file_1 = require("../../core/atomic-file");
18
+ const store_1 = require("../../core/journal/store");
19
+ const paths_1 = require("../../core/journal/paths");
20
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
21
+ function localBranch(worktreePath) {
22
+ return (0, child_process_2.execFileSync)('git', ['branch', '--show-current'], { cwd: worktreePath, encoding: 'utf8', stdio: process_1.EXEC_STDIO }).trim();
23
+ }
24
+ function defaultLaunchWatch(worktreePath) {
25
+ const cliEntry = path_1.default.resolve(__dirname, '..', '..', 'index.js');
26
+ const child = (0, child_process_1.spawn)(process.execPath, [cliEntry, 'watch'], { cwd: worktreePath, detached: true, stdio: 'ignore' });
27
+ child.unref();
28
+ }
29
+ /**
30
+ * Ejecuta el ciclo de vida completo del wrapper: claim exact-once -> identity
31
+ * sidecar -> init del journal local (idempotente, ya lo creó el supervisor
32
+ * del plan) -> readiness sidecar -> espera read-only al propio TrackRef del
33
+ * plan -> al pasar a ACTIVE, lanza `awm watch` y espera a que el lock quede
34
+ * adquirido. Mientras espera slot, NO corre gate, fingerprint ni tareas.
35
+ */
36
+ async function runSupervisorWrapper(opts) {
37
+ const dir = path_1.default.join(opts.worktreePath, '.awm');
38
+ fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
39
+ const claimPath = path_1.default.join(dir, 'supervisor.claim');
40
+ let fd;
41
+ try {
42
+ fd = fs_1.default.openSync(claimPath, 'wx', 0o600);
43
+ }
44
+ catch {
45
+ return 'already-claimed';
46
+ }
47
+ const nonce = opts.nonce;
48
+ try {
49
+ fs_1.default.writeFileSync(fd, JSON.stringify({ trackId: opts.trackId, nonce, claimedAt: new Date().toISOString(), pid: process.pid }) + '\n');
50
+ fs_1.default.fsyncSync(fd);
51
+ }
52
+ finally {
53
+ fs_1.default.closeSync(fd);
54
+ }
55
+ (0, atomic_file_1.fsyncDirSync)(dir);
56
+ (0, atomic_file_1.writeFileAtomicDurable)(path_1.default.join(dir, 'supervisor.identity.json'), JSON.stringify({ trackId: opts.trackId, nonce, self: (0, process_1.captureSelfRef)(nonce) }, null, 2) + '\n', 0o600);
57
+ // Idempotente: el supervisor del plan ya corrió `initTrackJournal` antes
58
+ // de spawnear este wrapper — esto solo defiende contra el caso en que
59
+ // ese paso quedó a medias (nunca sobreescribe: `initJournal` no toca un
60
+ // state.json ya existente).
61
+ (0, store_1.initJournal)(opts.worktreePath, localBranch(opts.worktreePath));
62
+ (0, atomic_file_1.writeFileAtomicDurable)(path_1.default.join(dir, 'supervisor.ready.json'), JSON.stringify({ readinessNonce: opts.readinessNonce, at: new Date().toISOString() }, null, 2) + '\n', 0o600);
63
+ const sleep = opts.sleep ?? defaultSleep;
64
+ const pollMs = opts.pollMs ?? 2000;
65
+ const maxIterations = opts.maxWaitIterations ?? Number.POSITIVE_INFINITY;
66
+ for (let i = 0; i < maxIterations; i++) {
67
+ const r = (0, store_1.readJournal)(opts.planRoot, opts.planBranch);
68
+ if (!r.corrupt && r.state !== null) {
69
+ const ref = r.state.tracks?.find((t) => t.trackId === opts.trackId);
70
+ if (ref?.phase === 'ACTIVE')
71
+ break;
72
+ // BLOCKED/REMOVED: el plan ya decidió que este track no avanza —
73
+ // el wrapper nunca lanza `awm watch` para un track que el plan
74
+ // bloqueó, ni insiste indefinidamente (C1: la barrera es del plan).
75
+ if (ref?.phase === 'BLOCKED' || ref?.phase === 'REMOVED')
76
+ return 'blocked';
77
+ }
78
+ if (i === maxIterations - 1)
79
+ return 'blocked';
80
+ await sleep(pollMs);
81
+ }
82
+ const launch = opts.launchWatch ?? defaultLaunchWatch;
83
+ launch(opts.worktreePath);
84
+ const lockPath = (0, paths_1.supervisorLockPath)(opts.worktreePath);
85
+ for (let i = 0; i < (opts.maxWaitIterations ?? 50) && !fs_1.default.existsSync(lockPath); i++) {
86
+ await sleep(Math.min(pollMs, 100));
87
+ }
88
+ return 'active';
89
+ }
@@ -3,19 +3,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.applyCyclePlanAppend = applyCyclePlanAppend;
7
+ exports.registerTrackIntegrationItems = registerTrackIntegrationItems;
6
8
  exports.consumePendingRequests = consumePendingRequests;
7
9
  // Consumo transaccional de requests (bloqueador 4): mutar estado ->
8
10
  // writeJournal -> RECIEN AHI borrar archivos. El replay es seguro por
9
11
  // requestId + idempotencyKey + digest.
10
12
  const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
11
14
  const crypto_1 = __importDefault(require("crypto"));
12
15
  const store_1 = require("../../core/journal/store");
13
16
  const requests_1 = require("../../core/journal/requests");
14
17
  const paths_1 = require("../../core/journal/paths");
15
18
  const atomic_file_1 = require("../../core/atomic-file");
16
19
  const redact_1 = require("../../core/journal/redact");
20
+ const git_1 = require("../../core/tracks/git");
17
21
  function now() { return new Date().toISOString(); }
18
- const VERIFICATION_KINDS = ['test', 'lint', 'sensors', 'review', 'qa', 'interlock'];
22
+ const VERIFICATION_KINDS = ['test', 'lint', 'sensors', 'review', 'qa', 'interlock', 'track-integration'];
19
23
  function verificationItems(value, field) {
20
24
  if (!Array.isArray(value) || !value.every((item) => typeof item === 'object' && item !== null
21
25
  && typeof item.id === 'string'
@@ -38,6 +42,22 @@ function stringArray(value, field) {
38
42
  throw new Error(`${field} requiere array de strings`);
39
43
  return value;
40
44
  }
45
+ /** R7 Task 12: `job-request.satisfies` migró de string a array (un mismo job
46
+ * puede satisfacer VARIOS items del ciclo a la vez — ej. el job canónico de
47
+ * integración final satisface todos los `track-integration:*` juntos).
48
+ * Acepta AMBAS formas en el payload (string legacy o array) — el emisor
49
+ * actual (`requestJob`) siempre manda array, pero una request en vuelo
50
+ * emitida por un binario anterior al deploy de este cambio no debe
51
+ * rechazarse por forma. */
52
+ function satisfiesArray(value, field) {
53
+ if (value === undefined)
54
+ return [];
55
+ if (typeof value === 'string')
56
+ return [value];
57
+ if (Array.isArray(value) && value.every((item) => typeof item === 'string'))
58
+ return value;
59
+ throw new Error(`${field} requiere string o array de strings`);
60
+ }
41
61
  function linkSatisfies(s, itemId, jobId) {
42
62
  const items = [...s.tasks.flatMap((t) => t.verificationPlan), ...s.cycleVerificationPlan];
43
63
  const item = items.find((i) => i.id === itemId);
@@ -45,7 +65,45 @@ function linkSatisfies(s, itemId, jobId) {
45
65
  throw new Error(`VerificationItem desconocido: ${itemId}`);
46
66
  item.satisfiedBy = jobId;
47
67
  }
48
- function applyRequestToState(s, env, digest) {
68
+ /** R7.2 (Task 12): append transaccional — agrega items NUEVOS por id sin
69
+ * jamás pisar un item ya existente (preserva `satisfiedBy` ya enlazado). Una
70
+ * colisión de `kind` para el mismo id rechaza el request COMPLETO (se llama
71
+ * siempre sobre el clone transaccional de `consumePendingRequests`, así que
72
+ * un throw acá descarta la mutación entera vía el catch de ese loop). */
73
+ function applyCyclePlanAppend(state, incoming) {
74
+ const byId = new Map(state.cycleVerificationPlan.map((item) => [item.id, item]));
75
+ for (const item of incoming) {
76
+ const prior = byId.get(item.id);
77
+ if (prior !== undefined && prior.kind !== item.kind)
78
+ throw new Error(`verification item ${item.id} cambia kind`);
79
+ if (prior === undefined)
80
+ byId.set(item.id, { ...item });
81
+ }
82
+ state.cycleVerificationPlan = [...byId.values()];
83
+ }
84
+ /** R7.3 (Task 12): declarar un track dentro de la cohorte agrega DE INMEDIATO
85
+ * su `track-integration:<trackId>` al CycleVerificationPlan, sin
86
+ * `satisfiedBy` — el gate global queda rojo desde el momento en que la
87
+ * cohorte se conoce, nunca solo al final. Idempotente por construcción
88
+ * (`applyCyclePlanAppend`): llamarla de nuevo con el mismo conjunto de
89
+ * tracks es un no-op para los ids ya presentes. */
90
+ function registerTrackIntegrationItems(state) {
91
+ const items = [...new Set((state.tracks ?? []).map((t) => t.trackId))].sort()
92
+ .map((trackId) => ({ id: `track-integration:${trackId}`, kind: 'track-integration' }));
93
+ applyCyclePlanAppend(state, items);
94
+ }
95
+ /** Convención determinista, sibling del repo del plan — nunca DENTRO de su
96
+ * working tree (git worktree add lo exige, y evita que el track pise el
97
+ * árbol que el propio plan está observando). El trackId ya pasó
98
+ * `gitCheckTrackId` antes de llegar acá, así que es seguro como segmento
99
+ * de path. NOTA (ver `concerns` del reporte de Task 8): esta función NO
100
+ * deriva ownership/dependsOn/sharedResources del plan — `reconcileTracks`
101
+ * (P1) no los necesita; quedan en `[]` hasta que un mecanismo futuro (fuera
102
+ * de alcance de R5-T8) los declare explícitamente. */
103
+ function trackWorktreePath(repoRoot, trackId) {
104
+ return path_1.default.join(path_1.default.dirname(repoRoot), `${path_1.default.basename(repoRoot)}.track-${trackId}`);
105
+ }
106
+ function applyRequestToState(s, env, digest, repoRoot) {
49
107
  const base = { requestId: env.requestId, idempotencyKey: env.idempotencyKey, payloadDigest: digest };
50
108
  if (env.kind === 'controller-heartbeat') {
51
109
  s.controllerHeartbeatAt = now();
@@ -71,12 +129,13 @@ function applyRequestToState(s, env, digest) {
71
129
  && (['received', 'spawn-intent', 'claimed', 'running'].includes(j.executionState)
72
130
  || (j.executionState === 'exited' && j.verdict === 'pass')));
73
131
  if (equivalent !== undefined) {
74
- if (typeof p.satisfies === 'string')
75
- linkSatisfies(s, p.satisfies, equivalent.id);
132
+ for (const itemId of satisfiesArray(p.satisfies, 'job-request satisfies'))
133
+ linkSatisfies(s, itemId, equivalent.id);
76
134
  (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: equivalent.id });
77
135
  return;
78
136
  }
79
137
  const jobId = `job-${Object.keys(s.jobs).length + 1}-${crypto_1.default.randomBytes(3).toString('hex')}`;
138
+ const satisfiesIds = satisfiesArray(p.satisfies, 'job-request satisfies');
80
139
  const job = {
81
140
  id: jobId,
82
141
  fingerprint: String(p.fingerprint), commandDigest: String(p.commandDigest),
@@ -86,14 +145,122 @@ function applyRequestToState(s, env, digest) {
86
145
  expandedPaths: p.expandedPaths === undefined ? [] : stringArray(p.expandedPaths, 'job-request expandedPaths'),
87
146
  executionState: 'received', observationState: 'progressing',
88
147
  phaseTimestamps: { received: now() },
89
- ...(typeof p.satisfies === 'string' ? { satisfies: p.satisfies } : {}),
148
+ ...(satisfiesIds.length > 0 ? { satisfies: satisfiesIds } : {}),
90
149
  };
91
150
  s.jobs[jobId] = job;
92
- if (typeof p.satisfies === 'string')
93
- linkSatisfies(s, p.satisfies, jobId);
151
+ for (const itemId of satisfiesIds)
152
+ linkSatisfies(s, itemId, jobId);
94
153
  (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: jobId });
95
154
  return;
96
155
  }
156
+ if (env.kind === 'track-prepare-request') {
157
+ // R4.1/R6.1: el ÚNICO efecto de esta request es declarar el TrackRef
158
+ // (fase DECLARED) — todo lo que sigue (worktree, journal, supervisor,
159
+ // ARMED, ACTIVE) es propiedad exclusiva de `reconcileTracks`
160
+ // (watch/tracks.ts), nunca de este consumo transaccional.
161
+ //
162
+ // ============================================================
163
+ // GAP CONOCIDO Y DELIBERADAMENTE DIFERIDO (post-review Task 8,
164
+ // item 3 — decisión: opción (b), NO implementar acá):
165
+ //
166
+ // `TrackContext.taskIds`/`planDigest` (asignados en
167
+ // `tracks.ts::runCreateTrackJournal`) y `TrackRef.ownership`/
168
+ // `dependsOn`/`sharedResources` (asignados acá abajo) quedan
169
+ // `[]`/`''` — NINGÚN task de 1 a 8 provee un mecanismo para que el
170
+ // supervisor localice y parsee "el" archivo de plan .md de la rama
171
+ // en ejecución. `parseTrackPlan` HOY solo se invoca desde
172
+ // `track verify-independence --plan <file>`, con el path pasado a
173
+ // mano; no existe convención de descubrimiento automático
174
+ // (ni glob de `docs/plans/*.md`, ni mapeo rama->archivo).
175
+ //
176
+ // Esto NO bloquea a Task 8 (el protocolo P1 de `nextProtocolEffect`
177
+ // jamás lee estos campos), pero SÍ va a romper en silencio:
178
+ // - Task 5's `assertTrackTask`/`computeTrackGate` (gating por
179
+ // `taskIds`/`planDigest` reales) una vez que algo intente usarlos
180
+ // de verdad contra un track creado por esta vía.
181
+ // - El supuesto de Task 14 (`docs/plans/2026-08-02-r5-parallel-
182
+ // tracks-plan.md:2587-2591`) de que `trackContext.taskIds` ya es
183
+ // confiable para ese momento.
184
+ //
185
+ // Requiere una decisión humana sobre qué task es dueño de: (a)
186
+ // definir la convención de descubrimiento del archivo de plan, y
187
+ // (b) extender `track add`/`emitTrackRequest` para llevarlo — o una
188
+ // enmienda al plan de 17 tasks. No se improvisa acá.
189
+ // ============================================================
190
+ const p = env.payload;
191
+ if (typeof p.trackId !== 'string' || p.trackId.length === 0)
192
+ throw new Error('track-prepare-request requiere trackId');
193
+ const trackId = p.trackId;
194
+ if (!(0, git_1.gitCheckTrackId)(trackId))
195
+ throw new Error(`track-prepare-request: trackId inválido: ${trackId}`);
196
+ if (!s.tracks?.some((t) => t.trackId === trackId)) {
197
+ const ref = {
198
+ trackId,
199
+ worktreePath: trackWorktreePath(repoRoot, trackId),
200
+ branch: `awm-track/${trackId}`,
201
+ ownership: [], sharedResources: [], dependsOn: [], // ver GAP arriba
202
+ // R4.10: tokens criptográficamente aleatorios — los hashes
203
+ // deterministas de protocol.ts son solo valores opacos del
204
+ // modelo puro, jamás la fuente real de un token de producción.
205
+ fencingToken: crypto_1.default.randomBytes(32).toString('hex'),
206
+ phase: 'DECLARED',
207
+ readinessNonce: crypto_1.default.randomBytes(32).toString('hex'),
208
+ };
209
+ s.tracks = [...(s.tracks ?? []), ref];
210
+ // R7.3 (Task 12): el gate global queda rojo desde el momento en
211
+ // que un track se conoce — no espera a que la cohorte termine de
212
+ // declararse ni a JOINING/FINAL_QA.
213
+ registerTrackIntegrationItems(s);
214
+ // C1: la barrera ARMED solo tiene sentido con el conjunto COMPLETO
215
+ // de la cohorte conocido — recién con >= 2 tracks declarados
216
+ // existe algo que `reconcileTracks` pueda reconciliar (Task 1
217
+ // exige al menos dos tracks en cualquier CohortProtocol válido).
218
+ if (s.tracks.length >= 2 && s.cohortPhase === undefined) {
219
+ s.cohortPhase = 'PREPARING';
220
+ s.cohortBaseSha = (0, git_1.headSha)(repoRoot);
221
+ }
222
+ }
223
+ (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied', resultRef: trackId });
224
+ return;
225
+ }
226
+ if (env.kind === 'track-freeze-request') {
227
+ // R5.2/R6.3 (Task 10): request administrativa CROSS-JOURNAL — el
228
+ // supervisor del PLAN la emite directamente al `requestsDir` de ESTE
229
+ // journal (el journal de UN track), vía el mismo primitivo durable
230
+ // (`emitRequest`) que cualquier otra request. El propio supervisor
231
+ // del track (SU `Supervisor.tick()`, corriendo en este mismo
232
+ // worktree) la consume acá como cualquier otra — el efecto es
233
+ // puramente declarativo (marcar la intención); las 6 observaciones
234
+ // reales (drenar jobs, recomputar gate, exigir árbol limpio,
235
+ // terminar la generación propia) las hace `Supervisor.tick()` en su
236
+ // propio loop, nunca este consumo transaccional (que no puede
237
+ // esperar terminaciones de proceso reales). Idempotente sin
238
+ // importar cuántas veces se re-emita, y jamás pisa un `frozen` ya
239
+ // persistido (R6.4: un track congelado no vuelve a mutar).
240
+ if (s.frozen === undefined)
241
+ s.freezeRequested = true;
242
+ (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied' });
243
+ return;
244
+ }
245
+ if (env.kind === 'track-finalize-request') {
246
+ // R7.2 (Task 12): a diferencia de `track-freeze-request` (cross-
247
+ // journal, un track por cada uno), esta request es PLAN-scoped — el
248
+ // propio controller del PLAN (corriendo `post-implementation-qa`
249
+ // sobre el HEAD ya mergeado de todos los tracks) la emite a SU PROPIO
250
+ // journal para reportar "ya corregí hallazgos y comiteé, este es mi
251
+ // HEAD limpio". El efecto acá es puramente declarativo (autoreporte);
252
+ // `reconcileTracks`/`runRequestGlobalQa` (watch/tracks.ts) es quien
253
+ // hace las dos observaciones reales independientes (HEAD real
254
+ // coincide, árbol/índice limpios) antes de aceptarlo como evidencia
255
+ // de `global-qa-pass` — jamás se confía ciegamente en el autoreporte
256
+ // (mismo criterio fail-closed que el freeze de un track, Task 10).
257
+ const p = env.payload;
258
+ if (typeof p.qaHeadSha !== 'string' || p.qaHeadSha.length === 0)
259
+ throw new Error('track-finalize-request requiere qaHeadSha');
260
+ s.qaFinalizeRequested = { headSha: p.qaHeadSha, at: now() };
261
+ (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied' });
262
+ return;
263
+ }
97
264
  if (env.kind === 'register-entity') {
98
265
  const p = env.payload;
99
266
  if (p.entity === 'task') {
@@ -119,11 +286,32 @@ function applyRequestToState(s, env, digest) {
119
286
  }
120
287
  if (p.entity === 'cycle-plan') {
121
288
  const items = verificationItems(p.items, 'register --entity cycle-plan items');
122
- // Idempotente por creacion-unica (Task 4): un re-registro defensivo
123
- // (ej. tras crash sin memoria de si ya se registro) NUNCA debe pisar
124
- // un plan ya existente y perder los `satisfiedBy` ya enlazados.
125
- if (s.cycleVerificationPlan.length === 0) {
126
- s.cycleVerificationPlan = items;
289
+ // R7.2 (Task 12): append transaccional por id — reemplaza el guard
290
+ // "solo si vacío" (Fix4/R1.4b original). Un re-registro defensivo
291
+ // (ej. tras crash sin memoria de si ya se registro) NUNCA pisa un
292
+ // item ya existente ni pierde su `satisfiedBy`, pero SÍ agrega los
293
+ // ids nuevos (ej. `track-integration:*` que `registerTrackIntegrationItems`
294
+ // fue agregando en paralelo) — el plan de ciclo crece por unión,
295
+ // nunca se reemplaza entero.
296
+ applyCyclePlanAppend(s, items);
297
+ (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied' });
298
+ return;
299
+ }
300
+ if (p.entity === 'track-integration') {
301
+ // R7/C3/C4 (Task 12): el contrato canónico ÚNICO del job de
302
+ // integración final de la cohorte — creación-única (como
303
+ // cycle-plan antes de R7.2): jamás se pisa un contrato ya
304
+ // registrado, porque dos contratos distintos en el tiempo
305
+ // producirían dos idempotencyKeys distintas para "la" integración
306
+ // final (violaría C4 — un solo job canónico).
307
+ const argv = stringArray(p.argv, 'register --entity track-integration argv');
308
+ if (argv.length === 0)
309
+ throw new Error('register --entity track-integration requiere argv no vacio');
310
+ const paths = p.paths === undefined ? [] : stringArray(p.paths, 'register --entity track-integration paths');
311
+ if (typeof p.planDigest !== 'string')
312
+ throw new Error('register --entity track-integration requiere planDigest (string)');
313
+ if (s.trackIntegration === undefined) {
314
+ s.trackIntegration = { argv, paths, planDigest: p.planDigest };
127
315
  }
128
316
  (0, requests_1.applyOutcome)(s, { ...base, outcome: 'applied' });
129
317
  return;
@@ -317,7 +505,7 @@ function consumePendingRequests(repoRoot, branch, activeToken) {
317
505
  // nunca deja un estado parcialmente contaminado que luego no se
318
506
  // pueda serializar o reintentar.
319
507
  const candidate = structuredClone(s);
320
- applyRequestToState(candidate, env, digest);
508
+ applyRequestToState(candidate, env, digest, repoRoot);
321
509
  s = candidate;
322
510
  applied++;
323
511
  stateTouched = true;
@@ -6,6 +6,8 @@ const init_1 = require("./init");
6
6
  const supervisor_1 = require("./supervisor");
7
7
  const process_1 = require("../../core/journal/process");
8
8
  const adapter_1 = require("../../core/journal/adapter");
9
+ const context_1 = require("../../core/tracks/context");
10
+ const concurrency_1 = require("../../core/tracks/concurrency");
9
11
  function currentBranch(cwd) {
10
12
  // stdio explicito (ver EXEC_STDIO en journal/process.ts): evita el relay
11
13
  // default de execFileSync del stderr de git hacia el stderr del llamante,
@@ -29,9 +31,20 @@ function registerWatchCommand(program) {
29
31
  .option('--provider <p>', adapter_1.WATCH_PROVIDERS.join(' | '), 'codex')
30
32
  .option('--heartbeat-timeout <min>', 'minutos de silencio de heartbeat', '5')
31
33
  .option('--activity-window <min>', 'minutos extra sin actividad de proceso', '10')
34
+ .option('--max-parallel <n>', 'tope de tracks ACTIVE simultáneos (default: derivado del benchmark empaquetado)')
32
35
  .action(async (opts) => {
33
36
  const repo = process.cwd();
34
37
  const branch = currentBranch(repo);
38
+ // R9.4: sin descriptor de track, no-op (modo plan de siempre); con
39
+ // descriptor presente que no autentica, rechaza ANTES de tocar el
40
+ // journal — mismo patron de salida que el resto de `awm job`.
41
+ try {
42
+ (0, context_1.resolveCommandContext)(repo, branch);
43
+ }
44
+ catch (e) {
45
+ process.stderr.write(`${e.message}\n`);
46
+ process.exit(1);
47
+ }
35
48
  if (opts.init) {
36
49
  const out = (0, init_1.initWatch)(repo, branch);
37
50
  process.stdout.write(`journal inicializado para ${branch}; verificadores requeridos: ${JSON.stringify(out.requiredVerifiers)}\n`);
@@ -51,6 +64,7 @@ function registerWatchCommand(program) {
51
64
  provider: opts.provider,
52
65
  heartbeatTimeoutMs: minutes('--heartbeat-timeout', opts.heartbeatTimeout),
53
66
  activityWindowMs: minutes('--activity-window', opts.activityWindow),
67
+ maxParallelTracks: opts.maxParallel !== undefined ? (0, concurrency_1.parseMaxParallel)(opts.maxParallel) : (0, concurrency_1.loadDefaultParallelism)(),
54
68
  };
55
69
  process.stdout.write(`awm watch: supervisor activo (${cfg.provider}) — Ctrl-C para terminar\n`);
56
70
  await (0, supervisor_1.runSupervisorLoop)(repo, branch, cfg);
@@ -169,12 +169,31 @@ function collectAndReconcile(repoRoot, branch, opts = {}) {
169
169
  }
170
170
  return { advanced, decisions: out.decisions };
171
171
  }
172
+ /** `dispatch:false` (R5.2/R6.3, Task 10 — "el supervisor del track deja de
173
+ * despachar" durante un freeze): el drenaje de jobs YA vivos sigue intacto
174
+ * (`collectAndReconcile` siempre corre, y un job `spawn-intent` sin claim
175
+ * aun se reintenta con el MISMO intent — la reconciliacion de
176
+ * `retry-same-intent` de abajo NUNCA se gatea por `dispatch`, corre
177
+ * siempre, freeze o no: es exactamente lo que permite que un job cuyo
178
+ * wrapper murio justo al pedirse el freeze siga avanzando hacia un estado
179
+ * terminal en vez de quedar varado en `spawn-intent` para siempre — sin
180
+ * esto, `liveJobs` jamas llegaria a cero y `attemptFreeze` jamas
181
+ * convergeria, R6.3), pero jamás se arranca un job GENUINAMENTE nuevo
182
+ * (`spawnPendingWrappers` para `received`) mientras el freeze está en
183
+ * curso — eso sí queda gateado por `dispatch`. Default `true`: ningún
184
+ * caller existente (loop normal del supervisor) cambia de comportamiento. */
172
185
  function runnerTick(repoRoot, branch, spawner, opts = {}) {
186
+ const dispatch = opts.dispatch ?? true;
173
187
  const collected = collectAndReconcile(repoRoot, branch, opts);
174
188
  const r = (0, store_1.readJournal)(repoRoot, branch);
175
189
  if (r.corrupt || r.state === null)
176
190
  throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
177
191
  const logs = (0, paths_1.logsDir)(repoRoot, branch);
192
+ // Reintento de intent YA vivo (nunca trabajo nuevo): un job en
193
+ // `spawn-intent` sin claim es, por definicion, un intent que YA se
194
+ // habia decidido antes del freeze — reemitir el MISMO spawn es
195
+ // progresar/drenar ese job, no arrancar algo nuevo. Corre SIEMPRE,
196
+ // incluso con `dispatch:false`.
178
197
  for (const decision of collected.decisions) {
179
198
  if (decision.action !== 'retry-same-intent')
180
199
  continue;
@@ -186,6 +205,6 @@ function runnerTick(repoRoot, branch, spawner, opts = {}) {
186
205
  }
187
206
  catch { /* el mismo intent durable se reintentara en otro tick */ }
188
207
  }
189
- const spawned = spawnPendingWrappers(repoRoot, branch, spawner);
208
+ const spawned = dispatch ? spawnPendingWrappers(repoRoot, branch, spawner) : 0;
190
209
  return { spawned, advanced: collected.advanced, decisions: collected.decisions };
191
210
  }