agentic-workflow-manager 8.5.1 → 9.0.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.
@@ -28,12 +28,19 @@ function currentRelease(lines) {
28
28
  return releases.length > 1 ? releases.at(-1) : undefined;
29
29
  }
30
30
  function marker(lines, name, release) {
31
- const expression = new RegExp(`^\\s*<!--\\s*${name}(?:\\s*:\\s*[^\\r\\n]*?)?\\s*-->\\s*$`);
31
+ // Split into two non-overlapping alternatives (no-colon vs colon) so no `\s*`
32
+ // ever sits directly adjacent to the lazy `[^\r\n]*?` group — that adjacency
33
+ // is what turns a long unterminated marker line into catastrophic backtracking.
34
+ const expression = new RegExp(`^\\s*<!--\\s*${name}(?:\\s*-->\\s*$|\\s*:[^\\r\\n]*?-->\\s*$)`);
32
35
  if (release === undefined)
33
36
  return lines.some((line) => expression.test(line));
34
37
  const releaseExpression = new RegExp(`\\bRelease\\s+${release.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\b`, 'i');
35
38
  return lines.some((line) => expression.test(line) && releaseExpression.test(line));
36
39
  }
40
+ /** `docs_pending` es un estado vivo del dashboard; el registro durable no lo conoce (schema 1). */
41
+ function forEvidence(state) {
42
+ return state === 'docs_pending' ? 'retro_pending' : state;
43
+ }
37
44
  /** Reads only structural lifecycle syntax; plan prose never crosses into evidence. */
38
45
  function planState(root, planPath, journal) {
39
46
  const file = path_1.default.join(root, planPath);
@@ -78,11 +85,15 @@ function planState(root, planPath, journal) {
78
85
  if (status !== 'IN_PROGRESS' && status !== 'COMPLETE' && status !== 'BLOCKED')
79
86
  throw new Error('journal cycle status is invalid');
80
87
  const release = currentRelease(visibleLines);
81
- return (0, plan_state_1.classifyPlanState)({
88
+ return forEvidence((0, plan_state_1.classifyPlanState)({
82
89
  ...(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) },
90
+ markers: {
91
+ qaComplete: marker(visibleLines, 'awm-qa-complete', release),
92
+ docsComplete: marker(visibleLines, 'awm-docs-complete', release),
93
+ retroComplete: marker(visibleLines, 'awm-retro-complete', release),
94
+ },
84
95
  tasks: { total, completed },
85
- });
96
+ }));
86
97
  }
87
98
  function registerEvidenceCommand(program) {
88
99
  const evidence = program.command('evidence').description('durable privacy-preserving cycle observations');
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.REMEDIATION_BY_FINDING_ID = void 0;
7
7
  exports.productionDashboardAdapters = productionDashboardAdapters;
8
+ exports.lifecycleForCycle = lifecycleForCycle;
8
9
  exports.collectDashboardSnapshot = collectDashboardSnapshot;
9
10
  const profile_1 = require("../profile");
10
11
  const fs_1 = __importDefault(require("fs"));
@@ -122,6 +123,7 @@ function productionDashboardAdapters(context) {
122
123
  return {
123
124
  execution: history.cycles.map((cycle) => cycleFinding('execution', 'Static preflight', cycle, cycle.cycleState === 'blocked')),
124
125
  qa: history.cycles.map((cycle) => cycleFinding('qa', 'Sensors', cycle, cycle.qa.fixes < cycle.qa.findings)),
126
+ docs: history.cycles.map((cycle) => cycleFinding('docs', 'Documentation', cycle, !lifecycleForCycle(cycle).markers.docsComplete)),
125
127
  retro: history.cycles.map((cycle) => cycleFinding('retro', 'Project context', cycle, cycle.plan.state !== 'executed')),
126
128
  };
127
129
  },
@@ -144,25 +146,32 @@ function journalExecutionFinding(overlay) {
144
146
  id: 'execution.current-journal', label: 'Static preflight', state: blocked ? 'attention' : 'ok',
145
147
  ...(blocked ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
146
148
  }],
147
- qa: [], retro: [],
149
+ qa: [], docs: [], retro: [],
148
150
  };
149
151
  }
150
152
  function lifecycleForJournal(overlay) {
151
- return { journal: { state: overlay.state }, markers: { qaComplete: false, retroComplete: false }, tasks: overlay.tasks };
153
+ return { journal: { state: overlay.state }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: overlay.tasks };
152
154
  }
