Repository navigation
First run: a card follows the first task from File one to its PR; the Start card counts only steps before Start (#741) [PROTECTED: owner merges] - #816
Conversation
…re Start The Start card counted every required step not done whose fix isn't Start (FirstRun.tsx). That included Your first task, which comes after Start and which Start leads to, so a person who had done every step before Start was told "1 required step isn't done yet: the coordinator may stop at it" before they had any reason to file a task (#741). The count is now startBlockers: the required steps not done before the Start step, in screen order. With no Start step in the mode, every required step not done counts, as before. Test (fails without): test/web-render.test.ts, "#741 the Start card counts only the steps before Start". The "still warns ... as before" test passes before and after: it keeps the warning for a step before Start. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…the events the engine records After "File one" the first-run page showed one line and the issue's link, then nothing until a PR showed up elsewhere (#741). GET /api/first-run (writers only, unchanged) now carries firstTask: the latest issue the form filed (its first_run.first_task_filed audit entry), followed through that issue's own events recorded after it: claimed, the worker's run, the evaluator's verdict, the PR the harness opened for it, that PR's checks, then merged, closed, or held. Nothing is guessed: a stage with no event is waiting, a verdict from an earlier attempt is stale once the worker runs again, and only events of that PR number count for its merge. Each stage not done carries an estimate that says its basis: the coordinator's tick (60 s plus up to 5 s; a test keeps it equal to src/service.ts), the median of earlier tasks here, a labelled guess (the review's 15-60 minutes to a PR, only when nothing here has timed one), or none yet. A held PR's reasons are said in plain words, one line per kind, with what lets it through (src/hold-words.ts: the table #706 slice 2 asks for, one entry per reason prefix mergeDecision emits; an unknown reason shows as its own text). In local mode the kill switch reads as #729 words it: every PR waits for you; no policy.yaml is named. The PR is linked only by GitHub's https page of that PR in this repo. The card shows no run path, run error text, advice or login. It is read-only, and viewers still get 403. Test (fails without): test/first-task-progress.test.ts (the /api/first-run test fails on main: firstTask is undefined), test/web-render.test.ts ("#741 the First task card"), test/viewer-events.test.ts ("#741 a held first task"). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…medians, shorter modules CI's package-size test failed on f739109: the tarball 1900939 bytes (budget 1900000), unpacked 5933368 (budget 5930000). Raising PACKAGE_BUDGET is the owner's call, so the card is made smaller instead. The per-stage medians of earlier tasks are gone: on a first run there is no earlier task, so they never showed where the card matters. The worker, evaluator and PR stages now carry the review's guess, labelled as one (15-60 minutes from the claim to a PR, for these stages together); checks say there is no estimate; the claim keeps the coordinator's tick. first-task-progress and hold-words are written more compactly, with the same behaviour otherwise. Locally, npm pack --dry-run: main 1879443 / 5869995, this branch 1883548 / 5885690. Test (fails without): test/package-files.test.ts (the size budget) failed in CI on f739109; test/first-task-progress.test.ts now asserts the guess and the absence of a made-up CI estimate. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: CHANGES NEEDED (round 1)Reviewed at head c7bae71, judged by the DECISION GUIDE. The Start count is correct and causes no regression. The fold handles another issue, an older PR, events before the filing, a refiled task, duplicates and a retried worker. Two things are wrong. (1) The merge stage can show a false state after the PR's head moves. (2) The card renders raw free text, run errors, paths and evaluator text, through the block line and through hold reasons that Scratch probes, fixtures only, no repo file edited: Acceptance criteria
Fail-without (measured)
Can the card show a false state? (check 1)
Leaks (check 2)
Principle 6 (check 3)Read-only. The card has one No regression (check 4)
Hold-words coverage (check 5)
Protection, scope, overlaps
CI (measured)
Required changes
Not required (suggestions)
For the owner
Browser check owed (human)Use the verify-dashboard skill against a stand-in dashboard (scratch state, no real events DB). Before and after screenshots of First run in local mode, with zero new console errors, showing:
|
…n from an older head Review round 1 on #816 (issuecomment-6272706512): - A merge.decided or merge.failed counts only at the PR's latest known head (its latest pr.opened, pr.status, pr.ready or pr.unready). A held decision at head A followed by a push of B no longer shows A's hold; an auto decision at A followed by failing checks on B no longer says "will merge it". The stage waits: "a new head: it waits for its checks and a fresh decision". - The card carries no free text. The block line is fixed words pointing to the Board (never issue.blocked.why); the issue's title is gone; a run's end reason and a verdict's confidence pass only as enum words. - src/hold-words.ts words every kind merge.decided carries today, not only mergeDecision's: GitHub can't merge cleanly, merging failed, GitHub still computing, the combined check (conflict, no branch point, fast tests failing), the docs-only ruleset hold, and conflict fix. An unknown prefix reads "Another reason (see the Board)", never its text. The design sentence now also names a possible weakened check (#820), worded as examples so it holds whether or not #820 lands first. - A new hold prefix fails a test by design: a source scan over every source of merge.decided reasons (merge-policy.ts, merge-flow.ts mergeOrWait, coordinator.ts combinedGate, pr-rules.ts rulesetHold, conflicts.ts conflictWaitReasons) checks each literal reason's prefix has words, and that every reason pushed from a variable is a traced one. Fixture text proves it catches a new prefix and a new untraced source. - prLink fails closed: no repo given, no link. Correction to 11c9f66's message: its "still warns ... as before" test does not pass before; it fails on main, which counts 2 steps (Your first task too) where the test expects 1. Test (fails without): test/first-task-progress.test.ts, "#741 a decision or a failed merge from an older head", "#741 the card shows no evaluator text, path or run detail", and the source-scan test (its fixture lines). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…kage headroom Merged with origin/main 128453a, npm pack --dry-run measured 1899892 tarball bytes of a 1900000 budget at 9d22da1. Only comments change. Test (fails without): none; behaviour unchanged (test/first-task-progress.test.ts passes as before). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 2: what changed (head 0f5ccf2)
Also:
Fail-without on c7bae71 (measured): 165 tests, 157 pass, 8 fail. These are the 7 new or changed fold tests and the web-render card test. Package (measured, Merged with main (measured): |
Evaluator verdict: CHANGES NEEDED (round 2)Reviewed at head 0f5ccf2, judged by the DECISION GUIDE. All four round-1 changes are in. Old-head decisions now wait, the block line and hold kinds are fixed words, and the claims are corrected. Three gaps remain, all cheap:
Every fix follows principles 1, 4 and 5, so none needs an owner decision. Probes (fixtures only, no repo file edited) are in the session scratchpad under
Everything ran with a scratch HOME, TMPDIR and state dirs, and a stand-in Round-1 changes
Check 2: attacking the source scan (measured, probe3.mjs)Emitters. What feeds that emitter's
Each attack below adds one line to the real source text in memory and runs the scan on it:
The card stays safe in every missed case: the reason reads "Another reason (see the Board)", never its text (measured). So a gap costs the owner the plain words, not a leak. Round 1 asked that a new prefix fail a test, and these cases don't. Check 3: #820
Check 4: every rendered field (component and API read; probe4.mjs)
The PR body's Compromised component says that since 9d22da1 the agent user "can't put text of its choice on the card". That is false: the agent user writes the DB directly, as the body itself assumes. Check 5: no regression
Check 6: the usual checks (measured)
Required changes
Not required
Browser check still owed (human)Use the verify-dashboard skill against a stand-in dashboard (scratch state, no real events DB). Take before and after screenshots of First run in local mode, with zero new console errors:
|
…ema type; the hold scan fails closed Review round 2 on #816 (issuecomment-6273455831): - Forged rows (the event log is never re-validated on read): a run's end reason and a verdict's confidence show only as members of the schema's own enums (EventSchemas run.finished reason, eval.verdict confidence), else "unknown"; the attempt, the PR number and the filed issue only as positive safe integers; every time only as a parseable date (normalized), else dropped. A forged "merge_it_now_on_the_board", "high_so_merge_it_yourself" or 1e21 no longer reaches the card. - Hold sentences made true: design names a dependency manifest that changes; doubt's list says "for example" and names no verdict, a doubted test and no design answer; merge-failed says "at least 3 times". - The source scan fails closed: - every emitter of merge.decided and conflict_fix.finished in src/ must sit in a scanned region (a conflict_fix.finished with reasons: [] in the call itself is allowed: recovery's "interrupted"); - new regions: mergeDecisionFor, fix-runs.ts conflictRun and giveUpConflict, and merge-policy's reason functions one by one; - traced values match whole expressions (a call: any arguments), each naming the region that builds what it carries in, and that region must be scanned; - a non-literal why/hold value or reasons initializer, and any unshift, splice or concat, fail unless traced; - the tokenizer skips strings, templates and comments. Each way round it the review found is proved on fixture text. Known gaps, named in the test: a traced helper or table lookup that changes what it returns, and reasons passed in through a new MergeInput field (the push in mergeDecision is scanned, the field's builder is not unless traced). Test (fails without): test/first-task-progress.test.ts, "forged rows" and "the scan fails closed". Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 3: what changed (head 27902ca)
Fail-without on 0f5ccf2 (measured): 167 tests, 166 pass, 1 fail ( Package (measured, Merged-tree tests (measured): first-task-progress, viewer-events, web-render, feature-map and package-files ran 179 of 179. |
Evaluator verdict: CHANGES NEEDED (round 3)Reviewed at head 27902ca and judged by the DECISION GUIDE. Most of round 2 is in:
Two defects remain, both cheap:
Both fixes follow principles 1, 4 and 5, so neither needs an owner decision. Probes (fixture text and in-memory rows only, no repo file edited) are in the session scratchpad under
Everything ran with a scratch HOME, TMPDIR and state dirs, and a stand-in Check 1: forged rows (measured, probe6.mjs)
Check 2: the scan (measured, probe5.mjs)Emitter set: complete (measured).
Probe 3's rows against the round-3 scan:
Why E is missed.
New forms the scan misses (measured):
As you allowed, these may be named rather than chased: each costs only the plain words ("Another reason (see the Board)"), and nothing leaks (measured in round 2). Check 3: the hold sentences (read against
|
… reads mergeDecisionFor's definition Review round 3 on #816 (issuecomment-6274088682): - The card never takes the first-run page down: the route builds it through firstTaskCard, which returns null (the card left out) when the fold throws, and logs that once; Start, the steps and the rest still answer. - The fold itself no longer throws on a forged row: no String() on a value from an event (a check outcome is compared as is, a non-string hold reason reads as an unknown one), a null payload reads as empty, and a pr.opened whose number is not a positive safe integer is not an opened PR (never a done stage "PR #null"). - The scan: region() requires its signature exactly once, and mergeDecisionFor's is now unique (" mergeDecisionFor(n: number"); before, the first match was a call at line 163, so the definition was never scanned. A fixture puts a call before the definition, and the review's E1-E3 are proved on merge-flow.ts's real text changed in memory. - recovery's exception is exactly `reasons: []` among the call's top-level fields with no spread (the review's N7 is a fixture). - The remaining scan gaps are named in the test: index writes, push.apply, a shorthand { hold }, ??=, and an emitter by a constant type, another method name or emit?.(. Test (fails without): test/first-task-progress.test.ts, "forged rows of every JS type" and "a card that cannot be built drops the card only"; the scan's E1-E3 and call-before-definition cases. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 4: what changed (head f93efce)
Fail-without on 27902ca (measured): 174 tests, 172 pass, 2 fail. After (measured): 174 of 174, also on the tree merged with origin/main d4b4503. Package: over budget, and I've stopped. Measured with
Main alone is already over the 1,900,000 / 5,930,000 budget, after #805 and #787. This PR adds about 4.2 KB to the tarball, more than any compaction can recover. The budget is unchanged: whether to raise it, or have the card wait, is the owner's call. |
… task card (#741) The owner's standing practice (2026-10-19): the new size plus 2%, rounded up to 1,000. Measured with npm pack --dry-run --json on #816 (f93efce) merged with origin/main d4b4503, the dashboard UI built: tarball 1,906,348, unpacked 5,950,518 (main alone: 1,902,158 / 5,934,411, already over the old budget). 1,906,348 x 1.02 = 1,944,475 -> 1,945,000; 5,950,518 x 1.02 = 6,069,529 -> 6,070,000. What grew, file by file against main (only intended code; no sourcemap, test, fixture or stray file; the other web assets only changed their hash names, at the same sizes): dist/src/first-task-progress.js +8,846 (new), dist/src/hold-words.js +3,758 (new), dist/web FirstRun chunk +3,024, dist/src/dashboard.js +406, the CSS +73 (the card's classes). Total +16,107 unpacked, +4,190 tarball. Test (fails without): test/package-files.test.ts, "the package stays within its size budget", failed in CI on f93efce (run 39302117550) with those numbers. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: CHANGES NEEDED (round 4)Reviewed at head 0c1a7ff (round-4 fixes f93efce, then the package-budget raise). Judged by the DECISION GUIDE. All four round-3 changes are in, and the budget raise checks out file by file. Two cheap items remain:
Neither needs an owner decision: principles 4, 5 and 9 settle both. How I ran things: fixture text and in-memory rows only, with no repo file edited. Every run used a scratch HOME, TMPDIR and state dirs, and a stand-in Round-3 required changes
Item 2 in detail:
The N7 fixture and the exception (measured, probe8)These forms are CAUGHT:
Recovery's real call at These forms are EXEMPT, but they shouldn't be:
Cause: Cost: only the plain words. A pushed fix's reasons with an unworded prefix read "Another reason (see the Board)", and nothing leaks (inferred from The package budget (measured,
|
| File | Unpacked |
|---|---|
dist/src/first-task-progress.js (new) |
+8,846 |
dist/src/hold-words.js (new) |
+3,758 |
dist/web/assets/FirstRun-*.js |
+3,024 |
dist/src/dashboard.js |
+406 |
dist/web/assets/index-*.css |
+73 |
No other file changed size. No new .map, .d.ts, test, fixture, .tgz or stray path. This matches the builder's breakdown exactly.
Merged numbers:
- Main d4b4503 alone packs to 1,902,158 / 5,934,411, the builder's numbers.
- Main plus this PR's growth gives unpacked 5,934,411 + 16,107 = 5,950,518. That is exactly the merged number CI printed on f93efce: run 39302117550, "the package's unpacked size is 5950518 bytes", and "the package's tarball is 1906348 bytes". Tarball sizes don't add up exactly under compression; my additive estimate is 1,906,591.
Arithmetic:
- Tarball: 1,906,348 × 1.02 = 1,944,474.96, rounded up to 1,945,000.
- Unpacked: 5,950,518 × 1.02 = 6,069,528.36, rounded up to 6,070,000.
Both match the PR.
Rule 10: across the PR, test/package-files.test.ts changes by +1/-1, the PACKAGE_BUDGET line only. The under-budget self-check (overBudget(p, { size: p.size - 1, … })) is untouched.
For #823, not a change here: the comment above PACKAGE_BUDGET (package-files.test.ts:108) still says "each rounded up to the next 10,000".
Merge order:
merge-treeshows test: raise the package budget to main's size + 2% after #805 and #787 [PROTECTED: owner merges] #823 conflicts on the budget line only, as expected: test: raise the package budget to main's size + 2% after #805 and #787 [PROTECTED: owner merges] #823 has 1,950,000 / 6,060,000 and this PR has 1,945,000 / 6,070,000.- Wizard: the Claude step matches the login to agent_runtime and offers an API key (#738) [PROTECTED: owner merges] #819, Tests: stateDir() refuses the real state dir in a test run; a lint that tests guard machine paths with onRealMachine (#815) [PROTECTED: owner merges] #818, doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803, Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794, Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788, Dashboard policy changes go through root's helper (set-policy), never a loosening (#726 part 2) [PROTECTED: owner merges] #770 and Single machine: policy.yaml is root's, and the engine refuses one that isn't (#726 part 1) [PROTECTED: owner merges] #769 now also conflict on that one line, each with its own raise.
- Each of those numbers is main plus that PR's own growth (inferred), so they add up. Whichever lands later should re-measure the merged tree, not just take the higher value.
Usual checks
- Fail-without (measured):
- Setup: 0c1a7ff's
test/first-task-progress.test.tson 27902ca, in a scratch worktree, then build and build:web. - Command:
node --import ./dist/test/isolate.js --test dist/test/{first-task-progress,viewer-events,web-render,feature-map}.test.js. - Result: 174 tests, 172 pass, 2 fail.
forged rows of every JS typefails witherror: '5.number = {}', anda card that cannot be built drops the card onlyfails with500 !== 200. This matches the builder's report. - The scan's fail-without is probe5: E1-E3 were MISSED at 27902ca and are CAUGHT at the head.
- Setup: 0c1a7ff's
- After (measured): first-task-progress, viewer-events, web-render, feature-map, package-files, merge-policy and brand ran 205 of 205 at the head.
- Typecheck, denylist, web (measured):
npm run typecheckis clean.npm run check:denylistreports "1298 files clean".npm run build:webbuilds. - DCO and attribution (measured):
node scripts/check-dco.mjs origin/main 0c1a7ff4reports "all commits signed off". Grepping the commit messages for Co-Authored-By, "Generated with", Claude Code or Anthropic finds 0. The body and the round-4 reply have none either. - Protection (measured):
- The title,
**Risk class:** protected, the needs-owner label, and the What this can never do and Compromised component sections are all present.src/**is engine. - "Closes First run: a card follows the first task from "File one" to its PR, and the Start card stops counting steps that come after Start #741" is there, and there are no new dependencies.
- Round 4 adds no route and no write:
firstTaskCardwraps the same fold (read from the diff). The viewer 403 test passes.
- The title,
- Adversarial pass: the section is present, but it has no round-4 entry. See required change 2.
- CI on 0c1a7ff (measured): all green. Run 39306492479 tested
f26ef2d Merge 0c1a7ff4… into d4b45039…, and d4b4503 is current origin/main. It reports "ok 661 - the package stays within its size budget" (node 22) and "✔ the package stays within its size budget" (node 24). - merge-tree of 0c1a7ff (measured):
- Clean against origin/main, Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820, Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789, Test runs: each run in a user namespace of its own; slirp4netns as the test user; a run can't join Docker's namespaces (#545) [PROTECTED: owner merges] #780 and docs/design: amend #698 decision 1: turning auto_merge on is root-only; off stays on every surface [PROTECTED: owner merges] #775.
- test: raise the package budget to main's size + 2% after #805 and #787 [PROTECTED: owner merges] #823, Wizard: the Claude step matches the login to agent_runtime and offers an API key (#738) [PROTECTED: owner merges] #819, Tests: stateDir() refuses the real state dir in a test run; a lint that tests guard machine paths with onRealMachine (#815) [PROTECTED: owner merges] #818, doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803, Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794, Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788, Dashboard policy changes go through root's helper (set-policy), never a loosening (#726 part 2) [PROTECTED: owner merges] #770 and Single machine: policy.yaml is root's, and the engine refuses one that isn't (#726 part 1) [PROTECTED: owner merges] #769 conflict only on the
PACKAGE_BUDGETline. I checked each hunk. - Remove the desktop app, its source, Tauri lockfile and docs; keep the desktop check one more release as a no-op (#744) [PROTECTED: owner merges] #746 conflicts in CHANGELOG.md, scripts/setup/lib.sh, test/engine-update.test.ts and test/update-checks.test.ts. This PR touches none of them.
- Rule A: the new route test uses
exampleProject()(a tmpdir),mkdtempSyncslots and a stand-in setup probe (read from the code). It ran under the isolation preload.
Required changes
- Make recovery's exception exact (principles 4 and 9).
- Exempt a
conflict_fix.finishedcall only when the argument after the event type is itself the object literal and is the call's last argument. Then read that literal's top-level fields, never the first{anywhere in the call. - Add N11a (
Object.assign({ reasons: [] }, finished)) and N11b (withReasons({ reasons: [] })) as fixtures that must be reported. recovery.ts:46stays exempt.- Correct the body's "exactly
reasons: []among the call's top-level fields" if the wording changes.
- Exempt a
- Correct the body (principle 5) and add round 4 to the Adversarial pass.
-
Stale and now false:
- "Attacks I tried": "Package: fits, 494 bytes under in tarball against main 128453a. No budget change."
- "Package, round 4": "per the owner's pending decision on the budget I stopped. The budget is unchanged."
- "For the owner": "Package budget (a decision for you) … Either the budget is raised, or the card waits."
Mark each as superseded by 0c1a7ff, under the standing practice.
-
Add a "Round 4" entry under Adversarial pass:
- the 17-field × 12-value forge;
- the injected fold fault;
- the call-before-definition fixture;
- N7;
- and what each found.
-
Browser check still owed (human)
Use the verify-dashboard skill against a stand-in dashboard: scratch state, no real events DB, local mode. Take before and after screenshots of First run, with zero new console errors.
- Start card:
- With every step before Start done and Your first task not, there is no orange "required step isn't done" line.
- With Claude login not done, it reads "1 required step isn't done yet".
- First task card, just filed:
- The header reads "First task #n filed … ago", with no title, and the card sits under Start.
- All six stages are "waiting".
- Claimed reads "Estimate: within about 65 s …".
- Worker, Evaluator and PR opened read "Guess: 15-60 minutes …".
- Checks reads "Estimate: no estimate …".
- Held:
- Merged is "held" with an orange dot.
- Each hold shows its sentence and "What lets it through: …".
- In local mode the kill switch reads "Local mode: every PR waits for you."
- An unknown reason reads "Another reason (see the Board)."
- No
policy.yamland no raw reason appears.
- A new head after a held decision: Merged is "waiting" with "a new head: it waits for its checks and a fresh decision", and there are no holds.
- Blocked: "Blocked: it waits for you on the Board, which says why.", with no run text.
- PR link: "PR #n" links to
https://github.com/<repo>/pull/<n>and opens in a new tab. - Round 3:
- With a filing whose time doesn't parse, the header shows no "filed …".
- A failed run with an unknown reason reads "its run ended: unknown".
- Round 4:
- A
pr.openedrow with number-41, inserted raw in the stand-in DB, shows PR opened as "waiting" ("no PR yet"), never "PR #null". - A card that can't be built leaves the card out, while Start, the steps and the policy form still render, with no error banner.
- A
- Layout: the hold sentences wrap at phone width (375 px) without horizontal scroll, in light and dark themes.
- Viewer: a viewer login gets the first-run refusal and no card.
… the call's last argument Review round 4 on #816 (issuecomment-6275370342): the exception for a conflict_fix.finished with no reasons read the first "{" anywhere in the call, so a wrapped payload (Object.assign({ reasons: [] }, finished), withReasons({ reasons: [] })) or one followed by another argument was exempt. It is now exempt only when the argument right after the event type is itself the object literal and the call's last argument, with exactly `reasons: []` among its top-level fields and no spread. recovery.ts's call stays exempt. Test (fails without): test/first-task-progress.test.ts, the N11a-c fixtures in "the scan fails closed" (exempt under the round-3 rule, as the review measured). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…he test's comment says The budget practice as corrected (2026-10-19): the PR merged with the current main, plus 2%, rounded up to the next 10,000 (the comment above PACKAGE_BUDGET, as #823 does). Main is still d4b4503; merged with it this PR packs to 1,906,348 / 5,950,518 (CI run 39302117550, measured again for 0c1a7ff by the round-4 review): x 1.02 = 1,944,475 -> 1,950,000; 6,069,529 -> 6,070,000 (unchanged). Test (fails without): test/package-files.test.ts, "the package stays within its size budget" (it passes with either value; this follows the stated rounding). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 5: what changed (head e0f3f2c)
|
…y (fail closed) Round-5 probes on #816 (e0f3f2c): a payload literal { reasons: [], <second key> } was still exempt when its second key sets reasons again at runtime: ['reasons']: why, [k]: why, 'reasons': why, "reasons": why, get reasons() { return why }, and either of those behind a comment. The exemption now requires every top-level field to start with a plain identifier key (or be shorthand), and never __proto__; a computed or quoted key, a getter, a method or a comment before a key loses it. recovery.ts's call stays exempt. Test (fails without): test/first-task-progress.test.ts, the round-5 fixtures in "the scan fails closed" (exempt under e0f3f2c's rule, as the round-5 probes measured). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
|
Round 6 (head 53e2dc5): recovery's exemption now requires every top-level field of the payload literal to start with a plain identifier key, or be shorthand, and never |
… package budget The owner's practice as changed (2026-10-19, late): a PR raises PACKAGE_BUDGET only when it would exceed it. #823 set main's budget to 1,950,000 / 6,060,000. Merged with d5674d0, this PR packs to 1,906,348 / 5,950,518 (npm pack --dry-run --json, the dashboard UI built), which fits, so the raise is dropped and test/package-files.test.ts is main's exactly. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
|
Merged origin/main d5674d0 (#823) as 640b6cd. Measured with npm pack --dry-run --json inside the worktree, UI built: the merged tree packs to 1,906,348 tarball / 5,950,518 unpacked. That fits main's budget of 1,950,000 / 6,060,000, so this PR's raise is dropped and test/package-files.test.ts is main's exactly (measured: the diff against origin/main is empty). This PR's own delta is +4,190 tarball / +16,107 unpacked; the body's "Package size" section breaks it down file by file. After the merge (measured): first-task-progress, viewer-events, web-render, feature-map and package-files ran 181 of 181, and typecheck is clean. |
Evaluator verdict: CHANGES NEEDED (round 6, PR body only)Round 6, a fresh final check on head 640b6cd (53e2dc5 plus a merge of main d5674d0). I'm an independent reviewer, not the builder. I did not merge, commit, push, rebase, cherry-pick or reset anything. The code passes every check below. The only required changes are to the PR body. They come from the owner rule of 2026-10-19: every new protection must be by design, a lint that reads source is only a backstop, and the body must name the by-design protection. By-design protection: what it is, and what rests on the scan alone
Earlier findings: each one is fixed
Probe (measured;
Fail-without for round 6 (measured)The scanner lives in the test file, so checking out 53e2dc5's test onto e0f3f2c brings the fix with it: 18/18 pass, which shows nothing. So I ran it the other way:
Result: 8 of the 9 round-5 fixtures are EXEMPT: At 640b6cd, all of them are reported. The fixtures prove the fix. Named gaps (not blockers)None of these lets a writer bypass the card, and none can take the page down (inferred from
Optional follow-up, if wanted: widen the emitter regex to Package size (measured)
Tests, typecheck and denylist on 640b6cd (measured)Every run used a scratch HOME, TMPDIR and state dirs, and a stand-in
CI, commits and body
Merge order with #820 (measured)
Owed
Required changes (PR body only; no code change is required)
|
…-783-weakening-hold Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com> # Conflicts: # CHANGELOG.md
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com> #816 landed first: its scan of merge.decided's reason sources needs the entry for reasons.push(...(i.weakening ?? [])) in mergeDecision. Test data only.
Intent: After "File one", the first-run page follows that issue to its PR. A First task card shows claimed, the worker, the evaluator's verdict, the PR, its checks, then merged or held. A held PR says why in fixed plain words and what lets it through. Every stage comes from events the engine already records, and the card shows no free text. Every estimate names its basis. The Start card also stops warning about Your first task, a step that comes after Start.
Risk class: protected. It touches
src/dashboard.ts(one field added to the existingGET /api/first-run, no new route) and addssrc/first-task-progress.tsandsrc/hold-words.ts. Anysrc/**change is engine. Commit 1 (the Start count) isweb/only and could ship alone.Closes #741
What changed
11c9f66,
web/only:startBlockersinweb/src/pages/FirstRun.tsx. Start's warning counts only the required steps not done that come before Start, in screen order. With no Start step in the mode, every required step not done counts, as before.f739109, c7bae71, 9d22da1, 0f5ccf2, the card:
src/first-task-progress.ts: a pure fold over the event log.first_run.first_task_filedaudit entry and reads only that issue's events recorded after it.merge.decidedormerge.failedcounts only at that PR's latest known head, taken from its latestpr.opened,pr.status,pr.readyorpr.unready. Otherwise the stage waits: "a new head: it waits for its checks and a fresh decision".src/service.ts.src/hold-words.ts: fixed words (why it waits, and what lets it through) for every kindmerge.decidedcarries today:mergeDecision's kinds;conflict fix:.An unknown prefix reads "Another reason (see the Board)", never its text.
designwords also name a possible weakened check, worded as examples ("for example ... or it may weaken a check"). That holds whether or not Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820 lands first.policy.yamlis named.src/merge-policy.tsis not touched.src/dashboard.ts:GET /api/first-runaddsfirstTask. It is memoized per event-log snapshot, and null in setup mode. The route is writers only, as before.web/src/components/FirstTaskProgress.tsx: the read-only card under Start. The block line is fixed words pointing to the Board. No issue title is shown.docs/feature-map.md: a row for the card, and one sentence on Start's row.Change 3 (a new hold prefix must fail a test): I built the source-scan test, not the shared kind list.
A shared list would touch
merge-policy.ts,merge-flow.ts,coordinator.ts,pr-rules.tsandconflicts.ts. Several open PRs hold those files (Review levels: a .gitattributes mark can't hide a change's lines or content; adding one waits for the owner (#693) [PROTECTED: owner merges] #802, Approvals: one role rule on every surface, decide only as root or the coordinator user, money paths read from main (#715) [PROTECTED: owner merges] #805, Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788, Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820).What the scan checks:
test/first-task-progress.test.tsscans every source ofmerge.decidedreasons:src/merge-policy.ts, the whole file;mergeOrWaitinsrc/coordinator/merge-flow.ts;combinedGateinsrc/coordinator.ts;rulesetHoldinsrc/pr-rules.ts;conflictWaitReasonsinsrc/conflicts.ts.Each literal reason, whether pushed, mapped, or a
why/holdvalue, must start with a prefix that has words. Each reason pushed from a variable must be one the test traces.Proof on fixture text, no repo file edited: the scan reports
reasons.push('stale: …'), a templatestale: ${n}, an untracedd.reasons.push(newWhy)and a newconst why = \held: …``.Round 3, 27902ca:
EventSchemas['run.finished'].shape.reason.options,EventSchemas['eval.verdict'].shape.confidence.options), else "unknown";designnames "a dependency manifest changes";doubtsays "for example" and adds no verdict, a doubted test and no design answer;merge-failedsays "at least 3 times".merge.decidedandconflict_fix.finishedinsrc/must sit in a scanned region. The one exception is aconflict_fix.finishedwhose call hasreasons: []itself (recovery's "interrupted"); the scan found that emitter.mergeDecisionFor, fix-runsconflictRunandgiveUpConflict, and merge-policy's reason functions one by one.why/holdvalue or reasons initializer fails unless traced. So does anyunshift,spliceorconcat.MergeInputfield whose builder isn't traced. These cost only the plain words: the card then reads "Another reason (see the Board)", and nothing leaks.Round 4, f93efce:
firstTaskCard. If the fold throws, it returns null (the card is left out) and logs once; Start, the steps and the rest still answer.String()is applied to an event value;pr.openedwhose number is not a positive safe integer is not an opened PR. It is never a done stage reading "PR #null".mergeDecisionFor's definition.region()now requires its signature exactly once.mergeDecisionFor's signature is now unique (mergeDecisionFor(n: number).reasons: []among its top-level fields, with no spread. The review's N7 and N11a-c are fixtures.53e2dc5 (round 6): every top-level field must also start with a plain identifier key, or be shorthand, and never
__proto__. These all lose the exemption and are fixtures:['reasons'],[k]);'reasons',"reasons");reasons;__proto__.recovery.ts:46stays exempt (measured:first-task-progress18 of 18).Correction, false through e0f3f2c's parent 0c1a7ff: the body said "exactly
reasons: []among the call's top-level fields". It read the first{anywhere in the call, so a wrapped payload was exempt.push.apply, a shorthand{ hold }or aholdvariable,??=;emit?.(.emit (,emit\n(,emit<'merge.decided'>(oremit(/* c */ '…';push?.(,push (orArray.prototype.push.call;Package size
No budget raise: this PR now fits main's budget.
PACKAGE_BUDGETonly if it would exceed it. test: raise the package budget to main's size + 2% after #805 and #787 [PROTECTED: owner merges] #823 has merged, setting main's budget to 1,950,000 / 6,060,000.npm pack --dry-run --jsonrun inside the worktree measures 1,906,348 tarball, 5,950,518 unpacked (measured). That is under the budget by 43,652 / 109,482.test/package-files.test.tsis now main's exactly (measured:git diff origin/main -- test/package-files.test.tsis empty).package-filesran 7 of 7, and the five affected test files ran 181 of 181.This PR's own delta: +4,190 tarball, +16,107 unpacked, over main's 1,902,158 / 5,934,411.
test/package-files.test.ts, which isn't packed (inferred), and test: raise the package budget to main's size + 2% after #805 and #787 [PROTECTED: owner merges] #823 states the same size.What grew, file by file against main (measured, a per-file diff of the two
npm pack --dry-run --jsonlistings):dist/src/first-task-progress.js(new)dist/src/hold-words.js(new)dist/web/assets/FirstRun-*.jsdist/src/dashboard.jsdist/web/assets/index-*.css(the card's classes)Every other web asset only changed its hash name, at the same size. No sourcemap,
.d.ts, test, fixture or stray file is packed, and the file count goes from 512 to 514: the two new modules.Why it grew: the card's fold, the hold-words table, the card's component and the route's one field. All of it is code this PR adds on purpose.
How it was verified
Fail-without, round 4 (measured): I ran f93efce's tests on 27902ca. Result: 174 tests, 172 pass, 2 fail:
forged rows of every JS type: "5.number = {}", so the PR stage was done with no PR;a card that cannot be built drops the card only: "500 !== 200".Round 4 after (measured): first-task-progress, viewer-events, web-render and feature-map ran 174 of 174. The same tests on the tree merged with origin/main d4b4503 also ran 174 of 174, so the scan holds on Approvals: one role rule on every surface, decide only as root or the coordinator user, money paths read from main (#715) [PROTECTED: owner merges] #805's and Project commands run as the agent user only in a sandbox that hides its credentials; their output is redacted (#713) [PROTECTED: owner merges] #787's merge-flow.
Package, round 4 (measured,
npm pack --dry-run; budget 1,900,000 tarball / 5,930,000 unpacked):Fail-without, round 3 (measured): I ran the final tests at 27902ca on 0f5ccf2, with the test files checked out from 27902ca. Result: 167 tests, 166 pass, 1 fail. The failure is
forged rows: "no attempt that is not a positive safe integer", actual 'attempt 1e+21 started; no finish recorded yet'.the scan fails closed).merge-policy.tsandmerge-flow.ts).Round 3 after (measured): at 27902ca, first-task-progress, viewer-events, web-render and feature-map ran 172 of 172. The tree merged with origin/main 128453a (
merge-treec9d81501, checked out in a scratch worktree) ran those four plus package-files: 179 of 179.Package, round 3 (measured, merged with origin/main 128453a): tarball 1,899,687 (313 under the 1,900,000 budget), unpacked 5,927,388 (2,612 under 5,930,000). The budget is unchanged.
Fail-without, round 2 (measured): I ran the final tests on c7bae71 in a scratch worktree. Only the three test files came from 0f5ccf2. The command was the named-file
node --import ./dist/test/isolate.js --test, with a scratch HOME, TMPDIR and state dirs and a stand-inclaude. Result: 165 tests, 157 pass, 8 fail:src/coordinator/merge-flow.tshas unworded prefixes;Fail-without, round 1 (measured, on 178c44e): 162 tests, 157 pass, 5 fail. These were the Start-card test, the "still warns" test, the card render, the
/api/first-runroute and the viewer test, as reported before.After (measured):
first-task-progress: 14 of 14 pass.first-task-progress,viewer-events,web-renderandfeature-map: 170 of 170 pass.git merge-treeof 9d22da1, checked out in a scratch worktree):first-task-progress,viewer-events,web-render,feature-mapandpackage-filesran 177 of 177 pass. So the scan passes against main's current sources.Package (measured,
npm pack --dry-run --json, merged with origin/main 128453a):Checks (measured): typecheck clean;
check:denylist"1312 files clean" (the round-6 reviewer's measurement on 640b6cd; it was 1298 on earlier heads); DCO "all commits signed off"; push gate clean.CI: see the latest run on this head.
Browser check owed (still not done): run the verify-dashboard skill against a stand-in dashboard: scratch state, no real events DB, local mode. Cover the cases the reviews list:
pr.openedwith number -41;Neither I nor any reviewer has run a browser.
Acceptance, each with a fail-without test
first-tasknot: no Start warning#741 the Start card counts only ...#741 the Start card still warns ...older headtest; web-render cardmerge.decidedcarries)held,no evaluator text, source scan#741 a held first taskBy-design protection
Under the owner's 2026-10-19 rule, these are the protections. Each is a runtime check or a permission:
firstTaskCard, whose owntryreturns null (the card left out) and logs once if the fold throws. Start, the steps and the policy form still answer.a card that cannot be built drops the card only, with a fault injected into the fold.holdWordsmaps reasons only through its fixed table, and any reason it doesn't know reads the fixed "Another reason (see the Board)".forged rows,forged rows of every JS type,no evaluator text./api/first-runis writers only, so a viewer gets 403 (measured: viewer-events#741).Backstops only, not protections:
test/first-task-progress.test.ts) only checks that each hold kind gets its own plain words. If it misses one, that kind reads "Another reason". Nothing leaks, and the page stays up.merge.decidedcarries gets its own plain words" is lint-only today. Its by-design fix, a typed hold kind from one shared list, is Hold reasons: a typed hold kind from one shared list, so a new merge.decided reason can't be emitted without its plain words (by design; #816 follow-up) #824. It touchesmerge-policy.ts,merge-flow.ts,coordinator.ts,pr-rules.tsandconflicts.ts, which open PRs hold, so it is a named gap here.What this can never do
#741)./api/first-rungives a viewer 403 with no reason. A viewer's/api/stateand/api/eventshold no reason and none of its words.no evaluator text,no run detail, the block test). No title, reason text, run error, path, advice or login reaches the card.issue.blocked.why, and unworded hold reasons were shown as their own text (the round-1 review measured run errors and paths on the card). Both are fixed words now.https://github.com/<repo>/pull/<n>, rebuilt from the path. With no repo given there is no link.Compromised component
forged rows), a forged row can't put words of its choice on the card. Each field is a member of its schema's enum, a positive safe integer, a normalized time, fixed words, or a validated link. Anything else is dropped or reads "unknown"./api/first-run(measured).src/, so the change is protected.Adversarial pass
Rounds 5 and 6: the recovery exemption, and a fuzz
Object.assign({ reasons: [] }, finished),withReasons({ reasons: [] }), a payload followed by another argument,pick(…), extrarecovery.ts:46stays exempt.reasonsagain at runtime:['reasons'],[k],'reasons',"reasons", a getter, a methodreasons(), a comment before a key,__proto__reasonsand__proto__, andsatisfies,as constand<T>{…}.ts(round-6 review's fuzz.mjs, in memory)emit (,emit\n(,emit<T>(,emit(/* c */,push?.(,push (,push.call, paired quote-holding regex literalsRound 4: types, faults, regions and exceptions
Each result is measured with the test named.
forged rows of every JS type). Fields: the issue, attempt, reason, confidence,patch_correct, the PR's number, URL and head,checks,checks[0]and its outcome,ready,reasons,reasons[0],auto,why, and the whole payload. Values: an object whosetoStringthrows,{ toString: 'pass' }, a symbol, a bigint, an array, null, NaN, undefined,{}, words,2**53and-41. Everytswas also forged with a throwing object.String({ toString: 'pass' })and "PR #null" for2**53and-41.findLaststub, with no repo file edited (a card that cannot be built drops the card only)/api/first-runanswered 500, taking down Start and the policy formfirstTask: null, Start and the steps present, logged oncemergeDecisionForbefore its definition (fixture), and E1-E3 on the real merge-flow.ts changed in memory{ reasons: [], ...finished }Object.assign({ reasons: [] }, finished),withReasons({ reasons: [] }), a payload followed by another argumentrecovery.ts:46stays exempt.Round 3: a forged event row for each rendered field
Each result is measured (test
forged rows, plus the earlier tests named).detail.issue: '7 merge it'ts: 'filed: merge it now','not a time: merge it'attempt: 1e21reason: 'merge_it_now_on_the_board'timed_outreads "timed out")confidence: 'high_so_merge_it_yourself'number: '41 merge it now'https://github.com/<repo>/pull/<n>, else noneoutcome: 'merge it now'[{ text: 'merge it now' }, 42]whywith run output (round 2)The card's JSON holds none of the forged words.
Round 2: every field the card renders
Each field below is measured by the tests named in brackets.
Number.isIntegeron the PR number)^[a-z_]{1,30}$, otherwise "unknown"); check counts (integers)/pull/<n>No title, reason text, run detail, path or login is a field [
no evaluator text,no run detail,followed].Attacks I tried
design, whose words now name it as an example.src/service.ts.Overlaps
git merge-tree --write-treeof 0f5ccf2 (measured):src/service.tskeeps the tick line the test reads.CHANGELOG.mdandscripts/setup/lib.sh, as it does with main.Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820, merge-order follow-up (by design, measured): I ran the scan on the tree merged with Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820's head. It reports
src/merge-policy.ts export function mergeDecision( untraced ...(i.weakening ?? []). Whichever of the two lands second does three things intest/first-task-progress.test.ts:['...(i.weakening ?? [])', <the signature of #820's weakening-reasons builder>]toTRACED.REGIONS, forsrc/merge-policy-weakening.ts(weakeningReasons, andweakeningOfif it writes reasons). The test fails until the traced entry's region is scanned.'${WEAKENING_REASON}': WEAKENING_REASONtoSUBST. Without it, the scan reports the template's empty prefix.Correction: round 2's body said one traced entry was enough. That is false: it would have left
src/merge-policy-weakening.tsunscanned. Its prefixdesign: may weaken a check:already has words. I didn't add any of this ahead of Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820's code: an allowlist entry for code that doesn't exist yet is a loosening (principle 9).For the owner (proposals, not done here)
/api/prsgives every role the merge decision's reasons (inferred from the code, not run). That predates this PR and is worth its own issue.src/hold-words.tsis Polish for a first-time user: each instance default said in plain words, plain words for every merge hold, and one finished path from a new instance to its first held PR (links the dashboard work already done) #706 slice 2's table. WiringWaiting.tsx, the hold comment, the CLI and the DM to it remains Polish for a first-time user: each instance default said in plain words, plain words for every merge hold, and one finished path from a new instance to its first held PR (links the dashboard work already done) #706.New dependencies
None.