agentic-workflow-manager 9.20.3 → 9.21.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.
@@ -19,6 +19,7 @@ const registries_2 = require("../../core/registries");
19
19
  const check_1 = require("../../core/currentness/check");
20
20
  const store_1 = require("../../core/model-policy/store");
21
21
  const setup_readiness_1 = require("../../core/model-policy/setup-readiness");
22
+ const awm_gitignore_1 = require("../../core/awm-gitignore");
22
23
  const MANIFEST = path_1.default.join('.awm', 'sensors.json');
23
24
  function checkRoutingSetup(cwd) {
24
25
  const guidance = (0, setup_readiness_1.routingSetupGuidance)((0, store_1.readEffectivePolicy)(cwd));
@@ -441,6 +442,12 @@ function extractHost(remote) {
441
442
  * a PR/MR at all (merge locally, keep the branch as-is), so gating the whole harness on
442
443
  * missing tooling here would be wrong. It is FYI, never a blocker.
443
444
  */
445
+ function checkAwmGitignore(cwd) {
446
+ const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(path_1.default.resolve(cwd));
447
+ if (result.ok)
448
+ return { id: 'awm-gitignore', ok: true, detail: result.detail };
449
+ return { id: 'awm-gitignore', ok: false, detail: result.detail, remedy: result.remedy };
450
+ }
444
451
  function checkHost(cwd) {
445
452
  let remote;
446
453
  try {
@@ -540,6 +547,8 @@ async function preflight(cwd = process.cwd(), opts = {}) {
540
547
  // Strict mode is the only path that contacts authoritative npm/Git sources.
541
548
  ...(opts.requireCurrent === true ? [checkCompatibility()] : []),
542
549
  ...checkRoutingSetup(cwd),
550
+ // Blocking .awm versioning policy (D-024 / #218).
551
+ checkAwmGitignore(cwd),
543
552
  // Runs unconditionally — orthogonal to sensor configuration entirely, this is
544
553
  // about PR/MR tooling, not sensors.
545
554
  checkHost(cwd),
@@ -12,6 +12,7 @@ const path_1 = __importDefault(require("path"));
12
12
  const pack_source_1 = require("./compatibility/pack-source");
13
13
  const bootstrap_1 = require("./bootstrap");
14
14
  const unresolved_1 = require("./unresolved");
15
+ const awm_gitignore_1 = require("../../core/awm-gitignore");
15
16
  /** Read a pack only through the shared source resolver. Init is a registry
16
17
  * boundary: a pack path that is absent is harmless, but a symlink or escaping
17
18
  * claimed source is never silently downgraded to an empty manifest. */
@@ -165,12 +166,17 @@ async function initSensors(opts = {}) {
165
166
  }
166
167
  if (plan.kind === 'migrate')
167
168
  throw new Error('initSensors does not migrate existing v2 manifests; run awm sensors bootstrap');
168
- if (plan.kind === 'noop')
169
+ if (plan.kind === 'noop') {
170
+ // Repair path: existing project `.awm/` should still get the managed block (GI-1 remedy).
171
+ if (fs_1.default.existsSync(path_1.default.join(cwd, '.awm')))
172
+ (0, awm_gitignore_1.upsertAwmGitignore)(cwd);
169
173
  return {
170
174
  // No manifest is written or synthesized here. The compatibility API only
171
175
  // retains its historical result shape for callers that inspect detection.
172
176
  manifest: {}, detection: detectStack(detectionCwd), configured: [], status: 'already-configured',
173
177
  };
178
+ }
174
179
  (0, bootstrap_1.applySensorBootstrap)(plan);
180
+ (0, awm_gitignore_1.upsertAwmGitignore)(cwd);
175
181
  return { manifest: plan.manifest, detection: detectStack(detectionCwd), configured: [], status: 'created', ...(plan.unresolved && plan.unresolved.length > 0 ? { unresolved: plan.unresolved } : {}) };
176
182
  }
@@ -215,7 +215,12 @@ function projectSensorCheck(sensor, cwd) {
215
215
  function packBoundOnly(manifest) {
216
216
  const sensors = Object.fromEntries(Object.entries(manifest.sensors).filter(([, sensor]) => !(0, manifest_1.isProjectDeclaredSensor)(sensor)));
217
217
  const pack = 'pack' in manifest && typeof manifest.pack === 'string' ? manifest.pack : '';
218
- return { schemaVersion: 2, pack, sensors };
218
+ return {
219
+ schemaVersion: 2,
220
+ pack,
221
+ sensors,
222
+ ...(manifest.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}),
223
+ };
219
224
  }
220
225
  /** Live pack sensor ids, or null when the pack cannot be resolved (fail closed). */
221
226
  function livePackSensorIds(source) {
@@ -18,6 +18,7 @@ const lock_1 = require("./lock");
18
18
  const validate_1 = require("../../core/plan/validate");
19
19
  const identity_1 = require("../../core/plan/identity");
20
20
  const native_bridge_1 = require("../../core/secure-fs/native-bridge");
21
+ const awm_gitignore_1 = require("../../core/awm-gitignore");
21
22
  const MAX_VERIFIER_SCAN_DEPTH = 64;
22
23
  const MAX_VERIFIER_SCAN_ENTRIES = 10000;
23
24
  const MAX_PACKAGE_JSON_BYTES = 256 * 1024;
@@ -81,22 +82,9 @@ function detectRequiredVerifiers(repoRoot) {
81
82
  visit(repoRoot, 0);
82
83
  return ['test', 'sensors'].filter((kind) => kinds.has(kind));
83
84
  }
84
- /** El journal es gitignoreado (R1.1): sus escrituras jamas alteran fingerprints. */
85
+ /** El journal es gitignoreado (R1.1): bloque gestionado D-024 / #218. */
85
86
  function ensureJournalGitignored(repoRoot) {
86
- const gi = path_1.default.join(repoRoot, '.gitignore');
87
- let current = '';
88
- try {
89
- if (fs_1.default.lstatSync(gi).isSymbolicLink())
90
- throw new Error('.gitignore must not be a symlink');
91
- current = fs_1.default.readFileSync(gi, 'utf8');
92
- }
93
- catch (error) {
94
- if (error.code !== 'ENOENT')
95
- throw error;
96
- }
97
- if (!current.split('\n').some((l) => l.trim() === '.awm/' || l.trim() === '.awm')) {
98
- fs_1.default.writeFileSync(gi, current.length > 0 && !current.endsWith('\n') ? `${current}\n.awm/\n` : `${current}.awm/\n`);
99
- }
87
+ (0, awm_gitignore_1.upsertAwmGitignore)(repoRoot);
100
88
  }
101
89
  const TERMINAL_JOB_STATES = new Set(['exited', 'cancelled']);
102
90
  function rebindBlocker(state) {
@@ -0,0 +1,211 @@
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.AWM_LOCAL_PATHS = exports.AWM_VERSIONED_PATHS = exports.AWM_GITIGNORE_END = exports.AWM_GITIGNORE_BEGIN = void 0;
7
+ exports.renderManagedBlock = renderManagedBlock;
8
+ exports.upsertAwmGitignore = upsertAwmGitignore;
9
+ exports.evaluateAwmGitignorePolicy = evaluateAwmGitignorePolicy;
10
+ const child_process_1 = require("child_process");
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ /** Marker lines for the managed `.gitignore` block (ASCII, exact). */
14
+ exports.AWM_GITIGNORE_BEGIN = '# AWM:GITIGNORE:BEGIN';
15
+ exports.AWM_GITIGNORE_END = '# AWM:GITIGNORE:END';
16
+ /** Project config under `.awm/` that must remain commit-able (D-024). */
17
+ exports.AWM_VERSIONED_PATHS = [
18
+ '.awm/sensors.json',
19
+ '.awm/sensors.baseline.json',
20
+ '.awm/profile.json',
21
+ '.awm/context-budget.json',
22
+ '.awm/context/index.json',
23
+ ];
24
+ /** Runtime / local state under `.awm/` that must not be tracked (D-024). */
25
+ exports.AWM_LOCAL_PATHS = [
26
+ '.awm/journal',
27
+ '.awm/ledger',
28
+ '.awm/evidence',
29
+ '.awm/track.json',
30
+ '.awm/sensors/runs',
31
+ ];
32
+ const MANAGED_LINES = [
33
+ '.awm/*',
34
+ '!.awm/sensors.json',
35
+ '!.awm/sensors.baseline.json',
36
+ '!.awm/profile.json',
37
+ '!.awm/context-budget.json',
38
+ '!.awm/context/',
39
+ '.awm/context/*',
40
+ '!.awm/context/index.json',
41
+ ];
42
+ /** Canonical managed block including begin/end markers and a trailing newline. */
43
+ function renderManagedBlock() {
44
+ return [exports.AWM_GITIGNORE_BEGIN, ...MANAGED_LINES, exports.AWM_GITIGNORE_END, ''].join('\n');
45
+ }
46
+ function isBlanketAwmIgnore(line) {
47
+ const t = line.trim();
48
+ return t === '.awm' || t === '.awm/';
49
+ }
50
+ /**
51
+ * Upsert the managed AWM `.gitignore` block at the repository root.
52
+ * Idempotent when the block already matches. Migrates bare `.awm` / `.awm/`
53
+ * lines outside the block. Refuses a symlink `.gitignore`.
54
+ */
55
+ function upsertAwmGitignore(repoRoot) {
56
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
57
+ throw new Error('upsertAwmGitignore requires a non-empty repository root');
58
+ }
59
+ const gi = path_1.default.join(repoRoot, '.gitignore');
60
+ let current = '';
61
+ try {
62
+ if (fs_1.default.lstatSync(gi).isSymbolicLink()) {
63
+ throw new Error('.gitignore must not be a symlink');
64
+ }
65
+ current = fs_1.default.readFileSync(gi, 'utf8');
66
+ }
67
+ catch (error) {
68
+ if (error.code !== 'ENOENT')
69
+ throw error;
70
+ }
71
+ const normalized = current.replace(/\r\n/g, '\n');
72
+ const block = renderManagedBlock();
73
+ const begin = normalized.indexOf(exports.AWM_GITIGNORE_BEGIN);
74
+ const end = normalized.indexOf(exports.AWM_GITIGNORE_END);
75
+ let withoutManaged;
76
+ if (begin !== -1 && end !== -1 && end > begin) {
77
+ const afterEnd = end + exports.AWM_GITIGNORE_END.length;
78
+ const skipNewline = normalized[afterEnd] === '\n' ? afterEnd + 1 : afterEnd;
79
+ withoutManaged = normalized.slice(0, begin) + normalized.slice(skipNewline);
80
+ }
81
+ else if (begin !== -1 || end !== -1) {
82
+ // Malformed markers: strip both marker lines and rebuild cleanly.
83
+ withoutManaged = normalized
84
+ .split('\n')
85
+ .filter((line) => line !== exports.AWM_GITIGNORE_BEGIN && line !== exports.AWM_GITIGNORE_END)
86
+ .join('\n');
87
+ }
88
+ else {
89
+ withoutManaged = normalized;
90
+ }
91
+ const preserved = withoutManaged
92
+ .split('\n')
93
+ .filter((line) => !isBlanketAwmIgnore(line))
94
+ .join('\n')
95
+ .replace(/\n+$/g, '');
96
+ const next = preserved.length > 0 ? `${preserved}\n${block}` : block;
97
+ if (next === normalized || next === current.replace(/\r\n/g, '\n')) {
98
+ // Already equivalent after normalize — avoid touching mtime when identical.
99
+ if (fs_1.default.existsSync(gi) && fs_1.default.readFileSync(gi, 'utf8').replace(/\r\n/g, '\n') === next)
100
+ return;
101
+ }
102
+ fs_1.default.writeFileSync(gi, next);
103
+ }
104
+ function gitOk(repoRoot, args) {
105
+ try {
106
+ const stdout = (0, child_process_1.execFileSync)('git', args, {
107
+ cwd: repoRoot,
108
+ encoding: 'utf8',
109
+ stdio: ['ignore', 'pipe', 'pipe'],
110
+ env: process.env,
111
+ });
112
+ return { ok: true, stdout };
113
+ }
114
+ catch (error) {
115
+ const err = error;
116
+ return { ok: false, code: typeof err.status === 'number' ? err.status : 1, stdout: typeof err.stdout === 'string' ? err.stdout : '' };
117
+ }
118
+ }
119
+ function isGitRepo(repoRoot) {
120
+ const result = gitOk(repoRoot, ['rev-parse', '--is-inside-work-tree']);
121
+ return result.ok && result.stdout.trim() === 'true';
122
+ }
123
+ function extractManagedInner(text) {
124
+ const begin = text.indexOf(exports.AWM_GITIGNORE_BEGIN);
125
+ const end = text.indexOf(exports.AWM_GITIGNORE_END);
126
+ if (begin === -1 || end === -1 || end <= begin)
127
+ return null;
128
+ return text.slice(begin + exports.AWM_GITIGNORE_BEGIN.length, end).replace(/^\n/, '').replace(/\n$/, '');
129
+ }
130
+ function managedBlockComplete(repoRoot) {
131
+ const gi = path_1.default.join(repoRoot, '.gitignore');
132
+ try {
133
+ const text = fs_1.default.readFileSync(gi, 'utf8').replace(/\r\n/g, '\n');
134
+ const inner = extractManagedInner(text);
135
+ if (inner === null)
136
+ return false;
137
+ const expected = MANAGED_LINES.join('\n');
138
+ return inner === expected;
139
+ }
140
+ catch (error) {
141
+ if (error.code === 'ENOENT')
142
+ return false;
143
+ throw error;
144
+ }
145
+ }
146
+ function pathExistsUnderRoot(repoRoot, rel) {
147
+ try {
148
+ fs_1.default.lstatSync(path_1.default.join(repoRoot, rel));
149
+ return true;
150
+ }
151
+ catch (error) {
152
+ if (error.code === 'ENOENT')
153
+ return false;
154
+ throw error;
155
+ }
156
+ }
157
+ function awmTreePresent(repoRoot) {
158
+ return pathExistsUnderRoot(repoRoot, '.awm') || exports.AWM_VERSIONED_PATHS.some((p) => pathExistsUnderRoot(repoRoot, p));
159
+ }
160
+ /**
161
+ * Blocking policy check for doctor/preflight (GI-6..GI-8).
162
+ * Skips cleanly when not inside a git work tree.
163
+ */
164
+ function evaluateAwmGitignorePolicy(repoRoot) {
165
+ if (typeof repoRoot !== 'string' || repoRoot.length === 0) {
166
+ throw new Error('evaluateAwmGitignorePolicy requires a non-empty repository root');
167
+ }
168
+ if (!isGitRepo(repoRoot)) {
169
+ return { ok: true, skipped: true, detail: 'skipped (not a git repository)' };
170
+ }
171
+ const trackedLocal = [];
172
+ for (const local of exports.AWM_LOCAL_PATHS) {
173
+ const listed = gitOk(repoRoot, ['ls-files', '-z', '--', local]);
174
+ if (!listed.ok)
175
+ continue;
176
+ const paths = listed.stdout.split('\0').filter(Boolean);
177
+ trackedLocal.push(...paths);
178
+ }
179
+ const ignoredVersioned = [];
180
+ for (const versioned of exports.AWM_VERSIONED_PATHS) {
181
+ if (!pathExistsUnderRoot(repoRoot, versioned))
182
+ continue;
183
+ const ignore = gitOk(repoRoot, ['check-ignore', '-q', '--', versioned]);
184
+ // exit 0 → ignored; exit 1 → not ignored; other → treat as not ignored (fail-open on check only)
185
+ if (ignore.ok || ignore.code === 0)
186
+ ignoredVersioned.push(versioned);
187
+ }
188
+ const needsBlock = awmTreePresent(repoRoot) || trackedLocal.length > 0;
189
+ const missingBlock = needsBlock && !managedBlockComplete(repoRoot);
190
+ if (trackedLocal.length === 0 && ignoredVersioned.length === 0 && !missingBlock) {
191
+ return { ok: true, detail: needsBlock ? 'managed .awm gitignore policy satisfied' : 'no .awm project state' };
192
+ }
193
+ const parts = [];
194
+ if (trackedLocal.length > 0) {
195
+ parts.push(`tracked local runtime: ${trackedLocal.slice(0, 8).join(', ')}${trackedLocal.length > 8 ? '…' : ''}`);
196
+ }
197
+ if (ignoredVersioned.length > 0) {
198
+ parts.push(`versioned config ignored: ${ignoredVersioned.join(', ')}`);
199
+ }
200
+ if (missingBlock) {
201
+ parts.push('managed # AWM:GITIGNORE block missing or incomplete');
202
+ }
203
+ const remedyParts = [
204
+ 'run `awm sensors init` or `awm watch init` to upsert the managed .gitignore block',
205
+ ];
206
+ if (trackedLocal.length > 0) {
207
+ const shown = trackedLocal.slice(0, 16).join(' ');
208
+ remedyParts.push(`then \`git rm --cached -- ${shown}${trackedLocal.length > 16 ? ' …' : ''}\``);
209
+ }
210
+ return { ok: false, detail: parts.join('; '), remedy: remedyParts.join('; ') };
211
+ }
@@ -7,6 +7,7 @@ exports.computeProviderOverall = computeProviderOverall;
7
7
  exports.runChecks = runChecks;
8
8
  // src/core/diagnostics/checks.ts
9
9
  const path_1 = __importDefault(require("path"));
10
+ const awm_gitignore_1 = require("../awm-gitignore");
10
11
  const cmd = (value) => ({ kind: 'command', value });
11
12
  const skillRemedy = (value) => ({ kind: 'skill', value });
12
13
  const none = { kind: 'none' };
@@ -152,6 +153,16 @@ function projectChecks(p) {
152
153
  out.push({ id: 'project.context', level: 'project', label: 'agent context (CLAUDE.md/AGENTS.md) missing', status: 'warn',
153
154
  remedy: skillRemedy('project-context-init') });
154
155
  }
156
+ // project.awm-gitignore — D-024 / #218 (blocking via status missing)
157
+ const gi = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(p.root);
158
+ if (gi.ok) {
159
+ out.push({ id: 'project.awm-gitignore', level: 'project', label: '.awm gitignore policy',
160
+ status: 'ok', detail: gi.detail, remedy: none });
161
+ }
162
+ else {
163
+ out.push({ id: 'project.awm-gitignore', level: 'project', label: '.awm gitignore policy',
164
+ status: 'missing', detail: gi.detail, remedy: cmd(gi.remedy) });
165
+ }
155
166
  return out;
156
167
  }
157
168
  // --- Task 9: overall status for the per-provider diagnostic matrix --------
@@ -0,0 +1,52 @@
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 child_process_1 = require("child_process");
10
+ const checks_1 = require("../../../src/commands/preflight/checks");
11
+ const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
12
+ function git(cwd, args) {
13
+ (0, child_process_1.execFileSync)('git', args, { cwd, stdio: 'pipe' });
14
+ }
15
+ describe('preflight awm-gitignore check (GI-6/GI-7)', () => {
16
+ let repo;
17
+ beforeEach(() => {
18
+ repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gi-pf-'));
19
+ git(repo, ['init']);
20
+ git(repo, ['config', 'user.email', 't@t']);
21
+ git(repo, ['config', 'user.name', 't']);
22
+ });
23
+ afterEach(() => {
24
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
25
+ });
26
+ it('degrades when versioned sensors.json is ignored', async () => {
27
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
28
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), JSON.stringify({
29
+ schemaVersion: 3, mode: 'opt-out', reason: 'test',
30
+ }));
31
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '.awm/\n');
32
+ const report = await (0, checks_1.preflight)(repo);
33
+ const check = report.checks.find((c) => c.id === 'awm-gitignore');
34
+ expect(check).toBeDefined();
35
+ expect(check.ok).toBe(false);
36
+ expect(check.advisory).not.toBe(true);
37
+ expect(check.remedy).toMatch(/awm sensors init|awm watch init/);
38
+ expect(report.status).not.toBe('ready');
39
+ });
40
+ it('includes a passing awm-gitignore check when managed block is present for opt-out manifest', async () => {
41
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
42
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), JSON.stringify({
43
+ schemaVersion: 3, mode: 'opt-out', reason: 'test',
44
+ }));
45
+ (0, awm_gitignore_1.upsertAwmGitignore)(repo);
46
+ git(repo, ['add', '-A']);
47
+ git(repo, ['commit', '-m', 'ok']);
48
+ const report = await (0, checks_1.preflight)(repo);
49
+ const check = report.checks.find((c) => c.id === 'awm-gitignore');
50
+ expect(check).toMatchObject({ ok: true });
51
+ });
52
+ });
@@ -0,0 +1,59 @@
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 awm_gitignore_1 = require("../../../src/core/awm-gitignore");
10
+ jest.mock('../../../src/commands/sensors/bootstrap', () => ({
11
+ planSensorBootstrap: jest.fn(),
12
+ applySensorBootstrap: jest.fn(),
13
+ }));
14
+ jest.mock('../../../src/commands/sensors/detection', () => ({
15
+ detectStack: jest.fn(() => ({ pack: 'js-ts', evidence: [] })),
16
+ }));
17
+ const { planSensorBootstrap, applySensorBootstrap } = require('../../../src/commands/sensors/bootstrap');
18
+ const { initSensors } = require('../../../src/commands/sensors/init');
19
+ describe('initSensors managed awm gitignore (GI-1)', () => {
20
+ let tmp;
21
+ beforeEach(() => {
22
+ tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensors-gi-'));
23
+ jest.clearAllMocks();
24
+ });
25
+ afterEach(() => {
26
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
27
+ });
28
+ it('upserts the managed block after a successful create', async () => {
29
+ planSensorBootstrap.mockResolvedValue({
30
+ kind: 'create',
31
+ projectRoot: tmp,
32
+ manifestPath: path_1.default.join(tmp, '.awm', 'sensors.json'),
33
+ dryRun: false,
34
+ changes: [],
35
+ manifest: { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, sensors: {} },
36
+ });
37
+ applySensorBootstrap.mockImplementation(() => {
38
+ fs_1.default.mkdirSync(path_1.default.join(tmp, '.awm'), { recursive: true });
39
+ fs_1.default.writeFileSync(path_1.default.join(tmp, '.awm', 'sensors.json'), '{}');
40
+ return 'created';
41
+ });
42
+ await initSensors({ cwd: tmp });
43
+ expect(applySensorBootstrap).toHaveBeenCalled();
44
+ const gi = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
45
+ expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
46
+ expect(gi).toContain('!.awm/profile.json');
47
+ });
48
+ it('repairs the managed block on noop when .awm/ already exists', async () => {
49
+ fs_1.default.mkdirSync(path_1.default.join(tmp, '.awm'), { recursive: true });
50
+ fs_1.default.writeFileSync(path_1.default.join(tmp, '.awm', 'sensors.json'), '{}');
51
+ fs_1.default.writeFileSync(path_1.default.join(tmp, '.gitignore'), '.awm/\n');
52
+ planSensorBootstrap.mockResolvedValue({ kind: 'noop' });
53
+ await initSensors({ cwd: tmp });
54
+ expect(applySensorBootstrap).not.toHaveBeenCalled();
55
+ const gi = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
56
+ expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
57
+ expect(gi.split('\n').map((l) => l.trim()).filter((l) => l === '.awm/')).toEqual([]);
58
+ });
59
+ });
@@ -268,6 +268,68 @@ describe('computeSensorStatus', () => {
268
268
  fs_1.default.rmSync(home, { recursive: true, force: true });
269
269
  }
270
270
  });
