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.
- 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/diagnostics/provider-checks.js +12 -9
- 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/profile.js +15 -1
- 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/core/update-check.js +5 -1
- 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/diagnostics/provider-tier.test.js +3 -3
- 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/profile.test.js +9 -2
- 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/core/update-check.test.js +34 -3
- 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/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
|
-
|
|
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
|
-
|
|
75
|
-
linkSatisfies(s,
|
|
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
|
-
...(
|
|
148
|
+
...(satisfiesIds.length > 0 ? { satisfies: satisfiesIds } : {}),
|
|
90
149
|
};
|
|
91
150
|
s.jobs[jobId] = job;
|
|
92
|
-
|
|
93
|
-
linkSatisfies(s,
|
|
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
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
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
|
}
|