@codedology/code-forge 0.7.6 → 0.8.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 +51 -0
- package/README.md +57 -2
- package/docs/README.md +2 -1
- package/docs/reference/blocks.json +171 -1
- package/docs/reference/config.md +26 -8
- package/package.json +1 -1
- package/schema/code-forge.schema.json +81 -4
- package/skill/SKILL.md +6 -6
- package/skill/references/code.md +33 -4
- package/skill/references/ledger.md +8 -4
- package/skill/references/plan.md +12 -0
- package/skill/references/proof.md +44 -3
- package/skill/references/review.md +4 -2
- package/skill/references/security.md +1 -1
- package/skill/scripts/forge +1 -1
- package/skill/templates/coder-brief.md +18 -1
- package/src/autopilot/binnacle.mjs +21 -3
- package/src/cli/block.mjs +337 -37
- package/src/cli/context.mjs +175 -0
- package/src/cli/gates.mjs +125 -15
- package/src/cli/ledger.mjs +46 -2
- package/src/cli/proof.mjs +62 -16
- package/src/cli/report.mjs +21 -4
- package/src/cli/review-file.mjs +61 -6
- package/src/cli/run.mjs +16 -6
- package/src/cli/status.mjs +212 -0
- package/src/config/validate.mjs +45 -11
- package/src/context/pack.mjs +319 -0
- package/src/decide/lane-facts.mjs +41 -0
- package/src/doctor/local.mjs +2 -1
- package/src/engines/sentinel.mjs +27 -6
- package/src/gates/detect.mjs +21 -8
- package/src/gates/expand.mjs +202 -0
- package/src/gates/owned-count.mjs +66 -0
- package/src/gates/run.mjs +49 -12
- package/src/install/wizard/answers.mjs +34 -5
- package/src/install/wizard/flags.mjs +8 -1
- package/src/install/wizard/project-file.mjs +2 -0
- package/src/install/wizard/run.mjs +6 -0
- package/src/install/wizard/steps.mjs +24 -4
- package/src/install/wizard/summary.mjs +13 -5
- package/src/ledger/export.mjs +1 -1
- package/src/ledger/outside-review.mjs +164 -0
- package/src/ledger/report.mjs +32 -6
- package/src/proof/export.mjs +93 -10
- package/src/proof/lock.mjs +28 -5
- package/src/proof/worktree.mjs +482 -0
- package/src/review/engine.mjs +3 -2
- package/src/review/gate-check.mjs +9 -5
- package/src/review/open-findings.mjs +59 -0
- package/src/state/block-end.mjs +290 -0
- package/src/state/block.mjs +111 -37
- package/src/state/brief-status.mjs +131 -0
- package/src/state/config-snapshot.mjs +68 -8
- package/src/state/registry.mjs +35 -4
- package/src/state/run.mjs +21 -2
- package/src/util/breadcrumbs.mjs +1 -0
- package/src/util/forbidden.mjs +36 -1
- package/src/util/gate-token.mjs +8 -0
- package/src/worker/engine.mjs +157 -17
- package/src/worker/loop.mjs +26 -7
- package/src/worker/queue.mjs +36 -10
- package/src/worker/ticket.mjs +46 -6
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `code-forge context pack --run <id> --block <id> [--json]` (block B63a; issue #13 item 5, issue
|
|
3
|
+
* #10 item 2). Builds the coder context pack of one block from the `context` key of the project's
|
|
4
|
+
* LIVE `.code-forge.yml` in the main tree (not the run-start snapshot: `context` is not authority)
|
|
5
|
+
* — see `src/context/pack.mjs` for the sections, the order and the line cap — and writes it to
|
|
6
|
+
* `<run root>/context/<block>.md`. The run root comes from the run's own config snapshot
|
|
7
|
+
* (`tmp.root` is fixed for the run), as for the worker. The orchestrator sends the pack's text with
|
|
8
|
+
* every coder task.
|
|
9
|
+
*
|
|
10
|
+
* Prints two lines: the path, then `tokens ~<n>` (the review packets' bytes / 4 estimate); with
|
|
11
|
+
* `--json` one line `{"path":…,"tokens":…,"lines":…,"cut":…,"sha256":…}`. Each pack written
|
|
12
|
+
* appends a signed `context.pack {block, path, sha256, tokens, lines, cut}` ledger row — unless
|
|
13
|
+
* the block's last verified `context.pack` row has the same sha256 and path (same pack: no row,
|
|
14
|
+
* same output). The ledger is read before anything is written. The new text goes to a temp file
|
|
15
|
+
* first; the signed row is appended while it is still there, and only then is it renamed over
|
|
16
|
+
* `<block>.md`. A row that cannot be appended removes only the temp file, so an earlier pack stays
|
|
17
|
+
* as it was and nothing was written. A rename that fails after the row was appended says so.
|
|
18
|
+
*
|
|
19
|
+
* Exit codes: 0 written, 1 refused (no `context` key, a rules or lessons file missing or outside
|
|
20
|
+
* the repository, a bad `context` value, a control character, an unknown run or block, a file or
|
|
21
|
+
* ledger write that fails), 2 usage. Never runs any command it lists.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { createHash } from 'node:crypto';
|
|
25
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { loadProjectConfig } from '../config/load.mjs';
|
|
28
|
+
import { assemblePack, ContextError, contextSettings, packTokens, readSources } from '../context/pack.mjs';
|
|
29
|
+
import { appendRow, readAllRows } from '../ledger/write.mjs';
|
|
30
|
+
import { runRootFor } from '../session/spawn.mjs';
|
|
31
|
+
import { parseFlags } from '../state/cli-args.mjs';
|
|
32
|
+
import { configInForce } from '../state/config-snapshot.mjs';
|
|
33
|
+
import { StateError } from '../state/paths.mjs';
|
|
34
|
+
import { readRun, writeSigned } from '../state/run.mjs';
|
|
35
|
+
import { loadKey, verifyRow } from '../state/signer.mjs';
|
|
36
|
+
import { assertBlockId, mainRootOf, WorkerError } from '../worker/ticket.mjs';
|
|
37
|
+
import { writeSafe } from '../util/redact.mjs';
|
|
38
|
+
|
|
39
|
+
const USAGE = 'usage: code-forge context pack --run <id> --block <id> [--json]\n';
|
|
40
|
+
|
|
41
|
+
/** @param {unknown} err @returns {string} the fs error's code (or message) for a one-line refusal. */
|
|
42
|
+
const fsCode = (err) => /** @type {NodeJS.ErrnoException} */ (err)?.code ?? (err instanceof Error ? err.message : 'error');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} ContextDeps
|
|
46
|
+
* @property {{write: (s: string) => unknown}} [stdout]
|
|
47
|
+
* @property {{write: (s: string) => unknown}} [stderr]
|
|
48
|
+
* @property {(row: Record<string, any>) => Promise<unknown>} [appendRow] - the ledger append
|
|
49
|
+
* (default: the project's ledger file).
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The last verified `context.pack` row of this run and block, or null.
|
|
54
|
+
* @param {string} runId @param {string} slug @param {string} block
|
|
55
|
+
* @returns {Promise<Record<string, any> | null>}
|
|
56
|
+
*/
|
|
57
|
+
async function lastPackRow(runId, slug, block) {
|
|
58
|
+
const rows = (await readAllRows(slug)).filter((r) => r?.event === 'context.pack' && r.run === runId && r.block === block);
|
|
59
|
+
if (rows.length === 0) return null;
|
|
60
|
+
const key = await loadKey(runId);
|
|
61
|
+
const valid = rows.filter((r) => verifyRow(r, key).ok);
|
|
62
|
+
return valid.length > 0 ? valid[valid.length - 1] : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string[]} args @param {ContextDeps} [deps]
|
|
67
|
+
* @returns {Promise<number>}
|
|
68
|
+
*/
|
|
69
|
+
export async function runContext(args, deps = {}) {
|
|
70
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
71
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
72
|
+
const [sub, ...rest] = args;
|
|
73
|
+
if (sub !== 'pack') {
|
|
74
|
+
writeSafe(stderr, USAGE);
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const { flags, positionals } = parseFlags(rest, { values: ['run', 'block'], booleans: ['json'] });
|
|
79
|
+
if (positionals.length > 0 || typeof flags.run !== 'string' || typeof flags.block !== 'string') throw new StateError('usage', 'context pack needs --run and --block');
|
|
80
|
+
const runId = flags.run;
|
|
81
|
+
let block;
|
|
82
|
+
try {
|
|
83
|
+
block = assertBlockId(flags.block);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new StateError('usage', `--block: ${JSON.stringify(flags.block)} is not a block id`);
|
|
86
|
+
}
|
|
87
|
+
const record = await readRun(runId);
|
|
88
|
+
const entry = record.blocks?.[block];
|
|
89
|
+
if (!entry) throw new ContextError('no-block', `run ${runId} has no block ${block}`);
|
|
90
|
+
// rules and lessons come from the MAIN tree, never from a block worktree; so does the config
|
|
91
|
+
const repoRoot = await mainRootOf(record.workspace);
|
|
92
|
+
// only `context` is read LIVE (it is not authority)
|
|
93
|
+
const loaded = await loadProjectConfig(repoRoot);
|
|
94
|
+
if (!loaded.ok) throw new ContextError('bad-config', loaded.message);
|
|
95
|
+
const live = /** @type {Record<string, any>} */ (loaded.config);
|
|
96
|
+
const settings = contextSettings(live);
|
|
97
|
+
if (settings === null) throw new ContextError('no-context', 'the config has no context key: add context (rules, lessons, commands, max_lines) to .code-forge.yml');
|
|
98
|
+
const sources = readSources(repoRoot, settings);
|
|
99
|
+
const { text, lines, cut } = assemblePack({
|
|
100
|
+
settings,
|
|
101
|
+
sources,
|
|
102
|
+
block: {
|
|
103
|
+
run: runId,
|
|
104
|
+
block,
|
|
105
|
+
level: entry.level ?? null,
|
|
106
|
+
owned: Array.isArray(entry.owned_files) ? entry.owned_files : [],
|
|
107
|
+
tests: Array.isArray(entry.test_files) ? entry.test_files : null,
|
|
108
|
+
worktree: entry.worktree && typeof entry.worktree.path === 'string' ? { path: entry.worktree.path, branch: entry.worktree.branch } : null,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const sha256 = createHash('sha256').update(text).digest('hex');
|
|
112
|
+
const tokens = packTokens(text);
|
|
113
|
+
// the run root from the run's own snapshot (`tmp.root` is fixed for the run, as the worker
|
|
114
|
+
// made it); a record from before snapshots has none, and the live value is all there is
|
|
115
|
+
const snapshot = configInForce(record) ?? live;
|
|
116
|
+
const tmpRoot = typeof snapshot?.tmp?.root === 'string' ? snapshot.tmp.root : undefined;
|
|
117
|
+
let runRoot;
|
|
118
|
+
try {
|
|
119
|
+
runRoot = runRootFor(runId, tmpRoot);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw new ContextError('no-run-root', `the run root cannot be used (${err instanceof Error ? err.message : 'error'})`);
|
|
122
|
+
}
|
|
123
|
+
const dir = path.join(runRoot, 'context');
|
|
124
|
+
const file = path.join(dir, `${block}.md`);
|
|
125
|
+
// every ledger read and check happens BEFORE the file is touched: a refusal here writes nothing
|
|
126
|
+
const last = await lastPackRow(runId, record.project, block);
|
|
127
|
+
const needsRow = last === null || last.sha256 !== sha256 || last.path !== file;
|
|
128
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
129
|
+
try {
|
|
130
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
131
|
+
writeFileSync(tmp, text, { mode: 0o600 });
|
|
132
|
+
} catch (err) {
|
|
133
|
+
rmSync(tmp, { force: true });
|
|
134
|
+
throw new ContextError('write-failed', `cannot write ${file} (${fsCode(err)})`);
|
|
135
|
+
}
|
|
136
|
+
// the row first, while the new text is only in the temp file: a failed append leaves the
|
|
137
|
+
// earlier pack (if any) exactly as it was
|
|
138
|
+
if (needsRow) {
|
|
139
|
+
const append = deps.appendRow ?? ((/** @type {Record<string, any>} */ row) => appendRow(row, { slug: record.project }));
|
|
140
|
+
try {
|
|
141
|
+
await writeSigned(runId, append, { event: 'context.pack', block, path: file, sha256, tokens, lines, cut });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
rmSync(tmp, { force: true });
|
|
144
|
+
writeSafe(stderr, `context: the context.pack ledger row could not be written (${err instanceof Error ? err.message : 'error'}); nothing was written\n`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
renameSync(tmp, file);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
rmSync(tmp, { force: true });
|
|
152
|
+
const after = needsRow ? 'its context.pack ledger row was already written' : 'nothing was written';
|
|
153
|
+
writeSafe(stderr, `context: cannot write ${file} (${fsCode(err)}); ${after}\n`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
if (flags.json === true) writeSafe(stdout, `${JSON.stringify({ path: file, tokens, lines, cut, sha256 })}\n`);
|
|
157
|
+
else writeSafe(stdout, `${file}\ntokens ~${tokens}\n`);
|
|
158
|
+
return 0;
|
|
159
|
+
} catch (thrown) {
|
|
160
|
+
if (thrown instanceof StateError && thrown.code === 'usage') {
|
|
161
|
+
writeSafe(stderr, `context: ${thrown.message}\n${USAGE}`);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
if (thrown instanceof ContextError || thrown instanceof StateError || thrown instanceof WorkerError) {
|
|
165
|
+
writeSafe(stderr, `context: ${thrown.message}; nothing was written\n`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
throw thrown;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {string[]} args @returns {Promise<number>} */
|
|
173
|
+
export default async function context(args) {
|
|
174
|
+
return runContext(args);
|
|
175
|
+
}
|
package/src/cli/gates.mjs
CHANGED
|
@@ -7,7 +7,24 @@
|
|
|
7
7
|
* gates detect --cwd <dir>
|
|
8
8
|
* gates run --cwd <dir> [--slug <slug>] [--timeout-ms <n>] [--run <r> --block <id>]
|
|
9
9
|
* (with --run/--block the gates run behind the proof lock, `runGatesGuarded`: another block
|
|
10
|
-
* holding the lock ⇒ `{"result":"proof.busy","holder":…}` and exit 1
|
|
10
|
+
* holding the lock ⇒ `{"result":"proof.busy","holder":…}` and exit 1. B60: a gate argv item
|
|
11
|
+
* that is exactly `{test_files}` runs the block's related tests, or is dropped for the full
|
|
12
|
+
* suite — over `gates.full_suite_threshold_files` owned files (globs and directories count
|
|
13
|
+
* the tracked and new (not ignored) files they cover), no test file known, or no --run/--block (`reason: no_tests`);
|
|
14
|
+
* that gate's result and `gate.red` row, and the `gate.done` row, carry `scope: related|full`
|
|
15
|
+
* with `test_count` or `reason: threshold|no_tests`, and with --run/--block the rows name `run`
|
|
16
|
+
* and `block`. A gate given as `{argv, env}` runs with those variables, shown by name only
|
|
17
|
+
* (`env_keys`). code-forge never prints or records an env value. Gate env is not a secret
|
|
18
|
+
* store. Child output is masked only for values of 8 or more characters.
|
|
19
|
+
* B62b: for a block with a worktree (`proof.isolation: worktree`) the gates run in that tree —
|
|
20
|
+
* at the workspace's place in it (`blockTree`) — and `{test_files}` and the threshold count
|
|
21
|
+
* the files there. The gates are detected in the tree and configured by the main workspace's
|
|
22
|
+
* `.code-forge.yml`. `--cwd` may be left out, or must name the workspace's place in the tree:
|
|
23
|
+
* the tree's top level when the workspace is the repository's top level, else the same
|
|
24
|
+
* subdirectory of the tree (then the tree's top level is refused too). Any other `--cwd`, and
|
|
25
|
+
* any `--cwd=<dir>` argument, is refused (exit 2, nothing run). A tree that is gone, malformed or off its branch is refused
|
|
26
|
+
* with exit 1 (the message on stderr, no gate run, no row). The JSON output and the `gate.red` and
|
|
27
|
+
* `gate.done` rows then carry `worktree: <tree path>`.)
|
|
11
28
|
* gates secret-scan --cwd <dir> --file <path> [--file <path> ...]
|
|
12
29
|
* gates safe-edit --cwd <dir> --base <sha>
|
|
13
30
|
* gates scope --cwd <dir> --base <sha>
|
|
@@ -15,31 +32,39 @@
|
|
|
15
32
|
* gates transcript-grep --file <path>
|
|
16
33
|
*/
|
|
17
34
|
|
|
35
|
+
import { realpathSync } from 'node:fs';
|
|
18
36
|
import { readFile } from 'node:fs/promises';
|
|
37
|
+
import path from 'node:path';
|
|
19
38
|
import { writeSafe, redactJSON } from '../util/redact.mjs';
|
|
20
39
|
import { appendRow } from '../ledger/write.mjs';
|
|
21
40
|
import { loadProjectConfig } from '../config/load.mjs';
|
|
22
41
|
import { detectGates } from '../gates/detect.mjs';
|
|
42
|
+
import { printableGates, thresholdOf } from '../gates/expand.mjs';
|
|
23
43
|
import { runGates } from '../gates/run.mjs';
|
|
24
44
|
import { runGatesGuarded } from '../proof/lock.mjs';
|
|
45
|
+
import { blockTree } from '../proof/worktree.mjs';
|
|
46
|
+
import { readRun } from '../state/run.mjs';
|
|
47
|
+
import { StateError } from '../state/paths.mjs';
|
|
25
48
|
import { scanFiles } from '../gates/secret-scan.mjs';
|
|
26
49
|
import { checkSafeEdit } from '../gates/safe-edit.mjs';
|
|
27
50
|
import { computeFileSet } from '../gates/scope.mjs';
|
|
28
51
|
import { checkAcceptance, parseTapPassed } from '../gates/acceptance.mjs';
|
|
29
52
|
import { checkTranscript } from '../gates/transcript-grep.mjs';
|
|
30
53
|
|
|
54
|
+
/** B60: the gate env rule, in the same words as the schema and proof.md. */
|
|
55
|
+
const GATE_ENV_RULE = 'code-forge never prints or records an env value. Gate env is not a secret store. Child output is masked only for values of 8 or more characters.';
|
|
56
|
+
|
|
31
57
|
/**
|
|
32
58
|
* Auto-detected gates (`detectGates`), overridden per-key by an on-disk `.code-forge.yml`'s
|
|
33
59
|
* `gates.{test,lint,types,format}` when that key is explicitly present — a project that already
|
|
34
60
|
* confirmed/edited its gates at `init` step 7 (plan §2 step 7) is authoritative over the table.
|
|
35
61
|
* A missing/unparseable config file is not an error here: detection alone is a valid answer.
|
|
36
62
|
* @param {string} cwd
|
|
37
|
-
* @
|
|
63
|
+
* @param {Record<string, any> | null} configured - the config's `gates` section ({@link configuredGates}).
|
|
64
|
+
* @returns {import('../gates/detect.mjs').DetectedGates}
|
|
38
65
|
*/
|
|
39
|
-
|
|
66
|
+
function resolveGates(cwd, configured) {
|
|
40
67
|
const detected = detectGates(cwd);
|
|
41
|
-
const loaded = await loadProjectConfig(cwd);
|
|
42
|
-
const configured = loaded.ok && loaded.config?.gates && typeof loaded.config.gates === 'object' ? loaded.config.gates : null;
|
|
43
68
|
if (!configured) return detected;
|
|
44
69
|
return {
|
|
45
70
|
stack: detected.stack,
|
|
@@ -50,6 +75,16 @@ async function resolveGates(cwd) {
|
|
|
50
75
|
};
|
|
51
76
|
}
|
|
52
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The on-disk `.code-forge.yml`'s `gates` section, or null when there is none (or it does not load).
|
|
80
|
+
* @param {string} cwd
|
|
81
|
+
* @returns {Promise<Record<string, any> | null>}
|
|
82
|
+
*/
|
|
83
|
+
async function configuredGates(cwd) {
|
|
84
|
+
const loaded = await loadProjectConfig(cwd);
|
|
85
|
+
return loaded.ok && loaded.config?.gates && typeof loaded.config.gates === 'object' ? loaded.config.gates : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
53
88
|
/**
|
|
54
89
|
* `production.markers`/`production.names` from an on-disk config, merged into the forbidden list
|
|
55
90
|
* `transcript-grep` scans with — absent/unparseable config yields no extra tokens.
|
|
@@ -91,7 +126,8 @@ function flagValue(args, flag) {
|
|
|
91
126
|
* go through `flagValue(...) ?? process.cwd()`, which cannot tell "absent" from "malformed" apart
|
|
92
127
|
* and silently ran gates in the wrong directory for the latter).
|
|
93
128
|
* @param {string[]} args @param {(s: string) => void} err @param {string} label
|
|
94
|
-
*
|
|
129
|
+
* `given` says whether `--cwd` was on the command line (`value` is then its value, not the default).
|
|
130
|
+
* @returns {{ok: true, value: string, given: boolean} | {ok: false}}
|
|
95
131
|
*/
|
|
96
132
|
function resolveCwd(args, err, label) {
|
|
97
133
|
const flag = readOptionalFlag(args, '--cwd');
|
|
@@ -99,7 +135,7 @@ function resolveCwd(args, err, label) {
|
|
|
99
135
|
err(`${label}: --cwd requires a value\n`);
|
|
100
136
|
return { ok: false };
|
|
101
137
|
}
|
|
102
|
-
return { ok: true, value: flag.value ?? process.cwd() };
|
|
138
|
+
return { ok: true, value: flag.value ?? process.cwd(), given: flag.value !== undefined };
|
|
103
139
|
}
|
|
104
140
|
|
|
105
141
|
/** @param {string[]} args @param {string} flag @returns {string[]} every value of a repeatable flag. */
|
|
@@ -111,6 +147,51 @@ function repeatedFlagValues(args, flag) {
|
|
|
111
147
|
return values;
|
|
112
148
|
}
|
|
113
149
|
|
|
150
|
+
/**
|
|
151
|
+
* B62b: the worktree the block `blockId` of run `runId` works in, with the run's main workspace;
|
|
152
|
+
* null for a block without one. A run or block that cannot be read is null too: the proof lock
|
|
153
|
+
* (`runGatesGuarded`) refuses it with its own message.
|
|
154
|
+
* @param {string} runId @param {string} blockId
|
|
155
|
+
* @returns {Promise<{tree: import('../proof/worktree.mjs').BlockTree, workspace: string} | null>}
|
|
156
|
+
* @throws {import('../state/paths.mjs').StateError} a `worktree-*` code when the recorded tree is
|
|
157
|
+
* malformed, gone or unusable; the message names the block and run and says no gate was run.
|
|
158
|
+
*/
|
|
159
|
+
async function blockWorktree(runId, blockId) {
|
|
160
|
+
let record;
|
|
161
|
+
try {
|
|
162
|
+
record = await readRun(runId);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
let tree;
|
|
167
|
+
try {
|
|
168
|
+
tree = await blockTree(record.workspace, record.blocks?.[blockId]);
|
|
169
|
+
} catch (thrown) {
|
|
170
|
+
if (thrown instanceof StateError) throw new StateError(thrown.code, `block ${blockId} of run ${runId}: ${thrown.message}; no gate was run`);
|
|
171
|
+
throw thrown;
|
|
172
|
+
}
|
|
173
|
+
return tree ? { tree, workspace: record.workspace } : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @param {string} p @returns {string} the realpath of `p`, or `p` resolved when it does not exist. */
|
|
177
|
+
function realOrResolved(p) {
|
|
178
|
+
try {
|
|
179
|
+
return realpathSync(p);
|
|
180
|
+
} catch {
|
|
181
|
+
return path.resolve(p);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* B60: the scope fields of a `{test_files}` gate's result (`scope` with `test_count` or `reason`),
|
|
187
|
+
* or none for any other gate.
|
|
188
|
+
* @param {Record<string, any>} result @returns {Record<string, any>}
|
|
189
|
+
*/
|
|
190
|
+
function scopeOf({ scope, test_count: testCount, reason }) {
|
|
191
|
+
if (scope === undefined) return {};
|
|
192
|
+
return scope === 'related' ? { scope, test_count: testCount } : { scope, reason };
|
|
193
|
+
}
|
|
194
|
+
|
|
114
195
|
/**
|
|
115
196
|
* @param {string[]} args
|
|
116
197
|
* @param {{stdout?: {write: (s: string) => unknown}, stderr?: {write: (s: string) => unknown}}} [deps]
|
|
@@ -121,7 +202,11 @@ export async function runGatesVerb(args, deps = {}) {
|
|
|
121
202
|
const out = (/** @type {object} */ value) => writeSafe(stdout, `${redactJSON(value)}\n`);
|
|
122
203
|
const err = (/** @type {string} */ s) => writeSafe(stderr, s);
|
|
123
204
|
const usage = () =>
|
|
124
|
-
err(
|
|
205
|
+
err(
|
|
206
|
+
'usage: code-forge gates detect|run|secret-scan|safe-edit|scope|acceptance|transcript-grep --cwd <dir> [...]\n' +
|
|
207
|
+
` gates run: an argv item exactly {test_files} runs the block's related tests (needs --run and --block); over gates.full_suite_threshold_files owned files, or with no test file known, the full suite runs.\n` +
|
|
208
|
+
` gates run: a gate may be {argv, env}. ${GATE_ENV_RULE}\n`,
|
|
209
|
+
);
|
|
125
210
|
|
|
126
211
|
const [sub] = args;
|
|
127
212
|
|
|
@@ -129,14 +214,16 @@ export async function runGatesVerb(args, deps = {}) {
|
|
|
129
214
|
if (sub === 'detect') {
|
|
130
215
|
const cwdArg = resolveCwd(args, err, 'gates detect');
|
|
131
216
|
if (!cwdArg.ok) return 2;
|
|
132
|
-
|
|
217
|
+
const gates = resolveGates(cwdArg.value, await configuredGates(cwdArg.value));
|
|
218
|
+
// B60: a gate given with env is printed with its variable NAMES (`env_keys`), never the values
|
|
219
|
+
out(printableGates(gates));
|
|
133
220
|
return 0;
|
|
134
221
|
}
|
|
135
222
|
|
|
136
223
|
if (sub === 'run') {
|
|
137
224
|
const cwdArg = resolveCwd(args, err, 'gates run');
|
|
138
225
|
if (!cwdArg.ok) return 2;
|
|
139
|
-
|
|
226
|
+
let cwd = cwdArg.value;
|
|
140
227
|
|
|
141
228
|
const timeoutFlag = readOptionalFlag(args, '--timeout-ms');
|
|
142
229
|
if (!timeoutFlag.ok) {
|
|
@@ -165,10 +252,28 @@ export async function runGatesVerb(args, deps = {}) {
|
|
|
165
252
|
return 2;
|
|
166
253
|
}
|
|
167
254
|
|
|
168
|
-
|
|
255
|
+
// B62b: a block with a worktree runs its gates in that tree, configured by the main workspace
|
|
256
|
+
const placed = runFlag.value !== undefined && blockFlag.value !== undefined ? await blockWorktree(runFlag.value, blockFlag.value) : null;
|
|
257
|
+
if (placed) {
|
|
258
|
+
// `--cwd=<dir>` is no form of --cwd: for a worktree block it is refused, never ignored
|
|
259
|
+
const joined = args.find((a) => a.startsWith('--cwd='));
|
|
260
|
+
if (joined !== undefined) {
|
|
261
|
+
err(`gates run: block ${blockFlag.value} of run ${runFlag.value} works in its worktree ${placed.tree.workspace}, not in ${joined} — drop it (--cwd=<dir> is not read as --cwd); no gate was run\n`);
|
|
262
|
+
return 2;
|
|
263
|
+
}
|
|
264
|
+
if (cwdArg.given && realOrResolved(cwd) !== realOrResolved(placed.tree.workspace)) {
|
|
265
|
+
err(`gates run: block ${blockFlag.value} of run ${runFlag.value} works in its worktree ${placed.tree.workspace}, not in --cwd ${cwd} — drop --cwd (or give the worktree); no gate was run\n`);
|
|
266
|
+
return 2;
|
|
267
|
+
}
|
|
268
|
+
cwd = placed.tree.workspace;
|
|
269
|
+
}
|
|
270
|
+
const configured = await configuredGates(placed ? placed.workspace : cwd); // loaded once: the gates and the threshold
|
|
271
|
+
const gates = resolveGates(cwd, configured);
|
|
272
|
+
const inTree = placed ? { worktree: placed.tree.path } : {};
|
|
169
273
|
let ran;
|
|
170
274
|
if (runFlag.value !== undefined && blockFlag.value !== undefined) {
|
|
171
|
-
const
|
|
275
|
+
const threshold = thresholdOf(configured?.full_suite_threshold_files);
|
|
276
|
+
const guarded = await runGatesGuarded({ runId: runFlag.value, blockId: blockFlag.value, gates, cwd, threshold, ...(timeoutMs ? { timeoutMs } : {}) });
|
|
172
277
|
if (guarded.result === 'proof.busy') {
|
|
173
278
|
out({ result: 'proof.busy', holder: /** @type {{holder: string}} */ (guarded).holder });
|
|
174
279
|
return 1;
|
|
@@ -180,14 +285,19 @@ export async function runGatesVerb(args, deps = {}) {
|
|
|
180
285
|
const { results, allOk } = ran;
|
|
181
286
|
|
|
182
287
|
if (slug) {
|
|
288
|
+
// B60: with --run/--block the rows name the run and the block, so the run's binnacle reads them
|
|
289
|
+
const owner = runFlag.value !== undefined ? { run: runFlag.value, block: blockFlag.value, ...inTree } : {};
|
|
183
290
|
for (const result of results) {
|
|
184
291
|
if (!result.skipped && !result.ok) {
|
|
185
|
-
await appendRow({ event: 'gate.red', gate: result.gate, code: result.code }, { slug });
|
|
292
|
+
await appendRow({ ...owner, event: 'gate.red', gate: result.gate, code: result.code, ...scopeOf(result) }, { slug });
|
|
186
293
|
}
|
|
187
294
|
}
|
|
188
|
-
|
|
295
|
+
// B60: one scope per run (every `{test_files}` gate shares it), named on gate.done too;
|
|
296
|
+
// without --run/--block that is `scope: full, reason: no_tests`
|
|
297
|
+
const scoped = results.find((r) => r.scope !== undefined);
|
|
298
|
+
await appendRow({ ...owner, event: 'gate.done', stack: gates.stack, all_ok: allOk, ...(scoped ? scopeOf(scoped) : {}) }, { slug });
|
|
189
299
|
}
|
|
190
|
-
out({ stack: gates.stack, results, allOk });
|
|
300
|
+
out({ stack: gates.stack, ...inTree, results, allOk });
|
|
191
301
|
return allOk ? 0 : 1;
|
|
192
302
|
}
|
|
193
303
|
|
package/src/cli/ledger.mjs
CHANGED
|
@@ -8,18 +8,26 @@
|
|
|
8
8
|
* ledger outcome --pr <n> --ci red|green|reverted --slug <slug>
|
|
9
9
|
* ledger tail --slug <slug> [--n <count> | -n <count>]
|
|
10
10
|
* ledger add coder --run <r> --block <b> --usd <n> [--note "..."]
|
|
11
|
+
* ledger outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>] [--block <id>]...
|
|
11
12
|
*
|
|
12
13
|
* B33: `ledger add coder` records manual/cloud coder spend (a session code-forge never saw) as a
|
|
13
14
|
* signed `session` row (`role: coder, manual: true, usd, cost_source: manual`) in the run's
|
|
14
15
|
* ledger, so `report` and the `budget.usd` check count it. The slug is the run record's project.
|
|
16
|
+
*
|
|
17
|
+
* B66: `ledger outside-review` records the rounds and findings of an outside review of one PR as a
|
|
18
|
+
* signed `outside_review` row in the run's project ledger (the `report` section `outside_review`
|
|
19
|
+
* puts it next to the code-forge cost of the PR's blocks). An orchestrator/human verb, never a
|
|
20
|
+
* coder one. A later call for the same PR replaces the earlier row in every reader.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
|
-
import { writeSafe } from '../util/redact.mjs';
|
|
18
23
|
import { buildCalibration } from '../ledger/calibration.mjs';
|
|
19
24
|
import { recordCiOutcome, reviewedEntriesFromRows, scanGitAndRecord } from '../ledger/outcome.mjs';
|
|
25
|
+
import { OUTSIDE_REVIEW_EVENT, OUTSIDE_REVIEW_USAGE, outsideReviewRecordedLine, parseOutsideReviewArgs } from '../ledger/outside-review.mjs';
|
|
20
26
|
import { roundUsd, runSpend } from '../ledger/spend.mjs';
|
|
21
27
|
import { appendRow, readAllRows } from '../ledger/write.mjs';
|
|
22
28
|
import { readRun, writeSigned } from '../state/run.mjs';
|
|
29
|
+
import { loadKey } from '../state/signer.mjs';
|
|
30
|
+
import { redact, writeSafe } from '../util/redact.mjs';
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
33
|
* @param {string[]} args @param {string} flag
|
|
@@ -63,10 +71,11 @@ export async function runLedger(args, deps = {}) {
|
|
|
63
71
|
const { stdout = process.stdout, stderr = process.stderr } = deps;
|
|
64
72
|
const out = (/** @type {string} */ s) => writeSafe(stdout, s);
|
|
65
73
|
const err = (/** @type {string} */ s) => writeSafe(stderr, s);
|
|
66
|
-
const usage = () => err('usage: code-forge ledger calibration|outcome|tail --slug <slug> [...] (tail: [--n|-n <count>]) | add coder --run <r> --block <b> --usd <n> [--note "..."]
|
|
74
|
+
const usage = () => err('usage: code-forge ledger calibration|outcome|tail --slug <slug> [...] (tail: [--n|-n <count>]) | add coder --run <r> --block <b> --usd <n> [--note "..."] | outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>] [--block <id>]...\n');
|
|
67
75
|
|
|
68
76
|
const [sub] = args;
|
|
69
77
|
if (sub === 'add') return addSpend(args.slice(1), { out, err });
|
|
78
|
+
if (sub === 'outside-review') return addOutsideReview(args.slice(1), { out, err });
|
|
70
79
|
const slug = flagValue(args, '--slug');
|
|
71
80
|
if (!slug) {
|
|
72
81
|
usage();
|
|
@@ -245,6 +254,41 @@ async function addSpend(args, { out, err }) {
|
|
|
245
254
|
}
|
|
246
255
|
}
|
|
247
256
|
|
|
257
|
+
/**
|
|
258
|
+
* `ledger outside-review --run <r> --pr <n> --rounds <n> --findings <n> [--link <url>]
|
|
259
|
+
* [--block <id>]...` (B66). Usage errors exit 2, an unknown run or block exits 1; both write nothing.
|
|
260
|
+
* The run key is loaded (and so registered as a secret) BEFORE the link is redacted.
|
|
261
|
+
* @param {string[]} args - after `outside-review`.
|
|
262
|
+
* @param {{out: (s: string) => void, err: (s: string) => void}} io
|
|
263
|
+
* @returns {Promise<number>}
|
|
264
|
+
*/
|
|
265
|
+
async function addOutsideReview(args, { out, err }) {
|
|
266
|
+
const parsed = parseOutsideReviewArgs(args);
|
|
267
|
+
if ('message' in parsed) {
|
|
268
|
+
err(`ledger outside-review: ${parsed.message}\n${OUTSIDE_REVIEW_USAGE}\n`);
|
|
269
|
+
return 2;
|
|
270
|
+
}
|
|
271
|
+
const { run, pr, rounds, findings, link, blocks } = parsed.value;
|
|
272
|
+
try {
|
|
273
|
+
const record = await readRun(run);
|
|
274
|
+
const known = record.blocks && typeof record.blocks === 'object' ? record.blocks : {};
|
|
275
|
+
const unknown = blocks.find((id) => !Object.hasOwn(known, id));
|
|
276
|
+
if (unknown !== undefined) {
|
|
277
|
+
err(`ledger outside-review: block ${JSON.stringify(unknown)} is not a block of run ${run}; nothing written\n`);
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
await loadKey(run);
|
|
281
|
+
const writeRow = (/** @type {Record<string, any>} */ row) => appendRow(row, { slug: record.project });
|
|
282
|
+
const row = { event: OUTSIDE_REVIEW_EVENT, pr, rounds, findings, link: link === null ? null : redact(link), blocks };
|
|
283
|
+
await writeSigned(run, writeRow, row);
|
|
284
|
+
out(outsideReviewRecordedLine(row));
|
|
285
|
+
return 0;
|
|
286
|
+
} catch (thrown) {
|
|
287
|
+
err(`ledger outside-review: ${thrown?.message ?? String(thrown)}\n`);
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
248
292
|
/** @param {string[]} args @returns {Promise<number>} */
|
|
249
293
|
export default async function ledger(args) {
|
|
250
294
|
return runLedger(args);
|
package/src/cli/proof.mjs
CHANGED
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
* `export` reads `proof.export.{link_dirs,copy_untracked}` from the workspace's `.code-forge.yml`
|
|
21
21
|
* (defaults §1.3) and writes a signed `proof` row `isolation: export`; `lock`/`unlock` write
|
|
22
22
|
* `isolation: lock` rows (B6 writer, B8 signer).
|
|
23
|
+
*
|
|
24
|
+
* B62b: for a block with a worktree, `export` and `red-green` read the block's files, the test
|
|
25
|
+
* and the base in its tree (`blockTree`, against the tree's `base_sha`) and measure in an export
|
|
26
|
+
* built FROM the tree into the main tree's `.code-forge/export/<block>` — never in the tree, which
|
|
27
|
+
* is left exactly as it was. The config still comes from the main tree. A recorded tree that is
|
|
28
|
+
* gone or malformed refuses (exit 1, `…; no proof was run`). `lock`, `unlock`, `restore` and
|
|
29
|
+
* `export --remove` touch only the run's lock and that export directory, as before.
|
|
23
30
|
*/
|
|
24
31
|
|
|
25
32
|
import { lstat } from 'node:fs/promises';
|
|
@@ -35,6 +42,7 @@ import { DEFAULT_COPY_UNTRACKED, DEFAULT_LINK_DIRS, buildExport, exportDirFor, r
|
|
|
35
42
|
import { acquireProofLock, lockHolder, releaseProofLock } from '../proof/lock.mjs';
|
|
36
43
|
import { MECHANISMS, readJournal, restoreJournal, runRedGreen } from '../proof/red-green.mjs';
|
|
37
44
|
import { tierFor } from '../proof/tiers.mjs';
|
|
45
|
+
import { blockTree } from '../proof/worktree.mjs';
|
|
38
46
|
import { blockFileSet, repoRelativePath } from '../review/gate-check.mjs';
|
|
39
47
|
import { ownsFile } from '../state/registry.mjs';
|
|
40
48
|
|
|
@@ -117,23 +125,53 @@ async function configAt(cwd) {
|
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/**
|
|
120
|
-
*
|
|
121
|
-
* @
|
|
128
|
+
* @typedef {object} ProofPlace
|
|
129
|
+
* @property {string} dir - where the block's files, its test and its base are read: the workspace,
|
|
130
|
+
* or the workspace's place in the block's worktree.
|
|
131
|
+
* @property {string} base - the block's `base_sha`, or its tree's.
|
|
132
|
+
* @property {string | null} tree - the block's worktree, null without one.
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* B62b: where a block's proof is measured. A block without a worktree: the workspace and the
|
|
137
|
+
* block's base, as before. A block with one: its tree (`blockTree`) and the tree's `base_sha`. A
|
|
138
|
+
* recorded tree that is gone or malformed refuses (exit 1), as `gates run` does.
|
|
139
|
+
* @param {string} runId @param {Record<string, any>} record @param {string} blockId @param {Record<string, any>} block
|
|
140
|
+
* @returns {Promise<ProofPlace>}
|
|
141
|
+
* @throws {StateError} `worktree-*`
|
|
142
|
+
*/
|
|
143
|
+
async function proofPlace(runId, record, blockId, block) {
|
|
144
|
+
let wt;
|
|
145
|
+
try {
|
|
146
|
+
wt = await blockTree(record.workspace, block);
|
|
147
|
+
} catch (thrown) {
|
|
148
|
+
if (thrown instanceof StateError) throw new StateError(thrown.code, `block ${blockId} of run ${runId}: ${thrown.message}; no proof was run`);
|
|
149
|
+
throw thrown;
|
|
150
|
+
}
|
|
151
|
+
return wt === null ? { dir: record.workspace, base: block.base_sha, tree: null } : { dir: wt.workspace, base: wt.base_sha, tree: wt.path };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build the block's measurement export and write its signed `proof` row (`step: export`). A block
|
|
156
|
+
* with a worktree is exported FROM its tree, into the main tree's `.code-forge/export/<block>`: the
|
|
157
|
+
* tree itself is never written to, and its row names it (`worktree`).
|
|
158
|
+
* @param {{runId: string, record: Record<string, any>, blockId: string, block: Record<string, any>, place: ProofPlace, config: Record<string, any>, writeRow: (row: Record<string, any>) => Promise<unknown>}} opts
|
|
122
159
|
*/
|
|
123
|
-
async function exportAndRecord({ runId, record, blockId, block, config, writeRow }) {
|
|
160
|
+
async function exportAndRecord({ runId, record, blockId, block, place, config, writeRow }) {
|
|
124
161
|
const exp = await buildExport({
|
|
125
|
-
cwd:
|
|
162
|
+
cwd: place.dir,
|
|
126
163
|
blockId,
|
|
127
|
-
baseSha:
|
|
164
|
+
baseSha: place.base,
|
|
128
165
|
owned: block.owned_files,
|
|
129
166
|
ownedKinds: block.owned_kinds,
|
|
130
167
|
linkDirs: config?.proof?.export?.link_dirs ?? DEFAULT_LINK_DIRS,
|
|
131
168
|
copyUntracked: config?.proof?.export?.copy_untracked ?? DEFAULT_COPY_UNTRACKED,
|
|
169
|
+
exportRoot: place.tree === null ? undefined : path.dirname(exportDirFor(record.workspace, blockId)),
|
|
132
170
|
});
|
|
133
171
|
await recordProof({
|
|
134
172
|
runId,
|
|
135
173
|
writeRow,
|
|
136
|
-
row: { block: blockId, isolation: 'export', step: 'export', base_sha: exp.base, restored: exp.restored, untracked: exp.untracked },
|
|
174
|
+
row: { block: blockId, isolation: 'export', step: 'export', base_sha: exp.base, restored: exp.restored, untracked: exp.untracked, ...(place.tree === null ? {} : { worktree: place.tree }) },
|
|
137
175
|
});
|
|
138
176
|
return exp;
|
|
139
177
|
}
|
|
@@ -193,6 +231,9 @@ export async function runProof(args, deps = {}) {
|
|
|
193
231
|
const writeRow = (/** @type {Record<string, any>} */ row) => appendRow(row, { slug: record.project });
|
|
194
232
|
|
|
195
233
|
if (sub === 'lock') {
|
|
234
|
+
// B62b: a block with its own tree never measures under the lock (see red-green)
|
|
235
|
+
const recorded = record.blocks?.[blockId]?.worktree;
|
|
236
|
+
if (recorded !== undefined && recorded !== null) throw new StateError('usage', `block ${blockId} is proven in an export built from its tree; proof lock does not apply`);
|
|
196
237
|
out({ locked: await acquireProofLock({ runId, blockId, writeRow }) });
|
|
197
238
|
return 0;
|
|
198
239
|
}
|
|
@@ -210,8 +251,10 @@ export async function runProof(args, deps = {}) {
|
|
|
210
251
|
return 0;
|
|
211
252
|
}
|
|
212
253
|
const config = await configAt(record.workspace);
|
|
213
|
-
|
|
214
|
-
|
|
254
|
+
const place = await proofPlace(runId, record, blockId, block);
|
|
255
|
+
// a block with its own tree is always proven in an export made from it (see red-green)
|
|
256
|
+
if (place.tree === null && config?.proof?.isolation === 'lock') throw new StateError('isolation-lock', 'proof.isolation is lock — use proof lock instead of an export');
|
|
257
|
+
out(await exportAndRecord({ runId, record, blockId, block, place, config, writeRow }));
|
|
215
258
|
return 0;
|
|
216
259
|
}
|
|
217
260
|
|
|
@@ -232,17 +275,20 @@ export async function runProof(args, deps = {}) {
|
|
|
232
275
|
const record = await readRun(runId);
|
|
233
276
|
const block = record.blocks?.[blockId];
|
|
234
277
|
if (block?.status !== 'open') throw new StateError('no-block', `block ${blockId} is not open in run ${runId}`);
|
|
235
|
-
await
|
|
278
|
+
const place = await proofPlace(runId, record, blockId, block);
|
|
279
|
+
await assertTestFile(place.dir, file);
|
|
236
280
|
const writeRow = (/** @type {Record<string, any>} */ row) => appendRow(row, { slug: record.project });
|
|
237
281
|
const config = await configAt(record.workspace);
|
|
238
|
-
|
|
282
|
+
// A block with its own tree is always proven in an export made from that tree (never in the
|
|
283
|
+
// tree itself, never under the lock), whatever the config says; otherwise lock or export.
|
|
284
|
+
const isolation = place.tree !== null ? 'export' : config?.proof?.isolation === 'lock' ? 'lock' : 'export';
|
|
239
285
|
if (isolation === 'lock' && (await lockHolder(runId)) !== blockId) {
|
|
240
286
|
throw new StateError('proof.busy', `proof.isolation is lock — run code-forge proof lock ${blockId} --run ${runId} first`);
|
|
241
287
|
}
|
|
242
288
|
// `revert` puts every changed non-test owned file back to base; `assertion-deletion` touches only the test.
|
|
243
289
|
const changed = await blockFileSet({
|
|
244
|
-
repoRoot:
|
|
245
|
-
base:
|
|
290
|
+
repoRoot: place.dir,
|
|
291
|
+
base: place.base,
|
|
246
292
|
owned: block.owned_files,
|
|
247
293
|
matchOwned: (f) => ownsFile(block.owned_files, f, block.owned_kinds),
|
|
248
294
|
});
|
|
@@ -250,12 +296,12 @@ export async function runProof(args, deps = {}) {
|
|
|
250
296
|
if (mechanism === 'revert' && sources.length === 0) {
|
|
251
297
|
throw new StateError('usage', `block ${blockId} changed no non-test source file to revert — use --mechanism assertion-deletion`);
|
|
252
298
|
}
|
|
253
|
-
const exp = isolation === 'export' ? await exportAndRecord({ runId, record, blockId, block, config, writeRow }) : null;
|
|
299
|
+
const exp = isolation === 'export' ? await exportAndRecord({ runId, record, blockId, block, place, config, writeRow }) : null;
|
|
254
300
|
if (exp) await assertTestFile(exp.dir, file); // the test must also be where it runs (owned, or tracked at base)
|
|
255
301
|
const result = await runRedGreen({
|
|
256
|
-
workDir: exp ? exp.dir :
|
|
257
|
-
repoDir:
|
|
258
|
-
base:
|
|
302
|
+
workDir: exp ? exp.dir : place.dir,
|
|
303
|
+
repoDir: place.dir,
|
|
304
|
+
base: place.base,
|
|
259
305
|
argv,
|
|
260
306
|
test: { file, ...(testCase === undefined ? {} : { case: testCase }), characterization: mechanism === 'assertion-deletion' },
|
|
261
307
|
sources,
|