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 =
|
|
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"
|
|
228
|
-
//
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|