155
+ /** Reconstructs a PlanStateInput (markers + tasks) from a stored historical
156
+ * evidence record's plan.state for display purposes — the inverse of the
157
+ * live→durable direction. `retro_pending` predates the docs phase: that
158
+ * evidence was written before `docsComplete` existed, so treating it as
159
+ * docs-done is the only non-regressive reading; reconstructing it as
160
+ * `docsComplete: false` would invent retroactive docs debt on every
161
+ * historical cycle. */
153
162
  function lifecycleForCycle(cycle) {
154
163
  const total = cycle.tasks.length;
155
164
  if (cycle.plan.state === 'blocked' || cycle.cycleState === 'blocked')
156
- return { journal: { state: 'blocked' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
165
+ return { journal: { state: 'blocked' }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
157
166
  if (cycle.plan.state === 'active')
158
- return { journal: { state: 'active' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
167
+ return { journal: { state: 'active' }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
159
168
  if (cycle.plan.state === 'executed')
160
- return { markers: { qaComplete: true, retroComplete: true }, tasks: { total, completed: total } };
169
+ return { markers: { qaComplete: true, docsComplete: true, retroComplete: true }, tasks: { total, completed: total } };
161
170
  if (cycle.plan.state === 'retro_pending')
162
- return { markers: { qaComplete: true, retroComplete: false }, tasks: { total, completed: total } };
171
+ return { markers: { qaComplete: true, docsComplete: true, retroComplete: false }, tasks: { total, completed: total } };
163
172
  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 } };
173
+ return { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total, completed: total } };
174
+ return { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
166
175
  }
167
176
  function cycleFinding(section, label, cycle, actionable) {
168
177
  return {
@@ -330,7 +339,7 @@ function collectDashboardSnapshot(options) {
330
339
  const machineSection = section('machine', 'available', machineItems);
331
340
  if (!root) {
332
341
  const degraded = machineSection.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable');
333
- return (0, validate_1.validateDashboardSnapshotV1)({ schema: 1, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: false, label: 'No project detected' }, confidence: 'none', sections: [machineSection] });
342
+ return (0, validate_1.validateDashboardSnapshotV1)({ schema: 2, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: false, label: 'No project detected' }, confidence: 'none', sections: [machineSection] });
334
343
  }
335
344
  const projectResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.project({ root })));
336
345
  const plansResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.plans({ root })));
@@ -345,6 +354,7 @@ function collectDashboardSnapshot(options) {
345
354
  ? { ...plan, detail: (0, plan_state_1.classifyPlanState)(plan.lifecycle) } : plan))) : { value: [], failed: false, failure: {} };
346
355
  const executionItems = isolatedFindings(execution?.execution);
347
356
  const qaItems = isolatedFindings(execution?.qa);
357
+ const docsItems = isolatedFindings(execution?.docs);
348
358
  const retroItems = isolatedFindings(execution?.retro);
349
359
  const evidenceResult = optional(() => evidenceHistoryItems(root));
350
360
  const executionUnavailable = !executionResult.failed && execution === undefined;
@@ -355,12 +365,17 @@ function collectDashboardSnapshot(options) {
355
365
  section('planning', plansResult.failed || planItemsResult.failed ? 'unavailable' : 'available', plansResult.failed
356
366
  ? canonicalOptionalFailure(plansResult.failure) : planItemsResult.failed ? [] : planItemsResult.value),
357
367
  section('execution', executionResult.failed || executionUnavailable || executionItems.failed ? 'unavailable' : 'available', executionResult.failed ? canonicalOptionalFailure(executionResult.failure) : executionUnavailable ? canonicalOptionalFailure({ findingId: 'execution.source.unavailable', remediationVerified: true }) : executionItems.value),
358
- // There is no read-only QA, retro, or history adapter in Release A. An
368
+ // There is no read-only QA, docs, retro, or history adapter in Release A. An
359
369
  // absent execution source is not evidence of a successful empty cycle.
360
370
  section('qa', executionResult.failed || executionUnavailable || qaItems.failed ? 'unavailable' : 'available', qaItems.value),
371
+ section('docs', executionResult.failed || executionUnavailable || docsItems.failed ? 'unavailable' : 'available', docsItems.value),
361
372
  section('retro', executionResult.failed || executionUnavailable || retroItems.failed ? 'unavailable' : 'available', retroItems.value),
362
373
  section('history', evidenceResult.failed ? 'unavailable' : 'available', evidenceResult.failed ? [] : evidenceResult.value.items),
374
+ // `processes` is a reserved section id with no adapter yet: a later
375
+ // release (R1) populates it. `not_applicable` here means exactly
376
+ // "this section doesn't apply here", not dead code.
377
+ section('processes', 'not_applicable', []),
363
378
  ];
364
379
  const degraded = sections.some((entry) => entry.availability === 'unavailable' || entry.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable'));
365
- 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 });
380
+ return (0, validate_1.validateDashboardSnapshotV1)({ schema: 2, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: true, label: projectSource?.label || 'Project detected' }, confidence: evidenceResult.failed ? 'none' : evidenceResult.value.confidence, sections });
366
381
  }
