@codedology/code-forge 0.3.2 → 0.5.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 +85 -0
- package/README.md +88 -21
- package/bin/code-forge.mjs +6 -1
- package/docs/README.md +5 -4
- package/docs/reference/blocks.json +236 -1
- package/docs/reference/config.md +13 -1
- package/package.json +6 -2
- package/schema/code-forge.schema.json +26 -4
- package/skill/SKILL.md +8 -7
- package/skill/references/adapters/claude-code.md +5 -1
- package/skill/references/adapters/solo.md +1 -1
- package/skill/references/adapters/subprocess.md +1 -1
- package/skill/references/code.md +4 -3
- package/skill/references/continuity.md +4 -0
- package/skill/references/decisions.md +5 -2
- package/skill/references/ledger.md +10 -2
- package/skill/references/plan.md +12 -3
- package/skill/references/review.md +13 -6
- package/skill/references/security.md +3 -1
- package/skill/scripts/forge +1 -1
- package/skill/templates/coder-brief.md +36 -0
- package/skill/templates/plan.md +2 -0
- package/src/cli/block.mjs +51 -8
- package/src/cli/facts.mjs +16 -1
- package/src/cli/jev.mjs +114 -17
- package/src/cli/ledger.mjs +99 -8
- package/src/cli/logs.mjs +193 -58
- package/src/cli/plan.mjs +27 -11
- package/src/cli/report.mjs +15 -1
- package/src/cli/resolve.mjs +16 -1
- package/src/cli/review-file.mjs +10 -5
- package/src/cli/review.mjs +40 -9
- package/src/cli/run.mjs +109 -8
- package/src/config/closed-book.mjs +75 -0
- package/src/config/validate.mjs +128 -7
- package/src/decide/escalation.mjs +112 -11
- package/src/decide/questions.mjs +27 -4
- package/src/doctor/index.mjs +2 -1
- package/src/doctor/local.mjs +15 -0
- package/src/doctor/probes.mjs +48 -6
- package/src/engines/builders/claude.mjs +4 -1
- package/src/engines/builders/codex.mjs +26 -11
- package/src/engines/builders/grok.mjs +3 -3
- package/src/engines/efforts.mjs +109 -0
- package/src/engines/sentinel.mjs +34 -0
- package/src/keys/onepassword.mjs +3 -0
- package/src/ledger/report.mjs +66 -29
- package/src/ledger/spend.mjs +288 -0
- package/src/review/engine.mjs +57 -16
- package/src/review/fixloop.mjs +29 -6
- package/src/review/session-retry.mjs +69 -0
- package/src/review/triage.mjs +26 -1
- package/src/review/validate-review.mjs +6 -3
- package/src/session/author.mjs +6 -2
- package/src/session/facts.mjs +218 -53
- package/src/session/plan-check.mjs +71 -6
- package/src/session/plan-sections.mjs +98 -0
- package/src/session/spawn.mjs +373 -20
- package/src/state/block.mjs +13 -4
- package/src/state/config-snapshot.mjs +150 -0
- package/src/state/run.mjs +86 -0
- package/src/util/breadcrumbs.mjs +145 -0
- package/src/util/error-hint.mjs +3 -2
- package/src/util/error-kind.mjs +2 -2
- package/src/util/error-log.mjs +109 -18
- package/src/util/forbidden.mjs +3 -1
- package/src/util/known-fixes.json +1 -0
- package/src/util/known-fixes.mjs +153 -0
- package/src/util/locks.mjs +104 -0
- package/src/worker/engine.mjs +122 -40
- package/src/worker/loop.mjs +326 -78
- package/src/worker/queue.mjs +68 -7
package/src/review/fixloop.mjs
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* round (`review.late_findings: sweep`, default; `block` makes it an open finding instead).
|
|
15
15
|
* 3. Strictly shrinking: open(n) ≥ open(n−1) ⇒ `trigger: review_stall` ⇒ +1 level now (rule 2b).
|
|
16
16
|
* 4. Ladder: `escalation.review_rounds_per_level` (2) exhausted with an open finding ⇒ +1 level
|
|
17
|
-
* (rule 2); the next rounds are coded at that level by a fresh session (`deps.fix`).
|
|
17
|
+
* (rule 2); the next rounds are coded at that level by a fresh session (`deps.fix`). Below L2,
|
|
18
|
+
* `escalation.after_rounds_with_warnings` (1) heavy rounds at the level — an open set with
|
|
19
|
+
* ≥ `escalation.warning_threshold` (2) warnings or any `critical` — climb at once (rule 2c,
|
|
20
|
+
* B34: `trigger: review_warnings`); `warn_rounds_at_level` counts them and resets on a climb.
|
|
18
21
|
* 5. Cap: `review.max_rounds_per_file` (4) reached with an open finding ⇒ the L3 patch rung
|
|
19
22
|
* (rule 6; once per block — shared with rule 5's ceiling), whose `patch_check` round is
|
|
20
23
|
* OUTSIDE the cap. The rung is also rule 5: rule 2/2b at the running ceiling (L2 — L3 is never
|
|
@@ -49,13 +52,14 @@
|
|
|
49
52
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
50
53
|
import { randomBytes } from 'node:crypto';
|
|
51
54
|
import path from 'node:path';
|
|
52
|
-
import { escalationAfterAttempt, reviewRoundStalled } from '../decide/escalation.mjs';
|
|
55
|
+
import { escalationAfterAttempt, heavyRound, reviewRoundStalled } from '../decide/escalation.mjs';
|
|
53
56
|
import { exec } from '../util/exec.mjs';
|
|
54
57
|
import { contentHash, gitChildEnv } from '../worker/ticket.mjs';
|
|
55
58
|
import { parseDiff } from './context.mjs';
|
|
56
59
|
import { assemblePacket, assertPacketPath, readFileDiff } from './packet.mjs';
|
|
57
60
|
import { cameFromJudge, resolvedBand, triageFindings } from './triage.mjs';
|
|
58
61
|
import { FINDING_SCHEMA, minTokensOut, validateReview } from './validate-review.mjs';
|
|
62
|
+
import { spawnWithTimeoutRetry } from './session-retry.mjs';
|
|
59
63
|
|
|
60
64
|
/** @typedef {import('./triage.mjs').Finding} Finding */
|
|
61
65
|
/** @typedef {import('./context.mjs').Hunk} Hunk */
|
|
@@ -74,6 +78,7 @@ import { FINDING_SCHEMA, minTokensOut, validateReview } from './validate-review.
|
|
|
74
78
|
* @property {string} level - the level the file's fixes are coded at now.
|
|
75
79
|
* @property {number} round - rounds counted so far (patch_check rounds excluded).
|
|
76
80
|
* @property {number} rounds_at_level
|
|
81
|
+
* @property {number} [warn_rounds_at_level] - heavy rounds at the current level (rule 2c, B34); absent in a state saved before B34 ⇒ 0.
|
|
77
82
|
* @property {Finding[]} open - the open `fix_now` set.
|
|
78
83
|
* @property {Finding[]} late
|
|
79
84
|
* @property {string | null} reviewed_content - the content the last counted round reviewed.
|
|
@@ -119,6 +124,8 @@ function settings(cfg) {
|
|
|
119
124
|
scope: r.recheck_scope === 'file' ? 'file' : 'fix_hunks',
|
|
120
125
|
late: r.late_findings === 'block' ? 'block' : 'sweep',
|
|
121
126
|
perLevel: int(e.review_rounds_per_level, 2),
|
|
127
|
+
warnAfter: int(e.after_rounds_with_warnings, 1),
|
|
128
|
+
warnThreshold: e.warning_threshold, // heavyRound falls back to 2 for anything but an integer ≥ 1
|
|
122
129
|
stopAt: typeof e.stop_at === 'string' ? e.stop_at : 'L3',
|
|
123
130
|
l3Mode: e.l3_mode === 'code' ? 'code' : 'patch',
|
|
124
131
|
};
|
|
@@ -129,7 +136,7 @@ function settings(cfg) {
|
|
|
129
136
|
* @returns {FileState}
|
|
130
137
|
*/
|
|
131
138
|
export function newFileState({ file, level, l3RungUsed = false }) {
|
|
132
|
-
return { file, level, round: 0, rounds_at_level: 0, open: [], late: [], reviewed_content: null, l3_rung_used: l3RungUsed, status: 'open', next: null, last_packet: null, pending_kind: null };
|
|
139
|
+
return { file, level, round: 0, rounds_at_level: 0, warn_rounds_at_level: 0, open: [], late: [], reviewed_content: null, l3_rung_used: l3RungUsed, status: 'open', next: null, last_packet: null, pending_kind: null };
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
/** @param {string} repoRoot @param {string} file @returns {{rel: string, content: string, hash: string}} */
|
|
@@ -209,11 +216,18 @@ async function recheckSession(packet, deps) {
|
|
|
209
216
|
/** @type {Record<string, any> | null} */
|
|
210
217
|
let res = null;
|
|
211
218
|
try {
|
|
219
|
+
// a `timeout` is spawned once more on the same packet and level (B30)
|
|
212
220
|
res = deps.spawn
|
|
213
|
-
?
|
|
221
|
+
? (
|
|
222
|
+
await spawnWithTimeoutRetry(
|
|
223
|
+
deps.spawn,
|
|
224
|
+
{ level: 'L2', role: 'reviewer', promptPath, schema: FINDING_SCHEMA, rowExtra: { lens: 'recheck', context_mode: packet.contextMode, ctx_tokens_in: packet.tokensIn } },
|
|
225
|
+
(row) => note(deps, row),
|
|
226
|
+
)
|
|
227
|
+
).res
|
|
214
228
|
: null;
|
|
215
229
|
} catch {
|
|
216
|
-
res = null;
|
|
230
|
+
res = null; // the helper never throws; if it ever did, it is still an `exit` failure
|
|
217
231
|
} finally {
|
|
218
232
|
rmSync(promptPath, { force: true });
|
|
219
233
|
}
|
|
@@ -246,12 +260,17 @@ function decideNext(state, openBefore, s, kind) {
|
|
|
246
260
|
openFixNowFinding: true,
|
|
247
261
|
reviewRoundsAtLevel: state.rounds_at_level,
|
|
248
262
|
reviewRoundsPerLevel: s.perLevel,
|
|
263
|
+
warningRoundsAtLevel: state.warn_rounds_at_level ?? 0,
|
|
264
|
+
afterRoundsWithWarnings: s.warnAfter,
|
|
249
265
|
stopAt: s.stopAt,
|
|
250
266
|
l3Mode: /** @type {'patch' | 'code'} */ (s.l3Mode),
|
|
251
267
|
l3RungAlreadyUsed: state.l3_rung_used,
|
|
252
268
|
});
|
|
253
269
|
if (esc.action === 'escalate' && esc.level) {
|
|
254
|
-
if (esc.level !== state.level)
|
|
270
|
+
if (esc.level !== state.level) {
|
|
271
|
+
state.rounds_at_level = 0;
|
|
272
|
+
state.warn_rounds_at_level = 0;
|
|
273
|
+
}
|
|
255
274
|
state.level = esc.level;
|
|
256
275
|
return { action: 'fix', level: state.level, trigger: esc.trigger };
|
|
257
276
|
}
|
|
@@ -390,10 +409,13 @@ export async function runRound(state, deps, opts = {}) {
|
|
|
390
409
|
if (kind !== 'patch_check') {
|
|
391
410
|
draft.round += 1;
|
|
392
411
|
draft.rounds_at_level += 1;
|
|
412
|
+
// THIS round's open set, after triage: only the warnings still open count
|
|
413
|
+
if (heavyRound(draft.open, s.warnThreshold)) draft.warn_rounds_at_level = (draft.warn_rounds_at_level ?? 0) + 1;
|
|
393
414
|
} else {
|
|
394
415
|
// the block continues at L2 after the rung with a fresh round counter (§3.6) — also here, not
|
|
395
416
|
// only in `converge`, because the worker runs the patch_check from its own ticket
|
|
396
417
|
draft.rounds_at_level = 0;
|
|
418
|
+
draft.warn_rounds_at_level = 0;
|
|
397
419
|
}
|
|
398
420
|
draft.reviewed_content = current.content;
|
|
399
421
|
draft.last_packet = packetInfo;
|
|
@@ -456,6 +478,7 @@ export async function converge(state, deps) {
|
|
|
456
478
|
if (next.action === 'patch') {
|
|
457
479
|
state.l3_rung_used = true;
|
|
458
480
|
state.rounds_at_level = 0;
|
|
481
|
+
state.warn_rounds_at_level = 0;
|
|
459
482
|
await deps.patch?.({ file: state.file, level: 'L3', open: [...state.open] });
|
|
460
483
|
await runRound(state, deps, { kind: 'patch_check' });
|
|
461
484
|
continue;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One review session with ONE automatic retry on `timeout` (block B30, issue #2).
|
|
3
|
+
*
|
|
4
|
+
* Field data: a reviewer CLI can hang for the whole `review.session_timeout_s` with almost no
|
|
5
|
+
* output, while the same packet reviews in seconds on a second try. So a session whose status is
|
|
6
|
+
* `timeout` is spawned once more with the SAME options (same packet file, same level, same lens);
|
|
7
|
+
* only when the second attempt times out too does the review report `unavailable: timeout`. Any
|
|
8
|
+
* other failure (exit, schema, unavailable…) is never retried here.
|
|
9
|
+
*
|
|
10
|
+
* Both attempts are recorded: `spawnSession` writes one ledger `session` row per spawn as always,
|
|
11
|
+
* and every timed-out attempt adds a `review.session_timeout` note `{lens, role, level, provider,
|
|
12
|
+
* model, attempt, retried, duration_ms, stderr_tail, tail_note?, stdout_bytes, stdout_events}`:
|
|
13
|
+
* the last 4 KB of the session's stderr, packet-free and redacted (`stderrTail`; null with a
|
|
14
|
+
* `tail_note` when withheld), and stdout only as its byte count and last event types
|
|
15
|
+
* (`stdoutEvents`) — never stdout text. The helper never throws: a throwing spawn is returned as
|
|
16
|
+
* `res: null` and a failed note is ignored. `onAttempt(n)` is told each attempt as it starts.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { logWarning } from '../util/error-log.mjs';
|
|
20
|
+
|
|
21
|
+
/** How many times a timed-out session is spawned again. */
|
|
22
|
+
export const TIMEOUT_RETRIES = 1;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {(opts: Record<string, any>) => Promise<Record<string, any>>} spawn
|
|
26
|
+
* @param {Record<string, any>} opts - the spawn options; passed unchanged to every attempt.
|
|
27
|
+
* @param {(row: Record<string, any>) => Promise<unknown>} note - best-effort ledger note.
|
|
28
|
+
* @param {(attempt: number) => void} [onAttempt] - told the attempt number as each one starts.
|
|
29
|
+
* @returns {Promise<{res: Record<string, any> | null, attempts: number}>} the last attempt's result
|
|
30
|
+
* (`null` when the spawn threw) and how many attempts ran.
|
|
31
|
+
*/
|
|
32
|
+
export async function spawnWithTimeoutRetry(spawn, opts, note, onAttempt = () => {}) {
|
|
33
|
+
let attempts = 0;
|
|
34
|
+
for (;;) {
|
|
35
|
+
attempts += 1;
|
|
36
|
+
onAttempt(attempts);
|
|
37
|
+
/** @type {Record<string, any> | null} */
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await spawn(opts);
|
|
41
|
+
} catch {
|
|
42
|
+
return { res: null, attempts }; // a throw is an `exit` failure, never retried
|
|
43
|
+
}
|
|
44
|
+
if (res?.status !== 'timeout') return { res, attempts };
|
|
45
|
+
const retried = attempts <= TIMEOUT_RETRIES;
|
|
46
|
+
try {
|
|
47
|
+
await note({
|
|
48
|
+
event: 'review.session_timeout',
|
|
49
|
+
lens: opts?.rowExtra?.lens ?? null,
|
|
50
|
+
role: opts?.role ?? null,
|
|
51
|
+
level: opts?.level ?? null,
|
|
52
|
+
provider: res.provider ?? null,
|
|
53
|
+
model: res.model ?? null,
|
|
54
|
+
attempt: attempts,
|
|
55
|
+
retried,
|
|
56
|
+
duration_ms: typeof res.duration_ms === 'number' ? res.duration_ms : null,
|
|
57
|
+
stderr_tail: typeof res.stderr_tail === 'string' ? res.stderr_tail : null,
|
|
58
|
+
...(typeof res.tail_note === 'string' ? { tail_note: res.tail_note } : {}),
|
|
59
|
+
stdout_bytes: typeof res.stdout_bytes === 'number' ? res.stdout_bytes : null,
|
|
60
|
+
stdout_events: Array.isArray(res.stdout_events) ? res.stdout_events : [],
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
// a failed note never stops the retry or the result
|
|
64
|
+
}
|
|
65
|
+
if (!retried) return { res, attempts };
|
|
66
|
+
// B37: the retry is a recoverable problem: one warning line in the local error log
|
|
67
|
+
await logWarning({ warning: 'review_retry', message: 'a review session timed out; it was retried once' }).catch(() => null);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/review/triage.mjs
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { buildQuestionPayload, isQuestionDisabled } from '../decide/questions.mjs';
|
|
19
19
|
import { bandFinding, bandResolved } from '../decide/thresholds.mjs';
|
|
20
20
|
import { alwaysResidue } from '../decide/fallback-rules.mjs';
|
|
21
|
+
import { logWarning } from '../util/error-log.mjs';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* @typedef {{id: string, file?: string, line_start: number, line_end: number, severity: 'critical' | 'warning' | 'nit', category?: string, claim?: string, evidence?: string, fix?: string}} Finding
|
|
@@ -37,6 +38,27 @@ async function note(writeRow, row) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The reasons a fallback warning may name: Jev client failure kinds (`jev-client.mjs`) plus
|
|
43
|
+
* `error` (the call threw) and `invalid_answer` (ok, but no probability). Anything else is `other`.
|
|
44
|
+
*/
|
|
45
|
+
const FALLBACK_REASONS = new Set(['unauthorized', 'invalid_request', 'rate_limited', 'unavailable', 'network', 'timeout', 'unexpected_status', 'invalid_response', 'error', 'invalid_answer']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* B37: Jev was asked and gave no usable answer, so §3.5's fallback decides: one warning line in the
|
|
49
|
+
* local error log (once per reason per process). The reason is a word from a fixed list, never
|
|
50
|
+
* Jev's text. Never throws: a logging failure cannot change the fallback.
|
|
51
|
+
* @param {unknown} reason
|
|
52
|
+
*/
|
|
53
|
+
async function fellBack(reason) {
|
|
54
|
+
const word = typeof reason === 'string' && FALLBACK_REASONS.has(reason) ? reason : 'other';
|
|
55
|
+
try {
|
|
56
|
+
await logWarning({ warning: 's1_fallback', message: `System 1 (Jev) gave no answer (${word}); fell back to the rules` }).catch(() => null);
|
|
57
|
+
} catch {
|
|
58
|
+
// the fallback stands without its warning line
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
40
62
|
/** @param {Finding} f @returns {'fix_now' | 'nit'} the verdict a judge's severity means. */
|
|
41
63
|
export const severityVerdict = (f) => (f.severity === 'nit' ? 'nit' : 'fix_now');
|
|
42
64
|
|
|
@@ -53,10 +75,13 @@ export async function askNoul(id, state, { jev, cfg = {} }) {
|
|
|
53
75
|
try {
|
|
54
76
|
res = await jev({ state, questions: buildQuestionPayload([id], cfg) });
|
|
55
77
|
} catch {
|
|
78
|
+
await fellBack('error');
|
|
56
79
|
return null;
|
|
57
80
|
}
|
|
58
81
|
const p = res?.ok === true ? res.answers?.[id]?.noul : undefined;
|
|
59
|
-
|
|
82
|
+
if (typeof p === 'number' && Number.isFinite(p) && p >= 0 && p <= 1) return p;
|
|
83
|
+
await fellBack(res?.ok === true ? 'invalid_answer' : res?.kind);
|
|
84
|
+
return null;
|
|
60
85
|
}
|
|
61
86
|
|
|
62
87
|
/**
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* 3. `reviewed_hunks` equals the packet's hunk headers EXACTLY and IN ORDER;
|
|
8
8
|
* 4. `tokens_out ≥ review.min_tokens_out` (120).
|
|
9
9
|
* Anything else is `unavailable` with one reason — `timeout`, `exit`, `schema`,
|
|
10
|
-
* `hunks_mismatch`, `too_short
|
|
10
|
+
* `hunks_mismatch`, `too_short`, or `budget` (B33: the spawner refused at `budget.usd`) — and is
|
|
11
|
+
* never approval. There is no "approve on missing output":
|
|
11
12
|
* no answer is a `schema` failure, no session is an `exit` failure.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import { readFileSync } from 'node:fs';
|
|
15
16
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
16
17
|
|
|
17
|
-
export const UNAVAILABLE_REASONS = Object.freeze(['exit', 'schema', 'hunks_mismatch', 'too_short', 'timeout']);
|
|
18
|
+
export const UNAVAILABLE_REASONS = Object.freeze(['exit', 'schema', 'hunks_mismatch', 'too_short', 'timeout', 'budget']);
|
|
18
19
|
|
|
19
20
|
export const DEFAULT_MIN_TOKENS_OUT = 120;
|
|
20
21
|
|
|
@@ -48,7 +49,7 @@ export function minTokensOut(cfg) {
|
|
|
48
49
|
* @property {boolean} ok
|
|
49
50
|
* @property {Record<string, any>} [review]
|
|
50
51
|
* @property {number} [tokens_out]
|
|
51
|
-
* @property {'exit' | 'schema' | 'hunks_mismatch' | 'too_short' | 'timeout'} [reason]
|
|
52
|
+
* @property {'exit' | 'schema' | 'hunks_mismatch' | 'too_short' | 'timeout' | 'budget'} [reason]
|
|
52
53
|
* @property {string} [detail]
|
|
53
54
|
*/
|
|
54
55
|
|
|
@@ -60,6 +61,8 @@ export function minTokensOut(cfg) {
|
|
|
60
61
|
export function validateReview(session, { hunkHeaders, minTokensOut: min = DEFAULT_MIN_TOKENS_OUT, smallDiff = false }) {
|
|
61
62
|
if (!session || typeof session !== 'object') return { ok: false, reason: 'exit', detail: 'no session result' };
|
|
62
63
|
if (session.status === 'timeout') return { ok: false, reason: 'timeout', detail: 'killed at the session timeout' };
|
|
64
|
+
// B33: the spawner refused the session because the run reached `budget.usd` — nothing ran
|
|
65
|
+
if (session.status === 'unavailable' && session.reason === 'budget') return { ok: false, reason: 'budget', detail: String(session.message ?? 'budget.usd reached') };
|
|
63
66
|
const exitCode = session.exit_code;
|
|
64
67
|
if (session.status === 'failed' || session.status === 'unavailable' || (exitCode !== undefined && exitCode !== 0)) {
|
|
65
68
|
return { ok: false, reason: 'exit', detail: `session ${session.status}${exitCode === undefined ? '' : ` (exit ${exitCode})`}` };
|
package/src/session/author.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { estimateCostUsd } from '../ledger/prices.mjs';
|
|
|
20
20
|
import { writeSafe } from '../util/redact.mjs';
|
|
21
21
|
import { currentRunRoot } from '../util/tmp.mjs';
|
|
22
22
|
import { checkSheetFresh } from './facts.mjs';
|
|
23
|
+
import { authorHeadingRules } from './plan-sections.mjs';
|
|
23
24
|
import { spawnSession } from './spawn.mjs';
|
|
24
25
|
|
|
25
26
|
export const JOBS = Object.freeze(['plan', 'harden']);
|
|
@@ -58,8 +59,11 @@ export const AUTHOR_LENS =
|
|
|
58
59
|
const JOB_RULES = {
|
|
59
60
|
plan: [
|
|
60
61
|
'Write the plan draft in markdown. Its §0 is the facts sheet below, VERBATIM (header lines included).',
|
|
61
|
-
|
|
62
|
-
'
|
|
62
|
+
...authorHeadingRules(),
|
|
63
|
+
'The level column of the block table is the lane recorded for each block by `forge jev ask lane --block <id> --plan <plan>`',
|
|
64
|
+
'(or `--rules`, the deterministic fallback, when Jev is unavailable): L0 trivial · L1 a plain feature following a pattern ·',
|
|
65
|
+
'L2 money, dates, tenancy, migrations, concurrency. It is never a block category. Leave it as `L?` when no lane is given in',
|
|
66
|
+
'the message; plan check refuses a level that differs from the recorded lane.',
|
|
63
67
|
],
|
|
64
68
|
harden: [
|
|
65
69
|
'Harden the draft: close every gap you can from the material given; for each decision only the human can make,',
|