muse-crew 0.14.9 → 0.14.11
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/API.md +1 -0
- package/docs/decisions/AGENTS.md +1 -0
- package/docs/decisions/publish-path.md +11 -0
- package/docs/ooda-report.md +21 -4
- package/lib/AGENTS.md +2 -2
- package/lib/crew-api.js +72 -3
- package/lib/read-ooda-verdict.js +61 -86
- package/lib/see-act.js +616 -6
- package/lib/test-worktree-backend.sh +42 -0
- package/lib/worktree-lifecycle.sh +64 -4
- package/package.json +1 -1
- package/seed/cron-body-template.md +6 -2
- package/workflows/bugfix.js +56 -5
- package/workflows/chore.js +53 -3
- package/workflows/standard.js +56 -5
|
@@ -45,6 +45,48 @@ grep -q "^path=$T/.worktrees/$tid\$" "$T/.worktrees/.registry/$tid" || fail "pre
|
|
|
45
45
|
out=$(bash "$LIFECYCLE" prepare "$tid") || fail "prepare (reuse) exited non-zero"
|
|
46
46
|
echo "$out" | grep -q '^REUSED' || fail "prepare: expected REUSED on second run: $out"
|
|
47
47
|
|
|
48
|
+
# Blocker 45 (2026-09-22, room #29 J1): prepare must verify its REUSED
|
|
49
|
+
# invariant. An agent-improvised branch (raw git, bypassing prepare) gets
|
|
50
|
+
# REPAIRED — registry re-registered under the actual branch, never deleted —
|
|
51
|
+
# never silently REUSED.
|
|
52
|
+
tid2="backendtest45"
|
|
53
|
+
out=$(bash "$LIFECYCLE" prepare "$tid2") || fail "prepare (45) exited non-zero: $out"
|
|
54
|
+
echo "$out" | grep -q '^CREATED' || fail "prepare (45): expected CREATED, got: $out"
|
|
55
|
+
# The agent improvises: checks out a branch outside the task/ namespace and
|
|
56
|
+
# commits there, bypassing prepare entirely.
|
|
57
|
+
git -C "$T/.worktrees/$tid2" checkout -qb crew-9eda33ca
|
|
58
|
+
echo improvised >> "$T/.worktrees/$tid2/f.txt"
|
|
59
|
+
(git -C "$T/.worktrees/$tid2" commit -qam improvised) || fail "improvised commit failed"
|
|
60
|
+
out=$(bash "$LIFECYCLE" prepare "$tid2") || fail "prepare (45 repair) exited non-zero: $out"
|
|
61
|
+
echo "$out" | grep -q '^REPAIRED' || fail "prepare: expected REPAIRED on improvised branch, got: $out"
|
|
62
|
+
echo "$out" | grep -q '^REUSED' && fail "prepare: must not print REUSED when the branch invariant fails"
|
|
63
|
+
# The agent's branch survives; the registry attributes the repair honestly.
|
|
64
|
+
git -C "$T" rev-parse --verify "crew-9eda33ca" >/dev/null 2>&1 || fail "repair: agent branch deleted"
|
|
65
|
+
grep -q '^branch=crew-9eda33ca$' "$T/.worktrees/.registry/$tid2" || fail "repair: registry branch not re-registered"
|
|
66
|
+
grep -q '^task_id=backendtest45$' "$T/.worktrees/.registry/$tid2" || fail "repair: registry lost task attribution"
|
|
67
|
+
base_before=$(grep '^base_commit=' "$T/.worktrees/.registry/$tid2")
|
|
68
|
+
# The repair is idempotent: a second prepare repairs again (no spurious
|
|
69
|
+
# REUSED), preserving the registry's other fields.
|
|
70
|
+
out=$(bash "$LIFECYCLE" prepare "$tid2") || fail "prepare (45 second repair) exited non-zero: $out"
|
|
71
|
+
echo "$out" | grep -q '^REPAIRED' || fail "prepare: expected idempotent REPAIRED, got: $out"
|
|
72
|
+
base_after=$(grep '^base_commit=' "$T/.worktrees/.registry/$tid2")
|
|
73
|
+
[ "$base_before" = "$base_after" ] || fail "repair: re-register must preserve base_commit"
|
|
74
|
+
# A worktree back on the canonical branch is REUSED with no repair.
|
|
75
|
+
git -C "$T/.worktrees/$tid2" checkout -q "task/$tid2"
|
|
76
|
+
out=$(bash "$LIFECYCLE" prepare "$tid2") || fail "prepare (45 canonical) exited non-zero: $out"
|
|
77
|
+
echo "$out" | grep -q '^REUSED' || fail "prepare: expected REUSED on canonical branch, got: $out"
|
|
78
|
+
echo "$out" | grep -q '^REPAIRED' && fail "prepare: no spurious REPAIRED on a canonical worktree"
|
|
79
|
+
# resolve-branch falls back to the worktree's actual checked-out branch when
|
|
80
|
+
# the registry is absent and no canonical/prefix ref matches — the
|
|
81
|
+
# classifier's mechanical truth of last resort (not "branch does not exist").
|
|
82
|
+
git -C "$T/.worktrees/$tid2" checkout -qb crew-deadbeef
|
|
83
|
+
rm -f "$T/.worktrees/.registry/$tid2"
|
|
84
|
+
git -C "$T" branch -D -q "task/$tid2"
|
|
85
|
+
resolved=$(bash "$LIFECYCLE" resolve-branch "$tid2") || fail "resolve-branch (45) exited non-zero"
|
|
86
|
+
[ "$resolved" = "crew-deadbeef" ] || fail "resolve-branch: expected the actual branch, got: $resolved"
|
|
87
|
+
bash "$LIFECYCLE" cleanup "$tid2" >/dev/null || fail "cleanup (45) failed"
|
|
88
|
+
[ -d "$T/.worktrees/$tid2" ] && fail "cleanup (45): worktree still exists"
|
|
89
|
+
|
|
48
90
|
# inspect: diffs the task branch against main
|
|
49
91
|
echo change >> "$T/.worktrees/$tid/f.txt"
|
|
50
92
|
(cd "$T/.worktrees/$tid" && git commit -qam work)
|
|
@@ -12,11 +12,23 @@
|
|
|
12
12
|
# bypassing prepare — task 4e1a1bba, 2026-09-17), resolve_branch() falls
|
|
13
13
|
# back to enumerating refs: the canonical task/<full-id> ref wins, then a
|
|
14
14
|
# task/<id-prefix> ref is tolerated (matched by strict prefix, never
|
|
15
|
-
# created)
|
|
15
|
+
# created), then the existing worktree's actual checked-out branch
|
|
16
|
+
# (blocker 45, 2026-09-22 — an agent-improvised branch outside the task/
|
|
17
|
+
# namespace is invisible to the prefix scan; the classifier must see the
|
|
18
|
+
# world, not "branch does not exist"). Canonical stays task/<full-id>;
|
|
19
|
+
# ambiguous prefixes fail closed.
|
|
20
|
+
#
|
|
21
|
+
# prepare's reuse contract (blocker 45, 2026-09-22): an existing worktree
|
|
22
|
+
# prints REUSED only when it is checked out on the canonical task/<id>
|
|
23
|
+
# branch. A worktree checked out on another branch (an agent improvised
|
|
24
|
+
# with raw git) is repaired — the registry is re-registered under the
|
|
25
|
+
# actual branch (the branch is never deleted: its commits may be the
|
|
26
|
+
# task's work) — and prepare prints REPAIRED instead of REUSED, so the
|
|
27
|
+
# repair is attributable in the record.
|
|
16
28
|
#
|
|
17
29
|
# Commands:
|
|
18
30
|
# validate <task_id> — check task ID is safe for branches/paths
|
|
19
|
-
# prepare <task_id> — create worktree + branch, register ownership
|
|
31
|
+
# prepare <task_id> — create worktree + branch, register ownership (existing worktree: REUSED, or REPAIRED when re-registered under an improvised branch)
|
|
20
32
|
# resolve-branch <task_id> — print the resolved task branch name
|
|
21
33
|
# inspect <task_id> — diff against the integration target for review
|
|
22
34
|
# classify-branch <task_id> — mechanical branch-state classification for Review:
|
|
@@ -136,6 +148,17 @@ resolve_branch() {
|
|
|
136
148
|
echo "ERROR: task id '$task_id' matches multiple task branches: ${matches[*]} — ambiguous, refusing to guess" >&2
|
|
137
149
|
return 1
|
|
138
150
|
fi
|
|
151
|
+
# Blocker 45 (2026-09-22): the worktree's actual checkout is the mechanical
|
|
152
|
+
# truth of last resort. An agent-improvised branch (outside the task/
|
|
153
|
+
# namespace) is invisible to the prefix scan above — the classifier must
|
|
154
|
+
# see the world, not "branch does not exist".
|
|
155
|
+
if [ -d "$WORKTREE_DIR/$task_id" ]; then
|
|
156
|
+
local actual
|
|
157
|
+
if actual=$(git -C "$WORKTREE_DIR/$task_id" rev-parse --abbrev-ref HEAD 2>/dev/null) && [ -n "$actual" ]; then
|
|
158
|
+
printf '%s' "$actual"
|
|
159
|
+
return 0
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
139
162
|
printf '%s' "$canonical"
|
|
140
163
|
}
|
|
141
164
|
|
|
@@ -239,6 +262,25 @@ unregister() {
|
|
|
239
262
|
rm -f "$REGISTRY_DIR/$1"
|
|
240
263
|
}
|
|
241
264
|
|
|
265
|
+
# Blocker 45 (2026-09-22): repair the registry's branch attribution without
|
|
266
|
+
# touching anything else. An existing entry keeps its task_id, base_commit,
|
|
267
|
+
# and created fields — only branch= is rewritten. A missing entry is
|
|
268
|
+
# registered fresh (base_commit "unknown" — honest, not fabricated). The
|
|
269
|
+
# agent's branch is never deleted: its commits may be the task's work.
|
|
270
|
+
reregister_branch() {
|
|
271
|
+
local task_id="$1" branch="$2"
|
|
272
|
+
local reg="$REGISTRY_DIR/$task_id"
|
|
273
|
+
mkdir -p "$REGISTRY_DIR"
|
|
274
|
+
if [ -f "$reg" ] && grep -q '^branch=' "$reg"; then
|
|
275
|
+
# Escape sed-replacement metacharacters (branch names can carry &).
|
|
276
|
+
local esc=${branch//\\/\\\\}
|
|
277
|
+
esc=${esc//&/\\&}
|
|
278
|
+
sed -i "s|^branch=.*|branch=$esc|" "$reg"
|
|
279
|
+
else
|
|
280
|
+
register "$task_id" "unknown" "$branch" "$WORKTREE_DIR/$task_id"
|
|
281
|
+
fi
|
|
282
|
+
}
|
|
283
|
+
|
|
242
284
|
# --- commands ---
|
|
243
285
|
|
|
244
286
|
cmd_validate() {
|
|
@@ -256,9 +298,27 @@ cmd_prepare() {
|
|
|
256
298
|
|
|
257
299
|
# Reuse existing worktree
|
|
258
300
|
if [ -d "$WORKTREE_DIR/$task_id" ]; then
|
|
259
|
-
|
|
260
|
-
local head
|
|
301
|
+
local head actual_branch canonical_branch
|
|
261
302
|
head=$(cd "$WORKTREE_DIR/$task_id" && git rev-parse HEAD)
|
|
303
|
+
if ! actual_branch=$(cd "$WORKTREE_DIR/$task_id" && git rev-parse --abbrev-ref HEAD 2>/dev/null); then
|
|
304
|
+
echo "ERROR: worktree .worktrees/$task_id exists but its checked-out branch is unreadable — refusing to REUSE an unverifiable worktree"
|
|
305
|
+
return 1
|
|
306
|
+
fi
|
|
307
|
+
canonical_branch="task/$task_id"
|
|
308
|
+
# Blocker 45 (2026-09-22): REUSED asserted an invariant it never checked —
|
|
309
|
+
# the worktree is on the task's canonical branch. Room #29: a Build agent
|
|
310
|
+
# improvised branch crew-9eda33ca; the retry's prepare REUSED it blindly
|
|
311
|
+
# and Review's classify-branch parked on "branch does not exist". Verify
|
|
312
|
+
# the invariant; on mismatch repair the registry (never delete the
|
|
313
|
+
# agent's branch — its commits may be the task's work) and attribute the
|
|
314
|
+
# repair with REPAIRED instead of REUSED.
|
|
315
|
+
if [ "$actual_branch" != "$canonical_branch" ]; then
|
|
316
|
+
reregister_branch "$task_id" "$actual_branch"
|
|
317
|
+
echo "REPAIRED: worktree .worktrees/$task_id checked out on branch $actual_branch (expected $canonical_branch); registry re-registered"
|
|
318
|
+
echo "HEAD: $head"
|
|
319
|
+
return 0
|
|
320
|
+
fi
|
|
321
|
+
echo "REUSED: worktree exists at .worktrees/$task_id"
|
|
262
322
|
echo "HEAD: $head"
|
|
263
323
|
return 0
|
|
264
324
|
fi
|
package/package.json
CHANGED
|
@@ -87,9 +87,13 @@ The failures below are settled and recorded in the Gate 1 OODA state. The author
|
|
|
87
87
|
The diff and rules are suggestions now, not byte-exact commands (this also sidesteps the platform newline-escaping defect on verbatim requests — the receipt file is what matters, not byte fidelity).
|
|
88
88
|
Edit-request contract: call artifact_edit exactly once. Never retry the edit yourself. Tool unavailable (the artifact namespace would not load), a timeout, a transport error, or any otherwise ambiguous result is inconclusive: record NOTHING for this task, log the ambiguity in one line, and let the claim expire; the next scan re-claims and step 2's receipt check decides. An unrecorded outcome is the only honest record of an unknown one.
|
|
89
89
|
After the call returns, inspect the result text. (2026-09-20 REVIEW) Do NOT record an "acknowledged" builder report — the positive builder-report path was circular (the worker knows the version, so the report proves nothing) and is removed; the ack scan reads the version receipt from the artifact's disk as the sole positive evidence. If the result is an explicit refusal → `node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-builder-report --json '{"task_id":"<task_id>","version":"<version>","outcome":"refused","report_text":"<refusal text verbatim>"}'` (the command MECHANICALLY rejects unless `report_text` contains the exact version — if the refusal text lacks the version, do NOT call record-builder-report; just proceed to the record step with `refused`).
|
|
90
|
+
3b. **Attest the payload (code, diagnosis only — never a verdict).** After the `artifact_edit` call returns (only when it completed without error and without an explicit refusal), attest what you transmitted as the payload — two branches, no judgment:
|
|
91
|
+
- **Verbatim paste:** if you pasted the staged diff file's content unchanged into the handoff's `SUGGESTED PATCH` slot, the transmitted bytes are exactly the staged bytes: attest the `diff_sha256` you already verified in step 1's `sha256sum` (reuse that output — no second hashing needed).
|
|
92
|
+
- **Re-typed or altered paste:** if you re-typed, reformatted, or otherwise changed the diff while pasting, write the EXACT diff text you transmitted — the text between the ` ```diff ` fences of the handoff's `SUGGESTED PATCH` slot, fences excluded — to a temp file with your file-write tool (never through the shell: interpolating diff bytes into a quoted shell string corrupts `$`, backticks, quotes, and backslashes and would hash bytes that were never transmitted), then `sha256sum /tmp/<task_id>-payload.txt`. The write tool preserves exact bytes, including the trailing newline.
|
|
93
|
+
The wrapper prose around the patch is not attested — there is no canonical intended bytes for it; the payload is the patch. Pass the 64-hex hash as `transmitted_payload_sha256` on the `record-intent-issuance` call (step 4). Code compares it to the staged `diff_sha256` at record time: MATCH means the attested bytes matched the staged bytes; MISMATCH means the paste deviated (re-typing in transit is the failure mode this catches — the blocker-26 family). A MISMATCH is carried into the `version-acknowledged` note (`payload-fidelity: MISMATCH (transmitted <h> != staged <h>)`) — it never changes any verdict (the ack contract prices receipt-attestation, not byte equality). If the paste deviated, attest the deviation honestly and proceed to step 4 — do not re-paste, do not re-issue (step 3: never retry the edit). Perfect attestation is impossible (the paste itself can deviate); this instrument measures self-reported consistency — a lower bound on the true mutation rate, not the rate. Only an honest worker's MISMATCH carries signal: MATCH is uninformative by construction (copying `diff_sha256` from step 1 is uncloseable), and pre-transport truncation of the paste yields a silent false-MATCH — the quiet direction, accepted under diagnosis-only. Omit `transmitted_payload_sha256` if you cannot attest honestly; absent attestation changes nothing.
|
|
90
94
|
4. **Record the outcome (code):**
|
|
91
|
-
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-intent-issuance --json '{"task_id": "<task_id>", "claim_expiry": "<claim_expiry>", "edit_outcome": "<accepted|refused>", "refusal_text": "<first line>", "reclaimed": <true|false>, "receipt_check": "<different|unreadable>"}'`
|
|
92
|
-
(omit `refusal_text` unless refused; copy `reclaimed` verbatim from the scan entry — it is mechanical, never inferred; `receipt_check` is REQUIRED when `reclaimed` is true and must name the exact step-2 outcome — omit `receipt_check` when `reclaimed` is false. A `match` in step 2 never reaches this step: the edit already landed, so you ack instead of issuing.) Copy `claim_expiry` verbatim from the scan entry — it is the compare-and-swap. If the command returns `recorded: false`, log the reason and stop: another tick owns the task. `accepted` writes the issuer-stamped `submitted` ledger entry and mirrors `publish: edit-issued <commit> attempt=<n> version=<commit>:<attempt>` (the 4.5 ack scan claims it on a later tick); `refused` writes `rejected` and the terminal `publish: publish-refused` note. There is no outcome for an inconclusive call (step 3) — do not call record-intent-issuance at all when the edit's fate is unknown.
|
|
95
|
+
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-intent-issuance --json '{"task_id": "<task_id>", "claim_expiry": "<claim_expiry>", "edit_outcome": "<accepted|refused>", "refusal_text": "<first line>", "reclaimed": <true|false>, "receipt_check": "<different|unreadable>", "transmitted_payload_sha256": "<64-hex attested in step 3b>"}'`
|
|
96
|
+
(omit `refusal_text` unless refused; omit `transmitted_payload_sha256` unless you attested in step 3b (accepted outcomes only); copy `reclaimed` verbatim from the scan entry — it is mechanical, never inferred; `receipt_check` is REQUIRED when `reclaimed` is true and must name the exact step-2 outcome — omit `receipt_check` when `reclaimed` is false. A `match` in step 2 never reaches this step: the edit already landed, so you ack instead of issuing.) Copy `claim_expiry` verbatim from the scan entry — it is the compare-and-swap. If the command returns `recorded: false`, log the reason and stop: another tick owns the task. `accepted` writes the issuer-stamped `submitted` ledger entry and mirrors `publish: edit-issued <commit> attempt=<n> version=<commit>:<attempt>` (the 4.5 ack scan claims it on a later tick); `refused` writes `rejected` and the terminal `publish: publish-refused` note. There is no outcome for an inconclusive call (step 3) — do not call record-intent-issuance at all when the edit's fate is unknown.
|
|
93
97
|
- Prose never mints `publish:` notes directly. Any tick-authored publish note goes through exactly one path (code asserts the verb against the closed registry, rejects unknown verbs):
|
|
94
98
|
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-publish-note --json '{"task_id":"<task_id>","note":"<full note text>"}'`
|
|
95
99
|
- Crash rules (fail closed, never blind re-issue): if this tick dies between the artifact_edit call and the record step, the next scan re-claims with `reclaimed: true` and step 2's version-receipt check decides between ack (`record-version-ack` with `via: "disk"`) and a fresh issuance — recorded with its explicit `receipt_check`, since a differing/unreadable receipt is not proof the prior attempt's edit did not land. The issuance contract is at-most-one RECORDED issuance per attempt; a duplicate physical issuance across a lease expiry is content-identical (the same checksummed staged diff) and bounded by the ack scan's 2-hour budget.
|
package/workflows/bugfix.js
CHANGED
|
@@ -825,8 +825,52 @@ if (CAPTURE_INDEX < 0) throw new Error("STEPS missing 'Capture' step");
|
|
|
825
825
|
const REWORK_STEP = STEPS[BUILD_INDEX].name;
|
|
826
826
|
// Shared rework budget: Review and QA rejections draw from the SAME pool of 2.
|
|
827
827
|
// E.g. 2 Review bounces + 1 QA bounce = 3 total > budget -> task blocks.
|
|
828
|
+
// Blocker 46 (2026-09-22): the budget is task-durable, not per-run. A run
|
|
829
|
+
// ends on every operational failure (agent timeout, worktree-confinement
|
|
830
|
+
// failure) and the dispatcher launches a fresh run — the old `let ... = 0`
|
|
831
|
+
// reset the counter on each launch, so a task whose runs kept ending
|
|
832
|
+
// operationally between rejections reworked unboundedly while the designed
|
|
833
|
+
// park stayed unreachable (room #29 J1: 3 rejections across 4 runs, never
|
|
834
|
+
// parked). The counter is seeded from the task's durable event stream at
|
|
835
|
+
// run start (seedReworkCount, after the pin block); in-run increments are
|
|
836
|
+
// unchanged.
|
|
828
837
|
const MAX_TOTAL_REWORK = 2;
|
|
829
838
|
let totalReworkCount = 0;
|
|
839
|
+
// Blocker 46: the first-claim test used totalReworkCount === 0 as a proxy
|
|
840
|
+
// for "this run is entering its first step". The counter is now seeded from
|
|
841
|
+
// task history, so the proxy lies on re-run launches — use an explicit
|
|
842
|
+
// run-local flag instead. The first claim (self-claim with raced-duplicate
|
|
843
|
+
// stand-down) must fire exactly once per run.
|
|
844
|
+
let firstClaimDone = false;
|
|
845
|
+
|
|
846
|
+
// Blocker 46: count verdict-level `rejected` events (Review/QA rejections —
|
|
847
|
+
// record-phase writes the event type equal to the session status) in the
|
|
848
|
+
// task's durable event stream. Operational failure ends are `failed`, never
|
|
849
|
+
// `rejected`, so they must not spend retry headroom. A failed seed must not
|
|
850
|
+
// strand the run: start at 0 and let the in-run budget govern this run; the
|
|
851
|
+
// seed is retried on the next run.
|
|
852
|
+
async function seedReworkCount() {
|
|
853
|
+
try {
|
|
854
|
+
var seed = await agent(
|
|
855
|
+
"Read this task's event history.\n" +
|
|
856
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-events", { task_id: taskId, limit: 100 }) + "\n" +
|
|
857
|
+
"Count the events whose type is exactly \"rejected\".\n" +
|
|
858
|
+
"Return JSON { \"rejected_count\": <integer> } and nothing else.",
|
|
859
|
+
{
|
|
860
|
+
key: "rework-seed-" + taskId,
|
|
861
|
+
label: "Seeding shared rework budget from task history",
|
|
862
|
+
schema: { type: "object", properties: { rejected_count: { type: "number" } }, required: ["rejected_count"] }
|
|
863
|
+
}
|
|
864
|
+
);
|
|
865
|
+
var n = Math.floor(Number(seed && seed.rejected_count) || 0);
|
|
866
|
+
if (!(n >= 0)) n = 0;
|
|
867
|
+
if (n > 0) log("Shared rework budget seeded from task history: " + n + " prior rejection(s)");
|
|
868
|
+
return n;
|
|
869
|
+
} catch (e) {
|
|
870
|
+
log("Rework-budget seed unavailable (" + ((e && e.message) || String(e)).slice(0, 120) + ") — starting at 0");
|
|
871
|
+
return 0;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
830
874
|
let rejectionNotes = inputs.rejection_notes || "";
|
|
831
875
|
let mapperSpec = "";
|
|
832
876
|
// Visual verdict state: Sage's experiential flag (true/false/null until the
|
|
@@ -940,6 +984,11 @@ log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
|
940
984
|
CREW_API = CREW_API_PINNED;
|
|
941
985
|
log("Crew API pinned to " + CREW_API);
|
|
942
986
|
|
|
987
|
+
// Blocker 46: seed the task-durable rework budget from the task's event
|
|
988
|
+
// history before the phase loop. Pinned copy, so the seed reads the same
|
|
989
|
+
// release this run executes.
|
|
990
|
+
totalReworkCount = await seedReworkCount();
|
|
991
|
+
|
|
943
992
|
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
944
993
|
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
945
994
|
// made every concurrent acquire take the stale path). Set exactly once on
|
|
@@ -948,7 +997,7 @@ let lockHolder = taskId;
|
|
|
948
997
|
|
|
949
998
|
while (i < STEPS.length) {
|
|
950
999
|
const step = STEPS[i];
|
|
951
|
-
const isFirstClaim = (i === startStepIndex &&
|
|
1000
|
+
const isFirstClaim = (i === startStepIndex && !firstClaimDone);
|
|
952
1001
|
|
|
953
1002
|
phase(step.name);
|
|
954
1003
|
log(step.name + " step (" + step.identity + ") for task " + taskId);
|
|
@@ -1113,6 +1162,7 @@ while (i < STEPS.length) {
|
|
|
1113
1162
|
return { status: "duplicate", task_id: taskId, reason: "task already claimed by another run" };
|
|
1114
1163
|
}
|
|
1115
1164
|
activeSessionId = claimResult.session_id;
|
|
1165
|
+
firstClaimDone = true; // blocker 46: exactly once per run
|
|
1116
1166
|
} else {
|
|
1117
1167
|
const claimResult = await agent(
|
|
1118
1168
|
"Claim a session for this task step.\n" +
|
|
@@ -1587,7 +1637,7 @@ while (i < STEPS.length) {
|
|
|
1587
1637
|
reviewChangeExam =
|
|
1588
1638
|
"The branch has no commits ahead of the integration target (see MECHANICAL FACT below) — there is no diff to examine. Judge the Build report's `repo_diff: none` claim on its plausibility.\n\n";
|
|
1589
1639
|
reviewBranchRule =
|
|
1590
|
-
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the
|
|
1640
|
+
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the branch is empty and no task-attributed merge was verified', then end your report with exactly this line: VERDICT: FAIL.\n";
|
|
1591
1641
|
} else {
|
|
1592
1642
|
// Mechanical FAIL — Cass is never dispatched (see the dispatch gate
|
|
1593
1643
|
// below). Instructions are unused; keep the shape.
|
|
@@ -2080,14 +2130,15 @@ while (i < STEPS.length) {
|
|
|
2080
2130
|
"You are code-blind QA. You NEVER read source files. Public docs are not source — read them as a user would.\n" +
|
|
2081
2131
|
"STEP 1: Experiential visual inspection — drive the fixed artifact as a user would, one browser step at a time, and verify the reported bug is actually fixed.\n" +
|
|
2082
2132
|
"You have a see-act driver: " + crewHome + "/current/lib/see-act.js (a node script; one browser action per invocation; it prints one JSON line to stdout). It launches its own Chromium through a self-contained loopback proxy — the ONLY url you may give it is the local artifact server you start below. Never point it at any other URL.\n" +
|
|
2133
|
+
"SESSION PROTOCOL (multi-step flows): one-shot invocations launch a fresh browser each time, so they cannot drive flows where a later step needs an earlier step's living page state (open a dialog, then confirm it — the second step needs the first step's page state). For those, hold ONE browser alive: node " + crewHome + "/current/lib/see-act.js session-start --name qa --url http://localhost:<N>/ --log " + crewHome + "/task-evidence/" + taskId + "/postchange/ooda-log.jsonl — launches Chromium once in a detached daemon (its control server binds 127.0.0.1 only; the daemon exits after 10 min with no act). The --log path is the SAME ooda-log.jsonl the QA guard reads: every session-act appends its own JSON line in the exact step schema (step, attempt, action, args, exit, screenshot, observation), so you do NOT call append-ooda-step.js for session acts — READ each act's JSON result (the screenshot field is the path to READ with your read tool) and decide the next action. Then drive it: node " + crewHome + "/current/lib/see-act.js session-act --name qa --attempt \"1\" <aria|shot|click|scroll|type> [args] — one action inside the living page. Prefix EVERY session-act invocation with SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ (shell env vars do not persist between your commands, so prefix each one). When done: node " + crewHome + "/current/lib/see-act.js session-end --name qa — closes the browser, prints the session summary, and keeps the session dir as evidence. If session-start exits 3 with a \"NOT POSSIBLE: <reason>\" string, the environment cannot drive a browser: report NOT POSSIBLE: <reason> and continue with the mechanical checks — your VERDICT then covers mechanical checks only.\n" +
|
|
2083
2134
|
"Actions: aria | shot [--out <png>] [--full] | click [--out <png>] --selector <css> | scroll [--out <png>] --y <pixels|bottom> | type [--out <png>] --selector <css> --text <text>. Add --viewport mobile for a 390x844 frame. The JSON reports console_errors — treat any as a defect signal. Exit code 3 with not_possible set (a \"NOT POSSIBLE: <reason>\" string) means this environment cannot drive a browser: report NOT POSSIBLE: <reason> and continue with the mechanical checks — your VERDICT then covers mechanical checks only.\n" +
|
|
2084
2135
|
"a. Verify the built artifact exists: test -d ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/client/dist && test -f ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/server/dist/actions.js — if either is missing, report NOT POSSIBLE: built artifact not present at ~/workspace/ts-spaces/" + PUBLISH_SLUG + " and continue with the mechanical checks.\n" +
|
|
2085
2136
|
"b. Start the local artifact server detached with a log file, then poll the log for readiness (single shell invocation): CREW_HOME=" + crewHome + " setsid nohup node " + crewHome + "/current/lib/serve-artifact.js --space-dir ~/workspace/ts-spaces/" + PUBLISH_SLUG + " --port 0 --tag " + taskId + "-qa > /tmp/qa-server-" + taskId + ".log 2>&1 < /dev/null & for i in $(seq 1 30); do grep -q READY /tmp/qa-server-" + taskId + ".log && break; sleep 1; done; grep -o 'READY port=[0-9]*' /tmp/qa-server-" + taskId + ".log | cut -d= -f2 — the printed number is <N>. If no READY appears, report NOT POSSIBLE: local artifact server did not start (see /tmp/qa-server-" + taskId + ".log) and continue with the mechanical checks.\n" +
|
|
2086
2137
|
"c. Bounded see-act loop, at most 8 steps: re-run the reproduction steps for the reported bug — does it still occur? Then check the surrounding views for regressions. Prefix EVERY see-act invocation with SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ (shell env vars do not persist between your commands, so prefix each one) — every screenshot is then archived automatically as 001-shot-desktop.png, 002-click-desktop.png, ..., so no frame can be lost. Omit --out; the JSON's screenshot field is the path to READ with your read tool (you can see images).\n" +
|
|
2087
|
-
"c2. After EVERY see-act invocation, append it to the OODA report: node " + crewHome + "/current/lib/append-ooda-step.js --log " + crewHome + "/task-evidence/" + taskId + "/postchange/ooda-log.jsonl --attempt \"1\" --step <N> --action <aria|shot|click|scroll|type> --exit <exit-code> --screenshot <screenshot-from-JSON> --observation \"<1-2 sentences: what the frame showed and what you concluded>\" (omit --screenshot for aria). Steps are strictly monotonic: your first step is --step 1, then 2, 3, ... — the script rejects anything else. If you run the loop again (a new idea, after a restart), that run is --attempt \"2\": attempts never overwrite, they accumulate. Your observation is the payload — say what you saw and what it means, not just what you did. A step you ran but did not log is invisible to everyone after you.\n" +
|
|
2138
|
+
"c2. After EVERY one-shot see-act invocation, append it to the OODA report: node " + crewHome + "/current/lib/append-ooda-step.js --log " + crewHome + "/task-evidence/" + taskId + "/postchange/ooda-log.jsonl --attempt \"1\" --step <N> --action <aria|shot|click|scroll|type> --exit <exit-code> --screenshot <screenshot-from-JSON> --observation \"<1-2 sentences: what the frame showed and what you concluded>\" (omit --screenshot for aria). (Session acts are self-logging — session-act already appended its line; for those, READ the result instead of re-logging.) Steps are strictly monotonic: your first step is --step 1, then 2, 3, ... — the script rejects anything else. If you run the loop again (a new idea, after a restart), that run is --attempt \"2\": attempts never overwrite, they accumulate. Your observation is the payload — say what you saw and what it means, not just what you did. A step you ran but did not log is invisible to everyone after you.\n" +
|
|
2088
2139
|
"c2b. IMAGE TOOLS (when a raw screenshot is not enough evidence): python3 " + crewHome + "/current/lib/edit-image.py crop --in <frame.png> --out <crop.png> --region x,y,w,h — pixel-exact crop; zoom --factor <f> --center x,y — pixel-crisp zoom back to the original frame size; label --text \"<caption>\" — caption bar; nup --cols 2 --in <a.png> --in <b.png> --labels \"before|after\" — side-by-side grid. Deterministic, no network. Log each with --action crop|zoom|label|nup, --attempt \"1\", and --screenshot <the derivative you READ>. For rich compositions (before/after with real typography, annotated callouts), write an HTML page and render it: node " + crewHome + "/current/lib/render-html.js --in <page.html> --out <composed.png> [--width 1200] — headless Chromium, hermetic (remote assets are blocked and fail the render), deterministic; log with --action compose and --screenshot <the composition you READ>. Derivatives supplement, never replace: keep the source frame archived and name it in --args. READ every derivative you log — an unread image is not evidence.\n" +
|
|
2089
2140
|
"c3. Start with: SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ node " + crewHome + "/current/lib/see-act.js --url http://localhost:<N>/ aria — read the JSON, log the step. Then: SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ node " + crewHome + "/current/lib/see-act.js --url http://localhost:<N>/ shot — READ the screenshot, log the step. Act on what you see: click, scroll, type, then re-observe, logging each step. Prefer aria (cheap text) to find controls; screenshot when the view changes and for your final verdict frames (one desktop, one mobile). If a click exits non-zero, do NOT retry the same ref blindly: re-run aria first (refs go stale between invocations), then click the fresh ref exactly once. If it still fails, log the failure and move on — a flaky control is a finding, not a loop.\n" +
|
|
2090
|
-
"d.
|
|
2141
|
+
"d. Reach: with the session protocol, any flow reachable by N in-page actions is drivable — open the dialog, then confirm it, then judge the result. Without a session (one-shot invocations), anything reachable by (navigate, one action) is testable and sequences needing prior in-page state are not — use a session for those. Report NOT POSSIBLE only when the tooling itself fails (session-start exits 3): a flow you could not reach is not NOT POSSIBLE — name the exact step that stopped you in verdict.json's missing evidence and continue with the mechanical checks.\n" +
|
|
2091
2142
|
"e. Judge as a user against the task description: is the reported bug fixed AND is nothing else visibly broken? Look for broken layout, overflow, missing or wrong content, stale data, and console errors. Compare against the task's expected behavior, never against source code (you are code-blind). Every frame you captured is already archived under " + crewHome + "/task-evidence/" + taskId + "/postchange/ and indexed in ooda-log.jsonl. A frame you did not read is not evidence. Loading, error, or blank frames never pass. If you cannot complete the loop, say exactly which steps are missing — unknown is not PASS.\n" +
|
|
2092
2143
|
"f. Kill ONLY the server you started: pkill -f 'serve-artifact[.]js.*--tag " + taskId + "-qa' — never another task's server. (The [.] keeps pkill from matching its own command line.) Do not leave it running.\n" +
|
|
2093
2144
|
"Then continue with the mechanical checks below. Your VERDICT covers both the visual and the mechanical checks.\n\n" +
|
|
@@ -2234,7 +2285,7 @@ while (i < STEPS.length) {
|
|
|
2234
2285
|
: "Branch state (classified by the workflow from git): empty-no-work — the task branch has no commits ahead of the integration target, " +
|
|
2235
2286
|
"no task-attributed merge on the target was verified" + (branchStateErr ? " (branch classification itself failed: " + branchStateErr + ")" : "") + ", " +
|
|
2236
2287
|
"and the accepted Build report declared no `repo_diff: none` deliverable. " +
|
|
2237
|
-
"No change was reviewed because there is no change to review. The
|
|
2288
|
+
"No change was reviewed because there is no change to review. The branch is empty and no task-attributed merge was verified.\n") +
|
|
2238
2289
|
"Worktree: " + WORKTREE_HINT + "\n" +
|
|
2239
2290
|
"Recovery: commit the deliverable on the task branch in the worktree above and re-run Review. If the deliverable is genuinely runtime state outside the repo, declare `repo_diff: none` in the Build report instead of leaving the branch empty without a declaration.\n" +
|
|
2240
2291
|
"VERDICT: FAIL";
|
package/workflows/chore.js
CHANGED
|
@@ -793,6 +793,50 @@ if (CAPTURE_INDEX < 0) throw new Error("STEPS missing 'Capture' step");
|
|
|
793
793
|
const REWORK_STEP = STEPS[BUILD_INDEX].name;
|
|
794
794
|
const MAX_REWORK = 2;
|
|
795
795
|
let reworkCount = 0;
|
|
796
|
+
// Blocker 46 (2026-09-22): the budget is task-durable, not per-run. A run
|
|
797
|
+
// ends on every operational failure (agent timeout, worktree-confinement
|
|
798
|
+
// failure) and the dispatcher launches a fresh run — the old `let ... = 0`
|
|
799
|
+
// reset the counter on each launch, so a task whose runs kept ending
|
|
800
|
+
// operationally between rejections reworked unboundedly while the designed
|
|
801
|
+
// park stayed unreachable (room #29 J1: 3 rejections across 4 runs, never
|
|
802
|
+
// parked). The counter is seeded from the task's durable event stream at
|
|
803
|
+
// run start (seedReworkCount, after the pin block); in-run increments are
|
|
804
|
+
// unchanged.
|
|
805
|
+
// Blocker 46: the first-claim test used reworkCount === 0 as a proxy for
|
|
806
|
+
// "this run is entering its first step". The counter is now seeded from
|
|
807
|
+
// task history, so the proxy lies on re-run launches — use an explicit
|
|
808
|
+
// run-local flag instead. The first claim (self-claim with raced-duplicate
|
|
809
|
+
// stand-down) must fire exactly once per run.
|
|
810
|
+
let firstClaimDone = false;
|
|
811
|
+
|
|
812
|
+
// Blocker 46: count verdict-level `rejected` events (Review/QA rejections —
|
|
813
|
+
// record-phase writes the event type equal to the session status) in the
|
|
814
|
+
// task's durable event stream. Operational failure ends are `failed`, never
|
|
815
|
+
// `rejected`, so they must not spend retry headroom. A failed seed must not
|
|
816
|
+
// strand the run: start at 0 and let the in-run budget govern this run; the
|
|
817
|
+
// seed is retried on the next run.
|
|
818
|
+
async function seedReworkCount() {
|
|
819
|
+
try {
|
|
820
|
+
var seed = await agent(
|
|
821
|
+
"Read this task's event history.\n" +
|
|
822
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-events", { task_id: taskId, limit: 100 }) + "\n" +
|
|
823
|
+
"Count the events whose type is exactly \"rejected\".\n" +
|
|
824
|
+
"Return JSON { \"rejected_count\": <integer> } and nothing else.",
|
|
825
|
+
{
|
|
826
|
+
key: "rework-seed-" + taskId,
|
|
827
|
+
label: "Seeding shared rework budget from task history",
|
|
828
|
+
schema: { type: "object", properties: { rejected_count: { type: "number" } }, required: ["rejected_count"] }
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
var n = Math.floor(Number(seed && seed.rejected_count) || 0);
|
|
832
|
+
if (!(n >= 0)) n = 0;
|
|
833
|
+
if (n > 0) log("Shared rework budget seeded from task history: " + n + " prior rejection(s)");
|
|
834
|
+
return n;
|
|
835
|
+
} catch (e) {
|
|
836
|
+
log("Rework-budget seed unavailable (" + ((e && e.message) || String(e)).slice(0, 120) + ") — starting at 0");
|
|
837
|
+
return 0;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
796
840
|
let rejectionNotes = inputs.rejection_notes || "";
|
|
797
841
|
let mapperSpec = "";
|
|
798
842
|
// Visual verdict state: Sage's experiential flag (true/false/null until the
|
|
@@ -904,6 +948,11 @@ log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
|
904
948
|
CREW_API = CREW_API_PINNED;
|
|
905
949
|
log("Crew API pinned to " + CREW_API);
|
|
906
950
|
|
|
951
|
+
// Blocker 46: seed the task-durable rework budget from the task's event
|
|
952
|
+
// history before the phase loop. Pinned copy, so the seed reads the same
|
|
953
|
+
// release this run executes.
|
|
954
|
+
reworkCount = await seedReworkCount();
|
|
955
|
+
|
|
907
956
|
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
908
957
|
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
909
958
|
// made every concurrent acquire take the stale path). Set exactly once on
|
|
@@ -912,7 +961,7 @@ let lockHolder = taskId;
|
|
|
912
961
|
|
|
913
962
|
while (i < STEPS.length) {
|
|
914
963
|
const step = STEPS[i];
|
|
915
|
-
const isFirstClaim = (i === startStepIndex &&
|
|
964
|
+
const isFirstClaim = (i === startStepIndex && !firstClaimDone);
|
|
916
965
|
|
|
917
966
|
phase(step.name);
|
|
918
967
|
log(step.name + " step (" + step.identity + ") for task " + taskId);
|
|
@@ -1084,6 +1133,7 @@ while (i < STEPS.length) {
|
|
|
1084
1133
|
return { status: "duplicate", task_id: taskId, reason: "task already claimed by another run" };
|
|
1085
1134
|
}
|
|
1086
1135
|
activeSessionId = claimResult.session_id;
|
|
1136
|
+
firstClaimDone = true; // blocker 46: exactly once per run
|
|
1087
1137
|
await telemetryEvent("claim", step.name + " claimed");
|
|
1088
1138
|
// Flush telemetry before proceeding — the claim is a critical point.
|
|
1089
1139
|
// If a subsequent agent() fails, we want the claim event persisted.
|
|
@@ -1450,7 +1500,7 @@ while (i < STEPS.length) {
|
|
|
1450
1500
|
reviewChangeExam =
|
|
1451
1501
|
"The branch has no commits ahead of the integration target (see MECHANICAL FACT below) — there is no diff to examine. Judge the Build report's `repo_diff: none` claim on its plausibility.\n\n";
|
|
1452
1502
|
reviewBranchRule =
|
|
1453
|
-
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the
|
|
1503
|
+
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the branch is empty and no task-attributed merge was verified', then end your report with exactly this line: VERDICT: FAIL.\n";
|
|
1454
1504
|
} else {
|
|
1455
1505
|
// Mechanical FAIL — Cass is never dispatched (see the dispatch gate
|
|
1456
1506
|
// below). Instructions are unused; keep the shape.
|
|
@@ -1935,7 +1985,7 @@ while (i < STEPS.length) {
|
|
|
1935
1985
|
: "Branch state (classified by the workflow from git): empty-no-work — the task branch has no commits ahead of the integration target, " +
|
|
1936
1986
|
"no task-attributed merge on the target was verified" + (branchStateErr ? " (branch classification itself failed: " + branchStateErr + ")" : "") + ", " +
|
|
1937
1987
|
"and the accepted Build report declared no `repo_diff: none` deliverable. " +
|
|
1938
|
-
"No change was reviewed because there is no change to review. The
|
|
1988
|
+
"No change was reviewed because there is no change to review. The branch is empty and no task-attributed merge was verified.\n") +
|
|
1939
1989
|
"Worktree: " + WORKTREE_HINT + "\n" +
|
|
1940
1990
|
"Recovery: commit the deliverable on the task branch in the worktree above and re-run Review. If the deliverable is genuinely runtime state outside the repo, declare `repo_diff: none` in the Build report instead of leaving the branch empty without a declaration.\n" +
|
|
1941
1991
|
"VERDICT: FAIL";
|
package/workflows/standard.js
CHANGED
|
@@ -790,8 +790,52 @@ if (CAPTURE_INDEX < 0) throw new Error("STEPS missing 'Capture' step");
|
|
|
790
790
|
const REWORK_STEP = STEPS[BUILD_INDEX].name;
|
|
791
791
|
// Shared rework budget: Review and QA rejections draw from the SAME pool of 2.
|
|
792
792
|
// E.g. 2 Review bounces + 1 QA bounce = 3 total > budget -> task blocks.
|
|
793
|
+
// Blocker 46 (2026-09-22): the budget is task-durable, not per-run. A run
|
|
794
|
+
// ends on every operational failure (agent timeout, worktree-confinement
|
|
795
|
+
// failure) and the dispatcher launches a fresh run — the old `let ... = 0`
|
|
796
|
+
// reset the counter on each launch, so a task whose runs kept ending
|
|
797
|
+
// operationally between rejections reworked unboundedly while the designed
|
|
798
|
+
// park stayed unreachable (room #29 J1: 3 rejections across 4 runs, never
|
|
799
|
+
// parked). The counter is seeded from the task's durable event stream at
|
|
800
|
+
// run start (seedReworkCount, after the pin block); in-run increments are
|
|
801
|
+
// unchanged.
|
|
793
802
|
const MAX_TOTAL_REWORK = 2;
|
|
794
803
|
let totalReworkCount = 0;
|
|
804
|
+
// Blocker 46: the first-claim test used totalReworkCount === 0 as a proxy
|
|
805
|
+
// for "this run is entering its first step". The counter is now seeded from
|
|
806
|
+
// task history, so the proxy lies on re-run launches — use an explicit
|
|
807
|
+
// run-local flag instead. The first claim (self-claim with raced-duplicate
|
|
808
|
+
// stand-down) must fire exactly once per run.
|
|
809
|
+
let firstClaimDone = false;
|
|
810
|
+
|
|
811
|
+
// Blocker 46: count verdict-level `rejected` events (Review/QA rejections —
|
|
812
|
+
// record-phase writes the event type equal to the session status) in the
|
|
813
|
+
// task's durable event stream. Operational failure ends are `failed`, never
|
|
814
|
+
// `rejected`, so they must not spend retry headroom. A failed seed must not
|
|
815
|
+
// strand the run: start at 0 and let the in-run budget govern this run; the
|
|
816
|
+
// seed is retried on the next run.
|
|
817
|
+
async function seedReworkCount() {
|
|
818
|
+
try {
|
|
819
|
+
var seed = await agent(
|
|
820
|
+
"Read this task's event history.\n" +
|
|
821
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-events", { task_id: taskId, limit: 100 }) + "\n" +
|
|
822
|
+
"Count the events whose type is exactly \"rejected\".\n" +
|
|
823
|
+
"Return JSON { \"rejected_count\": <integer> } and nothing else.",
|
|
824
|
+
{
|
|
825
|
+
key: "rework-seed-" + taskId,
|
|
826
|
+
label: "Seeding shared rework budget from task history",
|
|
827
|
+
schema: { type: "object", properties: { rejected_count: { type: "number" } }, required: ["rejected_count"] }
|
|
828
|
+
}
|
|
829
|
+
);
|
|
830
|
+
var n = Math.floor(Number(seed && seed.rejected_count) || 0);
|
|
831
|
+
if (!(n >= 0)) n = 0;
|
|
832
|
+
if (n > 0) log("Shared rework budget seeded from task history: " + n + " prior rejection(s)");
|
|
833
|
+
return n;
|
|
834
|
+
} catch (e) {
|
|
835
|
+
log("Rework-budget seed unavailable (" + ((e && e.message) || String(e)).slice(0, 120) + ") — starting at 0");
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
795
839
|
let rejectionNotes = inputs.rejection_notes || "";
|
|
796
840
|
let mapperSpec = "";
|
|
797
841
|
// Visual verdict state: Sage's experiential flag (true/false/null until the
|
|
@@ -899,6 +943,11 @@ log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
|
899
943
|
CREW_API = CREW_API_PINNED;
|
|
900
944
|
log("Crew API pinned to " + CREW_API);
|
|
901
945
|
|
|
946
|
+
// Blocker 46: seed the task-durable rework budget from the task's event
|
|
947
|
+
// history before the phase loop. Pinned copy, so the seed reads the same
|
|
948
|
+
// release this run executes.
|
|
949
|
+
totalReworkCount = await seedReworkCount();
|
|
950
|
+
|
|
902
951
|
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
903
952
|
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
904
953
|
// made every concurrent acquire take the stale path). Set exactly once on
|
|
@@ -907,7 +956,7 @@ let lockHolder = taskId;
|
|
|
907
956
|
|
|
908
957
|
while (i < STEPS.length) {
|
|
909
958
|
const step = STEPS[i];
|
|
910
|
-
const isFirstClaim = (i === startStepIndex &&
|
|
959
|
+
const isFirstClaim = (i === startStepIndex && !firstClaimDone);
|
|
911
960
|
|
|
912
961
|
phase(step.name);
|
|
913
962
|
log(step.name + " step (" + step.identity + ") for task " + taskId);
|
|
@@ -1072,6 +1121,7 @@ while (i < STEPS.length) {
|
|
|
1072
1121
|
return { status: "duplicate", task_id: taskId, reason: "task already claimed by another run" };
|
|
1073
1122
|
}
|
|
1074
1123
|
activeSessionId = claimResult.session_id;
|
|
1124
|
+
firstClaimDone = true; // blocker 46: exactly once per run
|
|
1075
1125
|
} else {
|
|
1076
1126
|
const claimResult = await agent(
|
|
1077
1127
|
"Claim a session for this task step.\n" +
|
|
@@ -1476,7 +1526,7 @@ while (i < STEPS.length) {
|
|
|
1476
1526
|
reviewChangeExam =
|
|
1477
1527
|
"The branch has no commits ahead of the integration target (see MECHANICAL FACT below) — there is no diff to examine. Judge the Build report's `repo_diff: none` claim on its plausibility.\n\n";
|
|
1478
1528
|
reviewBranchRule =
|
|
1479
|
-
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the
|
|
1529
|
+
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the branch is empty and no task-attributed merge was verified', then end your report with exactly this line: VERDICT: FAIL.\n";
|
|
1480
1530
|
} else {
|
|
1481
1531
|
// Mechanical FAIL — Cass is never dispatched (see the dispatch gate
|
|
1482
1532
|
// below). Instructions are unused; keep the shape.
|
|
@@ -1969,14 +2019,15 @@ while (i < STEPS.length) {
|
|
|
1969
2019
|
"Public docs (API.md, README, published action schemas) are NOT source code — read them freely, exactly as a user would.\n\n" +
|
|
1970
2020
|
"STEP 1: Experiential visual inspection — drive the artifact as a user would, one browser step at a time.\n" +
|
|
1971
2021
|
"You have a see-act driver: " + crewHome + "/current/lib/see-act.js (a node script; one browser action per invocation; it prints one JSON line to stdout). It launches its own Chromium through a self-contained loopback proxy — the ONLY url you may give it is the local artifact server you start below. Never point it at any other URL.\n" +
|
|
2022
|
+
"SESSION PROTOCOL (multi-step flows): one-shot invocations launch a fresh browser each time, so they cannot drive flows where a later step needs an earlier step's living page state (open a deck, then click Study — the second step needs the first step's page state). For those, hold ONE browser alive: node " + crewHome + "/current/lib/see-act.js session-start --name qa --url http://localhost:<N>/ --log " + crewHome + "/task-evidence/" + taskId + "/postchange/ooda-log.jsonl — launches Chromium once in a detached daemon (its control server binds 127.0.0.1 only; the daemon exits after 10 min with no act). The --log path is the SAME ooda-log.jsonl the QA guard reads: every session-act appends its own JSON line in the exact step schema (step, attempt, action, args, exit, screenshot, observation), so you do NOT call append-ooda-step.js for session acts — READ each act's JSON result (the screenshot field is the path to READ with your read tool) and decide the next action. Then drive it: node " + crewHome + "/current/lib/see-act.js session-act --name qa --attempt \"1\" <aria|shot|click|scroll|type> [args] — one action inside the living page. Prefix EVERY session-act invocation with SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ (shell env vars do not persist between your commands, so prefix each one). When done: node " + crewHome + "/current/lib/see-act.js session-end --name qa — closes the browser, prints the session summary, and keeps the session dir as evidence. If session-start exits 3 with a \"NOT POSSIBLE: <reason>\" string, the environment cannot drive a browser: report NOT POSSIBLE: <reason> and judge what you can.\n" +
|
|
1972
2023
|
"Actions: aria | shot [--out <png>] [--full] | click [--out <png>] --selector <css> | scroll [--out <png>] --y <pixels|bottom> | type [--out <png>] --selector <css> --text <text>. Add --viewport mobile for a 390x844 frame. The JSON reports console_errors — treat any as a defect signal. Exit code 3 with not_possible set (a \"NOT POSSIBLE: <reason>\" string) means this environment cannot drive a browser: report NOT POSSIBLE: <reason> and continue with the mechanical checks — your VERDICT then covers mechanical checks only.\n" +
|
|
1973
2024
|
"a. Verify the built artifact exists: test -d ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/client/dist && test -f ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/server/dist/actions.js — if either is missing, report NOT POSSIBLE: built artifact not present at ~/workspace/ts-spaces/" + PUBLISH_SLUG + " and continue with the mechanical checks.\n" +
|
|
1974
2025
|
"b. Start the local artifact server detached with a log file, then poll the log for readiness (single shell invocation): CREW_HOME=" + crewHome + " setsid nohup node " + crewHome + "/current/lib/serve-artifact.js --space-dir ~/workspace/ts-spaces/" + PUBLISH_SLUG + " --port 0 --tag " + taskId + "-qa > /tmp/qa-server-" + taskId + ".log 2>&1 < /dev/null & for i in $(seq 1 30); do grep -q READY /tmp/qa-server-" + taskId + ".log && break; sleep 1; done; grep -o 'READY port=[0-9]*' /tmp/qa-server-" + taskId + ".log | cut -d= -f2 — the printed number is <N>. If no READY appears, report NOT POSSIBLE: local artifact server did not start (see /tmp/qa-server-" + taskId + ".log) and continue with the mechanical checks.\n" +
|
|
1975
2026
|
"c. Bounded see-act loop, at most 8 steps: drive the artifact as a user would, one browser step at a time. Prefix EVERY see-act invocation with SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ (shell env vars do not persist between your commands, so prefix each one) — every screenshot is then archived automatically as 001-shot-desktop.png, 002-click-desktop.png, ..., so no frame can be lost. Omit --out; the JSON's screenshot field is the path to READ with your read tool (you can see images).\n" +
|
|
1976
|
-
"c2. After EVERY see-act invocation, append it to the OODA report: node " + crewHome + "/current/lib/append-ooda-step.js --log " + crewHome + "/task-evidence/" + taskId + "/postchange/ooda-log.jsonl --attempt \"1\" --step <N> --action <aria|shot|click|scroll|type> --exit <exit-code> --screenshot <screenshot-from-JSON> --observation \"<1-2 sentences: what the frame showed and what you concluded>\" (omit --screenshot for aria). Steps are strictly monotonic: your first step is --step 1, then 2, 3, ... — the script rejects anything else. If you run the loop again (a new idea, after a restart), that run is --attempt \"2\": attempts never overwrite, they accumulate. Your observation is the payload — say what you saw and what it means, not just what you did. A step you ran but did not log is invisible to everyone after you.\n" +
|
|
2027
|
+
"c2. After EVERY one-shot see-act invocation, append it to the OODA report: node " + crewHome + "/current/lib/append-ooda-step.js --log " + crewHome + "/task-evidence/" + taskId + "/postchange/ooda-log.jsonl --attempt \"1\" --step <N> --action <aria|shot|click|scroll|type> --exit <exit-code> --screenshot <screenshot-from-JSON> --observation \"<1-2 sentences: what the frame showed and what you concluded>\" (omit --screenshot for aria). (Session acts are self-logging — session-act already appended its line; for those, READ the result instead of re-logging.) Steps are strictly monotonic: your first step is --step 1, then 2, 3, ... — the script rejects anything else. If you run the loop again (a new idea, after a restart), that run is --attempt \"2\": attempts never overwrite, they accumulate. Your observation is the payload — say what you saw and what it means, not just what you did. A step you ran but did not log is invisible to everyone after you.\n" +
|
|
1977
2028
|
"c2b. IMAGE TOOLS (when a raw screenshot is not enough evidence): python3 " + crewHome + "/current/lib/edit-image.py crop --in <frame.png> --out <crop.png> --region x,y,w,h — pixel-exact crop; zoom --factor <f> --center x,y — pixel-crisp zoom back to the original frame size; label --text \"<caption>\" — caption bar; nup --cols 2 --in <a.png> --in <b.png> --labels \"before|after\" — side-by-side grid. Deterministic, no network. Log each with --action crop|zoom|label|nup, --attempt \"1\", and --screenshot <the derivative you READ>. For rich compositions (before/after with real typography, annotated callouts), write an HTML page and render it: node " + crewHome + "/current/lib/render-html.js --in <page.html> --out <composed.png> [--width 1200] — headless Chromium, hermetic (remote assets are blocked and fail the render), deterministic; log with --action compose and --screenshot <the composition you READ>. Derivatives supplement, never replace: keep the source frame archived and name it in --args. READ every derivative you log — an unread image is not evidence.\n" +
|
|
1978
2029
|
"c3. Start with: SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ node " + crewHome + "/current/lib/see-act.js --url http://localhost:<N>/ aria — read the JSON, log the step. Then: SEE_ACT_ARCHIVE_DIR=" + crewHome + "/task-evidence/" + taskId + "/postchange/ node " + crewHome + "/current/lib/see-act.js --url http://localhost:<N>/ shot — READ the screenshot, log the step. Act on what you see: click, scroll, type, then re-observe, logging each step. Prefer aria (cheap text) to find controls; screenshot when the view changes and for your final verdict frames (one desktop, one mobile). If a click exits non-zero, do NOT retry the same ref blindly: re-run aria first (refs go stale between invocations), then click the fresh ref exactly once. If it still fails, log the failure and move on — a flaky control is a finding, not a loop.\n" +
|
|
1979
|
-
"d.
|
|
2030
|
+
"d. Reach: with the session protocol, any flow reachable by N in-page actions is drivable — open the deck, then click Study, then judge the study view. Without a session (one-shot invocations), anything reachable by (navigate, one action) is testable and sequences needing prior in-page state are not — use a session for those. Report NOT POSSIBLE only when the tooling itself fails (session-start exits 3): a flow you could not reach is not NOT POSSIBLE — name the exact step that stopped you in verdict.json's missing evidence and judge what you did reach.\n" +
|
|
1980
2031
|
"e. Judge as a user against the task description: does the change render correctly? Look for broken layout, overflow, missing or wrong content, stale data, and console errors. Compare against the task's expected behavior, never against source code (you are code-blind). Every frame you captured is already archived under " + crewHome + "/task-evidence/" + taskId + "/postchange/ and indexed in ooda-log.jsonl. A frame you did not read is not evidence. Loading, error, or blank frames never pass. If you cannot complete the loop, say exactly which steps are missing — unknown is not PASS.\n" +
|
|
1981
2032
|
"f. Kill ONLY the server you started: pkill -f 'serve-artifact[.]js.*--tag " + taskId + "-qa' — never another task's server. (The [.] keeps pkill from matching its own command line.) Do not leave it running.\n" +
|
|
1982
2033
|
"Then continue with the mechanical checks below. Your VERDICT covers both the visual and the mechanical checks.\n\n" +
|
|
@@ -2101,7 +2152,7 @@ while (i < STEPS.length) {
|
|
|
2101
2152
|
: "Branch state (classified by the workflow from git): empty-no-work — the task branch has no commits ahead of the integration target, " +
|
|
2102
2153
|
"no task-attributed merge on the target was verified" + (branchStateErr ? " (branch classification itself failed: " + branchStateErr + ")" : "") + ", " +
|
|
2103
2154
|
"and the accepted Build report declared no `repo_diff: none` deliverable. " +
|
|
2104
|
-
"No change was reviewed because there is no change to review. The
|
|
2155
|
+
"No change was reviewed because there is no change to review. The branch is empty and no task-attributed merge was verified.\n") +
|
|
2105
2156
|
"Worktree: " + WORKTREE_HINT + "\n" +
|
|
2106
2157
|
"Recovery: commit the deliverable on the task branch in the worktree above and re-run Review. If the deliverable is genuinely runtime state outside the repo, declare `repo_diff: none` in the Build report instead of leaving the branch empty without a declaration.\n" +
|
|
2107
2158
|
"VERDICT: FAIL";
|