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,93 @@
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.fingerprint = fingerprint;
7
+ exports.readBaseline = readBaseline;
8
+ exports.writeBaseline = writeBaseline;
9
+ exports.partition = partition;
10
+ exports.buildBaseline = buildBaseline;
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const BASELINE_FILE = path_1.default.join('.awm', 'sensors.baseline.json');
15
+ /**
16
+ * Mask runs of digits so location noise embedded in messages (e.g. the tsc
17
+ * formatter writes "... line 199 ...") doesn't change the fingerprint when code
18
+ * shifts. Line/column fields are excluded from the fingerprint for the same reason.
19
+ */
20
+ function maskNumbers(s) {
21
+ return s.replace(/\d+/g, '#');
22
+ }
23
+ /**
24
+ * Identity of a finding, for matching against the accepted baseline.
25
+ *
26
+ * The basis deliberately EXCLUDES the human-readable message when a `rule` id is
27
+ * present (every real sensor — eslint `ruleId`, tsc error code, semgrep
28
+ * `check_id` — sets one). The message text is volatile: it changes with tool
29
+ * version bumps and rule-config tweaks (e.g. adding `argsIgnorePattern` makes
30
+ * eslint append "Allowed unused args must match ..."). When the message was
31
+ * part of the basis, any such change re-fingerprinted every existing finding, so
32
+ * the whole baseline silently went stale and reported hundreds of false "new"
33
+ * findings. Keying on `sensor|file|rule` makes the baseline immune to wording
34
+ * changes; occurrence counts (see `partition`) keep it precise.
35
+ *
36
+ * Findings without a rule (the generic formatter) fall back to the masked
37
+ * message, preserving the previous behaviour for that case.
38
+ */
39
+ function fingerprint(sensor, e) {
40
+ const basis = e.rule
41
+ ? `${sensor}|${e.file ?? ''}|${e.rule}`
42
+ : `${sensor}|${e.file ?? ''}|${maskNumbers(e.message)}`;
43
+ return crypto_1.default.createHash('sha1').update(basis).digest('hex');
44
+ }
45
+ function readBaseline(cwd) {
46
+ const p = path_1.default.join(cwd, BASELINE_FILE);
47
+ if (!fs_1.default.existsSync(p))
48
+ return null;
49
+ try {
50
+ return JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ function writeBaseline(cwd, baseline) {
57
+ fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
58
+ fs_1.default.writeFileSync(path_1.default.join(cwd, BASELINE_FILE), JSON.stringify(baseline, null, 2), 'utf-8');
59
+ }
60
+ /**
61
+ * Split a sensor's findings into new vs baseline-suppressed. With no accepted
62
+ * set, every finding is new (backward-compatible: no baseline file → current behavior).
63
+ */
64
+ function partition(sensor, errors, accepted) {
65
+ if (!accepted || accepted.length === 0)
66
+ return { newErrors: errors, suppressed: 0 };
67
+ // Count-based matching: the baseline holds one fingerprint per accepted
68
+ // occurrence (duplicates allowed). A finding is suppressed only while there's
69
+ // remaining "budget" for its fingerprint. This is what closes the gap left by
70
+ // keying on `sensor|file|rule` alone — if a file had 3 accepted findings of a
71
+ // rule and now has 5, the 2 extra are correctly reported as new.
72
+ const remaining = new Map();
73
+ for (const fp of accepted)
74
+ remaining.set(fp, (remaining.get(fp) ?? 0) + 1);
75
+ const newErrors = [];
76
+ for (const e of errors) {
77
+ const fp = fingerprint(sensor, e);
78
+ const budget = remaining.get(fp) ?? 0;
79
+ if (budget > 0)
80
+ remaining.set(fp, budget - 1);
81
+ else
82
+ newErrors.push(e);
83
+ }
84
+ return { newErrors, suppressed: errors.length - newErrors.length };
85
+ }
86
+ /** Snapshot the current findings of a full run as the accepted baseline. */
87
+ function buildBaseline(results) {
88
+ const baseline = {};
89
+ for (const r of results) {
90
+ baseline[r.name] = r.errors.map(e => fingerprint(r.name, e));
91
+ }
92
+ return baseline;
93
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseEslintOutput = parseEslintOutput;
4
+ function parseEslintOutput(raw) {
5
+ let parsed;
6
+ try {
7
+ parsed = JSON.parse(raw);
8
+ }
9
+ catch {
10
+ return [];
11
+ }
12
+ const errors = [];
13
+ const cwd = process.cwd();
14
+ for (const file of parsed) {
15
+ for (const msg of file.messages) {
16
+ if (msg.severity < 2 || msg.line == null || msg.column == null)
17
+ continue;
18
+ const rel = file.filePath.startsWith(cwd + '/') ? file.filePath.slice(cwd.length + 1) : file.filePath;
19
+ errors.push({
20
+ file: rel,
21
+ line: msg.line,
22
+ column: msg.column,
23
+ rule: msg.ruleId ?? 'unknown',
24
+ message: `SENSOR[lint] ${rel}:${msg.line} — ${msg.message} Fix: check rule ${msg.ruleId ?? 'unknown'}.`,
25
+ });
26
+ }
27
+ }
28
+ return errors;
29
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseGenericOutput = parseGenericOutput;
4
+ function parseGenericOutput(raw) {
5
+ if (!raw.trim())
6
+ return [];
7
+ return [{ message: `SENSOR[raw] ${raw.trim()}` }];
8
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSemgrepOutput = parseSemgrepOutput;
4
+ function parseSemgrepOutput(raw) {
5
+ let parsed;
6
+ try {
7
+ parsed = JSON.parse(raw);
8
+ }
9
+ catch {
10
+ return [];
11
+ }
12
+ return (parsed.results ?? []).map(r => ({
13
+ file: r.path,
14
+ line: r.start?.line ?? 0,
15
+ rule: r.check_id,
16
+ message: `SENSOR[security] ${r.path}:${r.start?.line ?? '?'} — ${r.extra?.message ?? 'unknown'} Fix: review rule ${r.check_id}.`,
17
+ }));
18
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTestOutput = parseTestOutput;
4
+ /**
5
+ * Tests are an exit-code sensor: the runner's exit status IS the signal, not the
6
+ * parsed output. A passing run prints output ("6 passed") that must NOT be treated
7
+ * as findings — so the success path yields no errors. The failure path (non-zero
8
+ * exit) is handled in runSensor via isExitCodeSensor, not here.
9
+ */
10
+ function parseTestOutput(_raw) {
11
+ return [];
12
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTscOutput = parseTscOutput;
4
+ const TSC_LINE = /^(.+)\((\d+),(\d+)\): error (TS\d+): (.+)$/;
5
+ function parseTscOutput(raw) {
6
+ const errors = [];
7
+ for (const line of raw.split('\n')) {
8
+ if (!line)
9
+ continue;
10
+ const m = TSC_LINE.exec(line);
11
+ if (!m)
12
+ continue;
13
+ const [, file, lineStr, colStr, code, msg] = m;
14
+ errors.push({
15
+ file,
16
+ line: parseInt(lineStr, 10),
17
+ column: parseInt(colStr, 10),
18
+ rule: code,
19
+ message: `SENSOR[typecheck] ${file} line ${lineStr} — ${msg} Fix: review the type annotation. Error code: ${code}.`,
20
+ });
21
+ }
22
+ return errors;
23
+ }
@@ -0,0 +1,94 @@
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.exitCodeFor = exitCodeFor;
7
+ exports.registerSensorsCommand = registerSensorsCommand;
8
+ const picocolors_1 = __importDefault(require("picocolors"));
9
+ const prompts_1 = require("@clack/prompts");
10
+ const run_1 = require("./run");
11
+ const init_1 = require("./init");
12
+ const status_1 = require("./status");
13
+ const install_1 = require("./install");
14
+ const baseline_1 = require("./baseline");
15
+ const registries_1 = require("../../core/registries");
16
+ /** Map a sensor run verdict to a process exit code. fail → 1; everything else → 0.
17
+ * not_certified intentionally exits 0: its signal lives in `overall`, because
18
+ * exit code 2 is a blocking error in Claude Code hooks. */
19
+ function exitCodeFor(output) {
20
+ return output.overall === 'fail' ? 1 : 0;
21
+ }
22
+ function registerSensorsCommand(program) {
23
+ const sensors = program.command('sensors').description('manage computational sensors for the current project');
24
+ sensors
25
+ .command('run')
26
+ .description('run sensors from .awm/sensors.json')
27
+ .option('--fast', 'run fast sensors only (tsc, lint)')
28
+ .option('--slow', 'run slow sensors only (semgrep, mutation)')
29
+ .option('--all', 'run all sensors regardless of speed')
30
+ .action((opts) => {
31
+ const output = (0, run_1.runSensors)({ fast: opts.fast, slow: opts.slow, all: opts.all });
32
+ // Emit the verdict ALWAYS — an empty `sensors` with overall:'not_certified'
33
+ // must be visible, never a silent exit-0 that reads as "clean".
34
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
35
+ const code = exitCodeFor(output);
36
+ if (code !== 0)
37
+ process.exit(code);
38
+ });
39
+ sensors
40
+ .command('init')
41
+ .description('detect stack and write .awm/sensors.json (+ copy pack config files)')
42
+ .option('--no-configure', 'skip copying sensor pack config files into the project')
43
+ .option('--registry-root <path>', 'path to AWM registry root')
44
+ .action((opts) => {
45
+ const registryRoot = opts.registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs') ?? undefined;
46
+ const result = (0, init_1.initSensors)({ configure: opts.configure, registryRoot });
47
+ prompts_1.log.success(`Detected: ${result.detection.pack} (${result.detection.indicators.join(', ') || 'fallback'})`);
48
+ prompts_1.log.success('Wrote .awm/sensors.json');
49
+ result.configured.forEach((f) => prompts_1.log.info(` Installed ${f}`));
50
+ });
51
+ sensors
52
+ .command('baseline')
53
+ .description('snapshot current findings as accepted — sensors then fail only on NEW ones')
54
+ .action(() => {
55
+ const manifestDir = (0, run_1.findManifestDir)(process.cwd());
56
+ const output = (0, run_1.runSensors)({ all: true, ignoreBaseline: true });
57
+ const baseline = (0, baseline_1.buildBaseline)(output.sensors.map(s => ({ name: s.name, errors: s.errors })));
58
+ const writeDir = manifestDir ?? process.cwd();
59
+ (0, baseline_1.writeBaseline)(writeDir, baseline);
60
+ const total = Object.values(baseline).reduce((n, fps) => n + fps.length, 0);
61
+ prompts_1.log.success(`Baseline guardado: ${total} hallazgos aceptados en .awm/sensors.baseline.json`);
62
+ prompts_1.log.info('Los sensors ahora fallan solo ante hallazgos nuevos. Re-corré `awm sensors baseline` tras reducir deuda.');
63
+ });
64
+ sensors
65
+ .command('status')
66
+ .description('check sensor health for the current project')
67
+ .action(() => {
68
+ const status = (0, status_1.computeSensorStatus)();
69
+ const icon = status.overall === 'HEALTHY' ? picocolors_1.default.green('✔') : picocolors_1.default.yellow('⚠');
70
+ console.log(`\nPack: ${status.pack ?? 'none'}`);
71
+ console.log(`Overall: ${icon} ${status.overall}\n`);
72
+ for (const [name, check] of Object.entries(status.checks)) {
73
+ const mark = check.ok ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘');
74
+ console.log(` ${mark} ${name.padEnd(12)} ${check.detail}`);
75
+ }
76
+ console.log('');
77
+ if (status.overall !== 'HEALTHY')
78
+ process.exit(1);
79
+ });
80
+ sensors
81
+ .command('install')
82
+ .description('install PostToolUse hook in ~/.claude/settings.json')
83
+ .action(() => {
84
+ const result = (0, install_1.installSensorHook)();
85
+ if (result.status === 'already-installed') {
86
+ prompts_1.log.info('PostToolUse hook already installed.');
87
+ }
88
+ else {
89
+ prompts_1.log.success('PostToolUse hook installed in ~/.claude/settings.json');
90
+ if (result.backupPath)
91
+ prompts_1.log.info(` Backup: ${result.backupPath}`);
92
+ }
93
+ });
94
+ }
@@ -0,0 +1,112 @@
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.detectStack = detectStack;
7
+ exports.detectSourceDirs = detectSourceDirs;
8
+ exports.buildManifest = buildManifest;
9
+ exports.initSensors = initSensors;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const STACK_DETECTORS = [
13
+ { pack: 'js-ts', files: ['package.json'] },
14
+ { pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg'] },
15
+ ];
16
+ function detectStack(cwd) {
17
+ for (const { pack, files } of STACK_DETECTORS) {
18
+ const found = files.filter(f => fs_1.default.existsSync(path_1.default.join(cwd, f)));
19
+ if (found.length > 0)
20
+ return { pack, indicators: found };
21
+ }
22
+ return { pack: 'generic', indicators: [] };
23
+ }
24
+ // Candidate source dirs in priority order. `depcheck` analyzes the ones that
25
+ // exist — a project may use `src/`, or App-Router-style `app/lib/components/...`.
26
+ const SOURCE_DIR_CANDIDATES = ['src', 'app', 'lib', 'components', 'hooks', 'pages'];
27
+ function detectSourceDirs(cwd) {
28
+ const found = SOURCE_DIR_CANDIDATES.filter(d => {
29
+ const p = path_1.default.join(cwd, d);
30
+ return fs_1.default.existsSync(p) && fs_1.default.statSync(p).isDirectory();
31
+ });
32
+ return found.length > 0 ? found : ['src'];
33
+ }
34
+ // Fallback defaults for packs that don't yet ship a pack.json in the registry
35
+ // (today: python). js-ts/generic are sourced from
36
+ // registry/sensor-packs/<pack>/pack.json — single source of truth.
37
+ const FALLBACK_DEFAULTS = {
38
+ python: {
39
+ typecheck: { cmd: 'mypy .', fast: true },
40
+ lint: { cmd: 'ruff check . --output-format json', fast: true },
41
+ security: { cmd: 'semgrep --config .semgrep.awm.yml --json .', fast: false },
42
+ mutation: { enabled: false },
43
+ },
44
+ };
45
+ /**
46
+ * Read sensor defaults from the pack's pack.json (the single source of truth).
47
+ * Maps `defaultCmd` → `cmd` and substitutes the `{{SOURCE_DIRS}}` placeholder
48
+ * with the project's actual source dirs. Returns null if the pack has no pack.json.
49
+ */
50
+ function readPackDefaults(pack, registryRoot, cwd) {
51
+ const packJsonPath = path_1.default.join(registryRoot, 'sensor-packs', pack, 'pack.json');
52
+ if (!fs_1.default.existsSync(packJsonPath))
53
+ return null;
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(fs_1.default.readFileSync(packJsonPath, 'utf-8'));
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ const sourceDirs = detectSourceDirs(cwd).join(' ');
62
+ const sensors = {};
63
+ for (const [name, def] of Object.entries(parsed.sensors ?? {})) {
64
+ const entry = {};
65
+ if (def.defaultCmd)
66
+ entry.cmd = def.defaultCmd.replace('{{SOURCE_DIRS}}', sourceDirs);
67
+ if (def.fast !== undefined)
68
+ entry.fast = def.fast;
69
+ if (def.enabled !== undefined)
70
+ entry.enabled = def.enabled;
71
+ sensors[name] = entry;
72
+ }
73
+ return sensors;
74
+ }
75
+ function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
76
+ const fromPack = registryRoot ? readPackDefaults(pack, registryRoot, cwd) : null;
77
+ const defaults = fromPack ?? FALLBACK_DEFAULTS[pack] ?? {};
78
+ const existingSensors = existing?.sensors ?? {};
79
+ return { pack, sensors: { ...defaults, ...existingSensors } };
80
+ }
81
+ function initSensors(opts = {}) {
82
+ const cwd = opts.cwd ?? process.cwd();
83
+ const configure = opts.configure ?? true; // configure (copy pack config files) by default
84
+ const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
85
+ const detection = detectStack(cwd);
86
+ let existing;
87
+ if (fs_1.default.existsSync(manifestPath)) {
88
+ try {
89
+ existing = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
90
+ }
91
+ catch { /* ignore corrupt manifest */ }
92
+ }
93
+ const manifest = buildManifest(detection.pack, existing, opts.registryRoot, cwd);
94
+ fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
95
+ const tmpPath = manifestPath + '.tmp';
96
+ fs_1.default.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2), 'utf-8');
97
+ fs_1.default.renameSync(tmpPath, manifestPath);
98
+ const configured = [];
99
+ if (configure && opts.registryRoot) {
100
+ const packDir = path_1.default.join(opts.registryRoot, 'sensor-packs', detection.pack);
101
+ if (fs_1.default.existsSync(packDir)) {
102
+ for (const file of fs_1.default.readdirSync(packDir).filter(f => f !== 'pack.json')) {
103
+ const dst = path_1.default.join(cwd, file);
104
+ if (!fs_1.default.existsSync(dst)) {
105
+ fs_1.default.copyFileSync(path_1.default.join(packDir, file), dst);
106
+ configured.push(file);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return { manifest, detection, configured };
112
+ }
@@ -0,0 +1,78 @@
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.installSensorHook = installSensorHook;
7
+ exports.uninstallSensorHook = uninstallSensorHook;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const POST_TOOL_USE_EVENT = 'PostToolUse';
12
+ const POST_TOOL_USE_MATCHER = 'Write|Edit|MultiEdit';
13
+ const AWM_SENSOR_CMD = 'awm sensors run --fast';
14
+ function defaultSettingsPath() {
15
+ return path_1.default.join(process.env.HOME ?? os_1.default.homedir(), '.claude', 'settings.json');
16
+ }
17
+ function readSettings(p) {
18
+ if (!fs_1.default.existsSync(p))
19
+ return {};
20
+ try {
21
+ return JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
22
+ }
23
+ catch {
24
+ return {};
25
+ }
26
+ }
27
+ function isAwmEntry(e) {
28
+ return e.matcher === POST_TOOL_USE_MATCHER &&
29
+ (e.hooks ?? []).some(h => h.command === AWM_SENSOR_CMD);
30
+ }
31
+ function backupSettings(settingsPath) {
32
+ if (!fs_1.default.existsSync(settingsPath))
33
+ return undefined;
34
+ const awmHome = process.env.AWM_HOME || path_1.default.join(process.env.HOME ?? os_1.default.homedir(), '.awm');
35
+ const backupDir = path_1.default.join(awmHome, 'backups');
36
+ fs_1.default.mkdirSync(backupDir, { recursive: true });
37
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').slice(0, 19);
38
+ const backupPath = path_1.default.join(backupDir, `settings.json.${ts}.sensor.bak`);
39
+ fs_1.default.copyFileSync(settingsPath, backupPath);
40
+ return backupPath;
41
+ }
42
+ function installSensorHook(settingsPath = defaultSettingsPath()) {
43
+ const settings = readSettings(settingsPath);
44
+ const entries = settings?.hooks?.[POST_TOOL_USE_EVENT] ?? [];
45
+ if (entries.some(isAwmEntry))
46
+ return { status: 'already-installed' };
47
+ const backupPath = backupSettings(settingsPath);
48
+ const newEntry = {
49
+ matcher: POST_TOOL_USE_MATCHER,
50
+ hooks: [{ type: 'command', command: AWM_SENSOR_CMD }],
51
+ };
52
+ const updated = {
53
+ ...settings,
54
+ hooks: {
55
+ ...(settings.hooks ?? {}),
56
+ [POST_TOOL_USE_EVENT]: [...entries, newEntry],
57
+ },
58
+ };
59
+ fs_1.default.mkdirSync(path_1.default.dirname(settingsPath), { recursive: true });
60
+ fs_1.default.writeFileSync(settingsPath, JSON.stringify(updated, null, 2), 'utf-8');
61
+ return { status: 'installed', backupPath };
62
+ }
63
+ function uninstallSensorHook(settingsPath = defaultSettingsPath()) {
64
+ if (!fs_1.default.existsSync(settingsPath))
65
+ return { status: 'not-found' };
66
+ const settings = readSettings(settingsPath);
67
+ const entries = settings?.hooks?.[POST_TOOL_USE_EVENT] ?? [];
68
+ const filtered = entries.filter(e => !isAwmEntry(e));
69
+ if (filtered.length === entries.length)
70
+ return { status: 'not-found' };
71
+ const updated = { ...settings, hooks: { ...(settings.hooks ?? {}), [POST_TOOL_USE_EVENT]: filtered } };
72
+ if (updated.hooks[POST_TOOL_USE_EVENT].length === 0)
73
+ delete updated.hooks[POST_TOOL_USE_EVENT];
74
+ if (Object.keys(updated.hooks).length === 0)
75
+ delete updated.hooks;
76
+ fs_1.default.writeFileSync(settingsPath, JSON.stringify(updated, null, 2), 'utf-8');
77
+ return { status: 'removed' };
78
+ }