@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.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/docs/reference/blocks.json +366 -1
- package/docs/reference/config.md +7 -0
- package/package.json +1 -1
- package/schema/code-forge.schema.json +22 -0
- package/scripts/first-use.mjs +11 -5
- package/skill/SKILL.md +2 -0
- package/skill/references/autopilot.md +2 -0
- package/skill/references/code.md +25 -1
- package/skill/references/review.md +10 -2
- package/skill/scripts/forge +1 -1
- package/src/autopilot/scopes.mjs +2 -1
- package/src/cli/block.mjs +235 -44
- package/src/cli/gates.mjs +36 -6
- package/src/cli/ledger.mjs +21 -2
- package/src/cli/report.mjs +14 -0
- package/src/cli/review-file.mjs +56 -16
- package/src/cli/run.mjs +44 -7
- package/src/cli/status.mjs +7 -3
- package/src/config/validate.mjs +13 -1
- package/src/gates/acceptance.mjs +7 -3
- package/src/gates/expand.mjs +25 -2
- package/src/gates/run.mjs +38 -8
- package/src/ledger/report.mjs +29 -7
- package/src/review/accept-hit.mjs +22 -4
- package/src/review/engine.mjs +30 -10
- package/src/review/finding.schema.json +2 -1
- package/src/review/fixloop.mjs +79 -14
- package/src/review/gate-check.mjs +22 -3
- package/src/review/lenses/recheck.md +3 -0
- package/src/review/packet.mjs +26 -0
- package/src/state/block.mjs +162 -20
- package/src/state/run.mjs +35 -8
- package/src/util/forbidden.mjs +14 -0
- package/src/util/reaper.mjs +3 -3
- package/src/worker/engine.mjs +60 -4
- package/src/worker/loop.mjs +19 -10
- package/src/worker/queue.mjs +5 -2
package/src/worker/loop.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/worker/queue.mjs
CHANGED
|
@@ -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
|
|
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
|
|