@forwardimpact/libharness 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +60 -57
  2. package/package.json +2 -2
  3. package/src/advisor.js +47 -41
  4. package/src/agent-runner.js +57 -47
  5. package/src/benchmark/apm-installer.js +28 -28
  6. package/src/benchmark/env-loader.js +24 -16
  7. package/src/benchmark/grade.js +44 -41
  8. package/src/benchmark/hidden-tests.js +25 -24
  9. package/src/benchmark/hook-env.js +11 -9
  10. package/src/benchmark/invariants.js +20 -17
  11. package/src/benchmark/judge.js +29 -28
  12. package/src/benchmark/npm-installer.js +9 -8
  13. package/src/benchmark/report.js +53 -50
  14. package/src/benchmark/result.js +24 -23
  15. package/src/benchmark/runner.js +75 -69
  16. package/src/benchmark/scheduler.js +17 -16
  17. package/src/benchmark/task-family.js +28 -26
  18. package/src/benchmark/trace-split.js +8 -7
  19. package/src/benchmark/workdir.js +27 -25
  20. package/src/claude-code-executable.js +11 -11
  21. package/src/commands/advisor-flags.js +8 -7
  22. package/src/commands/assert.js +16 -15
  23. package/src/commands/benchmark-definition.js +11 -11
  24. package/src/commands/benchmark-grade.js +12 -11
  25. package/src/commands/benchmark-report.js +5 -5
  26. package/src/commands/benchmark-run.js +31 -28
  27. package/src/commands/by-discussion.js +10 -10
  28. package/src/commands/callback.js +11 -11
  29. package/src/commands/discuss.js +8 -7
  30. package/src/commands/facilitate.js +15 -13
  31. package/src/commands/output.js +3 -2
  32. package/src/commands/run.js +14 -14
  33. package/src/commands/scan-logs.js +21 -19
  34. package/src/commands/selfedit.js +14 -14
  35. package/src/commands/supervise.js +11 -9
  36. package/src/commands/task-input.js +9 -9
  37. package/src/commands/tee.js +10 -9
  38. package/src/commands/trace.js +55 -42
  39. package/src/commands/work-tracker.js +4 -3
  40. package/src/cost.js +17 -17
  41. package/src/discuss-tools.js +16 -16
  42. package/src/discusser.js +39 -38
  43. package/src/events/github.js +54 -37
  44. package/src/facilitator.js +21 -21
  45. package/src/inbox-poller.js +4 -4
  46. package/src/judge.js +32 -30
  47. package/src/message-bus.js +12 -11
  48. package/src/orchestration-loop.js +35 -36
  49. package/src/orchestration-toolkit.js +58 -53
  50. package/src/orchestrator-helpers.js +2 -2
  51. package/src/profile-prompt.js +54 -53
  52. package/src/redaction.js +63 -57
  53. package/src/render/line-renderer.js +5 -5
  54. package/src/render/orchestrator-filter.js +3 -3
  55. package/src/render/palette.js +11 -9
  56. package/src/render/tool-hints.js +18 -15
  57. package/src/render/turn-renderer.js +4 -4
  58. package/src/reply-emitter.js +2 -2
  59. package/src/sequence-counter.js +4 -3
  60. package/src/signature-filter.js +7 -6
  61. package/src/supervisor.js +19 -18
  62. package/src/tee-writer.js +25 -25
  63. package/src/trace-collector.js +53 -48
  64. package/src/trace-github.js +53 -44
  65. package/src/trace-multi.js +15 -13
  66. package/src/trace-query.js +61 -52
  67. package/src/trace-render.js +18 -18
  68. package/src/trace-usage.js +31 -28
  69. package/src/transcript-recorder.js +24 -20
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * ApmInstaller — runs `apm install --target claude` in the family root to
3
- * materialise skills and agents, copies the resulting `.claude/` into a
4
- * staging directory, and computes the manifest fingerprint from the lockfile.
5
- * Per-task copy happens later in WorkdirManager.
3
+ * materialise skills and agents. It copies the resulting `.claude/` into a
4
+ * staging directory. It computes the manifest fingerprint from the lockfile.
5
+ * WorkdirManager makes the per-task copy later.
6
6
  *
7
- * Subprocess and filesystem access route through the injected `runtime` bag
8
- * (`runtime.subprocess.spawn` for the streaming `apm` child, `runtime.fs` for
9
- * the async staging copies). See `createApmInstaller` for the real-dependency
10
- * wiring; `installApm` is a thin free-function wrapper.
7
+ * Subprocess and filesystem access route through the injected `runtime` bag.
8
+ * The `apm` child streams through `runtime.subprocess.spawn`. The async
9
+ * staging copies use `runtime.fs`. See `createApmInstaller`, which wires the
10
+ * real dependencies. `installApm` is a thin free-function wrapper.
11
11
  */