@@ -13,12 +13,12 @@ function classifyPlanState(input) {
13
13
  assertRecord(input, 'state input');
14
14
  assertKeys(input, 'state input', ['journal', 'markers', 'tasks']);
15
15
  assertRecord(input.markers, 'markers');
16
- assertKeys(input.markers, 'markers', ['qaComplete', 'retroComplete']);
16
+ assertKeys(input.markers, 'markers', ['qaComplete', 'docsComplete', 'retroComplete']);
17
17
  assertRecord(input.tasks, 'tasks');
18
18
  assertKeys(input.tasks, 'tasks', ['total', 'completed']);
19
19
  const markers = input.markers;
20
20
  const tasks = input.tasks;
21
- if (typeof markers.qaComplete !== 'boolean' || typeof markers.retroComplete !== 'boolean')
21
+ if (typeof markers.qaComplete !== 'boolean' || typeof markers.docsComplete !== 'boolean' || typeof markers.retroComplete !== 'boolean')
22
22
  throw new Error('Plan markers must be boolean');
23
23
  if (typeof tasks.total !== 'number' || typeof tasks.completed !== 'number' || !Number.isInteger(tasks.total) || !Number.isInteger(tasks.completed) || tasks.total < 0 || tasks.completed < 0 || tasks.completed > tasks.total)
24
24
  throw new Error('Plan task counts are invalid');
@@ -36,8 +36,10 @@ function classifyPlanState(input) {
36
36
  return 'active';
37
37
  if (markers.retroComplete)
38
38
  return 'executed';
39
- if (markers.qaComplete)
39
+ if (markers.docsComplete)
40
40
  return 'retro_pending';
41
+ if (markers.qaComplete)
42
+ return 'docs_pending';
41
43
  if (tasks.total > 0 && tasks.completed === tasks.total)
42
44
  return 'qa_pending';
43
45
  return 'legacy_unverifiable';
@@ -4,7 +4,7 @@ exports.renderDashboardHtml = renderDashboardHtml;
4
4
  const styles_1 = require("./styles");
5
5
  const validate_1 = require("./validate");
6
6
  const CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
7
- const SECTION_TITLES = { machine: 'Machine / install', project: 'Project readiness', planning: 'Design / planning', execution: 'Execution', qa: 'QA', retro: 'Retro', history: 'Final / history' };
7
+ const SECTION_TITLES = { machine: 'Machine / install', project: 'Project readiness', planning: 'Design / planning', execution: 'Execution', qa: 'QA', docs: 'Docs', retro: 'Retro', history: 'Final / history', processes: 'Processes' };
8
8
  const STATE_TEXT = { ok: 'OK', attention: 'Attention', missing: 'Missing', unavailable: 'Unavailable', not_applicable: 'Not applicable' };
9
9
  const STATE_GLYPH = { ok: '●', attention: '▲', missing: '×', unavailable: '⊘', not_applicable: '—' };
10
10
  function escapeHtml(value) { return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
@@ -55,7 +55,7 @@ function projectEvidenceComposition(snapshot) {
55
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>`;
56
56
  }
57
57
  function projectComposition(snapshot) {
58
- const stageSections = ['planning', 'execution', 'qa', 'retro', 'history'];
58
+ const stageSections = ['planning', 'execution', 'qa', 'docs', 'retro', 'history'];
59
59
  const byId = new Map(snapshot.sections.map((section) => [section.id, section]));
60
60
  const stages = stageSections.map((id) => {
61
61
  const section = byId.get(id);
@@ -8,8 +8,10 @@ const SECTION_TITLES = {
8
8
  planning: 'Design / planning',
9
9
  execution: 'Execution',
10
10
  qa: 'QA',
11
+ docs: 'Docs',
11
12
  retro: 'Retro',
12
13
  history: 'Final / history',
14
+ processes: 'Processes',
13
15
  };
14
16
  const STATE_PRESENTATION = {
15
17
  ok: { glyph: '✔', text: 'ok' },
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanitizeDashboardSource = sanitizeDashboardSource;
4
4
  const crypto_1 = require("crypto");
5
5
  const STATES = new Set(['ok', 'attention', 'missing', 'unavailable', 'not_applicable', 'active', 'blocked']);
6
- const ALLOWED_KEYS = new Set(['findings', 'label', 'id', 'state', 'detail', 'remediation', 'remediationVerified', 'execution', 'qa', 'retro', 'history', 'lifecycle', 'journal', 'markers', 'tasks', 'total', 'completed', 'qaComplete', 'retroComplete']);
6
+ const ALLOWED_KEYS = new Set(['findings', 'label', 'id', 'state', 'detail', 'remediation', 'remediationVerified', 'execution', 'qa', 'docs', 'retro', 'history', 'lifecycle', 'journal', 'markers', 'tasks', 'total', 'completed', 'qaComplete', 'docsComplete', 'retroComplete']);
7
7
  const CANONICAL_LABELS = new Set([
8
8
  'Preferences', 'Registries', 'Profile', 'Sensors', 'Optional source unavailable',
9
- 'Extensions', 'Registry pins', 'Active bundles', 'Project context', 'Constitution', 'Static preflight',
9
+ 'Extensions', 'Registry pins', 'Active bundles', 'Project context', 'Constitution', 'Static preflight', 'Documentation',
10
10
  ]);
11
11
  const CANONICAL_FINDING_IDS = new Set([
12
12
  'machine.preferences.missing', 'machine.registries.stale', 'project.profile.missing',
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dashboardSnapshot = dashboardSnapshot;
4
- const PROJECT_SECTION_IDS = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
4
+ const PROJECT_SECTION_IDS = ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes'];
5
5
  /** Safe, deterministic V1 fixture and renderer input. */
6
6
  function dashboardSnapshot(overrides = {}) {
7
7
  const { project: projectOverride, sections: sectionsOverride, ...rest } = overrides;
@@ -10,7 +10,7 @@ function dashboardSnapshot(overrides = {}) {
10
10
  ? PROJECT_SECTION_IDS.map((id) => ({ id, availability: id === 'machine' ? 'available' : 'not_applicable', items: [] }))
11
11
  : [{ id: 'machine', availability: 'available', items: [] }]);
12
12
  return {
13
- schema: 1,
13
+ schema: 2,
14
14
  generatedAt: '2026-08-22T00:00:00.000Z',
15
15
  overall: 'healthy',
16
16
  project,
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DashboardValidationError = void 0;
4
4
  exports.validateDashboardSnapshotV1 = validateDashboardSnapshotV1;
5
- const SECTION_ORDER = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
5
+ const SECTION_ORDER = ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes'];
6
6
  const ITEM_STATES = ['ok', 'attention', 'missing', 'unavailable', 'not_applicable'];
7
7
  const AVAILABILITIES = ['available', 'unavailable', 'not_applicable'];
8
8
  const OVERALLS = ['healthy', 'degraded'];
@@ -42,8 +42,8 @@ function assertEnum(value, values, path) {
42
42
  function validateDashboardSnapshotV1(value) {
43
43
  assertRecord(value, 'snapshot');
44
44
  assertOnlyKeys(value, ['schema', 'generatedAt', 'overall', 'project', 'confidence', 'sections'], 'snapshot');
45
- if (value.schema !== 1)
46
- throw new DashboardValidationError('schema must be version 1');
45
+ if (value.schema !== 2)
46
+ throw new DashboardValidationError('schema must be version 2');
47
47
  assertNonEmptyString(value.generatedAt, 'generatedAt');
48
48
  if (Number.isNaN(Date.parse(value.generatedAt)))
49
49
  throw new DashboardValidationError('generatedAt must be a valid date-time');
@@ -5,11 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const collect_1 = require("../../../src/core/dashboard/collect");
7
7
  const sanitize_1 = require("../../../src/core/dashboard/sanitize");
8
+ const plan_state_1 = require("../../../src/core/dashboard/plan-state");
8
9
  const store_1 = require("../../../src/core/evidence/store");
9
10
  const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
10
11
  const fs_1 = __importDefault(require("fs"));
11
12
  const os_1 = __importDefault(require("os"));
12
13
  const path_1 = __importDefault(require("path"));
14
+ function lifecycleForCycleFixture(planState) {
15
+ const cycle = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), schema: 1, plan: { ref: 'plans/current.md', state: planState }, retries: 0, cureEfficacy: [] };
16
+ return (0, collect_1.lifecycleForCycle)(cycle);
17
+ }
13
18
  const fixedNow = '2026-08-22T00:00:00.000Z';
14
19
  describe('collectDashboardSnapshot', () => {
15
20
  it('returns only a healthy machine section outside a project', () => {
@@ -46,12 +51,22 @@ describe('collectDashboardSnapshot', () => {
46
51
  execution: () => ({}),
47
52
  },
48
53
  });
49
- expect(snapshot.sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history']);
54
+ expect(snapshot.sections.map((section) => section.id)).toEqual(// verifies R6.3
55
+ ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes']);
50
56
  expect(snapshot.sections.find((section) => section.id === 'machine')?.items[0].remediation).toBe('awm init');
51
57
  expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toHaveLength(2000);
52
58
  expect(snapshot.sections.find((section) => section.id === 'history')?.items).toHaveLength(0);
53
59
  expect(JSON.stringify(snapshot)).not.toMatch(/score|ranking/i);
54
60
  });
61
+ it('el snapshot declara schema 2', () => {
62
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
63
+ expect(snapshot.schema).toBe(2);
64
+ });
65
+ it('processes queda declarada no aplicable hasta R1', () => {
66
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
67
+ const processes = snapshot.sections.find((section) => section.id === 'processes');
68
+ expect(processes).toEqual({ id: 'processes', availability: 'not_applicable', items: [] });
69
+ });
55
70
  it('isolates optional adapter failures and omits unverified remediation', () => {
56
71
  const snapshot = (0, collect_1.collectDashboardSnapshot)({
57
72
  cwd: process.cwd(), now: fixedNow,
@@ -70,7 +85,7 @@ describe('collectDashboardSnapshot', () => {
70
85
  cwd: process.cwd(), now: fixedNow,
71
86
  adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined },
72
87
  });
73
- for (const id of ['execution', 'qa', 'retro']) {
88
+ for (const id of ['execution', 'qa', 'docs', 'retro']) {
74
89
  expect(snapshot.sections.find((section) => section.id === id)?.availability).toBe('unavailable');
75
90
  }
76
91
  });
@@ -91,7 +106,7 @@ describe('collectDashboardSnapshot', () => {
91
106
  expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
92
107
  expect(snapshot.sections.find((section) => section.id === 'planning')?.availability).toBe('available');
93
108
  });
94
- it.each(['execution', 'qa', 'retro'])('isolates malformed %s findings', (key) => {
109
+ it.each(['execution', 'qa', 'docs', 'retro'])('isolates malformed %s findings', (key) => {
95
110
  const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => ({ [key]: [{ id: '', label: 'Profile', state: 'ok' }] }) } });
96
111
  expect(snapshot.sections.find((section) => section.id === key)?.availability).toBe('unavailable');
97
112
  });
@@ -133,12 +148,12 @@ describe('collectDashboardSnapshot', () => {
133
148
  expect(snapshot.sections[0].items.map((item) => item.id)).toEqual(['machine.preferences.missing', 'machine.registries.stale']);
134
149
  });
135
150
  it.each(['blocked', 'active', 'executed', 'retro_pending', 'qa_pending', 'legacy_unverifiable'])('integrates lifecycle state %s into plan detail', (expected) => {
136
- const lifecycle = expected === 'blocked' ? { journal: { state: 'blocked' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
137
- : expected === 'active' ? { journal: { state: 'active' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
138
- : expected === 'executed' ? { markers: { qaComplete: true, retroComplete: true }, tasks: { total: 1, completed: 1 } }
139
- : expected === 'retro_pending' ? { markers: { qaComplete: true, retroComplete: false }, tasks: { total: 1, completed: 1 } }
140
- : expected === 'qa_pending' ? { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 1 } }
141
- : { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
151
+ const lifecycle = expected === 'blocked' ? { journal: { state: 'blocked' }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
152
+ : expected === 'active' ? { journal: { state: 'active' }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
153
+ : expected === 'executed' ? { markers: { qaComplete: true, docsComplete: true, retroComplete: true }, tasks: { total: 1, completed: 1 } }
154
+ : expected === 'retro_pending' ? { markers: { qaComplete: true, docsComplete: true, retroComplete: false }, tasks: { total: 1, completed: 1 } }
155
+ : expected === 'qa_pending' ? { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 1, completed: 1 } }
156
+ : { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
142
157
  const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [{ id: 'plan.lifecycle', label: 'Profile', state: 'ok', lifecycle }], execution: () => undefined } });
143
158
  expect(snapshot.sections.find((section) => section.id === 'planning')?.items[0].detail).toBe(expected);
144
159
  });
@@ -246,6 +261,19 @@ describe('sanitizeDashboardSource', () => {
246
261
  expect(JSON.stringify(safe)).not.toMatch(/Error|ghp_|\/tmp|TOKEN=/i);
247
262
  expect(JSON.stringify(safe)).not.toContain('detail');
248
263
  });
264
+ it('sanitize conserva docsComplete', () => {
265
+ const cleaned = (0, sanitize_1.sanitizeDashboardSource)({
266
+ lifecycle: { markers: { qaComplete: true, docsComplete: true, retroComplete: false }, tasks: { total: 1, completed: 1 } },
267
+ });
268
+ expect(cleaned.lifecycle.markers.docsComplete).toBe(true);
269
+ });
270
+ it('un ciclo historico retro_pending no retrocede a docs_pending', () => {
271
+ // Evidencia escrita antes de que existiera la fase: docs se considera hecho.
272
+ expect((0, plan_state_1.classifyPlanState)(lifecycleForCycleFixture('retro_pending'))).toBe('retro_pending');
273
+ });
274
+ it.each(['blocked', 'active', 'executed', 'qa_pending', 'legacy_unverifiable'])('lifecycleForCycle reconstruye %s de forma que classifyPlanState lo re-deriva igual', (planState) => {
275
+ expect((0, plan_state_1.classifyPlanState)(lifecycleForCycleFixture(planState))).toBe(planState);
276
+ });
249
277
  });
250
278
  test('exports canonical remediation commands', () => {
251
279
  expect(collect_1.REMEDIATION_BY_FINDING_ID['machine.preferences.missing']).toBe('awm init');
@@ -4,7 +4,7 @@ const validate_1 = require("../../../src/core/dashboard/validate");
4
4
  const types_1 = require("../../../src/core/dashboard/types");
5
5
  function snapshot() {
6
6
  return {
7
- schema: 1,
7
+ schema: 2,
8
8
  generatedAt: '2026-08-22T00:00:00.000Z',
9
9
  overall: 'healthy',
10
10
  project: { detected: true, label: 'Demo project' },
@@ -15,15 +15,17 @@ function snapshot() {
15
15
  { id: 'planning', availability: 'not_applicable', items: [] },
16
16
  { id: 'execution', availability: 'not_applicable', items: [] },
17
17
  { id: 'qa', availability: 'not_applicable', items: [] },
18
+ { id: 'docs', availability: 'not_applicable', items: [] },
18
19
  { id: 'retro', availability: 'not_applicable', items: [] },
19
20
  { id: 'history', availability: 'not_applicable', items: [] },
21
+ { id: 'processes', availability: 'not_applicable', items: [] },
20
22
  ],
21
23
  };
22
24
  }
23
25
  describe('validateDashboardSnapshotV1', () => {
24
26
  it('creates the safe deterministic dashboard snapshot default', () => {
25
27
  expect((0, types_1.dashboardSnapshot)()).toEqual({
26
- schema: 1,
28
+ schema: 2,
27
29
  generatedAt: '2026-08-22T00:00:00.000Z',
28
30
  overall: 'healthy',
29
31
  project: { detected: false, label: 'No project detected' },
@@ -36,7 +38,7 @@ describe('validateDashboardSnapshotV1', () => {
36
38
  });
37
39
  it('builds a complete canonical section set when callers select a project', () => {
38
40
  const value = (0, types_1.dashboardSnapshot)({ project: { detected: true, label: 'Demo project' }, confidence: 'provisional' });
39
- expect((0, validate_1.validateDashboardSnapshotV1)(value).sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history']);
41
+ expect((0, validate_1.validateDashboardSnapshotV1)(value).sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes']);
40
42
  });
41
43
  it('returns a valid V1 snapshot', () => {
42
44
  expect((0, validate_1.validateDashboardSnapshotV1)(snapshot())).toEqual(snapshot());
@@ -53,7 +55,7 @@ describe('validateDashboardSnapshotV1', () => {
53
55
  });
54
56
  it('rejects an invalid schema version', () => {
55
57
  const value = snapshot();
56
- value.schema = 2;
58
+ value.schema = 3;
57
59
  expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(validate_1.DashboardValidationError);
58
60
  });
59
61
  it('rejects invalid public enums', () => {
@@ -1,17 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const plan_state_1 = require("../../../src/core/dashboard/plan-state");
4
- const base = { journal: undefined, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 2, completed: 0 } };
4
+ const base = { journal: undefined, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 2, completed: 0 } };
5
5
  describe('classifyPlanState', () => {
6
6
  it('gives blocked journal state highest precedence', () => {
7
- expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'blocked' }, markers: { qaComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('blocked');
7
+ expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'blocked' }, markers: { qaComplete: true, docsComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('blocked');
8
8
  });
9
9
  it('gives active journal state precedence over completed markers', () => {
10
- expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'active' }, markers: { qaComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('active');
10
+ expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'active' }, markers: { qaComplete: true, docsComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('active');
11
11
  });
12
12
  it.each([
13
- [{ ...base, markers: { qaComplete: true, retroComplete: true } }, 'executed'],
14
- [{ ...base, markers: { qaComplete: true, retroComplete: false } }, 'retro_pending'],
13
+ [{ ...base, markers: { qaComplete: true, docsComplete: true, retroComplete: true } }, 'executed'],
14
+ [{ ...base, markers: { qaComplete: true, docsComplete: true, retroComplete: false } }, 'retro_pending'],
15
15
  [{ ...base, tasks: { total: 2, completed: 2 } }, 'qa_pending'],
16
16
  [base, 'legacy_unverifiable'],
17
17
  ])('classifies lifecycle state %s', (input, expected) => {
@@ -30,3 +30,41 @@ describe('classifyPlanState', () => {
30
30
  expect(() => (0, plan_state_1.classifyPlanState)(input)).toThrow(error);
31
31
  });
32
32
  });
33
+ describe('fase de documentacion', () => {
34
+ it('clasifica docs_pending con QA hecha y documentacion pendiente', () => {
35
+ expect((0, plan_state_1.classifyPlanState)({
36
+ markers: { qaComplete: true, docsComplete: false, retroComplete: false },
37
+ tasks: { total: 3, completed: 3 },
38
+ })).toBe('docs_pending');
39
+ });
40
+ it('vuelve a retro_pending cuando la documentacion esta hecha', () => {
41
+ expect((0, plan_state_1.classifyPlanState)({
42
+ markers: { qaComplete: true, docsComplete: true, retroComplete: false },
43
+ tasks: { total: 3, completed: 3 },
44
+ })).toBe('retro_pending');
45
+ });
46
+ it('conserva executed con retro hecho — el significado no cambia', () => {
47
+ expect((0, plan_state_1.classifyPlanState)({
48
+ markers: { qaComplete: true, docsComplete: true, retroComplete: true },
49
+ tasks: { total: 3, completed: 3 },
50
+ })).toBe('executed');
51
+ });
52
+ it('rechaza un marker desconocido', () => {
53
+ expect(() => (0, plan_state_1.classifyPlanState)({
54
+ markers: { qaComplete: true, docsComplete: false, retroComplete: false, bogusComplete: true },
55
+ tasks: { total: 1, completed: 1 },
56
+ })).toThrow(/unsupported fields/);
57
+ });
58
+ it('rechaza docsComplete no booleano', () => {
59
+ expect(() => (0, plan_state_1.classifyPlanState)({
60
+ markers: { qaComplete: true, docsComplete: 'yes', retroComplete: false },
61
+ tasks: { total: 1, completed: 1 },
62
+ })).toThrow(/must be boolean/);
63
+ });
64
+ it('docsComplete solo, sin qaComplete, ya alcanza retro_pending — el orden de la cadena no exige qaComplete primero', () => {
65
+ expect((0, plan_state_1.classifyPlanState)({
66
+ markers: { qaComplete: false, docsComplete: true, retroComplete: false },
67
+ tasks: { total: 3, completed: 3 },
68
+ })).toBe('retro_pending');
69
+ });
70
+ });
@@ -99,11 +99,29 @@ describe('productionDashboardAdapters', () => {
99
99
  expect(snapshot.sections.find((section) => section.id === 'qa')?.items).toEqual([
100
100
  expect.objectContaining({ state: 'ok' }),
101
101
  ]);
102
+ expect(snapshot.sections.find((section) => section.id === 'docs')?.items).toEqual([
103
+ expect.objectContaining({ state: 'ok' }),
104
+ ]);
102
105
  expect(snapshot.sections.find((section) => section.id === 'retro')?.items).toEqual([
103
106
  expect.objectContaining({ state: 'ok' }),
104
107
  ]);
105
108
  fs_1.default.rmSync(root, { recursive: true, force: true });
106
109
  });
110
+ it('flags the docs section as attention when a captured cycle has not documented yet', () => {
111
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-docs-attention-'));
112
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
113
+ (0, store_1.writeCycleEvidence)(root, {
114
+ ...(0, evidence_fixtures_1.cycleEvidenceFixture)(),
115
+ plan: { ref: 'docs/plans/current.md', state: 'qa_pending' },
116
+ });
117
+ const snapshot = (0, collect_1.collectDashboardSnapshot)({
118
+ cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
119
+ });
120
+ expect(snapshot.sections.find((section) => section.id === 'docs')?.items).toEqual([
121
+ expect.objectContaining({ state: 'attention' }),
122
+ ]);
123
+ fs_1.default.rmSync(root, { recursive: true, force: true });
124
+ });
107
125
  it.each([
108
126
  ['IN_PROGRESS', 'active', 'ok'],
109
127
  ['BLOCKED', 'blocked', 'attention'],
@@ -13,8 +13,10 @@ function snapshot(overrides = {}) {
13
13
  { id: 'planning', availability: 'available', items: [] },
14
14
  { id: 'execution', availability: 'available', items: [] },
15
15
  { id: 'qa', availability: 'available', items: [] },
16
+ { id: 'docs', availability: 'available', items: [] },
16
17
  { id: 'retro', availability: 'available', items: [] },
17
18
  { id: 'history', availability: 'available', items: [] },
19
+ { id: 'processes', availability: 'not_applicable', items: [] },
18
20
  ],
19
21
  ...overrides,
20
22
  });
@@ -27,7 +29,9 @@ describe('renderDashboardHtml', () => {
27
29
  { id: 'machine', availability: 'available', items: [{ id: 'hostile', label: '<script>alert(1)</script>', state: 'attention', detail: '"quoted"', remediation: 'awm sync && echo <unsafe>' }] },
28
30
  { id: 'project', availability: 'not_applicable', items: [] }, { id: 'planning', availability: 'not_applicable', items: [] },
29
31
  { id: 'execution', availability: 'not_applicable', items: [] }, { id: 'qa', availability: 'not_applicable', items: [] },
32
+ { id: 'docs', availability: 'not_applicable', items: [] },
30
33
  { id: 'retro', availability: 'not_applicable', items: [] }, { id: 'history', availability: 'not_applicable', items: [] },
34
+ { id: 'processes', availability: 'not_applicable', items: [] },
31
35
  ],
32
36
  })));
33
37
  expect(html).toContain(`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">`);
@@ -153,7 +157,7 @@ describe('renderDashboardHtml', () => {
153
157
  });
154
158
  it('renders every canonical project section once and in lifecycle order', () => {
155
159
  const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({ confidence: 'provisional' })));
156
- const ids = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
160
+ const ids = ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes'];
157
161
  const positions = ids.map((id) => html.indexOf(`<section id="${id}"`));
158
162
  expect(positions.every((position) => position >= 0)).toBe(true);
159
163
  expect([...positions].sort((left, right) => left - right)).toEqual(positions);
@@ -162,7 +166,7 @@ describe('renderDashboardHtml', () => {
162
166
  expect(html.match(/<section[\s>]/g)).toHaveLength(ids.length);
163
167
  });
164
168
  it('is deterministic and preserves every history and task observation without privacy leakage', () => {
165
- const sections = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'].map((id) => ({
169
+ const sections = ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes'].map((id) => ({
166
170
  id: id, availability: 'available',
167
171
  items: id === 'history' ? Array.from({ length: 500 }, (_, index) => ({ id: `history.${index}`, label: `Cycle ${index}`, state: 'ok' })) : id === 'execution' ? Array.from({ length: 2000 }, (_, index) => ({ id: `execution.${index}`, label: `Task ${index}`, state: 'ok' })) : [],
168
172
  }));
@@ -178,8 +182,9 @@ describe('renderDashboardHtml', () => {
178
182
  sections: [
179
183
  { id: 'machine', availability: 'available', items: [] }, { id: 'project', availability: 'available', items: [] },
180
184
  { 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: [] },
185
+ { id: 'execution', availability: 'available', items: [] }, { id: 'qa', availability: 'available', items: [] }, { id: 'docs', availability: 'available', items: [] }, { id: 'retro', availability: 'available', items: [] },
182
186
  { 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' }] },
187
+ { id: 'processes', availability: 'not_applicable', items: [] },
183
188
  ],
184
189
  })));
185
190
  expect(html).toContain('Confidence: supported');
@@ -14,8 +14,10 @@ function completeSnapshot(overrides = {}) {
14
14
  { id: 'planning', availability: 'available', items: [{ id: 'planning.plan', label: 'Active plan', state: 'ok', detail: 'executing' }] },
15
15
  { id: 'execution', availability: 'unavailable', items: [{ id: 'execution.source', label: 'Execution source', state: 'unavailable', remediation: 'awm doctor --full' }] },
16
16
  { id: 'qa', availability: 'available', items: [{ id: 'qa.gate', label: 'Verification gate', state: 'missing', remediation: 'awm sensors' }] },
17
+ { id: 'docs', availability: 'available', items: [{ id: 'docs.marker', label: 'Documentation', state: 'not_applicable' }] },
17
18
  { id: 'retro', availability: 'available', items: [{ id: 'retro.cure', label: 'Cure observations', state: 'not_applicable' }] },
18
19
  { id: 'history', availability: 'available', items: [{ id: 'history.cycle', label: 'Eligible cycle', state: 'ok', detail: '5 minutes' }] },
20
+ { id: 'processes', availability: 'not_applicable', items: [] },
19
21
  ],
20
22
  ...overrides,
21
23
  });
