agentic-workflow-manager 9.11.3 → 9.11.4

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.computeGate = computeGate;
4
4
  exports.computeTrackGate = computeTrackGate;
5
+ const store_1 = require("../../core/journal/store");
5
6
  const LIVE = ['received', 'spawn-intent', 'claimed', 'running', 'cancel-requested'];
6
7
  function sameSelection(left, right) {
7
8
  return left.selector.kind === right.selector.kind && left.selector.id === right.selector.id && left.effort.kind === right.effort.kind && left.effort.value === right.effort.value;
@@ -149,9 +150,19 @@ function evaluateEvidence(state, fingerprintNow, scope) {
149
150
  }
150
151
  return { pass: reasons.length === 0, reasons };
151
152
  }
152
- function computeGate(state, corrupt, fingerprintNow) {
153
+ /**
154
+ * `absent` defaults to false so every existing caller keeps today's behaviour
155
+ * exactly: an unreadable journal fails closed either way. What changes is the
156
+ * REASON an absent one reports — it used to claim corruption, sending operators
157
+ * to look for damage in a healthy repository (#173). The exit code does not
158
+ * change: this interlock has no plan binding to read an execution mode from when
159
+ * the journal is missing, so it stays fail-closed and names the remedy instead.
160
+ */
161
+ function computeGate(state, corrupt, fingerprintNow, absent = false) {
153
162
  if (corrupt || state === null) {
154
- return { pass: false, reasons: [{ category: 'corrupt', detail: 'state.json corrupto o ilegible: la corrupcion jamas certifica' }] };
163
+ return absent && !corrupt
164
+ ? { pass: false, reasons: [{ category: 'absent', detail: store_1.ABSENT_JOURNAL_DETAIL }] }
165
+ : { pass: false, reasons: [{ category: 'corrupt', detail: 'state.json corrupto o ilegible: la corrupcion jamas certifica' }] };
155
166
  }
156
167
  return evaluateEvidence(state, fingerprintNow, { requireGlobalKinds: true });
157
168
  }
@@ -161,9 +172,12 @@ function computeGate(state, corrupt, fingerprintNow) {
161
172
  * ahi) y rechaza cualquier tarea que aparezca en el journal pero no en la
162
173
  * asignacion del track (R2.3) — nunca certifica por una tarea ajena que se
163
174
  * haya colado. */
164
- function computeTrackGate(state, corruptState, fingerprintNow) {
165
- if (corruptState || state === null)
166
- return { pass: false, reasons: [{ category: 'corrupt-state', detail: 'journal ausente o corrupto' }] };
175
+ function computeTrackGate(state, corruptState, fingerprintNow, absent = false) {
176
+ if (corruptState || state === null) {
177
+ return absent && !corruptState
178
+ ? { pass: false, reasons: [{ category: 'absent-state', detail: store_1.ABSENT_JOURNAL_DETAIL }] }
179
+ : { pass: false, reasons: [{ category: 'corrupt-state', detail: 'journal corrupto o ilegible' }] };
180
+ }
167
181
  if (state.trackContext === undefined)
168
182
  return { pass: false, reasons: [{ category: 'wrong-context', detail: 'gate local requiere trackContext' }] };
169
183
  const assigned = new Set(state.trackContext.taskIds);
@@ -219,8 +219,13 @@ function registerJobCommand(program) {
219
219
  const branch = branchOf(repo);
220
220
  assertAuthenticatedCwd(repo, branch);
221
221
  const r = (0, store_1.readJournal)(repo, branch);
222
- if (r.corrupt || r.state === null) {
223
- process.stdout.write(JSON.stringify({ corruptState: true }, null, 2) + '\n');
222
+ const presence = (0, store_1.journalPresence)(r);
223
+ if (presence !== 'present' || r.state === null) {
224
+ // Absence and corruption need different responses, so they are
225
+ // reported as different facts. Both still fail closed (#173).
226
+ process.stdout.write(JSON.stringify(presence === 'absent'
227
+ ? { journal: 'absent', corruptState: false, remedy: store_1.ABSENT_JOURNAL_DETAIL }
228
+ : { journal: 'corrupt', corruptState: true }, null, 2) + '\n');
224
229
  process.exit(1);
225
230
  }
226
231
  try {
@@ -257,7 +262,7 @@ function registerJobCommand(program) {
257
262
  process.exit(1);
258
263
  }
259
264
  }
260
- const g = (0, gate_1.computeGate)(r.state, r.corrupt, realFingerprintNow(repo));
265
+ const g = (0, gate_1.computeGate)(r.state, r.corrupt, realFingerprintNow(repo), r.absent);
261
266
  process.stdout.write(JSON.stringify(g, null, 2) + '\n');
262
267
  if (!g.pass)
263
268
  process.exit(1); // falla cerrado (R3.2)
@@ -270,8 +275,9 @@ function registerJobCommand(program) {
270
275
  const branch = branchOf(repo);
271
276
  assertAuthenticatedCwd(repo, branch);
272
277
  const r = (0, store_1.readJournal)(repo, branch);
273
- if (r.corrupt || r.state === null) {
274
- process.stdout.write(JSON.stringify({ corruptState: true }) + '\n');
278
+ const presence = (0, store_1.journalPresence)(r);
279
+ if (presence !== 'present' || r.state === null) {
280
+ process.stdout.write(JSON.stringify({ journal: presence, corruptState: presence === 'corrupt' }) + '\n');
275
281
  process.exit(1);
276
282
  return;
277
283
  }
@@ -287,7 +293,7 @@ function registerJobCommand(program) {
287
293
  assertAuthenticatedCwd(repo, branch);
288
294
  const r = (0, store_1.readJournal)(repo, branch);
289
295
  if (r.corrupt || r.state === null) {
290
- process.stderr.write('journal corrupto o ausente\n');
296
+ process.stderr.write(`${r.absent && !r.corrupt ? store_1.ABSENT_JOURNAL_DETAIL : 'journal corrupto o ilegible'}\n`);
291
297
  process.exit(1);
292
298
  }
293
299
  const plan = (0, reap_1.planReap)(r.state);
@@ -308,7 +314,7 @@ function registerJobCommand(program) {
308
314
  assertAuthenticatedCwd(repo, branch);
309
315
  const r = (0, store_1.readJournal)(repo, branch);
310
316
  if (r.corrupt || r.state === null) {
311
- process.stderr.write('journal corrupto\n');
317
+ process.stderr.write(`${r.absent && !r.corrupt ? store_1.ABSENT_JOURNAL_DETAIL : 'journal corrupto o ilegible'}\n`);
312
318
  process.exit(1);
313
319
  }
314
320
  let baseline = null;
@@ -6,23 +6,27 @@ exports.queryShow = queryShow;
6
6
  const store_1 = require("../../core/journal/store");
7
7
  const process_1 = require("../../core/journal/process");
8
8
  /** Fuente unica de "que hay corriendo": cruza identidad completa contra
9
- * procesos vivos. corrupt es VISIBLE, nunca descartado (R1.6). */
9
+ * procesos vivos. corrupt es VISIBLE, nunca descartado (R1.6), y un journal
10
+ * que nunca existio se reporta como `absent`, no como corrupcion (#173). */
10
11
  function queryPs(repoRoot, branch) {
11
12
  const r = (0, store_1.readJournal)(repoRoot, branch);
12
- if (r.corrupt)
13
- return { corruptState: true, jobs: [] };
13
+ const journal = (0, store_1.journalPresence)(r);
14
+ if (journal !== 'present')
15
+ return { corruptState: journal === 'corrupt', journal, jobs: [] };
14
16
  const jobs = Object.values(r.state.jobs).map((j) => ({
15
17
  id: j.id, executionState: j.executionState, observationState: j.observationState, verdict: j.verdict,
16
18
  alive: j.processRef ? (0, process_1.refIsAlive)(j.processRef) : 'sin-pid',
17
19
  }));
18
- return { corruptState: false, jobs };
20
+ return { corruptState: false, journal, jobs };
19
21
  }
20
22
  function queryList(repoRoot, branch) {
21
23
  const r = (0, store_1.readJournal)(repoRoot, branch);
22
- if (r.corrupt)
23
- return { corruptState: true, cycleStatus: null, jobs: [] };
24
+ const journal = (0, store_1.journalPresence)(r);
25
+ if (journal !== 'present')
26
+ return { corruptState: journal === 'corrupt', journal, cycleStatus: null, jobs: [] };
24
27
  return {
25
28
  corruptState: false,
29
+ journal,
26
30
  cycleStatus: r.state.cycle.status,
27
31
  jobs: Object.values(r.state.jobs).map((j) => ({
28
32
  id: j.id, executionState: j.executionState, verdict: j.verdict, argv: j.argv, satisfies: j.satisfies,
@@ -31,7 +35,8 @@ function queryList(repoRoot, branch) {
31
35
  }
32
36
  function queryShow(repoRoot, branch, jobId) {
33
37
  const r = (0, store_1.readJournal)(repoRoot, branch);
34
- if (r.corrupt)
35
- return { corruptState: true, job: null };
36
- return { corruptState: false, job: r.state.jobs[jobId] ?? null };
38
+ const journal = (0, store_1.journalPresence)(r);
39
+ if (journal !== 'present')
40
+ return { corruptState: journal === 'corrupt', journal, job: null };
41
+ return { corruptState: false, journal, job: r.state.jobs[jobId] ?? null };
37
42
  }
@@ -56,7 +56,7 @@ function aggregateTrackStatus(planRoot, plan) {
56
56
  const observed = (0, store_1.readJournal)(ref.worktreePath, ref.branch);
57
57
  tracks[ref.trackId] = {
58
58
  phase: ref.phase,
59
- gate: (0, gate_1.computeTrackGate)(observed.state, observed.corrupt, fingerprintFor(ref.worktreePath)),
59
+ gate: (0, gate_1.computeTrackGate)(observed.state, observed.corrupt, fingerprintFor(ref.worktreePath), observed.absent),
60
60
  };
61
61
  }
62
62
  return { cohort: deriveCohortPhase(refs), tracks };
@@ -464,7 +464,7 @@ class Supervisor {
464
464
  if (freezing)
465
465
  return this.attemptFreeze();
466
466
  const r = (0, store_1.readJournal)(this.repoRoot, this.branch);
467
- const gate = (0, gate_1.computeGate)(r.state, r.corrupt, this.fingerprintNow);
467
+ const gate = (0, gate_1.computeGate)(r.state, r.corrupt, this.fingerprintNow, r.absent);
468
468
  const liveJobs = r.state === null ? 1 : Object.values(r.state.jobs).filter((j) => LIVE.includes(j.executionState)).length;
469
469
  // R7/C3/C4 (Task 12): un journal de PLAN con cohorte de tracks NUNCA
470
470
  // declara `cycle.status = COMPLETE` por este camino genérico mientras
@@ -640,8 +640,11 @@ exports.Supervisor = Supervisor;
640
640
  * generacion propia (cero huerfanos). */
641
641
  async function runSupervisorLoop(repoRoot, branch, cfg, spawner = (0, runner_1.defaultWrapperSpawner)(), trackRuntime, dispatchAdmission) {
642
642
  const r = (0, store_1.readJournal)(repoRoot, branch);
643
- if (r.corrupt || r.state === null)
644
- throw new Error('journal ausente o corrupto: corre `awm watch --init` primero');
643
+ if (r.corrupt || r.state === null) {
644
+ throw new Error(r.absent && !r.corrupt
645
+ ? 'journal ausente: corre `awm watch --init --plan <plan>` primero'
646
+ : 'journal corrupto o ilegible: inspecciona .awm/journal antes de continuar');
647
+ }
645
648
  if (r.state.schema !== 2 || !r.state.planBinding) {
646
649
  throw new Error('journal legacy o sin binding compacto: `awm watch` no puede ejecutar ni despachar trabajo');
647
650
  }
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ABSENT_JOURNAL_DETAIL = void 0;
7
+ exports.journalPresence = journalPresence;
6
8
  exports.assertJournalInitPaths = assertJournalInitPaths;
7
9
  exports.initJournal = initJournal;
8
10
  exports.initBoundJournal = initBoundJournal;
@@ -18,6 +20,16 @@ const types_1 = require("./types");
18
20
  const paths_1 = require("./paths");
19
21
  const identity_1 = require("../plan/identity");
20
22
  const validate_1 = require("../plan/validate");
23
+ function journalPresence(read) {
24
+ if (read.corrupt)
25
+ return 'corrupt';
26
+ if (read.absent || read.state === null)
27
+ return 'absent';
28
+ return 'present';
29
+ }
30
+ /** What an absent journal actually needs, naming the command rather than leaving
31
+ * the operator to infer it — and saying who this interlock is for. */
32
+ exports.ABSENT_JOURNAL_DETAIL = 'journal ausente (no corrupto): el interlock desatendido exige un journal ligado — corre `awm watch --init --plan <plan>`. Un ciclo interactivo no requiere journal.';
21
33
  /** Schema 1 evoluciono de forma aditiva durante R1/R5. Normalizamos solamente
22
34
  * campos que antes no existian; evidencia legacy queda deliberadamente con
23
35
  * fingerprint vacio para que el gate la considere stale, nunca certificada.
@@ -188,28 +200,35 @@ function initBoundJournal(repoRoot, branch, binding, report) {
188
200
  }
189
201
  }
190
202
  /** Lectura corrupt-aware (R1.6): sintaxis invalida O shape invalido => corrupt:true.
191
- * Los CONSUMIDORES deciden: consultas muestran 'corrupt'; gate/reconcile bloquean. */
203
+ * Un journal que nunca se creo es `absent`, jamas `corrupt`.
204
+ * Los CONSUMIDORES deciden: consultas muestran el estado; gate/reconcile bloquean. */
192
205
  function readJournal(repoRoot, branch) {
193
206
  const sp = (0, paths_1.statePath)(repoRoot, branch);
194
207
  let raw;
195
208
  try {
209
+ // Both tree assertions RETURN on ENOENT and throw a plain Error for an
210
+ // unsafe or linked segment, so the only ENOENT that can reach here comes
211
+ // from the state file itself. An unsafe tree is therefore never reported
212
+ // as absence — the same rule `watchJournalStatus` already states.
196
213
  assertJournalTree(repoRoot, branch);
197
214
  assertJournalFileNotSymlink(sp);
198
215
  raw = fs_1.default.readFileSync(sp, 'utf8');
199
216
  }
200
- catch {
201
- return { state: null, corrupt: true };
217
+ catch (error) {
218
+ return error.code === 'ENOENT'
219
+ ? { state: null, corrupt: false, absent: true }
220
+ : { state: null, corrupt: true, absent: false };
202
221
  }
203
222
  let parsed;
204
223
  try {
205
224
  parsed = normalizeSchemaOne(JSON.parse(raw));
206
225
  }
207
226
  catch {
208
- return { state: null, corrupt: true, raw };
227
+ return { state: null, corrupt: true, absent: false, raw };
209
228
  }
210
229
  if (!(0, types_1.isWellFormedState)(parsed))
211
- return { state: null, corrupt: true, raw };
212
- return { state: parsed, corrupt: false, raw };
230
+ return { state: null, corrupt: true, absent: false, raw };
231
+ return { state: parsed, corrupt: false, absent: false, raw };
213
232
  }
214
233
  /** Escritura canonica: SOLO el supervisor la invoca (single-writer). CAS por
215
234
  * revision monotonica: el snapshot que traes debe ser el vigente. */
@@ -219,6 +238,11 @@ function writeJournal(repoRoot, branch, state) {
219
238
  const current = readJournal(repoRoot, branch);
220
239
  if (current.corrupt)
221
240
  throw new Error('journal corrupto: no se escribe sobre corrupcion (R1.6)');
241
+ // Absence used to arrive here as `corrupt`, and this guard is what kept
242
+ // initialization create-only. Naming it explicitly preserves that exactly:
243
+ // only initJournal/initBoundJournal may bring a journal into existence.
244
+ if (current.absent)
245
+ throw new Error('journal inexistente: la creacion es de initJournal/initBoundJournal, no de writeJournal');
222
246
  if (current.state !== null && current.state.revision !== state.revision) {
223
247
  throw new Error(`revision desactualizada: disco=${current.state.revision} propuesta=${state.revision}`);
224
248
  }
@@ -0,0 +1,134 @@
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
+ const fs_1 = __importDefault(require("fs"));
7
+ const os_1 = __importDefault(require("os"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const gate_1 = require("../../../src/commands/job/gate");
10
+ const query_1 = require("../../../src/commands/job/query");
11
+ const store_1 = require("../../../src/core/journal/store");
12
+ const paths_1 = require("../../../src/core/journal/paths");
13
+ const types_1 = require("../../../src/core/journal/types");
14
+ // #173. A journal that was never created is a different FACT from a damaged
15
+ // one, and needs a different response: absence is resolved by
16
+ // `awm watch --init --plan`, corruption by inspecting the file. `readJournal`
17
+ // collapsed both into `corrupt: true`, so a healthy repository with no journal
18
+ // told the operator its state file was unreadable.
19
+ //
20
+ // These are two separate cases with two separate assertions on purpose: a single
21
+ // test on "not pass" would not distinguish them, which is how this survived.
22
+ const BRANCH = 'main';
23
+ describe('an absent journal is not a corrupt one', () => {
24
+ let repo;
25
+ let previous;
26
+ beforeEach(() => {
27
+ repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-173-'));
28
+ previous = { home: process.env.HOME, awmHome: process.env.AWM_HOME };
29
+ process.env.HOME = path_1.default.join(repo, 'operator-home');
30
+ process.env.AWM_HOME = path_1.default.join(repo, 'awm-home');
31
+ });
32
+ afterEach(() => {
33
+ for (const [key, value] of [['HOME', previous.home], ['AWM_HOME', previous.awmHome]]) {
34
+ if (value === undefined)
35
+ delete process.env[key];
36
+ else
37
+ process.env[key] = value;
38
+ }
39
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
40
+ });
41
+ function writeState(body) {
42
+ fs_1.default.mkdirSync((0, paths_1.journalDir)(repo, BRANCH), { recursive: true });
43
+ fs_1.default.writeFileSync((0, paths_1.statePath)(repo, BRANCH), body);
44
+ }
45
+ it('reports a repository that never had a journal as absent, never corrupt', () => {
46
+ const read = (0, store_1.readJournal)(repo, BRANCH);
47
+ expect(read).toEqual({ state: null, corrupt: false, absent: true });
48
+ expect((0, store_1.journalPresence)(read)).toBe('absent');
49
+ });
50
+ it.each([
51
+ ['truncated JSON', '{"schema":2,"branch":"'],
52
+ ['well-formed JSON of the wrong shape', '{"nope":1}'],
53
+ ['an empty file', ''],
54
+ ])('still reports %s as corrupt, not absent', (_case, body) => {
55
+ writeState(body);
56
+ const read = (0, store_1.readJournal)(repo, BRANCH);
57
+ expect(read.corrupt).toBe(true);
58
+ expect(read.absent).toBe(false);
59
+ expect((0, store_1.journalPresence)(read)).toBe('corrupt');
60
+ });
61
+ // The adversarial case this change had to survive: `readFileSync` on a
62
+ // dangling symlink raises ENOENT, so a naive ENOENT check would report an
63
+ // attacker-placed link as an innocent absence. The symlink assertion runs
64
+ // first and throws without an errno, which is what keeps that impossible.
65
+ it('never reports a symlinked state file as absence, even a dangling one', () => {
66
+ fs_1.default.mkdirSync((0, paths_1.journalDir)(repo, BRANCH), { recursive: true });
67
+ fs_1.default.symlinkSync(path_1.default.join(repo, 'no-such-target'), (0, paths_1.statePath)(repo, BRANCH));
68
+ const read = (0, store_1.readJournal)(repo, BRANCH);
69
+ expect(read.corrupt).toBe(true);
70
+ expect(read.absent).toBe(false);
71
+ });
72
+ it('never reports a symlinked journal directory as absence', () => {
73
+ fs_1.default.mkdirSync(path_1.default.join(repo, 'elsewhere'), { recursive: true });
74
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
75
+ fs_1.default.symlinkSync(path_1.default.join(repo, 'elsewhere'), path_1.default.join(repo, '.awm', 'journal'));
76
+ const read = (0, store_1.readJournal)(repo, BRANCH);
77
+ expect(read.corrupt).toBe(true);
78
+ expect(read.absent).toBe(false);
79
+ });
80
+ // Absence used to arrive at writeJournal as `corrupt`, and that is what kept
81
+ // journal creation exclusive to initJournal/initBoundJournal. Naming it must
82
+ // not have loosened that.
83
+ it('still refuses to create a journal through the write path', () => {
84
+ expect(() => (0, store_1.writeJournal)(repo, BRANCH, (0, types_1.emptyState)(BRANCH)))
85
+ .toThrow(/journal inexistente: la creacion es de initJournal/);
86
+ expect(fs_1.default.existsSync((0, paths_1.statePath)(repo, BRANCH))).toBe(false);
87
+ });
88
+ it('reports absence through the job queries without dereferencing a null state', () => {
89
+ expect((0, query_1.queryPs)(repo, BRANCH)).toEqual({ corruptState: false, journal: 'absent', jobs: [] });
90
+ expect((0, query_1.queryList)(repo, BRANCH)).toEqual({ corruptState: false, journal: 'absent', cycleStatus: null, jobs: [] });
91
+ expect((0, query_1.queryShow)(repo, BRANCH, 'j1')).toEqual({ corruptState: false, journal: 'absent', job: null });
92
+ });
93
+ it('reports corruption through the job queries with corruptState still true', () => {
94
+ writeState('{"schema":2,"branch":"');
95
+ expect((0, query_1.queryPs)(repo, BRANCH)).toEqual({ corruptState: true, journal: 'corrupt', jobs: [] });
96
+ expect((0, query_1.queryList)(repo, BRANCH)).toEqual({ corruptState: true, journal: 'corrupt', cycleStatus: null, jobs: [] });
97
+ expect((0, query_1.queryShow)(repo, BRANCH, 'j1')).toEqual({ corruptState: true, journal: 'corrupt', job: null });
98
+ });
99
+ });
100
+ describe('the gates name absence and still fail closed', () => {
101
+ const fingerprint = () => 'fp';
102
+ it('reports an absent journal as absent, and names the command that creates one', () => {
103
+ const gate = (0, gate_1.computeGate)(null, false, fingerprint, true);
104
+ expect(gate.pass).toBe(false);
105
+ expect(gate.reasons).toEqual([{ category: 'absent', detail: store_1.ABSENT_JOURNAL_DETAIL }]);
106
+ expect(gate.reasons[0].detail).toContain('awm watch --init --plan');
107
+ // The interlock has no plan binding to read an execution mode from when
108
+ // the journal is missing, so it does not certify on absence either.
109
+ expect(gate.reasons[0].detail).toContain('interactivo');
110
+ });
111
+ it('reports a corrupt journal as corrupt even when absence is also claimed', () => {
112
+ expect((0, gate_1.computeGate)(null, true, fingerprint, true).reasons)
113
+ .toEqual([expect.objectContaining({ category: 'corrupt' })]);
114
+ });
115
+ // Proves the 60 existing call sites are unchanged: without the new argument a
116
+ // null state still fails closed exactly as before.
117
+ it('keeps every caller that omits the new argument failing closed as corrupt', () => {
118
+ expect((0, gate_1.computeGate)(null, false, fingerprint).pass).toBe(false);
119
+ expect((0, gate_1.computeGate)(null, false, fingerprint).reasons)
120
+ .toEqual([expect.objectContaining({ category: 'corrupt' })]);
121
+ });
122
+ it('distinguishes the same two facts for a track gate', () => {
123
+ expect((0, gate_1.computeTrackGate)(null, false, fingerprint, true).reasons)
124
+ .toEqual([{ category: 'absent-state', detail: store_1.ABSENT_JOURNAL_DETAIL }]);
125
+ expect((0, gate_1.computeTrackGate)(null, true, fingerprint, true).reasons)
126
+ .toEqual([expect.objectContaining({ category: 'corrupt-state' })]);
127
+ expect((0, gate_1.computeTrackGate)(null, false, fingerprint).reasons)
128
+ .toEqual([expect.objectContaining({ category: 'corrupt-state' })]);
129
+ });
130
+ it('does not report absence for a journal that is present', () => {
131
+ const gate = (0, gate_1.computeGate)((0, types_1.emptyState)('main'), false, fingerprint, false);
132
+ expect(gate.reasons).not.toContainEqual(expect.objectContaining({ category: 'absent' }));
133
+ });
134
+ });
@@ -83,7 +83,7 @@ describe('aggregateTrackStatus — agregado read-only (R9.5, R9.6)', () => {
83
83
  expect(currentPlanState.tracks[0]).not.toHaveProperty('gate');
84
84
  expect(fs_1.default.readFileSync((0, paths_1.statePath)(planRoot, planBranch), 'utf8')).toBe(planBefore);
85
85
  });
86
- test('journal de track corrupto/ausente nunca se descarta en silencio: gate rojo con corrupt-state', () => {
86
+ test('journal de track ausente nunca se descarta en silencio: gate rojo con absent-state', () => {
87
87
  // ni siquiera se inicializa el journal del segundo track: readJournal
88
88
  // sobre un directorio sin state.json => corrupt:true (R1.6).
89
89
  const missingTrackRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-missing-'));
@@ -93,7 +93,10 @@ describe('aggregateTrackStatus — agregado read-only (R9.5, R9.6)', () => {
93
93
  (0, store_1.writeJournal)(planRoot, planBranch, planState);
94
94
  const out = (0, status_1.aggregateTrackStatus)(planRoot, (0, store_1.readJournal)(planRoot, planBranch).state);
95
95
  expect(out.tracks.ghost.gate.pass).toBe(false);
96
- expect(out.tracks.ghost.gate.reasons).toEqual([{ category: 'corrupt-state', detail: expect.any(String) }]);
96
+ // #173: the track journal here is ABSENT, not damaged. The gate is
97
+ // still red — it is never discarded in silence — but it now names the
98
+ // fact it observed instead of claiming corruption.
99
+ expect(out.tracks.ghost.gate.reasons).toEqual([{ category: 'absent-state', detail: expect.any(String) }]);
97
100
  }
98
101
  finally {
99
102
  fs_1.default.rmSync(missingTrackRoot, { recursive: true, force: true });
@@ -180,7 +180,10 @@ describe('watch --init: plan-vs-repo mecanico', () => {
180
180
  test('watch --init --plan bloquea un plan no valido antes de crear journal', () => {
181
181
  const invalid = { state: 'invalid', diagnostics: [{ code: 'PLAN_SHAPE', message: 'bad' }] };
182
182
  expect(() => (0, init_1.initWatch)(repo, 'rama', { path: 'docs/plan.md', report: invalid })).toThrow(/válido/i);
183
- expect((0, store_1.readJournal)(repo, 'rama').corrupt).toBe(true);
183
+ // #173: this asserted `.corrupt` as a proxy for "no journal was created",
184
+ // which is the exact confusion that issue is about. The fact it means is
185
+ // absence, and that is now what it says.
186
+ expect((0, store_1.readJournal)(repo, 'rama')).toMatchObject({ state: null, corrupt: false, absent: true });
184
187
  });
185
188
  test('watch --init --plan rechaza un plan interactivo antes de crear un binding desatendido', () => {
186
189
  const interactive = { ...validPlan('a'), executionMode: 'interactivo' };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.11.3",
3
+ "version": "9.11.4",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
Binary file
Binary file