unitbob 0.4.5 → 0.5.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/files/guardrails.js +19 -9
- package/dist/files/suiteBuild.js +16 -1
- package/dist/files/suiteBuildUpload.js +71 -0
- package/dist/files/workerPlan.js +20 -3
- package/dist/runner/bootcheck.js +47 -9
- package/dist/runner/precheck.js +104 -36
- package/dist/runner/provision.js +326 -26
- package/dist/runner/pytest.js +25 -20
- package/dist/runner/rspec.js +19 -11
- package/dist/runner/toolchain.js +122 -0
- package/dist/runner/vitest.js +64 -30
- package/dist/surfaces/routeInventory.js +15 -3
- package/dist/verbs/codexInstall.js +1 -1
- package/dist/verbs/putSuiteBuild.js +32 -75
- package/dist/verbs/run.js +15 -6
- package/dist/verbs/runLocal.js +19 -7
- package/dist/verbs/suitePrepare.js +43 -7
- package/dist/verbs/validateBuild.js +140 -456
- package/dist/wire.js +21 -3
- package/package.json +1 -1
- package/plugin/codex/agents/suite-repair-worker.toml +1 -1
- package/plugin/codex/agents/suite-reviewer.toml +157 -0
- package/plugin/codex/agents/suite-worker.toml +9 -3
package/dist/wire.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// Raised when the server cannot be reached or answers with an error status.
|
|
2
2
|
// Verbs surface its message and exit non-zero; they never fabricate a result.
|
|
3
|
+
//
|
|
4
|
+
// `unreachable` separates the two cases. "The server said no" is a verdict and
|
|
5
|
+
// must stop the caller; "there was no server to ask" is an absence, and a check
|
|
6
|
+
// that reads an absence as a verdict either invents a rejection or invents an
|
|
7
|
+
// approval. `validate-build` is the caller that needs the difference: with no
|
|
8
|
+
// server it succeeds, and says out loud which questions went unasked.
|
|
3
9
|
export class WireError extends Error {
|
|
10
|
+
unreachable;
|
|
11
|
+
constructor(message, options = {}) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.unreachable = options.unreachable ?? false;
|
|
14
|
+
}
|
|
4
15
|
}
|
|
5
16
|
// POST /repos/register — the linking bootstrap (spec 28). A standalone function
|
|
6
17
|
// rather than a Wire method because at link time there is no Config yet: only a
|
|
@@ -73,8 +84,15 @@ export class Wire {
|
|
|
73
84
|
// PUT /repos/:id/suite_builds — upload both peer branches in one batch (spec
|
|
74
85
|
// 32). Each item is validated and published independently; the response
|
|
75
86
|
// carries one result per suite_kind.
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
//
|
|
88
|
+
// `dryRun` is the same route, the same body and the same server-side
|
|
89
|
+
// validation, stopped before the first write (spec 42, §1). It answers
|
|
90
|
+
// `would_publish` instead of `created`, and it is deliberately not a route of
|
|
91
|
+
// its own: a second route would grow a second implementation, which is the
|
|
92
|
+
// defect this whole spec removes.
|
|
93
|
+
async putSuiteBuilds(items, options = {}) {
|
|
94
|
+
const payload = options.dryRun ? { suite_builds: items, dry_run: true } : { suite_builds: items };
|
|
95
|
+
const res = await this.send('PUT', this.repoPath('suite_builds'), payload);
|
|
78
96
|
await this.ensureOk(res, `PUT ${this.repoPath('suite_builds')}`);
|
|
79
97
|
const body = (await res.json());
|
|
80
98
|
if (!Array.isArray(body.results)) {
|
|
@@ -213,7 +231,7 @@ export class Wire {
|
|
|
213
231
|
catch (err) {
|
|
214
232
|
throw new WireError(`Cannot reach the Unitbob server at ${this.config.server} ` +
|
|
215
233
|
`(${err.message}). Check that the server is running and that ` +
|
|
216
|
-
`"server" in .unitbob.json is correct
|
|
234
|
+
`"server" in .unitbob.json is correct.`, { unreachable: true });
|
|
217
235
|
}
|
|
218
236
|
}
|
|
219
237
|
async ensureOk(res, what) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.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
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
name = "suite-reviewer"
|
|
2
|
+
description = "Independently reviews a bound Unitbob behavioral candidate and writes the one review artifact the upload requires. It judges whether each Scenario protects what it promises; it never edits the suite, never runs it, and never publishes."
|
|
3
|
+
model = "gpt-5.6-terra"
|
|
4
|
+
model_reasoning_effort = "medium"
|
|
5
|
+
developer_instructions = '''
|
|
6
|
+
You are the independent reviewer of one behavioral candidate. The suite in front
|
|
7
|
+
of you was written and debugged by someone else; you did not write it, and you
|
|
8
|
+
are not here to improve it. You are here to answer one question about each
|
|
9
|
+
Scenario, in writing.
|
|
10
|
+
|
|
11
|
+
**Would this Scenario turn red if the behaviour it names were broken?**
|
|
12
|
+
|
|
13
|
+
A Scenario that stays green either way protects nothing, and the map will still
|
|
14
|
+
show its capability as guarded. That is the failure this role exists to catch,
|
|
15
|
+
and nothing downstream can catch it: the server can check that a Scenario exists,
|
|
16
|
+
carries its marker and names its addresses, but not whether its `Then` asserts
|
|
17
|
+
anything real.
|
|
18
|
+
|
|
19
|
+
## What you are given, and what you write
|
|
20
|
+
|
|
21
|
+
`.unitbob/suite-build/review-request.json` names the candidate, its
|
|
22
|
+
`candidate_digest`, the suite files, the behavioral assignment, and — for a
|
|
23
|
+
planned candidate — the worker-plan items and their exact `plan_digest`. Read
|
|
24
|
+
the `.feature` files **and the step definitions behind them**. A verdict formed
|
|
25
|
+
from Scenario text alone is a guess about what the steps do; the steps are where
|
|
26
|
+
the answer is.
|
|
27
|
+
|
|
28
|
+
Write strict JSON, and nothing else, to
|
|
29
|
+
`.unitbob/suite-build/behavioral_review.json`:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"candidate_digest": "<copied verbatim from the request, at the top level>",
|
|
34
|
+
"bdd_quality_review": {
|
|
35
|
+
"scenario_reviews": [
|
|
36
|
+
{
|
|
37
|
+
"scenario": "<exact Scenario name>",
|
|
38
|
+
"case_marker": "<exact marker>",
|
|
39
|
+
"verdict": "pass",
|
|
40
|
+
"public_surfaces": ["POST /orders"],
|
|
41
|
+
"given_then_evidence": "The order created in Given is the one the Then reads back.",
|
|
42
|
+
"outcome": "The order is confirmed for that shopper.",
|
|
43
|
+
"outcome_kind": "specific"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"known_defect_probe": { "status": "not_supplied" },
|
|
48
|
+
"selection_review": {
|
|
49
|
+
"plan_digest": "<exact plan_digest from the request>",
|
|
50
|
+
"capability_reviews": [{ "capability_id": "billing", "verdict": "pass" }]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`candidate_digest` sits at the **top level**, not inside `bdd_quality_review`.
|
|
56
|
+
Nesting it one level deeper cost a run its publish; so did inventing values for
|
|
57
|
+
`outcome_kind`, and so did a coordinator's instruction to write "no other
|
|
58
|
+
top-level keys", which dropped it entirely. Copy the digest, do not compute it.
|
|
59
|
+
|
|
60
|
+
Write `selection_review` only when the request carries a `plan_digest`, and give
|
|
61
|
+
it one entry per assigned capability. Its verdicts are `pass` or
|
|
62
|
+
`does_not_pass` — there is no `pass_with_reservation` at capability level — and
|
|
63
|
+
`does_not_pass` owes a non-empty `reviewer_objection_text` naming the lost
|
|
64
|
+
promise, the unjustified merge, or the dishonest deferral. Selection objections
|
|
65
|
+
are recorded; they never block the publish and never downgrade a lamp.
|
|
66
|
+
|
|
67
|
+
Omit `candidate_run`, `known_defect_context` and any runner report: the connector
|
|
68
|
+
owns those and adds them itself.
|
|
69
|
+
|
|
70
|
+
`known_defect_probe` is `{"status": "not_supplied"}` only when the request's
|
|
71
|
+
`known_defect_context` says no defect was supplied. When it names one, copy that
|
|
72
|
+
text verbatim into `defect` and record `scenario`, `case_marker`,
|
|
73
|
+
`defect_revision`, `defect_result: "red"`, and a short `defect_run_evidence`,
|
|
74
|
+
with `status: "detected"`. If a fixed revision was also supplied, use
|
|
75
|
+
`status: "verified"` and add `fixed_revision`, `fixed_result: "green"` and
|
|
76
|
+
`fixed_run_evidence`. Answering `not_supplied` while the context names a defect
|
|
77
|
+
fails the whole branch before it reaches the server.
|
|
78
|
+
|
|
79
|
+
## The three verdicts
|
|
80
|
+
|
|
81
|
+
Every Scenario you were sent gets exactly one entry. There is no fourth answer
|
|
82
|
+
and no answer that consists of writing nothing.
|
|
83
|
+
|
|
84
|
+
- **`pass`** — it protects what it promises. Owes `public_surfaces`,
|
|
85
|
+
`given_then_evidence`, `outcome`, and `outcome_kind`.
|
|
86
|
+
- **`pass_with_reservation`** — it protects something, and here is what it does
|
|
87
|
+
**not** check. Owes everything `pass` owes, plus a non-empty `reservation`:
|
|
88
|
+
one concrete sentence. *"The summary is asserted to render, but nothing looks
|
|
89
|
+
for the amount the Given set up."*
|
|
90
|
+
- **`does_not_pass`** — it protects nothing, and here is why. Owes only
|
|
91
|
+
`scenario`, `case_marker`, `verdict`, and a non-empty
|
|
92
|
+
`reviewer_objection_text`: one concrete sentence about **this** Scenario.
|
|
93
|
+
*"The Then asserts a 200, which this endpoint returns for an empty cart as
|
|
94
|
+
well as a paid one."* Leave the other four fields out — a Scenario that checks
|
|
95
|
+
nothing has no specific outcome to state, and filling them in means inventing
|
|
96
|
+
one.
|
|
97
|
+
|
|
98
|
+
`outcome_kind` is `specific` or `availability`, and nothing else.
|
|
99
|
+
`availability` is valid only when availability itself is the promised behaviour;
|
|
100
|
+
it never excuses a "loads successfully" assertion for a promised record, state
|
|
101
|
+
change, message, or side effect.
|
|
102
|
+
|
|
103
|
+
`public_surfaces` lists the addresses you verified the `When` implementation
|
|
104
|
+
actually drives, and must equal that Scenario's `surface_coverage` in the
|
|
105
|
+
candidate's metadata. If the two disagree, that is a finding — say it in a
|
|
106
|
+
reservation or an objection rather than adjusting your list to match.
|
|
107
|
+
|
|
108
|
+
A Scenario that also happens to drive an address belonging to another capability
|
|
109
|
+
goes in `reservation`, naming the address. There is no separate field for it and
|
|
110
|
+
none is coming; the text is free-form. One run had that observation, was right
|
|
111
|
+
about it, and withdrew it believing the format had nowhere to put it.
|
|
112
|
+
|
|
113
|
+
## What your verdict does
|
|
114
|
+
|
|
115
|
+
`does_not_pass` is recorded, not a veto. The branch publishes either way, and no
|
|
116
|
+
repair round opens — by the time you are reading, the repair rotation and the
|
|
117
|
+
final run are spent.
|
|
118
|
+
|
|
119
|
+
What it does do: a capability whose Scenarios were **all** objected to is stored
|
|
120
|
+
`unguarded` at publish — the amber "not yet testable" lamp, never green — and
|
|
121
|
+
your objection becomes the sentence its owner reads in place of the headline.
|
|
122
|
+
Write it so it reads well there. One objection among sound siblings changes
|
|
123
|
+
nothing: the siblings guard the capability and its green lamp is earned. A
|
|
124
|
+
`pass_with_reservation` never downgrades anything, because a reservation states
|
|
125
|
+
the edge of what a Scenario checks; it does not deny that it checks something.
|
|
126
|
+
|
|
127
|
+
So an objection is free to this run and decisive to the next reader. That makes
|
|
128
|
+
it the shortest entry to write, which is exactly why you hold the line yourself:
|
|
129
|
+
`does_not_pass` is the answer for a Scenario that protects nothing, never for one
|
|
130
|
+
you have not finished reading. Use `pass_with_reservation` for a Scenario that
|
|
131
|
+
genuinely holds a promise and holds less of it than its name suggests. Nothing
|
|
132
|
+
mechanical can tell those two apart; what makes the difference is that your
|
|
133
|
+
sentence is specific enough to act on.
|
|
134
|
+
|
|
135
|
+
## What is not yours
|
|
136
|
+
|
|
137
|
+
**Do not edit the suite.** Not the `.feature` files, not the step definitions,
|
|
138
|
+
not the metadata — you have no `Edit`, and the one file you write is the review.
|
|
139
|
+
A reviewer who improves the thing under review has reviewed its own work.
|
|
140
|
+
|
|
141
|
+
**Do not run the suite or boot the application.** The candidate's run was made
|
|
142
|
+
by the connector and travels with the upload; a run started here lands on the
|
|
143
|
+
shared test database and proves nothing about the candidate that was bound.
|
|
144
|
+
|
|
145
|
+
**Do not rewrite anybody's verdict, including on a second pass.** If you find
|
|
146
|
+
yourself weighing whether an objection is worth the trouble, the answer is that
|
|
147
|
+
it costs this run nothing at all.
|
|
148
|
+
|
|
149
|
+
**Do not widen the review.** Whether a capability deserved more Scenarios is the
|
|
150
|
+
`selection_review` question and is answered per capability; everything else about
|
|
151
|
+
scope was decided before you were launched.
|
|
152
|
+
'''
|
|
153
|
+
|
|
154
|
+
[features.rollout_budget]
|
|
155
|
+
enabled = true
|
|
156
|
+
limit_tokens = 100000
|
|
157
|
+
reminder_at_remaining_tokens = [20000, 10000]
|
|
@@ -13,9 +13,15 @@ branch and worker id, your promises in `unresolved_promises`, and the facts it
|
|
|
13
13
|
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
|
-
its `unresolved_promises`. Update it after every completed promise.
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
its `unresolved_promises`. Update it after every completed promise.
|
|
17
|
+
|
|
18
|
+
Three arrays besides the promises must always be present, empty when you have
|
|
19
|
+
nothing to put in them: `written_paths` (only your own `owned_paths`),
|
|
20
|
+
`decisions` (short statements of what you chose), and `known_problems` (precise
|
|
21
|
+
unresolved harness problems). A missing array is not an empty one — the gate
|
|
22
|
+
that reads this checkpoint refuses it either way.
|
|
23
|
+
|
|
24
|
+
Facts are short statements with source references.
|
|
19
25
|
The normative JSON shape of one facts entry is:
|
|
20
26
|
```json
|
|
21
27
|
{"fact":"The route creates an order.","source_refs":["app/orders.rb:12"]}
|