@@ -23,7 +25,7 @@ function completeSnapshot(overrides = {}) {
23
25
  describe('renderFullTerminal', () => {
24
26
  it('renders all lifecycle sections in canonical order with status and remediation', () => {
25
27
  const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot()));
26
- const headings = ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Retro', 'Final / history'];
28
+ const headings = ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Docs', 'Retro', 'Final / history', 'Processes'];
27
29
  expect(headings.map((heading) => output.indexOf(heading))).toEqual([...headings.map((_, index) => expect.any(Number))].map((_, index) => expect.any(Number)));
28
30
  for (let index = 1; index < headings.length; index++)
29
31
  expect(output.indexOf(headings[index])).toBeGreaterThan(output.indexOf(headings[index - 1]));
@@ -53,8 +55,10 @@ describe('renderFullTerminal', () => {
53
55
  { id: 'planning', availability: 'available', items: [] },
54
56
  { id: 'execution', availability: 'available', items: execution },
55
57
  { id: 'qa', availability: 'available', items: [] },
58
+ { id: 'docs', availability: 'available', items: [] },
56
59
  { id: 'retro', availability: 'available', items: [] },
57
60
  { id: 'history', availability: 'available', items: history },
61
+ { id: 'processes', availability: 'not_applicable', items: [] },
58
62
  ] }));
59
63
  const first = (0, render_terminal_1.renderFullTerminal)(snapshot);
60
64
  expect((0, render_terminal_1.renderFullTerminal)(snapshot)).toBe(first);
@@ -7,6 +7,8 @@ const fs_1 = __importDefault(require("fs"));
7
7
  const os_1 = __importDefault(require("os"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const evidence_1 = require("../../../src/commands/evidence");
10
+ const types_1 = require("../../../src/core/evidence/types");
11
+ const completeJournal = { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] };
10
12
  describe('evidence capture CLI boundary', () => {
11
13
  let root;
12
14
  beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-command-')); });
@@ -57,4 +59,54 @@ describe('evidence capture CLI boundary', () => {
57
59
  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
60
  expect(result).toEqual(expect.objectContaining({ code: 2, error: expect.stringMatching(/invalid checklist/i) }));
59
61
  });
62
+ test('sin el marker de docs, el estado vivo seria docs_pending — se remapea a retro_pending en la evidencia', () => {
63
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# Plan\n<!-- awm-qa-complete: 2026-08-23 -->\n- [x] Task 1\n');
64
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
65
+ expect(result.code).toBe(0);
66
+ 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');
67
+ });
68
+ test('lee el marker awm-docs-complete del plan — sin awm-qa-complete, docsComplete solo alcanza para retro_pending', () => {
69
+ // Discrimina de verdad: si el parser NO leyera awm-docs-complete, este plan (sin qa-complete,
70
+ // sin retro-complete) clasificaria qa_pending por conteo de tasks. Si SI lo lee, va directo a
71
+ // retro_pending — la unica forma de que este resultado ocurra es que el marker se haya parseado.
72
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# Plan\n<!-- awm-docs-complete: 2026-08-23 -->\n- [x] Task 1\n');
73
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
74
+ expect(result.code).toBe(0);
75
+ 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');
76
+ });
77
+ test('no deja que un awm-docs-complete de otro release clasifique el release actual', () => {
78
+ // Mismo patron que el test analogo de qa/retro: el marker debe respetar el scoping por release.
79
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-docs-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');
80
+ 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: [] });
81
+ expect(result.code).toBe(0);
82
+ 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');
83
+ });
84
+ test('no filtra docs_pending al registro durable de evidencia', () => {
85
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# Plan\n<!-- awm-qa-complete: 2026-08-23 -->\n- [x] Task 1\n');
86
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
87
+ expect(result.code).toBe(0);
88
+ expect(types_1.PLAN_STATES).not.toContain('docs_pending');
89
+ const state = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state;
90
+ expect(types_1.PLAN_STATES).toContain(state);
91
+ });
92
+ test('con docs y retro completos el estado sigue siendo executed', () => {
93
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# Plan\n<!-- awm-qa-complete: 2026-08-23 -->\n<!-- awm-docs-complete: 2026-08-23 -->\n<!-- awm-retro-complete: 2026-08-23 -->\n- [x] Task 1\n');
94
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
95
+ expect(result.code).toBe(0);
96
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('executed');
97
+ });
98
+ test('un marker sin cerrar y muy largo no cuelga el parseo (ReDoS)', () => {
99
+ // Regresion: el regex original tenia \s* adyacente al grupo lazo [^\r\n]*?,
100
+ // lo que producia backtracking catastrofico ante una linea larga sin "-->".
101
+ const unterminated = `<!-- awm-qa-complete: ${' '.repeat(200000)}`;
102
+ fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), `# Plan\n${unterminated}\n- [x] Task 1\n`);
103
+ const start = Date.now();
104
+ const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
105
+ const elapsedMs = Date.now() - start;
106
+ expect(elapsedMs).toBeLessThan(1000);
107
+ expect(result.code).toBe(0);
108
+ // linea sin cerrar: el marker no matchea, y sin awm-qa-complete el plan
109
+ // no llega a docs_pending/retro_pending por conteo de tasks (1/1 completa).
110
+ 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');
111
+ });
60
112
  });
