agentic-workflow-manager 3.12.0 → 3.13.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.
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const status_1 = require("../sensors/status");
11
11
  const init_1 = require("../sensors/init");
12
+ const baseline_1 = require("../sensors/baseline");
12
13
  const paths_1 = require("../../core/paths");
13
14
  const MANIFEST = path_1.default.join('.awm', 'sensors.json');
14
15
  /**
@@ -36,6 +37,18 @@ function readManifest(cwd) {
36
37
  return null;
37
38
  }
38
39
  }
40
+ /**
41
+ * Total sensor entries and how many are enabled (`enabled !== false`, so a missing
42
+ * `enabled` field defaults to counted-as-enabled). Shared by `checkManifest` and
43
+ * `checkSensorsBaseline` — they were previously two copies of the identical
44
+ * predicate, which post-implementation-qa flagged: a hardening applied to one (e.g.
45
+ * guarding a malformed sensor entry) would silently NOT apply to the other unless
46
+ * someone remembered to edit both. One function, two callers, one place to harden.
47
+ */
48
+ function countEnabledSensors(manifest) {
49
+ const entries = Object.values(manifest.sensors ?? {});
50
+ return { total: entries.length, enabled: entries.filter(s => s.enabled !== false).length };
51
+ }
39
52
  /**
40
53
  * A repo may legitimately have no sensors — but it has to SAY so, in a committed file.
41
54
  *
@@ -62,8 +75,7 @@ function checkManifest(cwd, manifest) {
62
75
  remedy: 'fix or regenerate it with `awm sensors init`',
63
76
  };
64
77
  }
65
- const total = Object.keys(manifest.sensors ?? {}).length;
66
- const enabled = Object.values(manifest.sensors ?? {}).filter(s => s.enabled !== false).length;
78
+ const { total, enabled } = countEnabledSensors(manifest);
67
79
  // total === 0 is NOT an opt-out: a deliberate opt-out lists every known sensor NAME
68
80
  // explicitly with `enabled: false` (total > 0, enabled === 0). Zero entries means
69
81
  // nothing was ever configured — most commonly because the registry had no pack.json
@@ -135,6 +147,51 @@ function checkPack(cwd, manifest) {
135
147
  }
136
148
  return { id: 'pack', ok: true, detail: `${manifest.pack} matches the detected stack` };
137
149
  }
150
+ /**
151
+ * Advisory only — `ok` is ALWAYS `true`, same contract as `checkHost` below. A team
152
+ * adopting AWM on a legacy repo starts with pre-existing sensor findings; the ratchet
153
+ * (`awm sensors baseline`, `.awm/sensors.baseline.json`) exists precisely to snapshot
154
+ * those as accepted debt so the gate only fails on genuinely NEW findings. But nothing
155
+ * today surfaces that the mechanism exists — the team discovers it only after hitting a
156
+ * wall of red findings and going looking. This nudges them toward it before that
157
+ * happens. It never blocks preflight: a repo can legitimately have zero debt to
158
+ * snapshot (sensors enabled from day one), and "no baseline yet" is not itself a
159
+ * failure — only the operator's lack of awareness that baselining is an option is the
160
+ * problem this addresses.
161
+ *
162
+ * Only called when a manifest exists (see the conditional spread in `preflight()`) —
163
+ * there is nothing to baseline without sensors configured in the first place, so this
164
+ * mirrors how `checkTools`/`checkPack` are skipped entirely rather than reported on a
165
+ * repo that was never set up.
166
+ *
167
+ * Also requires at least one ENABLED sensor, via the same `countEnabledSensors` helper
168
+ * `checkManifest` uses — a deliberate opt-out (every sensor `enabled: false`) or an
169
+ * unparseable/empty manifest has nothing to baseline either, and nudging "run `awm
170
+ * sensors baseline`" there would be actively misleading rather than merely unnecessary.
171
+ *
172
+ * Presence is checked via `readBaseline` (same function `partition()` uses at gate time
173
+ * to decide suppression), not a raw `fs.existsSync` — a baseline PATH that exists but
174
+ * isn't a readable JSON file (e.g. a stray directory at that path) is treated by the
175
+ * real gate as "no baseline, nothing suppressed"; `existsSync` alone would have reported
176
+ * "baseline present" for that same case, reassuring the operator that debt is being
177
+ * suppressed when it silently is not.
178
+ */
179
+ function checkSensorsBaseline(cwd, manifest) {
180
+ const enabled = manifest ? countEnabledSensors(manifest).enabled : 0;
181
+ if (enabled === 0) {
182
+ return { id: 'sensors-baseline', ok: true, detail: 'no enabled sensors — nothing to baseline' };
183
+ }
184
+ if ((0, baseline_1.readBaseline)(cwd) !== null) {
185
+ return { id: 'sensors-baseline', ok: true, detail: 'baseline present' };
186
+ }
187
+ return {
188
+ id: 'sensors-baseline',
189
+ ok: true,
190
+ detail: 'sensors configured, no baseline yet — awm sensors baseline',
191
+ remedy: 'run `awm sensors baseline` to snapshot pre-existing findings as accepted debt, '
192
+ + 'so the gate only chases new problems',
193
+ };
194
+ }
138
195
  /**
139
196
  * Extract just the hostname portion of a git remote URL — never match against the
140
197
  * full URL string. A bare substring check against the whole remote (`remote.includes
@@ -224,9 +281,10 @@ function preflight(cwd = process.cwd()) {
224
281
  const checks = [
225
282
  checkContext(cwd),
226
283
  checkManifest(cwd, manifest),
227
- // Skipped when there is no manifest: reporting "tools broken" on a repo that was
228
- // never set up buries the one thing the operator needs to read.
229
- ...(manifestExists ? [checkTools(cwd), checkPack(cwd, manifest)] : []),
284
+ // Skipped when there is no manifest: reporting "tools broken" (or nudging toward
285
+ // a baseline that has nothing to snapshot) on a repo that was never set up
286
+ // buries the one thing the operator needs to read.
287
+ ...(manifestExists ? [checkTools(cwd), checkPack(cwd, manifest), checkSensorsBaseline(cwd, manifest)] : []),
230
288
  // Runs unconditionally — orthogonal to sensor configuration entirely, this is
231
289
  // about PR/MR tooling, not sensors.
232
290
  checkHost(cwd),
@@ -20,7 +20,12 @@ function exitCodeFor(report) {
20
20
  return report.status === 'ready' ? 0 : 1;
21
21
  }
22
22
  function formatReport(report) {
23
- const lines = report.checks.map(c => ` ${c.ok ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘')} ${c.id.padEnd(9)} ${c.detail}`
23
+ // Computed from the actual ids present, not hardcoded to the widest id THIS
24
+ // report happens to have — a fixed literal here silently misaligns the moment a
25
+ // longer `PreflightCheck['id']` is added (confirmed: 'sensors-baseline', at 16
26
+ // chars, broke a hardcoded 9-char pad).
27
+ const idWidth = Math.max(0, ...report.checks.map(c => c.id.length));
28
+ const lines = report.checks.map(c => ` ${c.ok ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘')} ${c.id.padEnd(idWidth)} ${c.detail}`
24
29
  + (c.remedy ? `\n ${picocolors_1.default.dim('→ ' + c.remedy)}` : ''));
25
30
  if (report.status === 'ready') {
26
31
  return `${picocolors_1.default.green('✔')} Harness ready — this project can be gated.\n${lines.join('\n')}\n`;
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BASELINE_FILE = void 0;
6
7
  exports.fingerprint = fingerprint;
7
8
  exports.readBaseline = readBaseline;
8
9
  exports.writeBaseline = writeBaseline;
@@ -11,7 +12,7 @@ exports.buildBaseline = buildBaseline;
11
12
  const crypto_1 = __importDefault(require("crypto"));
12
13
  const fs_1 = __importDefault(require("fs"));
13
14
  const path_1 = __importDefault(require("path"));
14
- const BASELINE_FILE = path_1.default.join('.awm', 'sensors.baseline.json');
15
+ exports.BASELINE_FILE = path_1.default.join('.awm', 'sensors.baseline.json');
15
16
  /**
16
17
  * Mask runs of digits so location noise embedded in messages (e.g. the tsc
17
18
  * formatter writes "... line 199 ...") doesn't change the fingerprint when code
@@ -43,7 +44,7 @@ function fingerprint(sensor, e) {
43
44
  return crypto_1.default.createHash('sha1').update(basis).digest('hex');
44
45
  }
45
46
  function readBaseline(cwd) {
46
- const p = path_1.default.join(cwd, BASELINE_FILE);
47
+ const p = path_1.default.join(cwd, exports.BASELINE_FILE);
47
48
  if (!fs_1.default.existsSync(p))
48
49
  return null;
49
50
  try {
@@ -55,7 +56,7 @@ function readBaseline(cwd) {
55
56
  }
56
57
  function writeBaseline(cwd, baseline) {
57
58
  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
+ fs_1.default.writeFileSync(path_1.default.join(cwd, exports.BASELINE_FILE), JSON.stringify(baseline, null, 2), 'utf-8');
59
60
  }
60
61
  /**
61
62
  * Split a sensor's findings into new vs baseline-suppressed. With no accepted
@@ -312,6 +312,81 @@ describe('preflight', () => {
312
312
  expect(check(report, 'host').detail).toContain('gitlab detected');
313
313
  });
314
314
  });
315
+ describe('sensors-baseline check (advisory — never changes the exit code)', () => {
316
+ it('nudges toward `awm sensors baseline` when sensors are configured but no baseline exists', () => {
317
+ // The team-rollout gap this addresses: a legacy repo adopts AWM, sensors get
318
+ // configured, and the ratchet mechanism exists to snapshot pre-existing debt —
319
+ // but nothing tells the operator it's there until they hit a wall of red
320
+ // findings and go looking for it.
321
+ const dir = make({
322
+ manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } },
323
+ bins: ['eslint'],
324
+ files: ['package.json'],
325
+ });
326
+ const report = (0, checks_1.preflight)(dir);
327
+ expect(check(report, 'sensors-baseline').ok).toBe(true);
328
+ expect(check(report, 'sensors-baseline').detail).toContain('no baseline yet');
329
+ expect(check(report, 'sensors-baseline').remedy).toContain('awm sensors baseline');
330
+ expect(report.status).toBe('ready');
331
+ });
332
+ it('reports the no-advisory-needed state when a baseline already exists, without nudging', () => {
333
+ const dir = make({
334
+ manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } },
335
+ bins: ['eslint'],
336
+ files: ['package.json'],
337
+ });
338
+ fs_1.default.mkdirSync(path_1.default.join(dir, '.awm'), { recursive: true });
339
+ fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'sensors.baseline.json'), JSON.stringify({ lint: [] }));
340
+ const report = (0, checks_1.preflight)(dir);
341
+ expect(check(report, 'sensors-baseline').ok).toBe(true);
342
+ expect(check(report, 'sensors-baseline').detail).toBe('baseline present');
343
+ expect(check(report, 'sensors-baseline').remedy).toBeUndefined();
344
+ expect(report.status).toBe('ready');
345
+ });
346
+ it('is omitted entirely when there is no manifest at all — nothing to baseline without sensors', () => {
347
+ const dir = make();
348
+ const report = (0, checks_1.preflight)(dir);
349
+ expect(report.checks.find(c => c.id === 'sensors-baseline')).toBeUndefined();
350
+ expect(report.status).toBe('not_configured');
351
+ });
352
+ it('does not nudge on a deliberate opt-out (every sensor disabled) — nothing to baseline', () => {
353
+ // Regression: the trigger condition originally checked only manifestExists, so a
354
+ // repo that deliberately opted out (checkManifest's own documented pattern: every
355
+ // sensor `enabled: false`) still got told to run `awm sensors baseline` — nothing
356
+ // to baseline when there's nothing enabled to have findings in the first place.
357
+ const dir = make({
358
+ manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .', enabled: false } } },
359
+ });
360
+ const report = (0, checks_1.preflight)(dir);
361
+ expect(check(report, 'sensors-baseline').ok).toBe(true);
362
+ expect(check(report, 'sensors-baseline').detail).toBe('no enabled sensors — nothing to baseline');
363
+ expect(check(report, 'sensors-baseline').remedy).toBeUndefined();
364
+ });
365
+ it('does not nudge on an unparseable manifest — nothing to baseline', () => {
366
+ const dir = make({ manifest: '{not valid json' });
367
+ const report = (0, checks_1.preflight)(dir);
368
+ expect(check(report, 'sensors-baseline').ok).toBe(true);
369
+ expect(check(report, 'sensors-baseline').detail).toBe('no enabled sensors — nothing to baseline');
370
+ expect(check(report, 'sensors-baseline').remedy).toBeUndefined();
371
+ });
372
+ it('still nudges when the baseline path exists but is not a readable file (e.g. a stray directory)', () => {
373
+ // Regression: checking presence via `fs.existsSync` alone would have reported
374
+ // "baseline present" here, reassuring the operator that debt is suppressed —
375
+ // but the real gate (`readBaseline`, used by `partition()`) treats an unreadable
376
+ // baseline path as "no baseline, nothing suppressed". The advisory must track
377
+ // what the runtime actually does, not just whether something exists at the path.
378
+ const dir = make({
379
+ manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } },
380
+ bins: ['eslint'],
381
+ files: ['package.json'],
382
+ });
383
+ fs_1.default.mkdirSync(path_1.default.join(dir, '.awm', 'sensors.baseline.json'), { recursive: true });
384
+ const report = (0, checks_1.preflight)(dir);
385
+ expect(check(report, 'sensors-baseline').ok).toBe(true);
386
+ expect(check(report, 'sensors-baseline').detail).toContain('no baseline yet');
387
+ expect(check(report, 'sensors-baseline').remedy).toContain('awm sensors baseline');
388
+ });
389
+ });
315
390
  it('tells the operator not to hand a broken harness to an unattended run', () => {
316
391
  const out = (0, preflight_1.formatReport)({
317
392
  status: 'not_configured',
@@ -320,4 +395,24 @@ describe('preflight', () => {
320
395
  expect(out).toContain('unattended');
321
396
  expect(out).toContain('awm sensors init');
322
397
  });
398
+ it('pads the id column to the widest id actually present, not a hardcoded width', () => {
399
+ // Regression: a literal `.padEnd(9)` silently misaligned once `sensors-baseline`
400
+ // (16 chars) was added as a check id — every detail column shifted left of where
401
+ // shorter ids' details landed. The width must be derived from the report itself.
402
+ // Marker prefixes (@@) pin exactly where each detail column starts, independent
403
+ // of the detail text's own content.
404
+ const out = (0, preflight_1.formatReport)({
405
+ status: 'ready',
406
+ checks: [
407
+ { id: 'host', ok: true, detail: '@@marker' },
408
+ { id: 'sensors-baseline', ok: true, detail: '@@marker' },
409
+ ],
410
+ });
411
+ const lines = out.split('\n').filter(l => l.includes('@@marker'));
412
+ expect(lines).toHaveLength(2);
413
+ expect(lines[0].indexOf('@@marker')).toBe(lines[1].indexOf('@@marker'));
414
+ // And the column is genuinely sized to the longest id (16, 'sensors-baseline'),
415
+ // not the old hardcoded 9 — the shorter id's row must carry visible padding.
416
+ expect(lines[0]).toMatch(/host {12,}@@marker/);
417
+ });
323
418
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "3.12.0",
3
+ "version": "3.13.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"