agentic-workflow-manager 8.4.0 → 8.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.
@@ -0,0 +1,95 @@
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.registerEvidenceCommand = registerEvidenceCommand;
7
+ exports.runEvidenceCapture = runEvidenceCapture;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const child_process_1 = require("child_process");
11
+ const capture_1 = require("../../core/evidence/capture");
12
+ const store_1 = require("../../core/evidence/store");
13
+ const store_2 = require("../../core/journal/store");
14
+ const store_3 = require("../../core/ledger/store");
15
+ function assertRepoRelativePlan(value) {
16
+ if (typeof value !== 'string' || !value || value.startsWith('--') || path_1.default.isAbsolute(value)
17
+ || value.includes('\\') || value.split('/').some((part) => part === '' || part === '.' || part === '..')) {
18
+ throw new Error('--plan requires a repo-relative path');
19
+ }
20
+ return value;
21
+ }
22
+ function registerEvidenceCommand(program) {
23
+ const evidence = program.command('evidence').description('durable privacy-preserving cycle observations');
24
+ evidence.command('capture')
25
+ .description('capture one local cycle observation')
26
+ .option('--plan <path>', 'repo-relative plan path')
27
+ .option('--pr-provider <provider>', 'github | gitlab | other')
28
+ .option('--pr-number <number>', 'pull request number')
29
+ .action((opts) => {
30
+ const result = runEvidenceCapture(process.cwd(), opts.plan, { prProvider: opts.prProvider, prNumber: opts.prNumber });
31
+ if (result.code === 0)
32
+ process.stdout.write(result.stdout);
33
+ else {
34
+ process.stderr.write(`awm evidence capture: ${result.error}\n`);
35
+ process.exitCode = 2;
36
+ }
37
+ });
38
+ }
39
+ function firstEvaluationGates(state) {
40
+ return state.cycleVerificationPlan.map((gate) => {
41
+ if (gate.kind === 'review') {
42
+ const verdict = state.verdicts.filter((candidate) => candidate.obligationId === gate.id)
43
+ .sort((left, right) => left.receivedAt.localeCompare(right.receivedAt) || left.id.localeCompare(right.id))[0];
44
+ return { required: true, passed: verdict?.result === 'pass' };
45
+ }
46
+ const evaluated = Object.values(state.jobs).filter((job) => job.satisfies?.includes(gate.id));
47
+ const first = evaluated.filter((job) => job.attemptOf === undefined || !evaluated.some((candidate) => candidate.id === job.attemptOf))
48
+ .sort((left, right) => jobTimestamp(left).localeCompare(jobTimestamp(right)) || left.id.localeCompare(right.id))[0];
49
+ return { required: true, passed: first?.verdict === 'pass' };
50
+ });
51
+ }
52
+ function jobTimestamp(job) {
53
+ const timestamp = job.result?.endedAt ?? job.phaseTimestamps.received ?? job.phaseTimestamps.exited;
54
+ if (typeof timestamp !== 'string' || Number.isNaN(Date.parse(timestamp)))
55
+ throw new Error(`gate job ${job.id} lacks a durable evaluation timestamp`);
56
+ return timestamp;
57
+ }
58
+ function repositoryIdentity(root, supplied) {
59
+ if (supplied !== undefined) {
60
+ if (typeof supplied !== 'string' || !supplied || supplied.length > 4096 || /[\r\n]/.test(supplied))
61
+ throw new Error('repository identity is invalid');
62
+ return supplied;
63
+ }
64
+ try {
65
+ const remote = (0, child_process_1.execFileSync)('git', ['config', '--get', 'remote.origin.url'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 }).trim();
66
+ if (!remote || remote.length > 4096 || /[\r\n]/.test(remote))
67
+ throw new Error('invalid');
68
+ return remote;
69
+ }
70
+ catch {
71
+ throw new Error('repository identity unavailable: configure remote.origin.url');
72
+ }
73
+ }
74
+ function runEvidenceCapture(root, plan, overrides) {
75
+ try {
76
+ const planPath = assertRepoRelativePlan(plan);
77
+ if (!fs_1.default.existsSync(path_1.default.join(root, planPath)))
78
+ throw new Error('--plan must reference an existing file');
79
+ const branch = (0, store_3.detectBranch)(root);
80
+ const read = overrides?.journal === undefined ? (0, store_2.readJournal)(root, branch) : { corrupt: false, state: overrides.journal };
81
+ if (read.corrupt || !read.state)
82
+ throw new Error('current journal is unavailable or corrupt');
83
+ let pr;
84
+ if (overrides?.prProvider !== undefined || overrides?.prNumber !== undefined) {
85
+ if (overrides.prProvider === undefined || overrides.prNumber === undefined || typeof overrides.prNumber !== 'string' || !/^\d+$/.test(overrides.prNumber))
86
+ throw new Error('--pr-provider and --pr-number must be supplied together');
87
+ pr = { provider: overrides.prProvider, number: Number(overrides.prNumber) };
88
+ }
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 }));
90
+ return { code: 0, stdout: `${saved.cycleId}\n` };
91
+ }
92
+ catch (error) {
93
+ return { code: 2, stdout: '', error: error.message };
94
+ }
95
+ }
@@ -1,9 +1,16 @@
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
  exports.REMEDIATION_BY_FINDING_ID = void 0;
4
7
  exports.productionDashboardAdapters = productionDashboardAdapters;
5
8
  exports.collectDashboardSnapshot = collectDashboardSnapshot;
6
9
  const profile_1 = require("../profile");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const history_1 = require("../evidence/history");
13
+ const types_1 = require("../evidence/types");
7
14
  const sanitize_1 = require("./sanitize");
8
15
  const validate_1 = require("./validate");
9
16
  const plan_state_1 = require("./plan-state");
@@ -118,6 +125,52 @@ function canonicalOptionalFailure(failure) {
118
125
  return [];
119
126
  return [{ id: failure.findingId, label: 'Optional source unavailable', state: 'unavailable', remediation: exports.REMEDIATION_BY_FINDING_ID[failure.findingId] }];
120
127
  }
128
+ function evidenceHistoryItems(root) {
129
+ const awmDirectory = path_1.default.join(root, '.awm');
130
+ const evidenceDirectory = path_1.default.join(awmDirectory, 'evidence');
131
+ const directory = path_1.default.join(evidenceDirectory, 'cycles');
132
+ for (const ancestor of [awmDirectory, evidenceDirectory, directory]) {
133
+ let stat;
134
+ try {
135
+ stat = fs_1.default.lstatSync(ancestor);
136
+ }
137
+ catch (error) {
138
+ if (error && typeof error === 'object' && error.code === 'ENOENT')
139
+ return { confidence: 'none', items: [] };
140
+ throw error;
141
+ }
142
+ if (stat.isSymbolicLink() || !stat.isDirectory())
143
+ throw new Error('evidence history directory is unsafe');
144
+ }
145
+ const seenCycleIds = new Set();
146
+ const records = fs_1.default.readdirSync(directory, { withFileTypes: true })
147
+ .sort((left, right) => left.name.localeCompare(right.name))
148
+ .map((entry) => {
149
+ if (!entry.isFile() || !entry.name.endsWith('.json'))
150
+ throw new Error('evidence history contains an unsupported entry');
151
+ const file = path_1.default.join(directory, entry.name);
152
+ if (fs_1.default.lstatSync(file).isSymbolicLink())
153
+ throw new Error('evidence history file is unsafe');
154
+ const evidence = (0, types_1.validateCycleEvidence)(JSON.parse(fs_1.default.readFileSync(file, 'utf8')));
155
+ if (entry.name !== `${evidence.cycleId}.json` || seenCycleIds.has(evidence.cycleId))
156
+ throw new Error('evidence history filename is invalid');
157
+ seenCycleIds.add(evidence.cycleId);
158
+ return evidence;
159
+ });
160
+ const history = (0, history_1.buildEvidenceHistory)(records);
161
+ return {
162
+ confidence: history.confidence,
163
+ items: history.cycles.map((cycle) => {
164
+ const cures = cycle.cureEfficacy.length === 0 ? 'none' : cycle.cureEfficacy.map((cure) => cure.efficacy).join(', ');
165
+ return {
166
+ id: `history.cycle.${cycle.cycleId}`,
167
+ label: `Cycle ${cycle.cycleId.slice(0, 12)}`,
168
+ state: cycle.cycleState === 'blocked' ? 'attention' : 'ok',
169
+ 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
+ };
171
+ }),
172
+ };
173
+ }
121
174
  function isolatedFindings(items) {
122
175
  return optional(() => findings(items, true));
123
176
  }
@@ -150,7 +203,7 @@ function collectDashboardSnapshot(options) {
150
203
  const executionItems = isolatedFindings(execution?.execution);
151
204
  const qaItems = isolatedFindings(execution?.qa);
152
205
  const retroItems = isolatedFindings(execution?.retro);
153
- const historyItems = isolatedFindings(execution?.history);
206
+ const evidenceResult = optional(() => evidenceHistoryItems(root));
154
207
  const executionUnavailable = !executionResult.failed && execution === undefined;
155
208
  const sections = [
156
209
  machineSection,
@@ -163,8 +216,8 @@ function collectDashboardSnapshot(options) {
163
216
  // absent execution source is not evidence of a successful empty cycle.
164
217
  section('qa', executionResult.failed || executionUnavailable || qaItems.failed ? 'unavailable' : 'available', qaItems.value),
165
218
  section('retro', executionResult.failed || executionUnavailable || retroItems.failed ? 'unavailable' : 'available', retroItems.value),
166
- section('history', executionResult.failed || executionUnavailable || historyItems.failed ? 'unavailable' : 'available', historyItems.value),
219
+ section('history', evidenceResult.failed ? 'unavailable' : 'available', evidenceResult.failed ? [] : evidenceResult.value.items),
167
220
  ];
168
221
  const degraded = sections.some((entry) => entry.availability === 'unavailable' || entry.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable'));
169
- return (0, validate_1.validateDashboardSnapshotV1)({ schema: 1, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: true, label: projectSource?.label || 'Project detected' }, confidence: 'provisional', sections });
222
+ return (0, validate_1.validateDashboardSnapshotV1)({ schema: 1, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: true, label: projectSource?.label || 'Project detected' }, confidence: evidenceResult.failed ? 'none' : evidenceResult.value.confidence, sections });
170
223
  }
