agentic-workflow-manager 3.8.0 → 3.9.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.
@@ -0,0 +1,123 @@
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.preflight = preflight;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const status_1 = require("../sensors/status");
10
+ const init_1 = require("../sensors/init");
11
+ const MANIFEST = path_1.default.join('.awm', 'sensors.json');
12
+ /**
13
+ * The agent needs project context delivered every session. A repo with neither file
14
+ * hands every agent — and every teammate's agent — a blank slate.
15
+ */
16
+ function checkContext(cwd) {
17
+ const present = ['AGENTS.md', 'CLAUDE.md', 'CONSTITUTION.md']
18
+ .filter(f => fs_1.default.existsSync(path_1.default.join(cwd, f)));
19
+ if (present.length === 0) {
20
+ return {
21
+ id: 'context',
22
+ ok: false,
23
+ detail: 'no AGENTS.md, CLAUDE.md or CONSTITUTION.md',
24
+ remedy: 'run the project-context-init skill (AGENTS.md) or project-constitution (CONSTITUTION.md)',
25
+ };
26
+ }
27
+ return { id: 'context', ok: true, detail: present.join(', ') };
28
+ }
29
+ function readManifest(cwd) {
30
+ try {
31
+ return JSON.parse(fs_1.default.readFileSync(path_1.default.join(cwd, MANIFEST), 'utf-8'));
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * A repo may legitimately have no sensors — but it has to SAY so, in a committed file.
39
+ *
40
+ * That is why opting out requires a manifest with its sensors disabled rather than
41
+ * simply having no manifest. "We decided not to gate this repo" and "nobody ever ran
42
+ * `awm sensors init`" look identical from the outside, and on a team the second one is
43
+ * the common case. Requiring the manifest turns the decision into a reviewable diff.
44
+ */
45
+ function checkManifest(cwd, manifest) {
46
+ if (!fs_1.default.existsSync(path_1.default.join(cwd, MANIFEST))) {
47
+ return {
48
+ id: 'manifest',
49
+ ok: false,
50
+ detail: 'no .awm/sensors.json',
51
+ remedy: 'run `awm sensors init` (to opt out deliberately, init and set every sensor '
52
+ + '`"enabled": false` — an unconfigured repo and a deliberate opt-out must not look alike)',
53
+ };
54
+ }
55
+ if (!manifest) {
56
+ return {
57
+ id: 'manifest',
58
+ ok: false,
59
+ detail: '.awm/sensors.json is not valid JSON',
60
+ remedy: 'fix or regenerate it with `awm sensors init`',
61
+ };
62
+ }
63
+ const total = Object.keys(manifest.sensors ?? {}).length;
64
+ const enabled = Object.values(manifest.sensors ?? {}).filter(s => s.enabled !== false).length;
65
+ return {
66
+ id: 'manifest',
67
+ ok: true,
68
+ detail: enabled === 0 ? `pack ${manifest.pack}, all ${total} sensors disabled (deliberate opt-out)`
69
+ : `pack ${manifest.pack}, ${enabled}/${total} sensors enabled`,
70
+ };
71
+ }
72
+ /** Every enabled sensor's command must resolve. This is the check nothing was calling. */
73
+ function checkTools(cwd) {
74
+ const status = (0, status_1.computeSensorStatus)(cwd);
75
+ if (status.overall === 'NOT_CONFIGURED') {
76
+ return { id: 'tools', ok: false, detail: 'no manifest to check', remedy: 'run `awm sensors init`' };
77
+ }
78
+ const broken = Object.entries(status.checks).filter(([, c]) => !c.ok);
79
+ if (broken.length > 0) {
80
+ return {
81
+ id: 'tools',
82
+ ok: false,
83
+ detail: broken.map(([name, c]) => `${name}: ${c.detail}`).join('; '),
84
+ remedy: 'install the missing tools/configs, or disable those sensors deliberately',
85
+ };
86
+ }
87
+ return { id: 'tools', ok: true, detail: `${Object.keys(status.checks).length} sensor(s) runnable` };
88
+ }
89
+ /**
90
+ * A manifest pinned to `generic` on a tree that clearly has a stack means the real
91
+ * sensors for that stack are simply absent — the gate runs, reports green, and has
92
+ * checked almost nothing. `runSensors` self-heals this at run time via `reconcilePack`,
93
+ * but only when a registry is reachable; saying it out loud here costs nothing.
94
+ */
95
+ function checkPack(cwd, manifest) {
96
+ if (!manifest)
97
+ return { id: 'pack', ok: true, detail: 'skipped (no manifest)' };
98
+ const detection = (0, init_1.detectStack)(cwd);
99
+ if (manifest.pack === 'generic' && detection.pack !== 'generic') {
100
+ return {
101
+ id: 'pack',
102
+ ok: false,
103
+ detail: `manifest on 'generic' but the tree looks like '${detection.pack}' (${detection.indicators.join(', ')})`,
104
+ remedy: 'run `awm sensors init` to pick up the real pack for this stack',
105
+ };
106
+ }
107
+ return { id: 'pack', ok: true, detail: `${manifest.pack} matches the detected stack` };
108
+ }
109
+ function preflight(cwd = process.cwd()) {
110
+ const manifest = readManifest(cwd);
111
+ const manifestExists = fs_1.default.existsSync(path_1.default.join(cwd, MANIFEST));
112
+ const checks = [
113
+ checkContext(cwd),
114
+ checkManifest(cwd, manifest),
115
+ // Skipped when there is no manifest: reporting "tools broken" on a repo that was
116
+ // never set up buries the one thing the operator needs to read.
117
+ ...(manifestExists ? [checkTools(cwd), checkPack(cwd, manifest)] : []),
118
+ ];
119
+ const status = !manifestExists ? 'not_configured'
120
+ : checks.every(c => c.ok) ? 'ready'
121
+ : 'degraded';
122
+ return { status, checks };
123
+ }
@@ -0,0 +1,50 @@
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.formatReport = formatReport;
8
+ exports.registerPreflightCommand = registerPreflightCommand;
9
+ const picocolors_1 = __importDefault(require("picocolors"));
10
+ const checks_1 = require("./checks");
11
+ /**
12
+ * Exit code. Anything but `ready` exits 1.
13
+ *
14
+ * Unlike `awm sensors run` — which exits 0 on `not_certified` because exit 2 is a
15
+ * blocking error in Claude Code hooks — preflight is never a hook. It is invoked
16
+ * explicitly by a phase gate, so the exit code can carry the verdict and the caller
17
+ * does not have to remember to read a field out of JSON.
18
+ */
19
+ function exitCodeFor(report) {
20
+ return report.status === 'ready' ? 0 : 1;
21
+ }
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}`
24
+ + (c.remedy ? `\n ${picocolors_1.default.dim('→ ' + c.remedy)}` : ''));
25
+ if (report.status === 'ready') {
26
+ return `${picocolors_1.default.green('✔')} Harness ready — this project can be gated.\n${lines.join('\n')}\n`;
27
+ }
28
+ const headline = report.status === 'not_configured'
29
+ ? `${picocolors_1.default.red('✘')} AWM is not configured in this project.`
30
+ : `${picocolors_1.default.red('✘')} Harness degraded — it declares sensors it cannot run.`;
31
+ return `${headline}\n${lines.join('\n')}\n\n`
32
+ + ` ${picocolors_1.default.bold('Do not hand this off to an unattended run.')} Every quality phase downstream\n`
33
+ + ` (implementer, reviewers, post-qa) consumes \`awm sensors run\`. With the harness in\n`
34
+ + ` this state the gate reports on checks that never ran, and nobody finds out until a\n`
35
+ + ` bad change is already merged.\n`;
36
+ }
37
+ function registerPreflightCommand(program) {
38
+ program
39
+ .command('preflight')
40
+ .description('verify the project harness can actually gate before development starts')
41
+ .option('--json', 'emit the report as JSON')
42
+ .option('--cwd <path>', 'project directory to check (default: current)')
43
+ .action((opts) => {
44
+ const report = (0, checks_1.preflight)(opts.cwd ?? process.cwd());
45
+ process.stdout.write(opts.json ? JSON.stringify(report, null, 2) + '\n' : formatReport(report));
46
+ const code = exitCodeFor(report);
47
+ if (code !== 0)
48
+ process.exit(code);
49
+ });
50
+ }
package/dist/src/index.js CHANGED
@@ -27,6 +27,7 @@ const hooks_1 = require("./commands/hooks");
27
27
  const sensors_1 = require("./commands/sensors");
