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
|
@@ -124,20 +124,24 @@ function skillsGlobalCheck(dir, owners, integrity, renderer, artifacts) {
|
|
|
124
124
|
// generados NO: quedan con el contenido de la version anterior hasta que alguien
|
|
125
125
|
// corra `awm sync`, y hasta ahora nada lo decia.
|
|
126
126
|
const stale = staleRendered(dir, artifacts);
|
|
127
|
+
const names = stale.map((r) => path_1.default.basename(r.targetPath));
|
|
127
128
|
return {
|
|
128
129
|
id: 'skills.global',
|
|
129
130
|
state: stale.length > 0 ? 'stale' : 'supported',
|
|
130
131
|
target: dir,
|
|
131
132
|
owners: owners.length > 1 ? owners : undefined,
|
|
132
133
|
detail: stale.length > 0
|
|
133
|
-
? `${stale.length} rendered file(s) no longer match the installed registry (${
|
|
134
|
+
? `${stale.length} rendered file(s) no longer match the installed registry (${names.slice(0, 3).join(', ')})`
|
|
135
|
+
: undefined,
|
|
136
|
+
// El remedio depende del ALCANCE, y se midio cual funciona en cada uno:
|
|
137
|
+
// `awm update` reconcilia los artefactos de maquina, `awm sync` los que el
|
|
138
|
+
// profile del proyecto declara. Ofrecer el equivocado seria mandar al usuario
|
|
139
|
+
// a un comando que corre limpio sin cambiar nada — el defecto que ya tuvo
|
|
140
|
+
// `open-hooks-trust` (D-010), y que la primera version de ESTE chequeo
|
|
141
|
+
// repitio: ofrecia `reinstall-bundle` porque dos mediciones mias estaban mal.
|
|
142
|
+
remediationCode: stale.length > 0
|
|
143
|
+
? (stale.some((r) => r.scope === 'local') ? 'awm-sync' : 'awm-update')
|
|
134
144
|
: undefined,
|
|
135
|
-
// `reinstall-bundle`, y NO `awm-sync` ni `awm-init`: se midio cual de los
|
|
136
|
-
// cuatro comandos refresca de verdad un renderizado viejo, y solo `awm add`
|
|
137
|
-
// lo hace. `init` y `sync` son idempotentes por presencia —ven el archivo y
|
|
138
|
-
// no lo tocan— y `update` refresca el registry, no lo derivado de el. Ofrecer
|
|
139
|
-
// un remedio que corre limpio sin cambiar nada es peor que no ofrecer ninguno.
|
|
140
|
-
remediationCode: stale.length > 0 ? 'reinstall-bundle' : undefined,
|
|
141
145
|
};
|
|
142
146
|
}
|
|
143
147
|
const shared = owners.length > 1;
|
|
@@ -213,8 +217,7 @@ function staleRendered(dir, records) {
|
|
|
213
217
|
// corresponden. Aca "no puedo comparar" no se convierte en "esta viejo".
|
|
214
218
|
return false;
|
|
215
219
|
}
|
|
216
|
-
})
|
|
217
|
-
.map((r) => path_1.default.basename(r.targetPath));
|
|
220
|
+
});
|
|
218
221
|
}
|
|
219
222
|
function corruptRendered(dir, files, renderer) {
|
|
220
223
|
const marker = (0, registry_1.rendererIntegrityMarker)(renderer);
|
|
@@ -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
|
+
}
|
package/dist/src/core/profile.js
CHANGED
|
@@ -154,5 +154,19 @@ function ensureSkillsGitignored(root, agents) {
|
|
|
154
154
|
* project extensions and stay out of `.awm/profile.json`.
|
|
155
155
|
*/
|
|
156
156
|
function shouldRecordExtension(bundleScope, effective) {
|
|
157
|
-
|
|
157
|
+
// El criterio es el ALCANCE EFECTIVO, no el del bundle.
|
|
158
|
+
//
|
|
159
|
+
// Antes exigia ademas `bundleScope === 'project'`, asi que un bundle baseline
|
|
160
|
+
// instalado explicitamente con `--scope local` no quedaba registrado en ningun lado:
|
|
161
|
+
// `awm sync` reconcilia lo que declara el profile, y esos artefactos no estaban ahi.
|
|
162
|
+
// Resultado: nadie los refrescaba nunca. `update` cubre lo global y `sync` lo del
|
|
163
|
+
// profile; eso caia en el medio.
|
|
164
|
+
//
|
|
165
|
+
// Y no es un caso raro: `awm add dev --scope local` es exactamente lo que el playbook
|
|
166
|
+
// de aceptacion (AG-03) le pide correr a cualquiera que verifique un proveedor.
|
|
167
|
+
//
|
|
168
|
+
// Si alguien pidio artefactos de proyecto, el profile lo dice — que es lo que hace
|
|
169
|
+
// que un companero que clona el repo y corre `awm sync` obtenga lo mismo.
|
|
170
|
+
void bundleScope;
|
|
171
|
+
return effective === 'local';
|
|
158
172
|
}
|