@gethmy/harness 1.2.1 → 1.3.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/dist/cli.js +489 -225
- package/dist/index.js +1222 -401
- package/package.json +2 -2
- package/src/ci-failure.ts +465 -0
- package/src/cli.ts +11 -1
- package/src/confine-to-repo.test.ts +324 -1
- package/src/confine-to-repo.ts +274 -22
- package/src/error-classifier.ts +52 -1
- package/src/gate-collectors.ts +11 -3
- package/src/git-pr.ts +461 -8
- package/src/index.ts +2 -0
- package/src/model-tier.test.ts +11 -6
- package/src/model-tier.ts +4 -4
- package/src/oracle-collector.ts +244 -23
- package/src/oracle.ts +856 -108
- package/src/pm.ts +15 -5
- package/src/repair-sandbox.test.ts +116 -0
- package/src/repair-sandbox.ts +303 -0
- package/src/run-sizing.test.ts +264 -66
- package/src/run-sizing.ts +146 -26
- package/src/sdk-agent-runner.ts +22 -1
package/src/oracle-collector.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Evidence
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Evidence collectors for the two held-test gates — `oracle_passed` (green: the
|
|
3
|
+
* held test must pass) and `oracle_red` (#921, red: it must run and fail). Both
|
|
4
|
+
* run only AFTER the stage's subagent has exited (see stage-run.ts): the held
|
|
5
|
+
* test and a model must never share a filesystem window.
|
|
6
|
+
*
|
|
7
|
+
* ONE held test serves both. The author stage writes it keyed to the downstream
|
|
8
|
+
* stage whose gate is `oracle_passed`; the red gate reads that same row one stage
|
|
9
|
+
* earlier (`OracleDeps.targetStageId`). So a `Fix a Bug` chain is red-green over a
|
|
10
|
+
* single contract: the author's test must fail before the fix and pass after it,
|
|
11
|
+
* and the implementer never sees it in either direction.
|
|
12
|
+
*
|
|
13
|
+
* The two polarities are NOT mirror images, and `HeldOracleCollector`'s doc
|
|
14
|
+
* comment explains where they diverge and why the divergence is one-directional.
|
|
5
15
|
*
|
|
6
16
|
* It produces evidence and nothing else — `gateEvaluate` in @harmony/shared turns
|
|
7
17
|
* it into a verdict. A missing oracle is `blocked`, never `passed`: a gate with no
|
|
@@ -27,28 +37,89 @@ import type {
|
|
|
27
37
|
GateEvidenceCollector,
|
|
28
38
|
GateEvidenceContext,
|
|
29
39
|
} from "@harmony/shared";
|
|
40
|
+
import { GATE_CONFIG_ERROR_MARK } from "./gate-config-error.js";
|
|
30
41
|
import { log } from "./log.js";
|
|
31
|
-
import
|
|
42
|
+
import {
|
|
43
|
+
gradeOracleRed,
|
|
44
|
+
type HeldOracle,
|
|
45
|
+
type OracleDeps,
|
|
46
|
+
type OracleRunSummary,
|
|
47
|
+
} from "./oracle.js";
|
|
32
48
|
|
|
33
49
|
const TAG = "oracle-collector";
|
|
34
50
|
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
/** The digest identifying the exact contract a verdict graded (card #927). */
|
|
52
|
+
interface OracleIdentity {
|
|
53
|
+
oracleId: string | null;
|
|
54
|
+
contentHash: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The shared half of both held-test gates: fetch the oracle, place it, run it,
|
|
59
|
+
* remove it — always. Only the two things that genuinely differ per polarity are
|
|
60
|
+
* abstract:
|
|
61
|
+
*
|
|
62
|
+
* - {@link oracleStageId} — WHICH stage the oracle row is keyed to. The green
|
|
63
|
+
* gate's own stage IS the target; the red gate's is one stage earlier.
|
|
64
|
+
* - {@link verdict} — how a completed run is graded.
|
|
65
|
+
*
|
|
66
|
+
* Everything else is deliberately NOT duplicated. `removeBestEffort` and the
|
|
67
|
+
* `finally` that calls it are the security-critical part of this module (a held
|
|
68
|
+
* test left in the worktree gets auto-committed and hands the implementer the
|
|
69
|
+
* one file it must never see), so there is exactly one copy of them and both
|
|
70
|
+
* gates inherit it. A second polarity that quietly forgot the `finally` is the
|
|
71
|
+
* failure this shape exists to make impossible.
|
|
72
|
+
*/
|
|
73
|
+
abstract class HeldOracleCollector {
|
|
74
|
+
constructor(protected readonly deps: OracleDeps) {}
|
|
37
75
|
|
|
38
|
-
|
|
76
|
+
/** Which stage's held oracle this gate grades. `null` ⇒ nothing to grade. */
|
|
77
|
+
protected abstract oracleStageId(context: GateEvidenceContext): string | null;
|
|
78
|
+
|
|
79
|
+
/** Grade a run that produced an exit code. Never sees a run that threw. */
|
|
80
|
+
protected abstract verdict(args: {
|
|
81
|
+
oracle: HeldOracle;
|
|
82
|
+
identity: OracleIdentity;
|
|
83
|
+
exitCode: number;
|
|
84
|
+
/** stdout + stderr merged — the operator's log. NOT the verdict channel. */
|
|
85
|
+
output: string;
|
|
86
|
+
/**
|
|
87
|
+
* The runner's own machine report, read from a file outside the worktree
|
|
88
|
+
* (#1019) — the red gate's only graded input. `null` ⇒ no trustworthy
|
|
89
|
+
* count, which that gate turns into `blocked`.
|
|
90
|
+
*/
|
|
91
|
+
report: OracleRunSummary | null;
|
|
92
|
+
}): GateEvidence;
|
|
39
93
|
|
|
40
94
|
async collect(context: GateEvidenceContext): Promise<GateEvidence> {
|
|
95
|
+
const stageId = this.oracleStageId(context);
|
|
96
|
+
if (!stageId) {
|
|
97
|
+
// Reached only by a red gate on a stage with no downstream
|
|
98
|
+
// `oracle_passed` stage — a playbook shape defect, not a fact about the
|
|
99
|
+
// card's code. So it carries the #823 config-error mark: re-running the
|
|
100
|
+
// stage cannot change the answer, and the engine should hold for a human
|
|
101
|
+
// instead of spending the card's whole attempt budget rediscovering it.
|
|
102
|
+
const reason =
|
|
103
|
+
`No stage downstream of ${context.stageId} declares an \`oracle_passed\` gate, ` +
|
|
104
|
+
"so there is no held test for this gate to grade. A red gate needs a later stage that runs the same test green.";
|
|
105
|
+
log.warn(TAG, `${reason} — blocked (config)`);
|
|
106
|
+
return {
|
|
107
|
+
result: "blocked",
|
|
108
|
+
structured: { reason, ...GATE_CONFIG_ERROR_MARK },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
41
112
|
const oracle = await this.deps.fetchOracle(
|
|
42
113
|
context.cardId,
|
|
43
|
-
|
|
114
|
+
stageId,
|
|
44
115
|
this.deps.sessionId,
|
|
45
116
|
);
|
|
46
117
|
if (!oracle) {
|
|
47
|
-
log.info(TAG, `No oracle held for stage ${
|
|
118
|
+
log.info(TAG, `No oracle held for stage ${stageId} — blocked`);
|
|
48
119
|
return {
|
|
49
120
|
result: "blocked",
|
|
50
121
|
structured: {
|
|
51
|
-
reason: `No oracle is held for stage ${
|
|
122
|
+
reason: `No oracle is held for stage ${stageId}.`,
|
|
52
123
|
},
|
|
53
124
|
};
|
|
54
125
|
}
|
|
@@ -68,7 +139,7 @@ export class OracleCollector implements GateEvidenceCollector {
|
|
|
68
139
|
};
|
|
69
140
|
await this.deps.place(this.deps.repoPath, oracle);
|
|
70
141
|
try {
|
|
71
|
-
const { exitCode, output } = await this.deps.run(
|
|
142
|
+
const { exitCode, output, report } = await this.deps.run(
|
|
72
143
|
this.deps.repoPath,
|
|
73
144
|
oracle,
|
|
74
145
|
);
|
|
@@ -81,18 +152,13 @@ export class OracleCollector implements GateEvidenceCollector {
|
|
|
81
152
|
} else {
|
|
82
153
|
log.warn(TAG, logLine);
|
|
83
154
|
}
|
|
84
|
-
return {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
output:
|
|
92
|
-
"withheld — oracle_passed is a secrecy gate; see the motor's local log",
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
};
|
|
155
|
+
return this.verdict({
|
|
156
|
+
oracle,
|
|
157
|
+
identity,
|
|
158
|
+
exitCode,
|
|
159
|
+
output,
|
|
160
|
+
report: report ?? null,
|
|
161
|
+
});
|
|
96
162
|
} catch (err) {
|
|
97
163
|
const message = errText(err);
|
|
98
164
|
log.warn(TAG, `Oracle run threw: ${message} — blocked`);
|
|
@@ -157,6 +223,161 @@ export class OracleCollector implements GateEvidenceCollector {
|
|
|
157
223
|
}
|
|
158
224
|
}
|
|
159
225
|
|
|
226
|
+
/**
|
|
227
|
+
* The GREEN gate: `oracle_passed`. The held test must pass against the
|
|
228
|
+
* implementer's worktree.
|
|
229
|
+
*
|
|
230
|
+
* Grades on the exit code alone, unchanged since Task 12. That is safe here
|
|
231
|
+
* precisely because it is the conservative direction: a run that could not
|
|
232
|
+
* start exits non-zero and reads as `failed`, which under-grants. Do not
|
|
233
|
+
* "improve" this to consult the runner's summary — the red gate needs that
|
|
234
|
+
* because its safe direction is the opposite one, and giving both gates the
|
|
235
|
+
* same machinery would trade a proven behavior for a symmetry nobody needs.
|
|
236
|
+
*/
|
|
237
|
+
export class OracleCollector
|
|
238
|
+
extends HeldOracleCollector
|
|
239
|
+
implements GateEvidenceCollector
|
|
240
|
+
{
|
|
241
|
+
readonly kind = "oracle_passed" as const;
|
|
242
|
+
|
|
243
|
+
/** The stage running this gate IS the stage the oracle is keyed to. */
|
|
244
|
+
protected oracleStageId(context: GateEvidenceContext): string | null {
|
|
245
|
+
return context.stageId;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
protected verdict({
|
|
249
|
+
oracle,
|
|
250
|
+
identity,
|
|
251
|
+
exitCode,
|
|
252
|
+
}: {
|
|
253
|
+
oracle: HeldOracle;
|
|
254
|
+
identity: OracleIdentity;
|
|
255
|
+
exitCode: number;
|
|
256
|
+
output: string;
|
|
257
|
+
report: OracleRunSummary | null;
|
|
258
|
+
}): GateEvidence {
|
|
259
|
+
return {
|
|
260
|
+
result: exitCode === 0 ? "passed" : "failed",
|
|
261
|
+
structured: {
|
|
262
|
+
oracle: {
|
|
263
|
+
exitCode,
|
|
264
|
+
path: oracle.path,
|
|
265
|
+
...identity,
|
|
266
|
+
output:
|
|
267
|
+
"withheld — oracle_passed is a secrecy gate; see the motor's local log",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The RED gate: `oracle_red` (#921). The same held test must RUN and FAIL
|
|
276
|
+
* against the current tree — the reproduction half of red-green.
|
|
277
|
+
*
|
|
278
|
+
* Why this is not `oracle_passed` with the verdict flipped: a red gate passes on
|
|
279
|
+
* failure, so anything it misreads as a failure it OVER-grants. And the exit code
|
|
280
|
+
* cannot tell a real failure from a runner that never started — measured against
|
|
281
|
+
* the real allow-listed runners, `npx --no-install vitest` with vitest absent,
|
|
282
|
+
* `bun test` on an unmatched path, and a genuine assertion failure all exit 1.
|
|
283
|
+
*
|
|
284
|
+
* So a pass here requires a positive count of failing tests from the runner's own
|
|
285
|
+
* machine report, and every case where that cannot be read is `blocked` with a
|
|
286
|
+
* reason naming which case it was. `blocked` can never satisfy a gate, so the
|
|
287
|
+
* gate is unsatisfiable exactly when it cannot be trusted.
|
|
288
|
+
*
|
|
289
|
+
* That report is read from a FILE the runner writes outside the worktree (#1019),
|
|
290
|
+
* never from the child's stdout or stderr. The held test shares those streams
|
|
291
|
+
* with the runner, and the drain window after the leader exits is precisely where
|
|
292
|
+
* a detached grandchild gets the last word — so no in-band parser could refuse
|
|
293
|
+
* the forgery.
|
|
294
|
+
*
|
|
295
|
+
* HOW FAR THAT GOES: it closes the stream forgeries outright, and it does NOT
|
|
296
|
+
* make the verdict unforgeable. The held test runs as the motor's own user and
|
|
297
|
+
* can write the report file itself — the directory is findable by a fixed name
|
|
298
|
+
* prefix, and pre-writing the report then dropping write permission needs no
|
|
299
|
+
* race at all. `runHeldOracle`'s mode check refuses that shape; a writer that
|
|
300
|
+
* leaves the modes alone still wins. This gate is evidence against a careless
|
|
301
|
+
* author, not a hostile one. `oracle.ts` has the full account, measurements
|
|
302
|
+
* included, above `OracleRunnerSpec`.
|
|
303
|
+
*
|
|
304
|
+
* Secrecy is inherited whole. The report is parsed in memory and only its two
|
|
305
|
+
* counts reach the persisted evidence — never the assertion text, which is itself
|
|
306
|
+
* a partial oracle. `runHeldOracle` removes the report directory on every path
|
|
307
|
+
* (best effort, and logged when it fails), which matters because vitest's JSON
|
|
308
|
+
* report carries `failureMessages` verbatim. The full output still goes to the
|
|
309
|
+
* motor's local log for a human operator, exactly as the green gate does.
|
|
310
|
+
*/
|
|
311
|
+
export class OracleRedCollector
|
|
312
|
+
extends HeldOracleCollector
|
|
313
|
+
implements GateEvidenceCollector
|
|
314
|
+
{
|
|
315
|
+
readonly kind = "oracle_red" as const;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The DOWNSTREAM stage's id, not this stage's: the author writes one held test
|
|
319
|
+
* keyed to the stage that must later pass it green, and this gate reads that
|
|
320
|
+
* same row early. See `OracleDeps.targetStageId`.
|
|
321
|
+
*/
|
|
322
|
+
protected oracleStageId(): string | null {
|
|
323
|
+
return this.deps.targetStageId ?? null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
protected verdict({
|
|
327
|
+
oracle,
|
|
328
|
+
identity,
|
|
329
|
+
exitCode,
|
|
330
|
+
report,
|
|
331
|
+
}: {
|
|
332
|
+
oracle: HeldOracle;
|
|
333
|
+
identity: OracleIdentity;
|
|
334
|
+
exitCode: number;
|
|
335
|
+
output: string;
|
|
336
|
+
report: OracleRunSummary | null;
|
|
337
|
+
}): GateEvidence {
|
|
338
|
+
// The runner's own MACHINE REPORT, and nothing else (#1019). There is
|
|
339
|
+
// deliberately no fallback to the merged buffer or to the runner's verdict
|
|
340
|
+
// stream: the held test shares both of those with the runner, and a
|
|
341
|
+
// fallback is all an attacker needs — make the report unreadable, and the
|
|
342
|
+
// gate drops back to a channel it can write. An unreadable report is
|
|
343
|
+
// `blocked`, which can never satisfy a gate.
|
|
344
|
+
//
|
|
345
|
+
// `output` stays in the signature because the base class logs it for the
|
|
346
|
+
// operator; this gate does not read it.
|
|
347
|
+
const graded = gradeOracleRed(report, exitCode);
|
|
348
|
+
const base = { exitCode, path: oracle.path, ...identity };
|
|
349
|
+
|
|
350
|
+
if (graded.outcome === "no_verdict") {
|
|
351
|
+
log.warn(TAG, `Red gate for ${oracle.path}: ${graded.reason} — blocked`);
|
|
352
|
+
return {
|
|
353
|
+
result: "blocked",
|
|
354
|
+
// NOT marked as a config error (#823 rule 1: opt in, and only for
|
|
355
|
+
// static config). A summary the motor could not read may well be a
|
|
356
|
+
// runner the next attempt resolves, or a transient tool failure — the
|
|
357
|
+
// retryable default is the honest one. The one genuinely static case,
|
|
358
|
+
// a red gate with no downstream green stage, is marked in `collect`.
|
|
359
|
+
structured: { oracle: base, reason: graded.reason },
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Counts only — never the assertion text. `total`/`failed` say THAT the
|
|
364
|
+
// held test failed, which is the verdict; they say nothing about WHAT it
|
|
365
|
+
// asserts, which is the secret.
|
|
366
|
+
return {
|
|
367
|
+
result: graded.outcome === "reproduced" ? "passed" : "failed",
|
|
368
|
+
structured: {
|
|
369
|
+
oracle: {
|
|
370
|
+
...base,
|
|
371
|
+
testsRun: graded.summary.total,
|
|
372
|
+
testsFailed: graded.summary.failed,
|
|
373
|
+
output:
|
|
374
|
+
"withheld — oracle_red is a secrecy gate; see the motor's local log",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
160
381
|
function errText(err: unknown): string {
|
|
161
382
|
return err instanceof Error ? err.message : String(err);
|
|
162
383
|
}
|