@codedology/code-forge 0.7.0 → 0.7.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 +8 -0
- package/README.md +9 -1
- package/docs/reference/blocks.json +15 -1
- package/docs/reference/config.md +1 -1
- package/package.json +1 -1
- package/schema/code-forge.schema.json +1 -1
- package/skill/references/review.md +1 -1
- package/skill/scripts/forge +1 -1
- package/src/review/engine.mjs +7 -8
- package/src/review/validate-review.mjs +11 -11
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ waits on a second real consumer (plan §10.4, Q17).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.1] — 2026-10-07
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- A structured clean pass (passed: true, no findings, every hunk acknowledged, a non-blank summary) is no longer refused
|
|
14
|
+
as too_short on a larger diff: it needs 40 output tokens whatever the diff size (clean passes measured 108-121 tokens;
|
|
15
|
+
issue #6). An answer with any finding, or a blank summary, keeps review.min_tokens_out.
|
|
16
|
+
|
|
9
17
|
## [0.7.0] — 2026-10-07
|
|
10
18
|
|
|
11
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -8,16 +8,24 @@ for cross-model, parallel, evidence-gated feature delivery — on Claude Code, C
|
|
|
8
8
|
> `examples/` have all landed (`docs/reference/blocks.json` lists every build block). What changed
|
|
9
9
|
> in each version is in [CHANGELOG.md](CHANGELOG.md).
|
|
10
10
|
|
|
11
|
-
## What's new in 0.
|
|
11
|
+
## What's new in 0.7
|
|
12
12
|
|
|
13
13
|
Next release:
|
|
14
14
|
|
|
15
|
+
- **Short clean reviews count.** A clean pass (no findings, every hunk acknowledged, a summary)
|
|
16
|
+
needs only 40 output tokens on any diff size, so a correct short "no defect" review is no
|
|
17
|
+
longer refused as `too_short`.
|
|
18
|
+
|
|
19
|
+
In 0.7.0:
|
|
20
|
+
|
|
15
21
|
- **The close gate now checks every file.** `block close` could close a block with critical
|
|
16
22
|
findings still open: files inside an owned folder were skipped, and so was work committed
|
|
17
23
|
before `block open` or done in another worktree. Now every reviewed file is checked, a newer
|
|
18
24
|
failed review beats an older approval, a critical needs your waiver, a change outside the
|
|
19
25
|
block's files is refused, and `--no-require-reviews` asks you at a terminal.
|
|
20
26
|
|
|
27
|
+
## What's new in 0.6
|
|
28
|
+
|
|
21
29
|
In 0.6.1:
|
|
22
30
|
|
|
23
31
|
- **Never relax a rule alone.** A new skill rule (R17): the orchestrator and the delegate never
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"plans/code-forge-plan-v1.3.md §10.4",
|
|
5
5
|
"plans/NIGHT-LOG.md"
|
|
6
6
|
],
|
|
7
|
-
"block_count":
|
|
7
|
+
"block_count": 56,
|
|
8
8
|
"baseline_ids": ["B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8"],
|
|
9
9
|
"baseline_note": "Wave 0-1 blocks, landed before v1.3; depends_on may name them, but they have no entry under blocks.",
|
|
10
10
|
"blocks": {
|
|
@@ -748,6 +748,20 @@
|
|
|
748
748
|
"B50"
|
|
749
749
|
],
|
|
750
750
|
"note": "root-added 2026-10-07; GitHub issue #2 field note 2026-10-07; also amends src/state/block.mjs, src/review/triage.mjs, src/worker/loop.mjs, skill/SKILL.md, skill/references/{code,review,decisions}.md, skill/references/adapters/{claude-code,solo}.md, docs/how-it-works.md, docs/tutorial-first-test.md, docs/concepts.md"
|
|
751
|
+
},
|
|
752
|
+
"B53": {
|
|
753
|
+
"title": "Clean-pass token floor for any diff size (issue #6): a structured clean pass (passed: true, no findings, hunks acknowledged in order, a non-blank summary) needs min(review.min_tokens_out, 40) output tokens; any finding, passed: false or a blank summary keeps the full floor",
|
|
754
|
+
"wave": 12,
|
|
755
|
+
"level": "L1",
|
|
756
|
+
"status": "landed",
|
|
757
|
+
"owned": [
|
|
758
|
+
"src/review/validate-review.mjs",
|
|
759
|
+
"test/review/validate-review.test.mjs"
|
|
760
|
+
],
|
|
761
|
+
"depends_on": [
|
|
762
|
+
"B52"
|
|
763
|
+
],
|
|
764
|
+
"note": "root-added 2026-10-07; GitHub issue #6; also amends src/review/engine.mjs (smallDiff removed), schema/code-forge.schema.json, docs/reference/config.md, docs/how-it-works.md, skill/references/review.md"
|
|
751
765
|
}
|
|
752
766
|
}
|
|
753
767
|
}
|
package/docs/reference/config.md
CHANGED
|
@@ -105,7 +105,7 @@ Every other key defaults as shown, or is entirely absent from a minimal file. `a
|
|
|
105
105
|
| `review.context.min_context_lines` | integer | | `10` | min 0 |
|
|
106
106
|
| `review.block_budget_tokens` | integer | | `150000` | min 1 |
|
|
107
107
|
| `review.block_budget_usd` | number \| null | | `null` | |
|
|
108
|
-
| `review.min_tokens_out` | integer | | `120` | min 1 |
|
|
108
|
+
| `review.min_tokens_out` | integer | | `120` | min 1; The stub guard: a review answer with fewer output tokens is unavailable (too_short). A clean pass (passed: true, no findings, a non-blank summary) needs only min(this, 40), whatever the diff size (B53). |
|
|
109
109
|
| `review.batch_small_files` | boolean | | `true` | |
|
|
110
110
|
| `review.session_timeout_s` | integer | | `300` | min 1 |
|
|
111
111
|
| `review.max_rounds_per_file` | integer | | `4` | min 2; max 6 |
|
package/package.json
CHANGED
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"anyOf": [{ "type": "number", "exclusiveMinimum": 0 }, { "type": "null" }],
|
|
137
137
|
"default": null
|
|
138
138
|
},
|
|
139
|
-
"min_tokens_out": { "type": "integer", "minimum": 1, "default": 120 },
|
|
139
|
+
"min_tokens_out": { "type": "integer", "minimum": 1, "default": 120, "description": "The stub guard: a review answer with fewer output tokens is unavailable (too_short). A clean pass (passed: true, no findings, a non-blank summary) needs only min(this, 40), whatever the diff size (B53)." },
|
|
140
140
|
"batch_small_files": { "type": "boolean", "default": true },
|
|
141
141
|
"session_timeout_s": { "type": "integer", "minimum": 1, "default": 300 },
|
|
142
142
|
"max_rounds_per_file": { "type": "integer", "minimum": 2, "maximum": 6, "default": 4 },
|
|
@@ -19,7 +19,7 @@ Every reviewer, recheck, judge and S2 session runs with cwd = a fresh empty temp
|
|
|
19
19
|
|
|
20
20
|
## §4 The stub guard — what counts as a review (R11)
|
|
21
21
|
|
|
22
|
-
A result is a review only if the process exited 0, the JSON validates against the finding schema, `reviewed_hunks` equals the packet's hunk headers exactly and in order, and the output has at least `review.min_tokens_out` tokens. Anything else is ledger `review.unavailable` (`exit`, `schema`, `hunks_mismatch`, `too_short`, `timeout`, `budget` — the run reached `budget.usd`, `ledger.md` §3.1) and **is not approval**; the worker retries once on the level's fallback, then the file stays unreviewed and the block cannot close. A session that hangs is killed at `review.session_timeout_s` (default 300), process group and all, and is spawned once more on the same packet and level; only a second timeout is `unavailable: timeout`. Each timed-out attempt leaves a `review.session_timeout` ledger note with the last 4 KB of its stderr (redacted, never the packet) and only the byte count and last event types of its stdout.
|
|
22
|
+
A result is a review only if the process exited 0, the JSON validates against the finding schema, `reviewed_hunks` equals the packet's hunk headers exactly and in order, and the output has at least `review.min_tokens_out` tokens (a clean pass — `passed: true`, no findings, a non-blank summary — needs only 40, whatever the diff size). Anything else is ledger `review.unavailable` (`exit`, `schema`, `hunks_mismatch`, `too_short`, `timeout`, `budget` — the run reached `budget.usd`, `ledger.md` §3.1) and **is not approval**; the worker retries once on the level's fallback, then the file stays unreviewed and the block cannot close. A session that hangs is killed at `review.session_timeout_s` (default 300), process group and all, and is spawned once more on the same packet and level; only a second timeout is `unavailable: timeout`. Each timed-out attempt leaves a `review.session_timeout` ledger note with the last 4 KB of its stderr (redacted, never the packet) and only the byte count and last event types of its stdout.
|
|
23
23
|
|
|
24
24
|
## §5 The convergence rule — bounded fix rounds (R15)
|
|
25
25
|
|
package/skill/scripts/forge
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# copied install with no package next to it); `test/skill/shim.test.mjs` asserts the pin
|
|
11
11
|
# equals package.json's version.
|
|
12
12
|
# The shim holds no logic of its own: every verb, flag and message is the CLI's.
|
|
13
|
-
PINNED_VERSION="0.7.
|
|
13
|
+
PINNED_VERSION="0.7.1"
|
|
14
14
|
set -eu
|
|
15
15
|
|
|
16
16
|
self="$0"
|
package/src/review/engine.mjs
CHANGED
|
@@ -33,7 +33,7 @@ import { blockKind, isDocsKind } from '../decide/escalation.mjs';
|
|
|
33
33
|
import { fallbackRisk } from '../decide/fallback-rules.mjs';
|
|
34
34
|
import { tierFor } from '../proof/tiers.mjs';
|
|
35
35
|
import { assembleJudgePacket, assemblePacket, attachFiles, budgetFor, readFileDiff } from './packet.mjs';
|
|
36
|
-
import { FINDING_SCHEMA, minTokensOut,
|
|
36
|
+
import { FINDING_SCHEMA, minTokensOut, validateReview } from './validate-review.mjs';
|
|
37
37
|
import { spawnWithTimeoutRetry } from './session-retry.mjs';
|
|
38
38
|
import { assertRowPath } from '../worker/ticket.mjs';
|
|
39
39
|
|
|
@@ -194,12 +194,11 @@ export async function reviewFile(input, deps) {
|
|
|
194
194
|
if (diff.diffText.trim().length === 0) return { status: 'no_change', approved: false, ...head, sessions: [] };
|
|
195
195
|
mkdirSync(workDir, { recursive: true, mode: 0o700 });
|
|
196
196
|
const min = minTokensOut(cfg);
|
|
197
|
-
const smallDiff = diff.plusCount <= SMALL_DIFF_ADDED_LINES;
|
|
198
197
|
|
|
199
198
|
/** @param {SessionSpec} spec @param {{text: string, hunkHeaders: string[], tokensIn: number, contextMode: string | null}} packet */
|
|
200
199
|
const run = async (spec, packet) => {
|
|
201
200
|
/** @type {Record<string, any>} */
|
|
202
|
-
let summary = await runOne(spec, packet.text, packet, { deps, workDir, min,
|
|
201
|
+
let summary = await runOne(spec, packet.text, packet, { deps, workDir, min, cfg });
|
|
203
202
|
const needs = summary.review?.needs_file;
|
|
204
203
|
if (spec.role === 'reviewer' && summary.status === 'ok' && Array.isArray(needs) && needs.length > 0) {
|
|
205
204
|
const paths = safeNeeds(needs);
|
|
@@ -210,7 +209,7 @@ export async function reviewFile(input, deps) {
|
|
|
210
209
|
extra = null;
|
|
211
210
|
}
|
|
212
211
|
if (extra === null) return { ...summary, status: 'unavailable', reason: 'needs_file-refused', review: null };
|
|
213
|
-
summary = { ...(await runOne(spec, `${packet.text}${extra}`, packet, { deps, workDir, min,
|
|
212
|
+
summary = { ...(await runOne(spec, `${packet.text}${extra}`, packet, { deps, workDir, min, cfg })), needs_file_round: true };
|
|
214
213
|
}
|
|
215
214
|
return summary;
|
|
216
215
|
};
|
|
@@ -234,7 +233,7 @@ export async function reviewFile(input, deps) {
|
|
|
234
233
|
if (judgeSpec) {
|
|
235
234
|
const bySlot = Object.fromEntries(done.map((s) => [s.slot, s.review]));
|
|
236
235
|
const packet = assembleJudgePacket({ diff, reports: { A: bySlot.A, B: bySlot.B }, cfg });
|
|
237
|
-
final = await runOne(judgeSpec, packet.text, packet, { deps, workDir, min,
|
|
236
|
+
final = await runOne(judgeSpec, packet.text, packet, { deps, workDir, min, cfg });
|
|
238
237
|
sessions.push(publicSummary(final));
|
|
239
238
|
if (final.status !== 'ok') {
|
|
240
239
|
await note(deps, { event: 'review.unavailable', reason: final.reason, lens: 'judge' });
|
|
@@ -250,9 +249,9 @@ export async function reviewFile(input, deps) {
|
|
|
250
249
|
* One session: packet file (0600, removed after), spawn, stub guard.
|
|
251
250
|
* @param {SessionSpec} spec @param {string} text
|
|
252
251
|
* @param {{hunkHeaders: string[], tokensIn: number, contextMode: string | null}} packet
|
|
253
|
-
* @param {{deps: ReviewDeps, workDir: string, min: number,
|
|
252
|
+
* @param {{deps: ReviewDeps, workDir: string, min: number, cfg: Record<string, any>}} env
|
|
254
253
|
*/
|
|
255
|
-
async function runOne(spec, text, packet, { deps, workDir, min
|
|
254
|
+
async function runOne(spec, text, packet, { deps, workDir, min }) {
|
|
256
255
|
const promptPath = path.join(workDir, `${spec.lens}-${randomBytes(6).toString('hex')}.md`);
|
|
257
256
|
const base = { lens: spec.lens, role: spec.role, level: spec.level, slot: spec.slot, context_mode: packet.contextMode, ctx_tokens_in: packet.tokensIn };
|
|
258
257
|
writeFileSync(promptPath, text, { mode: 0o600 });
|
|
@@ -285,7 +284,7 @@ async function runOne(spec, text, packet, { deps, workDir, min, smallDiff = fals
|
|
|
285
284
|
} finally {
|
|
286
285
|
rmSync(promptPath, { force: true });
|
|
287
286
|
}
|
|
288
|
-
const verdict = validateReview(res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min
|
|
287
|
+
const verdict = validateReview(res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
289
288
|
const meta = {
|
|
290
289
|
...base,
|
|
291
290
|
provider: res?.provider ?? null,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 1. the process exited 0 (not killed at the session timeout);
|
|
6
6
|
* 2. the answer is a JSON object that validates against `finding.schema.json`;
|
|
7
7
|
* 3. `reviewed_hunks` equals the packet's hunk headers EXACTLY and IN ORDER;
|
|
8
|
-
* 4. `tokens_out ≥ review.min_tokens_out` (120).
|
|
8
|
+
* 4. `tokens_out ≥ review.min_tokens_out` (120), or ≥ 40 for a structured clean pass (B53).
|
|
9
9
|
* Anything else is `unavailable` with one reason — `timeout`, `exit`, `schema`,
|
|
10
10
|
* `hunks_mismatch`, `too_short`, or `budget` (B33: the spawner refused at `budget.usd`) — and is
|
|
11
11
|
* never approval. There is no "approve on missing output":
|
|
@@ -20,13 +20,13 @@ export const UNAVAILABLE_REASONS = Object.freeze(['exit', 'schema', 'hunks_misma
|
|
|
20
20
|
export const DEFAULT_MIN_TOKENS_OUT = 120;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* A clean pass
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* A structured clean pass legitimately needs few words, whatever the diff size: GPT-6 Astra clean
|
|
24
|
+
* passes measured 108-121 output tokens while reviews with findings measured 300-1,400, so the full
|
|
25
|
+
* floor refused 11 of 19 clean files on one block (issue #6). A clean pass is `passed: true`, no
|
|
26
|
+
* findings, every hunk acknowledged in order (checked above) and a non-blank `summary`; for it the
|
|
27
|
+
* floor drops to this value. Any finding, or a blank summary, keeps the full floor (B53).
|
|
26
28
|
*/
|
|
27
29
|
export const CLEAN_PASS_MIN_TOKENS_OUT = 40;
|
|
28
|
-
/** A diff with at most this many added lines is "small" for the clean-pass floor. */
|
|
29
|
-
export const SMALL_DIFF_ADDED_LINES = 20;
|
|
30
30
|
|
|
31
31
|
/** The source answer schema (compiled per provider by the spawner). */
|
|
32
32
|
export const FINDING_SCHEMA = Object.freeze(JSON.parse(readFileSync(new URL('./finding.schema.json', import.meta.url), 'utf8')));
|
|
@@ -55,10 +55,10 @@ export function minTokensOut(cfg) {
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* @param {Record<string, any> | null | undefined} session - a `spawnSession` result.
|
|
58
|
-
* @param {{hunkHeaders: ReadonlyArray<string>, minTokensOut?: number
|
|
58
|
+
* @param {{hunkHeaders: ReadonlyArray<string>, minTokensOut?: number}} expect
|
|
59
59
|
* @returns {Verdict}
|
|
60
60
|
*/
|
|
61
|
-
export function validateReview(session, { hunkHeaders, minTokensOut: min = DEFAULT_MIN_TOKENS_OUT
|
|
61
|
+
export function validateReview(session, { hunkHeaders, minTokensOut: min = DEFAULT_MIN_TOKENS_OUT }) {
|
|
62
62
|
if (!session || typeof session !== 'object') return { ok: false, reason: 'exit', detail: 'no session result' };
|
|
63
63
|
if (session.status === 'timeout') return { ok: false, reason: 'timeout', detail: 'killed at the session timeout' };
|
|
64
64
|
// B33: the spawner refused the session because the run reached `budget.usd` — nothing ran
|
|
@@ -77,9 +77,9 @@ export function validateReview(session, { hunkHeaders, minTokensOut: min = DEFAU
|
|
|
77
77
|
return { ok: false, reason: 'hunks_mismatch', detail: `reviewed_hunks has ${got.length} entries; the packet has ${hunkHeaders.length} hunks` };
|
|
78
78
|
}
|
|
79
79
|
const tokensOut = Number(session.usage?.tokens_out ?? session.row?.tokens_out ?? 0);
|
|
80
|
-
const review = /** @type {{passed: boolean, findings: unknown[]}} */ (session.answer);
|
|
81
|
-
const cleanPass = review.passed === true && Array.isArray(review.findings) && review.findings.length === 0;
|
|
82
|
-
const floor =
|
|
80
|
+
const review = /** @type {{passed: boolean, summary: string, findings: unknown[]}} */ (session.answer);
|
|
81
|
+
const cleanPass = review.passed === true && Array.isArray(review.findings) && review.findings.length === 0 && review.summary.trim().length > 0;
|
|
82
|
+
const floor = cleanPass ? Math.min(min, CLEAN_PASS_MIN_TOKENS_OUT) : min;
|
|
83
83
|
if (!(tokensOut >= floor)) return { ok: false, reason: 'too_short', detail: `tokens_out ${tokensOut} < ${floor}` };
|
|
84
84
|
return { ok: true, review: session.answer, tokens_out: tokensOut };
|
|
85
85
|
}
|