agentic-workflow-manager 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/src/commands/job/exec-wrapper.js +136 -0
  2. package/dist/src/commands/job/export.js +94 -0
  3. package/dist/src/commands/job/gate.js +118 -0
  4. package/dist/src/commands/job/heartbeat.js +15 -0
  5. package/dist/src/commands/job/index.js +246 -0
  6. package/dist/src/commands/job/query.js +37 -0
  7. package/dist/src/commands/job/reap.js +24 -0
  8. package/dist/src/commands/job/reconcile.js +112 -0
  9. package/dist/src/commands/job/request.js +27 -0
  10. package/dist/src/commands/watch/apply.js +352 -0
  11. package/dist/src/commands/watch/generations.js +249 -0
  12. package/dist/src/commands/watch/index.js +49 -0
  13. package/dist/src/commands/watch/init.js +72 -0
  14. package/dist/src/commands/watch/lock.js +89 -0
  15. package/dist/src/commands/watch/runner.js +191 -0
  16. package/dist/src/commands/watch/supervisor.js +266 -0
  17. package/dist/src/core/atomic-file.js +31 -0
  18. package/dist/src/core/export/pack.js +7 -1
  19. package/dist/src/core/journal/adapter.js +27 -0
  20. package/dist/src/core/journal/fingerprint.js +80 -0
  21. package/dist/src/core/journal/paths.js +56 -0
  22. package/dist/src/core/journal/process.js +284 -0
  23. package/dist/src/core/journal/redact.js +142 -0
  24. package/dist/src/core/journal/requests.js +132 -0
  25. package/dist/src/core/journal/store.js +107 -0
  26. package/dist/src/core/journal/types.js +165 -0
  27. package/dist/src/index.js +4 -0
  28. package/dist/tests/commands/job/exec-wrapper.test.js +85 -0
  29. package/dist/tests/commands/job/export.test.js +76 -0
  30. package/dist/tests/commands/job/gate-reconcile.test.js +297 -0
  31. package/dist/tests/commands/job/reap-cli.test.js +101 -0
  32. package/dist/tests/commands/job/verbs.test.js +56 -0
  33. package/dist/tests/commands/job/verdict-determinism.test.js +138 -0
  34. package/dist/tests/commands/watch/apply.test.js +397 -0
  35. package/dist/tests/commands/watch/e2e-crash.test.js +157 -0
  36. package/dist/tests/commands/watch/generations.test.js +115 -0
  37. package/dist/tests/commands/watch/integration.test.js +124 -0
  38. package/dist/tests/commands/watch/lock.test.js +60 -0
  39. package/dist/tests/commands/watch/runner.test.js +239 -0
  40. package/dist/tests/commands/watch/supervisor-loop.test.js +203 -0
  41. package/dist/tests/commands/watch/watch-init.test.js +43 -0
  42. package/dist/tests/core/atomic-file-durable.test.js +42 -0
  43. package/dist/tests/core/journal/adapter.test.js +27 -0
  44. package/dist/tests/core/journal/fingerprint.test.js +164 -0
  45. package/dist/tests/core/journal/paths.test.js +35 -0
  46. package/dist/tests/core/journal/process.test.js +213 -0
  47. package/dist/tests/core/journal/redact.test.js +59 -0
  48. package/dist/tests/core/journal/requests.test.js +134 -0
  49. package/dist/tests/core/journal/store.test.js +88 -0
  50. package/dist/tests/core/journal/types.test.js +78 -0
  51. package/dist/tests/structural/exec-invocation-explicit-stdio.test.js +94 -0
  52. package/package.json +1 -1
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Supervisor = exports.DEFAULT_SUPERVISOR_CONFIG = void 0;
4
+ exports.runSupervisorLoop = runSupervisorLoop;
5
+ // Loop foreground (R4.4/R4.5): tick = apply -> collect/spawn -> stall -> gate.
6
+ // COMPLETE exige gate verde (que exige cero vivos): drenaje ANTES de declarar.
7
+ // Custodia BLOCKED: el loop sigue, el lock NO se libera, nada se mata.
8
+ const store_1 = require("../../core/journal/store");
9
+ const fingerprint_1 = require("../../core/journal/fingerprint");
10
+ const adapter_1 = require("../../core/journal/adapter");
11
+ const process_1 = require("../../core/journal/process");
12
+ const gate_1 = require("../job/gate");
13
+ const lock_1 = require("./lock");
14
+ const apply_1 = require("./apply");
15
+ const runner_1 = require("./runner");
16
+ const generations_1 = require("./generations");
17
+ exports.DEFAULT_SUPERVISOR_CONFIG = {
18
+ provider: 'codex',
19
+ heartbeatTimeoutMs: 5 * 60000, // R4.2 default 5 min
20
+ activityWindowMs: 10 * 60000, // R4.2 ventana propia adicional
21
+ tickMs: 5000,
22
+ termGraceMs: 30000, // R4.2b flush 30 s
23
+ killGraceMs: 5000,
24
+ reconcileGraceMs: 10000,
25
+ jobStallObservationMs: 5 * 60000, // R3.5 default: mismo orden de magnitud que heartbeatTimeoutMs, concern independiente
26
+ };
27
+ const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
28
+ class Supervisor {
29
+ repoRoot;
30
+ branch;
31
+ cfg;
32
+ spawner;
33
+ backoff = new generations_1.Backoff();
34
+ relaunchNotBefore = 0;
35
+ lastActivity = null;
36
+ lastGenerationToken = null;
37
+ constructor(repoRoot, branch, cfg, spawner) {
38
+ this.repoRoot = repoRoot;
39
+ this.branch = branch;
40
+ this.cfg = cfg;
41
+ this.spawner = spawner;
42
+ }
43
+ fingerprintNow = (argv, paths, cwd) => {
44
+ try {
45
+ return (0, fingerprint_1.computeFingerprint)(this.repoRoot, argv, paths, cwd).fingerprint;
46
+ }
47
+ catch {
48
+ return null;
49
+ } // no recomputable => el gate NO certifica (fail-closed)
50
+ };
51
+ ensureController(resumePrompt) {
52
+ if (Date.now() < this.relaunchNotBefore)
53
+ return 'deferred';
54
+ if (this.backoff.exhausted()) {
55
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'tope de intentos de launch/relaunch por hora alcanzado (R4.3)');
56
+ return 'custody';
57
+ }
58
+ try {
59
+ (0, generations_1.ensureControllerGeneration)(this.repoRoot, this.branch, this.cfg.provider, resumePrompt, this.spawner, this.cfg.reconcileGraceMs);
60
+ return 'ok';
61
+ }
62
+ catch (error) {
63
+ this.backoff.recordRelaunch();
64
+ const delayMs = this.backoff.nextMs();
65
+ this.relaunchNotBefore = Date.now() + delayMs;
66
+ (0, store_1.appendEvent)(this.repoRoot, this.branch, {
67
+ kind: 'controller-launch-failed', detail: error.message, retryAfterMs: delayMs,
68
+ });
69
+ if (this.backoff.exhausted()) {
70
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'tope de intentos de launch/relaunch por hora alcanzado (R4.3)');
71
+ return 'custody';
72
+ }
73
+ return 'deferred';
74
+ }
75
+ }
76
+ async tick() {
77
+ const before = (0, store_1.readJournal)(this.repoRoot, this.branch);
78
+ if (before.corrupt || before.state === null)
79
+ throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
80
+ (0, lock_1.verifyBranchInvariant)(this.repoRoot, before.state.branch);
81
+ if (before.state.cycle.status === 'COMPLETE')
82
+ return 'complete';
83
+ const pending = before.state?.cycle.nextAction;
84
+ const resumePrompt = pending !== undefined ? `el next_action ${pending.actionId} del journal` : 'el plan del ciclo desde el journal';
85
+ if (this.ensureController(resumePrompt) === 'custody')
86
+ return 'custody';
87
+ const r0 = (0, store_1.readJournal)(this.repoRoot, this.branch);
88
+ if (r0.corrupt || r0.state === null)
89
+ throw new Error('journal corrupto: el supervisor no opera sobre corrupcion (R1.6)');
90
+ const gen = (0, generations_1.activeGeneration)(r0.state);
91
+ (0, apply_1.consumePendingRequests)(this.repoRoot, this.branch, gen?.token ?? null);
92
+ const afterRequests = (0, store_1.readJournal)(this.repoRoot, this.branch);
93
+ if (afterRequests.state !== null && (0, generations_1.activeGeneration)(afterRequests.state) === undefined
94
+ && afterRequests.state.generations.length > 0 && afterRequests.state.cycle.status === 'IN_PROGRESS') {
95
+ (0, generations_1.beginGeneration)(this.repoRoot, this.branch);
96
+ if (this.ensureController(resumePrompt) === 'custody')
97
+ return 'custody';
98
+ }
99
+ (0, runner_1.runnerTick)(this.repoRoot, this.branch, this.spawner, { reconcileGraceMs: this.cfg.reconcileGraceMs, stallObservationMs: this.cfg.jobStallObservationMs });
100
+ const custody = await this.superviseController();
101
+ if (custody)
102
+ return 'custody';
103
+ const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
104
+ const gate = (0, gate_1.computeGate)(r.state, r.corrupt, this.fingerprintNow);
105
+ const liveJobs = r.state === null ? 1 : Object.values(r.state.jobs).filter((j) => LIVE.includes(j.executionState)).length;
106
+ if (gate.pass && liveJobs === 0) { // gate verde YA implica cero vivos; doble cinturon (R4.5)
107
+ const s = r.state;
108
+ for (const generation of s.generations) {
109
+ for (const ref of [generation.processRef, generation.wrapperRef]) {
110
+ // Los tests pueden ejecutar el wrapper in-process; nunca
111
+ // enviar una senial al propio supervisor.
112
+ if (ref?.pid === process.pid)
113
+ continue;
114
+ if (ref === undefined || (0, process_1.groupIsGone)(ref.processGroup))
115
+ continue;
116
+ const confirmed = await (0, process_1.terminateGroupConfirmed)(ref, { termGraceMs: this.cfg.termGraceMs, killGraceMs: this.cfg.killGraceMs });
117
+ if (!confirmed) {
118
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, `no se pudo terminar con identidad confirmada la generacion ${generation.n} antes de COMPLETE`);
119
+ return 'custody';
120
+ }
121
+ }
122
+ generation.state = 'terminated';
123
+ }
124
+ s.cycle.status = 'COMPLETE';
125
+ s.cycle.completedAt = new Date().toISOString();
126
+ (0, store_1.writeJournal)(this.repoRoot, this.branch, s);
127
+ (0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'cycle-complete' });
128
+ return 'complete';
129
+ }
130
+ return 'continue';
131
+ }
132
+ /** true => custodia (el caller NO libera lock ni sale). */
133
+ async superviseController() {
134
+ const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
135
+ if (r.corrupt || r.state === null)
136
+ throw new Error('journal corrupto (R1.6)');
137
+ const s = r.state;
138
+ const gen = (0, generations_1.activeGeneration)(s);
139
+ if (gen?.processRef === undefined)
140
+ return false; // sin controlador propio: nada que supervisar
141
+ if (this.lastGenerationToken !== gen.token) {
142
+ this.lastGenerationToken = gen.token;
143
+ this.lastActivity = null;
144
+ }
145
+ const adapter = (0, adapter_1.adapterFor)(this.cfg.provider);
146
+ const heartbeatAgeMs = Date.now() - Date.parse(s.controllerHeartbeatAt ?? gen.launchedAt);
147
+ const snap = adapter.activity(gen.processRef);
148
+ const key = JSON.stringify(snap);
149
+ if (this.lastActivity === null || this.lastActivity.key !== key) {
150
+ this.lastActivity = { key, changedAt: Date.now() };
151
+ }
152
+ const activityFrozenMs = Date.now() - this.lastActivity.changedAt;
153
+ const decision = (0, generations_1.decideStall)({ heartbeatAgeMs, activityFrozenMs, safeToReplace: adapter.safeToReplace(gen.processRef) }, { heartbeatTimeoutMs: this.cfg.heartbeatTimeoutMs, activityWindowMs: this.cfg.activityWindowMs });
154
+ if (decision === 'healthy') {
155
+ this.backoff.reset();
156
+ return false;
157
+ }
158
+ if (decision === 'suspected-stall-observe') {
159
+ if (gen.state !== 'controller-suspected-stall') {
160
+ gen.state = 'controller-suspected-stall'; // SOLO observacion (R4.2)
161
+ (0, store_1.writeJournal)(this.repoRoot, this.branch, s);
162
+ (0, store_1.appendEvent)(this.repoRoot, this.branch, { kind: 'controller-suspected-stall', n: gen.n });
163
+ }
164
+ return false;
165
+ }
166
+ if (decision === 'custody-blocked') {
167
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'doble senial de stall sin safeToReplace positivo del adapter (R4.2b)');
168
+ return true;
169
+ }
170
+ // resolve-generation
171
+ const resolved = await (0, generations_1.resolveGeneration)(this.repoRoot, this.branch, adapter, { termGraceMs: this.cfg.termGraceMs, killGraceMs: this.cfg.killGraceMs });
172
+ if (resolved === 'custody-blocked')
173
+ return true;
174
+ if (this.backoff.exhausted()) {
175
+ (0, generations_1.enterCustody)(this.repoRoot, this.branch, 'tope de relanzamientos por hora alcanzado (R4.3)');
176
+ return true;
177
+ }
178
+ if (Date.now() < this.relaunchNotBefore)
179
+ return false; // esperando backoff, auditando
180
+ (0, generations_1.beginGeneration)(this.repoRoot, this.branch);
181
+ const nextAction = (0, store_1.readJournal)(this.repoRoot, this.branch).state.cycle.nextAction;
182
+ const prompt = nextAction !== undefined ? `el next_action ${nextAction.actionId} del journal` : 'el plan del ciclo desde el journal';
183
+ const launched = this.ensureController(prompt);
184
+ if (launched === 'custody')
185
+ return true;
186
+ if (launched === 'ok') {
187
+ this.backoff.recordRelaunch();
188
+ this.relaunchNotBefore = Date.now() + this.backoff.nextMs();
189
+ }
190
+ return false;
191
+ }
192
+ }
193
+ exports.Supervisor = Supervisor;
194
+ /** Foreground, visible, terminable (R2.4): sin daemons. SIGINT/SIGTERM libera
195
+ * el lock y sale; COMPLETE => auto-exit liberando lock y terminando la
196
+ * generacion propia (cero huerfanos). */
197
+ async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)()) {
198
+ const r = (0, store_1.readJournal)(repoRoot, branch);
199
+ if (r.corrupt || r.state === null)
200
+ throw new Error('journal ausente o corrupto: corre `awm watch --init` primero');
201
+ (0, lock_1.verifyBranchInvariant)(repoRoot, r.state.branch);
202
+ if (r.state.cycle.status === 'COMPLETE')
203
+ return;
204
+ const handle = (0, lock_1.acquireLock)(repoRoot);
205
+ let shutdownRequested = false;
206
+ let wakeSleep = null;
207
+ let safeToRelease = false;
208
+ const onSignal = () => { shutdownRequested = true; wakeSleep?.(); };
209
+ process.on('SIGINT', onSignal);
210
+ process.on('SIGTERM', onSignal);
211
+ const sup = new Supervisor(repoRoot, branch, cfg, spawner);
212
+ try {
213
+ const s0 = (0, store_1.readJournal)(repoRoot, branch).state;
214
+ if ((0, generations_1.activeGeneration)(s0) === undefined) {
215
+ (0, generations_1.beginGeneration)(repoRoot, branch);
216
+ }
217
+ for (;;) {
218
+ if (shutdownRequested)
219
+ break;
220
+ const out = await sup.tick();
221
+ if (out === 'complete')
222
+ break;
223
+ // 'custody': NO liberar lock, NO salir — seguir auditando (R4.5)
224
+ await new Promise((resolve) => {
225
+ let settled = false;
226
+ const finish = () => { if (!settled) {
227
+ settled = true;
228
+ clearTimeout(timer);
229
+ wakeSleep = null;
230
+ resolve();
231
+ } };
232
+ const timer = setTimeout(finish, cfg.tickMs);
233
+ wakeSleep = finish;
234
+ });
235
+ }
236
+ // En shutdown explicito, drenar ownership ANTES de liberar el lock. En
237
+ // COMPLETE, tick() ya hizo exactamente esta confirmacion antes de
238
+ // persistir el estado terminal; el loop solo verifica el invariante.
239
+ (0, generations_1.collectControllerGeneration)(repoRoot, branch);
240
+ const sEnd = (0, store_1.readJournal)(repoRoot, branch).state;
241
+ for (const g of sEnd.generations) {
242
+ if ((0, generations_1.controllerGenerationHasUnresolvedClaim)(repoRoot, branch, g)) {
243
+ throw new Error(`ownership retenido: generacion ${g.n} tiene claim sin identidad ni resultado`);
244
+ }
245
+ for (const ref of [g.processRef, g.wrapperRef]) {
246
+ if (ref?.pid === process.pid)
247
+ continue;
248
+ if (ref === undefined || (0, process_1.groupIsGone)(ref.processGroup))
249
+ continue;
250
+ const confirmed = await (0, process_1.terminateGroupConfirmed)(ref, { termGraceMs: cfg.termGraceMs, killGraceMs: cfg.killGraceMs });
251
+ if (!confirmed)
252
+ throw new Error(`ownership retenido: generacion ${g.n} sigue viva o su identidad es indemostrable`);
253
+ }
254
+ g.state = 'terminated';
255
+ }
256
+ if (shutdownRequested)
257
+ (0, store_1.writeJournal)(repoRoot, branch, sEnd);
258
+ safeToRelease = true;
259
+ }
260
+ finally {
261
+ process.removeListener('SIGINT', onSignal);
262
+ process.removeListener('SIGTERM', onSignal);
263
+ if (safeToRelease)
264
+ (0, lock_1.releaseLock)(repoRoot, handle);
265
+ }
266
+ }
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.writeFileAtomic = writeFileAtomic;
7
+ exports.fsyncDirSync = fsyncDirSync;
8
+ exports.writeFileAtomicDurable = writeFileAtomicDurable;
7
9
  const fs_1 = __importDefault(require("fs"));
