agentic-workflow-manager 3.10.0 → 3.12.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 (54) hide show
  1. package/dist/src/commands/doctor.js +1 -1
  2. package/dist/src/commands/preflight/checks.js +27 -0
  3. package/dist/src/commands/sensors/formatters/mypy.js +30 -0
  4. package/dist/src/commands/sensors/formatters/ruff.js +45 -0
  5. package/dist/src/commands/sensors/formatters/shellcheck.js +45 -0
  6. package/dist/src/commands/sensors/index.js +11 -4
  7. package/dist/src/commands/sensors/init.js +80 -15
  8. package/dist/src/commands/sensors/run.js +36 -5
  9. package/dist/src/commands/sensors/status.js +8 -1
  10. package/dist/src/core/context/materializer.js +7 -0
  11. package/dist/src/core/context/orchestrator.js +26 -6
  12. package/dist/src/core/context/strategies/codex-agents.js +69 -15
  13. package/dist/src/core/diagnostics/context.js +11 -6
  14. package/dist/src/core/diagnostics/provider-checks.js +92 -11
  15. package/dist/src/core/init/mutation-targets.js +18 -2
  16. package/dist/src/core/init/provider-facts.js +5 -4
  17. package/dist/src/core/init/steps.js +16 -2
  18. package/dist/src/core/install-planner.js +56 -6
  19. package/dist/src/core/install-transaction.js +55 -6
  20. package/dist/src/core/provider-artifacts.js +1 -1
  21. package/dist/src/core/renderers/copilot-instructions.js +28 -0
  22. package/dist/src/core/renderers/cursor-mdc.js +49 -0
  23. package/dist/src/core/renderers/skill-source.js +50 -0
  24. package/dist/src/core/skill-integrity.js +1 -1
  25. package/dist/src/index.js +8 -0
  26. package/dist/src/providers/index.js +69 -2
  27. package/dist/tests/commands/add.test.js +96 -0
  28. package/dist/tests/commands/doctor.test.js +25 -0
  29. package/dist/tests/commands/init.test.js +56 -0
  30. package/dist/tests/commands/preflight/preflight.test.js +49 -14
  31. package/dist/tests/commands/sensors/formatters/mypy.test.js +60 -0
  32. package/dist/tests/commands/sensors/formatters/ruff.test.js +92 -0
  33. package/dist/tests/commands/sensors/formatters/shellcheck.test.js +65 -0
  34. package/dist/tests/commands/sensors/init.test.js +159 -4
  35. package/dist/tests/commands/sensors/run.test.js +91 -0
  36. package/dist/tests/commands/sensors/status.test.js +29 -0
  37. package/dist/tests/core/bundle-install.test.js +63 -0
  38. package/dist/tests/core/context/materializer.test.js +8 -0
  39. package/dist/tests/core/context/orchestrator.test.js +51 -0
  40. package/dist/tests/core/context/strategies/codex-agents.test.js +157 -20
  41. package/dist/tests/core/diagnostics/checks.test.js +1 -0
  42. package/dist/tests/core/diagnostics/provider-tier.test.js +292 -0
  43. package/dist/tests/core/init/mutation-targets.test.js +63 -0
  44. package/dist/tests/core/init/provider-facts.test.js +16 -0
  45. package/dist/tests/core/init/steps.test.js +37 -0
  46. package/dist/tests/core/install-planner.test.js +118 -0
  47. package/dist/tests/core/install-transaction.test.js +109 -0
  48. package/dist/tests/core/provider-artifacts.test.js +11 -0
  49. package/dist/tests/core/renderers/copilot-instructions.test.js +47 -0
  50. package/dist/tests/core/renderers/cursor-mdc.test.js +137 -0
  51. package/dist/tests/core/skill-integrity.test.js +18 -0
  52. package/dist/tests/providers/index.test.js +45 -1
  53. package/dist/tests/providers/injection-config.test.js +16 -0
  54. package/package.json +1 -1