@@ -45,8 +45,14 @@ function projectHeaderActions() {
45
45
  function projectChrome(project) {
46
46
  return `<div class="project-chrome"><p class="eyebrow" data-project-breadcrumb>AWM / Proyecto / ${project}</p><nav data-project-nav aria-label="Project navigation"><span aria-current="page">Proyecto</span><span>Ejecución</span><span>Configuración</span></nav></div>`;
47
47
  }
48
- function projectEvidenceComposition() {
49
- return `<div data-project-evidence role="group" aria-labelledby="project-evidence-heading"><h3 id="project-evidence-heading" class="sr-only">Project evidence</h3><div class="evidence-grid compact-composition"><div><h3>Planes de trabajo</h3><div class="plan-card active" data-plan-card="active"><div><strong>Plan activo</strong><span class="state attention">Sin observación</span></div><p>Progreso: sin observación</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div><div class="plan-card blocked" data-plan-card="blocked"><div><strong>Plan bloqueado</strong><span class="state unavailable">Sin observación</span></div><p>Progreso: sin observación</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div></div><div><h3>Impacto y trazabilidad</h3><table class="evidence-table"><thead><tr><th scope="col">Tipo</th><th scope="col">Fuente</th><th scope="col">Estado</th><th scope="col">Fecha</th></tr></thead><tbody><tr><td colspan="4" class="empty">No hay evidencia de impacto disponible en este snapshot.</td></tr></tbody></table></div></div><div class="closure-actions" data-closure-actions role="group" aria-labelledby="closure-actions-heading"><div><h3 id="closure-actions-heading">Qué falta para cerrar el ciclo</h3><p class="empty">La ausencia de marcador no equivale a cero hallazgos.</p></div><div><button type="button" disabled aria-describedby="static-controls-note">Completar QA (estático)</button><button type="button" disabled aria-describedby="static-controls-note">Registrar retro (estático)</button><button type="button" disabled aria-describedby="static-controls-note">Adjuntar evidencia (estático)</button></div></div></div>`;
48
+ function projectEvidenceComposition(snapshot) {
49
+ const planning = snapshot.sections.find((section) => section.id === 'planning')?.items ?? [];
50
+ const history = snapshot.sections.find((section) => section.id === 'history')?.items ?? [];
51
+ const planCards = planning.length === 0
52
+ ? `<div class="plan-card active" data-plan-card="active"><div><strong>Plan activo</strong><span class="state attention">Sin observación</span></div><p>Progreso: sin observación</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div><div class="plan-card blocked" data-plan-card="blocked"><div><strong>Plan bloqueado</strong><span class="state unavailable">Sin observación</span></div><p>Progreso: sin observación</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div>`
53
+ : planning.map((plan) => `<div class="plan-card ${plan.detail === 'blocked' ? 'blocked' : 'active'}" data-plan-card="${plan.detail === 'blocked' ? 'blocked' : 'active'}"><div><strong>${escapeHtml(plan.label)}</strong><span class="state ${plan.state}">${STATE_GLYPH[plan.state]} ${STATE_TEXT[plan.state]}</span></div><p>Estado: ${escapeHtml(plan.detail ?? 'sin observación')}</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div>`).join('');
54
+ const evidenceRows = history.length === 0 ? '<tr><td colspan="4" class="empty">No hay evidencia de impacto disponible en este snapshot.</td></tr>' : history.map((item) => `<tr><td>ciclo</td><td>${escapeHtml(item.label)}</td><td><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><br>${escapeHtml(item.detail ?? '—')}</td><td>—</td></tr>`).join('');
55
+ return `<div data-project-evidence role="group" aria-labelledby="project-evidence-heading"><h3 id="project-evidence-heading" class="sr-only">Project evidence</h3><div class="evidence-grid compact-composition"><div><h3>Planes de trabajo</h3>${planCards}</div><div><h3>Impacto y trazabilidad</h3><table class="evidence-table"><thead><tr><th scope="col">Tipo</th><th scope="col">Fuente</th><th scope="col">Estado</th><th scope="col">Fecha</th></tr></thead><tbody>${evidenceRows}</tbody></table></div></div><div class="closure-actions" data-closure-actions role="group" aria-labelledby="closure-actions-heading"><div><h3 id="closure-actions-heading">Qué falta para cerrar el ciclo</h3><p class="empty">La ausencia de marcador no equivale a cero hallazgos.</p></div><div><button type="button" disabled aria-describedby="static-controls-note">Completar QA (estático)</button><button type="button" disabled aria-describedby="static-controls-note">Registrar retro (estático)</button><button type="button" disabled aria-describedby="static-controls-note">Adjuntar evidencia (estático)</button></div></div></div>`;
50
56
  }
51
57
  function projectComposition(snapshot) {
52
58
  const stageSections = ['planning', 'execution', 'qa', 'retro', 'history'];
@@ -62,7 +68,7 @@ function projectComposition(snapshot) {
62
68
  const prepNames = ['installation', 'sensors', 'persistence'];
63
69
  const preparation = `<div class="machine-preparation-strip" data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"><h3 id="machine-preparation-heading">Preparación de máquina</h3><ul class="diagnostic-grid">${prepNames.map((name, index) => { const item = prepItems[index]; return `<li data-machine-preparation-card="${name}">${item ? `<span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'Sin detalle adicional'}</span>` : '<span class="state unavailable">⊘ Unavailable</span><strong>Sin observación</strong><span>Fuente no disponible</span>'}</li>`; }).join('')}</ul></div>`;
64
70
  const machineSupplement = `${preparation}<nav class="lifecycle-timeline" data-lifecycle-timeline aria-labelledby="lifecycle-timeline-heading"><h3 id="lifecycle-timeline-heading">Línea de ciclo</h3><ol class="timeline connected-timeline">${stages}</ol></nav>${provisional}`;
65
- const historySupplement = projectEvidenceComposition();
71
+ const historySupplement = projectEvidenceComposition(snapshot);
66
72
  return snapshot.sections.map((section) => sectionHtml(section, section.id === 'machine' ? machineSupplement : section.id === 'history' ? historySupplement : '')).join('');
67
73
  }
68
74
  /** Renders a portable, static, share-safe dashboard document. */
@@ -28,6 +28,8 @@ function renderFullTerminal(input) {
28
28
  ];
29
29
  for (const section of snapshot.sections) {
30
30
  lines.push('', SECTION_TITLES[section.id]);
31
+ if (section.id === 'history')
32
+ lines.push(` Eligible evidence rows: ${section.items.length}`);
31
33
  if (section.availability !== 'available')
32
34
  lines.push(` ⊘ source ${section.availability.replace('_', ' ')}`);
33
35
  if (section.items.length === 0)
@@ -0,0 +1,46 @@
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.captureCycleEvidence = captureCycleEvidence;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const types_1 = require("./types");
9
+ const hash = (value) => crypto_1.default.createHash('sha256').update(value).digest('hex');
10
+ const object = (value, name) => { if (!value || typeof value !== 'object' || Array.isArray(value))
11
+ throw new Error(`${name} must be an object`); return value; };
12
+ const iso = (value, name) => { if (typeof value !== 'string' || Number.isNaN(Date.parse(value)))
13
+ throw new Error(`${name} must be an ISO timestamp`); return value; };
14
+ const nonNegative = (value, name) => { if (!Number.isSafeInteger(value) || value < 0)
15
+ throw new Error(`${name} must be a non-negative integer`); return value; };
16
+ function captureCycleEvidence(input) {
17
+ if (!input || typeof input.root !== 'string' || !input.root)
18
+ throw new Error('capture root is required');
19
+ if (typeof input.repositoryIdentity !== 'string' || input.repositoryIdentity.length === 0 || input.repositoryIdentity.length > 4096 || /[\r\n]/.test(input.repositoryIdentity))
20
+ throw new Error('repository identity is invalid');
21
+ const repositoryIdentity = hash(input.repositoryIdentity);
22
+ if (typeof input.planPath !== 'string' || !input.planPath)
23
+ throw new Error('capture planPath is required');
24
+ const journal = object(input.journal, 'journal');
25
+ const cycle = object(journal.cycle, 'journal cycle');
26
+ const startedAt = iso(cycle.startedAt, 'cycle startedAt');
27
+ const endedAt = iso(cycle.completedAt ?? journal.controllerHeartbeatAt, 'cycle endedAt');
28
+ if (cycle.status !== 'COMPLETE' && cycle.status !== 'BLOCKED')
29
+ throw new Error('journal cycle must be complete or blocked');
30
+ if (!Array.isArray(journal.tasks) || !Array.isArray(journal.verdicts) || !Array.isArray(journal.fixes) || !Array.isArray(input.gates) || !Array.isArray(input.ledger))
31
+ throw new Error('capture sources are invalid');
32
+ const tasks = journal.tasks.map((item, index) => { const task = object(item, `task ${index}`); if (typeof task.id !== 'string')
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)
37
+ 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;
40
+ const gates = input.gates.map((item, index) => { const gate = object(item, `gate ${index}`); if (typeof gate.required !== 'boolean' || typeof gate.passed !== 'boolean')
41
+ throw new Error('gate is invalid'); return gate; }).filter((gate) => gate.required === true);
42
+ const cures = input.ledger.filter((item, index) => { const entry = object(item, `ledger ${index}`); if (typeof entry.signature !== 'string' || (entry.polarity !== 'finding' && entry.polarity !== 'win'))
43
+ throw new Error('ledger entry is invalid'); iso(entry.ts, 'ledger timestamp'); return entry.polarity === 'win'; }).map((entry) => { const source = object(entry, 'ledger'); return { signature: hash(`signature:${source.signature}`), curedAt: iso(source.ts, 'ledger timestamp') }; });
44
+ const pr = input.pr === undefined ? undefined : (() => { const raw = object(input.pr, 'pr'); return { provider: raw.provider, number: raw.number }; })();
45
+ return (0, types_1.validateCycleEvidence)({ schema: 1, cycleId: hash(`${repositoryIdentity}\0${input.planPath}\0${startedAt}`), startedAt, endedAt, durationMs: Date.parse(endedAt) - Date.parse(startedAt), cycleState: cycle.status === 'COMPLETE' ? 'completed' : 'blocked', plan: { ref: input.planPath, state: input.planState ?? (cycle.status === 'BLOCKED' ? 'blocked' : 'executed') }, tasks, qa: { findings: signatures.length, fixes: Math.min(fixes, signatures.length), signatures }, gates: { required: gates.length, firstEvaluationsPassed: gates.map((gate) => gate.passed), firstPass: gates.every((gate) => gate.passed === true) }, cures, ...(pr ? { pr } : {}) });
46
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.confidenceForCycles = confidenceForCycles;
4
+ exports.classifyCure = classifyCure;
5
+ exports.buildEvidenceHistory = buildEvidenceHistory;
6
+ const types_1 = require("./types");
7
+ function confidenceForCycles(count) {
8
+ if (!Number.isSafeInteger(count) || count < 0)
9
+ throw new Error('eligible cycle count must be a non-negative integer');
10
+ const eligibleCycles = count;
11
+ if (eligibleCycles === 0)
12
+ return 'none';
13
+ if (eligibleCycles === 1)
14
+ return 'provisional';
15
+ if (eligibleCycles < 5)
16
+ return 'observing';
17
+ return 'supported';
18
+ }
19
+ function classifyCure(input) {
20
+ if (!input || typeof input !== 'object' || Array.isArray(input))
21
+ throw new Error('cure observation must be an object');
22
+ const value = input;
23
+ if (Object.keys(value).some((key) => key !== 'laterEligibleCycles' && key !== 'recurred') || !Number.isSafeInteger(value.laterEligibleCycles) || value.laterEligibleCycles < 0 || typeof value.recurred !== 'boolean')
24
+ throw new Error('cure observation is invalid');
25
+ const laterEligibleCycles = value.laterEligibleCycles;
26
+ if (value.recurred)
27
+ return 'recurred';
28
+ if (laterEligibleCycles === 0)
29
+ return 'awaiting_observation';
30
+ return laterEligibleCycles >= 3 ? 'supported' : 'observing';
31
+ }
32
+ /** Validates, retains, and deterministically orders every eligible local observation. */
33
+ function buildEvidenceHistory(records) {
34
+ if (!Array.isArray(records))
35
+ throw new Error('evidence history records must be an array');
36
+ const valid = records.map((record) => (0, types_1.validateCycleEvidence)(record)).sort((left, right) => left.startedAt.localeCompare(right.startedAt) || left.cycleId.localeCompare(right.cycleId));
37
+ const completed = valid.filter((cycle) => cycle.cycleState === 'completed');
38
+ const cycles = valid.map((cycle, index) => ({
39
+ ...cycle,
40
+ retries: cycle.tasks.reduce((total, task) => total + task.retries, 0),
41
+ cureEfficacy: cycle.cures.map((cure) => {
42
+ const later = valid.slice(index + 1).filter((candidate) => candidate.cycleState === 'completed');
43
+ return {
44
+ signature: cure.signature,
45
+ efficacy: classifyCure({ laterEligibleCycles: later.length, recurred: later.some((candidate) => candidate.qa.signatures.includes(cure.signature)) }),
46
+ };
47
+ }),
48
+ }));
49
+ return { confidence: confidenceForCycles(completed.length), empty: cycles.length === 0, cycles };
50
+ }
@@ -0,0 +1,32 @@
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.writeCycleEvidence = writeCycleEvidence;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const atomic_file_1 = require("../atomic-file");
10
+ const types_1 = require("./types");
11
+ function writeCycleEvidence(root, evidence) {
12
+ if (typeof root !== 'string' || root.length === 0)
13
+ throw new Error('evidence root must be a non-empty string');
14
+ const valid = (0, types_1.validateCycleEvidence)(evidence);
15
+ const safeRoot = safeDirectory(root, 'root');
16
+ let directory = safeRoot;
17
+ for (const segment of ['.awm', 'evidence', 'cycles']) {
18
+ directory = path_1.default.join(directory, segment);
19
+ if (!fs_1.default.existsSync(directory))
20
+ fs_1.default.mkdirSync(directory, { mode: 0o700 });
21
+ directory = safeDirectory(directory, `evidence ${segment}`);
22
+ }
23
+ const file = path_1.default.join(directory, `${valid.cycleId}.json`);
24
+ (0, atomic_file_1.writeFileAtomicDurable)(file, JSON.stringify(valid, null, 2) + '\n', 0o600);
25
+ return valid;
26
+ }
27
+ function safeDirectory(directory, label) {
28
+ const stat = fs_1.default.lstatSync(directory);
29
+ if (stat.isSymbolicLink() || !stat.isDirectory())
30
+ throw new Error(`unsafe evidence ${label} directory symlink or non-directory`);
31
+ return fs_1.default.realpathSync(directory);
32
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLAN_STATES = void 0;
4
+ exports.validateCycleEvidence = validateCycleEvidence;
5
+ exports.PLAN_STATES = ['active', 'blocked', 'qa_pending', 'retro_pending', 'executed', 'legacy_unverifiable'];
6
+ const DIGEST = /^[a-f0-9]{64}$/;
7
+ const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
8
+ function record(value, label) {
9
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
10
+ throw new Error(`${label} must be an object`);
11
+ return value;
12
+ }
13
+ function keys(value, label, allowed) {
14
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
15
+ throw new Error(`${label} has unsupported fields`);
16
+ }
17
+ function timestamp(value, label) {
18
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) || Number.isNaN(Date.parse(value)) || new Date(value).toISOString() !== value)
19
+ throw new Error(`${label} must be a canonical ISO timestamp`);
20
+ return value;
21
+ }
22
+ function count(value, label) {
23
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
24
+ throw new Error(`${label} must be a non-negative integer`);
25
+ return value;
26
+ }
27
+ function digest(value, label) {
28
+ if (typeof value !== 'string' || !DIGEST.test(value))
29
+ throw new Error(`${label} must be a SHA-256 digest`);
30
+ return value;
31
+ }
32
+ function relativePlanRef(value) {
33
+ if (typeof value !== 'string' || !value || value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value)
34
+ || value.split('/').some((part) => part === '' || part === '.' || part === '..'))
35
+ throw new Error('plan.ref must be repo-relative');
36
+ return value;
37
+ }
38
+ /** Strict public durable-boundary validator: accepted evidence intentionally has no prose or identities. */
39
+ function validateCycleEvidence(input) {
40
+ const value = record(input, 'cycle evidence');
41
+ keys(value, 'cycle evidence', ['schema', 'cycleId', 'startedAt', 'endedAt', 'durationMs', 'cycleState', 'plan', 'tasks', 'qa', 'gates', 'cures', 'pr']);
42
+ if (value.schema !== 1)
43
+ throw new Error('cycle evidence schema must be 1');
44
+ const startedAt = timestamp(value.startedAt, 'startedAt');
45
+ const endedAt = timestamp(value.endedAt, 'endedAt');
46
+ const durationMs = count(value.durationMs, 'durationMs');
47
+ if (Date.parse(endedAt) < Date.parse(startedAt) || durationMs !== Date.parse(endedAt) - Date.parse(startedAt))
48
+ throw new Error('cycle duration is invalid');
49
+ if (value.cycleState !== 'completed' && value.cycleState !== 'blocked')
50
+ throw new Error('cycleState is invalid');
51
+ const plan = record(value.plan, 'plan');
52
+ keys(plan, 'plan', ['ref', 'state']);
53
+ if (typeof plan.state !== 'string' || !exports.PLAN_STATES.includes(plan.state))
54
+ throw new Error('plan state is invalid');
55
+ if (!Array.isArray(value.tasks))
56
+ throw new Error('tasks must be an array');
57
+ const tasks = value.tasks.map((item, index) => {
58
+ const task = record(item, `tasks[${index}]`);
59
+ keys(task, `tasks[${index}]`, ['id', 'attempts', 'retries']);
60
+ if (typeof task.id !== 'string' || !SAFE_ID.test(task.id))
61
+ throw new Error('task id is invalid');
62
+ const attempts = count(task.attempts, 'task attempts');
63
+ const retries = count(task.retries, 'task retries');
64
+ if (retries !== Math.max(attempts - 1, 0))
65
+ throw new Error('task retries must derive from attempts');
66
+ return { id: task.id, attempts, retries };
67
+ });
68
+ const qa = record(value.qa, 'qa');
69
+ keys(qa, 'qa', ['findings', 'fixes', 'signatures']);
70
+ if (!Array.isArray(qa.signatures))
71
+ throw new Error('qa signatures must be an array');
72
+ const signatures = qa.signatures.map((signature, index) => digest(signature, `qa.signatures[${index}]`));
73
+ const findings = count(qa.findings, 'qa findings');
74
+ const fixes = count(qa.fixes, 'qa fixes');
75
+ if (signatures.length !== findings || fixes > findings)
76
+ throw new Error('qa counts are inconsistent');
77
+ const gates = record(value.gates, 'gates');
78
+ keys(gates, 'gates', ['required', 'firstEvaluationsPassed', 'firstPass']);
79
+ const required = count(gates.required, 'required gates');
80
+ if (!Array.isArray(gates.firstEvaluationsPassed) || !gates.firstEvaluationsPassed.every((passed) => typeof passed === 'boolean') || gates.firstEvaluationsPassed.length !== required || typeof gates.firstPass !== 'boolean' || gates.firstPass !== gates.firstEvaluationsPassed.every(Boolean))
81
+ throw new Error('gate evaluations are inconsistent');
82
+ if (!Array.isArray(value.cures))
83
+ throw new Error('cures must be an array');
84
+ const cures = value.cures.map((item, index) => { const cure = record(item, `cures[${index}]`); keys(cure, `cures[${index}]`, ['signature', 'curedAt']); return { signature: digest(cure.signature, 'cure signature'), curedAt: timestamp(cure.curedAt, 'curedAt') }; });
85
+ let pr;
86
+ if (value.pr !== undefined) {
87
+ const raw = record(value.pr, 'pr');
88
+ keys(raw, 'pr', ['provider', 'number']);
89
+ if (raw.provider !== 'github' && raw.provider !== 'gitlab' && raw.provider !== 'other')
90
+ throw new Error('pr provider is invalid');
91
+ const number = count(raw.number, 'pr number');
92
+ if (number < 1)
93
+ throw new Error('pr number is invalid');
94
+ pr = { provider: raw.provider, number };
95
+ }
96
+ return { schema: 1, cycleId: digest(value.cycleId, 'cycleId'), startedAt, endedAt, durationMs, cycleState: value.cycleState, plan: { ref: relativePlanRef(plan.ref), state: plan.state }, tasks, qa: { findings, fixes, signatures }, gates: { required, firstEvaluationsPassed: [...gates.firstEvaluationsPassed], firstPass: gates.firstPass }, cures, ...(pr ? { pr } : {}) };
97
+ }
package/dist/src/index.js CHANGED
@@ -41,6 +41,7 @@ const agent_1 = require("./commands/agent");
41
41
  const job_1 = require("./commands/job");
42
42
  const watch_1 = require("./commands/watch");
43
43
  const track_1 = require("./commands/track");
44
+ const evidence_1 = require("./commands/evidence");
44
45
  const add_1 = require("./commands/add");
45
46
  const sync_1 = require("./commands/sync");
46
47
  const update_1 = require("./commands/update");
@@ -733,6 +734,7 @@ miroCmd.command('sync <storyMapPath>')
733
734
  (0, job_1.registerJobCommand)(program);
734
735
  (0, watch_1.registerWatchCommand)(program);
735
736
  (0, track_1.registerTrackCommand)(program);
737
+ (0, evidence_1.registerEvidenceCommand)(program);
736
738
  // Commander only waits for async action handlers through parseAsync(). The CLI has
737
739
  // async commands (including `sensors coverage`), so returning its promise keeps the
738
740
  // process alive until their JSON/output contract has been completed.
@@ -1,7 +1,15 @@
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");
4
7
  const sanitize_1 = require("../../../src/core/dashboard/sanitize");
8
+ const store_1 = require("../../../src/core/evidence/store");
9
+ const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
5
13
  const fixedNow = '2026-08-22T00:00:00.000Z';
6
14
  describe('collectDashboardSnapshot', () => {
7
15
  it('returns only a healthy machine section outside a project', () => {
@@ -35,13 +43,13 @@ describe('collectDashboardSnapshot', () => {
35
43
  machine: () => ({ findings: [{ id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' }] }),
36
44
  project: () => ({ label: 'Demo', findings: [{ id: 'project.profile.missing', label: 'Profile', state: 'missing' }] }),
37
45
  plans: () => Array.from({ length: 2000 }, (_, index) => ({ id: `plan.${index}`, label: `Plan ${index}`, state: 'ok' })),
38
- execution: () => ({ history: Array.from({ length: 500 }, (_, index) => ({ id: `history.${index}`, label: `History ${index}`, state: 'ok' })) }),
46
+ execution: () => ({}),
39
47
  },
40
48
  });
41
49
  expect(snapshot.sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history']);
42
50
  expect(snapshot.sections.find((section) => section.id === 'machine')?.items[0].remediation).toBe('awm init');
43
51
  expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toHaveLength(2000);
44
- expect(snapshot.sections.find((section) => section.id === 'history')?.items).toHaveLength(500);
52
+ expect(snapshot.sections.find((section) => section.id === 'history')?.items).toHaveLength(0);
45
53
  expect(JSON.stringify(snapshot)).not.toMatch(/score|ranking/i);
46
54
  });
47
55
  it('isolates optional adapter failures and omits unverified remediation', () => {
@@ -62,7 +70,7 @@ describe('collectDashboardSnapshot', () => {
62
70
  cwd: process.cwd(), now: fixedNow,
63
71
  adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined },
64
72
  });
65
- for (const id of ['execution', 'qa', 'retro', 'history']) {
73
+ for (const id of ['execution', 'qa', 'retro']) {
66
74
  expect(snapshot.sections.find((section) => section.id === id)?.availability).toBe('unavailable');
67
75
  }
68
76
  });
@@ -83,7 +91,7 @@ describe('collectDashboardSnapshot', () => {
83
91
  expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
84
92
  expect(snapshot.sections.find((section) => section.id === 'planning')?.availability).toBe('available');
85
93
  });
86
- it.each(['execution', 'qa', 'retro', 'history'])('isolates malformed %s findings', (key) => {
94
+ it.each(['execution', 'qa', 'retro'])('isolates malformed %s findings', (key) => {
87
95
  const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => ({ [key]: [{ id: '', label: 'Profile', state: 'ok' }] }) } });
88
96
  expect(snapshot.sections.find((section) => section.id === key)?.availability).toBe('unavailable');
89
97
  });
@@ -142,6 +150,67 @@ describe('collectDashboardSnapshot', () => {
142
150
  expect(snapshot.sections.map((section) => section.id)).toEqual(['machine']);
143
151
  expect(snapshot.overall).toBe('degraded');
144
152
  });
153
+ it('loads validated local evidence only after a project is detected and renders every cycle fact', () => {
154
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
155
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
156
+ (0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)());
157
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
158
+ const history = snapshot.sections.find((section) => section.id === 'history');
159
+ expect(snapshot.confidence).toBe('provisional');
160
+ expect(history?.availability).toBe('available');
161
+ expect(history?.items[0]?.detail).toContain('retries 1; QA 1/1; first-pass yes; cures awaiting_observation');
162
+ fs_1.default.rmSync(root, { recursive: true, force: true });
163
+ });
164
+ it.each(['.awm', 'evidence', 'cycles'])('does not follow a symlinked %s evidence ancestor', (ancestor) => {
165
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
166
+ const external = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-external-'));
167
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
168
+ const externalCycles = ancestor === '.awm' ? path_1.default.join(external, 'evidence', 'cycles') : path_1.default.join(external, 'cycles');
169
+ fs_1.default.mkdirSync(externalCycles, { recursive: true });
170
+ fs_1.default.writeFileSync(path_1.default.join(externalCycles, `${'a'.repeat(64)}.json`), JSON.stringify((0, evidence_fixtures_1.cycleEvidenceFixture)()));
171
+ if (ancestor === '.awm')
172
+ fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm'));
173
+ else if (ancestor === 'evidence') {
174
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm'));
175
+ fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm', 'evidence'));
176
+ }
177
+ else {
178
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm', 'evidence'), { recursive: true });
179
+ fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm', 'evidence', 'cycles'));
180
+ }
181
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
182
+ const history = snapshot.sections.find((section) => section.id === 'history');
183
+ expect(history).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
184
+ fs_1.default.rmSync(root, { recursive: true, force: true });
185
+ fs_1.default.rmSync(external, { recursive: true, force: true });
186
+ });
187
+ it('does not follow a symlinked evidence file', () => {
188
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
189
+ const external = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-external-'));
190
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
191
+ const cycleId = 'a'.repeat(64);
192
+ const externalFile = path_1.default.join(external, `${cycleId}.json`);
193
+ fs_1.default.writeFileSync(externalFile, JSON.stringify((0, evidence_fixtures_1.cycleEvidenceFixture)()));
194
+ const directory = path_1.default.join(root, '.awm', 'evidence', 'cycles');
195
+ fs_1.default.mkdirSync(directory, { recursive: true });
196
+ fs_1.default.symlinkSync(externalFile, path_1.default.join(directory, `${cycleId}.json`));
197
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
198
+ expect(snapshot.sections.find((section) => section.id === 'history')).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
199
+ fs_1.default.rmSync(root, { recursive: true, force: true });
200
+ fs_1.default.rmSync(external, { recursive: true, force: true });
201
+ });
202
+ it.each(['duplicate', 'mismatch'])('does not accept %s evidence filenames', (caseName) => {
203
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
204
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
205
+ (0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)());
206
+ const directory = path_1.default.join(root, '.awm', 'evidence', 'cycles');
207
+ const record = (0, evidence_fixtures_1.cycleEvidenceFixture)();
208
+ const filename = caseName === 'duplicate' ? `${'b'.repeat(64)}.json` : `${'b'.repeat(64)}.json`;
209
+ fs_1.default.writeFileSync(path_1.default.join(directory, filename), JSON.stringify(caseName === 'duplicate' ? record : { ...record, cycleId: 'c'.repeat(64) }));
210
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
211
+ expect(snapshot.sections.find((section) => section.id === 'history')).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
212
+ fs_1.default.rmSync(root, { recursive: true, force: true });
213
+ });
145
214
  });
146
215
  describe('sanitizeDashboardSource', () => {
147
216
  it('removes hostile paths and secrets before rendering', () => {
@@ -172,4 +172,20 @@ describe('renderDashboardHtml', () => {
172
172
  expect(first).toContain('Cycle 499');
173
173
  expect(first).toContain('Task 1999');
174
174
  });
175
+ it('renders supplied plans and evidence facts in the dense project panels', () => {
176
+ const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({
177
+ confidence: 'supported',
178
+ sections: [
179
+ { id: 'machine', availability: 'available', items: [] }, { id: 'project', availability: 'available', items: [] },
180
+ { id: 'planning', availability: 'available', items: [{ id: 'plan.alpha', label: 'Impact plan', state: 'ok', detail: 'executed' }] },
181
+ { id: 'execution', availability: 'available', items: [] }, { id: 'qa', availability: 'available', items: [] }, { id: 'retro', availability: 'available', items: [] },
182
+ { id: 'history', availability: 'available', items: [{ id: 'history.cycle.abc', label: 'Cycle abc', state: 'ok', detail: 'plan executed; tasks 3; retries 2; QA 2/2; first-pass yes; cures supported' }] },
183
+ ],
184
+ })));
185
+ expect(html).toContain('Confidence: supported');
186
+ expect(html).toContain('Impact plan');
187
+ expect(html).toContain('Cycle abc');
188
+ expect(html).toContain('retries 2; QA 2/2; first-pass yes; cures supported');
189
+ expect(html).not.toContain('Progreso: sin observación');
190
+ });
175
191
  });
@@ -62,4 +62,11 @@ describe('renderFullTerminal', () => {
62
62
  expect(first).toContain('Cycle 499');
63
63
  expect(first).not.toMatch(/score|ranking/i);
64
64
  });
65
+ it('renders explicit confidence and every evidence metric without improvement claims', () => {
66
+ const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot({ confidence: 'supported' })));
67
+ expect(output).toContain('Confidence: supported');
68
+ expect(output).toContain('Eligible evidence rows: 1');
69
+ expect(output).toContain('5 minutes');
70
+ expect(output).not.toMatch(/trend|percentage|improvement/i);
71
+ });
65
72
  });
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const capture_1 = require("../../../src/core/evidence/capture");
4
+ describe('captureCycleEvidence', () => {
5
+ test('derives counts and opaque signatures without journal prose or identities', () => {
6
+ const evidence = (0, capture_1.captureCycleEvidence)({
7
+ root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git',
8
+ planPath: 'plans/release.md',
9
+ journal: {
10
+ cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:10.000Z' },
11
+ journalId: 'local-repository-identity',
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' }],
14
+ fixes: [{ verdictId: 'v1', closed: true }],
15
+ },
16
+ gates: [{ required: true, passed: true }],
17
+ ledger: [{ signature: 'unsafe-input', polarity: 'win', ts: '2026-08-22T10:00:06.000Z' }],
18
+ pr: { provider: 'github', number: 12 },
19
+ });
20
+ expect(evidence).toMatchObject({
21
+ schema: 1, cycleState: 'completed', durationMs: 10_000,
22
+ plan: { ref: 'plans/release.md' }, tasks: [{ id: 'task-a', attempts: 3, retries: 2 }],
23
+ qa: { findings: 1, fixes: 1 }, gates: { required: 1, firstEvaluationsPassed: [true], firstPass: true },
24
+ pr: { provider: 'github', number: 12 },
25
+ });
26
+ expect(evidence.cycleId).toMatch(/^[a-f0-9]{64}$/);
27
+ expect(evidence.qa.signatures[0]).toMatch(/^[a-f0-9]{64}$/);
28
+ expect(evidence.cures[0]).toMatchObject({ signature: expect.stringMatching(/^[a-f0-9]{64}$/) });
29
+ expect(evidence.cures[0].signature).toBe(evidence.qa.signatures[0]);
30
+ expect(JSON.stringify(evidence)).not.toContain('Alice');
31
+ expect(JSON.stringify(evidence)).not.toContain('secret prompt');
32
+ });
33
+ test('derives cycle identity from stable repository identity, not checkout or journal identity', () => {
34
+ const source = {
35
+ planPath: 'plans/release.md', journal: { journalId: 'repository-identity', cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [] }, gates: [], ledger: [],
36
+ };
37
+ const first = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git' });
38
+ const second = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', journal: { ...source.journal, journalId: 'different-journal-identity' } });
39
+ const distinct = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:other/repository.git' });
40
+ expect(first.cycleId).toBe(second.cycleId);
41
+ expect(first.cycleId).not.toBe(distinct.cycleId);
42
+ expect(first.cycleState).toBe('blocked');
43
+ });
44
+ test('captures a blocked cycle from its durable controller heartbeat when completedAt is absent', () => {
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
+ expect(evidence).toMatchObject({ cycleState: 'blocked', endedAt: '2026-08-22T10:00:03.000Z', durationMs: 3_000 });
47
+ });
48
+ });
@@ -0,0 +1,37 @@
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 evidence_1 = require("../../../src/commands/evidence");
10
+ describe('evidence capture CLI boundary', () => {
11
+ let root;
12
+ beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-command-')); });
13
+ afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
14
+ test('returns exit 2 for a missing or invalid plan', () => {
15
+ expect((0, evidence_1.runEvidenceCapture)(root, undefined).code).toBe(2);
16
+ expect((0, evidence_1.runEvidenceCapture)(root, '../secret.md').code).toBe(2);
17
+ });
18
+ test('returns exactly the captured cycle id on success', () => {
19
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
20
+ expect((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: [] })).toEqual(expect.objectContaining({ code: 0, stdout: expect.stringMatching(/^[a-f0-9]{64}\n$/) }));
21
+ });
22
+ test('uses the first attempted gate evaluation instead of final satisfaction', () => {
23
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
24
+ 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: [], cycleVerificationPlan: [{ id: 'gate', kind: 'test', satisfiedBy: 'retry' }], jobs: { first: { id: 'first', satisfies: ['gate'], verdict: 'fail', phaseTimestamps: { received: '2026-08-22T10:00:00.000Z' } }, retry: { id: 'retry', satisfies: ['gate'], attemptOf: 'first', verdict: 'pass', phaseTimestamps: { received: '2026-08-22T10:00:01.000Z' } } } }, ledger: [] });
25
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
26
+ });
27
+ test('orders independent root evaluations by durable timestamp then id', () => {
28
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
29
+ 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:03.000Z' }, tasks: [], verdicts: [], fixes: [], cycleVerificationPlan: [{ id: 'gate', kind: 'test', satisfiedBy: 'late-pass' }], jobs: { late: { id: 'late-pass', satisfies: ['gate'], verdict: 'pass', phaseTimestamps: { received: '2026-08-22T10:00:02.000Z' } }, early: { id: 'early-fail', satisfies: ['gate'], verdict: 'fail', phaseTimestamps: { received: '2026-08-22T10:00:01.000Z' } } } }, ledger: [] });
30
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
31
+ });
32
+ test('uses the first review verdict rather than satisfiedBy final verdict', () => {
33
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
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
+ 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
+ });
37
+ });
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const history_1 = require("../../../src/core/evidence/history");
4
+ const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
5
+ describe('evidence history', () => {
6
+ it.each([[0, 'none'], [1, 'provisional'], [2, 'observing'], [4, 'observing'], [5, 'supported']])('classifies %i eligible cycles as %s', (count, expected) => expect((0, history_1.confidenceForCycles)(count)).toBe(expected));
7
+ it.each([
8
+ [0, false, 'awaiting_observation'], [1, false, 'observing'], [2, false, 'observing'],
9
+ [3, false, 'supported'], [1, true, 'recurred'],
10
+ ])('classifies cure observation honestly', (laterEligibleCycles, recurred, expected) => {
11
+ expect((0, history_1.classifyCure)({ laterEligibleCycles, recurred })).toBe(expected);
12
+ });
13
+ it('validates every record, retains every eligible row, and sorts stably by timestamp and cycle id', () => {
14
+ const later = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'd'.repeat(64), startedAt: '2026-08-22T11:00:00.000Z', endedAt: '2026-08-22T11:01:00.000Z', cures: [] };
15
+ const sameTimestamp = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: '0'.repeat(64), cures: [] };
16
+ const history = (0, history_1.buildEvidenceHistory)([later, (0, evidence_fixtures_1.cycleEvidenceFixture)(), sameTimestamp]);
17
+ expect(history.confidence).toBe('observing');
18
+ expect(history.empty).toBe(false);
19
+ expect(history.cycles.map((cycle) => cycle.cycleId)).toEqual(['0'.repeat(64), 'a'.repeat(64), 'd'.repeat(64)]);
20
+ expect(() => (0, history_1.buildEvidenceHistory)([{ ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), durationMs: -1 }])).toThrow(/duration/i);
21
+ });
22
+ it('reports zero cycles explicitly without a trend, percentage, or improvement claim', () => {
23
+ const history = (0, history_1.buildEvidenceHistory)([]);
24
+ expect(history).toEqual(expect.objectContaining({ empty: true, confidence: 'none', cycles: [] }));
25
+ expect(JSON.stringify(history)).not.toMatch(/trend|percentage|improvement/i);
26
+ });
27
+ it('retains blocked records but excludes them from confidence and cure observation windows', () => {
28
+ const blocked = Array.from({ length: 5 }, (_, index) => ({
29
+ ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: `${index}`.repeat(64), cycleState: 'blocked', cures: [],
30
+ }));
31
+ const history = (0, history_1.buildEvidenceHistory)(blocked);
32
+ expect(history.cycles).toHaveLength(5);
33
+ expect(history.confidence).toBe('none');
34
+ const cured = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'c'.repeat(64), endedAt: '2026-08-22T10:01:00.000Z' };
35
+ const laterBlocked = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'd'.repeat(64), cycleState: 'blocked', startedAt: '2026-08-22T11:00:00.000Z', endedAt: '2026-08-22T11:01:00.000Z', cures: [] };
36
+ const laterCompleted = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'e'.repeat(64), startedAt: '2026-08-22T12:00:00.000Z', endedAt: '2026-08-22T12:01:00.000Z', cures: [] };
37
+ const mixed = (0, history_1.buildEvidenceHistory)([cured, laterBlocked, laterCompleted]);
38
+ expect(mixed.confidence).toBe('observing');
39
+ expect(mixed.cycles[0].cureEfficacy[0].efficacy).toBe('observing');
40
+ });
41
+ });
@@ -0,0 +1,32 @@
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 evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
10
+ const store_1 = require("../../../src/core/evidence/store");
11
+ describe('cycle evidence store', () => {
12
+ let root;
13
+ beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-')); });
14
+ afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
15
+ test('durably replaces one observation for the same opaque cycle id', () => {
16
+ const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
17
+ (0, store_1.writeCycleEvidence)(root, evidence);
18
+ (0, store_1.writeCycleEvidence)(root, { ...evidence, qa: { ...evidence.qa, fixes: 0 } });
19
+ const dir = path_1.default.join(root, '.awm', 'evidence', 'cycles');
20
+ expect(fs_1.default.readdirSync(dir)).toEqual([`${evidence.cycleId}.json`]);
21
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, `${evidence.cycleId}.json`), 'utf8')).qa.fixes).toBe(0);
22
+ });
23
+ test('rejects symlinked evidence ancestors without writing outside root', () => {
24
+ if (process.platform === 'win32')
25
+ return;
26
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-outside-'));
27
+ fs_1.default.symlinkSync(outside, path_1.default.join(root, '.awm'));
28
+ expect(() => (0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)())).toThrow(/symlink/);
29
+ expect(fs_1.default.readdirSync(outside)).toEqual([]);
30
+ fs_1.default.rmSync(outside, { recursive: true, force: true });
31
+ });
32
+ });
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
4
+ const types_1 = require("../../../src/core/evidence/types");
5
+ describe('CycleEvidenceV1 validation', () => {
6
+ test('accepts the minimal privacy-preserving durable observation', () => {
7
+ expect((0, types_1.validateCycleEvidence)((0, evidence_fixtures_1.cycleEvidenceFixture)())).toEqual((0, evidence_fixtures_1.cycleEvidenceFixture)());
8
+ });
9
+ test.each([
10
+ ['unknown fields', { extra: 'raw prose' }],
11
+ ['absolute plan refs', { plan: { ref: '/Users/alice/plans/current.md', state: 'executed' } }],
12
+ ['raw prose', { qa: { findings: 1, fixes: 1, signatures: ['found alice failed a secret prompt'] } }],
13
+ ['host identities', { pr: { provider: 'github', number: 42, repository: 'alice/private' } }],
14
+ ['invalid retry totals', { tasks: [{ id: 'task-1', attempts: 2, retries: 0 }] }],
15
+ ['non-canonical timestamp', { startedAt: '2026-08-22T10:00:00Z' }],
16
+ ])('rejects %s', (_label, patch) => {
17
+ expect(() => (0, types_1.validateCycleEvidence)({ ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), ...patch })).toThrow();
18
+ });
19
+ test('stores first gate evaluations as booleans, not an aggregate count', () => {
20
+ const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
21
+ expect((0, types_1.validateCycleEvidence)({ ...evidence, gates: { required: 2, firstEvaluationsPassed: [true, false], firstPass: false } }).gates.firstEvaluationsPassed).toEqual([true, false]);
22
+ });
23
+ test('permits every dashboard plan state and an absent PR', () => {
24
+ for (const state of ['active', 'blocked', 'qa_pending', 'retro_pending', 'executed', 'legacy_unverifiable']) {
25
+ const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
26
+ delete evidence.pr;
27
+ expect((0, types_1.validateCycleEvidence)({ ...evidence, plan: { ...evidence.plan, state } }).pr).toBeUndefined();
28
+ }
29
+ });
30
+ });
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cycleEvidenceFixture = void 0;
4
+ const cycleEvidenceFixture = () => ({
5
+ schema: 1,
6
+ cycleId: 'a'.repeat(64),
7
+ startedAt: '2026-08-22T10:00:00.000Z',
8
+ endedAt: '2026-08-22T10:01:00.000Z',
9
+ durationMs: 60_000,
10
+ cycleState: 'completed',
11
+ plan: { ref: 'plans/current.md', state: 'executed' },
12
+ tasks: [{ id: 'task-1', attempts: 2, retries: 1 }],
13
+ qa: { findings: 1, fixes: 1, signatures: ['b'.repeat(64)] },
14
+ gates: { required: 1, firstEvaluationsPassed: [true], firstPass: true },
15
+ cures: [{ signature: 'c'.repeat(64), curedAt: '2026-08-22T10:00:30.000Z' }],
16
+ });
17
+ exports.cycleEvidenceFixture = cycleEvidenceFixture;
@@ -0,0 +1,177 @@
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 crypto_1 = __importDefault(require("crypto"));
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const cli = path_1.default.resolve(__dirname, '../../dist/src/index.js');
12
+ function treeHash(root, ignored = new Set()) {
13
+ const hash = crypto_1.default.createHash('sha256');
14
+ const walk = (directory) => {
15
+ for (const entry of fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
16
+ const item = path_1.default.join(directory, entry.name);
17
+ const relative = path_1.default.relative(root, item).split(path_1.default.sep).join('/');
18
+ if (ignored.has(relative))
19
+ continue;
20
+ hash.update(relative);
21
+ if (entry.isDirectory())
22
+ walk(item);
23
+ else if (entry.isFile())
24
+ hash.update(fs_1.default.readFileSync(item));
25
+ }
26
+ };
27
+ walk(root);
28
+ return hash.digest('hex');
29
+ }
30
+ function treeEntries(root, ignored = new Set()) {
31
+ const entries = [];
32
+ const walk = (directory) => {
33
+ for (const entry of fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
34
+ const item = path_1.default.join(directory, entry.name);
35
+ const relative = path_1.default.relative(root, item).split(path_1.default.sep).join('/');
36
+ if (ignored.has(relative))
37
+ continue;
38
+ entries.push(relative);
39
+ if (entry.isDirectory())
40
+ walk(item);
41
+ }
42
+ };
43
+ walk(root);
44
+ return entries;
45
+ }
46
+ function fixture(name, project = true) {
47
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), `awm-doctor-${name}-`));
48
+ const directory = project ? path_1.default.join(root, 'project') : path_1.default.join(root, 'machine-only');
49
+ const home = path_1.default.join(root, 'home');
50
+ fs_1.default.mkdirSync(directory, { recursive: true });
51
+ fs_1.default.mkdirSync(home, { recursive: true });
52
+ if (project)
53
+ fs_1.default.writeFileSync(path_1.default.join(directory, 'package.json'), JSON.stringify({ name, private: true }));
54
+ return { root, project: directory, home };
55
+ }
56
+ function command(f, ...args) {
57
+ return (0, child_process_1.spawnSync)(process.execPath, [cli, ...args], {
58
+ cwd: f.project,
59
+ encoding: 'utf8',
60
+ // The dashboard examines provider homes too. Do not inherit the
61
+ // runner's HOME/CODEX_HOME: that turns the machine-only fixture into
62
+ // a report about whatever agent state happens to be installed on CI.
63
+ env: {
64
+ ...process.env,
65
+ HOME: f.home,
66
+ USERPROFILE: f.home,
67
+ CODEX_HOME: path_1.default.join(f.home, '.codex'),
68
+ AWM_HOME: f.home,
69
+ AWM_NO_UPDATE_CHECK: '1',
70
+ },
71
+ });
72
+ }
73
+ function snapshot(result) {
74
+ if (!result.stdout.trim())
75
+ throw new Error(`doctor emitted no JSON: ${result.stderr}`);
76
+ return JSON.parse(result.stdout);
77
+ }
78
+ function writeEvidence(f, id) {
79
+ const directory = path_1.default.join(f.project, '.awm', 'evidence', 'cycles');
80
+ fs_1.default.mkdirSync(directory, { recursive: true });
81
+ fs_1.default.writeFileSync(path_1.default.join(directory, `${id}.json`), JSON.stringify({
82
+ schema: 1, cycleId: id, startedAt: '2026-08-22T10:00:00.000Z', endedAt: '2026-08-22T10:01:00.000Z', durationMs: 60_000,
83
+ cycleState: 'completed', plan: { ref: 'docs/plans/current.md', state: 'executed' }, tasks: [{ id: 'task-1', attempts: 1, retries: 0 }],
84
+ qa: { findings: 0, fixes: 0, signatures: [] }, gates: { required: 1, firstEvaluationsPassed: [true], firstPass: true }, cures: [],
85
+ }));
86
+ }
87
+ describe('built doctor dashboard end-to-end (R4.8, R8.3-R8.5)', () => {
88
+ jest.setTimeout(60_000);
89
+ afterEach(() => jest.restoreAllMocks());
90
+ test('machine-only-uninitialized, degraded, partial, and corrupt fixtures are real on-disk states with exact exits and discriminating output', () => {
91
+ const machine = fixture('machine', false);
92
+ const degraded = fixture('degraded');
93
+ const partial = fixture('partial');
94
+ const corrupt = fixture('corrupt');
95
+ fs_1.default.mkdirSync(path_1.default.join(degraded.project, '.awm'), { recursive: true });
96
+ fs_1.default.writeFileSync(path_1.default.join(degraded.project, '.awm', 'profile.json'), '{not-json');
97
+ fs_1.default.mkdirSync(path_1.default.join(partial.project, '.awm', 'evidence', 'cycles'), { recursive: true });
98
+ fs_1.default.mkdirSync(path_1.default.join(corrupt.project, '.awm', 'evidence', 'cycles'), { recursive: true });
99
+ fs_1.default.writeFileSync(path_1.default.join(corrupt.project, '.awm', 'evidence', 'cycles', `${'d'.repeat(64)}.json`), '{broken');
100
+ const cases = [
101
+ ['machine-only-uninitialized', machine, 1, /Machine \/ install/],
102
+ ['degraded-project', degraded, 1, /Project readiness/],
103
+ ['partial-source', partial, 1, /source unavailable/i],
104
+ ['corrupt-source', corrupt, 1, /Final \/ history\n Eligible evidence rows: 0\n ⊘ source unavailable/],
105
+ ];
106
+ try {
107
+ for (const [, f, exit, discriminant] of cases) {
108
+ const before = treeHash(f.root);
109
+ const result = command(f, 'doctor', '--full');
110
+ if (result.status !== exit) {
111
+ throw new Error(`${f.project}\nexpected exit ${exit}, received ${result.status}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
112
+ }
113
+ expect(result.stdout).toMatch(discriminant);
114
+ expect(treeHash(f.root)).toBe(before);
115
+ }
116
+ }
117
+ finally {
118
+ for (const [, f] of cases)
119
+ fs_1.default.rmSync(f.root, { recursive: true, force: true });
120
+ }
121
+ });
122
+ test('json compatibility, full static HTML CSP, hostile source text, long history, and large fixture counts remain safe', () => {
123
+ const f = fixture('hostile');
124
+ try {
125
+ // A hostile optional evidence file must not turn raw markup into HTML.
126
+ writeEvidence(f, 'a'.repeat(64));
127
+ for (let index = 1; index < 500; index++)
128
+ writeEvidence(f, crypto_1.default.createHash('sha256').update(String(index)).digest('hex'));
129
+ const json = command(f, 'doctor', '--json');
130
+ expect([0, 1]).toContain(json.status);
131
+ expect(snapshot(json)).toEqual(expect.objectContaining({ overall: expect.any(String), providers: expect.any(Array) }));
132
+ const before = treeHash(f.root);
133
+ const full = command(f, 'doctor', '--full');
134
+ expect(full.stdout).toContain('Final / history');
135
+ expect(full.stdout).toContain('Cycle');
136
+ expect(treeHash(f.root)).toBe(before);
137
+ // Feed hostile text through the evidence reader itself, not an
138
+ // unrelated project file: malformed evidence is isolated and its
139
+ // raw script is never copied into the static dashboard.
140
+ fs_1.default.writeFileSync(path_1.default.join(f.project, '.awm', 'evidence', 'cycles', `${'f'.repeat(64)}.json`), JSON.stringify({
141
+ schema: 1, cycleId: 'f'.repeat(64), startedAt: '2026-08-22T10:00:00.000Z', endedAt: '2026-08-22T10:01:00.000Z', durationMs: 60_000,
142
+ cycleState: 'completed', plan: { ref: 'docs/plans/current.md', state: 'executed' }, tasks: [{ id: '<script>hostile</script>', attempts: 1, retries: 0 }],
143
+ qa: { findings: 0, fixes: 0, signatures: [] }, gates: { required: 0, firstEvaluationsPassed: [], firstPass: true }, cures: [],
144
+ }));
145
+ const hostileBeforeHtml = treeHash(f.root);
146
+ const entriesBeforeHtml = treeEntries(f.root, new Set(['project/dashboard.html']));
147
+ const html = command(f, 'doctor', '--html', 'dashboard.html');
148
+ expect([0, 1]).toContain(html.status);
149
+ const page = fs_1.default.readFileSync(path_1.default.join(f.project, 'dashboard.html'), 'utf8');
150
+ expect(page).toContain('Content-Security-Policy');
151
+ expect(page).toContain("script-src 'none'");
152
+ expect(page).toContain('data-project-evidence');
153
+ expect(page).not.toContain('<script>hostile</script>');
154
+ expect(page).toContain('Source unavailable');
155
+ const afterHtml = treeHash(f.root, new Set(['project/dashboard.html']));
156
+ if (afterHtml !== hostileBeforeHtml) {
157
+ throw new Error(`doctor --html mutated files other than dashboard.html: before=${entriesBeforeHtml.join(',')} after=${treeEntries(f.root, new Set(['project/dashboard.html'])).join(',')}`);
158
+ }
159
+ }
160
+ finally {
161
+ fs_1.default.rmSync(f.root, { recursive: true, force: true });
162
+ }
163
+ });
164
+ test('invalid dashboard combinations exit 2 without mutation', () => {
165
+ const f = fixture('invalid');
166
+ try {
167
+ const before = treeHash(f.root);
168
+ const result = command(f, 'doctor', '--json', '--full');
169
+ expect(result.status).toBe(2);
170
+ expect(result.stderr).toMatch(/cannot be combined/i);
171
+ expect(treeHash(f.root)).toBe(before);
172
+ }
173
+ finally {
174
+ fs_1.default.rmSync(f.root, { recursive: true, force: true });
175
+ }
176
+ });
177
+ });
@@ -0,0 +1,180 @@
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.assertImmutableArtifacts = assertImmutableArtifacts;
7
+ exports.assertIssue87RegistryPin = assertIssue87RegistryPin;
8
+ exports.assertRetroCaptureContract = assertRetroCaptureContract;
9
+ const child_process_1 = require("child_process");
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const cliVersion = process.env.AWM_PUBLISHED_CLI_VERSION;
14
+ const registryTag = process.env.AWM_PUBLISHED_REGISTRY_TAG;
15
+ const registryCommit = process.env.AWM_PUBLISHED_REGISTRY_COMMIT;
16
+ const registryRemote = process.env.AWM_PUBLISHED_REGISTRY_REMOTE ?? 'https://github.com/Kodria/awm-baseline-registry.git';
17
+ // Local contributors lack publish coordinates and skip this network acceptance.
18
+ // A CI run that supplies the published CLI/tag but omits the commit MUST run and
19
+ // fail at the provenance assertion rather than silently describe.skip.
20
+ const enabled = Boolean(cliVersion && registryTag);
21
+ const acceptance = enabled ? describe : describe.skip;
22
+ function command(cwd, executable, args, env = process.env) {
23
+ return (0, child_process_1.spawnSync)(executable, args, { cwd, encoding: 'utf8', env });
24
+ }
25
+ /** Published evidence accepts only exact versions and immutable git tags. */
26
+ function assertImmutableArtifacts(version, tag, commit, remote) {
27
+ if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
28
+ throw new Error('published CLI version must be an exact immutable semver');
29
+ if (version.includes('file:') || version.includes('/') || version.includes('@'))
30
+ throw new Error('published CLI must not be a workspace, file dependency, or mutable tag');
31
+ if (!tag || !/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(tag))
32
+ throw new Error('published registry ref must be an exact immutable tag');
33
+ if (!commit || !/^[a-f0-9]{40}$/.test(commit))
34
+ throw new Error('published registry commit must be a full immutable SHA');
35
+ if (!/^https:\/\/github\.com\/Kodria\/awm-baseline-registry\.git$/.test(remote))
36
+ throw new Error('published registry remote must be the canonical immutable release remote');
37
+ }
38
+ /** R8.7 is bound to the Task 7 registry release, never the preceding tag. */
39
+ function assertIssue87RegistryPin(tag) {
40
+ if (tag !== 'v3.4.0')
41
+ throw new Error('published doctor evidence requires immutable registry tag v3.4.0');
42
+ }
43
+ function json(result) {
44
+ if (!result.stdout.trim())
45
+ throw new Error(`missing JSON output: ${result.stderr}`);
46
+ return JSON.parse(result.stdout);
47
+ }
48
+ function compareSemver(left, right) {
49
+ const parse = (value) => value.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
50
+ const a = parse(left);
51
+ const b = parse(right);
52
+ if (!a || !b)
53
+ throw new Error('published compatibility versions must be exact semver');
54
+ for (let index = 1; index <= 3; index++) {
55
+ const difference = Number(a[index]) - Number(b[index]);
56
+ if (difference !== 0)
57
+ return difference;
58
+ }
59
+ return (a[4] ? -1 : 0) - (b[4] ? -1 : 0);
60
+ }
61
+ /** The cloned registry, not a loose prose regex, owns the retro ordering contract. */
62
+ function assertRetroCaptureContract(registry, retro, version) {
63
+ if (!registry || typeof registry !== 'object' || Array.isArray(registry))
64
+ throw new Error('published registry metadata is invalid');
65
+ const floor = registry.minCliVersion;
66
+ if (typeof floor !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(floor))
67
+ throw new Error('published registry must declare minCliVersion');
68
+ if (compareSemver(version, floor) < 0)
69
+ throw new Error(`published CLI ${version} is below registry minCliVersion ${floor}`);
70
+ const commands = retro.split(/\r?\n/).map((line, index) => ({ line: line.trim(), index }))
71
+ .filter(({ line }) => !line.startsWith('#') && !line.startsWith('<!--'))
72
+ .filter(({ line }) => /^(?:\d+\.\s+)?`?awm (?:evidence capture|ledger archive)(?:\s|`|$)/.test(line));
73
+ const captures = commands.filter(({ line }) => /awm evidence capture(?:\s|`|$)/.test(line));
74
+ const archives = commands.filter(({ line }) => /awm ledger archive(?:\s|`|$)/.test(line));
75
+ if (captures.length !== 1)
76
+ throw new Error('installed harness-retro contract requires exactly one executable evidence capture command');
77
+ if (archives.length !== 1 || captures[0].index > archives[0].index)
78
+ throw new Error('installed harness-retro must capture evidence before archive');
79
+ }
80
+ acceptance('published doctor and evidence acceptance (R8.7)', () => {
81
+ jest.setTimeout(10 * 60_000);
82
+ test('installs exact npm and registry artifacts into a fresh consumer and executes the dashboard/evidence contract', () => {
83
+ assertImmutableArtifacts(cliVersion, registryTag, registryCommit, registryRemote);
84
+ assertIssue87RegistryPin(registryTag);
85
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-published-doctor-'));
86
+ try {
87
+ const artifacts = path_1.default.join(root, 'artifacts');
88
+ const registry = path_1.default.join(root, 'registry');
89
+ const project = path_1.default.join(root, 'project');
90
+ const home = path_1.default.join(root, 'home');
91
+ fs_1.default.mkdirSync(project, { recursive: true });
92
+ fs_1.default.mkdirSync(home, { recursive: true });
93
+ expect(command(root, 'npm', ['install', '--prefix', artifacts, '--ignore-scripts', '--no-audit', '--no-fund', `agentic-workflow-manager@${cliVersion}`]).status).toBe(0);
94
+ expect(command(root, 'git', ['clone', '--depth', '1', '--branch', registryTag, registryRemote, registry]).status).toBe(0);
95
+ expect(command(registry, 'git', ['describe', '--exact-match', '--tags', 'HEAD']).stdout.trim()).toBe(registryTag);
96
+ expect(command(registry, 'git', ['rev-parse', 'HEAD']).stdout.trim()).toBe(registryCommit);
97
+ expect(command(registry, 'git', ['rev-parse', `${registryTag}^{}`]).stdout.trim()).toBe(registryCommit);
98
+ const cliRoot = path_1.default.join(artifacts, 'node_modules', 'agentic-workflow-manager');
99
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(cliRoot, 'package.json'), 'utf8'))).toEqual(expect.objectContaining({ version: cliVersion }));
100
+ expect(fs_1.default.existsSync(path_1.default.join(cliRoot, 'dist', 'src', 'index.js'))).toBe(true);
101
+ const registryMetadata = JSON.parse(fs_1.default.readFileSync(path_1.default.join(registry, 'awm-registry.json'), 'utf8'));
102
+ const retro = fs_1.default.readFileSync(path_1.default.join(registry, 'skills', 'harness-retro', 'SKILL.md'), 'utf8');
103
+ assertRetroCaptureContract(registryMetadata, retro, cliVersion);
104
+ fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ name: 'published-doctor-fixture', private: true }));
105
+ fs_1.default.writeFileSync(path_1.default.join(project, 'plan.md'), '# published evidence fixture\n');
106
+ expect(command(project, 'git', ['init']).status).toBe(0);
107
+ expect(command(project, 'git', ['config', 'user.email', 'published@example.invalid']).status).toBe(0);
108
+ expect(command(project, 'git', ['config', 'user.name', 'Published acceptance']).status).toBe(0);
109
+ expect(command(project, 'git', ['add', '.']).status).toBe(0);
110
+ expect(command(project, 'git', ['commit', '-m', 'fixture']).status).toBe(0);
111
+ const branch = command(project, 'git', ['branch', '--show-current']).stdout.trim();
112
+ expect(branch).not.toBe('');
113
+ expect(command(project, 'git', ['remote', 'add', 'origin', 'https://github.com/example/published-doctor-fixture.git']).status).toBe(0);
114
+ const env = { ...process.env, AWM_HOME: home, AWM_NO_UPDATE_CHECK: '1' };
115
+ const invoke = (...args) => command(project, process.execPath, [path_1.default.join(cliRoot, 'dist', 'src', 'index.js'), ...args], env);
116
+ expect([0, 1]).toContain(invoke('doctor').status);
117
+ const report = invoke('doctor', '--json');
118
+ expect([0, 1]).toContain(report.status);
119
+ expect(json(report)).toHaveProperty('providers');
120
+ expect([0, 1]).toContain(invoke('doctor', '--full').status);
121
+ const html = invoke('doctor', '--html', 'doctor.html');
122
+ expect([0, 1]).toContain(html.status);
123
+ expect(fs_1.default.readFileSync(path_1.default.join(project, 'doctor.html'), 'utf8')).toContain("script-src 'none'");
124
+ // Seed a completed durable journal using the downloaded package's own
125
+ // journal store: this stays on the published-artifact boundary while
126
+ // making capture exercise its real CLI path, not an injected helper.
127
+ const journalScript = [
128
+ `const store=require(${JSON.stringify(path_1.default.join(cliRoot, 'dist', 'src', 'core', 'journal', 'store.js'))});`,
129
+ 'store.initJournal(process.argv[1], process.argv[2]);',
130
+ 'const state=store.readJournal(process.argv[1], process.argv[2]).state;',
131
+ "state.cycle={...state.cycle,status:'COMPLETE',completedAt:'2026-08-22T10:00:01.000Z'};",
132
+ 'store.writeJournal(process.argv[1], process.argv[2], state);',
133
+ ].join('');
134
+ expect(command(project, process.execPath, ['-e', journalScript, project, branch], env).status).toBe(0);
135
+ const capture = invoke('evidence', 'capture', '--plan', 'plan.md');
136
+ expect(capture.status).toBe(0);
137
+ expect(capture.stdout.trim()).toMatch(/^[a-f0-9]{64}$/);
138
+ expect(fs_1.default.existsSync(path_1.default.join(project, '.awm', 'evidence', 'cycles', `${capture.stdout.trim()}.json`))).toBe(true);
139
+ // The retrospective/ledger lifecycle happens after the durable
140
+ // observation, never instead of it.
141
+ expect(invoke('ledger', 'add', '--branch', branch, '--polarity', 'finding', '--class', 'quality', '--signature', 'published-retro-contract', '--severity', 'important', '--desc', 'published acceptance').status).toBe(0);
142
+ expect(invoke('ledger', 'archive', '--branch', branch).status).toBe(0);
143
+ }
144
+ finally {
145
+ fs_1.default.rmSync(root, { recursive: true, force: true });
146
+ }
147
+ });
148
+ });
149
+ describe('published artifact provenance guard', () => {
150
+ test.each(['file:../cli', '../cli', 'latest', 'workspace:*', '8.4.0@latest'])('rejects mutable CLI reference %s', (version) => {
151
+ expect(() => assertImmutableArtifacts(version, 'v3.2.0', 'a'.repeat(40), registryRemote)).toThrow(/published CLI/i);
152
+ });
153
+ test.each(['main', 'HEAD', '', 'v3', 'refs/heads/main'])('rejects mutable registry ref %s', (tag) => {
154
+ expect(() => assertImmutableArtifacts('8.4.0', tag, 'a'.repeat(40), registryRemote)).toThrow(/registry ref/i);
155
+ });
156
+ test('rejects a short or retagged registry commit pin', () => {
157
+ expect(() => assertImmutableArtifacts('8.4.1', 'v3.4.0', 'deadbeef', registryRemote)).toThrow(/full immutable SHA/);
158
+ expect(() => assertImmutableArtifacts('8.4.1', 'v3.4.0', undefined, registryRemote)).toThrow(/full immutable SHA/);
159
+ });
160
+ });
161
+ describe('future registry retro capture contract', () => {
162
+ const metadata = { minCliVersion: '8.4.1' };
163
+ const contract = '1. awm evidence capture --plan docs/plan.md\n2. awm ledger archive\n';
164
+ test('requires the declared semver floor and capture-before-archive ordering', () => {
165
+ expect(() => assertRetroCaptureContract(metadata, contract, '8.4.1')).not.toThrow();
166
+ expect(() => assertRetroCaptureContract(metadata, contract, '8.4.0')).toThrow(/below registry minCliVersion/);
167
+ });
168
+ test('fails when minCliVersion is removed or retro ordering is swapped', () => {
169
+ expect(() => assertRetroCaptureContract({}, contract, '8.4.1')).toThrow(/minCliVersion/);
170
+ expect(() => assertRetroCaptureContract(metadata, 'awm ledger archive\nawm evidence capture', '8.4.1')).toThrow(/before archive/);
171
+ });
172
+ test('ignores comments and rejects duplicate or non-executable capture text', () => {
173
+ expect(() => assertRetroCaptureContract(metadata, '# awm evidence capture\nawm ledger archive', '8.4.1')).toThrow(/exactly one executable/);
174
+ expect(() => assertRetroCaptureContract(metadata, `${contract}awm evidence capture --plan again`, '8.4.1')).toThrow(/exactly one executable/);
175
+ });
176
+ test('rejects the prepublication CLI and a preceding registry pin', () => {
177
+ expect(() => assertRetroCaptureContract(metadata, contract, '8.4.0')).toThrow(/below registry minCliVersion/);
178
+ expect(() => assertIssue87RegistryPin('v3.2.0')).toThrow(/v3\.4\.0/);
179
+ });
180
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "8.4.0",
3
+ "version": "8.5.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"