8
10
  const path_1 = __importDefault(require("path"));
9
11
  const crypto_1 = __importDefault(require("crypto"));
@@ -70,3 +72,32 @@ function writeFileAtomic(file, content, mode = 0o644) {
70
72
  throw error;
71
73
  }
72
74
  }
75
+ /** fsync del directorio contenedor: garantiza que una ENTRADA creada/renombrada
76
+ * sobrevive un crash del OS. Falla LANZANDO — la durabilidad de la transición
77
+ * es parte del contrato, nunca un best-effort silencioso (design R1.2,
78
+ * bloqueador 4 de la review del plan). */
79
+ function fsyncDirSync(dir) {
80
+ let dirFd;
81
+ try {
82
+ dirFd = fs_1.default.openSync(dir, 'r');
83
+ fs_1.default.fsyncSync(dirFd);
84
+ }
85
+ catch (error) {
86
+ throw new Error(`fsync de directorio fallo para ${dir}: ${error.message}`);
87
+ }
88
+ finally {
89
+ if (dirFd !== undefined) {
90
+ try {
91
+ fs_1.default.closeSync(dirFd);
92
+ }
93
+ catch {
94
+ // best-effort SOLO el close: el fsync ya ocurrio o ya lanzo.
95
+ }
96
+ }
97
+ }
98
+ }
99
+ /** writeFileAtomic + fsync del directorio contenedor tras el rename. */
100
+ function writeFileAtomicDurable(file, content, mode = 0o644) {
101
+ writeFileAtomic(file, content, mode);
102
+ fsyncDirSync(path_1.default.dirname(file));
103
+ }
@@ -13,6 +13,12 @@ exports.packSkill = packSkill;
13
13
  const fs_1 = __importDefault(require("fs"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  const child_process_1 = require("child_process");
16
+ // stdio explicito: sin esto, spawnSync relayea el stderr del `zip` hijo hacia
17
+ // el stderr del proceso llamante (default `inheritStderr` de Node cuando no
18
+ // se pasa `stdio`) — si ese fd fuera un pipe roto/destruido, el relay mismo
19
+ // dispara un EPIPE no catcheable que crashea al llamante (mismo bug de raiz
20
+ // que motivo EXEC_STDIO en core/journal/process.ts).
21
+ const EXEC_STDIO = ['ignore', 'pipe', 'pipe'];
16
22
  /** Refuses symlinks anywhere in the tree — copying/zipping them could dereference
17
23
  * into content outside the registry (info-leak) or embed a broken/unexpected
18
24
  * link for the recipient. Exported artifacts are plain files only. */
@@ -28,7 +34,7 @@ function assertNoSymlinks(dir) {
28
34
  }
29
35
  /** Capa 1: binario `zip` del sistema. ENOENT → missing (capa 2: carpeta). */
30
36
  const defaultZip = (cwd, zipName, folderName) => {
31
- const r = (0, child_process_1.spawnSync)('zip', ['-r', '-q', zipName, folderName], { cwd });
37
+ const r = (0, child_process_1.spawnSync)('zip', ['-r', '-q', zipName, folderName], { cwd, stdio: EXEC_STDIO });
32
38
  if (r.error && r.error.code === 'ENOENT') {
33
39
  return { ok: false, missing: true };
34
40
  }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.adapterFor = adapterFor;
4
+ const process_1 = require("./process");
5
+ const RESUME_PROMPT_PREFIX = 'Sos el orquestador SDD de este repo. Corre `awm job reconcile` y ejecuta ';
6
+ function baseSafeToReplace(ref) {
7
+ return (0, process_1.refIsAlive)(ref) ? 'indeterminate' : 'safe';
8
+ }
9
+ const codexAdapter = {
10
+ provider: 'codex',
11
+ launchArgv: (resumePrompt) => ['codex', 'exec', `${RESUME_PROMPT_PREFIX}${resumePrompt}`],
12
+ activity: process_1.activitySnapshot,
13
+ safeToReplace: baseSafeToReplace,
14
+ };
15
+ const claudeAdapter = {
16
+ provider: 'claude-code',
17
+ launchArgv: (resumePrompt) => ['claude', '-p', `${RESUME_PROMPT_PREFIX}${resumePrompt}`],
18
+ activity: process_1.activitySnapshot,
19
+ safeToReplace: baseSafeToReplace,
20
+ };
21
+ function adapterFor(provider) {
22
+ if (provider === 'codex')
23
+ return codexAdapter;
24
+ if (provider === 'claude-code')
25
+ return claudeAdapter;
26
+ throw new Error(`provider desconocido: ${provider} (validos: codex, claude-code)`);
27
+ }
@@ -0,0 +1,80 @@
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.resolveWorkingDirectory = resolveWorkingDirectory;
7
+ exports.computeFingerprint = computeFingerprint;
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const child_process_1 = require("child_process");
12
+ const process_1 = require("./process");
13
+ function sha(parts) {
14
+ return crypto_1.default.createHash('sha256').update(parts.join('\0')).digest('hex');
15
+ }
16
+ // Sin ceiling de maxBuffer: repos grandes (`ls-files` / `ls-files --stage` en
17
+ // miles de archivos) pueden superar el default de Node (1MB) y abortar con ENOBUFS.
18
+ // stdio explicito (ver EXEC_STDIO en process.ts): sin esto, execFileSync relayea
19
+ // el stderr de git hacia el stderr DEL SUPERVISOR — si ese fd es un pipe roto, el
20
+ // relay dispara un EPIPE no catcheable que crashea el proceso ENTERO (este helper
21
+ // backea computeFingerprint, invocado en CADA tick via FingerprintNow/computeGate).
22
+ function git(cwd, args) {
23
+ return (0, child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8', maxBuffer: Infinity, stdio: process_1.EXEC_STDIO });
24
+ }
25
+ function resolveWorkingDirectory(repoRoot, cwdRel) {
26
+ if (typeof cwdRel !== 'string' || cwdRel.length === 0)
27
+ throw new Error('cwd relativo requerido');
28
+ const relative = path_1.default.normalize(cwdRel);
29
+ if (path_1.default.isAbsolute(relative) || relative === '..' || relative.startsWith(`..${path_1.default.sep}`)) {
30
+ throw new Error(`cwd fuera del repo: ${JSON.stringify(cwdRel)}`);
31
+ }
32
+ const root = fs_1.default.realpathSync(repoRoot);
33
+ let cursor = root;
34
+ for (const segment of relative.split(path_1.default.sep).filter((part) => part !== '.')) {
35
+ cursor = path_1.default.join(cursor, segment);
36
+ const stat = fs_1.default.lstatSync(cursor);
37
+ if (stat.isSymbolicLink())
38
+ throw new Error(`cwd contiene symlink no permitido: ${JSON.stringify(cwdRel)}`);
39
+ }
40
+ const absolute = fs_1.default.realpathSync(path_1.default.join(root, relative));
41
+ if (absolute !== root && !absolute.startsWith(`${root}${path_1.default.sep}`))
42
+ throw new Error(`cwd fuera del repo: ${JSON.stringify(cwdRel)}`);
43
+ if (!fs_1.default.statSync(absolute).isDirectory())
44
+ throw new Error(`cwd no es directorio: ${JSON.stringify(cwdRel)}`);
45
+ return { relative: relative.split(path_1.default.sep).join('/'), absolute };
46
+ }
47
+ /** El journal jamás invalida evidencia: .awm/ queda fuera de toda expansión. */
48
+ const EXCLUDE_JOURNAL = ':(exclude).awm';
49
+ /** Componentes SEPARADOS (design R3.4, bloqueador 7 de la review):
50
+ * argv exacto + cwd relativo REAL + HEAD + índice real (`ls-files --stage`
51
+ * hasheado) + digest de contenido por archivo tracked/untracked/deleted. */
52
+ function computeFingerprint(repoRoot, argv, pathGlobs, cwdRel) {
53
+ if (!Array.isArray(argv) || argv.length === 0)
54
+ throw new Error('argv vacio');
55
+ const cwdNorm = resolveWorkingDirectory(repoRoot, cwdRel).relative;
56
+ const commandDigest = sha(argv);
57
+ const head = git(repoRoot, ['rev-parse', 'HEAD']).trim();
58
+ const pathspecs = pathGlobs.length > 0 ? pathGlobs : ['.'];
59
+ // Índice REAL: modos + blobs + stages + paths — un cambio staged-only con
60
+ // worktree idéntico produce salida distinta aquí. Sin -z a propósito: esta
61
+ // salida se hashea completa como texto opaco, nunca se separa en paths
62
+ // individuales, así que el quoting de core.quotePath es inofensivo aquí
63
+ // (a diferencia de expandedPaths abajo, cuyos paths SÍ se re-extraen para
64
+ // pasarlos a `hash-object` — por eso ese caso sí necesita -z).
65
+ const indexRaw = git(repoRoot, ['ls-files', '--stage', '--', ...pathspecs, EXCLUDE_JOURNAL]);
66
+ const indexDigest = sha([indexRaw]);
67
+ const expandedPaths = git(repoRoot, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', ...pathspecs, EXCLUDE_JOURNAL])
68
+ .split('\0').filter(Boolean).sort();
69
+ const perFile = expandedPaths.map((p) => {
70
+ try {
71
+ return `${p}:${git(repoRoot, ['hash-object', '--', p]).trim()}`;
72
+ }
73
+ catch {
74
+ return `${p}:deleted`; // listado pero ilegible/borrado del worktree: cuenta como cambio
75
+ }
76
+ });
77
+ const declaredPaths = pathGlobs.length > 0 ? pathGlobs : ['.'];
78
+ const fingerprint = sha([commandDigest, `cwd:${cwdNorm}`, `paths:${JSON.stringify(declaredPaths)}`, `head:${head}`, `index:${indexDigest}`, ...perFile]);
79
+ return { fingerprint, commandDigest, expandedPaths };
80
+ }
@@ -0,0 +1,56 @@
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.branchSlug = branchSlug;
7
+ exports.journalDir = journalDir;
8
+ exports.supervisorLockPath = supervisorLockPath;
9
+ exports.statePath = statePath;
10
+ exports.requestsDir = requestsDir;
11
+ exports.acksDir = acksDir;
12
+ exports.logsDir = logsDir;
13
+ exports.eventsPath = eventsPath;
14
+ exports.exportDir = exportDir;
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const path_1 = __importDefault(require("path"));
17
+ function branchSlug(branch) {
18
+ if (!branch || branch === '.' || branch.includes('..')) {
19
+ throw new Error(`branch inválida para slug: ${JSON.stringify(branch)}`);
20
+ }
21
+ // Escapa PRIMERO el propio caracter de escape (_), despues / y \ — así
22
+ // ningún guion bajo literal sobrevive sin escapar, lo que hace la
23
+ // codificación biyectiva: dos ramas distintas nunca pueden colisionar
24
+ // (bloqueador encontrado en code-quality review de Task 3).
25
+ return branch
26
+ .replace(/_/g, '_5F')
27
+ .replace(/\//g, '_2F')
28
+ .replace(/\\/g, '_5C');
29
+ }
30
+ function journalDir(repoRoot, branch) {
31
+ return path_1.default.join(repoRoot, '.awm', 'journal', branchSlug(branch));
32
+ }
33
+ /** Lock único por worktree FÍSICO: clavado por realpath, fuera del dir de rama
34
+ * (design R1.1, bloqueante v5-5: dos ramas jamás toman locks distintos sobre
35
+ * el mismo árbol). */
36
+ function supervisorLockPath(repoRoot) {
37
+ return path_1.default.join(fs_1.default.realpathSync(repoRoot), '.awm', 'journal', 'supervisor.lock');
38
+ }
39
+ function statePath(repoRoot, branch) {
40
+ return path_1.default.join(journalDir(repoRoot, branch), 'state.json');
41
+ }
42
+ function requestsDir(repoRoot, branch) {
43
+ return path_1.default.join(journalDir(repoRoot, branch), 'requests');
44
+ }
45
+ function acksDir(repoRoot, branch) {
46
+ return path_1.default.join(journalDir(repoRoot, branch), 'acks');
47
+ }
48
+ function logsDir(repoRoot, branch) {
49
+ return path_1.default.join(journalDir(repoRoot, branch), 'logs');
50
+ }
51
+ function eventsPath(repoRoot, branch) {
52
+ return path_1.default.join(journalDir(repoRoot, branch), 'events.jsonl');
53
+ }
54
+ function exportDir(repoRoot, branch) {
55
+ return path_1.default.join(journalDir(repoRoot, branch), 'export');
56
+ }