agentic-workflow-manager 8.3.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.
Files changed (36) hide show
  1. package/dist/src/commands/doctor.js +40 -1
  2. package/dist/src/commands/evidence/index.js +95 -0
  3. package/dist/src/core/dashboard/collect.js +223 -0
  4. package/dist/src/core/dashboard/plan-state.js +44 -0
  5. package/dist/src/core/dashboard/render-html.js +88 -0
  6. package/dist/src/core/dashboard/render-terminal.js +45 -0
  7. package/dist/src/core/dashboard/sanitize.js +62 -0
  8. package/dist/src/core/dashboard/styles.js +51 -0
  9. package/dist/src/core/dashboard/types.js +21 -0
  10. package/dist/src/core/dashboard/validate.js +106 -0
  11. package/dist/src/core/dashboard/write-html.js +88 -0
  12. package/dist/src/core/diagnostics/context.js +1 -1
  13. package/dist/src/core/evidence/capture.js +46 -0
  14. package/dist/src/core/evidence/history.js +50 -0
  15. package/dist/src/core/evidence/store.js +32 -0
  16. package/dist/src/core/evidence/types.js +97 -0
  17. package/dist/src/index.js +2 -0
  18. package/dist/tests/commands/doctor-is-read-only.test.js +54 -1
  19. package/dist/tests/commands/doctor.test.js +160 -0
  20. package/dist/tests/core/dashboard/collect.test.js +242 -0
  21. package/dist/tests/core/dashboard/contracts.test.js +92 -0
  22. package/dist/tests/core/dashboard/plan-state.test.js +32 -0
  23. package/dist/tests/core/dashboard/production-adapters.test.js +70 -0
  24. package/dist/tests/core/dashboard/render-html.test.js +191 -0
  25. package/dist/tests/core/dashboard/render-terminal.test.js +72 -0
  26. package/dist/tests/core/dashboard/write-html.test.js +112 -0
  27. package/dist/tests/core/evidence/capture.test.js +48 -0
  28. package/dist/tests/core/evidence/command.test.js +37 -0
  29. package/dist/tests/core/evidence/history.test.js +41 -0
  30. package/dist/tests/core/evidence/store.test.js +32 -0
  31. package/dist/tests/core/evidence/types.test.js +30 -0
  32. package/dist/tests/helpers/dashboard-fixtures.js +66 -0
  33. package/dist/tests/helpers/evidence-fixtures.js +17 -0
  34. package/dist/tests/integration/doctor-dashboard.e2e.test.js +177 -0
  35. package/dist/tests/integration/published-doctor-evidence.e2e.test.js +180 -0
  36. package/package.json +1 -1
@@ -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,66 @@
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.captureDoctorJsonFixture = captureDoctorJsonFixture;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const doctor_1 = require("../../src/commands/doctor");
11
+ /** Captures the legacy doctor JSON in an isolated, deterministic filesystem. */
12
+ function captureDoctorJsonFixture(kind) {
13
+ const tempRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-json-fixture-'));
14
+ const previousHome = process.env.HOME;
15
+ const previousAwmHome = process.env.AWM_HOME;
16
+ const output = [];
17
+ fs_1.default.mkdirSync(path_1.default.join(tempRoot, '.awm'), { recursive: true });
18
+ const writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
19
+ output.push(String(chunk));
20
+ return true;
21
+ });
22
+ process.env.HOME = tempRoot;
23
+ process.env.AWM_HOME = path_1.default.join(tempRoot, '.awm');
24
+ fs_1.default.writeFileSync(path_1.default.join(tempRoot, '.awm', 'preferences.json'), JSON.stringify({
25
+ defaultAgent: 'copilot',
26
+ enabledAgents: ['copilot'],
27
+ installMethod: 'symlink',
28
+ defaultScope: 'local',
29
+ }, null, 2) + '\n');
30
+ if (kind === 'project') {
31
+ fs_1.default.writeFileSync(path_1.default.join(tempRoot, '.awm', 'profile.json'), '{\n "extensions": []\n}\n');
32
+ fs_1.default.writeFileSync(path_1.default.join(tempRoot, 'AGENTS.md'), '<!-- AWM:START -->\nstale project context\n<!-- AWM:END -->\n');
33
+ }
34
+ try {
35
+ const code = (0, doctor_1.runDoctor)({ cwd: tempRoot, json: true });
36
+ return {
37
+ code,
38
+ output: output.join(''),
39
+ cleanup: () => {
40
+ writeSpy.mockRestore();
41
+ fs_1.default.rmSync(tempRoot, { recursive: true, force: true });
42
+ if (previousHome === undefined)
43
+ delete process.env.HOME;
44
+ else
45
+ process.env.HOME = previousHome;
46
+ if (previousAwmHome === undefined)
47
+ delete process.env.AWM_HOME;
48
+ else
49
+ process.env.AWM_HOME = previousAwmHome;
50
+ },
51
+ };
52
+ }
53
+ catch (error) {
54
+ writeSpy.mockRestore();
55
+ fs_1.default.rmSync(tempRoot, { recursive: true, force: true });
56
+ if (previousHome === undefined)
57
+ delete process.env.HOME;
58
+ else
59
+ process.env.HOME = previousHome;
60
+ if (previousAwmHome === undefined)
61
+ delete process.env.AWM_HOME;
62
+ else
63
+ process.env.AWM_HOME = previousAwmHome;
64
+ throw error;
65
+ }
66
+ }
@@ -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.3.0",
3
+ "version": "8.5.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"