@codedology/code-forge 0.1.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 +64 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/bin/code-forge.mjs +145 -0
- package/docs/README.md +37 -0
- package/docs/reference/blocks.json +288 -0
- package/docs/reference/config.md +162 -0
- package/examples/first-use/README.md +11 -0
- package/examples/first-use/clamp.acceptance.yml +8 -0
- package/examples/first-use/clamp.brief.md +16 -0
- package/examples/first-use/coder/src/math.mjs +31 -0
- package/examples/first-use/coder/test/clamp.test.mjs +26 -0
- package/examples/node-lib/.env.example +3 -0
- package/examples/node-lib/.gitattributes +3 -0
- package/examples/node-lib/.github/workflows/test.yml +11 -0
- package/examples/node-lib/CHANGELOG.md +13 -0
- package/examples/node-lib/README.md +17 -0
- package/examples/node-lib/gitignore +3 -0
- package/examples/node-lib/package.json +19 -0
- package/examples/node-lib/src/list.mjs +23 -0
- package/examples/node-lib/src/math.mjs +21 -0
- package/examples/node-lib/src/strings.mjs +34 -0
- package/examples/node-lib/test/list.test.mjs +12 -0
- package/examples/node-lib/test/math.test.mjs +13 -0
- package/examples/node-lib/test/package.test.mjs +18 -0
- package/examples/node-lib/test/strings.test.mjs +28 -0
- package/package.json +52 -0
- package/schema/code-forge.schema.json +331 -0
- package/schema/facts.schema.json +28 -0
- package/scripts/first-use.mjs +517 -0
- package/scripts/gen-config-doc.mjs +241 -0
- package/skill/SKILL.md +79 -0
- package/skill/references/adapters/claude-code.md +19 -0
- package/skill/references/adapters/solo.md +34 -0
- package/skill/references/adapters/subprocess.md +15 -0
- package/skill/references/code.md +64 -0
- package/skill/references/continuity.md +31 -0
- package/skill/references/decisions.md +50 -0
- package/skill/references/degraded.md +40 -0
- package/skill/references/harden.md +26 -0
- package/skill/references/ledger.md +23 -0
- package/skill/references/plan.md +43 -0
- package/skill/references/proof.md +34 -0
- package/skill/references/review.md +42 -0
- package/skill/references/rules-core.md +72 -0
- package/skill/references/security.md +29 -0
- package/skill/scripts/forge +40 -0
- package/skill/templates/block-record.md +19 -0
- package/skill/templates/facts-sheet.md +22 -0
- package/skill/templates/plan.md +53 -0
- package/src/cli/author.mjs +87 -0
- package/src/cli/block.mjs +262 -0
- package/src/cli/doctor.mjs +74 -0
- package/src/cli/facts.mjs +124 -0
- package/src/cli/gates.mjs +280 -0
- package/src/cli/help.mjs +48 -0
- package/src/cli/init.mjs +14 -0
- package/src/cli/jev.mjs +175 -0
- package/src/cli/keys.mjs +170 -0
- package/src/cli/ledger.mjs +160 -0
- package/src/cli/list.mjs +68 -0
- package/src/cli/models.mjs +131 -0
- package/src/cli/plan.mjs +70 -0
- package/src/cli/proof.mjs +316 -0
- package/src/cli/remove.mjs +76 -0
- package/src/cli/report.mjs +73 -0
- package/src/cli/resolve.mjs +79 -0
- package/src/cli/review-file.mjs +152 -0
- package/src/cli/run.mjs +159 -0
- package/src/cli/s2.mjs +93 -0
- package/src/cli/spawn.mjs +104 -0
- package/src/cli/upgrade.mjs +103 -0
- package/src/cli/validate.mjs +108 -0
- package/src/cli/version.mjs +18 -0
- package/src/cli/worker.mjs +76 -0
- package/src/config/defaults/anthropic.mjs +18 -0
- package/src/config/defaults/index.mjs +24 -0
- package/src/config/defaults/openai.mjs +16 -0
- package/src/config/defaults/xai.mjs +18 -0
- package/src/config/known-ids.mjs +171 -0
- package/src/config/load.mjs +135 -0
- package/src/config/migrate.mjs +46 -0
- package/src/config/refresh.mjs +276 -0
- package/src/config/schema-compile.mjs +211 -0
- package/src/config/schema-paths.mjs +84 -0
- package/src/config/secret-patterns.mjs +85 -0
- package/src/config/validate.mjs +694 -0
- package/src/decide/author-loop.mjs +90 -0
- package/src/decide/escalation.mjs +262 -0
- package/src/decide/fallback-rules.mjs +172 -0
- package/src/decide/jev-client.mjs +216 -0
- package/src/decide/questions.mjs +161 -0
- package/src/decide/thresholds.mjs +199 -0
- package/src/doctor/index.mjs +105 -0
- package/src/doctor/local.mjs +162 -0
- package/src/doctor/probes.mjs +358 -0
- package/src/doctor/rows.mjs +51 -0
- package/src/doctor/worker-probe.mjs +125 -0
- package/src/engines/alias-map.mjs +78 -0
- package/src/engines/builders/claude.mjs +224 -0
- package/src/engines/builders/codex.mjs +242 -0
- package/src/engines/builders/grok.mjs +132 -0
- package/src/engines/builders/index.mjs +35 -0
- package/src/engines/builders/validate-params.mjs +34 -0
- package/src/engines/codex-home.mjs +230 -0
- package/src/engines/isolation.mjs +84 -0
- package/src/engines/probe.mjs +116 -0
- package/src/engines/provider-cli.mjs +24 -0
- package/src/engines/render-rules.mjs +41 -0
- package/src/engines/sentinel.mjs +133 -0
- package/src/engines/usage-parse.mjs +149 -0
- package/src/gates/acceptance.mjs +95 -0
- package/src/gates/detect.mjs +187 -0
- package/src/gates/invariants.mjs +43 -0
- package/src/gates/run.mjs +72 -0
- package/src/gates/safe-edit.mjs +266 -0
- package/src/gates/scope.mjs +154 -0
- package/src/gates/secret-scan.mjs +79 -0
- package/src/gates/transcript-grep.mjs +27 -0
- package/src/install/agent-env.mjs +76 -0
- package/src/install/detect.mjs +121 -0
- package/src/install/harnesses.mjs +131 -0
- package/src/install/link.mjs +463 -0
- package/src/install/wizard/answers.mjs +389 -0
- package/src/install/wizard/flags.mjs +248 -0
- package/src/install/wizard/project-file.mjs +128 -0
- package/src/install/wizard/run.mjs +366 -0
- package/src/install/wizard/steps.mjs +201 -0
- package/src/keys/backends/constants.mjs +7 -0
- package/src/keys/backends/file.mjs +99 -0
- package/src/keys/backends/keychain.mjs +81 -0
- package/src/keys/backends/security-cli.mjs +105 -0
- package/src/keys/onepassword.mjs +46 -0
- package/src/keys/store.mjs +347 -0
- package/src/ledger/calibration.mjs +109 -0
- package/src/ledger/export.mjs +52 -0
- package/src/ledger/outcome.mjs +397 -0
- package/src/ledger/paths.mjs +30 -0
- package/src/ledger/prices.mjs +49 -0
- package/src/ledger/report.mjs +250 -0
- package/src/ledger/write.mjs +279 -0
- package/src/proof/export.mjs +358 -0
- package/src/proof/lock.mjs +138 -0
- package/src/proof/red-green.mjs +519 -0
- package/src/proof/red-parse.mjs +103 -0
- package/src/proof/tiers.mjs +62 -0
- package/src/review/budget.mjs +241 -0
- package/src/review/context.mjs +156 -0
- package/src/review/engine.mjs +264 -0
- package/src/review/finding.schema.json +47 -0
- package/src/review/fixloop.mjs +467 -0
- package/src/review/gate-check.mjs +249 -0
- package/src/review/lenses/A.md +22 -0
- package/src/review/lenses/B.md +23 -0
- package/src/review/lenses/full.md +23 -0
- package/src/review/lenses/judge.md +21 -0
- package/src/review/lenses/quick.md +21 -0
- package/src/review/lenses/recheck.md +23 -0
- package/src/review/packet.mjs +355 -0
- package/src/review/triage.mjs +149 -0
- package/src/review/validate-review.mjs +70 -0
- package/src/session/author.mjs +228 -0
- package/src/session/facts.mjs +855 -0
- package/src/session/plan-check.mjs +294 -0
- package/src/session/s2.mjs +164 -0
- package/src/session/spawn.mjs +610 -0
- package/src/state/block.mjs +289 -0
- package/src/state/cli-args.mjs +67 -0
- package/src/state/paths.mjs +92 -0
- package/src/state/registry.mjs +189 -0
- package/src/state/run.mjs +266 -0
- package/src/state/signer.mjs +132 -0
- package/src/util/exec.mjs +315 -0
- package/src/util/forbidden.mjs +787 -0
- package/src/util/git.mjs +381 -0
- package/src/util/log.mjs +100 -0
- package/src/util/reaper.mjs +300 -0
- package/src/util/redact.mjs +212 -0
- package/src/util/tmp.mjs +409 -0
- package/src/worker/engine.mjs +338 -0
- package/src/worker/loop.mjs +392 -0
- package/src/worker/queue.mjs +186 -0
- package/src/worker/review-state.mjs +145 -0
- package/src/worker/ticket.mjs +195 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review engine (plan §4.2 adaptive, §4.4 consensus, §4.3 isolation; block B12a).
|
|
3
|
+
*
|
|
4
|
+
* `reviewFile(input, deps)` reviews ONE file at its current content:
|
|
5
|
+
* 1. plans the sessions (`planSessions`):
|
|
6
|
+
* adaptive (default) — `risk < 1` ⇒ one L2 `quick`; `1 ≤ risk < 2` ⇒ one L2 `full`;
|
|
7
|
+
* `risk ≥ 2` ⇒ two blind L2 sessions (lens `A`, lens `B`) + one L3 `judge`;
|
|
8
|
+
* consensus (`review.multimodel: true`) — one L2 `full` per EFFECTIVE provider
|
|
9
|
+
* (`resolve(L2)` and `review.second_levels.L2` / `review.second_provider`) + one L3 judge;
|
|
10
|
+
* the same effective provider twice is refused (`consensus-same-provider`);
|
|
11
|
+
* 2. reads the diff once and assembles one packet per lens (`packet.mjs`);
|
|
12
|
+
* 3. runs every session through `deps.spawn` — a fresh closed-book process in an empty cwd with
|
|
13
|
+
* the packet on stdin (`spawnSession`, B9a; `--safe-mode --tools ""` from the B4 builder);
|
|
14
|
+
* 4. passes each answer through the stub guard (`validate-review.mjs`); an answer that fails it
|
|
15
|
+
* is `unavailable` with its reason and is NEVER approval; a failed lens means no judge;
|
|
16
|
+
* 5. a valid answer with `needs_file` gets ONE more round with those files attached (git-tracked
|
|
17
|
+
* files only, never secret-like paths — `attachFiles`); a second `needs_file` is ignored; a
|
|
18
|
+
* request with a non-repo-relative path is refused (`needs_file-refused`, never approval).
|
|
19
|
+
* Approval (until B12b's triage lands) = the final answer (the judge's in dual/consensus mode, else
|
|
20
|
+
* the single reviewer's) passed the guard, says `passed: true`, and carries no `critical` or
|
|
21
|
+
* `warning` finding.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { randomBytes } from 'node:crypto';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { resolveLevel } from '../config/known-ids.mjs';
|
|
28
|
+
import { fallbackRisk } from '../decide/fallback-rules.mjs';
|
|
29
|
+
import { tierFor } from '../proof/tiers.mjs';
|
|
30
|
+
import { assembleJudgePacket, assemblePacket, attachFiles, budgetFor, readFileDiff } from './packet.mjs';
|
|
31
|
+
import { FINDING_SCHEMA, minTokensOut, validateReview } from './validate-review.mjs';
|
|
32
|
+
import { assertRowPath } from '../worker/ticket.mjs';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {{lens: 'quick' | 'full' | 'A' | 'B' | 'judge', role: 'reviewer' | 'judge', level: 'L2' | 'L3', slot: 'A' | 'B' | null, cfg?: Record<string, any>}} SessionSpec -
|
|
36
|
+
* `cfg`: a per-session config (the consensus second reviewer: `levels.L2` = its effective level).
|
|
37
|
+
* @typedef {{mode: 'adaptive' | 'consensus', depth: 'quick' | 'full' | 'dual' | 'consensus' | null, sessions: SessionSpec[], refused?: string}} SessionPlan
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The second reviewer's effective L2 level for consensus mode.
|
|
42
|
+
* @param {Record<string, any>} cfg
|
|
43
|
+
* @returns {{ok: boolean, level?: Record<string, any>, primary?: string, reason?: string}}
|
|
44
|
+
*/
|
|
45
|
+
export function secondLevel(cfg) {
|
|
46
|
+
let primary;
|
|
47
|
+
try {
|
|
48
|
+
primary = resolveLevel(cfg, 'L2').provider;
|
|
49
|
+
} catch {
|
|
50
|
+
return { ok: false, reason: 'consensus-no-l2' };
|
|
51
|
+
}
|
|
52
|
+
const override = cfg?.review?.second_levels?.L2 ?? null;
|
|
53
|
+
const provider = override?.provider ?? cfg?.review?.second_provider ?? null;
|
|
54
|
+
if (typeof provider !== 'string' || provider.length === 0) return { ok: false, reason: 'consensus-no-second-provider' };
|
|
55
|
+
if (provider === primary) return { ok: false, reason: 'consensus-same-provider' };
|
|
56
|
+
if (typeof override?.model !== 'string' || override.model.length === 0) return { ok: false, reason: 'consensus-no-second-model' };
|
|
57
|
+
// the second reviewer's fallback ladder never lands on the first reviewer's provider
|
|
58
|
+
const fallback = (Array.isArray(override.fallback) ? override.fallback : []).filter((/** @type {any} */ f) => f?.provider !== primary);
|
|
59
|
+
return { ok: true, level: { ...override, provider, fallback }, primary };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {{risk: number, cfg: Record<string, any>}} opts
|
|
64
|
+
* @returns {SessionPlan}
|
|
65
|
+
*/
|
|
66
|
+
export function planSessions({ risk, cfg }) {
|
|
67
|
+
if (typeof risk !== 'number' || !Number.isFinite(risk) || risk < 0 || risk > 3) throw new TypeError('planSessions: risk must be a number from 0 to 3');
|
|
68
|
+
const judge = /** @type {SessionSpec} */ ({ lens: 'judge', role: 'judge', level: 'L3', slot: null });
|
|
69
|
+
if (cfg?.review?.multimodel === true) {
|
|
70
|
+
const second = secondLevel(cfg);
|
|
71
|
+
if (!second.ok) return { mode: 'consensus', depth: null, sessions: [], refused: second.reason };
|
|
72
|
+
return {
|
|
73
|
+
mode: 'consensus',
|
|
74
|
+
depth: 'consensus',
|
|
75
|
+
sessions: [
|
|
76
|
+
{ lens: 'full', role: 'reviewer', level: 'L2', slot: 'A' },
|
|
77
|
+
{ lens: 'full', role: 'reviewer', level: 'L2', slot: 'B', cfg: { ...cfg, levels: { ...cfg.levels, L2: second.level } } },
|
|
78
|
+
judge,
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (risk < 1) return { mode: 'adaptive', depth: 'quick', sessions: [{ lens: 'quick', role: 'reviewer', level: 'L2', slot: null }] };
|
|
83
|
+
if (risk < 2) return { mode: 'adaptive', depth: 'full', sessions: [{ lens: 'full', role: 'reviewer', level: 'L2', slot: null }] };
|
|
84
|
+
return {
|
|
85
|
+
mode: 'adaptive',
|
|
86
|
+
depth: 'dual',
|
|
87
|
+
sessions: [
|
|
88
|
+
{ lens: 'A', role: 'reviewer', level: 'L2', slot: 'A' },
|
|
89
|
+
{ lens: 'B', role: 'reviewer', level: 'L2', slot: 'B' },
|
|
90
|
+
judge,
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The rules risk (§3.5 fallback; S1 `risk` without Jev): a `proof.tiers.high.paths` match ⇒ 3,
|
|
97
|
+
* a migration/policy/middleware path ⇒ 2, more than 200 added lines ⇒ 1, else 0.
|
|
98
|
+
* @param {{file: string, plusCount: number, cfg: Record<string, any>}} opts
|
|
99
|
+
* @returns {number}
|
|
100
|
+
*/
|
|
101
|
+
export function rulesRisk({ file, plusCount, cfg }) {
|
|
102
|
+
const highPaths = cfg?.proof?.tiers?.high?.paths;
|
|
103
|
+
const pathFloorHit = tierFor({ file, risk: 0, highPaths: Array.isArray(highPaths) ? highPaths : [] }).reason === 'path';
|
|
104
|
+
return fallbackRisk({
|
|
105
|
+
pathFloorHit,
|
|
106
|
+
filesChanged: 1,
|
|
107
|
+
linesAdded: plusCount,
|
|
108
|
+
touchesMigration: /migration/i.test(file),
|
|
109
|
+
touchesPolicyOrMiddleware: /polic(y|ies)|middleware/i.test(file),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @typedef {object} ReviewInput
|
|
115
|
+
* @property {string} repoRoot - realpath'd repository root.
|
|
116
|
+
* @property {string} file - repo-root-relative.
|
|
117
|
+
* @property {string | null} [base] - the block's base SHA (null ⇒ HEAD).
|
|
118
|
+
* @property {Record<string, any>} cfg
|
|
119
|
+
* @property {number} [risk] - the S1 risk (0–3); absent ⇒ the rules risk.
|
|
120
|
+
* @property {string} [rulesDigest] @property {string} [factsExcerpt]
|
|
121
|
+
* @property {string} workDir - where packet files are written (under the run's temp root).
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @typedef {object} ReviewDeps
|
|
126
|
+
* @property {(opts: Record<string, any>) => Promise<Record<string, any>>} spawn - one isolated
|
|
127
|
+
* session (`spawnSession` opts minus the worker-pinned ones); a per-call `cfg` selects another
|
|
128
|
+
* provider's level for the same slot (consensus).
|
|
129
|
+
* @property {(row: Record<string, any>) => Promise<unknown>} [writeRow] - ledger rows
|
|
130
|
+
* (`review.plan`, `review.unavailable`); a failed write never fails the review.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
/** @param {ReviewDeps} deps @param {Record<string, any>} row */
|
|
134
|
+
async function note(deps, row) {
|
|
135
|
+
try {
|
|
136
|
+
await deps.writeRow?.(row);
|
|
137
|
+
} catch {
|
|
138
|
+
// the review result stands; the ledger row is best-effort
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The `needs_file` request of a model answer, or null when any entry is not a plain
|
|
144
|
+
* repo-relative path (absolute, `..`, `.git/`, `.code-forge/`, not a string).
|
|
145
|
+
* @param {unknown} needs @returns {string[] | null}
|
|
146
|
+
*/
|
|
147
|
+
function safeNeeds(needs) {
|
|
148
|
+
if (!Array.isArray(needs)) return null;
|
|
149
|
+
try {
|
|
150
|
+
return needs.map((p) => assertRowPath(p));
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {ReviewInput} input @param {ReviewDeps} deps
|
|
158
|
+
* @returns {Promise<{status: string, approved: boolean, engine: string, sessions: Array<Record<string, any>>} & Record<string, any>>}
|
|
159
|
+
*/
|
|
160
|
+
export async function reviewFile(input, deps) {
|
|
161
|
+
const { repoRoot, cfg, workDir } = input;
|
|
162
|
+
const diff = await readFileDiff({ repoRoot, file: input.file, base: input.base ?? null });
|
|
163
|
+
const risk = input.risk ?? rulesRisk({ file: diff.file, plusCount: diff.plusCount, cfg });
|
|
164
|
+
const plan = planSessions({ risk, cfg });
|
|
165
|
+
const head = { engine: plan.mode, depth: plan.depth, risk, file: diff.file };
|
|
166
|
+
if (plan.refused) return { status: 'refused', reason: plan.refused, approved: false, ...head, sessions: [] };
|
|
167
|
+
if (diff.diffText.trim().length === 0) return { status: 'no_change', approved: false, ...head, sessions: [] };
|
|
168
|
+
mkdirSync(workDir, { recursive: true, mode: 0o700 });
|
|
169
|
+
const min = minTokensOut(cfg);
|
|
170
|
+
|
|
171
|
+
/** @param {SessionSpec} spec @param {{text: string, hunkHeaders: string[], tokensIn: number, contextMode: string | null}} packet */
|
|
172
|
+
const run = async (spec, packet) => {
|
|
173
|
+
/** @type {Record<string, any>} */
|
|
174
|
+
let summary = await runOne(spec, packet.text, packet, { deps, workDir, min, cfg });
|
|
175
|
+
const needs = summary.review?.needs_file;
|
|
176
|
+
if (spec.role === 'reviewer' && summary.status === 'ok' && Array.isArray(needs) && needs.length > 0) {
|
|
177
|
+
const paths = safeNeeds(needs);
|
|
178
|
+
let extra = null;
|
|
179
|
+
try {
|
|
180
|
+
extra = paths ? await attachFiles({ repoRoot, paths, budgetTokens: budgetFor(cfg, 'full_in') }) : null;
|
|
181
|
+
} catch {
|
|
182
|
+
extra = null;
|
|
183
|
+
}
|
|
184
|
+
if (extra === null) return { ...summary, status: 'unavailable', reason: 'needs_file-refused', review: null };
|
|
185
|
+
summary = { ...(await runOne(spec, `${packet.text}${extra}`, packet, { deps, workDir, min, cfg })), needs_file_round: true };
|
|
186
|
+
}
|
|
187
|
+
return summary;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const reviewers = plan.sessions.filter((s) => s.role === 'reviewer');
|
|
191
|
+
const packets = reviewers.map((spec) => assemblePacket({ diff, lens: spec.lens, rulesDigest: input.rulesDigest, factsExcerpt: input.factsExcerpt, cfg }));
|
|
192
|
+
const split = packets.find((p) => p.status === 'split_required');
|
|
193
|
+
if (split) return { status: 'split_required', approved: false, ...head, tokens_in: split.tokensIn, budget: split.budget, sessions: [] };
|
|
194
|
+
const firstPacket = /** @type {any} */ (packets[0]);
|
|
195
|
+
await note(deps, { event: 'review.plan', depth_unconstrained: plan.depth, depth_chosen: plan.depth, degrade_step: 0, risk, context_mode: firstPacket?.contextMode ?? null, forecast_tokens: packets.reduce((n, p) => n + p.tokensIn, 0) });
|
|
196
|
+
const done = await Promise.all(reviewers.map((spec, i) => run(spec, /** @type {any} */ (packets[i]))));
|
|
197
|
+
const sessions = done.map(publicSummary);
|
|
198
|
+
const failed = done.find((s) => s.status !== 'ok');
|
|
199
|
+
if (failed) {
|
|
200
|
+
await note(deps, { event: 'review.unavailable', reason: failed.reason, lens: failed.lens });
|
|
201
|
+
return { status: 'unavailable', reason: failed.reason, approved: false, ...head, sessions };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let final = done[0];
|
|
205
|
+
const judgeSpec = plan.sessions.find((s) => s.role === 'judge');
|
|
206
|
+
if (judgeSpec) {
|
|
207
|
+
const bySlot = Object.fromEntries(done.map((s) => [s.slot, s.review]));
|
|
208
|
+
const packet = assembleJudgePacket({ diff, reports: { A: bySlot.A, B: bySlot.B }, cfg });
|
|
209
|
+
final = await runOne(judgeSpec, packet.text, packet, { deps, workDir, min, cfg });
|
|
210
|
+
sessions.push(publicSummary(final));
|
|
211
|
+
if (final.status !== 'ok') {
|
|
212
|
+
await note(deps, { event: 'review.unavailable', reason: final.reason, lens: 'judge' });
|
|
213
|
+
return { status: 'unavailable', reason: final.reason, approved: false, ...head, sessions };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const findings = final.review.findings;
|
|
217
|
+
const approved = final.review.passed === true && findings.every((/** @type {any} */ f) => f.severity === 'nit');
|
|
218
|
+
return { status: 'reviewed', approved, ...head, summary: final.review.summary, findings, sessions };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* One session: packet file (0600, removed after), spawn, stub guard.
|
|
223
|
+
* @param {SessionSpec} spec @param {string} text
|
|
224
|
+
* @param {{hunkHeaders: string[], tokensIn: number, contextMode: string | null}} packet
|
|
225
|
+
* @param {{deps: ReviewDeps, workDir: string, min: number, cfg: Record<string, any>}} env
|
|
226
|
+
*/
|
|
227
|
+
async function runOne(spec, text, packet, { deps, workDir, min }) {
|
|
228
|
+
const promptPath = path.join(workDir, `${spec.lens}-${randomBytes(6).toString('hex')}.md`);
|
|
229
|
+
const base = { lens: spec.lens, role: spec.role, level: spec.level, slot: spec.slot, context_mode: packet.contextMode, ctx_tokens_in: packet.tokensIn };
|
|
230
|
+
writeFileSync(promptPath, text, { mode: 0o600 });
|
|
231
|
+
/** @type {Record<string, any> | null} */
|
|
232
|
+
let res = null;
|
|
233
|
+
try {
|
|
234
|
+
res = await deps.spawn({
|
|
235
|
+
level: spec.level,
|
|
236
|
+
role: spec.role,
|
|
237
|
+
promptPath,
|
|
238
|
+
schema: FINDING_SCHEMA,
|
|
239
|
+
rowExtra: { lens: spec.lens, context_mode: packet.contextMode, ctx_tokens_in: packet.tokensIn },
|
|
240
|
+
...(spec.cfg ? { cfg: spec.cfg } : {}),
|
|
241
|
+
});
|
|
242
|
+
} catch {
|
|
243
|
+
res = null; // a spawn that throws (bad config, refused argv) is an `exit` failure, never approval
|
|
244
|
+
} finally {
|
|
245
|
+
rmSync(promptPath, { force: true });
|
|
246
|
+
}
|
|
247
|
+
const verdict = validateReview(res, { hunkHeaders: packet.hunkHeaders, minTokensOut: min });
|
|
248
|
+
const meta = {
|
|
249
|
+
...base,
|
|
250
|
+
provider: res?.provider ?? null,
|
|
251
|
+
model: res?.model ?? null,
|
|
252
|
+
fallback_step: res?.fallback_step ?? 0,
|
|
253
|
+
tokens_in: res?.usage?.tokens_in ?? null,
|
|
254
|
+
tokens_out: res?.usage?.tokens_out ?? null,
|
|
255
|
+
};
|
|
256
|
+
if (!verdict.ok) return { ...meta, status: 'unavailable', reason: verdict.reason, review: null };
|
|
257
|
+
return { ...meta, status: 'ok', reason: null, review: verdict.review };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The per-session summary the worker signs: the answer's verdict and findings, never the packet. */
|
|
261
|
+
function publicSummary(/** @type {Record<string, any>} */ s) {
|
|
262
|
+
const { review, ...rest } = s;
|
|
263
|
+
return review ? { ...rest, passed: review.passed === true, findings: review.findings } : rest;
|
|
264
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://code-forge.local/schema/review/finding.schema.json",
|
|
4
|
+
"title": "code-forge review answer (plan §4.2 finding schema)",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["passed", "summary", "reviewed_hunks", "findings"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"passed": { "type": "boolean" },
|
|
10
|
+
"summary": { "type": "string", "maxLength": 300 },
|
|
11
|
+
"reviewed_hunks": { "type": "array", "items": { "type": "string" } },
|
|
12
|
+
"findings": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"required": ["id", "file", "line_start", "line_end", "severity", "category", "claim", "evidence", "fix"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": { "type": "string", "minLength": 1, "maxLength": 64 },
|
|
20
|
+
"file": { "type": "string" },
|
|
21
|
+
"line_start": { "type": "integer", "minimum": 0 },
|
|
22
|
+
"line_end": { "type": "integer", "minimum": 0 },
|
|
23
|
+
"severity": { "type": "string", "enum": ["critical", "warning", "nit"] },
|
|
24
|
+
"category": { "type": "string", "maxLength": 64 },
|
|
25
|
+
"claim": { "type": "string", "maxLength": 300 },
|
|
26
|
+
"evidence": { "type": "string", "maxLength": 300 },
|
|
27
|
+
"fix": { "type": "string", "maxLength": 300 },
|
|
28
|
+
"agreement": { "type": "string", "enum": ["both", "A-only", "B-only"] }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"resolved": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["id", "resolved", "why"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"id": { "type": "string" },
|
|
40
|
+
"resolved": { "type": "boolean" },
|
|
41
|
+
"why": { "type": "string", "maxLength": 200 }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"needs_file": { "type": "array", "items": { "type": "string" } }
|
|
46
|
+
}
|
|
47
|
+
}
|