@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,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The author loop (plan §3.7, O18; §3.8 rule 2; block B9b).
|
|
3
|
+
*
|
|
4
|
+
* `runAuthor` is one round: a fresh L3 closed-book `author` session (empty cwd, the packet —
|
|
5
|
+
* brief, facts sheet, prior draft, the human's answers — piped on stdin by B9a's spawner, V3)
|
|
6
|
+
* that returns `{draft, questions[], cost}`. The Plan job refuses to run without a facts sheet
|
|
7
|
+
* (`facts sheet required: run forge facts first`) and refuses a sheet built from another version
|
|
8
|
+
* of the brief (`facts sheet is stale: brief changed after it was built`).
|
|
9
|
+
*
|
|
10
|
+
* `runAuthorLoop` is the harden loop: write the draft, hand `questions[]` to the human through the
|
|
11
|
+
* injected `ask` (the human answers — the orchestrator never answers for them), re-invoke with the
|
|
12
|
+
* draft and the answers, and stop when B3's `isAuthorLoopDone` says so. Each round prints its cost.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { randomBytes } from 'node:crypto';
|
|
16
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { isAuthorLoopDone } from '../decide/author-loop.mjs';
|
|
19
|
+
import { estimateCostUsd } from '../ledger/prices.mjs';
|
|
20
|
+
import { writeSafe } from '../util/redact.mjs';
|
|
21
|
+
import { currentRunRoot } from '../util/tmp.mjs';
|
|
22
|
+
import { checkSheetFresh } from './facts.mjs';
|
|
23
|
+
import { spawnSession } from './spawn.mjs';
|
|
24
|
+
|
|
25
|
+
export const JOBS = Object.freeze(['plan', 'harden']);
|
|
26
|
+
|
|
27
|
+
export const FACTS_REQUIRED = 'facts sheet required: run forge facts first';
|
|
28
|
+
|
|
29
|
+
/** The author answer schema (source form; `compileSchema` makes the OpenAI strict variant). */
|
|
30
|
+
export const AUTHOR_SCHEMA = Object.freeze({
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
draft: { type: 'string' },
|
|
34
|
+
questions: {
|
|
35
|
+
type: 'array',
|
|
36
|
+
items: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
id: { type: 'string' },
|
|
40
|
+
question: { type: 'string' },
|
|
41
|
+
options: { type: 'array', items: { type: 'string' } },
|
|
42
|
+
why: { type: 'string' },
|
|
43
|
+
blocking: { type: 'boolean' },
|
|
44
|
+
},
|
|
45
|
+
required: ['id', 'question', 'options', 'why', 'blocking'],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
required: ['draft', 'questions'],
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const AUTHOR_LENS =
|
|
55
|
+
'You are the plan author for code-forge: closed-book, no tools, no files. Everything you may use is in the message. ' +
|
|
56
|
+
'Never invent a flag, path, version or API that the facts sheet does not back. Reply once, with a JSON object that matches the schema.';
|
|
57
|
+
|
|
58
|
+
const JOB_RULES = {
|
|
59
|
+
plan: [
|
|
60
|
+
'Write the plan draft in markdown. Its §0 is the facts sheet below, VERBATIM (header lines included).',
|
|
61
|
+
'Include a section titled "Acceptance clauses the facts sheet cannot back": every clause that cites a claim the sheet',
|
|
62
|
+
'marks NOT-FOUND or UNVERIFIABLE, each with a tolerance naming the block; write `none` when there is none.',
|
|
63
|
+
],
|
|
64
|
+
harden: [
|
|
65
|
+
'Harden the draft: close every gap you can from the material given; for each decision only the human can make,',
|
|
66
|
+
'ask ONE question (options, why, blocking true when the plan cannot be dispatched without the answer).',
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** An error raised before the session (`code`: `usage`, `facts-required`, `stale`, `bad-sheet`). */
|
|
71
|
+
export class AuthorError extends Error {
|
|
72
|
+
/** @param {string} code @param {string} message */
|
|
73
|
+
constructor(code, message) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = 'AuthorError';
|
|
76
|
+
this.code = code;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* §3.8 rule 2: the Plan job needs a facts sheet, and any sheet given must match the brief.
|
|
82
|
+
* @param {{job: string, briefPath: string, factsPath?: string}} opts
|
|
83
|
+
* @throws {AuthorError}
|
|
84
|
+
*/
|
|
85
|
+
export function assertFacts({ job, briefPath, factsPath }) {
|
|
86
|
+
if (factsPath === undefined) {
|
|
87
|
+
if (job === 'plan') throw new AuthorError('facts-required', FACTS_REQUIRED);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let text;
|
|
91
|
+
try {
|
|
92
|
+
text = readFileSync(factsPath, 'utf8');
|
|
93
|
+
} catch {
|
|
94
|
+
throw new AuthorError('bad-sheet', '--facts cannot be read');
|
|
95
|
+
}
|
|
96
|
+
const fresh = checkSheetFresh(text, briefPath);
|
|
97
|
+
if ('code' in fresh) throw new AuthorError(fresh.code, fresh.message);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {{job: string, brief: string, facts?: string, draft?: string, answers?: string}} parts - texts.
|
|
102
|
+
* @returns {string} the author packet.
|
|
103
|
+
*/
|
|
104
|
+
export function buildAuthorPacket({ job, brief, facts, draft, answers }) {
|
|
105
|
+
const rules = JOB_RULES[/** @type {'plan'|'harden'} */ (job)];
|
|
106
|
+
const out = [`# Author job: ${job}`, '', ...rules, '', 'Reply with {"draft": markdown, "questions": [{"id", "question", "options", "why", "blocking"}]} — questions empty when none remain.', ''];
|
|
107
|
+
out.push('## Brief', '', brief.trimEnd(), '');
|
|
108
|
+
if (facts !== undefined) out.push('## Facts sheet', '', facts.trimEnd(), '');
|
|
109
|
+
if (draft !== undefined) out.push('## Prior draft', '', draft.trimEnd(), '');
|
|
110
|
+
if (answers !== undefined) out.push("## The human's answers", '', answers.trimEnd(), '');
|
|
111
|
+
return out.join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @typedef {object} AuthorOpts
|
|
116
|
+
* @property {Record<string, any>} cfg
|
|
117
|
+
* @property {string} job - `plan` or `harden`.
|
|
118
|
+
* @property {string} briefPath @property {string} [factsPath] @property {string} [draftPath] @property {string} [answersPath]
|
|
119
|
+
* @property {string} [runRoot] @property {string} [run] @property {string} [block] @property {string} [slug]
|
|
120
|
+
* @property {number} [timeoutMs]
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @typedef {{tokens_in: number | null, tokens_out: number | null, tokens_source: string | null, usd_estimated: number | null}} AuthorCost
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/** @param {Record<string, any>} result @returns {AuthorCost} */
|
|
128
|
+
function costOf(result) {
|
|
129
|
+
const row = result.row ?? {};
|
|
130
|
+
let usd = null;
|
|
131
|
+
try {
|
|
132
|
+
usd = estimateCostUsd({ provider: result.provider, level: 'L3', tokensIn: row.tokens_in ?? 0, tokensOut: row.tokens_out ?? 0 });
|
|
133
|
+
} catch {
|
|
134
|
+
usd = null;
|
|
135
|
+
}
|
|
136
|
+
return { tokens_in: row.tokens_in ?? null, tokens_out: row.tokens_out ?? null, tokens_source: row.tokens_source ?? null, usd_estimated: usd };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* One author round.
|
|
141
|
+
* @param {AuthorOpts} opts
|
|
142
|
+
* @param {import('./spawn.mjs').SessionDeps} [deps]
|
|
143
|
+
* @returns {Promise<{status: string, reason?: string | null, draft?: string, questions?: import('../decide/author-loop.mjs').AuthorQuestion[], cost?: AuthorCost}>}
|
|
144
|
+
* @throws {AuthorError} before any session runs.
|
|
145
|
+
*/
|
|
146
|
+
export async function runAuthor(opts, deps = {}) {
|
|
147
|
+
if (!JOBS.includes(opts.job)) throw new AuthorError('usage', `--job must be one of ${JOBS.join(', ')}`);
|
|
148
|
+
assertFacts(opts);
|
|
149
|
+
/** @param {string | undefined} file @param {string} flag */
|
|
150
|
+
const read = (file, flag) => {
|
|
151
|
+
if (file === undefined) return undefined;
|
|
152
|
+
try {
|
|
153
|
+
return readFileSync(file, 'utf8');
|
|
154
|
+
} catch {
|
|
155
|
+
throw new AuthorError('usage', `--${flag} cannot be read`);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const packet = buildAuthorPacket({
|
|
159
|
+
job: opts.job,
|
|
160
|
+
brief: /** @type {string} */ (read(opts.briefPath, 'brief')),
|
|
161
|
+
facts: read(opts.factsPath, 'facts'),
|
|
162
|
+
draft: read(opts.draftPath, 'draft'),
|
|
163
|
+
answers: read(opts.answersPath, 'answers'),
|
|
164
|
+
});
|
|
165
|
+
const root = opts.runRoot ?? currentRunRoot();
|
|
166
|
+
const dir = path.join(root, 'author', `${Date.now()}-${randomBytes(4).toString('hex')}`);
|
|
167
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
168
|
+
const packetPath = path.join(dir, 'packet.md');
|
|
169
|
+
try {
|
|
170
|
+
writeFileSync(packetPath, packet, { mode: 0o600 });
|
|
171
|
+
const result = await spawnSession(
|
|
172
|
+
{ cfg: opts.cfg, level: 'L3', role: 'author', promptPath: packetPath, schema: AUTHOR_SCHEMA, systemPromptText: AUTHOR_LENS, runRoot: root, run: opts.run, block: opts.block, slug: opts.slug, timeoutMs: opts.timeoutMs },
|
|
173
|
+
deps,
|
|
174
|
+
);
|
|
175
|
+
if (result.status !== 'ok') return { status: result.status, reason: result.reason ?? null };
|
|
176
|
+
return { status: 'ok', draft: result.answer.draft, questions: result.answer.questions, cost: costOf(result) };
|
|
177
|
+
} finally {
|
|
178
|
+
rmSync(dir, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @typedef {object} AskReply
|
|
184
|
+
* @property {Record<string, string>} answers - question id ⇒ the human's answer.
|
|
185
|
+
* @property {string[]} [skipped] - NON-blocking ids the human chose to skip.
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The harden loop (§3.7).
|
|
190
|
+
* @param {AuthorOpts & {draftOut: string, ask: (questions: import('../decide/author-loop.mjs').AuthorQuestion[], round: number) => Promise<AskReply>, maxRounds?: number}} opts
|
|
191
|
+
* `draftOut`: where each round's draft is written (the plans dir); `ask`: the human.
|
|
192
|
+
* @param {import('./spawn.mjs').SessionDeps} [deps]
|
|
193
|
+
* @returns {Promise<{status: string, reason?: string | null, rounds: Array<{round: number, questions: number, cost: AuthorCost}>, draftPath: string}>}
|
|
194
|
+
*/
|
|
195
|
+
export async function runAuthorLoop(opts, deps = {}) {
|
|
196
|
+
const maxRounds = opts.maxRounds ?? 6;
|
|
197
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
198
|
+
const root = opts.runRoot ?? currentRunRoot();
|
|
199
|
+
const answersPath = path.join(root, 'author', `answers-${randomBytes(4).toString('hex')}.json`);
|
|
200
|
+
mkdirSync(path.dirname(answersPath), { recursive: true, mode: 0o700 });
|
|
201
|
+
mkdirSync(path.dirname(opts.draftOut), { recursive: true });
|
|
202
|
+
/** @type {Array<{round: number, questions: number, cost: AuthorCost}>} */
|
|
203
|
+
const rounds = [];
|
|
204
|
+
let draftPath = opts.draftPath;
|
|
205
|
+
let answers = opts.answersPath;
|
|
206
|
+
try {
|
|
207
|
+
for (let round = 1; round <= maxRounds; round += 1) {
|
|
208
|
+
const res = await runAuthor({ ...opts, runRoot: root, draftPath, answersPath: answers }, deps);
|
|
209
|
+
if (res.status !== 'ok') return { status: res.status, reason: res.reason ?? null, rounds, draftPath: opts.draftOut };
|
|
210
|
+
const questions = /** @type {import('../decide/author-loop.mjs').AuthorQuestion[]} */ (res.questions);
|
|
211
|
+
const cost = /** @type {AuthorCost} */ (res.cost);
|
|
212
|
+
writeFileSync(opts.draftOut, /** @type {string} */ (res.draft));
|
|
213
|
+
rounds.push({ round, questions: questions.length, cost });
|
|
214
|
+
writeSafe(stderr, `author round ${round}: ${questions.length} question(s), tokens ${cost.tokens_in ?? '?'}/${cost.tokens_out ?? '?'} (${cost.tokens_source ?? 'unknown'}), ~$${cost.usd_estimated ?? '?'}\n`);
|
|
215
|
+
if (isAuthorLoopDone(questions)) return { status: 'ok', rounds, draftPath: opts.draftOut };
|
|
216
|
+
const reply = await opts.ask(questions, round);
|
|
217
|
+
if (isAuthorLoopDone(questions, reply.skipped ?? [])) return { status: 'ok', rounds, draftPath: opts.draftOut };
|
|
218
|
+
// the next round is closed-book: it sees each question's text beside the human's answer
|
|
219
|
+
const answered = questions.map((q) => ({ id: q.id, question: q.question, options: q.options ?? [], answer: reply.answers[q.id] ?? null }));
|
|
220
|
+
writeFileSync(answersPath, `${JSON.stringify(answered, null, 2)}\n`, { mode: 0o600 });
|
|
221
|
+
draftPath = opts.draftOut;
|
|
222
|
+
answers = answersPath;
|
|
223
|
+
}
|
|
224
|
+
return { status: 'max-rounds', reason: `questions remain after ${maxRounds} rounds`, rounds, draftPath: opts.draftOut };
|
|
225
|
+
} finally {
|
|
226
|
+
rmSync(answersPath, { force: true });
|
|
227
|
+
}
|
|
228
|
+
}
|