12
12
 
13
13
  import { createHash } from "node:crypto";
@@ -18,7 +18,7 @@ export class ApmInstaller {
18
18
  /**
19
19
  * @param {object} deps
20
20
  * @param {import("@forwardimpact/libutil/runtime").Runtime} deps.runtime -
21
- * Ambient collaborators; uses `subprocess.spawn` and `fs`.
21
+ * Ambient collaborators. The installer uses `subprocess.spawn` and `fs`.
22
22
  */
23
23
  constructor({ runtime }) {
24
24
  if (!runtime) throw new Error("runtime is required");
@@ -30,8 +30,8 @@ export class ApmInstaller {
30
30
  * @param {string} outputDir - The benchmark run's output directory.
31
31
  * @param {object} [options]
32
32
  * @param {string|null} [options.skillsFrom] - Stage `.claude/` from this
33
- * directory instead of running apm install. The path is a root containing
34
- * a `.claude/` tree (e.g. a working tree), letting a run exercise local,
33
+ * directory and do not run apm install. The path is a root that contains
34
+ * a `.claude/` tree (e.g. a working tree). A run can then exercise local,
35
35
  * unpublished skills.
36
36
  * @returns {Promise<{stagingDir: string, skillSetHash: string, judgeProfilesDir: string}>}
37
37
  */
@@ -44,7 +44,7 @@ export class ApmInstaller {
44
44
  : join(family.rootPath, ".claude");
45
45
  const apmYml = join(family.rootPath, "apm.yml");
46
46
 
47
- // --skills-from takes precedence over apm install: the caller is supplying
47
+ // --skills-from takes precedence over apm install. The caller supplies
48
48
  // the skill tree explicitly, so no remote fetch runs.
49
49
  const hasApm =
50
50
  !skillsFrom &&
@@ -59,7 +59,7 @@ export class ApmInstaller {
59
59
  await fs.access(sourceClaude);
60
60
  } catch {
61
61
  throw new Error(
62
- `apm install did not produce .claude/ at ${sourceClaude}; check the family's apm.yml`,
62
+ `apm install did not produce .claude/ at ${sourceClaude}. Check the family's apm.yml`,
63
63
  );
64
64
  }
65
65
  }
@@ -78,18 +78,18 @@ export class ApmInstaller {
78
78
  await fs.mkdir(stagedClaude, { recursive: true });
79
79
  }
80
80
 
81
- // apm's claude target deploys a pack's skills/ into .claude/skills/ but
82
- // never its agents/ subtree (agent profiles + references). Stage that from
83
- // the installed apm_modules into .claude/agents/ so a skill that cites an
84
- // agent reference (e.g. the work-item tracker matrix) resolves in the
85
- // agent CWD. No-op when --skills-from supplied a tree or no apm_modules
86
- // exist.
81
+ // apm's claude target deploys a pack's skills/ into .claude/skills/. It
82
+ // never deploys that pack's agents/ subtree (agent profiles +
83
+ // references). Stage that subtree from the installed apm_modules into
84
+ // .claude/agents/. A skill that cites an agent reference (e.g. the
85
+ // work-item tracker matrix) then resolves in the agent CWD. This is a
86
+ // no-op when --skills-from supplied a tree or no apm_modules exist.
87
87
  if (!skillsFrom) {
88
88
  await this.#stageApmAgents(family.rootPath, stagedClaude);
89
89
  }
90
90
 
91
- // Stage the family-local judge profile outside .claude/ so it is available
92
- // to the judge but never copied into the agent-under-test's CWD.
91
+ // Stage the family-local judge profile outside .claude/, so the judge can
92
+ // reach it. Nothing copies it into the agent-under-test's CWD.
93
93
  const judgeSource = join(family.rootPath, "judge.md");
94
94
  const judgeProfilesDir = join(stagingDir, "judge-profiles");
95
95
  try {
@@ -106,7 +106,7 @@ export class ApmInstaller {
106
106
  "sha256:" +
107
107
  createHash("sha256").update(normalizeLf(lockBytes)).digest("hex");
108
108
  } catch {
109
- // No lockfile — family doesn't use skill packs.
109
+ // No lockfile. The family doesn't use skill packs.
110
110
  }
111
111
 
112
112
  return { stagingDir, skillSetHash, judgeProfilesDir };
@@ -115,8 +115,8 @@ export class ApmInstaller {
115
115
  /**
116
116
  * Merge each installed pack's `agents/` subtree (profiles + references) from
117
117
  * `apm_modules/<owner>/<pack>/agents/` into the staged `.claude/agents/`.
118
- * apm's claude target deploys `skills/` only, so without this an agent
119
- * reference a skill cites is absent from the agent CWD.
118
+ * apm's claude target deploys `skills/` only. Without this merge, an agent
119
+ * reference that a skill cites is absent from the agent CWD.
120
120
  * @param {string} familyRoot
121
121
  * @param {string} stagedClaude
122
122
  */
@@ -127,7 +127,7 @@ export class ApmInstaller {
127
127
  try {
128
128
  owners = await fs.readdir(modulesRoot, { withFileTypes: true });
129
129
  } catch {
130
- return; // no apm_modules — nothing to stage
130
+ return; // no apm_modules, so nothing to stage
131
131
  }
132
132
  const stagedAgents = join(stagedClaude, "agents");
133
133
  for (const owner of owners) {
@@ -159,8 +159,8 @@ export class ApmInstaller {
159
159
  ["install", "--target", "claude"],
160
160
  { cwd, stdio: ["ignore", "pipe", "pipe"] },
161
161
  );
162
- // Drain stdout concurrently so the child never blocks on backpressure;
163
- // capture stderr for the failure message.
162
+ // Drain stdout concurrently so the child never blocks on backpressure.
163
+ // Capture stderr for the failure message.
164
164
  let stderr = "";
165
165
  const drainStdout = (async () => {
166
166
  for await (const _chunk of child.stdout) {
@@ -199,8 +199,8 @@ export function createApmInstaller(deps) {
199
199
  * @param {import("./task-family.js").TaskFamily} family
200
200
  * @param {string} outputDir
201
201
  * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
202
- * @param {object} [options] - Forwarded to `ApmInstaller.install` (e.g.
203
- * `{ skillsFrom }`).
202
+ * @param {object} [options] - This function forwards these to
203
+ * `ApmInstaller.install` (e.g. `{ skillsFrom }`).
204
204
  */
205
205
  export function installApm(family, outputDir, runtime, options = {}) {
206
206
  return new ApmInstaller({ runtime }).install(family, outputDir, options);
@@ -1,17 +1,20 @@
1
1
  /**
2
2
  * Env-loader — auto-discover `.env` / `.env.local` files in a task family
3
- * and its tasks, load them into `process.env`, and render the merged result
3
+ * and its tasks. Load them into `process.env`. Render the merged result
4
4
  * into each agent CWD.
5
5
  *
6
- * Discovery paths (loaded in this order, first value per key wins):
7
- * 1. process.env (CI secrets, shell env — never overwritten)
6
+ * The loader reads the discovery paths in this order. The first value per
7
+ * key wins:
8
+ * 1. process.env (CI secrets and shell env, which the loader never
9
+ * overwrites)
8
10
  * 2. <family>/.env.local
9
11
  * 3. <family>/.env
10
12
  * 4. tasks/<id>/.env.local
11
13
  * 5. tasks/<id>/.env
12
14
  *
13
- * Every discovered env file — family or task — is loaded into process.env
14
- * AND rendered (with resolved values) into the agent working directory.
15
+ * The loader loads every discovered env file, family or task, into
16
+ * process.env. It also renders the file (with resolved values) into the
17
+ * agent working directory.
15
18
  */
16
19
 
17
20
  import { join } from "node:path";
@@ -20,7 +23,8 @@ const ENV_FILES = [".env.local", ".env"];
20
23
 
21
24
  /**
22
25
  * Parse a `.env` file into an array of {key, value} pairs.
23
- * Handles KEY=VALUE, # comments, blank lines, and single/double-quoted values.
26
+ * It handles KEY=VALUE, # comments, blank lines, and single/double-quoted
27
+ * values.
24
28
  * @param {string} content
25
29
  * @returns {Array<{key: string, value: string}>}
26
30
  */
@@ -46,7 +50,7 @@ export function parseEnvFile(content) {
46
50
  }
47
51
 
48
52
  /**
49
- * Read and parse an env file, returning [] if the file does not exist.
53
+ * Read and parse an env file. Return [] if the file does not exist.
50
54
  * @param {object} fs - Async filesystem surface (`runtime.fs`).
51
55
  * @param {string} filePath
52
56
  * @returns {Promise<Array<{key: string, value: string}>>}
@@ -62,10 +66,11 @@ async function readEnvFile(fs, filePath) {
62
66
  }
63
67
 
64
68
  /**
65
- * Load entries into the process env map. Existing keys are never overwritten.
69
+ * Load entries into the process env map. This function never overwrites an
70
+ * existing key.
66
71
  * @param {Record<string, string|undefined>} env - The `runtime.proc.env` map.
67
72
  * @param {Array<{key: string, value: string}>} entries
68
- * @returns {string[]} var names that were loaded
73
+ * @returns {string[]} the var names it loaded
69
74
  */
70
75
  function applyToProcessEnv(env, entries) {
71
76
  const names = [];
@@ -79,7 +84,8 @@ function applyToProcessEnv(env, entries) {
79
84
  }
80
85
 
81
86
  /**
82
- * Load one env file: apply to the env map, record keys in the merged map.
87
+ * Load one env file. Apply it to the env map. Record the keys in the merged
88
+ * map.
83
89
  * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
84
90
  * @param {string} dir
85
91
  * @param {string} file
@@ -100,7 +106,7 @@ async function loadOneEnvFile(runtime, dir, file, names, merged) {
100
106
  }
101
107
 
102
108
  /**
103
- * Scan directories for env files, load into the env map, and collect
109
+ * Scan directories for env files. Load them into the env map. Collect
104
110
  * a merged key manifest per filename.
105
111
  * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
106
112
  * @param {string[]} dirs
@@ -118,7 +124,7 @@ async function collectEnvEntries(runtime, dirs) {
118
124
  }
119
125
 
120
126
  /**
121
- * Write resolved env files into the agent CWD and warn about empty values.
127
+ * Write resolved env files into the agent CWD. Warn about empty values.
122
128
  * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
123
129
  * @param {Map<string, Map<string, true>>} merged
124
130
  * @param {string} agentCwd
@@ -142,14 +148,16 @@ async function renderEnvFiles(runtime, merged, agentCwd) {
142
148
  }
143
149
 
144
150
  /**
145
- * Discover `.env` / `.env.local` in one or more directories, load them
146
- * into the process env map, and render the resolved values into the agent CWD.
151
+ * Discover `.env` / `.env.local` in one or more directories. Load them
152
+ * into the process env map. Render the resolved values into the agent CWD.
147
153
  *
148
154
  * @param {string[]} dirs - Directories to scan (family root, task dir, etc.)
149
155
  * @param {string} agentCwd - Agent working directory to render into.
150
156
  * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime - Ambient
151
- * collaborators; uses `fs` (async read/write), `proc.env`, `proc.stderr`.
152
- * @returns {Promise<string[]>} All var names discovered (for redaction).
157
+ * collaborators. It uses `fs` (async read/write), `proc.env`, and
158
+ * `proc.stderr`.
159
+ * @returns {Promise<string[]>} Every var name the loader discovered (for
160
+ * redaction).
153
161
  */
154
162
  export async function loadEnv(dirs, agentCwd, runtime) {
155
163
  const { names, merged } = await collectEnvEntries(runtime, dirs);
@@ -1,46 +1,50 @@
1
1
  /**
2
- * Grading derivation — the sole home of the check-row arithmetic.
2
+ * Grade derivation — the sole home of the check-row arithmetic.
3
3
  *
4
- * Check rows are the single authoritative grading channel. Every row is a
5
- * check by default; a row declares its role with its own fields, checked in
6
- * order:
4
+ * Check rows are the one authoritative input to the grade. Every row is a
5
+ * check by default. A row declares its role with its own fields. The
6
+ * classifier checks the roles in this order:
7
7
  *
8
8
  * 1. Gate — `gate` is exactly `true`, `pass` is boolean, and no
9
- * `weight` key is present. Any failing gate → `gatesPass`
10
- * false.
11
- * 2. Diagnostic — no `gate` key and `weight` is exactly `0`. Free-form;
12
- * never graded.
9
+ * `weight` key is present. A gate that fails sets
10
+ * `gatesPass` to false.
11
+ * 2. Diagnostic — no `gate` key and `weight` is exactly `0`. The row is
12
+ * free-form. The grader never scores it.
13
13
  * 3. Scored — no `gate` key, boolean `pass`, `weight` absent (defaults
14
14
  * to 1) or finite > 0.
15
- * 4. Malformed — everything else: any `gate`+`weight` co-occurrence (a
16
- * stray weight must never silently disarm a gate), a
15
+ * 4. Malformed — everything else: any `gate`+`weight` co-occurrence, a
17
16
  * non-boolean `gate`, a missing or non-boolean `pass` on a
18
17
  * graded row, an invalid `weight`, an fd-3 line that failed
19
- * to parse, a non-object row. Counts as a **failing scored
20
- * check** — dropping a defect could mint full marks;
21
- * failing the whole run would zero completed work.
18
+ * to parse, a non-object row. A stray weight must never
19
+ * silently disarm a gate. A malformed row counts as a
20
+ * **scored check that fails**. If the grader dropped the
21
+ * defect, the cell could mint full marks. If it failed the
22
+ * whole run, it would zero completed work.
22
23
  *
23
- * The producers' `source` stamp is display metadata, never a grading input.
24
+ * The producers' `source` stamp is display metadata. The grader never reads
25
+ * it.
24
26
  */
25
27
 
26
28
  /**
27
29
  * @typedef {object} GradeResult
28
30
  * @property {"pass" | "fail"} verdict - `healthy ∧ gatesPass ∧ fullMarks`.
29
31
  * @property {boolean} gatesPass - Every gate row passes (vacuously true).
30
- * @property {number | null} score - Weighted fraction of passing scored
31
- * checks; `null` when the cell has zero scored checks (binary task).
32
- * @property {boolean} fullMarks - Integer count predicate: no malformed rows
33
- * and every scored check passes. Never a float comparison, so fractional
34
- * weights carry no equality hazard. Vacuously true with zero scored checks.
32
+ * @property {number | null} score - Weighted fraction of the scored checks
33
+ * that pass. It is `null` when the cell has zero scored checks (binary
34
+ * task).
35
+ * @property {boolean} fullMarks - An integer count predicate. It is true when
36
+ * no row is malformed and every scored check passes. It is never a float
37
+ * comparison, so fractional weights carry no equality hazard. It is
38
+ * vacuously true with zero scored checks.
35
39
  * @property {number} malformed - Malformed row count.
36
40
  */
37
41
 
38
42
  /**
39
43
  * Grade the merged check rows against grader health.
40
44
  *
41
- * `healthy` is the completion signal a crashed grader cannot fake: when it is
42
- * false the verdict is `fail` whatever the rows say, so a hook that dies
43
- * after emitting passing rows can never mint marks.
45
+ * `healthy` is the completion signal a crashed grader cannot fake. When it is
46
+ * false, the verdict is `fail` whatever the rows say. A hook that emits rows
47
+ * that pass and then dies can never mint marks.
44
48
  * @param {unknown[]} details - Merged check rows from both producers.
45
49
  * @param {boolean} healthy - Invariants exited 0 AND the hidden-test engine
46
50
  * did not throw.
@@ -73,7 +77,7 @@ export function gradeChecks(details, healthy) {
73
77
  }
74
78
 
75
79
  /**
76
- * Fold one row into the running tally per its classified role.
80
+ * Fold one row into the tally per its classified role.
77
81
  * @param {{gatesPass: boolean, malformed: number, scored: number, passing: number, weightAll: number, weightPassing: number}} tally
78
82
  * @param {unknown} row
79
83
  */
@@ -96,11 +100,10 @@ function tallyRow(tally, row) {
96
100
  }
97
101
 
98
102
  /**
99
- * Run both check-row producers and grade the merged rows — the one
100
- * composition shared by the runner and the `grade` subcommand. An engine
101
- * throw is grader fault: its message lands on the returned `engineError`
102
- * and health fails, so a crashed grader can never mint marks from rows it
103
- * happened to emit first.
103
+ * Run both check-row producers and grade the merged rows. The runner and the
104
+ * `grade` subcommand share this one composition. An engine throw is grader
105
+ * fault. Its message lands on the returned `engineError` and health fails. A
106
+ * crashed grader can never mint marks from rows it emitted first.
104
107
  * @param {import("./task-family.js").Task} task
105
108
  * @param {{cwd: string, port: number, runDir: string, familyDir?: string|null}} ctx
106
109
  * @param {import("@forwardimpact/libutil/runtime").Runtime} runtime
@@ -126,8 +129,8 @@ export async function runProducersAndGrade(task, ctx, runtime, producers) {
126
129
 
127
130
  /**
128
131
  * Merge the two producers' rows (invariants first) and stamp each row's
129
- * provenance. The stamp is display metadata, never a grading input, and
130
- * non-object rows (malformed by contract) pass through verbatim.
132
+ * provenance. The stamp is display metadata. The grader never reads it.
133
+ * Non-object rows (malformed by contract) pass through verbatim.
131
134
  * @param {unknown[]} invariantsDetails
132
135
  * @param {unknown[]} hiddenDetails
133
136
  * @returns {unknown[]}
@@ -147,9 +150,9 @@ function stampSource(row, source) {
147
150
  }
148
151
 
149
152
  /**
150
- * Project the raw `gradeChecks` return onto the record schema: `fullMarks`
151
- * is derivable and dropped, `score` is omitted on binary tasks (`null`),
152
- * `malformed` is omitted when clean.
153
+ * Project the raw `gradeChecks` return onto the record schema. `fullMarks` is
154
+ * derivable, so this function drops it. It omits `score` on binary tasks
155
+ * (`null`). It omits `malformed` when the rows are clean.
153
156
  * @param {GradeResult} raw
154
157
  * @returns {{verdict: "pass"|"fail", gatesPass: boolean, score?: number, malformed?: number}}
155
158
  */
@@ -177,9 +180,9 @@ function classifyRow(row) {
177
180
  }
178
181
 
179
182
  /**
180
- * A row carrying a `gate` key: valid only as `gate: true` with a boolean
181
- * `pass` and no `weight` key — any co-occurring weight is malformed so a
182
- * stray weight can never silently disarm a gate.
183
+ * Classify a row that has a `gate` key. The row is valid only as `gate: true`
184
+ * with a boolean `pass` and no `weight` key. Any weight that co-occurs makes
185
+ * the row malformed. A stray weight can never silently disarm a gate.
183
186
  * @param {object} row
184
187
  * @returns {"gate" | "malformed"}
185
188
  */
@@ -191,9 +194,9 @@ function classifyGateRow(row) {
191
194
  }
192
195
 
193
196
  /**
194
- * A gate-less row carrying a `weight` key: exactly 0 is a diagnostic, a
195
- * finite positive weight with a boolean `pass` is scored, anything else is
196
- * malformed.
197
+ * Classify a gate-less row that has a `weight` key. A weight of exactly 0 is
198
+ * a diagnostic. A finite positive weight with a boolean `pass` is scored.
199
+ * Anything else is malformed.
197
200
  * @param {object} row
198
201
  * @returns {"diagnostic" | "scored" | "malformed"}
199
202
  */
@@ -205,8 +208,8 @@ function classifyWeightedRow(row) {
205
208
  }
206
209
 
207
210
  /**
208
- * A malformed row fails at its own weight when it carries a valid positive
209
- * one, else at unit weight 1.
211
+ * A malformed row fails at its own weight when that weight is valid and
212
+ * positive. Otherwise it fails at unit weight 1.
210
213
  * @param {unknown} row
211
214
  * @returns {number}
212
215
  */
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Hidden-test engine — executes a task's `tests/` overlay against the
3
- * post-run agent CWD: stage each file at its mirrored path, run each check
4
- * with `node --test`, convert the exit status into one check row, and
5
- * restore the tree so the judge sees the workdir exactly as the agent left
6
- * it.
2
+ * Hidden-test engine — runs a task's `tests/` overlay against the post-run
3
+ * agent CWD. The engine stages each file at its mirrored path. It runs each
4
+ * check with `node --test`. It converts the exit status into one check row.
5
+ * It restores the tree, so the judge sees the workdir exactly as the agent
6
+ * left it.
7
7
  *
8
- * Fault attribution is the engine's contract: a stage or spawn failure (the
9
- * agent deleted the scaffold) is a *failing row* — agent fault; the engine
10
- * itself throwing is grader fault, which the caller records as unhealthy so
11
- * a crashed grader can never mint marks.
8
+ * Fault attribution is the engine's contract. A stage or spawn failure (the
9
+ * agent deleted the scaffold) is agent fault, so the engine returns a *row
10
+ * that fails*. A throw from the engine itself is grader fault. The caller
11
+ * records that as unhealthy, so a crashed grader can never mint marks.
12
12
  */
13
13
 
14
14
  import { dirname, join } from "node:path";
@@ -16,8 +16,8 @@ import { dirname, join } from "node:path";
16
16
  import { buildHookEnv } from "./hook-env.js";
17
17
 
18
18
  // Fixed per-check budget. A wedged test process runs outside the agent
19
- // watchdog, so this bound is what keeps a hung hidden test from stalling the
20
- // cell; the timeout row keeps the failure visible.
19
+ // watchdog. Without this bound, a hung hidden test would stall the cell. The
20
+ // timeout row keeps the failure visible.
21
21
  const CHECK_TIMEOUT_MS = 120_000;
22
22
  const STDERR_TAIL_CHARS = 500;
23
23
 
@@ -50,9 +50,9 @@ export async function runHiddenTests(task, ctx, runtime, opts = {}) {
50
50
  }
51
51
 
52
52
  /**
53
- * Stage one check, run it, and restore its staging — the check's own row is
54
- * the only trace it leaves. A stage failure is the agent's fault (a deleted
55
- * scaffold), so it becomes a failing row rather than a throw.
53
+ * Stage one check, run it, then put the tree back. The check's own row is the
54
+ * only trace it leaves. A stage failure is the agent's fault (a deleted
55
+ * scaffold). The engine returns a row that fails. It does not throw.
56
56
  */
57
57
  async function runOneCheck(task, ctx, runtime, timeoutMs, check) {
58
58
  const stager = newStager();
@@ -71,7 +71,8 @@ async function runOneCheck(task, ctx, runtime, timeoutMs, check) {
71
71
  /**
72
72
  * Spawn `node --test <staged path>` from the agent CWD under the hook env
73
73
  * and map the exit status onto one row. The clock timer SIGKILLs a child
74
- * that outlives the per-check budget; the row fails with a timeout message.
74
+ * that outlives the per-check budget. The row then fails with a timeout
75
+ * message.
75
76
  */
76
77
  async function spawnCheck(task, ctx, runtime, timeoutMs, check) {
77
78
  const env = buildHookEnv(runtime.proc.env, {
@@ -83,8 +84,8 @@ async function spawnCheck(task, ctx, runtime, timeoutMs, check) {
83
84
  familyDir: ctx.familyDir,
84
85
  });
85
86
  // An inherited test-runner context makes the child `node --test` report
86
- // exit 0 even when its tests fail — a failing check would mint a passing
87
- // row whenever the harness itself runs under `node --test`.
87
+ // exit 0 even when its tests fail. A check that fails would then mint a row
88
+ // that passes whenever the harness itself runs under `node --test`.
88
89
  delete env.NODE_TEST_CONTEXT;
89
90
  const child = runtime.subprocess.spawn("node", ["--test", check.stagePath], {
90
91
  cwd: ctx.cwd,
@@ -129,8 +130,8 @@ function newStager() {
129
130
  }
130
131
 
131
132
  /**
132
- * Copy the symlink-resolved source to its mirrored path under the agent CWD,
133
- * backing up a collided file's bytes and tracking every directory created so
133
+ * Copy the symlink-resolved source to its mirrored path under the agent CWD.
134
+ * Back up the bytes of a collided file. Track every directory created, so
134
135
  * `unstage` can put the tree back exactly.
135
136
  */
136
137
  async function stageFile(fs, cwd, stager, { sourcePath, stagePath }) {
@@ -154,7 +155,7 @@ async function ensureParents(fs, cwd, stager, dir) {
154
155
  await fs.access(dir);
155
156
  return;
156
157
  } catch {
157
- // missing — create below
158
+ // missing, so create it below
158
159
  }
159
160
  await ensureParents(fs, cwd, stager, dirname(dir));
160
161
  await fs.mkdir(dir);
@@ -162,10 +163,10 @@ async function ensureParents(fs, cwd, stager, dir) {
162
163
  }
163
164
 
164
165
  /**
165
- * Reverse the staging: staged copies out, collided bytes back, created
166
- * directories removed (deepest first — a check's own artifacts inside a
167
- * created directory go with it, since that directory did not exist when the
168
- * agent finished).
166
+ * Reverse the stage step. Remove the staged copies. Write the collided bytes
167
+ * back. Remove the created directories, deepest first. A check's own
168
+ * artifacts inside a created directory go with it, because that directory did
169
+ * not exist when the agent finished.
169
170
  */
170
171
  async function unstage(fs, stager) {
171
172
  for (const target of stager.staged) {
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Shared environment builder for the benchmark hook scripts (`preflight.sh` and
3
- * `invariants.sh`). Keeping both spawns on one helper guarantees they expose the
4
- * same variable set, so hook authors never have to wonder which vars a given
5
- * hook receives.
2
+ * Shared environment builder for the benchmark hook scripts (`preflight.sh`
3
+ * and `invariants.sh`). One helper serves both spawns, so both expose the
4
+ * same variable set. Hook authors never have to wonder which vars a hook
5
+ * receives.
6
6
  *
7
7
  * Path vars (TASK_DIR, FAMILY_DIR, HOOKS_DIR) let hooks reference real
8
- * locations instead of reconstructing them from `$0`. They are paths, not
9
- * secrets, so they need no redaction allowlist entry.
8
+ * locations. Hooks do not have to rebuild the locations from `$0`. They are
9
+ * paths. They are not secrets, so they need no entry in the redaction
10
+ * allowlist.
10
11
  */
11
12
 
12
13
  /**
@@ -27,9 +28,10 @@ export function buildHookEnv(
27
28
  ) {
28
29
  return {
29
30
  ...baseEnv,
30
- // The agent CWD itself — hooks reference emitted files as `$AGENT_CWD/<path>`.
31
- // Distinct from the `invariants` CLI's `--run-dir` (the parent that
32
- // *contains* `cwd/`), so the two are never confused.
31
+ // The agent CWD itself. Hooks reference emitted files as
32
+ // `$AGENT_CWD/<path>`. This var is distinct from the `invariants` CLI's
33
+ // `--run-dir` (the parent that *contains* `cwd/`), so the two are never
34
+ // confused.
33
35
  AGENT_CWD: cwd,
34
36
  PORT: String(port),
35
37
  TASK_ID: taskId,
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * Invariants — runs `<task.paths.hooks>/invariants.sh` from the template path
3
- * against the post-run agent CWD. A pure collector with no verdict of its
4
- * own: structured per-check rows arrive on fd 3 (`$RESULTS_FD=3`) as NDJSON
5
- * and grading happens downstream over the merged rows. The exit code is
6
- * script health only — nonzero means the grader itself failed, never that a
7
- * check failed.
3
+ * against the post-run agent CWD. The module is a pure collector with no
4
+ * verdict of its own. Structured per-check rows arrive on fd 3
5
+ * (`$RESULTS_FD=3`) as NDJSON. Downstream code grades the merged rows. The
6
+ * exit code is script health only. A nonzero code means the grader itself
7
+ * failed. It never means that a check failed.
8
8
  *
9
- * Subprocess access flows through `runtime.subprocess.spawn`; the fd-3 backing
10
- * store and the stderr log use the sync filesystem surface (`runtime.fsSync`) —
11
- * the only surface this module touches, per design Decision 7.
9
+ * Subprocess access flows through `runtime.subprocess.spawn`. The fd-3
10
+ * backing store and the stderr log use the sync filesystem surface
11
+ * (`runtime.fsSync`). That surface is the only one this module touches, per
12
+ * design Decision 7.
12
13
  */
13
14
 
14
15
  import { join } from "node:path";
@@ -18,11 +19,12 @@ import { buildHookEnv } from "./hook-env.js";
18
19
  /**
19
20
  * @typedef {object} InvariantsResult
20
21
  * @property {Array<object>} details
21
- * @property {number} exitCode - Script health: nonzero means the hook itself
22
- * failed, never that a check failed.
22
+ * @property {number} exitCode - Script health. A nonzero code means the hook
23
+ * itself failed. It never means that a check failed.
23
24
  * @property {string} [stderr] - Trimmed script stderr, present only when the
24
- * script wrote to stderr. Surfaces hook failures (e.g. a missing tool) that
25
- * leave `details` empty, so they read distinctly from a real invariant miss.
25
+ * script wrote to stderr. This field surfaces hook failures (e.g. a missing
26
+ * tool) that leave `details` empty. A reader can then tell them apart from
27
+ * a real invariant miss.
26
28
  */
27
29
 
28
30
  /**
@@ -43,8 +45,8 @@ export async function runInvariants(task, ctx, runtime) {
43
45
 
44
46
  // Bun's child_process pipe setup for fd >= 3 is racy under load (it
45
47
  // creates a unix socket pair and the connect() can return ENOENT). Use
46
- // a temp file as the fd-3 backing store instead — the script still
47
- // writes via `$RESULTS_FD`, but we hand it a real file descriptor.
48
+ // a temp file as the fd-3 backing store instead. The script still writes
49
+ // through `$RESULTS_FD`, but we hand it a real file descriptor.
48
50
  const fd3Path = join(ctx.runDir, "invariants.fd3.ndjson");
49
51
  const fd3File = fsSync.openSync(fd3Path, "w+");
50
52
 
@@ -69,7 +71,8 @@ export async function runInvariants(task, ctx, runtime) {
69
71
  throw e;
70
72
  }
71
73
 
72
- // Drain stdout (do not require consumers to read it); capture stderr to log.
74
+ // Drain stdout (do not require consumers to read it). Capture stderr to
75
+ // the log.
73
76
  const drainStdout = (async () => {
74
77
  for await (const _chunk of child.stdout) {
75
78
  // discard
@@ -127,8 +130,8 @@ function readAndUnlink(fsSync, path) {
127
130
  }
128
131
 
129
132
  /**
130
- * Parse the fd-3 buffer (read from the temp-file backing) into one NDJSON
131
- * row per detail entry.
133
+ * Parse the fd-3 buffer (read from the temp-file backing store) into one
134
+ * NDJSON row per detail entry.
132
135
  */
133
136
  function parseFd3Buffer(buf, details) {
134
137
  if (!buf) return;