agentic-workflow-manager 9.11.4 → 9.11.5

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 (33) hide show
  1. package/dist/src/commands/sensors/init.js +0 -10
  2. package/dist/src/commands/sensors/run.js +0 -1
  3. package/dist/src/commands/watch/supervisor.js +3 -0
  4. package/dist/src/core/artifact-name.js +0 -1
  5. package/dist/src/core/dashboard/render-html.js +0 -7
  6. package/dist/src/core/ledger/scan.js +0 -2
  7. package/dist/src/core/miro.js +0 -1
  8. package/dist/src/core/profile.js +0 -1
  9. package/dist/src/core/text.js +2 -2
  10. package/dist/src/core/tracks/teardown.js +25 -0
  11. package/dist/tests/commands/context.test.js +1 -1
  12. package/dist/tests/commands/multi-agent-targeting.test.js +1 -1
  13. package/dist/tests/commands/preflight/preflight.test.js +0 -1
  14. package/dist/tests/commands/process.test.js +1 -1
  15. package/dist/tests/commands/track/supervisor-wrapper-cli.test.js +0 -3
  16. package/dist/tests/commands/watch/apply.test.js +1 -1
  17. package/dist/tests/core/context/provider.test.js +2 -2
  18. package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
  19. package/dist/tests/core/dashboard/render-terminal.test.js +1 -1
  20. package/dist/tests/core/diagnostics/provider-checks.test.js +0 -1
  21. package/dist/tests/core/discovery.test.js +0 -1
  22. package/dist/tests/core/init/orchestrator.test.js +1 -1
  23. package/dist/tests/core/journal/process.test.js +1 -1
  24. package/dist/tests/core/orchestrators.test.js +4 -4
  25. package/dist/tests/core/secure-fs/native-bridge.test.js +0 -1
  26. package/dist/tests/core/text.test.js +1 -1
  27. package/dist/tests/structural/directory-fsync-is-single-source.test.js +1 -1
  28. package/dist/tests/structural/lint-covers-typescript.test.js +64 -5
  29. package/package.json +2 -1
  30. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  31. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  32. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  33. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -139,16 +139,6 @@ function buildManifest(pack, existing, registryRoot, cwd = process.cwd()) {
139
139
  }
140
140
  return { pack, sensors };
141
141
  }
