Compare commits
108
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97535587b2 | ||
|
|
59142e9fbb | ||
|
|
41983e2659 | ||
|
|
36c2d98b81 | ||
|
|
c746ce378c | ||
|
|
dfebd16cc1 | ||
|
|
47a35bf436 | ||
|
|
6b32424adb | ||
|
|
bd5f11dedd | ||
|
|
e40b74bebc | ||
|
|
ce74477e14 | ||
|
|
4d9d62ae89 | ||
|
|
5e5ba94b19 | ||
|
|
97bbe7fd30 | ||
|
|
91f7029923 | ||
|
|
be11a1f7e6 | ||
|
|
d87483d683 | ||
|
|
be2b008cc0 | ||
|
|
ab7377bcf1 | ||
|
|
fd6dd327c3 | ||
|
|
5fd178e44d | ||
|
|
345f3ace67 | ||
|
|
54ec93d06f | ||
|
|
4f68485272 | ||
|
|
c48239ec3d | ||
|
|
5d2c274420 | ||
|
|
9e5ad7c599 | ||
|
|
939356a1c2 | ||
|
|
3521c23c3e | ||
|
|
2311a50e1c | ||
|
|
29daebf548 | ||
|
|
2f08fd8f23 | ||
|
|
b8c3d00362 | ||
|
|
14124d96d4 | ||
|
|
e7fb59f225 | ||
|
|
e122aad8e2 | ||
|
|
4c4b8559cf | ||
|
|
b0fd28b6f3 | ||
|
|
b765f5cb81 | ||
|
|
da5d3651d7 | ||
|
|
82c8281166 | ||
|
|
ceadad993c | ||
|
|
f5611c937e | ||
|
|
343b6dd766 | ||
|
|
f40f2e770e | ||
|
|
23c3d62183 | ||
|
|
70e289ba73 | ||
|
|
ab50de9fe3 | ||
|
|
b3665f5ab1 | ||
|
|
c27207f6e6 | ||
|
|
ce6a96ad61 | ||
|
|
59f708ca21 | ||
|
|
f751505ada | ||
|
|
0fa7bf421f | ||
|
|
93d211c295 | ||
|
|
08c896e3c5 | ||
|
|
03ed2bccce | ||
|
|
ad6020754d | ||
|
|
5c8f983dde | ||
|
|
f1267e49af | ||
|
|
d2be97c5fc | ||
|
|
c13c68c813 | ||
|
|
4bdc7e0e86 | ||
|
|
86f95bb27f | ||
|
|
5365adaef6 | ||
|
|
0bbf0f6d18 | ||
|
|
95f4bd8fdc | ||
|
|
d8034c51bc | ||
|
|
b28baf7ee8 | ||
|
|
3673463182 | ||
|
|
7991662dc0 | ||
|
|
378a76b854 | ||
|
|
ddfb29af7c | ||
|
|
9c97329833 | ||
|
|
a849d89b73 | ||
|
|
0042f713dc | ||
|
|
f0d0b31e9b | ||
|
|
92be0db025 | ||
|
|
02da6a2ae4 | ||
|
|
2dd9c80c1c | ||
|
|
137ddd0079 | ||
|
|
e3c0446f55 | ||
|
|
dcd8d4ddaa | ||
|
|
eb9672c910 | ||
|
|
3315977095 | ||
|
|
41b0cf7551 | ||
|
|
5f5b51dd65 | ||
|
|
da9555ca2f | ||
|
|
0dbb242905 | ||
|
|
ea6989d4ed | ||
|
|
db1e7732aa | ||
|
|
5d2437f899 | ||
|
|
28e3d82265 | ||
|
|
524e169b8c | ||
|
|
5f73b388c4 | ||
|
|
52f36e921e | ||
|
|
6ba8a582e8 | ||
|
|
aa772d731d | ||
|
|
3dcd3651ed | ||
|
|
eb017d77f1 | ||
|
|
f842462bdd | ||
|
|
ad9cefcf2d | ||
|
|
5c91c96df5 | ||
|
|
1509c7cc57 | ||
|
|
74dea06d92 | ||
|
|
3576afffb3 | ||
|
|
73222011f2 | ||
|
|
ea0b2a16d6 |
@@ -5213,3 +5213,82 @@ Before: managed project/thread verbs had no Console conversation area. After: fi
|
||||
2026-10-21T21:43:02Z | Della mos-1223 / Vikunja 460 phase C synthesis | Before: independent plans/reviews complete, Mos selected Della base. After: docs/plans/2026-10-21_email-plan.md merges Ludwig grafts and accepted corrections, with 16 post-v1 rows (eight READY, eight WAIT/optional), eleven deduplicated Jason questions, per-row test paths and explicit Q210 non-v1 scheduling. Original Della plan/review preserved; Ludwig originals remain on their branch. Static checks cover graft/row/question/citation anchors and original blobs. No source code, suite, mailbox, secret file, service, MVP or queue change. Candidate goes to Ludwig before Sage landing/queue handoff, copying Mos.
|
||||
|
||||
2026-10-21T21:45:20Z | Della mos-1223 / ludwig-0004 focused corrections | Before: b096a16cc left content-key location implicit and E05 core registration marked ready. After: merged plan 399 lines fixes external configDir secret key/lifecycle namespace outside dataRoot backup/restore, E01 schema and E03 missing/expired-key restore tests, and E05/E06 test-local registry readiness with core kinds.mjs registration gated on approved E01/Q4. Sixteen rows and eleven questions retained; originals unchanged. Focused static checks and queue guard apply, no product/live changes. Reviewer will check changed lines only.
|
||||
|
||||
2026-10-21T22:24:12Z | Huey | T1 item 7 sessions design r1, mos-1232, Vikunja 463 | Before: origin/refactor cca206cdc keys assistant claims/stores/messages by thread and renders Pi compaction markers; row 194 r4 excludes thread context clear. After: docs/plans/2026-10-21_t1-sessions.md proposes independent per-agent sessions within threads on Pi/Claude, scoped claims/stores/messages/workspaces, completed compaction counts, owner-write/RBAC matrix, idempotent proven-stop/fresh-successor clear, common admission/kill checks, migration and seven acceptance groups, replacement REQ-ROLE-2/REQ-THREAD-2 text and three owner questions. PRD 1.12 USER-3/4 candidate 1509c7cc5 cited separately from base. Verification: 22 pinned references resolve, eight-line summary and three exact owner quotes; manual source/consistency review, whitespace and installed queue guard checked. No code, queue/PRD edits, builds, suites, credential reads, live changes or paid calls. MVP priority ahead of Q198 S1. Review via Scrooge, then Sage/Ludwig as Mos authorized.
|
||||
|
||||
2026-10-21T22:28:56Z | Huey | T1 sessions design r2, scrooge-0006, mos-1235/1236, Vikunja 463 | Before: r1 347278044 lacked the exact per-session checkout/single-writer invariant and safe runner drain ordering, and admitted other role instances. After: F1 exact workspace path, fenced checkout setup, legacy shared-workspace quiescence, clear retains old dirty/untracked/unpushed checkout and creates fresh approved checkout, T2 mutant; F2 dequeue fence and generation-bound turn-boundary idle proof before runner signal, busy drain stays clearing, T4 cases; F3 THREAD_DEFINITION only per Mos scope ruling, no role eligibility change; N1 private titles/history vs owner-visible count/activity projection; N2 Claude offline adapter/managed-store-reader dependency flagged as MVP risk. Section 8 records Mos’s Q1–Q3 readings, no questions to Jason. Verification: 27 pinned references resolve, summary within 15 lines, source/delta consistency review, whitespace and installed queue guard checked. No code, queue/PRD edits, builds/suites/live changes/provider calls/credential reads.
|
||||
|
||||
2026-10-21T22:30:20Z | Huey | T1 sessions design r3, scrooge-0007 R1, Vikunja 463 | Before: r2 e19e30df2 nested new session checkouts under the retained legacy instance workspace, allowing project-name collision or historical parent inspection to reach live siblings. After: exact path is dataRoot/session-workspaces/business/instance/session, outside the legacy root; canonicalized ancestor/descendant overlap refuses; T2 nesting mutant covers historical mount/inspection and a legacy project named sessions. All other r2 content retained as approved, revision label updated. Verification: old nested path absent, manual R1-only delta, whitespace and installed queue guard checked; no code/builds/suites/live changes/provider calls/credential reads.
|
||||
|
||||
2026-10-20 18:51:10 UTC | Row 191 #1689 design phase | Before: per-business launch revoke and per-binding Discord STOP leave task containers, active sessions and background work without one switch. Proposed per-data-root durable halt with human-only resume, generation-fenced starts, bounded concurrent termination, write-once stop receipts and existing event mirror; no schema migration. DESIGN.md inventories starter/continuation paths and container control-mount restriction. Sent to Dewey with Sage copied; implementation waits independent design approval. No tests, commits or live actions.
|
||||
|
||||
2026-10-20 18:57:51 UTC | Row 191 design revision | Dewey r1 changes: interim Console --send absence, reserved processing events and missing-state semantics required correction. DESIGN-r2.md now names CLI interim path, unavailable UI controls, trusted initialization and fail-closed missing leaf, container delete/replace mount tests, reservations and id-bound reconciliation. Small notes addressed including diagnostic seats and aliases. Recommended smaller Docker scope to Sage; design and scope approval pending, no code or test runs.
|
||||
|
||||
2026-10-20T19:50:56Z | Rocko | row 191 #1689 implementation stage: from independent brakes to shared root state, generation-fenced launcher and tracked stops, Discord/task guards and control HTTP/UI. Evidence is agents/rocko/work/queue-191/logs/ and PROGRESS.md. Fixture single files only so far; heavy verification and candidate not complete. Scope additions sage-1367 (one read-only CODE file) and sage-1392 (processing-only client socket timeout); no schema, proof baseline, live file/unit, commit or push change.
|
||||
|
||||
2026-10-20 22:58:45 UTC | Rocko | row 191 #1689 rebase inspection candidate | Rebuilt uncommitted kill-switch tree from da230ca48 onto landed row 190 base 00d4cc1f6 under new branch rocko/row191-r2-00d4. Hand merges retained instance flags and SSO/sign-out changes; fixture tests exposed and fixed SSO signer compatibility and two unknown-runtime cleanup defects. Final touched singles 249/249 across 26 files; 15 overlapping Client reader files 102/102. Manifest a4f780c2 has 78 paths; packet 185 files verified with scan clear. Mutants and owner gate remain pending; no review-ready or activation claim, no commit/push/live action.
|
||||
|
||||
2026-10-21 23:39:21 UTC | Rocko | row 191 #1689 inspection snapshot | Sage-2242 authorized a local inspection commit for Honker's row 194 S6. Created d52b4db1c on new rocko/row191-inspection-20261021, parent 00d4cc1f6, preserving the reviewed-design build and candidate a4f780c2 (78 paths). Static manifest/index/source scan and diff check verified; no tests run, no push credential or push attempt. Existing single-file evidence is historical, 249/249 and overlapping Client readers 102/102; suites/mutants/gate remain pending. Current code has verified runtime signalling and instance halt, but no dedicated per-run stop API yet; Honker and Sage notified of that seam directly. Snapshot is not review-ready or approved for live use.
|
||||
|
||||
2026-10-22 00:04:05 UTC | Rocko | row 191 #1689 per-run stop inspection | Implemented agreed Honker-0079/0081 stopRun contract with verified per-signal identity, fixed 5s+2s, durable first-owner join and immutable result plus later-exit observations. Focused CLEAR sage-2250: final 9 files 62/62, no skipped, after correcting host guard placement and peer-discovery fixture timeout isolation; initial failures and failed teardown logs retained, only own failed group stopped. Slot returned with cwd sweep clear. Sage published f7bf7dfec; sage-2265 clean rebase creates 7aa18b0f6 on dcd8d4dda under new branch, explicit Rocko identity, all 79 manifest product blobs identical so 62/62 evidence carried. Direct S6 handoff sent to Honker and Sage. Wider suites, mutants, review and activation remain pending.
|
||||
|
||||
2026-10-22T00:18:12Z | Sage (Opus 5.5) | row 254 landing (#1759), library.sync human timeout | Before: the human transport gave library.sync the 5-second default, so a sync longer than that reported outcome-unknown, and six library-* codes reached the Console as 502. After: 331597709 lands Megavolt's candidate (7 paths, tree d49e4626f, manifest digest be63cfbe recomputed and matched). human-cli.mjs waits 130 s for library.sync, past the server's 120 s hold, and 5 s for other verbs. webui bus.mjs maps library-sync-busy, library-diverged and library-accept-stale to 409, library-accept-invalid to 400, and library-credential-missing and library-fetch-failed to 503. channel-proof re-pinned. Merged-tree gate on the landing commit: library 77/0, bus 628/0, webui 305/0 (1 skip), cli 749/0 (23:44:33-23:55:11Z). eb9672c91 adds the README request-timeout sentence (Dewey N1) and a DEFERRED entry for the four library codes still at 502 (Dewey N2, binds the Console library routes row). Overlap sweep before push: 268 rebased to r2 ecd9b35eb (README only, approval carried webby-0096), 194 rebased to b7. Done rev 3072, #1759 closed.
|
||||
|
||||
2026-10-22 00:24:34 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 full gate on c78f48946 | stopped on second integration failure per sage-2275; singles 269/269, processing-control 61/61, bus 646/646; CLI interrupted with 26 observed failures; no further edits; cwd sweep empty and slot returned to Sage.
|
||||
|
||||
2026-10-22 00:48:20 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 eb3abba12 full gate | stopped at Discord 190/192; 32 singles 304/304, processing-control 61/61, bus 646/646, cli 761/761; two uninitialized Discord fixtures reported to Sage; no later run; cwd sweep empty, slot clear.
|
||||
|
||||
2026-10-22 00:58:27 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2294 fixture fix | e6800380d on eb3abba12, two test files/four additions, assertions preserved; 338-entry static all-package sweep recorded; no tests; sent Sage for publication and Honker for dependent gate.
|
||||
|
||||
2026-10-22 01:30:04 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 e6800380d resume | binding15/15, recover5/5, Discord192/192; stopped WebUI314 passed/4 failed/1 esbuild skip; processing polling violates sign-out/manual redirect and bar changes layout; no fixes/retries, cwd sweep empty, slot returned.
|
||||
|
||||
2026-10-22 01:38:08 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2317 Console integration fix | 8e1f55c1d on e6800380d; shared sign-out/manual redirect/interval and desktop frame fixes, assertions retained, six browser cases, 73 mutant sites preflight; syntax only, no tests; sent Sage/Honker.
|
||||
|
||||
2026-10-22 02:06:38 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 rebase sage-2329 | eafcbd3e7 on ddfb29af7, new branch, dispatcher retains processing and access; CLI/readme auto-merges checked; 87-path fd0f2740 manifest and 73-site preflight; Sage2333 published, awaiting CLEAR; no tests.
|
||||
|
||||
2026-10-22 02:20:51 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 eafcbd3e7 full gate | stopped singles313/6: six new browser cases count app freshness timer as processing timer; diagnosis sent Sage, no fix/retry; no suites/shells/mutants; cwd sweep empty, slot clear.
|
||||
|
||||
2026-10-22 02:23:13 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2345 timer fixture | 4db8b5778 on eafcbd3e7, one line attributes timer to processing.js; all assertions retained, no product change or tests; sent Sage and Honker; awaiting publication/CLEAR.
|
||||
|
||||
2026-10-22 02:25:29 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 sage-2349 record commit | 0f8235277 on 4db8b5778 adds packet BUILD.md cause record; product manifest remains ffb46fbd/87 paths; no amend, product change or tests; Sage/Honker notified.
|
||||
|
||||
2026-10-22 02:48:36 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 0f8235277 full gate second red | singles330/330, processing61/61, bus646/646, cli783/783, discord192/192; WebUI323/1/1 host esbuild skip; shell geometry1062vs1000; stopped/no retry, no later suites/shells/mutants; cwd sweep empty, slot returned, Sage escalates Mos.
|
||||
|
||||
2026-10-22 02:57:36 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 #1689 mos-1248 desktop layout restart | FF 5cd32f4c1 on 0f8235277: desktop grid allocates processing bar, existing shell assertions kept, visibility/no-scroll assertions added; 74 static mutant sites; candidate 2f93ef19; sent Webby/Sage for static review; no runs, awaiting review and CLEAR.
|
||||
|
||||
2026-10-22 03:11:09 UTC | Rocko (Codex gpt-6.1-sol, d99326cc) | row 191 static nav layout, mos-1249/1250 | FF da4334806 on 5cd32f4c1; controls first in nav, non-sticky per sage-2386/webby-0108; candidate-only placement assertion changed, landed shell assertions preserved; 77 mutants statically preflighted; manifest 245dceb1; Webby/Sage handoff sent, no runs, awaiting static look/publish/debug CLEAR.
|
||||
|
||||
2026-10-22 03:15:11 UTC | Rocko (d99326cc) | row 191 sage-2394 debug | da4334806 shell5/5 and processing-browser9/9, awk14/0/0; geometry heights reported with read-only distinction; cwd sweep[] and slot returned; full window awaits separate CLEAR.
|
||||
|
||||
2026-10-22 02:01:30 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 202 #1700 FED R5 landing | ddfb29af7 on 9c9732983: 6 manifest paths from a74cd9f87 (digests match CANDIDATE.sha256, patch-id 9d89077b equal); Duckworth APPROVE r5 rev 3026; C1 gate by Quackerjack on merge 3b86180b9: cli 771/771 skip 0 (run 2, run 1 red on gate TMPDIR length), 10 test-*.sh rc 0, M1-M13 killed; done rev 3108; landing note refused for length before the done move, so the record is the commit body and #1700 c30265; 191 and 194 rebased onto it (one scripts/mosaic case line)
|
||||
|
||||
2026-10-22 03:35:55 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 273 #1778 activate --send/--no-sso landing | 0bbf0f6d1 landed on 378a76b85, a rebase of cac7d0f67 (0042f713d) with all 6 range-diff pairs = and equal patch-ids; 4-path manifest e1031023 unchanged; Filbert APPROVE rev 3111 (c30308) carried merge-scoped under mos-1160, carry comment c30319 names 0bbf0f6d1. Gate on 0bbf0f6d1 by Darkwing: stack-release.test.mjs 218/218 skip 0, stack-release-external.test.mjs 25/25 skip 0, test-release.sh rc 0 (selftest 14/0, no skips), T58 controls c0 11/11 pass and c1 11/11 fail, M1-M7 killed. Rerun because row 202 changed scripts/mosaic, which stack-release.test.mjs copies. Red 1 was 978143bf4 (T53 test 6 usage pin). This doc commit adds the TOOLS.md usage line and F39 rules (Filbert N1).
|
||||
|
||||
2026-10-22 04:02:12 UTC | Rocko (d99326cc) | row191 full window0fa7bf421 | touched35files336pass, packages2403pass/1esbuildskip, shells425selftests+156queue node pass; SR01–07killed, SR08timeout,69unrun; stopped/no retry;88blobs restored,cwd sweep[],owned network removed,slot returned Sage; await Mos ruling.
|
||||
|
||||
2026-10-22 04:14:56 UTC | Rocko (d99326cc) | row191 control/resume sage-2427 | control12/12;SR08separate timeoutkill;49resumed assertionkills thenKS43survived;total56assertionkills+1timeout,1survivor,19unrun;88blobs restored,cwd sweep[],slot returned,Sage/Mos informed;no retry/edit.
|
||||
|
||||
2026-10-22 04:21:30 UTC | Rocko (d99326cc) | row191 KS43+19 retarget run | KS43assertionkill thenKS44invalid mutation syntax;57assertionkills+1timeout,1invalid,18unrun;stopped,no edit/retry;88hashes restored,cwd sweep[],slot returned,Sage/Mos notified.
|
||||
|
||||
2026-10-22 04:30:06 UTC | Rocko (d99326cc) | row191 KS44+18 resume | 10assertionkills thenKS54timeout-without-control despite dependency assertion;total67assertionkills+1timeoutkill,1unqualifiedtimeout,8unrun;88hashes restored,cwd sweep[],slot returned,Sage/Mos notified,no retry/edit.
|
||||
|
||||
2026-10-22 04:41:46 UTC | Rocko (Codex gpt-6.1-sol,d99326cc) | row191 #1689 builder complete/in-review | published0fa7bf421,manifest0bcbf337;35singles336pass,9packages2403pass/1knownskip,10shells425selftests+156queue nodepass;75assertionkills+2controlledtimeoutkills=77;88hashrestore,cwd sweep[],slotreturned;in-review rev3114,expected no-token failure3115(exit1);Sage/Dewey handoff,no live action.
|
||||
|
||||
2026-10-22 05:03:18 UTC | Rocko (Codex gpt-6.1-sol,d99326cc) | Vikunja502 processing test/runner hardening | brief+isolatedrocko-v502;expected wordingred→green,Node27/27,Python4/4,5/5assertionkills(includingSR08andKS54nohang),0skips/timeouts;sweep[],candidate5hashrestored,slotreturned05:01:29Z;candidate968d6ee3,runnerc5bdc764;packetready,no commit/push/liveaction.
|
||||
|
||||
2026-10-22 05:07:34 UTC | Rocko (d99326cc) | Vikunja502 committed/review handoff sage-2482 | newbranchrocko/vikunja502-ce6a-20261022,source8e3c6a79b+packet9bf4683c6 on ce6a96ad6;5paths no base drift,range-diff=,patch-id37135ebe unchanged;manifest968d6ee3,packet93889519 verifies39/39;focused runs carry;Sage publish/Dewey review handoff sent,no push/liveaction.
|
||||
|
||||
2026-10-22 05:10:00 UTC | Sage (1ef1e4f8) | serial landing 205 then 268 | 205: 3 commits, 4 paths, digest 858463b1, every scripts/test-*.sh green on ce6a96ad6, Duckworth N1-N4 carried (N1/N3 to 205b or R3, N2 to the 193b2 brief, N4 #1782 before any user thread goes live). 268 r4: c27207f6e on 205, 9 paths, digest caa0fa4e, patch-id 2942acc8 equal to r3. Overlap checks clean before both pushes. No live action; LIVE stays 86f95bb27.
|
||||
|
||||
2026-10-22 05:58:59 UTC | Rocko | row 279 #1784: before, deadline overshoot skipped final observation and returned unknown; after, one-line for (;;) retains final mapping and unchanged unreachable fallback. Regression red as specified; whole stop-run 13/13 and processing-control 62/62; M1 assertion kill, no skips, restored and swept. Source 1016b8584, packet cad927137; manifest 312ed511d8f1cff826ee2fd547de8368923130c7bfe06236d8e29d8655b80aaa. Pending Sage publication and Dewey review; no live action.
|
||||
|
||||
2026-10-22 06:15:54 UTC | Sage (1ef1e4f8) | row 279 #1784 landed (mos-1269) | Before: stopRun returned unknown for a live run when a sleep overshot the deadline. After: LANDED ceadad993 as a carry of cad927137 onto 343b6dd76 (range-diff 1:= 2:=, patch-ids 6d8ae7f4/e484c297 equal, Dewey carry c30410). Sage gate on cad927137: business 209/0/0, cli 794/0/0, discord processing + processing-stop 9/0/0. Correction: the first discord run wrote to a wrong out path and ran nothing; the rerun is the result. Overlap sweep clean (13, 257). Note rev 3144, done rev 3145. No live action; LIVE stays 86f95bb27.
|
||||
|
||||
2026-10-22 06:37:43 UTC | Sage (1ef1e4f8) | row 194 b16 third red (mos-1269/mos-1270) | Gate on ceadad993 + 9cdc47d04: business 228/0, cli 826/0 (T9a passes), control-board 124/0, bus 705/1 at grant-executor.test.mjs G13 (timed out 10044 ms); conversation, webui and scripts/test-*.sh never ran. runb8 green 436/0/0, 17/17 mutants. Correction to sage-2524 (mos-1270): the stuck wait is the first until at :733, the holding file after decision.resolve, so the executor never started the install and hosts.revoke at :734 never ran; it is not a revoke-path hang. Ruling: a diagnosis run, 30 base + 30 candidate alternating under cli load, no gate retry.
|
||||
|
||||
2026-10-22 07:51:04 UTC | Rocko | row 280 #1785: before, any TCP connect could accept a port our sshd failed to bind; after, attempt-local exact log proof plus liveness and connect, non-ephemeral picker, bounded scratch remote sleep cleanup. Both occupied regressions red on old acceptance; CRLF build failure captured and fixed under sage-2535. Committed source b765f5cb8, packet d6c924764, manifest27806c0579c76d0999d2e6be4180c3867570df0268af9d7e0d7b971af799e6f5. Repeats07:29:03Z–07:48:38Z grant1230/1230,host780/780,CLI1588/1588; final M1/M2 four assertion kills,0skips/timeouts,sweepclear. Sage publishes/gates; Duckworth reviews.
|
||||
2026-10-22 08:00:02 UTC | Sage (1ef1e4f8) | row 280 #1785 landed | before: the bus tests' scratch sshd helper returned a port on a connect alone, and 194 b16's gate hung at G13 (base hung too, honker-0127). After: b0fd28b6f on refactor, a ff from da5d3651d (b765f5cb8 + Rocko slim packet; full evidence d6c924764 on rocko/row280-da5d-20261022). Duckworth approve rev 3153 (manifest 27806c05). Sage gate bus 656/0/0, ge 41/0/0, hr 26/0/0 (c30431). Rocko repeat 2010/2010 under cli 1588/1588, no timeout; M1/M2 killed 4/4. Two build-time slips recorded in Rocko's packet: a runner root path (no test ran) and the CRLF sshd log line. Overlap 13/257 clean. Note rev 3154, done 3155, #1785 closed c30432, P19 511 comment 623. 194 b17 rebases on b0fd28b6f (sage-2542).
|
||||
|
||||
2026-10-22 08:38:11 UTC | Sage (1ef1e4f8) | row 194 #1692 landed | before: 194 b16 9cdc47d04 went red three times (stopRun deadline, then the bus test sshd port race), fixed by rows 279 and 280. After: fd6dd327c on refactor, a ff on e122aad8e. b18 is b17 3f451772d rebased after my doc and queue push blocked a ff (16 =, series patch-id 64303253f8e6, packages equal b17). Webby approve rev 3160 on manifest 29f131ab. Honker gate on fd6dd327c: runb8 503/0/0, mutants 17/17; 18 packages 3668/0, ten scripts rc=0, Docker up, env skips named. The secret scan hit three fake_*_token fixtures in bus tests (false positive). Note 3161, done 3162; #1692 closed c30435; P19 414 comment 625. LIVE still 86f95bb27.
|
||||
2026-10-22 08:42:04 UTC | Honker (Claude Opus 5.5, ecd17786) | row 194 #1692 agent lifecycle build | before: no Console or CLI lifecycle for agents (create, launch, terminate, regenerate, delete) and no per-agent hire/fire veto. After: 16 commits landed as fd6dd327c (b18, a rebase of b17 3f451772d on e122aad8e: 16 =, series patch-id 64303253f8e6, packages equal b17). Manifest 53 paths, digest 29f131ab. Final run on fd6dd327c: runb8 39 files whole 503/0/0, mutants 17/17 killed, restores matched; gate.sh merge onto e122aad8e, 18 packages 3668/0, ten scripts rc=0, Docker up, four env skips named (webui esbuild pin, queue render outside canonical root, test-task live provider x2); sweep clean. Corrections: b16 went red at G13; my diagnosis (60 runs, base hang on port 38927 sshd bind EADDRINUSE) placed it outside 194, fixed by row 280; the g13diag load kill missed its process group and overran about 8 minutes; one runb8 launch failed rc=126 on a lost exec bit before any test ran; the b17 runb8 was stopped unscored when refactor moved. In-review revs 3156/3157, Webby approve rev 3160.
|
||||
2026-10-22 09:43:34 UTC | Sage (Claude Opus 5.5, 1ef1e4f8) | row 236 #1742 Gitea grant landing, and row 281 filing | 236 landed bd5f11ded at 09:37:31Z, a fast-forward on be2b008cc: 08840f3c1 rebased past 194 (commit 1 unions only, 2-7 =), e40b74beb for two stale out-of-manifest pins, bd5f11ded for a gitea-not-configured raise test. Dewey approve rev 3168 on manifest 0b00269b (34 paths). Megavolt gate under my slot: 2c bus 131, cli 224, route 28, docker 1; whole bus 765, business 228, cli 832, ten scripts rc=0; mutants 40/40; skips named, env only. Overlap with 13 and 257 clean. Note 3169, done 3170; #1742 closed c30451; P19 436 comment 639. Correction: the first whole run on ce74477e1 went red 762/764 on the two stale pins; that run is void. Follow-up per mos-1271: row 281 (#1786), a fire or delete revokes host and Gitea grants, Megavolt owns, Dewey reviews; brief docs/plans/2026-10-22_fire-revokes-grants.md; DEFERRED entry points at it.
|
||||
|
||||
@@ -22,6 +22,7 @@ RUN npm ci --ignore-scripts
|
||||
# runtime adapters.
|
||||
COPY contracts /opt/mosaic/contracts
|
||||
COPY src /opt/mosaic/src
|
||||
COPY packages/processing-control/src/state.mjs packages/processing-control/src/entrypoint.mjs /opt/mosaic/processing-control/
|
||||
COPY adapters /opt/mosaic/adapters
|
||||
RUN chmod 0555 /opt/mosaic/contracts /opt/mosaic/contracts/* \
|
||||
&& chmod 0555 /opt/mosaic/src /opt/mosaic/src/*.sh \
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Row 273 (#1778): activate --send and --no-sso
|
||||
|
||||
Darkwing, 2026-10-22. Brief: `docs/plans/2026-10-22_activate-send-nosso.md` (blob in f0d0b31e9), approved by
|
||||
Mos in mos-1246 (F39). Base: origin/refactor 0042f713d. Reviewer: Filbert. Gate owner: Sage.
|
||||
|
||||
## What changed
|
||||
|
||||
Only `scripts/stack-release.sh` and its two node test files.
|
||||
|
||||
- `activate --send` puts `--send` into the run's Console flags after `--sso` and before the tester
|
||||
flags, where webui-service.sh renders it. It refuses `send-needs-sso` unless those flags hold `--sso`,
|
||||
given or carried. The plan line is `send: on; --send was given`, or `send: carried` when the pointer
|
||||
already holds it. `check_target` lets it name the pointer's copy only when the pointer holds no `--send`,
|
||||
so a second `--send` on the same copy refuses `already-active`.
|
||||
- `activate --no-sso` refuses `sso-not-on` when the pointer holds no `--sso`. Otherwise it takes the
|
||||
Console flags from a rollback set (below) and keeps the pointer's tester flags. `send_for_target`
|
||||
drops `--send`, whether it came from the set or the pointer. It is the only place that does, so one
|
||||
mutant there (M3) shows the drop is tested. The plan prints `sso: off; Console flags from <set>/<file>; restores <flags>`
|
||||
and a send line, `send: dropped; --no-sso drops --send (F39)` or `send: off; --no-sso was given and the
|
||||
pointer holds no --send`. The steps log gets `drop --send; --no-sso was given` and
|
||||
`turn off --sso; the Console flags come from <set>/<file>`.
|
||||
- Usage errors: `--send` with `--no-send`; `--no-sso` with `--sso`, `--send`, a proof pair or
|
||||
`--no-proof`; either flag on any verb but activate; either flag under `--units external`.
|
||||
`--no-sso --no-send` is allowed, since the brief doesn't forbid it and both drop the same flag.
|
||||
- The set read that `sso_base` did inline for a rollback to a Console without `--sso` moved into
|
||||
`set_flags`, which both paths call. Its three messages are unchanged, and T57 still pins them.
|
||||
- `from_log` has a new mode, `sets`: the activation of each ok activate and rollback line, newest first,
|
||||
stopping at the adopt line, which took no rollback set.
|
||||
|
||||
## The consequential choice: which set --no-sso reads
|
||||
|
||||
The brief says the flags come from "the rollback set of the activation that turned `--sso` on", and that
|
||||
"a set whose Console already ran with `--sso`" refuses. Each set holds the Console unit from before its
|
||||
run. So the activation that turned `--sso` on is the newest one whose set holds a Console without
|
||||
`--sso`: every activation after it ran with `--sso`, and so did every set it left.
|
||||
|
||||
`no_sso_base` walks the log's sets newest first and stops at the first set whose Console has no `--sso`.
|
||||
It refuses `sso-off-flags` at the first set it can't read (missing, failing its SHA256SUMS, or with no
|
||||
argv the script can parse), since it can't tell whether that set was the one. If every set ran with
|
||||
`--sso`, it refuses with `the Console in <set>/<file> already ran with --sso, and the log names no
|
||||
earlier activation`. A log with no set at all refuses with `the log names no activation with a rollback
|
||||
set`.
|
||||
|
||||
The tradeoff: a later same-copy re-pin that carried `--sso` (a `--no-send`, or a re-pin onto a newer
|
||||
copy) doesn't break `--no-sso`, which the phase 2 window needs if a re-pin lands between step 5 and a
|
||||
rollback. The cost is that "a set that ran with `--sso` refuses" now applies only when no older set
|
||||
without `--sso` exists. A tampered set on the walk still refuses, even one newer than the set the walk
|
||||
would have used. Filbert, please rule on this reading.
|
||||
|
||||
## Filbert's early look: B1 and N1 (sage-2315)
|
||||
|
||||
Filbert accepted the walk. Sage ruled both findings into this row.
|
||||
|
||||
- B1: row 200's rollback to a copy whose Console has no `--sso` took the flags from the set and kept
|
||||
the pointer's `--send`. From an `--sso --send` pointer, which this row's `--send` can now make, that
|
||||
gave a Console with `--send` and no `--sso`, which F39 b forbids. `send_for_target` now drops `--send`
|
||||
whenever the run drops the pointer's `--sso`, so it is still the one place that drops it. The plan
|
||||
line reads `send: dropped; the run drops --sso, and --send needs it (F39)`, and the steps log gets
|
||||
`drop --send; the run drops --sso`. Row 200's sso line now names only the tester as following the
|
||||
pointer.
|
||||
- Changed existing assertion, for the review to approve by name: T57 item 3 (sage-1389), "the
|
||||
plain case", ends with `assert.deepEqual(pointer(g).console,[...noSend(gbase),...pair(f)])`. It
|
||||
used to expect `'--send'` at the end. The case seeds a legacy `--send` pointer, turns `--sso` on
|
||||
(which carries `--send`), then rolls back to B, whose Console has no `--sso`. A carried `--send`
|
||||
that ends without `--sso` breaks F39 b whatever its source, so the old pin asserted the wrong
|
||||
outcome (Sage accepted this in sage-2321). Row 200's two sso plan strings also lose "--send and"
|
||||
(`ROLLBACK_PLAN` and the round 3 named-instance case), since `--send` no longer follows the pointer
|
||||
on that path.
|
||||
- N1: the walk skipped an ok line with no activation. Skipping it could land on an older set that
|
||||
isn't the right one, so it now refuses `sso-off-flags` with `an ok line in the log names no
|
||||
activation`. The walk reads the log through `mapfile`. That gives no entry for an empty log, so a
|
||||
log with no set still gets its own message.
|
||||
|
||||
## test-release.sh
|
||||
|
||||
The window plan says test-release.sh "gets cases for both". The brief lists it only as a file to run
|
||||
whole. test-release.sh is release.sh's Docker selftest and doesn't call stack-release.sh, so the cases
|
||||
live in the node tests and test-release.sh runs unchanged.
|
||||
|
||||
## Tests (T58 in stack-release.test.mjs)
|
||||
|
||||
- T1: `--sso --send` from B onto C1 with a tester port: plan, pointer, unit and steps. `--send` beside a
|
||||
carried `--sso` re-pins C1 once; a second `--send` refuses `already-active`; C carries it.
|
||||
- T2: `--send` without `--sso` refuses on another copy and on the pointer's copy, also when the pointer
|
||||
carries `--send` alone; `--plan` lists the refusal.
|
||||
- T2 (sage-2315 B1): `--sso --send` onto C1, then a rollback to B, whose Console has no `--sso`. The
|
||||
pointer and the unit end with neither flag, and the plan and steps name the drop.
|
||||
- T2 and T4 usage: every forbidden pairing and verb, checked against the full usage text.
|
||||
- T3: from an `--sso --send` pointer, `--no-sso` on the same copy gives back the pre-`--sso` ExecStart
|
||||
line byte for byte, without `--send`. A second case walks past a `--no-send` re-pin that ran with
|
||||
`--sso` and keeps the pointer's tester port (7334), which differs from the set's (7333). A third
|
||||
(sage-2305 a) turns `--sso` on, then re-pins twice with `--send` and `--no-send`, so the two newest
|
||||
sets both hold `--sso`. `--no-sso` then gives back the arrival's ExecStart line byte for byte.
|
||||
- T4 (sage-2305 b): two sets, both edited or written to hold `--sso` with sums intact, refuse
|
||||
`sso-off-flags` through `refused()`.
|
||||
- T4: `sso-not-on`; an ok line with no activation; a set that fails its sums, ran with `--sso`, has no
|
||||
argv or is missing; a log with no set. Each runs through `refused()`, so nothing stops and nothing else changes.
|
||||
- T5: stack-release-external.test.mjs adds `--send` and `--no-sso` to the row 232 usage list.
|
||||
- T55 test 5 and the T57 usage test match the new usage line.
|
||||
|
||||
## Mutants
|
||||
|
||||
| # | Change | Expected killer |
|
||||
|---|---|---|
|
||||
| M1 | `set_flags` skips the SHA256SUMS comparison when `--no-sso` is given | T4 tampered set |
|
||||
| M2 | `no_sso_base` skips the `sso-not-on` refusal | T4 `sso-not-on` |
|
||||
| M3 | the `--no-sso` branch of `send_for_target` keeps `--send` | T3 |
|
||||
| M4 | `--send` skips the `send-needs-sso` refusal | T2 |
|
||||
| M5 | parse_opts drops the `--send`/`--no-send` rule | T2 and T4 usage |
|
||||
| M6 | a rollback to a Console without `--sso` keeps `--send` (B1) | T2 (sage-2315 B1) |
|
||||
| M7 | the walk skips an ok line with no activation (N1) | T4 no-activation line |
|
||||
|
||||
Runs and results go in the round's notes once Sage clears the slot.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Row 268 build notes (Gizmoduck)
|
||||
|
||||
Case 2 C2-01c, playground levels. Gitea #1773, Vikunja 464. Brief: docs/plans/2026-10-20_case2-rows.md,
|
||||
"C2-01c playground levels: universe-level prefixes and draft universes" (blob a6ac627c8). The ruling is
|
||||
agents/gosalyn/work/case2/DESIGN.md "Ruling after r11: playgrounds at three levels" (:49-88 at
|
||||
db1e7732a), and it wins where the two differ. Reviewer Webby, gate owner Sage. Branch gizmoduck/row268
|
||||
from refactor db1e7732a. Line numbers are on this branch unless a commit is named.
|
||||
|
||||
## mapping.mjs
|
||||
|
||||
- `kind` (:71). An entry with a `kind` key must say `"hybrid"` or `"draft"`. Before, a plain universe
|
||||
with `kind` refused as an unknown key, and `kind: "draft"` did the same.
|
||||
- A draft (:73-86) refuses `books` with its own message before the key check, so the cause reads
|
||||
plainly. It takes `kind`, `name` and `playground`, needs one prefix or more, and comes back as
|
||||
`{ kind: 'draft', name, books: [], playground }`, frozen with the rest.
|
||||
- The prefix level check moved out of the per-entry loop to :127-140, after every entry is read,
|
||||
because it needs every entry's books. It runs after the book-holds-book, prefix-holds-prefix and
|
||||
listed-twice checks, so an overlap still reports as before. For each prefix and each book folder:
|
||||
- equal to any book folder: `library-mapping-invalid` (brief, What ships, "or equal to a book folder");
|
||||
- inside or holding another entry's book: `library-playground-other-universe`, and the message says
|
||||
which, with the book and its entry;
|
||||
- holding its own entry's book: `library-mapping-invalid`.
|
||||
A prefix inside its own book is book level, and one that touches no book is universe level. A draft
|
||||
has no books, so its prefixes are either outside every book or refused as another entry's.
|
||||
- `sources` (:117-120) now refuses any entry with a `kind`, so a draft can't be a source either. The
|
||||
message changed from "not a non-hybrid universe" to "not a plain universe". mapping.test.mjs:61 on
|
||||
db1e7732a pinned the old words, and that line moves with it.
|
||||
- `library-playground-outside-book` is gone from packages and docs/case2.
|
||||
|
||||
## scope.mjs
|
||||
|
||||
`classify` (:12-24) now runs two passes. The first finds a prefix in any entry, and the file is
|
||||
`playground` with that entry's id and `book` set to the entry's book folder that holds the prefix, or
|
||||
`null`. The second finds a book folder, and the file is `canon`. `place` (:26-32) works out `type` and
|
||||
`folder` from the first folder under the prefix or the book, as before. The validator keeps prefixes
|
||||
from overlapping, so the first pass finds at most one.
|
||||
|
||||
## Readers on this base
|
||||
|
||||
- reader.mjs:182 and :194 spread `classify`'s answer into each file record, so a universe-level or
|
||||
draft file carries `book: null`. Nothing in the reader reads `book`.
|
||||
- reader.mjs:206-209. R5 fires for a canon file linking into any playground. R6 fires when the target's
|
||||
universe differs and isn't a source. A draft can't be a source, so a link into one gives R5 and R6.
|
||||
A link into the file's own universe-level prefix gives R5 only. No code change was needed. The new
|
||||
test covers both.
|
||||
- survey.mjs:66 and mirror.mjs:246 read only `scope`. mirror.mjs:251 calls `readLibrary` and checks
|
||||
only `path`.
|
||||
- No other package calls this `classify` or reads `universes` (git grep over packages/*/src).
|
||||
- C2-05a (row 216) and C2-06 (row 217) have no candidate branch on origin at the time of writing. I
|
||||
check again before in-review.
|
||||
|
||||
## Tests
|
||||
|
||||
- mapping.test.mjs: the old outside-book test splits into the other-universe refusals (inside another
|
||||
entry's book, holding one, a draft prefix inside a book), the mapping-invalid ones (equal to a book,
|
||||
own or another's, and holding its own), universe level in a plain universe and in a hybrid with
|
||||
`books: []`, and the draft shape (frozen with `books: []`, with `books`, with none or empty
|
||||
prefixes, an extra key, and five bad `kind` values). The overlap test gains a cross-entry hold and a
|
||||
cross-entry duplicate. The hybrid test gains a draft source. The caller-supplied `fail` test now
|
||||
uses `Second World/Playground/` and sees the new code, since `Elsewhere/` is valid now.
|
||||
- scope.test.mjs, new: one case per level (book, universe in a plain universe, universe in a hybrid,
|
||||
draft), `.canvas` under each new prefix staying unmapped, prefixes first inside a book that holds a
|
||||
prefix, and the reader case for R5 and R6, with `book: null` on the read records.
|
||||
- business.test.mjs: `Elsewhere/Playground/` now loads, and `Second World/Playground/` refuses with
|
||||
`library-playground-other-universe` through `validateBusinessDocument`.
|
||||
|
||||
## Docs
|
||||
|
||||
- packages/library/README.md: the universe keys, a line on the three levels, the refusal table and
|
||||
the Scope section.
|
||||
- docs/case2/SETUP-SHEET.md "Your playground": one line per level and the draft entry.
|
||||
- docs/case2/SURVEY-RUN.md:114: the new code and message.
|
||||
|
||||
## Hash pins
|
||||
|
||||
`git grep -E '[0-9a-f]{64}'` over packages/*/tests finds no pin on any changed file.
|
||||
|
||||
## Runs
|
||||
|
||||
None yet. Every run waits for Sage's CLEAR.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,707 @@
|
||||
# Row 194 design: agent lifecycle from the Console and the CLI, with a per-agent veto
|
||||
|
||||
Brief: `docs/plans/2026-10-20_agent-lifecycle.md` (blob a907109409ac). Gitea #1692. MVP T1 item 5 (mos-0974,
|
||||
mos-0977, DECISIONS Q176). Owner Honker, reviewer Webby, gate owner Sage. Base: refactor at 53bb59462.
|
||||
Branch `honker/row194`.
|
||||
|
||||
Related designs read: row 190 `agents/darkwing/work/queue-190/DESIGN.md` (r2, after mos-0986), row 191
|
||||
`agents/rocko/work/queue-191/DESIGN-r2.md`. Row 193 is still in design. All line numbers below are at
|
||||
53bb59462.
|
||||
|
||||
## Summary
|
||||
|
||||
- One verb family on the CLI, `mosaic business instance <verb> BUSINESS INSTANCE`, and one Console section.
|
||||
Create and delete change the business file. Launch, terminate and regenerate act on runs.
|
||||
- Every human verb goes through the bus with the human proof. Launch, regenerate and delete are requests the
|
||||
launcher takes once and carries out under `serialize`. Launches go through the one `start()`, so
|
||||
`--no-launch`, the limits, `launch.revoke` and row 191's switch all apply.
|
||||
- The chief hires (adds and lists an instance) and fires (ends its runs and delists it). It never deletes.
|
||||
`role.hire` and `role.fire` are new hard waits, so Jason resolves each one (Mos, mos-1017). The routed
|
||||
form waits on a PRD 1.7 draft in this packet.
|
||||
- A hire allowlist in code: the chief can hire only a definition whose every action is on `HIRE_ALLOWED`
|
||||
(message, record, review request, assigned-task updates, and two actions that matter after row 199).
|
||||
Launch, end, organization, naming, hire, verdict, merge and every later action stay out.
|
||||
- The per-agent forbid is two lists in the business file that only the human writes: `launch.keep`
|
||||
(instances the chief can't fire, replace or end) and `launch.noHire` (definitions the chief can't hire).
|
||||
- The broker reloads a business from its file after each committed write. Create, delete, hire, fire and
|
||||
veto changes then count without a bus restart. Hand edits still need one, except `bus.tester`.
|
||||
- No bus schema change and no migration. Named for review: two new vocabulary actions (hard waits), the
|
||||
role-file `hireable` key, `"hireable": []` in `roles/chief-of-staff.json`, and three business-file keys.
|
||||
|
||||
## What exists today (checked at 53bb59462)
|
||||
|
||||
| # | Fact | Where |
|
||||
|---|---|---|
|
||||
| E1 | Instances are keys of the business file's `roles`. `launch.instances` lists those the chief may launch. The list must be non-empty | `business.mjs:121-132` |
|
||||
| E2 | The human adds an instance and changes its definition, vars and listing. No delete. Credentialed definitions are refused | `instance-settings.mjs:1-5, 22-31, 75-156` |
|
||||
| E3 | The broker copies the businesses once at start and refuses a role it doesn't know | `broker.mjs:208, 446` |
|
||||
| E4 | The chief launches over `launch.sock`. `fromSession` checks identity, then `check()` (which reloads the file), then `authorizeLaunch`, then `start()` | `launcher.mjs:232-327, 624-668` |
|
||||
| E5 | The chief's `launch` tool takes an enum of the instances listed when its bundle was built | `harness/src/tools.mjs:116-119` |
|
||||
| E6 | The chief ends a run with `role.revoke` plus a reason (`role.end`). Ending then launching again is already a replace | `broker.mjs:1601-1628`, `tools.mjs:167` |
|
||||
| E7 | `mosaic stop RUN` is a same-uid SIGTERM. It has no bus call and no human proof | `cli.mjs:447-454`, `seat/src/session.mjs:249-270` |
|
||||
| E8 | The Console stops only the assistant (`assistant.stop`) and writes instances through `instances.set` | `serve.mjs:282-289, 315-363, 535` |
|
||||
| E9 | The launcher already starts runs for the human on its 2 s tick: threads with pending messages, and merge holders | `launcher.mjs:133, 487-597` |
|
||||
| E10 | `launch.revoke` and `launch.restore` are human-only. A revoked business refuses `role.launch` | `broker.mjs:770-774, 2087-2095` |
|
||||
| E11 | Only the assistant is END_PROTECTED. Coder, reviewer, pm and cto all declare gitea and vikunja credentials | `vocabulary.mjs:135`, `roles/*.json` |
|
||||
|
||||
The chief's veto today comes from the human-only membership in E2. The chief can only launch what the human
|
||||
listed. It has no way to create an instance, and the human has no way to stop the chief ending a given
|
||||
instance's runs.
|
||||
|
||||
## D1. The verbs
|
||||
|
||||
All of them are human verbs. Each is a broker verb behind `#human(s)` and then the D4 hook. Each logs its
|
||||
actor. The CLI usage joins `INSTANCES_USAGE` (`cli/src/instances.mjs:9`).
|
||||
|
||||
| Verb | CLI | Bus verb | Effect |
|
||||
|---|---|---|---|
|
||||
| V1 create | `business instance add B I --definition DEF [--vars FILE] [--launch] --revision REV` | `instances.set` op `add` | as today, with no restart (D3) |
|
||||
| V2 launch | `business instance launch B I [--wait SECONDS]` | `instance.launch` | one run, through the launcher |
|
||||
| V3 terminate | `business instance terminate B I [--reason TEXT]` | `instance.end` | ends every run of I and cancels I's open requests |
|
||||
| V4 regenerate | `business instance regenerate B I [--wait SECONDS]` | `instance.regenerate` | V3, then V2 once the run has exited |
|
||||
| V5 delete | `business instance delete B I --revision REV [--wait SECONDS]` | `instances.set` op `delete` | a request. The launcher removes I from the business file and renames its workspace |
|
||||
| V6 veto | `business veto B (keep\|unkeep) I` and `business veto B (nohire\|hire) DEF`, each `--revision REV` | `veto.set` | edits `launch.keep` and `launch.noHire` |
|
||||
|
||||
`mosaic business instances B` (read) gains, per instance: `running` (run ids from the bus claims), `kept`
|
||||
and `retired`. The top level gains `noHire`. `mosaic stop RUN` stays as it is. It is the same-uid operator
|
||||
tool, and V3 is the human verb.
|
||||
|
||||
### D1.1 Requests (V2, V4, V5)
|
||||
|
||||
The broker never starts a process or touches a workspace. Launch, regenerate and delete are requests the
|
||||
launcher carries out, as E9 already does for the human's threads. A request has three states, open, taken
|
||||
and closed, all recorded as `action.allowed` events with reserved action names:
|
||||
|
||||
| Event | Written by | Body |
|
||||
|---|---|---|
|
||||
| `lifecycle.launch`, `lifecycle.regenerate`, `lifecycle.delete` | the broker, at the verb | `{ request, via }`, subject I, actor the human or `tester`. A `human.input` event goes with it, as `launch.revoke` does |
|
||||
| `lifecycle.taken` | host op `lifecycleTake` | `{ request }` |
|
||||
| `lifecycle.done` | host op `lifecycleDone` | `{ request, run? , refusal? }` |
|
||||
| `lifecycle.cancelled` | V3, for each open request of I | `{ request, by }` |
|
||||
|
||||
1. The launcher's new `lifecyclePass` runs on the same tick as `threadPass` and `landPass`. It lists open
|
||||
requests with host op `lifecycleRequests`.
|
||||
2. For each one, under `serialize`, it calls `lifecycleTake(request)`. That op runs in one store
|
||||
transaction. It re-checks, in order: the request is still open (not taken, cancelled or closed), it is
|
||||
less than 60 s old (else it closes as `request-expired`), the business isn't revoked (`launch_state`,
|
||||
`launch-revoked`), I isn't retired, row 191's switch is off (`processing-halted`) for launch and
|
||||
regenerate, and for a tester request `bus.tester` is true in the file (D4.1). Any failure closes the
|
||||
request with that code. Otherwise it writes `lifecycle.taken` and answers go.
|
||||
3. Only after go does the pass act: `check(I, { human: true })` then `start()` for a launch, the V5 steps
|
||||
for a delete.
|
||||
4. The pass then calls `lifecycleDone`. A taken request never runs again, whatever happens after the take.
|
||||
If `lifecycleDone` throws, the request stays taken with no done event. The CLI reports that as
|
||||
`outcome-unknown` with the request id, and the launcher log names the run.
|
||||
5. The CLI polls a read verb, `instance.request`, until the request closes or `--wait` runs out (default
|
||||
30 s). A timeout exits 3 with the request id. An untaken request can still be taken within its 60 s.
|
||||
|
||||
So the human's `launch.revoke` stops every untaken request, not only new ones (B4), and a bus restart
|
||||
can't start a request older than 60 s.
|
||||
|
||||
### V2 launch
|
||||
|
||||
1. At the verb the broker checks that I exists, isn't a thread instance (`not-thread-launch`: threads start
|
||||
on a message), isn't retired, and that the business isn't revoked and, with row 191, isn't halted. These
|
||||
repeat at the take (D1.1 item 2).
|
||||
2. After the take, `check(I, { human: true })` and `start({ instance: I, launchedBy })`, where
|
||||
`launchedBy` is the business human or `tester`. `human: true` lets the human launch an instance that
|
||||
isn't in `launch.instances`. The listing limits the chief, not the human. `launch.by` still goes through
|
||||
the `pm: true` branch (`launcher.mjs:235`), as `--pm` does today. Every other step of `check()` and
|
||||
`start()` is unchanged.
|
||||
|
||||
### V3 terminate
|
||||
|
||||
1. The broker revokes every claim I holds (`#claimEnd(s, h, 'revoke', null, reason, actor)`). A thread
|
||||
instance has all its thread claims revoked, which is exactly `assistant.stop`.
|
||||
2. In the same transaction it writes `lifecycle.cancelled` for every open (untaken) request of I, so no
|
||||
launch the human asked for before the terminate starts after it (B5).
|
||||
3. It writes `lifecycle.end` with the runs. The launcher pass sends SIGTERM to each run's runner after
|
||||
checking its start time, as the idle stop does (`launcher.mjs:526`). It also signals any run of I in the
|
||||
launcher's registry that has spawned but not yet bound, which holds no claim yet. The existing exit
|
||||
handler records the end, so the launch journal and `session.ended` are unchanged.
|
||||
4. Nothing held, nothing open and nothing in the registry answers `not-held`.
|
||||
5. Terminate is allowed while row 191's switch is on. It starts nothing.
|
||||
|
||||
A taken launch request is past cancelling. Its run reaches the registry under `serialize`, and the next
|
||||
terminate ends it. The CLI's terminate output says so when a taken request exists.
|
||||
|
||||
There is no SIGKILL escalation in this row. A run that ignores SIGTERM keeps running. `mosaic stop` and the
|
||||
limits are the operator's tools for it (N6).
|
||||
|
||||
### V4 regenerate
|
||||
|
||||
A fresh session with the same identity and role. It is V3 then V2 under one request. The pass takes the
|
||||
request, waits for the old run's exit handler, then runs `check()` and `start()`. The workspace stays, as
|
||||
it does across every launch today. The session is new because every non-thread launch gets a new run dir
|
||||
(`seat/src/session.mjs:107`, newRun). If the old run hasn't exited by the request's 60 s mark, the request
|
||||
closes as `regenerate-timeout`, nothing starts, and the old run is still running (N6).
|
||||
|
||||
A thread instance (the assistant) refuses `regenerate-thread`. Its context lives in the thread's session
|
||||
directory, and clearing that is T1 item 7 (clear context). This row doesn't take it.
|
||||
|
||||
### V5 delete
|
||||
|
||||
`instances.set` gains op `delete`. At the verb the broker and the writer refuse, in this order:
|
||||
|
||||
| Refusal | When |
|
||||
|---|---|
|
||||
| `tester-refused` | the caller is the tester (B6) |
|
||||
| `organization-definition-refused` | I is END_PROTECTED |
|
||||
| `organization-instance-referenced` | I is `launch.by` or an arbiter. The message names the reference |
|
||||
| `organization-instance-kept` | I is in `launch.keep`. The human runs `veto unkeep` first. Delete never clears a veto silently |
|
||||
| `organization-credentials-out-of-scope` | I holds credentials or a tracker bot. That stays a hand edit until the credentials row (row 199) |
|
||||
| `instance-running` | the broker sees a claim for I. The human terminates first |
|
||||
| `instance-starting` | an open or taken request for I has no done event |
|
||||
| BusinessError | I is the last name in `launch.instances` (E1). The message says so |
|
||||
|
||||
The checks pass with no write, and the broker records a `lifecycle.delete` request. After the take, the
|
||||
launcher pass, under `serialize` so no `start()` of I runs alongside it:
|
||||
|
||||
1. Re-checks the registry for I (`instance-running`) and re-runs the writer's checks against the file as it
|
||||
stands. The `--revision` the human gave must still match, else `revision-conflict`.
|
||||
2. Commits the file through the organization writer. It removes `roles.I` and the references that exist
|
||||
only for I: its `launch.instances` entry, its `launch.limits.tokens.instances` entry and its
|
||||
per-instance model entry. It adds I to a new top-level list, `retired`. The build lists every place
|
||||
`validateBusinessDocument` reads a role instance name and puts each one in one of the two groups (refuse
|
||||
or remove). A reference the table doesn't name stops the build and goes back to Webby.
|
||||
3. Renames `workspaces/<b>/I/` to `workspaces/<b>/.retired/I-<transaction>/`, mode 0700. No workspace is
|
||||
no rename.
|
||||
4. Calls `lifecycleDone` with the transaction.
|
||||
|
||||
The file goes first. Once it is committed, `check()` refuses I as unknown, so no launch can open the old
|
||||
workspace even if step 3 fails. A failed rename closes the request with `workspace-rename-failed`, the
|
||||
transaction and the path left behind. A second `delete` of a retired name skips steps 1 and 2 and retries
|
||||
step 3 only. The file and evidence are already right, so this is the whole recovery.
|
||||
|
||||
What stays:
|
||||
|
||||
- Run evidence (`launches/<b>/<run>/`, `launches/<b>.jsonl`) and bus rows are untouched. They are
|
||||
write-once.
|
||||
- The workspace, under `.retired`. This row deletes no file. A prune with a receipt is a later act.
|
||||
- `retired` stops a later create or hire from reusing the name. Decisions bind to the role instance and
|
||||
outlive restarts (`broker.mjs:783-788`). A new `coder` would otherwise inherit an approval raised by the
|
||||
old one. The tradeoff is a name that can't come back without a hand edit. I chose that over voiding
|
||||
decisions, which would touch a second store.
|
||||
|
||||
### V6 veto
|
||||
|
||||
`veto.set` args: `{ revision, op: "keep" | "unkeep" | "nohire" | "hire", name }`. It goes through the
|
||||
organization writer, so it is logged with actor and diff in `changes.jsonl`. `keep` needs a declared,
|
||||
non-END_PROTECTED instance. `nohire` needs a name that matches the id pattern. A no-op answers
|
||||
`organization-no-change`. It is human-only and isn't in `TESTER_ORGANIZATION_VERBS`.
|
||||
|
||||
## D2. Chief hire and fire
|
||||
|
||||
### D2.1 Authority (Mos, mos-1015 and mos-1017, through Sage, sage-1362 and sage-1365)
|
||||
|
||||
This row adds `role.hire` and `role.fire` to `ACTIONS` (`vocabulary.mjs:11`). Under PRD REQ-WAIT-3 a new
|
||||
action is a hard wait until a PRD version says it isn't (`vocabulary.mjs:52-57`), so both ship as hard
|
||||
waits. Jason resolves each one. There is no PRD change and no grant in a role file: `role.mjs:42` refuses
|
||||
a withinRole that lists a hard wait, and REQ-WAIT-2 forbids it.
|
||||
|
||||
The flow in this row:
|
||||
|
||||
1. The chief raises a `role.hire` or `role.fire` decision whose body carries the arguments (business,
|
||||
name, definition, vars, or name and reason). A hard wait goes to the human only. No arbiter resolves it.
|
||||
2. Jason resolves it in the Console or the CLI.
|
||||
3. The chief calls `instance.hire` or `instance.fire` citing the decision. The broker checks it with
|
||||
`#consumeAuthority(s, 'role.hire', { decision, target })`, as the `role.revoke` decision branch does,
|
||||
and the arguments must equal the decision body field by field (N1, webby-0010). For hire that is
|
||||
`name`, `definition` and `vars` (vars compared with `canonicalJson`, absent equal to absent only). For
|
||||
fire it is `name` and `reason`. A key in the call that the body lacks, or a body key the call lacks,
|
||||
refuses `decision-mismatch`. Every check below runs again at this call, so a veto set after the resolve
|
||||
still refuses.
|
||||
|
||||
The routed form, where the chief hires and fires without a decision each time, needs a PRD version that
|
||||
moves both actions to `ROUTED`. My draft is `agents/honker/work/queue-194/PRD-1.7-DRAFT.md`. It quotes
|
||||
TARGET.md line 66 and carries Mos's bounds. Jason approves it as a `prd.approve` decision in the Console,
|
||||
and Sage opens that decision. When it lands, a later commit adds the two actions to both `ROUTED` lists
|
||||
(`vocabulary.mjs`, `broker.mjs:43-69`) and to `roles/chief-of-staff.json` withinRole. The broker then
|
||||
takes the `reason` form instead of a decision. The verbs and every check below stay the same. This row
|
||||
builds and tests the hard-wait form only. It writes the `hireable` key and its allowlist now (D2.3), because
|
||||
both forms use them.
|
||||
|
||||
### D2.2 What the chief may do
|
||||
|
||||
| Act | Bus verb | Rule |
|
||||
|---|---|---|
|
||||
| launch | `launch` (host) | as today: listed instances only |
|
||||
| end a run | `role.revoke` + reason | as today, and refused `instance-kept` when the target is kept (D2.4) |
|
||||
| hire | `instance.hire { name, definition, vars?, decision }` | adds and lists an instance. Refusals below |
|
||||
| fire | `instance.fire { name, reason, decision }` | ends I's runs, cancels I's open requests, and removes I from `launch.instances`. Refusals below |
|
||||
|
||||
Only the business's chief, the instance named by `launch.by`, may call either verb. Any other caller
|
||||
refuses `role-not-chief`, even with a resolved decision, as `#classification` already limits
|
||||
`role.launch`. The broker reads `hireable` from that chief's own role file. Both act only in the chief's
|
||||
own business. Neither verb takes a business argument. The broker uses the
|
||||
cap's business, and a decision raised in another business refuses `decision-mismatch`. Row 191's switch
|
||||
refuses both while halted (D6). Every hire and fire writes `changes.jsonl` through the organization writer
|
||||
with actor the chief's instance and run, and the decision id.
|
||||
|
||||
Hire refuses, in order:
|
||||
|
||||
1. `definition-not-hireable` when the definition holds an action outside `HIRE_ALLOWED` (D2.3), whatever
|
||||
any list says.
|
||||
2. `definition-not-hireable` when the definition isn't in the chief role file's `hireable` list, or is in
|
||||
the business's `launch.noHire`.
|
||||
3. `organization-definition-refused` for END_PROTECTED, `organization-credentials-out-of-scope` for a
|
||||
credentialed definition (until the credentials row, row 199), `invalid-request` for version 1.
|
||||
4. `organization-instance-retired` when the name is retired.
|
||||
5. When the name exists: a fired instance (in `roles`, not listed) is listed again only if `definition`
|
||||
equals its current one and `vars` is absent. Any other definition or vars refuses
|
||||
`organization-instance-exists`, and so does a listed name. A re-hire never moves an instance's
|
||||
decisions to a different role (B3).
|
||||
|
||||
Hire's `vars` may hold only the keys the chief can already set on an instance through `organization.set`
|
||||
(the model key, `broker.mjs:1266-1269`) and narrowing `limits.*` keys. Anything else, such as `harness`
|
||||
or a route, refuses `invalid-request`. The human sets those (N3).
|
||||
|
||||
Fire refuses `instance-kept` for a kept instance, `organization-definition-refused` for END_PROTECTED,
|
||||
`organization-instance-referenced` for `launch.by`, and a BusinessError for the last listed instance. A
|
||||
fired instance stays in `roles`. The chief never deletes. Delete stays a human act because it retires the
|
||||
name.
|
||||
|
||||
Fire ends the fired instance's processes, not only its claims (F4, Megavolt through sage-1374). Every run
|
||||
gets the shared model token at start: `ENV_ALLOW` (`packages/seat/src/session.mjs:83-94`) passes
|
||||
`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN` and the other provider variables into the sandbox. A revoke
|
||||
can't take that token back, so a fired agent whose process lives on still holds it.
|
||||
|
||||
The delist is a business file commit through the organization writer, and the revoke and events are store
|
||||
rows. No transaction covers both (webby-0011 B2), so fire runs in a fixed order, store side first:
|
||||
|
||||
A. One store transaction: steps 1 to 3 below, plus `fire.stopped` with the decision id. The decision isn't
|
||||
consumed yet.
|
||||
B. The delist through the organization writer (`launch.instances` loses I, logged in `changes.jsonl`).
|
||||
C. One store write consuming the decision and recording `fire.done`.
|
||||
|
||||
If B or C fails, fire answers the step's code and the decision stays unconsumed. A second fire citing the
|
||||
same decision runs again. A finds nothing to revoke, cancel or stop, and that isn't a refusal for fire
|
||||
(unlike V3's `not-held`). B finds I already delisted and counts as done. C consumes. So a fire is safe to
|
||||
repeat until it's consumed. The other order would leave a delisted agent with a live process and its model
|
||||
token when the store step fails, which is the case F4 exists to stop. A halt between A and B doesn't stop
|
||||
B or C, because neither starts anything. A second fire while halted refuses (D6), and the halt's own stop
|
||||
covers the runs.
|
||||
|
||||
Steps 1 to 3 are V3's, with one change:
|
||||
|
||||
1. It revokes every claim the instance holds.
|
||||
2. It writes `lifecycle.cancelled` for each of the instance's open requests.
|
||||
3. It writes `lifecycle.end` with the runs and `kill: true`. The launcher pass stops each runner, spawned
|
||||
but unbound registry runs included, with escalation (sage-1380 item 1):
|
||||
- SIGTERM after the identity check.
|
||||
- After the grace, SIGKILL to any runner still alive, after the same identity check again, and
|
||||
`lifecycle.end.killed` with the run and pid.
|
||||
- A runner still alive after SIGKILL and the observe wait gets `lifecycle.end.alive`. The Console's
|
||||
Agents section shows it to the human.
|
||||
A runner whose identity no longer matches is never signalled. Every check, the recheck after the
|
||||
grace included, uses the identity (start ticks plus boot id), never a bare pid, so a reused pid never
|
||||
counts as the fired run (webby-0011 N2).
|
||||
|
||||
Fire's answer lists the runs it is stopping. The delist stops any new chief launch (`launch` takes listed
|
||||
instances only). A human launch request already taken is past cancelling, as in V3. The human made it, the
|
||||
fire answer names it, and the human's terminate ends its run.
|
||||
|
||||
The stop is row 191's per-run stop, not a second one, so the two can't fight (sage-1380 item 1). Row 191's
|
||||
design r2 (`agents/rocko/work/queue-191/DESIGN-r2.md`, "Stop policy and completion") sends SIGTERM, waits
|
||||
5 s, sends SIGKILL to verified groups and namespaces, and observes for 2 s. Its identity check is start
|
||||
ticks plus boot id, which replaces the start-time check at `launcher.mjs:526`. Fire calls that routine for
|
||||
one instance's runs and uses its constants, so the grace is row 191's 5 s, not the 10 s in r3. The order:
|
||||
|
||||
- Fire is refused while halted (D6), so a halt sweep never meets a fire that started after it.
|
||||
- A halt that comes during a fire's stop finds those runs already stopping. Each run has one stop receipt
|
||||
(row 191's), and the first stop owns it. The halt sweep joins it instead of sending a second SIGTERM or
|
||||
restarting the deadline, and counts the run in its remaining or quiescent totals.
|
||||
- The record says which stop owned it: reason `fire` with the decision, or row 191's `kill-switch` with
|
||||
its generation.
|
||||
|
||||
V3 terminate is unchanged. It still sends SIGTERM only (N6). Only fire escalates, because only a fired
|
||||
agent must lose the token at once. If row 191 hasn't landed when this row builds, the build chains on its
|
||||
candidate (Q177), as D6 already says.
|
||||
|
||||
### D2.3 The hire allowlist (B2, sage-1365 item 4, webby-0010 B1)
|
||||
|
||||
r2 and r3 had a deny list, `HIRE_FLOOR`. Webby's B1 showed it missed routed actions (`git.merge.protected`,
|
||||
which `roles/merge-gate.json` holds with no credentials, `decision.resolve.technical`, `policy.change`,
|
||||
`prd.approve`), and that any action added later would be hireable by default. That isn't fail-closed. r4
|
||||
replaces it with an allowlist.
|
||||
|
||||
A definition is hireable only if every action in its withinRole and crossRole is on `HIRE_ALLOWED`:
|
||||
|
||||
| Allowed action | Held today by |
|
||||
|---|---|
|
||||
| `message.send`, `record.write` | every v2 role file |
|
||||
| `review.request`, `task.update.assigned` | coder, cto, pm, reviewer |
|
||||
| `task.scope.change`, `git.push.working` | coder, cto, pm (credentialed, so they matter only after row 199) |
|
||||
|
||||
Everything else in `ACTIONS` fails, including the actions of the old floor (`role.launch`, `role.end`,
|
||||
`role.revoke`, `organization.change`, `instance.name`, `review.verdict`, `role.hire`, `role.fire`) and those
|
||||
B1 named. An action added to `ACTIONS` later fails until a reviewed commit adds it here.
|
||||
`git.push.remote` (row 199, routed to coder by PRD 1.7) isn't on the list. A coder holding it stays
|
||||
unhireable until a reviewed commit adds it, after the credentials bound is lifted.
|
||||
|
||||
Against today's role files: chief-of-staff, cto, merge-gate, pm, reviewer and security-reviewer fail.
|
||||
Assistant, researcher, coder and the five C-suite files pass the allowlist. Assistant is END_PROTECTED and
|
||||
coder is credentialed, so D2.2 item 3 refuses them. The C-suite files hold only `message.send` and
|
||||
`record.write`, so hiring one gives no authority beyond its name. `hireable` still has to list it.
|
||||
|
||||
The list is a constant in `business/src/vocabulary.mjs`, `HIRE_ALLOWED`. It is checked twice:
|
||||
|
||||
- At load. `role.mjs` validates the chief role file's `hireable` list: distinct definition names, each
|
||||
present in `rolesDir`, version 2, and every action on `HIRE_ALLOWED`. A list naming one that isn't refuses
|
||||
the role file (`hireable names <def>, which holds <action>`). The check reads only the named file's authority lists,
|
||||
never its `hireable`, so a list can't recurse. `hireable` is an optional key on any role file. This is a
|
||||
role schema change, named for review.
|
||||
- At hire, in the broker, against the definition as loaded then. A role file edited after load is caught
|
||||
here.
|
||||
|
||||
`hireable` in `roles/chief-of-staff.json` stays empty in this row (`[]`, Q4, sage-1374). Naming the first
|
||||
role kind is a separate policy commit in `roles/`, made after Jason resolves the `prd.approve` decision, as
|
||||
AGENTS.md invariant 7 requires. With an empty list the chief can hire nothing, even with Jason's resolve,
|
||||
until that commit. The row's tests use scratch role files whose `hireable` names a kind.
|
||||
|
||||
### D2.4 The per-agent forbid and the veto (sage-1365 item 3)
|
||||
|
||||
TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as needed; unless user forbids
|
||||
per agent". The forbid is two lists in the business file that only the human writes:
|
||||
|
||||
- `launch.keep`: instance names. The chief can't fire, replace or end a kept instance. The broker refuses
|
||||
`instance-kept` for every chief act aimed at it except launch:
|
||||
- `instance.fire`, even with a resolved `role.fire` decision. Mos's limit is that Jason's veto applies
|
||||
to every hire and fire, so the human unkeeps before a fire.
|
||||
- a hire of that name.
|
||||
- `role.revoke` in the reason branch (`broker.mjs:1607-1613`). End then launch is a replace (E6).
|
||||
- an `organization.set` write of that instance's model key (B7). A model change is a replace at the next
|
||||
launch.
|
||||
|
||||
The `role.revoke` decision branch (`broker.mjs:1614-1615`) still ends a kept run. It ends a run, it
|
||||
doesn't fire or replace, and it cites a decision the human resolved (Webby's Q2 ruling). The human's V3
|
||||
and the limits also still end a kept run.
|
||||
- `launch.noHire`: definition names the chief can't hire into this business (D2.2 item 2).
|
||||
|
||||
Why the chief can't change the forbid:
|
||||
|
||||
- Only `veto.set` writes the lists. It is human-only (`#human(s)` then the D4 hook) and refuses the tester.
|
||||
- `organization.set`, the chief's one write verb, reaches only `ORGANIZATION_KEYS` (`broker.mjs:145`),
|
||||
which exclude `launch` and `retired`.
|
||||
- `instances.set` is human or tester only (`broker.mjs:1248-1298`).
|
||||
- Hire and fire carry no field that writes either list.
|
||||
|
||||
T18 tests each of those paths from a chief session and checks the business file bytes are unchanged.
|
||||
|
||||
END_PROTECTED instances can't be kept (N7). That is intended: Q169 lets the chief end the assistant's
|
||||
runs, and the assistant's own stop is `assistant.stop`. Default, per "unless user forbids": nothing is
|
||||
kept and nothing is on `noHire`.
|
||||
|
||||
### D2.5 Where it lives and how the broker enforces it
|
||||
|
||||
The forbid lives in the business file, under `launch`, beside the membership it limits:
|
||||
`launch.keep: [instance]`, `launch.noHire: [definition]`, and top-level `retired: [instance]`. `hireable`
|
||||
lives in the chief's role file, because it bounds the role, not one business.
|
||||
|
||||
`checkLaunch` (`business.mjs:118`) gains the two keys. Each list is distinct. `keep` names declared,
|
||||
non-protected instances. `noHire` entries need only the id pattern (N2). A stale deny entry is harmless,
|
||||
and a reviewed commit that removes a role file must not stop every business that lists it. `retired`
|
||||
names none of `roles`. This is the business-file schema change for review. Old files without the keys load
|
||||
unchanged. An old bus loader rejects a file that has them (`rejectUnknownKeys`), so a rollback past this
|
||||
row needs the keys removed first. The activation plan has to say so (R4).
|
||||
|
||||
The broker enforces from its live copy of the business, which D3 keeps current. The checks sit in the
|
||||
`role.revoke` reason branch, in `organization.set`'s model write and in the two chief verbs, before any
|
||||
write.
|
||||
|
||||
## D3. Live changes
|
||||
|
||||
Today the broker's `#businesses` is fixed at start (E3), and `instances.set` answers `restart: true` for
|
||||
add and redefine.
|
||||
|
||||
1. `Broker.reloadBusiness(id, document)` swaps in one business's validated document. It is synchronous, so
|
||||
no request sees half of it. It refuses an unknown id and a document whose `human` changed (that is row
|
||||
193's ground).
|
||||
2. The runtime's organization callback (`runtime.mjs:121-133`) calls it in one place, after every verb
|
||||
that commits a write, whatever the verb (N1). Today that is `limits.set`, `instances.set`, `routes.set`
|
||||
and `plugins.set`, and after this row `veto.set`, `instance.hire`, `instance.fire` and the launcher's
|
||||
delete through the same writer. It loads from the file through the loader `bootConfig` uses
|
||||
(`cli/src/config.mjs:88-105`), so the broker holds exactly what a restart would give it. Then
|
||||
`limits.refresh(id)` runs, as `plugins.set` does now.
|
||||
3. Other startup consumers of `businesses` in `startBroker` (`runtime.mjs:75-81`): credential references,
|
||||
usage, the web services and the tasks adapter's bots. A create or hire adds no credentials, no tracker
|
||||
bot and no web grant, because credentialed definitions are refused. The build checks each consumer
|
||||
against that claim. Any change that would need one of them still answers `restart: true` and doesn't
|
||||
reload.
|
||||
4. A definition change on a running instance refuses `instance-running`. A live cap must not change
|
||||
authority under its run. This is new: today the change waits for the restart. Existing tests that pin
|
||||
`restart: true` change with it (N4): `business/tests/instance-settings.test.mjs`,
|
||||
`bus/tests/instances-verb.test.mjs`, `cli/tests/instances.test.mjs:76-94` (the `RESTART_NEEDED` text and
|
||||
the restart line) and `webui/tests/instances.test.mjs:25, 71, 74, 140-145`. The build names each
|
||||
changed assertion in its manifest.
|
||||
5. The chief's `launch` tool (E5) takes `instance` as a string with the id pattern instead of the start-time
|
||||
enum. Its description says the host refuses an unlisted instance. `check()` already refuses one
|
||||
(`launcher.mjs:235`). Without this, the chief can't launch its own hire until its session restarts.
|
||||
`tools.mjs` may carry hash pins in tests. The build greps for them (R5).
|
||||
6. Hand edits of the business file still need a restart. Nothing watches the file. The one exception is
|
||||
`bus.tester` (D4.1), read from the file at each tester call.
|
||||
|
||||
After this, `restart` is false for create, delete, hire, fire and veto changes, and true only under item 3.
|
||||
|
||||
## D4. RBAC until row 193
|
||||
|
||||
Every human verb above calls `#human(s)` and then one hook, `#mayHuman(s, verb, target)`. Until row 193
|
||||
lands, the hook passes for the business's one `human`, which is the owner. The verbs are listed once, in
|
||||
an exported `LIFECYCLE_HUMAN_VERBS` (`instance.launch`, `instance.end`, `instance.regenerate`, `veto.set`,
|
||||
and `instances.set` ops `add`, `change`, `delete`), so row 193 maps each verb to the human roles that may
|
||||
use it. Row 193 replaces the hook body and nothing else.
|
||||
|
||||
### D4.1 The tester (Sage, sage-1355)
|
||||
|
||||
What the tester may do after this row:
|
||||
|
||||
| Verb | Tester |
|
||||
|---|---|
|
||||
| `instance.launch`, `instance.end` | yes, new |
|
||||
| `instances.set` ops `add`, `change` | yes, as today (row 175, `broker.mjs:103`, `serve.mjs:537`) |
|
||||
| `instances.set` op `delete` | no, `tester-refused` in the broker (B6) |
|
||||
| `instance.regenerate`, `veto.set` | no, `tester-refused` |
|
||||
|
||||
The tester's cap is bound to one business, and only when that business sets `bus.tester` to true
|
||||
(`broker.mjs:542-546`). That stays Jason's switch, and no bus verb writes it. Two checks run on each tester
|
||||
lifecycle call, before any effect, and again at the take (D1.1):
|
||||
|
||||
1. The request's business is the cap's business. Any other refuses `tester-refused`.
|
||||
2. `bus.tester` is exactly true in the business file as it stands, read through `settings.current(id)` as
|
||||
`harnessOn` does (`runtime.mjs:136-142`), not from the broker's copy (B8). An unreadable file refuses
|
||||
`tester-off`. So a hand edit turning testing off counts at the next tester call, without a restart.
|
||||
|
||||
The request and done events record actor `tester`. The tester's Q173 tests of the Agents section then cover
|
||||
launch and terminate on a scratch business, and the rest through fixtures.
|
||||
|
||||
## D5. One stack instance
|
||||
|
||||
Row 190 carries the stack instance as the config `MOSAIC_CONFIG` names. These verbs take no instance name.
|
||||
The CLI reaches the broker socket under that config's `dataRoot`. The broker acts on its own businesses,
|
||||
launcher and data root. A business id that exists in two stack instances is two separate businesses. A test
|
||||
runs two scratch roots with the same business id and shows a launch, a terminate and a delete in one leave
|
||||
the other byte-identical (T11).
|
||||
|
||||
## D6. The kill switch
|
||||
|
||||
- Launch and regenerate end in `start()`, which row 191 gates (its D3 inventory, `launcher.mjs:329-457`).
|
||||
The new pass adds no second start path.
|
||||
- The broker refuses `instance.launch` and `instance.regenerate` with `processing-halted` while the switch
|
||||
is on (V2 step 1), and the take checks again (D1.1), so no request is waiting when a human resumes.
|
||||
- `instance.hire` and `instance.fire` are agent work verbs, so row 191's default refuses them while halted.
|
||||
The build adds them to row 191's work table, not its read list. A hire Jason resolved before a halt
|
||||
still refuses while halted, and the chief calls it again after the resume.
|
||||
- Terminate, create, delete and veto start nothing and stay allowed.
|
||||
- If row 191 hasn't landed when this row builds, the build chains on its candidate (Q177). The two halted
|
||||
checks are written against its reader and tested with its fixture.
|
||||
|
||||
## D7. The Console
|
||||
|
||||
A new Settings section, "Agents", in its own `agents.js` file. Settings itself isn't rewritten. It lists
|
||||
each instance with definition, listed, kept, retired and running runs, and flags a run with a
|
||||
`lifecycle.end.alive` event as "still running after fire" (D2.2). It has these actions:
|
||||
|
||||
- Create (definition select, from `definitions`), Delete (with a confirm naming what stays: the evidence
|
||||
and the workspace under `.retired`).
|
||||
- Launch, Terminate (with an optional reason), Regenerate (with a confirm).
|
||||
- Keep and Unkeep per instance, and a No-hire toggle per definition.
|
||||
|
||||
The routes are `POST /api/instances` (op `delete` joins `add` and `change`), `POST /api/agent/launch`,
|
||||
`/api/agent/terminate`, `/api/agent/regenerate` and `POST /api/veto`. Each uses `instanceSend`'s boundary:
|
||||
signed-in user, exact Origin, same-origin Fetch Metadata, JSON within 16 KiB, strict keys, one bus call
|
||||
through the human transport, and `outcome-unknown` on an unexpected answer. Launch and regenerate show the
|
||||
request, then poll `GET /api/agent/request?id=` until it closes.
|
||||
|
||||
`agents.js` joins the static file list (`serve.mjs:28`) and `index.html` (N5).
|
||||
|
||||
When `/api/config` says `send` is false, every action button shows as unavailable with the text "Agent
|
||||
actions are unavailable here. Use mosaic business instance from the instance CLI." This is the live state
|
||||
until Q175 phase 2. Every write test uses a fixture server and fixture writers, as rows 165 and 191 do. No
|
||||
route gets a `--send` exception.
|
||||
|
||||
## D8. Tests
|
||||
|
||||
Every test uses scratch roots under `TMPDIR` and the existing fixtures (`business/tests/helpers.mjs`,
|
||||
`cli/tests/helpers.mjs`, `webui/tests/tester-fixture.mjs`). None touches `~/.mosaic-dev`,
|
||||
`~/.config/mosaic-dev`, a live unit or a slot file.
|
||||
|
||||
| Test | Shows | File |
|
||||
|---|---|---|
|
||||
| T1 | V1 create on a running bus: a launch of the new instance binds with no restart, and the answer says `restart: false` | cli `instances.test.mjs` |
|
||||
| T2 | V2: a human launch of an unlisted instance runs. `launch.by` goes through the `pm` branch (N8). A thread instance, a retired one and a revoked business refuse at the verb. A request older than 60 s never launches. A refused `check()` closes the request with its code | cli `lifecycle.test.mjs` (new) |
|
||||
| T2a | Take: a `launch.revoke` between the request and the pass closes it `launch-revoked` with no process. A `lifecycleDone` that throws once leaves the request taken, and the next tick starts nothing. A taken request is never taken twice | same |
|
||||
| T3 | V2 with `--no-launch` and with each limit refusal: no process, request closed with the code | same |
|
||||
| T4 | V3: claims revoked, runner SIGTERMed after the start-time check, one end record. A mismatched start time sends no signal. An open request is cancelled and never starts. A spawned, unbound run in the registry is signalled | same |
|
||||
| T5 | V4: the new run starts only after the old exit, with a new run dir and the same workspace. A run that ignores SIGTERM closes the request `regenerate-timeout` with nothing started. The assistant refuses `regenerate-thread` | same |
|
||||
| T6 | V5: each refusal in the V5 table, in order, including `instance-starting` with an open request. A clean delete leaves the evidence bytes unchanged, commits the file before the rename, adds `retired`, and a create or hire of the same name refuses. A failed rename closes `workspace-rename-failed`, `check()` refuses the name, and a second delete renames only | business `instance-settings.test.mjs` and cli `lifecycle.test.mjs` |
|
||||
| T7 | V6: each op, the no-op refusal, an unknown `noHire` name accepted, and a `changes.jsonl` entry with actor `human` | business |
|
||||
| T8 | Chief hire (hard-wait form): no decision refuses. A decision raised by the chief goes to the human only, no arbiter. After the resolve, the hire adds and lists the instance, logs actor and decision, and the same chief session launches it (D3 item 5). Arguments that differ from the decision body refuse. A non-chief caller with a resolved decision refuses `role-not-chief`. Outside `hireable`, in `noHire`, credentialed, END_PROTECTED, retired, and extra vars each refuse | bus `chief-of-staff.test.mjs` |
|
||||
| T8a | Re-hire (B3): a fired name with the same definition and no vars is listed again. Another definition or vars refuses `organization-instance-exists` | same |
|
||||
| T8b | Hire allowlist (D2.3): one case per action in `ACTIONS` that is neither on `HIRE_ALLOWED` nor a hard wait (`role.mjs` already refuses those), looped from the lists, not typed by hand. A fixture definition, clean when the chief's role file loads and listed in `hireable`, is rewritten to hold that action before the hire. The hire refuses `definition-not-hireable`. A definition holding one allowed and one refused action refuses. One holding only allowed actions hires. A pin test fixes `HIRE_ALLOWED` to the six actions | same |
|
||||
| T8c | Allowlist at load: a role file whose `hireable` names a definition holding each refused action refuses at `loadRole`, one case per action, looped the same way. The mixed case refuses. A clean list loads. A missing, version 1 or duplicate name refuses | business `role.test.mjs` |
|
||||
| T9 | Chief fire: runs end, open requests cancelled, delisted, still in `roles`. Kept (even with a resolved decision), END_PROTECTED, `launch.by` and last-listed refuse | bus `chief-of-staff.test.mjs` |
|
||||
| T9a | Fire ends processes (F4, sage-1380 item 1): with a bound run and a spawned but unbound registry run of I, both fixture runners holding a stand-in model variable, fire answers both runs. After the launcher pass, neither runner pid is alive and I holds no claim. A fixture runner that ignores SIGTERM gets SIGKILL after the grace, is gone, and has `lifecycle.end.killed` with its run and pid. A stand-in whose kill is stubbed to fail gets `lifecycle.end.alive`. A runner whose identity changed before the SIGTERM, or between SIGTERM and SIGKILL, gets no signal. V3 terminate of a SIGTERM-ignoring runner sends no SIGKILL | cli `launcher.test.mjs` |
|
||||
| T9b | Fire and row 191's halt: a halt during a fire's stop sends no second SIGTERM, keeps the fire's deadline, writes one stop receipt with reason `fire` and the decision, and counts the run in its totals. A fire while halted refuses (D6) | cli `launcher.test.mjs`, on row 191's fixture |
|
||||
| T9c | Fire order (webby-0011 B2): the organization writer stubbed to fail after A commits. Fire answers the writer's code, I's claims are revoked and its runs stopped, I is still listed, and the decision is unconsumed. A second fire citing the same decision delists I and consumes it. A third fire refuses `decision-consumed`. With C stubbed to fail, the second fire finds I delisted and only consumes | bus `chief-of-staff.test.mjs` |
|
||||
| T10 | Keep: chief `role.revoke` with a reason, fire, hire of that name and an `organization.set` model write aimed at a kept instance each refuse `instance-kept`. A `role.revoke` citing a human-resolved decision ends the kept run (Q2). The human's V3 works | same |
|
||||
| T11 | Two scratch stack instances with the same business id: V2, V3 and V5 in A leave B's root and config byte-identical. A chief of business A can't hire into business B on the same bus | cli `lifecycle.test.mjs`, bus |
|
||||
| T12 | Agent and reader sessions refuse every human verb before any effect. The tester gets exactly the D4.1 table, including `tester-refused` for `instances.set` op `delete` (B6). The D4 hook is called with the verb | bus `instances-verb.test.mjs` |
|
||||
| T12a | Tester launch and terminate work on the cap's business with `bus.tester` true, record actor `tester`, and refuse `tester-refused` for a second business on the same bus. A file edit setting `bus.tester` to false, with no restart, refuses the next call and the take `tester-off`. An unreadable file refuses `tester-off` | bus `instances-verb.test.mjs` |
|
||||
| T13 | `organization.set` with `launch`, `retired`, `keep` or `noHire` keys refuses | bus |
|
||||
| T14 | Live reload: after each committed verb, `plugins.set` and `routes.set` included, the broker answers from the new document. A definition change on a running instance refuses `instance-running` | bus |
|
||||
| T15 | Kill switch (with row 191's fixture): launch and regenerate refuse `processing-halted` at the verb and at the take, hire and fire refuse, terminate works | bus and cli |
|
||||
| T16 | Console: each route's boundary refusals, a fixture write per action, `outcome-unknown` on a bad shape, unavailable buttons when `send` is false, request polling, and `agents.js` served from the static list | webui `agents.test.mjs` (new) |
|
||||
| T17 | Business file: the new keys validate, each bad entry refuses, and a file without them loads byte for byte as before | business `business.test.mjs` |
|
||||
| T18 | The forbid can't be changed by the chief (D2.4): from a chief session, `veto.set`, `organization.set` with each list key, `instances.set`, and hire and fire with extra fields all refuse, and the business file bytes are unchanged | bus `chief-of-staff.test.mjs` |
|
||||
|
||||
### Mutants
|
||||
|
||||
Each one must fail at least one test. They run against the candidate, and the file is restored and its
|
||||
sha256 checked after each one.
|
||||
|
||||
| # | Change | Killer |
|
||||
|---|---|---|
|
||||
| M1 | `instance.launch` skips `#human` | T12 |
|
||||
| M2 | the pass calls `check()` without `human: true` | T2 |
|
||||
| M3 | the take accepts a request older than 60 s | T2 |
|
||||
| M4 | the take skips the `launch_state` check | T2a |
|
||||
| M5 | terminate signals without the start-time check | T4 |
|
||||
| M6 | regenerate starts before the old exit | T5 |
|
||||
| M7 | delete skips the running check | T6 |
|
||||
| M8 | delete skips `retired` | T6 |
|
||||
| M9 | delete clears `keep` instead of refusing | T6 |
|
||||
| M10 | delete removes the workspace instead of renaming it | T6 |
|
||||
| M11 | the `keep` check is removed from the `role.revoke` reason branch | T10 |
|
||||
| M12 | `noHire` is not read by hire | T8 |
|
||||
| M13 | fire deletes the instance from `roles` | T9 |
|
||||
| M14 | `reloadBusiness` isn't called after `instances.set` | T1, T14 |
|
||||
| M15 | the tool enum is kept | T8 |
|
||||
| M16 | the halted check is dropped from `instance.launch` | T15 |
|
||||
| M17 | the Console route skips the Origin check | T16 |
|
||||
| M18 | `veto.set` joins `TESTER_ORGANIZATION_VERBS` | T12 |
|
||||
| M19 | the tester's business scope check is dropped | T12a |
|
||||
| M20 | the tester's `bus.tester` file read is dropped | T12a |
|
||||
| M21 | hire skips the role file's `hireable` list | T8 |
|
||||
| M22 | the take doesn't write `lifecycle.taken`, so a request can be taken twice | T2a |
|
||||
| M23 | terminate doesn't cancel open requests | T4 |
|
||||
| M24 | delete skips the `instance-starting` check | T6 |
|
||||
| M25 | delete renames before it commits the file | T6 |
|
||||
| M26 | the tester's `instances.set` op `delete` refusal is dropped | T12 |
|
||||
| M27 | the `keep` check is dropped from `organization.set`'s model write | T10 |
|
||||
| M28 | the `keep` check is added to the `role.revoke` decision branch | T10 |
|
||||
| M29 | a re-hire accepts a different definition | T8a |
|
||||
| M30 to M36 | the broker's hire check accepts one action outside `HIRE_ALLOWED`, one mutant per action: `role.launch`, `role.end`, `organization.change`, `instance.name`, `review.verdict`, `git.merge.protected`, `decision.resolve.technical` | T8b |
|
||||
| M37 to M43 | the same seven, accepted by the load check in `role.mjs` only | T8c |
|
||||
| M44 | fire skips `instance-kept` when a decision is cited | T9 |
|
||||
| M45 | `instance.hire` skips the `launch.by` caller check | T8 |
|
||||
| M46 | `veto.set` skips `#human` | T18 |
|
||||
| M47 | the broker's hire check needs some action on `HIRE_ALLOWED` instead of every action | T8b |
|
||||
| M48 | the same in `role.mjs` only | T8c |
|
||||
| M49 | fire revokes the claims but writes no `lifecycle.end`, so no runner is signalled | T9a |
|
||||
| M50 | fire's signal skips spawned but unbound registry runs | T9a |
|
||||
| M51 | fire's stop drops the SIGKILL escalation | T9a |
|
||||
| M52 | SIGKILL is sent without the second identity check | T9a |
|
||||
| M53 | a killed runner gets no `lifecycle.end.killed` | T9a |
|
||||
| M54 | V3 terminate escalates to SIGKILL as fire does | T9a |
|
||||
| M55 | the halt sweep sends its own SIGTERM and restarts the deadline on a run fire is stopping | T9b |
|
||||
| M56 | fire delists before the store step | T9c |
|
||||
| M57 | fire consumes the decision in step A | T9c |
|
||||
| M58 | a second fire with nothing to revoke refuses `not-held` | T9c |
|
||||
|
||||
## Scope and boundaries
|
||||
|
||||
- In the row: the verbs and passes above in `business` (vocabulary, role validation, instance-settings,
|
||||
business validation), `bus` (broker verbs, reload, runtime callback), `cli` (instances command, launcher
|
||||
pass, host ops), `harness` (the launch tool's schema, and the hire and fire tools), `webui` (routes and
|
||||
the Agents section), the tests, the package READMEs and the PRD draft in this packet. `docs/TOOLS.md`
|
||||
text goes to Sage.
|
||||
- Named for review:
|
||||
- Vocabulary: `role.hire` and `role.fire` join `ACTIONS` as hard waits, under REQ-WAIT-3. The commit
|
||||
message names them as hard waits, as REQ-WAIT-3 requires.
|
||||
- Role schema: the optional `hireable` key, validated in `role.mjs` (D2.3).
|
||||
- Role file: `roles/chief-of-staff.json` gains `"hireable": []`. No authority change.
|
||||
- Business file schema: `launch.keep`, `launch.noHire`, `retired` (D2.5).
|
||||
- Not in the row: the routed form (after PRD 1.7), filling `hireable`, credentialed hires and deletes (row
|
||||
199), clear context for the assistant (T1 item 7), human roles (row 193), SIGKILL escalation, and any
|
||||
live activation or Console start.
|
||||
|
||||
## Risks
|
||||
|
||||
- R1. Case 1's working roles (coder, reviewer, pm, cto) all declare credentials (E11), and pm, cto and
|
||||
reviewer fail the hire allowlist anyway. Neither the human verb nor the chief can create or delete credentialed
|
||||
roles until the credentials row (row 199). That is the largest gap against Case 1, and it isn't this
|
||||
row's to close.
|
||||
- R2. The human proof is path-bound and same-uid (P19 333). Any same-uid process that runs the tree's
|
||||
`human-cli.mjs` can launch and delete. Row 190b's separate uid and row 193 narrow it. This row adds no
|
||||
check of its own.
|
||||
- R3. The 60 s request window means a slow `check()` (checkouts) can still start a run up to that point
|
||||
after the CLI gave up. The CLI says so on timeout.
|
||||
- R4. Rollback past this row needs `keep`, `noHire` and `retired` removed from each business file, and
|
||||
`hireable` from the role file, or the old loaders refuse them. The activation plan states this.
|
||||
- R5. `tools.mjs`, `serve.mjs`, `broker.mjs`, `vocabulary.mjs` and `role.mjs` may carry sha256 pins in
|
||||
other packages' tests. The build greps the tests for every changed path plus a 64-hex value before
|
||||
in-review, without a `head`.
|
||||
- R6. With `hireable` empty, the chief can hire nothing until the policy commit in `roles/` that follows
|
||||
the `prd.approve` resolve names a kind (Q4). Case 1's "as needed" waits on that commit and on PRD 1.7.
|
||||
- R7. Fire's SIGKILL closes the SIGTERM-ignoring case (sage-1380). A runner still alive after SIGKILL
|
||||
(an unkillable process, or a stop whose identity check fails) keeps the shared model token until the
|
||||
human acts on `lifecycle.end.alive`. Only a per-agent model token (row 199 Q-M4, a later proxy row)
|
||||
would let a fire revoke the token itself.
|
||||
|
||||
## Questions and rulings
|
||||
|
||||
- Q1, ruled by Mos (mos-1015, mos-1017, through sage-1365): `role.hire` and `role.fire` ship as hard
|
||||
waits Jason resolves. The routed form needs PRD 1.7, drafted in `PRD-1.7-DRAFT.md`. D2.1.
|
||||
- Q2, ruled by Webby (webby-0003): keep blocks the chief's `role.revoke` reason branch, not its decision
|
||||
branch. D2.4.
|
||||
- Q3, ruled by Sage (sage-1355): launch and terminate are tester verbs scoped to the tester's own
|
||||
business, behind `bus.tester`. D4.1.
|
||||
- Q4, ruled by Sage (sage-1374): `hireable` stays empty in this row. Naming the first role kind is a
|
||||
separate policy commit in `roles/` after Jason resolves the `prd.approve` decision. Tests use scratch role
|
||||
files that name a kind. D2.3.
|
||||
|
||||
## r2 changes against webby-0003
|
||||
|
||||
| Finding | Where |
|
||||
|---|---|
|
||||
| B1 | D2.1: hard waits under REQ-WAIT-3, no role grant. The routed form is PRD 1.7 (packet draft) |
|
||||
| B2 | D2.3: the floor from sage-1365 item 4, checked at load and at hire, T8b, T8c, M30 to M43 (an allowlist since r4) |
|
||||
| B3 | D2.2 hire item 5: a re-hire keeps the definition, T8a, M29 |
|
||||
| B4 | D1.1: one `lifecycleTake` transaction re-checks and writes `lifecycle.taken`, T2a, M4, M22 |
|
||||
| B5 | V3 cancels open requests and signals unbound runs. V5 refuses `instance-starting`, runs under `serialize`, commits the file before the rename, and states the recovery. T4, T6, M23 to M25 |
|
||||
| B6 | V5 table and D4.1: the tester's delete refuses, T12, M26 |
|
||||
| B7 | D2.4: keep covers `organization.set`'s model write, T10, M27 |
|
||||
| B8 | D4.1: `bus.tester` is read from the file at each call and at the take, T12a, M20 |
|
||||
| Q2 | D2.4, T10, M28 |
|
||||
| N1 | D3 item 2: one reload after every committed write |
|
||||
| N2 | D2.5: `noHire` takes any id-pattern name |
|
||||
| N3 | D2.2: hire's vars limited to the model key and narrowing `limits.*` |
|
||||
| N4 | D3 item 4 names the tests that change |
|
||||
| N5 | D7 and T16: `agents.js` joins `serve.mjs:28` and `index.html` |
|
||||
| N6 | V3 and V4: no SIGKILL, `regenerate-timeout` |
|
||||
| N7 | D2.4: intended, Q169 |
|
||||
| N8 | V2 item 2 and T2 |
|
||||
|
||||
## r3 changes against sage-1374, sage-1377 and Megavolt's F4
|
||||
|
||||
| Item | Where |
|
||||
|---|---|
|
||||
| Q4 ruled: `hireable` stays empty, the first kind is a later policy commit in `roles/` | D2.3, R6, Questions |
|
||||
| `instance.name` joins the floor (Webby B2, mos-1023 item 6) | D2.3 table, T8b, T8c, M47, M48 |
|
||||
| F4: fire ends the fired agent's processes, since `ENV_ALLOW` gives every run the shared model token | D2.2, D7, T9a, M49 to M51, R7 |
|
||||
| `git.push.remote` routed to coder only (mos-1028 item 3, row 199 r3) | `PRD-1.7-DRAFT.md`, REQ-LAND-2 and the REQ-WAIT-3 append |
|
||||
|
||||
## r4 changes against webby-0010, sage-1380 and megavolt-0008
|
||||
|
||||
| Item | Where |
|
||||
|---|---|
|
||||
| webby-0010 B1: the deny-list floor becomes the allowlist `HIRE_ALLOWED`, so merge-gate, `decision.resolve.technical`, `policy.change`, `prd.approve` and any later action are refused | D2.3, Summary, R1, T8b, T8c, M30 to M43, M47, M48, PRD REQ-COS-3 bound |
|
||||
| webby-0010 N1: field-by-field compare of the call against the decision body, extra or missing keys refuse | D2.1 item 3 |
|
||||
| webby-0010 N2: the model write is in REQ-COS-3's text | `PRD-1.7-DRAFT.md` |
|
||||
| sage-1380 item 1: fire escalates to SIGKILL through row 191's per-run stop, and the order against a halt is fixed | D2.2, T9a, T9b, M51 to M55, R7 |
|
||||
| sage-1380 item 2 and megavolt-0008: REQ-LAND-2 binds one commit oid, and Megavolt's three text changes against row 199 r4 (e1aa6216c) | `PRD-1.7-DRAFT.md` |
|
||||
| webby-0011 B1: same as webby-0010 B1, M47 and M48 now allowlist mutants | D2.3 |
|
||||
| webby-0011 B2: fire runs store side first, then the delist, then the consume, safe to repeat | D2.2 order A to C, T9c, M56 to M58 |
|
||||
| webby-0011 N1: the remote-owner host check at each push | sent to Megavolt for row 199 |
|
||||
| webby-0011 N2: identity, not bare pid, at every check | D2.2 step 3 |
|
||||
| Q4: Webby's view matches Sage's ruling (empty) | no change |
|
||||
@@ -0,0 +1,327 @@
|
||||
# PRD 1.8 draft: trusted actions within role guardrails, the manual mode, and grants the chief of staff
|
||||
# requests and the user approves
|
||||
|
||||
Draft by Honker for Vikunja #434, under Mos's assignment mos-1117 (through Sage, sage-1525). Not approved.
|
||||
It changes nothing until Jason approves it as a `prd.approve` decision. Sage opens that decision and sends
|
||||
Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 008746b6e, plus the version 1.7 text Jason
|
||||
approved (PRD-1.7-DRAFT.md r2, 9ba31b5b3, decision d52febb5, mos-1089). Where 1.7 is not yet written into
|
||||
the PRD file, this draft's anchors name 1.7's sections.
|
||||
|
||||
Revision 2 makes Mos's edits a to f (mos-1124, through Sage, sage-1534) and fixes Duckworth's B1 and B2
|
||||
with notes N1 to N6 (duckworth-0057) on r1 at 3c30f1036. Revision 3 fixes Duckworth's C1 and takes
|
||||
notes N1 and N2 (duckworth-0058) on r2 at 60f5ca8fa, with Gizmoduck's enrollment wording
|
||||
(gizmoduck-0067). Revision 4 makes Mos's rulings on duckworth-0057 (mos-1129, through Sage, sage-1538)
|
||||
on r3 at d16ccdb91: only an Approve creates a grant, one decision for a grant that makes an account, and
|
||||
the arbiter route for `host.sudo`. Revision 5 fixes two stale notes from Duckworth's r4 check
|
||||
(duckworth-0061 N1), with no change to the requirement text. Revision 6 makes Mos's wording for those
|
||||
notes and adds two items for Jason at approval (mos-1138, through Sage, sage-1548), again with no change
|
||||
to the requirement text.
|
||||
|
||||
For Jason at approval: "The main agent or chief of staff" in Q10 is read as one role under two names,
|
||||
the chief of staff. So only the chief raises a grant request, and your assistant passes your own request
|
||||
to the chief. That reading is Mos's (mos-1124). To change it, say so when you approve, and REQ-GRANT-1
|
||||
changes before the text goes into the PRD.
|
||||
|
||||
Also for Jason at approval:
|
||||
|
||||
- Only an Approve creates a grant. A grant written into the business file by hand makes nothing. An agent
|
||||
that runs as the same OS user as the stack can still write the stack's own store. That is the B7
|
||||
residual (Q182 on dragon-lin, #423 on .53 and .54), and this version doesn't close it.
|
||||
- Who decides a root command on another host changes. Once `host.sudo` is routed, a raise for it that
|
||||
cites the spec and isn't within-role goes to the arbiter for its domain, as version 1.7 set for
|
||||
`git.push.remote`, not to you. The grant and its allowlist still limit the command. In manual mode the
|
||||
decision comes to you (REQ-TRUST-2).
|
||||
- Revoke: Q188 says "you, Mos or the tester". "Mos" is read as your assistant (REQ-ROLE-4), limited to
|
||||
grants in its own organization, and the tester may revoke only on a dev instance. That reading is Mos's
|
||||
(mos-1124 d, mos-1132). To change it, say so when you approve, and REQ-GRANT-4 changes before the text
|
||||
goes into the PRD.
|
||||
- The broker enforces only that the chief of staff raised a grant request. The rest of the chain of
|
||||
command is the messaging lists and the role contracts, not a broker check (REQ-GRANT-1).
|
||||
|
||||
## What it changes
|
||||
|
||||
Today an agent acts without a raise only on actions its role file grants within-role, and the role
|
||||
files grant few. Every other use of a tool or command waits on a decision. Row 210 builds `host.sudo` as a
|
||||
hard wait, so Jason would decide each command run on another machine. Grants themselves are hand work:
|
||||
Jason makes every token and key from a runbook (REQ-CRED-1).
|
||||
|
||||
Jason's ruling Q188 turns this around. Roles set what each agent may do, and by default agents do it at
|
||||
will within their sandbox and guardrails. A per-use raise happens only in a manual mode. No agent creates
|
||||
a grant. The chief of staff brings a grant request to the user's inbox, the user clicks Approve or Deny, and
|
||||
protected stack code does every step of making the grant. The user's own tokens are entered once in the
|
||||
WebUI and never reach an agent.
|
||||
|
||||
This version adds:
|
||||
|
||||
1. Trusted actions within role guardrails as the default mode (REQ-TRUST-1).
|
||||
2. Manual mode, a list of actions that each need a decision per use: who sets it, where it lives and what
|
||||
it changes (REQ-TRUST-2).
|
||||
3. Grant requests, raised only by the chief of staff through the chain of command (REQ-GRANT-1).
|
||||
4. Approve and the protected grant code, with three grant methods: machine key, Gitea and GitHub
|
||||
(REQ-GRANT-2).
|
||||
5. The user's tokens, entered in the WebUI and used only by the grant code (REQ-GRANT-3).
|
||||
6. Row 210's REQ-CRED-4, generalized from `host.sudo` to every role-listed action that uses a grant
|
||||
(REQ-CRED-4).
|
||||
7. Instant revoke (REQ-GRANT-4).
|
||||
|
||||
The hard waits stay with the user (Mos, mos-1117): spending money, deleting data, new external accounts,
|
||||
messages outside the fleet and the rest of REQ-WAIT-1. Approving a grant is the user resolving one of
|
||||
those hard waits, `credential.mint`. A grant that makes an account, such as a seat's own Gitea user
|
||||
(Q190), is the same one decision: its method says it makes an account, and the inbox shows the account
|
||||
name before Approve. So no hard wait moves to an agent.
|
||||
|
||||
When this version lands, reviewed commits:
|
||||
|
||||
1. add the manual-mode list to the organization file schema, written only on the human's path
|
||||
(REQ-TRUST-2), and make the broker classify a listed action as needing a decision for each use, before
|
||||
any other routing.
|
||||
2. add the grant request: a decision the chief of staff raises with action `credential.mint`, naming the
|
||||
agent, the method and target, the exact rights and, when the method makes an account, the account
|
||||
name (REQ-GRANT-1). The broker refuses a `credential.mint` raise from any other role. An
|
||||
`account.create` raise isn't a grant request and routes as it does in version 1.7, such as a PM's
|
||||
sign-up for a service.
|
||||
3. add the grant code that runs on a resolved Approve, one method at a time, and the grant record it
|
||||
writes (REQ-GRANT-2, Vikunja #435 and #436).
|
||||
4. add the user's token store, written from the WebUI (or the CLI until the Console knows the signed-in
|
||||
human) and read only by the grant code (REQ-GRANT-3).
|
||||
5. move `host.sudo` into both `ROUTED` lists, as row 199 does for `git.push.remote`, and route it under
|
||||
REQ-CRED-4, with the arbiter route version 1.7 set for `git.push.remote`. Which definitions list it
|
||||
within-role is a separate reviewed role commit in `roles/`.
|
||||
6. update the role contracts that list the hard waits: `roles/chief-of-staff.md:75` raises grant
|
||||
requests with the REQ-GRANT-1 fields, and `roles/pm.md:55` asks the chief of staff for a grant instead
|
||||
of raising `credential.mint` itself (line numbers on origin/refactor). Other roles that list the hard
|
||||
waits get the same line as the PM. A role's messaging list carries the chain of command.
|
||||
|
||||
## Text for the PRD
|
||||
|
||||
Status line, appended to the current paragraph:
|
||||
|
||||
> Version 1.8 records Jason's ruling Q188 of 2026-10-20: trusted actions within role guardrails by
|
||||
> default, a manual mode for per-use raises, and grants the chief of staff requests and the user
|
||||
> approves, approved by Jason in decision <prd.approve decision id>.
|
||||
|
||||
Source, two new bullets above "Version 1.7 source":
|
||||
|
||||
> - Version 1.8 source, Jason's ruling Q188 (DECISIONS, 2026-10-20), his answers to Q8 and Q10, in part.
|
||||
> Q8: "I would rather grant the grant right to an agent that runs with strict guardrails. That agent
|
||||
> presents the final authorization for the user. Upon approval, the grant is created programmatically.
|
||||
> I see this as: User configures their PATs, API tokens, authorizations within the WebUI. The agent
|
||||
> presents the request for grant with X rights for another agent. The user reviews, then clicks
|
||||
> Approve/Deny. Click of Approve fires off protected mechanical methods to handle the grants on Github,
|
||||
> Gitea, .... services as needed. The tooling would need to exist and must be guarded." And "Disallowing
|
||||
> the agents to create grants autonomously is a necessary evil for better security. Requiring the user
|
||||
> to manually perform the steps is a no-fly." Q10: "Agents should have tools and command access based on
|
||||
> roles. Those roles dictate the allowed tools and commands. The use of a tool or command should not be
|
||||
> raised to the user unless the system is configured with a manual mode that requires user
|
||||
> interaction. By default I want to have the agents capable of running commands and tools at will
|
||||
> within their sandbox/guardrails. This allows autonomous actions by agents within their lanes. Perhaps
|
||||
> a "Trusted actions within approved guardrails" methodology." And "The main agent or chief of staff
|
||||
> would be responsible for surfacing the request to the user's inbox for approval. Not all agents are
|
||||
> capable or authorized to request directly. It must follow the chain of command properly." He agreed
|
||||
> with Mos's Q10 points 3 to 6: protected stack code does every step on Approve, his tokens are entered
|
||||
> in the WebUI and agents never see them, a command on the approved list runs without a click and any
|
||||
> other is refused, and he, Mos or the tester can revoke a grant at once.
|
||||
> - Version 1.8 source, Jason's ruling Q190 (DECISIONS, 2026-10-20), his answer "Q12: O1": each agent seat
|
||||
> gets its own account on his Gitea, created through the Q188 approval path once the Gitea grants exist.
|
||||
|
||||
Under "Decisions", after REQ-DEC-6, a new section:
|
||||
|
||||
> ### Trusted actions
|
||||
>
|
||||
> Added in version 1.8 (Q188).
|
||||
>
|
||||
> - **REQ-TRUST-1.** Agents act within their role guardrails without asking. The default mode is
|
||||
> "trusted actions within approved guardrails". An action a role file grants within-role is done at
|
||||
> will, inside the agent's sandbox, its tool limit and its grants, and is logged. It raises nothing to
|
||||
> the user. An action the file doesn't grant within-role still routes by REQ-DEC-2, and a hard wait
|
||||
> still goes to the user (REQ-WAIT-1). Only a reviewed role change adds an action to a role file, and
|
||||
> REQ-WAIT-2 still keeps every hard wait out of every role file.
|
||||
> - **REQ-TRUST-2.** The user can put chosen actions in manual mode. The organization file holds the list
|
||||
> of actions in manual mode, empty by default. While an action is on the list, every use of it by any
|
||||
> role becomes a decision for that use, raised to the user, and the action waits until the user
|
||||
> resolves it. A decision denied, or not resolved, refuses the action. Each decision is used once. For a
|
||||
> command on another host, the decision names the host, the command and its input. Actions not on the
|
||||
> list are unchanged. Only the user changes the list, through the WebUI or the CLI. The chief of
|
||||
> staff's organization writes (REQ-ROLE-3) can't change it, and no agent can. Every change is logged
|
||||
> with who made it. A change applies to the next use, and a run already in progress keeps no
|
||||
> exemption.
|
||||
|
||||
Under "Credentials", after REQ-CRED-3 (version 1.7):
|
||||
|
||||
> Added in version 1.8 (Q188):
|
||||
>
|
||||
> - **REQ-CRED-4.** An action a role definition lists within-role, and that uses a grant, runs without a
|
||||
> decision for each use only when all of these hold:
|
||||
> - the definition declares the credential kind the action uses;
|
||||
> - a grant names that agent, and that host or service;
|
||||
> - for a command, the command matches the grant's allowlist. A grant lists exact argv prefixes. A
|
||||
> wildcard grant is refused;
|
||||
> - the agent's key or token for that grant is issued and not revoked.
|
||||
>
|
||||
> A command off the allowlist is refused, and adding one takes a new grant request (REQ-GRANT-1). An
|
||||
> action whose definition doesn't list it within-role still needs a decision for each use. Only a
|
||||
> reviewed role change adds an action to a definition. `host.sudo` routes under this requirement.
|
||||
> - **REQ-GRANT-1.** No agent creates or writes a grant. A grant request follows the chain of command,
|
||||
> and only the chief of staff raises it to the user's inbox. Any other role that needs a grant for
|
||||
> itself or for an agent it directs asks the chief of staff, through its chain: a worker role through its
|
||||
> PM, the PM and the CTO directly. The assistant passes the user's own request to the chief of staff
|
||||
> (REQ-ASSIST-1). The request names the agent, the grant method and its target, the exact rights, and a
|
||||
> rotate-by or expiry date. The chief raises one only for an agent whose reviewed role definition
|
||||
> allows that kind of credential, and the broker refuses any other. A grant can't name a wildcard, a
|
||||
> shell or an empty command. The request is a decision with the action `credential.mint`, so it goes
|
||||
> to the user as a hard wait (REQ-WAIT-1). A grant whose method makes an account, such as a seat's own
|
||||
> Gitea user (Q190), is the same one decision: the method in the grant says it makes an account, the
|
||||
> request names the account, and the inbox shows the account name before Approve. The user's one
|
||||
> resolution covers the account and its credential, so the hard-waits list doesn't change. An
|
||||
> `account.create` raise isn't a grant request and routes as version 1.7 has it. The broker checks
|
||||
> only that the chief of staff raised the request. The chain of command before that is in the
|
||||
> messaging lists and the role contracts, not a broker check.
|
||||
> - **REQ-GRANT-2.** The user approves or denies each grant request in the inbox. Approve is the only thing
|
||||
> that creates a grant, and the code uses a grant only when it has a matching Approve record. No agent
|
||||
> creates a grant. A grant written into the business file by hand makes nothing. Row 210's hand-written
|
||||
> grants stay a test path until Vikunja #436a moves `host.run` to grants made by Approve. On Approve,
|
||||
> protected stack code does every step, not an agent and not the user: it records the grant, makes the
|
||||
> key or token, installs it on the machine or creates it on the service, and logs each step. On Deny
|
||||
> nothing is made, and the denial is logged. The grant methods are:
|
||||
> - machine key: a key for one agent on one enrolled host, with the command allowlist of REQ-CRED-4. A
|
||||
> host's identity comes from the host side when the user approves its enrollment, never from a first
|
||||
> connection. A host user with sudo receives a grant only after the user approved that user by name;
|
||||
> - Gitea: a token for one agent, and for an agent seat its own Gitea user (Q190), with an expiry. The
|
||||
> all scope and the admin scope are never granted;
|
||||
> - GitHub: a grant for one agent.
|
||||
>
|
||||
> The Gitea and GitHub rights units come in a later design that Jason approves.
|
||||
>
|
||||
> The approval binds the exact rights the user saw and the target or its enrolled identity. A change to
|
||||
> the agent, its definition, the target, the target's identity or the rights after the raise voids it. An
|
||||
> approval is used once. A grant the code couldn't finish is undone at once on the stack's side, so it
|
||||
> is never used. What it left on the host or service stays visible as pending until it is removed, as
|
||||
> for a revoke (REQ-GRANT-4). A new grant needs a new approval, never an automatic retry. The grant
|
||||
> code runs only on a request the user resolved. A request resolved by an arbiter, the tester or any
|
||||
> agent makes nothing. A `credential.mint` decision raised before version 1.8, or without the fields of
|
||||
> REQ-GRANT-1, makes nothing when it is approved. Until the Console
|
||||
> knows the signed-in human (Vikunja #412), Approve is `mosaic decide` in the CLI (REQ-DEC-3).
|
||||
> - **REQ-GRANT-3.** The user enters their own personal access tokens and API tokens in the WebUI, or the
|
||||
> CLI. Only the grant code reads them, to make grants. No agent ever sees a token value or a key's
|
||||
> private half. Values appear in no event, log, message, prompt or agent file, and no agent can read
|
||||
> them back (REQ-VAR-2). Until the Console knows the signed-in human, they are entered through the
|
||||
> CLI.
|
||||
> - **REQ-GRANT-4.** A grant can be revoked at once by the user, by the assistant for a grant in its own
|
||||
> organization, or by the tester on a dev instance (Q173). A revoke needs no decision and no new trust.
|
||||
> It takes effect at once on the stack's side, without waiting on the network, so the next use is
|
||||
> refused. Removing the key or token at the host or service follows. Until that removal is done it stays
|
||||
> visible as pending. A revoke while the grant is being made stops it, and nothing it made stays usable.
|
||||
> Every revoke is logged with who made it.
|
||||
|
||||
REQ-CRED-1, appended:
|
||||
|
||||
> Since version 1.8, a token or key for a grant is made by the grant code on the user's Approve
|
||||
> (REQ-GRANT-2), not by hand. Tokens that exist stay as they are.
|
||||
|
||||
REQ-WAIT-3, appended:
|
||||
|
||||
> `host.sudo` was added as a hard wait and routes under REQ-CRED-4 since version 1.8.
|
||||
|
||||
Under "Risks and open questions", a new bullet:
|
||||
|
||||
> - Trusted mode lets an agent run its allowed commands at will. On another host, an allowed command that
|
||||
> writes a file as root or starts a shell can plant a key, and the seat sandbox doesn't confine the
|
||||
> network, so root can outlive the grant and a revoke. The allowlist narrows this. A seat sandbox with
|
||||
> its own network namespace closes it. Until then the host's own audit log is the check after the fact
|
||||
> (row 210 design section 6).
|
||||
|
||||
## Notes for the reviewer and for Mos
|
||||
|
||||
- REQ-TRUST-1 restates what REQ-DEC-2 already does for within-role actions ("routine and within-role
|
||||
decisions are logged") and names it as the default mode. It grants nothing new by itself. What widens
|
||||
is the role files, one reviewed commit at a time.
|
||||
- The hard waits stay with the user (mos-1117). REQ-GRANT-1 makes the grant request a `credential.mint`
|
||||
decision, already a hard wait under REQ-WAIT-1, so Approve is the user resolving it. REQ-WAIT-2 stands:
|
||||
no role file grants `credential.mint`. The grant code acts on the user's resolution, not on a role
|
||||
grant. A grant that makes an account, such as a seat's Gitea user under Q190, is the same one
|
||||
`credential.mint` decision. So 1.8 doesn't change the hard-waits list (mos-1124 a, mos-1129 N4).
|
||||
- Manual mode: Sage asked who sets it, where it lives and what it changes. Mos accepted these choices
|
||||
(mos-1124 b), and Jason can change them: a list of actions in the organization file, empty by
|
||||
default, changed only by the user, outside the chief's organization writes. A list rather than one
|
||||
switch follows Gizmoduck's #436 draft section 7 (gizmoduck/machine-grant-design f6b6a04b6), so Jason
|
||||
can make host commands manual without making every action manual. Listing every within-role action
|
||||
gives the whole-organization manual mode. Row 210's per-call `host.run` decision, bound to host, argv
|
||||
and stdin and spent once, is this mode's decision for a host command (Q188, Mos's reading).
|
||||
- Gizmoduck's draft proposes REQ-CRED-5 (section 8) for the machine grant. Its content is in REQ-GRANT-1
|
||||
to REQ-GRANT-4 here, generalized to all three methods, so 1.8 has no REQ-CRED-5. Their bounds in
|
||||
gizmoduck-0062 items 4a to 4g are in the text: the chief raises only for an allowed definition, the
|
||||
approval binds the rights shown, it is used once with no automatic retry, no value is ever seen, revoke
|
||||
is at once on the stack's side with the host or service removal visible until done, every grant has a
|
||||
rotate-by or expiry date, and the host identity comes from the host side. Duckworth has reviewed their
|
||||
design r1 to r4 (duckworth-0056 and duckworth-0060), so a later change there may need a change here.
|
||||
- B1 (duckworth-0057, mos-1129): a grant the code couldn't finish is undone at once on the stack's side.
|
||||
What it left on the host or service stays visible as pending until it is removed, as for a revoke.
|
||||
- B2 (duckworth-0057, mos-1129): Mos chose Duckworth's second option. Only an Approve creates a grant,
|
||||
and the code uses a grant only when it has a matching Approve record. A grant written by hand makes
|
||||
nothing. Jason's Q188 gives the reason: "Disallowing the agents to create grants autonomously is a
|
||||
necessary evil for better security" and "Upon approval, the grant is created programmatically." Row
|
||||
210's hand-written grants stay a test path until #436a moves `host.run` to grants made by Approve. r2
|
||||
and r3 had a hand path in the business file, and r4 takes it out, with r3's load check on hand grants
|
||||
(duckworth-0058 N1) and the note that a revoke never rewrites the user's file (duckworth-0058 N2).
|
||||
The same-uid write to the stack's own store stays the B7 residual, and the top of this draft names it
|
||||
for Jason.
|
||||
- C1 (duckworth-0058, sage-1538): `account.create` is the hard wait for any new external account, not
|
||||
only a grant (REQ-WAIT-1, `vocabulary.mjs:43-46`, `roles/pm.md:55`). It stays as version 1.7 has it.
|
||||
Only `credential.mint` is chief-only, and a grant that makes an account is a `credential.mint`
|
||||
decision (N4), so a PM's sign-up for a service routes as before.
|
||||
- Gizmoduck's machine-grant r2 (eb4c756a1, gizmoduck-0067): enrollment is a request the user approves
|
||||
and the stack runs (mos-1120), and a host user with sudo is enrolled by name, the user's Approve
|
||||
covering that user (mos-1125). REQ-GRANT-2's machine key says both. Their design r3 and r4 (1c5705d6d
|
||||
section 6) give revoke to the user, the assistant in its own organization and the tester on a dev
|
||||
instance, as REQ-GRANT-4 does (mos-1124 d).
|
||||
- N1 (duckworth-0057, mos-1129): the approval binds the target or its enrolled identity.
|
||||
- N2 (duckworth-0057, mos-1129): a revoke during creation stops it, and nothing it made stays usable.
|
||||
- N3 (duckworth-0057, mos-1129): landing step 6 lists the role contract updates (`roles/pm.md:55`,
|
||||
`roles/chief-of-staff.md:75`). A `credential.mint` decision raised before 1.8, or one without the
|
||||
grant fields, makes nothing when it is approved.
|
||||
- N4 (duckworth-0057, mos-1129): a decision carries one action (`a.action`, broker.mjs:1678 on
|
||||
origin/refactor). A grant that makes an account is one `credential.mint` decision: the method in the
|
||||
grant says it makes an account, and the inbox shows the account name before Approve. The user clicks
|
||||
once, and there is no separate `account.create` action for it. Gizmoduck's machine-grant r2 reads it
|
||||
the same way.
|
||||
- N5 (duckworth-0057, mos-1129): `host.sudo` keeps the arbiter route version 1.7 set for
|
||||
`git.push.remote`. A raise for it that cites the spec and isn't within-role goes to the arbiter for
|
||||
its domain (broker.mjs:1677-1678 on origin/refactor), and the grant and its allowlist still limit the
|
||||
command. In manual mode the decision goes to the user. The top of this draft names this for Jason as a
|
||||
change in who decides a root command on another host.
|
||||
- N6 (duckworth-0057, mos-1129): the broker enforces only that the chief of staff raised a grant request.
|
||||
The rest of the chain of command is the messaging lists and the role contracts (step 6 of "When this
|
||||
version lands"). REQ-GRANT-1 says so.
|
||||
- Approving a grant doesn't by itself let a command run without a click. That needs REQ-CRED-4 and
|
||||
`host.sudo` in the routed lists, step 5 of "When this version lands" (gizmoduck-0062 item 4i).
|
||||
- Which roles may request: Q188 says "Not all agents are capable or authorized to request directly. It
|
||||
must follow the chain of command properly." The text makes the chief of staff the only role that raises,
|
||||
the PM and the CTO ask it directly, and worker roles ask through their PM. Jason's words also name "the
|
||||
main agent", but REQ-ASSIST-1 keeps the assistant out of directing agents, so it passes the user's
|
||||
request to the chief rather than raising one. Mos accepted this (mos-1124 c) and reads "the main agent
|
||||
or chief of staff" as one role under two names. That reading is Mos's, so the top of this draft puts it
|
||||
to Jason at approval.
|
||||
- REQ-CRED-4 keeps row 210's conditions (origin/gizmoduck/row210-on-199-r2,
|
||||
agents/gizmoduck/work/queue-210/DESIGN.md:376-392): the definition declares the kind, the grant names the
|
||||
agent, an exact argv-prefix allowlist with wildcards refused, the key issued, and only a reviewed role
|
||||
change adds the action. Row 210 put the grant in a hand-edited business file, which stays a test path until
|
||||
#436a. Under 1.8 the grant is what Approve records, so "the business file grants that agent that host"
|
||||
becomes "a grant names that agent and that host".
|
||||
- Revoke: Q188 says "you, Mos or the tester". Mos is Jason's assistant (REQ-ROLE-4), so the text says the
|
||||
assistant, limited to grants in its own organization, and the tester only on a dev instance (Q173).
|
||||
Mos accepted this with those limits (mos-1124 d).
|
||||
- Approve in the Console needs the Console to know the signed-in human (Q175 phase 2, #412). Until then
|
||||
Approve is `mosaic decide`, which REQ-DEC-3 already allows (mos-1124 e). This draft doesn't take
|
||||
REQ-WEB-2, which stays with the Console decide buttons amendment (Vikunja 19/266). That amendment, or a
|
||||
later version, moves Approve into the Console.
|
||||
- Roles already use a scoped Gitea token (`<read|write>:<category>`, all and admin never granted,
|
||||
`packages/business/src/vocabulary.mjs:105-106` on origin/refactor). It is a likely Gitea unit, not a settled
|
||||
one. Only the machine key is designed so far. Gitea and GitHub are a later #436 draft, and minting a Gitea
|
||||
token needs an admin credential the user enters, which is a design of its own (Gizmoduck's draft section
|
||||
11). GitHub's rights unit isn't settled. So the text leaves both units to a later design Jason approves
|
||||
(mos-1124 f), and keeps only the bounds: an expiry, and never the all or admin scope. The PRD names outcomes
|
||||
and checks, not the grant code's steps, as REQ-CRED-3 does.
|
||||
- Jason expects the inbox Approve and the Gitea and GitHub grants by 10-27 (Q188 point 6). That is a
|
||||
delivery date, so it is in the source and not in the requirement text.
|
||||
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
|
||||
paragraph never edited in place except to append.
|
||||
@@ -0,0 +1,149 @@
|
||||
# Row 236 build notes: #436b Gitea grant path (#1742, schema 3j-3k)
|
||||
|
||||
Builder Megavolt, reviewer Dewey, gate owner Sage. Draft; the results sections are filled in after the
|
||||
runs on Sage's gate.
|
||||
|
||||
## Sources
|
||||
|
||||
- Brief: `docs/plans/2026-10-20_agent-credentials.md`, "Brief: #436b Gitea grant path, build (schema 3j-3k)",
|
||||
blob 9259ba2a1.
|
||||
- Design: `agents/megavolt/work/436b/DESIGN.md` r5 at 1be535aca (sha256 68df02cd…f4), unedited. Scope:
|
||||
sections 2 to 7, and section 9's X-G1 to X-G15 and mutants. Section 10 (GitHub half) is out of scope.
|
||||
- Build carry: `agents/megavolt/work/436b/BUILD-CARRY.md` at 3330c901b (N9, N10).
|
||||
- Schema label 3j-3k per sage-2237. If 264a or 206 lands first, this rebases onto a new branch name and reruns
|
||||
the whole bus suite, the schema.test counts and the T46 lists.
|
||||
- Rebases: onto 86f95bb27 after row 273 (sage-2403, no conflicts), then onto 0fa7bf421 after row 191
|
||||
(sage-2452, sage-2457). The 191 rebase took three unions in commit 1, 191's `processing` first and then the
|
||||
grant options: the Broker constructor (broker.mjs:317), the runtime's Broker options (runtime.mjs:173) and
|
||||
the cli case line in `scripts/mosaic` (191's `halt|resume|processing|` plus `grants`).
|
||||
|
||||
## What changed
|
||||
|
||||
- `packages/bus/src/gitea-grant.mjs` (new): the grant body check, the admin slot (set, rotate, remove), the
|
||||
admin and seat API calls, the start-time scan (`giteaStart`, `openGiteaGrants`, `tokenUnknown`).
|
||||
- `packages/bus/src/broker.mjs`: the gitea executor (steps 0 to 8), settle, restart reconcile, revoke and fire,
|
||||
sweep, status, and the `grants.*` verbs.
|
||||
- `packages/bus/src/credentials.mjs`: `held` pairs at start; the register and claim paths the executor uses.
|
||||
- `packages/bus/src/migrations/3j-3k.sql`, `schema.sql`, `store.mjs`: `gitea_users` and `gitea_grants`, new
|
||||
tables only.
|
||||
- `packages/bus/src/runtime.mjs`, `server.mjs`, `human-cli.mjs`: wiring of the start scan and the verbs.
|
||||
- `packages/cli/src/grants.mjs` (new), `cli.mjs`, `scripts/mosaic`: `mosaic grants`.
|
||||
- Tests: `gitea-grant.test.mjs`, `gitea-migration.test.mjs`, `gitea-grant-docker.test.mjs` (X-G3),
|
||||
`fake-gitea-api.mjs`, `fixtures/schema-3j.sql` (02da6a2ae's schema.sql), `cli/tests/grants.test.mjs`, and the
|
||||
pin updates in the migration tests, `schema.test.mjs`, `channel-proof.test.mjs` and `stack-release.test.mjs`
|
||||
(T46).
|
||||
- READMEs of bus and cli.
|
||||
|
||||
## Choices and assumptions
|
||||
|
||||
Each is a reading of the design where it left room. Dewey may rule otherwise.
|
||||
|
||||
1. Admin rotation is due once today is past `rotateBy` (the date itself is still good).
|
||||
2. A seat-token rotation moves the record at step 8, after the old token's delete (step 7).
|
||||
3. A step 7 failure leaves the grant applied, with the old id in `serviceLeft` and the notice "The previous
|
||||
token X is still live".
|
||||
4. A failure after the commit step's rename returns `settle-pending`; the next start settles it.
|
||||
5. tester-cli keeps its 5 s timeout; the grant verbs answer at once and the executor runs after.
|
||||
6. A revoke of a token-unknown agent takes both ids from that decision's last commit step.
|
||||
7. A user adopted at restart is upserted in the same transaction as its `grant.failed interrupted`.
|
||||
8. `#giteaUnlink` (first-grant commit undo) uses a temporary reference dated `9999-12-31`, so it is never due
|
||||
while it lives.
|
||||
9. X-G12's org-team case: the fake models team membership as collaborator reach on the repository, which is
|
||||
what `/user/repos` shows (F11, F12).
|
||||
10. X-G15's look-alikes are stored under `action.refused` and under a non-grant kind, since the 3j trigger
|
||||
refuses the human's NULL pair on `action.allowed`. The trigger cases of X-G13 (session, NULL pair,
|
||||
`grant.host.enrolled`) are in `grant-migration.test.mjs`.
|
||||
11. Step 6 faults: a pre-rename failure is injected by making `gitea.tmp` a directory; a step 8 store fault by
|
||||
a TEMP trigger on `gitea_grants`.
|
||||
12. The credentials-verb revoke close is tracked, so `giteaIdle()` waits for it.
|
||||
13. `openGiteaGrants` gives `agent: null` for a spend with no step yet, so `status.running` can show a null
|
||||
agent; the CLI prints it empty.
|
||||
14. N9: a restart ignores a settle-pending commit with a `grant.revoked` after it and ends it `revoked` with
|
||||
no message (notify false). A 404 on a delete by id counts as done.
|
||||
15. N10: `serviceLeft` is a list in every body and answer.
|
||||
16. A restart claims the agent's key for each open grant it undoes. One executor runs per agent
|
||||
(grant-busy), so a killed child leaves at most one open grant per agent. If a store ever held two, the
|
||||
second's claim fails and the undo runs with no admin: its token is left and named in `serviceLeft`,
|
||||
never deleted unclaimed.
|
||||
17. X-G7 changes the binding with a fixture decision whose hash differs (row 234's G9 pattern), since the
|
||||
broker keeps its own copy of the business. The admin url check takes row 199's `parseGiteaUrl`, which
|
||||
accepts the origin with or without a trailing slash.
|
||||
18. r2, Dewey B1 (dewey-0781): step 8 and both settle branches (forward, back) check for a `grant.revoked`
|
||||
of the agent after the decision's raise, in the same transaction as their write. A revoke that came
|
||||
while step 7's or the settle's delete awaited took the record and both tokens, so the grant ends
|
||||
`revoked` with the human's notice, as a revoke of a running executor ends it (X-G11), and no record is
|
||||
written back. N9's restart case stays silent: a restart never settles a revoked rotation.
|
||||
19. r2, Dewey B2: a sweep retry of `tokens` or `login` checks the user's id first, as the revoke close does,
|
||||
against the `gitea_users` row (it survives a fire). A 404 skips with `gitea-user-gone`, a 200 with
|
||||
another id with `gitea-user-foreign`, any other status leaves the entry. Skips go in `grant.swept` and
|
||||
the sweep answer as `skipped: [{agent, user, id, reason}]`; a skipped entry is not left, so the next
|
||||
sweep does not retry it.
|
||||
20. r3, Dewey N5 (dewey-0783): the settle's 401 branch and its grant-token-unknown branch take the same
|
||||
revoke check. A revoke during the whoami ends the grant `revoked`, not `grant-lost`, and the
|
||||
token-unknown branch neither holds the agent again nor writes `grant-token-unknown`, so no start
|
||||
names a file the revoke unlinked.
|
||||
21. sage-2453, ruling (a) on megavolt-0123: 191's `#processingGate` refuses every agent verb off its read list
|
||||
while processing is halted. `grants.status` is a read like `hosts.status`, and the kill-switch plan
|
||||
(`docs/plans/2026-10-20_kill-switch.md`:16) says "Reads and the human's own verbs, including the clear,
|
||||
still work", so it joins the list. This touches row 191's list (one entry). An agent's `grants.revoke`
|
||||
stays refused while halted, as `hosts.revoke` does.
|
||||
|
||||
## Named for DEFERRED (r2, Dewey N1-N4)
|
||||
|
||||
None is a small local change, so none is in r2. sage-2388: all four DEFERRED; N3 and N4 get a bus README
|
||||
sentence each in r3 (a left admin token is deleted by hand at Gitea; a running executor keeps the admin
|
||||
api it started with).
|
||||
|
||||
- N1: an admin rotate cut between the mint and the slot write leaves a token at Gitea that no event names.
|
||||
A `grant.admin rotate-begin {oldName}` before the mint would close it; that is a new event shape.
|
||||
- N2: `adminTokenLeft` is `unknown` when `/api/v1/token` gives no name. Which 1.27 F-fact shows the name
|
||||
needs a check against the container, not a code change.
|
||||
- N3: admin set or remove records `adminTokenLeft: null`, so a left token drops out of status. Carrying it
|
||||
forward lets `rotate --finish` delete by name under a slot of another account or url; refusing remove
|
||||
while one is left blocks the human's remove when Gitea is down. Either needs a ruling.
|
||||
- N4: rotate or remove while a gitea claim is held. Refusing it blocks the human's remove during a run;
|
||||
the running executor keeps the api it began with. Needs a ruling or a line in the README.
|
||||
|
||||
## Q188
|
||||
|
||||
No test creates a grant except through fixtures or the human's yes on a scratch store. No test reaches
|
||||
git.mosaicstack.dev, tasks.woltje.com or any provider. Unit tests use the fake Gitea on 127.0.0.1; X-G3 uses a
|
||||
throwaway gitea/gitea:1.27.1 container from the local image (never pulled), removed after. Every token is a
|
||||
fake or a scratch-container token; values are compared with `assert.ok`, never printed.
|
||||
|
||||
## Runs
|
||||
|
||||
(filled in after Sage's CLEAR)
|
||||
|
||||
## Mutants
|
||||
|
||||
(section 9's list, each with the test that kills it; filled in after the runs)
|
||||
|
||||
A static pass over the 39 mutants (all apply, problems=0) found five with no test to kill them. Four tests
|
||||
were added before any mutant run:
|
||||
|
||||
- M8, M9 (user or mint step written after its call) and M16 (grant.result written before its call): "X-G6
|
||||
and X-G8: the user and mint steps are written before their calls, and each result after its call". It
|
||||
reads the store from the fake while user-create and token-create are at Gitea, and checks every result's
|
||||
status.
|
||||
- M29 (fork parent not checked): "X-G4: a fork by the seat user of another repository than the project
|
||||
refuses fork-foreign".
|
||||
- M28 (memory and file revoked after the Gitea delete): "X-G11: a revoke unloads memory and removes the file
|
||||
before its Gitea delete", which holds the DELETE open and reads the file and state.
|
||||
- M37 (evidence without its kind filter): "X-G15: a look-alike spend under another kind with the grant writer
|
||||
pair is not an open grant".
|
||||
|
||||
r2 tests, each red on 8bf442be9 by reading:
|
||||
|
||||
- B1: "X-G11: a revoke during a rotation step 7 ends it revoked, and step 8 writes no record back" and
|
||||
"X-G11: a revoke while a sweep settles a rotation, forward or back, ends it revoked with no record".
|
||||
On 8bf442be9 step 8 and the settle write the record and `grant.applied` (forward) or `grant.failed
|
||||
interrupted settled:back` after the revoke deleted it.
|
||||
- B2: "X-G14: a sweep retries a fire only for the user it fired: a remade or gone user is skipped and
|
||||
named". On 8bf442be9 the retry fires the remade user (token list, delete, PATCH) and has no `skipped`.
|
||||
- N5 (r3, sage-2388): "X-G11: a revoke during a settle whoami that finds a foreign token ends it revoked, not
|
||||
held" and "X-G11: a revoke during a settle whoami that gets a 401 ends it revoked, not lost". On 77dd43c9e
|
||||
they end `grant-token-unknown` (the agent back in tokenUnknown) and `grant-lost`.
|
||||
- Choice 21 (sage-2453): "kill switch (plan :16): while halted the chief still reads grants.status, and an
|
||||
agent grants.revoke refuses". M38 drops `'grants.status'` from the read list; the chief's status then
|
||||
refuses `processing-halted` and the test fails.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Row 254 build: library sync follow-up (C2-03a deferrals)
|
||||
|
||||
Row 254, #1759, reviewer Dewey, gate owner Sage. Brief: docs/plans/2026-10-21_library-sync-followup.md
|
||||
(0de97f429), for the deferrals Dewey named in the row 222 approval (#1723 comment 30138). Built on
|
||||
origin/refactor c3d88840c, which carries row 222 (0b33f5e9d) and row 196 (7247d80c7). Launch: sage-2163.
|
||||
|
||||
## What it does
|
||||
|
||||
1. **Client timeout.** `packages/bus/src/human-cli.mjs` gives `library.sync` a 130 s client timeout. Every
|
||||
other verb keeps Client's 5 s default (`timeout: undefined` takes the default). The server holds a library
|
||||
verb's socket for 120 s (`packages/bus/src/server.mjs`, the `socket.setTimeout(120_000)` branch), and the
|
||||
fetch alone may take 100 s (GIT_LIMITS). So the client waits past the server and gets the server's own
|
||||
answer, including its `request-timeout`, instead of giving up first.
|
||||
- The Console has no library route, so there is no Console timeout to change (brief item 1, "when one
|
||||
exists"). When a route is added, note that webui `humanCall` kills its child at 30 s by default. The
|
||||
route would need `timeoutMs` above 130 s for library.sync.
|
||||
- `tester-cli.mjs` is unchanged: library.sync is human-only, so the tester gets `human-required`.
|
||||
2. **STATUS codes (215 N3).** `packages/webui/src/bus.mjs` maps six codes:
|
||||
|
||||
| Code | Status | Why |
|
||||
|---|---|---|
|
||||
| library-sync-busy | 409 | a sync is running; read status again |
|
||||
| library-diverged | 409 | held for a human accept |
|
||||
| library-accept-stale | 409 | the tip moved; read status again |
|
||||
| library-accept-invalid | 400 | a malformed accept |
|
||||
| library-credential-missing | 503 | the remote can't be reached, like no bus to ask |
|
||||
| library-fetch-failed | 503 | the same |
|
||||
|
||||
Neither 503 is 502, the default for an unknown code, so dropping any one entry changes its status and the
|
||||
test catches it.
|
||||
3. **README (222 N4).** `packages/library/README.md` Sync section: the transport timeouts, that a client
|
||||
timeout leaves the sync running, that a second call gets `library-sync-busy`, and that `library.status`
|
||||
shows the result. It also says the Console has no library route yet.
|
||||
- Correction to row 222's BUILD.md, "What it does": "`webui/src/cli.mjs`: the Console gets the call" was
|
||||
not true. `cli.mjs` builds `librarySync` and passes it to `startServer`, and `serve.mjs` never reads it
|
||||
(checked on c3d88840c: no library reference in serve.mjs).
|
||||
|
||||
## Channel proof re-pin
|
||||
|
||||
`packages/bus/tests/channel-proof.test.mjs` pins both changed files by hash. The test change is two baseline
|
||||
values and a header comment, nothing else:
|
||||
|
||||
| File | Old | New |
|
||||
|---|---|---|
|
||||
| packages/bus/src/human-cli.mjs | 269c3a66008c0d57… | 9c9af66b98ea0a490c9a880fc81c3c62414a8ee0b5ba6478608771b8c4f87932 |
|
||||
| packages/webui/src/bus.mjs | 934ab2f9b6bde72a… | e69d17d4385a4d190d15373ed1a26e270a5c6906eec9e61fcc2bb6b05849cabb |
|
||||
|
||||
human.mjs and transport.mjs are unchanged, and so are the entry-path test and the plug-in import ban. No
|
||||
other test pins either file: `git grep` for both old hashes finds only queue.json history and old packets,
|
||||
and the one 64-hex string in webui service.test.mjs is a made-up UID.
|
||||
|
||||
## Dewey's other 222 notes
|
||||
|
||||
None is a few lines in a file this row changes, so all three stay deferred:
|
||||
- N1: a close waits on every entry left in a pass (runtime.mjs).
|
||||
- N2: a held tip still gets its tree read, and a refused read hides the divergence (sync.mjs).
|
||||
- N3: held states log every tick (runtime.mjs).
|
||||
|
||||
## Tests
|
||||
|
||||
- `packages/webui/tests/library-status.test.mjs` (new): each of the six codes maps to its status, and an
|
||||
unknown code is 502.
|
||||
- `packages/bus/tests/library-sync.test.mjs`, a new test. A stand-in bus socket answers every request after 6 s,
|
||||
and the real human-cli.mjs runs two requests in parallel. library.sync gets the answer, and library.status
|
||||
gets `outcome-unknown` at 5 s. It takes about 6 s.
|
||||
- `packages/bus/tests/channel-proof.test.mjs`: the new baselines.
|
||||
|
||||
Planned mutants on Sage's slot:
|
||||
1. Drop the human-cli timeout line: the library-sync test fails (sync gets outcome-unknown) and the
|
||||
channel-proof hash fails.
|
||||
2. Drop one STATUS entry (`library-fetch-failed`): library-status test fails, and so does the channel-proof hash.
|
||||
|
||||
Both files are hash-pinned, so channel-proof kills any edit to them. Each mutant is also expected to be killed
|
||||
by its behaviour test, and the run lists every failing test so that can be checked.
|
||||
@@ -0,0 +1,610 @@
|
||||
# Row 281 build notes: a fire or a delete revokes the instance's grants (#1786)
|
||||
|
||||
Builder Megavolt, reviewer Dewey, gate owner Sage. Design draft d6 for Dewey's approval. No code until it is
|
||||
approved. The results sections are filled in after the runs on Sage's gate.
|
||||
|
||||
History: d1 dfebd16cc. d2 c746ce378 added Sage's Q1/Q2 rulings (sage-2573). d3 36c2d98b8 added the
|
||||
approved-before-fire tests (sage-2574). d4 answers Dewey's CHANGES on d3 (dewey-0823):
|
||||
- B1: the set takes every grant decision with no outcome, and a raise refuses a fired or firing agent.
|
||||
- B2: step G runs detached from the verb, so neither caller waits on the network.
|
||||
- B3: one step G per agent, with named refusals for everything that could race it.
|
||||
- N1-N3 are folded in.
|
||||
|
||||
d5 adds sage-2575: two requests per granted delete, with its five conditions. It also adds a `job` id to every
|
||||
answer, event and message, and tests T3, T3b and T3c with mutants M16-M18.
|
||||
|
||||
d6 answers Dewey's CHANGES on d5 (dewey-0825):
|
||||
- C1: the job's step C records authority without the chief's claim (`#authorityFor`, as a landing's record
|
||||
does), processing-halted doesn't hold the job, and a step B or C failure records `fire.failed {job, step,
|
||||
code}` with its own message. Tests T11 and T12, mutants M19 and M20.
|
||||
- N4: boot closes a job a restart interrupted, with one message. Test T13, mutant M21.
|
||||
- N5: section 7 names the refusals the manual verbs gain.
|
||||
- N6: the set counts Q1 leftover lines, in fire step 3 and in the delete text.
|
||||
|
||||
## Sources
|
||||
|
||||
- Brief: `docs/plans/2026-10-22_fire-revokes-grants.md`, "Brief: no grant outlives its instance" (sage-2568,
|
||||
mos-1271, Vikunja 524).
|
||||
- Code read at 47a35bf43: `packages/bus/src/broker.mjs`, `runtime.mjs`, `credentials.mjs`, `host-grant.mjs`,
|
||||
`gitea-grant.mjs`, `packages/business/src/instance-settings.mjs`, `tools/host/mosaic-keys-write`.
|
||||
- Row 256's delete (honker/row256-r2 at 1e761962a, not landed) changes only the delete's checks. Its write still
|
||||
goes through `lifecycleDelete` and the same `revoke()`, so this design covers it unchanged.
|
||||
- Row 235 r6 section 6 (host revoke) and #436b r5 section 6 (Gitea revoke and the fire steps).
|
||||
|
||||
## 1. Why: what happens today (read from the code, to be pinned by a test before the change)
|
||||
|
||||
In short: today a delete of a granted instance unlinks the key files but leaves both grant records, the host
|
||||
lines and the Gitea token live, and a fire refuses `credential-revoke-failed` (sage-2573 asked for this here
|
||||
and on #1786). The details:
|
||||
|
||||
- **Fire.** `revoke()` (runtime.mjs:151) revokes only the roles entry's `credentials` services, then lists the
|
||||
agent's store directory and refuses `credential-revoke-failed` if any file is left. A host grant's key is
|
||||
`<store>/<business>/<agent>/host.<id>` (host-grant.mjs:102), and a Gitea grant's token is `.../gitea`
|
||||
(gitea-grant.mjs:379). Neither is a roles-entry service, so a fire of an instance with a grant refuses at
|
||||
step B with "host.<id> still in the store". It doesn't leak, but it can't finish until a human runs
|
||||
`mosaic hosts revoke` or `mosaic grants revoke` by hand. If the business file also names `gitea` as a
|
||||
roles-entry credential, `credentials.revoke(instance, 'gitea')` unlinks the granted token file with no
|
||||
`gitea_grants` change and no Gitea call.
|
||||
- **Delete.** `credentials.remove(instance)` (credentials.mjs:400) revokes every registered reference,
|
||||
`host.<id>` and `gitea` included, and unlinks the directory. The `host_grants` and `gitea_grants` records
|
||||
stay, the host line stays on each host, and the Gitea token, user and login stay live. This is the leak.
|
||||
The next start then finds a grant record whose key file is gone. What the start does with that isn't read
|
||||
yet. The T1 test below pins it.
|
||||
- **An open grant decision.** A fire delists the instance but keeps its roles entry. An approved host or Gitea
|
||||
grant decision for that agent that hasn't run yet still passes `#hostDefinition` and `#giteaRole`, so it can
|
||||
still apply after the fire. A delete removes the roles entry, so its executor refuses `unknown-role` or
|
||||
`gitea-not-declared`.
|
||||
- **A raise after the fire** (dewey-0823 B1). `#raiseGrant` (broker.mjs:1529) checks only that the caller is
|
||||
the chief, and the grant bindings check only the roles entry. So the chief can raise a new grant for a fired
|
||||
agent, and if the human approves it, it applies.
|
||||
|
||||
## 2. Where the grant revokes sit (D1, rewritten in d4 for B2)
|
||||
|
||||
d3 awaited step G inside the verb. Dewey's B2 showed that neither caller can wait that long:
|
||||
|
||||
- **Delete.** The launcher's `host.op` waits `REQUEST_TIMEOUT_MS = 10000` (cli/src/host.mjs:31). On a timeout,
|
||||
`breakChannel` stops the whole bus host. One SSH remove can take 30 s (host-grant.mjs:231, :311), and each
|
||||
Gitea call 15 s (`CALL_MS`, gitea-grant.mjs:38).
|
||||
- **Fire.** server.mjs answers a verb that isn't on its async list synchronously (:154), and the async list
|
||||
gives 120 s (:150). The chief's runner client times out at 30 s (harness/src/runner.mjs:449). Two hosts
|
||||
plus the Gitea calls can pass both limits.
|
||||
|
||||
So step G never runs inside a verb. The fire and the delete both use the same answer-at-once job, for the same
|
||||
timeout reason (sage-2575 condition 5). Both verbs stay synchronous and answer at once. Step G runs as tracked
|
||||
broker work after the answer, as row 236's grant executors do (`#track`, `setImmediate`). No caller waits on
|
||||
the network, so `server.mjs`, `process.mjs`, `cli/src/host.mjs` and the launcher don't change.
|
||||
|
||||
Each job gets an id, `job` (a UUID), when it starts. The verb's answer, the job's start event, its end event
|
||||
and every inbox message about it carry that id. The human and the chief can then match a later outcome to
|
||||
the request that started it.
|
||||
|
||||
### The fire
|
||||
|
||||
1. `write('instance.fireCheck')`, as today.
|
||||
2. Step A, as today, with one addition. In the same transaction, after `fire.stopped`, the grant set is read
|
||||
(section 3). Reading it there means a grant raise either committed before step A, and is in the set, or
|
||||
comes after `fire.stopped` and refuses (section 3d).
|
||||
3. If the set is empty, steps B and C run synchronously, exactly as today. A fire with no grant answers
|
||||
today's result with today's events (T7). The set counts everything step G would touch (d6, N6): grant
|
||||
rows, open decisions, the agent's Gitea leftovers (section 5) and its Q1 leftover host lines (section 5).
|
||||
Any of them makes the set non-empty and goes to the job.
|
||||
4. Otherwise the verb takes the agent's step G lock (section 3e), records `fire.revoking {job, grants}` in
|
||||
step A's transaction, starts the job, and answers at once:
|
||||
`{...stopped, job, revoking: ['host:<id>' | 'gitea', ...], done: false}`. There's no `transaction`, since
|
||||
step B hasn't run.
|
||||
5. The job runs step G, then step B (`write('instance.fire')`) and step C (`#recordAuthority`, below, then
|
||||
`fire.done {job, grants}`). It releases the lock when it ends, whichever way it ends, `fire.failed`
|
||||
included.
|
||||
|
||||
What the chief sees meanwhile: the instance is "firing". That's an existing state (`#firing`, broker.mjs:645):
|
||||
`fire.stopped` with no later `fire.done`. Starts already refuse `instance-firing`.
|
||||
- A repeat fire while the job runs refuses `fire-in-progress`, a new code.
|
||||
- A repeat after the job finishes takes the delisted path (webby-0094 N1) and answers done.
|
||||
- A repeat after a refused step G runs step G again (section 5).
|
||||
|
||||
The human sees the outcome either way: `fire.done` names the job and the grants, and a refusal sends the
|
||||
inbox message in section 4.
|
||||
|
||||
**Steps B and C in the job (d6, C1).** d4 left "step C without the chief's run" as a build-time check, and
|
||||
Dewey checked it: it fails.
|
||||
- `#consumeAuthority` calls `#checkAuthority` (broker.mjs:2852). That runs `#processingWork()` and
|
||||
`#agent(s)`, and `#agent` refuses `not-holder` once the chief's run no longer holds its claim (:749).
|
||||
- After the verb answers, the job can take a minute or more. If the chief's run ends meanwhile, step C would
|
||||
throw after step B had delisted. The instance would be left delisted with no `fire.done`.
|
||||
|
||||
So the verb does the full check, and the job only records:
|
||||
- The verb runs `#checkAuthority(s, action, authority)`, as today, before step A. The chief holds its claim
|
||||
then.
|
||||
- The job's step C runs `#authorityFor(s, action, authority)`, the check without the holder test. It then
|
||||
runs the `#spent` check and the `action.allowed` event, as `#consumeAuthority` does. That's the shape a
|
||||
landing's record already uses (`recordPublish`, broker.mjs:3174, row 137): the run that asked held its
|
||||
claim when it asked, and may have ended since. It's one small private method, `#recordAuthority`, shared
|
||||
with nothing else.
|
||||
- The job keeps a copy of the session fields `#authorityFor` and `#event` read (business, role, thread,
|
||||
run). It doesn't keep the live session object.
|
||||
- `processing-halted` doesn't hold the job. A halt stops agent work. Step G only removes access, and steps
|
||||
B and C finish a decision the chief made while processing ran. Holding them would leave a delisted or
|
||||
half-revoked instance "firing" for the length of the halt. The verb itself still refuses during a halt,
|
||||
through `#checkAuthority`, as today.
|
||||
|
||||
**A step B or C failure in the job** gets its own record, not `grant.revoke.refused`, since every grant in the
|
||||
set was revoked by then:
|
||||
- It records `fire.failed {job, step: 'B' | 'C', code}`. `code` is the thrown code, for example the writer's
|
||||
`credential-revoke-failed`, or `decision-consumed`.
|
||||
- It sends an inbox message:
|
||||
|
||||
> Fire of <agent>: job <job> revoked its grants (<list>) but stopped at step <B|C> (<code>). The instance
|
||||
> is still firing. Run the fire again once <code> is cleared.
|
||||
|
||||
- The instance stays firing. A repeat fire finds an empty set and takes today's path for that state, with the
|
||||
chief's own claim: B and C after a step B failure (T12), the delisted path (webby-0094 N1) after a step C
|
||||
failure.
|
||||
|
||||
`grant.revoke.refused` with reason `internal` stays only for an unexpected throw inside step G.
|
||||
|
||||
### The delete (sage-2575)
|
||||
|
||||
`lifecycleDelete` (broker.mjs:3293) stays synchronous. After its running and starting checks, it reads the
|
||||
grant set.
|
||||
|
||||
- **No grant.** An empty set: `instances.set delete`, one request, exactly as today (condition 4, T3c).
|
||||
- **The first request.** For a non-empty set, it takes the lock and records `grants.revoking {job, agent,
|
||||
verb: 'lifecycle.delete', grants}`. It starts the job and refuses `grants-revoking` at once. The launcher
|
||||
closes the request with that code, well inside its 10 s.
|
||||
|
||||
The launcher can carry only the code, so the broker also sends the human an inbox message (condition 1):
|
||||
|
||||
> Delete of <agent> is waiting on its grants. Revoking started (job <job>) for: host:<id>, ..., gitea.
|
||||
> Raise the delete again when the inbox says the revoke is done.
|
||||
|
||||
- **A delete raised while the job runs.** It refuses `grants-revoking` again and starts no second job
|
||||
(condition 2, the 3e lock). No second message goes out either: the first one still applies.
|
||||
- **The job ends.** The human gets one inbox message (condition 3).
|
||||
- If it completed, the message names each grant revoked, with any `grantsLeft` (section 5), then "Raise the
|
||||
delete again." It records `grants.revoked.done {job, agent, verb, grants, grantsLeft}`.
|
||||
- If it refused, the message names the grant, the reason and how to clear it by hand, as section 4
|
||||
describes. It records `grant.revoke.refused {job, ...}`.
|
||||
- **The delete raised after a completed job** finds an empty set and goes as today.
|
||||
- **The delete raised after a refused job** doesn't complete over a live grant. Its set is non-empty, since
|
||||
the refused grant's record is still there. It refuses `grant-revoke-failed`, naming the earlier job's
|
||||
reason in a message:
|
||||
|
||||
> Delete of <agent> refused: job <job> couldn't revoke <grant> (<reason>). Retrying now as job <new>. Raise
|
||||
> the delete again when the inbox says it's done, or clear <grant> by hand first.
|
||||
|
||||
It starts a new job for what's left. The broker knows the last job refused because the agent's latest job
|
||||
end event is a `grant.revoke.refused` with verb `lifecycle.delete`, and no `grants.revoked.done` came after
|
||||
it. A delete after the human cleared the grant by hand doesn't find an empty set if a manual revoke left a
|
||||
line (Q1). The leftover line makes the set non-empty, so that delete starts a job for the line removal
|
||||
and refuses `grants-revoking` (d6, N6, and T3b's second case). If nothing is left, the set is empty and the
|
||||
delete goes on.
|
||||
|
||||
The tradeoff (ruled by sage-2575): deleting a granted instance now always takes two requests. That keeps the
|
||||
cli and the host channel out of row 281, and a delete of an instance with a grant is rare. Holding the
|
||||
launcher's request open past its 10 s would need a host channel change and a strand path, and polling would
|
||||
widen the row into the launcher for a one-time wait.
|
||||
|
||||
While a delete's step G runs, the instance is still listed and not firing, so it could be started. That's
|
||||
harmless: its grants are being revoked, and the repeat delete refuses `instance-running` as today. No new
|
||||
start check.
|
||||
|
||||
### Why not in `beforeWrite` or `revoke()`
|
||||
|
||||
Both are synchronous and run inside the organization writer, after its checks. A host `remove` is SSH and a
|
||||
Gitea revoke is HTTP, so they can't run there. The business package also has no reach into the broker's grant
|
||||
tables, and shouldn't get any.
|
||||
|
||||
### Why after step A
|
||||
|
||||
A fire is the decision to stop the agent. If step G came first, a host that can't be reached would keep the
|
||||
fired agent running. After step A, a refused step G leaves the same state as a failed step B today: the
|
||||
instance is "firing" (stopped, still listed) until a repeat. Row 194 already handles that state. The repeat
|
||||
fire's delisted path (webby-0094 N1) also runs step G, since the set is read before step B on both paths.
|
||||
|
||||
## 3. Step G: what it revokes, and how
|
||||
|
||||
The set, for (business, agent), is read in one store transaction: step A's for a fire, the delete's checks
|
||||
for a delete (section 2). It holds:
|
||||
|
||||
- each `host_grants` row of the agent;
|
||||
- each host named by an open host grant decision for the agent;
|
||||
- the Gitea grant if the agent has a `gitea_grants` row, a `gitea_users` row, or an open Gitea grant decision.
|
||||
|
||||
An open decision (d4, B1) is any grant decision raised for the agent with no outcome yet. That is: not denied,
|
||||
not expired, no `grant.applied` or `grant.failed`, and no `grant.revoked` of the same (agent, host), or of the
|
||||
agent's Gitea, recorded after its raise. A raised decision the human hasn't decided is open. d3 required
|
||||
"approved", which left that one out.
|
||||
|
||||
An empty set means no step G and no new records. A fire of an instance with no grant is then exactly today's
|
||||
fire, with no new events (brief tests, fourth bullet).
|
||||
|
||||
### 3a. A host grant: the host line first (D2)
|
||||
|
||||
hosts.revoke goes local first: the record drops, the key leaves memory and its file, and only then is
|
||||
`remove <user> <tag>` sent. If the host can't be reached, the revoke closes with `hostLineLeft` and the
|
||||
record is already gone. The brief says a fire may not drop a grant record while its key could still be on the
|
||||
host. So the fire's host revoke reverses one step and keeps the rest the same:
|
||||
|
||||
1. Abort an executor in flight for (agent, host), with no record change (`hostKeys.abortClaim`, as the
|
||||
`credential-unknown` branch of `#revokeHost` already does).
|
||||
2. `remove <user> <tag>` through the installer, pinned to the host record's keys. `mosaic-keys-write` says
|
||||
"a missing tag changes nothing", so this is safe to repeat and safe for a host named only by an open
|
||||
decision. If the host record is gone, or the remove doesn't exit 0, step G refuses for this host
|
||||
(section 4). Nothing local has changed for this grant: the record, the key file and the memory copy stay.
|
||||
3. hosts.revoke's step 1 transaction, unchanged: the `host_grants` row goes and `grant.revoked {revoke, agent,
|
||||
host, tag, hostLine: 'pending', granted, reason, by}` is recorded, plus `fire: true` (or `delete: true`)
|
||||
so the human can tell it from a manual revoke.
|
||||
4. hosts.revoke's step 2, unchanged: `hostKeys.revoke`, so the key leaves memory and its file goes.
|
||||
5. `grant.revoke.closed {hostLine: 'removed'}` with no second remove. Step 2 already did it.
|
||||
|
||||
This is one shared method, `#revokeHostGrant(business, agent, host, reason, by, { lineFirst })`.
|
||||
hosts.revoke calls it with `lineFirst: false` and its behavior and records don't change (brief item 4). The
|
||||
fire and the delete call it with `lineFirst: true`.
|
||||
|
||||
The tradeoff: hosts.revoke goes local first so that "the part that matters can't wait on the network" (r6
|
||||
section 6). For a fire, step A has already stopped the agent's runs, and a refused fire keeps the instance
|
||||
"firing". So waiting on the host costs nothing in exposure, and the record never lies about the host.
|
||||
|
||||
### 3b. The Gitea grant (D3)
|
||||
|
||||
The Gitea revoke stays grants.revoke's steps with `fire: true`, which is what `credentials delete` already
|
||||
runs (#436b section 6). Step 1 takes the token out of memory and unlinks the file. Step 2 is
|
||||
`#giteaRevokeLocal(..., fire: true)`. Step 3 is `#giteaCloseRevoke`: every token deleted and
|
||||
`prohibit_login: true`. The fire awaits step 3 instead of leaving it to `setImmediate`.
|
||||
|
||||
The Gitea side can't go remote first the way a host does. The record has to drop and `grant.revoked` has to
|
||||
name the tokens before the DELETE calls, or a restart in between would find a live record whose tokens are
|
||||
gone (section 5's reconcile). Two changes keep the fire closed:
|
||||
|
||||
- **Preflight.** Before step 1, `#giteaAdminChecked` runs. If the admin token can't be read or checked,
|
||||
step G refuses with nothing changed.
|
||||
- **Left means not done.** If `grant.revoke.closed` lists anything in `serviceLeft`, step G refuses. The
|
||||
token ids stay in the evidence, which is exactly what `mosaic grants sweep gitea` retries, so nothing
|
||||
forgets them. The record is gone, but no key of this stack's is still usable: the file and memory copy are
|
||||
gone, and only the Gitea side is left. That is the case the brief's host rule guards against, and it is
|
||||
kept visible here, not hidden.
|
||||
|
||||
**A Gitea executor in flight** (d3 T6c, and B3 seen from the executor's side). Step G's Gitea part goes in
|
||||
this order:
|
||||
1. Record `grant.revoked {granted: false}` for each open Gitea decision (3c).
|
||||
2. Await this agent's tracked Gitea executors and remote undos, if any are running. Each one ends `revoked` at
|
||||
its next `#giteaRevokedSince` check, or it committed `applied` before step 1.
|
||||
3. Read the `gitea_grants` row again and run the steps above.
|
||||
|
||||
`#giteaCloseRevoke` with `fire: true` lists the user's tokens and deletes every one. The list comes after
|
||||
step 2, so it includes any token an in-flight executor created. `#giteaRuns` is a single Set today. It
|
||||
becomes keyed by (business, agent) so step G can await one agent's executors alone.
|
||||
|
||||
### 3c. Open decisions
|
||||
|
||||
A host named only by an open decision has no record. Step 3a still runs `remove` (a no-op) and records
|
||||
`grant.revoked {granted: false}`, as hosts.revoke does for a revoke with no record (N4). The executor's
|
||||
`#revokedSince` then ends that decision `revoked`. The Gitea side is the same through
|
||||
`#giteaRevokedSince`.
|
||||
|
||||
This is the case where a decision is approved before the fire and executed after it (sage-2574). Both checks
|
||||
look for a `grant.revoked` of the agent recorded after the decision was raised (broker.mjs:1156 and :1678 at
|
||||
47a35bf43). A fire's `grant.revoked` is always later than the approval, so the executor ends the decision
|
||||
whenever it runs. The fired agent keeps its roles entry, so `#hostDefinition` and `#giteaRole` still pass, and
|
||||
this check is the only thing that stops the decision. T6 and T6b test it. A delete also removes the roles
|
||||
entry, so the executor refuses there as well.
|
||||
|
||||
The same holds for a decision raised but not yet approved (B1). It's in the set, so it gets its
|
||||
`grant.revoked {granted: false}`. If the human approves it later, its executor ends `revoked` (T6d). The
|
||||
decision stays approvable in the inbox. Approving it does nothing but end it, which `grant.failed` reports as
|
||||
it does for any revoked grant.
|
||||
|
||||
### 3d. A raise for a fired agent (d4, B1)
|
||||
|
||||
`#hostGrantBinding` and `#giteaGrantBinding` both run inside the raise transaction (broker.mjs:1539 and
|
||||
:1548; the host raise's keyscan is awaited before that transaction). Both gain one check. The raise refuses
|
||||
`instance-fired` if the agent is:
|
||||
- firing (`#firing`);
|
||||
- fired, by the writer's own rule (instance-settings.mjs, hire): a roles entry, not in `launch.instances`,
|
||||
and not `launch.by`.
|
||||
|
||||
A rehire lists the name again, so a rehired agent can be granted again.
|
||||
|
||||
Because the check is in the transaction that records the decision, there's no gap. A raise that commits
|
||||
before step A's transaction is in the set. One that commits after it refuses. A raise for an agent whose
|
||||
delete's step G is running refuses `grant-revoke-running` (3e).
|
||||
|
||||
No executor recheck is added. A raise can't commit after `fire.stopped`, and every decision raised before it
|
||||
is in the set and gets its `grant.revoked`.
|
||||
|
||||
### 3e. One step G per agent (d4, B3)
|
||||
|
||||
The broker keeps `#revoking`, a map from (business, agent) to the verb whose step G is running. The verb sets
|
||||
it synchronously, in the same turn as the set read, before it answers. The job clears it in a `finally`.
|
||||
While it's set:
|
||||
|
||||
| Caller | Answer |
|
||||
|---|---|
|
||||
| `instance.fire` | refuses `fire-in-progress` |
|
||||
| `lifecycleDelete` | refuses `grants-revoking`, no second job |
|
||||
| `hosts.revoke` / `grants.revoke` for the agent | refuse `grant-revoke-running` |
|
||||
| grant raise for the agent | refuses `grant-revoke-running` |
|
||||
| `grants.sweep` | skips this agent's entries and names it as `running` in its result |
|
||||
|
||||
Two fires started together (the chief retrying after a client timeout): the first takes the lock before it
|
||||
answers, and the second refuses. The map is in memory. After a broker restart the job is gone and so is the
|
||||
lock. The instance is still firing, or the delete still has a non-empty set, and the repeat starts a fresh
|
||||
step G (section 5).
|
||||
|
||||
A manual revoke can't race step G, since it refuses while the lock is held. Step G's transactions still
|
||||
check what they read: a `host_grants` row already gone at 3a step 3 records `grant.revoked {granted: false}`
|
||||
and goes on, as hosts.revoke does today for a revoke with no record.
|
||||
|
||||
## 4. A refusal (D4)
|
||||
|
||||
Step G stops at the first grant that can't be revoked. Grants already revoked in this step G stay revoked and
|
||||
recorded, and the rest stay as they were. The job doesn't run step B: the organization file is unchanged, and
|
||||
the credentials are unrevoked, since `revoke()` never ran. A fire stays "firing". A delete's request was
|
||||
already closed with `grants-revoking` (section 2).
|
||||
|
||||
The verb has already answered, so the refusal is a record, not a thrown code:
|
||||
- One grant-evidence event, `grant.revoke.refused {job, agent, grant: 'host:<id>' | 'gitea', reason, verb:
|
||||
'instance.fire' | 'lifecycle.delete', by}`.
|
||||
- `reason` is one of `host-unknown`, `host-remove-failed`, `gitea-admin-unavailable` and `gitea-left:<ids>`.
|
||||
- `grant-revoke-failed` is the code in the event and in the inbox message.
|
||||
|
||||
A message lands in the human's inbox, as `grant.failed` does today (broker.mjs:1139). It names the instance,
|
||||
the grant and the reason, with the manual way out beside the retry (N2):
|
||||
|
||||
> Fire of <agent> stopped: its grant on <host> couldn't be revoked (<reason>). Run the fire again once <host>
|
||||
> is reachable. If <host> won't come back, run `mosaic hosts revoke <agent> <host>` first: the grant record
|
||||
> goes, the line is named as left, and the next fire completes and names that line (section 5).
|
||||
|
||||
The delete's message has the same shape, with "raise the delete again". For Gitea, the way out is
|
||||
`mosaic grants sweep gitea` once Gitea is back.
|
||||
|
||||
A job that throws something unexpected inside step G records `grant.revoke.refused` with reason `internal`,
|
||||
sends the same message, and releases the lock. There's no unhandled rejection. A failure at step B or C is
|
||||
`fire.failed` (section 2).
|
||||
|
||||
**A restart mid-job (d6, N4).** The lock and the job are in memory, so a restart ends them with no end
|
||||
record. A fire would stay "firing" until the chief happened to repeat it. A delete's request is already
|
||||
closed, so no one would hear anything. Boot closes that gap:
|
||||
- A job is a start record (`fire.revoking` or `grants.revoking`) whose `job` has no end record. The end
|
||||
records are `fire.done`, `fire.failed`, `grants.revoked.done`, `grant.revoke.refused` and the new
|
||||
`grants.job.interrupted`.
|
||||
- When the broker starts, for each such job of each business, it records `grants.job.interrupted {job,
|
||||
agent, verb}` and sends the human one inbox message:
|
||||
|
||||
> <Fire|Delete> of <agent>: job <job> was interrupted by a bus restart. Some grants may be revoked. Run the
|
||||
> fire again, or raise the delete again. The repeat goes on from what's left.
|
||||
|
||||
- Recording the end record in the same transaction as the message means a second boot finds nothing to
|
||||
send.
|
||||
- Boot reads only events, with no network call, so it adds nothing to the start time beyond one query.
|
||||
|
||||
## 5. A repeat fire (D5)
|
||||
|
||||
A repeat runs the whole fire again: the checks, step A (nothing left to stop), the set read, then step G, B
|
||||
and C in the job. A repeat while the job runs refuses `fire-in-progress` (3e). A repeat delete works the same
|
||||
way through the human's new request.
|
||||
|
||||
- A host grant already revoked has no row and no open decision, so it isn't in the set. It's done.
|
||||
- A host grant that refused still has its row, so it's retried from 3a step 1.
|
||||
- Gitea (d4, N1): if the agent has entries in `#giteaLeft` or `#giteaOpenRevokes`, step G retries only those.
|
||||
It calls `#giteaCloseRevoke` for each, as `#sweepGitea` does per entry. It doesn't run the business-wide
|
||||
sweep, which would retry every agent's entries and settle every open rotation under the chief's name. Any
|
||||
entry still left for this agent refuses `gitea-left`. An entry left by an earlier manual
|
||||
`mosaic grants revoke` of the same agent blocks the fire the same way. That's intended: no grant outlives
|
||||
its instance, whichever verb started its revoke. These entries count toward a non-empty set (section 2,
|
||||
fire step 3), so they go to the job and never run inside the verb.
|
||||
- A host line left by an earlier manual `hosts.revoke` doesn't block the fire (Q1 below). These are the
|
||||
agent's `grant.revoke.closed` records with `hostLineLeft` and no later line removal for that tag. For each
|
||||
one whose host record still exists, step G sends `remove <user> <tag>` once. On success it records
|
||||
`grant.revoke.closed {revoke: <the old revoke id>, hostLine: 'removed', fire: true}`, so a later fire doesn't
|
||||
find it again. On failure it records nothing new and the fire goes on. The host and the tag go in
|
||||
`grantsLeft` (section 6). These leftover lines also make the set non-empty, so the retry runs in the job,
|
||||
inside B2's answer (dewey-0823 accepted the single retry on that condition).
|
||||
|
||||
## Rulings
|
||||
|
||||
- **Q1 (sage-2573):** a leftover line doesn't block the fire. If the host is reachable, the fire tries the
|
||||
removal once, because a fired agent's key line still on a host is the exact risk this row closes. If that
|
||||
try fails, the fire doesn't refuse. It names the host and the leftover line in the result and in the event
|
||||
the human sees. The human accepted that residue when their manual `hosts.revoke` left it, so it shouldn't
|
||||
stop a fire forever on a dead host. Dewey may push back on the retry if it widens step G more than it's
|
||||
worth.
|
||||
- **Q2 (sage-2573):** a delete refused at step G closes its request through the launcher's normal refusal
|
||||
path, and the human raises it again. A request left taken would strand, which is the shape DEFERRED already
|
||||
records for the >64-stops done. Conditions: the refusal names the instance, the grant and the reason (the
|
||||
`grant.revoke.refused` event, section 4). The repeat delete skips grants already revoked, as the repeat fire
|
||||
does. T3b tests both.
|
||||
- **Q2 extended (sage-2575):** every delete of a granted instance takes two requests. The first starts the
|
||||
revoke job and refuses `grants-revoking`, and the human raises the delete again when the inbox says it's
|
||||
done. This keeps the cli and the host channel out of the row, and such a delete is rare. Conditions, each
|
||||
with a test:
|
||||
1. The first refusal tells the human what happened and what's next: the named grants, and to raise the
|
||||
delete again when the inbox says done. A bare code isn't enough (T3).
|
||||
2. A delete raised while the job runs refuses `grants-revoking` again and starts no second job (T3, T9).
|
||||
3. The job's end message names each grant revoked. If one failed, it names the grant, the reason and the
|
||||
manual way out (`mosaic hosts revoke` or `mosaic grants revoke`). The next delete then refuses with that
|
||||
reason and doesn't complete over a live grant (T3b).
|
||||
4. A delete of an instance with no grant completes in one request, exactly as today (T3c).
|
||||
5. Section 2 says the fire uses the same answer-at-once job, and the fire's result and event name the job
|
||||
(T2).
|
||||
|
||||
## 6. The result and the event (D6)
|
||||
|
||||
- **A fire with a non-empty set** answers at once with `job`, `revoking` and `done: false`, and records
|
||||
`fire.revoking {job, grants}` (section 2).
|
||||
- **The job's outcome** is recorded, not returned. `fire.done` (step C) carries `job` and `grants: [{grant:
|
||||
'host:<id>' | 'gitea', revoke, granted}]`, plus `grantsLeft` for any leftover from section 5's last bullet.
|
||||
So the event the human sees names them.
|
||||
- **A delete's job** records `grants.revoking {job, ...}` at the start and `grants.revoked.done {job, agent,
|
||||
verb: 'lifecycle.delete', grants, grantsLeft}` at the end. It sends the inbox messages in section 2.
|
||||
- **A fire with an empty set** carries none of these keys, so its shape is exactly today's.
|
||||
|
||||
## 7. What doesn't change (brief item 4)
|
||||
|
||||
- `hosts.revoke`, `grants.revoke` and `grants.sweep`: same callers and the same records. Their refusals stay
|
||||
the same (`tester-dev-only` included) with one addition from section 3e (d6, N5): while a step G job holds
|
||||
the agent's lock, `hosts.revoke` and `grants.revoke` for that agent refuse `grant-revoke-running`, and
|
||||
`grants.sweep` skips the agent and names it as `running`. hosts.revoke's own order stays local first.
|
||||
- No new verb, no Console route, no CLI change, no schema change. Every new record is an `events` row in the
|
||||
existing grant-evidence shape.
|
||||
- `revoke()` in runtime.mjs and the organization writer don't change.
|
||||
- `server.mjs`, `process.mjs`, `cli/src/host.mjs` and `cli/src/launcher.mjs` don't change. Both verbs stay
|
||||
synchronous. The launcher already closes a request with whatever code `lifecycleDelete` refuses.
|
||||
- New codes: `fire-in-progress`, `grants-revoking`, `grant-revoke-running`, `instance-fired`, and
|
||||
`grant-revoke-failed` in events and messages. New event types: `fire.revoking`, `grants.revoking`,
|
||||
`grants.revoked.done`, `grant.revoke.refused`, `fire.failed` and `grants.job.interrupted`.
|
||||
- The broker's start gains one events query for interrupted jobs (section 4, N4). `lifecycleDone` checks a refusal against `LIFECYCLE_CODE =
|
||||
/^[a-z-]{1,64}$/` (broker.mjs:149), and `grants-revoking` matches it.
|
||||
|
||||
## 8. Tests (each in the owning package, whole files)
|
||||
|
||||
The bus tests use the fake installer and the fake Gitea from rows 235 and 236. They wait on the job with a new
|
||||
trusted test API, `grantsIdle()`, which resolves once every step G job and Gitea executor has ended. It's the
|
||||
same shape as `giteaIdle()`. Unless a test says otherwise, every fire or delete below is awaited that way.
|
||||
|
||||
- T1 (pins section 1 before the change): a fire of an instance with a host grant refuses
|
||||
`credential-revoke-failed` today. A delete leaves the `host_grants` row. Then: what the next start does with
|
||||
a record whose key file is gone.
|
||||
- T2: a fire with a host grant on two hosts and a Gitea grant. The verb answers with a `job` id, `revoking`
|
||||
with all three, and `done: false`, and `fire.revoking` carries the same job (sage-2575, condition 5). After
|
||||
the job:
|
||||
- all three are revoked, each with `grant.revoked` and `grant.revoke.closed` as the manual verbs record
|
||||
them, plus `fire: true`;
|
||||
- the line is gone on both fake hosts, every token is deleted, and login is prohibited at the fake Gitea;
|
||||
- `fire.done` names all three;
|
||||
- that agent's `host.run` and Gitea use both refuse.
|
||||
- T3 (sage-2575, conditions 1-3): the same grants through the delete (`lifecycleTake` then
|
||||
`lifecycleDelete`).
|
||||
- The first delete refuses `grants-revoking` and starts the job. The human's inbox gets the start message
|
||||
naming the job and all three grants, and saying to raise the delete again when it's done.
|
||||
- A delete raised while the job is held refuses `grants-revoking`, starts no second job and sends no second
|
||||
message.
|
||||
- After the job, the end message names the job and each grant revoked.
|
||||
- A third request completes as today.
|
||||
- T3b (Q2, sage-2575 condition 3): a delete whose job refuses (host B unreachable).
|
||||
- The end message and `grant.revoke.refused` name the instance, `host:B`, the reason and
|
||||
`mosaic hosts revoke`.
|
||||
- The file and the instance are unchanged.
|
||||
- The next delete refuses `grant-revoke-failed`, not `grants-revoking`. Its message names the earlier job's
|
||||
reason and the new job, and the instance is not deleted.
|
||||
- With B back, that retry job revokes only B and Gitea, skipping host A, and the delete after it completes.
|
||||
- A second case clears B by hand with `hosts.revoke` before the next delete. That delete starts a job only
|
||||
for Gitea and the leftover line.
|
||||
- T3c (sage-2575 condition 4): a delete of an instance with no grant completes in one request. Same result,
|
||||
same events counted, no inbox message.
|
||||
- T4: host B unreachable in a fire. The job records `grant.revoke.refused` naming the agent, `host:B` and
|
||||
`host-remove-failed`, and the inbox message names `mosaic hosts revoke` (N2). Afterwards:
|
||||
- the file is unchanged and the instance is still firing;
|
||||
- host A is revoked and recorded, and host B's row and key file are still there;
|
||||
- the Gitea grant is untouched, since it comes after B in the order;
|
||||
- the credentials are unrevoked.
|
||||
A repeat with B reachable completes and revokes only B and Gitea.
|
||||
- T4b (Q1): an earlier manual `hosts.revoke` left a line on host C. A fire with C reachable removes it and
|
||||
records the close. With C unreachable, the fire completes and names C and the tag in `grantsLeft` and in
|
||||
`fire.done`.
|
||||
- T5: Gitea's admin token can't be checked. The job refuses with the Gitea grant intact.
|
||||
- T5b: a token DELETE fails. The job refuses `gitea-left`. A repeat with Gitea back retries only this agent's
|
||||
entries and completes. Another agent's left entry is still left afterwards (N1).
|
||||
- T6 (sage-2574): a host grant decision approved before the fire, executed after it. The fire records
|
||||
`grant.revoked {granted: false}` for (agent, host). The executor runs after `fire.done` and ends the decision
|
||||
`revoked`: no `host_grants` row, no key file, no `add` sent to the host.
|
||||
- T6b (sage-2574): the same for a Gitea grant decision. The executor ends it `revoked` before any Gitea call:
|
||||
no `gitea_grants` row, no token file, and the fake Gitea saw no token create.
|
||||
- T6c: a host or Gitea executor already in flight when step G starts.
|
||||
- The host side aborts its claim (3a step 1) and ends `revoked`.
|
||||
- The Gitea side: step G awaits the executor (3b), and then the fake Gitea holds no token for the user. That
|
||||
includes one the executor created mid-flight.
|
||||
- T6d (B1): a host decision and a Gitea decision, raised before the fire and still undecided. The fire puts
|
||||
both in the set. The human approves both after `fire.done`, and both executors end `revoked`, with nothing
|
||||
applied.
|
||||
- T6e (B1): a host raise and a Gitea raise refuse `instance-fired`:
|
||||
- while the instance is firing;
|
||||
- after `fire.done`.
|
||||
After a rehire of the same name, a raise goes through. While a delete's job runs, a raise refuses
|
||||
`grant-revoke-running`.
|
||||
- T7: an instance with no grant fires exactly as today, synchronously. Same result keys, no `revoking`, and
|
||||
the same events, counted.
|
||||
- T8: hosts.revoke and grants.revoke keep their records, their order and their tester refusals.
|
||||
- T9 (B3): with the job held on a remove:
|
||||
- a second fire refuses `fire-in-progress`;
|
||||
- a delete of the same agent refuses `grants-revoking` and starts no second job;
|
||||
- `hosts.revoke` and `grants.revoke` for the agent refuse `grant-revoke-running`;
|
||||
- `grants.sweep` names the agent as `running`.
|
||||
After a release, one set of `grant.revoked` records exists per grant.
|
||||
- T10 (B2): the fake installer holds a remove and the fake Gitea holds a DELETE, neither answering until
|
||||
released.
|
||||
- `instance.fire` and `lifecycleDelete` both answer before either is released. The test checks this with a
|
||||
timer, not a wall-clock bound.
|
||||
- Meanwhile the broker answers another verb from another session.
|
||||
- After release, the jobs complete.
|
||||
- A cli test drives the launcher's delete pass against a bus whose remove is held. The pass closes the
|
||||
request `grants-revoking` and the host channel stays up.
|
||||
- T11 (d6, C1): a fire whose job is held on a remove. While it's held, the chief's run ends and its claim is
|
||||
released, and processing is halted. After the release, the job completes: `fire.done` is recorded with the
|
||||
job, the consumed `action.allowed` event is there, a second use of the same authority refuses
|
||||
`decision-consumed`, and the instance is out of the file. No `fire.failed` and no `grant.revoke.refused`.
|
||||
- T12 (d6, C1): a fire with a grant whose step B refuses in the job. The test uses T1's setup, where
|
||||
`revoke()` refuses `credential-revoke-failed`. After the job:
|
||||
- `fire.failed` carries the job, step `B` and `credential-revoke-failed`, and the inbox message names the
|
||||
job, the revoked grants, the step and the code;
|
||||
- there's no `grant.revoke.refused` and no `fire.done`;
|
||||
- the instance is still firing and still in the file.
|
||||
With the cause cleared, a repeat fire finds an empty set, runs B and C synchronously and completes.
|
||||
- T13 (d6, N4): a fire and a delete, each with its job held, then the broker is closed and a new one opened
|
||||
on the same data root. The new broker records `grants.job.interrupted` for both jobs and sends two inbox
|
||||
messages, one saying to fire again and one to raise the delete again. A third broker on the same root sends
|
||||
nothing more. A job that ended before the restart gets no interrupted record.
|
||||
|
||||
Files (N3). Every file listed runs whole in the manifest round, touched or not:
|
||||
- New: `packages/bus/tests/fire-revokes-grants.test.mjs` holds T2-T13.
|
||||
- Changed:
|
||||
- `grant-host.test.mjs` and `gitea-grant.test.mjs`: T1's pins, and the T8 regressions next to their
|
||||
existing cases;
|
||||
- `packages/cli/tests/lifecycle.test.mjs`: the T10 launcher pass;
|
||||
- `fake-gitea-api.mjs` and `host-fixtures.mjs`, if T10's holds need a hook there.
|
||||
- Run whole because they call `instance.fire` or `lifecycleDelete` at 47a35bf43:
|
||||
- bus: `chief-of-staff`, `lifecycle-revoke`, `lifecycle-verbs`, `reload`;
|
||||
- cli: `lifecycle`, `instances`;
|
||||
- harness: `tools`.
|
||||
- Also run: `grant-executor.test.mjs` (the executors' `#giteaRuns` change) and `gitea-grant-docker.test.mjs`
|
||||
(brief gate).
|
||||
- `server.mjs`, `process.mjs`, `host.mjs` and `launcher.mjs` are unchanged, so no test changes there for this
|
||||
row.
|
||||
|
||||
## 9. Mutants
|
||||
|
||||
- M1: step G skips the host revokes. Killed by T2.
|
||||
- M2: step G skips the Gitea revoke. Killed by T2.
|
||||
- M3: a refused grant revoke is ignored and the job goes on to step B. Killed by T4.
|
||||
- M4: `lifecycleDelete` skips step G. Killed by T3.
|
||||
- M5: the fire's host revoke goes local first (`lineFirst: false`). Killed by T4, because B's row is gone.
|
||||
- M6: a Gitea revoke with something in `serviceLeft` counts as done. Killed by T5b.
|
||||
- M7: open decisions left out of the set. Killed by T6 and T6b.
|
||||
- M8: step G skips the leftover-line retry. Killed by T4b.
|
||||
- M9: a failed leftover-line retry refuses the fire. Killed by T4b.
|
||||
- M10: the set takes only approved decisions (d3's rule). Killed by T6d.
|
||||
- M11: the raise bindings skip the fired check. Killed by T6e.
|
||||
- M12: no `#revoking` lock. Killed by T9.
|
||||
- M13: the verb awaits the job before it answers. Killed by T10.
|
||||
- M14: step G's Gitea part doesn't await the in-flight executor. Killed by T6c.
|
||||
- M15: step G's Gitea retry runs the business-wide `#sweepGitea`. Killed by T5b.
|
||||
- M16: the delete's first refusal sends no start message. Killed by T3.
|
||||
- M17: a delete after a refused job refuses `grants-revoking` instead of `grant-revoke-failed`. Killed by T3b.
|
||||
- M18: a delete of an instance with no grant goes through the job. Killed by T3c.
|
||||
- M19: the job's step C uses `#consumeAuthority`, with the holder and processing checks. Killed by T11.
|
||||
- M20: a step B or C failure in the job is recorded as `grant.revoke.refused internal`. Killed by T12.
|
||||
- M21: boot skips the interrupted-job check. Killed by T13.
|
||||
|
||||
## 10. Order and rebase
|
||||
|
||||
Built from 47a35bf43 for this read. The code goes on whatever refactor is when the build starts. Rows 274, 256
|
||||
and 194's follow-ups touch the broker and instance-settings, so expect a rebase and a whole rerun. The row lands
|
||||
before the .53 Case 1 install hires an instance with a grant. It doesn't hold the dragon-lin re-pin.
|
||||
|
||||
## Runs
|
||||
|
||||
(after the gate)
|
||||
|
||||
## Mutants
|
||||
|
||||
(after the gate)
|
||||
@@ -0,0 +1,82 @@
|
||||
# Row 205a (193b1a, #1703): the trail and replies message filter
|
||||
|
||||
Design: DESIGN.md d2 in this directory, with rulings in section 7 (sage-2288). Base: refactor 02da6a2ae. No schema
|
||||
change. Nothing has run yet: every test run waits for Sage's CLEAR.
|
||||
|
||||
## Changes
|
||||
|
||||
`packages/bus/src/broker.mjs`:
|
||||
- `#hiddenMessage(s, id)`: the message's thread (`message_threads` joined to `threads`) is owned by a `user:`
|
||||
other than `#owner(s)`, and `#readable(s, t)` is false. A message with no thread, or in the business human's
|
||||
or the tester's thread, is never hidden. A reader session owns nothing, so every user thread is hidden from it.
|
||||
The tester reads through its reader view, and `#owner` still gives `tester`.
|
||||
- `#readable(s, t)` returns false. Rule 1 (a share in force) can't hold before 193b2's schema 3j, so nobody
|
||||
reads another's thread. 193b2 adds the share rule and rules 2-4 here.
|
||||
- `#trail`: a hidden subject returns `[]` before anything is read, exactly as an unknown id does (ruling 3).
|
||||
After the rows are collected, it drops hidden `messages` rows, `deliveries` rows of a hidden message, and
|
||||
`events` rows whose subject is a hidden message. Decisions, decision events, task snapshots, run events and
|
||||
claims stay.
|
||||
- `replies`: a hidden id fails `message-not-found`, as an unknown id does. Hidden replies are left out in SQL
|
||||
(`NOT EXISTS` a thread whose owner starts with `user:` and isn't the caller), so the `REPLIES` limit counts
|
||||
only visible rows.
|
||||
- The SQL clause doesn't call `#readable`. That is exact while `#readable` is false. 193b2 must add the share
|
||||
exception to the clause and to `#readable` together.
|
||||
|
||||
`packages/bus/tests/message-filter.test.mjs` (new). It writes a `user:alice` thread and its messages
|
||||
straight to the store, as R3's `thread.send` and the run's `to: 'user'` reply will write them, because nothing
|
||||
makes a user session before R3.
|
||||
- `trail` on a hidden id equals `trail` on an unknown id, byte for byte, for the owner, a reader and the tester.
|
||||
- `replies` on a hidden id equals `replies` on an unknown id (code and message), for all three.
|
||||
- `trail` on a user-thread run's decision shows the decision and the owner's message, and drops the user
|
||||
thread message, its delivery and its event.
|
||||
- `replies` with `REPLIES` hidden replies ahead of a visible one returns the visible one.
|
||||
- Messages in the business human's and the tester's threads stay visible on both verbs (row 177 control).
|
||||
- The governance pin, one assertion per item in the user sentence: inbox entry, claim, task, project file and
|
||||
the closed decision in `decisions {list: 'recent'}` show to the owner. `trail` and `replies` hide the user
|
||||
message, and the decision's trail carries no user body.
|
||||
|
||||
## Mutants (each must fail its named test in message-filter.test.mjs)
|
||||
|
||||
| Mutant | Edit | Named test |
|
||||
|---|---|---|
|
||||
| M1 | `#readable` returns true | governance pin: the owner sees decisions, inbox entries, claims, tasks and project files, but not the user messages |
|
||||
| M11 | the trail drop loop iterates nothing | trail on a decision keeps the decision and drops the user thread messages, their deliveries and their events |
|
||||
| M12 | the replies `NOT EXISTS` clause removed | replies leaves out other users thread messages and still fills a page |
|
||||
| M13 | trail fails `message-not-found` for a hidden subject | trail on a hidden message id answers exactly as on an unknown id, for the owner, a reader and the tester |
|
||||
| M14 | replies returns `[]` for a hidden id | replies on a hidden message id answers exactly as on an unknown id, for the owner, a reader and the tester |
|
||||
|
||||
## Gate
|
||||
|
||||
The kit is /mnt/storage/scratch/morgana-r205 (`slot205.sh`, `mutate.py`, `busy.sh`), run as
|
||||
`bash busy.sh && CAND=<sha> bash slot205.sh` on Sage's CLEAR. It runs these files whole:
|
||||
- message-filter;
|
||||
- the bus files that call trail or replies: replies, tester, tester-threads, tester-writes, broker, decisions,
|
||||
decision-close, end-launch and task-read.
|
||||
|
||||
Then it runs the bus suite, then M1 and M11-M14, each on its named test and restored after each. It uses umask
|
||||
022 and TMPDIR from `mktemp -d /mnt/storage/scratch/tmp/g205XXXX`. It refuses unless HEAD is the candidate
|
||||
and the tree is clean, and it stops at the first unexpected failure (sage-2371).
|
||||
|
||||
Sage's gate adds every `scripts/test-*.sh`. The webui tests that read trail or replies run there.
|
||||
|
||||
Static check: `node --check` on broker.mjs and the new test, run from `/`. Both parse. All five mutant anchors
|
||||
match exactly once.
|
||||
|
||||
## r2 (after the 742cba805 slot, failure 1 of the window, sage-2428)
|
||||
|
||||
slot205 on 742cba805 stopped red in message-filter, 4 pass and 2 fail. Both fixes are in the test only.
|
||||
broker.mjs is unchanged (sage-2430).
|
||||
|
||||
- "trail on a decision ..." (:124, its event shows). `#trail` reaches events on a decision trail by
|
||||
`subject` = the decision or `body.$.decision` = the decision. `put()` wrote every event body as `{}`, so the
|
||||
shown message's event was out of reach. That also made the "no hidden event" check (:127) vacuous: the hidden
|
||||
event was never collected, so dropping it proved nothing. Fix: `put()` writes `{decision}` as the event body
|
||||
when the message has a decision. Both events are now collected; the filter keeps one and drops the other.
|
||||
- Governance pin, claims item (:175). A thread run's `role.claim` writes `thread_claims` only and returns
|
||||
(broker.mjs:2682). `agents` (:3309) reads `role_claims` only. No owner verb shows a thread claim. The gap
|
||||
predates 205. Ruling sage-2430, option (a): the pin checks through `threadsState` (the launcher view) that
|
||||
the claim is recorded with holder `assistant-run` and isn't filtered. `agents` is unchanged in 205.
|
||||
Follow-up: Sage adds a row for an owner-visible thread-claim read. Like the filter, it lands before any
|
||||
user thread goes live, since PRD :1084 and the FED R6 sentence promise that the owner sees claims.
|
||||
|
||||
M1 still fails the pin: with `#readable` true, `trail` on the user message returns rows instead of `[]`.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Row 205 (193b1, #1703): viewer verbs, board refusals and the message filter
|
||||
|
||||
Owner Morgana, reviewer Duckworth, gate owner Sage (sage-2280). Brief: `docs/plans/2026-10-20_human-users-build.md`,
|
||||
"Brief: 193b1, viewer verbs, board refusals and the message filter" (blob a73e1bba0, rev 3088). Design:
|
||||
row 193 DESIGN.md r5 (e2d161e2e, Duckworth approved), sections 4.3, 5.4, 5.5, 5.7, 8 and 9. Base: refactor
|
||||
02da6a2ae. Draft 2: Sage's rulings (sage-2288) are in section 7. Sections 1-6 are d1 as sent (morgana-0063).
|
||||
|
||||
## 1. The dependency gap
|
||||
|
||||
The brief chains 193b1 on FED R3, FED R4 and row 196's verifier. Only row 196 exists (7247d80c7). On 02da6a2ae:
|
||||
|
||||
- R3 and R4 aren't filed. The FED brief (`docs/plans/2026-10-20_federation-r1-r2-r5.md`:12-13) files R3 "after
|
||||
R1 and row 196" and R4 after R3, R5 and 193a. No `*/fed-r3` branch is on origin.
|
||||
- Row 196's `verifyPrincipal` returns the `user` kind, and server.mjs accepts the `user` envelope, but
|
||||
runtime.mjs wires only `authenticateHuman` and `authenticateTester`. The broker has no `bindUser`, no
|
||||
`s.user`, no per-request grant read and no collaborator verb list. FED 3.6 and row R3 own all of these.
|
||||
- So nothing makes a user session today, and no thread is owned by `user:<id>` except through a direct
|
||||
store write.
|
||||
|
||||
193b1 has two halves that depend on R3 in different ways.
|
||||
|
||||
| Item | Needs a user session (R3) | Testable on 02da6a2ae |
|
||||
|---|---|---|
|
||||
| Viewer verb list (4.3) | yes: it extends R3's collaborator list | no |
|
||||
| Per-grant level checks via `covers(..., {level})` | yes: R3's grant read | no |
|
||||
| Board reads refused to users (5.5) | yes | no |
|
||||
| `trail` and `replies` filter (5.7) | no: it filters owner, reader and tester sessions | yes, with seeded `user:<id>` threads |
|
||||
| `#readable` owner-only | no | yes |
|
||||
| Governance pin test (owner side) | no: the owner reads decisions, inbox, agents, tasks and `project.file` | yes, with a seeded user-thread run |
|
||||
| Mutants 6 (viewer `thread.send`), 10 (user allowed `trail`) | yes | no |
|
||||
| Mutants 11, 12 (trail, replies filter removed) | no | yes |
|
||||
| Mutants 1, 3 (`#readable` rules 1 and 3 dropped) | see 4 | see 4 |
|
||||
|
||||
## 2. Recommendation: split 205 into 205a now and 205b after R3
|
||||
|
||||
- **205a, the message filter.** `trail` and `replies` hide messages in other users' threads from the owner,
|
||||
reader sessions and the tester, with `#readable` owner-only. Plus the owner-side governance pin test and
|
||||
mutants 11 and 12. No R3 dependency, no schema change.
|
||||
- **205b, viewer verbs and board refusals.** Built on R3's candidate, as Q177 chains a row on its parent's
|
||||
candidate. The viewer list, the level checks, users refused on board reads, the user-session half of the
|
||||
pin test, mutants 6 and 10.
|
||||
|
||||
Why split. The filter is the item the boundary orders first: "No user thread goes live before 193b1's
|
||||
message filter has landed" (mos-1021 B2, FED R8). Landing it before R3 means R3 and every later row can't
|
||||
create a user thread that the owner can read on `trail`. Waiting for R3 holds the filter behind a row that
|
||||
isn't filed. The other option, building all of 193b1 against R3's named interface as 193a did with R1,
|
||||
doesn't work here: 193a only called R1's exported `covers`, while 193b1 would have to write `bindUser`, the
|
||||
grant read and the collaborator list itself, which is R3's scope and owner.
|
||||
|
||||
Tradeoff: two rows and two reviews in place of one, and 205b rebases onto R3. The split is a brief change,
|
||||
so it's Sage's to rule.
|
||||
|
||||
## 3. 205a design
|
||||
|
||||
### 3.1 The hidden-message check
|
||||
|
||||
One broker method, `#hiddenMessage(s, id)`:
|
||||
|
||||
```
|
||||
SELECT t.owner FROM message_threads mt JOIN threads t ON t.id = mt.thread
|
||||
WHERE mt.message = ? AND t.business = ?
|
||||
```
|
||||
|
||||
A message is hidden when that owner starts with `user:`, isn't `#owner(s)`, and `#readable(s, t)` is false.
|
||||
A message with no thread row, or in a thread owned by the business human or `tester`, is never hidden, so
|
||||
row 177's behaviour is unchanged. A reader session has no `#owner`, so every user thread's messages are
|
||||
hidden from it.
|
||||
|
||||
`#readable(s, t)` in 205a returns false. Before 193b2's schema 3j there is no `thread_shares` table, so rule 1
|
||||
(a share in force) can't hold, and the function is false for anyone but the thread's owner (5.7: "b1 hides
|
||||
every message in another user's thread"). It is written as its own method so 193b2 adds the share rule and
|
||||
rules 2-4 in one place.
|
||||
|
||||
### 3.2 `trail`
|
||||
|
||||
- If `subject` is a hidden message id, return `[]`. Design 5.7 says `message-not-found`, "as an unknown id
|
||||
does", but `trail` on an unknown id returns `[]` today (it has no not-found check). A different code for a
|
||||
hidden id would confirm the id exists, which is what 5.7 wants to prevent, so I follow its reason over its
|
||||
code. Question 3.
|
||||
- After `#trail` collects its rows, drop: `messages` rows that are hidden, `deliveries` rows whose `message`
|
||||
is hidden, and `events` rows whose `subject` is a hidden message id. This covers each way `#trail` reaches
|
||||
a message: by id, by `decision`, and through `task.created` → `human.input` → `body.message`.
|
||||
- Decision rows, decision events, task snapshots, run events and role claims stay. 5.7 puts them on the
|
||||
governance side.
|
||||
|
||||
### 3.3 `replies`
|
||||
|
||||
A hidden `id` fails `message-not-found`, and hidden messages are dropped from the result. The `LIMIT`
|
||||
applies before the filter today; the filter moves into the SQL (`AND NOT EXISTS` a thread owned by another
|
||||
`user:`) so a page of hidden replies doesn't return short. For 205a that SQL form is exact because
|
||||
`#readable` is false; 193b2 adds the share exception to the same clause.
|
||||
|
||||
### 3.4 Tests (packages/bus/tests/message-filter.test.mjs, new)
|
||||
|
||||
Fixtures seed a project, a thread owned by `user:alice` with messages in `message_threads`, a delivery and an
|
||||
event on one of them, and a decision raised by that thread's run that cites the message. Sessions: the
|
||||
business human, a reader session and the tester.
|
||||
|
||||
- `trail` on a hidden message id: `[]` for all three sessions, the same as an unknown id.
|
||||
- `trail` on the decision: the decision row and its events show; the user's message, its delivery and its
|
||||
events don't.
|
||||
- `trail` on a human-owned thread's message: unchanged (row 177 control).
|
||||
- `replies` on a hidden id: `message-not-found`. `replies` on a visible id with hidden and visible replies:
|
||||
only the visible ones, and a full page when hidden replies come first.
|
||||
- Governance pin, owner side: for the user-thread run, `decisions {list: 'recent'}`, `inbox`, `agents`,
|
||||
`tasks` and `project.file` each show its item, and `trail` and `replies` hide the user's message body.
|
||||
Each item in the user sentence is one named assertion.
|
||||
|
||||
### 3.5 Mutants
|
||||
|
||||
| Mutant | Edit | Named test |
|
||||
|---|---|---|
|
||||
| 11 | the `trail` filter removed | trail on the decision hides the user's message, delivery and events |
|
||||
| 12 | the `replies` filter removed | replies hides other users' thread messages |
|
||||
| 11b | the hidden-subject early return removed | trail on a hidden message id returns [] |
|
||||
| 1 | `#readable` returns true | governance pin: trail and replies hide the user body |
|
||||
|
||||
## 4. Mutants 1 and 3
|
||||
|
||||
Design section 8 lists mutants 1 (rule 1 dropped) and 3 (rule 3 dropped) for b1. With no shares table,
|
||||
rule 1 is always false, so `#readable` is false whatever rules 2-4 say. Dropping rule 3 changes nothing a
|
||||
test can see until 193b2 adds shares, and rules 2 and 3 need R3's grant read. So:
|
||||
|
||||
- Mutant 1 in 205a is "`#readable` returns true", killed by the pin test above.
|
||||
- Mutant 3 moves to 193b2, where rule 1 can hold and rule 3 decides.
|
||||
|
||||
This is a second brief change for Sage to rule on.
|
||||
|
||||
## 5. Gate (Sage's slot, on CLEAR only)
|
||||
|
||||
The brief's gate: Duckworth approves the manifest; the bus suite and every `scripts/test-*.sh` green on
|
||||
Sage's gate; the mutants killed. The new test file whole, the touched existing files whole, then the bus
|
||||
suite, then each mutant on its named test with a restore check. umask 022, TMPDIR from
|
||||
`mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`. No test touches the live data root, config, units, slot
|
||||
files, any live access.json or `~/.mosaic`.
|
||||
|
||||
## 6. Questions for Sage
|
||||
|
||||
1. Split 205 into 205a (filter, now) and 205b (viewer verbs and board refusals, after R3's candidate)?
|
||||
2. Mutant 1 as "`#readable` returns true" in 205a, and mutant 3 moved to 193b2?
|
||||
3. A hidden `trail` subject returns `[]`, the same as an unknown id, in place of 5.7's `message-not-found`
|
||||
(3.2)? `replies` already fails `message-not-found` on an unknown id, so there the code stays.
|
||||
|
||||
## 7. Rulings (sage-2288, re morgana-0063)
|
||||
|
||||
1. **Split approved.** 205a is built now: the 5.7 `trail` and `replies` filter over owner, reader and tester
|
||||
sessions, `#readable` owner-only, the owner-side governance pin test, and mutants 11 and 12. 205b comes
|
||||
after R3's candidate and R4: viewer verbs, `covers` level checks, board reads refused to users, the
|
||||
user-side pin, and mutants 6 and 10. 205a's base is origin/refactor 02da6a2ae or later. R3 builds on
|
||||
205a's candidate and extends the filter to user sessions, so the filter isn't written twice.
|
||||
2. **Mutant 1** in 205a is "`#readable` returns true", killed by the pin test. **Mutant 3** moves to 193b2
|
||||
(row 206). Sage changes the brief to match.
|
||||
3. **Hidden equals unknown.** A hidden `trail` subject returns `[]`, exactly as an unknown id does. `replies`
|
||||
keeps `message-not-found` for both. The point of 5.7 is that a caller can't tell hidden from unknown.
|
||||
So 205a adds:
|
||||
- one test per verb comparing the hidden response with the unknown-id response byte for byte
|
||||
(`JSON.stringify` of the result, or of the error's code and message), for each of the three sessions;
|
||||
- a mutant M13 that makes the hidden path distinguishable: `trail` fails `message-not-found` for a
|
||||
hidden subject. The `trail` equality test must kill it. M14 does the same for `replies`: a hidden id
|
||||
returns `[]` instead of failing. The `replies` equality test must kill it.
|
||||
|
||||
### 7.1 205a mutant table (replaces 3.5)
|
||||
|
||||
| Mutant | Edit | Named test |
|
||||
|---|---|---|
|
||||
| M1 (design 1) | `#readable` returns true | governance pin: the owner sees decisions, inbox entries, claims, tasks and project files, but not the user's messages |
|
||||
| M11 (design 11) | the `trail` filter of hidden rows removed | trail on the decision hides the user's message, its delivery and its events |
|
||||
| M12 (design 12) | the `replies` filter removed | replies drops other users' thread messages and still fills a page |
|
||||
| M13 | `trail` fails `message-not-found` for a hidden subject | trail on a hidden id answers exactly as on an unknown id |
|
||||
| M14 | `replies` returns `[]` for a hidden id | replies on a hidden id answers exactly as on an unknown id |
|
||||
|
||||
Queue: Sage re-pins 205 to the split brief and adds 205b and the R3 row at the next gap. I claim after
|
||||
that. No run before Sage's CLEAR.
|
||||
@@ -1,144 +0,0 @@
|
||||
# Row 257: Q198 S1 pressure parser and pure rules evaluator
|
||||
|
||||
Issue #1762. Brief: `docs/plans/2026-10-21_post-v1-row-briefs.md`, "Brief: Q198 S1 pressure parser and
|
||||
pure rules evaluator" and "Gate: Q198 S1" (cca206cdc). Design: `docs/plans/2026-10-21_q198-system-manager.md`
|
||||
r6.1, section 8 row S1 and section 4 (D4). Owner Morgana, reviewer Filbert, gate owner Sage (sage-2196).
|
||||
Post-v1: MVP rows and re-pin windows go ahead of it on the slot. Base: refactor cca206cdc.
|
||||
|
||||
## Recorded choices (sage-2196 accepted each)
|
||||
|
||||
1. **Placement.** A new package, `packages/supervisor` (`@mosaic/supervisor`), shaped like
|
||||
packages/runs. Nothing on refactor parsed loadavg, PSI, meminfo or cgroup limits (`git grep`; the
|
||||
only hit was an awk line in scripts/case2-precheck.sh). S2 adds the sampler process to the same
|
||||
package. Packages aren't registered centrally, so no other file changes.
|
||||
2. **Provider scope** (429, quota, Retry-After, the half-open probe) is out. The brief puts it in S3
|
||||
and S5.
|
||||
3. **PSI** is not required on a host that has never shown it, so a kernel built without PSI skips the
|
||||
PSI rows instead of holding forever. Once a sample has carried PSI, it is required: a later missing
|
||||
or stale PSI signal holds with `health-unknown`. That is the case sage-2196 asked for.
|
||||
- Tradeoff: I latch on the first sample that carries PSI, not only on the very first sample. So PSI
|
||||
that appears late and then disappears also holds. This is stricter than "present at the first
|
||||
sample", and it never skips a signal the host has shown.
|
||||
4. **Memory under a cgroup.** Available memory = min(MemAvailable, limit - memory.current) at each
|
||||
limited level. memory.current counts page cache, so this errs toward holding.
|
||||
- Follow-up, not in this row: subtract reclaimable cache using memory.stat `inactive_file`, if S2's
|
||||
live samples show holds that the host's MemAvailable doesn't support.
|
||||
|
||||
Rulings on two readings from the build (sage-2202, re morgana-0057):
|
||||
|
||||
5. **PSI latch.** PSI becomes required on the first sample that carries it. This is stricter than
|
||||
"present at the first sample" and fails closed: a PSI file that disappears after it was seen is a
|
||||
missing signal, so health-unknown holds.
|
||||
6. **PSI count across resources.** Design line 152 says "CPU some ≥20, memory full ≥2, or I/O full
|
||||
≥10, for 3 samples". The window applies to the whole disjunction, so any PSI condition on 3
|
||||
consecutive samples holds. If Filbert reads it per resource at review, the review finding decides,
|
||||
and the fix is a single counter.
|
||||
|
||||
Other readings of the D4 table, made in the build:
|
||||
- **Memory floors.** "Whichever demands more free memory" means the larger floor applies. Hold below
|
||||
max(15% x limit, 1 GiB); release above max(25% x limit, 2 GiB).
|
||||
- **Edges as written.** Hold at ≥ for load and PSI, < for memory. Release at ≤ 0.55 for load, < for
|
||||
PSI, > for memory. Critical below 5% of the limit and below 1 GiB of disk.
|
||||
- **Sample counts.** "For 3 samples" means 3 consecutive samples. The count resets after a gap of more
|
||||
than 6 s (the stale limit) between samples. The PSI row counts as one row (ruling 6), and the
|
||||
reasons name the resources over on the latest sample.
|
||||
- **Memory holds on one sample.** The table's memory row has no sample count.
|
||||
- **Release** needs every applicable release condition on every sample for 30 s, by the samples' own
|
||||
times (`>= 30000` ms after the first qualifying sample). A dead-band sample or any hold restarts the
|
||||
window, and so does a gap of more than 6 s.
|
||||
- **Start held.** `initialState()` is held, because nothing has been seen yet. The first healthy
|
||||
sample starts the 30 s window.
|
||||
- **Missing or stale.** Required signals are load, memory, oom and disk, plus PSI once seen. A signal
|
||||
holds at once with `health-unknown` if it is:
|
||||
- missing;
|
||||
- non-numeric, negative or zero where a divisor;
|
||||
- more than 6 s older than its sample (6 s exactly is fresh);
|
||||
- stamped later than its sample.
|
||||
Disk and OOM are required because the critical row can't be evaluated without them.
|
||||
- **Host-only OOM.** `/proc/vmstat` `oom_kill` gives the OOM counter when there's no cgroup
|
||||
memory.events. The parser returns null for a kernel without it, and the sampler then has no OOM
|
||||
signal (health-unknown).
|
||||
- **Critical** latches. It ends only when the caller passes `reconciled` with no critical condition in
|
||||
the sample, then goes through held and the normal 30 s release. The evaluator never sets
|
||||
`reconciled` and has no kill-switch path. Critical reasons accumulate while latched.
|
||||
- **OOM counter.** The first OOM sample is the baseline. A falling counter (a recreated cgroup) resets
|
||||
the baseline with a note, not an event.
|
||||
- **Clock going backwards.** A sample earlier than the last one holds with `health-unknown` (unknown
|
||||
`clock`, note `clock-backwards`).
|
||||
- **Swap activity** is parsed (`pswpin`, `pswpout`) and has no rule. D4 lists it as a signal with no
|
||||
threshold.
|
||||
- **Settings.** `resolveRules` refuses:
|
||||
- unknown keys and non-numbers;
|
||||
- a release at or past its hold (load, each PSI resource);
|
||||
- memory release floors at or below the hold floors;
|
||||
- fractions outside (0, 1);
|
||||
- PSI values outside 0 to 100;
|
||||
- sample counts that aren't integers ≥ 1;
|
||||
- a negative release window;
|
||||
- a stale limit at or below the sample interval;
|
||||
- a critical memory fraction at or above the hold fraction;
|
||||
- a non-positive disk floor.
|
||||
- **Large byte values.** Parsed with `Number`. A memory.max above 2^53 loses precision, but it only
|
||||
takes part in a min against MemTotal.
|
||||
|
||||
## Round 2 (Filbert r1 APPROVE c30188 notes, sage-2212)
|
||||
|
||||
- N1: `merge` checked unknown keys with `key in base`, so inherited names (`toString`, `constructor`,
|
||||
`__proto__`) passed the check and were dropped instead of refused. It now uses
|
||||
`Object.hasOwn(base, key)`, and the refusal test has those three cases.
|
||||
- N2: the critical-memory edge was tested only on a 32 GiB limit, where 5% is not an integer, so a
|
||||
`<` to `<=` mutant survived. The test adds a 20 GiB limit, where 5% is exactly 1 GiB, and M8 is that
|
||||
mutant. I checked the other memory edges for the same gap:
|
||||
- The hold floor is non-integer on 32 GiB but is tested at exactly 1 GiB on a 4 GiB limit.
|
||||
- The release floors are 8 GiB on 32 GiB and 2 GiB on 4 GiB, both integers.
|
||||
- The disk edge is 1 GiB.
|
||||
|
||||
None has the gap.
|
||||
|
||||
## Files
|
||||
|
||||
- `packages/supervisor/package.json`, `README.md`
|
||||
- `src/errors.mjs`: `SupervisorError` with `kind` parse, rules or sample.
|
||||
- `src/pressure.mjs`: the nine parsers, `effectiveCpus` and `effectiveMemory`.
|
||||
- `src/rules.mjs`: `DEFAULT_RULES`, `resolveRules`, `initialState`, `evaluate`.
|
||||
- `src/index.mjs`
|
||||
- `tests/pressure.test.mjs`: parsers on kernel-shaped fixture text, with malformed, truncated and
|
||||
repeated-key cases. Effective CPU cases cover the host-only fallback, cpuset, fractional and nested
|
||||
`cpu.max`, and `max`. Effective memory cases cover the host-only fallback, `max`, nested levels,
|
||||
usage over the limit, a limit above MemTotal and a missing memory.current.
|
||||
- `tests/rules.test.mjs`: the D4 table on a fake clock.
|
||||
- Each hold and release threshold on both sides of its edge.
|
||||
- Three-sample counting and the gap reset.
|
||||
- The 30 s release at 29 999 and 30 000 ms, and the dead-band restart.
|
||||
- Missing, stale, future and malformed signals.
|
||||
- PSI never present, and PSI lost after it was present.
|
||||
- Critical OOM, memory and disk at their edges, the reconciliation latch and the counter reset.
|
||||
- A backwards clock, input state left unchanged, and settings refusals and overrides.
|
||||
- `tests/purity.test.mjs`: a static check that src imports only its own files and uses no
|
||||
fs, child_process, net, os, process, dynamic import, Date or timers.
|
||||
|
||||
No test reads /proc, /sys or a cgroup, starts a build, opens a network socket or generates load. No new
|
||||
dependency.
|
||||
|
||||
## Mutants (each must fail its named test; the brief requires M1 and M2)
|
||||
|
||||
| Mutant | Edit | Named test |
|
||||
|---|---|---|
|
||||
| M1 hold edge | `ratio >= rules.load.hold` becomes `>` | load holds at 0.80 for 3 samples and not at 0.7999 |
|
||||
| M2 early release | `now - next.releaseSince >= rules.releaseMs` becomes `>= 0` | the first state is held, and healthy samples open it after exactly 30 s |
|
||||
| M3 memory floor | the hold floor `Math.max` becomes `Math.min` | memory holds below the larger floor, on one sample |
|
||||
| M4 sample count | `next.loadCount >= rules.load.samples` becomes `>= 1` | load holds at 0.80 for 3 samples and not at 0.7999 |
|
||||
| M5 stale | the `now - signal.at > rules.staleMs` check is removed | a signal is stale at more than 6 s old and fresh at exactly 6 s |
|
||||
| M6 effective CPU | the quota `Math.min` becomes `Math.max` | effective CPUs take the smallest of host, cpuset and every quota level |
|
||||
| M7 critical latch | the `!reconciled` critical branch is removed | critical stays until reconciliation, then still waits the 30 s release |
|
||||
| M8 critical memory edge (r2) | `available < rules.critical.memoryFraction * limit` becomes `<=` | memory under 5% and disk under 1 GiB are critical, at their edges |
|
||||
|
||||
## Gate (Sage's slot, on CLEAR only)
|
||||
|
||||
- The three test files, each whole, then `node --test tests/` in packages/supervisor as the package
|
||||
suite.
|
||||
- Then M1 to M8, each on its named test, restored after each.
|
||||
- umask 022, TMPDIR from `mktemp -d /mnt/storage/scratch/tmp/gXXXXXX`.
|
||||
- The kit is /mnt/storage/scratch/morgana-r257 (slot257.sh, mutate.py). It refuses unless HEAD is the
|
||||
candidate and the tree is clean, and stops after two unexpected failures (mos-0823).
|
||||
- Static checks before the slot: `node --check` on every file, run from `/`. No test has run yet.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Row 202 BUILD: FED R5, `mosaic access list|grant|revoke`
|
||||
|
||||
Quackerjack, 2026-10-20. Plan: `PLAN.md` (commit 2d0aa2937). Manifest: `MANIFEST.md` in this directory.
|
||||
Brief: `docs/plans/2026-10-20_federation-r1-r2-r5.md`, "Brief: FED R5". Reviewer Duckworth. Gate owner Sage.
|
||||
Review record: round 2 approved as duckworth-0041 (commit 2a1d17da7) on 48a3fb96e; viewer carry c204bef73 after it.
|
||||
|
||||
## What was built
|
||||
|
||||
- `packages/cli/src/access.mjs`: the three verbs, the human proof, the context (`accessContextFor`), the
|
||||
atomic write, the lock and the receipt.
|
||||
- `packages/cli/src/cli.mjs`: routes `access` to `accessCommand`.
|
||||
- `scripts/mosaic`: sends `access` to the CLI.
|
||||
- `packages/cli/README.md`: the verbs, the human proof, the lock, the `instances` limit, the removal order,
|
||||
and the limits on peers and subjects.
|
||||
- `packages/cli/tests/access.test.mjs`: 21 tests (round 2; 18 in round 1). Single file, green on this branch.
|
||||
|
||||
R1 (`packages/business/src/access.mjs`) is merged in unchanged at 80fe31071, the Duckworth-approved
|
||||
candidate. 44e36fc4c, the next commit on that branch, only removes a stray pyc file.
|
||||
|
||||
## The lock file (not in PLAN.md; added at Sage's request)
|
||||
|
||||
- Path: `<configDir>/access.json.lock`, the access file's path plus `.lock`. Mode 0600.
|
||||
- Created with `O_EXCL` (`wx`). The holder writes its pid and a newline into it, then holds it for the
|
||||
read, the check, the temp write and the rename. It is removed in a `finally`.
|
||||
- A second writer that finds the lock reads the pid. If the pid is alive (or the pid is reused by a live
|
||||
process), it refuses with `refused: ... being written by another command; try again` (exit 3). Nothing
|
||||
is written.
|
||||
- A lock whose pid is gone (`kill(pid, 0)` returns ESRCH) also refuses, and nothing reclaims it. The
|
||||
refusal names the path and the pid. A human checks that no write is running and removes the file. This
|
||||
is the crash case: the holder died mid-write, and a human clears it.
|
||||
- An empty lock, or one that is not a pid, refuses. Its holder may still be between create and write. A
|
||||
holder that died in that gap leaves an empty lock; a human removes it.
|
||||
|
||||
Reclaim was removed at Sage's ruling (sage-1429, 10-20). It was the one design with a race, so the
|
||||
race below is gone, not accepted.
|
||||
|
||||
Former limit, now removed: reclaim race. The old design let a writer take a stale lock by unlinking it
|
||||
and retaking it. The case that lost: writers A and B both read the same dead pid N. A unlinks the lock
|
||||
and takes it. B then unlinks A's live lock and takes its own. Both read the same revision, both write
|
||||
it, and the second rename silently replaces the first. The revision check did not catch it, because both
|
||||
had read the old revision before either wrote.
|
||||
|
||||
Known limits, stated plainly:
|
||||
1. A dead holder's lock blocks every write until a human removes it. That is the fail-closed price of
|
||||
no reclaim. The refusal says which pid and which path.
|
||||
2. A reused pid reads as alive, so a stale lock refuses. The fix is to remove the file by hand.
|
||||
3. The lock is per access file. It does not cover the bus's own reads.
|
||||
4. Crash-safety of the write path: the temp file is removed on any failure before or at the rename. The
|
||||
directory is not fsynced after the rename, so a power loss right after a rename can lose the rename.
|
||||
The bytes are never mixed.
|
||||
|
||||
Tests for the lock (all in `access.test.mjs`):
|
||||
- "a live holder's lock refuses and leaves the file and the lock alone": pid = this process.
|
||||
- "an empty or unreadable lock refuses": empty file, and `not-a-pid`.
|
||||
- "a lock left by a dead pid refuses, names the pid, and is not reclaimed": a child that has exited. The
|
||||
file, the lock and the receipt are unchanged.
|
||||
- "the lock is mode 0600 while held and removed after a refused write".
|
||||
|
||||
Tests for the write path and the refusals (added in this round):
|
||||
- "a failed rename leaves the old bytes, removes the temp file, and writes no receipt": the fs seam
|
||||
(`deps.fs`) makes `renameSync` throw. Checks the old bytes, no `.access-*.tmp`, no lock, no receipt.
|
||||
- "a group-writable access file refuses with exit 3, names the file, and changes nothing": a 0664 file.
|
||||
Exit 3, the file path in the message, nothing written.
|
||||
- "an agent or tester caller is refused before the context or the access file is read": `contextFor`
|
||||
throws if called, and the file is 0664. The refusal must come first.
|
||||
|
||||
## The human proof, and the bus behavior behind test 10
|
||||
|
||||
The proof is one `project.list` call through `humanTransport`. The bus gates it with `#human`
|
||||
(`packages/bus/src/broker.mjs`, the `project.list` case at :2319, and `#human` at :377). The call is in
|
||||
one function, `proveHuman` in `access.mjs`, for R4 to swap.
|
||||
|
||||
Test 10 runs the real `human-cli.mjs` on a scratch bus from agent ancestry (`CLAUDECODE=1` in the child's
|
||||
env). The bus refuses. The client reports `outcome-unknown`, not `human-required`, because the bus closes
|
||||
the socket on a failed proof and `packages/bus/src/client.mjs:32-33` maps a closed socket to
|
||||
`outcome-unknown`. Exit 1 (`busExit`). The test pins that code and asserts the access file and the
|
||||
receipt are unchanged.
|
||||
|
||||
This is a bus behavior outside R5. It is not changed here. Gizmoduck and Gander own the verifier (row 196).
|
||||
Until it changes, a human who hits the agent refusal sees `outcome-unknown`.
|
||||
|
||||
## Context
|
||||
|
||||
`accessContextFor(system, env)`:
|
||||
- `providers`: `system.console.sso.providers[].id`. Row 192's shape (packages/webui/src/sso-config.mjs on
|
||||
row 192's candidate).
|
||||
- `ssoSubjects`: each provider's `subjects[].sub`, paired with the provider id. Gizmoduck's design says
|
||||
`console.sso.subjects`; Sage ruled that is shorthand for this (erratum).
|
||||
- `peers`: `[]` until row 203 adds the federation key. A grant that names a peer home refuses.
|
||||
- `businesses`: `loadBusiness` for each `<configDir>/businesses/*.json`; `human` and the role names as
|
||||
`instances`. A business file that fails to load fails the command.
|
||||
|
||||
No `console` key gives empty providers and subjects, so every grant naming a subject refuses. The
|
||||
`checkConsole` check from row 192 is not on this branch yet. R5 rebases onto row 192's landing, and then
|
||||
reads `console` through `checkConsole`.
|
||||
|
||||
## Mutant list (one per refusal; run on Sage's go, before review)
|
||||
|
||||
| # | Mutant | Expected kill |
|
||||
|---|---|---|
|
||||
| M1 | remove the `refuseInsideAgent` call | "an agent caller is refused before the bus is asked" |
|
||||
| M2 | remove the `--tester` check | "--tester is refused on every verb" |
|
||||
| M3 | the proof's error is swallowed | "a bus that refuses the human proof blocks the write" |
|
||||
| M4 | the revision compare is removed | "a stale revision refuses with revision-conflict" |
|
||||
| M5 | `takeLock` unlinks a dead-pid lock and retakes it (the reclaim this round removed) | "a lock left by a dead pid refuses, names the pid, and is not reclaimed" |
|
||||
| M6 | `pidAlive` always returns true (a dead pid reads as live) | "a lock left by a dead pid refuses, names the pid, and is not reclaimed" |
|
||||
| M11 | `writeAtomic` writes straight to access.json, no temp file or rename | "a failed rename leaves the old bytes, removes the temp file, and writes no receipt" |
|
||||
| M12 | the BusinessError catch in `accessCommand` is removed | "a group-writable access file refuses with exit 3, names the file, and changes nothing" |
|
||||
| M13 | `contextFor()` is called before the agent and tester refusals | "an agent or tester caller is refused before the context or the access file is read" |
|
||||
| M7 | `checkAccess` is skipped before the write | "a grant naming an unknown instance is refused before any write" |
|
||||
| M8 | the receipt append is removed | "grant writes ... a receipt line" |
|
||||
| M9 | a project name is written as given (no lookup) | "a project name resolves to its id" |
|
||||
| M10 | the lock is not taken | "a live holder's lock refuses" |
|
||||
|
||||
Round 2 closed the direct-write gap: `writeAtomic` takes the fs calls through `deps.fs` (default: the real
|
||||
`node:fs` functions), and a rename that throws is tested (M11 is the direct-write mutant). Not yet run:
|
||||
the mutant batch, the cli suite and `scripts/test-*.sh`. All wait on Sage's slot.
|
||||
|
||||
Round 2 also changed the file's refusal path: a `BusinessError` from `loadAccess` or `loadBusiness` now
|
||||
exits 3 with the file path and key path on stderr, no stack trace (Duckworth B1, Sage sage-1437).
|
||||
|
||||
## Not done in this row
|
||||
|
||||
- `--peers` and the federation key: row 203.
|
||||
- `access.reload`: row R4.
|
||||
- Gizmoduck's `console.sso.subjects` name: erratum, the flattened shape is used.
|
||||
- The suite (`packages/cli` and every `scripts/test-*.sh`) and the mutant batch: on Sage's gate.
|
||||
@@ -0,0 +1,814 @@
|
||||
# Row 191 stopRun inspection round
|
||||
|
||||
Design: ../DESIGN-r2.md, approved by Dewey in 29473; scope sage-2244;
|
||||
per-run contract ../STOP-RUN-CONTRACT.md, honker-0079/0081. Not review-ready:
|
||||
full package/shell/Docker gate and acceptance/guard mutants remain pending.
|
||||
|
||||
Current commit 7aa18b0f6e1a09188fb602853ebb02815b642ac9, new branch
|
||||
rocko/row191-stoprun-dcd8-20261022, parent dcd8d4dda. Worktree
|
||||
/mnt/storage/scratch/rocko-r191-r3. Earlier inspection f7bf7dfec is preserved.
|
||||
All 79 manifest paths have identical blobs at f7bf7dfec and 7aa18b0f6;
|
||||
rebase-product-diff.stat is empty, rebase-product-check.txt names the check.
|
||||
The manifest SHA-256 stays
|
||||
9971ac5fe56d0d36eb914ecaf2c282e14ab3cd29860fd27b268ec0baadaf0e52.
|
||||
Rocko author and committer identity were selected explicitly. No attribution
|
||||
lines, force-push or branch deletion. Sage publishes through jarvis.
|
||||
|
||||
## Changes Dewey must review: two focused-run fixes
|
||||
|
||||
1. packages/bus/src/broker.mjs, requestHost: the new processingWork guard was
|
||||
initially inserted into the inner refusal logger's try block. That let the
|
||||
original request reach validation, then prevented its refusal being logged
|
||||
while halted, returning storage-unavailable. Move it to the main admission
|
||||
try, before keys/credentials/authority/effects. The refusal logger retains
|
||||
its existing unconditional audit behavior. The new host-admission test now
|
||||
expects processing-halted and proves neither executor method was called.
|
||||
Initial evidence: ../logs/stoprun-focused-packages_bus_tests_processing.test.mjs.log.
|
||||
|
||||
2. packages/bus/tests/processing.test.mjs, real socket await test: row 196's
|
||||
asynchronous kernel-peer subprocess now precedes dispatch. The old 5 ms
|
||||
server deadline could fire during that subprocess, before the processing
|
||||
await branch extends it. This test already stubs authenticateHuman; give it
|
||||
a deterministic peerReader too, with read and close, and assert exactly two
|
||||
proof reads for halt/resume. Keep the 5 ms ordinary timeout and 30 ms halt
|
||||
delay. Production peer discovery, proof/transport and timeout behavior are
|
||||
unchanged. The test still fails if the async branch timeout is removed.
|
||||
The first fixture retry omitted close(); its teardown failed and left its
|
||||
own server running. Only that verified test group 2831504 was killed. Both
|
||||
unsuccessful logs remain. Corrected evidence:
|
||||
../logs/stoprun-focused-bus-processing-fixed-2.log (14/14).
|
||||
|
||||
These are diagnosed code/fixture mistakes, not declared flakes. No failed log
|
||||
was removed and no existing security assertion was weakened.
|
||||
|
||||
## Focused evidence, sage-2250
|
||||
|
||||
Ran singly on 41b0cf755 because none of row 254's seven paths overlaps ours.
|
||||
Sage-2265 carries this evidence to dcd8d4dda after the empty 79-path diff.
|
||||
Scripted busy check exited 0 before the first file. Every test runner used its
|
||||
own process group, 180 s timeout, scratch TMPDIR
|
||||
/mnt/storage/scratch/rocko-r191-tmp and DOCKER_HOST=unix:///nonexistent.sock.
|
||||
There was no Docker, browser, package suite, shell suite or mutant run.
|
||||
|
||||
| File | Final passed | Failed | Skipped |
|
||||
|---|---:|---:|---:|
|
||||
| processing-control/tests/stop-run.test.mjs | 12 | 0 | 0 |
|
||||
| processing-control/tests/runtimes.test.mjs | 10 | 0 | 0 |
|
||||
| processing-control/tests/coordinator.test.mjs | 3 | 0 | 0 |
|
||||
| processing-control/tests/containers.test.mjs | 6 | 0 | 0 |
|
||||
| processing-control/tests/effects.test.mjs | 5 | 0 | 0 |
|
||||
| processing-control/tests/task.test.mjs | 7 | 0 | 0 |
|
||||
| bus/tests/processing.test.mjs | 14 | 0 | 0 |
|
||||
| bus/tests/processing-runtime.test.mjs | 2 | 0 | 0 |
|
||||
| bus/tests/project-runtime.test.mjs | 3 | 0 | 0 |
|
||||
| Script sum | 62 | 0 | 0 |
|
||||
|
||||
All final runners exited 0. The batch stopped at the initial bus failure,
|
||||
then that file was diagnosed/rerun before the final two files. Counts are from
|
||||
focused-run-table-final.json and include only each final successful file log.
|
||||
Earlier bus failures are reported above, not silently included as green.
|
||||
Approximate run span 23:59Z–00:00:41Z; start second was not recorded.
|
||||
Cwd-scoped node/chromium sweep was empty at 00:00:41Z; slot returned to Sage.
|
||||
|
||||
## stopRun implementation
|
||||
|
||||
The export resolves host-owned run identity even without a bound bus claim.
|
||||
It checks boot/start/group identity immediately before each signal, preserves
|
||||
verified member anchors, waits 5 s before KILL and observes for 2 s. Signals
|
||||
are acknowledgements only, never invented. Root/business/run cache joins the
|
||||
same Promise. Atomic hard-link publication plus durable exclusive claim
|
||||
coordinates other host processes. First owner/reason/deadline/result persists;
|
||||
completed retries never re-escalate or overwrite that result. Halt links its
|
||||
generation to the first fire receipt without relabelling the runtime's stop
|
||||
cause. A later verified exit is separate immutable observation evidence.
|
||||
The seam writes no bus events. Honker maps killed/alive to lifecycle events.
|
||||
|
||||
No global unit, live config, binding, token, slot file or live store touched.
|
||||
No schema migration. First activation still requires stopping old processing
|
||||
before trusted initialization and aligning the copy/image/read-only mount in
|
||||
Mos's window. Existing result.json evidence remains write-once.
|
||||
|
||||
## Integration and outstanding work
|
||||
|
||||
The 12 latest-base conflict resolutions are listed in STATUS.md. New landed
|
||||
host execution, publish and library sync paths are now wrapped in processing
|
||||
scopes; host credential cancellation composes with its existing revocation
|
||||
signal. This changes only files already owned by 191. Those wider integrations
|
||||
still need the full owning-file/package gate and mutant evidence.
|
||||
|
||||
Fifteen proposed per-run mutants preflight exact once in stoprun-mutants.json;
|
||||
none has been applied or run. The broader kill-switch acceptance/guard table
|
||||
and executable mutant runner are still being completed. No kill count or
|
||||
review-ready claim is made. Ask Sage for the later CLEAR when the packet is
|
||||
complete, as sage-2259/2262 requires.
|
||||
|
||||
### Gate preparation after the focused slot
|
||||
|
||||
The published product implementation is unchanged. Three uncommitted test-file
|
||||
strengthenings are saved in post-inspection-tests.patch: effects.test.mjs adds a
|
||||
4-second bounded abort expectation for the watcher mutant; Discord's early
|
||||
start fence is also checked before a malformed journal can be read; WebUI's
|
||||
body limit now uses oversized valid JSON, and a direct-handler test isolates
|
||||
its own origin/site guards from serve's outer fence. These edits are unrun and
|
||||
are not included in the published 9971ac5f manifest. They must pass before a
|
||||
new candidate is cut. Thus 62/62 is prior focused evidence, not a claim for
|
||||
these changed test bytes.
|
||||
|
||||
The complete proposed table has 70 exact-once sites in all-mutants.json,
|
||||
including expected killing test names. Static preflight is clean; no mutant
|
||||
has been applied. run-mutants.py validates preflight, uses new process groups,
|
||||
tracks only its own verified descendant identities, kills those groups on
|
||||
timeout, checks named TAP failures, and restores all source bytes in finally.
|
||||
A timeout or wrong-test failure is not counted as a valid kill. The table and
|
||||
runner are prepared evidence, not a kill result. The requested nine-package,
|
||||
ten-shell, seventy-mutant gate is in gate-plan.json; Sage CLEAR is pending.
|
||||
|
||||
## Published test-strengthening commit and full gate (sage-2269/2271)
|
||||
|
||||
81889b58d57889b12a584aa225e1ac26e90e5700 is one fast-forward commit on
|
||||
7aa18b0f6, same branch rocko/row191-stoprun-dcd8-20261022. It commits the
|
||||
three test changes saved in post-inspection-tests.patch; that patch remains
|
||||
historical evidence, not an uncommitted candidate. Product implementation is
|
||||
unchanged. Earlier manifest 9971ac5f belongs to 7aa18b0f6; candidate.txt now
|
||||
names the manifest for 81889b58d. Sage published this tip before granting CLEAR.
|
||||
|
||||
The full gate checks HEAD equality and clean product bytes before its first
|
||||
file. Changed test files run singly, then nine packages, all ten shell suites,
|
||||
then seventy mutants. Each step gates on its exit status and retains its full
|
||||
log. run-gate.sh uses pipefail and an EXIT trap for the label-verified Compose
|
||||
network rocko-r191-gate_default, project rocko-r191-gate. TMPDIR is
|
||||
/mnt/storage/scratch/tmp/rocko-r191-gate. Docker is available only for test-task
|
||||
and test-release; other steps use unix:///nonexistent.sock. Live provider calls
|
||||
are disabled. No daemon, socket, volume or live configuration changes.
|
||||
|
||||
Gate start: 2026-10-22T00:18:00Z. Results remain pending until gate-results.json
|
||||
and mutant-results.json provide actual exit statuses and counts.
|
||||
|
||||
### First full-gate stop and fixture correction (sage-2274)
|
||||
|
||||
The 81889b58d run stopped at its first failure, 00:18:00Z–00:19:21Z.
|
||||
27 files completed: 268 passed, 1 failed, 0 skipped. Packages, shells and
|
||||
mutants did not start. Full logs/results are in gate-attempt-81889/.
|
||||
The tester refusal test's fixture requested tester:true over TCP, which row
|
||||
196 now refuses before route handling. This is a stale fixture, not a flake.
|
||||
The correction uses the existing startTesterSocket helper, a private fixture
|
||||
root, socketPath requests, TESTER_ORIGIN and no human identity header. Every
|
||||
assertion remains; the request now reaches the intended processing refusal
|
||||
rather than failing listener setup or the outer identity fence. A grep of all
|
||||
changed test files found no other tester:true TCP setup. The cwd node/chromium
|
||||
sweep returned []; no Docker test workload ran. Commit c78f48946 is a single
|
||||
fast-forward test-only fix on 81889b58d, authorized by sage-2274. The complete
|
||||
gate restarts from its first single file only after Sage publishes this tip.
|
||||
|
||||
Sage-2275 published c78f48946 and granted full restart. Second attempt begins
|
||||
2026-10-22T00:21:03Z; any further failure stops before additional edits.
|
||||
Candidate manifest b49fd493dc75ef2484ffc255213dc8eeaaaa348dee416c8d26d1c2bab8e20067.
|
||||
|
||||
### Second attempt stopped; no further changes (sage-2275)
|
||||
|
||||
Run 00:21:03Z–00:23:50Z on published c78f48946: all 27 changed singles
|
||||
269/269, processing-control 61/61, bus 646/646. The CLI package emitted 26
|
||||
failed subtests before its verified own runner group 3119310 was stopped.
|
||||
Its result is interrupted, not a complete package count. Six gate-lane image
|
||||
checks refuse because scripts/testing/lane-image.mjs pins the old four COPY
|
||||
lines and 191 adds processing-control to Containerfile. Twenty launch-account,
|
||||
launch-limit-checks, launch-limits and launch-routes cases hit
|
||||
processing-uninitialized on fixture roots with no explicit control init.
|
||||
No edits, retries, later packages, shells, Docker test workloads or mutants
|
||||
followed this second failure. Parent runner exited; cwd node/chromium sweep
|
||||
returned [] at 00:23:50Z. Slot returned to Sage at 00:24:05Z with the exact
|
||||
failures and a request for scope on image-manifest and fixture initialization.
|
||||
This is an integration failure, not a declared flake or a passing gate.
|
||||
|
||||
## Image-input manifest and launcher fixture scope (sage-2278, sage-2282)
|
||||
|
||||
Commit eb3abba12 is one fast-forward commit on c78f48946. This is an image-input
|
||||
manifest change: scripts/testing/lane-image.mjs now pins the exact Containerfile
|
||||
COPY of packages/processing-control/src/state.mjs and entrypoint.mjs and hashes
|
||||
both named source paths, without a wildcard. Its only two changed lines add
|
||||
those inputs. Without them, a lane could reuse a stale image after a reader or
|
||||
entrypoint change. The image test fixtures copy both files and each has its
|
||||
own receipt-invalidating test. Mutant KS56 drops state.mjs from the hash list;
|
||||
its named killer is the state.mjs receipt-invalidating test. Static preflight
|
||||
is 71 sites / 0 errors. No new run result is claimed before CLEAR.
|
||||
|
||||
The launch-account, launch-limit-checks, launch-limits and launch-routes fixtures
|
||||
now explicitly call initializeControl(f.dataRoot) before constructing their
|
||||
launchers. Existing assertion lines are byte-identical. A new launch-limit-checks
|
||||
test leaves initialization off and asserts processing-uninitialized, no
|
||||
mayLaunch/authorizeLaunch, and no registered session. A live root gets control
|
||||
initialized by the bus broker. The re-pin order is bus start before any seat
|
||||
launch; a launcher started before that refuses. This supplements the existing
|
||||
window precondition to stop old processing before the first trusted init.
|
||||
|
||||
Sage-2282 supersedes the test-only image ruling: lane-image.mjs is a product
|
||||
blob, so mos-1200 carries no earlier results. The complete plan restarts from
|
||||
the first single file, all nine packages, all ten shells and all 71 mutants
|
||||
only after publication and Sage CLEAR. Earlier attempts remain in their
|
||||
separate gate-attempt directories; neither is a passing full gate.
|
||||
|
||||
Sage-2283 published eb3abba12. Current 85-path candidate manifest is
|
||||
01a2b33651414c9b0cb094d2108f9e39c48a41b79974dcc16bdec374f8371091.
|
||||
The gate prints HEAD and this digest, verifies every manifest blob before its
|
||||
first test, and carries no old results. Awaiting CLEAR after Gander 253.
|
||||
|
||||
## Full restart on eb3abba12 stopped at Discord (sage-2286)
|
||||
|
||||
Verified HEAD and all 85 manifest blobs before tests. Run span:
|
||||
2026-10-22T00:34:50Z–00:47:33Z. No old result carried.
|
||||
|
||||
| Step | Passed | Failed | Skipped | Exit |
|
||||
|---|---:|---:|---:|---:|
|
||||
| 32 changed test files singly | 304 | 0 | 0 | 0 |
|
||||
| processing-control | 61 | 0 | 0 | 0 |
|
||||
| bus | 646 | 0 | 0 | 0 |
|
||||
| cli | 761 | 0 | 0 | 0 |
|
||||
| discord | 190 | 2 | 0 | 1 |
|
||||
|
||||
Singles sum independently confirmed by awk over all 32 TAP logs. Discord's
|
||||
binding.test.mjs:201 STOP check and recover.test.mjs:145 ready recovery check
|
||||
both get processing-uninitialized before the expected behavior. Those older
|
||||
fixture roots do not explicitly initialize control. The gate stopped at this
|
||||
step; no subsequent package, shell suite, Docker test workload or mutant ran.
|
||||
Docker availability was checked read-only (29.7.2); no daemon/volume changes.
|
||||
The EXIT trap completed. Cwd node/chromium sweep returned [] at 00:47:49Z,
|
||||
source tree remains clean, and the slot was handed back at 00:47:58Z.
|
||||
No test or product edits or retries followed this failure; Sage has the table
|
||||
and the fixture-initialization recommendation. This is not a green full gate.
|
||||
|
||||
## Discord fixture init and all-package static sweep (sage-2294)
|
||||
|
||||
Commit e6800380dc0765ce3f1477044e2ce4f773ebb43c is test-only and fast-forwards eb3abba12.
|
||||
Only binding.test.mjs and recover.test.mjs change: one initializeControl import
|
||||
and one initializeControl(dep.dataRoot) call in each affected CLI fixture.
|
||||
Every original assertion line is retained. Product blobs are unchanged.
|
||||
FIXTURE-SWEEP.md and fixture-sweep.json enumerate 338 source call-site hits,
|
||||
with the two fixes and the already-initialized chains, plus the screens for
|
||||
rootless optional contexts and read-only/usage CLI calls. The generator reads
|
||||
repository source only and invokes no fixture. Deliberate missing/deleted/
|
||||
corrupt-state refusal tests remain negative; no shared helper masks them.
|
||||
|
||||
Current manifest be36af8f0855f57336e6a33283aa0a8e94297f338b1f8180936cbccf1119c60a, 87 paths. Sage-2294 carries only unchanged
|
||||
processing-control 61/61, bus 646/646, CLI 761/761 and unchanged singles from
|
||||
gate-attempt-eb3/. The resume must run both touched files wholly, whole Discord,
|
||||
then the never-run WebUI/tasks/seat/project/library packages, every shell suite
|
||||
and all 71 mutants. No result is claimed for the new test bytes before CLEAR.
|
||||
The live initialization order remains bus start before any seat or Discord
|
||||
start; no fixture change initializes or repairs a live root.
|
||||
|
||||
## Resume on e6800380d stopped at WebUI (sage-2311)
|
||||
|
||||
Start 2026-10-22T01:27:40Z; failed-step exit 01:28:45Z. Startup verified HEAD,
|
||||
manifest be36af8f and all 87 blobs, and read-only Docker availability (29.7.2).
|
||||
|
||||
| New step | Passed | Failed | Skipped | Exit |
|
||||
|---|---:|---:|---:|---:|
|
||||
| binding.test.mjs whole | 15 | 0 | 0 | 0 |
|
||||
| recover.test.mjs whole | 5 | 0 | 0 | 0 |
|
||||
| Discord whole | 192 | 0 | 0 | 0 |
|
||||
| WebUI whole | 314 | 4 | 1 | 1 |
|
||||
|
||||
Carried unchanged under sage-2294: 32 singles 304/304, processing-control 61/61,
|
||||
bus 646/646 and CLI 761/761, from gate-attempt-eb3/. These are explicitly carried
|
||||
results, not fresh runs on e6800380d.
|
||||
|
||||
Four WebUI failures are retained in gate-logs/package-webui.log:
|
||||
- board-held.test.mjs:91: processing's config/state GETs omit redirect:manual.
|
||||
- board-held.test.mjs:194 and send-browser.test.mjs:58: processing's independent
|
||||
hash/focus/interval refresh continues to read after the shell signs out.
|
||||
- shell.test.mjs:104: the added control bar makes the short desktop page 1095px
|
||||
high at a 1000px viewport. Its new height is not accounted for by the existing
|
||||
frame/sidebar/footer layout.
|
||||
|
||||
Static diagnosis: processing.js ignores the existing body.signed-out state and
|
||||
mosaic-signed-out event that app.js and bus.js already share. Its GETs also lack
|
||||
manual redirect handling. processing.css adds an external bar while shell.css's
|
||||
frame minimum and sidebar heights still subtract only bar-h and foot-h. These
|
||||
are product integration defects; no assertions were weakened and no fix was
|
||||
made after the failed step. Sage received the table and recommendations.
|
||||
|
||||
One TAP skip: the editor rebuild requires exactly esbuild 0.28.2; installed
|
||||
shared node_modules has 0.28.1. Full skip line is in tap-skips.json. No shell
|
||||
suite ran, so there are no ^skip shell lines (skip-lines.txt remains empty).
|
||||
No tasks/seat/project/library suite, shell Docker workload or mutant ran after
|
||||
WebUI failed. Parent runner exited, EXIT cleanup completed, cwd node/chromium
|
||||
sweep returned [] at 01:29:00Z, source tree is clean, and slot returned to Sage.
|
||||
No retries or source edits were made. This is not a passing gate.
|
||||
|
||||
## Console shared sign-out and bounded desktop layout (sage-2317)
|
||||
|
||||
Commit 8e1f55c1db6a096e227b5114cd13a7142493b112, fast-forward on e6800380d, changes only the
|
||||
processing assets and their owning two tests. Current 87-path manifest:
|
||||
d3b014e6a0e9e547a29ae780eb27b1c0e57cc7b0e73dc49954abbff623929f41.
|
||||
|
||||
All three processing fetch sites use redirect:manual. responseBody dispatches
|
||||
the existing mosaic-signed-out event for an opaque redirect, or its signIn
|
||||
variant for exactly 401/sign-in required. No independent sign-out state is
|
||||
added: refresh/action guards read body.signed-out. The existing event cancels
|
||||
the interval, invalidates pending reads, clears stale state and disables
|
||||
writes. In-flight answers cannot restore state after sign-out. Initial work
|
||||
waits for DOMContentLoaded because this deferred asset precedes app.js in the
|
||||
HTML and needs its shared sign-out listener registered first. app.js/bus.js
|
||||
and their proof baselines remain untouched.
|
||||
|
||||
The CSS keeps the control bar in normal document flow. On desktop, the frame
|
||||
flexes into the remaining viewport rather than imposing another full-height
|
||||
minimum below the bar. Fixed navigation retains the shell's command/footer
|
||||
bounds and existing responsive column widths. Mobile retains natural flow.
|
||||
No short-page, sidebar or footer assertion is changed; rendering remains to
|
||||
be verified on the next authorized gate, not claimed from static checks.
|
||||
|
||||
The VM fixture models the existing body class, document events and DOM ready;
|
||||
its original assertion lines are unchanged. Six new browser cases cover config,
|
||||
state and POST opaque redirects and sign-in-required replies, manual redirect
|
||||
requests, interval cancellation and no subsequent fetch from forced refreshes
|
||||
or clicks. KS57 removes manual redirect from the config GET; KS58 removes the
|
||||
refresh signed-out guard. Both name the config-redirect browser case as their
|
||||
killer. Static preflight: 73 exact-once sites / 0 errors. Syntax checks pass;
|
||||
no owning test or mutant has run on this commit.
|
||||
|
||||
A product blob changes, so sage-2317/mos-1200 carries nothing. The next CLEAR
|
||||
runs all changed files singly, all nine package suites, all ten shell suites
|
||||
with Docker available and all 73 mutants. The editor rebuild's esbuild0.28.1
|
||||
vs pinned0.28.2 skip remains host drift and is reported separately. Earlier
|
||||
failed attempts remain immutable in gate-attempt-* directories.
|
||||
|
||||
## Rebase onto row 202 (sage-2329)
|
||||
|
||||
New branch rocko/row191-stoprun-ddfb-20261022, tip eafcbd3e7d6ef6e259fb59d87a7b0857b42337aa,
|
||||
base ddfb29af7. The prior published branch is preserved; no force-push, delete
|
||||
or old-name reuse. Six Rocko commits rebased, with one manual conflict in
|
||||
scripts/mosaic's human CLI case: prepend halt|resume|processing and retain
|
||||
row 202's appended access. packages/cli/src/cli.mjs auto-merges retain the
|
||||
access import/branch and all three processing commands. CLI README retains
|
||||
both the Access and Instance processing switch sections.
|
||||
|
||||
The full old/new tip diff has 15 paths (base plans/queue/logs, row 202's new
|
||||
access files and its three shared CLI paths), 2327 insertions/32 deletions.
|
||||
Within row 191's owned paths only CLI README, cli.mjs and scripts/mosaic differ,
|
||||
with the row 202 additions. rebase-ddfb-log.txt, rebase-ddfb-tip-diff.stat,
|
||||
rebase-ddfb-owned-diff.stat and rebase-ddfb-range-diff.txt retain exact evidence.
|
||||
|
||||
Manifest fd0f2740b1cccadaaad8c613640fdddb0497cdab33ec2c0fd11debf3cae6c4ee, 87 candidate paths. The package command lists were
|
||||
regenerated from this base; CLI includes access.test.mjs. All 73 mutant sites
|
||||
must preflight again. No tests ran, no result carries, and the complete gate
|
||||
waits for publication and Sage CLEAR after Megavolt's debug run.
|
||||
|
||||
Sage-2333 published eafcbd3e7. Their per-file patch-id comparison matches
|
||||
the prior six-commit range except the exactly ruled scripts/mosaic dispatch
|
||||
line; binary/secrets/attribution checks pass. Gate remains unrun pending CLEAR.
|
||||
|
||||
## eafcbd3e7 full gate stopped on timer fixture attribution (sage-2339)
|
||||
|
||||
Run 2026-10-22T02:18:04Z–02:19:25Z. HEAD, 87 blobs and fd0f2740 manifest
|
||||
verified. Short mktemp TMPDIR /mnt/storage/scratch/tmp/gxbC; named Compose
|
||||
project rocko-r191-gate. Nothing carried. 32 changed files completed:
|
||||
313 passed, 6 failed, 0 skipped. All six new processing-browser sign-out
|
||||
cases stop at their common precondition, processingTimers length2 vs1.
|
||||
The original two browser cases pass. Their recorder attributes every 5000ms
|
||||
interval to processing, but app.js:307 already arms its own freshness timer.
|
||||
The intended processing interval is at processing.js:87. These new cases have
|
||||
not yet exercised sign-out or killed KS57/KS58, so no behavior proof is claimed.
|
||||
|
||||
The gate stopped at this first red step. No package, shell, Docker workload
|
||||
or mutant ran; the esbuild rebuild was not reached. Runner exited, EXIT cleanup
|
||||
completed and cwd node/chromium sweep returned [] at 02:19:34Z. No source edit
|
||||
or retry followed the failure. Sage received the exact failure and a proposed
|
||||
test-only caller-attribution filter with the assertions unchanged. This is a
|
||||
fixture error, not a passing gate or a declared flake.
|
||||
|
||||
## Timer recorder attribution correction (sage-2345)
|
||||
|
||||
Commit 4db8b57785895bc62f5f015468a69515da7fc77f, fast-forward on eafcbd3e7, is a one-line
|
||||
test-only correction in processing-browser.test.mjs. Cause: app.js:307's
|
||||
freshness interval is also 5000ms, and the recorder incorrectly counted it as
|
||||
a processing interval. The recorder now requires the processing.js caller
|
||||
stack. Every assertion remains byte-identical, including exactly one timer
|
||||
before sign-out and zero after it. A second 5000ms interval from processing.js
|
||||
would still be counted and fail the exactly-one assertion. No product byte
|
||||
changes, and no test or mutant ran for this correction.
|
||||
|
||||
Manifest ffb46fbdb875f97ff2eb7a7bed8bf6d5c4df53b8a63c738ffcde5fc5f6b7f6ad, 87 paths. Full restart after publication and CLEAR,
|
||||
with nothing carried. Sage-2345 counts the timer-fixture red as the first in
|
||||
this 191 window; any second red stops and is escalated to Mos. Previous logs
|
||||
are retained in gate-attempt-eaf/.
|
||||
|
||||
## Full gate on 0f8235277: second red (sage-2351)
|
||||
|
||||
Candidate: 0f8235277e1fc21588edb6f2931d66c8e154a400 on ddfb29af7.
|
||||
Manifest: ffb46fbdb875f97ff2eb7a7bed8bf6d5c4df53b8a63c738ffcde5fc5f6b7f6ad.
|
||||
No prior result carried. Start 2026-10-22T02:33:41Z; failed-step exit
|
||||
2026-10-22T02:46:17Z. Fresh TMPDIR /mnt/storage/scratch/tmp/giNF.
|
||||
Compose project rocko-r191-gate, trapped ownership-checked network cleanup.
|
||||
|
||||
| Step | Passed | Failed | Skipped | Exit |
|
||||
|---|---:|---:|---:|---:|
|
||||
| 34 changed test files singly | 330 | 0 | 0 | 0 |
|
||||
| processing-control | 61 | 0 | 0 | 0 |
|
||||
| bus | 646 | 0 | 0 | 0 |
|
||||
| cli (including access) | 783 | 0 | 0 | 0 |
|
||||
| discord | 192 | 0 | 0 | 0 |
|
||||
| webui | 323 | 1 | 1 | 1 |
|
||||
| tasks, seat, project, library | not run | — | — | — |
|
||||
| all ten scripts/test-*.sh | not run | — | — | — |
|
||||
| all 73 mutants | not run | — | — | — |
|
||||
|
||||
Singles sum independently confirmed with awk over all 34 TAP logs.
|
||||
The sole failure is shell.test.mjs:380 in the full shell-browser test:
|
||||
scrollHeight 1062 vs innerHeight 1000. Full evidence is
|
||||
gate-logs/package-webui.log (not ok 271, line 1626).
|
||||
All prior manual-redirect/sign-out failures and eight processing-browser cases
|
||||
pass, but the desktop short-page geometry is still wrong. No passing layout
|
||||
or full-gate claim is made.
|
||||
|
||||
Known host-drift TAP skip: editor rebuild requires exactly esbuild 0.28.2,
|
||||
while shared node_modules has 0.28.1. Full line is in tap-skips.json. This skip
|
||||
is not counted as a red. No shell suite ran, so skip-lines.txt has no ^skip
|
||||
shell line. No Docker test workload or mutant started.
|
||||
|
||||
Stopped at the first failed step without retry or product edits, per sage-2351.
|
||||
This is the second red in the 191 window and is with Sage for Mos escalation
|
||||
under mos-0823. Parent runner exited; EXIT cleanup completed. Cwd-scoped node
|
||||
and chromium sweep returned [] at 02:47:14Z, product worktree is clean, and the
|
||||
slot was returned. No in-review move was made.
|
||||
|
||||
|
||||
### mos-1248 / sage-2363: desktop viewport allocation (unrun static candidate)
|
||||
|
||||
The second red on 0f8235277 was shell.test.mjs:380 (1062 versus 1000).
|
||||
The shell gives .frame a viewport-based minimum at packages/webui/src/public/shell.css:135;
|
||||
processing.css:12-14 replaced that minimum but used a column flex body with only
|
||||
min-height:100vh, so the new bar was not allocated a bounded share of the viewport.
|
||||
The desktop body now uses explicit grid rows for the command bar, controls, optional
|
||||
banner, frame and footer. The frame row is minmax(min-content,1fr): it takes the
|
||||
remaining short-page space while long content may grow the document. No clipping,
|
||||
overflow suppression, fixed body height, test viewport change or assertion removal.
|
||||
The existing shell.test.mjs:380 equality stays byte-identical. Added assertions
|
||||
check actual processing-bar visibility, positive height, viewport placement and
|
||||
no document scroll at 1440x1000; the existing 1440x420 long-page checks remain.
|
||||
KS59 reverts processing.css to 0f8235277's bytes and must fail that same shell
|
||||
browser test. Total: 74 planned mutants. No test or mutant has run on this fix.
|
||||
Webby must statically review this delta before Sage opens the full no-carry gate.
|
||||
|
||||
|
||||
### Correction after Webby B1 (webby-0104, 2026-10-22 03:00Z)
|
||||
|
||||
The cause stated in the mos-1248/sage-2363 note above is withdrawn.
|
||||
processing.css loads after shell.css and already overrides the frame minimum.
|
||||
With the tree fixed and inspector hidden, the remaining floor is the Board's
|
||||
main content. The grid min-content row preserves that floor, so 5cd32f4c1
|
||||
is not an accepted layout fix. Its static review is CHANGES at Webby's
|
||||
REVIEW-layout.md, commit e2e7c60e5. No run established a new layout measurement.
|
||||
Next step requires a granted fixture probe for main/control heights and an
|
||||
owner layout ruling; no further product change or gate is authorized yet.
|
||||
Existing shell assertions remain unchanged.
|
||||
|
||||
|
||||
### Static nav placement: mos-1249/mos-1250, sage-2383/2385/2386, webby-0106/0108
|
||||
|
||||
Fast-forward on 5cd32f4c1; the rejected grid is removed. The unchanged labeled
|
||||
region is now the first child of nav.tree, before its Console h2. The banner
|
||||
stays at its original body location with every id, role, label and shipped
|
||||
string unchanged. No processing.js change and no runtime reparenting.
|
||||
At >=760 the tree is the same fixed box, with the same width breakpoints and
|
||||
ordinary overflow:auto. The controls are non-shrinking and non-sticky; they
|
||||
scroll with overflowing nav links (sage-2386 withdraws persistent visibility).
|
||||
Below760 the region takes a full flex line before Console/Projects. Its mobile
|
||||
location changes from before .frame to first in the static nav; the banner
|
||||
therefore precedes it in a halted mobile view. Banner behavior itself is unchanged.
|
||||
Specificity (0,3,0) resets .btn margin/order and .small margin/basis, leaving
|
||||
the shell rules unchanged. No content is shortened and no overflow is hidden.
|
||||
|
||||
Correct height arithmetic (Webby B1): page=max(innerHeight,86+C), where C is
|
||||
main content height. Controls consume space within the existing nav box, not
|
||||
another body row. The old failing layout was max(innerHeight,86+barHeight+C).
|
||||
The halted banner remains a body row; Sage tracks that separately.
|
||||
|
||||
Candidate-test design change at processing-ui.test.mjs:67 under mos-1250.
|
||||
Old assertion:
|
||||
```js
|
||||
assert.ok(html.indexOf('id="processing-controls"') < html.indexOf('<div class="frame">'));
|
||||
```
|
||||
New assertion text:
|
||||
```js
|
||||
const controlsAt = html.indexOf('id="processing-controls"');
|
||||
const mainAt = html.indexOf('<main id="main"'), inspectorAt = html.indexOf('<aside class="inspector"');
|
||||
assert.ok(controlsAt >= 0 && controlsAt < mainAt, 'controls are outside and before #main');
|
||||
assert.ok(!(inspectorAt < controlsAt && controlsAt < html.indexOf('</aside>', inspectorAt)),
|
||||
'controls are outside aside.inspector');
|
||||
```
|
||||
Reason: the bar moves into the frame's first column under mos-1249 while
|
||||
remaining outside route-specific #main and aside.inspector. This is a change
|
||||
to our unlanded candidate assertion, not a landed assertion.
|
||||
|
||||
All existing shell assertions stay unchanged. Added bar-inside-tree assertion
|
||||
and diagnostic height output. New processing browser coverage checks 400px
|
||||
Stop order=0, visibility, focus and position above Projects; also on-load
|
||||
visibility/focus at1440x1000,1440x420,800x420. Debug logs report controls/tree
|
||||
heights at those desktop sizes and main height; Sage judges controls over half
|
||||
the 334px nav at1440x420 before the window. No run yet.
|
||||
|
||||
Mutant delta: KS59 now reverts the complete layout (HTML and CSS together);
|
||||
KS60 hides the controls; KS61 moves them into #main; KS62 drops the (0,3,0)
|
||||
button reset. Total77. Every new site has its named killing test.
|
||||
|
||||
|
||||
### sage-2394 debug on published da4334806 (2026-10-22)
|
||||
|
||||
03:14:37Z–03:14:53Z: shell.test.mjs whole 5/5, processing-browser.test.mjs
|
||||
whole 9/9, 0 failures/skips. awk sum14/0/0. Original shell short-page and
|
||||
long-page assertions pass unchanged. Logs/results and empty cwd sweep are in
|
||||
debug-da4334806/. Slot clear sent Sage/Webby03:15:11Z after sweep.
|
||||
Controls/tree/main heights: read-only shell1440x1000 212.5/914/914; writable
|
||||
processing fixture1440x1000 127/914/914;1440x420 127/334/907.734375;800x420
|
||||
127/334/1131.65625. Stop visible/order0/focusable/aboveProjects at400x1000.
|
||||
Writable127 is below half the334 nav. The read-only212.5 height at1000 was
|
||||
flagged for Sage's usability call; that state was not measured at420, so no
|
||||
claim is made that all states use less than half the short nav.
|
||||
No mutants or full gate run under this debug grant. Await separate CLEAR.
|
||||
|
||||
|
||||
### R6 accepted: sage-2399 / webby-0111 (2026-10-22 03:16Z)
|
||||
|
||||
Sage accepts the short-nav usability tradeoff. Writable controls occupy38%
|
||||
of the334px nav at1440x420. The read-only recovery note makes that about64%,
|
||||
inferred from the measured212.5px height at the same width. The region is
|
||||
non-sticky and scrolls away; Projects was already below the fold at that size
|
||||
before191. Webby confirmed the debug TAP and has no window blocker. Sage
|
||||
tracks the read-only note length with the halt-banner follow-up; no change
|
||||
to this row.
|
||||
|
||||
Do not rebase or run now. Row273 lands first. On Sage's landing message,
|
||||
rebase the candidate including the two latest layout commits onto the new
|
||||
refactor under a new branch name (mos-1086), then provide range-diff,
|
||||
patch-ids and the refreshed88-product-path manifest. The full no-carry
|
||||
window requires a separate Sage CLEAR on that published tip.
|
||||
|
||||
|
||||
### Rebase after row273: sage-2402 (2026-10-22 03:42Z)
|
||||
|
||||
New branch rocko/row191-stoprun-86f9-20261022, tip
|
||||
0fa7bf421fc153a226b5f06ed484e089e48e23ba, on86f95bb27. All10 commits
|
||||
rebase cleanly and range-diff pairs each with '='; no conflict resolution.
|
||||
All88 per-file patch-ids match the old candidate. Whole product diff stable
|
||||
patch-id before/after:0846823aefe165363601a6c68134ad46cc701c73.
|
||||
87 product blobs are identical; only docs/TOOLS.md changes because the new
|
||||
base carries row273's --send/--no-sso usage and three explanatory F39 lines.
|
||||
191's TOOLS hunks stay unchanged. New88-path manifestSHA256:
|
||||
0bcbf337433f6d46d809ee1dcaf8a5a8503160403d273cf73d6dac64adef3c02.
|
||||
Range-diff, per-path patch-ids and TOOLS comparison are rebase-86f9-* files.
|
||||
Static77-site exact-once preflight has0errors. No tests run, nothing carried
|
||||
into the required full window; await Sage publication and separate CLEAR.
|
||||
|
||||
|
||||
### Full no-carry window on0fa7bf421: stopped at SR08 (sage-2407/2418)
|
||||
|
||||
03:44:56Z–04:01:29Z. All35touched files passed336/0/0; nine packages
|
||||
passed2403/0 with1known editor/esbuild skip. Every shell exited0:425selftests
|
||||
plus156queue node tests passed; the canonical-root queue check and two
|
||||
explicit live-provider exclusions are saved in skip-lines.txt. See
|
||||
GATE-RESULTS.md for per-suite counts and mutant table.
|
||||
SR01–SR07 killed. SR08 timed out at90seconds without named assertion failure
|
||||
(TAP header only); runner restored all88candidate blobs and stopped. The69
|
||||
later mutants did not run. SR08 is not counted as killed. Cwd sweep[] and
|
||||
owned network cleanup completed; slot clear sent Sage04:02:12Z.
|
||||
Saved mutation, timeout policy lines64–89 and shell/skip table sent Sage in
|
||||
response to sage-2418. No source edit or retry; no in-review move. Await
|
||||
Mos ruling and explicit Sage CLEAR before any control/replay/resume.
|
||||
|
||||
|
||||
### SR08 timeout policy: mos-1259 / sage-2423 (2026-10-22 04:07Z)
|
||||
|
||||
Cause confirmed by Mos: runtimes.mjs:333 loops on now() <= deadline, while
|
||||
stop-run.test.mjs:106-108 gives the second host a fixed fake clock with real
|
||||
sleep. Removing the join branch enters the stop loop without advancing that
|
||||
clock. Original SR08 log contains TAP header only; original timeout record
|
||||
stays immutable. Mos permits separate classification as killed-by-timeout
|
||||
only after the unmutated same whole test file passes the same90second
|
||||
harness in this resumed run. **Control result: not run; awaiting CLEAR.**
|
||||
No assertion kill is claimed for SR08.
|
||||
|
||||
Runner review requested from Dewey, Sage copied. SHA256 of run-mutants.py:
|
||||
fe0d360f86d86ab2df967da5338479c593c27c88d06eb38a1389d99a08489369. Diff:runner-mos1259.diff; baseline:
|
||||
run-mutants.before-mos1259.py. Shared execution harness uses90seconds, own
|
||||
process groups, verified descendant cleanup and restored source bytes. A
|
||||
control must pass every test with exit0, no not-ok/skip/cancel/fail. Its record
|
||||
binds invocation id, candidate HEAD/manifest and test hash; all88candidate
|
||||
blobs verified before and after control, and before each mutation. The
|
||||
approved historical SR08 classification and69-site SR09+resume use separate
|
||||
files so the original seven kills and timeout evidence remain intact.
|
||||
Assertion kills and timeout kills have separate counters. No control for the
|
||||
same file in this invocation means a timeout stops; survivors and invalid
|
||||
failures stop too. Every timeout gets a cause/control record before review.
|
||||
|
||||
No product/test change, no control/mutant execution. Wait for independent
|
||||
runner approval and Sage CLEAR. Green suites carry only on unchanged0fa7bf421
|
||||
and manifest0bcbf337. Test clock fix is deferred to Vikunja502 as Mos ruled.
|
||||
|
||||
|
||||
### Independent runner approval: dewey-0789 (2026-10-22 04:11:20Z)
|
||||
|
||||
Dewey APPROVES the frozen fe0d360f86d86ab2df967da5338479c593c27c88d06eb38a1389d99a08489369
|
||||
runner, static only. Use only the proposed --control-file stop-run.test.mjs
|
||||
--adjudicate-timeout SR08 --resume-from SR09 invocation. A positional-id or
|
||||
full run would overwrite the original evidence and is not this resume.
|
||||
|
||||
Report the complete tally as7 prior assertion kills (old status killed),
|
||||
plus SR08 separately adjudicated by timeout after control passes, plus69
|
||||
resumed sites. The summary file counts only this invocation. No control or
|
||||
resume result exists yet; Sage CLEAR remains required.
|
||||
|
||||
Classification change: the reviewed runner recognizes indented not-ok lines
|
||||
with ^\s* rather than only top-level lines. Controls therefore reject nested
|
||||
failures too, and mutant assertion kills still require the named pattern in
|
||||
a failing test name. SR01–SR07 evidence was classified by the original runner.
|
||||
|
||||
Historical SR08 has no HEAD field. Its binding to0fa7bf421 relies on the saved
|
||||
commit.txt/log timestamps and the new candidate HEAD+88blob check, not a
|
||||
retroactively invented HEAD field in the old result. Preserve this provenance
|
||||
in the adjudication report. Any newly detected timeout needs a concrete cause
|
||||
before review; the generic placeholder is not accepted review evidence.
|
||||
|
||||
|
||||
### Control and resume: sage-2427, stopped on KS43
|
||||
|
||||
Control/resume began04:13:44Z; last saved mutant result2026-10-22T04:14:08Z; process
|
||||
absence confirmed04:14:56Z and slot returned to Sage. Whole unmutated
|
||||
stop-run.test.mjs control passed12/12, exit0, no not-ok, no skips/cancels,
|
||||
in0.459seconds. The reviewed runner adjudicated original SR08 separately
|
||||
as killed-by-timeout under mos-1259. The immutable original SR08 row/log
|
||||
stay unchanged; adjudication binds the control, exact cause, current HEAD
|
||||
and manifest. Historical HEAD provenance remains as Dewey N4 described.
|
||||
|
||||
Resume stopped at KS43, not retried. It removes the inner processingHTTP
|
||||
origin check and its selected outer-route test passed1/1(exit0). Saved
|
||||
evidence:resume-mutant-logs/KS43.tap and resume-mutant-results.json.
|
||||
Tally:7prior assertion kills +49resumed assertion kills =56assertion kills;
|
||||
1historical timeout kill;1survivor(KS43);19unrun. No new timeouts or placeholder
|
||||
causes. All88candidate blobs restored and resume cwd sweep[]; no product or
|
||||
test change. No in-review move. Sage and Mos notified directly.
|
||||
|
||||
Static diagnosis only: KS43 selects processing.test.mjs:52's outer-server
|
||||
fence test. The unchanged direct-handler test at:95 already isolates this
|
||||
origin guard from serve's outer refusal. Its pattern was not selected. Sent
|
||||
recommendation to Sage/Mos to correct only the mutant's named killer after
|
||||
their ruling, then run KS43+19remaining. No metadata edit or rerun authorized
|
||||
yet; no claim the direct-handler test kills it until the run proves it.
|
||||
|
||||
|
||||
### KS43 metadata retarget: mos-1261 (2026-10-22 04:16:41Z)
|
||||
|
||||
Original pattern `processing write fences refuse` selected processing.test:52.
|
||||
KS43 survived with1passing test and no writer call, preserved in
|
||||
resume-mutant-results.json and resume-mutant-logs/KS43.tap. The serve-level
|
||||
test cannot isolate the handler guard: outer fences and later refusal can
|
||||
mask its removal. No product defect or new kill is claimed from static read.
|
||||
New pattern: `processing handler independently refuses wrong origin and cross-site before its writer`,
|
||||
the existing direct processingHTTP test at:95-107 with valid user/site/content
|
||||
headers, asserted403 and no writer call. Only KS43's pattern changed in the
|
||||
77-site table; product/test/mutation bytes unchanged. Generator mirrors it.
|
||||
|
||||
TableSHA256:5dbc8308c995e1974d6da63b3daa967896d822154eab38a8bb96154be667edf5. Diff:mutant-table-mos1261.diff,
|
||||
baseline:all-mutants.before-mos1261.json. Dewey must independently review
|
||||
this metadata diff before the next run. Static audit of all19unrun sites:
|
||||
mutant-pattern-audit-mos1261.md; no other serve-level/direct-handler mismatch
|
||||
with an existing direct test warrants retargeting. KS44 body-bound case has
|
||||
no direct-handler counterpart; its oversized whitespace+{} case is valid
|
||||
JSON without the bound, so it retains the existing pattern.
|
||||
|
||||
Carry56assertion kills+1timeout kill and green suites only on unchanged
|
||||
0fa7bf421. Next approved scope is KS43+19sites after review and Sage CLEAR.
|
||||
No run authorized by this metadata change.
|
||||
|
||||
|
||||
### Retarget review APPROVE and C1 archive: dewey-0791 (2026-10-22 04:19:55Z)
|
||||
|
||||
Dewey approves table5dbc8308 and the19-site audit, conditional on preserving
|
||||
the first resume evidence before any next run. Archive:mos-1261-archive/,
|
||||
full per-file inventory:mos-1261-archive/sha256-manifest.txt.
|
||||
|
||||
- `resume-mutant-results.json` SHA256 `f5725ef26cdb488441285081fadaa9ce4a3e7d390ab2a1c6d04acfd27f105fc6`
|
||||
|
||||
- `resume-mutant-logs/KS43.tap` SHA256 `c778ee33edb667f795ab560a328fd5c1c04ef6acf99d7b79c364f6302cf29864`
|
||||
|
||||
- `unmutated-control.json` SHA256 `ee9ddcd69d24276165abe1185f159a2737157fcae8fd37f731768e9324b765e5`
|
||||
|
||||
- `SR08-timeout-adjudication.json` SHA256 `214a04696d3329ef67bf10578daf4d251599816fd246bc31abecae1a6230e1e0`
|
||||
|
||||
The50-row result, all first-resume logs and control/adjudication records
|
||||
were copied byte-for-byte. Original paths remain untouched too. A separate
|
||||
retarget-mos1261/ directory holds byte-identical approved runnerfe0d360f,
|
||||
reviewed table5dbc8308 and identical candidate/commit files. The proposed
|
||||
--resume-from KS43 invocation there selects20sites and writes its own logs
|
||||
and results without overwriting either original or archive. No control flag
|
||||
is needed for these tests; any timeout without a same-file control stops.
|
||||
Final tally will combine7old assertionkills +49first-resume assertionkills +
|
||||
SR08separate timeout adjudication +20retarget results. Still no run; awaiting
|
||||
Sage CLEAR. Product/test bytes remain unchanged.
|
||||
|
||||
|
||||
### KS43 retarget run: sage-2436, stopped on KS44 invalid mutation
|
||||
|
||||
Run began04:21:06Z; KS43 is killed by the named direct-handler assertion.
|
||||
KS44 then fails import with SyntaxError Unexpected token catch at
|
||||
processing-http.mjs:40. The failing TAP name is the test file, not the
|
||||
selected test, so runner records invalid-failure and stops without a kill.
|
||||
Evidence remains in retarget-mos1261/console.log,resume-mutant-results.json
|
||||
and resume-mutant-logs/KS43.tap,KS44.tap. No original evidence overwritten.
|
||||
|
||||
Cause: our KS44 mutation uses `// no body bound` to replace an inline
|
||||
`if (size > 1024) throw Error();` in a one-line for-await body. The comment
|
||||
swallows the remaining chunks.push and closing brace. This is malformed
|
||||
mutation metadata, not an observed product failure. No metadata correction
|
||||
or retry yet. Proposed to Sage/Mos: replace only the new text with `void 0;`,
|
||||
then independent review and a new CLEAR for KS44+18 on unchanged candidate.
|
||||
|
||||
Current tally57assertionkills (7+49+1),1SR08timeoutkill,1invalid(KS44),18unrun.
|
||||
Cwd sweep[] and all88candidate hashes restored04:21:30Z; slot returned and
|
||||
Sage/Mos notified. No in-review move. Wait for ruling before any edit or run.
|
||||
|
||||
|
||||
### KS44 syntax metadata correction: mos-1264 (2026-10-22 04:22:16Z)
|
||||
|
||||
Only KS44.new changes from `// no body bound` to `void 0;`. Old site,
|
||||
test and pattern stay unchanged. The invalid import result remains in
|
||||
retarget-mos1261/resume-mutant-results.json and resume-mutant-logs/KS44.tap.
|
||||
TableSHA256:85d9a7b13ad510e840f258d892afbfce393cefbc4006e1f808605b83adc84570. Diff:mutant-table-mos1264.diff;
|
||||
baseline:all-mutants.before-mos1264.json. Generator mirrors the one field.
|
||||
|
||||
Authorized syntax-class preflight applied KS44–KS62 individually in scratch,
|
||||
ran node --check on each JavaScript file, and restored before advancing.
|
||||
All15JS checks passed; no other replacement fix was needed. KS59–KS62
|
||||
change only HTML/CSS and are explicitly asset-only/not-applicable for the
|
||||
Node parser; Mos/Sage informed, no false JS syntax pass claimed. All19
|
||||
mutations restored and88candidate hashes match. Per-site logs/results and
|
||||
restoration record:syntax-mos1264/. No behavioral test or assertion-mutant
|
||||
run occurred. Dewey must review table diff and syntax results before next
|
||||
Sage CLEAR for KS44+18. Carry57assertionkills+1timeoutkill only on unchanged
|
||||
0fa7bf421. No product or test change.
|
||||
|
||||
|
||||
### dewey-0795 approval: syntaxfixed-mos1264 staged (unrun)
|
||||
|
||||
Dewey APPROVES table85d9a7b1 and syntax preflight, static only. Conditions
|
||||
met:syntaxfixed-mos1264/ holds byte copies of runnerfe0d360f, table85d9a7b1,
|
||||
commit.txt,candidate.txt,candidate-manifest.sha256. Full SHA256 and byte-cmp
|
||||
list is syntaxfixed-mos1264/byte-copy-check.txt. Invocation after Sage CLEAR:
|
||||
python3 syntaxfixed-mos1264/run-mutants.py --resume-from KS44 (19sites).
|
||||
All previous invalid/survivor evidence stays untouched in its original dirs.
|
||||
No behavioral run occurred during preparation.
|
||||
|
||||
|
||||
### Final19 resume stopped at KS54: sage-2444 (2026-10-22)
|
||||
|
||||
Run began04:27:45Z; KS44–KS53 are10named assertion kills. KS54 then timed out
|
||||
at90.073seconds. Its log contains the expected dependency-list assertion:
|
||||
"the runtime refuses an invalid adapter and closes a valid one" at
|
||||
bus/tests/tasks.test.mjs:263. The mutation drops processing from runtime.mjs
|
||||
tasks factory deps. The test opens runtime then asserts before close at:265;
|
||||
a failing assertion skips the close and leaves Node alive. No same-file
|
||||
control ran in this invocation, so this is timeout-without-control, not a
|
||||
kill. No assertion or timeout kill is claimed for KS54.
|
||||
|
||||
Raw resume result's generic cause sentence says a control passed, which is
|
||||
incorrect: its control:null and timeout-without-control status establish the
|
||||
opposite. The raw evidence remains unchanged; the concrete cause and
|
||||
correction are in syntaxfixed-mos1264/KS54-timeout-report.json. This explicit
|
||||
report replaces the placeholder as explanation without altering the log.
|
||||
|
||||
Current tally67assertionkills(7+49+1+10),1adjudicatedSR08timeoutkill,
|
||||
1unqualifiedKS54timeout,8unrun. All88candidate blobs restored; cwd sweep[]
|
||||
and slot returned04:30:06Z to Sage, Mos directly copied as instructed. No
|
||||
retry, product/test edit or in-review move. Sage's disclosed syntaxcheck
|
||||
caused no busy refusal or foreign-node report. Await owner ruling/CLEAR.
|
||||
|
||||
|
||||
### Final controls/mutants GREEN: mos-1267, sage-2448, dewey-0798
|
||||
|
||||
On unchanged0fa7bf421, whole-file tasks control9/9 passed in0.815s; KS54
|
||||
rerun killed-by-timeout with the exact dependency not-ok at tasks.test:263.
|
||||
KS55–KS62 are8assertionkills. Final run04:35:32Z–04:37:17Z, cwd sweep[] and
|
||||
88hashrestore04:37:48Z; slot returned to Sage and direct handoff to Dewey/Mos.
|
||||
Final77-site tally:75assertionkills(7+49+1+10+8),2separate timeoutkills
|
||||
(SR08adjudication,KS54control+rerun). final-mutant-results.json consolidates
|
||||
latest accepted evidence by id; no prior record or log overwritten.
|
||||
GATE-RESULTS.md is the final per-suite/mutant table; prior narrative saved
|
||||
as GATE-RESULTS-before-final.md.
|
||||
|
||||
KS54 raw placeholder result saved as resume-mutant-results.raw.json before
|
||||
filling concrete cause in its review result. Same-invocation control id
|
||||
1792643732624962173; prior syntaxfixed eleven-row result file remains exact
|
||||
0ede036954ff77c21f29e1a744b21d1d9b18eb75736f78ecd1cf4ae559358911.
|
||||
Dewey independently read logs/control/restoration and confirmed tally in
|
||||
dewey-0798, explicitly not yet a manifest verdict. N6test cleanup stays
|
||||
deferred under mos-1267/Vikunja502; no candidate byte changes permitted.
|
||||
|
||||
|
||||
### Claimant review handoff
|
||||
|
||||
Queue move rocko-191-review-r1 entered in-review at revision3114, round1
|
||||
on#1689, manifest0bcbf337. Revision3115 records the expected missing-token
|
||||
request failure; command exit1. Receipt:in-review-receipt.txt. Sage posts
|
||||
the request as jarvis; Dewey owns the manifest verdict. Candidate source
|
||||
remains published0fa7bf421 with88product paths, base86f95bb27. No live
|
||||
action, push, source change or further gate authorized by this handoff.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Row279: stopRun deadline final observation
|
||||
|
||||
Scope: sage-2512, mos-1269; brief docs/plans/2026-10-22_stoprun-deadline.md.
|
||||
Base f40f2e770. Reviewer Dewey; gate owner Sage. V502 stays separate.
|
||||
|
||||
## Cause and smallest change
|
||||
|
||||
The loop's precondition now()<=deadline can skip the existing final status
|
||||
read when a sleep wakes beyond deadline. Use for(;;) so the existing
|
||||
now()>=deadline branch is the only time exit. Change that loop header only;
|
||||
all record/identity/ended exits and result mapping remain byte-identical.
|
||||
The old trailing fallback remains unreachable in the fixed loop. M1 restores
|
||||
only the old header and thereby restores the complete old fall-through path.
|
||||
|
||||
## Regression and evidence plan
|
||||
|
||||
Add one test to stop-run.test.mjs, preserving existing tests. Its fake sleep
|
||||
advances requested duration plus1ms, so the last sleep crosses the deadline.
|
||||
Its signal stub records TERM/KILL and returns success without killing the
|
||||
always-live fixture process. Expect both signals, time>7000ms and alive.
|
||||
The old loop should fail only the alive assertion with unknown.
|
||||
|
||||
Ask Sage CLEAR before any run, including the expected regression red. Then
|
||||
apply the one-line fix, run the touched whole file and processing-control
|
||||
package as granted, and kill M1 with that same named regression. Busy check,
|
||||
short scratch TMPDIR,deadDocker,owned process groups,timeouts,restore,sweep.
|
||||
No live state,units,store,provider,browser,Docker or V502 changes.
|
||||
|
||||
Packet includes base,patch,candidate hashes,regression/gate/mutant logs and
|
||||
exact source-diff check. Commit candidate and packet on the new branch only
|
||||
after authorized checks. No token is available; Sage publishes as jarvis.
|
||||
The business/CLI suites remain Sage's landing gate unless separately granted.
|
||||
|
||||
## Authorized run and result
|
||||
|
||||
Claim rev 3138, op rocko-279-start-r1. Sage granted this exact sequence in
|
||||
sage-2516 (2026-10-22T05:55:21Z). Runs: 05:56:03Z to 05:58:03Z.
|
||||
|
||||
| Step | Pass | Fail | Skip | Result |
|
||||
| --- | ---: | ---: | ---: | --- |
|
||||
| Regression on unfixed base | 0 | 1 | 0 | Expected assertion: unknown versus alive |
|
||||
| stop-run.test.mjs whole | 13 | 0 | 0 | Green |
|
||||
| processing-control package whole | 62 | 0 | 0 | Green |
|
||||
| M1 old loop | 0 | 1 | 0 | Killed by the named regression, unknown versus alive |
|
||||
|
||||
M1's killer is `a sleep past the stop deadline still observes an alive run
|
||||
after both signals`. It exits 1 with that assertion, never a syntax error or
|
||||
timeout. Logs and results.json retain the intentional failures separately.
|
||||
Green runs total 75 passes (the whole-file tests are also part of the package).
|
||||
No skips. All commands used a short scratch TMPDIR, a dead Docker socket and
|
||||
fixture-only configuration. Each node command ran in its own process group
|
||||
with a 120-second timeout. No timeout occurred. Final cwd sweep: empty.
|
||||
The source restoration and exact one-line product scope are checked in
|
||||
source-scope-check.json. The trailing unknown return stays unchanged and is
|
||||
unreachable after the fix. Business and CLI suites remain Sage's owner gate.
|
||||
Slot returned to Sage at 05:58:03Z. No live action or V502 change.
|
||||
@@ -0,0 +1 @@
|
||||
f40f2e770e0d8be394f1ef53b404045fd6a49c0e
|
||||
@@ -0,0 +1,35 @@
|
||||
diff --git a/packages/processing-control/src/runtimes.mjs b/packages/processing-control/src/runtimes.mjs
|
||||
index 05e439c1a..a11cb8e52 100644
|
||||
--- a/packages/processing-control/src/runtimes.mjs
|
||||
+++ b/packages/processing-control/src/runtimes.mjs
|
||||
@@ -330,7 +330,7 @@ async function ownStop(args, key, options) {
|
||||
};
|
||||
const termEnd = started + grace, deadline = termEnd + observe;
|
||||
let termTried = false, killTried = false;
|
||||
- while (now() <= deadline) {
|
||||
+ for (;;) {
|
||||
try { record = runtime(root, record.id); }
|
||||
catch {
|
||||
const present = readdirSync(directory(root, 'runtimes')).includes(record.id + '.json');
|
||||
diff --git a/packages/processing-control/tests/stop-run.test.mjs b/packages/processing-control/tests/stop-run.test.mjs
|
||||
index ed69bc111..5277ea7fe 100644
|
||||
--- a/packages/processing-control/tests/stop-run.test.mjs
|
||||
+++ b/packages/processing-control/tests/stop-run.test.mjs
|
||||
@@ -61,6 +61,17 @@ test('failed signal delivery observes alive, records no invented signals and nev
|
||||
assert.equal(f.sleeps.reduce((a,b)=>a+b,0), 7000);
|
||||
assert.deepEqual(await stopRun(f.args, f.options), result);
|
||||
});
|
||||
+test('a sleep past the stop deadline still observes an alive run after both signals', async t => {
|
||||
+ const f = setup(t); let time = 0;
|
||||
+ f.options.now = () => time;
|
||||
+ f.options.sleep = async ms => { time += ms + 1; };
|
||||
+ f.options.signal = (_r, sig) => { f.signals.push(sig); return true; };
|
||||
+ const result = await stopRun(f.args, f.options);
|
||||
+ assert.ok(time > 7000, 'the last sleep wakes past the complete stop deadline');
|
||||
+ assert.deepEqual(f.signals, ['SIGTERM', 'SIGKILL']);
|
||||
+ assert.equal(result.outcome, 'alive');
|
||||
+ assert.deepEqual(result.signals, ['SIGTERM', 'SIGKILL']);
|
||||
+});
|
||||
test('missing run and another business never borrow a pid or signal', async t => {
|
||||
const f = setup(t);
|
||||
for (const args of [{ ...f.args, run: 'missing' }, { ...f.args, business: 'other' }]) {
|
||||
@@ -0,0 +1,2 @@
|
||||
7145420aa354180689a5c84423928fc60c8123476dcfed3364c9cf4542f8c7ea packages/processing-control/src/runtimes.mjs
|
||||
7ce7a99363b987f8525f8992fd57940f4305597e870c6162fc158962f2101ff6 packages/processing-control/tests/stop-run.test.mjs
|
||||
@@ -0,0 +1 @@
|
||||
1016b85846c60dc159d49373f99ca36794a51fd9
|
||||
@@ -0,0 +1,2 @@
|
||||
packages/processing-control/src/runtimes.mjs
|
||||
packages/processing-control/tests/stop-run.test.mjs
|
||||
@@ -0,0 +1 @@
|
||||
1016b85846c60dc159d49373f99ca36794a51fd9
|
||||
@@ -0,0 +1,32 @@
|
||||
TAP version 13
|
||||
# Subtest: a sleep past the stop deadline still observes an alive run after both signals
|
||||
not ok 1 - a sleep past the stop deadline still observes an alive run after both signals
|
||||
---
|
||||
duration_ms: 40.278473
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/rocko-r279/packages/processing-control/tests/stop-run.test.mjs:64:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
'unknown' !== 'alive'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'alive'
|
||||
actual: 'unknown'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r279/packages/processing-control/tests/stop-run.test.mjs:72:10)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 0
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 89.021437
|
||||
@@ -0,0 +1,382 @@
|
||||
TAP version 13
|
||||
# Subtest: container admission publishes its generated name and root/runtime labels before dispatch
|
||||
ok 1 - container admission publishes its generated name and root/runtime labels before dispatch
|
||||
---
|
||||
duration_ms: 9.883733
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: container sweep sends TERM then KILL only after verifying exact id and both labels
|
||||
ok 2 - container sweep sends TERM then KILL only after verifying exact id and both labels
|
||||
---
|
||||
duration_ms: 66.167157
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: foreign labels and a replaced container id stay unknown and are never signalled
|
||||
ok 3 - foreign labels and a replaced container id stay unknown and are never signalled
|
||||
---
|
||||
duration_ms: 5.105671
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an unverified missing container remains unknown rather than claiming a pending start ended
|
||||
ok 4 - an unverified missing container remains unknown rather than claiming a pending start ended
|
||||
---
|
||||
duration_ms: 15.538468
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a removed --rm container is settled only by its trusted owner completion receipt
|
||||
ok 5 - a removed --rm container is settled only by its trusted owner completion receipt
|
||||
---
|
||||
duration_ms: 28.897791
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: after signalling a verified container, successful empty Docker listing proves --rm removed that id
|
||||
ok 6 - after signalling a verified container, successful empty Docker listing proves --rm removed that id
|
||||
---
|
||||
duration_ms: 24.215826
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt publishes before awaiting stops, resume refuses pending, and only clear permits new work
|
||||
ok 7 - halt publishes before awaiting stops, resume refuses pending, and only clear permits new work
|
||||
---
|
||||
duration_ms: 161.569582
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a new coordinator replays a persisted halt and an unresolved start keeps clear refused
|
||||
ok 8 - a new coordinator replays a persisted halt and an unresolved start keeps clear refused
|
||||
---
|
||||
duration_ms: 1102.680874
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a new halt generation never inherits a previous quiescent status before its sweep
|
||||
ok 9 - a new halt generation never inherits a previous quiescent status before its sweep
|
||||
---
|
||||
duration_ms: 4.450121
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt aborts an active local request and prevents a subsequent effect after halt/resume
|
||||
ok 10 - halt aborts an active local request and prevents a subsequent effect after halt/resume
|
||||
---
|
||||
duration_ms: 35.831508
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: processing scopes isolate roots and cancel only the registered child group
|
||||
ok 11 - processing scopes isolate roots and cancel only the registered child group
|
||||
---
|
||||
duration_ms: 59.946603
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halted scopes never invoke their body and unscoped human work has no processing context
|
||||
ok 12 - halted scopes never invoke their body and unscoped human work has no processing context
|
||||
---
|
||||
duration_ms: 3.067699
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: host git is stopped promptly by the active scope rather than waiting for its long deadline
|
||||
ok 13 - host git is stopped promptly by the active scope rather than waiting for its long deadline
|
||||
---
|
||||
duration_ms: 151.711241
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a child leader exiting does not erase an unverified surviving group receipt
|
||||
ok 14 - a child leader exiting does not erase an unverified surviving group receipt
|
||||
---
|
||||
duration_ms: 117.914283
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the actual nested read-only control mount refuses unlink, replacement and parent removal
|
||||
ok 15 - the actual nested read-only control mount refuses unlink, replacement and parent removal
|
||||
---
|
||||
duration_ms: 14.941666
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: the image entrypoint refuses halted and stale generations using the shared reader
|
||||
ok 16 - the image entrypoint refuses halted and stale generations using the shared reader
|
||||
---
|
||||
duration_ms: 89.020485
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt terminates a tracked stand-in and leaves honest write-once stop evidence
|
||||
ok 17 - halt terminates a tracked stand-in and leaves honest write-once stop evidence
|
||||
---
|
||||
duration_ms: 95.55672
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a TERM-resistant workload is killed after the bounded wait
|
||||
ok 18 - a TERM-resistant workload is killed after the bounded wait
|
||||
---
|
||||
duration_ms: 146.00258
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: wrong start ticks or boot identity cannot signal a live replacement
|
||||
ok 19 - wrong start ticks or boot identity cannot signal a live replacement
|
||||
---
|
||||
duration_ms: 65.844444
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a pending start cannot be reported quiescent and a second root is never signalled
|
||||
ok 20 - a pending start cannot be reported quiescent and a second root is never signalled
|
||||
---
|
||||
duration_ms: 93.749051
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: escalation still kills the tracked group after its leader exits
|
||||
ok 21 - escalation still kills the tracked group after its leader exits
|
||||
---
|
||||
duration_ms: 192.463864
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: session TERM targets its verified runner before forcing the outer namespace
|
||||
ok 22 - session TERM targets its verified runner before forcing the outer namespace
|
||||
---
|
||||
duration_ms: 100.923976
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: new runtime and stop directories sync their containing directories
|
||||
ok 23 - new runtime and stop directories sync their containing directories
|
||||
---
|
||||
duration_ms: 42.560646
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a vanished group leader with unverified surviving members stays unknown, never quiescent
|
||||
ok 24 - a vanished group leader with unverified surviving members stays unknown, never quiescent
|
||||
---
|
||||
duration_ms: 130.848284
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: persisted member identity anchors a group after the leader exited before the sweep
|
||||
ok 25 - persisted member identity anchors a group after the leader exited before the sweep
|
||||
---
|
||||
duration_ms: 124.574935
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: an unconfirmed secondary brake does not leave a verified workload running or claim success
|
||||
ok 26 - an unconfirmed secondary brake does not leave a verified workload running or claim success
|
||||
---
|
||||
duration_ms: 75.057063
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt persists across readers and resume needs the current generation
|
||||
ok 27 - halt persists across readers and resume needs the current generation
|
||||
---
|
||||
duration_ms: 9.437871
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: first initialization is explicit; missing state after control exists fails closed and halt repairs it
|
||||
ok 28 - first initialization is explicit; missing state after control exists fails closed and halt repairs it
|
||||
---
|
||||
duration_ms: 5.039685
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: refuses invalid JSON instead of enabling work
|
||||
ok 29 - refuses invalid JSON instead of enabling work
|
||||
---
|
||||
duration_ms: 1.867281
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: refuses unknown keys instead of enabling work
|
||||
ok 30 - refuses unknown keys instead of enabling work
|
||||
---
|
||||
duration_ms: 2.65956
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: refuses bad timestamp instead of enabling work
|
||||
ok 31 - refuses bad timestamp instead of enabling work
|
||||
---
|
||||
duration_ms: 2.144084
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: refuses nonboolean state instead of enabling work
|
||||
ok 32 - refuses nonboolean state instead of enabling work
|
||||
---
|
||||
duration_ms: 2.039112
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: refuses bad generation instead of enabling work
|
||||
ok 33 - refuses bad generation instead of enabling work
|
||||
---
|
||||
duration_ms: 2.085806
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: refuses loose mode instead of enabling work
|
||||
ok 34 - refuses loose mode instead of enabling work
|
||||
---
|
||||
duration_ms: 2.34307
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: refuses oversized file instead of enabling work
|
||||
ok 35 - refuses oversized file instead of enabling work
|
||||
---
|
||||
duration_ms: 2.916273
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a symlink leaf never follows its target; human halt replaces only the leaf
|
||||
ok 36 - a symlink leaf never follows its target; human halt replaces only the leaf
|
||||
---
|
||||
duration_ms: 4.530858
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: unsafe parent and interrupted initialization never get repaired as off
|
||||
ok 37 - unsafe parent and interrupted initialization never get repaired as off
|
||||
---
|
||||
duration_ms: 0.385291
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: repeated halt is a no-op and a different root keeps running
|
||||
ok 38 - repeated halt is a no-op and a different root keeps running
|
||||
---
|
||||
duration_ms: 7.00566
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: transition rejects nonstring identity before publishing state
|
||||
ok 39 - transition rejects nonstring identity before publishing state
|
||||
---
|
||||
duration_ms: 3.121939
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: bootstrap syncs the containing root as well as the state and control directory
|
||||
ok 40 - bootstrap syncs the containing root as well as the state and control directory
|
||||
---
|
||||
duration_ms: 36.144072
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: state ownership and bounded read guards refuse before trusting a document
|
||||
ok 41 - state ownership and bounded read guards refuse before trusting a document
|
||||
---
|
||||
duration_ms: 101.892703
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: canonical root aliases address one switch and never create independent control state
|
||||
ok 42 - canonical root aliases address one switch and never create independent control state
|
||||
---
|
||||
duration_ms: 4.179236
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: unbound run stops by verified registry identity, fixed 5s grace and 2s observe policy
|
||||
ok 43 - unbound run stops by verified registry identity, fixed 5s grace and 2s observe policy
|
||||
---
|
||||
duration_ms: 38.851257
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: same run joins the identical promise and completed repeats preserve owner and send no signals
|
||||
ok 44 - same run joins the identical promise and completed repeats preserve owner and send no signals
|
||||
---
|
||||
duration_ms: 27.976805
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: identity changed before TERM or between TERM and KILL refuses the changed signal
|
||||
ok 45 - identity changed before TERM or between TERM and KILL refuses the changed signal
|
||||
---
|
||||
duration_ms: 15.642865
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: failed signal delivery observes alive, records no invented signals and never restarts escalation
|
||||
ok 46 - failed signal delivery observes alive, records no invented signals and never restarts escalation
|
||||
---
|
||||
duration_ms: 32.602087
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sleep past the stop deadline still observes an alive run after both signals
|
||||
ok 47 - a sleep past the stop deadline still observes an alive run after both signals
|
||||
---
|
||||
duration_ms: 31.045314
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: missing run and another business never borrow a pid or signal
|
||||
ok 48 - missing run and another business never borrow a pid or signal
|
||||
---
|
||||
duration_ms: 10.113655
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt joins an in-progress fire without another TERM or deadline and links its first receipt
|
||||
ok 49 - halt joins an in-progress fire without another TERM or deadline and links its first receipt
|
||||
---
|
||||
duration_ms: 30.224351
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a separate host joins durable first ownership without duplicate signals
|
||||
ok 50 - a separate host joins durable first ownership without duplicate signals
|
||||
---
|
||||
duration_ms: 99.128156
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: corrupt and symlinked final receipts refuse as unknown rather than signalling again
|
||||
ok 51 - corrupt and symlinked final receipts refuse as unknown rather than signalling again
|
||||
---
|
||||
duration_ms: 22.649789
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a later exit gets separate halt evidence while the original alive result stays immutable
|
||||
ok 52 - a later exit gets separate halt evidence while the original alive result stays immutable
|
||||
---
|
||||
duration_ms: 55.85559
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: root aliases join but a different root never shares the run stop
|
||||
ok 53 - root aliases join but a different root never shares the run stop
|
||||
---
|
||||
duration_ms: 45.6493
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a changed runtime record cannot replace the original stop identity after TERM
|
||||
ok 54 - a changed runtime record cannot replace the original stop identity after TERM
|
||||
---
|
||||
duration_ms: 8.507771
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: identity is rechecked at actual signal delivery, not just at the loop observation
|
||||
ok 55 - identity is rechecked at actual signal delivery, not just at the loop observation
|
||||
---
|
||||
duration_ms: 6.500324
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: task halt keeps the true Docker exit status, writes kill-switch once and leaves snapshots unchanged
|
||||
ok 56 - task halt keeps the true Docker exit status, writes kill-switch once and leaves snapshots unchanged
|
||||
---
|
||||
duration_ms: 201.141886
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt refuses direct task launch before Docker or a run record; clear allows a fresh normal result
|
||||
ok 57 - halt refuses direct task launch before Docker or a run record; clear allows a fresh normal result
|
||||
---
|
||||
duration_ms: 167.652567
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt refuses hello.sh before Docker, credentials or release alignment
|
||||
ok 58 - halt refuses hello.sh before Docker, credentials or release alignment
|
||||
---
|
||||
duration_ms: 122.654442
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt refuses verify.sh before Docker, credentials or release alignment
|
||||
ok 59 - halt refuses verify.sh before Docker, credentials or release alignment
|
||||
---
|
||||
duration_ms: 122.064969
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt refuses agent.sh before Docker, credentials or release alignment
|
||||
ok 60 - halt refuses agent.sh before Docker, credentials or release alignment
|
||||
---
|
||||
duration_ms: 120.632718
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a nonzero Docker client exit retains its unresolved runtime receipt (task)
|
||||
ok 61 - a nonzero Docker client exit retains its unresolved runtime receipt (task)
|
||||
---
|
||||
duration_ms: 93.3117
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a nonzero Docker client exit retains its unresolved runtime receipt (container-cli)
|
||||
ok 62 - a nonzero Docker client exit retains its unresolved runtime receipt (container-cli)
|
||||
---
|
||||
duration_ms: 97.234461
|
||||
type: 'test'
|
||||
...
|
||||
1..62
|
||||
# tests 62
|
||||
# suites 0
|
||||
# pass 62
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 1323.026518
|
||||
@@ -0,0 +1,32 @@
|
||||
TAP version 13
|
||||
# Subtest: a sleep past the stop deadline still observes an alive run after both signals
|
||||
not ok 1 - a sleep past the stop deadline still observes an alive run after both signals
|
||||
---
|
||||
duration_ms: 53.588908
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/rocko-r279/packages/processing-control/tests/stop-run.test.mjs:64:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly equal:
|
||||
|
||||
'unknown' !== 'alive'
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected: 'alive'
|
||||
actual: 'unknown'
|
||||
operator: 'strictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r279/packages/processing-control/tests/stop-run.test.mjs:72:10)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3)
|
||||
...
|
||||
1..1
|
||||
# tests 1
|
||||
# suites 0
|
||||
# pass 0
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 100.649138
|
||||
@@ -0,0 +1,88 @@
|
||||
TAP version 13
|
||||
# Subtest: unbound run stops by verified registry identity, fixed 5s grace and 2s observe policy
|
||||
ok 1 - unbound run stops by verified registry identity, fixed 5s grace and 2s observe policy
|
||||
---
|
||||
duration_ms: 36.122553
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: same run joins the identical promise and completed repeats preserve owner and send no signals
|
||||
ok 2 - same run joins the identical promise and completed repeats preserve owner and send no signals
|
||||
---
|
||||
duration_ms: 24.270208
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: identity changed before TERM or between TERM and KILL refuses the changed signal
|
||||
ok 3 - identity changed before TERM or between TERM and KILL refuses the changed signal
|
||||
---
|
||||
duration_ms: 13.289288
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: failed signal delivery observes alive, records no invented signals and never restarts escalation
|
||||
ok 4 - failed signal delivery observes alive, records no invented signals and never restarts escalation
|
||||
---
|
||||
duration_ms: 27.743499
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a sleep past the stop deadline still observes an alive run after both signals
|
||||
ok 5 - a sleep past the stop deadline still observes an alive run after both signals
|
||||
---
|
||||
duration_ms: 26.252224
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: missing run and another business never borrow a pid or signal
|
||||
ok 6 - missing run and another business never borrow a pid or signal
|
||||
---
|
||||
duration_ms: 9.103416
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: halt joins an in-progress fire without another TERM or deadline and links its first receipt
|
||||
ok 7 - halt joins an in-progress fire without another TERM or deadline and links its first receipt
|
||||
---
|
||||
duration_ms: 25.918671
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a separate host joins durable first ownership without duplicate signals
|
||||
ok 8 - a separate host joins durable first ownership without duplicate signals
|
||||
---
|
||||
duration_ms: 99.095272
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: corrupt and symlinked final receipts refuse as unknown rather than signalling again
|
||||
ok 9 - corrupt and symlinked final receipts refuse as unknown rather than signalling again
|
||||
---
|
||||
duration_ms: 23.700708
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a later exit gets separate halt evidence while the original alive result stays immutable
|
||||
ok 10 - a later exit gets separate halt evidence while the original alive result stays immutable
|
||||
---
|
||||
duration_ms: 55.938599
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: root aliases join but a different root never shares the run stop
|
||||
ok 11 - root aliases join but a different root never shares the run stop
|
||||
---
|
||||
duration_ms: 42.386018
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: a changed runtime record cannot replace the original stop identity after TERM
|
||||
ok 12 - a changed runtime record cannot replace the original stop identity after TERM
|
||||
---
|
||||
duration_ms: 7.168469
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: identity is rechecked at actual signal delivery, not just at the loop observation
|
||||
ok 13 - identity is rechecked at actual signal delivery, not just at the loop observation
|
||||
---
|
||||
duration_ms: 6.715559
|
||||
type: 'test'
|
||||
...
|
||||
1..13
|
||||
# tests 13
|
||||
# suites 0
|
||||
# pass 13
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 445.396108
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"M1","file":"packages/processing-control/src/runtimes.mjs","old":" for (;;) {\n try { record = runtime(root, record.id); }","new":" while (now() <= deadline) {\n try { record = runtime(root, record.id); }","test":"packages/processing-control/tests/stop-run.test.mjs","pattern":"a sleep past the stop deadline still observes an alive run after both signals","required":"named assertion kill, never syntax or timeout"}
|
||||
@@ -0,0 +1,20 @@
|
||||
6d33d35302499d66036145976f8f777a4c438c6b3f5c7b613fcfa5651c49faab BUILD.md
|
||||
8521af77deed1d3b2c722f30b776159eb373df407a2a698ee116ef4174125f01 base.txt
|
||||
74efdcea480e6c089f586b7330591b66164a3aa338fb1e1a60b74f6b7b9f3b0a build.patch
|
||||
312ed511d8f1cff826ee2fd547de8368923130c7bfe06236d8e29d8655b80aaa candidate-manifest.sha256
|
||||
960c366a732e4dc20133bf490d842cc42a345fefad1432ca6eba6dbd0058f337 candidate.txt
|
||||
3099049a5acd051207fb5631b7bb6362dd84c246191009a44eca1ba696d27d73 changed-paths.txt
|
||||
960c366a732e4dc20133bf490d842cc42a345fefad1432ca6eba6dbd0058f337 commit.txt
|
||||
98a4da4e1c62f7e4ed5f50adc16cf99462a5e012e9736e658441e8eda3db894c logs/M1.log
|
||||
7f878c13be43640f501b72d36e8d9e0c8d844bc5451f969154cd75d8fab0ee42 logs/processing-control-whole.log
|
||||
62f83e511ad860d0f7da42d87e063eb691a95f29a0806b88784912374d2e7c47 logs/regression-red.log
|
||||
4de8028674472cc75f61dc78cac624c336c8db785580a189e0b705584f80d636 logs/stop-run-whole.log
|
||||
7669ce9f04189f659dbc23dae6b635d5ffac354e03d00fbae10e6fadc7e98791 mutant-plan.json
|
||||
84f1080a9689374f7000197119074e53b459641cd709745f95eb0ebe37b5ef2f reader-search.txt
|
||||
dd6b2ea95782154e1c9839bae7a6d76a60566762a2374901ae64b22f9a7f54f7 results.json
|
||||
b9408890f30a72451d9171d8ceec40eb4f4f671811b30ba74f50f178e0eb8350 run-step.py
|
||||
b8140e3919a5d342eb92c1cd34b13bbb62d6419c6a5a01a9cb4870e9a94c64ed scan-result.json
|
||||
9e5c2e8cd2b59ac4559b0d67348a2a98e59b44a71eec7359018ea7a84d7a4082 skips.txt
|
||||
43d77fe5bffa916f444a77bacda5b4ddbe78035b6ca7631378056c3158aa3253 source-scope-check.json
|
||||
37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570 sweep.json
|
||||
74b3eae937fdcc92cf2b0dd15fb67f4f6addd4a046f74e934febc565e4b5d2ed tmpdir.txt
|
||||
@@ -0,0 +1,12 @@
|
||||
packages/processing-control/tests/runtimes.test.mjs:9:const api = await import('../src/runtimes.mjs').catch(e => { if (e.code === 'ERR_MODULE_NOT_FOUND') return {}; throw e; });
|
||||
packages/processing-control/tests/runtimes.test.mjs:139: new URL('../src/runtimes.mjs', import.meta.url).href, new URL('../src/state.mjs', import.meta.url).href, root], { encoding: 'utf8' });
|
||||
packages/processing-control/tests/effects.test.mjs:60: const { listRuntimes } = await import('../src/runtimes.mjs');
|
||||
packages/processing-control/tests/coordinator.test.mjs:9:import { registerRuntime, updateRuntime, processIdentity, unregisterRuntime } from '../src/runtimes.mjs';
|
||||
packages/processing-control/tests/containers.test.mjs:62: const { updateRuntime } = await import('../src/runtimes.mjs');
|
||||
packages/processing-control/tests/task.test.mjs:12: const runtimes = new URL('../src/runtimes.mjs', import.meta.url).href;
|
||||
packages/processing-control/tests/stop-run.test.mjs:7:import { registerRuntime, updateRuntime, runtime, stopRun, sweepRuntimes, readStops } from '../src/runtimes.mjs';
|
||||
packages/processing-control/tests/stop-run.test.mjs:104: const f = setup(t), source = new URL('../src/runtimes.mjs', import.meta.url).href;
|
||||
packages/processing-control/tests/stop-run.test.mjs:148: const { processIdentity } = await import('../src/runtimes.mjs');
|
||||
packages/bus/tests/processing-runtime.test.mjs:10:import { registerRuntime, updateRuntime, processIdentity } from '../../processing-control/src/runtimes.mjs';
|
||||
packages/discord/tests/processing-stop.test.mjs:35: const { registerRuntime, updateRuntime } = await import('../../processing-control/src/runtimes.mjs');
|
||||
packages/discord/tests/processing.test.mjs:68: const { listRuntimes, sweepRuntimes, processIdentity } = await import('../../processing-control/src/runtimes.mjs');
|
||||
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"step": "regression-red",
|
||||
"start": "2026-10-22T05:56:03Z",
|
||||
"end": "2026-10-22T05:56:03Z",
|
||||
"exit": 1,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"skipped": 0
|
||||
},
|
||||
"failing": [
|
||||
"a sleep past the stop deadline still observes an alive run after both signals"
|
||||
],
|
||||
"log": "/mnt/storage/scratch/rocko-r279/agents/rocko/work/queue-279/logs/regression-red.log"
|
||||
},
|
||||
{
|
||||
"step": "stop-run-whole",
|
||||
"start": "2026-10-22T05:57:47Z",
|
||||
"end": "2026-10-22T05:57:47Z",
|
||||
"exit": 0,
|
||||
"counts": {
|
||||
"tests": 13,
|
||||
"pass": 13,
|
||||
"fail": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"failing": [],
|
||||
"log": "/mnt/storage/scratch/rocko-r279/agents/rocko/work/queue-279/logs/stop-run-whole.log"
|
||||
},
|
||||
{
|
||||
"step": "processing-control-whole",
|
||||
"start": "2026-10-22T05:57:47Z",
|
||||
"end": "2026-10-22T05:57:49Z",
|
||||
"exit": 0,
|
||||
"counts": {
|
||||
"tests": 62,
|
||||
"pass": 62,
|
||||
"fail": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"failing": [],
|
||||
"log": "/mnt/storage/scratch/rocko-r279/agents/rocko/work/queue-279/logs/processing-control-whole.log"
|
||||
},
|
||||
{
|
||||
"step": "M1",
|
||||
"start": "2026-10-22T05:58:03Z",
|
||||
"end": "2026-10-22T05:58:03Z",
|
||||
"exit": 1,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"skipped": 0
|
||||
},
|
||||
"failing": [
|
||||
"a sleep past the stop deadline still observes an alive run after both signals"
|
||||
],
|
||||
"log": "/mnt/storage/scratch/rocko-r279/agents/rocko/work/queue-279/logs/M1.log"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
from pathlib import Path
|
||||
import subprocess,signal,os,json,time,sys,re
|
||||
root=Path('/mnt/storage/scratch/rocko-r279');p=root/'agents/rocko/work/queue-279';logs=p/'logs';logs.mkdir(exist_ok=True);name=sys.argv[1];command=sys.argv[2:]
|
||||
start=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime());log=logs/(name+'.log')
|
||||
with log.open('w') as out:
|
||||
child=subprocess.Popen(command,cwd=root,env=os.environ,stdout=out,stderr=subprocess.STDOUT,start_new_session=True)
|
||||
try:rc=child.wait(timeout=120)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(child.pid,signal.SIGKILL);child.wait();rc=124
|
||||
text=log.read_text();counts={k:int(v) for k,v in re.findall(r'^# (tests|pass|fail|skipped) (\d+)\s*$',text,re.M)};failing=re.findall(r'^not ok \d+ - (.*)$',text,re.M)
|
||||
row=dict(step=name,start=start,end=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),exit=rc,counts=counts,failing=failing,log=str(log));result=p/'results.json';rows=json.loads(result.read_text()) if result.exists() else [];rows.append(row);result.write_text(json.dumps(rows,indent=2)+'\n');print(json.dumps(row));print(text[-3200:])
|
||||
if name in ['regression-red','M1']:
|
||||
ok=rc==1 and failing==['a sleep past the stop deadline still observes an alive run after both signals'] and "actual: 'unknown'" in text and "expected: 'alive'" in text
|
||||
print('EXPECTED_UNKNOWN_VS_ALIVE',ok);raise SystemExit(0 if ok else 1)
|
||||
raise SystemExit(rc)
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"secret_matches": [],
|
||||
"binary_files": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
None. All four TAP records report skipped 0.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"base": "f40f2e770e0d8be394f1ef53b404045fd6a49c0e",
|
||||
"only_product_change": "while (now() <= deadline) -> for (;;)",
|
||||
"other_product_bytes_unchanged": true,
|
||||
"trailing_unknown_fallback": "unchanged; unreachable"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
/mnt/storage/scratch/tmp/gwxq
|
||||
@@ -0,0 +1,222 @@
|
||||
# Slim packet and full-evidence location
|
||||
|
||||
This packet keeps the build note, candidate manifest and patch, summaries,
|
||||
per-run counts/durations/exit statuses, mutant definitions and cleanup
|
||||
records. Raw TAP logs, per-attempt config/log trees, diagnostic byte dumps,
|
||||
runner scripts and transient TMPDIR notes remain only in the full packet.
|
||||
References below to those files resolve at the full evidence commit.
|
||||
|
||||
Full evidence is retained permanently at commit
|
||||
`d6c924764d387ea320365c4bb160b4b57ed8d635` on
|
||||
`origin/rocko/row280-da5d-20261022`, under
|
||||
`agents/rocko/work/queue-280/`. Its 3,531-entry inventory SHA256 is
|
||||
`7d155092cb6edb547dbb7c8ba2fbabb1dd74f966c5c20965ef4fe6af2e7121b8`.
|
||||
The full branch is retained; this slim packet does not replace its evidence.
|
||||
The inventory in this directory describes only this slim packet.
|
||||
|
||||
Sage-2541 requested this doc-only follow-up on b765f5cb8 after the green
|
||||
owner gate: bus 656/0/0, grant-executor 41/0/0, host-run 26/0/0 (#1785 c30431).
|
||||
No source or test byte changes and no additional test run.
|
||||
|
||||
## Portability limit and carry notes
|
||||
|
||||
Duckworth N2: M2's demonstrated kill holds only while the host's ephemeral
|
||||
range has lo <= 32768 <= hi. On another range, the picker test still checks
|
||||
the outside-range property, but the old-picker mutant may not be killed by
|
||||
a fixed sample. The evidence here applies to this host's 32768-60999 range.
|
||||
|
||||
For the next row touching scratch-sshd.mjs, carry these non-blocking notes:
|
||||
- N3: add one helper comment line naming MOSAIC_SSHD_EVIDENCE.
|
||||
- N4: remove the double blank lines in both owning test files.
|
||||
|
||||
These are follow-ups, not changes to the tested candidate in this packet.
|
||||
|
||||
---
|
||||
|
||||
# Row 280: scratch sshd owns its port
|
||||
|
||||
Design before code; Sage assignment sage-2530, mos-1270. Base
|
||||
`da5d3651d75bff113b27d91abefc824c077dabbe`. Claim rev 3148,
|
||||
`rocko-280-start-r1`. Reviewer Duckworth; gate owner Sage.
|
||||
|
||||
## Proposed change
|
||||
|
||||
Both helpers currently return on `listening(port)` alone
|
||||
(grant-executor.test.mjs:86 and host-run.test.mjs:91). A foreign listener
|
||||
can satisfy that probe while our daemon fails its bind. Move only their
|
||||
common launch/retry/readiness loop and port selection into a shared test
|
||||
helper, `packages/bus/tests/scratch-sshd.mjs`; retain their distinct key,
|
||||
authorized-key and command-stub setup in their existing files.
|
||||
|
||||
For each of the existing five attempts, write a fresh, attempt-specific
|
||||
sshd log, start the child, and accept only after all three checks hold:
|
||||
a successful loopback connect, the exact `Server listening on 127.0.0.1
|
||||
port N.` line from this attempt's log, and the child's exitCode/signalCode
|
||||
still null after the asynchronous connect. No previous attempt's log can
|
||||
prove a later attempt. Early exit or failed bind advances to a fresh pick;
|
||||
kill and await an unsuccessful child before retry. Exhaustion keeps the
|
||||
existing `scratch sshd did not start` error, never a skip. Register cleanup
|
||||
for the successful child as well.
|
||||
|
||||
Read and validate `/proc/sys/net/ipv4/ip_local_port_range`. Select uniformly
|
||||
from eligible ports 1024 through 65535 outside the inclusive range, refusing
|
||||
invalid input or an empty eligible set. No fixed 32768 assumption. Expose a
|
||||
first-attempt port override only to the regression, then use the normal picker.
|
||||
|
||||
In each owning file, a regression opens a plain TCP listener, supplies that
|
||||
occupied port for attempt one, and asserts the helper rejects it and returns
|
||||
a different port. The helper's log/liveness proof still governs that returned
|
||||
port. Register TCP listener cleanup before awaiting startup.
|
||||
Add a deterministic picker assertion using injected random samples spanning
|
||||
[0,1); every selected port must be unprivileged and outside the actual proc
|
||||
range. M2 restores the old picker. If the actual host range makes M2 escape
|
||||
this assertion, record it and ask Duckworth to adjudicate as the brief allows.
|
||||
|
||||
## X5 closure
|
||||
|
||||
DEFERRED.md:311-321 binds the `sleep 30` after the grant in G22/X5.
|
||||
Use the fixture's existing fake-command PATH: a `sleep` stub delegates every
|
||||
other argument to /usr/bin/sleep, but for 30 waits in short intervals only
|
||||
while its scratch directory exists. The existing `sleep 30` argv, grant,
|
||||
sudo-log assertion and revoke assertions stay unchanged. Remote cleanup no
|
||||
longer leaves a 30-second child after the fixture directory is removed.
|
||||
Append a closing line to the DEFERRED entry; do not rewrite its history.
|
||||
|
||||
## Evidence and slot request plan
|
||||
|
||||
No run before Sage CLEAR. First run the new occupied-port regressions red
|
||||
against the old acceptance, then green after the helper change. Run both
|
||||
whole files, M1 (old connect-only acceptance) and M2 (old random picker),
|
||||
with each killing test named. Do not classify a timeout or syntax error as
|
||||
an assertion kill. Save process-group timeout output and restore hashes.
|
||||
|
||||
After the whole-file durations are known, refine the repeat estimate.
|
||||
Provisional estimate: allow 60-90 minutes for both 30-run whole-file repeats
|
||||
under continuous CLI-suite load, with per-run counts and elapsed time saved.
|
||||
The load and repeats are intentional concurrency within one granted slot;
|
||||
all other commands stay sequential. Both files must complete all 30 without
|
||||
timeout, and the CLI load must remain green. Any unexpected red stops for
|
||||
Sage/Mos. Use a fresh short scratch TMPDIR, dead Docker socket, no live
|
||||
configuration, no Docker or browser, and kill owned process groups on timeout.
|
||||
Sweep owned node and sshd/stub processes and restore candidate hashes before
|
||||
returning the slot. Sage runs the whole bus suite in the owner gate.
|
||||
|
||||
Owned paths: the two existing test files, the shared test helper,
|
||||
`docs/plans/DEFERRED.md`, and this packet. No product change, no live action.
|
||||
|
||||
## Phase 1 runner setup correction
|
||||
|
||||
The initial red-grant invocation at 07:22:02Z ran no test: packet runner
|
||||
parents[5] selected the scratch parent instead of the worktree. The invalid
|
||||
record and log stay unchanged. Sage-2534 approved parents[4], a git-root
|
||||
and test-file existence preflight, and a fresh CLEAR. No source or test
|
||||
byte changed for this correction; it is not a test red.
|
||||
|
||||
## Accepted design conditions and first test stop
|
||||
|
||||
Sage-2533 and duckworth-0120 approved the design. C1 samples are
|
||||
[0, 0.25, 0.5, 0.6384, 0.75, 0.999999]; M2 uses rand(), never Math.random().
|
||||
C2 checks attempt-zero's config names the occupied port and its log has no
|
||||
listening proof for that port. C3's 30/60-second stub delegates other
|
||||
arguments to absolute /usr/bin/sleep, so hold-add/remove's 0.05 sleeps do
|
||||
not recurse. The same stub covers host-run's remote long sleeps (N1).
|
||||
Attempt logs are sshd-N.log; exhaustion names the final log (N2).
|
||||
|
||||
After sage-2534's runner correction, both old-acceptance regression runs
|
||||
failed exactly on returned port equaling the occupied listener port.
|
||||
The first green regression then failed: no accepted sshd after five
|
||||
attempts (07:22:56Z–07:23:09Z, 12.193 s), while the picker assertion passed.
|
||||
The exact log comparison may be rejecting line framing, but this is only a
|
||||
hypothesis: fixture cleanup removed the raw attempt logs. Stopped without
|
||||
retry and requested diagnostic CLEAR to capture the logs before cleanup.
|
||||
No later whole-file run or mutant was executed. Slot returned 07:23:34Z;
|
||||
own cwd node/sshd sweep is empty. This evidence is not a passing gate.
|
||||
|
||||
## Confirmed log framing and correction
|
||||
|
||||
Sage-2535 granted a diagnostic rerun and the conditional CR-only correction.
|
||||
The saved diagnostic TAP and diagnostic-bytes.json prove the occupied-port
|
||||
bind refusal, then four listening lines with trailing hex 0d before newline.
|
||||
Only the comparison changed: the exact expected line, optionally followed
|
||||
by one CR, is accepted. No trim or substring match is used. The temporary
|
||||
dump is removed. Both focused files pass 2/2 after this correction.
|
||||
The earlier diagnostic red is retained as build evidence, not hidden.
|
||||
|
||||
## Phase 1 complete
|
||||
|
||||
Whole grant-executor: 41/41 in 20.673 s; whole host-run: 26/26 in
|
||||
14.367 s. Focused regressions: 2/2 in each file. No skips or timeouts.
|
||||
M1 and M2 are each assertion-killed by the named regression in both files
|
||||
(4/4 executions, two distinct mutant sites). M2 deterministically hits
|
||||
32768 at injected sample 0.6384. Source restored. Post-run pgrep finds no
|
||||
process naming the fixture TMPDIR; cwd node/sshd sweep is empty, proving
|
||||
the long-sleep stub cleanup requested in C3. Logs retain the prior invalid
|
||||
runner invocation and the diagnosed CRLF build failure separately.
|
||||
|
||||
## Repeat preparation (sage-2536)
|
||||
|
||||
Before the repeat commit, the shared helper adds optional fixture evidence
|
||||
output via MOSAIC_SSHD_EVIDENCE. It saves each attempted config/log before
|
||||
accepting or abandoning that attempt, so test cleanup cannot erase the
|
||||
startup evidence. The repeat runner assigns a packet directory per run.
|
||||
No acceptance, picker, stub or existing assertion changes for this capture.
|
||||
The repeated full files verify this final committed helper.
|
||||
|
||||
Repeat source commit: b765f5cb812e60789e4129ed2ab8009f5439a5be. Four-path manifest SHA256: 27806c0579c76d0999d2e6be4180c3867570df0268af9d7e0d7b971af799e6f5.
|
||||
All four blobs are rechecked before and after each run. No candidate
|
||||
bytes may change after this point.
|
||||
|
||||
## Final committed-byte evidence
|
||||
|
||||
Sage-2536 granted the repeat on committed bytes, with no changes carried
|
||||
through the run. Source commit b765f5cb812e60789e4129ed2ab8009f5439a5be;
|
||||
manifest 27806c0579c76d0999d2e6be4180c3867570df0268af9d7e0d7b971af799e6f5.
|
||||
Repeat window: 2026-10-22T07:29:03Z to 07:48:38Z. Both whole files ran
|
||||
sequentially under continuous CLI-suite load, with full output, exit status,
|
||||
counts and durations retained per run in repeat-results.json/repeat-logs/.
|
||||
Each run checks HEAD and all four hashes before and after. The load is the
|
||||
actual complete CLI glob, not a substitute stress loop. Sage received the
|
||||
required 15-minute report at 07:44:01Z.
|
||||
|
||||
| Repeat file | Whole runs | Pass | Fail | Skip | Timeout |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| grant-executor.test.mjs | 30 | 1230 | 0 | 0 | 0 |
|
||||
| host-run.test.mjs | 30 | 780 | 0 | 0 | 0 |
|
||||
| CLI load, two whole cycles | 2 | 1588 | 0 | 0 | 0 |
|
||||
|
||||
Repeat assertions total 2010/2010; load assertions 1588/1588. The in-progress
|
||||
second CLI load was allowed to finish whole before stopping. No skips.
|
||||
Each repeat has a 180 s process-group timeout; each load a 900 s timeout.
|
||||
Neither bound fired. Attempt config/log evidence was saved before cleanup
|
||||
under attempt-logs/<run>/<fixture>/; these are generated fixture paths only.
|
||||
|
||||
grant-executor: seconds min 20.611, median 20.811, max 26.419.
|
||||
|
||||
host-run: seconds min 14.207, median 14.408, max 14.609.
|
||||
|
||||
cli-load: seconds min 587.109, median 587.409, max 587.709.
|
||||
|
||||
Sage-2537 granted final M1/M2 replays after the complete CLI load exited,
|
||||
because the optional attempt-log capture was added before the repeat commit.
|
||||
07:48:56Z–07:48:57Z: four focused executions, all assertion kills:
|
||||
|
||||
| Mutant | Named killer, in both owning files | Result |
|
||||
| --- | --- | --- |
|
||||
| M1: connect-only acceptance | row 280: an occupied TCP port is attempted, refused, and retried with our sshd | Both fail returned-port inequality |
|
||||
| M2: 20000 + Math.floor(rand() * 20000) | row 280: every injected picker sample is unprivileged and outside the ephemeral range | Both fail at sample 0.6384, port 32768 |
|
||||
|
||||
No timeout or syntax failure is counted as a kill. Phase-1 runner, table and
|
||||
logs remain separately recorded; final replays use the exact b765 helper.
|
||||
Restored-hashes.json records 4/4 with no drift. Final-sweep.json records no
|
||||
node, chromium or sshd under owned cwd paths and no process naming either
|
||||
owned TMPDIR. This also supplies C3's post-test long-sleep cleanup evidence.
|
||||
Slot returned at 07:49:17Z. No source byte changed during the repeat; the
|
||||
final mutations were restored under sage-2537. No live file, unit or config
|
||||
was touched. The whole bus suite remains Sage's owner gate.
|
||||
|
||||
Existing assertions/refusals remain unchanged (289 grant lines, 125 host
|
||||
lines); assertion-preservation.json records the static check. The reader
|
||||
search found only the intended shared-helper imports and no outside reader
|
||||
of either owning file. Duckworth also confirmed no owning-file hash pin.
|
||||
The two extra blank-line pairs are acknowledged style nits, left unchanged
|
||||
after the committed repeat (sage-2537).
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"packages/bus/tests/grant-executor.test.mjs": {
|
||||
"existing_assertion_refusal_lines": 289,
|
||||
"all_preserved": true
|
||||
},
|
||||
"packages/bus/tests/host-run.test.mjs": {
|
||||
"existing_assertion_refusal_lines": 125,
|
||||
"all_preserved": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
da5d3651d75bff113b27d91abefc824c077dabbe
|
||||
@@ -0,0 +1,333 @@
|
||||
diff --git a/docs/plans/DEFERRED.md b/docs/plans/DEFERRED.md
|
||||
index 197a29ecb..7945a6829 100644
|
||||
--- a/docs/plans/DEFERRED.md
|
||||
+++ b/docs/plans/DEFERRED.md
|
||||
@@ -319,6 +319,10 @@ at every gate. Started 2026-09-12 during the control board MVP.
|
||||
waits 30 s before it calls a lone `sleep 30` a leak. The row 197
|
||||
lanes scan only at lane end, about 15 minutes after pkg-bus, so they
|
||||
can't see it (mos-1169). (2026-10-21)
|
||||
+ Closing note, row 280 (2026-10-22): both scratch sshd fixtures install a
|
||||
+ bounded 30/60-second sleep stub that exits when its test directory is gone;
|
||||
+ other sleep arguments exec /usr/bin/sleep unchanged. Evidence is recorded
|
||||
+ in agents/rocko/work/queue-280/BUILD.md.
|
||||
|
||||
- **Row 66 live checks: the unslop notice on a live managed session, both harnesses.**
|
||||
(mos-1186, Vikunja #454.) Row 66's gate asked for one landing session per
|
||||
diff --git a/packages/bus/tests/grant-executor.test.mjs b/packages/bus/tests/grant-executor.test.mjs
|
||||
index 1992f1525..b27d91dc4 100644
|
||||
--- a/packages/bus/tests/grant-executor.test.mjs
|
||||
+++ b/packages/bus/tests/grant-executor.test.mjs
|
||||
@@ -6,9 +6,9 @@
|
||||
// reaches another host.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
-import { execFileSync, spawn } from 'node:child_process';
|
||||
+import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
-import { connect } from 'node:net';
|
||||
+import { createServer } from 'node:net';
|
||||
import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir, userInfo } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
@@ -19,6 +19,8 @@ import { hostExecutor, runHost } from '../src/host-run.mjs';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { grantDecision, hostRecord } from './host-fixtures.mjs';
|
||||
|
||||
+import { ephemeralRange, installSleepStub, pickPort, startScratchSshd } from './scratch-sshd.mjs';
|
||||
+
|
||||
const SSHD = '/usr/bin/sshd';
|
||||
const USER = userInfo().username;
|
||||
const AT = '2026-10-21T00:00:00.000Z';
|
||||
@@ -35,12 +37,7 @@ const DEFINITIONS = {
|
||||
pm: { roleVersion: 2, credentials: [] },
|
||||
ops: { roleVersion: 2, credentials: [{ service: 'host' }] },
|
||||
};
|
||||
-const listening = (port) =>
|
||||
- new Promise((resolve) => {
|
||||
- const s = connect(port, '127.0.0.1');
|
||||
- s.on('connect', () => (s.destroy(), resolve(true)));
|
||||
- s.on('error', () => resolve(false));
|
||||
- });
|
||||
+
|
||||
async function until(fn, ms = 10000) {
|
||||
const end = Date.now() + ms;
|
||||
while (!fn()) {
|
||||
@@ -54,12 +51,13 @@ async function until(fn, ms = 10000) {
|
||||
// Both also stop when the test dir is gone, since a held stub on the sshd side outlives its test (#1750).
|
||||
// wrap.sh logs every request. The sudo stub logs its arguments to sudo.log and runs the command as the
|
||||
// test's user.
|
||||
-async function sshd(t, dir) {
|
||||
+async function sshd(t, dir, options) {
|
||||
if (!existsSync(SSHD)) throw new Error(`${SSHD} is missing: the executor tests need it and fail, never skip`);
|
||||
keygen(join(dir, 'hostkey'));
|
||||
keygen(join(dir, 'installer'));
|
||||
writeFileSync(join(dir, 'authorized_keys'), readFileSync(join(dir, 'installer.pub')), { mode: 0o600 });
|
||||
mkdirSync(join(dir, 'fake'));
|
||||
+ installSleepStub(dir);
|
||||
const stub = (name, body) => writeFileSync(join(dir, 'fake', name), `#!/bin/sh\nD='${dir}'\n${body}\n`, { mode: 0o755 });
|
||||
stub('list', '[ -e "$D/fail-list" ] && exit 1\n[ -e "$D/granted" ] && grep " mosaic-[0-9a-f]*$" "$D/granted"\nexit 0');
|
||||
stub(
|
||||
@@ -76,26 +74,42 @@ async function sshd(t, dir) {
|
||||
`#!/bin/sh\nprintf '%s\\n' "$SSH_ORIGINAL_COMMAND" >> '${dir}/requests.log'\nPATH=${dir}/fake:/usr/bin:/bin; export PATH\nexec /bin/sh -c "$SSH_ORIGINAL_COMMAND"\n`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
- for (let attempt = 0; attempt < 5; attempt++) {
|
||||
- const port = 20000 + Math.floor(Math.random() * 20000);
|
||||
- writeFileSync(
|
||||
- join(dir, 'sshd_config'),
|
||||
- [`Port ${port}`, 'ListenAddress 127.0.0.1', `HostKey ${dir}/hostkey`, `AuthorizedKeysFile ${dir}/authorized_keys ${dir}/granted`,
|
||||
- `PidFile ${dir}/sshd.pid`, 'UsePAM no', 'StrictModes no', 'PasswordAuthentication no', 'KbdInteractiveAuthentication no',
|
||||
- `ForceCommand ${dir}/wrap.sh`, ''].join('\n'),
|
||||
- );
|
||||
- const daemon = spawn(SSHD, ['-D', '-f', join(dir, 'sshd_config'), '-E', join(dir, 'sshd.log')], { stdio: 'ignore' });
|
||||
- t.after(() => daemon.kill('SIGKILL'));
|
||||
- const deadline = Date.now() + 3000;
|
||||
- while (Date.now() < deadline && daemon.exitCode === null) {
|
||||
- if (await listening(port)) return port;
|
||||
- await sleep(30);
|
||||
- }
|
||||
- daemon.kill('SIGKILL');
|
||||
- }
|
||||
- throw new Error('scratch sshd did not start');
|
||||
+ return startScratchSshd(t, dir, [
|
||||
+ 'ListenAddress 127.0.0.1', `HostKey ${dir}/hostkey`,
|
||||
+ `AuthorizedKeysFile ${dir}/authorized_keys ${dir}/granted`, `PidFile ${dir}/sshd.pid`,
|
||||
+ 'UsePAM no', 'StrictModes no', 'PasswordAuthentication no', 'KbdInteractiveAuthentication no',
|
||||
+ `ForceCommand ${dir}/wrap.sh`,
|
||||
+ ], options);
|
||||
}
|
||||
|
||||
+
|
||||
+test('row 280: an occupied TCP port is attempted, refused, and retried with our sshd', async t => {
|
||||
+ const dir = mkdtempSync(join(tmpdir(), 'sshd280-'));
|
||||
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
+ const server = createServer(socket => socket.end());
|
||||
+ await new Promise((resolve, reject) => {
|
||||
+ server.once('error', reject);
|
||||
+ server.listen(0, '127.0.0.1', resolve);
|
||||
+ });
|
||||
+ t.after(() => new Promise(resolve => server.close(resolve)));
|
||||
+ const occupied = server.address().port;
|
||||
+ const port = await sshd(t, dir, { firstPort: occupied });
|
||||
+ assert.match(readFileSync(join(dir, 'sshd-0.conf'), 'utf8'), new RegExp(`^Port ${occupied}\\n`));
|
||||
+ assert.equal(readFileSync(join(dir, 'sshd-0.log'), 'utf8').includes(`Server listening on 127.0.0.1 port ${occupied}.`), false);
|
||||
+ assert.notEqual(port, occupied);
|
||||
+ assert.ok([1, 2, 3, 4].some(n => existsSync(join(dir, `sshd-${n}.log`)) &&
|
||||
+ readFileSync(join(dir, `sshd-${n}.log`), 'utf8').includes(`Server listening on 127.0.0.1 port ${port}.`)));
|
||||
+});
|
||||
+
|
||||
+test('row 280: every injected picker sample is unprivileged and outside the ephemeral range', () => {
|
||||
+ const [lo, hi] = ephemeralRange();
|
||||
+ for (const sample of [0, 0.25, 0.5, 0.6384, 0.75, 0.999999]) {
|
||||
+ const port = pickPort(() => sample);
|
||||
+ assert.ok(port >= 1024 && port <= 65535);
|
||||
+ assert.ok(port < lo || port > hi, `sample ${sample}: ${port} inside ${lo}-${hi}`);
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
async function setup(t, opts = {}) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'g235x-'));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
diff --git a/packages/bus/tests/host-run.test.mjs b/packages/bus/tests/host-run.test.mjs
|
||||
index 6f5949b5c..f7eebd45e 100644
|
||||
--- a/packages/bus/tests/host-run.test.mjs
|
||||
+++ b/packages/bus/tests/host-run.test.mjs
|
||||
@@ -4,7 +4,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
-import { connect, createServer } from 'node:net';
|
||||
+import { createServer } from 'node:net';
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir, userInfo } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
@@ -19,6 +19,8 @@ import { hostGrant } from '../src/host-grant.mjs';
|
||||
import { startBroker } from '../src/runtime.mjs';
|
||||
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
|
||||
|
||||
+import { ephemeralRange, installSleepStub, pickPort, startScratchSshd } from './scratch-sshd.mjs';
|
||||
+
|
||||
const SSHD = '/usr/bin/sshd';
|
||||
const REPO = fileURLToPath(new URL('../../../', import.meta.url));
|
||||
const USER = userInfo().username;
|
||||
@@ -50,22 +52,18 @@ async function until(fn, ms = 5000) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
-const listening = (port) =>
|
||||
- new Promise((resolve) => {
|
||||
- const s = connect(port, '127.0.0.1');
|
||||
- s.on('connect', () => (s.destroy(), resolve(true)));
|
||||
- s.on('error', () => resolve(false));
|
||||
- });
|
||||
+
|
||||
|
||||
// A scratch sshd. ForceCommand puts the fake sudo first on PATH and hands the command to /bin/sh, so
|
||||
// the remote side parses the command line the way a real host's POSIX shell does.
|
||||
-async function sshd(t, dir) {
|
||||
+async function sshd(t, dir, options) {
|
||||
if (!existsSync(SSHD)) throw new Error(`${SSHD} is missing: the executor tests need it and fail, never skip`);
|
||||
keygen(join(dir, 'hostkey'));
|
||||
keygen(join(dir, 'otherhost'));
|
||||
keygen(join(dir, 'user'));
|
||||
writeFileSync(join(dir, 'authorized_keys'), readFileSync(join(dir, 'user.pub')), { mode: 0o600 });
|
||||
mkdirSync(join(dir, 'fake'));
|
||||
+ installSleepStub(dir);
|
||||
writeFileSync(
|
||||
join(dir, 'fake', 'sudo'),
|
||||
`#!/bin/sh\n'${process.execPath}' -e 'require("fs").appendFileSync(process.argv[1], JSON.stringify(process.argv.slice(2)) + "\\n")' '${dir}/argv.json' "$@"\nshift 2\nexec "$@"\n`,
|
||||
@@ -75,26 +73,42 @@ async function sshd(t, dir) {
|
||||
writeFileSync(join(dir, 'fake', 'fail255'), '#!/bin/sh\necho "Host key verification failed." >&2\nexit 255\n', { mode: 0o755 });
|
||||
writeFileSync(join(dir, 'wrap.sh'), `#!/bin/sh\nPATH=${dir}/fake:/usr/bin:/bin; export PATH\nexec /bin/sh -c "$SSH_ORIGINAL_COMMAND"\n`);
|
||||
chmodSync(join(dir, 'wrap.sh'), 0o755);
|
||||
- for (let attempt = 0; attempt < 5; attempt++) {
|
||||
- const port = 20000 + Math.floor(Math.random() * 20000);
|
||||
- writeFileSync(
|
||||
- join(dir, 'sshd_config'),
|
||||
- [`Port ${port}`, 'ListenAddress 127.0.0.1', `HostKey ${dir}/hostkey`, `AuthorizedKeysFile ${dir}/authorized_keys`,
|
||||
- `PidFile ${dir}/sshd.pid`, 'UsePAM no', 'StrictModes no', 'PasswordAuthentication no', 'KbdInteractiveAuthentication no',
|
||||
- `ForceCommand ${dir}/wrap.sh`, ''].join('\n'),
|
||||
- );
|
||||
- const daemon = spawn(SSHD, ['-D', '-f', join(dir, 'sshd_config'), '-E', join(dir, 'sshd.log')], { stdio: 'ignore' });
|
||||
- t.after(() => daemon.kill('SIGKILL'));
|
||||
- const deadline = Date.now() + 3000;
|
||||
- while (Date.now() < deadline && daemon.exitCode === null) {
|
||||
- if (await listening(port)) return port;
|
||||
- await sleep(30);
|
||||
- }
|
||||
- daemon.kill('SIGKILL');
|
||||
- }
|
||||
- throw new Error('scratch sshd did not start');
|
||||
+ return startScratchSshd(t, dir, [
|
||||
+ 'ListenAddress 127.0.0.1', `HostKey ${dir}/hostkey`,
|
||||
+ `AuthorizedKeysFile ${dir}/authorized_keys`, `PidFile ${dir}/sshd.pid`,
|
||||
+ 'UsePAM no', 'StrictModes no', 'PasswordAuthentication no', 'KbdInteractiveAuthentication no',
|
||||
+ `ForceCommand ${dir}/wrap.sh`,
|
||||
+ ], options);
|
||||
}
|
||||
|
||||
+
|
||||
+test('row 280: an occupied TCP port is attempted, refused, and retried with our sshd', async t => {
|
||||
+ const dir = mkdtempSync(join(tmpdir(), 'sshd280-'));
|
||||
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
+ const server = createServer(socket => socket.end());
|
||||
+ await new Promise((resolve, reject) => {
|
||||
+ server.once('error', reject);
|
||||
+ server.listen(0, '127.0.0.1', resolve);
|
||||
+ });
|
||||
+ t.after(() => new Promise(resolve => server.close(resolve)));
|
||||
+ const occupied = server.address().port;
|
||||
+ const port = await sshd(t, dir, { firstPort: occupied });
|
||||
+ assert.match(readFileSync(join(dir, 'sshd-0.conf'), 'utf8'), new RegExp(`^Port ${occupied}\\n`));
|
||||
+ assert.equal(readFileSync(join(dir, 'sshd-0.log'), 'utf8').includes(`Server listening on 127.0.0.1 port ${occupied}.`), false);
|
||||
+ assert.notEqual(port, occupied);
|
||||
+ assert.ok([1, 2, 3, 4].some(n => existsSync(join(dir, `sshd-${n}.log`)) &&
|
||||
+ readFileSync(join(dir, `sshd-${n}.log`), 'utf8').includes(`Server listening on 127.0.0.1 port ${port}.`)));
|
||||
+});
|
||||
+
|
||||
+test('row 280: every injected picker sample is unprivileged and outside the ephemeral range', () => {
|
||||
+ const [lo, hi] = ephemeralRange();
|
||||
+ for (const sample of [0, 0.25, 0.5, 0.6384, 0.75, 0.999999]) {
|
||||
+ const port = pickPort(() => sample);
|
||||
+ assert.ok(port >= 1024 && port <= 65535);
|
||||
+ assert.ok(port < lo || port > hi, `sample ${sample}: ${port} inside ${lo}-${hi}`);
|
||||
+ }
|
||||
+});
|
||||
+
|
||||
const GRANT = [['true'], ['false'], ['echo'], ['sleep'], ['head'], ['cat'], ['systemctl', 'status']];
|
||||
async function hostFixture(t, { commands = GRANT, reviewerHosts, wrongHostKey = false, coderWeb2 = false } = {}) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'h210-'));
|
||||
diff --git a/packages/bus/tests/scratch-sshd.mjs b/packages/bus/tests/scratch-sshd.mjs
|
||||
new file mode 100644
|
||||
index 000000000..2dde26de3
|
||||
--- /dev/null
|
||||
+++ b/packages/bus/tests/scratch-sshd.mjs
|
||||
@@ -0,0 +1,88 @@
|
||||
+// Row 280: fixture-only sshd startup. A TCP connect alone does not prove ownership.
|
||||
+import { spawn } from 'node:child_process';
|
||||
+import { once } from 'node:events';
|
||||
+import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
+import { connect } from 'node:net';
|
||||
+import { basename, join } from 'node:path';
|
||||
+
|
||||
+export const ephemeralRange = () => {
|
||||
+ const text = readFileSync('/proc/sys/net/ipv4/ip_local_port_range', 'utf8').trim();
|
||||
+ if (!/^\d+\s+\d+$/.test(text)) throw Error('invalid ephemeral port range');
|
||||
+ const range = text.split(/\s+/).map(Number);
|
||||
+ if (range[0] < 1 || range[1] > 65535 || range[0] > range[1]) throw Error('invalid ephemeral port range');
|
||||
+ return range;
|
||||
+};
|
||||
+
|
||||
+export function pickPort(rand = Math.random) {
|
||||
+ const [lo, hi] = ephemeralRange();
|
||||
+ const below = Math.max(0, Math.min(lo, 65536) - 1024);
|
||||
+ const above = Math.max(0, 65535 - Math.max(hi, 1023));
|
||||
+ const sample = rand();
|
||||
+ if (!below && !above) throw Error('no scratch sshd port available');
|
||||
+ if (!(sample >= 0 && sample < 1)) throw Error('invalid random sample');
|
||||
+ const offset = Math.floor(sample * (below + above));
|
||||
+ return offset < below ? 1024 + offset : Math.max(hi + 1, 1024) + offset - below;
|
||||
+}
|
||||
+
|
||||
+const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
+const listening = port => new Promise(resolve => {
|
||||
+ const socket = connect(port, '127.0.0.1');
|
||||
+ const done = result => { socket.destroy(); resolve(result); };
|
||||
+ socket.setTimeout(200, () => done(false));
|
||||
+ socket.on('connect', () => done(true));
|
||||
+ socket.on('error', () => done(false));
|
||||
+});
|
||||
+const stop = async daemon => {
|
||||
+ if (daemon.pid === undefined || daemon.exitCode !== null || daemon.signalCode !== null) return;
|
||||
+ const exited = once(daemon, 'exit');
|
||||
+ daemon.kill('SIGKILL');
|
||||
+ await exited;
|
||||
+};
|
||||
+
|
||||
+export async function startScratchSshd(t, dir, config, { firstPort } = {}) {
|
||||
+ let lastLog;
|
||||
+ for (let attempt = 0; attempt < 5; attempt++) {
|
||||
+ const port = attempt === 0 && firstPort !== undefined ? firstPort : pickPort();
|
||||
+ const configFile = join(dir, `sshd-${attempt}.conf`);
|
||||
+ lastLog = join(dir, `sshd-${attempt}.log`);
|
||||
+ writeFileSync(configFile, [`Port ${port}`, ...config, ''].join('\n'));
|
||||
+ writeFileSync(lastLog, '');
|
||||
+ const daemon = spawn('/usr/bin/sshd', ['-D', '-f', configFile, '-E', lastLog], { stdio: 'ignore' });
|
||||
+ t.after(() => stop(daemon));
|
||||
+ const saveLog = () => {
|
||||
+ if (!process.env.MOSAIC_SSHD_EVIDENCE) return;
|
||||
+ const evidence = join(process.env.MOSAIC_SSHD_EVIDENCE, basename(dir));
|
||||
+ mkdirSync(evidence, { recursive: true });
|
||||
+ writeFileSync(join(evidence, basename(lastLog)), readFileSync(lastLog));
|
||||
+ writeFileSync(join(evidence, basename(configFile)), readFileSync(configFile));
|
||||
+ };
|
||||
+ let spawnError;
|
||||
+ daemon.on('error', error => { spawnError = error; });
|
||||
+ const deadline = Date.now() + 3000;
|
||||
+ while (Date.now() < deadline && !spawnError && daemon.exitCode === null && daemon.signalCode === null) {
|
||||
+ if (await listening(port) && daemon.exitCode === null && daemon.signalCode === null &&
|
||||
+ readFileSync(lastLog, 'utf8').split('\n').some(line =>
|
||||
+ (line === `Server listening on 127.0.0.1 port ${port}.` ||
|
||||
+ line === `Server listening on 127.0.0.1 port ${port}.\r`))) { saveLog(); return port; }
|
||||
+ await sleep(30);
|
||||
+ }
|
||||
+ await stop(daemon);
|
||||
+ saveLog();
|
||||
+ }
|
||||
+ throw Error(`scratch sshd did not start: ${lastLog}`);
|
||||
+}
|
||||
+
|
||||
+// Long remote sleeps remain in flight for their original duration, but cannot outlive the fixture.
|
||||
+export function installSleepStub(dir) {
|
||||
+ const script = `const fs = require("node:fs");
|
||||
+const end = Date.now() + Number(process.argv[2]) * 1000;
|
||||
+const timer = setInterval(() => {
|
||||
+ if (!fs.existsSync(process.argv[1]) || Date.now() >= end) { clearInterval(timer); }
|
||||
+}, 50);`;
|
||||
+ writeFileSync(join(dir, 'fake', 'sleep'), `#!/bin/sh
|
||||
+case "$#:$1" in
|
||||
+ 1:30|1:60) exec '${process.execPath}' -e '${script}' '${dir}' "$1" ;;
|
||||
+ *) exec /usr/bin/sleep "$@" ;;
|
||||
+esac
|
||||
+`, { mode: 0o755 });
|
||||
+}
|
||||
@@ -0,0 +1,4 @@
|
||||
1cb0971e745745e3db329c7c75f36197664d7bf0687def33c0819134f121975f packages/bus/tests/grant-executor.test.mjs
|
||||
140ec358fff212b747412691e3cb5952844c13c7fe43d3ff1013d24f0df886d2 packages/bus/tests/host-run.test.mjs
|
||||
3b647e59f09e7fc43b41d072e46c3cea5ed7641d5d4a95c5b39c240f73cdb75e packages/bus/tests/scratch-sshd.mjs
|
||||
94c21723cd42f551bbd6249d5133cbfaa12ac25efef5a72bf525998ac3c52100 docs/plans/DEFERRED.md
|
||||
@@ -0,0 +1 @@
|
||||
b765f5cb812e60789e4129ed2ab8009f5439a5be
|
||||
@@ -0,0 +1,4 @@
|
||||
packages/bus/tests/grant-executor.test.mjs
|
||||
packages/bus/tests/host-run.test.mjs
|
||||
packages/bus/tests/scratch-sshd.mjs
|
||||
docs/plans/DEFERRED.md
|
||||
@@ -0,0 +1 @@
|
||||
b765f5cb812e60789e4129ed2ab8009f5439a5be
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"cwd_processes": [],
|
||||
"pgrep_checks": [
|
||||
{
|
||||
"tmpdir": "/mnt/storage/scratch/tmp/gInD",
|
||||
"exit": 1,
|
||||
"matches": []
|
||||
},
|
||||
{
|
||||
"tmpdir": "/mnt/storage/scratch/tmp/gVtU",
|
||||
"exit": 1,
|
||||
"matches": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"helper_sha256": "3b647e59f09e7fc43b41d072e46c3cea5ed7641d5d4a95c5b39c240f73cdb75e",
|
||||
"restored": true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"id": "M1",
|
||||
"old": " if (await listening(port) && daemon.exitCode === null && daemon.signalCode === null &&\n readFileSync(lastLog, 'utf8').split('\\n').some(line =>\n (line === `Server listening on 127.0.0.1 port ${port}.` ||\n line === `Server listening on 127.0.0.1 port ${port}.\\r`))) { saveLog(); return port; }",
|
||||
"new": " if (await listening(port)) return port;",
|
||||
"pattern": "row 280: an occupied TCP port"
|
||||
},
|
||||
{
|
||||
"id": "M2",
|
||||
"old": "export function pickPort(rand = Math.random) {\n const [lo, hi] = ephemeralRange();\n const below = Math.max(0, Math.min(lo, 65536) - 1024);\n const above = Math.max(0, 65535 - Math.max(hi, 1023));\n const sample = rand();\n if (!below && !above) throw Error('no scratch sshd port available');\n if (!(sample >= 0 && sample < 1)) throw Error('invalid random sample');\n const offset = Math.floor(sample * (below + above));\n return offset < below ? 1024 + offset : Math.max(hi + 1, 1024) + offset - below;\n}\n",
|
||||
"new": "export function pickPort(rand = Math.random) {\n return 20000 + Math.floor(rand() * 20000);\n}\n",
|
||||
"pattern": "row 280: every injected picker sample"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
d5d82d7cb679332e7d90ed26220cd7d3170931d2e8aa2ccdfc35ceea12147c11 BUILD.md
|
||||
0e8f1e76f8c13d7de5cbea1ae8b8c2f178ca5aa0283ac0dbd00bffcd4c39bea5 assertion-preservation.json
|
||||
052e9e79aa3c9ba1b6f8dedc7af00440e104d308ed9451a076e872c903704c7d base.txt
|
||||
5898d3c45b4274a9cbf117018811f2b6206987e3849e97ce171c3aaa42d314da build.patch
|
||||
27806c0579c76d0999d2e6be4180c3867570df0268af9d7e0d7b971af799e6f5 candidate-manifest.sha256
|
||||
634e46d73bbcdbdea70660954fcc8290dc3d6c8d15554218d59c78feafbd0b08 candidate.txt
|
||||
84f2091c0eb1b55c34b4462775160b5f808a6843e68b3e4cb54e1f7a2421eaf3 changed-paths.txt
|
||||
634e46d73bbcdbdea70660954fcc8290dc3d6c8d15554218d59c78feafbd0b08 commit.txt
|
||||
eb9f55fd5de0ec370742702f15d737fb91e35542dbda727d43502adce92903a9 final-sweep.json
|
||||
f9a578b95bd6f9e07dc231002a28ea9c3ae14fe2e269f3fbca50c4fc97ba61e0 mutant-restore.json
|
||||
72cfbf942b4437720d6b92ddef0bc138d27bd6ecef1f1da0b32a2b0250b3cd61 mutants.json
|
||||
b435b731ea4ccff12f90e1b53023cbc54bf65c08ad0f7feaf8a810088156a0b6 post-phase1-pgrep.txt
|
||||
9f8458487a1c5146230e70d40428b6fb218c93f378fd1a8e8607fab80c446b2c reader-search.txt
|
||||
634e46d73bbcdbdea70660954fcc8290dc3d6c8d15554218d59c78feafbd0b08 repeat-commit.txt
|
||||
42e240961767b817fb5f1d183bc949074b3b90c72335d65c3e0278a3db4294ea repeat-results.json
|
||||
e26b6ecefd1a6db69fbaaa38c4f08abe0ad69035ecaa8e2e29f5da2961e5da15 repeat-summary.json
|
||||
3e1903ee103b0a9ef8febeb4d2073848aca0e42fd4ecd1c586381f2ab7404be2 restored-hashes.json
|
||||
f38eaa23861b8acb1738ed082f25a0e6661d976b87da140993373de41bc4053d results.json
|
||||
76c5c03767ca34bc38a7f69c14fbf1c06e8ebcc93120bc577a44e2271f6fdb16 scan-result.json
|
||||
e7f560ce5fc957432ddcad231897a5decd33be06f1201926adabb62d3c218c83 skips.txt
|
||||
37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570 sweep-phase1.json
|
||||
@@ -0,0 +1 @@
|
||||
No process names the owned fixture TMPDIR.
|
||||
@@ -0,0 +1,28 @@
|
||||
Search: packages/bus/tests/grant-executor.test.mjs
|
||||
(none)
|
||||
|
||||
Search: grant-executor.test.mjs
|
||||
(none)
|
||||
|
||||
Search: packages/bus/tests/host-run.test.mjs
|
||||
(none)
|
||||
|
||||
Search: host-run.test.mjs
|
||||
(none)
|
||||
|
||||
Search: packages/bus/tests/scratch-sshd.mjs
|
||||
(none)
|
||||
|
||||
Search: scratch-sshd.mjs
|
||||
packages/bus/tests/grant-executor.test.mjs:22:import { ephemeralRange, installSleepStub, pickPort, startScratchSshd } from './scratch-sshd.mjs';
|
||||
packages/bus/tests/host-run.test.mjs:22:import { ephemeralRange, installSleepStub, pickPort, startScratchSshd } from './scratch-sshd.mjs';
|
||||
|
||||
Search: docs/plans/DEFERRED.md
|
||||
packages/queue/tests/fixtures/genesis-render.md:35:| 30 | Open gaps from the MVP work | unassigned | — | parked | — | Jason reopens (jason) | `docs/plans/DEFERRED.md` § Open | Fixed only when a gate needs them |
|
||||
packages/queue/tests/fixtures/queue-marked.md:25:Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
|
||||
|
||||
Search: DEFERRED.md
|
||||
packages/queue/tests/fixtures/genesis-render.md:35:| 30 | Open gaps from the MVP work | unassigned | — | parked | — | Jason reopens (jason) | `docs/plans/DEFERRED.md` § Open | Fixed only when a gate needs them |
|
||||
packages/queue/tests/fixtures/queue-marked.md:25:Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
|
||||
packages/queue/tests/fixtures/queue-marked.md:83:| Open gaps from the MVP work | Fixed only when a gate needs them | DEFERRED.md, "Open" |
|
||||
packages/queue/tests/fixtures/queue-marked.md:92:- 2026-09-13 — created; rows 1 to 8 taken from CURRENT.md, DEFERRED.md and the plan page.
|
||||
@@ -0,0 +1 @@
|
||||
b765f5cb812e60789e4129ed2ab8009f5439a5be
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"head": "b765f5cb812e60789e4129ed2ab8009f5439a5be",
|
||||
"manifest_sha256": "27806c0579c76d0999d2e6be4180c3867570df0268af9d7e0d7b971af799e6f5",
|
||||
"start": "2026-10-22T07:29:03Z",
|
||||
"end": "2026-10-22T07:48:38Z",
|
||||
"failed": false,
|
||||
"repeat_runs": 60,
|
||||
"load_runs": 2,
|
||||
"counts": {
|
||||
"repeat": {
|
||||
"tests": 2010,
|
||||
"pass": 2010,
|
||||
"fail": 0,
|
||||
"skipped": 0,
|
||||
"cancelled": 0
|
||||
},
|
||||
"cli-load": {
|
||||
"tests": 1588,
|
||||
"pass": 1588,
|
||||
"fail": 0,
|
||||
"skipped": 0,
|
||||
"cancelled": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"paths": 4,
|
||||
"drift": []
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
[
|
||||
{
|
||||
"step": "red-grant",
|
||||
"start": "2026-10-22T07:22:02Z",
|
||||
"end": "2026-10-22T07:22:02Z",
|
||||
"seconds": 0.029,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: an occupied TCP port",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "red-grant-actual",
|
||||
"start": "2026-10-22T07:22:42Z",
|
||||
"end": "2026-10-22T07:22:43Z",
|
||||
"seconds": 0.111,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: an occupied TCP port",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "red-host-actual",
|
||||
"start": "2026-10-22T07:22:43Z",
|
||||
"end": "2026-10-22T07:22:43Z",
|
||||
"seconds": 0.143,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: an occupied TCP port",
|
||||
"packages/bus/tests/host-run.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "green-grant-regressions",
|
||||
"start": "2026-10-22T07:22:56Z",
|
||||
"end": "2026-10-22T07:23:09Z",
|
||||
"seconds": 12.193,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 2,
|
||||
"pass": 1,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280:",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "diagnostic-log-bytes",
|
||||
"start": "2026-10-22T07:24:22Z",
|
||||
"end": "2026-10-22T07:24:34Z",
|
||||
"seconds": 12.196,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: an occupied TCP port",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "green-grant-regressions-crlf",
|
||||
"start": "2026-10-22T07:25:03Z",
|
||||
"end": "2026-10-22T07:25:03Z",
|
||||
"seconds": 0.178,
|
||||
"exit": 0,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 2,
|
||||
"pass": 2,
|
||||
"fail": 0,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280:",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "green-host-regressions-crlf",
|
||||
"start": "2026-10-22T07:25:03Z",
|
||||
"end": "2026-10-22T07:25:03Z",
|
||||
"seconds": 0.212,
|
||||
"exit": 0,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 2,
|
||||
"pass": 2,
|
||||
"fail": 0,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280:",
|
||||
"packages/bus/tests/host-run.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "grant-whole",
|
||||
"start": "2026-10-22T07:25:03Z",
|
||||
"end": "2026-10-22T07:25:24Z",
|
||||
"seconds": 20.673,
|
||||
"exit": 0,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 41,
|
||||
"pass": 41,
|
||||
"fail": 0,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "host-whole",
|
||||
"start": "2026-10-22T07:25:24Z",
|
||||
"end": "2026-10-22T07:25:38Z",
|
||||
"seconds": 14.367,
|
||||
"exit": 0,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 26,
|
||||
"pass": 26,
|
||||
"fail": 0,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"packages/bus/tests/host-run.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "M1-grant-executor",
|
||||
"start": "2026-10-22T07:26:25Z",
|
||||
"end": "2026-10-22T07:26:25Z",
|
||||
"seconds": 0.113,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: an occupied TCP port",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "M1-host-run",
|
||||
"start": "2026-10-22T07:26:25Z",
|
||||
"end": "2026-10-22T07:26:26Z",
|
||||
"seconds": 0.146,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: an occupied TCP port",
|
||||
"packages/bus/tests/host-run.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "M2-grant-executor",
|
||||
"start": "2026-10-22T07:26:26Z",
|
||||
"end": "2026-10-22T07:26:26Z",
|
||||
"seconds": 0.095,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: every injected picker sample",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "M2-host-run",
|
||||
"start": "2026-10-22T07:26:26Z",
|
||||
"end": "2026-10-22T07:26:26Z",
|
||||
"seconds": 0.128,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: every injected picker sample",
|
||||
"packages/bus/tests/host-run.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "M1-final-grant-executor",
|
||||
"start": "2026-10-22T07:48:56Z",
|
||||
"end": "2026-10-22T07:48:57Z",
|
||||
"seconds": 0.12,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: an occupied TCP port",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "M1-final-host-run",
|
||||
"start": "2026-10-22T07:48:57Z",
|
||||
"end": "2026-10-22T07:48:57Z",
|
||||
"seconds": 0.164,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: an occupied TCP port",
|
||||
"packages/bus/tests/host-run.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "M2-final-grant-executor",
|
||||
"start": "2026-10-22T07:48:57Z",
|
||||
"end": "2026-10-22T07:48:57Z",
|
||||
"seconds": 0.108,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: every injected picker sample",
|
||||
"packages/bus/tests/grant-executor.test.mjs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step": "M2-final-host-run",
|
||||
"start": "2026-10-22T07:48:57Z",
|
||||
"end": "2026-10-22T07:48:57Z",
|
||||
"seconds": 0.138,
|
||||
"exit": 1,
|
||||
"timeout": false,
|
||||
"counts": {
|
||||
"tests": 1,
|
||||
"pass": 0,
|
||||
"fail": 1,
|
||||
"cancelled": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
"--test",
|
||||
"--test-reporter=tap",
|
||||
"--test-name-pattern=row 280: every injected picker sample",
|
||||
"packages/bus/tests/host-run.test.mjs"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"binary_files": [],
|
||||
"secret_matches": []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
None. All 60 repeat files and both complete CLI load cycles report skipped 0.
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,72 @@
|
||||
# Row 216 (C2-05a, #1717): Sage's rulings on the phase A build notes
|
||||
|
||||
Sage, 2026-10-21T23:12Z. These answer `agents/researcher/work/queue-216/BUILD.md` (phase A, uncommitted
|
||||
in worktree `.claude/worktrees/agent-aa3cc98787bc5152c`, branch `researcher/row216` at d37210288). The
|
||||
numbers follow that file's "Choices" list. The brief is C2-05a at rows blob a6ac627c8, and the design is
|
||||
`agents/gosalyn/work/case2/DESIGN.md` as landed in 5d2437f89 (ruling 51fdfb7e2).
|
||||
|
||||
## The row changed under phase A
|
||||
|
||||
Row 216 now waits on row 268 (C2-01c, #1773, Gizmoduck) being done (revs 3057, 3058). PRD 1.14 and the
|
||||
ruling after r11 added three playground levels. The brief at a6ac627c8 now asks for these, one test each:
|
||||
|
||||
- a lookup that names a `book` leaves `book: null` files out
|
||||
- `library_manuscript` never reads a `book: null` file
|
||||
- a draft universe has no canon, so its lookups return files only with `scope: "playground"`
|
||||
- `library_universe` on a draft returns its kind, name and prefixes
|
||||
|
||||
Phase A was written before that, on d37210288. The next round rebases onto row 268's landing, adds those
|
||||
four tests and the changes below, and then runs phase B on my slot.
|
||||
|
||||
## Rulings
|
||||
|
||||
1. Paging with `offset` and `next`: approved as built.
|
||||
2. `get` takes a required `universe`: approved. Universe isolation in code outranks the table's short
|
||||
argument list. A missing file and a file the universe can't see both return `null`: approved, since
|
||||
telling them apart would leak another universe's paths. The optional `from` line is approved.
|
||||
3. `get` over the cap: page the links, don't refuse. `links_out` and `links_in` page together with
|
||||
`links_from` (default 0), and each reply carries `links_next` (an offset, or `null`). Lines keep
|
||||
`from` and `next`. Every page carries `frontmatter`. `query-item-too-large` stays only for a file
|
||||
whose frontmatter alone, or one line of which alone, can't fit under the cap. A hub note with many
|
||||
backlinks has to be readable.
|
||||
4. `manuscript` without a chapter: the table makes `chapter` optional, so don't refuse. With no chapter,
|
||||
return the book's first chapter file in path order. Every reply carries `next_chapter`, the next
|
||||
chapter's name or `null`. Drop `query-chapter-required`. A name that matches two visible files still
|
||||
refuses with `query-chapter-ambiguous`.
|
||||
5. `search` excerpt: the whole line, as built. A line too long to fit alone doesn't refuse the search.
|
||||
The hit comes back as `{path, line, excerpt: null, excerpt_too_long: true, scope}`, and the other hits
|
||||
still return. Nothing is cut.
|
||||
6. `exclude`: keep the path-prefix reading only. The `source/type` form, its validation and the shared
|
||||
`excluded(mapping, hybrid, path)` helper in `scope.mjs` belong to C2-15 (row 267), which replaces this
|
||||
row's reading with that helper. Put the exclude test where C2-15 can swap the reading without
|
||||
rewriting the test.
|
||||
7. `pin` hides nothing. `universeInfo` reports it: confirmed. C2-15 checks pins.
|
||||
8. `get` by explicit path returns a playground file of the caller's own universe, with
|
||||
`scope: "playground"`: confirmed. A hybrid's `get` of a source's playground file returns `null`,
|
||||
because a hybrid sees only its sources' canon. Add a test for that case.
|
||||
9. Links: confirmed as built. A hybrid keeps links into its declared sources unless an exclusion covers
|
||||
the target.
|
||||
10. `find` matching: confirmed.
|
||||
11. Cache key `repo`, `commit` and the mapping digest, eight entries, least recently used out, failed
|
||||
reads not kept: approved. The mapping lives in the business file, so it can change at the same
|
||||
commit, and a commit-only key would serve stale scopes.
|
||||
12. `universeInfo` builds no index: confirmed.
|
||||
13. Every reply carries the index commit: confirmed.
|
||||
|
||||
## Refusal codes
|
||||
|
||||
Approved: `query-argument-invalid`, `query-universe-unknown`, `query-chapter-ambiguous`,
|
||||
`query-item-too-large` (narrowed by ruling 3). Dropped: `query-chapter-required` (ruling 4). The reused
|
||||
reader and mapping codes stay as listed.
|
||||
|
||||
## The tmpdir flag
|
||||
|
||||
No separate row is needed. `helpers.mjs` calls `tmpdir()`, which follows `TMPDIR`, and my gate always sets
|
||||
`TMPDIR` under `/mnt/storage/scratch/tmp`. The new `query-fixture.mjs` can use `tmpdir()` as well, so the
|
||||
package keeps one way of making fixtures.
|
||||
|
||||
## Commit
|
||||
|
||||
The commit waits until phase B is green, under invariant 8. The working tree holds the phase A changes
|
||||
until then. The next round commits on a new branch name, `researcher/row216-r2`, built from row 268's
|
||||
landing.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Row 220 (C2-10a, #1721): Sage's rulings on the phase A questions
|
||||
|
||||
Sage, 2026-10-21T23:14Z. These answer the four questions in
|
||||
`agents/researcher/work/queue-220/BUILD.md` on branch `researcher/row220` (3fa833715, on d37210288). The
|
||||
brief is C2-10a at rows blob a6ac627c8. The design is `agents/gosalyn/work/case2/DESIGN.md` as landed in
|
||||
5d2437f89, rows M7 and M8 in 5.1. None of these rulings changes what a requirement allows. They pick the
|
||||
input M7 and M8 need, which the design leaves open.
|
||||
|
||||
## Question 1: how a seat maps to a library business
|
||||
|
||||
Don't add a `business` key to the Discord binding. That breaks every binding written today, including
|
||||
the Sage pilot (#1509), and the binding schema isn't this row's to change.
|
||||
|
||||
Build one helper in `packages/business/src`, for example `library-seats.mjs`, exported from the package
|
||||
index. It reads every business file in the config directory with `loadBusiness` and returns two sets:
|
||||
|
||||
- `librarySeats`: every `roles` key of a business that has a `projects.<id>.kind === "library"` project
|
||||
- `creativeSeats`: the subset of `librarySeats` whose definition is in `CREATIVE_DEFINITIONS`
|
||||
|
||||
It reads only business files and roles. Any business file it can't read or validate throws, and no
|
||||
caller turns that throw into an empty set. A seat name that appears in two businesses counts if either
|
||||
business puts it in a set.
|
||||
|
||||
- M7: the control board passes `isCreative = (root) => creativeSeats.has(root.seat)` to `createReader`.
|
||||
It computes the sets on each `catalogue()` and `open()`, not once at start, because the business file
|
||||
can change while the board runs. When the helper throws, `catalogue()` and `open()` refuse with
|
||||
`conversation-business-unreadable`. They don't list everything, and they don't silently list nothing.
|
||||
- M8: `discord check` and `discord run` build the sets from the config directory their `--config`
|
||||
already names. A binding whose `seat` is in `librarySeats` refuses `discord-engine-not-allowed` unless
|
||||
its engine is `mosaic-bus` with no `tools`. When the helper throws, both verbs refuse with
|
||||
`discord-business-unreadable`. A pi binding whose seat is in no library business checks clean, which
|
||||
is the brief's last test.
|
||||
|
||||
Your reading of the session paths is right. Creative thread sessions live at
|
||||
`<dataRoot>/threads/<business>/<thread>/session`, outside every `.pi/state` root the board scans today, so
|
||||
M7 is a second wall. Write that in the manifest notes. Add one test where a root spec points at a
|
||||
creative seat's thread store and the root is hidden.
|
||||
|
||||
Scope amendment. The row adds these files: `packages/business/src/library-seats.mjs` (or the name you
|
||||
pick), its export line in `packages/business/src/index.mjs`, a test in `packages/business/tests`,
|
||||
`packages/control-board/src/serve.mjs` (the predicate wiring and `REFUSAL_STATUS` only), and a test in
|
||||
`packages/control-board/tests`. `packages/discord/src/binding.mjs` stays untouched. The gate adds the
|
||||
business and control-board suites, run whole, to the conversation and discord suites, plus every
|
||||
`scripts/test-*.sh`.
|
||||
|
||||
## Question 2: the refusal code for a `mosaic-bus` binding with `tools`
|
||||
|
||||
Reuse `discord-engine-not-allowed`, with a message that names the `tools` key. Approved as proposed.
|
||||
|
||||
## Question 3: `reconcile` on `conversation-not-listed`
|
||||
|
||||
`false`: confirmed. `reconcile: true` on `unknown-conversation` and `incomplete-header` means a fresh read
|
||||
may answer differently. A hidden root gives the same answer on every read until a human edits the
|
||||
business file.
|
||||
|
||||
## Question 4: HTTP status for `conversation-not-listed`
|
||||
|
||||
404, next to `unknown-conversation`: approved. `conversation-business-unreadable` gets 503. Both entries
|
||||
go in `REFUSAL_STATUS`, which ruling 1 put in scope.
|
||||
|
||||
## Other points
|
||||
|
||||
- The control-board serve test: keep it in phase B, run whole.
|
||||
- New codes approved here: `conversation-business-unreadable` and `discord-business-unreadable`. Nothing
|
||||
else is new.
|
||||
- The phase A commit 3fa833715 stays on `researcher/row220`. The next round goes on a new branch name,
|
||||
`researcher/row220-r2`. Phase B runs only on my "slot clear".
|
||||
@@ -0,0 +1,83 @@
|
||||
# Row 264 (T1 item 7 sessions, #1769): Sage's rulings on the phase A questions
|
||||
|
||||
Sage, 2026-10-21T23:34Z. These answer Q1-Q7 in `agents/gizmoduck/work/queue-264/BUILD.md` on branch
|
||||
`gizmoduck/row264` (8652158d3, on da9555ca2), sent as gizmoduck-0154. The brief is
|
||||
`docs/plans/2026-10-21_t1-sessions.md` at blob ff0be0c79 (design r3, Mos's rulings mos-1235 in section 8).
|
||||
None of these rulings changes what a requirement allows. They set order, labels and integration points the
|
||||
brief leaves open.
|
||||
|
||||
## Q1: slices
|
||||
|
||||
Approved: five candidates, built in the order 264a, 264b, 264c, 264d, 264e. Each gets its own manifest,
|
||||
its own Duckworth round and its own landing. Row 264 carries 264a. I add one row each for 264b-264e,
|
||||
with a Gitea issue, in the next slot gap. Their `after` lists:
|
||||
|
||||
- 264b after 264a.
|
||||
- 264c after 264b. It reads the per-session store, so it can't land before the store exists.
|
||||
- 264d after 264b and row 194 (see Q4).
|
||||
- 264e after 264a. Its clear and count parts switch on as 264c and 264d land. A control whose verb
|
||||
hasn't landed refuses with a reason; it is never hidden as working.
|
||||
|
||||
All five are MVP. TARGET.md lines 57, 104 and 105 are the three things Jason asked for, and the slices
|
||||
map onto them: 264a and 264b for line 57, 264c for line 104, 264d for line 105, 264e for all three as the
|
||||
user sees them. None of them is cut by this ruling. Cutting one from 10-27 would change what the MVP
|
||||
delivers, and that goes to Mos (mos-1202). I report the schedule risk on the standup, not as a question.
|
||||
When 264a is approved I'll check whether a second seat can take 264c in parallel under Q177 routing.
|
||||
|
||||
## Q2: schema label
|
||||
|
||||
The label goes by landing order, not by row. Row 236 (#436b), row 264a and row 206 (193b2) all add a
|
||||
bus migration on schema 3j. The first of the three to land takes 3j-3k. The next one rebases and takes
|
||||
the next label, and so on. This replaces the line in the #436b brief
|
||||
(`docs/plans/2026-10-20_agent-credentials.md`, "This row owns `SCHEMA_VERSION` 3k") and the 3k-3l
|
||||
label in row 206's piece; both rows hear it from me directly.
|
||||
|
||||
Build 264a as `3j-3k.sql` now. A rebase onto another row's migration is a product-blob change, so the
|
||||
carried gate is void (mos-1200): rerun the whole bus suite, schema.test.mjs with its step-count pins, and
|
||||
the T46 restore-guard table lists. Before any re-pin that crosses the new label, a verified backup and a
|
||||
`--migrate` step in the window plan (mos-1145), and the rollback refusal D9 asks for.
|
||||
|
||||
## Q3: PRD
|
||||
|
||||
Nothing to wait for. PRD 1.13 has landed on refactor with both replacements:
|
||||
`docs/prd/mosaic-stack.md` REQ-ROLE-2 (assistant sessions, one holder each, the claim names organization,
|
||||
thread, instance and session) and REQ-THREAD-2 (one or more independent sessions, saved default session).
|
||||
264a builds to that text. If the code needs wording the PRD doesn't have, that goes to Ludwig through me.
|
||||
|
||||
## Q4: stop proof
|
||||
|
||||
Row 194's lifecycle path is the stop owner. The brief names it (F12 and D7 step 2, "the reviewed
|
||||
lifecycle path"). Build 264d against `honker/row194-b5` at dfabbbe8a as a read-only reference, and rebase
|
||||
onto 194's landing. 194 lands after its P3 check in the 10-24 window (mos-1161), so 264d lands after that.
|
||||
Don't copy 194 code into 264d's manifest.
|
||||
|
||||
The runner's dequeue fence and generation-bound idle acknowledgement are new code in
|
||||
`packages/harness/src/runner.mjs`, and they belong to 264d. Approved as a named review item. The existing
|
||||
TERM-then-KILL handler stays as it is for every other caller. D7 forbids calling it while a child runs.
|
||||
|
||||
## Q5: kill checks
|
||||
|
||||
Approved, as D8 already says. Use today's launcher limit and revocation path. Add one shared admission
|
||||
hook that every session start goes through. Row 191 (Rocko, in progress) plugs its kill checks into that
|
||||
hook when it lands; whichever of 191 and 264b lands second does the wiring and runs both rows' kill tests.
|
||||
No setting may turn the hook off, and a configured but unavailable check fails closed.
|
||||
|
||||
## Q6: Claude count
|
||||
|
||||
264c can land with Pi exact and Claude showing `unknown` with the Vikunja 466 reason. T3 passes for Pi
|
||||
in 264c. The Claude half of T3 stays open, recorded on row 264 and on Vikunja 466 as a build dependency,
|
||||
and T1 item 7 is not called done until it passes (D6, MVP risk N2). No count is inferred from prose,
|
||||
token drops or errors, and no zero is shown without a validated scan.
|
||||
|
||||
## Q7: RBAC
|
||||
|
||||
Approved, as D4 says. Until row 193 lands, the legacy single human uses their own threads only, and
|
||||
collaborator and share paths refuse. T5 in this row covers that case, forged identity and cross-instance
|
||||
IDs. The A4 `session.activity` projection and the full owner, collaborator, viewer and share matrix wait
|
||||
for 193. The row that lands second wires them and runs the full T5.
|
||||
|
||||
## Process
|
||||
|
||||
- Claim the row in the first slot gap. Tests and queue writes run only on my CLEAR.
|
||||
- One branch per round. A rebase goes on a new branch name (mos-1086).
|
||||
- Design notes stay in `agents/gizmoduck/work/queue-264/BUILD.md`.
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-22: row 191 (kill switch) landed, comment on P19 414 (Q174 landing comment). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r191-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 414, title: /^MVP platform: the Given list for Cases 1 and 2/, body: [
|
||||
"Row 191 (kill switch, stopRun) landed on refactor as 0fa7bf421 on 10-22 (recorded 04:49Z), a fast-forward on 86f95bb27. An owner can halt an instance: agent verbs that start or continue work refuse with a fixed code, reads and the human's own verbs (including the clear) still work, and running runs are stopped. Gitea #1689 is closed (comment 30360). Queue row 191 is done (rev 3120).",
|
||||
"Rocko built it, and Dewey approved the manifest (88 paths, digest 0bcbf337, comment 30359, rev 3118). Gate on 0fa7bf421: 35 touched files whole, every package suite, the ten scripts/test-*.sh, and 77/77 mutants checked. Sage ran the runs suite 43/0/0 for Dewey's N7. Follow-ups are on Vikunja 502 (real clock, exact HTTP refusals, runtime close in finally, runner cause text).",
|
||||
"It isn't live yet. Live is still 86f95bb27. The switch reaches dragon-lin at the next re-pin, through the window plan and Mos.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,48 @@
|
||||
// Sage, 2026-10-21: row 215 (C2-04) landed, comment on P19 416 (Q174 landing comment). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r215-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 416, title: /^MVP Case 2: creative writing hub instance/, body: [
|
||||
"Row 196 landed (7247d80c7). Two rows were waiting on it. Row 253 (#1758, Gander, Duckworth reviewing) moves case2-unit.sh and case1-accept.sh to --tester-socket. Until it lands, .53 and .54 take no copy carrying 196. Row 254 (#1759, Megavolt, Dewey reviewing) holds the library sync deferrals from row 222.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,49 @@
|
||||
// Sage, 2026-10-21: row 215 (C2-04) landed, comment on P19 416 (Q174 landing comment). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r215-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 435, title: /^Console inbox: Approve\/Deny for grant requests by the signed-in human \(DECISIONS Q188\)$/, body: [
|
||||
"Row 196 (the human boundary across uids and humans, #1694) landed on refactor as 7247d80c7 at 20:57Z. The bus binds each human proof to the socket peer with SO_PEERCRED and records the interpreter it trusts in bus/peercred-starts.jsonl. The Console's tester listener moves from TCP --tester-port to a private socket, --tester-socket. Gander built it, and Duckworth approved round 1 (rev 2973) and round 2 (rev 2986). Gate gander-0066 passed 2334 with 0 failures and 1 skip, plus 38/38 mutants. #1694 is closed (comment 30159), and queue row 196 is done (rev 2988).",
|
||||
"No live change yet. The 10-24 re-pin (plan draft 1, approved by Mos at 3fb00018e) carries 196 on dragon-lin and switches the live Console from --tester-port 7333 to --tester-socket. Until row 253 (#1758) moves case2-unit.sh and case1-accept.sh to the socket form, .53 and .54 take no copy carrying 196 (mos-1198).",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,52 @@
|
||||
// Sage, 2026-10-21: row 215 (C2-04) landed, comment on P19 416 (Q174 landing comment). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r215-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 435, title: /J2|uid|separat|human boundary|peer/i, body: [
|
||||
"Row 196 (the human boundary across uids and humans, #1694) landed on refactor as 7247d80c7 at 20:57Z. The bus binds each human proof to the socket peer with SO_PEERCRED and records the interpreter it trusts in bus/peercred-starts.jsonl. The Console's tester listener moves from TCP --tester-port to a private socket, --tester-socket. Gander built it, and Duckworth approved round 1 (rev 2973) and round 2 (rev 2986). Gate gander-0066 passed 2334 with 0 failures and 1 skip, plus 38/38 mutants. #1694 is closed (comment 30159), and queue row 196 is done (rev 2988).",
|
||||
"No live change yet. The 10-24 re-pin carries 196 on dragon-lin and switches the live Console from --tester-port 7333 to --tester-socket. Until row 253 (#1758) moves case2-unit.sh and case1-accept.sh to the socket form, .53 and .54 take no copy carrying 196 (mos-1198).",
|
||||
] },
|
||||
{ id: 416, title: /^MVP Case 2: creative writing hub instance/, body: [
|
||||
"Row 196 landed (7247d80c7). Two rows were waiting on it. Row 253 (#1758, Gander, Duckworth reviewing) moves case2-unit.sh and case1-accept.sh to --tester-socket. Until it lands, .53 and .54 take no copy carrying 196. Row 254 (#1759, Megavolt, Dewey reviewing) holds the library sync deferrals from row 222.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 210 landed, comment on P19 424, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r210-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 424, title: /^Credential store: per-agent host access/, body: [
|
||||
"Row 210 (agent host access with sudo through the credential store) landed on refactor as f5753c0e8, pushed. Gitea #1707 closed with the landing comment 29853.",
|
||||
"Built by Gizmoduck (r5 0485384e9), reviewed by Duckworth (approve rounds 2 and 3, comment 29851). Mutants 52 of 53 killed, M22b equivalent with coverage moved to row 235. Gate green: 15 package suites 2796 pass, 10 script suites 429 pass, 0 fail.",
|
||||
"No live change yet. It goes live in the 10-24 re-pin window with rows 190, 199, 237 and 234 (mos-1145). Next on this line: row 234 (machine grant row a, Gizmoduck) is briefed, then row 235 (row b) and row 236 (#436b).",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 215 (C2-04) landed, comment on P19 416 (Q174 landing comment). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r215-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 416, title: /^MVP Case 2: creative writing hub instance/, body: [
|
||||
"Row 215 (C2-04: the library entry in the business file, consumer skips, M9 organization-projects-protected, the bare mirror, library.file and library.status) landed on refactor as e54e05463 at 19:24Z. Gitea #1716 is closed (comment 30133). Queue row 215 is done (rev 2955).",
|
||||
"Herb built it, and Filbert approved the merge-scoped round 2 (rev 2924). The landing patch-id equals the candidate dde3445ab on a36523713. Herb ran the gate on grant sage-2071, with the business, seat, cli, project, bus and webui suites whole plus every scripts/test-*.sh: 2634 pass, 0 fail, and one environment skip in webui.",
|
||||
"Row 222 (C2-03a fetch-only sync, Megavolt) was waiting on this landing and now rebases onto e54e05463. Nothing is re-pinned yet.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 232 (installer I1) landed, comment on P19 432, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r232-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 432, title: /^Installer row: one-command install and upgrade for a fresh host/, body: [
|
||||
"I1 landed: row 232 (stack-release --units external for root-owned units, with confirm, revert and gate --since; stack-copy copy --bundle and wrapper --create) is on refactor as e65a3ab4d, pushed. Gitea #1734 closed with the landing comment 29874.",
|
||||
"Built by Darkwing (round 2 da2da2f6c, a rebase over row 230's README hunk), reviewed by Filbert (approve rounds 1 and 2, comments 29829 and 29865). Mutants 17 of 17 killed. Landing gate green: packages/cli 593 pass, 10 script suites 429 pass, 0 fail.",
|
||||
"No live change; --units user stays the default. Installer line: I2 is rows 230 (landed c8bc5cf4c) and 226, I3 is row 233 (briefed, Darkwing), I4 is row 240 (hostcheck).",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 233 (installer I3) landed, comment on P19 432, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r233-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 432, title: /^Installer row: one-command install and upgrade for a fresh host/, body: [
|
||||
"I3 landed: row 233 (scripts/stack-upgrade.sh, the upgrade driver, and docs/guides/host-run-sheet.md) is on refactor as 57f59e0c3, pushed 05:39Z. Gitea #1735 closed with the landing comment 29931.",
|
||||
"Built by Darkwing (round 1 1a1dc80ca), reviewed by Filbert (approve, comment 29922). Mutants m1-m8 killed. Landing gate on the landing tree: packages/cli 617 pass, 10 script suites 585 pass, 0 fail. Patch-id and the manifest match the candidate.",
|
||||
"No live change. Filbert's N1, N4 and N5 go to I4 (row 240, Darkwing, briefed now). N2 and N3 are in DEFERRED.md. Row 234 round 3 is rebuilt on this landing.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Sage, 2026-10-21: row 234 (machine grant row a) landed, comment on P19 436, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r234-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 436, title: /^Guarded grant methods: on Approve, stack code creates the grant/, body: [
|
||||
"Row a landed: row 234 (machine grant row a: host enrollment, the hosts and host_grants store records, bus schema 3i to 3j) is on refactor as c771a36f4, pushed 06:16Z. Gitea #1740 closed with the landing comment 29938.",
|
||||
"Built by Gizmoduck (round 3 c783d5e39, one commit on the row 233 landing), reviewed by Duckworth (round 3 approve, comment 29935, rev 2683). 25 of 25 mutants killed. Sage's landing gate on the candidate tree: 15 packages 2890 pass and ten script suites 585 pass, 0 fail. Patch-id and the manifest match the candidate.",
|
||||
"No live change. The 3i to 3j migration runs in the 10-24 window (mos-1145), with a backup first. Duckworth's N10 and N12 go to row 235 (row b), which Gizmoduck rebuilds on this landing next.",
|
||||
] },
|
||||
];
|
||||
if (/LANDSHA|PUSHTIME|CLOSEID|GATE\b/.test(JSON.stringify(POSTS))) throw new Error("placeholder left; refusing");
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,53 @@
|
||||
// Sage, 2026-10-21: row 237 landed, rows 241 and 242 opened, comments on P19 440 and 441, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r237-rows-440-441.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 440, title: /^Bus publish: a retry after publish-timeout/, body: [
|
||||
"Row 237 landed on refactor as d7d943199 (Gitea #1743, closed, comment 29836). checkPublish refuses publish-busy while an open ticket holds the decision. Gate green, mutant M1 killed.",
|
||||
"This task is now queue row 242 (Gitea #1748, Megavolt, reviewer Duckworth), briefed. Brief: docs/plans/2026-10-20_agent-credentials.md, \"Brief: row 237 follow-up N1, a retry after publish-timeout pushes once\" (9d165be8c). The fix either holds the decision until the bus's push ends or kills the push and waits, with a slow-push retry test and a mutant.",
|
||||
] },
|
||||
{ id: 441, title: /^Bus land: check whether checkLand has the same open-ticket race/, body: [
|
||||
"Megavolt's analysis (megavolt-0068, megavolt-0069): no end-to-end race. land-busy (git-socket.mjs:351) allows one landing per socket. finishLand moves main by compare-and-swap (seat working.mjs:391, base-moved). recordLand reruns #landFacts, which refuses decision-consumed (broker.mjs:945). Sage accepted it in sage-1651.",
|
||||
"This task is now queue row 241 (Gitea #1747, Megavolt, reviewer Duckworth). It changes no source. It adds a two-ticket broker land test and three mutants, one per guard. Brief (c25785d3d): \"Brief: row 237 follow-up, one decision lands once, with tests at the broker\". Megavolt claimed it at rev 2606.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 240 (installer I4) landed, comment on P19 432, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r240-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 432, title: /^Installer row: one-command install and upgrade for a fresh host/, body: [
|
||||
"I4 landed: row 240 (scripts/case1-hostcheck.sh with its pins in case1-pins.sh, the boot unit mosaic-hostcheck.service, and the I3 fixes in stack-upgrade.sh and the host run sheet) is on refactor as c515273db, pushed 06:57Z. Gitea #1746 closed with the landing comment 29954.",
|
||||
"Built by Darkwing (ad1b23ab2), reviewed by Filbert (approve, comment 29945). Mutants M1-M8 killed. Landing gate on the landing tree: packages/cli 639 pass, 10 script suites 585 pass, 0 fail. Patch-id and the manifest match the candidate.",
|
||||
"No live change; the host check is installed by hand on a Case 1 host from the run sheet. Filbert's N1 (the boot unit orders only mosaic-bus@mosaic-stack) and N2 (the apt hook goes in before the first --check) are in DEFERRED.md. N1 must be fixed before an install whose business id isn't mosaic-stack.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 232 (installer I1) landed, comment on P19 432, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r232-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 441, title: /^Bus land: check whether checkLand has the same open-ticket race/, body: [
|
||||
"Row 241 landed: 5d4a61776 on refactor, pushed. Gitea #1747 closed with the landing comment 29883.",
|
||||
"Answer: landing has no row 237 race. Three guards keep one decision from landing twice: land-busy at the git socket (cli git-socket.mjs:351), the CAS on main in finishLand (seat working.mjs:391), and the recordLand recheck of #spent (bus broker.mjs:963). Megavolt's new broker test opens two checkLand tickets on one decision and shows the second recordLand refuses decision-consumed and records nothing.",
|
||||
"Built by Megavolt (3e3ed303e, tests and packet only, no source change), reviewed by Duckworth (approve, comment 29870). Sage's mutants M1-M3 on the three guards all killed. Landing gate green: 15 packages 2797 pass, 10 script suites 429 pass, 0 fail.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 232 (installer I1) landed, comment on P19 432, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r232-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 440, title: /^Bus publish: a retry after publish-timeout/, body: [
|
||||
"Row 242 landed: 5fb736b92 on refactor, pushed. Gitea #1748 closed with the landing comment 29894.",
|
||||
"Built by Megavolt (635c10924), reviewed by Duckworth (approve, comment 29889). Mutants M1 and M2 killed. Landing gate green: 15 packages 2826 pass, 10 script suites 429 pass, 0 fail.",
|
||||
"Open follow-ups: Duckworth's doc notes N1, N2 and N4 (a doc-only commit by Megavolt), and the late-record notice to the run (comment 432 above).",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 232 (installer I1) landed, comment on P19 432, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r232-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 440, title: /^Bus publish: a retry after publish-timeout/, body: [
|
||||
"Follow-up from row 242 (Megavolt's residual, Duckworth N3, ruled sage-1691). Row 242 makes a retry after publish-timeout push once: the bus holds the decision until its push ends and records a late push. But when a push is recorded after the agent was already told publish-timeout, the run gets no notice. The allowed event and the bus log line are the only trace, and a later publish on the decision gets decision-consumed.",
|
||||
"Risk: an agent told publish-timeout can tell the user the publish failed while the push is on the fork. That ends up as false evidence in the run.",
|
||||
"Proposed later row: when the bus records a late push, it sends the run a notice with the decision id and the ref, and a later decision-consumed refusal names the late record. This is a new message in the run protocol, so it gets its own brief. Not queued before 10-27 unless MVP work needs it.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Sage, 2026-10-21: row 242 doc follow-up (P19 440) and the row 210 T26 follow-up (P19 424), per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r242d-r210-t26.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 440, title: /^Bus publish: a retry after publish-timeout/, body: [
|
||||
"Row 242 doc follow-up landed: fc1cb856b on refactor, pushed. This is Megavolt's c5a125f4b (Duckworth N1, N2, N4): comment lines in packages/cli/src/git-socket.mjs plus BUILD.md, no test hash pins, so no gate under the doc-only rule. Patch-id matches. The late-record notice (comment 432) is still the open follow-up.",
|
||||
] },
|
||||
{ id: 424, title: /^Credential store: per-agent host access/, body: [
|
||||
"Row 210 follow-up, test hygiene (found by Gizmoduck in the row 234 singles, 2026-10-21): T26 in packages/bus/tests/host-run.test.mjs spawns /bin/sh -c 'sleep 60; :' at :488 (planted) and :496 (decoy), detached. Each has a t.after (:489, :497) that SIGKILLs the sh pid only, so its sleep child lives on. Two sleeps outlive each run by up to 60 s. Fix: since both are detached, kill the process group instead (process.kill(-pid, 'SIGKILL')). It doesn't affect any result, but a mutant or busy check that counts processes sees them. A small follow-up, not a blocker.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Sage, 2026-10-21: row 249 (grant undo hostLineLeft) landed, comment on P19 445, per mos-0882. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r249-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 445, title: /^Grant undo can't prove the remote add has ended/, body: [
|
||||
"Row 249 landed on refactor as 9de5a57ab, pushed at 14:56Z with 7b9737953, which adds the DEFERRED.md entry. Gitea #1751 is closed with landing comment 30074.",
|
||||
"Gizmoduck built it (05595794b) and Duckworth approved it (comment 30070). The broker marks install done only after it returns. When it never returned, the undo records hostLineLeft, and hosts.remove refuses until the sweep clears the line.",
|
||||
"Gate on Sage's slot: the grant-executor test file passed 39/0 and the bus suite 598/0, with no stub left. A mutant that drops the new branch was killed by G8 and both G23 tests. Webby's N1, the sleep 30 test, is deferred and binds the next row that touches that file. The live stack is unchanged.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,49 @@
|
||||
// Sage, 2026-10-21: row 215 (C2-04) landed, comment on P19 416 (Q174 landing comment). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r215-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 450, title: /^K12 cohort test: populated undefined under load/, body: [
|
||||
"Row 250 landed on refactor as 1b6158ce2 at 19:42Z. K12 now names the cause when the cgroup.events read fails, holds the stop's release while it reads the shim, and the release answers with the events read's reason. R13 closes its controller. Gitea #1755 is closed (comment 30139). Queue row 250 is done (rev 2965).",
|
||||
"Gizmoduck built it, and Filbert approved round 2 (rev 2957). The landing patch-id equals the candidate c1a858975. Gizmoduck ran the gate on grant sage-2109: the probes failed and passed as predicted, conversation whole 3x 183/183 plus a concurrent pair, and control-board whole 124/124.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,52 @@
|
||||
// Sage, 2026-10-21: row 215 (C2-04) landed, comment on P19 416 (Q174 landing comment). Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r215-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 458, title: /Pocket ID|OIDC|Q203/i, body: [
|
||||
"Queue row 255 (rev 2976), Gitea #1760. It's a design row. Launchpad owns it, Webby reviews, Sage owns the gate, and it runs after row 252. The brief is docs/plans/2026-10-21_bundled-pocket-id.md (7d08237e1). Jason's Q203 answer is quoted verbatim, and Mos's reading is labelled as his. The design covers the installer choice, the component under REQ-INST-1, the Q175 start checks with no relaxation, the client secret slot, first-person passkey enrollment, reach (the outside steps are Jason's), agents, instances, stand-in tests and the build rows.",
|
||||
"PRD 1.10 adds REQ-INST-2, post-v1, as Mos ruled in mos-1204. A 1.10 note records that the out-of-scope list still names 'Pocket ID and SSO'. Jason hasn't ruled on that line (vikunja:19/459).",
|
||||
] },
|
||||
{ id: 416, title: /^MVP Case 2: creative writing hub instance/, body: [
|
||||
"Row 222 (C2-03a, fetch-only library sync) landed on refactor as 0b33f5e9d at 20:13Z. It adds the library.sync bus verb, a 60 s timer per library entry with a sync user, and the askpass helper. Gitea #1723 is closed (comment 30147), and queue row 222 is done (rev 2972). Megavolt built it, Dewey approved it (rev 2966), and megavolt-0098 ran the gate green. Dewey's deferrals are row 254 (#1759): the client timeout, STATUS codes and the README line. It runs after row 196.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,54 @@
|
||||
// Sage, 2026-10-21: row 66 (unslop check) landed, comment on P19 212 and 454, per mos-1186. Sage's own key (bot user 13); the sync key reads back. Comments only: no task edits.
|
||||
// Checks each task's title before posting. Prints no secret.
|
||||
// Usage: node comment-r249-landed.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const POSTS = [
|
||||
{ id: 212, title: /^Row 66: Managed sessions run the unslop check on both harnesses/, body: [
|
||||
"Row 66 landed on refactor as 2c38325ec, pushed at 18:51Z. 3b0aef995 adds the DEFERRED.md entry for the two live checks. Queue row 66 is done (rev 2921).",
|
||||
"Round 4 (82c830779) was approved by Dewey (comment 30085) and Filbert (30086), then rebased onto a36523713 after row 197 (mos-1186 item 4). The cherry-pick had no conflict. The patch-id without agents/ is d67275ac4251 on both bases. The harness suite went 149/0 on the rebased tree. The r4d gate was green: unslop, harness, plugins and every scripts/test-*.sh, with cli 682/0 on the TMPDIR rerun.",
|
||||
"Landing sessions on a scratch root from the candidate tree: Claude Code run r4ab24630d323 put the operator note on the RESULT, the correction notice reached the next turn as hook context, and unslop.jsonl counted checked 2, flagged 1, notices 1. The pi run r1dd94171bede is launch only until #383. The live checks are task 454.",
|
||||
] },
|
||||
{ id: 454, title: /^Row 66 live checks: unslop notice on Claude Code/, body: [
|
||||
"Row 66 landed as 2c38325ec, and the DEFERRED.md entry is 3b0aef995. Both checks here are open. Claude Code runs in the first re-pin whose tree includes 2c38325ec, backup first (Q173). Pi runs one full session with a tell after #383.",
|
||||
"Reference from the scratch root (agents/sage/work/queue-66/LANDING-SESSION.md): Claude Code run r4ab24630d323 showed the note, the injected notice and counts checked 2, flagged 1, notices 1. The pi launch r1dd94171bede bundled the unslop extension but ran no turn.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) rec.problems.push(`comment ${P.id} read-back: ${back.status}`);
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,63 @@
|
||||
// Sage, 2026-10-21: files row 249 (mos-1166, #1751) as a P19 task under #436, then relates it to 436 as a subtask.
|
||||
// Sage's own key (bot user 13); the sync key reads back. No labels, assignees, due dates or bucket moves. Refuses if
|
||||
// the title exists. Prints no secret.
|
||||
// Usage: node file-r249.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (sig, xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(sig)}</p>`;
|
||||
const TASKS = [
|
||||
{
|
||||
title: "Grant undo can't prove the remote add has ended: record hostLineLeft when install never returned (row 249, #1751)",
|
||||
description: ps(SIG, [
|
||||
"Filed under #436 by Mos's ruling mos-1166. Gizmoduck answered in gizmoduck-0137 and 0139, and Duckworth checked the answer in duckworth-0091. The row 197 r8 proof found the leftover stubs that show it.",
|
||||
"An abort or timeout during install kills the local ssh, but sshd sends the remote add no SIGHUP, so it keeps running. If it writes after the undo's single remove, hostLineLeft stays unset. hosts.status shows the host clean, and hosts.remove can drop the record while the line is still on the host. After that a sweep refuses host-unknown. The line authorizes a key nobody holds.",
|
||||
"Queue row 249, owner Gizmoduck, reviewer Duckworth, gate Sage. Brief: docs/plans/2026-10-20_host-credentials.md (6de1ebc62).",
|
||||
]),
|
||||
},
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
async function existing() {
|
||||
const out = [];
|
||||
for (let page = 1; page < 20; page++) {
|
||||
const r = await api("GET", `/api/v2/projects/${PROJECT}/tasks?per_page=50&page=${page}`, undefined, sync);
|
||||
if (r.status !== 200) throw new Error(`list tasks: ${r.status}`);
|
||||
out.push(...list(r.json));
|
||||
if (list(r.json).length < 50) return out;
|
||||
}
|
||||
throw new Error("more than 19 pages");
|
||||
}
|
||||
const rec = { at: new Date().toISOString(), project: PROJECT, created: null, problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
const have = await existing();
|
||||
for (const T of TASKS) if (have.some((t) => t.title === T.title)) throw new Error("title exists; refusing");
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
rec.created = [];
|
||||
for (const T of TASKS) {
|
||||
const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, { title: T.title, description: T.description }, key);
|
||||
const c = { status: r.status, id: r.json?.id ?? null };
|
||||
rec.created.push(c);
|
||||
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) { rec.problems.push(`create: ${r.status} ${r.json?.message ?? ""}`); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync);
|
||||
Object.assign(c, { readBack: back.status, done: back.json?.done });
|
||||
if (back.status !== 200 || back.json?.title !== T.title || back.json?.done) rec.problems.push(`read-back differs for ${r.json.id}`);
|
||||
const rel = await api("PUT", `/api/v1/tasks/${r.json.id}/relations`, { other_task_id: 436, relation_kind: "parenttask" }, key);
|
||||
c.relation = rel.status;
|
||||
if (rel.status >= 300) rec.problems.push(`relation: ${rel.status} ${rel.json?.message ?? ""}`);
|
||||
}
|
||||
save();
|
||||
console.log(JSON.stringify(rec));
|
||||
@@ -0,0 +1,61 @@
|
||||
// Sage, 2026-10-22: files the 10-27 owner thread-claim row (mos-1262) as a P19 task. No relation, labels, assignees,
|
||||
// due dates or bucket moves. Sage's own key (bot user 13); the sync key reads back. Refuses if the title exists.
|
||||
// Prints no secret.
|
||||
// Usage: node file-thread-claims.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON ROW ISSUE
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT, ROW, ISSUE] = process.argv.slice(2);
|
||||
if (!/^[0-9]+$/.test(ROW ?? "") || !/^[0-9]+$/.test(ISSUE ?? "")) throw new Error("ROW and ISSUE must be numbers");
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const SIG = "Created by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (sig, xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(sig)}</p>`;
|
||||
const TASKS = [
|
||||
{
|
||||
title: `The owner sees a thread agent's claims: agents lists held thread claims (row ${ROW}, #${ISSUE}, 10-27)`,
|
||||
description: ps(SIG, [
|
||||
"Filed by Mos's ruling mos-1262 as a 10-27 row, not post-v1. Row 205's message filter found it (morgana-0072, Sage ruling sage-2430).",
|
||||
"A thread run's role.claim writes thread_claims only (broker.mjs:2682), and the owner's agents verb reads role_claims only (:3309). So the owner can't see a thread agent's claim, which PRD :1083-1084 and the FED R6 user sentence promise. No user thread goes live before this row lands, the same bind as the 193b1a filter. Q176 Case 1 needs it.",
|
||||
`Queue row ${ROW}, after row 205, owner Morgana, reviewer Duckworth, gate Sage. Brief: docs/plans/2026-10-22_owner-thread-claims.md. If it can't make 10-27, that's a scope question for Jason, and it goes to Mos first.`,
|
||||
]),
|
||||
},
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
async function existing() {
|
||||
const out = [];
|
||||
for (let page = 1; page < 20; page++) {
|
||||
const r = await api("GET", `/api/v2/projects/${PROJECT}/tasks?per_page=50&page=${page}`, undefined, sync);
|
||||
if (r.status !== 200) throw new Error(`list tasks: ${r.status}`);
|
||||
out.push(...list(r.json));
|
||||
if (list(r.json).length < 50) return out;
|
||||
}
|
||||
throw new Error("more than 19 pages");
|
||||
}
|
||||
const rec = { at: new Date().toISOString(), project: PROJECT, created: null, problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
const have = await existing();
|
||||
for (const T of TASKS) if (have.some((t) => t.title === T.title)) throw new Error("title exists; refusing");
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
rec.created = [];
|
||||
for (const T of TASKS) {
|
||||
const r = await api("PUT", `/api/v1/projects/${PROJECT}/tasks`, { title: T.title, description: T.description }, key);
|
||||
const c = { status: r.status, id: r.json?.id ?? null };
|
||||
rec.created.push(c);
|
||||
if (r.status >= 300 || !Number.isSafeInteger(r.json?.id)) { rec.problems.push(`create: ${r.status} ${r.json?.message ?? ""}`); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${r.json.id}`, undefined, sync);
|
||||
Object.assign(c, { readBack: back.status, done: back.json?.done });
|
||||
if (back.status !== 200 || back.json?.title !== T.title || back.json?.done) rec.problems.push(`read-back differs for ${r.json.id}`);
|
||||
}
|
||||
save();
|
||||
console.log(JSON.stringify(rec));
|
||||
@@ -0,0 +1,69 @@
|
||||
// Sage, 2026-10-22: re-pin window 7247d80c7 to 86f95bb27 done (mos-1253, mos-1263). Comments on P19 386 and 435 (Mos posted 412 himself, mos-1266),
|
||||
// text from Darkwing (darkwing-0160). No task is closed. Sage's own key (bot user 13); the sync key reads back.
|
||||
// Checks each task's title before anything is posted. Prints no secret.
|
||||
// Usage: node post-w12-86f95bb27.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13). Text from Darkwing, the window operator.";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const WIN = "agents/darkwing/work/window-repin-86f95bb27/";
|
||||
const POSTS = [
|
||||
{ id: 386, title: /^Q173 test system/, close: false, body: [
|
||||
"Re-pin 7247d80c7 to 86f95bb27 done on dragon-lin, 2026-10-22, with Darkwing operating. Activation 20261022T040256Z-86f95bb27f6c, pointer at 04:03:00Z UTC, schema 3j with no migration. P1 to P6, B0, W0 to W5 and W9 to W11 passed. Nothing was rolled back, and there's no counted failure (mos-1260). W5 backup SHA256SUMS 45b18429.",
|
||||
"S1 to S2 (03:55:33Z to 04:24:42Z UTC): schema 3j at both, events 1798 to 1817 with no grant.* kind, and hosts and host_grants at 0 rows.",
|
||||
"W12 (w12c.sh, mos-1263) W12_PASS at 04:26:43Z. Backup w12-20261022T042555Z first (SHA256SUMS 210ce775). The chief relayed the pm read and launched pm r2cf8536da2c3, which answered PRD 1.14 with 85 ids. Row 66 on chief rd0b8fdb4d62d: turn 1 drew 4 banned words and the [unslop: note, turn 2 got the notice, and counts went from 1/0/0 to checked 6, flagged 1, notices 1, broken null.",
|
||||
`STATE: ${WIN}STATE.md`,
|
||||
] },
|
||||
{ id: 435, title: /^Console inbox: Approve\/Deny/, close: false, body: [
|
||||
"Re-pin to 86f95bb27 (activation 20261022T040256Z-86f95bb27f6c). Row 202's access verbs are live, and nobody created a grant. The bus journal since W5 has no grant.* event, and hosts and host_grants hold 0 rows.",
|
||||
`STATE: ${WIN}STATE.md`,
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], closes: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
const before = {};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
if (P.close && t.json.done) throw new Error(`task ${P.id} is already done; nothing posted`);
|
||||
before[P.id] = { title: t.json.title, description: t.json.description };
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) { rec.problems.push(`comment ${P.id} read-back: ${back.status}`); save(); continue; }
|
||||
if (P.close) {
|
||||
const full = await api("GET", `/api/v1/tasks/${P.id}`, undefined, key);
|
||||
if (full.status !== 200 || full.json?.id !== P.id) { rec.problems.push(`v1 get ${P.id}: ${full.status}`); save(); continue; }
|
||||
const u = await api("POST", `/api/v1/tasks/${P.id}`, { ...full.json, done: true }, key);
|
||||
const b2 = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
const x = { task: P.id, status: u.status, readBack: b2.status, done: b2.json?.done, titleSame: b2.json?.title === before[P.id].title, descriptionSame: b2.json?.description === before[P.id].description, inverse: `mark undone: POST /api/v1/tasks/${P.id} done false (sage key)` };
|
||||
rec.closes.push(x);
|
||||
if (u.status >= 300 || b2.status !== 200 || b2.json?.done !== true || !x.titleSame || !x.descriptionSame) rec.problems.push(`close ${P.id}: update or read-back differs`);
|
||||
}
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,83 @@
|
||||
// Sage, 2026-10-21: re-pin window 4429a104f to 7247d80c7 W12 result (mos-1234) and the PRD 1.11 and 1.12 landings.
|
||||
// Comments on P19 454, 386, 412 and 435, then comments on 459 and 462 and marks those two done (mos-1234 item 4).
|
||||
// Sage's own key (bot user 13); the sync key reads back. A close sends the task as the v1 GET returned it with only
|
||||
// done changed, and the read-back checks that title and description are the same. Rollback is marking it undone.
|
||||
// Checks each task's title before anything is posted. Prints no secret.
|
||||
// Usage: node post-w12-prd.mjs https://tasks.woltje.com SAGE_TOKEN_FILE SYNC_TOKEN_FILE OUT_JSON
|
||||
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
||||
const [ORIGIN, KEY, SYNC, OUT] = process.argv.slice(2);
|
||||
if (!/^https:\/\/tasks\.woltje\.com$/.test(ORIGIN)) throw new Error("tasks.woltje.com only");
|
||||
if (!OUT || existsSync(OUT)) throw new Error("OUT_JSON missing or exists; refusing");
|
||||
const PROJECT = 19;
|
||||
const PSIG = "Posted by Sage (T3 1ef1e4f8) with Sage's own key (bot user 13).";
|
||||
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const ps = (xs) => xs.map((x) => `<p>${esc(x)}</p>`).join("") + `<p>${esc(PSIG)}</p>`;
|
||||
const WIN = "agents/sage/work/window-repin-7247d80c7/";
|
||||
const POSTS = [
|
||||
{ id: 454, title: /^Row 66 live checks: unslop notice on Claude Code/, close: false, body: [
|
||||
"The Claude Code half passed in W12 of the 7247d80c7 re-pin (w12b.sh, 22:24:48Z to 22:27:13Z, W12_PASS, mos-1234). It ran on the live chief r7646a7a4a2bd (claude-code). Baseline unslopCounts were checked 2, flagged 0, notices 0. Turn 1 asked for one sentence using four banned words, and its RESULT carried the operator note \"[unslop: 1 reply of this session flagged since the last note ... The session gets a correction notice on its next turn.]\". Turn 2 replied \"received\". After the turns: checked 5, flagged 1 (ms-unslop:7 x4), notices 1, broken null.",
|
||||
"The pi half still waits on #383, so this stays open.",
|
||||
"The pm requirements read is open, not a window failure (mos-1234 item 3). No tester path reaches a pm: the chief relayed the request as bus message ed1063ea but launched no pm, because a tester message authorizes nothing. The read runs at the first pm launch from a human message or a real bus notice, and ed1063ea waits unread for that pm. Nothing goes to Jason for it.",
|
||||
`Evidence: ${WIN}STATE.md (W12 section), W.txt, .w12b-counts.json, w12b.out. The W12 store backup is SHA256SUMS a4ecd7ba.`,
|
||||
] },
|
||||
{ id: 386, title: /^Q173 test system/, close: false, body: [
|
||||
"The 7247d80c7 re-pin window is done. W0 to W12 passed, nothing was rolled back, and the one counted failure was P2 attempt 1 (a script path bug before any live step). The live Console now serves the tester through the private socket ~/.mosaic-dev/console/tester.sock with --tester-socket, and nothing listens on 7333. Two tester reads through the socket matched the old listener's /api/config line. Every step ran as tester, with no --by jason, no Save, no mosaic name, and bus.tester untouched.",
|
||||
`STATE report: ${WIN}STATE.md. It goes into the window record commit next.`,
|
||||
] },
|
||||
{ id: 412, title: /^Console SSO/, close: false, body: [
|
||||
"Baseline for phase 2 (mos-1195), from W10 of the 7247d80c7 re-pin at 22:18:51Z: POST /api/seen and POST /api/reply through 127.0.0.1:7332 with no session and no proxy header both return 401 \"sign-in required\". Phase 2 (config entry with clientId fvchAepp9…, --sso --send re-pin, Jason's first sign-in, the sub read, the unsigned-write check, the sign-out landing check) runs when Jason starts it (mos-1217).",
|
||||
] },
|
||||
{ id: 435, title: /^Console inbox: Approve\/Deny/, close: false, body: [
|
||||
"Row 196 is live on dragon-lin since the 7247d80c7 re-pin (activation 20261021T215926Z-7247d80c7af7, store 3j). The tester reaches the Console only through the private socket now. Hosts and host_grants exist in the store with 0 rows, and the window added no grant.* event (mos-1225). Per mos-1198, .53 and .54 take no copy carrying 196 until row 253 lands.",
|
||||
] },
|
||||
{ id: 459, title: /^PRD v1 scope: 'Pocket ID and SSO'/, close: true, body: [
|
||||
"Closing: PRD 1.11 landed on refactor at 22:30:53Z as ea0b2a16d and 73222011f, a fast-forward from cca206cdc (Ludwig, mos-1227, mos-1229). It records Q211, so v1 is the 10-27 goal, and Mos's reading of the review path. The v1 VMs' OIDC provider stays open under 461. requirements.json moves from 1.10 with 70 ids to 1.11 with 71. Before the push, packages/tasks/tests/requirements.test.mjs and packages/bus/tests/requirements.test.mjs each passed 3/3 run whole on 73222011f, and verbs.test.mjs passed 31/31.",
|
||||
] },
|
||||
{ id: 462, title: /^PRD 1\.12: requirement ids/, close: true, body: [
|
||||
"Closing: PRD 1.12 landed on refactor at 22:30:59Z as 3576afffb, 74dea06d9 and 1509c7cc5, a fast-forward from 73222011f. Della approved it in della-0010. requirements.json is 1.12 with 82 ids, 11 of them new, and the REQ-DEC-3 title changed. Before the push, both requirements test files passed 3/3 each, run whole on 1509c7cc5 as the final candidate, and verbs.test.mjs passed 31/31. The follow-ups 463, 464 and 465 stay open.",
|
||||
] },
|
||||
];
|
||||
const key = readFileSync(KEY, "utf8").trim(), sync = readFileSync(SYNC, "utf8").trim();
|
||||
async function api(method, path, body, auth) {
|
||||
const r = await fetch(ORIGIN + path, { method, headers: { Authorization: `Bearer ${auth}`, "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), redirect: "error" });
|
||||
const t = await r.text(); let json = null; try { json = JSON.parse(t); } catch {}
|
||||
return { status: r.status, json };
|
||||
}
|
||||
const list = (j) => j?.items ?? (Array.isArray(j) ? j : []);
|
||||
const rec = { at: new Date().toISOString(), comments: [], closes: [], problems: [] };
|
||||
const save = () => {
|
||||
const out = JSON.stringify(rec, null, 1) + "\n";
|
||||
if (out.includes(key) || out.includes(sync)) throw new Error("secret in the record; not written");
|
||||
writeFileSync(OUT, out);
|
||||
};
|
||||
const before = {};
|
||||
for (const P of POSTS) {
|
||||
const t = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
if (t.status !== 200 || t.json?.project_id !== PROJECT || !P.title.test(t.json?.title ?? "")) throw new Error(`task ${P.id}: ${t.status}, title check failed; nothing posted`);
|
||||
if (P.close && t.json.done) throw new Error(`task ${P.id} is already done; nothing posted`);
|
||||
before[P.id] = { title: t.json.title, description: t.json.description };
|
||||
}
|
||||
writeFileSync(OUT, "", { flag: "wx" });
|
||||
for (const P of POSTS) {
|
||||
const c = await api("PUT", `/api/v1/tasks/${P.id}/comments`, { comment: ps(P.body) }, key);
|
||||
const e = { task: P.id, status: c.status, id: c.json?.id ?? null };
|
||||
rec.comments.push(e);
|
||||
if (c.status >= 300 || !Number.isSafeInteger(c.json?.id)) { rec.problems.push(`comment ${P.id}: ${c.status} ${c.json?.message ?? ""}`); save(); continue; }
|
||||
const back = await api("GET", `/api/v2/tasks/${P.id}/comments?per_page=50`, undefined, sync);
|
||||
const found = list(back.json).find((x) => x.id === c.json.id);
|
||||
Object.assign(e, { readBack: back.status, found: Boolean(found) });
|
||||
if (back.status !== 200 || !found) { rec.problems.push(`comment ${P.id} read-back: ${back.status}`); save(); continue; }
|
||||
if (P.close) {
|
||||
const full = await api("GET", `/api/v1/tasks/${P.id}`, undefined, key);
|
||||
if (full.status !== 200 || full.json?.id !== P.id) { rec.problems.push(`v1 get ${P.id}: ${full.status}`); save(); continue; }
|
||||
const u = await api("POST", `/api/v1/tasks/${P.id}`, { ...full.json, done: true }, key);
|
||||
const b2 = await api("GET", `/api/v2/tasks/${P.id}`, undefined, sync);
|
||||
const x = { task: P.id, status: u.status, readBack: b2.status, done: b2.json?.done, titleSame: b2.json?.title === before[P.id].title, descriptionSame: b2.json?.description === before[P.id].description, inverse: `mark undone: POST /api/v1/tasks/${P.id} done false (sage key)` };
|
||||
rec.closes.push(x);
|
||||
if (u.status >= 300 || b2.status !== 200 || b2.json?.done !== true || !x.titleSame || !x.descriptionSame) rec.problems.push(`close ${P.id}: update or read-back differs`);
|
||||
}
|
||||
save();
|
||||
}
|
||||
save();
|
||||
process.stdout.write(JSON.stringify(rec, null, 1) + "\n");
|
||||
if (rec.problems.length) process.exit(2);
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-21T02:51:32.855Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 424,
|
||||
"status": 201,
|
||||
"id": 429,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-21T03:21:23.327Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 432,
|
||||
"status": 201,
|
||||
"id": 430,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-21T05:42:18.847Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 432,
|
||||
"status": 201,
|
||||
"id": 436,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-21T06:17:35.760Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 436,
|
||||
"status": 201,
|
||||
"id": 437,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"at": "2026-10-21T01:55:44.446Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 440,
|
||||
"status": 201,
|
||||
"id": 427,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 441,
|
||||
"status": 201,
|
||||
"id": 428,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-21T06:58:53.918Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 432,
|
||||
"status": 201,
|
||||
"id": 438,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"at": "2026-10-21T13:13:35.441Z",
|
||||
"project": 19,
|
||||
"created": [
|
||||
{
|
||||
"status": 201,
|
||||
"id": 445,
|
||||
"readBack": 200,
|
||||
"done": false,
|
||||
"relation": 201
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"at": "2026-10-21T14:57:03.574Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 445,
|
||||
"status": 201,
|
||||
"id": 442,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"at": "2026-10-21T20:44:23.676Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 458,
|
||||
"status": 201,
|
||||
"id": 478,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 416,
|
||||
"status": 201,
|
||||
"id": 479,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"at": "2026-10-21T22:32:05.204Z",
|
||||
"comments": [
|
||||
{
|
||||
"task": 454,
|
||||
"status": 201,
|
||||
"id": 508,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 386,
|
||||
"status": 201,
|
||||
"id": 509,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 412,
|
||||
"status": 201,
|
||||
"id": 510,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 435,
|
||||
"status": 201,
|
||||
"id": 511,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 459,
|
||||
"status": 201,
|
||||
"id": 512,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
},
|
||||
{
|
||||
"task": 462,
|
||||
"status": 201,
|
||||
"id": 513,
|
||||
"readBack": 200,
|
||||
"found": true
|
||||
}
|
||||
],
|
||||
"closes": [
|
||||
{
|
||||
"task": 459,
|
||||
"status": 200,
|
||||
"readBack": 200,
|
||||
"done": true,
|
||||
"titleSame": true,
|
||||
"descriptionSame": true,
|
||||
"inverse": "mark undone: POST /api/v1/tasks/459 done false (sage key)"
|
||||
},
|
||||
{
|
||||
"task": 462,
|
||||
"status": 200,
|
||||
"readBack": 200,
|
||||
"done": true,
|
||||
"titleSame": true,
|
||||
"descriptionSame": true,
|
||||
"inverse": "mark undone: POST /api/v1/tasks/462 done false (sage key)"
|
||||
}
|
||||
],
|
||||
"problems": []
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
# Re-pin 7247d80c7 to 86f95bb27 (rows 251, 244, 254, 202, 273, PRD 1.11-1.14), no migration
|
||||
|
||||
Sage, 2026-10-22, for mos-1253 under DECISIONS Q173 and mos-1246. The step layout is
|
||||
`repin-1024-draft1.md`, with the copy changed and `--migrate` dropped. Where a step is unchanged I name it
|
||||
and say only what differs. Mos checks this file's sha256 before P0, and any change after that means a new
|
||||
check (mos-0849/0850). This file stays uncommitted until row 191's window ends, because refactor doesn't
|
||||
move while a rebased candidate's gate runs.
|
||||
|
||||
## Who, and what nobody does
|
||||
|
||||
- Darkwing operates if free. Otherwise I operate. Mos verifies each step's output before the next one,
|
||||
runs P3, and gives the W5 go. Row 191's full window keeps first claim on the slot (mos-1225). Steps that
|
||||
start no test and no stub run alongside it: P1, W0, P4, P5, P6, B0, W1-W4. P2 waits for a gap or CLEAR,
|
||||
because it starts node stubs that a busy check counts.
|
||||
- Jason has no step. Nothing is pruned. Nobody acts as Jason: no `--by jason`, no human shell or tmux
|
||||
pane, every bus read and write carries `--tester`, and `bus.tester` stays as it is. If it isn't `true`,
|
||||
stop at P1.
|
||||
- No pi (#383). No Save in the human Console, no `mosaic name`.
|
||||
- Outside reach doesn't change. No `--send`, no `--sso`, no proof pair. Both go on together in the phase 2
|
||||
window, which still waits for Jason's start (Vikunja 412). The tunnel forwards 7332 only.
|
||||
- dragon-lin only. .53 and .54 take nothing (mos-1198, mos-1167).
|
||||
|
||||
## Names
|
||||
|
||||
- OLD, live: `7247d80c7af7ffa70fc61557406bf491cdf3bfc7`, schema 3j, pointer `pm: true`, activation
|
||||
`20261021T215926Z-7247d80c7af7`. Console flags at W5 last time: `--port 7332 --public-host
|
||||
mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --tester-socket`. P1
|
||||
re-reads them from `status`. I don't copy them from this file.
|
||||
- NEW: `86f95bb27f6cb237bf9fa9ba53320bace6c4de0b`, 59 commits past OLD (`git rev-list --count`). The last
|
||||
code landing in it is 0bbf0f6d1 (row 273). The two commits after that are docs and queue files.
|
||||
- `WIN=agents/darkwing/work/window-repin-86f95bb27/` if Darkwing operates, else
|
||||
`agents/sage/work/window-repin-86f95bb27/`. The scripts adapt last window's, in
|
||||
`agents/sage/work/window-repin-7247d80c7/`, with NEW and OLD changed and every `--migrate` removed.
|
||||
|
||||
## What NEW carries over OLD
|
||||
|
||||
Non-test paths changed from OLD to NEW (`git diff --name-only`, tests and READMEs left out):
|
||||
|
||||
| Row | Commit | Paths | Live effect to check |
|
||||
|---|---|---|---|
|
||||
| 251, 244 | d37210288, e69260a3c | scripts/test-release.sh, test-task.sh, scripts/testing/* | Gate tooling. Not in the live path. |
|
||||
| PRD 1.11-1.14 | ea0b2a16d..db1e7732a | packages/tasks/requirements.json | W9: the pm's requirements read gives 1.14 with 85 ids. |
|
||||
| 254 | 331597709 | packages/bus/src/human-cli.mjs (library.sync client timeout 130 s), packages/webui/src/bus.mjs (six `library-*` STATUS codes) | No library entry is live, so neither path runs. |
|
||||
| 202 | ddfb29af7 | packages/cli/src/access.mjs, cli.mjs, scripts/mosaic | `mosaic access list/grant/revoke`. Nobody runs them in this window (Q188, mos-1141). |
|
||||
| 273 | 7991662dc..0bbf0f6d1 | scripts/stack-release.sh | `--send` and `--no-sso`. With neither given, the plan keeps the pointer's Console flags. W3 checks that. |
|
||||
|
||||
Nothing else in packages/bus/src or packages/webui/src changed (`git diff --quiet` with those two files left
|
||||
out). The four service scripts and scripts/stack-copy.sh are unchanged from OLD (`git diff --quiet 7247d80c7
|
||||
86f95bb27 --` on them). package-lock.json is unchanged.
|
||||
|
||||
## Schema and backups
|
||||
|
||||
- `SCHEMA_VERSION` is 3j at both commits (`packages/bus/src/store.mjs:17`). No migration step and no
|
||||
schema.sql change in the range. W3's plan must show no migration line. If it asks for `--migrate`, stop.
|
||||
- B0 is the backup taken first. It's an online copy of the live store through sqlite's backup API, read only
|
||||
on the source, as in last window's W12. It is laid out the way `rehearse --backup PATH` reads it
|
||||
(`stack-release.sh:2232-2235`): `$DATA/backups/b0-<STAMP>/bus/bus.sqlite`, `SHA256SUMS` written from
|
||||
inside `bus/`, `SCHEMA` reading 3j, and `CHECK` holding the integrity_check result, the event count and
|
||||
maxSeq. The operator sends Mos the path and the SHA256SUMS sha256 before P3.
|
||||
- W5 takes the stopped-store backup every activate takes, plus the rollback set. That one is the rollback
|
||||
point.
|
||||
- The grant look-alike count from draft 1 is dropped. It guarded the 3i-3j migration, and there is none.
|
||||
- Rollback is a pointer flip. OLD and NEW share 3j, so `rollback --plan` then the real run, with no
|
||||
`--restore`. Any restore goes to Mos and ends the window (mos-1226, mos-1253 item 5).
|
||||
|
||||
## Before the window (nothing stopped)
|
||||
|
||||
| # | Who | Step |
|
||||
|---|-----|------|
|
||||
| P0 | Mos | The sha check of NEW and of this file. |
|
||||
| P1 | operator | `status`: pointer, activation, 3j, Console flags, unit flags match. Business file sha256 and `business-check.mjs` LOAD_OK for OLD now and for NEW after W0. `ls -ld $DATA/console` is 0700 and `tester.sock` is 0600 (row 196 made them at the last W5). `bus.tester` is `true`. |
|
||||
| W0 | operator | `scripts/stack-copy.sh copy $NEW`. The copy has `tools/unslop-hook` and `packages/library/bin/askpass` with NEW's mode. |
|
||||
| P2 | operator, in a gap | The two stub runs from last window's p2.sh against NEW's copy, then render both units from REPO's service scripts and diff them against the live units. The only difference allowed is the copy path. |
|
||||
| B0 | operator | The backup above. Path and SHA256SUMS sha256 to Mos. |
|
||||
| P3 | Mos | `stack-release.sh rehearse $NEW mosaic-stack --backup <B0 path> --port <scratch>`, no `--migrate`. It must show no migration, the interpreter preflight passing, one receipt line, no `library:` line, and one pm reply. Mos also reads healthz, `/api/config` and the two tester-http reads on NEW's Console with the pointer's flags, as in mos-1107. |
|
||||
| P4 | operator | The tunnel read. |
|
||||
| P5 | operator | The four service scripts held at NEW, and empty porcelain on them. |
|
||||
| P6 | operator | The 7332 matrix (a)-(g) on OLD as `$WIN/matrix-old.txt`, plus the two tester-http reads, for W10. |
|
||||
|
||||
## The window
|
||||
|
||||
| # | Who | Step |
|
||||
|---|-----|------|
|
||||
| W0b | operator | The business file copy. |
|
||||
| W1 | operator | Idle check, start stamp from `date -u`, S1 with `$SNAP --held`. Open pm tasks: the pm finishes or parks them, or the operator records which piece crosses the window. |
|
||||
| W2 | operator | `status`: pointer on OLD, `pm: true`, 3j, unit flags match. |
|
||||
| W3 | operator | `stack-release.sh activate $NEW mosaic-stack --pm --tester-socket --plan`. It must show OLD to NEW, no migration, the backup line, and a Console line with OLD's flags exactly: `--tester-socket`, no `--send`, no `--sso`, no proof pair, no drop line. Any other difference stops here. |
|
||||
| W4 | operator | The unit reads, as last window. W3 and W4 go to Mos for the W5 go. |
|
||||
| W5 | operator, on Mos's go | The same command without `--plan`. Exit 0, `result ok`, every gate check passing, `pm_launched` included. Record the activation id, backup path, STAMP and the backup SHA256SUMS sha256. Then `status`. |
|
||||
| W9 | operator, after Mos verifies W5 | Last window's W9 list: no `Sign-in:` line in the webui journal since the W5 stamp (with `UTC`), `/proc/<Console pid>/cwd` names NEW's copy, `tester.sock` 0600 in a 0700 `console/`, one new `peercred-starts.jsonl` line, no `library:` line in the bus journal. The pm's requirements read gives 1.14 with 85 ids. If the pm isn't relaunched by the restart (mos-1230), the read moves to W12, as last time. |
|
||||
| W10 | operator | The matrix again as `$WIN/matrix-new.txt`, identical to `matrix-old.txt` apart from named lines. The two tester-http reads match P6's. No listener on 7333 (`ss -ltn`). P4 again. A Console POST on the main port with no signed-in session is refused (the mos-1195 baseline). |
|
||||
| W11 | operator | STATE report in `$WIN/STATE.md`, posted on P19 386, 412 and 435, and on the Gitea issues for 202 (#1700), 254 (#1759) and 273 (#1778). |
|
||||
| W12 | operator, after W11 | Row 66's live check as last time: backup first, one Claude Code managed session with a known tell, the RESULT note, the next-turn notice and the `unslop.jsonl` counts. Pi isn't run (#383). |
|
||||
|
||||
After W12 I write draft 3 of the phase 2 plan naming 86f95bb27. That window still waits for Jason.
|
||||
|
||||
## Steps skipped, and why
|
||||
|
||||
Mos allowed skipping a step that only repeats a test row 273's or row 202's gate already ran on the same
|
||||
code. The blob ids at the gated commit and at NEW are equal for every one of those files (`git rev-parse`):
|
||||
scripts/stack-release.sh `7c7676c71` at 0bbf0f6d1 and NEW, and access.mjs `a0a7c9364`, cli.mjs `b14b8fd79`,
|
||||
scripts/mosaic `91c79ab4b` and human-cli.mjs `27b67fbf7` at ddfb29af7 and NEW.
|
||||
|
||||
- No separate run of the `--send`/`--no-sso` walk or the rollback flag carry. Darkwing's gate on 0bbf0f6d1
|
||||
ran stack-release.test.mjs 218/218 (T53, T57, T58 c0/c1) and test-release.sh rc 0. W3's live plan line is
|
||||
the only check left.
|
||||
- No live `mosaic access` run. Row 202's gate ran access.test.mjs on ddfb29af7. A live grant would also
|
||||
break Q188.
|
||||
- The grant look-alike count, for the reason under Schema. It's a scope drop, not a gate repeat.
|
||||
|
||||
Nothing else is skipped. P2 and P3 aren't gate repeats: they check NEW's copy as built on dragon-lin.
|
||||
|
||||
## Stop conditions and rollback
|
||||
|
||||
As in draft 1. The first failure stops the step and goes to Mos. A second failure, or any restore, ends
|
||||
the window (mos-0823, mos-1253 item 5). Before W5 there's nothing to undo. After W5, rollback is
|
||||
`rollback --plan` then the real run, which flips the pointer to OLD with the same Console flags. Keep every
|
||||
copy. Prune nothing.
|
||||
@@ -0,0 +1,2 @@
|
||||
1ba3c7efbc84a1d7eb8ee0068d32b7917f4e75cdc4066703944cea4c20db5b93 console-stub.mjs
|
||||
93e7f9740cd330c4f6fdb6fc4fe35e3a4fc1d606322debb4e514ed6033365d8f console-stub-tester.mjs
|
||||
@@ -0,0 +1,25 @@
|
||||
// Re-pin 7247d80c7 P2, second run: console-stub.mjs's loopback listener plus NEW's tester listener on a
|
||||
// private socket under a scratch data root, as cli.mjs starts it for --tester-socket. Usage: COPY DATAROOT.
|
||||
// DATAROOT must be a fresh scratch directory, never ~/.mosaic-dev. projectSave, send and every tester
|
||||
// write record a call; the loopback POST must reach none of them.
|
||||
const [copy, dataRoot] = process.argv.slice(2);
|
||||
if (!dataRoot || dataRoot.includes('.mosaic-dev')) { console.error('usage: COPY SCRATCH_DATAROOT'); process.exit(2); }
|
||||
const { startServer } = await import(`${copy}/packages/webui/src/serve.mjs`);
|
||||
const { startTesterSocket } = await import(`${copy}/packages/webui/src/tester-socket.mjs`);
|
||||
const H = 'a'.repeat(40), calls = [];
|
||||
const files = async () => ({ head: H, files: [{ path: 'docs/README.md' }, { path: 'spec/north-star.md' }] });
|
||||
const file = async ({ path }) => ({ path, content: 'stub', commit: H, head: H });
|
||||
const record = name => async () => { calls.push(name); return { ok: true }; };
|
||||
const server = await startServer({ port: 0, board: 'http://127.0.0.1:9', log: () => {},
|
||||
preview: { host: 'mosaic.example', header: 'x-test-user', user: 'tester' },
|
||||
send: async () => { calls.push('send'); return { id: 1 }; },
|
||||
projectFiles: files, projectFile: file,
|
||||
projectSave: async () => { calls.push('save'); return { path: 'x', commit: H }; } });
|
||||
const tester = await startTesterSocket({ dataRoot, board: 'http://127.0.0.1:9', log: () => {},
|
||||
testerNoSignIn: false, testerProofHeader: null, testerAuthHeader: 'x-test-user',
|
||||
bus: { names: async () => ({}), instances: async () => ({}), routes: async () => ({}) },
|
||||
projectFiles: files, projectFile: file,
|
||||
instancesWrite: record('instances.set'), routesWrite: record('routes.set'), nameWrite: record('name.set') });
|
||||
console.log(`PORT ${server.address().port}`);
|
||||
console.log(`SOCK ${tester.address()}`);
|
||||
process.on('SIGTERM', () => { console.log(`CALLS ${JSON.stringify(calls)}`); server.close(); tester.close(() => process.exit(0)); });
|
||||
@@ -0,0 +1,13 @@
|
||||
// Positive control for console-check.sh: NEW's startServer with stub bus reads, on a free loopback port.
|
||||
// projectSave and send record a call; the loopback POST must reach neither.
|
||||
const [copy] = process.argv.slice(2);
|
||||
const { startServer } = await import(`${copy}/packages/webui/src/serve.mjs`);
|
||||
const H = 'a'.repeat(40), calls = [];
|
||||
const server = await startServer({ port: 0, board: 'http://127.0.0.1:9', log: () => {},
|
||||
preview: { host: 'mosaic.example', header: 'x-test-user', user: 'tester' },
|
||||
send: async () => { calls.push('send'); return { id: 1 }; },
|
||||
projectFiles: async () => ({ head: H, files: [{ path: 'docs/README.md' }, { path: 'spec/north-star.md' }] }),
|
||||
projectFile: async ({ path }) => ({ path, content: 'stub', commit: H, head: H }),
|
||||
projectSave: async () => { calls.push('save'); return { path: 'x', commit: H }; } });
|
||||
console.log(`PORT ${server.address().port}`);
|
||||
process.on('SIGTERM', () => { console.log(`CALLS ${JSON.stringify(calls)}`); process.exit(0); });
|
||||
@@ -0,0 +1,3 @@
|
||||
chief r097436c91816 claude-code since 2026-10-20T22:02:08.951Z
|
||||
pm rc9e993da24cf claude-code since 2026-10-20T22:40:47.969Z
|
||||
# exit 0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user