agentic-workflow-manager 2.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.
Files changed (145) hide show
  1. package/README.md +62 -0
  2. package/dist/src/commands/doctor.js +77 -0
  3. package/dist/src/commands/hooks/index.js +118 -0
  4. package/dist/src/commands/hooks/install.js +113 -0
  5. package/dist/src/commands/hooks/resync.js +50 -0
  6. package/dist/src/commands/hooks/status.js +74 -0
  7. package/dist/src/commands/hooks/uninstall.js +59 -0
  8. package/dist/src/commands/init.js +181 -0
  9. package/dist/src/commands/ledger/index.js +73 -0
  10. package/dist/src/commands/pin.js +75 -0
  11. package/dist/src/commands/registry/add.js +62 -0
  12. package/dist/src/commands/registry/index.js +166 -0
  13. package/dist/src/commands/registry/install-bundles.js +36 -0
  14. package/dist/src/commands/registry/remove.js +18 -0
  15. package/dist/src/commands/registry/status.js +31 -0
  16. package/dist/src/commands/sensors/baseline.js +93 -0
  17. package/dist/src/commands/sensors/formatters/eslint.js +29 -0
  18. package/dist/src/commands/sensors/formatters/generic.js +8 -0
  19. package/dist/src/commands/sensors/formatters/semgrep.js +18 -0
  20. package/dist/src/commands/sensors/formatters/test.js +12 -0
  21. package/dist/src/commands/sensors/formatters/tsc.js +23 -0
  22. package/dist/src/commands/sensors/index.js +94 -0
  23. package/dist/src/commands/sensors/init.js +112 -0
  24. package/dist/src/commands/sensors/install.js +78 -0
  25. package/dist/src/commands/sensors/run.js +217 -0
  26. package/dist/src/commands/sensors/status.js +85 -0
  27. package/dist/src/commands/sensors/types.js +2 -0
  28. package/dist/src/core/bundle-install.js +116 -0
  29. package/dist/src/core/bundles.js +122 -0
  30. package/dist/src/core/cli-version.js +30 -0
  31. package/dist/src/core/context/materializer.js +30 -0
  32. package/dist/src/core/context/orchestrator.js +75 -0
  33. package/dist/src/core/context/project-constitution-inject.js +47 -0
  34. package/dist/src/core/context/provider.js +29 -0
  35. package/dist/src/core/context/regenerate.js +61 -0
  36. package/dist/src/core/context/strategies/config-instructions.js +73 -0
  37. package/dist/src/core/context/strategies/hook-merge.js +23 -0
  38. package/dist/src/core/context/strategies/strategy.js +2 -0
  39. package/dist/src/core/context/types.js +2 -0
  40. package/dist/src/core/diagnostics/checks.js +141 -0
  41. package/dist/src/core/diagnostics/context.js +181 -0
  42. package/dist/src/core/diagnostics/types.js +2 -0
  43. package/dist/src/core/discovery.js +135 -0
  44. package/dist/src/core/executor.js +41 -0
  45. package/dist/src/core/init/detector.js +67 -0
  46. package/dist/src/core/init/orchestrator.js +54 -0
  47. package/dist/src/core/init/steps.js +279 -0
  48. package/dist/src/core/init/types.js +2 -0
  49. package/dist/src/core/ledger/store.js +73 -0
  50. package/dist/src/core/ledger/types.js +2 -0
  51. package/dist/src/core/miro.js +314 -0
  52. package/dist/src/core/profile-pins.js +36 -0
  53. package/dist/src/core/profile.js +146 -0
  54. package/dist/src/core/registries.js +205 -0
  55. package/dist/src/core/registry.js +28 -0
  56. package/dist/src/core/skill-integrity.js +97 -0
  57. package/dist/src/core/story-map-parser.js +83 -0
  58. package/dist/src/core/update-check-worker.js +10 -0
  59. package/dist/src/core/update-check.js +106 -0
  60. package/dist/src/core/versioning.js +112 -0
  61. package/dist/src/index.js +689 -0
  62. package/dist/src/providers/index.js +63 -0
  63. package/dist/src/utils/config.js +35 -0
  64. package/dist/src/utils/grouping.js +51 -0
  65. package/dist/src/utils/registry-view.js +154 -0
  66. package/dist/tests/commands/doctor.test.js +98 -0
  67. package/dist/tests/commands/hooks/install.test.js +149 -0
  68. package/dist/tests/commands/hooks/resync.test.js +135 -0
  69. package/dist/tests/commands/hooks/router.test.js +26 -0
  70. package/dist/tests/commands/hooks/status.test.js +88 -0
  71. package/dist/tests/commands/hooks/uninstall.test.js +104 -0
  72. package/dist/tests/commands/init.test.js +87 -0
  73. package/dist/tests/commands/ledger/index.test.js +64 -0
  74. package/dist/tests/commands/pin.test.js +72 -0
  75. package/dist/tests/commands/registry/add.test.js +223 -0
  76. package/dist/tests/commands/registry/install-bundles.test.js +87 -0
  77. package/dist/tests/commands/registry/remove.test.js +49 -0
  78. package/dist/tests/commands/registry/status.test.js +43 -0
  79. package/dist/tests/commands/sensors/baseline.test.js +106 -0
  80. package/dist/tests/commands/sensors/formatters/eslint.test.js +32 -0
  81. package/dist/tests/commands/sensors/formatters/semgrep.test.js +38 -0
  82. package/dist/tests/commands/sensors/formatters/tsc.test.js +25 -0
  83. package/dist/tests/commands/sensors/index.test.js +18 -0
  84. package/dist/tests/commands/sensors/init.test.js +137 -0
  85. package/dist/tests/commands/sensors/install.test.js +60 -0
  86. package/dist/tests/commands/sensors/router.test.js +23 -0
  87. package/dist/tests/commands/sensors/run.test.js +353 -0
  88. package/dist/tests/commands/sensors/status.test.js +98 -0
  89. package/dist/tests/core/base-remote.test.js +70 -0
  90. package/dist/tests/core/bundle-install.test.js +198 -0
  91. package/dist/tests/core/bundles-multiroot.test.js +68 -0
  92. package/dist/tests/core/bundles-overrides.test.js +49 -0
  93. package/dist/tests/core/bundles.test.js +96 -0
  94. package/dist/tests/core/cli-version.test.js +17 -0
  95. package/dist/tests/core/context/materializer.test.js +46 -0
  96. package/dist/tests/core/context/orchestrator.test.js +127 -0
  97. package/dist/tests/core/context/project-constitution-inject.test.js +82 -0
  98. package/dist/tests/core/context/provider.test.js +45 -0
  99. package/dist/tests/core/context/regenerate.test.js +106 -0
  100. package/dist/tests/core/context/strategies/config-instructions.test.js +88 -0
  101. package/dist/tests/core/context/strategies/hook-merge.test.js +71 -0
  102. package/dist/tests/core/context/types.test.js +15 -0
  103. package/dist/tests/core/diagnostics/checks.test.js +208 -0
  104. package/dist/tests/core/diagnostics/context.test.js +170 -0
  105. package/dist/tests/core/discovery-multiroot.test.js +63 -0
  106. package/dist/tests/core/discovery-overrides.test.js +105 -0
  107. package/dist/tests/core/discovery.test.js +113 -0
  108. package/dist/tests/core/executor.test.js +44 -0
  109. package/dist/tests/core/init/detector.test.js +56 -0
  110. package/dist/tests/core/init/orchestrator.test.js +122 -0
  111. package/dist/tests/core/init/steps.test.js +363 -0
  112. package/dist/tests/core/ledger/gitignore.test.js +13 -0
  113. package/dist/tests/core/ledger/store.test.js +122 -0
  114. package/dist/tests/core/miro-layout.test.js +150 -0
  115. package/dist/tests/core/profile-pins.test.js +131 -0
  116. package/dist/tests/core/profile-registries.test.js +59 -0
  117. package/dist/tests/core/profile.test.js +110 -0
  118. package/dist/tests/core/registries-capability.test.js +52 -0
  119. package/dist/tests/core/registries-seed.test.js +66 -0
  120. package/dist/tests/core/registries-sync.test.js +205 -0
  121. package/dist/tests/core/registries.test.js +91 -0
  122. package/dist/tests/core/registry-manifest.test.js +95 -0
  123. package/dist/tests/core/registry-versioned-sync.test.js +113 -0
  124. package/dist/tests/core/registry.test.js +51 -0
  125. package/dist/tests/core/skill-integrity.test.js +126 -0
  126. package/dist/tests/core/story-map-parser.test.js +136 -0
  127. package/dist/tests/core/sync-gates.test.js +97 -0
  128. package/dist/tests/core/update-check.test.js +106 -0
  129. package/dist/tests/core/versioning.test.js +169 -0
  130. package/dist/tests/integration/pack-e2e.test.js +50 -0
  131. package/dist/tests/providers/hooks-config.test.js +45 -0
  132. package/dist/tests/providers/index.test.js +58 -0
  133. package/dist/tests/providers/injection-config.test.js +25 -0
  134. package/dist/tests/registry/b3-ledger-wiring.test.js +74 -0
  135. package/dist/tests/registry/catalog-consistency.test.js +47 -0
  136. package/dist/tests/registry/design-skills.test.js +146 -0
  137. package/dist/tests/registry/prose-agnostic.test.js +18 -0
  138. package/dist/tests/registry/sensor-packs.test.js +45 -0
  139. package/dist/tests/registry/skill-versions.test.js +26 -0
  140. package/dist/tests/registry/using-awm.test.js +39 -0
  141. package/dist/tests/utils/config.test.js +31 -0
  142. package/dist/tests/utils/grouping.test.js +43 -0
  143. package/dist/tests/utils/registry-view-overrides.test.js +41 -0
  144. package/dist/tests/utils/registry-view.test.js +156 -0
  145. package/package.json +49 -0