271
+ it('keeps packSelection explicit through status so explicit-opt-in sensors do not false-drift', async () => {
272
+ // Regression for #231: packBoundOnly used to drop packSelection before
273
+ // resolveStaticV2Compatibility. Without it, applies() leaves explicit-opt-in
274
+ // sensors not-applicable and status reports "compatibility drift: initialized
275
+ // prettier, resolved not-applicable" even though run() (which keeps the field)
276
+ // would certify the same manifest.
277
+ const previousHome = process.env.AWM_HOME;
278
+ const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-home-'));
279
+ try {
280
+ process.env.AWM_HOME = home;
281
+ const registry = path_1.default.join(home, 'registries', 'baseline');
282
+ fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
283
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
284
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
285
+ schemaVersion: 2, name: 'js-ts', description: 'test', detects: ['package.json'],
286
+ coverage: { schemaVersion: 1, classes: { format: { description: 'format', detectors: [{ sensor: 'format' }], remedy: { summary: 'Fix format', command: 'awm sensors init --pack js-ts' } } } },
287
+ sensors: { format: {
288
+ applicability: { kind: 'explicit-opt-in' },
289
+ variants: [{
290
+ id: 'prettier', priority: 10, certifiedRange: '>=3.0.0 <4.0.0',
291
+ requirements: { tool: 'prettier', toolRange: '>=3.0.0 <4.0.0', runtime: 'node', runtimeRange: '>=0.0.0' },
292
+ assets: [], formatter: 'prettier', probe: { kind: 'package-script-present', script: 'format' },
293
+ command: { executable: 'prettier', resolution: 'node-modules-bin', args: ['.'] },
294
+ }],
295
+ } },
296
+ }));
297
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({
298
+ devDependencies: { prettier: '^3.0.0' },
299
+ scripts: { format: 'prettier .' },
300
+ }));
301
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'prettier'), { recursive: true });
302
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'prettier', 'package.json'), JSON.stringify({ version: '3.3.3' }));
303
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', '.bin'), { recursive: true });
304
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'prettier'), '#!/bin/sh\nexit 0\n');
305
+ fs_1.default.chmodSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'prettier'), 0o755);
306
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
307
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
308
+ schemaVersion: 2, pack: 'js-ts', packSelection: 'explicit',
309
+ sensors: { format: {
310
+ enabled: true, variantId: 'prettier',
311
+ command: { executable: 'prettier', resolution: 'node-modules-bin', args: ['.'] },
312
+ assets: [],
313
+ initializedCompatibility: {
314
+ state: 'certified', reason: 'range-and-probe', variantId: 'prettier',
315
+ toolVersion: '3.3.3', runtimeVersion: process.versions.node,
316
+ certifiedRange: '>=3.0.0 <4.0.0', evidence: [],
317
+ },
318
+ } },
319
+ }));
320
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
321
+ expect(result.checks.format?.detail ?? '').not.toMatch(/compatibility drift/i);
322
+ expect(result.checks.format).toMatchObject({ ok: true });
323
+ expect(result.overall).toBe('READY');
324
+ }
325
+ finally {
326
+ if (previousHome === undefined)
327
+ delete process.env.AWM_HOME;
328
+ else
329
+ process.env.AWM_HOME = previousHome;
330
+ fs_1.default.rmSync(home, { recursive: true, force: true });
331
+ }
332
+ });
271
333
  it('marks disabled sensors as ok', async () => {
272
334
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
273
335
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
@@ -0,0 +1,34 @@
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 child_process_1 = require("child_process");
10
+ const init_1 = require("../../../src/commands/watch/init");
11
+ const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
12
+ const git_1 = require("../../../src/core/tracks/git");
13
+ describe('initWatch managed awm gitignore (GI-2/GI-10)', () => {
14
+ let repo;
15
+ beforeEach(() => {
16
+ repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-watch-gi-'));
17
+ (0, child_process_1.execFileSync)('git', ['init'], { cwd: repo, stdio: 'pipe' });
18
+ fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), JSON.stringify({ scripts: { test: 'jest' } }));
19
+ });
20
+ afterEach(() => {
21
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
22
+ });
23
+ it('writes the managed block instead of a bare .awm/ allow-nothing rule', () => {
24
+ (0, init_1.initWatch)(repo, 'rama');
25
+ const gi = fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8');
26
+ expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
27
+ expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_END);
28
+ expect(gi).toContain('!.awm/sensors.json');
29
+ expect(gi).toContain('!.awm/profile.json');
30
+ const lines = gi.split('\n').map((l) => l.trim()).filter(Boolean);
31
+ expect(lines.filter((l) => l === '.awm' || l === '.awm/')).toEqual([]);
32
+ expect((0, git_1.isAwmGitignored)(repo)).toBe(true);
33
+ });
34
+ });
@@ -301,13 +301,10 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
301
301
  }
