agentic-workflow-manager 6.2.1 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WATCH_PROVIDERS = void 0;
|
|
3
|
+
exports.CONTROLLER_ARGV_ENV = exports.WATCH_PROVIDERS = void 0;
|
|
4
4
|
exports.isWatchProvider = isWatchProvider;
|
|
5
5
|
exports.adapterFor = adapterFor;
|
|
6
6
|
const process_1 = require("./process");
|
|
@@ -27,10 +27,50 @@ exports.WATCH_PROVIDERS = ['codex', 'claude-code'];
|
|
|
27
27
|
function isWatchProvider(value) {
|
|
28
28
|
return typeof value === 'string' && exports.WATCH_PROVIDERS.includes(value);
|
|
29
29
|
}
|
|
30
|
+
/** Nombre del env var que redirige el LANZAMIENTO del controller a un comando propio.
|
|
31
|
+
* Exportado para que tests y documentacion nunca reescriban el literal. */
|
|
32
|
+
exports.CONTROLLER_ARGV_ENV = 'AWM_CONTROLLER_ARGV';
|
|
33
|
+
/**
|
|
34
|
+
* El supervisor declara no conocer providers: conoce el contrato `ControllerAdapter`. Esa
|
|
35
|
+
* afirmacion era INFALSIFICABLE desde afuera mientras los unicos adapters posibles fueran
|
|
36
|
+
* `codex` y `claude-code` — no habia forma de plantar un controller ajeno y comprobar que
|
|
37
|
+
* el supervisor se comporta igual.
|
|
38
|
+
*
|
|
39
|
+
* `AWM_CONTROLLER_ARGV` cierra eso: un array JSON de strings que reemplaza el argv de
|
|
40
|
+
* lanzamiento, conservando TODO lo demas del adapter elegido (actividad, `safeToReplace`,
|
|
41
|
+
* fencing, custodia). El prompt de la generacion se agrega como ultimo argumento, igual que
|
|
42
|
+
* hacen los adapters nativos, para que el controller reciba su token por la misma via.
|
|
43
|
+
*
|
|
44
|
+
* Array JSON y jamas una linea de shell: es la misma doctrina que el argv de integracion de
|
|
45
|
+
* los tracks (C4) — una string interpretada por un shell convierte un nombre de archivo en
|
|
46
|
+
* un operador. Se valida en el borde: cualquier cosa que no sea un array no vacio de strings
|
|
47
|
+
* no vacios es un error explicito, nunca un fallback silencioso al provider nativo (un
|
|
48
|
+
* override mal escrito que "funciona igual" lanzaria el agente real sin que nadie lo note).
|
|
49
|
+
*/
|
|
50
|
+
function controllerArgvOverride() {
|
|
51
|
+
const raw = process.env[exports.CONTROLLER_ARGV_ENV]?.trim();
|
|
52
|
+
if (raw === undefined || raw.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error(`${exports.CONTROLLER_ARGV_ENV} debe ser un array JSON de strings, no una linea de shell: ${raw}`);
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((x) => typeof x !== 'string' || x.length === 0)) {
|
|
62
|
+
throw new Error(`${exports.CONTROLLER_ARGV_ENV} debe ser un array JSON no vacio de strings no vacios: ${raw}`);
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
30
66
|
function adapterFor(provider) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
67
|
+
const base = provider === 'codex' ? codexAdapter
|
|
68
|
+
: provider === 'claude-code' ? claudeAdapter
|
|
69
|
+
: null;
|
|
70
|
+
if (base === null)
|
|
71
|
+
throw new Error(`provider desconocido: ${provider} (validos: ${exports.WATCH_PROVIDERS.join(', ')})`);
|
|
72
|
+
const override = controllerArgvOverride();
|
|
73
|
+
if (override === null)
|
|
74
|
+
return base;
|
|
75
|
+
return { ...base, launchArgv: (resumePrompt) => [...override, resumePrompt] };
|
|
36
76
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.branchSlug = branchSlug;
|
|
7
7
|
exports.journalDir = journalDir;
|
|
8
8
|
exports.supervisorLockPath = supervisorLockPath;
|
|
9
|
+
exports.integrationLockPath = integrationLockPath;
|
|
9
10
|
exports.statePath = statePath;
|
|
10
11
|
exports.requestsDir = requestsDir;
|
|
11
12
|
exports.acksDir = acksDir;
|
|
@@ -36,6 +37,14 @@ function journalDir(repoRoot, branch) {
|
|
|
36
37
|
function supervisorLockPath(repoRoot) {
|
|
37
38
|
return path_1.default.join(fs_1.default.realpathSync(repoRoot), '.awm', 'journal', 'supervisor.lock');
|
|
38
39
|
}
|
|
40
|
+
/** Lock de integración (R5.8/R5.9/C7, Task 10): único por PLAN físico, fuera
|
|
41
|
+
* de cualquier dir de rama — mismo criterio de `supervisorLockPath` (clavado
|
|
42
|
+
* por realpath). Se adquiere ANTES del primer join de una cohorte y retiene
|
|
43
|
+
* ownership mientras la generación del plan está pausada y ningún controller
|
|
44
|
+
* administrado corre. */
|
|
45
|
+
function integrationLockPath(repoRoot) {
|
|
46
|
+
return path_1.default.join(fs_1.default.realpathSync(repoRoot), '.awm', 'journal', 'integration.lock');
|
|
47
|
+
}
|
|
39
48
|
function statePath(repoRoot, branch) {
|
|
40
49
|
return path_1.default.join(journalDir(repoRoot, branch), 'state.json');
|
|
41
50
|
}
|
|
@@ -381,6 +381,17 @@ function refIsAlive(ref) {
|
|
|
381
381
|
if ((0, paths_1.isWindowsNative)()) {
|
|
382
382
|
if (!pidExistsNative(ref.pid))
|
|
383
383
|
return false;
|
|
384
|
+
// DELIBERADO (Ronda 3, ver el test 'refIsAlive en win32 usa process.kill(pid,0),
|
|
385
|
+
// NUNCA ps/pgrep'): esta rama NO consulta identidad — ni ps/pgrep ni WMI. La razon
|
|
386
|
+
// es un bug real de CI: `ps`/`pgrep` emulados por MSYS son ciegos a procesos nativos
|
|
387
|
+
// y devuelven exit 1 para pids genuinamente vivos, y confiar en ellos declaraba
|
|
388
|
+
// muertes falsas. `process.kill(pid, 0)` es la unica observacion no ciega.
|
|
389
|
+
//
|
|
390
|
+
// CONSECUENCIA CONOCIDA Y ACEPTADA: un pid RECICLADO por un proceso ajeno se reporta
|
|
391
|
+
// como "nuestro proceso sigue vivo". Eso sobre-reporta vida, que es la direccion
|
|
392
|
+
// SEGURA — jamas autoriza matar nada ajeno (de eso se encarga `win32LeaderReused`
|
|
393
|
+
// antes de cualquier senial) — a costa de que un teardown pueda quedarse esperando a
|
|
394
|
+
// un proceso que nunca fue nuestro. Se prefiere la espera sobre el riesgo.
|
|
384
395
|
return ref.processGroup === ref.pid;
|
|
385
396
|
}
|
|
386
397
|
try {
|
|
@@ -530,10 +541,83 @@ async function terminateGroupConfirmed(ref, opts) {
|
|
|
530
541
|
catch { /* idem */ }
|
|
531
542
|
return waitUntilGone(opts.killGraceMs);
|
|
532
543
|
}
|
|
544
|
+
/** true <=> el slot de PID que identifica al grupo (`ref.processGroup` — para
|
|
545
|
+
* todo caller real de esta funcion coincide con `ref.pid`, el lider que el
|
|
546
|
+
* propio caller spawneo: `captureRefFor`/`spawnStructured` siempre fijan
|
|
547
|
+
* `processGroup` al pgid real de ese pid recien creado, que en un detached
|
|
548
|
+
* spawn -setsid- es su PROPIO pid) esta hoy ocupado por un proceso VIVO cuya
|
|
549
|
+
* identidad (startTime + digest de `ps args`) NO coincide con la que
|
|
550
|
+
* capturamos para `ref`. Cubre reuso de PGID (post-review, hallazgo de
|
|
551
|
+
* revision de Task 13): el SO reciclo ese numero de pid hacia un proceso
|
|
552
|
+
* nuevo y no relacionado que se convirtio en lider de su propio grupo
|
|
553
|
+
* (`setpgid`/`setsid`) con el MISMO numero — sin este chequeo, la senial de
|
|
554
|
+
* `terminatePreviouslyOwnedGroup` de mas abajo alcanzaria a ese proceso
|
|
555
|
+
* ajeno. Una ausencia total o un zombie en ese slot NUNCA cuentan como
|
|
556
|
+
* reuso: o es nuestro propio lider aun sin reap (zombie), o el slot esta
|
|
557
|
+
* libre y cualquier miembro remanente del pgid es necesariamente
|
|
558
|
+
* descendiente nuestro (el SO no puede reutilizar el NUMERO de pid del
|
|
559
|
+
* lider para un proceso nuevo mientras ese pid siga ocupado, vivo o
|
|
560
|
+
* zombie). Fail-closed ante cualquier duda (R2.1/R4.8): un `ps` que no pudo
|
|
561
|
+
* correr, o cualquier campo indeterminado, se trata como reuso — nunca
|
|
562
|
+
* como autorizacion para senializar. */
|
|
563
|
+
/** Hermano win32 de `groupLeaderReused` (R4.8). Mismo contrato, distinta fuente de verdad:
|
|
564
|
+
* POSIX pregunta a `ps` por lstart/pgid/args; win32 pregunta a WMI por creationDate y
|
|
565
|
+
* commandLine, exactamente los dos campos con los que `captureRefFor` construyo el `ref`
|
|
566
|
+
* en esta plataforma. `true` <=> el PID esta ocupado por algo que NO es nuestro proceso.
|
|
567
|
+
*
|
|
568
|
+
* Fail-closed identico al POSIX: WMI indisponible o ilegible (`null`) NO prueba que el
|
|
569
|
+
* proceso sea nuestro, asi que se trata como ajeno y la senial jamas sale. Un `ref`
|
|
570
|
+
* degradado a `'unknown'` (WMI no respondio al capturarlo) tampoco autoriza nada — es la
|
|
571
|
+
* consecuencia correcta de no haber podido probar identidad al momento del spawn.
|
|
572
|
+
* `'absent'` es el unico caso que devuelve `false`: no hay nadie ahi a quien confundir. */
|
|
573
|
+
function win32LeaderReused(ref) {
|
|
574
|
+
const info = win32ProcessInfo(ref.processGroup);
|
|
575
|
+
if (info === 'absent')
|
|
576
|
+
return false;
|
|
577
|
+
if (info === null)
|
|
578
|
+
return true;
|
|
579
|
+
if (info.creationDate !== ref.startTime)
|
|
580
|
+
return true;
|
|
581
|
+
return identityDigest(info.commandLine, ref.spawnNonce, ref.argvDigest) !== ref.psArgsDigest;
|
|
582
|
+
}
|
|
583
|
+
function groupLeaderReused(ref) {
|
|
584
|
+
try {
|
|
585
|
+
const stat = psField(ref.processGroup, 'stat');
|
|
586
|
+
if (stat === null || stat.startsWith('Z'))
|
|
587
|
+
return false; // ausente o zombie: nunca un impostor vivo
|
|
588
|
+
const start = psField(ref.processGroup, 'lstart');
|
|
589
|
+
if (start === null || start !== ref.startTime)
|
|
590
|
+
return true;
|
|
591
|
+
const argsDig = psArgsDigestOf(ref.processGroup, ref.spawnNonce, ref.argvDigest);
|
|
592
|
+
if (argsDig === null || argsDig !== ref.psArgsDigest)
|
|
593
|
+
return true;
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return true; // sin evidencia, jamas autorizar la senial (R2.1)
|
|
598
|
+
}
|
|
599
|
+
}
|
|
533
600
|
/** Drena un grupo cuya propiedad fue capturada por el caller mientras el
|
|
534
601
|
* lider aun estaba vivo. Se usa inmediatamente tras el exit del lider para
|
|
535
602
|
* eliminar descendientes remanentes; el PGID no puede reutilizarse mientras
|
|
536
|
-
* esos miembros sigan presentes.
|
|
603
|
+
* esos miembros sigan presentes.
|
|
604
|
+
*
|
|
605
|
+
* Post-review (hallazgo de revision de Task 13): a diferencia de
|
|
606
|
+
* `terminateGroupConfirmed`, esta funcion se usa PRECISAMENTE cuando el lider
|
|
607
|
+
* ya se espera muerto — por eso no puede reusar `refIsAlive` (exige lider
|
|
608
|
+
* vivo) como gate de identidad. `groupLeaderReused` cubre el mismo riesgo
|
|
609
|
+
* (R4.8: jamas senializar sin identidad verificada) sin asumir que el lider
|
|
610
|
+
* sigue vivo. Antes de este fix, esta funcion confiaba en que el CALLER
|
|
611
|
+
* hubiera verificado identidad momentos antes: `exec-wrapper.ts` lo hace por
|
|
612
|
+
* construccion (llama esto en una ventana de milisegundos justo tras
|
|
613
|
+
* observar el exit del propio hijo), y el caller de Task 13
|
|
614
|
+
* (`stopOwnSupervisor` en `tracks.ts`) tambien, indirectamente — la capa de
|
|
615
|
+
* arriba (`gatherTeardownObservation`) ya recalcula `refIsAlive` en la MISMA
|
|
616
|
+
* tick antes de siquiera elegir la decision `stop-own-supervisor`. Pero esa
|
|
617
|
+
* garantia era implicita y externa al primitivo: un caller futuro (o un
|
|
618
|
+
* cambio en esa capa de arriba) podia perderla sin que este archivo lo
|
|
619
|
+
* notara. Con el chequeo ACA DENTRO, el primitivo deja de depender de la
|
|
620
|
+
* disciplina del caller — se auto-defiende, igual que `terminateGroupConfirmed`. */
|
|
537
621
|
async function terminatePreviouslyOwnedGroup(ref, opts) {
|
|
538
622
|
const waitUntilGone = async (maxMs) => {
|
|
539
623
|
const deadline = Date.now() + maxMs;
|
|
@@ -546,13 +630,29 @@ async function terminatePreviouslyOwnedGroup(ref, opts) {
|
|
|
546
630
|
};
|
|
547
631
|
if (groupIsGone(ref.processGroup))
|
|
548
632
|
return true;
|
|
633
|
+
// Windows PRIMERO: sale sin tocar la logica de grupos, que es semantica POSIX.
|
|
634
|
+
// El guard `groupLeaderReused` de abajo razona sobre PGIDs y no aplica aca — pero el
|
|
635
|
+
// PELIGRO que evita si aplica, y durante un tiempo win32 no tuvo ninguna proteccion
|
|
636
|
+
// equivalente: `groupIsGone` en esta plataforma solo pregunta si el PID existe
|
|
637
|
+
// (`pidExistsNative`), sin verificar identidad, asi que un PID reciclado por un proceso
|
|
638
|
+
// AJENO vivo se leia como "nuestro supervisor sigue vivo" y `killTreeWindows` lo mataba.
|
|
639
|
+
// Eso es exactamente el `kill(pid)` crudo que R4.8 prohibe. `win32LeaderReused` es el
|
|
640
|
+
// hermano que faltaba, con el mismo criterio fail-closed: sin prueba positiva de que el
|
|
641
|
+
// proceso es NUESTRO, jamas se le manda una senial.
|
|
549
642
|
if ((0, paths_1.isWindowsNative)()) {
|
|
643
|
+
if (win32LeaderReused(ref))
|
|
644
|
+
return false;
|
|
550
645
|
killTreeWindows(ref.pid);
|
|
551
646
|
if (await waitUntilGone(opts.termGraceMs))
|
|
552
647
|
return true;
|
|
553
648
|
killTreeWindows(ref.pid);
|
|
554
649
|
return waitUntilGone(opts.killGraceMs);
|
|
555
650
|
}
|
|
651
|
+
// Un PGID cuyo slot de lider fue reciclado hacia un proceso vivo ajeno
|
|
652
|
+
// NUNCA se confunde con el nuestro (mismo criterio que `refIsAlive`
|
|
653
|
+
// aplica en `terminateGroupConfirmed`, adaptado a un lider ya-muerto).
|
|
654
|
+
if (groupLeaderReused(ref))
|
|
655
|
+
return false;
|
|
556
656
|
try {
|
|
557
657
|
process.kill(-ref.processGroup, 'SIGTERM');
|
|
558
658
|
}
|
|
@@ -65,7 +65,11 @@ function emitRequest(repoRoot, branch, env) {
|
|
|
65
65
|
(0, atomic_file_1.fsyncDirSync)(dir); // la ENTRADA renombrada tambien debe sobrevivir un crash (R1.3)
|
|
66
66
|
return { requestId, idempotencyKey: env.idempotencyKey, payloadDigest: digestOf(payload), file: final };
|
|
67
67
|
}
|
|
68
|
-
const KNOWN_KINDS = [
|
|
68
|
+
const KNOWN_KINDS = [
|
|
69
|
+
'job-request', 'register-entity', 'controller-heartbeat', 'verdict',
|
|
70
|
+
'track-prepare-request', 'track-freeze-request', 'track-join-request',
|
|
71
|
+
'track-teardown-request', 'track-finalize-request',
|
|
72
|
+
];
|
|
69
73
|
function isRecord(x) {
|
|
70
74
|
return typeof x === 'object' && x !== null && !Array.isArray(x);
|
|
71
75
|
}
|
|
@@ -8,12 +8,17 @@ exports.readJournal = readJournal;
|
|
|
8
8
|
exports.writeJournal = writeJournal;
|
|
9
9
|
exports.appendEvent = appendEvent;
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
12
|
const atomic_file_1 = require("../atomic-file");
|
|
12
13
|
const types_1 = require("./types");
|
|
13
14
|
const paths_1 = require("./paths");
|
|
14
|
-
/** Schema 1 evoluciono de forma aditiva durante R1. Normalizamos solamente
|
|
15
|
+
/** Schema 1 evoluciono de forma aditiva durante R1/R5. Normalizamos solamente
|
|
15
16
|
* campos que antes no existian; evidencia legacy queda deliberadamente con
|
|
16
|
-
* fingerprint vacio para que el gate la considere stale, nunca certificada.
|
|
17
|
+
* fingerprint vacio para que el gate la considere stale, nunca certificada.
|
|
18
|
+
* `tracks`/`trackContext` NO se normalizan por ausencia: son opcionales
|
|
19
|
+
* (solo el journal de un plan-con-tracks o de un track individual los
|
|
20
|
+
* lleva), asi que faltar es una forma legitima, no legacy — no se les
|
|
21
|
+
* inventa un default para un journal que jamas los tuvo (R9.2). */
|
|
17
22
|
function normalizeSchemaOne(value) {
|
|
18
23
|
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
19
24
|
return value;
|
|
@@ -24,12 +29,39 @@ function normalizeSchemaOne(value) {
|
|
|
24
29
|
parsed.requestProblems = [];
|
|
25
30
|
if (parsed.custodyDecisions === undefined)
|
|
26
31
|
parsed.custodyDecisions = [];
|
|
32
|
+
// R9.3: journals legacy pre-R5 no tenian journalId. Se materializa
|
|
33
|
+
// DETERMINISTICAMENTE desde (branch, cycle.startedAt) para que dos
|
|
34
|
+
// lecturas del MISMO snapshot legacy nunca produzcan identidades
|
|
35
|
+
// distintas (a diferencia de emptyState, que usa randomUUID solo para
|
|
36
|
+
// journals genuinamente nuevos). Se persiste en la siguiente escritura
|
|
37
|
+
// CAS normal — la lectura por si sola sigue siendo read-only.
|
|
38
|
+
if (parsed.journalId === undefined) {
|
|
39
|
+
const branch = typeof parsed.branch === 'string' ? parsed.branch : '';
|
|
40
|
+
const cycle = typeof parsed.cycle === 'object' && parsed.cycle !== null
|
|
41
|
+
? parsed.cycle : {};
|
|
42
|
+
const startedAt = typeof cycle.startedAt === 'string' ? cycle.startedAt : '';
|
|
43
|
+
parsed.journalId = `legacy-${crypto_1.default.createHash('sha256')
|
|
44
|
+
.update(`${branch}\0${startedAt}`).digest('hex').slice(0, 32)}`;
|
|
45
|
+
}
|
|
27
46
|
if (typeof parsed.cycle === 'object' && parsed.cycle !== null && !Array.isArray(parsed.cycle)) {
|
|
28
47
|
const cycle = parsed.cycle;
|
|
29
48
|
if (cycle.status === 'IN_PROGRESS' && cycle.nextAction === undefined) {
|
|
30
49
|
cycle.nextAction = { actionId: 'bootstrap-cycle', type: 'plan-cycle', target: 'cycle', preconditions: [], attempt: 0, state: 'pending' };
|
|
31
50
|
}
|
|
32
51
|
}
|
|
52
|
+
// R7 Task 12: `Job.satisfies` migró de `string` a `string[]` (varios items
|
|
53
|
+
// satisfechos por un mismo job, ej. el job canónico de integración final).
|
|
54
|
+
// Un journal legacy con `satisfies` string se normaliza a un array de un
|
|
55
|
+
// elemento — jamás se pierde el enlace ya persistido.
|
|
56
|
+
if (typeof parsed.jobs === 'object' && parsed.jobs !== null && !Array.isArray(parsed.jobs)) {
|
|
57
|
+
for (const job of Object.values(parsed.jobs)) {
|
|
58
|
+
if (typeof job !== 'object' || job === null || Array.isArray(job))
|
|
59
|
+
continue;
|
|
60
|
+
const j = job;
|
|
61
|
+
if (typeof j.satisfies === 'string')
|
|
62
|
+
j.satisfies = [j.satisfies];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
33
65
|
if (Array.isArray(parsed.verdicts)) {
|
|
34
66
|
for (const item of parsed.verdicts) {
|
|
35
67
|
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Única fuente de tipos del journal. CONSTITUTION: estados separados, nunca
|
|
3
3
|
// sobrecargados; shape validation antes de usar campos deserializados.
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
4
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
8
|
exports.GENERATION_STATES = exports.EXECUTION_STATES = void 0;
|
|
6
9
|
exports.emptyState = emptyState;
|
|
7
10
|
exports.isWellFormedState = isWellFormedState;
|
|
8
11
|
exports.isWellFormedProcessRef = isWellFormedProcessRef;
|
|
9
12
|
exports.isWellFormedJob = isWellFormedJob;
|
|
13
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
14
|
+
const types_1 = require("../tracks/types");
|
|
10
15
|
exports.EXECUTION_STATES = [
|
|
11
16
|
'received', 'spawn-intent', 'claimed', 'running',
|
|
12
17
|
'exited', 'cancel-requested', 'cancelled', 'orphaned',
|
|
@@ -14,9 +19,21 @@ exports.EXECUTION_STATES = [
|
|
|
14
19
|
exports.GENERATION_STATES = [
|
|
15
20
|
'active', 'controller-suspected-stall', 'terminated', 'superseded',
|
|
16
21
|
];
|
|
22
|
+
/** Debe permanecer sincronizado con el union type `CohortPhase` de
|
|
23
|
+
* `../tracks/types` — ese módulo no exporta un const array propio (a
|
|
24
|
+
* diferencia de TRACK_PHASES), así que replicamos la lista SOLO para shape
|
|
25
|
+
* validation en el journal (nunca para lógica de protocolo, que vive en
|
|
26
|
+
* tracks/protocol.ts). */
|
|
27
|
+
const COHORT_PHASES = [
|
|
28
|
+
'PREPARING', 'ACTIVE', 'JOINING', 'FINAL_QA',
|
|
29
|
+
'FINAL_INTEGRATION', 'FINAL_INTERLOCK', 'COMPLETE',
|
|
30
|
+
'FALLBACK_PENDING', 'SERIAL', 'BLOCKED',
|
|
31
|
+
];
|
|
32
|
+
const _cohortPhasesComplete = true;
|
|
33
|
+
const VERIFICATION_KINDS = ['test', 'lint', 'sensors', 'review', 'qa', 'interlock', 'track-integration'];
|
|
17
34
|
function emptyState(branch) {
|
|
18
35
|
return {
|
|
19
|
-
schema: 1, revision: 0, branch,
|
|
36
|
+
schema: 1, revision: 0, journalId: `j-${crypto_1.default.randomUUID()}`, branch,
|
|
20
37
|
cycle: {
|
|
21
38
|
status: 'IN_PROGRESS', startedAt: new Date().toISOString(),
|
|
22
39
|
nextAction: { actionId: 'bootstrap-cycle', type: 'plan-cycle', target: 'cycle', preconditions: [], attempt: 0, state: 'pending' },
|
|
@@ -35,6 +52,8 @@ function isWellFormedState(x) {
|
|
|
35
52
|
return false;
|
|
36
53
|
if (typeof x.revision !== 'number')
|
|
37
54
|
return false;
|
|
55
|
+
if (typeof x.journalId !== 'string' || x.journalId.length === 0)
|
|
56
|
+
return false;
|
|
38
57
|
if (typeof x.branch !== 'string')
|
|
39
58
|
return false;
|
|
40
59
|
if (!isObj(x.cycle) || !['IN_PROGRESS', 'COMPLETE', 'BLOCKED'].includes(String(x.cycle.status))
|
|
@@ -48,7 +67,7 @@ function isWellFormedState(x) {
|
|
|
48
67
|
return false;
|
|
49
68
|
if (!Array.isArray(x.cycleVerificationPlan) || !Array.isArray(x.verdicts) || !Array.isArray(x.fixes))
|
|
50
69
|
return false;
|
|
51
|
-
if (!Array.isArray(x.requiredVerifiers) || !x.requiredVerifiers.every((kind) =>
|
|
70
|
+
if (!Array.isArray(x.requiredVerifiers) || !x.requiredVerifiers.every((kind) => VERIFICATION_KINDS.includes(kind))
|
|
52
71
|
|| !Array.isArray(x.dispatches) || !x.dispatches.every(isWellFormedDispatch))
|
|
53
72
|
return false;
|
|
54
73
|
if (!isObj(x.jobs) || !Object.values(x.jobs).every(isWellFormedJob))
|
|
@@ -65,6 +84,33 @@ function isWellFormedState(x) {
|
|
|
65
84
|
return false;
|
|
66
85
|
if (!x.verdicts.every(isWellFormedVerdict) || !x.fixes.every(isWellFormedFix))
|
|
67
86
|
return false;
|
|
87
|
+
if (x.tracks !== undefined && (!Array.isArray(x.tracks) || !x.tracks.every(isWellFormedTrackRef)))
|
|
88
|
+
return false;
|
|
89
|
+
if (x.trackContext !== undefined && !isWellFormedTrackContext(x.trackContext))
|
|
90
|
+
return false;
|
|
91
|
+
if (x.cohortPhase !== undefined && !COHORT_PHASES.includes(String(x.cohortPhase)))
|
|
92
|
+
return false;
|
|
93
|
+
if (x.cohortBaseSha !== undefined && typeof x.cohortBaseSha !== 'string')
|
|
94
|
+
return false;
|
|
95
|
+
if (x.cohortPlanHeadSha !== undefined && typeof x.cohortPlanHeadSha !== 'string')
|
|
96
|
+
return false;
|
|
97
|
+
if (x.cohortFallbackReason !== undefined && typeof x.cohortFallbackReason !== 'string')
|
|
98
|
+
return false;
|
|
99
|
+
if (x.trackIntegration !== undefined && !isWellFormedTrackIntegration(x.trackIntegration))
|
|
100
|
+
return false;
|
|
101
|
+
if (x.freezeRequested !== undefined && typeof x.freezeRequested !== 'boolean')
|
|
102
|
+
return false;
|
|
103
|
+
if (x.frozen !== undefined && !(isObj(x.frozen) && typeof x.frozen.headSha === 'string' && typeof x.frozen.at === 'string'))
|
|
104
|
+
return false;
|
|
105
|
+
if (x.cohortParallelInvalidatedBy !== undefined && !strings(x.cohortParallelInvalidatedBy))
|
|
106
|
+
return false;
|
|
107
|
+
if (x.globalQaHeadSha !== undefined && typeof x.globalQaHeadSha !== 'string')
|
|
108
|
+
return false;
|
|
109
|
+
if (x.finalIntegrationJobId !== undefined && typeof x.finalIntegrationJobId !== 'string')
|
|
110
|
+
return false;
|
|
111
|
+
if (x.qaFinalizeRequested !== undefined
|
|
112
|
+
&& !(isObj(x.qaFinalizeRequested) && typeof x.qaFinalizeRequested.headSha === 'string' && typeof x.qaFinalizeRequested.at === 'string'))
|
|
113
|
+
return false;
|
|
68
114
|
return true;
|
|
69
115
|
}
|
|
70
116
|
function isWellFormedNextAction(x) {
|
|
@@ -80,7 +126,7 @@ function strings(x) {
|
|
|
80
126
|
}
|
|
81
127
|
function isWellFormedVerificationItem(x) {
|
|
82
128
|
return isObj(x) && typeof x.id === 'string'
|
|
83
|
-
&&
|
|
129
|
+
&& VERIFICATION_KINDS.includes(x.kind)
|
|
84
130
|
&& (x.satisfiedBy === undefined || typeof x.satisfiedBy === 'string');
|
|
85
131
|
}
|
|
86
132
|
function isWellFormedReviewObligation(x) {
|
|
@@ -159,7 +205,50 @@ function isWellFormedJob(x) {
|
|
|
159
205
|
&& (x.logPath === undefined || typeof x.logPath === 'string')
|
|
160
206
|
&& (x.result === undefined || (isObj(x.result) && typeof x.result.exitCode === 'number'
|
|
161
207
|
&& typeof x.result.endedAt === 'string' && typeof x.result.resultPath === 'string'))
|
|
162
|
-
&& (x.satisfies === undefined ||
|
|
208
|
+
&& (x.satisfies === undefined || strings(x.satisfies))
|
|
163
209
|
&& (x.attemptOf === undefined || typeof x.attemptOf === 'string')
|
|
164
210
|
&& exports.EXECUTION_STATES.includes(x.executionState);
|
|
165
211
|
}
|
|
212
|
+
function isWellFormedTrackContext(x) {
|
|
213
|
+
return isObj(x) && typeof x.trackId === 'string' && x.trackId.length > 0
|
|
214
|
+
&& strings(x.taskIds)
|
|
215
|
+
&& typeof x.planDigest === 'string'
|
|
216
|
+
&& typeof x.baseSha === 'string'
|
|
217
|
+
&& typeof x.planJournalId === 'string' && x.planJournalId.length > 0;
|
|
218
|
+
}
|
|
219
|
+
function isWellFormedSupervisorIntent(x) {
|
|
220
|
+
return isObj(x) && typeof x.nonce === 'string' && x.nonce.length > 0
|
|
221
|
+
&& strings(x.argv) && typeof x.claimPath === 'string';
|
|
222
|
+
}
|
|
223
|
+
function isWellFormedJoinIntent(x) {
|
|
224
|
+
return isObj(x) && typeof x.expectedPlanHeadSha === 'string' && typeof x.expectedTrackHeadSha === 'string'
|
|
225
|
+
&& x.strategy === types_1.JOIN_STRATEGY_NO_FF;
|
|
226
|
+
}
|
|
227
|
+
function isWellFormedTeardownIntent(x) {
|
|
228
|
+
return isObj(x) && typeof x.worktreePath === 'string' && typeof x.branch === 'string'
|
|
229
|
+
&& (x.supervisorNonce === undefined || typeof x.supervisorNonce === 'string');
|
|
230
|
+
}
|
|
231
|
+
/** Shape completa (R9.2/R9.7): fencingToken y readinessNonce nunca vacios —
|
|
232
|
+
* igual criterio que ProcessRef, la identidad es todo-o-nada. */
|
|
233
|
+
function isWellFormedTrackRef(x) {
|
|
234
|
+
if (!isObj(x))
|
|
235
|
+
return false;
|
|
236
|
+
return typeof x.trackId === 'string' && x.trackId.length > 0
|
|
237
|
+
&& typeof x.worktreePath === 'string' && x.worktreePath.length > 0
|
|
238
|
+
&& typeof x.branch === 'string' && x.branch.length > 0
|
|
239
|
+
&& strings(x.ownership) && strings(x.sharedResources) && strings(x.dependsOn)
|
|
240
|
+
&& typeof x.fencingToken === 'string' && x.fencingToken.length > 0
|
|
241
|
+
&& types_1.TRACK_PHASES.includes(x.phase)
|
|
242
|
+
&& typeof x.readinessNonce === 'string' && x.readinessNonce.length > 0
|
|
243
|
+
&& (x.readinessAt === undefined || typeof x.readinessAt === 'string')
|
|
244
|
+
&& (x.frozenHeadSha === undefined || typeof x.frozenHeadSha === 'string')
|
|
245
|
+
&& (x.supervisorIntent === undefined || isWellFormedSupervisorIntent(x.supervisorIntent))
|
|
246
|
+
&& (x.supervisorProcessRef === undefined || isWellFormedProcessRef(x.supervisorProcessRef))
|
|
247
|
+
&& (x.joinIntent === undefined || isWellFormedJoinIntent(x.joinIntent))
|
|
248
|
+
&& (x.teardownIntent === undefined || isWellFormedTeardownIntent(x.teardownIntent))
|
|
249
|
+
&& (x.joinedCommitSha === undefined || typeof x.joinedCommitSha === 'string')
|
|
250
|
+
&& (x.blockedReason === undefined || typeof x.blockedReason === 'string');
|
|
251
|
+
}
|
|
252
|
+
function isWellFormedTrackIntegration(x) {
|
|
253
|
+
return isObj(x) && strings(x.argv) && strings(x.paths) && typeof x.planDigest === 'string';
|
|
254
|
+
}
|
package/dist/src/core/paths.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.isWindowsNative = isWindowsNative;
|
|
|
12
12
|
exports.platformLabel = platformLabel;
|
|
13
13
|
exports.noteWindowsCaveat = noteWindowsCaveat;
|
|
14
14
|
exports.resolveOnPath = resolveOnPath;
|
|
15
|
+
exports.sameExistingPath = sameExistingPath;
|
|
15
16
|
// cli/src/core/paths.ts
|
|
16
17
|
//
|
|
17
18
|
// Single source of truth for home / AWM_HOME resolution and platform detection.
|
|
@@ -158,3 +159,53 @@ function resolveOnPath(bin) {
|
|
|
158
159
|
const entries = (process.env.PATH ?? '').split(path_1.default.delimiter).filter(Boolean);
|
|
159
160
|
return entries.some((dir) => matches(path_1.default.join(dir, bin)));
|
|
160
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* ¿Dos rutas nombran el MISMO directorio/archivo existente?
|
|
164
|
+
*
|
|
165
|
+
* Comparar `fs.realpathSync(a) !== fs.realpathSync(b)` como strings parece obvio y es
|
|
166
|
+
* incorrecto en Windows: el mismo directorio tiene más de una grafía legítima —
|
|
167
|
+
* `C:\Users\RUNNER~1\AppData\...` (nombre corto 8.3, lo que devuelve `os.tmpdir()`) y
|
|
168
|
+
* `C:/Users/runneradmin/AppData/...` (lo que devuelve `git worktree list`, con separadores
|
|
169
|
+
* POSIX) — y `realpathSync` NO reconcilia las dos. En CI eso hacía que
|
|
170
|
+
* `ownedWorktreeExists` respondiera "este worktree no es mío" sobre un worktree propio.
|
|
171
|
+
*
|
|
172
|
+
* Falla cerrado, así que no corrompía nada — pero dejaba la adopción tras crash inservible
|
|
173
|
+
* en Windows: el teardown no podía probar propiedad, los tracks quedaban `BLOCKED`, y la
|
|
174
|
+
* cohorte no llegaba nunca ni a `SERIAL` ni a `ACTIVE`. Un solo bug de comparación, quince
|
|
175
|
+
* tests en cascada.
|
|
176
|
+
*
|
|
177
|
+
* La identidad se toma del filesystem — `(dev, ino)` — que es agnóstica a separadores, a
|
|
178
|
+
* mayúsculas y a nombres cortos. Cuando el volumen no expone inode (`ino === 0`, posible en
|
|
179
|
+
* algunos filesystems de Windows) se cae a `realpathSync.native` — que sí resuelve 8.3 vía
|
|
180
|
+
* la API del SO — normalizando separadores y capitalización.
|
|
181
|
+
*
|
|
182
|
+
* Cualquier fallo de `stat` significa que al menos una no existe: `false`. La identidad
|
|
183
|
+
* nunca se afirma sin prueba.
|
|
184
|
+
*/
|
|
185
|
+
function sameExistingPath(a, b) {
|
|
186
|
+
let sa;
|
|
187
|
+
let sb;
|
|
188
|
+
try {
|
|
189
|
+
sa = fs_1.default.statSync(a);
|
|
190
|
+
sb = fs_1.default.statSync(b);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
// `ino === 0` en AMBOS lados no prueba identidad: probaría que el filesystem no la
|
|
196
|
+
// reporta. Tratarlo como match haría coincidir dos rutas cualesquiera del mismo volumen.
|
|
197
|
+
if (sa.ino !== 0 && sb.ino !== 0)
|
|
198
|
+
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
199
|
+
const canonical = (p) => {
|
|
200
|
+
try {
|
|
201
|
+
const real = fs_1.default.realpathSync.native(p).replaceAll('\\', '/');
|
|
202
|
+
return isWindowsNative() ? real.toLowerCase() : real;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const ca = canonical(a);
|
|
209
|
+
const cb = canonical(b);
|
|
210
|
+
return ca !== null && ca === cb;
|
|
211
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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.deriveDefaultParallelism = deriveDefaultParallelism;
|
|
7
|
+
exports.parseMaxParallel = parseMaxParallel;
|
|
8
|
+
exports.scheduleTracks = scheduleTracks;
|
|
9
|
+
exports.loadDefaultParallelism = loadDefaultParallelism;
|
|
10
|
+
// R10.2/R10.3: tope de paralelismo derivado de una medición real (fingerprint
|
|
11
|
+
// budget, ver docs/research/r5/benchmark-fingerprint.mjs), nunca de un número
|
|
12
|
+
// escrito a mano. Funciones puras/testeables — sin I/O aquí salvo
|
|
13
|
+
// loadDefaultParallelism, que es un loader delgado sobre el artefacto
|
|
14
|
+
// empaquetado y cae a serial (1) si no lo puede resolver (nunca crashea `awm watch`).
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
/** R10.3: el mayor N de supervisores concurrentes cuyo p95 medido no supera
|
|
18
|
+
* 1.5x el baseline serial (N=1) ni el 20% del tick — nunca N=1 si ninguna
|
|
19
|
+
* medición lo habilita (R10.2, fail-closed hacia serial). */
|
|
20
|
+
function deriveDefaultParallelism(budget) {
|
|
21
|
+
if (!Number.isInteger(budget.cpuCount) || budget.cpuCount < 1 || budget.samples.length === 0)
|
|
22
|
+
throw new Error('budget inválido');
|
|
23
|
+
const baseline = budget.samples.find((s) => s.supervisors === 1);
|
|
24
|
+
if (baseline === undefined || baseline.p95Ms <= 0)
|
|
25
|
+
throw new Error('budget sin baseline N=1');
|
|
26
|
+
return budget.samples
|
|
27
|
+
.filter((s) => Number.isInteger(s.supervisors) && s.supervisors <= budget.cpuCount
|
|
28
|
+
&& s.p95Ms <= baseline.p95Ms * 1.5 && s.p95Ms <= budget.tickMs * 0.2)
|
|
29
|
+
.reduce((max, s) => Math.max(max, s.supervisors), 1);
|
|
30
|
+
}
|
|
31
|
+
/** Parsea `--max-parallel <n>`, mismo estilo de validación que `minutes()` en
|
|
32
|
+
* watch/index.ts: entero >= 1, fail-closed en cualquier otro caso. */
|
|
33
|
+
function parseMaxParallel(raw) {
|
|
34
|
+
const n = Number(raw);
|
|
35
|
+
if (!Number.isInteger(n) || n < 1)
|
|
36
|
+
throw new Error('--max-parallel requiere un entero >= 1');
|
|
37
|
+
return n;
|
|
38
|
+
}
|
|
39
|
+
/** Todos los tracks alcanzan ARMED; solo hasta `maxParallel` (menos los ya
|
|
40
|
+
* activos) pasan a arrancar. El resto queda `waiting` — el wrapper excedente
|
|
41
|
+
* espera sin arrancar su loop de watch/fingerprint hasta que un slot libere. */
|
|
42
|
+
function scheduleTracks(tracks, active, maxParallel) {
|
|
43
|
+
if (!Number.isInteger(maxParallel) || maxParallel < 1)
|
|
44
|
+
throw new Error('maxParallel debe ser entero >= 1');
|
|
45
|
+
const capacity = Math.max(0, maxParallel - active.size);
|
|
46
|
+
const candidates = tracks.filter((t) => !active.has(t));
|
|
47
|
+
return { start: candidates.slice(0, capacity), waiting: candidates.slice(capacity) };
|
|
48
|
+
}
|
|
49
|
+
/** Busca `docs/research/r5/fingerprint-budget.json` subiendo desde `startDir`
|
|
50
|
+
* (por defecto este archivo), sin asumir una profundidad fija de directorios.
|
|
51
|
+
* `startDir` es parametrizable solo para poder testear los caminos de fallo
|
|
52
|
+
* de `loadDefaultParallelism` con un árbol de directorios controlado —
|
|
53
|
+
* ver docs/research/r5/benchmark-fingerprint.mjs para el estado real de
|
|
54
|
+
* distribución de este artefacto (hoy NO viaja en el npm publish). */
|
|
55
|
+
function findBudgetArtifact(startDir) {
|
|
56
|
+
let dir = startDir;
|
|
57
|
+
for (let i = 0; i < 8; i += 1) {
|
|
58
|
+
const candidate = path_1.default.join(dir, 'docs', 'research', 'r5', 'fingerprint-budget.json');
|
|
59
|
+
if (fs_1.default.existsSync(candidate))
|
|
60
|
+
return candidate;
|
|
61
|
+
const parent = path_1.default.dirname(dir);
|
|
62
|
+
if (parent === dir)
|
|
63
|
+
break;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** `derivedDefault` del artefacto, si se puede resolver y es válido. Si el
|
|
69
|
+
* artefacto no resuelve, tiene JSON corrupto, o `derivedDefault` falta / no
|
|
70
|
+
* es entero / es < 1, cae a serial (1) — nunca crashea `awm watch` por esto.
|
|
71
|
+
* `startDir` default = este archivo; solo se sobreescribe en tests. */
|
|
72
|
+
function loadDefaultParallelism(startDir = __dirname) {
|
|
73
|
+
const artifactPath = findBudgetArtifact(startDir);
|
|
74
|
+
if (artifactPath === null)
|
|
75
|
+
return 1;
|
|
76
|
+
try {
|
|
77
|
+
const data = JSON.parse(fs_1.default.readFileSync(artifactPath, 'utf8'));
|
|
78
|
+
if (Number.isInteger(data.derivedDefault) && data.derivedDefault >= 1)
|
|
79
|
+
return data.derivedDefault;
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
}
|