@@ -0,0 +1,44 @@
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
+ // tests/core/executor.test.ts
7
+ const executor_1 = require("../../src/core/executor");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ describe('Executor Engine', () => {
11
+ const sourceDir = path_1.default.join(__dirname, 'mock_source');
12
+ const targetDir = path_1.default.join(__dirname, 'mock_target');
13
+ beforeEach(() => {
14
+ fs_1.default.mkdirSync(sourceDir, { recursive: true });
15
+ fs_1.default.writeFileSync(path_1.default.join(sourceDir, 'test.txt'), 'hello');
16
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
17
+ });
18
+ afterEach(() => {
19
+ fs_1.default.rmSync(sourceDir, { recursive: true, force: true });
20
+ fs_1.default.rmSync(targetDir, { recursive: true, force: true });
21
+ });
22
+ it('creates a symlink successfully', () => {
23
+ const dest = path_1.default.join(targetDir, 'my-skill');
24
+ (0, executor_1.installArtifact)(sourceDir, dest, 'symlink');
25
+ expect(fs_1.default.lstatSync(dest).isSymbolicLink()).toBe(true);
26
+ });
27
+ it('copies the directory successfully', () => {
28
+ const dest = path_1.default.join(targetDir, 'my-copied-skill');
29
+ (0, executor_1.installArtifact)(sourceDir, dest, 'copy');
30
+ expect(fs_1.default.lstatSync(dest).isDirectory()).toBe(true);
31
+ expect(fs_1.default.existsSync(path_1.default.join(dest, 'test.txt'))).toBe(true);
32
+ });
33
+ it('removes an installed artifact', () => {
34
+ const dest = path_1.default.join(targetDir, 'to-remove');
35
+ (0, executor_1.installArtifact)(sourceDir, dest, 'symlink');
36
+ expect(fs_1.default.existsSync(dest)).toBe(true);
37
+ (0, executor_1.removeArtifact)(dest);
38
+ expect(fs_1.default.existsSync(dest)).toBe(false);
39
+ });
40
+ it('throws when removing a non-existent artifact', () => {
41
+ const dest = path_1.default.join(targetDir, 'does-not-exist');
42
+ expect(() => (0, executor_1.removeArtifact)(dest)).toThrow('Artifact not found');
43
+ });
44
+ });
@@ -0,0 +1,56 @@
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 detector_1 = require("../../../src/core/init/detector");
10
+ function tmpRepo() {
11
+ return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-detector-'));
12
+ }
13
+ describe('detectExtensions', () => {
14
+ let root;
15
+ beforeEach(() => { root = tmpRepo(); });
16
+ afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
17
+ it('proposes frontend when package.json has a frontend dep', () => {
18
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ dependencies: { next: '14.0.0' } }));
19
+ const r = (0, detector_1.detectExtensions)(root);
20
+ expect(r.proposed).toContain('frontend');
21
+ expect(r.signals.some((s) => s.includes('next'))).toBe(true);
22
+ });
23
+ it('does NOT propose docs for a lone README', () => {
24
+ fs_1.default.mkdirSync(path_1.default.join(root, 'docs'));
25
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'README.md'), '# readme');
26
+ expect((0, detector_1.detectExtensions)(root).proposed).not.toContain('docs');
27
+ });
28
+ it('proposes docs when a docs config is present', () => {
29
+ fs_1.default.writeFileSync(path_1.default.join(root, 'mkdocs.yml'), 'site_name: x');
30
+ expect((0, detector_1.detectExtensions)(root).proposed).toContain('docs');
31
+ });
32
+ it('proposes docs when docs/ has 2+ markdown files', () => {
33
+ fs_1.default.mkdirSync(path_1.default.join(root, 'docs'));
34
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'a.md'), '# a');
35
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'b.md'), '# b');
36
+ expect((0, detector_1.detectExtensions)(root).proposed).toContain('docs');
37
+ });
38
+ it('proposes nothing for a backend-only repo', () => {
39
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ dependencies: { express: '4.0.0' } }));
40
+ const r = (0, detector_1.detectExtensions)(root);
41
+ expect(r.proposed).toEqual([]);
42
+ expect(r.deferred).toEqual([]);
43
+ });
44
+ it('defers infra signals (no bundle yet)', () => {
45
+ fs_1.default.writeFileSync(path_1.default.join(root, 'Dockerfile'), 'FROM node');
46
+ const r = (0, detector_1.detectExtensions)(root);
47
+ expect(r.proposed).toEqual([]);
48
+ expect(r.deferred.some((d) => d.includes('infra'))).toBe(true);
49
+ });
50
+ it('proposes both for a combined repo (Next + real docs)', () => {
51
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ devDependencies: { astro: '4.0.0' } }));
52
+ fs_1.default.writeFileSync(path_1.default.join(root, 'docusaurus.config.js'), 'module.exports = {}');
53
+ const r = (0, detector_1.detectExtensions)(root);
54
+ expect(r.proposed).toEqual(expect.arrayContaining(['frontend', 'docs']));
55
+ });
56
+ });
@@ -0,0 +1,122 @@
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
+ // Siembra un registry baseline mínimo en <contentRoot> (= ~/.awm/registries/baseline).
10
+ // El content root ES la raíz del registry — no hay subdir "registry/" en la nueva arquitectura.
11
+ function seedRegistry(contentRoot) {
12
+ fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'skills', 'brainstorming'), { recursive: true });
13
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'skills', 'brainstorming', 'SKILL.md'), '# brainstorming');
14
+ fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'skills', 'using-awm'), { recursive: true });
15
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'skills', 'using-awm', 'SKILL.md'), '# using-awm');
16
+ fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'hooks'), { recursive: true });
17
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'hooks', 'session-start'), '#!/bin/sh\n');
18
+ fs_1.default.chmodSync(path_1.default.join(contentRoot, 'hooks', 'session-start'), 0o755);
19
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'hooks', 'run-hook.cmd'), '#!/bin/sh\n');
20
+ fs_1.default.chmodSync(path_1.default.join(contentRoot, 'hooks', 'run-hook.cmd'), 0o755);
21
+ fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts'), { recursive: true });
22
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({ sensors: { lint: { defaultCmd: 'eslint {{SOURCE_DIRS}}', fast: true } } }));
23
+ // catalog + bundle dev (baseline)
24
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'catalog.json'), JSON.stringify({
25
+ version: 1, bundles: [{ name: 'dev', source: './bundles/dev', version: '1.0.0', scope: 'baseline' }],
26
+ }));
27
+ fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'bundles', 'dev'), { recursive: true });
28
+ fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'bundles', 'dev', 'bundle.json'), JSON.stringify({
29
+ name: 'dev', version: '1.0.0', scope: 'baseline', dependsOn: [],
30
+ skills: [{ name: 'brainstorming' }, { name: 'using-awm' }], workflows: [], agents: [],
31
+ }));
32
+ // Seed .git in the content root so gatherMachine.registryCache.present is true.
33
+ fs_1.default.mkdirSync(path_1.default.join(contentRoot, '.git'), { recursive: true });
34
+ }
35
+ describe('runInitSteps — orchestrator', () => {
36
+ let tmpHome;
37
+ let originalHome;
38
+ let originalAwmHome;
39
+ beforeEach(() => {
40
+ tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-orch-'));
41
+ originalHome = process.env.HOME;
42
+ originalAwmHome = process.env.AWM_HOME;
43
+ process.env.HOME = tmpHome;
44
+ process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
45
+ jest.resetModules();
46
+ });
47
+ afterEach(() => {
48
+ fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
49
+ if (originalHome === undefined)
50
+ delete process.env.HOME;
51
+ else
52
+ process.env.HOME = originalHome;
53
+ if (originalAwmHome === undefined)
54
+ delete process.env.AWM_HOME;
55
+ else
56
+ process.env.AWM_HOME = originalAwmHome;
57
+ });
58
+ function buildDeps(cwd) {
59
+ const { gatherContext } = require('../../../src/core/diagnostics/context');
60
+ const { discoverAllBundles } = require('../../../src/core/bundles');
61
+ const { contentRoots, REGISTRIES_DIR } = require('../../../src/core/registries');
62
+ const { defaultActions } = require('../../../src/core/init/steps');
63
+ // Seed content at ~/.awm/registries/baseline (content root IS the registry root).
64
+ const contentRoot = path_1.default.join(process.env.AWM_HOME, 'registries', 'baseline');
65
+ fs_1.default.mkdirSync(contentRoot, { recursive: true });
66
+ seedRegistry(contentRoot);
67
+ // Register the baseline registry in registries.json so contentRoots() picks it up.
68
+ const registriesJson = path_1.default.join(process.env.AWM_HOME, 'registries.json');
69
+ fs_1.default.writeFileSync(registriesJson, JSON.stringify([
70
+ { name: 'baseline', remote: 'https://example.com/baseline.git' },
71
+ ], null, 2));
72
+ const roots = contentRoots();
73
+ const bundles = discoverAllBundles(roots);
74
+ const contentDir = roots[0] ?? '';
75
+ const ctx = gatherContext({ cwd, bundles });
76
+ return {
77
+ cwd, ctx, bundles, agent: 'claude-code', installMethod: 'symlink',
78
+ registryRoot: contentRoot, contentDir, sensorPacksRoot: contentRoot,
79
+ confirmExtensions: async (p) => p,
80
+ // syncCache es no-op: el cache ya está sembrado en disco
81
+ actions: { ...defaultActions, syncCache: async () => { } },
82
+ };
83
+ }
84
+ it('machine-only on a bare cwd installs baseline + hook, reaches healthy machine', async () => {
85
+ const { runInitSteps } = require('../../../src/core/init/orchestrator');
86
+ const bareCwd = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bare-'));
87
+ try {
88
+ const deps = buildDeps(bareCwd); // bareCwd no es repo → project null
89
+ deps.ctx.project = null;
90
+ const out = await runInitSteps(deps);
91
+ expect(out.applied).toBeGreaterThan(0);
92
+ expect(out.steps.some((s) => s.id === 'machine.devCore' && s.action === 'applied')).toBe(true);
93
+ expect(out.after.results.find((r) => r.id === 'machine.devCore').status).toBe('ok');
94
+ }
95
+ finally {
96
+ fs_1.default.rmSync(bareCwd, { recursive: true, force: true });
97
+ }
98
+ });
99
+ it('project repo: applies activation/sensors, flags constitution+context as pending', async () => {
100
+ const root = path_1.default.join(tmpHome, 'repo');
101
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm'), { recursive: true });
102
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ dependencies: { next: '14.0.0' } }));
103
+ const deps = buildDeps(root);
104
+ const { runInitSteps } = require('../../../src/core/init/orchestrator');
105
+ const out = await runInitSteps(deps);
106
+ expect(out.steps.some((s) => s.id === 'project.sensors' && s.action === 'applied')).toBe(true);
107
+ expect(out.steps.find((s) => s.id === 'project.constitution').action).toBe('pending');
108
+ expect(out.steps.find((s) => s.id === 'project.context').action).toBe('pending');
109
+ });
110
+ it('is idempotent: a second run applies nothing and yields an identical after-report', async () => {
111
+ const root = path_1.default.join(tmpHome, 'repo2');
112
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm'), { recursive: true });
113
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ dependencies: { next: '14.0.0' } }));
114
+ const { runInitSteps } = require('../../../src/core/init/orchestrator');
115
+ const out1 = await runInitSteps(buildDeps(root));
116
+ expect(out1.applied).toBeGreaterThan(0);
117
+ // segundo run: re-gather refleja el estado ya materializado
118
+ const out2 = await runInitSteps(buildDeps(root));
119
+ expect(out2.applied).toBe(0);
120
+ expect(out2.after).toEqual(out1.after);
121
+ });
122
+ });
@@ -0,0 +1,363 @@
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 steps_1 = require("../../../src/core/init/steps");
10
+ const providers_1 = require("../../../src/providers");
11
+ function bundle(name, scope, skills) {
12
+ return {
13
+ name, description: '', version: '1.0.0', scope, visibility: 'public',
14
+ dependsOn: [], skills: skills.map((s) => ({ name: s, onSignal: false })),
15
+ workflows: [], agents: [],
16
+ };
17
+ }
18
+ function machine() {
19
+ return {
20
+ registryCache: { present: true, gitState: 'clean' },
21
+ hook: { present: true, degraded: false },
22
+ devCore: { present: true, brokenLinks: [] },
23
+ ambient: { wanted: [], installed: [] },
24
+ contextInjection: [],
25
+ globalSkills: { valid: [], repairable: [], dead: [] },
26
+ };
27
+ }
28
+ function project(over = {}) {
29
+ return {
30
+ root: '/repo',
31
+ profile: { present: true, extensions: [] },
32
+ activeBundles: { expected: [], linked: [], broken: [] },
33
+ sensors: { present: true },
34
+ constitution: { present: true },
35
+ context: { present: true, file: 'CLAUDE.md' },
36
+ ...over,
37
+ };
38
+ }
39
+ function spies() {
40
+ return {
41
+ syncCache: jest.fn(async () => { }),
42
+ installHook: jest.fn(() => ({ status: 'installed' })),
43
+ installBundle: jest.fn(() => ({ installed: ['a'], skipped: [] })),
44
+ syncProfile: jest.fn(() => ({ installed: ['a'], skipped: [], extensions: ['frontend'] })),
45
+ initSensors: jest.fn(() => ({ detection: { pack: 'js-ts' } })),
46
+ addExtension: jest.fn(),
47
+ ensureProfile: jest.fn(),
48
+ gatherProject: jest.fn((_cwd, _bundles) => null),
49
+ contextStatus: jest.fn(() => 'absent'),
50
+ installContext: jest.fn(),
51
+ repairGlobalSkills: jest.fn(() => ({ relinked: [], pruned: [], failed: [] })),
52
+ injectProjectConstitution: jest.fn(() => 'injected'),
53
+ };
54
+ }
55
+ function deps(ctx, actions, over = {}) {
56
+ return {
57
+ cwd: '/repo', ctx, bundles: [bundle('dev', 'baseline', ['brainstorming'])],
58
+ agent: 'claude-code', installMethod: 'symlink',
59
+ registryRoot: '/cache', contentDir: '/cache/registry', sensorPacksRoot: '/cache/registry',
60
+ confirmExtensions: async (p) => p, actions, ...over,
61
+ };
62
+ }
63
+ describe('stepCache', () => {
64
+ it('skips when cli present and not behind', async () => {
65
+ const a = spies();
66
+ const r = await (0, steps_1.stepCache)(deps({ machine: machine(), project: null }, a));
67
+ expect(r.action).toBe('skipped');
68
+ expect(a.syncCache).not.toHaveBeenCalled();
69
+ });
70
+ it('syncs when registry cache absent', async () => {
71
+ const a = spies();
72
+ const m = machine();
73
+ m.registryCache = { present: false };
74
+ const r = await (0, steps_1.stepCache)(deps({ machine: m, project: null }, a));
75
+ expect(r.action).toBe('applied');
76
+ expect(a.syncCache).toHaveBeenCalled();
77
+ });
78
+ it('syncs when registry cache behind', async () => {
79
+ const a = spies();
80
+ const m = machine();
81
+ m.registryCache = { present: true, gitState: 'behind' };
82
+ expect((await (0, steps_1.stepCache)(deps({ machine: m, project: null }, a))).action).toBe('applied');
83
+ });
84
+ it('reports failed when syncCache throws (does not throw)', async () => {
85
+ const a = spies();
86
+ a.syncCache = jest.fn(async () => { throw new Error('net down'); });
87
+ const m = machine();
88
+ m.registryCache = { present: false };
89
+ const r = await (0, steps_1.stepCache)(deps({ machine: m, project: null }, a));
90
+ expect(r.action).toBe('failed');
91
+ expect(r.error).toContain('net down');
92
+ });
93
+ });
94
+ describe('stepHook / stepDevCore / stepAmbient', () => {
95
+ it('hook skips when present and healthy', () => {
96
+ const a = spies();
97
+ expect((0, steps_1.stepHook)(deps({ machine: machine(), project: null }, a)).action).toBe('skipped');
98
+ expect(a.installHook).not.toHaveBeenCalled();
99
+ });
100
+ it('hook installs when absent', () => {
101
+ const a = spies();
102
+ const m = machine();
103
+ m.hook = { present: false };
104
+ expect((0, steps_1.stepHook)(deps({ machine: m, project: null }, a)).action).toBe('applied');
105
+ expect(a.installHook).toHaveBeenCalled();
106
+ });
107
+ it('hook reinstalls when present but degraded', () => {
108
+ const a = spies();
109
+ const m = machine();
110
+ m.hook = { present: true, degraded: true };
111
+ expect((0, steps_1.stepHook)(deps({ machine: m, project: null }, a)).action).toBe('applied');
112
+ expect(a.installHook).toHaveBeenCalled();
113
+ });
114
+ it('devCore installs baseline when links broken', () => {
115
+ const a = spies();
116
+ const m = machine();
117
+ m.devCore = { present: true, brokenLinks: ['brainstorming'] };
118
+ expect((0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a)).action).toBe('applied');
119
+ expect(a.installBundle).toHaveBeenCalled();
120
+ });
121
+ it('devCore installs when not present at all', () => {
122
+ const a = spies();
123
+ const m = machine();
124
+ m.devCore = { present: false, brokenLinks: [] };
125
+ expect((0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a)).action).toBe('applied');
126
+ expect(a.installBundle).toHaveBeenCalled();
127
+ });
128
+ it('ambient installs only missing wanted', () => {
129
+ const a = spies();
130
+ const m = machine();
131
+ m.ambient = { wanted: ['personal-notion', 'docs'], installed: ['docs'] };
132
+ const r = (0, steps_1.stepAmbient)(deps({ machine: m, project: null }, a));
133
+ expect(r.action).toBe('applied');
134
+ expect(a.installBundle).toHaveBeenCalledTimes(1);
135
+ });
136
+ it('ambient skips when nothing wanted', () => {
137
+ const a = spies();
138
+ expect((0, steps_1.stepAmbient)(deps({ machine: machine(), project: null }, a)).action).toBe('skipped');
139
+ });
140
+ });
141
+ describe('stepProfile', () => {
142
+ it('adds confirmed extension when detector finds a match', async () => {
143
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-step-profile-'));
144
+ try {
145
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ dependencies: { next: '14.0.0' } }));
146
+ const a = spies();
147
+ const ctx = {
148
+ machine: machine(),
149
+ project: project({ root, profile: { present: true, extensions: [] } }),
150
+ };
151
+ const r = await (0, steps_1.stepProfile)(deps(ctx, a, { confirmExtensions: async (p) => p }));
152
+ expect(r.action).toBe('applied');
153
+ expect(a.addExtension).toHaveBeenCalledWith(root, 'frontend');
154
+ }
155
+ finally {
156
+ fs_1.default.rmSync(root, { recursive: true, force: true });
157
+ }
158
+ });
159
+ it('skips when confirm returns empty', async () => {
160
+ const a = spies();
161
+ const ctx = { machine: machine(), project: project() };
162
+ const r = await (0, steps_1.stepProfile)(deps(ctx, a, { confirmExtensions: async () => [] }));
163
+ expect(a.addExtension).not.toHaveBeenCalled();
164
+ expect(r.action).toBe('skipped');
165
+ });
166
+ it('does not invoke confirmExtensions when no new extensions are proposed (#1 guard)', async () => {
167
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-ext-'));
168
+ try {
169
+ const a = spies();
170
+ const confirm = jest.fn(async (p) => p);
171
+ const ctx = {
172
+ machine: machine(),
173
+ project: project({ root, profile: { present: true, extensions: [] } }),
174
+ };
175
+ const r = await (0, steps_1.stepProfile)(deps(ctx, a, { confirmExtensions: confirm }));
176
+ expect(confirm).not.toHaveBeenCalled();
177
+ expect(r.action).toBe('skipped');
178
+ }
179
+ finally {
180
+ fs_1.default.rmSync(root, { recursive: true, force: true });
181
+ }
182
+ });
183
+ it('bootstraps an empty profile when none exists and no extensions are proposed', async () => {
184
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bootstrap-profile-'));
185
+ try {
186
+ const a = spies();
187
+ const ctx = {
188
+ machine: machine(),
189
+ project: project({ root, profile: { present: false, extensions: [] } }),
190
+ };
191
+ const r = await (0, steps_1.stepProfile)(deps(ctx, a, { confirmExtensions: async (p) => p }));
192
+ expect(r.action).toBe('applied');
193
+ expect(a.ensureProfile).toHaveBeenCalledWith(root);
194
+ expect(a.addExtension).not.toHaveBeenCalled();
195
+ }
196
+ finally {
197
+ fs_1.default.rmSync(root, { recursive: true, force: true });
198
+ }
199
+ });
200
+ it('does not bootstrap when the profile already exists', async () => {
201
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-existing-profile-'));
202
+ try {
203
+ const a = spies();
204
+ const ctx = {
205
+ machine: machine(),
206
+ project: project({ root, profile: { present: true, extensions: [] } }),
207
+ };
208
+ const r = await (0, steps_1.stepProfile)(deps(ctx, a));
209
+ expect(r.action).toBe('skipped');
210
+ expect(a.ensureProfile).not.toHaveBeenCalled();
211
+ }
212
+ finally {
213
+ fs_1.default.rmSync(root, { recursive: true, force: true });
214
+ }
215
+ });
216
+ });
217
+ describe('stepActivation', () => {
218
+ it('skips when expected all linked and none broken', () => {
219
+ const a = spies();
220
+ a.gatherProject = jest.fn((_cwd, _bundles) => project({ activeBundles: { expected: ['x'], linked: ['x'], broken: [] } }));
221
+ const r = (0, steps_1.stepActivation)(deps({ machine: machine(), project: project() }, a));
222
+ expect(r.action).toBe('skipped');
223
+ expect(a.syncProfile).not.toHaveBeenCalled();
224
+ });
225
+ it('syncs when links missing', () => {
226
+ const a = spies();
227
+ a.gatherProject = jest.fn((_cwd, _bundles) => project({ activeBundles: { expected: ['x', 'y'], linked: ['x'], broken: [] } }));
228
+ const r = (0, steps_1.stepActivation)(deps({ machine: machine(), project: project() }, a));
229
+ expect(r.action).toBe('applied');
230
+ expect(a.syncProfile).toHaveBeenCalled();
231
+ });
232
+ it('passes target agent to gatherProject (#4 — project local skills scoped to agent)', () => {
233
+ const a = spies();
234
+ const m = machine();
235
+ m.devCore = { present: true, brokenLinks: [] };
236
+ const ctx = { machine: m, project: project() };
237
+ (0, steps_1.stepActivation)(deps(ctx, a, { agent: 'opencode' }));
238
+ expect(a.gatherProject).toHaveBeenCalledWith(expect.any(String), expect.any(Array), 'opencode');
239
+ });
240
+ });
241
+ describe('stepSensors', () => {
242
+ it('skips when sensors present', () => {
243
+ const a = spies();
244
+ expect((0, steps_1.stepSensors)(deps({ machine: machine(), project: project() }, a)).action).toBe('skipped');
245
+ });
246
+ it('inits sensors when absent', () => {
247
+ const a = spies();
248
+ const r = (0, steps_1.stepSensors)(deps({ machine: machine(), project: project({ sensors: { present: false } }) }, a));
249
+ expect(r.action).toBe('applied');
250
+ expect(a.initSensors).toHaveBeenCalledWith({ cwd: '/repo', registryRoot: '/cache/registry', configure: true }); // sensorPacksRoot='/cache/registry' from deps()
251
+ });
252
+ });
253
+ describe('stepConstitution / stepContext (frontera agente)', () => {
254
+ it('constitution pending + names the skill, never writes', () => {
255
+ const a = spies();
256
+ const r = (0, steps_1.stepConstitution)(deps({ machine: machine(), project: project({ constitution: { present: false } }) }, a));
257
+ expect(r.action).toBe('pending');
258
+ expect(r.detail).toContain('project-constitution');
259
+ });
260
+ it('context pending names project-context-init', () => {
261
+ const a = spies();
262
+ const r = (0, steps_1.stepContext)(deps({ machine: machine(), project: project({ context: { present: false } }) }, a));
263
+ expect(r.action).toBe('pending');
264
+ expect(r.detail).toContain('project-context-init');
265
+ });
266
+ it('both skip when present', () => {
267
+ const a = spies();
268
+ const ctx = { machine: machine(), project: project() };
269
+ expect((0, steps_1.stepConstitution)(deps(ctx, a)).action).toBe('skipped');
270
+ expect((0, steps_1.stepContext)(deps(ctx, a)).action).toBe('skipped');
271
+ });
272
+ });
273
+ describe('stepGlobalSkillsRepair', () => {
274
+ it('skips when nothing is broken', () => {
275
+ const a = spies();
276
+ const m = machine(); // globalSkills.repairable=[], dead=[] by default
277
+ const r = (0, steps_1.stepGlobalSkillsRepair)(deps({ machine: m, project: null }, a));
278
+ expect(r.action).toBe('skipped');
279
+ expect(a.repairGlobalSkills).not.toHaveBeenCalled();
280
+ });
281
+ it('applies repair when there are broken links', () => {
282
+ const a = spies();
283
+ a.repairGlobalSkills = jest.fn(() => ({ relinked: ['b'], pruned: ['c'], failed: [] }));
284
+ const m = machine();
285
+ m.globalSkills = { valid: ['a'], repairable: ['b'], dead: ['c'] };
286
+ const r = (0, steps_1.stepGlobalSkillsRepair)(deps({ machine: m, project: null }, a));
287
+ expect(r.action).toBe('applied');
288
+ expect(a.repairGlobalSkills).toHaveBeenCalledTimes(1);
289
+ expect(r.detail).toContain('re-linked 1');
290
+ expect(r.detail).toContain('pruned 1');
291
+ });
292
+ it('repairs the target agent skills dir, not Claude (#4)', () => {
293
+ const a = spies();
294
+ a.repairGlobalSkills = jest.fn(() => ({ relinked: ['b'], pruned: [], failed: [] }));
295
+ const m = machine();
296
+ m.globalSkills = { valid: [], repairable: ['b'], dead: [] };
297
+ const r = (0, steps_1.stepGlobalSkillsRepair)(deps({ machine: m, project: null }, a, { agent: 'opencode' }));
298
+ expect(r.action).toBe('applied');
299
+ expect(a.repairGlobalSkills).toHaveBeenCalledWith(providers_1.PROVIDERS['opencode'].skill.global, expect.any(Array));
300
+ });
301
+ });
302
+ describe('stepConstitutionInjection (#6)', () => {
303
+ it('injects for a config-instructions agent when CONSTITUTION.md is present', () => {
304
+ const a = spies();
305
+ const r = (0, steps_1.stepConstitutionInjection)(deps({ machine: machine(), project: project({ constitution: { present: true } }) }, a, { agent: 'opencode' }));
306
+ expect(r.action).toBe('applied');
307
+ expect(a.injectProjectConstitution).toHaveBeenCalledWith({ projectRoot: '/repo', agent: 'opencode' });
308
+ });
309
+ it('skips for Claude (delivered by the hook), never touching the action', () => {
310
+ const a = spies();
311
+ const r = (0, steps_1.stepConstitutionInjection)(deps({ machine: machine(), project: project({ constitution: { present: true } }) }, a, { agent: 'claude-code' }));
312
+ expect(r.action).toBe('skipped');
313
+ expect(a.injectProjectConstitution).not.toHaveBeenCalled();
314
+ });
315
+ it('skips when CONSTITUTION.md is absent', () => {
316
+ const a = spies();
317
+ const r = (0, steps_1.stepConstitutionInjection)(deps({ machine: machine(), project: project({ constitution: { present: false } }) }, a, { agent: 'opencode' }));
318
+ expect(r.action).toBe('skipped');
319
+ expect(a.injectProjectConstitution).not.toHaveBeenCalled();
320
+ });
321
+ it('maps already → skipped when CONSTITUTION.md was already in opencode.json', () => {
322
+ const a = spies();
323
+ a.injectProjectConstitution.mockReturnValue('already');
324
+ const r = (0, steps_1.stepConstitutionInjection)(deps({ machine: machine(), project: project({ constitution: { present: true } }) }, a, { agent: 'opencode' }));
325
+ expect(r.action).toBe('skipped');
326
+ expect(r.detail).toBe('already');
327
+ });
328
+ });
329
+ describe('stepContextInjection', () => {
330
+ it('skips claude-code (covered by stepHook)', () => {
331
+ const a = spies();
332
+ const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'claude-code' }));
333
+ expect(r.action).toBe('skipped');
334
+ expect(a.installContext).not.toHaveBeenCalled();
335
+ });
336
+ it('skips an agent without an injection mechanism', () => {
337
+ const a = spies();
338
+ const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'antigravity' }));
339
+ expect(r.action).toBe('skipped');
340
+ expect(a.installContext).not.toHaveBeenCalled();
341
+ });
342
+ it('skips opencode when already injected', () => {
343
+ const a = spies();
344
+ a.contextStatus = jest.fn(() => 'injected');
345
+ const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'opencode' }));
346
+ expect(r.action).toBe('skipped');
347
+ expect(a.installContext).not.toHaveBeenCalled();
348
+ });
349
+ it('installs context for opencode when absent', () => {
350
+ const a = spies();
351
+ a.contextStatus = jest.fn(() => 'absent');
352
+ const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'opencode' }));
353
+ expect(r.action).toBe('applied');
354
+ expect(a.installContext).toHaveBeenCalledWith(expect.objectContaining({ agent: 'opencode', scope: 'global' }));
355
+ });
356
+ it('installs context for opencode when stale', () => {
357
+ const a = spies();
358
+ a.contextStatus = jest.fn(() => 'stale');
359
+ const r = (0, steps_1.stepContextInjection)(deps({ machine: machine(), project: null }, a, { agent: 'opencode' }));
360
+ expect(r.action).toBe('applied');
361
+ expect(a.installContext).toHaveBeenCalled();
362
+ });
363
+ });
@@ -0,0 +1,13 @@
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
+ test('.awm (and therefore .awm/ledger) is gitignored — raw ledger never committed', () => {
9
+ const gitignore = fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../../.gitignore'), 'utf-8');
10
+ const lines = gitignore.split('\n').map(l => l.trim());
11
+ const coversAwm = lines.includes('.awm') || lines.includes('.awm/') || lines.includes('.awm/ledger') || lines.includes('.awm/ledger/');
12
+ expect(coversAwm).toBe(true);
13
+ });