@codedology/code-forge 0.14.1 → 0.15.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.
@@ -42,12 +42,13 @@ import path from 'node:path';
42
42
  import { fileURLToPath } from 'node:url';
43
43
  import { expiryChecks } from '../autopilot/limits.mjs';
44
44
  import { loadProjectConfig } from '../config/load.mjs';
45
+ import { reviewWorkers } from '../config/validate.mjs';
45
46
  import { cacheHoursFrom, createDefaultKeyStore, resolveKey } from '../keys/store.mjs';
46
47
  import { appendRow, readAllRows } from '../ledger/write.mjs';
47
48
  import { spawnSession } from '../session/spawn.mjs';
48
49
  import { snapshotFor } from '../state/config-snapshot.mjs';
49
50
  import { runRecordPath } from '../state/paths.mjs';
50
- import { readRun, reattachWorker } from '../state/run.mjs';
51
+ import { processStartTime, readRun, reattachWorker } from '../state/run.mjs';
51
52
  import { loadKey, signRow } from '../state/signer.mjs';
52
53
  import { logWarning } from '../util/error-log.mjs';
53
54
  import { withLock, tryLock } from '../util/file-locks.mjs';
@@ -260,6 +261,7 @@ export function recordedVerdict(result) {
260
261
  */
261
262
  export async function createWorker(opts, deps = {}) {
262
263
  const { runId, repoRoot, cfg, runRootDir, slug, key } = opts;
264
+ const identity = { pid: process.pid, started_at: await processStartTime(process.pid) };
263
265
  const env = deps.env ?? process.env;
264
266
  const review = deps.review ?? reviewTicket;
265
267
  const spawn = deps.spawn ?? spawnSession;
@@ -382,6 +384,7 @@ export async function createWorker(opts, deps = {}) {
382
384
  { env: childEnv, ...(deps.bins ? { bins: deps.bins } : {}), ...(deps.stderr ? { stderr: deps.stderr } : {}) },
383
385
  ),
384
386
  writeRow: (row) => ledger({ ...row, block: ticket.block, file: ticket.file, content_hash: ticket.content_hash }),
387
+ writeBlockRow: (row) => ledger({ ...row, block: ticket.block }),
385
388
  };
386
389
  try {
387
390
  const outcome = await review(ticket, ctx);
@@ -484,12 +487,14 @@ export async function createWorker(opts, deps = {}) {
484
487
  * @param {string} id @param {Record<string, any>} outcome - recorded through {@link recordedVerdict}.
485
488
  */
486
489
  async function complete(id, outcome) {
487
- const result = recordedVerdict(outcome);
490
+ /** @type {Record<string, any>} */
491
+ const result = { ...recordedVerdict(outcome), worker: identity };
488
492
  const signed = writeResult({ repoRoot, runId, ticket: id, result, key });
489
493
  try {
490
494
  await ledger({
491
495
  event: 'review.result',
492
496
  ticket: id,
497
+ worker: identity,
493
498
  block: result.block ?? null,
494
499
  file: result.file ?? null,
495
500
  content_hash: result.content_hash ?? null,
@@ -793,9 +798,6 @@ export async function createWorker(opts, deps = {}) {
793
798
  return { processTicket, drain, run, once, stop, release, announce, watchTick: tickWatch, jevKeyResolved: jevKey !== null, sessionEnv: childEnv };
794
799
  }
795
800
 
796
- /** This block permits one registered worker per queue. */
797
- const WORKER_LIMIT = 1;
798
-
799
801
  /**
800
802
  * Start-up for the `worker` verb: run record, repo root, config, run temp root (owned by this
801
803
  * process), run key; refuses when another live worker already serves this queue.
@@ -831,7 +833,10 @@ export async function bootWorker(opts, deps = {}) {
831
833
  const legacy = liveWorker(repoRoot);
832
834
  if (legacy && !entries.some((entry) => entry.worker?.pid === legacy.pid && entry.worker.start_time === legacy.start_time)) count++;
833
835
  await deps.hooks?.afterCount?.(count);
834
- if (count >= WORKER_LIMIT) throw new WorkerError('worker-running', 'a live or unreadable worker entry already serves this queue');
836
+ const limit = reviewWorkers(cfg);
837
+ if (entries.some((entry) => entry.live && !entry.worker) || (legacy && legacy.run !== opts.runId) || entries.some((entry) => entry.live && entry.worker?.run !== opts.runId) || count >= limit) {
838
+ throw new WorkerError('worker-running', `review.workers limit ${limit}: a live or unreadable worker entry already serves this queue`);
839
+ }
835
840
  worker.announce();
836
841
  });
837
842
  return { ...worker, repoRoot, runRootDir };
@@ -844,10 +849,10 @@ export async function bootWorker(opts, deps = {}) {
844
849
  * `expectPid` (a `run start` that just launched a worker) pins ONLY that pid: an older or stale
845
850
  * announcement naming another live pid is refused (`worker_mismatch`) instead of being pinned
846
851
  * over the new worker.
847
- * @param {{runId: string, repoRoot: string, writeRow?: (row: Record<string, any>) => Promise<unknown>, probe?: import('../state/run.mjs').StartTimeProbe, expectPid?: number}} opts
852
+ * @param {{runId: string, repoRoot: string, writeRow?: (row: Record<string, any>) => Promise<unknown>, probe?: import('../state/run.mjs').StartTimeProbe, expectPid?: number, add?: boolean}} opts
848
853
  */
849
- export async function repinWorker({ runId, repoRoot, writeRow, probe, expectPid }) {
850
- const live = liveWorker(repoRoot);
854
+ export async function repinWorker({ runId, repoRoot, writeRow, probe, expectPid, add = false }) {
855
+ const live = (expectPid === undefined ? null : liveWorker(repoRoot, { pid: expectPid, registeredOnly: true })) ?? liveWorker(repoRoot);
851
856
  if (!live) throw new WorkerError('worker_down', 'no live worker serves this queue');
852
857
  if (live.run !== runId) throw new WorkerError('other-run', `the live worker serves run ${live.run}`);
853
858
  if (expectPid !== undefined && live.pid !== expectPid) {
@@ -855,7 +860,7 @@ export async function repinWorker({ runId, repoRoot, writeRow, probe, expectPid
855
860
  }
856
861
  const { project } = await readRun(runId);
857
862
  const write = writeRow ?? ((/** @type {Record<string, any>} */ row) => appendRow(row, { slug: project }));
858
- return reattachWorker({ runId, workerPid: live.pid, writeRow: write, ...(probe ? { probe } : {}) });
863
+ return reattachWorker({ runId, workerPid: live.pid, writeRow: write, add, ...(probe ? { probe } : {}) });
859
864
  }
860
865
 
861
866
  /** The CLI entry the worker is launched from. */
@@ -887,5 +892,9 @@ export async function launchWorker({ runId, workspace, env = process.env, timeou
887
892
  // already gone
888
893
  }
889
894
  }
895
+ const other = liveWorker(repoRoot);
896
+ if (other && other.run !== runId) {
897
+ throw new WorkerError('worker_down', `the worker did not start: run ${other.run} still has a live worker (pid ${other.pid}); end it with: code-forge run end --run ${other.run}`);
898
+ }
890
899
  throw new WorkerError('worker_down', 'the worker did not start (run `code-forge worker --run <id>` by hand to see why)');
891
900
  }
@@ -516,11 +516,14 @@ export function readTicket(repoRoot, id) {
516
516
  * @returns {Record<string, any>} the signed result
517
517
  */
518
518
  export function writeResult({ repoRoot, runId, ticket, result, key }) {
519
- const signed = signRow({ ...result, run: runId, ticket: assertTicketId(ticket) }, key);
519
+ const split = result.status === 'split_required' || (result.status === 'stopped' && result.stopped === 'split_required');
520
+ // The engine's depth identifies the round-1 packet; rechecks always use the recheck lens.
521
+ const measured = split ? { budget: result.budget ?? null, lens: result.lens ?? (result.status === 'stopped' ? 'recheck' : result.depth === 'quick' ? 'quick' : result.depth === 'dual' ? 'A' : 'full') } : {};
522
+ const signed = signRow({ ...result, ...measured, run: runId, ticket: assertTicketId(ticket) }, key);
520
523
  const file = path.join(reviewsDir(repoRoot, runId), `${ticket}.json`);
521
524
  writeAtomic(file, `${JSON.stringify(signed)}\n`);
522
525
  const openFindings = signed.status === 'reviewed' && signed.approved !== true && Array.isArray(signed.findings) && signed.findings.length > 0;
523
- writeAtomic(path.join(queueDir(repoRoot), `${ticket}.done`), `${JSON.stringify({ ticket, status: signed.status, ...(openFindings ? { open_findings: true } : {}), result: path.relative(repoRoot, file) })}\n`);
526
+ writeAtomic(path.join(queueDir(repoRoot), `${ticket}.done`), `${JSON.stringify({ ticket, status: signed.status, ...measured, ...(openFindings ? { open_findings: true } : {}), result: path.relative(repoRoot, file) })}\n`);
524
527
  return signed;
525
528
  }
526
529