agentic-workflow-manager 8.5.0 → 8.5.1

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.
@@ -12,6 +12,7 @@ const capture_1 = require("../../core/evidence/capture");
12
12
  const store_1 = require("../../core/evidence/store");
13
13
  const store_2 = require("../../core/journal/store");
14
14
  const store_3 = require("../../core/ledger/store");
15
+ const plan_state_1 = require("../../core/dashboard/plan-state");
15
16
  function assertRepoRelativePlan(value) {
16
17
  if (typeof value !== 'string' || !value || value.startsWith('--') || path_1.default.isAbsolute(value)
17
18
  || value.includes('\\') || value.split('/').some((part) => part === '' || part === '.' || part === '..')) {
@@ -19,6 +20,70 @@ function assertRepoRelativePlan(value) {
19
20
  }
20
21
  return value;
21
22
  }
23
+ function currentRelease(lines) {
24
+ const releases = lines.flatMap((line) => {
25
+ const match = /^\s*\d+\.\s+\*\*Release\s+([A-Za-z0-9][A-Za-z0-9_-]*)\s*\/\s*#\d+:\*\*/.exec(line);
26
+ return match ? [match[1]] : [];
27
+ });
28
+ return releases.length > 1 ? releases.at(-1) : undefined;
29
+ }
30
+ function marker(lines, name, release) {
31
+ const expression = new RegExp(`^\\s*<!--\\s*${name}(?:\\s*:\\s*[^\\r\\n]*?)?\\s*-->\\s*$`);
32
+ if (release === undefined)
33
+ return lines.some((line) => expression.test(line));
34
+ const releaseExpression = new RegExp(`\\bRelease\\s+${release.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\b`, 'i');
35
+ return lines.some((line) => expression.test(line) && releaseExpression.test(line));
36
+ }
37
+ /** Reads only structural lifecycle syntax; plan prose never crosses into evidence. */
38
+ function planState(root, planPath, journal) {
39
+ const file = path_1.default.join(root, planPath);
40
+ const stat = fs_1.default.lstatSync(file);
41
+ if (!stat.isFile() || stat.isSymbolicLink())
42
+ throw new Error('--plan must reference a regular file');
43
+ let source;
44
+ try {
45
+ source = new TextDecoder('utf-8', { fatal: true }).decode(fs_1.default.readFileSync(file));
46
+ }
47
+ catch {
48
+ throw new Error('--plan must be valid UTF-8');
49
+ }
50
+ if (source.includes('\0'))
51
+ throw new Error('--plan must not contain NUL bytes');
52
+ let fenced = false;
53
+ let total = 0;
54
+ let completed = 0;
55
+ const visibleLines = [];
56
+ for (const line of source.split(/\r?\n/)) {
57
+ if (/^\s*(?:```|~~~)/.test(line)) {
58
+ fenced = !fenced;
59
+ continue;
60
+ }
61
+ if (fenced)
62
+ continue;
63
+ visibleLines.push(line);
64
+ if (!/^\s*[-*+]\s+\[/.test(line))
65
+ continue;
66
+ const candidate = /^\s*[-*+]\s+\[([^\]])\](?:\s+(.*))?$/.exec(line);
67
+ if (!candidate)
68
+ throw new Error('--plan contains an invalid checklist task');
69
+ if ((candidate[1] !== ' ' && candidate[1] !== 'x' && candidate[1] !== 'X') || !candidate[2]?.trim())
70
+ throw new Error('--plan contains an invalid checklist task');
71
+ total += 1;
72
+ if (candidate[1] === 'x' || candidate[1] === 'X')
73
+ completed += 1;
74
+ }
75
+ if (fenced)
76
+ throw new Error('--plan contains an unclosed fenced block');
77
+ const status = journal.cycle?.status;
78
+ if (status !== 'IN_PROGRESS' && status !== 'COMPLETE' && status !== 'BLOCKED')
79
+ throw new Error('journal cycle status is invalid');
80
+ const release = currentRelease(visibleLines);
81
+ return (0, plan_state_1.classifyPlanState)({
82
+ ...(status === 'IN_PROGRESS' ? { journal: { state: 'active' } } : status === 'BLOCKED' ? { journal: { state: 'blocked' } } : {}),
83
+ markers: { qaComplete: marker(visibleLines, 'awm-qa-complete', release), retroComplete: marker(visibleLines, 'awm-retro-complete', release) },
84
+ tasks: { total, completed },
85
+ });
86
+ }
22
87
  function registerEvidenceCommand(program) {
23
88
  const evidence = program.command('evidence').description('durable privacy-preserving cycle observations');
24
89
  evidence.command('capture')
@@ -86,7 +151,7 @@ function runEvidenceCapture(root, plan, overrides) {
86
151
  throw new Error('--pr-provider and --pr-number must be supplied together');
87
152
  pr = { provider: overrides.prProvider, number: Number(overrides.prNumber) };
88
153
  }
89
- const saved = (0, store_1.writeCycleEvidence)(root, (0, capture_1.captureCycleEvidence)({ root, repositoryIdentity: repositoryIdentity(root, overrides?.repositoryIdentity), planPath, journal: read.state, gates: firstEvaluationGates(read.state), ledger: overrides?.ledger ?? (0, store_3.listEntries)(root, branch), pr }));
154
+ const saved = (0, store_1.writeCycleEvidence)(root, (0, capture_1.captureCycleEvidence)({ root, repositoryIdentity: repositoryIdentity(root, overrides?.repositoryIdentity), planPath, journal: read.state, gates: firstEvaluationGates(read.state), ledger: overrides?.ledger ?? (0, store_3.listEntries)(root, branch), pr, planState: planState(root, planPath, read.state) }));
90
155
  return { code: 0, stdout: `${saved.cycleId}\n` };
91
156
  }
92
157
  catch (error) {
@@ -11,6 +11,9 @@ const fs_1 = __importDefault(require("fs"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const history_1 = require("../evidence/history");
13
13
  const types_1 = require("../evidence/types");
14
+ const store_1 = require("../ledger/store");
15
+ const paths_1 = require("../journal/paths");
16
+ const store_2 = require("../journal/store");
14
17
  const sanitize_1 = require("./sanitize");
15
18
  const validate_1 = require("./validate");
16
19
  const plan_state_1 = require("./plan-state");
@@ -23,6 +26,7 @@ exports.REMEDIATION_BY_FINDING_ID = {
23
26
  'planning.source.unavailable': 'awm preflight',
24
27
  'execution.source.unavailable': 'awm sensors status',
25
28
  };
29
+ const BLOCKED_CYCLE_REMEDIATION = 'awm preflight';
26
30
  const EMPTY_ADAPTERS = {
27
31
  machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined,
28
32
  };
@@ -84,8 +88,88 @@ function productionDashboardAdapters(context) {
84
88
  return {
85
89
  machine: () => ({ findings: machineFindings }),
86
90
  project: () => ({ label: 'Project detected', findings: projectFindings }),
87
- plans: () => [],
88
- execution: () => undefined,
91
+ plans: ({ root }) => {
92
+ const history = readEvidenceHistory(root);
93
+ const journal = readJournalOverlay(root);
94
+ const latestCycleId = history.cycles.at(-1)?.cycleId;
95
+ const latestByPlan = new Map();
96
+ for (const cycle of history.cycles)
97
+ latestByPlan.set(cycle.plan.ref, cycle);
98
+ if (latestByPlan.size === 0 && journal)
99
+ return [journalPlanFinding(journal)];
100
+ return [...latestByPlan.values()].map((cycle) => {
101
+ const overlay = cycle.cycleId === latestCycleId ? journal : undefined;
102
+ const blocked = overlay?.state === 'blocked' || cycle.plan.state === 'blocked';
103
+ return {
104
+ id: `planning.cycle.${cycle.cycleId}`,
105
+ // The plan reference is intentionally never rendered: a plan path
106
+ // can expose repository-specific names. Its opaque cycle ID gives
107
+ // the dashboard deterministic identity without exporting it.
108
+ label: 'Project context',
109
+ state: blocked ? 'attention' : 'ok',
110
+ ...(blocked ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
111
+ lifecycle: overlay ? lifecycleForJournal(overlay) : lifecycleForCycle(cycle),
112
+ };
113
+ });
114
+ },
115
+ execution: ({ root }) => {
116
+ const journal = readJournalOverlay(root);
117
+ if (!evidenceDirectoryExists(root))
118
+ return journal ? journalExecutionFinding(journal) : undefined;
119
+ const history = readEvidenceHistory(root);
120
+ if (history.cycles.length === 0)
121
+ return journal ? journalExecutionFinding(journal) : undefined;
122
+ return {
123
+ execution: history.cycles.map((cycle) => cycleFinding('execution', 'Static preflight', cycle, cycle.cycleState === 'blocked')),
124
+ qa: history.cycles.map((cycle) => cycleFinding('qa', 'Sensors', cycle, cycle.qa.fixes < cycle.qa.findings)),
125
+ retro: history.cycles.map((cycle) => cycleFinding('retro', 'Project context', cycle, cycle.plan.state !== 'executed')),
126
+ };
127
+ },
128
+ };
129
+ }
130
+ /** A journal has no durable plan reference. Use a generic fixed row so its
131
+ * authoritative lifecycle remains visible without publishing branch or path. */
132
+ function journalPlanFinding(overlay) {
133
+ const blocked = overlay.state === 'blocked';
134
+ return {
135
+ id: 'planning.current-journal', label: 'Project context', state: blocked ? 'attention' : 'ok',
136
+ ...(blocked ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
137
+ lifecycle: lifecycleForJournal(overlay),
138
+ };
139
+ }
140
+ function journalExecutionFinding(overlay) {
141
+ const blocked = overlay.state === 'blocked';
142
+ return {
143
+ execution: [{
144
+ id: 'execution.current-journal', label: 'Static preflight', state: blocked ? 'attention' : 'ok',
145
+ ...(blocked ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
146
+ }],
147
+ qa: [], retro: [],
148
+ };
149
+ }
150
+ function lifecycleForJournal(overlay) {
151
+ return { journal: { state: overlay.state }, markers: { qaComplete: false, retroComplete: false }, tasks: overlay.tasks };
152
+ }
153
+ function lifecycleForCycle(cycle) {
154
+ const total = cycle.tasks.length;
155
+ if (cycle.plan.state === 'blocked' || cycle.cycleState === 'blocked')
156
+ return { journal: { state: 'blocked' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
157
+ if (cycle.plan.state === 'active')
158
+ return { journal: { state: 'active' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
159
+ if (cycle.plan.state === 'executed')
160
+ return { markers: { qaComplete: true, retroComplete: true }, tasks: { total, completed: total } };
161
+ if (cycle.plan.state === 'retro_pending')
162
+ return { markers: { qaComplete: true, retroComplete: false }, tasks: { total, completed: total } };
163
+ if (cycle.plan.state === 'qa_pending')
164
+ return { markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: total } };
165
+ return { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
166
+ }
167
+ function cycleFinding(section, label, cycle, actionable) {
168
+ return {
169
+ id: `${section}.cycle.${cycle.cycleId}`,
170
+ label,
171
+ state: actionable ? 'attention' : 'ok',
172
+ ...(actionable ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
89
173
  };
90
174
  }
91
175
  function findings(items, optional = false) {
@@ -125,7 +209,62 @@ function canonicalOptionalFailure(failure) {
125
209
  return [];
126
210
  return [{ id: failure.findingId, label: 'Optional source unavailable', state: 'unavailable', remediation: exports.REMEDIATION_BY_FINDING_ID[failure.findingId] }];
127
211
  }
128
- function evidenceHistoryItems(root) {
212
+ function evidenceDirectoryExists(root) {
213
+ try {
214
+ return fs_1.default.lstatSync(path_1.default.join(root, '.awm', 'evidence', 'cycles')).isDirectory();
215
+ }
216
+ catch (error) {
217
+ if (error && typeof error === 'object' && error.code === 'ENOENT')
218
+ return false;
219
+ throw error;
220
+ }
221
+ }
222
+ /** Reads only the current branch journal. Missing journals are not observations;
223
+ * corrupt or redirected journal paths fail closed rather than changing lifecycle
224
+ * state from untrusted content. */
225
+ function readJournalOverlay(root) {
226
+ const branch = (0, store_1.detectBranch)(root);
227
+ const file = (0, paths_1.statePath)(root, branch);
228
+ if (!safeJournalStateExists(root, branch, file))
229
+ return undefined;
230
+ const journal = (0, store_2.readJournal)(root, branch);
231
+ if (journal.corrupt || !journal.state)
232
+ throw new Error('current journal is unavailable or corrupt');
233
+ if (journal.state.cycle.status === 'COMPLETE')
234
+ return undefined;
235
+ return {
236
+ state: journal.state.cycle.status === 'BLOCKED' ? 'blocked' : 'active',
237
+ tasks: { total: journal.state.tasks.length, completed: journal.state.tasks.filter((task) => task.status === 'done').length },
238
+ };
239
+ }
240
+ function safeJournalStateExists(root, branch, file) {
241
+ for (const directory of [path_1.default.join(root, '.awm'), path_1.default.join(root, '.awm', 'journal'), (0, paths_1.journalDir)(root, branch)]) {
242
+ let stat;
243
+ try {
244
+ stat = fs_1.default.lstatSync(directory);
245
+ }
246
+ catch (error) {
247
+ if (error && typeof error === 'object' && error.code === 'ENOENT')
248
+ return false;
249
+ throw error;
250
+ }
251
+ if (stat.isSymbolicLink() || !stat.isDirectory())
252
+ throw new Error('journal directory is unsafe');
253
+ }
254
+ let stat;
255
+ try {
256
+ stat = fs_1.default.lstatSync(file);
257
+ }
258
+ catch (error) {
259
+ if (error && typeof error === 'object' && error.code === 'ENOENT')
260
+ return false;
261
+ throw error;
262
+ }
263
+ if (stat.isSymbolicLink() || !stat.isFile())
264
+ throw new Error('journal state is unsafe');
265
+ return true;
266
+ }
267
+ function readEvidenceHistory(root) {
129
268
  const awmDirectory = path_1.default.join(root, '.awm');
130
269
  const evidenceDirectory = path_1.default.join(awmDirectory, 'evidence');
131
270
  const directory = path_1.default.join(evidenceDirectory, 'cycles');
@@ -136,7 +275,7 @@ function evidenceHistoryItems(root) {
136
275
  }
137
276
  catch (error) {
138
277
  if (error && typeof error === 'object' && error.code === 'ENOENT')
139
- return { confidence: 'none', items: [] };
278
+ return (0, history_1.buildEvidenceHistory)([]);
140
279
  throw error;
141
280
  }
142
281
  if (stat.isSymbolicLink() || !stat.isDirectory())
@@ -157,7 +296,10 @@ function evidenceHistoryItems(root) {
157
296
  seenCycleIds.add(evidence.cycleId);
158
297
  return evidence;
159
298
  });
160
- const history = (0, history_1.buildEvidenceHistory)(records);
299
+ return (0, history_1.buildEvidenceHistory)(records);
300
+ }
301
+ function evidenceHistoryItems(root) {
302
+ const history = readEvidenceHistory(root);
161
303
  return {
162
304
  confidence: history.confidence,
163
305
  items: history.cycles.map((cycle) => {
@@ -166,6 +308,7 @@ function evidenceHistoryItems(root) {
166
308
  id: `history.cycle.${cycle.cycleId}`,
167
309
  label: `Cycle ${cycle.cycleId.slice(0, 12)}`,
168
310
  state: cycle.cycleState === 'blocked' ? 'attention' : 'ok',
311
+ ...(cycle.cycleState === 'blocked' ? { remediation: BLOCKED_CYCLE_REMEDIATION } : {}),
169
312
  detail: `plan ${cycle.plan.state}; tasks ${cycle.tasks.length}; retries ${cycle.retries}; QA ${cycle.qa.findings}/${cycle.qa.fixes}; first-pass ${cycle.gates.firstPass ? 'yes' : 'no'}; cures ${cures}`,
170
313
  };
171
314
  }),
@@ -31,12 +31,32 @@ function captureCycleEvidence(input) {
31
31
  throw new Error('capture sources are invalid');
32
32
  const tasks = journal.tasks.map((item, index) => { const task = object(item, `task ${index}`); if (typeof task.id !== 'string')
33
33
  throw new Error('task id is invalid'); const attempts = nonNegative(task.attempts, 'task attempts'); return { id: task.id, attempts, retries: Math.max(attempts - 1, 0) }; });
34
- const failures = journal.verdicts.filter((item, index) => { const verdict = object(item, `verdict ${index}`); if (verdict.result !== 'pass' && verdict.result !== 'fail' && verdict.result !== 'inconclusive')
35
- throw new Error('verdict result is invalid'); iso(verdict.receivedAt, 'verdict receivedAt'); return verdict.result === 'fail'; });
36
- const signatures = failures.map((item) => { const verdict = object(item, 'verdict'); if (typeof verdict.fingerprint !== 'string' || !verdict.fingerprint)
34
+ const verdictIds = new Set();
35
+ const adverseVerdictIds = new Set();
36
+ const adverseVerdicts = journal.verdicts.filter((item, index) => {
37
+ const verdict = object(item, `verdict ${index}`);
38
+ if (typeof verdict.id !== 'string' || !verdict.id)
39
+ throw new Error('verdict id is invalid');
40
+ if (verdictIds.has(verdict.id))
41
+ throw new Error('verdict id is duplicated');
42
+ verdictIds.add(verdict.id);
43
+ if (verdict.result !== 'pass' && verdict.result !== 'fail' && verdict.result !== 'inconclusive')
44
+ throw new Error('verdict result is invalid');
45
+ iso(verdict.receivedAt, 'verdict receivedAt');
46
+ if (verdict.result !== 'pass')
47
+ adverseVerdictIds.add(verdict.id);
48
+ return verdict.result !== 'pass';
49
+ });
50
+ const signatures = adverseVerdicts.map((item) => { const verdict = object(item, 'verdict'); if (typeof verdict.fingerprint !== 'string' || !verdict.fingerprint)
37
51
  throw new Error('verdict fingerprint is required'); return hash(`signature:${verdict.fingerprint}`); });
38
- const fixes = journal.fixes.filter((item, index) => { const fix = object(item, `fix ${index}`); if (typeof fix.closed !== 'boolean')
39
- throw new Error('fix closed is invalid'); return fix.closed; }).length;
52
+ const fixes = journal.fixes.filter((item, index) => {
53
+ const fix = object(item, `fix ${index}`);
54
+ if (typeof fix.closed !== 'boolean')
55
+ throw new Error('fix closed is invalid');
56
+ if (typeof fix.verdictId !== 'string' || !fix.verdictId || !adverseVerdictIds.has(fix.verdictId))
57
+ throw new Error('fix verdictId must reference an adverse verdict');
58
+ return fix.closed;
59
+ }).length;
40
60
  const gates = input.gates.map((item, index) => { const gate = object(item, `gate ${index}`); if (typeof gate.required !== 'boolean' || typeof gate.passed !== 'boolean')
41
61
  throw new Error('gate is invalid'); return gate; }).filter((gate) => gate.required === true);
42
62
  const cures = input.ledger.filter((item, index) => { const entry = object(item, `ledger ${index}`); if (typeof entry.signature !== 'string' || (entry.polarity !== 'finding' && entry.polarity !== 'win'))
@@ -161,6 +161,16 @@ describe('collectDashboardSnapshot', () => {
161
161
  expect(history?.items[0]?.detail).toContain('retries 1; QA 1/1; first-pass yes; cures awaiting_observation');
162
162
  fs_1.default.rmSync(root, { recursive: true, force: true });
163
163
  });
164
+ it('renders a blocked evidence cycle as an actionable history item with a verified remedy', () => {
165
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
166
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
167
+ (0, store_1.writeCycleEvidence)(root, { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleState: 'blocked', plan: { ref: 'docs/plans/current.md', state: 'blocked' } });
168
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
169
+ expect(snapshot.sections.find((section) => section.id === 'history')?.items).toEqual([
170
+ expect.objectContaining({ state: 'attention', remediation: 'awm preflight' }),
171
+ ]);
172
+ fs_1.default.rmSync(root, { recursive: true, force: true });
173
+ });
164
174
  it.each(['.awm', 'evidence', 'cycles'])('does not follow a symlinked %s evidence ancestor', (ancestor) => {
165
175
  const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
166
176
  const external = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-external-'));
@@ -1,6 +1,17 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const collect_1 = require("../../../src/core/dashboard/collect");
7
+ const store_1 = require("../../../src/core/evidence/store");
8
+ const store_2 = require("../../../src/core/journal/store");
9
+ const types_1 = require("../../../src/core/journal/types");
10
+ const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
11
+ const child_process_1 = require("child_process");
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
4
15
  function context() {
5
16
  return {
6
17
  machine: {},
@@ -67,4 +78,82 @@ describe('productionDashboardAdapters', () => {
67
78
  ]));
68
79
  expect(JSON.stringify(snapshot)).not.toMatch(/remediationVerified|private|0\.145\.0|token/i);
69
80
  });
81
+ it('derives plan lifecycle and execution evidence from validated local cycle records', () => {
82
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-production-evidence-'));
83
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
84
+ (0, store_1.writeCycleEvidence)(root, {
85
+ ...(0, evidence_fixtures_1.cycleEvidenceFixture)(),
86
+ plan: { ref: 'docs/plans/current.md', state: 'executed' },
87
+ tasks: [{ id: 'task-1', attempts: 2, retries: 1 }],
88
+ qa: { findings: 1, fixes: 1, signatures: ['a'.repeat(64)] },
89
+ });
90
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
91
+ cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
92
+ });
93
+ expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
94
+ expect.objectContaining({ detail: 'executed', state: 'ok' }),
95
+ ]);
96
+ expect(snapshot.sections.find((section) => section.id === 'execution')?.items).toEqual([
97
+ expect.objectContaining({ state: 'ok' }),
98
+ ]);
99
+ expect(snapshot.sections.find((section) => section.id === 'qa')?.items).toEqual([
100
+ expect.objectContaining({ state: 'ok' }),
101
+ ]);
102
+ expect(snapshot.sections.find((section) => section.id === 'retro')?.items).toEqual([
103
+ expect.objectContaining({ state: 'ok' }),
104
+ ]);
105
+ fs_1.default.rmSync(root, { recursive: true, force: true });
106
+ });
107
+ it.each([
108
+ ['IN_PROGRESS', 'active', 'ok'],
109
+ ['BLOCKED', 'blocked', 'attention'],
110
+ ])('overlays the current branch journal %s over prior executed evidence', (status, expectedLifecycle, expectedState) => {
111
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-journal-overlay-'));
112
+ const branch = 'dashboard-overlay';
113
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
114
+ (0, child_process_1.execFileSync)('git', ['init', '--initial-branch', branch], { cwd: root, stdio: 'ignore' });
115
+ (0, child_process_1.execFileSync)('git', ['config', 'user.email', 'dashboard@example.test'], { cwd: root, stdio: 'ignore' });
116
+ (0, child_process_1.execFileSync)('git', ['config', 'user.name', 'Dashboard Test'], { cwd: root, stdio: 'ignore' });
117
+ (0, child_process_1.execFileSync)('git', ['commit', '--allow-empty', '-m', 'fixture'], { cwd: root, stdio: 'ignore' });
118
+ (0, store_1.writeCycleEvidence)(root, { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), plan: { ref: 'docs/plans/current.md', state: 'executed' } });
119
+ (0, store_2.initJournal)(root, branch);
120
+ const journal = (0, types_1.emptyState)(branch);
121
+ journal.cycle.status = status;
122
+ (0, store_2.writeJournal)(root, branch, journal);
123
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
124
+ cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
125
+ });
126
+ expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
127
+ expect.objectContaining({ detail: expectedLifecycle, state: expectedState }),
128
+ ]);
129
+ expect(JSON.stringify(snapshot)).not.toMatch(/dashboard-overlay|dashboard@example|Dashboard Test/i);
130
+ fs_1.default.rmSync(root, { recursive: true, force: true });
131
+ });
132
+ it.each([
133
+ ['IN_PROGRESS', 'active', 'ok'],
134
+ ['BLOCKED', 'blocked', 'attention'],
135
+ ])('renders the current branch journal %s without prior evidence', (status, expectedLifecycle, expectedState) => {
136
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-journal-only-'));
137
+ const branch = 'dashboard-journal-only';
138
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
139
+ (0, child_process_1.execFileSync)('git', ['init', '--initial-branch', branch], { cwd: root, stdio: 'ignore' });
140
+ (0, child_process_1.execFileSync)('git', ['config', 'user.email', 'dashboard@example.test'], { cwd: root, stdio: 'ignore' });
141
+ (0, child_process_1.execFileSync)('git', ['config', 'user.name', 'Dashboard Test'], { cwd: root, stdio: 'ignore' });
142
+ (0, child_process_1.execFileSync)('git', ['commit', '--allow-empty', '-m', 'fixture'], { cwd: root, stdio: 'ignore' });
143
+ (0, store_2.initJournal)(root, branch);
144
+ const journal = (0, types_1.emptyState)(branch);
145
+ journal.cycle.status = status;
146
+ (0, store_2.writeJournal)(root, branch, journal);
147
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
148
+ cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
149
+ });
150
+ expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
151
+ expect.objectContaining({ detail: expectedLifecycle, state: expectedState }),
152
+ ]);
153
+ expect(snapshot.sections.find((section) => section.id === 'execution')).toEqual(expect.objectContaining({
154
+ availability: 'available', items: [expect.objectContaining({ state: expectedState })],
155
+ }));
156
+ expect(JSON.stringify(snapshot)).not.toMatch(/dashboard-journal-only|dashboard@example|Dashboard Test/i);
157
+ fs_1.default.rmSync(root, { recursive: true, force: true });
158
+ });
70
159
  });
@@ -10,7 +10,7 @@ describe('captureCycleEvidence', () => {
10
10
  cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:10.000Z' },
11
11
  journalId: 'local-repository-identity',
12
12
  tasks: [{ id: 'task-a', attempts: 3 }],
13
- verdicts: [{ result: 'fail', fingerprint: 'unsafe-input', detail: 'Alice saw secret prompt', receivedAt: '2026-08-22T10:00:05.000Z' }],
13
+ verdicts: [{ id: 'v1', result: 'fail', fingerprint: 'unsafe-input', detail: 'Alice saw secret prompt', receivedAt: '2026-08-22T10:00:05.000Z' }],
14
14
  fixes: [{ verdictId: 'v1', closed: true }],
15
15
  },
16
16
  gates: [{ required: true, passed: true }],
@@ -45,4 +45,38 @@ describe('captureCycleEvidence', () => {
45
45
  const evidence = (0, capture_1.captureCycleEvidence)({ root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md', journal: { cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z' }, controllerHeartbeatAt: '2026-08-22T10:00:03.000Z', tasks: [], verdicts: [], fixes: [] }, gates: [], ledger: [] });
46
46
  expect(evidence).toMatchObject({ cycleState: 'blocked', endedAt: '2026-08-22T10:00:03.000Z', durationMs: 3_000 });
47
47
  });
48
+ test('counts inconclusive verdicts as adverse findings because they create fix obligations', () => {
49
+ const evidence = (0, capture_1.captureCycleEvidence)({
50
+ root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
51
+ journal: {
52
+ cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' },
53
+ tasks: [], verdicts: [{ id: 'v1', result: 'inconclusive', fingerprint: 'probe-unavailable', receivedAt: '2026-08-22T10:00:00.000Z' }],
54
+ fixes: [{ verdictId: 'v1', closed: true }],
55
+ }, gates: [], ledger: [],
56
+ });
57
+ expect(evidence.qa).toMatchObject({ findings: 1, fixes: 1, signatures: [expect.stringMatching(/^[a-f0-9]{64}$/)] });
58
+ });
59
+ test('counts only closed fixes that reference an adverse verdict', () => {
60
+ const evidence = (0, capture_1.captureCycleEvidence)({
61
+ root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
62
+ journal: {
63
+ cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [],
64
+ verdicts: [
65
+ { id: 'v-fail', result: 'fail', fingerprint: 'failure', receivedAt: '2026-08-22T10:00:00.000Z' },
66
+ { id: 'v-inconclusive', result: 'inconclusive', fingerprint: 'unknown', receivedAt: '2026-08-22T10:00:00.000Z' },
67
+ ],
68
+ fixes: [{ verdictId: 'v-fail', closed: true }, { verdictId: 'v-inconclusive', closed: false }],
69
+ }, gates: [], ledger: [],
70
+ });
71
+ expect(evidence.qa).toMatchObject({ findings: 2, fixes: 1 });
72
+ });
73
+ test.each([
74
+ { verdicts: [], verdictId: 'missing' },
75
+ { verdicts: [{ id: 'v-pass', result: 'pass', fingerprint: 'passed', receivedAt: '2026-08-22T10:00:00.000Z' }], verdictId: 'v-pass' },
76
+ ])('rejects a fix whose verdict reference is absent or not adverse', ({ verdicts, verdictId }) => {
77
+ expect(() => (0, capture_1.captureCycleEvidence)({
78
+ root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
79
+ journal: { cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts, fixes: [{ verdictId, closed: true }] }, gates: [], ledger: [],
80
+ })).toThrow(/fix verdictId/i);
81
+ });
48
82
  });
@@ -34,4 +34,27 @@ describe('evidence capture CLI boundary', () => {
34
34
  const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], fixes: [], cycleVerificationPlan: [{ id: 'review-gate', kind: 'review', satisfiedBy: 'final' }], jobs: {}, verdicts: [{ id: 'first', obligationId: 'review-gate', result: 'fail', fingerprint: 'review-gate', receivedAt: '2026-08-22T10:00:00.000Z' }, { id: 'final', obligationId: 'review-gate', result: 'pass', fingerprint: 'review-gate', receivedAt: '2026-08-22T10:00:01.000Z' }] }, ledger: [] });
35
35
  expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
36
36
  });
37
+ test('derives retro_pending from completed checklist tasks and the QA marker in the plan file', () => {
38
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: 2026-08-22 -->\n- [x] Build\n- [X] Verify\n');
39
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
40
+ expect(result.code).toBe(0);
41
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('retro_pending');
42
+ });
43
+ test('uses a blocked journal state over completed plan markers', () => {
44
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: 2026-08-22 -->\n<!-- awm-retro-complete: 2026-08-22 -->\n- [x] Build\n');
45
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
46
+ expect(result.code).toBe(0);
47
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('blocked');
48
+ });
49
+ test('does not let Release A markers classify a multi-release plan whose current Release B has no lifecycle markers', () => {
50
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: Release A / #86 -->\n<!-- awm-retro-complete: Release A / #86 -->\n\n## Delivery order\n1. **Release A / #86:** dashboard\n2. **Release B / #87:** evidence\n\n- [x] Release B task\n');
51
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
52
+ expect(result.code).toBe(0);
53
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('qa_pending');
54
+ });
55
+ test('rejects a malformed checklist instead of silently classifying a plan', () => {
56
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n- [z] Unknown state\n');
57
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
58
+ expect(result).toEqual(expect.objectContaining({ code: 2, error: expect.stringMatching(/invalid checklist/i) }));
59
+ });
37
60
  });
@@ -20,6 +20,23 @@ describe('CycleEvidenceV1 validation', () => {
20
20
  const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
21
21
  expect((0, types_1.validateCycleEvidence)({ ...evidence, gates: { required: 2, firstEvaluationsPassed: [true, false], firstPass: false } }).gates.firstEvaluationsPassed).toEqual([true, false]);
22
22
  });
23
+ test.each([Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, -1])('rejects a non-finite or negative duration: %p', (durationMs) => {
24
+ const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
25
+ expect(() => (0, types_1.validateCycleEvidence)({
26
+ ...evidence,
27
+ startedAt: evidence.endedAt,
28
+ durationMs,
29
+ })).toThrow('durationMs must be a non-negative integer');
30
+ });
31
+ test.each([
32
+ ['task attempts', (evidence) => ({ ...evidence, tasks: [{ ...evidence.tasks[0], attempts: -1 }] })],
33
+ ['task retries', (evidence) => ({ ...evidence, tasks: [{ ...evidence.tasks[0], retries: -1 }] })],
34
+ ['QA findings', (evidence) => ({ ...evidence, qa: { ...evidence.qa, findings: -1 } })],
35
+ ['QA fixes', (evidence) => ({ ...evidence, qa: { ...evidence.qa, fixes: -1 } })],
36
+ ['required gates', (evidence) => ({ ...evidence, gates: { ...evidence.gates, required: -1 } })],
37
+ ])('rejects a negative %s count', (_label, patch) => {
38
+ expect(() => (0, types_1.validateCycleEvidence)(patch((0, evidence_fixtures_1.cycleEvidenceFixture)()))).toThrow('must be a non-negative integer');
39
+ });
23
40
  test('permits every dashboard plan state and an absent PR', () => {
24
41
  for (const state of ['active', 'blocked', 'qa_pending', 'retro_pending', 'executed', 'legacy_unverifiable']) {
25
42
  const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
@@ -0,0 +1,21 @@
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 path_1 = __importDefault(require("path"));
8
+ const CLI_ROOT = path_1.default.resolve(__dirname, '..', '..');
9
+ const RELEASE_WORKFLOW = path_1.default.join(CLI_ROOT, '..', '.github', 'workflows', 'release.yml');
10
+ describe('release registry provenance', () => {
11
+ const workflow = fs_1.default.readFileSync(RELEASE_WORKFLOW, 'utf8');
12
+ it('resolves the immutable registry tag before publishing the CLI', () => {
13
+ expect(workflow).toContain('Resolve immutable Task 7 registry commit');
14
+ expect(workflow).toContain('refs/tags/${REGISTRY_TAG}^{}');
15
+ expect(workflow).toContain('AWM_PUBLISHED_REGISTRY_TAG: v3.4.0');
16
+ expect(workflow).not.toContain('AWM_REGISTRY_V3_4_0_COMMIT');
17
+ expect(workflow.indexOf('Resolve immutable Task 7 registry commit'))
18
+ .toBeLessThan(workflow.indexOf('- name: Release'));
19
+ expect(workflow).toMatch(/REGISTRY_TAG: v3\.4\.0[\s\S]*AWM_PUBLISHED_REGISTRY_TAG: v3\.4\.0/);
20
+ });
21
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "8.5.0",
3
+ "version": "8.5.1",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"