@codedology/code-forge 0.14.1 → 0.15.1
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 +29 -0
- package/README.md +1 -1
- package/docs/reference/blocks.json +495 -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 +4 -0
- package/skill/references/autopilot.md +2 -0
- package/skill/references/code.md +28 -2
- package/skill/references/review.md +10 -2
- package/skill/references/security.md +2 -0
- package/skill/scripts/forge +1 -1
- package/skill/templates/coder-brief.md +3 -1
- package/src/autopilot/scopes.mjs +2 -1
- package/src/cli/block.mjs +271 -50
- 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 +33 -7
- package/src/review/lenses/recheck.md +3 -0
- package/src/review/packet.mjs +26 -0
- package/src/state/block.mjs +196 -22
- package/src/state/run.mjs +35 -8
- package/src/util/forbidden.mjs +230 -6
- package/src/util/reaper.mjs +3 -3
- package/src/worker/engine.mjs +74 -6
- package/src/worker/loop.mjs +19 -10
- package/src/worker/queue.mjs +38 -8
package/src/worker/queue.mjs
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { createHash, randomBytes } from 'node:crypto';
|
|
32
32
|
import { chmodSync, existsSync, linkSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
33
33
|
import path from 'node:path';
|
|
34
|
+
import { runRecordPath } from '../state/paths.mjs';
|
|
34
35
|
import { loadKey, signRow, verifyRow } from '../state/signer.mjs';
|
|
35
36
|
import { isAlive, readStartTime, UNKNOWN_START_TIME } from '../util/reaper.mjs';
|
|
36
37
|
import { assertRowPath, assertTicketId, contentHash, ticketId, WorkerError } from './ticket.mjs';
|
|
@@ -75,7 +76,7 @@ function readJSON(file) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
|
-
* @typedef {{ticket: string, run: string, block: string, file: string, content_hash: string, enqueued_at: string, tree?: string, kind?: 'answers', by_file?: string, by_hash?: string, answers?: string[]}} Ticket
|
|
79
|
+
* @typedef {{ticket: string, run: string, block: string, file: string, content_hash: string, enqueued_at: string, tree?: string, acceptance_revision?: string, kind?: 'answers', by_file?: string, by_hash?: string, answers?: string[]}} Ticket
|
|
79
80
|
* `tree` (B62b): the block's worktree the content was hashed in. It is part of the ticket id,
|
|
80
81
|
* and the worker only checks it against the tree it takes from the run record (a ticket whose
|
|
81
82
|
* tree is not that one is `unavailable`, `worktree-mismatch`); it never reads content there.
|
|
@@ -93,7 +94,30 @@ export function resolveTicket({ repoRoot, run, block, file, tree = null }) {
|
|
|
93
94
|
const rel = assertRowPath(file);
|
|
94
95
|
const root = tree === null ? null : realTree(tree);
|
|
95
96
|
const content_hash = contentHash(root ?? repoRoot, rel);
|
|
96
|
-
|
|
97
|
+
const revision = acceptanceRevision(repoRoot, run, block);
|
|
98
|
+
return { rel, root, content_hash, ticket: reviewTicketId({ run, block, file: rel, content_hash, tree: root, acceptance_revision: revision }) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The mirror lets sandboxed callers resolve the same ticket without the private record.
|
|
102
|
+
* @param {string} repoRoot @param {string} run @param {string} block @returns {string | undefined}
|
|
103
|
+
*/
|
|
104
|
+
function acceptanceRevision(repoRoot, run, block) {
|
|
105
|
+
const record = readJSON(runRecordPath(run)) ?? readJSON(path.join(repoRoot, '.code-forge', 'runs', `${run}.json`));
|
|
106
|
+
const revision = record?.blocks?.[block]?.acceptance_revision;
|
|
107
|
+
return typeof revision === 'string' ? revision : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {{run: string, block: string, file: string, content_hash: string, tree?: string | null, acceptance_revision?: string}} parts */
|
|
111
|
+
function reviewTicketId(parts) {
|
|
112
|
+
const id = ticketId(parts);
|
|
113
|
+
return parts.acceptance_revision === undefined ? id : createHash('sha256').update(`${id}\0${parts.acceptance_revision}`).digest('hex').slice(0, 24);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Reset only the commit-watch cursor; review evidence and results remain intact.
|
|
117
|
+
* @param {string} repoRoot @param {string} block
|
|
118
|
+
*/
|
|
119
|
+
export function resetAcceptanceWatch(repoRoot, block) {
|
|
120
|
+
rmSync(path.join(queueDir(repoRoot), 'watch', `${block}.json`), { force: true });
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
/**
|
|
@@ -118,9 +142,10 @@ export function enqueue({ repoRoot, run, block, file, tree = null, now = new Dat
|
|
|
118
142
|
export function enqueueHash({ repoRoot, run, block, file, content_hash, tree = null, now = new Date() }) {
|
|
119
143
|
const rel = assertRowPath(file);
|
|
120
144
|
const root = tree === null ? null : realTree(tree);
|
|
121
|
-
const
|
|
145
|
+
const acceptance_revision = acceptanceRevision(repoRoot, run, block);
|
|
146
|
+
const ticket = reviewTicketId({ run, block, file: rel, content_hash, tree: root, acceptance_revision });
|
|
122
147
|
const target = path.join(queueDir(repoRoot), `${ticket}.json`);
|
|
123
|
-
const named = root === null ? {} : { tree: root };
|
|
148
|
+
const named = { ...(root === null ? {} : { tree: root }), ...(acceptance_revision === undefined ? {} : { acceptance_revision }) };
|
|
124
149
|
if (!existsSync(target)) {
|
|
125
150
|
/** @type {Ticket} */
|
|
126
151
|
const body = { ticket, run, block, file: rel, content_hash, enqueued_at: now.toISOString(), ...named };
|
|
@@ -444,7 +469,8 @@ export function blockTickets({ repoRoot, run, block, tree = null }) {
|
|
|
444
469
|
throw new WorkerError('queue-unreadable', 'the review queue cannot be read');
|
|
445
470
|
}
|
|
446
471
|
const tickets = names.filter((n) => /^[0-9a-f]{24}\.json$/.test(n)).map((n) => readTicket(repoRoot, n.slice(0, 24)));
|
|
447
|
-
|
|
472
|
+
const revision = acceptanceRevision(repoRoot, run, block);
|
|
473
|
+
return tickets.filter((t) => !t.kind && t.run === run && t.block === block && (t.tree ?? null) === tree && t.acceptance_revision === revision)
|
|
448
474
|
.sort((a, b) => a.enqueued_at.localeCompare(b.enqueued_at) || a.ticket.localeCompare(b.ticket));
|
|
449
475
|
}
|
|
450
476
|
|
|
@@ -505,7 +531,8 @@ export function readTicket(repoRoot, id) {
|
|
|
505
531
|
if (typeof t.by_hash !== 'string' || ids.join(',') !== t.answers.join(',') || answersTicketId(t) !== id) throw new WorkerError('bad-ticket', `ticket ${id} does not match its id`);
|
|
506
532
|
return /** @type {Ticket} */ (t);
|
|
507
533
|
}
|
|
508
|
-
if (
|
|
534
|
+
if (t.acceptance_revision !== undefined && typeof t.acceptance_revision !== 'string') throw new WorkerError('bad-ticket', `ticket ${id} has an invalid acceptance revision`);
|
|
535
|
+
if (reviewTicketId(t) !== id) throw new WorkerError('bad-ticket', `ticket ${id} does not match its id`);
|
|
509
536
|
return /** @type {Ticket} */ (t);
|
|
510
537
|
}
|
|
511
538
|
|
|
@@ -516,11 +543,14 @@ export function readTicket(repoRoot, id) {
|
|
|
516
543
|
* @returns {Record<string, any>} the signed result
|
|
517
544
|
*/
|
|
518
545
|
export function writeResult({ repoRoot, runId, ticket, result, key }) {
|
|
519
|
-
const
|
|
546
|
+
const split = result.status === 'split_required' || (result.status === 'stopped' && result.stopped === 'split_required');
|
|
547
|
+
// The engine's depth identifies the round-1 packet; rechecks always use the recheck lens.
|
|
548
|
+
const measured = split ? { budget: result.budget ?? null, lens: result.lens ?? (result.status === 'stopped' ? 'recheck' : result.depth === 'quick' ? 'quick' : result.depth === 'dual' ? 'A' : 'full') } : {};
|
|
549
|
+
const signed = signRow({ ...result, ...measured, run: runId, ticket: assertTicketId(ticket) }, key);
|
|
520
550
|
const file = path.join(reviewsDir(repoRoot, runId), `${ticket}.json`);
|
|
521
551
|
writeAtomic(file, `${JSON.stringify(signed)}\n`);
|
|
522
552
|
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`);
|
|
553
|
+
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
554
|
return signed;
|
|
525
555
|
}
|
|
526
556
|
|