@@ -128,7 +128,7 @@ acceptance('published doctor and evidence acceptance (R8.7)', () => {
128
128
  `const store=require(${JSON.stringify(path_1.default.join(cliRoot, 'dist', 'src', 'core', 'journal', 'store.js'))});`,
129
129
  'store.initJournal(process.argv[1], process.argv[2]);',
130
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'};",
131
+ "state.cycle={...state.cycle,status:'COMPLETE',completedAt:new Date(Date.now()+1_000).toISOString()};",
132
132
  'store.writeJournal(process.argv[1], process.argv[2], state);',
133
133
  ].join('');
134
134
  expect(command(project, process.execPath, ['-e', journalScript, project, branch], env).status).toBe(0);
@@ -138,7 +138,7 @@ acceptance('published doctor and evidence acceptance (R8.7)', () => {
138
138
  expect(fs_1.default.existsSync(path_1.default.join(project, '.awm', 'evidence', 'cycles', `${capture.stdout.trim()}.json`))).toBe(true);
139
139
  // The retrospective/ledger lifecycle happens after the durable
140
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);
141
+ expect(invoke('ledger', 'add', '--branch', branch, '--polarity', 'finding', '--class', 'proceso', '--signature', 'published-retro-contract', '--severity', 'important', '--desc', 'published acceptance').status).toBe(0);
142
142
  expect(invoke('ledger', 'archive', '--branch', branch).status).toBe(0);
143
143
  }
144
144
  finally {
@@ -0,0 +1,29 @@
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 ROOT = path_1.default.resolve(__dirname, '../../..');
9
+ const read = (file) => fs_1.default.readFileSync(path_1.default.join(ROOT, file), 'utf8');
10
+ describe('la fase de documentacion esta mecanizada, no solo enunciada', () => {
11
+ it('CONSTITUTION.md enuncia la regla y nombra el marker', () => {
12
+ const text = read('CONSTITUTION.md');
13
+ expect(text).toMatch(/awm-docs-complete/);
14
+ expect(text).toMatch(/post-implementation-docs/);
15
+ });
16
+ it('CONSTITUTION.md apunta al mecanismo, no lo reemplaza', () => {
17
+ const text = read('CONSTITUTION.md');
18
+ // debe citar el archivo que efectivamente hace cumplir la regla
19
+ expect(text).toMatch(/plan-state\.ts/);
20
+ expect(text).toMatch(/development-process/);
21
+ });
22
+ it('el enunciado NO es el unico lugar donde la fase existe', () => {
23
+ // Si esto pasa solo por CONSTITUTION.md, la regla es decorativa.
24
+ expect(read('cli/src/core/dashboard/plan-state.ts')).toMatch(/docsComplete/);
25
+ expect(read('cli/src/core/dashboard/plan-state.ts')).toMatch(/docs_pending/);
26
+ expect(read('cli/src/core/dashboard/sanitize.ts')).toMatch(/docsComplete/);
27
+ expect(read('cli/src/commands/evidence/index.ts')).toMatch(/awm-docs-complete/);
28
+ });
29
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "8.5.1",
3
+ "version": "9.0.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"