28
28
  const ledger_1 = require("./commands/ledger");
29
29
  const context_budget_1 = require("./commands/context-budget");
30
+ const preflight_1 = require("./commands/preflight");
30
31
  const doctor_1 = require("./commands/doctor");
31
32
  const backup_1 = require("./commands/backup");
32
33
  const init_1 = require("./commands/init");
@@ -608,6 +609,7 @@ miroCmd.command('sync <storyMapPath>')
608
609
  (0, sensors_1.registerSensorsCommand)(program);
609
610
  (0, ledger_1.registerLedgerCommand)(program);
610
611
  (0, context_budget_1.registerContextBudgetCommand)(program);
612
+ (0, preflight_1.registerPreflightCommand)(program);
611
613
  (0, doctor_1.registerDoctorCommand)(program);
612
614
  (0, backup_1.registerBackupCommand)(program);
613
615
  (0, init_1.registerInitCommand)(program);
@@ -0,0 +1,123 @@
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 checks_1 = require("../../../src/commands/preflight/checks");
10
+ const preflight_1 = require("../../../src/commands/preflight");
11
+ /** CLAUDE.md: no test may reach the real ~/.awm. Everything here is a tmpdir. */
12
+ function project(opts = {}) {
13
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-'));
14
+ for (const f of opts.context ?? ['AGENTS.md'])
15
+ fs_1.default.writeFileSync(path_1.default.join(dir, f), '# ctx\n');
16
+ for (const f of opts.files ?? []) {
17
+ fs_1.default.mkdirSync(path_1.default.dirname(path_1.default.join(dir, f)), { recursive: true });
18
+ fs_1.default.writeFileSync(path_1.default.join(dir, f), '');
19
+ }
20
+ for (const b of opts.bins ?? []) {
21
+ fs_1.default.mkdirSync(path_1.default.join(dir, 'node_modules', '.bin'), { recursive: true });
22
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'node_modules', '.bin', b), '');
23
+ }
24
+ if (opts.manifest !== undefined) {
25
+ fs_1.default.mkdirSync(path_1.default.join(dir, '.awm'), { recursive: true });
26
+ fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), typeof opts.manifest === 'string' ? opts.manifest : JSON.stringify(opts.manifest));
27
+ }
28
+ return dir;
29
+ }
30
+ const dirs = [];
31
+ const make = (o) => { const d = project(o); dirs.push(d); return d; };
32
+ afterAll(() => dirs.forEach(d => fs_1.default.rmSync(d, { recursive: true, force: true })));
33
+ const check = (r, id) => r.checks.find(c => c.id === id);
34
+ describe('preflight', () => {
35
+ it('reports not_configured when no sensor manifest exists', () => {
36
+ // The team-rollout case: a developer clones the repo and never runs
37
+ // `awm sensors init`. Today nothing notices until an unattended run is already
38
+ // in flight and every quality phase is consuming a gate that certifies nothing.
39
+ const dir = make();
40
+ const report = (0, checks_1.preflight)(dir);
41
+ expect(report.status).toBe('not_configured');
42
+ expect(check(report, 'manifest').ok).toBe(false);
43
+ });
44
+ it('keeps not_configured and degraded apart', () => {
45
+ // "You never set this up" and "you set it up and it broke" need different
46
+ // remedies. Collapsing them is how an absent check reads as a passing one.
47
+ const never = make();
48
+ const broken = make({
49
+ manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } }, // no local eslint
50
+ });
51
+ expect((0, checks_1.preflight)(never).status).toBe('not_configured');
52
+ expect((0, checks_1.preflight)(broken).status).toBe('degraded');
53
+ });
54
+ it('is ready when the declared sensors can actually run', () => {
55
+ const dir = make({
56
+ manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } },
57
+ bins: ['eslint'],
58
+ files: ['package.json'],
59
+ });
60
+ const report = (0, checks_1.preflight)(dir);
61
+ expect(report.status).toBe('ready');
62
+ expect((0, preflight_1.exitCodeFor)(report)).toBe(0);
63
+ });
64
+ it('catches a sensor whose tool is not installed locally', () => {
65
+ // This is the check that existed and nothing in the flow was calling.
66
+ const dir = make({
67
+ manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } },
68
+ files: ['package.json'],
69
+ });
70
+ const report = (0, checks_1.preflight)(dir);
71
+ expect(report.status).toBe('degraded');
72
+ expect(check(report, 'tools').detail).toContain('eslint');
73
+ });
74
+ it('accepts a deliberate opt-out, but only when it is written down', () => {
75
+ // A repo may legitimately have no sensors — it just has to SAY so in a committed
76
+ // file, so "we decided not to gate this" cannot be mistaken for "nobody set it up".
77
+ const optedOut = make({
78
+ manifest: { pack: 'generic', sensors: { security: { cmd: 'semgrep .', enabled: false } } },
79
+ });
80
+ const report = (0, checks_1.preflight)(optedOut);
81
+ expect(report.status).toBe('ready');
82
+ expect(check(report, 'manifest').detail).toContain('opt-out');
83
+ });
84
+ it('flags a manifest stuck on generic while the tree has a real stack', () => {
85
+ // The gate would run, report green, and have checked almost nothing.
86
+ const dir = make({
87
+ manifest: { pack: 'generic', sensors: {} },
88
+ files: ['package.json'],
89
+ });
90
+ const report = (0, checks_1.preflight)(dir);
91
+ expect(report.status).toBe('degraded');
92
+ expect(check(report, 'pack').ok).toBe(false);
93
+ });
94
+ it('flags a repo with no context contract at all', () => {
95
+ const dir = make({
96
+ context: [],
97
+ manifest: { pack: 'generic', sensors: {} },
98
+ });
99
+ expect(check((0, checks_1.preflight)(dir), 'context').ok).toBe(false);
100
+ });
101
+ it('treats an unparseable manifest as a failure, not as absent', () => {
102
+ const dir = make({ manifest: '{ not json' });
103
+ const report = (0, checks_1.preflight)(dir);
104
+ expect(report.status).toBe('degraded');
105
+ expect(check(report, 'manifest').detail).toContain('not valid JSON');
106
+ });
107
+ it('exits non-zero for anything but ready, so the caller need not parse JSON', () => {
108
+ // Unlike `awm sensors run` — which exits 0 on not_certified because exit 2 blocks
109
+ // Claude Code hooks — preflight is never a hook, so the verdict rides the exit code
110
+ // instead of depending on every agent remembering to read a field.
111
+ expect((0, preflight_1.exitCodeFor)({ status: 'not_configured', checks: [] })).toBe(1);
112
+ expect((0, preflight_1.exitCodeFor)({ status: 'degraded', checks: [] })).toBe(1);
113
+ expect((0, preflight_1.exitCodeFor)({ status: 'ready', checks: [] })).toBe(0);
114
+ });
115
+ it('tells the operator not to hand a broken harness to an unattended run', () => {
116
+ const out = (0, preflight_1.formatReport)({
117
+ status: 'not_configured',
118
+ checks: [{ id: 'manifest', ok: false, detail: 'no .awm/sensors.json', remedy: 'run `awm sensors init`' }],
119
+ });
120
+ expect(out).toContain('unattended');
121
+ expect(out).toContain('awm sensors init');
122
+ });
123
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"