302
302
  function setUpGreenTrack() {
303
303
  initCompactWatch(repo, BRANCH);
304
- // `initWatch` ESCRIBE `.gitignore` (`.awm/`) pero no lo commitea — un
305
- // `.gitignore` sin trackear es en sí mismo un path "untracked" para
306
- // `git status --porcelain` (R6.4's chequeo de limpieza no distingue
307
- // "sucio de verdad" de "el propio bootstrap del journal"). Sin este
308
- // commit, `isWorktreeClean` nunca sería `true` y el freeze jamás
309
- // convergería — no por un bug del driver, sino por un repo de test
310
- // que nunca terminó su propio setup.
304
+ // `initWatch` upserts the managed `# AWM:GITIGNORE` block but does not
305
+ // commit it — an untracked/modified `.gitignore` keeps the worktree
306
+ // dirty for R6.4's clean check. Commit the bootstrap side-effect so
307
+ // freeze can converge.
311
308
  git(repo, 'add', '.gitignore');
312
309
  git(repo, 'commit', '-qm', 'gitignore .awm');
313
310
  const s = (0, store_1.readJournal)(repo, BRANCH).state;
@@ -526,6 +523,13 @@ describe('loop end-to-end: plan (reconcileTracks) + track (Supervisor.tick) cier
526
523
  test('reconcileTracks real (plan) + Supervisor.tick real (track), en loop contra dos worktrees reales, cierran request -> drenaje -> persist -> observación -> FROZEN', async () => {
527
524
  // --- lado TRACK: registrar el trabajo y drenarlo con un Supervisor REAL ---
528
525
  initCompactWatch(trackWorktree, TRACK_BRANCH);
526
+ // `initWatch` migrates the seeded `.awm/` ignore into the managed
527
+ // `.awm/*` allowlist block. That (1) dirties `.gitignore` and (2)
528
+ // un-ignores `.awm/sensors.json`, which then shows up as untracked.
529
+ // Both must be committed or R6.4's clean-worktree gate never passes
530
+ // and this loop times out (same class of fixture bug as setUpGreenTrack).
531
+ git(trackWorktree, 'add', '.gitignore', '.awm/sensors.json');
532
+ git(trackWorktree, 'commit', '-qm', 'awm gitignore + sensors');
529
533
  const ts0 = (0, store_1.readJournal)(trackWorktree, TRACK_BRANCH).state;
530
534
  ts0.trackContext = { trackId: 'a', taskIds: ['T1'], planDigest: '', baseSha, planJournalId: 'j-plan' };
531
535
  (0, store_1.writeJournal)(trackWorktree, TRACK_BRANCH, ts0);
@@ -154,7 +154,9 @@ describe('watch --init: plan-vs-repo mecanico', () => {
154
154
  const out = (0, init_1.initWatch)(repo, 'rama');
155
155
  expect(out.requiredVerifiers).toEqual(['test']);
156
156
  expect((0, store_1.readJournal)(repo, 'rama').state.requiredVerifiers).toEqual(['test']);
157
- expect(fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8')).toContain('.awm/');
157
+ const gi = fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8');
158
+ expect(gi).toContain('# AWM:GITIGNORE:BEGIN');
159
+ expect(gi).toContain('.awm/*');
158
160
  expect(() => (0, init_1.initWatch)(repo, 'rama')).not.toThrow(); // idempotente
159
161
  });
160
162
  test('no escribe .gitignore cuando es un symlink', () => {
@@ -0,0 +1,58 @@
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 awm_gitignore_1 = require("../../src/core/awm-gitignore");
10
+ describe('awm-gitignore managed block', () => {
11
+ let tmp;
12
+ beforeEach(() => {
13
+ tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gi-'));
14
+ });
15
+ afterEach(() => {
16
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
17
+ });
18
+ it('renders the canonical managed block with versioned allowlist entries', () => {
19
+ expect((0, awm_gitignore_1.renderManagedBlock)()).toBe([
20
+ awm_gitignore_1.AWM_GITIGNORE_BEGIN,
21
+ '.awm/*',
22
+ '!.awm/sensors.json',
23
+ '!.awm/sensors.baseline.json',
24
+ '!.awm/profile.json',
25
+ '!.awm/context-budget.json',
26
+ '!.awm/context/',
27
+ '.awm/context/*',
28
+ '!.awm/context/index.json',
29
+ awm_gitignore_1.AWM_GITIGNORE_END,
30
+ '',
31
+ ].join('\n'));
32
+ });
33
+ it('creates .gitignore with the managed block and is idempotent', () => {
34
+ (0, awm_gitignore_1.upsertAwmGitignore)(tmp);
35
+ const first = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
36
+ expect(first).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
37
+ expect(first).toContain(awm_gitignore_1.AWM_GITIGNORE_END);
38
+ (0, awm_gitignore_1.upsertAwmGitignore)(tmp);
39
+ expect(fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8')).toBe(first);
40
+ });
41
+ it('migrates bare .awm/ and .awm lines into the managed block without dropping unrelated rules', () => {
42
+ fs_1.default.writeFileSync(path_1.default.join(tmp, '.gitignore'), 'node_modules\n.awm/\ndist\n');
43
+ (0, awm_gitignore_1.upsertAwmGitignore)(tmp);
44
+ const text = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
45
+ expect(text).toContain('node_modules');
46
+ expect(text).toContain('dist');
47
+ expect(text).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
48
+ const outside = text.slice(0, text.indexOf(awm_gitignore_1.AWM_GITIGNORE_BEGIN));
49
+ expect(outside.split('\n').map((l) => l.trim()).filter(Boolean)).not.toContain('.awm/');
50
+ expect(outside.split('\n').map((l) => l.trim()).filter(Boolean)).not.toContain('.awm');
51
+ });
52
+ it('refuses a symlink .gitignore', () => {
53
+ const target = path_1.default.join(tmp, 'ignore-target');
54
+ fs_1.default.writeFileSync(target, 'x\n');
55
+ fs_1.default.symlinkSync(target, path_1.default.join(tmp, '.gitignore'));
56
+ expect(() => (0, awm_gitignore_1.upsertAwmGitignore)(tmp)).toThrow(/symlink/i);
57
+ });
58
+ });
@@ -0,0 +1,101 @@
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 child_process_1 = require("child_process");
10
+ const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
11
+ const checks_1 = require("../../../src/core/diagnostics/checks");
12
+ function git(cwd, args) {
13
+ (0, child_process_1.execFileSync)('git', args, { cwd, stdio: 'pipe' });
14
+ }
15
+ const stubMachine = () => ({
16
+ registryCache: { present: true, gitState: 'clean' },
17
+ hook: { present: true, applicable: true },
18
+ devCore: { present: true, brokenLinks: [] },
19
+ ambient: { wanted: [], installed: [] },
20
+ contextInjection: [],
21
+ globalSkills: { valid: [], repairable: [], dead: [], usurped: [] },
22
+ });
23
+ const stubProject = (root) => ({
24
+ root,
25
+ profile: { present: false, extensions: [] },
26
+ activeBundles: { expected: [], linked: [], broken: [] },
27
+ orphanLinks: { repairable: [], dead: [], usurped: [] },
28
+ sensors: { present: true },
29
+ constitution: { present: true },
30
+ context: { present: true, file: 'AGENTS.md' },
31
+ });
32
+ describe('evaluateAwmGitignorePolicy + doctor project check', () => {
33
+ let repo;
34
+ beforeEach(() => {
35
+ repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gi-doc-'));
36
+ git(repo, ['init']);
37
+ git(repo, ['config', 'user.email', 't@t']);
38
+ git(repo, ['config', 'user.name', 't']);
39
+ });
40
+ afterEach(() => {
41
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
42
+ });
43
+ it('fails when local runtime is tracked and proposes git rm --cached', () => {
44
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm', 'journal'), { recursive: true });
45
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'journal', 'x.json'), '{}');
46
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), 'node_modules\n');
47
+ git(repo, ['add', '-A']);
48
+ git(repo, ['commit', '-m', 'track journal']);
49
+ const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(repo);
50
+ expect(result.ok).toBe(false);
51
+ if (result.ok)
52
+ return;
53
+ expect(result.detail).toMatch(/tracked local runtime/i);
54
+ expect(result.remedy).toMatch(/git rm --cached/);
55
+ expect(result.remedy).toMatch(/awm sensors init|awm watch init/);
56
+ });
57
+ it('fails when sensors.json exists but is ignored by bare .awm/', () => {
58
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
59
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
60
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '.awm/\n');
61
+ const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(repo);
62
+ expect(result.ok).toBe(false);
63
+ if (result.ok)
64
+ return;
65
+ expect(result.detail).toMatch(/versioned config ignored/i);
66
+ expect(result.detail).toMatch(/managed.*block missing/i);
67
+ expect(result.remedy).toMatch(/awm sensors init|awm watch init/);
68
+ });
69
+ it('fails when markers exist but the block body is incomplete', () => {
70
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
71
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
72
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '# AWM:GITIGNORE:BEGIN\n.awm/*\n# AWM:GITIGNORE:END\n');
73
+ const result = (0, awm_gitignore_1.evaluateAwmGitignorePolicy)(repo);
74
+ expect(result.ok).toBe(false);
75
+ if (result.ok)
76
+ return;
77
+ expect(result.detail).toMatch(/managed.*block missing|incomplete/i);
78
+ });
79
+ it('passes when the managed block is installed', () => {
80
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
81
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
82
+ (0, awm_gitignore_1.upsertAwmGitignore)(repo);
83
+ git(repo, ['add', '.gitignore', '.awm/sensors.json']);
84
+ git(repo, ['commit', '-m', 'ok']);
85
+ expect((0, awm_gitignore_1.evaluateAwmGitignorePolicy)(repo)).toMatchObject({ ok: true });
86
+ });
87
+ it('runChecks emits project.awm-gitignore missing when policy fails', () => {
88
+ fs_1.default.mkdirSync(path_1.default.join(repo, '.awm'), { recursive: true });
89
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.awm', 'sensors.json'), '{}');
90
+ fs_1.default.writeFileSync(path_1.default.join(repo, '.gitignore'), '.awm/\n');
91
+ const ctx = {
92
+ machine: stubMachine(),
93
+ project: stubProject(repo),
94
+ providers: [],
95
+ };
96
+ const report = (0, checks_1.runChecks)(ctx);
97
+ const row = report.results.find((r) => r.id === 'project.awm-gitignore');
98
+ expect(row).toMatchObject({ status: 'missing' });
99
+ expect(report.overall).toBe('degraded');
100
+ });
101
+ });
@@ -8,6 +8,11 @@ const path_1 = __importDefault(require("path"));
8
8
  test('.awm (and therefore .awm/ledger) is gitignored — raw ledger never committed', () => {
9
9
  const gitignore = fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../../.gitignore'), 'utf-8');
10
10
  const lines = gitignore.split('\n').map(l => l.trim());
11
- const coversAwm = lines.includes('.awm') || lines.includes('.awm/') || lines.includes('.awm/ledger') || lines.includes('.awm/ledger/');
11
+ // D-024: managed block uses `.awm/*` (with allowlist exceptions), not a bare `.awm` / `.awm/`.
12
+ const coversAwm = lines.includes('.awm/*') ||
13
+ lines.includes('.awm') ||
14
+ lines.includes('.awm/') ||
15
+ lines.includes('.awm/ledger') ||
16
+ lines.includes('.awm/ledger/');
12
17
  expect(coversAwm).toBe(true);
13
18
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.20.3",
3
+ "version": "9.21.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
@@ -26,6 +26,7 @@
26
26
  "lint": "eslint . --config eslint.config.awm.mjs --cache --format json",
27
27
  "depcheck": "depcruise --config .dep-cruiser.awm.js src",
28
28
  "test": "jest --runInBand",
29
+ "test:wait-npm-publication": "node --test ../.github/scripts/wait-npm-publication.test.cjs",
29
30
  "start": "ts-node src/index.ts",
30
31
  "prerelease": "npm run build",
31
32
  "release": "node dist/src/release/index.js",
Binary file
Binary file