142
- /** Pack names present as directories under `<registryRoot>/sensor-packs/`, sorted. */
143
- function availablePacks(registryRoot) {
144
- const packsDir = path_1.default.join(registryRoot, 'sensor-packs');
145
- if (!fs_1.default.existsSync(packsDir) || !fs_1.default.statSync(packsDir).isDirectory())
146
- return [];
147
- return fs_1.default.readdirSync(packsDir, { withFileTypes: true })
148
- .filter(e => e.isDirectory())
149
- .map(e => e.name)
150
- .sort();
151
- }
152
142
  async function initSensors(opts = {}) {
153
143
  const cwd = opts.cwd ?? process.cwd();
154
144
  const detectionCwd = opts.packageRoot ? path_1.default.resolve(cwd, opts.packageRoot) : cwd;
@@ -29,7 +29,6 @@ const project_1 = require("./project");
29
29
  // Sensor JSON output can be several MB on large repos (e.g. `eslint --format json`
30
30
  // with thousands of findings). A 1MB cap killed the child with SIGTERM when
31
31
  // exceeded — which previously surfaced as a false "timeout".
32
- const MAX_BUFFER = 64 * 1024 * 1024;
33
32
  /** Hard ceiling on parallel sensors: past this, they only contend for the same cores. */
34
33
  const MAX_CONCURRENCY = 4;
35
34
  const READ_ONLY_MAX_ENTRIES = 250_000;
@@ -5,6 +5,9 @@ exports.routingFacts = routingFacts;
5
5
  exports.defaultDispatchAdmission = defaultDispatchAdmission;
6
6
  exports.admissionForConfig = admissionForConfig;
7
7
  exports.runSupervisorLoop = runSupervisorLoop;
8
+ // Loop foreground (R4.4/R4.5): tick = apply -> collect/spawn -> stall -> gate.
9
+ // COMPLETE exige gate verde (que exige cero vivos): drenaje ANTES de declarar.
10
+ // Custodia BLOCKED: el loop sigue, el lock NO se libera, nada se mata.
8
11
  const store_1 = require("../../core/journal/store");
9
12
  const fingerprint_1 = require("../../core/journal/fingerprint");
10
13
  const validate_1 = require("../../core/plan/validate");
@@ -50,7 +50,6 @@ function isSafeArtifactName(name) {
50
50
  if (/[. ]$/.test(value))
51
51
  return false;
52
52
  // Bytes de control y NUL: truncan la ruta a nivel syscall en algunos SO.
53
- // eslint-disable-next-line no-control-regex
54
53
  if (/[\u0000-\u001f\u007f]/.test(value))
55
54
  return false;
56
55
  // Cualquier separador (de ambas plataformas) convierte esto en una ruta,
@@ -14,13 +14,6 @@ function sectionHtml(section, supplement = '') {
14
14
  const rows = section.items.length === 0 ? '<p class="empty">No observations reported.</p>' : `<table><thead><tr><th scope="col">Observation</th><th scope="col">State</th><th scope="col">Detail</th><th scope="col">Remediation</th></tr></thead><tbody>${section.items.map((item) => `<tr><td data-label="Observation">${escapeHtml(item.label)}</td><td data-label="State"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span></td><td data-label="Detail">${item.detail ? escapeHtml(item.detail) : '—'}</td><td data-label="Remediation">${item.remediation ? `<code>${escapeHtml(item.remediation)}</code>` : '—'}</td></tr>`).join('')}</tbody></table>`;
15
15
  return `<section id="${section.id}" aria-label="${title}"><header><h2>${title}</h2><span class="eyebrow">${escapeHtml(section.availability.replace('_', ' '))}</span></header><div class="section-body">${availability}${supplement}${rows}</div></section>`;
16
16
  }
17
- function diagnosticCards(items, attribute) {
18
- const labels = ['installation', 'sensors', 'permissions'];
19
- const cards = items.slice(0, 3).map((item, index) => `<li data-diagnostic-card="${labels[index] ?? 'diagnostic'}"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'No additional detail'}</span></li>`).join('') || '<li><span class="state not_applicable">— Not applicable</span><strong>No machine observations</strong><span>Machine diagnostics are not available.</span></li>';
20
- if (attribute === 'data-machine-diagnostics')
21
- return `<section data-machine-diagnostics aria-labelledby="machine-diagnostics-heading"><header><h2 id="machine-diagnostics-heading">Machine diagnostics</h2></header><div class="section-body"><ul class="diagnostic-grid">${cards}</ul></div></section>`;
22
- return `<div class="machine-preparation-strip" data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"><h3 id="machine-preparation-heading">Preparación de máquina</h3><ul class="diagnostic-grid">${cards}</ul></div>`;
23
- }
24
17
  function machineBento(items) {
25
18
  const names = ['Instalación', 'Sensores globales', 'Persistencia'];
26
19
  return `<div class="machine-bento" data-machine-bento>${names.map((name, index) => {
@@ -136,7 +136,6 @@ function scanProjectLedgers(projectRoot, overrides = {}) {
136
136
  throw new Error('ledger archive directory escapes project root');
137
137
  directories.push({ directory: archive, archive: true });
138
138
  }
139
- let filesSeen = 0;
140
139
  let linesSeen = 0;
141
140
  let entryLimitReached = false;
142
141
  let omittedEvidenceRefs = 0;
@@ -145,7 +144,6 @@ function scanProjectLedgers(projectRoot, overrides = {}) {
145
144
  for (const { isArchive, target } of collected.candidates.slice(0, limits.maxFiles)) {
146
145
  if (entryLimitReached)
147
146
  break;
148
- filesSeen += 1;
149
147
  const stat = fs_1.default.lstatSync(target);
150
148
  if (stat.isSymbolicLink()) {
151
149
  skip('symlink-entry');
@@ -85,7 +85,6 @@ function computeLayout(storyMap) {
85
85
  let groupLeftX = PADDING; // running X from frame left edge
86
86
  for (let ai = 0; ai < activities.length; ai++) {
87
87
  const activity = activities[ai];
88
- const numTaskCols = Math.max(1, activity.tasks.length);
89
88
  const groupWidth = activityGroupWidths[ai];
90
89
  // Activity card — spans its task columns
91
90
  const activityX = frameLeft + groupLeftX + groupWidth / 2;
@@ -27,7 +27,6 @@ function findProjectRoot(startDir) {
27
27
  catch {
28
28
  return null;
29
29
  }
30
- // eslint-disable-next-line no-constant-condition
31
30
  while (true) {
32
31
  if (fs_1.default.existsSync(path_1.default.join(dir, '.git')) ||
33
32
  fs_1.default.existsSync(path_1.default.join(dir, 'package.json')) ||
@@ -14,13 +14,13 @@ exports.sanitizeDeclaredField = sanitizeDeclaredField;
14
14
  * `AGENTS.md` documenta bajo `defensive-guard-consistency`: endurecer una copia
15
15
  * y dejar la otra atrás. */
16
16
  function stripControlChars(text) {
17
- // eslint-disable-next-line no-control-regex -- necesitamos matchear C0 deliberadamente
17
+ // Necesitamos matchear C0 deliberadamente.
18
18
  return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
19
19
  }
20
20
  /** Keeps an untrusted diagnostic field on one terminal line, without changing
21
21
  * the whitespace policy for multiline public text or composed markdown. */
22
22
  function sanitizeDiagnosticText(text) {
23
- // eslint-disable-next-line no-control-regex -- remove all C0/C1 controls, including terminal escape bytes
23
+ // Remove all C0/C1 controls, including terminal escape bytes.
24
24
  return text.replace(/\r\n|[\r\n\t\u2028\u2029]/g, ' ').replace(/[\x00-\x1f\x7f-\x9f]/g, '');
25
25
  }
26
26
  /**
@@ -2,6 +2,31 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.decideTeardown = void 0;
4
4
  exports.worktreeOwnershipProven = worktreeOwnershipProven;
5
+ // Task 13 (R4.2/R4.3/R4.6/R4.10/C2/C9): `decideTeardown` YA vive en
6
+ // `protocol.ts` (autoridad única de decisión) — este módulo NO crea una
7
+ // segunda copia (mismo criterio que `join.ts` con `decideJoinReconciliation`,
8
+ // T11). `teardown.test.ts` prueba la matriz completa IMPORTANDO este
9
+ // re-export, así que cualquier fix que descubra tiene que vivir en
10
+ // `protocol.ts` (y volver a correr la exploración de T1 en el mismo commit)
11
+ // para que el gate siga significando algo.
12
+ //
13
+ // Lo que SÍ aporta este archivo es la única prueba de OWNERSHIP real que
14
+ // `tracks.ts` necesita antes de siquiera considerar `remove-owned-worktree`
15
+ // como decisión segura: cuatro condiciones simultáneas (R4.10) —
16
+ // 1. `teardownIntent.worktreePath` (snapshot durable tomado en
17
+ // `TEARDOWN_REQUESTED` -> `TEARDOWN_INTENT`) resuelve al MISMO target
18
+ // canónico que `ref.worktreePath` (el dato vivo del journal).
19
+ // 2. `teardownIntent.branch === ref.branch`.
20
+ // 3. `.awm/track.json` del worktree (`readDescriptor`) coincide en
21
+ // `planJournalId`/`trackId`/`fencingToken` — la identidad completa que
22
+ // el propio track escribió al armarse (Task 9), nunca reconstruida a
23
+ // mano.
24
+ // 4. `git worktree list --porcelain` asocia ESE path exactamente con la
25
+ // branch determinista del track (`ownedWorktreeExists`, ya construida
26
+ // en Task 9 sobre el mismo criterio).
27
+ // Cualquier chequeo indemostrable (no existe, no legible, mismatch, fallo de
28
+ // git) es "ownership NO probado" — fail-closed (R4.6): el caller nunca
29
+ // adopta ni borra lo que no pudo probar.
5
30
  const descriptor_1 = require("./descriptor");
6
31
  const paths_1 = require("../paths");
7
32
  const git_1 = require("./git");
@@ -66,7 +66,7 @@ describe('awm context orchestrators', () => {
66
66
  it('sanea bytes de control antes de escribir a la terminal', () => {
67
67
  const hostil = { name: 'x\x1b[31m', appliesWhen: 'w\x07', terminatesTo: 't\x00' };
68
68
  const r = (0, context_1.runContextOrchestrators)(collected([hostil]), { json: false });
69
- // eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
69
+ // Verificamos la ausencia deliberada de C0.
70
70
  expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
71
71
  });
72
72
  });
@@ -180,7 +180,7 @@ describe('multi-agent targeting (add/remove/sync/update/doctor)', () => {
180
180
  fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.com/baseline.git' }], null, 2));
181
181
  const { runUpdateCore } = require('../../src/commands/update');
182
182
  const syncRegistries = jest.fn(async () => [SYNCED_REGISTRY]);
183
- const planReconciliation = jest.fn((o) => ({ operations: [], records: [], reports: [] }));
183
+ const planReconciliation = jest.fn((_operations) => ({ operations: [], records: [], reports: [] }));
184
184
  const applyInstallPlan = jest.fn(() => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }));
185
185
  const resyncInstalledHooks = jest.fn(() => []);
186
186
  const outcome = await runUpdateCore({}, {
@@ -25,7 +25,6 @@ jest.mock('../../../src/commands/sensors/run', () => ({
25
25
  jest.mock('../../../src/core/currentness/check', () => ({
26
26
  checkCurrentness: jest.fn(),
27
27
  }));
28
- const mockExecSync = child_process_1.execSync;
29
28
  const mockRunSensors = run_1.runSensors;
30
29
  const mockCheckCurrentness = check_1.checkCurrentness;
31
30
  /** Turn a tmpdir into a real git repo with (optionally) an `origin` remote. */
@@ -79,7 +79,7 @@ describe('awm process show (texto)', () => {
79
79
  });
80
80
  const r = (0, process_1.runProcessShow)({ models: [hostile], diagnostics: [] }, 'mi-proceso', false);
81
81
  expect(r.code).toBe(0);
82
- // eslint-disable-next-line no-control-regex -- necesitamos verificar la ausencia deliberada de C0
82
+ // Necesitamos verificar la ausencia deliberada de C0.
83
83
  expect(r.stdout).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
84
84
  expect(r.stdout).toContain('Objetivo[31mmalicioso[0m.');
85
85
  expect(r.stdout).toContain('Uno]0;pwned');
@@ -25,9 +25,6 @@ class ExitSignal extends Error {
25
25
  this.code = code;
26
26
  }
27
27
  }
28
- function git(repo, args) {
29
- return (0, child_process_1.execFileSync)('git', args, { cwd: repo, encoding: 'utf8' }).trim();
30
- }
31
28
  function gitInit(repo, branch) {
32
29
  (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', 'init', '-q', '-b', branch], { cwd: repo });
33
30
  fs_1.default.writeFileSync(path_1.default.join(repo, 'f.txt'), 'x');
@@ -126,7 +126,7 @@ describe('aplicacion transaccional de requests', () => {
126
126
  expect(Object.keys((0, store_1.readJournal)(repo, 'rama').state.jobs)).toHaveLength(1); // sin duplicados
127
127
  });
128
128
  test('register-entity CREA task con VerificationPlan y ReviewObligations; cycle-plan y dispatch (R1.4/R1.4b)', () => {
129
- const request = (0, requests_1.emitRequest)(repo, 'rama', {
129
+ (0, requests_1.emitRequest)(repo, 'rama', {
130
130
  kind: 'register-entity', generationToken: 'g1', idempotencyKey: 'e1',
131
131
  payload: {
132
132
  entity: 'task', taskId: 'T1', title: 'implementar',
@@ -201,7 +201,7 @@ describe('composedOrchestrators', () => {
201
201
  { name: 'a\x1bx', appliesWhen: 'w\x07', terminatesTo: 't\x00' },
202
202
  ]);
203
203
  expect(out[0].name).toBe('ax');
204
- // eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
204
+ // Verificamos la ausencia deliberada de C0.
205
205
  const controlCharPattern = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
206
206
  expect(out[0].name).not.toMatch(controlCharPattern);
207
207
  expect(out[0].appliesWhen).not.toMatch(controlCharPattern);
@@ -216,7 +216,7 @@ describe('composedOrchestrators', () => {
216
216
  { name: 'evil\x1b[31m', appliesWhen: 'w', terminatesTo: 't' },
217
217
  ],
218
218
  });
219
- // eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
219
+ // Verificamos la ausencia deliberada de C0.
220
220
  expect(ctx.markdown).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
221
221
  });
222
222
  it('lo que renderiza el payload sale de esta misma funcion', () => {
@@ -47,7 +47,7 @@ describe('ConfigInstructionsStrategy.inject', () => {
47
47
  });
48
48
  describe('ConfigInstructionsStrategy.remove', () => {
49
49
  it('removes only the sentinel, preserving user entries', () => {
50
- const { configPath, absPath, provider, input } = setup({ instructions: ['docs/rules.md'] });
50
+ const { configPath, provider, input } = setup({ instructions: ['docs/rules.md'] });
51
51
  strat.inject(input, provider);
52
52
  strat.remove(input, provider);
53
53
  const cfg = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
@@ -26,7 +26,7 @@ describe('renderFullTerminal', () => {
26
26
  it('renders all lifecycle sections in canonical order with status and remediation', () => {
27
27
  const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot()));
28
28
  const headings = ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Docs', 'Retro', 'Final / history', 'Processes'];
29
- expect(headings.map((heading) => output.indexOf(heading))).toEqual([...headings.map((_, index) => expect.any(Number))].map((_, index) => expect.any(Number)));
29
+ expect(headings.map((heading) => output.indexOf(heading))).toEqual(headings.map(() => expect.any(Number)));
30
30
  for (let index = 1; index < headings.length; index++)
31
31
  expect(output.indexOf(headings[index])).toBeGreaterThan(output.indexOf(headings[index - 1]));
32
32
  expect(output).toContain('⚠ Sensors [project.sensors] — 2 stale');
@@ -191,7 +191,6 @@ describe('gatherProviderChecks — shared skills.global does not mask broken lin
191
191
  });
192
192
  it('still reports shared (healthy, non-degrading) when the shared dir has no broken links', () => {
193
193
  const { gatherContext } = require('../../../src/core/diagnostics/context');
194
- const { computeProviderOverall } = require('../../../src/core/diagnostics/checks');
195
194
  const scanSkills = jest.fn(() => ({ valid: ['ok-skill'], repairable: [], dead: [], usurped: [] }));
196
195
  const ctx = gatherContext({ cwd: tmpHome, bundles: [], agents: ['opencode', 'codex'], scanSkills });
197
196
  for (const provider of ctx.providers) {
@@ -14,7 +14,6 @@ const SKILLS_ROOT = CONTENT_ROOT;
14
14
  const WORKFLOWS_ROOT = CONTENT_ROOT;
15
15
  // For backward-compat with mock expectations that check SKILLS_DIR-style paths:
16
16
  const SKILLS_DIR = path_1.default.join(CONTENT_ROOT, 'skills');
17
- const WORKFLOWS_DIR = path_1.default.join(CONTENT_ROOT, 'workflows');
18
17
  const directoryStat = { dev: 1n, ino: 3n, isDirectory: () => true, isSymbolicLink: () => false };
19
18
  const skillStat = { dev: 1n, ino: 2n, size: 0n, isFile: () => true, isSymbolicLink: () => false };
20
19
  describe('Artifact Discovery', () => {
@@ -62,7 +62,7 @@ describe('runInitSteps — orchestrator', () => {
62
62
  function buildDeps(cwd) {
63
63
  const { gatherContext } = require('../../../src/core/diagnostics/context');
64
64
  const { discoverAllBundles } = require('../../../src/core/bundles');
65
- const { contentRoots, registriesDir } = require('../../../src/core/registries');
65
+ const { contentRoots } = require('../../../src/core/registries');
66
66
  const { defaultActions } = require('../../../src/core/init/steps');
67
67
  // Seed content at ~/.awm/registries/baseline (content root IS the registry root).
68
68
  const contentRoot = path_1.default.join(process.env.AWM_HOME, 'registries', 'baseline');
@@ -222,7 +222,7 @@ describe('process identity', () => {
222
222
  });
223
223
  test('captureRefFor degrada a unknown si ps falla, nunca crashea al capturar identidad (R2.1)', () => {
224
224
  const cp = require('child_process');
225
- const spy = jest.spyOn(cp, 'execFileSync').mockImplementation((...args) => {
225
+ const spy = jest.spyOn(cp, 'execFileSync').mockImplementation((..._args) => {
226
226
  const err = new Error('spawn ps ENOENT');
227
227
  err.code = 'ENOENT';
228
228
  throw err;
@@ -197,7 +197,7 @@ describe('readDeclaredOrchestrators', () => {
197
197
  stderrBoundary(result.diagnostics);
198
198
  expect(stderrBoundary.mock.calls[0][0].join('')).not.toMatch(/[\u2028\u0085]/);
199
199
  expect(stderrBoundary.mock.results[0].value).not.toMatch(/[\u2028\u0085]/);
200
- // eslint-disable-next-line no-control-regex -- reject all terminal controls and line separators
200
+ // Reject all terminal controls and line separators.
201
201
  expect(result.diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
202
202
  });
203
203
  it.each(['inspection', 'read', 'parse', 'shape', 'fields'])('sanitizes %s diagnostics, including paths and error text', (branch) => {
@@ -224,7 +224,7 @@ describe('readDeclaredOrchestrators', () => {
224
224
  expect(result.diagnostics[0]).toContain('awm-registry.json');
225
225
  expect(result.diagnostics[0]).toContain('before');
226
226
  expect(result.diagnostics[0]).toContain('after');
227
- // eslint-disable-next-line no-control-regex -- reject all terminal controls and line separators
227
+ // Reject all terminal controls and line separators.
228
228
  expect(result.diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
229
229
  });
230
230
  it('preserves valid raw fields and structural parsing without skill discovery', () => {
@@ -290,7 +290,7 @@ describe('collectDeclaredOrchestrators', () => {
290
290
  writeSkill('first', 'real-skill');
291
291
  const { diagnostics } = (0, orchestrators_1.collectDeclaredOrchestrators)();
292
292
  expect(diagnostics).toHaveLength(1);
293
- // eslint-disable-next-line no-control-regex -- diagnostic must contain no terminal controls or line separators
293
+ // Diagnostic must contain no terminal controls or line separators.
294
294
  expect(diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
295
295
  expect(diagnostics[0]).toContain('shared');
296
296
  expect(diagnostics[0]).toContain('forged');
@@ -304,7 +304,7 @@ describe('collectDeclaredOrchestrators', () => {
304
304
  expect(diagnostics).toHaveLength(1);
305
305
  expect(diagnostics[0]).toContain('discovery unavailable');
306
306
  expect(diagnostics[0]).toContain('forged');
307
- // eslint-disable-next-line no-control-regex -- diagnostic must contain no terminal controls or line separators
307
+ // Diagnostic must contain no terminal controls or line separators.
308
308
  expect(diagnostics[0]).not.toMatch(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/);
309
309
  });
310
310
  it('excludes a later colliding root without losing earlier or subsequent healthy roots', () => {
@@ -676,7 +676,6 @@ windowsOnly('native secure-fs Windows handle fixtures', () => {
676
676
  const junction = path_1.default.join(root, 'junction');
677
677
  fs_1.default.mkdirSync(outside);
678
678
  fs_1.default.symlinkSync(outside, junction, 'junction');
679
- const target = path_1.default.join(junction, 'sensors.json');
680
679
  expect(() => binding.writeProjectTransaction(root, 'junction/sensors.json', Buffer.from('new bytes'), { mode: 'create', createParents: false })).toThrow(/rejected path ancestor/i);
681
680
  expect(fs_1.default.existsSync(path_1.default.join(outside, 'sensors.json'))).toBe(false);
682
681
  });
@@ -28,7 +28,7 @@ describe('sanitizeDeclaredField', () => {
28
28
  expect((0, text_1.sanitizeDeclaredField)('a\n## Forjado *b* `c` <d>')).toBe('a Forjado b c d');
29
29
  });
30
30
  it('no deja ESC ni otros C0 sobrevivir aunque el saneo markdown no los toque', () => {
31
- // eslint-disable-next-line no-control-regex -- verificamos la ausencia deliberada de C0
31
+ // Verificamos la ausencia deliberada de C0.
32
32
  expect((0, text_1.sanitizeDeclaredField)('name\x1b[31mred\x07')).not.toMatch(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/);
33
33
  });
34
34
  it('es idempotente sobre texto ya limpio', () => {
@@ -86,7 +86,7 @@ describe('directory fsync has exactly one implementation', () => {
86
86
  ];
87
87
  expect(rejects(regression, 1)).toBe(true);
88
88
  // The sanctioned replacement carries no directory open at all.
89
- expect(regression.map((_, i) => i).some(i => rejects(['fsyncDirSync(path.dirname(file));'], 0))).toBe(false);
89
+ expect(rejects(['fsyncDirSync(path.dirname(file));'], 0)).toBe(false);
90
90
  // Flushing a FILE descriptor stays legal — that is a real, portable fsync.
91
91
  const fileFlush = ["const fd = fs.openSync(temporary, 'wx', 0o600);", 'fs.fsyncSync(fd);'];
92
92
  expect(rejects(fileFlush, 0)).toBe(false);
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const child_process_1 = require("child_process");
7
+ const fs_1 = __importDefault(require("fs"));
7
8
  const path_1 = __importDefault(require("path"));
8
9
  // #171: the lint gate linted 14 files — all js/mjs — and exited 0 on a
9
10
  // TypeScript CLI. `eslint.config.awm.mjs` imports a project config that does
@@ -23,12 +24,37 @@ const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
23
24
  * repo already documents in tests/integration/published-doctor-evidence.e2e.test.ts,
24
25
  * and the one this test hit on both Windows legs of its first CI run. Going
25
26
  * through `process.execPath` needs no shell, so no argument escaping either. */
26
- function lint() {
27
- const raw = (0, child_process_1.execFileSync)(process.execPath, [
27
+ function lint(target = '.') {
28
+ const args = [
28
29
  path_1.default.join(CLI_ROOT, 'node_modules', 'eslint', 'bin', 'eslint.js'),
29
- '.', '--config', 'eslint.config.awm.mjs', '--format', 'json',
30
- ], { cwd: CLI_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
31
- return JSON.parse(raw);
30
+ target, '--config', 'eslint.config.awm.mjs', '--format', 'json',
31
+ ];
32
+ try {
33
+ const raw = (0, child_process_1.execFileSync)(process.execPath, args, { cwd: CLI_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
34
+ return JSON.parse(raw);
35
+ }
36
+ catch (error) {
37
+ // eslint exits non-zero when it reports an error, and the JSON report is
38
+ // still on stdout. A probe that EXPECTS a finding must read it, not die.
39
+ const stdout = error.stdout;
40
+ if (typeof stdout !== 'string' || stdout.trim() === '')
41
+ throw error;
42
+ return JSON.parse(stdout);
43
+ }
44
+ }
45
+ /** Lint one throwaway `.ts` file inside the repo, so the real config's `.ts`
46
+ * block applies to it, and report what the gate said about it. */
47
+ function probe(body) {
48
+ const directory = fs_1.default.mkdtempSync(path_1.default.join(CLI_ROOT, 'tests', '.lint-probe-'));
49
+ const file = path_1.default.join(directory, 'probe.ts');
50
+ try {
51
+ fs_1.default.writeFileSync(file, body);
52
+ const results = lint(path_1.default.relative(CLI_ROOT, file).split(path_1.default.sep).join('/'));
53
+ return results.flatMap(entry => entry.messages);
54
+ }
55
+ finally {
56
+ fs_1.default.rmSync(directory, { recursive: true, force: true });
57
+ }
32
58
  }
33
59
  describe('the lint gate covers TypeScript (#171)', () => {
34
60
  jest.setTimeout(10 * 60_000);
@@ -53,4 +79,37 @@ describe('the lint gate covers TypeScript (#171)', () => {
53
79
  .map(m => `${entry.filePath}: ${m.message}`));
54
80
  expect(unresolved).toEqual([]);
55
81
  });
82
+ // The gate examining TypeScript is worth nothing if it cannot fail on it.
83
+ // #171 left the rule at 'warn' with a backlog behind it; these assert the
84
+ // gate now BLOCKS, and that the tree it blocks on is clean.
85
+ test('the tree it lints is clean, with nothing left to report', () => {
86
+ const findings = results.flatMap(entry => entry.messages.map(m => `${entry.filePath}: ${m.ruleId ?? 'directive'} ${m.message}`));
87
+ expect(findings).toEqual([]);
88
+ });
89
+ test('an unused variable in TypeScript is an error, not a warning', () => {
90
+ const messages = probe('const neverUsed = 1;\nexport const used = 2;\n');
91
+ const unused = messages.filter(m => m.ruleId === '@typescript-eslint/no-unused-vars');
92
+ expect(unused).toHaveLength(1);
93
+ expect(unused[0].severity).toBe(2);
94
+ expect(unused[0].message).toContain('neverUsed');
95
+ });
96
+ test('an underscore-prefixed unused binding stays deliberate and unreported', () => {
97
+ expect(probe('const _deliberatelyUnused = 1;\nexport const used = 2;\n')).toEqual([]);
98
+ });
99
+ // The reason the rule had to become the TypeScript-aware one rather than the
100
+ // base rule: eslint's own `no-unused-vars` does not understand type
101
+ // annotations, so it flagged every parameter NAME inside a function TYPE.
102
+ // That was 195 of the 259 findings #171 exposed. Renaming those to `_x` would
103
+ // have made the types less readable to silence noise.
104
+ test('parameter names inside a function type are not reported as unused', () => {
105
+ const messages = probe('export type Fingerprint = (argv: string[], paths: string[], cwd: string) => string | null;\n');
106
+ expect(messages).toEqual([]);
107
+ });
108
+ test('a caught error that is never inspected is reported too', () => {
109
+ const messages = probe('export function f(value: number): number {\n try { return value; } catch (error) { return 0; }\n}\n');
110
+ const unused = messages.filter(m => m.ruleId === '@typescript-eslint/no-unused-vars');
111
+ expect(unused).toHaveLength(1);
112
+ expect(unused[0].severity).toBe(2);
113
+ expect(unused[0].message).toContain('error');
114
+ });
56
115
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "9.11.4",
3
+ "version": "9.11.5",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"
@@ -59,6 +59,7 @@
59
59
  "@types/js-yaml": "4.0.9",
60
60
  "@types/node": "^25.3.0",
61
61
  "@types/semver": "^7.7.1",
62
+ "@typescript-eslint/eslint-plugin": "^8.70.0",
62
63
  "@typescript-eslint/parser": "^8.70.0",
63
64
  "dependency-cruiser": "16.10.4",
64
65
  "eslint": "10.8.1",
Binary file
Binary file