unitbob 0.6.2 → 0.7.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 +5 -4
- package/dist/files/guardrails.js +1 -1
- package/dist/files/packets.js +319 -0
- package/dist/files/suiteBuild.js +34 -2
- package/dist/files/suiteBuildUpload.js +1 -1
- package/dist/files/workerPlan.js +79 -5
- package/dist/runner/failureDigest.js +98 -16
- package/dist/runner/provision.js +174 -9
- package/dist/runner/pytest.js +1 -1
- package/dist/runner/pytestBddPlugin.js +5 -0
- package/dist/runner/rspec.js +1 -1
- package/dist/runner/vitest.js +1 -1
- package/dist/surfaces/graph.js +33 -0
- package/dist/surfaces/routeInventory.js +2 -33
- package/dist/verbs/acceptWorkerPlan.js +0 -0
- package/dist/verbs/putSuiteBuild.js +1 -1
- package/dist/verbs/run.js +2 -2
- package/dist/verbs/runLocal.js +66 -2
- package/dist/verbs/suitePrepare.js +78 -2
- package/dist/wire.js +1 -1
- package/package.json +1 -1
- package/plugin/codex/agents/suite-repair-worker.toml +1 -1
- package/plugin/codex/agents/suite-worker.toml +13 -5
- package/dist/verbs/validateWorkerPlan.js +0 -9
package/dist/verbs/runLocal.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { branchRunner, readHostSuiteOutputsPerBranch, readSuiteBuildRequest, } from "../files/suiteBuild.js";
|
|
2
|
-
import { digestOf, failureSet, readRunState, rememberFailures } from "../runner/failureDigest.js";
|
|
2
|
+
import { digestOf, failureSet, readRunState, rememberFailures, reportedFailures, } from "../runner/failureDigest.js";
|
|
3
3
|
import { placeProblem } from "../runner/place.js";
|
|
4
4
|
import { placeAdvice } from "../runner/placeAdvice.js";
|
|
5
5
|
import { runnerEnvironmentPlaceProblem } from "../runner/placeEnvironment.js";
|
|
@@ -143,8 +143,72 @@ async function runOneBranch(config, d, suiteKind, output) {
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
d.stdout.write(report(result));
|
|
146
|
+
d.stdout.write(failureLines(runner, result));
|
|
146
147
|
return { runner, result };
|
|
147
148
|
}
|
|
149
|
+
// How many lines of one failure's message are worth printing here. Enough for an
|
|
150
|
+
// assertion diff and the frame under it; not the whole backtrace, which is in
|
|
151
|
+
// the report file the line above names.
|
|
152
|
+
const DETAIL_LINES = 6;
|
|
153
|
+
const DETAIL_CHARS = 600;
|
|
154
|
+
// Spec 37-2, criterion 5. The report is read by the side that knows its format.
|
|
155
|
+
//
|
|
156
|
+
// Both behavioral report shapes and all three structural ones are already parsed
|
|
157
|
+
// in this connector, for the stall comparison — and the coordinator still opened
|
|
158
|
+
// `pytest_bdd_report.json` with an inline `node -e` and `pytest_result.xml` with
|
|
159
|
+
// an inline `python3`, because nothing printed what it found. That happened on
|
|
160
|
+
// the most expensive turns of the run: the repair stretch of the microblog bench
|
|
161
|
+
// cost 79 coordinator turns and 47% of its input, each turn re-reading a 300,000
|
|
162
|
+
// token conversation.
|
|
163
|
+
//
|
|
164
|
+
// Every failure, up to a budget on the whole block — this list is what the
|
|
165
|
+
// repair packets of step 12 are cut from, so a top-N would send the coordinator
|
|
166
|
+
// back to the file for the rest, which is the cost this removes. But the run
|
|
167
|
+
// this most exists for is the first red one, where the harness is not wired yet
|
|
168
|
+
// and every case fails: unbounded, that is half a megabyte into the very context
|
|
169
|
+
// this spec is trying to make cheaper. What does not fit says so, by count, and
|
|
170
|
+
// the report file is named on the line above.
|
|
171
|
+
const FAILURE_BLOCK_CHARS = 40_000;
|
|
172
|
+
function failureLines(runner, result) {
|
|
173
|
+
const failures = reportedFailures(runner, result.report);
|
|
174
|
+
if (!failures || failures.length === 0)
|
|
175
|
+
return '';
|
|
176
|
+
const head = `\n${failures.length} ${failures.length === 1 ? 'case' : 'cases'} failed, read out of ` +
|
|
177
|
+
`${result.resultPath}. Do not open that file to find this again:\n`;
|
|
178
|
+
const printed = [];
|
|
179
|
+
let spent = 0;
|
|
180
|
+
for (const [index, failure] of failures.entries()) {
|
|
181
|
+
const line = one(failure, index);
|
|
182
|
+
if (spent + line.length > FAILURE_BLOCK_CHARS && printed.length > 0)
|
|
183
|
+
break;
|
|
184
|
+
printed.push(line);
|
|
185
|
+
spent += line.length;
|
|
186
|
+
}
|
|
187
|
+
const left = failures.length - printed.length;
|
|
188
|
+
const tail = left === 0
|
|
189
|
+
? ''
|
|
190
|
+
: `\n …and ${left} more failed ${left === 1 ? 'case' : 'cases'}, not printed to keep this readable. ` +
|
|
191
|
+
`They are in ${result.resultPath}; a branch failing this widely is usually one harness problem, ` +
|
|
192
|
+
'not that many repairs.\n';
|
|
193
|
+
return head + printed.join('') + tail;
|
|
194
|
+
}
|
|
195
|
+
function one(failure, index) {
|
|
196
|
+
const where = failure.file ? ` — ${failure.file}` : '';
|
|
197
|
+
const lines = [`\n ${index + 1}. ${failure.name || failure.marker || '(the runner named no case)'}${where}`];
|
|
198
|
+
if (failure.marker && failure.name.includes(failure.marker) === false)
|
|
199
|
+
lines.push(` ${failure.marker}`);
|
|
200
|
+
if (failure.step)
|
|
201
|
+
lines.push(` step: ${failure.step}`);
|
|
202
|
+
for (const line of trimmed(failure.detail))
|
|
203
|
+
lines.push(` ${line}`);
|
|
204
|
+
return `${lines.join('\n')}\n`;
|
|
205
|
+
}
|
|
206
|
+
function trimmed(detail) {
|
|
207
|
+
const lines = detail.split('\n').slice(0, DETAIL_LINES);
|
|
208
|
+
const kept = lines.join('\n').slice(0, DETAIL_CHARS).split('\n');
|
|
209
|
+
const complete = kept.join('\n') === detail;
|
|
210
|
+
return complete ? kept : [...kept, '…'];
|
|
211
|
+
}
|
|
148
212
|
// The command first, and always — including on a green run. It is the answer to
|
|
149
213
|
// "how do I run just this one file again", which is the question the whole
|
|
150
214
|
// iteration loop is made of, and printing it only on failure would hide it at
|
|
@@ -178,7 +242,7 @@ function outputTail(result) {
|
|
|
178
242
|
}
|
|
179
243
|
// The suite blob's own project-relative paths, exactly as the runners expect
|
|
180
244
|
// them: the main file first, then every other file of the branch. The main file
|
|
181
|
-
// stopped being the whole suite in spec
|
|
245
|
+
// stopped being the whole suite in spec one-place-per-rule, §6 — a branch is one file per
|
|
182
246
|
// assignment now — and running it alone would exercise a fraction of what the
|
|
183
247
|
// answer claims to guard.
|
|
184
248
|
//
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { clearRunState } from "../runner/failureDigest.js";
|
|
2
2
|
import { materializeHelper } from "../files/guardrails.js";
|
|
3
3
|
import { materializeBehavioralWorld } from "../files/behavioral.js";
|
|
4
|
-
import {
|
|
4
|
+
import { PACKETS_DIR, writeSuitePackets } from "../files/packets.js";
|
|
5
|
+
import { movePreviousRunAside, recipeNameFor, writeSuiteBuildRequest, } from "../files/suiteBuild.js";
|
|
5
6
|
import { bddStepLoading } from "../runner/bdd.js";
|
|
6
7
|
import { bootCheck, SIGNAL_STRENGTH } from "../runner/bootcheck.js";
|
|
7
8
|
import { anyStackPrecheck, behavioralHarnessNotice, detectBddRunner, detectStructuralRunner, runnerReadyPrecheck, } from "../runner/precheck.js";
|
|
@@ -230,16 +231,54 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
230
231
|
throw new Error(`No suite branch can be built this run:\n${blockedNotices.join('\n')}\nNothing was written and nothing was uploaded.`);
|
|
231
232
|
}
|
|
232
233
|
const request = writeSuiteBuildRequest(config.projectRoot, branches, defectContext);
|
|
234
|
+
// Spec 37-1. The assignment names entrypoints; the packets are the files
|
|
235
|
+
// behind them, resolved from this machine's own graph and copied where a
|
|
236
|
+
// worker can open them. Built here because the entrypoints are known from the
|
|
237
|
+
// request and the workers are not yet — a packet belongs to an entrypoint,
|
|
238
|
+
// not to whoever ends up guarding it.
|
|
239
|
+
//
|
|
240
|
+
// Not written into `request.json`: the coordinator reads that file whole and
|
|
241
|
+
// pays for it on every turn of the longest-lived context in the run.
|
|
242
|
+
const sourcePackets = buildPackets(config.projectRoot, request);
|
|
233
243
|
// A new request is a new build, and a new build has no previous run to be
|
|
234
244
|
// stuck against (spec 34-6, criterion 3). Re-running this verb is a documented
|
|
235
245
|
// step of the loop, so a failure set remembered from the build before it would
|
|
236
246
|
// stop a branch that has not run once yet.
|
|
237
247
|
clearRunState(config.projectRoot);
|
|
248
|
+
// Spec 37-2, criterion 3. Same reasoning, applied to the papers that were left
|
|
249
|
+
// behind rather than cleared: a plan and its checkpoints outlive the
|
|
250
|
+
// `request.json` they were digested against, and every one of them is refused
|
|
251
|
+
// by its own gate from here on. Moved rather than removed — see
|
|
252
|
+
// `movePreviousRunAside`.
|
|
253
|
+
//
|
|
254
|
+
// Wrapped for the same reason `buildPackets` is: by this line the request is
|
|
255
|
+
// written and the build is real. A read-only checkout or a permission the move
|
|
256
|
+
// does not have is a note, not a build that dies after its own work landed
|
|
257
|
+
// and before it could say so.
|
|
258
|
+
let displaced = [];
|
|
259
|
+
let displaceProblem = '';
|
|
260
|
+
try {
|
|
261
|
+
displaced = movePreviousRunAside(config.projectRoot);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
displaceProblem = err.message;
|
|
265
|
+
}
|
|
238
266
|
const kinds = branches.map((branch) => branch.suite_kind).join(' and ');
|
|
239
267
|
const nextCommand = branches.some((branch) => branch.suite_kind === 'behavioral')
|
|
240
268
|
? '`unitbob suite-review-prepare` before upload'
|
|
241
269
|
: '`unitbob put-suite-build`';
|
|
242
270
|
actual.stdout.write(`Suite build request written to ${request.project_root}/.unitbob/suite-build/request.json\n`);
|
|
271
|
+
if (displaced.length > 0) {
|
|
272
|
+
actual.stdout.write(`The previous run's ${displaced.join(', ')} moved to ` +
|
|
273
|
+
`${request.project_root}/.unitbob/suite-build/previous/ — none of it is left where this build will ` +
|
|
274
|
+
'look, and none of it was deleted.\n');
|
|
275
|
+
}
|
|
276
|
+
if (displaceProblem) {
|
|
277
|
+
actual.stdout.write(`\nThe previous run's files could not be moved out of the way (${displaceProblem}). This build is fine, ` +
|
|
278
|
+
'but a plan or checkpoint left over from it will be refused by its own gate — the digests belong to ' +
|
|
279
|
+
'the request that was just replaced.\n');
|
|
280
|
+
}
|
|
281
|
+
actual.stdout.write(packetNotice(request.project_root, sourcePackets));
|
|
243
282
|
actual.stdout.write(`Next: build ${branches.length === 1 ? 'the' : 'both'} peer ${branches.length === 1 ? 'suite' : 'suites'} (${kinds}) following each branch's \`recipe\` and \`assignment\`, ` +
|
|
244
283
|
`write your answer to ${request.output_path} as a branches array — one entry per branch named above, and a branch you cannot ` +
|
|
245
284
|
`finish says so in its own entry rather than being left out of the array. Run each locally with \`unitbob run-local\` (the same ` +
|
|
@@ -288,8 +327,45 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
288
327
|
'\n');
|
|
289
328
|
}
|
|
290
329
|
}
|
|
330
|
+
// A checkout we cannot write packets into is a run without packets, not a
|
|
331
|
+
// failed build: the workers search the source themselves, exactly as they did
|
|
332
|
+
// before this spec. The same rule the route inventory follows for the same
|
|
333
|
+
// reason — a read-only checkout or a full disk must not take a build down.
|
|
334
|
+
function buildPackets(projectRoot, request) {
|
|
335
|
+
try {
|
|
336
|
+
return writeSuitePackets(projectRoot, request);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
return err.message;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function packetNotice(projectRoot, packets) {
|
|
343
|
+
if (typeof packets === 'string') {
|
|
344
|
+
return (`\nNo packets were written this run (${packets}). Workers find their own source, as before.\n`);
|
|
345
|
+
}
|
|
346
|
+
if (packets.targets === 0)
|
|
347
|
+
return '';
|
|
348
|
+
const where = `${projectRoot}/${PACKETS_DIR}`;
|
|
349
|
+
const head = packets.files === 0
|
|
350
|
+
? `\nNo packet was written this run, so ${where} holds only its index.\n`
|
|
351
|
+
: `\n${packets.files} ${packets.files === 1 ? 'packet' : 'packets'} (${packets.bytes.toLocaleString('en-US')} bytes) ` +
|
|
352
|
+
`written to ${where}: the source behind ${packets.resolved} of ${packets.targets} entrypoints, resolved from this ` +
|
|
353
|
+
`machine's own graph and route inventory without asking a model. Hand each worker the paths of its packets, ` +
|
|
354
|
+
'never their contents — `unitbob accept-worker-plan` prints them per worker.\n';
|
|
355
|
+
// Two different outcomes, never merged: a file that was found and not carried
|
|
356
|
+
// still saves the worker the search, and a name nothing answered to does not.
|
|
357
|
+
const carried = packets.located - packets.resolved;
|
|
358
|
+
const unknown = packets.targets - packets.located;
|
|
359
|
+
if (carried === 0 && unknown === 0)
|
|
360
|
+
return head;
|
|
361
|
+
return (head +
|
|
362
|
+
`${carried + unknown} of ${packets.targets} entrypoints have no packet` +
|
|
363
|
+
(carried > 0 ? `; ${carried} name a file that was found but not carried` : '') +
|
|
364
|
+
` (${packets.notes.join('; ')}). ` +
|
|
365
|
+
`Each says why in ${where}/index.json.\n`);
|
|
366
|
+
}
|
|
291
367
|
// The runner's own rule for which step files it will load, in the words of the
|
|
292
|
-
// side that loads them (spec
|
|
368
|
+
// side that loads them (spec ask-before-you-spend, §3.2). The same object is in `request.json`, on
|
|
293
369
|
// the behavioral branch; this is the copy the coordinator sees without opening a
|
|
294
370
|
// file.
|
|
295
371
|
//
|
package/dist/wire.js
CHANGED
|
@@ -87,7 +87,7 @@ export class Wire {
|
|
|
87
87
|
// carries one result per suite_kind.
|
|
88
88
|
//
|
|
89
89
|
// `dryRun` is the same route, the same body and the same server-side
|
|
90
|
-
// validation, stopped before the first write (spec
|
|
90
|
+
// validation, stopped before the first write (spec one-place-per-rule, §1). It answers
|
|
91
91
|
// `would_publish` instead of `created`, and it is deliberately not a route of
|
|
92
92
|
// its own: a second route would grow a second implementation, which is the
|
|
93
93
|
// defect this whole spec removes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Unitbob connector — thin local hands for the Unitbob Rails brain. Owns no domain logic: it runs tools, relays bytes over the wire, and prints what the server returns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@ markers, or paths. Do not edit production code, host-owned shared files, the
|
|
|
21
21
|
connector-owned harness, or another slice.
|
|
22
22
|
|
|
23
23
|
After every owned edit, run
|
|
24
|
-
`npx -y --loglevel=error unitbob@0.
|
|
24
|
+
`npx -y --loglevel=error unitbob@0.7.0 run-local <branch>` and inspect the machine
|
|
25
25
|
report. Look only at examples or scenarios matching your owned paths or case
|
|
26
26
|
markers. Do not require a green exit code from the whole branch: foreign failures
|
|
27
27
|
and an already-confirmed product red do not widen your scope. Repeat the bounded
|
|
@@ -7,10 +7,10 @@ You receive exactly one worker-plan item and the request paths it references.
|
|
|
7
7
|
That item is your complete scope. Do not add capabilities, promises, examples,
|
|
8
8
|
or scenarios after fan-out.
|
|
9
9
|
|
|
10
|
-
Your checkpoint already exists:
|
|
11
|
-
path the workflow prescribes, with the exact request and plan digests, your
|
|
12
|
-
branch and worker id, your promises in `unresolved_promises
|
|
13
|
-
had already verified. Never initialize it again — not on the first incarnation,
|
|
10
|
+
Your checkpoint already exists: `accept-worker-plan` wrote it before fan-out, at
|
|
11
|
+
the path the workflow prescribes, with the exact request and plan digests, your
|
|
12
|
+
branch and worker id, and your promises in `unresolved_promises`; the
|
|
13
|
+
coordinator added the facts it had already verified. Never initialize it again — not on the first incarnation,
|
|
14
14
|
and not on an explicitly approved fresh incarnation after a native budget stop,
|
|
15
15
|
where you preserve the supplied checkpoint and completed files and continue only
|
|
16
16
|
its `unresolved_promises`. Update it after every completed promise.
|
|
@@ -62,7 +62,15 @@ workers got it as verified. One of them looked, disagreed, and kept its scenario
|
|
|
62
62
|
honest, which is the only reason that access hole came back red instead of green.
|
|
63
63
|
Nothing mechanical enforces any of this; it holds because you keep it.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Your source packets are the starting point: do not go looking for what is
|
|
66
|
+
already in one. A source packet is the whole file behind one of your entrypoints,
|
|
67
|
+
found for you and put on disk, so opening it is a read and not a search. Your
|
|
68
|
+
task names the paths — sometimes a path to open in place, when the file was too
|
|
69
|
+
large to carry. If it names none, or the code you need is not in the ones it
|
|
70
|
+
names, then search as you would have anyway.
|
|
71
|
+
|
|
72
|
+
Read only the source packets, the `source_paths` and the dependencies your
|
|
73
|
+
finite planned cases need.
|
|
66
74
|
Ask closed questions with the files to look in. For a closed missing fact, use
|
|
67
75
|
the named `fact-finder` agent, as often as the work genuinely needs. A lookup
|
|
68
76
|
may confirm implementation facts but may not expand the plan.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { validateWorkerPlanFiles, workerPlanDigest } from "../files/workerPlan.js";
|
|
2
|
-
export async function validateWorkerPlan(config, _args = [], deps = { stdout: process.stdout }) {
|
|
3
|
-
const errors = validateWorkerPlanFiles(config.projectRoot);
|
|
4
|
-
if (errors.length > 0)
|
|
5
|
-
throw new Error(`Worker plan is invalid:\n- ${errors.join('\n- ')}`);
|
|
6
|
-
const planDigest = workerPlanDigest(config.projectRoot);
|
|
7
|
-
deps.stdout.write(`Worker plan valid (${planDigest}). Fan-out may start.\n`);
|
|
8
|
-
return { plan_digest: planDigest };
|
|
9
|
-
}
|