@@ -101,7 +101,7 @@ function renderProviderReport(report) {
101
101
  lines.push(picocolors_1.default.bold('AWM · harness status'));
102
102
  lines.push('');
103
103
  for (const provider of report.providers) {
104
- lines.push(`Provider: ${provider.label}`);
104
+ lines.push(`Provider: ${provider.label} ${picocolors_1.default.dim(`(${provider.tier})`)}`);
105
105
  for (const check of provider.checks)
106
106
  lines.push(providerCheckLine(check));
107
107
  lines.push('');
@@ -64,6 +64,19 @@ function checkManifest(cwd, manifest) {
64
64
  }
65
65
  const total = Object.keys(manifest.sensors ?? {}).length;
66
66
  const enabled = Object.values(manifest.sensors ?? {}).filter(s => s.enabled !== false).length;
67
+ // total === 0 is NOT an opt-out: a deliberate opt-out lists every known sensor NAME
68
+ // explicitly with `enabled: false` (total > 0, enabled === 0). Zero entries means
69
+ // nothing was ever configured — most commonly because the registry had no pack.json
70
+ // for the detected stack, so `awm sensors init` built an honest, empty manifest
71
+ // rather than inventing defaults. That must not read as "all sensors disabled".
72
+ if (total === 0) {
73
+ return {
74
+ id: 'manifest',
75
+ ok: false,
76
+ detail: `pack '${manifest.pack}' has no sensors — the registry has no pack.json for it`,
77
+ remedy: `registry has no pack for '${manifest.pack}': run \`awm update\` or add a registry that has it`,
78
+ };
79
+ }
67
80
  return {
68
81
  id: 'manifest',
69
82
  ok: true,
@@ -77,6 +90,20 @@ function checkTools(cwd) {
77
90
  if (status.overall === 'NOT_CONFIGURED') {
78
91
  return { id: 'tools', ok: false, detail: 'no manifest to check', remedy: 'run `awm sensors init`' };
79
92
  }
93
+ // A manifest with zero sensor entries (honest-degraded — no pack.json reachable in
94
+ // the registry for this stack) makes `Object.entries({}).filter(...)` vacuously
95
+ // empty, which used to read as "0 broken out of 0" — a clean pass for a manifest
96
+ // that checks nothing at all. Mirrors the same zero-sensors signal `checkManifest`
97
+ // already guards against; this defends the invariant independently rather than
98
+ // relying solely on `checkManifest`'s gate to catch this exact manifest shape.
99
+ if (Object.keys(status.checks).length === 0) {
100
+ return {
101
+ id: 'tools',
102
+ ok: false,
103
+ detail: 'no sensors configured to check (0 sensor entries in the manifest)',
104
+ remedy: `registry has no pack for '${status.pack}': run \`awm update\` or add a registry that has it`,
105
+ };
106
+ }
80
107
  const broken = Object.entries(status.checks).filter(([, c]) => !c.ok);
81
108
  if (broken.length > 0) {
82
109
  return {
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseMypyOutput = parseMypyOutput;
4
+ // mypy's plain-text output (no --output json flag in this pack's defaultCmd), one
5
+ // finding per line: `file:line: error: message [code]`. The `[code]` suffix is
6
+ // separated from the message by two spaces and is OPTIONAL — some mypy error kinds
7
+ // omit it. No column: this pack's defaultCmd has no --show-column-numbers.
8
+ //
9
+ // Deliberately excluded, not matched by this pattern:
10
+ // - `note:` lines (e.g. `reveal_type` output, supplementary context) — not failures.
11
+ // - the trailing summary line (`Found N errors in M files…` / `Success: …`).
12
+ const MYPY_LINE = /^(.+):(\d+): error: (.*?)(?: \[(\S+)\])?$/;
13
+ function parseMypyOutput(raw) {
14
+ const errors = [];
15
+ for (const line of raw.split('\n')) {
16
+ if (!line)
17
+ continue;
18
+ const m = MYPY_LINE.exec(line);
19
+ if (!m)
20
+ continue;
21
+ const [, file, lineStr, msg, code] = m;
22
+ errors.push({
23
+ file,
24
+ line: parseInt(lineStr, 10),
25
+ rule: code,
26
+ message: `SENSOR[typecheck] ${file}:${lineStr} — ${msg} Fix: review the type annotation. Error code: ${code ?? 'n/a'}.`,
27
+ });
28
+ }
29
+ return errors;
30
+ }
@@ -0,0 +1,45 @@
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.parseRuffOutput = parseRuffOutput;
7
+ const path_1 = __importDefault(require("path"));
8
+ function parseRuffOutput(raw) {
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(raw);
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ // Valid JSON syntax does not guarantee the expected shape — `{}`, `null`, `42` all
17
+ // parse successfully but are not arrays, and an array element can itself be `null`
18
+ // or missing the fields this parser reads. Guard both, or a well-formed-but-wrong
19
+ // shape from `ruff --output-format=json` throws instead of degrading to [].
20
+ if (!Array.isArray(parsed))
21
+ return [];
22
+ const cwd = process.cwd();
23
+ const errors = [];
24
+ for (const item of parsed) {
25
+ if (!item || typeof item !== 'object')
26
+ continue;
27
+ const msg = item;
28
+ if (typeof msg.filename !== 'string')
29
+ continue;
30
+ if (!msg.location || typeof msg.location !== 'object'
31
+ || typeof msg.location.row !== 'number' || typeof msg.location.column !== 'number')
32
+ continue;
33
+ const rel = msg.filename.startsWith(cwd + path_1.default.sep)
34
+ ? path_1.default.relative(cwd, msg.filename)
35
+ : msg.filename;
36
+ errors.push({
37
+ file: rel,
38
+ line: msg.location.row,
39
+ column: msg.location.column,
40
+ rule: msg.code ?? 'unknown',
41
+ message: `SENSOR[lint] ${rel}:${msg.location.row} — ${msg.message ?? ''} Fix: check rule ${msg.code ?? 'unknown'}.`,
42
+ });
43
+ }
44
+ return errors;
45
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseShellcheckOutput = parseShellcheckOutput;
4
+ // shellcheck's own levels, from most to least severe: error, warning, info, style.
5
+ // `info`/`style` are advisory — quoting preferences, portability nits — not genuine
6
+ // problems (mirrors eslint.ts's `severity < 2` filter, which drops eslint's "warn" the
7
+ // same way: findings should be real breakage, not 100% of the tool's advisory noise).
8
+ // Only `error`/`warning` are reported as SensorErrors.
9
+ const FAILING_LEVELS = new Set(['error', 'warning']);
10
+ function parseShellcheckOutput(raw) {
11
+ let parsed;
12
+ try {
13
+ parsed = JSON.parse(raw);
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ // Valid JSON syntax does not guarantee the expected shape — `{}`, `null`, `42` all
19
+ // parse successfully but are not arrays, and an array element can itself be `null`
20
+ // or missing the fields this parser reads. Guard both, or a well-formed-but-wrong
21
+ // shape from `shellcheck -f json` throws instead of degrading to [].
22
+ if (!Array.isArray(parsed))
23
+ return [];
24
+ const errors = [];
25
+ for (const item of parsed) {
26
+ if (!item || typeof item !== 'object')
27
+ continue;
28
+ const msg = item;
29
+ if (typeof msg.file !== 'string' || typeof msg.line !== 'number'
30
+ || typeof msg.column !== 'number' || typeof msg.code !== 'number'
31
+ || !msg.level)
32
+ continue;
33
+ if (!FAILING_LEVELS.has(msg.level))
34
+ continue;
35
+ const rule = `SC${msg.code}`;
36
+ errors.push({
37
+ file: msg.file,
38
+ line: msg.line,
39
+ column: msg.column,
40
+ rule,
41
+ message: `SENSOR[lint] ${msg.file}:${msg.line} — ${msg.message ?? ''} Fix: see https://www.shellcheck.net/wiki/${rule}.`,
42
+ });
43
+ }
44
+ return errors;
45
+ }
@@ -46,12 +46,19 @@ function registerSensorsCommand(program) {
46
46
  .description('detect stack and write .awm/sensors.json (+ copy pack config files)')
47
47
  .option('--no-configure', 'skip copying sensor pack config files into the project')
48
48
  .option('--registry-root <path>', 'path to AWM registry root')
49
+ .option('--pack <name>', 'skip auto-detection, use this pack explicitly')
49
50
  .action((opts) => {
50
51
  const registryRoot = opts.registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs') ?? undefined;
51
- const result = (0, init_1.initSensors)({ configure: opts.configure, registryRoot });
52
- prompts_1.log.success(`Detected: ${result.detection.pack} (${result.detection.indicators.join(', ') || 'fallback'})`);
53
- prompts_1.log.success('Wrote .awm/sensors.json');
54
- result.configured.forEach((f) => prompts_1.log.info(` Installed ${f}`));
52
+ try {
53
+ const result = (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack });
54
+ prompts_1.log.success(`Detected: ${result.detection.pack} (${result.detection.indicators.join(', ') || 'fallback'})`);
55
+ prompts_1.log.success('Wrote .awm/sensors.json');
56
+ result.configured.forEach((f) => prompts_1.log.info(` Installed ${f}`));
57
+ }
58
+ catch (e) {
59
+ prompts_1.log.error(e instanceof Error ? e.message : String(e));
60
+ process.exit(1);
61
+ }
55
62
  });
56
63
  sensors
57
64
  .command('baseline')
@@ -11,14 +11,37 @@ const fs_1 = __importDefault(require("fs"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const STACK_DETECTORS = [
13
13
  { pack: 'js-ts', files: ['package.json'] },
14
- { pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg'] },
14
+ { pack: 'python', files: ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile'] },
15
15
  ];
16
+ // Shell detection is a glob (`*.sh` in the repo root or in `scripts/`), unlike the
17
+ // exact-filename matches above — so it needs its own scan rather than fitting the
18
+ // STACK_DETECTORS table. Tried last, after js-ts and python both fail: a Python
19
+ // project that also ships a root `deploy.sh` must still detect as `python`, never
20
+ // `shell`. Order of specificity: js-ts > python > shell > generic.
21
+ const SHELL_SCAN_DIRS = ['.', 'scripts'];
22
+ function findShellIndicators(cwd) {
23
+ const found = [];
24
+ for (const dir of SHELL_SCAN_DIRS) {
25
+ const full = path_1.default.join(cwd, dir);
26
+ if (!fs_1.default.existsSync(full) || !fs_1.default.statSync(full).isDirectory())
27
+ continue;
28
+ for (const entry of fs_1.default.readdirSync(full, { withFileTypes: true })) {
29
+ if (entry.isFile() && entry.name.endsWith('.sh')) {
30
+ found.push(dir === '.' ? entry.name : path_1.default.join(dir, entry.name));
31
+ }
32
+ }
33
+ }
34
+ return found;
35
+ }
16
36
  function detectStack(cwd) {
17
37
  for (const { pack, files } of STACK_DETECTORS) {
18
38
  const found = files.filter(f => fs_1.default.existsSync(path_1.default.join(cwd, f)));
19
39
  if (found.length > 0)
20
40
  return { pack, indicators: found };
21
41
  }
42
+ const shellIndicators = findShellIndicators(cwd);
43
+ if (shellIndicators.length > 0)
44
+ return { pack: 'shell', indicators: shellIndicators };
22
45
  return { pack: 'generic', indicators: [] };
23
46
  }
24
47
  // Candidate source dirs in priority order. `depcheck` analyzes the ones that
@@ -31,17 +54,6 @@ function detectSourceDirs(cwd) {
31
54
  });
32
55
  return found.length > 0 ? found : ['src'];
33
56
  }
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
57
  /**
46
58
  * Read sensor defaults from the pack's pack.json (the single source of truth).
47
59
  * Maps `defaultCmd` → `cmd` and substitutes the `{{SOURCE_DIRS}}` placeholder
@@ -76,21 +88,74 @@ function readPackDefaults(pack, registryRoot, cwd) {
76
88
  entry.changedCmd = def.changedCmd;
77
89
  if (def.changedExtensions)
78
90
  entry.changedExtensions = def.changedExtensions;
91
+ // Carries the real tool name (`mypy`, `ruff`, `shellcheck`…) so the runner can
92
+ // dispatch to the right output parser instead of guessing from the sensor name —
93
+ // see `SensorConfig.formatter`.
94
+ if (def.formatter)
95
+ entry.formatter = def.formatter;
79
96
  sensors[name] = entry;
80
97
  }
81
98
  return sensors;
82
99
  }
83
100
  function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
84
101
  const fromPack = registryRoot ? readPackDefaults(pack, registryRoot, cwd) : null;
85
- const defaults = fromPack ?? FALLBACK_DEFAULTS[pack] ?? {};
102
+ // No registry root, or the pack has no pack.json there → `{}` is the honest floor,
103
+ // not a bug to paper over with CLI-hardcoded defaults. `checkManifest` (preflight)
104
+ // and `computeSensorStatus` both surface a zero-sensor manifest as degraded, with a
105
+ // remedy pointing at the registry — never silently inventing sensors here instead.
106
+ const defaults = fromPack ?? {};
86
107
  const existingSensors = existing?.sensors ?? {};
87
- return { pack, sensors: { ...defaults, ...existingSensors } };
108
+ // Per-FIELD merge, not whole-sensor-object replacement: if `existingSensors.foo`
109
+ // exists at all, a naive `{ ...defaults, ...existingSensors }` would replace
110
+ // `defaults.foo` wholesale, permanently dropping any field that only lives in the
111
+ // (newer) pack default — e.g. a pre-`formatter`-era manifest re-merged against a
112
+ // pack.json that now declares `formatter` would silently lose it forever. Merging
113
+ // field-by-field within each sensor entry lets a user's hand-edited field (e.g. a
114
+ // custom `cmd`) win, while still inheriting any field the existing manifest doesn't
115
+ // specify.
116
+ const sensorNames = new Set([...Object.keys(defaults), ...Object.keys(existingSensors)]);
117
+ const sensors = {};
118
+ for (const name of sensorNames) {
119
+ sensors[name] = { ...defaults[name], ...existingSensors[name] };
120
+ }
121
+ return { pack, sensors };
122
+ }
123
+ /**
124
+ * Validate that `pack` exists as a directory under `<registryRoot>/sensor-packs/`.
125
+ * Throws (not a swallow-and-return) so `awm sensors init --pack bogus` actually stops
126
+ * instead of silently writing a manifest for a pack that doesn't exist. Lists every
127
+ * pack directory actually present, sorted, so the user immediately sees valid options.
128
+ */
129
+ function assertPackExists(pack, registryRoot) {
130
+ const packsDir = path_1.default.join(registryRoot, 'sensor-packs');
131
+ if (!fs_1.default.existsSync(packsDir) || !fs_1.default.statSync(packsDir).isDirectory()) {
132
+ throw new Error('registry has no sensor-packs directory');
133
+ }
134
+ const available = fs_1.default.readdirSync(packsDir, { withFileTypes: true })
135
+ .filter(e => e.isDirectory())
136
+ .map(e => e.name)
137
+ .sort();
138
+ if (!available.includes(pack)) {
139
+ throw new Error(`pack '${pack}' not found in registry (available: ${available.join(', ')})`);
140
+ }
88
141
  }
89
142
  function initSensors(opts = {}) {
90
143
  const cwd = opts.cwd ?? process.cwd();
91
144
  const configure = opts.configure ?? true; // configure (copy pack config files) by default
92
145
  const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
93
- const detection = detectStack(cwd);
146
+ // --pack skips the heuristic entirely. Only validate against the registry when a
147
+ // registryRoot was actually given — same tolerance pattern as readPackDefaults /
148
+ // buildManifest elsewhere in this file for a missing registry: nothing to validate
149
+ // against, so nothing is validated.
150
+ let detection;
151
+ if (opts.pack) {
152
+ if (opts.registryRoot)
153
+ assertPackExists(opts.pack, opts.registryRoot);
154
+ detection = { pack: opts.pack, indicators: ['--pack override'] };
155
+ }
156
+ else {
157
+ detection = detectStack(cwd);
158
+ }
94
159
  let existing;
95
160
  if (fs_1.default.existsSync(manifestPath)) {
96
161
  try {
@@ -17,6 +17,9 @@ const eslint_1 = require("./formatters/eslint");
17
17
  const semgrep_1 = require("./formatters/semgrep");
18
18
  const generic_1 = require("./formatters/generic");
19
19
  const test_1 = require("./formatters/test");
20
+ const mypy_1 = require("./formatters/mypy");
21
+ const ruff_1 = require("./formatters/ruff");
22
+ const shellcheck_1 = require("./formatters/shellcheck");
20
23
  const baseline_1 = require("./baseline");
21
24
  const changed_1 = require("./changed");
22
25
  const init_1 = require("./init");
@@ -117,7 +120,35 @@ function shouldRun(isFast, opts) {
117
120
  return true;
118
121
  return false;
119
122
  }
120
- function getFormatter(name) {
123
+ /**
124
+ * Dispatch by the pack's `formatter` field (the real tool behind the sensor slot —
125
+ * `lint` is eslint on js-ts but ruff on python, shellcheck on shell) when present.
126
+ * Manifests written before this field existed carry no `formatter`, so they fall back
127
+ * to the pre-existing name-based dispatch — nothing already installed breaks.
128
+ */
129
+ function getFormatter(name, formatterField) {
130
+ // A `formatter` field that is PRESENT but unrecognized (a typo in a pack.json, or a
131
+ // future pack declaring a tool this CLI version doesn't know about yet) is a
132
+ // different situation from no field at all. Falling through to name-based dispatch
133
+ // in that case would silently misparse a foreign output shape via the wrong parser
134
+ // (e.g. a `bandit` formatter falling through to `parseSemgrepOutput`, reading
135
+ // bandit's differently-shaped JSON and producing garbage findings). Only the
136
+ // ABSENT case (old manifest, written before this field existed) gets name-based
137
+ // backward-compat dispatch; a present-but-unknown value degrades honestly to the
138
+ // generic raw-wrap formatter instead.
139
+ if (formatterField !== undefined) {
140
+ switch (formatterField) {
141
+ case 'tsc': return tsc_1.parseTscOutput;
142
+ case 'eslint-llm': return eslint_1.parseEslintOutput;
143
+ case 'semgrep': return semgrep_1.parseSemgrepOutput;
144
+ case 'test': return test_1.parseTestOutput;
145
+ case 'mypy': return mypy_1.parseMypyOutput;
146
+ case 'ruff': return ruff_1.parseRuffOutput;
147
+ case 'shellcheck': return shellcheck_1.parseShellcheckOutput;
148
+ case 'generic': return generic_1.parseGenericOutput;
149
+ default: return generic_1.parseGenericOutput;
150
+ }
151
+ }
121
152
  if (name === 'typecheck')
122
153
  return tsc_1.parseTscOutput;
123
154
  if (name === 'lint')
@@ -131,9 +162,9 @@ function getFormatter(name) {
131
162
  function isExitCodeSensor(name) {
132
163
  return name === 'test';
133
164
  }
134
- async function runSensor(name, cmd, timeout, cwd) {
165
+ async function runSensor(name, cmd, timeout, cwd, formatterField) {
135
166
  const res = await (0, exec_1.runCommand)(cmd, { timeout, cwd, maxBuffer: MAX_BUFFER });
136
- const format = getFormatter(name);
167
+ const format = getFormatter(name, formatterField);
137
168
  // The shell itself never started (bad cwd, no shell). Nothing ran.
138
169
  if (res.spawnError) {
139
170
  return {
@@ -290,7 +321,7 @@ async function runSensors(opts = {}) {
290
321
  // in manifest order, so the reported order stays stable.
291
322
  const tasks = [];
292
323
  const settled = (r) => () => Promise.resolve(r);
293
- for (const [name, config] of Object.entries(activeManifest.sensors)) {
324
+ for (const [name, config] of Object.entries(activeManifest.sensors ?? {})) {
294
325
  const isFast = config.fast ?? false;
295
326
  if (!shouldRun(isFast, opts))
296
327
  continue;
@@ -336,7 +367,7 @@ async function runSensors(opts = {}) {
336
367
  }
337
368
  const timeout = config.timeout ?? (isFast ? DEFAULT_FAST_TIMEOUT : DEFAULT_SLOW_TIMEOUT);
338
369
  tasks.push(async () => {
339
- const result = await runSensor(name, cmd, timeout, cwd);
370
+ const result = await runSensor(name, cmd, timeout, cwd, config.formatter);
340
371
  const scoped = scope ? { ...result, scope } : result;
341
372
  return baseline ? applyBaseline(scoped, baseline[name]) : scoped;
342
373
  });
@@ -66,7 +66,7 @@ function computeSensorStatus(cwd = process.cwd()) {
66
66
  return { overall: 'NOT_CONFIGURED', pack: null, checks: {} };
67
67
  }
68
68
  const checks = {};
69
- for (const [name, config] of Object.entries(manifest.sensors)) {
69
+ for (const [name, config] of Object.entries(manifest.sensors ?? {})) {
70
70
  if (config.enabled === false) {
71
71
  checks[name] = { ok: true, detail: 'disabled' };
72
72
  continue;
@@ -77,6 +77,13 @@ function computeSensorStatus(cwd = process.cwd()) {
77
77
  }
78
78
  checks[name] = checkCmd(config.cmd, cwd);
79
79
  }
80
+ // `Object.values({}).every(...)` is vacuously true — a manifest with zero sensor
81
+ // entries (the registry had no pack.json for this stack; see init.ts) must not read
82
+ // as HEALTHY just because there was nothing to fail. Same false-green `checkManifest`
83
+ // guards against in preflight.
84
+ if (Object.keys(manifest.sensors ?? {}).length === 0) {
85
+ return { overall: 'DEGRADED', pack: manifest.pack, checks };
86
+ }
80
87
  const allOk = Object.values(checks).every(c => c.ok);
81
88
  return { overall: allOk ? 'HEALTHY' : 'DEGRADED', pack: manifest.pack, checks };
82
89
  }
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.globalContextPath = globalContextPath;
7
+ exports.projectContextPath = projectContextPath;
7
8
  exports.materialize = materialize;
8
9
  // cli/src/core/context/materializer.ts
9
10
  const fs_1 = __importDefault(require("fs"));
@@ -13,6 +14,12 @@ const paths_1 = require("../paths");
13
14
  function globalContextPath() {
14
15
  return path_1.default.join((0, paths_1.awmHome)(), 'context', 'awm-context.md');
15
16
  }
17
+ /** Project-scoped materialized-source path — mirrors globalContextPath()'s shape,
18
+ * rooted in the project instead of ~/.awm. This is the MATERIALIZED SOURCE
19
+ * content path, distinct from the injection TARGET file (<projectRoot>/AGENTS.md). */
20
+ function projectContextPath(projectRoot) {
21
+ return path_1.default.join(projectRoot, '.awm', 'context', 'awm-context.md');
22
+ }
16
23
  function materialize(ctx, absPath, scope) {
17
24
  let onDisk = null;
18
25
  try {
@@ -28,25 +28,42 @@ class InjectionOrchestrator {
28
28
  case 'managed-agents-md': return new codex_agents_1.CodexAgentsStrategy();
29
29
  }
30
30
  }
31
+ /** Materialized-source content path for `op.scope` — global under ~/.awm, local under the project. */
32
+ contextPathFor(op) {
33
+ if (this.overrides.contextPathOverride)
34
+ return this.overrides.contextPathOverride;
35
+ if (op.scope === 'local') {
36
+ if (!op.projectRoot)
37
+ throw new Error('projectRoot is required for local-scope context operations');
38
+ return (0, materializer_1.projectContextPath)(op.projectRoot);
39
+ }
40
+ return (0, materializer_1.globalContextPath)();
41
+ }
31
42
  /** Full input: builds context from registry and materializes to disk. Used by installContext only. */
32
43
  inputFor(op) {
33
44
  const ctx = (0, provider_1.buildContext)({ registryRoot: op.registryRoot, profileExtensions: op.profileExtensions });
34
- const absPath = this.overrides.contextPathOverride ?? (0, materializer_1.globalContextPath)();
45
+ const absPath = this.contextPathFor(op);
35
46
  const ref = (0, materializer_1.materialize)(ctx, absPath, op.scope);
36
- return { ref, registryRoot: op.registryRoot, installMethod: op.installMethod, agent: op.agent, scope: op.scope };
47
+ return {
48
+ ref, registryRoot: op.registryRoot, installMethod: op.installMethod,
49
+ agent: op.agent, scope: op.scope, projectRoot: op.projectRoot,
50
+ };
37
51
  }
38
52
  /** Path-only input: no buildContext, no materialize. Safe for remove() which never reads contentHash. */
39
53
  pathInputFor(op) {
40
- const absPath = this.overrides.contextPathOverride ?? (0, materializer_1.globalContextPath)();
54
+ const absPath = this.contextPathFor(op);
41
55
  const ref = { absPath, scope: op.scope, contentHash: '' };
42
- return { ref, registryRoot: op.registryRoot, installMethod: op.installMethod, agent: op.agent, scope: op.scope };
56
+ return {
57
+ ref, registryRoot: op.registryRoot, installMethod: op.installMethod,
58
+ agent: op.agent, scope: op.scope, projectRoot: op.projectRoot,
59
+ };
43
60
  }
44
61
  /**
45
62
  * Status input: builds context from registry (to get expected hash) but does NOT materialize.
46
63
  * Avoids silently correcting a stale file before the strategy can observe it.
47
64
  */
48
65
  statusInputFor(op) {
49
- const absPath = this.overrides.contextPathOverride ?? (0, materializer_1.globalContextPath)();
66
+ const absPath = this.contextPathFor(op);
50
67
  let contentHash = '';
51
68
  try {
52
69
  const ctx = (0, provider_1.buildContext)({ registryRoot: op.registryRoot, profileExtensions: op.profileExtensions });
@@ -59,7 +76,10 @@ class InjectionOrchestrator {
59
76
  throw err;
60
77
  }
61
78
  const ref = { absPath, scope: op.scope, contentHash };
62
- return { ref, registryRoot: op.registryRoot, installMethod: op.installMethod, agent: op.agent, scope: op.scope };
79
+ return {
80
+ ref, registryRoot: op.registryRoot, installMethod: op.installMethod,
81
+ agent: op.agent, scope: op.scope, projectRoot: op.projectRoot,
82
+ };
63
83
  }
64
84
  installContext(op) {
65
85
  const provider = this.provider(op.agent);