Compare commits
126
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b14952e2 | ||
|
|
860afc2b66 | ||
|
|
cd366fc685 | ||
|
|
535b05adbf | ||
|
|
e3fb4e6c90 | ||
|
|
156e5c6040 | ||
|
|
8b1a2b0fed | ||
|
|
833ec1cc8d | ||
|
|
a147042103 | ||
|
|
062599252a | ||
|
|
aabaf97b60 | ||
|
|
e0cb1db2c8 | ||
|
|
0cae2dad55 | ||
|
|
4764bc9eb3 | ||
|
|
712a084124 | ||
|
|
cb3a456989 | ||
|
|
546366f4af | ||
|
|
604523b200 | ||
|
|
094124b4e8 | ||
|
|
5f4e578cf4 | ||
|
|
fe2752954a | ||
|
|
84530f6c5e | ||
|
|
86d8dbc035 | ||
|
|
ae3acc3bda | ||
|
|
40501ab3e1 | ||
|
|
ea4eaf388f | ||
|
|
f9202a7701 | ||
|
|
689a0a3c91 | ||
|
|
45e3ca7bdb | ||
|
|
582b86935a | ||
|
|
fbe57a05b1 | ||
|
|
b64e681edf | ||
|
|
2cc076fec5 | ||
|
|
8e2885d265 | ||
|
|
63b9c64431 | ||
|
|
6b8743c857 | ||
|
|
fc858800bf | ||
|
|
303216ae25 | ||
|
|
ccfb8308bc | ||
|
|
04f3dc4f5f | ||
|
|
4310555874 | ||
|
|
45788f92c1 | ||
|
|
93694c5185 | ||
|
|
8d2856ee3c | ||
|
|
252b29bd8c | ||
|
|
54d9e07bb7 | ||
|
|
de6419f30f | ||
|
|
9ed0f3374a | ||
|
|
efcd3f0fc4 | ||
|
|
b358427677 | ||
|
|
675813594f | ||
|
|
3f1aa76b03 | ||
|
|
bcd1f34c2d | ||
|
|
c87ddc6041 | ||
|
|
ce4040542f | ||
|
|
f2f76fd2c3 | ||
|
|
59226f878f | ||
|
|
f67e5c15c5 | ||
|
|
e2b8733904 | ||
|
|
fd3d2a99fe | ||
|
|
661890e909 | ||
|
|
3046fd60a3 | ||
|
|
b97f9df1ee | ||
|
|
8352da88c5 | ||
|
|
45dca69f6e | ||
|
|
293b42fee9 | ||
|
|
369607e92d | ||
|
|
59f013247c | ||
|
|
e8bb13df84 | ||
|
|
dba2ebdb85 | ||
|
|
45ea3fa0d2 | ||
|
|
f69c962009 | ||
|
|
5b6c878143 | ||
|
|
8c2c5303cf | ||
|
|
23e3d09fa9 | ||
|
|
795a645e65 | ||
|
|
076ca8d6e4 | ||
|
|
68be863248 | ||
|
|
264fad2e26 | ||
|
|
24672fd9d6 | ||
|
|
30e6bb6803 | ||
|
|
7defb58e97 | ||
|
|
9c8877bdf3 | ||
|
|
2d0318a44a | ||
|
|
bd3fa2a484 | ||
|
|
ab575ee553 | ||
|
|
1676ad0de8 | ||
|
|
4874d1e83a | ||
|
|
e909ef51a2 | ||
|
|
4607e15b83 | ||
|
|
8b42010551 | ||
|
|
4863e2caf4 | ||
|
|
7b25a60633 | ||
|
|
e86df37d76 | ||
|
|
fe4ee8c7b5 | ||
|
|
0621c439e0 | ||
|
|
d243f6b25f | ||
|
|
869670d1b1 | ||
|
|
dbbda25373 | ||
|
|
a9da046f83 | ||
|
|
4a71b56abc | ||
|
|
5208894196 | ||
|
|
8dc132bec8 | ||
|
|
08b8831bce | ||
|
|
d339892301 | ||
|
|
0ea3db297b | ||
|
|
f859d06d20 | ||
|
|
70651f45d8 | ||
|
|
490f45f024 | ||
|
|
1e629c892d | ||
|
|
84de3fbca3 | ||
|
|
1e4c1e6e94 | ||
|
|
5b846b048a | ||
|
|
59f0e44f47 | ||
|
|
bb031ec840 | ||
|
|
07315e6514 | ||
|
|
fb04cbc431 | ||
|
|
9d929911c8 | ||
|
|
bcf3c1dacf | ||
|
|
d52d9f1f2f | ||
|
|
830f3d8bff | ||
|
|
e48d602165 | ||
|
|
cabcb81d49 | ||
|
|
353d65f577 | ||
|
|
2c289f6f0d | ||
|
|
a863015a51 |
No files matched your search
@@ -5389,3 +5389,47 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
|
||||
|
||||
2026-10-23T11:03:46Z | Gyro Vikunja571 mos-1444 PRD d8 follow-on | Branch gyro/editor-571-d8 from origin/refactor c16e943b9. Both designs now keep library.convert outside ACTIONS/ROUTED and broker processing, retain REQ-WAIT-1/hard-wait test unchanged, constrain unmarked story eligibility to mapped book/playground scope, and add whole-host pre-import restore recovery from remote import manifests plus the Gitea archive commit. Lookups refuse until one current version is proven; VM1012 restore needs Jason present under Q185. W7 no longer owns hard-waits.test.mjs. Canonical queue next gyro returned nothing; direct Mos assignment governs. Static diff, exact replacement and append-only checks pass. No code, tests, build, browser, private-file or live action. Fenton re-check requested after authorized branch push.
|
||||
|
||||
2026-10-22 | Huey | Row 272 / 264e (#1777) preparation on huey/row272, base 2d0d407ba. Before: bus session metadata and launcher isolation exist, CLI/Console expose only the saved thread default. Draft: session CLI, Console chooser with isolated drafts/cursors/receipts, metadata counts and safe-clear operation receipts against Gizmoduck's 0260 contracts; narrow deferred B6 reply fence. Tests authored, not run: Gander row 253 holds Sage's slot; no Node, claim, HTTPS git, push or live changes. This entry records preparation after the first draft, not a green acceptance result.
|
||||
|
||||
2026-10-22 | Huey | Row 272 draft, 264d additive contract gizmoduck-0272 (78e14352f). Added confirmed cancellation through the human channel, cancelled receipt retirement distinction, named refusals and routed-message cancellation output. Before: cancelled was not recognized as terminal; after: draft CLI stops polling and Console shows the before/after retirement outcome. Acceptance remains unverified: no Node/tests/claim/commit/push under Sage's gate. No runtime/lifecycle authority expanded.
|
||||
|
||||
2026-10-23 | Huey | Row 272 / 264e sage-3141 local rebase. Before: uncommitted draft on 2d0d407ba. After: same draft on new huey/row272-b23783f39 at b23783f39, with 270/271 landed (7328da8ca) and schema 3n. Product changes auto-merged; both append-only logs retain the full new base and original Huey additions. Checked landed session.list and clear/cancel/operation envelopes against the wired draft: no incompatible contract change. Historical unavailable-dependency cases stay as stand-ins; actual base supplies counters and lifecycle state. No tests/Node/claim/commit/push while Gander holds row 574 run 2. Existing-test fixture/assertion delta is named in the packet for Mos's ruling via Sage before execution.
|
||||
|
||||
2026-10-23 | Huey | Row 272 sage-3145 preservation correction. refs/stash is shared, so it was unsafe as sole draft storage. Pinned exact stash commit 07dc9d2f7944b4781b5e80dfa18425f4749f7a45 to local huey/row272-stash-keep and left the entry untouched. Sage explicitly directed committing the rebased draft before testing to preserve it: an untested local checkpoint, no acceptance claim. No Node/test/queue/network/live action. Separate claims and test CLEARs remain pending; old-test delta is with Mos via sage-3144.
|
||||
|
||||
2026-10-23 | Huey | Row 272 early static review follow-up, scrooge-272-early-read-huey-1. Before: a bad-session 404 erased sibling draft/recovery caches, malformed clear/cancel write receipts omitted the audit line, and another session's op key produced an ambiguous error. Draft after: purge only on named thread-level refusals, write one redacted malformed-receipt audit line, and identify key reuse without a write. New browser/HTTP/CLI tests authored, unrun under test hold; approved old-test hunks unchanged. Published 850b22680/round-1 manifest unchanged; follow-up is uncommitted, no acceptance claim.
|
||||
|
||||
2026-10-23 | Huey | Row 272 round 2 on row 219 landing 5fdfd7275, after 295. Replayed round 1 and B1/N1/N2; seven conflicts resolved retaining both log histories, both test sets, 219 instance chooser and 295 owner-settings-only transport. Static comparison confirms old tests differ only by mos-1388 approved hunks. Sage3357/Mos1442 authorize bounded granted-user legacy fallback: only session.list access-refused and omitted-session route; no new session grants or owner routing. RED first under3358 on new file only: 1 expected legacy assertion failure, 3 guards pass, 3.504s, TMPDIR mktemp/no underscore, no compose network; slot returned as huey0035. Then implemented fallback and added timeout/bus/malformed guards plus isolated-copy mutant runner, all unrun afterward. Candidate commit/push precedes Sage's full gate by3354; no green claim.
|
||||
|
||||
2026-10-23 | Huey | Row 272 round 3 per mos-1455 / sage-3435. Gate run 1 on b44332a93 RED: bus 891/0, business 231/0, CLI 940/1, plugins 110/1, webui 372/20; no mutants. Fixed only new CLI fixture role authority and three null-old render guards. Catching browser tests unchanged. manifest.r3 freezes the same 17 paths. Authorized pre-gate follow-up commit/push; no local tests, Node or queue writes; separate compactions dependency and Sage gate remain pending.
|
||||
|
||||
2026-10-23 | Sage (1ef1e4f8) | Row 272 landing (#1777, 264e) | Gate r4 on bea0c8e1c over 1e629c892: bus 891/0, cli 941/0, webui 392/0, business 231/0; plugins 110/1, the single red being row 65 harness.test.mjs:192 naming compactions.mjs, which row 307 owned and fixed. M1-M6 and F1-F4 killed, restore clean. Landed d33989230, queue commit 8dc132bec.
|
||||
|
||||
2026-10-23 | Sage (1ef1e4f8) | Row 307 landing (#1814, Vikunja 598) | Six first-parent commits replayed onto 8dc132bec with matching patch-ids, 16 paths equal to 6fdfe3790, manifest digest 17c9f8d9 rebuilt from git. Gate green 14:37:21Z: bus 891/0, cli 941/0, plugins 119/0, webui 392/0, business 231/0, every other package 0 failed 0 skipped, every scripts/test-*.sh passed (skips: test-queue canonical root, test-task live provider x2). M1-M4 killed, restore clean. Landed d243f6b25, DEFERRED 307 N1 0621c439e, queue commit fe4ee8c7b. Correction: my first note said Vikunja 598 done when I had only commented (951); Mos closed it (952).
|
||||
|
||||
2026-10-23T17:29:46Z | Gyro Vikunja571 sage-3542 Connections amendment | Branch gyro/editor-connections-design cut exactly from landed PRD1.17 30e6bb680; read REQ-LIB-5 in that PRD. Replaced stale J-b waits in parent and W4. Proposed W8 owns current authorized Markdown/DOCX links/backlinks list, labels, versioned refresh, leakage refusals and fixture tests. Build order under mos-1470/comment961 is W7 closed routing candidate, W8 landing, then W7 human first-real-import opening. No REQ-LIB-8 condition or acceptance change. Q234/mos-1363/S0 M1-M14 and post-10-27/MVP priority fixed. Static diff, twelve brief sections and append-only checks pass. No repo Node, tests, build, browser, queue mutation or live action. Fenton reviews after authorized docs branch push; Sage lands on approval.
|
||||
|
||||
2026-10-23T17:32:37Z | Gyro sage-3544 fenton-0016 N1/N2 delta | Before: W7 Gate/final real-conversion list omit the W8 build-order cross-reference and W8 human-view filtering cites agent settings. After: both locations explicitly put W8 landing before human opening as mos-1470 build order, not acceptance; comment/tracked-deletion links are not current story links and never appear in the human Connections view regardless of agent settings. New commit atop approved 5af060fb5, no amend. Static diff and append-only checks pass. Docs only, no repo Node/tests/live action. Fenton checks only this delta.
|
||||
|
||||
2026-10-23 | Gladstone | Gitea #1816 / Vikunja 614 hostcheck item 5 and bundle verification | Before: dpkg's correct 750 form falsely fails item 5, and two run-sheet bundle verifies require a missing repository. Draft after: exact root/mosaic/path with octal 0750 equivalence; stat/output unchanged; both verifies use cleaned-up bare scratch repos. Three additive-only test blocks, two planned mutants. Untested under Sage-3738; Sage-3740 authorizes untested branch push for static review. Queue row pending; Sage owns gate and landing.
|
||||
|
||||
2026-10-23 | Gladstone | #1816 / Vikunja 614 scope amendment mos-1534, sage-3741 | F1 narrowed to exact 750/0750 and single spaces; F3 allows @ in absolute unit paths with unsafe-character refusals retained; F4 moves copy_tree to / after path resolution. AS_USER audit found git -C and stack-release's existing checkout cd cover the other steps. Added new unit and unreadable-cwd copy tests only. Untested under Sage's no-Node hold; no live action.
|
||||
|
||||
2026-10-23 | Gladstone | #1816 / Vikunja 614 F5 draft (mos-1536) | Before: the run sheet omits the explicit notify decision and precheck misses a bus-start refusal. After: section 9a creates a private null-binding decision as mosaic; precheck directly reuses readNotifyConfig and private_dir, and new tests cover accepted and refused decisions. Existing fixture setup additions await Mos approval through Sage; installer unchanged. Untested under no-Node hold.
|
||||
|
||||
2026-10-23 | Gladstone | #1816 / Vikunja 614 fixture ruling mos-1537 | Added only three setup lines each to host(t)/world(t) for the explicit null notifier decision. Existing assertions/fakes/lines intact. New install test checks decision bytes/mode preserved and missing decision remains absent. F5 missing-file fault removes the setup file. Untested under Sage's hold; separate follow-up atop 33950cf9b.
|
||||
|
||||
2026-10-23 | Gladstone | #1816 / Vikunja 614 F6 and notify parent (mos-1537) | Before: the run sheet's copy check lacks its required CLI wrapper; mkdir -p -m does not guarantee the notify parent mode. After: a separately Mos-gated wrapper --create step follows install and precedes check, as mosaic from cwd /; section 9a explicitly creates both notify directories at 0700. Docs only, no live action; Node/tests still held.
|
||||
|
||||
2026-10-23 | Gladstone | Row320 / #1816 first review CHANGES (morgana-0217) | Reviewer run: hostcheck19/19, unit47/47, install24/24, copy17/18; seven of eight mutants killed, M4 survived because both base/mutant failed only cleanup. Fixed this row's F4 test to enter cwd then chmod000, assert no read/search, pass an absolute outside bundle, and restore0700 in finally. Existing tests/setup and production scripts unchanged. Correction untested locally pending Morgana's focused rerun; no acceptance claim.
|
||||
|
||||
2026-10-23 | Boyd | Row 298 (#1804, Q228), before implementation completion | Took over Megavolt 7a141269c on boyd/row298 at 45dca69f6 under mos-1496/1502. Approved D1-D6 govern human-only grants.scope.set, empty fallback, in-scope close/evidence and spend recheck. All inherited code UNTESTED; no Node/tests until Sage CLEAR. Plan and static gaps recorded in agents/boyd/work/queue-298/BUILD.md; no live grant or credential access.
|
||||
|
||||
2026-10-23 | Boyd | Row 298 (#1804, Q228), targeted verification | On sage-3642 T1 lane B, five whole files serial: business scope 7/0, bus scope 14/0, scope runtime 1/0, CLI scope 4/0, project runtime 3/0 (29/0, no skips). Initial bus fixture typo recorded and corrected. M1-M10 killed by named assertions, source restored and final files green. Dewey dewey-0933 positive control and notes addressed; manifest and evidence in agents/boyd/work/queue-298/BUILD.md. Full package/script gate remains Sage's. No live scope set.
|
||||
|
||||
2026-10-23T22:41:07Z | Boyd | Row 298 (#1804), rebase after row 239 | Rebased onto 833ec1cc8d7c11842efb82e57c1a0b256c8d22d7 on boyd/row298-r2 without altering published boyd/row298. Only append-log conflicts; upstream bytes preserved and Boyd suffixes appended. Product merges automatic; row 239 writer-lock revoke and post-revoke error handling preserved, organization.mjs unchanged from new base. Static checks only; UNTESTED on new base pending Sage CLEAR. Details in Boyd BUILD.md.
|
||||
|
||||
2026-10-23 | Goldie | Row 324 / #1820 before (Q247, mos-1555) | New branch goldie/row324 at row298 approved860afc2b6. Design/readers audit in agents/goldie/work/queue-324/BUILD.md: normalize omitted tracker to null, preserve present validation, guard sync readers/adapter config; runtime/instance-settings/broker checks already optional. Existing business.test.mjs98 explicitly requires tracker, so implementation held for Mos approval of removing that one list entry; no old test or product edit, no tests run.
|
||||
|
||||
2026-10-23 | Goldie | Row 324 / #1820 draft built (sage-3836, mos-1556) | Claim rev3759. Omitted tracker normalizes null; present including null/malformed stays validated. Loader, business validate, bootConfig trackerFor and Console sync reader handle absence. Row298's guarded runtime/instance-settings/broker paths preserved. Two new test files (9 planned tests); sole old-test edit drops tracker from required-key list under mos-1556. node --check6/6 and diff check pass. No tests run: untested branch push expressly ordered before CLEAR; Duckworth review and Sage gate pending.
|
||||
@@ -47,6 +47,14 @@ its launch configuration remains fixed, including on resume. Use Filbert's
|
||||
wrapper to select that configuration; a direct shared-helper invocation uses
|
||||
its own supplied flags or the system defaults.
|
||||
|
||||
Any run that includes the webui suite, whether a seat's slot run or a landing
|
||||
gate, sets `MOSAIC_REQUIRE_EDITOR_BUILD=1`, so a missing editor toolchain fails
|
||||
the run instead of skipping the rebuild test. A worktree that links the
|
||||
canonical `node_modules` also runs `npm ci --ignore-scripts --no-audit --no-fund`
|
||||
in its own `packages/webui` first. The root install carries esbuild 0.28.1
|
||||
through pi-coding-agent, and the rebuild needs the 0.28.2 that
|
||||
`packages/webui/package-lock.json` pins (Vikunja 597, mos-1453).
|
||||
|
||||
All agents follow repository governance and current user direction. Team
|
||||
leadership does not add deployment or push authority. Coordinate overlapping
|
||||
work with Sage and preserve other sessions' changes.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Beakley
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 217 (C2-06, handed over from Launchpad).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 88b97f65) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `beakley/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,8 @@
|
||||
# Boyd
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 298 (the chief-of-staff grant scope, handed over from Megavolt).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 48a3ae38) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `boyd/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,272 @@
|
||||
# Row 298 (#1804, Q228): chief-of-staff grant scope
|
||||
|
||||
Owner Boyd (48a3ae38), reviewer Dewey. Branch `boyd/row298`, base `45dca69f6`.
|
||||
Assignment: mos-1496/Q245. Handoff: Megavolt megavolt-0203, local `7a141269c`;
|
||||
cherry-picked as `4de3cec27` (design) and `7cea739de` (UNTESTED code).
|
||||
Brief: `docs/plans/2026-10-23_mvp-readiness-rows.md`, chief-of-staff grant scope.
|
||||
Megavolt's original design remains in `agents/megavolt/work/queue-298/BUILD.md`.
|
||||
|
||||
## Authority and accepted design
|
||||
|
||||
Sage sage-3594, restated in sage-3612: launch.grantScope, lenient read with empty
|
||||
fallback, strict human-only write. No general organization setter may write it.
|
||||
Per-method agents/projects or agents/hosts/commands; host-enroll never in scope.
|
||||
Host command arrays match whole tokens and exact lengths. `grants.scope.set`
|
||||
from the human CLI is Approve; no Console page or chief-raised scope change.
|
||||
Mos mos-1496/1502 requires the same human-shell guard as `mosaic access`.
|
||||
The broker closes an in-scope request via scope and writes grant.scope.used;
|
||||
missing evidence must refuse execution. Narrowing before spend fails
|
||||
scope-changed. Empty scope ships; only Jason sets the live value (Q188).
|
||||
|
||||
No credential files read, no live grants, no deployment, no queue writes. Push
|
||||
only boyd/*, additive commits, no amend/force-push or attribution trailers.
|
||||
Sage owns queue, shared test slot and landing. No Node/tests before CLEAR.
|
||||
|
||||
## Implementation plan and ledger
|
||||
|
||||
Using writing-plans and executing-plans inline; the approved brief and D1-D6
|
||||
are the spec. This packet is the requested plan/tracking location.
|
||||
|
||||
- [x] Read brief, roster, implementation and handoff; register session.
|
||||
- [x] Bring Megavolt's two commits onto Boyd's branch without changing history.
|
||||
- [x] Add independent CLI tests in `packages/cli/tests/grants-scope.test.mjs`:
|
||||
scope read/set requests; agent-marker rejection before stdin/transport;
|
||||
--tester rejection; missing revision, malformed/empty/oversize input.
|
||||
- [x] Strengthen new business/bus grant-scope tests for exact commands,
|
||||
scope evidence binding, host-enroll exclusion and spent scope-changed.
|
||||
- [ ] On Sage CLEAR, observe targeted failures before fixes; fix inherited
|
||||
implementation using the existing human proof and grant executors.
|
||||
- [ ] Update bus/CLI docs, append BUILD-LOG before/after evidence, manifest.
|
||||
- [ ] On Sage CLEAR run touched suites; record exact results and mutants.
|
||||
- [ ] Commit UNTESTED until verified; push boyd/row298 only when slot permits;
|
||||
report to Sage and Dewey, cc Mos for pushes/blockers. Dewey reviews manifest.
|
||||
|
||||
Pre-flight interfaces: business checkGrantScope/grantScopeOf/grantInScope feed
|
||||
the instance writer and broker. Runtime supplies a fresh scope from the org
|
||||
file and routes the human setter to the same organization writer. CLI callFor
|
||||
supplies refuseInsideAgent and humanTransport. The grant executors consume the
|
||||
broker's immutable close/evidence. Tests use scratch files/fake Gitea only.
|
||||
|
||||
Review focus: exact argv length, stale scope before spend, forged/missing scope
|
||||
evidence, malformed fallback, and CLI agent ancestry. Tests must cover each.
|
||||
|
||||
Static findings before any Boyd Node/test run:
|
||||
|
||||
- Handoff introduced commandPrefixes beyond the accepted D2 shape, allowing
|
||||
trailing args. Remove it; only exact command arrays are authorized.
|
||||
- scope.used carries a digest without validating it, and no org revision.
|
||||
Bind evidence to the canonical scope snapshot and grant/close sequence;
|
||||
preserve historical grants when scope is later changed, recheck at spend.
|
||||
- Gitea recheck throws inside the spend transaction and rolls back spend on
|
||||
scope-changed. Preserve spend and log failure for that case (D6).
|
||||
- The optional giteaStart?. assertion always passes when the API is absent;
|
||||
use the real runGiteaGrant entry point. Fake Gitea tokens() does exist.
|
||||
|
||||
## Existing test changes (mos-1272)
|
||||
|
||||
Approved by Mos mos-1510 via Sage sage-3624; edited exactly as approved:
|
||||
|
||||
| File | Actual numstat | Reason |
|
||||
| --- | --- | --- |
|
||||
| packages/bus/tests/project-runtime.test.mjs | +1 / -1 | Vocabulary import pin adds grantScopeOf and grantInScope. |
|
||||
|
||||
Other test work is confined to row 298's new files. No pre-existing CLI tests
|
||||
will change. Record final numstat here before review.
|
||||
|
||||
## Human proof and organization ownership
|
||||
|
||||
CLI: `packages/cli/src/cli.mjs` callFor runs refuseInsideAgent before constructing
|
||||
humanTransport, as `accessCommand` does. The setter takes no --tester flag.
|
||||
`packages/bus/src/human.mjs` verifyHuman checks the nonce, CLI path and every
|
||||
ancestor's markers/argv; `bindHuman` and #human in broker.mjs require the
|
||||
business human, via cli, outsideAgent true. Tests will pin this chain's caller
|
||||
refusal. The scope receipt records by/via/outsideAgent and old/new sha256.
|
||||
|
||||
Org file: `packages/business/src/organization.mjs` regular() requires a regular,
|
||||
non-symlink file owned by the process uid and not writable by group/other;
|
||||
write() uses revision checks, locked durable receipts and atomic rename.
|
||||
The OS owner uid can edit it directly; the agent sandbox does not mount the
|
||||
user's config (`packages/seat/src/sandbox.mjs`). This is the existing cooperative
|
||||
human/process boundary, not a hostile same-uid isolation claim.
|
||||
|
||||
Chief enrollment: broker instances.set calls #human and #mayHuman before
|
||||
add/change; credential issue/delete use #requestCredential's human gate.
|
||||
launch.by is an organization-file setting, excluded from general setters;
|
||||
only that chief may auto-approve within scope. Additional chief instances
|
||||
remain on the per-grant human approval path.
|
||||
|
||||
## Validation
|
||||
|
||||
UNTESTED. No Boyd Node/test run yet; waiting for Sage CLEAR.
|
||||
M1: bypass in-scope check. M2: bypass invalid-value fallback. M3: remove human
|
||||
setter guard. M4: drop grant.scope.used. All need named failing tests on gate.
|
||||
|
||||
## Session correction
|
||||
|
||||
The initial SESSIONS entry labels startup 19:13Z; startup was approximately
|
||||
19:08Z. Message header timestamps in boyd-0001 through 0004 were estimates;
|
||||
use T3's receipt timestamps as authoritative. Subsequent reports use date -u.
|
||||
|
||||
## Departures from Megavolt 7a141269c (Sage sage-3624)
|
||||
|
||||
1. Removed commandPrefixes; host scope commands match exact token arrays only.
|
||||
2. grantScope callback now supplies one organization read `{value,revision}`.
|
||||
grant.scope.used includes that revision, canonical scope snapshot and digest;
|
||||
#approvedGrant validates the digest, shape, method, agent and close sequence.
|
||||
It uses the immutable snapshot so later narrowing doesn't revoke issued grants.
|
||||
3. Gitea scope-changed is caught inside the spend transaction and logs a terminal
|
||||
failure without rolling back its spend. Other recheck failures keep their
|
||||
existing behavior. Host already catches recheck failures after spending.
|
||||
|
||||
Additional handoff corrections: replaced optional nonexistent giteaStart with
|
||||
runGiteaGrant; added successful host executor/revoke and enrollment-exclusion
|
||||
coverage; added real runtime writer coverage and independent CLI guard tests.
|
||||
|
||||
Sage scheduled push at 19:41Z with MOSAIC_GITEA_CREDENTIAL_FILE unset, then T1
|
||||
lane B around 19:47Z (after explicit CLEAR), four new files whole and serial,
|
||||
hard stop 20:00Z. Fethry lane A is disjoint. No test was run early. Production
|
||||
corrections are marked UNTESTED under that slot mandate; baseline regression
|
||||
checks against the inherited implementation will run only on CLEAR.
|
||||
|
||||
## Candidate preparation (before T1)
|
||||
|
||||
The three static corrections, CLI/runtime coverage and docs are prepared but
|
||||
UNTESTED. `git diff --check` is clean. The approved import-pin edit is exactly
|
||||
1 addition / 1 deletion. M1-M4 are encoded in mutants.py (not run). No Node,
|
||||
https git, full suite or webui build has run in Boyd's worktree.
|
||||
|
||||
Review package: product/test/documentation files in MANIFEST.sha256, excluding
|
||||
append-only project logs and the preserved predecessor packet. The manifest
|
||||
identifies exact bytes for Dewey; each later product change regenerates it and
|
||||
requires a review delta. Existing tests changed: only project-runtime, 1/1.
|
||||
|
||||
## Dewey static review dewey-0933
|
||||
|
||||
N1: Added a positive control to the malformed-evidence fixture. A valid event
|
||||
must pass scope authorization and reach decision-mismatch on the deliberately
|
||||
absent definition binding; invalid events remain not-authorized. No spend.
|
||||
N2: Additional named mutants cover spend recheck, exact argv length, Gitea spend
|
||||
rollback, launch.by eligibility and accepting any evidence row (results below).
|
||||
N3: At spend only grant scope membership is rechecked, not whether the raising
|
||||
chief is still launch.by. This implements sage-3594 Q5; changing the launcher
|
||||
later does not by itself invalidate a pending scope-approved request.
|
||||
N4: The scope shortcut returns the closed decision view to the chief without
|
||||
sending a separate close message; documented in the bus verb table.
|
||||
N5: grant.scope is logged after the organization file commit in a separate bus
|
||||
transaction. A crash between them leaves durable changes.jsonl as the receipt,
|
||||
as with other organization writers. The setter does not claim cross-store
|
||||
atomicity.
|
||||
|
||||
## T1 lane B results (sage-3642, 2026-10-23 19:23Z–19:29Z)
|
||||
|
||||
TMPDIR: `/mnt/storage/scratch/tmp/boyd298-4bzr6fn7`. Each whole file ran serially
|
||||
as `timeout -k 10 180 node --test --test-concurrency=1 FILE`, TMPDIR set to that
|
||||
scratch directory. Final restored candidate results:
|
||||
|
||||
| Whole file | Pass | Fail | Skip |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| business/tests/grant-scope.test.mjs | 7 | 0 | 0 |
|
||||
| bus/tests/grant-scope.test.mjs | 14 | 0 | 0 |
|
||||
| bus/tests/grant-scope-runtime.test.mjs | 1 | 0 | 0 |
|
||||
| cli/tests/grants-scope.test.mjs | 4 | 0 | 0 |
|
||||
| bus/tests/project-runtime.test.mjs | 3 | 0 | 0 |
|
||||
|
||||
29/0/0 total. `final-1.log` through `final-5.log` hold the final output. The
|
||||
runtime's reload-needs-restart notice is expected: the fixture's boot document
|
||||
has fewer credential references than its organization document, as in the
|
||||
existing instances runtime tests. No credential contents are loaded from the
|
||||
real user config.
|
||||
|
||||
Initial run: business 7/0; bus 13/1 due to Boyd's test typo, an out-of-scope
|
||||
`opts` reference in the missing-scope assertion. Corrected only that new-test
|
||||
line, then bus 14/0 and the remaining files green. Dewey N1 positive control
|
||||
was added and the whole bus file passed again; final rerun above followed all
|
||||
mutants. No production code changed during this test window.
|
||||
|
||||
Mutants: all whole files, serial, with timeout -k 10 and the same TMPDIR.
|
||||
`mutants.py --sage-clear sage-3642 --logs SCRATCH/review-mutants --deadline
|
||||
2026-10-23T19:38:00+00:00` restores exact original source after each run.
|
||||
M1/M2/M3/M4 are brief/Sage mutants; M5–M10 cover review focus and dewey-0933.
|
||||
Each exited 1 with named assertion failures, no loader/syntax kill.
|
||||
|
||||
| Mutant | Change | Pass/fail | Named killer |
|
||||
| --- | --- | --- | --- |
|
||||
| M1 | skip raise scope check | 11/3 | bus out-of-scope Gitea waits; empty/bad scope waits; host argv exclusion |
|
||||
| M2 | skip invalid-value fallback | 5/2 | business missing/bad scope fallback; bad file scope fallback |
|
||||
| M3 | remove #human from scope verb | 13/1 | bus Q3 caller refusals |
|
||||
| M4 | drop grant.scope.used write | 9/5 | bus Q4 protected evidence assertion; four executor waits also fail |
|
||||
| M5 | skip scope digest check | 13/1 | bus Q4 bad evidence: decision-mismatch instead of not-authorized |
|
||||
| M6 | rethrow narrowing, rolling spend back | 13/1 | bus Q5 Gitea spent assertion |
|
||||
| M7 | same without length check | 6/1 | business Q2 shorter command prefix exclusion |
|
||||
| M8 | no-op #scopeRecheck | 12/2 | bus Q5 Gitea and host scope-changed outcomes |
|
||||
| M9 | remove launch.by check | 13/1 | bus Q6 second chief stays open |
|
||||
| M10 | accept any matched scope evidence row | 13/1 | bus Q4 bad evidence, with positive control |
|
||||
|
||||
Mutation log sha256 (under scratch/review-mutants):
|
||||
|
||||
```text
|
||||
55e9973af5fca0088963e2ca11fd81b2fb46dfead813a577c0e0aad2d6269b68 M1.log
|
||||
abd1b784eff5ae1333a7b47085d3e199cad6d27a00e2584edf0b62417ad4c888 M2.log
|
||||
1a56085e548d1e244f0f88fe12a45f77c162375880794f9ef88b241ed3a1704d M3.log
|
||||
9c120f3f7128488edaf8bfd6d41bae712e69e76da4195a0d4dc0e8336b02038f M4.log
|
||||
1bba88a3752823454acdaa51abde6aad66fdd5488801b4aceda02a3b42c8123d M5.log
|
||||
a4ce3cacca9f7ea56b8cdfd503c76173a8e4ff1d8a7ec07032c93f0b51aa60d8 M6.log
|
||||
acecd5afc93d68d96d713f8a9a2dc08351831a0b7360a13b4e30a572ad4167bf M7.log
|
||||
fce7fa505ccf4f2d4190bd03c209fd19f1cf2770d3ae1246a3bd03697594019e M8.log
|
||||
a830a7964890dd04a2dcc7a932a1502f1dea06419a1eae6b91a85f7878dc5681 M9.log
|
||||
38ce3e7f69f6c6ffe10133397465d52538a930a830f76256a8b9280f8df78e4b M10.log
|
||||
```
|
||||
|
||||
Full touched-package suites and every scripts/test-*.sh remain for Sage's full gate; this CLEAR authorized only the five whole files. Candidate remains
|
||||
marked Untested for the full gate. No queue row has been changed by Boyd.
|
||||
|
||||
## Dewey final delta review (dewey-0935, 2026-10-23 19:31:55Z)
|
||||
|
||||
Dewey reviewed 574ece13c..bcd93dc08 with local git only, no Node: CLEAN,
|
||||
nothing blocks. Verified all 13 manifest entries, exact package-file coverage,
|
||||
project-runtime 1/1, M5-M10 mutation sites, all ten mutation log hashes and
|
||||
counts, named assertion kills without loader/syntax errors, final five logs
|
||||
29/0/0, and N1-N5 coverage. No production code delta in this review range.
|
||||
|
||||
Formal Gitea APPROVE on #1804 and queue record wait on the row 291 hold release
|
||||
and Sage's go. Sage's touched-package and every scripts/test-*.sh gate remains
|
||||
required. Boyd has released lane B and has no running test processes.
|
||||
|
||||
## Rebase after row 239 (sage-3794)
|
||||
|
||||
Current branch: `boyd/row298-r2`, rebased from published row 298 tip
|
||||
`2fc27bce6bfac2debe9703782bf308132c26b393` onto
|
||||
`833ec1cc8d7c11842efb82e57c1a0b256c8d22d7`, which includes row 239 through
|
||||
`e0cb1db2c`. Original `boyd/row298` remains intact locally and remotely. A new
|
||||
Boyd branch carries the rebased candidate so no published history is amended
|
||||
or force-pushed. This section supersedes the original base/branch header for
|
||||
current candidate work.
|
||||
|
||||
Conflicts: only the first Boyd log append in `BUILD-LOG.md` and
|
||||
`docs/SESSIONS.md` conflicted while replaying 574ece13c. For each, verified the
|
||||
old commit's log started with the merge base bytes, then retained the new
|
||||
base's complete log bytes and appended only Boyd's suffix (434 and 281 bytes
|
||||
respectively). Later Boyd appends replayed automatically. No prior entries
|
||||
were removed or rewritten. Five commits replayed; the third/fourth range-diff
|
||||
entries differ only in append-log context; package-only range-diff is all `=`.
|
||||
|
||||
The overlapping product files merged automatically: broker.mjs, runtime.mjs
|
||||
and instance-settings.mjs had no textual conflicts. Inspected their diff
|
||||
against the new base: only the existing row 298 scope additions remain.
|
||||
Preserved row 239's injected writer, fire/delete beforeWrite callback, and
|
||||
commit() passing beforeWrite into writer.write. There is no pre-lock revoke
|
||||
call in commit(). `packages/business/src/organization.mjs` is byte-identical
|
||||
to the new base: beforeWrite still runs inside withLock, after revision and
|
||||
validation checks; post-revoke errors still carry revoked true. Broker's
|
||||
row-239 error handling and runtime's lock/revoke wiring remain intact.
|
||||
|
||||
Static checks only: package patch range-diff all equal, organization writer
|
||||
unchanged, diff whitespace clean, refreshed 13-file package manifest verifies.
|
||||
The only pre-existing row-298 test change remains project-runtime.test.mjs
|
||||
exactly 1/1 under mos-1510. No test files were changed for this rebase.
|
||||
UNTESTED on the new base: no Node, tests, mutants, builds or queue writes run.
|
||||
Earlier 29/0 and M1-M10 results apply to the old base only. Await Sage CLEAR
|
||||
for whole touched-file reruns; Dewey reviews the new candidate.
|
||||
|
||||
Row 321 (#1817) review assignment acknowledged; waiting for Sage's request
|
||||
and Gladstone's pushed docs-only F7/F10 run-sheet candidate.
|
||||
@@ -0,0 +1,13 @@
|
||||
612488b705853e75e6f8952e41552526f4d07dd297d7619d608d434ed75121d6 packages/bus/README.md
|
||||
63fdf6c048f0994af3551510119eeb294c71f8116702772844214930b827cc56 packages/bus/src/broker.mjs
|
||||
65425ead353017c99e04f1e0bcc3f85c4ba60054da293badaa0360e681e418dc packages/bus/src/runtime.mjs
|
||||
e7065db949cacfd4679a1031eebf5cde09ed196ce85612e8400ae563f02a0f18 packages/bus/tests/grant-scope-runtime.test.mjs
|
||||
4030cf584481a1db12d9e6f744015c48fdebd468335d065ead3e90180858d366 packages/bus/tests/grant-scope.test.mjs
|
||||
bc432d8d430433b9d84c00883584dc53d4b27afb5e93f739fe3ca28e80f01efa packages/bus/tests/project-runtime.test.mjs
|
||||
84d6dd15d5ca2926d8f7a34548c70c3e7a3c58e692f0b009414f1af35cb096cc packages/business/src/business.mjs
|
||||
e3b425f9ad2a0d4e6e0b55bd48827e144015662d95ace4163fe6474e5f98da81 packages/business/src/instance-settings.mjs
|
||||
fed4177325f65a2b9823a40a9c35c01d60c0520e3ff40b4574c3cf2c29cc096b packages/business/src/vocabulary.mjs
|
||||
b7f96e45b9ffca2a7ebc18dc446f0adcaac72d3900f098c60157cf6cbf1bfc13 packages/business/tests/grant-scope.test.mjs
|
||||
60fc267b7f8f9e83eeb0d0207c8abdef68d554f32b08d508e98dbf59b442a366 packages/cli/README.md
|
||||
b586907ca8f351f60dc19ec2683df75d2d70ddb12955d09fb14560c9d3bed3dc packages/cli/src/grants.mjs
|
||||
fb7c30b31facd887b03ff02dd6734d48a815978481b33bb21ec48604a51c5ff5 packages/cli/tests/grants-scope.test.mjs
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Row 298 mutants. Run only with Sage's explicit CLEAR in Boyd's slot.
|
||||
|
||||
Each mutation is applied to this worktree, runs one whole new test file,
|
||||
then restores the exact original bytes, including after an interruption.
|
||||
Logs go to a caller-supplied scratch directory, never the repository root.
|
||||
"""
|
||||
import argparse
|
||||
import datetime
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[4]
|
||||
BROKER = ROOT / 'packages/bus/src/broker.mjs'
|
||||
VOCABULARY = ROOT / 'packages/business/src/vocabulary.mjs'
|
||||
BUS_TEST = 'packages/bus/tests/grant-scope.test.mjs'
|
||||
BUSINESS_TEST = 'packages/business/tests/grant-scope.test.mjs'
|
||||
|
||||
|
||||
def replace_once(source, old, new):
|
||||
if source.count(old) != 1:
|
||||
raise RuntimeError(f'expected exactly one mutant site, found {source.count(old)}')
|
||||
return source.replace(old, new, 1)
|
||||
|
||||
|
||||
def mutate(name, source):
|
||||
if name == 'M1':
|
||||
return replace_once(source,
|
||||
'if (!raise?.grant || !grantInScope(scope, raise.grant)) return view;',
|
||||
'if (!raise?.grant) return view;')
|
||||
if name == 'M2':
|
||||
return replace_once(source,
|
||||
'return scope === null ? { scope: GRANT_SCOPE_EMPTY, fallback: true } : { scope, fallback: false };',
|
||||
'return { scope: scope ?? value, fallback: false };')
|
||||
if name == 'M3':
|
||||
return replace_once(source,
|
||||
' #grantScopeVerb(s, verb, a) {\n this.#human(s);',
|
||||
' #grantScopeVerb(s, verb, a) {')
|
||||
if name == 'M4':
|
||||
old = """ this.#grantEvent(s.business, {
|
||||
action: 'grant.scope.used', grantDecision: d.id, decisionSeq: seq, method: raise.grant.method,
|
||||
agent: raise.grant.agent, scope, revision, scopeSha256: sha256(JSON.stringify(scope)),
|
||||
}, d.id);"""
|
||||
return replace_once(source, old, ' // MUTANT: scope evidence omitted.')
|
||||
if name == 'M5':
|
||||
return replace_once(source,
|
||||
"evidence.agent === grant.agent && evidence.scopeSha256 === sha256(JSON.stringify(checked.scope)) &&",
|
||||
"evidence.agent === grant.agent &&")
|
||||
if name == 'M6':
|
||||
start = source.index(" checked = this.#store.transaction(() => {\n const consumed = this.#consumeGrant")
|
||||
end = source.index('\n } catch (e) {', start)
|
||||
return source[:start] + " checked = this.#store.transaction(() => this.#giteaRecheck(business, decision, this.#consumeGrant(business, decision, 'gitea')));" + source[end:]
|
||||
if name == 'M7':
|
||||
return replace_once(source,
|
||||
'const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);',
|
||||
'const same = (a, b) => a.every((x, i) => x === b[i]);')
|
||||
if name == 'M8':
|
||||
return replace_once(source,
|
||||
" if (this.#closed(decision)?.via === 'scope' && !grantInScope(this.#scopeNow(business).scope, g)) fail('scope-changed');",
|
||||
" // MUTANT: no spend-time scope recheck.")
|
||||
if name == 'M9':
|
||||
return replace_once(source,
|
||||
' if (s.role !== this.#business(s.business).launch?.by) return view;',
|
||||
' // MUTANT: any chief instance can use scope.')
|
||||
if name == 'M10':
|
||||
start = source.index(' const evidence = JSON.parse(row.body);', source.index(' #scopeUsed('))
|
||||
end = source.index('\n }', start)
|
||||
return source[:start] + ' return true;' + source[end:]
|
||||
raise ValueError(name)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--sage-clear', required=True, help='CLEAR message key; required before any Node run')
|
||||
parser.add_argument('--logs', type=pathlib.Path, required=True)
|
||||
parser.add_argument('--mutant', choices=['M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8', 'M9', 'M10', 'all'], default='all')
|
||||
parser.add_argument('--deadline', default='2026-10-23T19:38:00+00:00')
|
||||
args = parser.parse_args()
|
||||
deadline = datetime.datetime.fromisoformat(args.deadline)
|
||||
args.logs.mkdir(parents=True, exist_ok=True)
|
||||
names = ['M1', 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', 'M8', 'M9', 'M10'] if args.mutant == 'all' else [args.mutant]
|
||||
for name in names:
|
||||
remaining = (deadline - datetime.datetime.now(datetime.timezone.utc)).total_seconds()
|
||||
if remaining <= 15:
|
||||
raise RuntimeError('Sage slot deadline reached; no new test run')
|
||||
path, test = (VOCABULARY, BUSINESS_TEST) if name in ['M2', 'M7'] else (BROKER, BUS_TEST)
|
||||
original = path.read_bytes()
|
||||
mutated = mutate(name, original.decode())
|
||||
log = args.logs / f'{name}.log'
|
||||
try:
|
||||
path.write_text(mutated)
|
||||
with log.open('w') as output:
|
||||
result = subprocess.run(['timeout', '-k', '10', str(int(min(180, remaining - 13))), 'node', '--test', '--test-concurrency=1', test],
|
||||
cwd=ROOT, stdout=output, stderr=subprocess.STDOUT,
|
||||
timeout=min(195, remaining - 1), check=False)
|
||||
print(f'{name}: exit={result.returncode} log={log}', flush=True)
|
||||
finally:
|
||||
path.write_bytes(original)
|
||||
# Exit 1 needs human inspection of the failing test names; errors are not kills.
|
||||
if result.returncode == 0:
|
||||
raise RuntimeError(f'{name} survived')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
# Bubba
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 288 (agent write notices and library revert, handed over from Honker).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 3d6672b5) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `bubba/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,30 @@
|
||||
# Row 239 round 1 verdict: honker/row239-r4 e0cb1db2c (duckworth-0295)
|
||||
|
||||
Request: honker-0206 (in-review round 1, rev 3719). Candidate: e0cb1db2c on refactor 546366f4a.
|
||||
Method: git reads of local objects only. No node, no tests, no fetch, no queue reads.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- **The replay is exact.** `git range-diff 93694c518..5b8492b0f 546366f4a..0cae2dad5` gives four `=`
|
||||
lines. None of the 16 manifest paths changed between 93694c518 and 546366f4a, and
|
||||
`git diff 5b8492b0f 0cae2dad5` over those 16 paths is empty. My r3 read (REBASE-r3-5b8492b0f.md,
|
||||
duckworth-0292) applies unchanged, and so do the 0165 design approval and the 0168 code approval with
|
||||
C1 and C2 met.
|
||||
- **e0cb1db2c is doc only.** It appends 26 lines to agents/honker/work/queue-239/BUILD.md. No test reads
|
||||
that file.
|
||||
- **Pins at e0cb1db2c.** All four 64-hex pins in channel-proof.test.mjs match the files' sha256:
|
||||
human.mjs, human-cli.mjs, transport.mjs and webui bus.mjs (e3f52172…). Upstream changed library
|
||||
src/index.mjs and business src/index.mjs, and no test or script pins them. The only hit in my grep
|
||||
is a basename collision (`cli.mjs` matched `human-cli.mjs`).
|
||||
- **The run, as Honker reported it (sage-3773 CLEAR, on 0cae2dad5. I didn't rerun it):**
|
||||
- The six touched test files ran whole: 65/65, 0 skipped, no first-run red.
|
||||
- Mutants M1-M3g: 9/9 killed as assertion failures in their named tests.
|
||||
- M3f killed T6, which matches my static trace in 0292.
|
||||
- Every restore matched by sha256, and the tree was clean afterwards.
|
||||
Running on the replay rather than r3's base was right, because bus runtime.mjs imports library
|
||||
src/index.mjs and the cli imports business src/index.mjs, and both changed upstream.
|
||||
- **What remains is Sage's.** The full gate runs on the replay at landing (sage-3730). Sage's call.
|
||||
|
||||
## Notes
|
||||
|
||||
- None new. 0168 N1 (endsWith access-revoked) stands as recorded.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Fethry
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 225 (C2-10b, handed over from Morgana).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 14f80e3c) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `fethry/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,82 @@
|
||||
# Row 305 (#1812, Vikunja 591): shell suites remove the compose network they create
|
||||
|
||||
Owner Gizmoduck. Reviewer Filbert. Gate owner Sage. Brief:
|
||||
docs/plans/2026-10-23_gate-env-rows.md, "Brief: shell suites remove the compose network they create
|
||||
(Vikunja 591)", at c16e943b9. Base c16e943b9. Mos approved the change in mos-1442 point 2. Mos approved
|
||||
the two new checks in mos-1445 under mos-1272, adding the presence half.
|
||||
|
||||
## The leak, as the code has it at c16e943b9
|
||||
|
||||
- `packages/processing-control/src/containers.mjs:16-19`. `containerArgs` builds
|
||||
`docker compose run --rm ...`. `--rm` removes the container. The project's `<project>_default`
|
||||
network stays.
|
||||
- `compose.yaml` has no top-level `name:`, so compose names the project after the checkout
|
||||
directory. Every gate worktree left one network. By 10-23 they had used up Docker's address pools.
|
||||
|
||||
## Does COMPOSE_PROJECT_NAME reach compose? Yes, on every path the two suites take
|
||||
|
||||
- Task path. `scripts/run-task.sh:29` execs `node scripts/mosaic-task.mjs`. Nothing scrubs the env
|
||||
before that: `scripts/common.sh` has no `env -i`, no `unset` and no COMPOSE. `mosaic-task.mjs:326`
|
||||
sets `spawnEnv = { ...process.env }`, `:405` passes `env: spawnEnv` to `runContainer`, and
|
||||
`containers.mjs:25` passes those options to `spawnSync('docker', ...)`.
|
||||
- test-release. `release.sh activate` calls `health_check`, and `health_check` calls `run-task.sh`
|
||||
at `:54`, which is the task path above. `env MOSAIC_MOCK_RESPONSE=...` adds to the env and keeps
|
||||
the rest. The suite calls only `status`, `activate` and `rollback`, so `release.sh:122`
|
||||
`compose build` is off its path.
|
||||
- test-task seat cases (`:240`, `:315-351`). `agent.sh:209` execs
|
||||
`packages/processing-control/src/container-cli.mjs`, and `container-cli.mjs:16` calls
|
||||
`runContainer` with `env: process.env`.
|
||||
- `build.sh:13`, `release.sh:122` and `verify.sh:40` also run compose. Neither suite calls them.
|
||||
|
||||
## Change (scripts/test-release.sh, scripts/test-task.sh only)
|
||||
|
||||
- After `SANDBOX="$(mktemp -d)"`, export
|
||||
`COMPOSE_PROJECT_NAME=mosaic-test-<release|task>-<id>`. `<id>` is the sandbox name after its last
|
||||
dot, lowercased, alphanumeric only.
|
||||
- `remove_compose_network` runs `docker network rm "${COMPOSE_PROJECT_NAME}_default"`:
|
||||
- If there's no docker binary, it returns 0.
|
||||
- If the error says `not found` or `No such network`, it returns 0.
|
||||
- On any other failure it prints one stderr line naming the network, then returns 0. That
|
||||
includes a daemon that is down. Sage kept that line in sage-3367 (a).
|
||||
- Each existing EXIT trap keeps everything it did and gains `; remove_compose_network` at the end.
|
||||
The trap's own exit status doesn't change the script's.
|
||||
- One new check per suite, just before the summary. That is after the last Docker step whether or
|
||||
not the live cases ran. Both halves sit inside a `docker info` branch:
|
||||
- the network is present under the run's project name before the explicit
|
||||
`remove_compose_network` (this proves the export reached compose);
|
||||
- after the remove, it is absent.
|
||||
|
||||
A failing `docker network ls` fails the check. Without the daemon, the check prints a skip line.
|
||||
- No `docker compose down`, and no other network is touched. No product file changes.
|
||||
|
||||
Commits: 541dee2b7 (draft), 3bce852d5 (presence half, mos-1445).
|
||||
|
||||
## Gate (Sage's slot, in a gap)
|
||||
|
||||
- Before the first run, run `docker network rm` on a name that was never created and record the
|
||||
daemon's exact wording (sage-3367 b). If the wording matches neither pattern, stop and report to
|
||||
Sage.
|
||||
- Each suite runs whole from a gate worktree, under `timeout -k 10 900`. That is far more than
|
||||
3x the row 302 gate's walls: test-release 3 s, test-task 16 s (mos-1275).
|
||||
- TMPDIR comes from `mktemp -d /mnt/storage/scratch/tmp/gXXX`.
|
||||
- Before and after each run, record `docker network ls` filtered to `mosaic-test-`. The run's
|
||||
network must be absent after.
|
||||
- Grep `^skip` and name every skip.
|
||||
- A run killed by the timeout's SIGKILL skips the EXIT trap and leaks its one network. The name is
|
||||
per-run under `mosaic-test-`, so the gate's after-listing finds it and removes it by its exact name
|
||||
(Filbert N1; no code change).
|
||||
- If `docker info` works but there is no network to find, that's a first-run red. It goes to Sage
|
||||
for Mos, unfixed (mos-1445).
|
||||
|
||||
## Mutants (mos-1445)
|
||||
|
||||
- (a) Drop the export. The new check must go red, because compose uses the directory's project and
|
||||
nothing exists under the run's name.
|
||||
- (b) Make the function's `docker network rm` a no-op. The new check must go red, because the
|
||||
network is still present after the remove.
|
||||
- (c) Drop `remove_compose_network` from the trap. This one is a known survivor: the explicit call
|
||||
covers the normal path, and the trap covers early exits.
|
||||
|
||||
Each mutant runs on an isolated copy of the file. Restores are checked by sha256. After each mutant, the network it
|
||||
leaves is removed by its exact name (mos-1438). Mutant (a) leaves the worktree directory's
|
||||
`<dir>_default`, and mutant (b) leaves the run's `${COMPOSE_PROJECT_NAME}_default`.
|
||||
@@ -0,0 +1,88 @@
|
||||
start 2026-10-23T11:14:01Z HEAD 8ddba89b7 refactor b99ca86b9 docker 29.7.2
|
||||
== wording
|
||||
rc=1 stderr: Error response from daemon: network mosaic-test-absent-46829331_default not found
|
||||
exit status 1
|
||||
== green
|
||||
TMPDIR /mnt/storage/scratch/tmp/gIj4
|
||||
nets[before test-task]:
|
||||
test-task rc=0 wall 16 s :: selftest: 92 passed, 0 failed
|
||||
OK compose network made under the run's project and removed after the run
|
||||
skip: skip live provider cases (row 244: no provider call in the gate)
|
||||
skip: skip live provider cases (row 244: no provider call in the gate)
|
||||
nets[after test-task]:
|
||||
TMPDIR /mnt/storage/scratch/tmp/gDYF
|
||||
nets[before test-release]:
|
||||
test-release rc=0 wall 4 s :: selftest: 15 passed, 0 failed
|
||||
OK compose network made under the run's project and removed after the run
|
||||
nets[after test-release]:
|
||||
== mutants
|
||||
Ma scripts/test-task.sh | 2 +-
|
||||
TMPDIR /mnt/storage/scratch/tmp/g6SN
|
||||
nets[before Ma-task]:
|
||||
Ma-task rc=1 wall 16 s :: selftest: 91 passed, 1 failed
|
||||
FAIL compose network made under the run's project and removed after the run
|
||||
red: FAIL compose network made under the run's project and removed after the run
|
||||
skip: skip live provider cases (row 244: no provider call in the gate)
|
||||
skip: skip live provider cases (row 244: no provider call in the gate)
|
||||
nets[after Ma-task]: gizmo-305_default
|
||||
restore ok scripts/test-task.sh
|
||||
Ma-task KILLED
|
||||
cleaned gizmo-305_default
|
||||
nets[after cleanup Ma-task]:
|
||||
Ma scripts/test-release.sh | 2 +-
|
||||
TMPDIR /mnt/storage/scratch/tmp/g48Q
|
||||
nets[before Ma-release]:
|
||||
Ma-release rc=1 wall 3 s :: selftest: 14 passed, 1 failed
|
||||
FAIL compose network made under the run's project and removed after the run
|
||||
red: FAIL compose network made under the run's project and removed after the run
|
||||
nets[after Ma-release]: gizmo-305_default
|
||||
restore ok scripts/test-release.sh
|
||||
Ma-release KILLED
|
||||
cleaned gizmo-305_default
|
||||
nets[after cleanup Ma-release]:
|
||||
Mb scripts/test-task.sh | 2 +-
|
||||
TMPDIR /mnt/storage/scratch/tmp/gcm1
|
||||
nets[before Mb-task]:
|
||||
Mb-task rc=1 wall 15 s :: selftest: 91 passed, 1 failed
|
||||
FAIL compose network made under the run's project and removed after the run
|
||||
red: FAIL compose network made under the run's project and removed after the run
|
||||
skip: skip live provider cases (row 244: no provider call in the gate)
|
||||
skip: skip live provider cases (row 244: no provider call in the gate)
|
||||
nets[after Mb-task]: mosaic-test-task-pf33rz8tic_default
|
||||
restore ok scripts/test-task.sh
|
||||
Mb-task KILLED
|
||||
cleaned mosaic-test-task-pf33rz8tic_default
|
||||
nets[after cleanup Mb-task]:
|
||||
Mb scripts/test-release.sh | 2 +-
|
||||
TMPDIR /mnt/storage/scratch/tmp/gSxg
|
||||
nets[before Mb-release]:
|
||||
Mb-release rc=1 wall 3 s :: selftest: 14 passed, 1 failed
|
||||
FAIL compose network made under the run's project and removed after the run
|
||||
red: FAIL compose network made under the run's project and removed after the run
|
||||
nets[after Mb-release]: mosaic-test-release-db4p70csnf_default
|
||||
restore ok scripts/test-release.sh
|
||||
Mb-release KILLED
|
||||
cleaned mosaic-test-release-db4p70csnf_default
|
||||
nets[after cleanup Mb-release]:
|
||||
Mc scripts/test-task.sh | 2 +-
|
||||
TMPDIR /mnt/storage/scratch/tmp/g6WJ
|
||||
nets[before Mc-task]:
|
||||
Mc-task rc=0 wall 16 s :: selftest: 92 passed, 0 failed
|
||||
OK compose network made under the run's project and removed after the run
|
||||
skip: skip live provider cases (row 244: no provider call in the gate)
|
||||
skip: skip live provider cases (row 244: no provider call in the gate)
|
||||
nets[after Mc-task]:
|
||||
restore ok scripts/test-task.sh
|
||||
Mc-task survived (known)
|
||||
nets[after cleanup Mc-task]:
|
||||
Mc scripts/test-release.sh | 2 +-
|
||||
TMPDIR /mnt/storage/scratch/tmp/g2jz
|
||||
nets[before Mc-release]:
|
||||
Mc-release rc=0 wall 3 s :: selftest: 15 passed, 0 failed
|
||||
OK compose network made under the run's project and removed after the run
|
||||
nets[after Mc-release]:
|
||||
restore ok scripts/test-release.sh
|
||||
Mc-release survived (known)
|
||||
nets[after cleanup Mc-release]:
|
||||
tree clean after mutants
|
||||
end 2026-10-23T11:15:17Z
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 305 gate (sage-3374): wording, both suites whole, mutants a/b/c with exact-name cleanup.
|
||||
set -uo pipefail
|
||||
WT=/mnt/storage/scratch/gizmo-305
|
||||
OUT=/mnt/storage/scratch/gizmo-305-out/gate
|
||||
mkdir -p "$OUT"
|
||||
cd "$WT" || exit 9
|
||||
[ "$(git rev-parse HEAD)" = 8ddba89b7442f09fc71cf24d7f250e40a7f60fdf ] || { echo "HEAD drift"; exit 9; }
|
||||
[ -z "$(git status --porcelain)" ] || { echo "dirty tree"; exit 9; }
|
||||
[ -L node_modules ] || ln -s /mnt/storage/src/mosaic-stack/node_modules node_modules
|
||||
[ -L node_modules ] || { echo "no node_modules link"; exit 8; }
|
||||
DIRNET=gizmo-305_default
|
||||
nets() { echo " nets[$1]: $(docker network ls --format '{{.Name}}' | grep -E '^(mosaic-test-|gizmo-305_)' | tr '\n' ' ')"; }
|
||||
newtmp() { export TMPDIR="$(mktemp -d /mnt/storage/scratch/tmp/gXXX)"; echo " TMPDIR $TMPDIR"; }
|
||||
run() { # name script
|
||||
local name=$1 s=$2; newtmp; nets "before $name"
|
||||
local t0=$SECONDS
|
||||
timeout -k 10 900 "$s" > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
echo "$name rc=$rc wall $((SECONDS-t0)) s :: $(grep -E '^selftest:' "$OUT/$name.log")"
|
||||
[ $rc -eq 124 ] || [ $rc -eq 137 ] && echo "SHELL-PHASE TIMEOUT (mos-1275): to Mos unplaced"
|
||||
grep -E 'compose network' "$OUT/$name.log" | sed 's/^/ /'
|
||||
grep -E '^FAIL|could not remove' "$OUT/$name.log" | sed 's/^/ red: /'
|
||||
grep -E '^skip' "$OUT/$name.log" | sed 's/^/ skip: /'
|
||||
nets "after $name"
|
||||
LASTRC=$rc
|
||||
}
|
||||
echo "start $(date -u +%FT%TZ) HEAD $(git rev-parse --short HEAD) refactor $(git rev-parse --short origin/refactor) docker $(docker version --format '{{.Server.Version}}')"
|
||||
echo "== wording"
|
||||
ABS="mosaic-test-absent-$RANDOM$RANDOM"_default
|
||||
docker network rm "$ABS" >/dev/null 2>"$OUT/absent.txt"; echo " rc=$? stderr: $(cat "$OUT/absent.txt")"
|
||||
grep -qE 'not found|No such network' "$OUT/absent.txt" || { echo "STOP: wording matches neither pattern"; exit 3; }
|
||||
echo "== green"
|
||||
for s in task release; do run "test-$s" "scripts/test-$s.sh"; [ $LASTRC -eq 0 ] || { echo "STOP: red on green run"; exit 4; }; done
|
||||
nets "after green" | grep -qE 'mosaic-test-|gizmo-305_' && { echo "STOP: network left after green"; exit 5; }
|
||||
echo "== mutants"
|
||||
mut() { # tag python-edit expected(red|green)
|
||||
local tag=$1 edit=$2 want=$3
|
||||
for s in task release; do
|
||||
local f="scripts/test-$s.sh" sum; sum=$(sha256sum "$f" | cut -d' ' -f1)
|
||||
cp -p "$f" "$OUT/$s.orig"
|
||||
python3 -B -c "$edit" "$f" || { echo "mutant $tag edit failed"; exit 6; }
|
||||
git diff --stat -- "$f" | head -1 | sed "s/^/ M$tag /"
|
||||
run "M$tag-$s" "$f"
|
||||
cp -p "$OUT/$s.orig" "$f"
|
||||
[ "$(sha256sum "$f" | cut -d' ' -f1)" = "$sum" ] && echo " restore ok $f" || { echo "RESTORE MISMATCH $f"; exit 7; }
|
||||
if [ "$want" = red ]; then grep -q '^FAIL.*compose network' "$OUT/M$tag-$s.log" && echo " M$tag-$s KILLED" || echo " M$tag-$s SURVIVED (unexpected)"
|
||||
else grep -q '^OK.*compose network' "$OUT/M$tag-$s.log" && echo " M$tag-$s survived (known)" || echo " M$tag-$s unexpected red"; fi
|
||||
# exact-name cleanup (mos-1438)
|
||||
for n in $(docker network ls --format '{{.Name}}' | grep -E '^(mosaic-test-(task|release)-[a-z0-9]+_default|gizmo-305_default)$'); do
|
||||
docker network rm "$n" >/dev/null && echo " cleaned $n"
|
||||
done
|
||||
nets "after cleanup M$tag-$s"
|
||||
done
|
||||
}
|
||||
E='import sys;p=sys.argv[1];s=open(p).read();o=%s;n=%s;assert s.count(o)==1;open(p,"w").write(s.replace(o,n))'
|
||||
mut a "$(printf "$E" "'export COMPOSE_PROJECT_NAME='" "'COMPOSE_PROJECT_NAME='")" red
|
||||
mut b "$(printf "$E" "'err=\"\$(docker network rm \"\$net\" 2>&1 >/dev/null)\" && return 0'" "'err=\"\" && return 0'")" red
|
||||
mut c "$(printf "$E" "'; remove_compose_network'\"'\"' EXIT'" "''\"'\"' EXIT'")" green
|
||||
[ -z "$(git status --porcelain | grep -v '^?? node_modules$')" ] && echo "tree clean after mutants" || { echo "TREE DIRTY"; git status --porcelain; }
|
||||
echo "end $(date -u +%FT%TZ)"
|
||||
@@ -0,0 +1,180 @@
|
||||
# Row 307 (#1814, Vikunja 598): move row 270's pi compaction reading behind the harness kind
|
||||
|
||||
Owner Gizmoduck. Reviewer Duckworth. Gate owner Sage. Brief:
|
||||
docs/plans/2026-10-23_harness-kind-compactions.md. Base 1e629c892. Design: DESIGN.md beside this file,
|
||||
approved (static) by Duckworth on 67daf6403 (duckworth-0280), with C1, C2, N1 and N2 folded in at
|
||||
cb048b93c. Mos ruled go on the four old-test edits in mos-1459, with the bounds below.
|
||||
|
||||
## Commits
|
||||
|
||||
1. 1f6db1589, red first. The two new test files and capture.mjs. Its product files are base's:
|
||||
`git diff --stat 1e629c892 1f6db1589` lists only this packet, capture.mjs and the two tests.
|
||||
2. 44104adb1, the build. The product files and the four old-test edits.
|
||||
|
||||
## What the build does
|
||||
|
||||
- `packages/plugins/src/kinds.mjs:52-57`: the harness kind's exports end in `"compactions"`, and the
|
||||
comment gains its contract line. `interfaceVersion` stays 1 (DESIGN.md "interfaceVersion stays 1").
|
||||
- New `packages/plugins/harness/pi/compactions.mjs`: base's `scanPi`, `readRange`, `digest`, `STREAM`,
|
||||
`TAIL`, the chunk size, `PI_VERSION` and `claudeHistory`, unchanged in behaviour, plus the export.
|
||||
`scanPi` is no longer exported and takes no defaults: its one caller passes all four arguments.
|
||||
- `pi/index.mjs` re-exports it. `claude-code/index.mjs` answers harness-unsupported with version null.
|
||||
- `packages/seat/src/compactions.mjs`: keeps the ledger. `readLedger` sets `unsupported` on
|
||||
`r.reason === "harness-unsupported"` only (:113). `checkAnswer` (:147-164) refuses an answer out of
|
||||
contract. `collectCompactions(store, { name, compactions }, opts)` (:174-205) calls the export, checks
|
||||
it, and writes the same lines in the same order as base.
|
||||
- `packages/cli/src/launcher.mjs`: the `compactions` closure takes the loaded module and calls the
|
||||
export directly, with the N1 comment on why it skips `call()`. Both call sites pass `harness`.
|
||||
- `packages/plugins/README.md`: the Harness section gains the `compactions` bullet.
|
||||
|
||||
## Old-test edits (mos-1459 bounds)
|
||||
|
||||
| File | Edit | Bound |
|
||||
| --- | --- | --- |
|
||||
| plugins/tests/kinds.test.mjs | `"compactions"` appended to the pinned list | :17-21, append only |
|
||||
| seat/tests/compactions.test.mjs | two module imports; `collect` passes `{ name, compactions }` | :11/:21, helper only |
|
||||
| seat/tests/compactions-budget.test.mjs | pi import; `collect` passes `{ name, compactions }` | :13/:24-25, helper only |
|
||||
| cli/tests/compactions-launch.test.mjs | pi import; base :344's call (now :345) passes `{ name: "pi", compactions: pi }` | call only, `.count === 1` kept |
|
||||
|
||||
No call or expected value in those files changed. harness.test.mjs, cli helpers.mjs:133-151,
|
||||
launch-routes.test.mjs:17-23 and launcher.test.mjs:694-697 are unedited.
|
||||
|
||||
## New tests
|
||||
|
||||
- `packages/seat/tests/compactions-ledger.test.mjs`:
|
||||
- FIXTURES: seven ledgers row 270's code writes (known, store-incomplete, store-unreadable,
|
||||
not-collected, harness-unsupported from claude-code, harness-unsupported from Claude history, and
|
||||
torn), each with its streams, projects and steps. Per fixture, test A reads `compactionCount` and
|
||||
test B runs one collection through `{ name: "pi", compactions: pi.compactions }`.
|
||||
- Test B runs over the fixture's own streams, not an empty session as DESIGN.md:189 says. Over an
|
||||
empty session every fixture with a cursor would go store-incomplete (the stream went away), so
|
||||
"the others go known" could not hold. The expected answers: known stays known (1), store-unreadable
|
||||
goes known with 0 (the cursor is at the end), not-collected goes known with 1, and
|
||||
store-incomplete, torn and both harness-unsupported fixtures stay unknown.
|
||||
- The malformed-answer test (17 answers, plus a module that throws, over a fresh store and over the
|
||||
known fixture) and the argument test (exact keys; core passes a copy of the cursors).
|
||||
- `packages/plugins/tests/harness-compactions.test.mjs`: CASES plain, budget (100 bytes), cap (1
|
||||
stream of 2), shrank and header against pi; pi's `first` with and without Claude history;
|
||||
claude-code; and a module re-exporting every pi export but `compactions`, refused `missing-export`.
|
||||
- capture.mjs reads both capture blocks from the test files. It replays each fixture with base's
|
||||
`collectCompactions(store, "<name>", ...)` and compares the ledger byte for byte, and it runs base's
|
||||
`scanPi` per case and compares the answer under base's reason order (:301 at base) and
|
||||
`PI_VERSION`. It prints base's `compactionCount` per fixture: that is the before half.
|
||||
|
||||
## Expected red on 1f6db1589
|
||||
|
||||
- seat compactions-ledger: the seven test A pass (the ledger rules are base's). Test B fails for known,
|
||||
store-unreadable and not-collected: base takes the object as a harness that isn't pi and writes
|
||||
harness-unsupported. Test B passes on base for the other four, whose expected answer is unknown with
|
||||
the same reason. The malformed-answer and argument tests fail: base never calls the module.
|
||||
Correction after the run: test B fails on base for claude-history too, so the reds are known,
|
||||
store-unreadable, not-collected and claude-history. On base the object harness takes :304, which
|
||||
writes `collected` without a scan, so the count stays null. Pi on the tip scans on a later
|
||||
collection, finds c1 and answers 1, still unknown with harness-unsupported. The test is right and
|
||||
this list was wrong (Duckworth agreed, duckworth-0287).
|
||||
- plugins harness-compactions: the five CASES, the `first` test and the claude-code test fail with a
|
||||
TypeError (`compactions` isn't exported on base). The missing-export test fails: base's loader loads
|
||||
the module.
|
||||
- harness.test.mjs:192 is already red on base. That's the brief's starting point.
|
||||
|
||||
## Run plan (mos-1275, `timeout -k 10`)
|
||||
|
||||
All on a CLEAR from Sage. TMPDIR comes from `mktemp -d /mnt/storage/scratch/tmp/gXXX`. Gate worktrees
|
||||
under /mnt/storage/scratch, node_modules linked right after `git worktree add`, pipefail and tee.
|
||||
|
||||
| Run | Longest recorded | LIMIT |
|
||||
| --- | --- | --- |
|
||||
| capture.mjs on a 1f6db1589 worktree (base and candidate both it) | none | 120 s |
|
||||
| red first: the two new files on 1f6db1589, each whole | none (seat suite 7 s) | 300 s |
|
||||
| the touched test files whole on 44104adb1, one at a time | compactions-launch alone: none; other files 15 s | 600 s |
|
||||
| bus suite | 25 s (270 r5) | 900 s |
|
||||
| cli suite | 682 s (270 r5) | 2100 s |
|
||||
| plugins suite | none recorded | 900 s |
|
||||
| webui suite | 58.2 s (294) | 600 s |
|
||||
| business suite | none recorded | 900 s |
|
||||
| seat suite whole, `node --test tests/` (Duckworth C1) | 7 s (270 r5) | 600 s |
|
||||
| each scripts/test-*.sh | 42 s (test-foundation) | 900 s |
|
||||
| each mutant, its kill files whole | 1 s (264a probes) | 300 s |
|
||||
|
||||
- In the cli run, compactions-launch's T3 tests (base :206, and :305 with the :344 edit; :207, :306
|
||||
and :345 after the import line) must show as run, not skipped on `sandboxProblem()` (base :36, now
|
||||
:37). The report names every skip (Duckworth C2).
|
||||
- Plugins must have zero failures.
|
||||
- A timeout with no failed test is a hang and goes to Sage. A timeout in the shell phase goes to Mos
|
||||
unplaced. A first-run red in a new 307 test is placed and reported after (mos-1272). A red in an old
|
||||
test goes to Sage, unfixed.
|
||||
|
||||
## Mutants
|
||||
|
||||
Each on an isolated copy of the file, restore checked by sha256, only on a green base.
|
||||
|
||||
| # | Change | Kill files |
|
||||
| --- | --- | --- |
|
||||
| M1 | `readLedger`: `if (r.harness !== "pi" \|\| r.reason === "harness-unsupported")` restored | plugins harness.test.mjs (:192) |
|
||||
| M2 | pi `compactions` drops the `first && claudeHistory(store)` branch | seat compactions-budget.test.mjs (base :86, now :87), plugins harness-compactions.test.mjs |
|
||||
| M3 | `readLedger` never sets `unsupported` | seat compactions-ledger.test.mjs, seat compactions.test.mjs (base :197, now :199) |
|
||||
| M4 | `collectCompactions` skips `checkAnswer` | seat compactions-ledger.test.mjs |
|
||||
|
||||
## Runs
|
||||
|
||||
Sage's CLEARs: sage-3455 for steps 1-3, sage-3465 for steps 4-6. Gate worktrees at
|
||||
/mnt/storage/scratch/g307-base (1f6db1589), g307-tip and g307-mut (1f00d4f91), with node_modules
|
||||
linked. TMPDIR was /mnt/storage/scratch/tmp/gTIK. Outputs went to /mnt/storage/scratch/gizmo-307-out/s1 and s2.
|
||||
|
||||
### Steps 1-3 (13:31:21Z-13:31:36Z, then the capture rerun at 13:32Z)
|
||||
|
||||
- capture.mjs crashed with ENOENT on its first run. Base `sessionStore` (session.mjs:78
|
||||
`privateDirs`, called from :117) makes its directories one level at a time, and :44 passed a
|
||||
per-fixture root that didn't exist yet. The fix is one line before :44,
|
||||
`` mkdirSync(join(root, `f-${name}`)) ``. A copy with that line, run on g307-base as both base and
|
||||
candidate, exited rc 0 and printed `all match`: 7 ledgers byte for byte, 7 base counts and 5 scans.
|
||||
Base counts: known 1 known; store-incomplete, store-unreadable and not-collected null, unknown
|
||||
with their own reason; claude-code and claude-history null, harness-unsupported; torn 1,
|
||||
store-incomplete.
|
||||
- Red first on 1f6db1589, both rc 1. Seat compactions-ledger: 9 pass, 7 fail (the corrected list
|
||||
above, plus the malformed-answer and argument tests). Plugins harness-compactions: 0 pass, 8 fail.
|
||||
Seven fail with `TypeError: pi.compactions is not a function` or `claude.compactions is not a
|
||||
function`. The missing-export test fails because base loads the module.
|
||||
- Touched files whole on 1f00d4f91, all rc 0: compactions-ledger 16/16, harness-compactions 8/8,
|
||||
kinds 5/5, harness 9/9 (:192 green), compactions 20/20, compactions-budget 10/10,
|
||||
compactions-launch 5/5 with 0 skipped.
|
||||
|
||||
### Steps 4-6 (14:00:10Z-14:14:00Z, on 1f00d4f91)
|
||||
|
||||
| Run | rc | Time | Result |
|
||||
| --- | --- | --- | --- |
|
||||
| bus | 0 | 24 s | 890 pass, 0 fail, 0 skipped |
|
||||
| cli | 0 | 631 s | 927 pass, 0 fail, 0 skipped; T3 :207, :306, :345 ran |
|
||||
| plugins | 0 | 0 s | 119 pass, 0 fail, 0 skipped |
|
||||
| webui | 0 | 57 s | 374 pass, 0 fail, 1 skipped (below) |
|
||||
| business | 0 | 2 s | 231 pass, 0 fail, 0 skipped |
|
||||
| seat | 0 | 7 s | 218 pass, 0 fail, 0 skipped |
|
||||
| test-auth | 0 | 0 s | 15 passed, 0 failed |
|
||||
| test-conductor | 0 | 7 s | |
|
||||
| test-config | 0 | 3 s | |
|
||||
| test-discord | 0 | 7 s | |
|
||||
| test-extension-package | 0 | 2 s | |
|
||||
| test-foundation | 0 | 39 s | |
|
||||
| test-land-work | 0 | 12 s | |
|
||||
| test-queue | 0 | 20 s | one skip (below) |
|
||||
| test-release | 0 | 3 s | 15 passed, 0 failed |
|
||||
| test-task | 0 | 14 s | 92 passed, 0 failed, two skips (below) |
|
||||
|
||||
Skips:
|
||||
- webui: "rebuilding from the source gives the same bytes" needs the pinned esbuild. 307 doesn't
|
||||
touch the editor.
|
||||
- test-queue: "queue verify and render --check", because the gate worktree isn't the queue's
|
||||
canonical root.
|
||||
- test-task: "live provider cases" twice (row 244, no provider call in the gate).
|
||||
|
||||
Mutants ran in g307-mut. Each restore matched its sha256, and the tree was clean after M4.
|
||||
|
||||
| # | Killed by |
|
||||
| --- | --- |
|
||||
| M1 | plugins harness.test.mjs:192 |
|
||||
| M2 | seat compactions-budget.test.mjs:87 and :104; plugins harness-compactions.test.mjs:47 |
|
||||
| M3 | seat compactions-ledger.test.mjs:123 (test B, claude-code and claude-history); seat compactions.test.mjs:199 |
|
||||
| M4 | seat compactions-ledger.test.mjs:151 (the malformed-answer test) |
|
||||
|
||||
There were no reds, so nothing was placed under mos-1272. The sweep found no node or chromium left in
|
||||
my cwds.
|
||||
@@ -0,0 +1,241 @@
|
||||
# Row 307 (#1814, Vikunja 598): row 270's pi compaction reading moves behind the harness kind
|
||||
|
||||
Owner Gizmoduck. Reviewer Duckworth. Gate owner Sage. Brief:
|
||||
`docs/plans/2026-10-23_harness-kind-compactions.md` at 1e629c892, which is the base. Mos ruled the
|
||||
move in mos-1455 item 3 and declined an EXCEPT entry.
|
||||
|
||||
## What is red, as the code has it at 1e629c892
|
||||
|
||||
`packages/plugins/tests/harness.test.mjs:192` greps core sources for a quoted harness name. It finds
|
||||
three lines in `packages/seat/src/compactions.mjs`:
|
||||
|
||||
- `:232` in `readLedger`: `if (r.harness !== "pi" || r.reason === "harness-unsupported") unsupported = true;`
|
||||
- `:283` and `:287` in `collectCompactions`: the `harness === "pi"` branches.
|
||||
|
||||
Everything else row 270 put in that file is either harness-free (the ledger, the budget and stream
|
||||
settings, `storeDir`, `compactionCount`) or pi-only (`scanPi`, `PI_VERSION`, `claudeHistory`, and
|
||||
the helpers only `scanPi` uses: `readRange`, `digest`, `STREAM`, `TAIL`).
|
||||
|
||||
## The new export: `compactions`
|
||||
|
||||
`packages/plugins/src/kinds.mjs:54` appends `"compactions"` to the harness kind's exports, so the
|
||||
loader refuses a module without it (`loader.mjs:61-66`, code `missing-export`).
|
||||
|
||||
```
|
||||
compactions({ store, cursors, budget, streams, first })
|
||||
-> { found, cursors, known, reason, version }
|
||||
```
|
||||
|
||||
Arguments, all from core:
|
||||
|
||||
- `store` is `{ session, projects }`, the two native store directories `sessionStore` makes. The
|
||||
module never gets `store.dir`, the directory that holds the ledger.
|
||||
- `cursors` is a copy of the ledger's cursor map, `{ [stream]: { bytes, tail } }`.
|
||||
- `budget` and `streams` are `readBudget(...)` and `readStreams(...)`, as the launcher passes them
|
||||
now (`launcher.mjs:299`, `:305`). Core keeps both readers and their defaults.
|
||||
- `first` is true when the store has no ledger yet (today's `!prior`).
|
||||
|
||||
The answer:
|
||||
|
||||
- `found`: `[{ entry, completedAt, stream }]`, the compactions read past the cursors. `entry` is a
|
||||
non-empty string, `completedAt` a string or null, `stream` a non-empty string.
|
||||
- `cursors`: `{ [stream]: { bytes, tail } }` for each stream the module read. `bytes` is a safe
|
||||
integer of 0 or more and `tail` 64 lowercase hex.
|
||||
- `reason`: null, `store-incomplete`, `store-unreadable`, `not-collected` or `harness-unsupported`.
|
||||
- `known`: a boolean, true exactly when `reason` is null.
|
||||
- `version`: the store format the module read against as a string, or null when it read nothing.
|
||||
|
||||
Core checks the answer before it writes. Anything else throws `harness <name> compactions: bad
|
||||
answer: <what>` and nothing is appended. A throw from the module itself is the same case. Both reach
|
||||
the launcher's existing catch (`launcher.mjs:313-316`), which logs the message and records the run's
|
||||
`compactions` event as unknown with `store-unreadable`, as a scan that throws today does.
|
||||
|
||||
### interfaceVersion stays 1
|
||||
|
||||
- Every harness module lives in this tree, and the loader loads nothing else (decision 143,
|
||||
`loader.mjs:1-5`). Both shipped modules gain the export in the same commit.
|
||||
- A module without the export already refuses at load, as `missing-export` naming `compactions`.
|
||||
That is the brief's third test. A bump would add a second refusal for the same fact.
|
||||
- No existing export changes its arguments or answer. Row 125 moved limit-check to 2 because
|
||||
`mayLaunch`, `mustStop` and `report` changed shape. Nothing like that happens here.
|
||||
- `harness.test.mjs:36` pins `interfaceVersion` 1, and the brief keeps that file unchanged.
|
||||
|
||||
The tradeoff: a module someone wrote against version 1 without `compactions` reports
|
||||
`missing-export`, not `interface-version`. The message names the export, which is the more useful of
|
||||
the two.
|
||||
|
||||
## What moves where
|
||||
|
||||
New file `packages/plugins/harness/pi/compactions.mjs`, imported by `pi/index.mjs`. The loader
|
||||
checks only `index.mjs` itself (`loader.mjs:42-54`), and claude-code's module already imports files
|
||||
outside its directory (`claude-code/index.mjs:11-12`). It takes, unchanged in behaviour:
|
||||
|
||||
- `scanPi`, with `readRange`, `digest`, `STREAM`, `TAIL` and the chunk size it reads by;
|
||||
- `PI_VERSION` ("0.85.1");
|
||||
- `claudeHistory(store)`;
|
||||
- the reason order `["store-incomplete", "store-unreadable", "not-collected"]` from `:301`;
|
||||
- the header comment's Pi paragraphs (`compactions.mjs:12-19`, `:21-24`).
|
||||
|
||||
pi's `compactions`:
|
||||
|
||||
- `first` and `claudeHistory(store)`: answers `{ found: [], cursors: {}, known: false, reason:
|
||||
"harness-unsupported", version: "0.85.1" }`. That is today's `:283-286`.
|
||||
- otherwise: `scanPi(store.session, cursors, budget, streams)`, and answers its `found` and
|
||||
`cursors` with the first reason in the order above, `known` when that's null, and version
|
||||
"0.85.1". That is today's `:287-302` before the dedupe.
|
||||
|
||||
claude-code's `compactions` reads nothing and answers `{ found: [], cursors: {}, known: false,
|
||||
reason: "harness-unsupported", version: null }`. That is today's `:303-304`. Vikunja 466 still has
|
||||
no pinned Claude Code format.
|
||||
|
||||
## Core keeps the ledger
|
||||
|
||||
`packages/seat/src/compactions.mjs` keeps `LEDGER`, `READ_BYTES`, `STREAMS`, `readBudget`,
|
||||
`readStreams`, `storeDir`, `readLedger`, `compactionCount`, the key and the single O_APPEND write.
|
||||
|
||||
`collectCompactions(store, harness, { now, budget, streams })` takes `harness = { name,
|
||||
compactions }`, the name the route used and the loaded module's export. It:
|
||||
|
||||
1. reads the ledger (`prior`), as now;
|
||||
2. calls `harness.compactions({ store: { session: store.session, projects: store.projects },
|
||||
cursors: { ...ledger.cursors }, budget, streams, first: prior === null })` and checks the answer;
|
||||
3. appends, in today's order: one `compaction` line per key the ledger doesn't hold yet, one
|
||||
`cursor` line per stream whose cursor moved, then `{ v: 1, kind: "collected", at, known, reason,
|
||||
harness: name, version }`. These are the same lines and field order as today's `:288-302` and
|
||||
`:304`. `name` is a variable, never a literal;
|
||||
4. returns `compactionCount(store.dir)`.
|
||||
|
||||
`readLedger`'s unsupported test becomes `r.reason === "harness-unsupported"`, a field every answer
|
||||
carries. No core line names a harness. The core comments drop the quoted names too, because the
|
||||
grep reads comments.
|
||||
|
||||
`packages/cli/src/launcher.mjs:309-318`: the `compactions` closure takes the loaded module too and
|
||||
calls `collectCompactions(store, { name: model.harness, compactions: harness.compactions }, {
|
||||
budget, streams })`. Both call sites, `:509` and `:650`, have `harness` in scope (it's
|
||||
`startLocked`'s `let harness` from `:503`; the `exited.then` callback at `:632` closes over it).
|
||||
The closure calls the export directly, not through `call()` (`launcher.mjs:196`), because `call()`
|
||||
turns a throw into a run refusal. The closure gets a comment saying so (Duckworth N1).
|
||||
`bus/src/runtime.mjs:282` and `broker.mjs:885-889` keep calling `compactionCount` and don't change.
|
||||
|
||||
`packages/plugins/README.md`, Harness section, and the comment at `kinds.mjs:35-51` each gain the
|
||||
`compactions` line with the contract above.
|
||||
|
||||
## A ledger row 270 wrote reads the same count
|
||||
|
||||
Row 270's code writes a `collected` line in exactly three places:
|
||||
|
||||
| Line | harness | reason | version | old rule (`:232`) | new rule |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `:286`, Claude history on the first pi collection | `"pi"` | `harness-unsupported` | "0.85.1" | unsupported | unsupported |
|
||||
| `:302`, a pi scan | `"pi"` | null, `store-incomplete`, `store-unreadable` or `not-collected` | "0.85.1" | supported | supported |
|
||||
| `:304`, any other harness name (`claude-code`, or `claude` in the tests) | that name | `harness-unsupported` | null | unsupported | unsupported |
|
||||
|
||||
The two rules agree on every line row 270 can write. The rest of `readLedger` (the `bad`, `torn`,
|
||||
`incomplete` and `events` handling) and all of `compactionCount` don't change. So every ledger row
|
||||
270 wrote reads the same `{ count, known, reason, at }` after the move. Nothing rewrites a ledger;
|
||||
the next collection only appends.
|
||||
|
||||
The rules would differ only on a `collected` line naming a harness other than `"pi"` with a reason
|
||||
other than `harness-unsupported`. Row 270 never writes one, and the launcher is the ledger's only
|
||||
writer (`compactions.mjs:7-10`).
|
||||
|
||||
New collections after the move append the same bytes as before for the same store state. pi's
|
||||
answer is today's scan result, core keeps the dedupe and the line format, and `harness` in the line
|
||||
is still the route's name.
|
||||
|
||||
## Existing tests that change (to Mos through Sage, mos-1272)
|
||||
|
||||
Grepped for `interfaceVersion`, the harness exports, `write("harness"`, `fakeHarnesses` and
|
||||
`collectCompactions(` in `packages/*/tests/*` and `scripts/`.
|
||||
|
||||
Four existing files change:
|
||||
|
||||
1. `packages/plugins/tests/kinds.test.mjs:17-21`: the pinned harness export list gains
|
||||
`"compactions"` at the end.
|
||||
2. `packages/seat/tests/compactions.test.mjs:11` and `:21`: import the two shipped modules. `setup`'s
|
||||
`collect(harness = "pi")` passes `{ name: harness, compactions: MODULES[harness].compactions }`,
|
||||
with `pi` mapped to the pi module and `claude` to claude-code's. The 27 `f.collect()` calls and
|
||||
the two named ones at `:199` and `:201` stay as written.
|
||||
3. `packages/seat/tests/compactions-budget.test.mjs:13` and `:24-25`: the same change in its
|
||||
`collect`. Its calls at `:67-69` and `:129-134` stay as written.
|
||||
4. `packages/cli/tests/compactions-launch.test.mjs:344`: the one direct
|
||||
`collectCompactions(legacy, "pi")` becomes `collectCompactions(legacy, { name: "pi",
|
||||
compactions: pi.compactions })`, with an import of the pi module.
|
||||
|
||||
Fixtures that build a harness module and need no edit, because they re-export a real module and
|
||||
inherit `compactions`:
|
||||
|
||||
- `packages/cli/tests/helpers.mjs:133-151` `fakeHarnesses`, used by `compactions-launch`,
|
||||
`idle-stop-race`, `launcher`, `lifecycle`, `session-clear`, `session-launch`, `thread-launch` and
|
||||
`thread-revoke`;
|
||||
- `packages/cli/tests/launch-routes.test.mjs:17-23`: `down`, `sparse`, `metered` and `throws`
|
||||
re-export pi. `broken` lacks every export but `adapter` on purpose. The loader still refuses it at
|
||||
`levels` first, so `:114` and `:138` (`missing-export`) hold;
|
||||
- `packages/cli/tests/launcher.test.mjs:694-697`: `down` and `fixture` re-export pi.
|
||||
|
||||
`packages/plugins/tests/harness.test.mjs` doesn't change. `:33-37` loops over the kind's exports,
|
||||
which both modules then have, and `:192` goes green.
|
||||
|
||||
No `scripts/test-*.sh` builds a harness module. `test-config.sh:266-280` checks only the two
|
||||
config keys.
|
||||
|
||||
## New tests, red first (mos-1272)
|
||||
|
||||
1. `packages/seat/tests/compactions-ledger.test.mjs`: one fixture ledger per reason row 270 writes:
|
||||
known, `store-incomplete`, `store-unreadable`, `not-collected`, `harness-unsupported` from
|
||||
claude-code, `harness-unsupported` from Claude history, and a torn ledger.
|
||||
- Each is literal JSONL in the test. Before the build, in the slot, a capture script runs base
|
||||
1e629c892's `collectCompactions` with a fixed `now` over the same stores. The literals must
|
||||
equal its output byte for byte. That proves row 270's code wrote them.
|
||||
- `compactionCount` reads each one. I run this half on the base and on the candidate, and record
|
||||
both outputs in BUILD.md as the before and after.
|
||||
- Then one more collection through `{ name, compactions }` over an empty session gives each
|
||||
fixture's expected next answer: unsupported and `store-incomplete` stay, the others go known.
|
||||
This half is red on the base, where core treats the object as a harness that isn't pi.
|
||||
2. `packages/plugins/tests/harness-compactions.test.mjs`:
|
||||
- pi's `compactions` over fixture streams answers what base `scanPi` answers, captured by the same
|
||||
script: a plain count, a budget that stops short (cursor at the last whole line,
|
||||
`not-collected`), more streams than the cap (no cursor for unopened streams, `not-collected`),
|
||||
a stream that shrank under its cursor (`store-incomplete`), a non-session header
|
||||
(`store-unreadable`). Each answer has version "0.85.1".
|
||||
- pi with `first` and a non-empty `projects` answers `harness-unsupported` without scanning.
|
||||
Without `first`, the same store scans.
|
||||
- claude-code answers `harness-unsupported` with version null, over directories that don't exist.
|
||||
- A harness module that re-exports every other pi export by name, without `compactions`, refuses
|
||||
at load: `missing-export`, "doesn't export compactions".
|
||||
3. In the same seat file: core refuses each malformed answer and appends nothing. The cases are
|
||||
`found` not an array, an empty `entry`, a cursor whose `bytes` is negative or `tail` isn't 64
|
||||
hex, `known` true with a reason, an unknown reason, a numeric `version`, and a module that
|
||||
throws. The module gets exactly `{ store: { session, projects }, cursors, budget, streams,
|
||||
first }`.
|
||||
|
||||
## Mutants
|
||||
|
||||
- M1, the brief's first: core branches on a harness name again (`readLedger`'s `r.harness !== "pi"
|
||||
||` restored). Killed by `harness.test.mjs:192`.
|
||||
- M2, the brief's second: pi's module drops the Claude-history check. Killed by
|
||||
`compactions-budget.test.mjs:86` and new test 2's `first` case.
|
||||
- M3, the brief's third: `readLedger` never sets `unsupported`. Killed by new test 1's two
|
||||
`harness-unsupported` fixtures and `compactions.test.mjs:197`.
|
||||
- M4, mine: core skips the answer check. Killed by new test 3.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves this design, then the manifest. On Sage's CLEAR:
|
||||
|
||||
- the capture script on base 1e629c892 first, then the new tests red on the base;
|
||||
- the bus, cli, plugins (zero failures), webui and business suites, and every `scripts/test-*.sh`;
|
||||
- the seat suite whole (`packages/seat`, `node --test tests/`), where two old-test edits, new
|
||||
tests 1 and 3, and the M2 and M3 kills live (Duckworth C1);
|
||||
- in the cli run, `compactions-launch`'s T3 tests (`:206`, and `:305` with the `:344` edit) must
|
||||
run, not skip on `sandboxProblem()` (`:36`). BUILD.md names every skip (Duckworth C2);
|
||||
- the mutants, each on an isolated copy with its restore checked by sha256.
|
||||
|
||||
Each suite runs under `timeout -k 10` at 3x or more of its longest recorded wall (mos-1275). I'll
|
||||
put the numbers in BUILD.md before the first run. TMPDIR comes from
|
||||
`mktemp -d /mnt/storage/scratch/tmp/gXXX`.
|
||||
|
||||
## Round 1
|
||||
|
||||
Duckworth approved round 1 on 67daf6403 (duckworth-0280). This revision takes C1 and C2 into
|
||||
the gate, adds the N1 comment to the launcher plan, and corrects the N2 line refs (`:302`, `:301`).
|
||||
@@ -0,0 +1,79 @@
|
||||
// Row 307 (#1814): checks the new tests' literals against row 270's code at base 1e629c892.
|
||||
//
|
||||
// node capture.mjs <base checkout at 1e629c892> <candidate checkout>
|
||||
//
|
||||
// Reads the FIXTURES and CASES blocks (between `// capture:begin` and `// capture:end`) from the
|
||||
// candidate's two new test files. Replays each fixture's steps with the base's collectCompactions,
|
||||
// harness named by string as row 270 takes it, with the clock at 12:00:00 and one second a step,
|
||||
// and compares the ledger byte for byte. Prints the base's compactionCount for each. Runs the
|
||||
// base's scanPi over each case and compares the answer the base's reason order gives (:301) with
|
||||
// version PI_VERSION. Exit 0 only when every one matches. Writes only under TMPDIR.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { appendFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const [base, cand] = process.argv.slice(2).map((p) => p && resolve(p));
|
||||
if (!base || !cand) {
|
||||
console.error("usage: node capture.mjs <base checkout> <candidate checkout>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function block(file) {
|
||||
const text = readFileSync(join(cand, file), "utf8");
|
||||
const m = /\n\/\/ capture:begin\nconst [A-Z]+ = (\{[\s\S]*?\});\n\/\/ capture:end\n/.exec(text);
|
||||
if (!m) throw new Error(`${file}: no capture block`);
|
||||
return JSON.parse(m[1]);
|
||||
}
|
||||
|
||||
const FIXTURES = block("packages/seat/tests/compactions-ledger.test.mjs");
|
||||
const CASES = block("packages/plugins/tests/harness-compactions.test.mjs");
|
||||
const seat = await import(pathToFileURL(join(base, "packages/seat/src/compactions.mjs")).href);
|
||||
const { sessionStore } = await import(pathToFileURL(join(base, "packages/seat/src/session.mjs")).href);
|
||||
|
||||
const root = mkdtempSync(join(tmpdir(), "g307-capture-"));
|
||||
let failed = 0;
|
||||
const report = (what, same, detail) => {
|
||||
console.log(`${same ? "MATCH" : "DIFF "} ${what}${detail ? ` ${detail}` : ""}`);
|
||||
if (!same) failed++;
|
||||
};
|
||||
try {
|
||||
for (const [name, fx] of Object.entries(FIXTURES)) {
|
||||
mkdirSync(join(root, `f-${name}`));
|
||||
const store = sessionStore(join(root, `f-${name}`), "acme", "th1", "s1");
|
||||
for (const [file, text] of Object.entries(fx.streams)) writeFileSync(join(store.session, file), text);
|
||||
for (const [file, text] of Object.entries(fx.projects ?? {})) writeFileSync(join(store.projects, file), text);
|
||||
let tick = 0;
|
||||
const now = () => new Date(Date.UTC(2026, 9, 22, 12, 0, tick++)).toISOString();
|
||||
for (const step of fx.steps) {
|
||||
if (step.tear !== undefined) appendFileSync(join(store.dir, seat.LEDGER), step.tear);
|
||||
else seat.collectCompactions(store, step.collect, { now, ...(step.budget === undefined ? {} : { budget: step.budget }) });
|
||||
}
|
||||
const got = readFileSync(join(store.dir, seat.LEDGER), "utf8");
|
||||
const want = fx.ledger.join("\n") + "\n";
|
||||
report(`ledger ${name}`, got === want, got === want ? `(${Buffer.byteLength(got)} bytes)` : `\n--- base wrote\n${got}--- test has\n${want}`);
|
||||
const count = seat.compactionCount(store.dir);
|
||||
report(`base count ${name}`, JSON.stringify(count) === JSON.stringify(fx.count), JSON.stringify(count));
|
||||
}
|
||||
for (const [name, c] of Object.entries(CASES)) {
|
||||
const dir = join(root, `c-${name}`);
|
||||
mkdirSync(dir);
|
||||
for (const [file, text] of Object.entries(c.streams)) writeFileSync(join(dir, file), text);
|
||||
const scan = seat.scanPi(dir, c.cursors, c.budget, c.cap);
|
||||
const reason = ["store-incomplete", "store-unreadable", "not-collected"].find((p) => scan.problems.includes(p)) ?? null;
|
||||
const answer = { found: scan.found, cursors: scan.cursors, known: reason === null, reason, version: seat.PI_VERSION };
|
||||
let same = true;
|
||||
try {
|
||||
assert.deepStrictEqual(answer, c.answer);
|
||||
} catch {
|
||||
same = false;
|
||||
}
|
||||
report(`scan ${name}`, same, JSON.stringify(scan));
|
||||
}
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
console.log(failed ? `${failed} differ` : "all match");
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,9 @@
|
||||
# Gladstone
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos (mos-1531, mos-1532). Owns the Case 1/2 host fixes for the 433 P1 rerun,
|
||||
F1 to F6 (#1816, Vikunja 614).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 433058fc) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `gladstone/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,304 @@
|
||||
# Hostcheck item 5 and bundle verification (Gitea #1816, Vikunja 614)
|
||||
|
||||
Queue row pending from Sage. Owner Gladstone, reviewer Morgana, gate/landing Sage.
|
||||
Base: fetched origin/refactor 93694c5185d0a7f989d2fe78fb620e44599b744e;
|
||||
branch gladstone/hostcheck-item5. Scope: mos-1531 / ops-0256, sage-3738 and sage-3740.
|
||||
|
||||
## Before and after
|
||||
|
||||
Ubuntu 24.04 dpkg 1.22.6ubuntu6.6 prints `root mosaic 750 /usr/bin/bwrap`.
|
||||
Item 5 compared the literal `0750`, falsely refusing a correctly configured host;
|
||||
--enforce would unload the profile, close bwrap and record DRIFT.
|
||||
The candidate accepts exactly four fields, root, mosaic, octal 0750, and $BWRAP.
|
||||
`0*750` represents all octal spellings numerically equal to 0750 without arithmetic
|
||||
conversion/overflow. Horizontal whitespace separates fields; extra fields and
|
||||
embedded newlines refuse. Owner, group and path remain exact. The stat comparison
|
||||
and existing output strings remain unchanged. Only the item 5 comment also changes.
|
||||
|
||||
The two release-transport bundle checks previously ran outside any repository.
|
||||
Each now creates an empty bare scratch repository, verifies with git -C, and removes
|
||||
the scratch directory using an EXIT trap. A failure exits the shell before cloning
|
||||
or fetching. No other run-sheet text changes.
|
||||
|
||||
## Tests and mutants
|
||||
|
||||
Only packages/cli/tests/case1-hostcheck.test.mjs changes among tests, by appending
|
||||
three new test() blocks; the existing passing 0750 fake and failing fake are intact.
|
||||
The pass test exercises 750, 0750 and 000750. The enforce test checks no unload,
|
||||
chown, chmod, DRIFT or hostcheck log for 750. Refusals independently cover 755,
|
||||
0755, 4750, 75, 7500, 075, non-octal strings, other owner/group/path, missing path,
|
||||
extra fields, multiple lines, blank/empty output and a missing fixture file.
|
||||
|
||||
| Mutant | Change | Killer | Result |
|
||||
|---|---|---|---|
|
||||
| M1 | Restore the old literal root mosaic 0750 comparison (drop 750) | new check pass test | Unrun |
|
||||
| M2 | Accept any mode while preserving owner/group/path | new refusal test | Unrun |
|
||||
|
||||
Mutant procedure: preserve script bytes, assert one replacement, run the named
|
||||
killer alone, require failure for its intended assertion, restore in finally and
|
||||
check the original bytes/hash. Run the original targeted file again after restore.
|
||||
|
||||
## Verification status
|
||||
|
||||
Untested. Sage-3738 prohibits all Node until CLEAR because the joint full gate
|
||||
runs alone. Sage-3740 explicitly directs pushing this untested build for Morgana's
|
||||
static review before CLEAR. This supersedes test-first execution for this draft;
|
||||
no Node, tests, node --check, full suites or live-host operations have run.
|
||||
Queue next refused linked-worktree execution; Sage owns queue creation/movement.
|
||||
Push only gladstone/*, with MOSAIC_GITEA_CREDENTIAL_FILE unset; no amend/force.
|
||||
|
||||
Static draft receipt: `git diff --check` passed. Test-file numstat against the
|
||||
base is `46 0 packages/cli/tests/case1-hostcheck.test.mjs` (added/deleted).
|
||||
|
||||
## Scope and implementation amendment (mos-1534, sage-3741)
|
||||
|
||||
F1 correction: Sage requires precisely `750` or `0750` and exactly single spaces,
|
||||
so the candidate now uses `0?750` and literal spaces. It refuses 00750/000750,
|
||||
a doubled space and a tab. This supersedes the initial numeric-equivalence design
|
||||
above. Only this row's own appended tests change; no base test line changes.
|
||||
|
||||
F3: case2-unit.sh plain() adds @. abs_plain is its only caller and requires an
|
||||
absolute normalized path, so @ cannot become an ExecStart prefix. In sed @ is
|
||||
ordinary replacement text (the delimiter is |); &, backslash and | still refuse.
|
||||
Environment=MOSAIC_CLAUDE_BIN carries @ literally; the parser compares the complete
|
||||
value, with no shell evaluation. @anthropic-ai also cannot match the residual
|
||||
@[A-Z_]+@ token check. Tests render/reinstall with a regular executable under
|
||||
@anthropic-ai, checking Environment and the new copy's WorkingDirectory. Since a
|
||||
nonroot test cannot create root ownership, a stat fake models root:0755 only for
|
||||
that file and its ancestor chain. The real file type/executable checks, render,
|
||||
installed-unit parser, and all other stat calls remain active. A separate block
|
||||
refuses space, %, $, both quote characters, backslash, & and |.
|
||||
|
||||
F4: copy_tree changes to / before archive/npm/find, after all its path arguments
|
||||
are absolute. copy_bundle retains its existing relative-bundle resolution before
|
||||
calling copy_tree. Checkout copies have absolute REPO and COPIES too. Source-only
|
||||
use from stack-release.sh changes no cwd because copy_tree is not called there.
|
||||
Audit of every AS_USER command in stack-upgrade.sh: fetch and merge use git -C;
|
||||
plan, gate, revert, confirm-rollback, stage (activate/rollback), confirm and rehearse
|
||||
invoke stack-release.sh, which changes to its checkout at line 34. No other
|
||||
AS_USER command needs a fix. The new test enters a directory mode 0111, verifies
|
||||
that ls cannot read it, then runs both a relative-bundle copy and checkout copy,
|
||||
asserting success, no find restore error, and an immutable completed copy.
|
||||
|
||||
Additional mutants (unrun under the same no-Node hold):
|
||||
|
||||
| Mutant | Change | Killer | Result |
|
||||
|---|---|---|---|
|
||||
| M3 | Drop @ from plain() | new factory scoped Claude-path test | Unrun |
|
||||
| M4 | Remove copy_tree's cd / | new unreadable-cwd copy test | Unrun |
|
||||
|
||||
All four fixes remain untested. Follow-up is an additive commit on the pushed
|
||||
branch, per sage-3741, with no amend or force. Morgana's static F1/F2 read of
|
||||
f30c74b12 was CLEAN (morgana-0205); F3/F4 and execution remain open.
|
||||
|
||||
Morgana N1: the outer bundle-verification failure now prints
|
||||
`STOP: bundle verify failed; do not continue` instead of exiting the interactive
|
||||
root shell. The subshell still returns failure and cleans up. This follows the
|
||||
sheet's human read-and-stop convention and avoids sending remaining pasted lines
|
||||
to a parent shell after exiting sudo -i. Inner mktemp failure still exits only the
|
||||
subshell. This supersedes the initial outer-shell failure behavior noted above.
|
||||
|
||||
## File numstat against 93694c518 (added / deleted)
|
||||
|
||||
| File | Added | Deleted |
|
||||
|---|---:|---:|
|
||||
| `BUILD-LOG.md` | 4 | 0 |
|
||||
| `agents/gladstone/work/hostcheck-item5/BUILD.md` | 114 | 0 |
|
||||
| `docs/SESSIONS.md` | 1 | 0 |
|
||||
| `docs/guides/host-run-sheet.md` | 12 | 2 |
|
||||
| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 |
|
||||
| `packages/cli/tests/case2-unit.test.mjs` | 29 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 0 |
|
||||
| `scripts/case1-hostcheck.sh` | 2 | 2 |
|
||||
| `scripts/case2-unit.sh` | 1 | 1 |
|
||||
| `scripts/stack-copy.sh` | 3 | 0 |
|
||||
|
||||
## F5 (mos-1536 / ops-0260): explicit notifier decision
|
||||
|
||||
Before: no run-sheet step creates the required notifier decision, and precheck
|
||||
accepts a host whose bus will refuse missing notify.json. After: section 9a
|
||||
requires the mosaic user to create its business notify directory at 0700 and an
|
||||
explicit {"notifyVersion": 1, "binding": null} file at 0600 before section 10.
|
||||
The shell runs with cwd / and umask 077; noclobber refuses an existing decision.
|
||||
Named binding is explicitly a later per-host choice requiring Mos's go.
|
||||
case2-install.sh is unchanged and makes no silent default.
|
||||
|
||||
case2-precheck.sh's existing facts helper dynamically imports readNotifyConfig
|
||||
from cli.mjs and calls it directly. File type, uid, exact 0600 mode, JSON keys,
|
||||
version and binding-name validation therefore stay identical to the bus. The
|
||||
shell checks the business notify directory with existing private_dir (same uid,
|
||||
not a symlink, exactly 0700) and prints the new notifier config line. No notifier
|
||||
is started and no configuration is written. Tests append pass cases for null and
|
||||
a valid named binding, and refusal cases for missing file, 0644, 0755 directory,
|
||||
extra key, malformed JSON, invalid binding/version and a symlinked file.
|
||||
|
||||
M5 (unrun): remove the notifier shell check, including its refusal branch; the
|
||||
new notifier refusal test must fail. Also include Morgana's F3 negative mutant
|
||||
M3b (allow $ in plain), killed by the new unsafe-character test.
|
||||
|
||||
Existing positive precheck tests need an explicit notify fixture. Approval
|
||||
requested via Sage in gladstone-0006/-0007 for minimal setup-only additions to
|
||||
case2-unit.test.mjs host(t) and case2-install.test.mjs world(t); assertions remain
|
||||
unchanged. Those additions are not yet applied. Correction to gladstone-0007:
|
||||
case1-accept.test.mjs also names case2-precheck.sh, but uses a stand-in; it does
|
||||
not execute the changed script and needs no fixture amendment.
|
||||
|
||||
Execution remains held by Sage: no Node/tests run. Morgana-0207 static F1-F4
|
||||
verdict on a75532db1 is CLEAN; execution and F5 review are still open.
|
||||
|
||||
F5 acceptance from mos-1536 (for review):
|
||||
1. Before section 10, as mosaic, explicitly create <dataRoot>/notify/<business>
|
||||
at 0700 and notify.json at 0600 with default binding null; a later named
|
||||
binding needs Mos's go. Do not make case2-install.sh write it.
|
||||
2. Precheck must refuse missing file, non-0600 file, directory looser than 0700
|
||||
and a document outside the CLI's exact shape. Reuse/match cli.mjs validation,
|
||||
and name the new line in this run-sheet step's Precheck entry.
|
||||
3. Append-only test blocks cover pass, missing, 0644, 0755 dir, extra key; record
|
||||
a skip-check mutant, the touched test file and its numstat.
|
||||
4. F3/F4 precede F5, each follow-up commit stays on gladstone/hostcheck-item5;
|
||||
send pushes to Sage/Morgana with Mos cc. No Node until Sage CLEAR.
|
||||
|
||||
Static F5 draft receipt: git diff --check passed. Existing fixture approvals
|
||||
remain pending; F5's new pass test creates its own explicit decisions. No claim
|
||||
that the old positive precheck tests pass without the pending setup changes.
|
||||
|
||||
## F5 candidate file numstat against 93694c518 (added / deleted)
|
||||
|
||||
| File | Added | Deleted |
|
||||
|---|---:|---:|
|
||||
| `BUILD-LOG.md` | 6 | 0 |
|
||||
| `agents/gladstone/work/hostcheck-item5/BUILD.md` | 179 | 0 |
|
||||
| `docs/SESSIONS.md` | 1 | 0 |
|
||||
| `docs/guides/host-run-sheet.md` | 34 | 2 |
|
||||
| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 |
|
||||
| `packages/cli/tests/case2-unit.test.mjs` | 65 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 0 |
|
||||
| `scripts/case1-hostcheck.sh` | 2 | 2 |
|
||||
| `scripts/case2-precheck.sh` | 19 | 0 |
|
||||
| `scripts/case2-unit.sh` | 1 | 1 |
|
||||
| `scripts/stack-copy.sh` | 3 | 0 |
|
||||
|
||||
## Fixture approval and separate follow-up (mos-1537)
|
||||
|
||||
Mos approved exactly setup-only additions to case2-unit.test.mjs host(t) and
|
||||
case2-install.test.mjs world(t): each gets three added lines for a 0700
|
||||
notify/acme directory and 0600 notify.json with {"notifyVersion":1,"binding":null}.
|
||||
No existing line, fake or assertion changes or is deleted. F5's missing-file
|
||||
negative case removes the file after setup, so the new default cannot mask it.
|
||||
The newly appended install test checks preserved bytes and mode after success,
|
||||
and separately removes the decision before install and checks it stays absent.
|
||||
case1-accept.test.mjs is unchanged. This follow-up is committed separately on
|
||||
33950cf9b as directed, with no amend. No Node/tests run; suite claims remain open.
|
||||
Morgana-0211 found F5 source/run sheet CLEAN statically; the setup prerequisite B1
|
||||
is addressed by this approved fixture patch, pending execution.
|
||||
|
||||
Additional planned mutants from Morgana-0211 (unrun): M5b drops private_dir,
|
||||
killed by the directory fault; M5c prints NOTIFY ok unconditionally, killed by
|
||||
the document faults. Existing test preservation is checked by git diff showing
|
||||
only additions (numstat below).
|
||||
|
||||
Fixture follow-up numstat: case2-unit.test.mjs +3/-0,
|
||||
case2-install.test.mjs +18/-0. Against the original base: case1-hostcheck.test.mjs
|
||||
+48/-0, case2-unit.test.mjs +68/-0, stack-copy.test.mjs +16/-0,
|
||||
case2-install.test.mjs +18/-0. git diff --check passed.
|
||||
|
||||
## F6 and notify parent correction (mos-1537 / ops-0261)
|
||||
|
||||
Before: install creates a copy but no human CLI wrapper; the sheet's
|
||||
stack-copy.sh check --units external therefore fails even with correct units.
|
||||
After: the sheet explicitly places a wrapper step after successful install and
|
||||
before that check. On Mos's separate live-step go, mosaic changes cwd to /,
|
||||
creates $HOME/.npm-global/bin if absent, then invokes its clone's stack-copy.sh
|
||||
wrapper <sha> --create. Existing wrappers refuse and require review; install is
|
||||
unchanged. This is documentation of a gated host action, not execution of it.
|
||||
There are no tests pinning these sheet steps to adjust.
|
||||
|
||||
Section 9a now explicitly creates/chmods <dataRoot>/notify and then its business
|
||||
leaf, each 0700. This addresses ops-0261's observation that mkdir -p -m only
|
||||
controls the leaf. The existing umask 077 remains. All exits are inside the
|
||||
unprivileged shell, so no failure exits the operator's interactive root shell.
|
||||
|
||||
Static receipt: git diff --check passed. No Node, tests or live action. F6 and
|
||||
this parent-directory documentation correction are a separate commit after the
|
||||
approved fixture-only 5fd2328b6 follow-up. Reviewer should retain the earlier
|
||||
F1-F5 source verdicts and inspect this delta. Queue row still pending from Sage.
|
||||
|
||||
## F6 candidate file numstat against 93694c518 (added / deleted)
|
||||
|
||||
| File | Added | Deleted |
|
||||
|---|---:|---:|
|
||||
| `BUILD-LOG.md` | 10 | 0 |
|
||||
| `agents/gladstone/work/hostcheck-item5/BUILD.md` | 241 | 0 |
|
||||
| `docs/SESSIONS.md` | 1 | 0 |
|
||||
| `docs/guides/host-run-sheet.md` | 51 | 2 |
|
||||
| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 |
|
||||
| `packages/cli/tests/case2-install.test.mjs` | 18 | 0 |
|
||||
| `packages/cli/tests/case2-unit.test.mjs` | 68 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 16 | 0 |
|
||||
| `scripts/case1-hostcheck.sh` | 2 | 2 |
|
||||
| `scripts/case2-precheck.sh` | 19 | 0 |
|
||||
| `scripts/case2-unit.sh` | 1 | 1 |
|
||||
| `scripts/stack-copy.sh` | 3 | 0 |
|
||||
|
||||
## F6 gate citation (sage-3747)
|
||||
|
||||
Added scripts/stack-copy.sh:3 to the F6 wrapper step's explicit Mos-go requirement.
|
||||
This is doc-only; commands and tests are unchanged. Sage ruled the delete-after-
|
||||
setup missing-file negative meets mos-1537, so no fixture change is needed.
|
||||
Morgana-0213 found the fixture/F6 deltas CLEAN statically. No Node/tests run;
|
||||
Morgana's eventual run waits for Sage's row and CLEAR after the 220+216 landing.
|
||||
Numstat for this follow-up: host-run-sheet.md +1/-1; BUILD.md +9/-0.
|
||||
Static receipt: git diff --check passed.
|
||||
|
||||
Correction: 0f3d874d4 added 10 BUILD.md lines, not 9; the git numstat is authoritative.
|
||||
|
||||
## First review run and F4 test correction (morgana-0217)
|
||||
|
||||
Morgana ran af201b4a9 (6fa47e07d only relocates this file) under her authorized
|
||||
slot with timeout -k 10 and TMPDIR under /mnt/storage/scratch/tmp. Receipts:
|
||||
/mnt/storage/scratch/morgana-g320-out/base-summary.txt and mutants-summary.txt.
|
||||
|
||||
| Targeted file | Passed | Failed |
|
||||
|---|---:|---:|
|
||||
| case1-hostcheck.test.mjs | 19 | 0 |
|
||||
| case2-unit.test.mjs | 47 | 0 |
|
||||
| case2-install.test.mjs | 24 | 0 |
|
||||
| stack-copy.test.mjs | 17 | 1 |
|
||||
|
||||
Seven of eight mutants were killed: M1 literal0750, M2 anymode, M3a drop@,
|
||||
M3b allow$, M5 drop shell block, M5b drop private_dir, M5c always NOTIFY ok.
|
||||
M4 drop cd / SURVIVED: base and mutant both failed only setup's cleanup hook.
|
||||
This is a test defect, not a green acceptance result. The 0111 cwd retained
|
||||
search permission and did not reproduce the inaccessible inherited cwd. It
|
||||
also blocked setup's chmod -R cleanup because the directory remained unreadable.
|
||||
|
||||
B1 correction touches only this row's appended F4 test: the child shell enters
|
||||
an initially searchable directory, chmods it 000, checks both ! -r and ! -x, then
|
||||
execs stack-copy. The parent asserts all read/search bits remain cleared. The
|
||||
bundle now lives outside cwd and is passed absolutely, matching the real flow;
|
||||
existing row232 coverage still tests relative bundles from an accessible cwd.
|
||||
Both bundle and checkout paths remain covered.
|
||||
B2 correction: each child run/assertion sequence has a finally that restores the
|
||||
cwd to0700 before setup's existing cleanup. No old setup/test lines change.
|
||||
Production scripts are unchanged; the cd / fix remains correct by static review.
|
||||
|
||||
Correction is untested locally: no builder Node/test CLEAR was given. Morgana
|
||||
will rerun stack-copy whole, M4 and residue cleanup on the pushed correction;
|
||||
other files need no rerun unless changed. git diff --check passed.
|
||||
|
||||
## F4 corrected candidate numstat against 93694c518 (added / deleted)
|
||||
|
||||
| File | Added | Deleted |
|
||||
|---|---:|---:|
|
||||
| `BUILD-LOG.md` | 12 | 0 |
|
||||
| `agents/gladstone/work/queue-320/BUILD.md` | 304 | 0 |
|
||||
| `docs/SESSIONS.md` | 1 | 0 |
|
||||
| `docs/guides/host-run-sheet.md` | 51 | 2 |
|
||||
| `packages/cli/tests/case1-hostcheck.test.mjs` | 48 | 0 |
|
||||
| `packages/cli/tests/case2-install.test.mjs` | 18 | 0 |
|
||||
| `packages/cli/tests/case2-unit.test.mjs` | 68 | 0 |
|
||||
| `packages/cli/tests/stack-copy.test.mjs` | 22 | 0 |
|
||||
| `scripts/case1-hostcheck.sh` | 2 | 2 |
|
||||
| `scripts/case2-precheck.sh` | 19 | 0 |
|
||||
| `scripts/case2-unit.sh` | 1 | 1 |
|
||||
| `scripts/stack-copy.sh` | 3 | 0 |
|
||||
@@ -0,0 +1,9 @@
|
||||
# Goldie
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-1542 and mos-1546. Owns F8 and F9, the library persona
|
||||
import in `scripts/case2-install.sh` (#1818), for the 433 Run B rerun.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no credentials of its
|
||||
own; it pushes only its own `goldie/*` branches through the repository credential helper (mos-0981), never `refactor`,
|
||||
`main` or another seat's branch. Claude seats orchestrate and review the install path (mos-1542 rule 4). Registered by
|
||||
Sage on 2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,60 @@
|
||||
# Optional business tracker (Q247) implementation plan — row 324 / #1820
|
||||
|
||||
Goldie, 2026-10-23. Base 860afc2b66e7bbd045e9d5e25d227ef12f62c55b, approved row298 candidate. New worktree /home/jwoltje/.t3/worktrees/mosaic-stack/goldie-row324, branch goldie/row324. Spec: git object f375f4e92:docs/plans/2026-10-23_optional-tracker-brief.md, section Brief: tracker becomes optional in the business file. Sage-3836 and mos-1555/Q247 authorize optional tracker; Duckworth reviews credential registration.
|
||||
|
||||
## Goal and design
|
||||
|
||||
A raw business document may omit tracker. Normalize omission to tracker:null, consistent with existing optional launch/role-tracker representations. A present tracker (including null, false, malformed objects) must continue through checkTracker and its existing refusals; distinguish absence with Object.hasOwn rather than a truthiness check. Keep checkTracker unchanged. Required top-level keys no longer include tracker. Only compare role bots with the sync bot when a checked business tracker exists. Role definitions needing vikunja still require their own tracker and credentials unchanged.
|
||||
|
||||
CLI validate collects sync credential refs only when tracker is present. bootConfig's trackerFor returns null before interpreting tracker variables when business tracker is absent, so an old tracker.baseUrl variable cannot accidentally create sync/task adapter configuration. Console businessView emits no sync credential rows for absent tracker. Runtime registration, fixed-configuration reload comparison, instance-settings bot collision checks and broker credential display already use optional chaining on the row298 base; preserve those checks. The tasks adapter remains fail-closed for an explicitly inconsistent raw adapter config, and no-tracker bootConfig never configures it for this business.
|
||||
|
||||
## Reader audit (before implementation)
|
||||
|
||||
Command: git grep -n -E 'tracker\.sync|\.tracker\b' -- 'packages/*/src/*'. Every result is grouped here, with base line numbers:
|
||||
|
||||
| Reader | Absent business tracker behavior / action |
|
||||
| --- | --- |
|
||||
| business/src/business.mjs:85-96,296-300 | checkTracker remains exact for present input; add omission normalization and guard own-sync-bot comparison. |
|
||||
| business/src/business.mjs:143-150 | Per-role tracker, independent of business tracker. Keep requirement/collision checks unchanged. |
|
||||
| business/src/cli.mjs:85 | Guard Object.values sync credential refs; absent contributes zero. |
|
||||
| business/src/instance-settings.mjs:108,120,126-127,133,223 | Role add/change and bot collision/retirement: old.tracker?.sync already skips absent sync; role trackers unchanged. No edit needed. |
|
||||
| business/src/launch-settings.mjs:161,235-236 | Per-role tracker assistant view/update, independent of business tracker; no edit. |
|
||||
| business/src/resolve.mjs:93 | Per-role tracker in resolved output; no edit. |
|
||||
| bus/src/runtime.mjs:131 | Existing b.tracker?.sync?.credentials check registers no @sync when absent; preserve and exercise with new boot test. |
|
||||
| bus/src/runtime.mjs:243,247 | Reload compares per-role tracker/null and canonical business sync/null; safe already, no edit. |
|
||||
| bus/src/broker.mjs:3969-3970 | Optional per-role tracker bot map; independent, no edit. |
|
||||
| bus/src/broker.mjs:4497 | Optional per-role tracker returned from instance write; independent, no edit. |
|
||||
| bus/src/broker.mjs:6242 | Credential display resolves optional sync bot; no @sync row exists on trackerless boot. Safe already. |
|
||||
| cli/src/config.mjs:57-60,137-138 (trackerFor; not caught by literal grep) | Add business-tracker absence guard; do not configure tracker adapter even if tracker.baseUrl remains set. |
|
||||
| cli/src/instances.mjs:118 | Builds per-role tracker update request only when user supplies bot; unrelated to business tracker. |
|
||||
| tasks/src/adapter.mjs:25,28-30,35,45,81 | settings rejects missing sync with controlled tracker-config before dereferencing labels/botId; files uses optional chaining. Factory iterates only configured trackers; absent business never configured by bootConfig. Preserve explicit-inconsistent-config refusal. |
|
||||
| webui/src/reads.mjs:164,166 | Role credentials independent; guard sync credential display at166 so trackerless Console does not throw. |
|
||||
| webui/src/public/bus.js:785 | Per-role editor tracker patch; independent of business tracker. |
|
||||
| conversation/src/controller.mjs:443,502,748,807,820,831,888,935,961,997,1008,1021,1048,1056,1268,1308,1330,1334,1413,1447 | Different execution-tracking object/class, unrelated to business/Vikunja config. No change. |
|
||||
|
||||
Positive control: grep finds business.mjs:296 and webui reads:166, the unsafe business-tracker readers. Review focus: present null/malformed tracker still refuses; per-role Vikunja trackers still required; stale tracker variables do not enable sync; Console credential view remains usable; no @sync registration during runtime boot/reload.
|
||||
|
||||
## Steps and evidence plan
|
||||
|
||||
1. Document design and full reader list (this file); claim row324 in-progress as Goldie.
|
||||
2. Add new tests for absent tracker loader+CLI validate, malformed/present preservation, per-role tracker requirement, stale-variable bootConfig behavior, actual bus boot with empty credential list/no @sync, and Console business view. New test files only unless Mos expressly approves old-test change below.
|
||||
3. Make the minimal four-source-file change (business loader, business CLI, cli config, webui reads). Existing runtime/instance-settings/broker optional checks require coverage, not gratuitous edits.
|
||||
4. Static diff checks. Push untested candidate only as expressly ordered by sage-3836, then ask Sage for CLEAR. No tests before CLEAR.
|
||||
5. On CLEAR, new test files whole, applicable existing whole files/suites, then a mutant removing loader presence check. Restore and check source bytes. Record outputs; full landing gate is Sage's. Duckworth review follows.
|
||||
|
||||
## Pre-existing test boundary
|
||||
|
||||
Inspection finds packages/business/tests/business.test.mjs:98 loops over [human,arbiters,projects,tracker,roles] and :99 asserts deletion of each refuses. Q247 directly invalidates the tracker iteration. Proposed sole old-test edit: remove just the tracker string from that required-key list, leaving all other iterations/assertions intact, and add explicit tracker-omission coverage in a new test file. This requires Mos OK under mos-1272; stop implementation and report before editing existing tests. No tests have run.
|
||||
|
||||
|
||||
## Implementation draft and old-test ruling
|
||||
|
||||
Sage-3840 relays Mos mos-1556: mos-1272 OK to remove only tracker from business.test.mjs's required-key list, preserving the four other keys and the assertion. Done exactly, one addition/one deletion at original line98; no other old test edited. All positive/negative coverage is in two new files: business/tests/optional-tracker.test.mjs and cli/tests/optional-tracker.test.mjs. The latter covers Console businessView as required by sage-3840.
|
||||
|
||||
Design now implemented in the four source files identified above; README explains omission versus literal null. New tests cover loader/CLI absence, present value preservation, malformed/present undefined or null refusal, unchanged role-bot requirement, stale business/project tracker variables, actual runtime startup with no credential files or @sync reference, Console view, and continued missing-token refusal for a present tracker. Runtime test uses useFile on unknown @sync to distinguish an unregistered reference from a registered but unavailable credential; callback must never run. Fixture project tracker.project is set, so removing trackerFor's presence guard would actually configure sync instead of passing accidentally through the no-project guard.
|
||||
|
||||
No tests run. New node_modules symlink points to canonical checkout dependencies and is ignored; it will not be committed. Sage-3836 expressly requests push before any tests; that specific sequencing overrides the usual green-before-commit default only for this draft push. Status is untested until CLEAR. Claim wrote goldie-0324-start-1 revision3759, briefed→in-progress. Row323 remains paused at the product boundary until324 lands.
|
||||
|
||||
Static verification: node --check passed for all four edited source modules and both new test files; git diff --check passed. No node --test or shell suite was run. reader-audit.txt captures the requested grep on the draft. MANIFEST.sha256 pins README, four source files, the one existing test and both new tests. mutant.py prepares the required omission-presence mutant and restores/checks bytes in finally; it has not run.
|
||||
|
||||
Proposed CLEAR run order: new business optional-tracker.test.mjs whole (5 tests), new cli optional-tracker.test.mjs whole (4 tests), existing business.test.mjs whole, then the targeted presence mutant and restored new tests. Broaden to business/CLI/Console/bus package gates only when Sage assigns that scope/slot. All test runs use timeout -k 10 and an explicitly allocated TMPDIR under /mnt/storage/scratch/tmp.
|
||||
@@ -0,0 +1,8 @@
|
||||
ecd63bcbeae04236c445e74493d425e4a5d1bfb723d72fb58446e1d020e4d429 packages/business/README.md
|
||||
e76db42158f396ec6a44f97320355d6c2254dccb5fc53cd04e8ac42843b0d60a packages/business/src/business.mjs
|
||||
afef5c581beb26d0b0797faeb2acd3597597d48c0ce03cf31cce8d9e9b2690b9 packages/business/src/cli.mjs
|
||||
2ec0c80361f12c0d90669a3c149dfc5c9607fc9dbad98ccd30d24cf14c0de9b6 packages/cli/src/config.mjs
|
||||
46defeb3b4be0e1a22e4b89445872ad6b15cdee182076e9e6222219ae29997f4 packages/webui/src/reads.mjs
|
||||
3f99c2d7e41b37f82c3f9edeac3bc795f982dac9a3728f7694cfd5668e944691 packages/business/tests/business.test.mjs
|
||||
e92a976bcd6bf97c1cf7832d9e3fe4dec350d92809f26f7b43b938bad7e5dc49 packages/business/tests/optional-tracker.test.mjs
|
||||
7723e299fb9a11cf313d3b26df8c0270b15982731dfc0af2463c96f23e1582a2 packages/cli/tests/optional-tracker.test.mjs
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Q247 targeted presence-check mutant; run only in an approved test slot."""
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
root = Path(__file__).resolve().parents[4]
|
||||
source = root / 'packages/business/src/business.mjs'
|
||||
fixed = source.read_bytes()
|
||||
needle = b'Object.hasOwn(document, "tracker") ? checkTracker(document.tracker, file) : null'
|
||||
assert fixed.count(needle) == 1
|
||||
mutant = fixed.replace(needle, b'checkTracker(document.tracker, file)')
|
||||
try:
|
||||
source.write_bytes(mutant)
|
||||
result = subprocess.run([
|
||||
'timeout', '-k', '10', '90', 'node', '--test', '--test-reporter=tap',
|
||||
'--test-name-pattern=Q247: omitting tracker',
|
||||
'packages/business/tests/optional-tracker.test.mjs',
|
||||
], cwd=root, env=os.environ, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
Path(__file__).with_suffix('.log').write_text(result.stdout)
|
||||
print(f'presence mutant: exit {result.returncode}', flush=True)
|
||||
assert result.returncode == 1, result.stdout
|
||||
assert '# fail 1\n' in result.stdout and 'tracker' in result.stdout, result.stdout
|
||||
finally:
|
||||
source.write_bytes(fixed)
|
||||
assert source.read_bytes() == fixed
|
||||
print('restored sha256 ' + hashlib.sha256(fixed).hexdigest(), flush=True)
|
||||
@@ -0,0 +1,62 @@
|
||||
packages/bus/src/broker.mjs:3969: .filter(([, r]) => r.tracker?.botId !== undefined)
|
||||
packages/bus/src/broker.mjs:3970: .map(([role, r]) => [r.tracker.botId, role]),
|
||||
packages/bus/src/broker.mjs:4497: return { transaction: out.transaction, retry: false, ...(out.tracker ? { bot: out.tracker.bot } : {}) };
|
||||
packages/bus/src/broker.mjs:6242: bot = sync ? b.tracker?.sync?.bot : service === 'vikunja' ? b.roles?.[role]?.tracker?.bot : null;
|
||||
packages/bus/src/runtime.mjs:131: if (b.tracker?.sync?.credentials) references[business + '/@sync'] = b.tracker.sync.credentials;
|
||||
packages/bus/src/runtime.mjs:243: tracker: r?.tracker ?? null,
|
||||
packages/bus/src/runtime.mjs:247: sync: canonicalJson(b.tracker?.sync ?? null),
|
||||
packages/business/src/business.mjs:89: requireObject(tracker.sync, `${where}.sync`);
|
||||
packages/business/src/business.mjs:90: rejectUnknownKeys(tracker.sync, ["bot", "botId", "credentials"], `${where}.sync`);
|
||||
packages/business/src/business.mjs:92: ...checkBot({ bot: tracker.sync.bot, botId: tracker.sync.botId }, `${where}.sync`),
|
||||
packages/business/src/business.mjs:93: credentials: checkCredentialMap(tracker.sync.credentials, ["vikunja"], `${where}.sync.credentials`),
|
||||
packages/business/src/business.mjs:143: if (entry.tracker === undefined) refuse(`${at} needs "tracker" with its Vikunja bot, because ${entry.definition} uses vikunja`);
|
||||
packages/business/src/business.mjs:144: tracker = checkBot(entry.tracker, `${at}.tracker`);
|
||||
packages/business/src/business.mjs:145: if (bots.has(tracker.bot)) refuse(`${at}.tracker.bot ${tracker.bot} is used by another instance`);
|
||||
packages/business/src/business.mjs:146: if (botIds.has(tracker.botId)) refuse(`${at}.tracker.botId ${tracker.botId} is used by another instance`);
|
||||
packages/business/src/business.mjs:149: } else if (entry.tracker !== undefined) {
|
||||
packages/business/src/business.mjs:296: const tracker = Object.hasOwn(document, "tracker") ? checkTracker(document.tracker, file) : null;
|
||||
packages/business/src/business.mjs:298: if (tracker && role.tracker && (role.tracker.bot === tracker.sync.bot || role.tracker.botId === tracker.sync.botId)) {
|
||||
packages/business/src/business.mjs:299: refuse(`${file} tracker.sync must use its own bot, not the one roles.${instance} uses`);
|
||||
packages/business/src/cli.mjs:85: ...Object.values(business.tracker?.sync?.credentials ?? {}),
|
||||
packages/business/src/instance-settings.mjs:108:// needs a bus restart, then row 199's issue. A vikunja definition needs args.tracker, an existing
|
||||
packages/business/src/instance-settings.mjs:120: tracker = checkBot(args.tracker, "tracker");
|
||||
packages/business/src/instance-settings.mjs:126: const taken = Object.entries(old.roles).filter(([name, entry]) => name !== instance && entry.tracker).map(([name, entry]) => [name, entry.tracker]);
|
||||
packages/business/src/instance-settings.mjs:127: if (old.tracker?.sync) taken.push(["tracker.sync", old.tracker.sync]);
|
||||
packages/business/src/instance-settings.mjs:133: out.tracker = tracker;
|
||||
packages/business/src/instance-settings.mjs:223: const bot = old.roles[instance]?.tracker?.bot;
|
||||
packages/business/src/launch-settings.mjs:161: definition: entry.definition, vars: entry.vars ?? {}, tracker: entry.tracker ?? null,
|
||||
packages/business/src/launch-settings.mjs:235: if (entry.tracker === null) delete next.roles[instance].tracker;
|
||||
packages/business/src/launch-settings.mjs:236: else if (Object.hasOwn(entry, "tracker")) next.roles[instance].tracker = entry.tracker;
|
||||
packages/business/src/resolve.mjs:93: tracker: entry.tracker,
|
||||
packages/cli/src/config.mjs:58: if (!b.tracker) return null;
|
||||
packages/cli/src/instances.mjs:118: if (bot) args.tracker = { bot: values["--bot"], botId: Number(values["--bot-id"]) };
|
||||
packages/conversation/src/controller.mjs:443: exec.tracker = new Tracker({
|
||||
packages/conversation/src/controller.mjs:502: if (exec.link.poisoned || exec.tracker.overlapped || this.closers.has("uncertain")) return;
|
||||
packages/conversation/src/controller.mjs:748: return { fence: true, closer, fenceIdx: this.exec?.tracker.idx ?? 0 };
|
||||
packages/conversation/src/controller.mjs:807: seam: { replay: "unavailable", streamEpoch: exec?.streamEpoch ?? null, fromSequence: (exec?.seq ?? 0) + 1, reconcile: true, quiet: !exec?.tracker.current && !exec?.tracker.slot },
|
||||
packages/conversation/src/controller.mjs:820: if (exec?.tracker.slot) return refused(BUSY);
|
||||
packages/conversation/src/controller.mjs:831: exec.tracker.attach(slot);
|
||||
packages/conversation/src/controller.mjs:888: const tr = slot.exec.tracker;
|
||||
packages/conversation/src/controller.mjs:935: const tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:961: const exec = slot.exec, tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:997: this.#settleFailure(exec.tracker.slot, RUN_OVERLAP);
|
||||
packages/conversation/src/controller.mjs:1008: this.evidence.gaps.push({ kind, detail, idx: exec.tracker.idx });
|
||||
packages/conversation/src/controller.mjs:1021: const tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1048: const tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1056: const tr = exec.tracker, s = tr.slot;
|
||||
packages/conversation/src/controller.mjs:1268: const exec = this.exec, tr = exec?.tracker;
|
||||
packages/conversation/src/controller.mjs:1308: const tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1330: exec.tracker.clearWindow = Math.max(0, exec.tracker.clearWindow - 1);
|
||||
packages/conversation/src/controller.mjs:1334: const { exec, stop } = ctx, tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1413: const { exec } = ctx, tr = exec.tracker;
|
||||
packages/conversation/src/controller.mjs:1447: const { exec, stop } = ctx, tr = exec.tracker;
|
||||
packages/tasks/src/adapter.mjs:25: if (!positive(b.tracker?.sync?.botId)) throw new BusError('tracker-config');
|
||||
packages/tasks/src/adapter.mjs:28: if (r.tracker) {
|
||||
packages/tasks/src/adapter.mjs:29: if (!positive(r.tracker.botId) || typeof r.definition !== 'string') throw new BusError('tracker-config');
|
||||
packages/tasks/src/adapter.mjs:30: roles[role] = { definition: r.definition, botId: r.tracker.botId };
|
||||
packages/tasks/src/adapter.mjs:35: for (const [title, id] of Object.entries(b.tracker.labels ?? {})) {
|
||||
packages/tasks/src/adapter.mjs:45: if (b.tracker?.sync?.credentials?.vikunja?.file) out['@sync'] = b.tracker.sync.credentials.vikunja.file;
|
||||
packages/tasks/src/adapter.mjs:81: bots: new Set([businesses[name].tracker.sync.botId, ...Object.values(s.roles).map((r) => r.botId)]),
|
||||
packages/webui/src/public/bus.js:785: if(f==='tracker'&&text.trim()===''){patch.tracker=null;continue;}
|
||||
packages/webui/src/reads.mjs:164: for (const [service, ref] of Object.entries(r.credentials)) credentials.push(credential(service, ref, service === 'vikunja' ? r.tracker?.bot ?? null : null, instance, now));
|
||||
packages/webui/src/reads.mjs:166: for (const [service, ref] of Object.entries(b.tracker?.sync?.credentials ?? {})) credentials.push(credential(service, ref, b.tracker.sync.bot, 'tracker sync', now));
|
||||
@@ -0,0 +1,331 @@
|
||||
# Row 226 build notes, C2-13b install script and library precheck (#1727)
|
||||
|
||||
Brief: docs/plans/2026-10-20_case2-rows.md "C2-13b install script and library precheck" (:1073-1134 at
|
||||
d37210288). Design: agents/gosalyn/work/case2/DESIGN.md r11 sections 4 and 10, on gosalyn/case2-design
|
||||
75fb3e10e. Row 195's needs: agents/darkwing/work/queue-195/DESIGN.md "What row 195 needs from row 226".
|
||||
|
||||
This is design round d2. Nothing is built yet. d1 was f46e66883, and Webby's review of it is webby-0085
|
||||
(CHANGES, B1 and N1-N6) and webby-0086 (the ordering problem). Sage ruled on both in sage-2189. The
|
||||
changes from d1 are listed at the end.
|
||||
|
||||
## Base
|
||||
|
||||
Branch gosalyn/c2-13b from refactor d37210288. Rows 215, 221, 222 and 230 are done.
|
||||
|
||||
## Facts the design rests on
|
||||
|
||||
- No CLI writes a business file. `mosaic business validate <id>` checks one and exits 0 ok, 2 invalid
|
||||
(packages/business/src/cli.mjs). A business file needs `human`, `arbiters`, `projects`, `tracker`
|
||||
with a sync bot and a Vikunja credential ref, and `roles`, each role with its tracker bot when its
|
||||
definition uses Vikunja (business.mjs:23, :84-100, :131-149).
|
||||
- The library is a project, `projects.<id>.kind === "library"`, checked by `checkMapping`
|
||||
(packages/library/src/mapping.mjs). There is no top-level `library` key.
|
||||
- `library.sync` and `library.status` are bus verbs, human only (broker.mjs:92, :2452-2476). The only
|
||||
client is `packages/bus/src/human-cli.mjs` over the bus socket.
|
||||
- The bus makes each library mirror at start, `ensureMirror({ business, project, dataRoot })`
|
||||
(packages/bus/src/runtime.mjs:143-145). Before the bus first starts there is no mirror.
|
||||
- `sync()` fetches outside `withLibraryLock`. Only a per-process `running` set keeps two syncs apart
|
||||
(packages/library/src/sync.mjs:97-98), so a second process calling `sync()` would race the bus's
|
||||
timer.
|
||||
- `libraryStatus(context)` reads the recorded state under the flock and writes nothing
|
||||
(mirror.mjs:232-239). A missing or unsafe mirror fails `library-git` (`ownedRoot`, mirror.mjs:43-49).
|
||||
A mirror that has never synced returns the empty state, `lastFetch: null`, `lastError: null`,
|
||||
`problems: []` (mirror.mjs:27, :205-210). `lastError` is `{ code, at }` with `code` matching
|
||||
`^[a-z-]{1,64}$` (mirror.mjs:225).
|
||||
- The bus builds the library context as `{ business, project, dataRoot }` and the slot as
|
||||
`<slotDir>/secrets/<business.id>/library-gitea` (runtime.mjs:146-151, :162-164).
|
||||
- `case2-persona-import.mjs install --staging DIR --config-dir DIR --business ID` writes
|
||||
`<configDir>/personas/<business>/<instance>.md` at 0600 in 0700 dirs, never overwriting, and prints
|
||||
`instance bean: persona.name Bean, persona.sha256 <hex>` per persona.
|
||||
- A Discord binding is `<dataRoot>/discord/<name>.json` (packages/discord/src/binding.mjs:70-83).
|
||||
- `case2-unit.sh --copy ID` needs `<home>/.local/share/mosaic-stack/copies/<ID>`, which
|
||||
`stack-copy.sh copy COMMIT --copies DIR` makes. The copy runs `npm ci` from package-lock.json
|
||||
(stack-copy.sh:70), which installs the pinned pi.
|
||||
|
||||
## Design
|
||||
|
||||
### D1. The business file is an input
|
||||
|
||||
Unchanged from d1. `case2-install.sh` takes `--business-file FILE`, a JSON business document written for
|
||||
the host from Jason's go. The script doesn't build one from flags, because bots, botIds and credential
|
||||
refs are host values and the brief says every host value is an input. It checks the input, adds what
|
||||
only it knows, writes it, and then runs `mosaic business validate`, so business.mjs stays the only
|
||||
schema.
|
||||
|
||||
What the script checks before it writes anything, beyond `validate`:
|
||||
- `id` equals `--business`.
|
||||
- `library`: at least one library project, `bus.tester` not true, roles `bean` and `chet` with
|
||||
definition `companion`, and no `persona.*` var in the input, since D3 adds them.
|
||||
- `factory`: no library project, and `bus.tester` true. These are the precheck's rules at
|
||||
case2-precheck.sh:127-135, so the install refuses what the precheck would refuse.
|
||||
|
||||
### D2. Install steps, in order
|
||||
|
||||
1. Refuse to run as root.
|
||||
2. `scripts/bootstrap.sh`. It's idempotent and never rewrites an existing config.
|
||||
3. `scripts/stack-copy.sh copy <--commit> --copies <home>/.local/share/mosaic-stack/copies`. The copy
|
||||
`case2-unit.sh --copy` names has to exist before root renders the units (row 195 need 3).
|
||||
4. `library` only: the persona import's `install` step from `--personas DIR` (D3).
|
||||
5. Write `<configDir>/businesses/<id>.json` at 0600 with O_EXCL (D4).
|
||||
6. `library` only: make `<configDir>/secrets/<business>/` at 0700, empty. The token is Jason's, design 10
|
||||
step 4.
|
||||
7. `mosaic business validate <id>`. If it refuses and step 5 created the file in this run, remove that
|
||||
file, then refuse. A file that already matched on a re-run (D4) is never removed (N4).
|
||||
8. Print the copy id and the next steps: root's 2c, Jason's 2d, 3 and 4, then the `--library-status`
|
||||
check (D6b).
|
||||
|
||||
d1's step 3, `release.sh package` and `activate`, is gone (Q4a, webby-0085 B1, sage-2189). Package is a
|
||||
`docker compose build`, and activate runs a pi Compose health check that mounts `PI_AUTH_FILE`
|
||||
(compose.yaml:43). Neither fits 2a, which has no Docker and no pi credential, and the hub doesn't run
|
||||
containers. The pinned harness comes from the copy's `npm ci` (Q3). The `CASE2_RELEASE` seam goes with
|
||||
it.
|
||||
|
||||
It starts no unit, writes no unit file, doesn't write `/etc/mosaic-stack/dev.args` and doesn't run
|
||||
`adopt`. These are my answers to row 195's needs 4 and 5 (gosalyn-0087), unchanged. Row 195's W8 now
|
||||
passes `--business-file` and has no release step.
|
||||
|
||||
### D3. Personas come from the staging dir
|
||||
|
||||
Unchanged from d1. `library` requires `--personas DIR`, the staging dir from `case2-persona-import.mjs
|
||||
stage`. The install runs `case2-persona-import.mjs install --staging DIR --config-dir <configDir>
|
||||
--business <id>` and reads its two `instance` lines into `roles.bean.vars` and `roles.chet.vars`. Only
|
||||
those two lines go into the business file. They hold a name and a digest, never text.
|
||||
|
||||
This moves the persona import's stage part of design 10 step 6 ahead of 2b: the read of her Hermes
|
||||
profiles and Jason's check of the printed pairing. Webby agreed in webby-0085 (Q1).
|
||||
|
||||
### D4. Re-runs
|
||||
|
||||
Unchanged from d1, plus N4. Each step is safe to run again. bootstrap and the copy are idempotent. An
|
||||
existing business file that matches the one the script would write passes, and the script remembers it
|
||||
didn't create it. One that differs refuses `business-file-differs`, and the script doesn't touch it.
|
||||
The persona import already refuses to overwrite. On a re-run the install compares each existing
|
||||
persona file's sha256 with the staging manifest, and refuses on a mismatch.
|
||||
|
||||
### D5. Inputs and seams
|
||||
|
||||
```
|
||||
case2-install.sh --business ID --business-file FILE --commit SHA [--profile factory|library]
|
||||
[--personas DIR] [--home DIR]
|
||||
```
|
||||
|
||||
`--home` defaults to `$HOME`, which is `/srv/mosaic` under `sudo -u mosaic -H`. `MOSAIC_CONFIG` is
|
||||
honoured as bootstrap.sh honours it. One test seam, in case2-unit.sh's `CASE2_*` pattern:
|
||||
`CASE2_STACK_COPY` is the copy script, and the test uses a stub that makes the copy dir.
|
||||
|
||||
Exit codes: 0 ok, 1 refused, 4 usage, as case2-unit.sh uses.
|
||||
|
||||
### D6. Precheck library checks, split in two (Q4b, sage-2189)
|
||||
|
||||
d1 ran `sync()` from the precheck. Webby's N1 and webby-0086 showed two faults. The precheck runs before
|
||||
the units start, so there is no mirror yet (runtime.mjs:143-145). And a precheck `sync()` after the bus
|
||||
is up would race the bus's timer, since `sync()` fetches outside the lock (sync.mjs:97-98). Sage split
|
||||
the check in sage-2189, and the brief's "runs library.sync" becomes "reads the bus's recorded sync
|
||||
state".
|
||||
|
||||
#### D6a. Before the units start, `--profile library`
|
||||
|
||||
These run where the precheck runs today, after 2c's render and before the start. They sync nothing and
|
||||
read no mirror. Each is a `refuse` line in the script's style.
|
||||
- The business file is owned by the uid, with no group or other bit (`business-file-mode`).
|
||||
`businesses/.history/<id>/` is 0700 and its `changes.jsonl` is 0600, when present (`history-mode`).
|
||||
- The persona dirs are 0700. Each creative role's persona file is regular, owned and 0600
|
||||
(`persona-mode`), and its sha256 equals the role's `persona.sha256` (`persona-mismatch`). The output
|
||||
prints names and digests, never text.
|
||||
- No `<dataRoot>/discord/*.json` exists (`discord-binding`).
|
||||
|
||||
#### D6b. `case2-precheck.sh --business ID --library-status`, after the bus is up and after step 4
|
||||
|
||||
A new mode, run as `<user>` once the bus is up and Jason has placed the token. It changes nothing. A
|
||||
node block builds the context the bus builds, `{ business, project, dataRoot }` for each library
|
||||
project, with `dataRoot` from the config (N3, runtime.mjs:146-151, :162-164). For each one it:
|
||||
- refuses `library-mirror-missing` when the mirror path doesn't exist. It checks this itself, so the
|
||||
message says the bus hasn't made it, not `library-git`;
|
||||
- calls `libraryStatus(context)` only, and refuses with the thrown code when that fails, for example
|
||||
`library-git` for an unsafe mirror or `library-sync-busy` when the flock wait runs out;
|
||||
- refuses `library-never-synced` when `lastFetch` is null;
|
||||
- refuses `library-last-error <code>` when `lastError` is not null. The code is the fixed
|
||||
`^[a-z-]{1,64}$` string, so it's safe to print. This also covers Webby's N2;
|
||||
- prints each recorded R-problem as `problem R<n> <path>`, with the path through `JSON.stringify`, then
|
||||
one `problems <count>` line (N5). Her file names are the only part that prints, never text.
|
||||
R-problems don't refuse (Q2, agreed by Webby).
|
||||
|
||||
The bus syncs each library with a `user` every 60 s, so the first run after step 4 may need a minute,
|
||||
or Jason's own `library.sync`. The run sheet says so.
|
||||
|
||||
### D7. host-run-sheet.md fix
|
||||
|
||||
`docs/guides/host-run-sheet.md:240` says "I2's install writes it" about `dev.args`. It becomes: root
|
||||
writes it at the unit render, from the values the go names, and the install never writes it, because
|
||||
`<user>` can't write `/etc` and root reads nothing `<user>` wrote. Sage asked in sage-1832 for this fix
|
||||
to go in this build commit, with its path in the manifest. Section 9's `library-gitea` line (:275) says
|
||||
"C2-13b's `library.sync` (not landed)". It becomes the `--library-status` check (D6b). The install
|
||||
section also gets a line that the copy's `npm ci` needs the npm registry from the host (Q3, webby-0085).
|
||||
|
||||
### D8. Design 10 text
|
||||
|
||||
Sage ruled that design 10 gets the same change when 226 lands, with the line in this row's manifest.
|
||||
In agents/gosalyn/work/case2/DESIGN.md:
|
||||
- step 2b drops `release.sh package` and `activate`;
|
||||
- a new step after step 4 runs `case2-precheck.sh --business <business> --library-status`;
|
||||
- the precheck's library text says it reads the bus's recorded sync state, not that it runs
|
||||
`library.sync`.
|
||||
|
||||
### D9. Tests, `packages/cli/tests/case2-install.test.mjs`
|
||||
|
||||
These follow case2-unit.test.mjs: scratch HOME, MOSAIC_CONFIG, stub bin dir and seams, a local bare repo
|
||||
cloned `--bare --no-local` as sync.test.mjs does, and the fixture's key never in output.
|
||||
- Install `library`:
|
||||
- writes the business file at 0600 with bean's and chet's persona vars;
|
||||
- writes the persona files at 0600 in 0700 dirs and the empty slot dir at 0700;
|
||||
- logs the copy call and no release call;
|
||||
- starts nothing;
|
||||
- passes on a re-run;
|
||||
- refuses `business-file-differs` and leaves the file unchanged.
|
||||
- Install `factory`:
|
||||
- writes no library mapping, no persona file and no slot dir, which is the brief's case;
|
||||
- refuses a library project and `bus.tester` off.
|
||||
- Install refuses: run as root, a business id mismatch, missing `--personas` under library, and a
|
||||
`validate` refusal, after which no business file is left.
|
||||
- N4: a re-run where the matching business file is already there and `validate` refuses leaves that
|
||||
file in place, byte for byte.
|
||||
- D6a refusals, one case each: a persona file at 0644, a digest mismatch, a business file at 0644, a
|
||||
`changes.jsonl` at 0644, and a Discord binding. A clean library host passes D6a with no mirror.
|
||||
- D6b, with the mirror made by `ensureMirror` and the state written by a real `sync()` against the local
|
||||
bare repo, as the bus would:
|
||||
- no mirror refuses `library-mirror-missing`;
|
||||
- a fresh mirror refuses `library-never-synced`;
|
||||
- a sync against a remote that doesn't resolve refuses `library-last-error` with its code;
|
||||
- a synced bare repo with one R2 path prints its `problem` line and `problems 1`, and passes.
|
||||
|
||||
Every new refusal gets a mutant that removes it. The count goes in the slot request.
|
||||
|
||||
### D10. Files
|
||||
|
||||
Owned by the brief: `scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (library checks and
|
||||
`--library-status` only) and `packages/cli/tests/case2-install.test.mjs` (new). Also
|
||||
`docs/guides/host-run-sheet.md` (D7, sage-1832), agents/gosalyn/work/case2/DESIGN.md (D8, sage-2189)
|
||||
and this BUILD.md. Before the slot request I'll grep for outside readers of case2-precheck.sh, since
|
||||
case2-unit.test.mjs runs it.
|
||||
|
||||
## Questions
|
||||
|
||||
All three d1 questions are settled:
|
||||
- Q1, persona stage before 2b: Webby agreed (webby-0085).
|
||||
- Q2, R-problems don't refuse: Webby agreed.
|
||||
- Q3, the pinned harness is the copy's `npm ci`: Webby agreed, and asked for the registry note in D7.
|
||||
- Q4a and Q4b: Sage, sage-2189.
|
||||
|
||||
One choice for Webby to check: D6b's `problem` line carries the path through `JSON.stringify`. N5 asked
|
||||
for a prefix and a count. I kept the path because Jason needs it to tell her which file to fix, and the
|
||||
names are hers, not text.
|
||||
|
||||
## Changes from d1
|
||||
|
||||
- B1: release package and activate dropped, with the `CASE2_RELEASE` seam and its test (D2, D5).
|
||||
- N1 and webby-0086: the sync is gone from the precheck. D6 splits into D6a before the start and D6b,
|
||||
`--library-status`, after the bus is up and step 4 (sage-2189).
|
||||
- N2: covered by D6b's `lastError` refusal.
|
||||
- N3: D6b builds the context and slot as the bus does.
|
||||
- N4: step 7 removes only a file this run created, with a test.
|
||||
- N5: `problem` prefix and a `problems` count.
|
||||
- N6: the run sheet lines are :240 and :275.
|
||||
- New D8, the design 10 text change, and its path in D10.
|
||||
|
||||
## Build
|
||||
|
||||
Webby approved d2 in webby-0087 (REVIEW-design-d2.md, webby/reviews 2f488b4ea). Nothing above this
|
||||
section changed after approval. This section records the build.
|
||||
|
||||
### D8 lands on case2-design
|
||||
|
||||
Sage ruled in sage-2195, after Webby's N1, that D8 goes on gosalyn/case2-design, because
|
||||
agents/gosalyn/work/case2/DESIGN.md isn't on refactor. The change is commit 624212842 there, pushed:
|
||||
step 2b drops `release.sh package` and `activate`, new step 4a runs `--library-status`, and the C2-13b
|
||||
row text says the precheck reads the bus's recorded sync state. DESIGN.md is not in this row's manifest.
|
||||
|
||||
The brief's lines docs/plans/2026-10-20_case2-rows.md:1093-1096 still name release package, activate and
|
||||
a library.sync fetch. Sage owns the brief and will amend those lines to match D8 in a doc-only commit
|
||||
after the re-pin window (sage-2205). They are not in this row's manifest.
|
||||
|
||||
### Webby's d2 notes
|
||||
|
||||
- N2: the `problem` line keeps the path. After `JSON.stringify` the precheck also escapes DEL, the C1
|
||||
controls U+0080-009F, U+200E and U+200F, U+2028 and U+2029, and the bidi controls U+202A-202E and
|
||||
U+2066-2069 as `\uXXXX`. Webby named C1 and bidi. I added the rest because `JSON.stringify` leaves
|
||||
them raw too, and each one can change how a terminal shows the line. The test path holds U+202E and
|
||||
U+0085, and the test checks that neither prints raw.
|
||||
- N3: `lastError` is checked before `lastFetch`. A first sync with no token records
|
||||
`library-credential-missing` and leaves `lastFetch` null, so it refuses `library-last-error`, not
|
||||
`library-never-synced`. The fresh-mirror test has `lastError` null and refuses `library-never-synced`.
|
||||
|
||||
### Choices made in the build
|
||||
|
||||
- The config dir. The persona import refuses a config dir with group or other bits, and bootstrap makes
|
||||
one with the default mode. So the install makes the config dir at 0700 when it's absent, before
|
||||
bootstrap, and refuses `config-dir-mode` when it exists with a group or other bit. It checks
|
||||
`secrets/` and `secrets/<id>/` the same way under library (`secrets-mode`, `slot-mode`).
|
||||
- `--home` must equal `$HOME` (`home-mismatch`). Every path comes from `$HOME` and `MOSAIC_CONFIG`, as
|
||||
bootstrap's does, so a different `--home` would only name a second home the script doesn't use.
|
||||
- A plan step runs before anything is written. It reads the input and the staging manifest, computes
|
||||
the persona vars from the manifest, and compares the result with what's on disk. It refuses every
|
||||
input rule, `business-file-differs` and `persona-differs` there, so a refused run writes nothing.
|
||||
After the import the script checks that the import's two `instance` lines carry the manifest's
|
||||
digests.
|
||||
- A re-run with both persona files present and matching skips the import, which would refuse to
|
||||
overwrite them. One matching file and one missing refuses `persona-differs`.
|
||||
- A diverged library prints one line under `--library-status`, naming the tip `library.sync --accept`
|
||||
would take. It doesn't refuse. The design doesn't list it as a refusal, and the bus still serves the
|
||||
old tip.
|
||||
- The precheck's library checks print `ok` lines for each check they pass, as the rest of the script
|
||||
does. A business file that's missing prints no extra line, since the existing business-file check
|
||||
already refuses it.
|
||||
- The business id must match the persona import's rule, `^[a-z0-9][a-z0-9-]{0,63}$`. That's narrower
|
||||
than the precheck's, which also takes `.` and `_`, because the persona dir uses the id.
|
||||
|
||||
### Outside readers
|
||||
|
||||
`git grep` over packages/*/tests/*, packages/*/src/* and scripts/test-*.sh finds case2-precheck.sh read
|
||||
by case2-unit.test.mjs, which runs it, and case1-accept.test.mjs, which names a copy's path but runs a
|
||||
fake. No test reads host-run-sheet.md. No test pins a hash of either script. case2-unit.test.mjs's
|
||||
precheck host has a 0600 business file and no persona vars, so the new D6a checks pass there.
|
||||
|
||||
### Mutants
|
||||
|
||||
24, in /mnt/storage/scratch/gosalyn-r226/mutate.py. Each replaces one exact string once. Line numbers
|
||||
are the candidate's. run.sh stops with BASE RED before any mutant unless the base run is rc=0 and
|
||||
fail 0.
|
||||
|
||||
| Mutant | Line | Check removed |
|
||||
|---|---|---|
|
||||
| I1 | install :73 | run-as-root |
|
||||
| I2 | install :74 | home-mismatch |
|
||||
| I3 | install :81 | config-dir-mode |
|
||||
| I4 | install :101 | business-id-mismatch |
|
||||
| I5 | install :107 | profile-library-business |
|
||||
| I6 | install :108 | bus-tester-off |
|
||||
| I7 | install :111 | bus-tester-on |
|
||||
| I8 | install :115 | persona-var-input |
|
||||
| I9 | install :158 | business-file-differs |
|
||||
| I10 | install :152 | persona-differs |
|
||||
| I11 | install :218 | N4, removes the file even when this run didn't write it |
|
||||
| I12 | install :218 | validate's removal of the file this run wrote |
|
||||
| P1 | precheck :229 | business-file-mode |
|
||||
| P2 | precheck :235 | history-mode for changes.jsonl |
|
||||
| P3 | precheck :246 | persona-mode for a persona file |
|
||||
| P4 | precheck :251 | persona-mismatch |
|
||||
| P5 | precheck :241 | persona-mode for a persona dir |
|
||||
| P6 | precheck :258 | discord-binding |
|
||||
| P7 | precheck :104 | library-mirror-missing |
|
||||
| P8 | precheck :107 | library-last-error |
|
||||
| P9 | precheck :107 | N3, the order swapped |
|
||||
| P10 | precheck :108 | library-never-synced |
|
||||
| P11 | precheck :92 | N2, the escape after stringify |
|
||||
| P12 | precheck :65 | --library-status refuses --profile |
|
||||
|
||||
`history-mode` for the history dir itself has no mutant and no test. The brief names only the
|
||||
`changes.jsonl` case.
|
||||
|
||||
### Tests
|
||||
|
||||
Not run yet. They wait for a slot.
|
||||
@@ -0,0 +1,177 @@
|
||||
# Row 239 (#1745) build notes (append only)
|
||||
|
||||
## 2026-10-22 b1 (Honker)
|
||||
|
||||
Design d1 5da5f28f6 was approved by Duckworth with C1 and C2 and no blockers (duckworth-0165, note
|
||||
agents/duckworth/work/row239/REVIEW-design-d1.md). The code is on honker/row239 from aca16404c. No test
|
||||
has run yet: every run waits for Sage's CLEAR.
|
||||
|
||||
### Duckworth's conditions and answers
|
||||
|
||||
- C1 (option a): `outcome-unknown-revoked` is in the Console's `OPEN` list (webui bus.mjs), so
|
||||
`sendOutcome` answers it as `outcome-unknown` with `sent: 'unknown'` and never "Nothing changed". T6
|
||||
covers it and M3f is its mutant.
|
||||
- C2: `write()` sets `revoked = true` after `beforeWrite()` returns, and a catch around the whole
|
||||
`withLock(...)` call marks the error. So a throw from the lock's release in its finally is marked as
|
||||
well. This is structural and has no test, as Duckworth said.
|
||||
- Q1: `beforeWrite` runs after both validations. Q2: two codes. Q3: M2 removes the `:188` check (now
|
||||
organization.mjs:195).
|
||||
|
||||
### Notes N1-N5
|
||||
|
||||
- N1: `beforeWrite` must not write the same business, because it would find its own lock and refuse
|
||||
`organization-busy`. This is said in the comments on `write()` and `commit()`. Today's revokes (the
|
||||
bus's credential revoke) write no business file.
|
||||
- N2: the lock is held across the revoke. A fire or a delete now keeps other writers of that business
|
||||
out for as long as the revoke takes. They refuse `organization-busy`, as they do for any held lock.
|
||||
- N3: a revoke that fails partway (some tokens revoked, then `credential-revoke-failed`) behaves as
|
||||
today. The error carries no `revoked` mark, because `beforeWrite` didn't return, and nothing is
|
||||
written. This row doesn't change that.
|
||||
- N4: a delete closed with `outcome-unknown-revoked` exits 3 in the CLI, as every closed refusal does.
|
||||
Exit 1 (transport EXIT) applies only to a direct verb answer.
|
||||
- N5: I grepped for "before the write" and similar comments. The ones left (lifecycle-revoke.test:2,
|
||||
instance-settings:56, cli instances.mjs:53, serve.mjs:365) are still true and are unchanged. T21 in
|
||||
bus lifecycle-revoke.test:173 is unchanged.
|
||||
|
||||
### Differences from the design
|
||||
|
||||
- The business tests are in lifecycle-settings.test.mjs, not instance-settings.test.mjs. The fire tests
|
||||
and their fixture already live there. The fixture now returns `dir` and `rolesDir` too.
|
||||
- T3 covers T3b (the delete) in the same test, looping over fire and delete. It also checks that a
|
||||
revoke throwing `credential-revoke-failed` carries no mark and writes nothing.
|
||||
- T4b is the plain-code check. The design's "T4b, outcome-unknown-revoked for an fs-style error" is in
|
||||
T4, which checks EIO with and without the mark for both a fire and a delete.
|
||||
- The bus test's fake org gets `deleteError`, thrown from its `instances.set` branch, so T4 can drive a
|
||||
delete.
|
||||
- transport.test gets one assertion, `busExit("outcome-unknown-revoked") === 1`, for M3g.
|
||||
- A latent Console text: a direct (not closed-request) `revision-conflict-revoked` answer in the
|
||||
Console would still get " Nothing changed." appended, because only `OPEN` codes skip it. That sentence
|
||||
is true there: the file is unchanged. No direct human path gives this code today, since a delete
|
||||
reaches the Console as a closed request's refusal.
|
||||
- The Console's STATUS entry for `outcome-unknown-revoked` (503) can't be reached while the code is in
|
||||
`OPEN`. It stays for symmetry with the CLI and is harmless.
|
||||
|
||||
### What changed
|
||||
|
||||
- packages/business/src/organization.mjs: `write()` takes `beforeWrite`, runs it under the lock after
|
||||
the revision check and both validations, and marks any later error `revoked`. The body is reindented
|
||||
inside the try.
|
||||
- packages/business/src/instance-settings.mjs: `instanceSettings` takes an optional `writer`. `commit()`
|
||||
passes `beforeWrite` to `writer.write` instead of calling it. The early check stays, and its comment
|
||||
is rewritten.
|
||||
- packages/bus/src/broker.mjs: `#organizationFail` maps a marked error to `revision-conflict-revoked` or
|
||||
`outcome-unknown-revoked` before the existing mappings. `#fireJob`'s notice adds "Its credential
|
||||
tokens may already be revoked." for a `-revoked` code.
|
||||
- packages/bus/src/runtime.mjs: comment only.
|
||||
- packages/cli/src/instances.mjs: two REASONS entries. packages/cli/src/transport.mjs: EXIT
|
||||
`outcome-unknown-revoked` 1.
|
||||
- packages/webui/src/serve.mjs: two AGENT_REASONS entries. packages/webui/src/bus.mjs: `OPEN` entry and
|
||||
two STATUS entries.
|
||||
- Tests:
|
||||
- business lifecycle-settings.test: "row 239 T1", "row 239 T2", "row 239 T3".
|
||||
- bus fire-revokes-grants.test: "row 239 T4", "row 239 T4b".
|
||||
- cli instances.test: "row 239 T5 CLI". cli transport.test: one assertion in "busExit and
|
||||
refuseInsideAgent".
|
||||
- webui agents.test: "row 239 T6".
|
||||
- bus channel-proof.test re-pins packages/cli/src/transport.mjs (662642a8...) and
|
||||
packages/webui/src/bus.mjs (1eba9774...), with a comment on each. Neither changes a proof or the
|
||||
transport. The design's file list missed this. The pin grep found it before the first run.
|
||||
### Mutants for the gate (exact-once replacements)
|
||||
|
||||
- M1 (instance-settings.mjs, beforeWrite before the lock): replace
|
||||
` const receipt = writer.write(business, next, { revision, actor, context, beforeWrite });` with
|
||||
` if (beforeWrite) beforeWrite();\n const receipt = writer.write(business, next, { revision, actor, context });`.
|
||||
The kill is lifecycle-settings "row 239 T1" (the revoke spy has a call).
|
||||
- M2 (organization.mjs, the fire refusal dropped): delete the line
|
||||
` if (revision !== before) fail("revision-conflict", "organization revision changed; read again");`.
|
||||
The kill is "row 239 T1" (the write lands over the hand edit).
|
||||
- M3a (organization.mjs, the mark dropped): ` if (revoked && e instanceof Error) e.revoked = true;`
|
||||
becomes ` void revoked;`. The kills are "row 239 T2" and "row 239 T3" (business). T4 drives the mark
|
||||
through a fake org, so it doesn't see M3a.
|
||||
- M3b (broker.mjs): delete the line
|
||||
` if (e.revoked) fail(e.code === "revision-conflict" ? "revision-conflict-revoked" : "outcome-unknown-revoked");`.
|
||||
The kill is fire-revokes-grants "row 239 T4".
|
||||
- M3c (cli instances.mjs): delete the five lines from ` "revision-conflict-revoked": "the business file`
|
||||
through ` "business file again with \`mosaic business instances\` before trying again",`. The kill is
|
||||
instances.test "row 239 T5 CLI".
|
||||
- M3d (webui serve.mjs): delete the two lines starting ` 'revision-conflict-revoked': 'the business file`
|
||||
and ` 'outcome-unknown-revoked': 'the agent\'s tokens`. The kill is agents.test "row 239 T6".
|
||||
- M3e (broker.mjs): ` const tokens = code.endsWith('-revoked') ? ' Its credential tokens may already be revoked.' : '';`
|
||||
becomes ` const tokens = '';`. The kill is "row 239 T4".
|
||||
- M3f (webui bus.mjs, C1): the `OPEN` line loses `, 'outcome-unknown-revoked'`. The kill is "row 239 T6"
|
||||
(the launch POST answers 503 outcome-unknown, sent unknown).
|
||||
- M3g (cli transport.mjs): delete the line ` "outcome-unknown-revoked": 1,`. The kill is transport.test
|
||||
"busExit and refuseInsideAgent".
|
||||
|
||||
A kill counts only as a failed assertion in the named test, not a load error or a different test.
|
||||
|
||||
### Gate plan (mos-1275 timeouts)
|
||||
|
||||
- My row 256 gate (2026-10-22 10:34Z, same machine) took 590s for the cli package, 57s for webui and
|
||||
38s for test-foundation, the longest shell script.
|
||||
- Packages: `timeout -k 10 1800` per package (3x the cli run).
|
||||
- Each scripts/test-*.sh: `timeout -k 10 300` (more than 3x the 38s longest).
|
||||
- Single files and mutants: `timeout -k 10 600` per file run (cli instances.test whole is the longest
|
||||
single file).
|
||||
- Order on Sage's CLEAR: the touched test files whole (business lifecycle-settings, bus
|
||||
fire-revokes-grants and channel-proof, cli instances and transport, webui agents), then M1-M3g, then
|
||||
the full gate.
|
||||
- Before the in-review move: grep packages/*/tests for the changed comment and error text and for
|
||||
64-hex pins on the changed files, and drift-check BASE..refactor against them.
|
||||
|
||||
## 2026-10-22 r2 rebase (Honker)
|
||||
|
||||
- Duckworth approved b1 b0c28366f statically, with no blockers (duckworth-0168). On N1 (`endsWith('-revoked')`
|
||||
would also match `access-revoked`, which can't reach a fire job today), the check stays as it is, so the
|
||||
approved candidate doesn't change before the gate (honker-0163).
|
||||
- Rows 253 (48e450ce8) and 275 landed. b1 is rebased onto refactor 94c33a43c as honker/row239-r2. The
|
||||
push branch honker/row239 isn't rewritten.
|
||||
- Drift between aca16404c and 94c33a43c on the 15 paths: only packages/bus/src/broker.mjs, where row 275
|
||||
changes the viewer verbs. Neither of row 239's two hunks (`#fireJob` and `#organizationFail`) moves in
|
||||
meaning. The rebase applied with no conflict. The two pinned files (cli transport.mjs, webui bus.mjs)
|
||||
and channel-proof.test have no upstream change, so the b1 pins stand.
|
||||
- Each M1-M3g replacement string still matches exactly once.
|
||||
- The touched test files run whole again on the new base, in the slot (the rebase rule).
|
||||
|
||||
## 2026-10-23 r3 rebase (Honker, sage-3730)
|
||||
|
||||
- r2 (70c6e3772) is rebased onto refactor 93694c518 as honker/row239-r3. honker/row239-r2 isn't rewritten.
|
||||
- Between 94c33a43c and 93694c518, five of the 16 paths changed upstream: bus broker.mjs and runtime.mjs,
|
||||
channel-proof.test, and webui bus.mjs and serve.mjs. Every file merged on its own except channel-proof.test,
|
||||
where row 301 (59f0e44f4, mos-1449) re-pinned webui bus.mjs for the user transport fold. The merged
|
||||
pin keeps row 301's comment and row 239's, and takes sha256 e3f52172…1c2a89 of the merged bus.mjs.
|
||||
The transport.mjs pin (662642a8…6e55b) still matches the file.
|
||||
- Row 239's hunks in broker.mjs (`#fireJob`, `#organizationFail`), runtime.mjs (the revoke comment),
|
||||
bus.mjs (OPEN, STATUS) and serve.mjs are unchanged apart from their position. The range-diff shows
|
||||
only the channel-proof pin and the hunk context moved.
|
||||
- Each M1-M3g replacement string still matches exactly once. M3f's target is the whole `OPEN` line,
|
||||
since `, 'outcome-unknown-revoked'` also appears in the STATUS line.
|
||||
- No 64-hex pin in packages/*/tests or scripts names a changed file except channel-proof.test.
|
||||
- On the CLEAR (sage-3730): the touched files whole, then M1-M3g, then the counts here. Sage runs the
|
||||
full gate on the replay at landing.
|
||||
|
||||
## 2026-10-23 r4 replay and the slot run (Honker, sage-3773 CLEAR)
|
||||
|
||||
- On the CLEAR, refactor was 546366f4a (row 320 landed at 094124b4e). None of the 16 manifest paths changed
|
||||
between 93694c518 and 546366f4a. Upstream did change library src/index.mjs, which bus runtime.mjs imports,
|
||||
and business src/index.mjs, which the cli imports, so the run was on a replay onto 546366f4a rather than
|
||||
on r3's base. That replay is honker/row239-r4 0cae2dad5. The range-diff of r3 against r4 is four `=`
|
||||
lines with equal patch-ids, so duckworth-0292's read of r3 applies to r4. r3 isn't rewritten.
|
||||
- The run was on 0cae2dad5 with a clean tree, TMPDIR under /mnt/storage/scratch/tmp, 22:01:27Z-22:01:52Z.
|
||||
- The touched files, run whole: business lifecycle-settings 12/12, bus fire-revokes-grants 24/24, bus
|
||||
channel-proof 2/2, cli instances 15/15, cli transport 3/3, webui agents 9/9. That's 65/65 with 0 skipped,
|
||||
and no first-run red.
|
||||
- Mutants: 9 of 9 killed. Each was applied exactly once, and each made its named test fail as an assertion:
|
||||
- M1 lifecycle: 3 fail (T1, T2, T3).
|
||||
- M2: T1.
|
||||
- M3a: T2 and T3.
|
||||
- M3b fire-revokes-grants: T4.
|
||||
- M3c instances: T5 CLI.
|
||||
- M3d agents: T6.
|
||||
- M3e: T4.
|
||||
- M3f agents: T6.
|
||||
- M3g transport: "busExit and refuseInsideAgent".
|
||||
|
||||
T4b passed under M3b and M3e. Every restore matched the HEAD blob by sha256, and the tree was clean
|
||||
after the run.
|
||||
- No full gate (sage-3730): Sage runs it on the replay at landing.
|
||||
@@ -0,0 +1,159 @@
|
||||
# Row 239 design: the revoke under the writer's lock, and a fire test
|
||||
|
||||
Brief: docs/plans/2026-10-20_agent-lifecycle.md, "Brief: row 194 follow-up, the revoke under the writer's
|
||||
lock and a fire test" (blob 3b3b3b2b9), from Duckworth's row 194 b3 N1 and mos-1158. Base aca16404c (row
|
||||
194 landed as fd6dd327c, row 281 as e479521d3). Builder Honker, reviewer Duckworth, gate owner Sage.
|
||||
Draft 1, 2026-10-22.
|
||||
|
||||
## Today
|
||||
|
||||
- `instance-settings.mjs` `commit()` runs the checks, then compares the caller's revision with a fresh
|
||||
`writer.read` (row 194 b3, sage-1634), then calls `beforeWrite` (the fire's or the delete's credential
|
||||
revoke), then `writer.write`.
|
||||
- `writer.write` (`organization.mjs`) takes the lock, runs `recover`, checks the revision again (`:188`),
|
||||
validates, prepares, checks the bytes once more before publication (`:221`), renames and commits.
|
||||
- So two windows are left.
|
||||
- A write that lands between `commit()`'s read and the writer's lock still gets a revoke and then a
|
||||
`revision-conflict`.
|
||||
- Any refusal after the revoke ran (the `:221` check, or an I/O failure the bus reports as
|
||||
`outcome-unknown`) uses the same text as one before it, so nothing says the tokens may be gone.
|
||||
- Row 281 left this path alone. Its fire job (`broker.mjs` `#fireJob`) runs step G (grants), then step B,
|
||||
which is this same `write('instance.fire')`, and reports a step B failure as `fire.failed` along with a
|
||||
human notice that names the code. Its delete job only does step G. The delete itself still commits
|
||||
through `lifecycleDelete` → `instances.set` op delete → `commit()`.
|
||||
|
||||
## What changes
|
||||
|
||||
### 1. `beforeWrite` runs inside `writer.write`, under the lock (organization.mjs)
|
||||
|
||||
`write(id, document, { revision, actor, detail, context, beforeWrite = null })`. The order under the lock:
|
||||
|
||||
1. `recover`, read, then the `:188` revision check. A refusal here revokes nothing.
|
||||
2. Projects-protected and both validations. A refusal here revokes nothing either. I put `beforeWrite`
|
||||
after these rather than straight after `:188`, so a document the writer would refuse anyway never
|
||||
costs a revoke. Q1 asks Duckworth to confirm this.
|
||||
3. `beforeWrite()`. If it throws (today `credential-revoke-failed`), the error propagates as it is and
|
||||
nothing has been written, as now.
|
||||
4. Prepare, the `:221` check, rename and commit, wrapped so that any error thrown after step 3 gets
|
||||
`revoked = true` set on it before it is rethrown. That covers `revision-conflict` from `:221`, an
|
||||
`organization-refused` from `log()`, a barrier throw, and an fs error.
|
||||
|
||||
`record()` and the other writers (`launch-settings`, `plugin-settings`, `credentialVerbs`) pass no
|
||||
`beforeWrite` and behave as before.
|
||||
|
||||
### 2. commit() passes beforeWrite through (instance-settings.mjs)
|
||||
|
||||
`commit()` passes `beforeWrite` to `writer.write` and no longer calls it itself. The row 194 b3 early check
|
||||
stays. It costs one read, keeps T21's refusal before any validation work, and is no longer what protects
|
||||
the revoke. Its comment is rewritten to say so, and the "(BUILD.md follow-up)" sentence goes.
|
||||
|
||||
`instanceSettings({ ..., writer = organizationWriter({ dir, rolesDir }) })` takes an optional writer, so a
|
||||
test can inject one (the brief's "injected writer"). `runtime.mjs` passes none.
|
||||
|
||||
### 3. The bus names a refusal after a revoke (broker.mjs `#organizationFail`)
|
||||
|
||||
The bus answers with a code only, and the CLI and Console look the text up by code. So the "may already be
|
||||
revoked" signal has to be a code. Two new codes:
|
||||
|
||||
- `revision-conflict-revoked`: `e.revoked && e.code === 'revision-conflict'`.
|
||||
- `outcome-unknown-revoked`: `e.revoked` with any other code. The write may or may not have landed, since a
|
||||
failure after the rename is recovered as committed.
|
||||
|
||||
Both are checked before the existing mappings. An error without `revoked` maps exactly as today, so a
|
||||
refusal before any revoke keeps today's code and text. Q2 asks about the names.
|
||||
|
||||
### 4. The texts
|
||||
|
||||
- CLI `instances.mjs` REASONS:
|
||||
- `revision-conflict-revoked`: "the business file changed before the write, after the agent's tokens
|
||||
were revoked; the tokens may already be revoked and the instance can't use them; read the instances
|
||||
again and run the same command again, which is safe".
|
||||
- `outcome-unknown-revoked`: "the write's outcome is unknown and the agent's tokens may already be
|
||||
revoked; read the instances before trying again".
|
||||
- Console `serve.mjs` AGENT_REASONS: the same two, in the Console's wording ("delete again").
|
||||
- `#fireJob`'s `fire.failed` notice: when the code ends in `-revoked`, it adds "Its credential tokens may
|
||||
already be revoked." The chief's synchronous fire (no grants) answers with the code, as every chief verb
|
||||
does.
|
||||
- `transport.mjs` EXIT: `outcome-unknown-revoked` exits 1, as `outcome-unknown` does. Console
|
||||
`bus.mjs` STATUS: `revision-conflict-revoked` 409, `outcome-unknown-revoked` 503. A delete's code
|
||||
reaches both as a closed request's `refusal`, so these two only matter for a direct verb answer.
|
||||
|
||||
## Tests
|
||||
|
||||
- **T1, the brief's fire test** (business `instance-settings.test.mjs`). The fire uses an injected writer
|
||||
that wraps the real one. Its `write` makes one hand edit of the business file (a vars change on another
|
||||
instance) and then calls the real `write`, so the edit lands after `commit()`'s early check and before
|
||||
the lock's `:188` check. Expected:
|
||||
- the fire refuses `revision-conflict` without `revoked`;
|
||||
- the revoke spy has no calls;
|
||||
- the business file's bytes are the hand edit's;
|
||||
- changes.jsonl has no new prepared entry.
|
||||
- **T2** (same file). A fire on a real writer built with `barrier: stage => stage === 'prepared' && edit()`.
|
||||
The revoke runs, then `:221` refuses. Expected: `revision-conflict` with `revoked === true`, one revoke
|
||||
call, the file's bytes are the edit's, and the last receipt is `aborted`.
|
||||
- **T3** (same file). The same, with a `prepared` barrier that throws. Expected: the error has
|
||||
`revoked === true`, one revoke call, and the file unchanged. A delete through the same writer gives the
|
||||
same result (T3b).
|
||||
- **T4** (bus `fire-revokes-grants.test.mjs`, its injected `f.org`). `f.org.fireError` is a
|
||||
`revision-conflict` with `revoked: true`. Expected:
|
||||
- the job's `fire.failed` carries `revision-conflict-revoked`, and the inbox message says "may already
|
||||
be revoked";
|
||||
- the same with no `grants` (the synchronous fire) refuses `revision-conflict-revoked`;
|
||||
- a plain `revision-conflict` still maps to `revision-conflict` and the message lacks the sentence.
|
||||
- T4b: `outcome-unknown-revoked` for an fs-style error with `revoked`.
|
||||
- **T5** (cli `lifecycle.test.mjs` or `instances.test.mjs`, whichever already reads a closed delete's
|
||||
refusal). A delete closed with `revision-conflict-revoked` prints the new text, and one closed with
|
||||
`revision-conflict` prints today's.
|
||||
- **T6** (webui `agents.test.mjs`). The same pair for the Console's reason.
|
||||
- T21 (bus `lifecycle-revoke.test.mjs`) stays as it is, for delete.
|
||||
|
||||
## Mutants
|
||||
|
||||
| Mutant | Change | Killed by |
|
||||
| --- | --- | --- |
|
||||
| M1 | `beforeWrite` before the lock: `commit()` calls `beforeWrite()` itself and passes none to `write` | T1 (the revoke spy has a call) |
|
||||
| M2 | The fire refusal dropped: the `:188` revision check removed | T1 (the write lands over the hand edit) |
|
||||
| M3a | The mark dropped: no `revoked = true` in the catch | T2, T3, and T4 through the codes |
|
||||
| M3b | `#organizationFail` ignores `revoked` | T4 |
|
||||
| M3c | CLI text: the two REASONS entries removed | T5 |
|
||||
| M3d | Console text: the two AGENT_REASONS entries removed | T6 |
|
||||
| M3e | The notice: the `fire.failed` sentence removed | T4 |
|
||||
|
||||
M3a-M3e together are the brief's "drop the may already be revoked text".
|
||||
|
||||
## Files
|
||||
|
||||
- `packages/business/src/organization.mjs`
|
||||
- `packages/business/src/instance-settings.mjs`
|
||||
- `packages/business/tests/instance-settings.test.mjs`
|
||||
- `packages/bus/src/broker.mjs`
|
||||
- `packages/bus/tests/fire-revokes-grants.test.mjs`
|
||||
- `packages/cli/src/instances.mjs`
|
||||
- `packages/cli/src/transport.mjs`
|
||||
- one cli test file
|
||||
- `packages/webui/src/serve.mjs`
|
||||
- `packages/webui/src/bus.mjs`
|
||||
- `packages/webui/tests/agents.test.mjs`
|
||||
- This packet.
|
||||
|
||||
## Gate
|
||||
|
||||
As the brief says:
|
||||
|
||||
- Duckworth approves the manifest.
|
||||
- The bus, business, cli and webui suites and every `scripts/test-*.sh` are green on Sage's slot, with
|
||||
the touched test files run whole.
|
||||
- M1-M3e are killed.
|
||||
|
||||
Before the in-review move I grep the tests for the changed comment and error text, and for hash pins on the
|
||||
changed files.
|
||||
|
||||
## Questions for Duckworth
|
||||
|
||||
- **Q1.** Should `beforeWrite` run after the writer's validations (my pick) or straight after `:188` (the
|
||||
brief's literal place)? Running it later means a document the writer would refuse revokes nothing.
|
||||
- **Q2.** Are two new codes, `revision-conflict-revoked` and `outcome-unknown-revoked`, the right way to
|
||||
carry the signal? The alternative is to give `BusError` a detail field, which every CLI and Console
|
||||
reader would then have to learn. The codes are additive and no existing code changes meaning.
|
||||
- **Q3.** The brief's mutant "drop the fire refusal": I read it as removing the `:188` check (M2). If you
|
||||
meant the fire test's own refusal, say so and I'll redefine it.
|
||||
@@ -0,0 +1,276 @@
|
||||
# Row 283 (#1788) C2-01d build notes (Honker)
|
||||
|
||||
Append-only. Design: DESIGN.md d2 16e809457, APPROVE webby-0172. Gate owner Sage, reviewer Webby.
|
||||
|
||||
## 2026-10-23 build on 45dca69f6 (sage-3577)
|
||||
|
||||
Branch honker/row283-build from origin/refactor 45dca69f6e4bce52834587f66f2ff33ba839ab8a. The old branch
|
||||
honker/row283 (16e809457) held only the design, so its two commits were cherry-picked as 398314cf4
|
||||
(d1) and 4f66dccde (d2). The DESIGN.md blob equals 16e809457's.
|
||||
|
||||
Commits:
|
||||
- 331653b38: code, README and tests T1-T8.
|
||||
- 28918824c: the survey.test:73 re-pin, alone (mos-1330 item 4).
|
||||
- this file.
|
||||
|
||||
Nothing has run. No node, test or mutant ran during the build: Darkwing held the slot. Every count
|
||||
below is a plan until Sage's CLEAR.
|
||||
|
||||
### Drift check (sage-3577)
|
||||
|
||||
`git diff --stat 94c33a43c 45dca69f6` (design base to build base) over the paths the tests read or
|
||||
hash-pin:
|
||||
- No change in packages/library, packages/business/src/business.mjs or
|
||||
packages/business/tests/business.test.mjs.
|
||||
- No test hash-pins a library file. bus library.test and library-sync.test import index.mjs, and
|
||||
stack-copy.test only checks that index.mjs exists.
|
||||
- webui library-cli.test changed one line and doesn't read scope. The `classify` in webui reads.mjs is
|
||||
the business one, not the library's.
|
||||
|
||||
I redo the drift check on BASE..refactor before the in-review move.
|
||||
|
||||
### N3 (webby-0169): deepEquals of the mapping output outside packages/library
|
||||
|
||||
`git grep -n playground -- 'packages/*/tests/*' ':!packages/library/*'` and the files naming
|
||||
`universes` give two files:
|
||||
- business.test.mjs:20 compares `validateBusinessDocument(...).projects.books` with
|
||||
`checkMapping(mappingEntry())`. That is output against output, so `supporting: []` shows up on both
|
||||
sides. :27 compares only `.playground`.
|
||||
- library-projects-protected.test.mjs compares file bytes (:31, :32) and change paths (:58), never the
|
||||
mapping output.
|
||||
|
||||
No re-pin is needed outside library. Inside library, only the draft deepEqual at mapping.test:48 gains
|
||||
`supporting: []`, as the design says.
|
||||
|
||||
### Q1 departure (sage-2923) and the :73 re-pin (mos-1330 item 4)
|
||||
|
||||
survey.mjs:65 counts `supporting: 0` and formatSurvey prints it. T8 is the only added test
|
||||
(survey.test "C2-01d: with --mapping the survey counts supporting files").
|
||||
|
||||
mos-1330 item 4, packages/library/tests/survey.test.mjs:73, commit 28918824c:
|
||||
- old: `'scopes canon 5, playground 1, unmapped 3; indexed 6',`
|
||||
- new: `'scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6',`
|
||||
|
||||
N2: the pin gains only `supporting 0`. No other text on the line or in that test moved.
|
||||
|
||||
### Q2 tradeoff (webby-0169)
|
||||
|
||||
A supporting prefix that holds a playground prefix refuses `library-mapping-invalid`. That goes past
|
||||
the brief, but it is the fail-closed reading, and it uses an existing code: classify checks supporting
|
||||
first, so that playground prefix could never match. The cost is that she can't nest a playground under
|
||||
a supporting folder, though she can nest the other way (a playground holding a supporting folder).
|
||||
|
||||
### Handoffs
|
||||
|
||||
- Q4 (webby-0169, sage-2930): the M12 delete rule ("A delete also needs the path to exist at the tip as
|
||||
a 100644 blob", case2 DESIGN.md :308) is not in supportingPath. It binds row 288 (C2-02) via Gitea
|
||||
#1793 comment 30738, with tests for a missing path and a non-100644 entry.
|
||||
- A save with no `editor` refusing is C2-02's (DESIGN.md section 1).
|
||||
|
||||
### Choices the design left open
|
||||
|
||||
- The book loop is now `againstBooks(owners, list, code)` in mapping.mjs, called once per list. The
|
||||
playground's messages and codes are byte-equal to before.
|
||||
- A non-string path refuses `library-write-story-path` ("the path must be a string") as part of
|
||||
check 1. A non-array `tree` is a TypeError (DESIGN.md section 3).
|
||||
- Check 8 builds the tree's names as every path and every folder above it, keyed by
|
||||
`normalize('NFC').toLowerCase()`. A name on the way refuses if its key holds any name other than
|
||||
itself, so a tree that already holds case twins refuses a write to either (R7).
|
||||
|
||||
### Mutants (exact strings)
|
||||
|
||||
Each applies one replacement to the build tip. Run one at a time, and restore with `git checkout --`
|
||||
on the one file before the next.
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M1 | src/scope.mjs | `const prefix = entry.supporting.find(p => inside(p, path));` | `const prefix = entry.supporting.find(p => inside(p, path) && !entry.playground.some(q => inside(q, path)));` | scope.test "supporting comes before playground", supporting.test "passes at book level" (both-lists path) |
|
||||
| M2 | src/mapping.mjs | `if ((inside \|\| holding) && owner !== id)` | `if ((inside \|\| holding) && owner !== id && list !== 'supporting')` | mapping.test "each supporting prefix rule", business.test C2-04 |
|
||||
| M3 | src/supporting.mjs | `for (const p of own) if (LINKS.has(modes.get(p)))` | `for (const p of own) if (false && LINKS.has(modes.get(p)))` | supporting.test "symlink or gitlink" |
|
||||
| M4 | src/supporting.mjs | `const fold = name => name.normalize('NFC').toLowerCase();` | `const fold = name => name.normalize('NFC');` | supporting.test "case or Unicode form" |
|
||||
| M4b | src/supporting.mjs | `const fold = name => name.normalize('NFC').toLowerCase();` | `const fold = name => name.toLowerCase();` | supporting.test "case or Unicode form" (NFD case) |
|
||||
| M5 | src/supporting.mjs | `Buffer.byteLength(path, 'utf8') > MAX_BYTES` | `path.length > MAX_BYTES` | supporting.test "1025 bytes" |
|
||||
| M6 | src/mapping.mjs | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/;` | `const IDENT_BAD = /[<>]/;` | mapping.test "editor is optional" |
|
||||
| M6b | src/mapping.mjs | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f<>]/;` | `const IDENT_BAD = /[\u0000-\u001f\u007f-\u009f]/;` | mapping.test "editor is optional" |
|
||||
| M7 | src/supporting.mjs | `if (place.scope !== 'supporting' \|\| place.universe !== universe)` | `if (place.scope !== 'supporting')` | supporting.test "outside the universe's supporting folders" |
|
||||
| M8 | src/mapping.mjs | `if (sid === pid && s !== p && holds(s, p))` | `if (false && sid === pid && s !== p && holds(s, p))` | mapping.test "each supporting prefix rule" (Workshop case) |
|
||||
|
||||
(The `\|` in the table is an escaped `|` in the source text.) Each mutant runs the one test file named,
|
||||
plus business.test for M2, under `timeout -k 10 600`. A mutant passes the gate only when its run is red
|
||||
on the named test and the restored file's sha256 equals the build tip's.
|
||||
|
||||
### Gate plan (on Sage's CLEAR)
|
||||
|
||||
1. The touched files whole, each under `timeout -k 10 600`: library mapping, scope, supporting and
|
||||
survey tests, and business.test.mjs. Counts go below.
|
||||
2. M1-M8 as above.
|
||||
3. The full gate, under mos-1275 timeouts: `node --test packages/library/tests/` and
|
||||
`node --test packages/business/tests/` each under `timeout -k 10 1800`, and every scripts/test-*.sh
|
||||
under `timeout -k 10 300`. pipefail and tee, with skips named (test-task Docker groups).
|
||||
4. Before in-review: grep packages/*/tests for a 64-hex pin on any changed file and for the changed
|
||||
message text, then the drift check on BASE..refactor.
|
||||
|
||||
## 2026-10-23 Webby early look (webby-0227, static, not a verdict)
|
||||
|
||||
- E1: business.test.mjs is an existing test file with a 4-line addition after the playground refusal.
|
||||
The in-review request lists it next to survey.test:73.
|
||||
- E3 fixed: supportingPath now runs the string check before `JSON.stringify`, so a BigInt (or any
|
||||
non-string) path refuses `library-write-story-path` instead of throwing a TypeError.
|
||||
supporting.test's string case now covers 7, null, undefined, 1n and an array.
|
||||
- E2 open: `editor` takes surrounding whitespace, and git trims it at commit, so the saved ident and
|
||||
the commit ident can differ. DESIGN.md section 1 says "non-empty after trim". Refusing
|
||||
`value.trim() !== value` would change that approved rule, so it went to Sage instead of into the
|
||||
build.
|
||||
|
||||
## 2026-10-23 E2 departure (sage-3601)
|
||||
|
||||
A departure from DESIGN.md section 1 (which stays unedited). The design says each `editor` value is
|
||||
"non-empty after trim". Sage ruled that surrounding whitespace refuses too, because git trims an
|
||||
ident at commit, so a padded value would save one name and commit another. mapping.mjs now adds
|
||||
`if (value.trim() !== value) bad(`editor.${key} must not start or end with whitespace`)` after the
|
||||
IDENT_BAD check. `trim()` covers Unicode whitespace, so U+00A0 and U+3000 refuse as well. Inner
|
||||
whitespace is kept ("Her Middle Name" passes).
|
||||
|
||||
T3 gains a case per key for leading, trailing and both-ends spaces, U+00A0 and U+3000, plus one inner
|
||||
whitespace pass.
|
||||
|
||||
M6 grows a third part:
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6c | src/mapping.mjs | `if (value.trim() !== value) bad(` | `if (false && value.trim() !== value) bad(` | mapping.test "editor is optional" (padded cases) |
|
||||
|
||||
Fixture grep: `git grep -nE "\beditor\b|\bauthor\b" -- 'packages/library/tests/*' 'packages/business/tests/*'`,
|
||||
leaving out mapping.test.mjs, finds nothing. packages/business/tests has no fixtures directory. The
|
||||
library fixture mapping.json has no `editor`. The positive control (`vaultPrefix` in mapping.json)
|
||||
matches once. No fixture maps an editor or author with surrounding whitespace.
|
||||
|
||||
## 2026-10-23 E4 (webby-0230, sage-3606), same departure as E2
|
||||
|
||||
Claimed: `queue move 283 in-progress`, op honker-283-start, rev 3642, 19:10:49Z (briefed→in-progress).
|
||||
|
||||
Git's ident.c `crud()` strips any byte <= 0x20 and `, : ; < > " \ '` from both ends of an ident. I
|
||||
checked that with git 2.55.0: `Jane,` `"Jane"` `'Jane'` `Jane;` `Jane:` `Jane\` and `,Jane` all
|
||||
commit as `Jane`, while `Jane.` and `Ja,ne` are kept. mapping.mjs now takes the set from crud()
|
||||
rather than from that list: `IDENT_CRUD_END = /^[\u0000-\u0020,:;<>"\\']|[\u0000-\u0020,:;<>"\\']$/`.
|
||||
The E2 check became `if (value.trim() !== value || IDENT_CRUD_END.test(value))`, which refuses
|
||||
library-mapping-invalid with "editor.<key> must not start or end with whitespace or any of , : ; " ' \".
|
||||
`trim()` still catches Unicode whitespace (U+00A0, U+3000), which crud() leaves alone.
|
||||
|
||||
Inner characters: IDENT_BAD (`/[\u0000-\u001f\u007f-\u009f<>]/`) already refuses `<`, `>`, newline,
|
||||
tab and every other control anywhere in the value. T3 now pins that at the start, the end and the
|
||||
middle. Inner `, : ; " ' \` are kept, since git keeps them, and T3 pins that too.
|
||||
|
||||
Tests:
|
||||
- T3 has one case per character in [space, U+00A0, U+3000, `,`, `:`, `;`, `"`, `'`, `\`], at the start
|
||||
and at the end, for each key. `<`, `>`, newline and tab are pinned at the start, end and middle.
|
||||
Four accepted names have inner crud characters.
|
||||
- New round-trip test, "an accepted editor round-trips through a real git commit unchanged". It
|
||||
commits in a scratch repo with editor `Doe, Jane Q.` / `[email protected].` as author and
|
||||
committer, with GIT_AUTHOR_* and GIT_COMMITTER_* stripped from the env, and asserts `git log`
|
||||
shows exactly the saved values. I preflighted that commit with git 2.55.0 by hand: both values came
|
||||
back unchanged.
|
||||
|
||||
M6c now replaces the whole end check (this supersedes the earlier M6c row):
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6c | src/mapping.mjs | `if (value.trim() !== value \|\| IDENT_CRUD_END.test(value))` | `if (false)` | mapping.test "editor is optional" (end cases) |
|
||||
| M6d | src/mapping.mjs | `if (value.trim() !== value \|\| IDENT_CRUD_END.test(value))` | `if (value.trim() !== value)` | mapping.test "editor is optional" (`,` `:` `;` `"` `'` `\` cases) |
|
||||
|
||||
## 2026-10-23 E5 (webby-0236, not blocking), fixed
|
||||
|
||||
The round-trip test stripped only GIT_AUTHOR_* and GIT_COMMITTER_*. With GIT_DIR, GIT_WORK_TREE or
|
||||
GIT_INDEX_FILE set (inside a hook, say), GIT_DIR beats `-C root` and the commit would land in the
|
||||
outer repo, and the test read global and system config. It now deletes GIT_DIR, GIT_WORK_TREE,
|
||||
GIT_COMMON_DIR and GIT_INDEX_FILE (as queue/tests/helpers.mjs gitEnv does) and sets
|
||||
GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1 (as ledger.test.mjs does). The `-c user.*`
|
||||
ident still applies. Test only; no mutant changes.
|
||||
|
||||
## 2026-10-23 sage-2925: optional editor.user (sage-3649)
|
||||
|
||||
sage-2925 (recorded in ludwig/row286-r2 agents/ludwig/work/queue-286/BUILD.md, Q4) ruled that row 283
|
||||
adds an optional `editor.user`, her Gitea login for the `library-human` slot, which row 288 (C2-02)
|
||||
passes to `humanCredential(context, {user})`. It reached this row after the DESIGN.md d2 approval
|
||||
(ludwig-0068, bubba-0009, sage-3649), so it departs from DESIGN.md section 1 ("exactly the keys
|
||||
`name` and `email`"). DESIGN.md stays unedited.
|
||||
|
||||
- mapping.mjs: `only(entry.editor, ['name', 'email', 'user'], 'editor')`. When present, `user` must be
|
||||
a string matching USER (`/^[A-Za-z0-9_.-]{1,100}$/`, the entry `user` check), else
|
||||
library-mapping-invalid "editor.user must be a plain account name". The output carries `user` only
|
||||
when given. An absent `editor.user` refusing library-human-missing is 288's (bubba-0009).
|
||||
- README: the `editor` line names `user` and the end-character rule from E2/E4.
|
||||
- Tests: T3 (mapping.test "editor is optional") gains one accepted case (`her.login-1`, kept in the
|
||||
output) and refused cases for '', an inner space, '@', 101 characters, 7 and null. They go in
|
||||
mapping.test with the other editor cases, not in the new supporting.test, which tests write paths.
|
||||
- No test outside mapping.test reads `editor` or pins mapping.mjs or README (git grep).
|
||||
|
||||
| Mutant | File | Replace | With | Killed by |
|
||||
|---|---|---|---|---|
|
||||
| M6e | src/mapping.mjs | `if (Object.hasOwn(entry.editor, 'user') && (typeof entry.editor.user !== 'string' \|\| !USER.test(entry.editor.user)))` | `if (false)` | mapping.test "editor is optional" (editor.user refusals) |
|
||||
| M6f | src/mapping.mjs | `...(Object.hasOwn(entry.editor, 'user') ? { user: entry.editor.user } : {})` | `...({})` | mapping.test "editor is optional" (`her.login-1` kept) |
|
||||
|
||||
## 2026-10-23 T2 run (sage-3695 CLEAR, lane A, library and business only)
|
||||
|
||||
Node v26.8.1. refactor was e2b873390 (226 landed); `git diff --stat 45dca69f6 e2b873390 -- packages/library
|
||||
packages/business` is empty. TMPDIR was a mktemp -d under /mnt/storage/scratch/tmp, with pipefail.
|
||||
Singles and mutants ran under `timeout -k 10 600`, suites under `timeout -k 10 1800`.
|
||||
|
||||
First-run red (mos-1272, placed): the first base run on 46bad7ea3 at 20:01:09Z was red in
|
||||
supporting.test "malformed paths refuse by class, in order". 5 of 6 passed, with `TypeError: Do not know
|
||||
how to serialize a BigInt`. The cause was the test helper, not the product: `refuses()` labelled each
|
||||
assertion with `JSON.stringify(path)`, and the 1n case added for E3 can't be stringified. In c18ad3e12
|
||||
the label uses `util.inspect(path)`, a 2-line test-only change. The product's 1n refusal was never in
|
||||
question: supportingPath type-checks before it stringifies (E3). The whole run below is on c18ad3e12.
|
||||
|
||||
Base, the touched files whole (20:01:43Z), all rc=0:
|
||||
- library mapping.test 16/16, scope.test 6/6, supporting.test 6/6, survey.test 7/7;
|
||||
- business business.test 21/21;
|
||||
- 0 skipped, 0 cancelled, 0 todo.
|
||||
|
||||
Mutants (20:01:44Z). Each was applied by exact string with count 1, then restored with `git checkout --`.
|
||||
Every restored sha256 equals HEAD's blob, and the tree was clean after.
|
||||
|
||||
| Mutant | Run | Result | Failing test |
|
||||
|---|---|---|---|
|
||||
| M1 | scope.test | rc=1, 5/6 | supporting comes before playground; a folder in both lists is supporting… |
|
||||
| M1 | supporting.test | rc=1, 5/6 | a path passes at book level, universe level, in a draft and in a folder in both lists |
|
||||
| M2 | mapping.test | rc=1, 15/16 | each supporting prefix rule refuses with its code |
|
||||
| M2 | business.test | rc=1, 20/21 | C2-04: library mappings load without roots, remain frozen, and retain mapping refusals |
|
||||
| M3 | supporting.test | rc=1, 5/6 | a symlink or gitlink on the way, or at the path itself, refuses |
|
||||
| M4 | supporting.test | rc=1, 5/6 | a name that differs from a tree name only in case or Unicode form refuses… |
|
||||
| M4b | supporting.test | rc=1, 5/6 | the same case/Unicode test |
|
||||
| M5 | supporting.test | rc=1, 5/6 | a path of exactly 1024 bytes passes, and 1025 bytes… refuses |
|
||||
| M6 | mapping.test | rc=1, 15/16 | editor is optional, kept when given, and refuses anything but a plain name and email |
|
||||
| M6b | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M6c | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M6d | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M6e | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M6f | mapping.test | rc=1, 15/16 | the same editor test |
|
||||
| M7 | supporting.test | rc=1, 5/6 | a path outside the universe's supporting folders refuses |
|
||||
| M8 | mapping.test | rc=1, 15/16 | each supporting prefix rule refuses with its code |
|
||||
|
||||
14 of 14 mutants killed, each on its named test only.
|
||||
|
||||
Suites (20:01:47Z):
|
||||
- `packages/library`: `node --test tests/*.test.mjs` rc=0, 98/98, 0 skipped.
|
||||
- `packages/business`: `node --test tests/` rc=0, 231/231, 0 skipped.
|
||||
|
||||
Not run here: every scripts/test-*.sh and the other packages. Those are Sage's 283 gate, after Webby's
|
||||
APPROVE.
|
||||
|
||||
## 2026-10-23 G1: mos-1528 OK for the mapping.test draft deepEqual (sage-3713)
|
||||
|
||||
This entry completes the N3 line above (BUILD.md:42, "only the draft deepEqual at mapping.test:48 gains
|
||||
`supporting: []`"). BUILD.md is append-only, so :42 stays as written. Mos OK'd the change of that old
|
||||
assertion in mos-1528. It is now at packages/library/tests/mapping.test.mjs:52, and commit 331653b38
|
||||
changed it. Everything else in that commit's hunk is new test lines, not changed old ones.
|
||||
|
||||
- Old, mapping.test.mjs:48 at 45dca69f6:
|
||||
`assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'] });`
|
||||
- New, mapping.test.mjs:52 at b2a1f4092:
|
||||
`assert.deepEqual(m.universes.idea, { kind: 'draft', name: 'New World', books: [], playground: ['Drafts/New World/'], supporting: [] });`
|
||||
|
||||
The line gains only `, supporting: []`; nothing else in it moved. The business.test and scope.test hunks
|
||||
add tests only (sage-3713: no OK needed). The other old-test change in this row is survey.test:73
|
||||
(mos-1330 item 4, 28918824c). Doc only.
|
||||
@@ -0,0 +1,191 @@
|
||||
# Row 283 (#1788) C2-01d design, draft 2 (Honker)
|
||||
|
||||
Brief: docs/plans/2026-10-20_case2-rows.md "C2-01d" at refactor 94c33a43c. Sources: the design at
|
||||
agents/gosalyn/work/case2/DESIGN.md b68d79663, the ruling after r11 (:207), the M12 paragraph (:300),
|
||||
1.1 (:755) and 1.2 (:817). Base 94c33a43c. Reviewer Webby, gate owner Sage. Nothing is built yet. The
|
||||
build waits for 282 to land (sage-2906).
|
||||
|
||||
## 1. Mapping (packages/library/src/mapping.mjs)
|
||||
|
||||
`supporting`:
|
||||
- Every universe kind takes an optional `supporting` list: plain `['name','books','playground','supporting']`,
|
||||
hybrid adds it to its seven keys, and a draft gets `['kind','name','playground','supporting']`.
|
||||
- Each value is checked as a playground prefix is: a string ending in `/` whose body passes `folder()`.
|
||||
- The output carries `supporting` on every universe, `[]` when absent, the way a draft carries
|
||||
`books: []`. classify then needs no `?? []`. The business writer writes the raw document
|
||||
(organization.mjs:195), not this output, so no business file changes shape.
|
||||
- The prefix rules, across every entry (ruling after r11):
|
||||
- A supporting prefix holding another supporting prefix, or one listed twice, refuses
|
||||
`library-mapping-invalid`.
|
||||
- A prefix equal to one of its own entry's book folders, or holding one, refuses `library-mapping-invalid`.
|
||||
- A prefix inside or holding another entry's book folder refuses `library-supporting-other-universe`.
|
||||
- Level comes from where the prefix sits, as for the playground: inside one of its own entry's books
|
||||
is book level, and outside every book is universe level. A draft's prefixes are all universe level.
|
||||
- Between the two lists (my choice, Q2):
|
||||
- A supporting prefix may equal a playground prefix of the same entry. That folder is writable.
|
||||
- A playground prefix may hold a supporting prefix of the same entry. The inner folder is supporting
|
||||
and the rest stays playground.
|
||||
- A supporting prefix that holds a playground prefix refuses `library-mapping-invalid`. classify
|
||||
checks supporting first, so that playground prefix could never match.
|
||||
- A supporting prefix equal to, holding or inside a playground prefix of another entry refuses
|
||||
`library-mapping-invalid`. Otherwise one path would have two universes.
|
||||
- The book loop at mapping.mjs:128-140 becomes a helper that both lists call with their own code and
|
||||
label. The playground's messages and codes stay byte-equal, so no existing test text moves.
|
||||
|
||||
`editor`:
|
||||
- The entry takes an optional `editor`. When present, it is an object with exactly the keys `name` and
|
||||
`email` (via `only()`). Each value is a string that is non-empty after trim and has no control
|
||||
character (`[\u0000-\u001f\u007f-\u009f]`), `<` or `>` (webby-0169 R1: git drops them from an ident
|
||||
and commits under a different name, so C2-02 would never see a failure). Anything else refuses
|
||||
`library-mapping-invalid`.
|
||||
- The output carries `editor: { name, email }` only when present, as `user` and `vaultPrefix` do. A save
|
||||
with no `editor` refusing is C2-02's job.
|
||||
|
||||
## 2. Scope (scope.mjs)
|
||||
|
||||
- `classify` gets a supporting loop before the playground loop (design 1.2). It returns `place(...)`
|
||||
with `{ scope: 'supporting', universe, book }`. The book is the book folder holding the prefix, or
|
||||
null at universe level and in a draft. The type and folder come from the first folder under the
|
||||
prefix, as for the playground.
|
||||
- The `.md` check stays first, so a `.canvas` or `.base` under a supporting prefix stays unmapped.
|
||||
- The comment above `classify` names the three prefix scopes and the order.
|
||||
|
||||
## 3. supportingPath (new supporting.mjs)
|
||||
|
||||
`supportingPath(mapping, universe, path, tree)` returns `path` or throws
|
||||
`LibraryError('library-write-story-path', ...)`. It is pure and reads no git.
|
||||
|
||||
- `tree` is the array `listTree` returns (reader.mjs:26, `{ mode, type, oid, size, path }`), so the
|
||||
C2-02 broker passes its listing as it is (Q3). A non-array `tree` throws a TypeError, because that is
|
||||
a caller bug, not a path class.
|
||||
- The checks run in this order, each with its own message naming the class:
|
||||
1. The path must be a string of at most 1,024 UTF-8 bytes (`Buffer.byteLength`).
|
||||
2. It must have no control character, using the same control class as `editor` (not `<` or `>`).
|
||||
3. It must have no backslash.
|
||||
4. It must not be absolute (a leading `/`).
|
||||
5. It must have no empty, `.` or `..` segment. That also catches `//` and a trailing `/`.
|
||||
6. `classify(mapping, path)` must give scope `supporting` and that same `universe`. This one check
|
||||
covers an unknown universe, an unmapped path, a `.canvas`, `.base` or attachment, a canon or
|
||||
story path, a playground path that isn't also supporting, and another universe's prefix. The scope
|
||||
rule then has one source, and M1 below hits both.
|
||||
7. Every proper prefix of the path (`a`, `a/b`, ...) and the path itself must not be in `tree` with
|
||||
mode `120000` (symlink) or `160000` (gitlink).
|
||||
8. No prefix of the path, and not the path itself, may equal a tree path or a tree directory
|
||||
(derived from the entries' paths) after `normalize('NFC').toLowerCase()` on both sides, unless
|
||||
it equals that tree path exactly. NFC as well as case, because APFS treats NFC and NFD forms of
|
||||
a name as one file (webby-0169 N1).
|
||||
Directories are checked too, because the folders under a supporting prefix are hers to name. A
|
||||
path in a tree that holds both `a.md` and `A.md` refuses, which matches R7.
|
||||
- The refusal messages quote the path with JSON, except for the control-character class, whose message
|
||||
leaves the path out.
|
||||
- The delete rule ("A delete also needs the path to exist at the tip as a 100644 blob", M12) isn't in
|
||||
the brief's supportingPath list. It stays with C2-02's delete (Q4).
|
||||
The C2-02 brief doesn't name it (only design :308), so BUILD.md carries it to C2-02 under Handoffs
|
||||
(webby-0169 Q4).
|
||||
- index.mjs exports `supportingPath`.
|
||||
|
||||
## 4. README (packages/library/README.md)
|
||||
|
||||
- Mapping: `supporting` and its rules, the cross-list rules, and `editor`.
|
||||
- Refusals: a `library-supporting-other-universe` row, and the new cases in the
|
||||
`library-mapping-invalid` row.
|
||||
- Scope: supporting first, then playground, then canon.
|
||||
- A new section, "Agent write paths", for supportingPath and its classes.
|
||||
|
||||
## 5. Callers of classify outside files owned (Q1 for Sage)
|
||||
|
||||
- survey.mjs:65 builds `{ canon: 0, playground: 0, unmapped: 0 }` and increments
|
||||
`scopes[classify(...).scope]`. A supporting file adds `supporting: NaN`, and formatSurvey (:92) never
|
||||
prints it. I propose a departure: add `supporting: 0` and print it, so the line becomes
|
||||
`scopes canon 5, playground 1, supporting 0, unmapped 3; indexed 6`. That re-pins survey.test:73 and
|
||||
adds a count test. Without it, the survey reports garbage once she marks a folder. The departure
|
||||
covers two files, packages/library/src/survey.mjs and packages/library/tests/survey.test.mjs.
|
||||
- No change in the rest, each for a reason:
|
||||
- reader.mjs:183 skips only unmapped, so supporting files are indexed like playground files.
|
||||
- R5 (:207) flags only canon to playground, as design 1.3 R5 says. A canon link to a supporting file
|
||||
raises nothing.
|
||||
- render.mjs:23 gives supporting a null badge. The supporting badge belongs to C2-10b (design :1415).
|
||||
- mirror.mjs:246 refuses only unmapped. Reading supporting files by scope is C2-05c's.
|
||||
- No bus or webui source filters on scope (git grep at 94c33a43c).
|
||||
- M9 already protects the whole `projects` key from agent edits (library-projects-protected.test). No
|
||||
test is added there.
|
||||
|
||||
## 6. Tests
|
||||
|
||||
- mapping.test:
|
||||
- T1: `supporting` at book level, at universe level, in a hybrid and in a draft. The output carries
|
||||
it, with `[]` when absent. The draft `deepEqual` at :48 gains `supporting: []`.
|
||||
- T2: one test per prefix rule.
|
||||
- These refuse `library-mapping-invalid`: no trailing `/`, a bad folder, one holding another, one
|
||||
listed twice, one equal to its own book, one holding its own book, one holding a playground
|
||||
prefix, and one overlapping another entry's playground.
|
||||
- These refuse `library-supporting-other-universe`: one inside another universe's book, and one
|
||||
holding another universe's book.
|
||||
- An equal same-entry playground passes.
|
||||
- T3: `editor` passes, and absent passes. It refuses `library-mapping-invalid` for a non-object, an
|
||||
array, an extra key, a missing key, an empty or blank value, a non-string, and a control character,
|
||||
`<` or `>` in either value.
|
||||
- scope.test, T4:
|
||||
- Supporting at book level, universe level and in a draft each give their universe and book.
|
||||
- A folder in both lists is `supporting`, and a playground-only folder is `playground`.
|
||||
- A supporting `.canvas` or `.base` is unmapped.
|
||||
- supporting.test (new):
|
||||
- T5 passes at book level, universe level, in a draft, in a both-lists folder, and at exactly 1,024 bytes.
|
||||
- T6 has one refusal per class against a fixture tree with a symlink, a gitlink and a case variant.
|
||||
The classes are:
|
||||
- unmapped, `.canvas`, `.base`, an attachment, canon, playground-only, another universe's
|
||||
supporting prefix, and an unknown universe;
|
||||
- absolute, `..`, `.`, `//`, a trailing `/`, a backslash, a C0 control and DEL;
|
||||
- a symlink folder, a gitlink folder, and a symlink at the path itself;
|
||||
- a case-variant file, a case-variant folder, and an NFD-form variant of an NFC tree name;
|
||||
- 1,025 bytes made with a multi-byte character, so bytes and characters differ.
|
||||
- Each asserts the code `library-write-story-path`.
|
||||
- business.test: T7 adds one assertion that `validateBusinessDocument` refuses
|
||||
`library-supporting-other-universe`.
|
||||
- survey.test (only if Q1 is approved): T8 counts one supporting file, and :73 is re-pinned.
|
||||
|
||||
## 7. Mutants (exact strings go in BUILD.md)
|
||||
|
||||
| Mutant | Change | Killed by |
|
||||
|---|---|---|
|
||||
| M1 | classify checks playground before supporting (the brief's mutant) | T4 both-lists, T5 both-lists |
|
||||
| M2 | the other-universe fail for supporting is dropped | T2 |
|
||||
| M3 | the symlink and gitlink check is dropped | T6 |
|
||||
| M4 | the case check is dropped | T6 |
|
||||
| M4b | check 8 drops `normalize('NFC')` | T6 NFD variant |
|
||||
| M5 | the byte length uses `path.length` | T6 1,025 bytes |
|
||||
| M6 | the `editor` control-character check is dropped | T3 |
|
||||
| M6b | the `editor` `<` and `>` check is dropped | T3 |
|
||||
| M7 | supportingPath skips the universe match | T6 other universe |
|
||||
| M8 | the supporting-holds-playground check is dropped | T2 |
|
||||
|
||||
## 8. Gate
|
||||
|
||||
- Webby approves the manifest.
|
||||
- The library and business suites and every scripts/test-*.sh are green on Sage's gate.
|
||||
- Timeouts (mos-1275): each package `timeout -k 10 1800`, each script `timeout -k 10 300`, each single
|
||||
file and mutant `timeout -k 10 600`. These come from my row 256 gate at 10:34Z.
|
||||
- Before in-review, grep packages/*/tests for 64-hex pins on the changed files and for the changed
|
||||
message text, then drift-check BASE..refactor.
|
||||
|
||||
## 9. Questions
|
||||
|
||||
- Q1 (Sage): approve the survey.mjs and survey.test.mjs departure in section 5?
|
||||
- Q2 (Webby): the cross-list rules in section 1?
|
||||
- Q3 (Webby): `tree` as the `listTree` array?
|
||||
- Q4 (Webby): the delete's 100644 check stays with C2-02?
|
||||
- Q5 (Webby): `editor` refuses only blank values and control characters, as the brief says. `<` and `>`
|
||||
would break a git ident. I'd leave them to C2-02's commit step unless you want them refused here.
|
||||
|
||||
## 10. Draft 2 (webby-0169)
|
||||
|
||||
- R1 folded: `editor` refuses `<` and `>` too, with T3 cases and mutant M6b.
|
||||
- Q2 accepted. BUILD.md records the tradeoff: refusing a supporting prefix that holds a playground
|
||||
prefix goes past the brief, but it is the fail-closed reading and uses an existing code. The cost
|
||||
is that she can't nest a playground under a supporting folder, though she can nest the other way.
|
||||
- Q3 accepted. Q4: carried to C2-02 under Handoffs in BUILD.md, and Sage was asked to add it to C2-02's brief.
|
||||
- N1 folded into check 8 (NFC and case), with an NFD case in T6 and mutant M4b.
|
||||
- N2: the survey.test:73 pin gains only `supporting`, and the count test is the only addition
|
||||
(mos-1330, its own commit).
|
||||
- N3: before the build, re-grep tests outside packages/library for a `deepEqual` of the mapping output
|
||||
on the build base.
|
||||
@@ -0,0 +1,342 @@
|
||||
# Row 272 / 264e (#1777): session CLI and Console chooser
|
||||
|
||||
Owner Huey; reviewer Scrooge per sage-2816 / mos-1310 item 2 (supersedes sage-2796). Current base 5fdfd7275, branch huey/row272-5fdfd7275 (sage-3354). Original draft base 2d0d407ba; round 1 was 850b22680 on b23783f39.
|
||||
Brief: docs/plans/2026-10-21_t1-sessions.md (ff0be0c79), section 8; row 264 rulings
|
||||
at 5f5b51dd6. Dependencies: 264a-d landed; 270/271 landing is 7328da8ca.
|
||||
Count/clear implementation and contracts are now in the current base. No runtime, launcher, library adapter,
|
||||
role, schema or queue edits belong to this slice.
|
||||
|
||||
## Execution and evidence
|
||||
|
||||
1. Read session/channel/HTTP/browser contracts and write isolation/validation tests.
|
||||
2. Add session CLI through the existing verified human channel, preserving defaults.
|
||||
3. Add authenticated Console chooser, per-session drafts/history/cursors, count and
|
||||
clear status. Unavailable dependency verbs refuse explicitly; no simulated success.
|
||||
4. On Sage's CLEAR: claim, run focused tests and required suites/mutants, repair and
|
||||
record exact evidence. No Node, tests, HTTPS git, claim or push under Gander's gate.
|
||||
5. After 253 and 269 land, rebase on a new branch name. Review packet to Scrooge,
|
||||
Mos, Gizmoduck and Sage; push only when permitted.
|
||||
|
||||
2026-10-22 preparation: clean worktree switched locally to the requested base. Read
|
||||
approved brief and all Q1-Q7 rulings. Della notified: no startBroker/library changes.
|
||||
Gizmoduck asked to pin count/clear contracts. Tests will be written first; Sage's
|
||||
explicit gate restriction defers RED/GREEN execution. No test result claimed.
|
||||
|
||||
Verification plan: CLI real-broker isolation and channel refusal; HTTP strict keys,
|
||||
Origin/Fetch Metadata, forged selector/refusal and no retries; fixture-browser two
|
||||
sessions, isolated draft/cursor/receipt, deep-link/refresh, count unknown/known,
|
||||
read-only writes, clear confirmation/pending. Add selector omission and cursor/draft
|
||||
sharing mutants. Broader cli/webui/plugins suites use Sage's gate only.
|
||||
|
||||
264c/d contracts received gizmoduck-0260: session.list gains compactions and revision/state;
|
||||
session.clear and session.operation use the pinned envelopes. CLI polls at most 30 s
|
||||
then reports pending; Console status checks use GET only. Counts absent on 264b show
|
||||
unavailable; unknown never becomes zero. Clear absent on 264b is disabled with a reason.
|
||||
USER_VERBS and tester listener write policy remain unchanged (Q7). Existing collaborators
|
||||
therefore cannot enter the new session surface until the reviewed RBAC integration.
|
||||
|
||||
Consumer audit found the explicitly deferred B6 user-reply scope (DEFERRED.md:455-459,
|
||||
queue-264/BUILD.md:317-322) bound to 272. Added a narrow broker predicate change from
|
||||
thread to session and a sibling-session regression fixture, without widening user verbs.
|
||||
This is the only planned broker change and does not overlap Della's runtime adapter.
|
||||
|
||||
2026-10-22 static review: added explicit --session to thread read/open and talk/follow;
|
||||
omission remains the saved default. CLI clear first reads a recorded operation, so a
|
||||
repeat after a lifecycle revision change cannot become a conflicting second write.
|
||||
Console deep links name both thread and session; accepted-send receipts, drafts and
|
||||
history pagination use that tuple. Clear-status GETs refresh the envelope without
|
||||
reissuing clear. Read failures discard private metadata; late generations cannot paint
|
||||
another selection. Scrooge is reviewer per sage-2816 / mos-1310, superseding the first
|
||||
assignment. CLEAR remains after 269, 275, 203 and 207. No execution evidence yet.
|
||||
|
||||
## Acceptance / mutant map (prepared, unexecuted)
|
||||
|
||||
| Check | Evidence file / intended kill |
|
||||
| --- | --- |
|
||||
| T5 legacy human, forged agent, foreign session, strict HTTP identity/Origin/site | session-cli.test.mjs, chat.test.mjs; unchanged USER_VERBS matrix in user-principal.test.mjs |
|
||||
| T7 saved default, explicit CLI session selector, metadata-only create | session-cli.test.mjs real broker and thread CLI compatibility tests |
|
||||
| T7 Console chooser, deep link, draft/cursor/receipt isolation, unknown count, confirmation/pending | chat-browser.test.mjs; existing history/sign-out/late-read tests run whole |
|
||||
| Count/clear contract absence | CLI unavailable refusal; Console disabled clear reason; exact zero vs unknown lower bound tests |
|
||||
| B6 assigned follow-up | user-principal.test.mjs sibling-session citation refusal and valid same-session reply |
|
||||
| M1 omit session in chat.mjs messages forwarding | chat.test.mjs explicit selector deepEqual fails |
|
||||
| M2 collapse browser state key from thread/session to thread | chooser draft isolation assertion fails |
|
||||
| M3 treat known:false count:0 as exact zero | CLI label assertion and browser unknown-count assertion fail |
|
||||
| M4 restore B6 mt.thread/s.thread predicate | user-principal sibling refusal assertion fails |
|
||||
| M5 remove session from CLI thread.send payload | explicit --session talk assertion fails |
|
||||
| M6 repeat a clear write in place of session.operation poll | CLI exactly-one-clear assertion fails |
|
||||
|
||||
Intended focused commands, only after CLEAR, one process at a time:
|
||||
|
||||
- `timeout -k 10 300 node --test packages/cli/tests/session-cli.test.mjs`
|
||||
- `timeout -k 10 300 node --test packages/cli/tests/thread-cli.test.mjs`
|
||||
- `timeout -k 10 300 node --test packages/bus/tests/user-principal.test.mjs`
|
||||
- `timeout -k 10 300 node --test packages/plugins/tests/channel.test.mjs`
|
||||
- `timeout -k 10 300 node --test packages/webui/tests/chat.test.mjs`
|
||||
- `timeout -k 10 600 node --test packages/webui/tests/chat-browser.test.mjs`
|
||||
|
||||
Whole touched suites on Sage's slot: bus (900 s), cli (1800 s, >=3x recorded 591 s),
|
||||
webui (600 s), plugins (600 s), plus required integration scripts per final gate brief.
|
||||
Mutants run in disposable copies of the pinned candidate, never against a live stack;
|
||||
only failed assertions count as kills, never a timeout or import/setup failure.
|
||||
No test process, browser, provider, credential read or live service action ran in this
|
||||
preparation. `git diff --check` is the only executed draft verification so far.
|
||||
|
||||
Next: Sage CLEAR for claim/focused runs; repair against actual results, then rebase on
|
||||
a newly named branch once 253 and 269 land. Generate final manifest only after those
|
||||
changes. Review handoff is Scrooge and Mos, copying Gizmoduck and Sage; no handoff to
|
||||
Duckworth under the superseded assignment. No candidate commit or green claim yet.
|
||||
|
||||
2026-10-22, gizmoduck-0272 / sage-2828 additive contract at 78e14352f: added
|
||||
session.cancel to CLI/channel/Console with confirmed human writes and strict keys.
|
||||
Cancelled envelopes retain `retired:null` (old active) or `true` (startup cancelled,
|
||||
old retired); cancelled is terminal for polling. CLI reports routed message cancellation
|
||||
with stored ID and exit 3, never waits for delivery or replays it. HTTP maps
|
||||
session-name-too-long and session-clear-complete without reflecting transport text.
|
||||
Added unexecuted before/after cancellation, identity/Origin, confirmation and message
|
||||
receipt tests. Existing clear/create/read wiring is preserved. Asked Gizmoduck whether
|
||||
the deterministic successor keeps 264a's UUID shape, as selectors currently require.
|
||||
No Node, test run, claim, commit, push, provider call or live action. Static diff only.
|
||||
|
||||
Gizmoduck-0274 resolves the selector compatibility check: successors are lowercase
|
||||
RFC 9562 v8 UUIDs (variant 10), while 264a sessions are v4. Existing generic UUID
|
||||
validators accept both and stay unchanged. CLI explicit-selector and Console chooser
|
||||
fixtures now include mixed v4/v8 IDs to prevent a later v4-only regression. Unexecuted.
|
||||
|
||||
|
||||
## sage-3141 rebase and landed-contract check (2026-10-23)
|
||||
|
||||
Reapplied the uncommitted draft onto b23783f39 in the same isolated worktree, on new
|
||||
branch huey/row272-b23783f39. No candidate commits existed to replay. Preserved the
|
||||
original draft in the named local stash `huey row272 draft before sage-3141 rebase`.
|
||||
Product files merged without conflict. Only append-only BUILD-LOG.md and SESSIONS.md
|
||||
conflicted: kept every new-base byte and appended the original Huey entries unchanged.
|
||||
No force push, fetch, Node, test, queue write or live change. No claim or candidate
|
||||
commit yet. Static `git diff --check` clean after resolution.
|
||||
|
||||
Checked the actual landed broker, not just the older contract packet:
|
||||
|
||||
- broker.mjs:687-707 clearEnvelope supplies the same op/thread/session/successor/state/
|
||||
start/refusal/at fields and cancelled-only retired:null|true. The generic UUID
|
||||
selectors still accept both original v4 and successor v8 IDs.
|
||||
- broker.mjs:6227-6245 session.list supplies compactions, state, revision and successor;
|
||||
count reader/unknown behavior is the landed 270 contract. Real-base tests must now
|
||||
exercise these available fields; stand-ins still exercise an unavailable dependency.
|
||||
- broker.mjs:6270-6330 clear/cancel/operation inputs and receipts match the wired contract.
|
||||
clear repeats preserve the op; cancel also refuses a failed clear as
|
||||
session-clear-complete. Our existing refusal mapping already covers it.
|
||||
- queue-271/BUILD.md:225-238 clarifies cancellation after retirement cancels this clear's
|
||||
startup, while later deliberate human open/send can start the fresh successor. It
|
||||
does not permanently disable that new session. No frontend authority change needed.
|
||||
- store.mjs:17 is 3n; migration is 3m-3n.sql. This slice adds/pins no schema step,
|
||||
migration blob or schema test count. No DDL copied from an older branch.
|
||||
|
||||
Plan after the Gander 574 run-2 hold: on a claims-only CLEAR, claim only; run no Node
|
||||
or tests beyond the explicit scope of Sage's grant. On test CLEAR run new CLI tests
|
||||
first, then new broker/HTTP/browser cases and whole touched files, sequentially under
|
||||
the recorded timeouts. Place/report a first-run red only in a new test; do not alter
|
||||
an old test, loosen an assertion or fix a second red without Mos's ruling via Sage.
|
||||
Whole-suite/mutant execution remains on Sage's slot. Candidate commit and push follow
|
||||
applicable green evidence and an allowed gap, then Scrooge/Mos handoff with final SHA,
|
||||
manifest, copying Gizmoduck/Sage.
|
||||
|
||||
Existing-test edits in the inherited draft need Mos's ruling through Sage before
|
||||
execution: channel.test.mjs extends its existing supported-verbs table for five
|
||||
session verbs; chat-browser.test.mjs extends the existing fetch fixture with session
|
||||
catalog/operation responses and changes its exact send-payload assertion to include
|
||||
the selected session. Assertions are not loosened. All other added test cases are
|
||||
new tests. These changes were prepared before sage-3141 and have never run. Ask Sage
|
||||
to forward this precise delta rather than treating a later old-test red as permission.
|
||||
|
||||
sage-3145 correction: refs/stash is shared across worktrees; using it as sole draft
|
||||
storage was wrong. Pinned the exact draft commit 07dc9d2f7944b4781b5e80dfa18425f4749f7a45
|
||||
on local branch huey/row272-stash-keep before any stash cleanup. The entry remains
|
||||
untouched; the branch retains its tracked and untracked snapshots. Future seat draft
|
||||
preservation uses owned branches/local commits, not shared stash positions.
|
||||
|
||||
Sage explicitly directed a local draft commit before tests to preserve the rebased
|
||||
work. This overrides the normal green-before-commit gate for this storage checkpoint
|
||||
only. It is an untested draft, not an approved candidate, test result, push or landing.
|
||||
Claims await the post-574-run-2 CLEAR; test execution awaits Mos's ruling and Sage's
|
||||
separate test CLEAR. Old-test deltas were forwarded as sage-3144; no ruling assumed.
|
||||
|
||||
## Scrooge early static read, 850b22680 (2026-10-23)
|
||||
|
||||
Review key scrooge-272-early-read-huey-1. B1 confirmed: the render catch used any
|
||||
403/404 to delete every sibling's draft/receipt and clear/create operation key.
|
||||
Uncommitted follow-up restricts that purge to thread-not-owned, thread-not-found and
|
||||
access-refused. Session-not-found still shows an unavailable read, preserving sibling
|
||||
state and pending recovery coordinates. New browser test retains A's draft and a lost
|
||||
clear key across a bad-session deep link, then proves Clear queries the retained op
|
||||
without another write.
|
||||
|
||||
N1: malformed clear/cancel write receipts now add the same redacted outcome-unknown
|
||||
audit line as other writes. New HTTP test checks one write log and no read log.
|
||||
N2: a valid recorded clear belonging to another session now refuses with a precise
|
||||
operation-key-reuse message (exit 2), rather than ambiguous outcome unknown; new CLI
|
||||
test proves no clear write. Malformed envelopes retain the outcome-unknown refusal.
|
||||
No approved old assertion or shared stub edited. Static diff check only; all new tests
|
||||
remain unrun pending Sage's test CLEAR. Published round 1 still pins 850b22680 and its
|
||||
original manifest; these follow-up edits do not replace that evidence. No new commit,
|
||||
push, queue write, provider call or live change for this early-read follow-up.
|
||||
|
||||
Execution watch: inspect message-filter.test first if the bus suite reports a B6
|
||||
fixture red (manual message_threads rows must name the run's bound session); do not
|
||||
change an old fixture/assertion without Mos's ruling via Sage. thread-shares uses the
|
||||
persisted session_defaults mapping per Scrooge's check. No runtime verdict claimed.
|
||||
|
||||
## Granted-user legacy compatibility (sage-3357; pre-candidate design)
|
||||
|
||||
219/295 are retained on base 5fdfd7275. USER_VERBS still excludes session verbs;
|
||||
193/T5 user session grants are a separate DEFERRED item, outside 272. No owner
|
||||
transport carries a user session operation. Bounded negotiation applies only to the
|
||||
session.list read: only `access-refused` and an omitted-session route permit legacy
|
||||
thread-default read/send/open. All other errors keep the unavailable/error state, and
|
||||
explicit session links always refuse. On fallback session controls are hidden and one
|
||||
status line says sessions are not available for this account. No fake session ID,
|
||||
count or lifecycle status is invented; the broker continues choosing its persisted
|
||||
default under current grants on every existing thread request. Owner routes keep the
|
||||
normal chooser and explicit selected-session arguments.
|
||||
|
||||
New file chat-sessions-access.test.mjs uses the real broker with an already granted
|
||||
principal over loopback and the existing repository browser fixture. It checks legacy
|
||||
read/send/open with no session controls, explicit-link refusal, another-error refusal,
|
||||
and unchanged owner chooser. No shared stub or existing assertion changes. The three
|
||||
required mutants remove the access-refused check, remove the omitted-session check,
|
||||
and display the controls during fallback; guard tests must kill each by assertion.
|
||||
|
||||
RED-first slot sage-3358: only this new file, before fallback implementation. Expected
|
||||
run 100 s including four Chromium fixtures; bound 300 s (3x), command
|
||||
`timeout -k 10 300 node --test packages/webui/tests/chat-sessions-access.test.mjs`.
|
||||
TMPDIR must be assigned from mktemp -d, with no underscore. No compose service is used;
|
||||
check/remove the generated directory's compose default network if any exists. Expected
|
||||
legacy-positive red is placed/reported afterward under mos-1272. Any other red is
|
||||
reported to Sage before a fix. No other test file/suite is authorized in this slot.
|
||||
|
||||
sage-3358 RED run completed before the fallback implementation. Only the new
|
||||
chat-sessions-access.test.mjs ran, under the 300 s bound, TMPDIR allocated by
|
||||
`mktemp -d /tmp/huey272-red-XXXXXXXX` as /tmp/huey272-red-0q66Jcf4. Exit 1;
|
||||
4 tests, expected legacy-positive assertion red and 3 passes, no other reds/skips/
|
||||
cancellations/timeouts, 3.504 s. Exact output: r2-red-access.tap. No matching compose
|
||||
network existed. Slot returned to Sage/Mos as huey-0035; no further tests run.
|
||||
|
||||
mos-1442 / sage-3361 adds explicit timeout, bus-error and malformed-answer refusal
|
||||
coverage. The new private fixture adds those guard cases (unrun), with a widening-key
|
||||
mutant alongside the access-refused/omitted-selector/control mutants. No shared stub
|
||||
or existing assertion changed. Native preview status/open both reported unavailable
|
||||
in this headless environment before the authorized repository-browser regression.
|
||||
|
||||
Implementation now local: the catalog request catches only access-refused on omitted
|
||||
session routes. Its internal null result selects the existing broker-chosen thread
|
||||
default with no fabricated session ID. Read/send/open omit the session property in
|
||||
that mode. One status line replaces all session controls. Explicit links and any
|
||||
other failure never select that mode. USER_VERBS and all owner transport routing
|
||||
remain byte-unchanged. Guard cases are preserved on the pre-fallback baseline; their
|
||||
RED evidence comes from the required mutants, as Sage accepted in sage-3358.
|
||||
|
||||
Fallback mutants are executable via fallback-mutants-r2.py, only on Sage's slot.
|
||||
It copies packages/roles into a private mktemp directory and mutates only the copied
|
||||
chat.js, never candidate bytes. Each run has a 300 s bound and requires a named
|
||||
assertion failure; timeout/import/setup failures do not count. F1 removes the exact
|
||||
access-refused key (human-required guard); F2 removes the omitted-selector check;
|
||||
F3 shows session controls; F4 widens the key to timeout/unreachable, no-bus-host and
|
||||
invalid-response (timeout guard). The test file also independently checks bus-error
|
||||
and malformed-answer refusal. These mutants and the post-implementation tests have
|
||||
not run locally. The earlier six non-fallback mutants in this packet remain required
|
||||
in the full gate; the new runner covers the four fallback mutants only.
|
||||
|
||||
|
||||
## Round 2 rebase receipt (sage-3354)
|
||||
|
||||
Queue rev 3510 returned row 272 to in-progress. On new branch huey/row272-5fdfd7275,
|
||||
replayed round 1 and the preserved B1/N1/N2 fixes onto 5fdfd7275 (219, after 295).
|
||||
Resolved exactly the seven named conflicts. Test append conflicts retain both complete
|
||||
sets of tests; existing test content equals the new base plus only the mos-1388 hunks.
|
||||
No old assertion or shared stub was changed beyond those approved hunks. Static byte
|
||||
comparison confirms the entire upstream test file prefixes and approved modifications.
|
||||
Both append-only logs retain the complete 5fdfd7275 prefix plus Huey's entries.
|
||||
|
||||
219's per-instance thread picker, explicit createThread instance forwarding, choice
|
||||
refresh/revocation behavior and tests remain present. 295's createChat owner-settings
|
||||
read stays unchanged: only plugins uses the owner transport; user chat verbs stay on
|
||||
the user transport. Removed a duplicate access-refused status entry introduced by
|
||||
combining those patches. USER_VERBS is unchanged. No schema or migration changed.
|
||||
|
||||
This is a pre-gate candidate per Sage's explicit instruction to commit/push/request
|
||||
round 2 before Sage runs the full gate. Only the expected-red new fallback test ran
|
||||
under sage-3358. No green test claim, no suite/mutant result and no old-file run is
|
||||
implied. Every touched test file still needs its whole-file run on Sage's slot, plus
|
||||
all touched suites and mutants. Any first-run red in launcher/creative/chat-browser
|
||||
returns to Mos via Sage before fixing. TMPDIR for gate runs comes only from mktemp -d,
|
||||
with no underscore; remove a matching compose default network only if created.
|
||||
|
||||
|
||||
## Round 3 preparation — gate run 1 (sage-3433)
|
||||
|
||||
Sage ran candidate b44332a93 over refactor 1e4c1e6e9 with the pinned esbuild
|
||||
and MOSAIC_REQUIRE_EDITOR_BUILD=1. Gate RED: bus 891/0, business 231/0,
|
||||
CLI 940/1, plugins 110/1, webui 372/20. No mutants ran. Sage reproduced
|
||||
every failing file on b44332a93 alone, excluding overlay drift as the cause.
|
||||
Evidence: /mnt/storage/scratch/sage-r272/run-*.txt and split-*.txt.
|
||||
|
||||
A: the new session CLI real-broker fixture omitted role authority. Verified
|
||||
checkBusiness requires withinRole and crossRole arrays on every role; added
|
||||
empty arrays to chief and assistant in that fixture only, as sage-3433 permits.
|
||||
No production authority or existing assertion changed. No local tests ran.
|
||||
|
||||
B remains held for Mos under mos-1316/1317: initial bare-chat rendering compares
|
||||
old?.id with undefined id, then dereferences null old at the sessions/session/key
|
||||
initializers. Sage proposes old && old.id === id at all three sites. No B edit
|
||||
made pending the ruling. C (plugins harness compactions) is separately owned.
|
||||
|
||||
Round 3 is not committed or pushed yet. The manifest path set remains the same
|
||||
17 paths; regenerate hashes when the authorized fixes are complete. Future
|
||||
launcher, creative and chat-browser runs use a TMPDIR allocated with
|
||||
mktemp -d /mnt/storage/scratch/tmp/<alnum>XXXXXX, because bwrap hides /tmp.
|
||||
Sage owns the next full gate. No tests, repo Node or queue writes here.
|
||||
|
||||
|
||||
### Round 3 candidate — mos-1455 / sage-3435
|
||||
|
||||
Mos authorized B: all three initial render comparisons now use old && old.id === id,
|
||||
so an initial list route cannot dereference null old. A remains fixture-only.
|
||||
chat-browser.test.mjs and granted-user.test.mjs are byte-identical to b44332a93.
|
||||
No pre-existing assertion, authority validator or shared stub changed.
|
||||
|
||||
Frozen manifest: manifest.r3.txt, 17 paths unchanged; SHA256 f98c5b7a884ed1e5951a584227ece16d403baf62199d405aaa1981d667e9e075.
|
||||
Parent candidate: b44332a9360504291dda3f81131c934b286f3226; base remains 5fdfd7275.
|
||||
Only session-cli.test.mjs and public/chat.js product hashes differ from round 2.
|
||||
This follow-up commit/push is authorized before the round 3 gate by sage-3435.
|
||||
No local tests, repo Node, mutants or queue writes ran. Gate remains RED until
|
||||
Sage reruns; plugins harness compactions is a separately owned dependency, and
|
||||
Mos declined an EXCEPT edit. Scrooge reviews this A/B delta; Sage posts round 3.
|
||||
|
||||
|
||||
## Round 4 — scoped browser heading waits (mos-1460/1461)
|
||||
|
||||
Round 3 gate on 72690cae7: bus 891/0, CLI 941/0, business 231/0; plugins
|
||||
had only the allowed exact base red, checked by Mos; webui 390/2. Both reds
|
||||
were newly added chat-browser missing-session waits at lines 361 and 437.
|
||||
Whole-file rerun reproduced 17/2. No round 3 green acceptance claim.
|
||||
|
||||
Diagnosis authorized by sage-3450/3453 and mos-1460: unchanged whole-file run
|
||||
17/2, 28.223 s; scratch-only CDP probes showed the route reaching chat.js:176,
|
||||
catalog rows A/B without M, and catch receiving session-not-found with status
|
||||
404. DOM had first h1 "Control board" (shell), then .chat-area h1 "Thread
|
||||
unavailable". Product refusal and painting worked. Evidence retained under
|
||||
/mnt/storage/scratch/tmp/hueyP1enyB/ (baseline.tap, probe.tap, probe-catch.tap,
|
||||
proposed.diff); scratch probe source removed, tracked tree remained clean.
|
||||
|
||||
mos-1461 / sage-3458 authorizes exactly two selector changes: document-wide
|
||||
h1 becomes .chat-area h1 in those waits. Expected text, timeout and every
|
||||
other line of chat-browser.test.mjs remain byte-identical to 72690cae7.
|
||||
This scopes the same assertion to its component; it does not weaken the
|
||||
composer/write/draft/recovery checks. No production code changes in round 4.
|
||||
|
||||
manifest.r4.txt retains the same 17 paths; SHA256 a47ed1a23fb98c1ffe6cbcc4e7130e5c515387a2c5dc41e5ae1cbf04640d8c1b.
|
||||
Only chat-browser.test.mjs hash differs from r3. New local follow-up commit
|
||||
is authorized before execution; push and single-file run await Sage CLEAR
|
||||
while Gizmoduck owns the slot. Required pass: whole chat-browser.test.mjs,
|
||||
19/19 and skipped 0, timeout -k 10 300, TMPDIR from
|
||||
mktemp -d /mnt/storage/scratch/tmp/hueyXXXXXX. No round 4 run yet.
|
||||
After green, claimant moves row to in-review; Sage posts request and
|
||||
Scrooge reviews the exact two-line delta. Full five-suite gate remains Sage's.
|
||||
@@ -0,0 +1 @@
|
||||
5fdfd7275ce3048e488d93dcdb1f9f2aa5961a69
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run only on Sage's slot. Mutate disposable sources, never the candidate tree."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
GUARD = "if (err.code === 'access-refused' && !match[3]) return null;"
|
||||
CONTROLS = " if (current.legacyDefault) return '<p role=\"status\">Sessions are not available for this account.</p>';"
|
||||
MUTANTS = {
|
||||
'F1-any-code': (GUARD, "if (!match[3]) return null;", 'human-required catalog failure'),
|
||||
'F2-explicit-link': (GUARD, "if (err.code === 'access-refused') return null;", 'granted explicit-session route'),
|
||||
'F3-controls': (CONTROLS, ' // mutant: show session controls on a legacy fallback', 'granted legacy route'),
|
||||
'F4-widened-key': (GUARD, "if (['access-refused', 'unreachable', 'no-bus-host', 'invalid-response'].includes(err.code) && !match[3]) return null;", 'timeout catalog failure'),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument('--out', type=Path, required=True)
|
||||
ap.add_argument('--mutant', choices=list(MUTANTS), action='append')
|
||||
args = ap.parse_args()
|
||||
source = Path(__file__).resolve().parents[4]
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
for name in args.mutant or MUTANTS:
|
||||
# Explicit mktemp, no underscores: same TMPDIR rule as the real gate.
|
||||
scratch = Path(subprocess.check_output(['mktemp', '-d', '/tmp/huey272-fallback-mutant-XXXXXXXX'], text=True).strip())
|
||||
try:
|
||||
for directory in ['packages', 'roles']:
|
||||
shutil.copytree(source / directory, scratch / directory)
|
||||
if (source / 'node_modules').exists():
|
||||
(scratch / 'node_modules').symlink_to((source / 'node_modules').resolve(), target_is_directory=True)
|
||||
for filename in ['package.json', 'RELEASE']:
|
||||
if (source / filename).exists():
|
||||
shutil.copy2(source / filename, scratch / filename)
|
||||
path = scratch / 'packages/webui/src/public/chat.js'
|
||||
old, new, target = MUTANTS[name]
|
||||
text = path.read_text()
|
||||
if text.count(old) != 1:
|
||||
raise RuntimeError(f'{name}: mutation anchor is not unique')
|
||||
path.write_text(text.replace(old, new, 1))
|
||||
env = dict(os.environ, TMPDIR=str(scratch))
|
||||
result = subprocess.run(['timeout', '-k', '10', '300', 'node', '--test', '--test-reporter=tap',
|
||||
'packages/webui/tests/chat-sessions-access.test.mjs'],
|
||||
cwd=scratch, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
(args.out / f'{name}.tap').write_text(result.stdout)
|
||||
killed = result.returncode == 1 and 'ERR_ASSERTION' in result.stdout and bool(
|
||||
re.search(r'not ok [^\n]*' + re.escape(target), result.stdout))
|
||||
print(json.dumps({'mutant': name, 'returncode': result.returncode, 'assertion_kill': killed}), flush=True)
|
||||
if not killed:
|
||||
raise RuntimeError(f'{name}: not an assertion kill; stop, diagnose and report before any fix')
|
||||
finally:
|
||||
# These tests never use compose. Remove the disposable copy after browser teardown;
|
||||
# retained TAP lives only in --out. No live network/volume/service cleanup is performed.
|
||||
shutil.rmtree(scratch)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,16 @@
|
||||
fb518c370aeb531220de123464f0a809156fc546b51d7195f4f9ca660409cf22 packages/bus/src/broker.mjs
|
||||
35d044b0904ce913b3356b02cdc5bf22babecf738b511dca80c75f704e4ef78e packages/bus/tests/user-principal.test.mjs
|
||||
e795bc4ee5aa038835edfeabe3498e53fdd700df478198cac6767498e2fac0b7 packages/cli/README.md
|
||||
96c4f93628964b6ea10fb87d5cf936f9ecf2e7286aa20c0914ae8b0fb32c47e7 packages/cli/src/cli.mjs
|
||||
0c31ac7260e77556f30f33a9932dc59a30f394982d8556650af7c521d2bcc947 packages/cli/src/session.mjs
|
||||
58888b7731f8009d4c3f8be2caf4445f13dd8f534e1633f6e2720b259fcc3f92 packages/cli/src/thread.mjs
|
||||
27fb7dd91007ec5f729fbe854219f423177cf4db2d44d1f8b04c7e8c5e6db90a packages/cli/tests/session-cli.test.mjs
|
||||
5e7e1ec169392ee3065f8f47d19732ebbe8bdd90f9193581425b956e66da6b73 packages/plugins/channel/cli/index.mjs
|
||||
aa086c365c4a6b0eaf84359a20ef2b30e723013ada6573fb0133163d5bf98a74 packages/plugins/channel/webui/index.mjs
|
||||
143bf601badf5cc2b25cb24368ace2ec34840abec858d272af239d6a4ddcdecb packages/plugins/tests/channel.test.mjs
|
||||
0720f66a57d965c9275f598b7c7ac438f238a0fac52808fc8f10312b85ba2727 packages/webui/src/chat-http.mjs
|
||||
65fd3fd2337aaa88245a9fb10b55fd872f9150085ff9a53fe522e758a93c64fa packages/webui/src/chat.mjs
|
||||
88dd353f5df0603ed1fd43dd543ef7b45af93fee72b6bb6a03a26c068202fbd3 packages/webui/src/public/chat.css
|
||||
074d570aee4e2b6cdcbd6b8be399a2a21e863f2d620f95dad8d93ffc7d9c46da packages/webui/src/public/chat.js
|
||||
2161c4830a3f73f0343e76521b948b933e401f7b9ba4ef8a5c75bf99c565d6b6 packages/webui/tests/chat-browser.test.mjs
|
||||
b79a8809951580187b581450cdd4012245f2d88a371bf7d2f123580af8815449 packages/webui/tests/chat.test.mjs
|
||||
@@ -0,0 +1,17 @@
|
||||
c21f1bd9af6841126baf19f5c9926ca23ba8e56b9af342373d8e4fefcee63632 packages/bus/src/broker.mjs
|
||||
3957243d59e50eaa2326b88b9205ed8e8b5d33bde2ce1ef62e68f20e56283568 packages/bus/tests/user-principal.test.mjs
|
||||
0a920131a981c2a1c9b73428a998966f446babae4dfe7d10eebdd6455571c0c9 packages/cli/README.md
|
||||
96c4f93628964b6ea10fb87d5cf936f9ecf2e7286aa20c0914ae8b0fb32c47e7 packages/cli/src/cli.mjs
|
||||
1a30048032f5bb64419ae9716a1692978bc1a137220a094d1900298aca6ba1d7 packages/cli/src/session.mjs
|
||||
58888b7731f8009d4c3f8be2caf4445f13dd8f534e1633f6e2720b259fcc3f92 packages/cli/src/thread.mjs
|
||||
359c9fbfe30d860046ee3d29d23681900d9c73b3a4c3a91cd2f4e1d2c6be143e packages/cli/tests/session-cli.test.mjs
|
||||
5e7e1ec169392ee3065f8f47d19732ebbe8bdd90f9193581425b956e66da6b73 packages/plugins/channel/cli/index.mjs
|
||||
aa086c365c4a6b0eaf84359a20ef2b30e723013ada6573fb0133163d5bf98a74 packages/plugins/channel/webui/index.mjs
|
||||
143bf601badf5cc2b25cb24368ace2ec34840abec858d272af239d6a4ddcdecb packages/plugins/tests/channel.test.mjs
|
||||
6e743878e507be20d87ae10afdb01c105f6f4fab76d02410e065f3f20d18e597 packages/webui/src/chat-http.mjs
|
||||
7a5a18a9879fe7132f20a651c6927b007475571bd8fc45314eda9d6809288cea packages/webui/src/chat.mjs
|
||||
88dd353f5df0603ed1fd43dd543ef7b45af93fee72b6bb6a03a26c068202fbd3 packages/webui/src/public/chat.css
|
||||
e4c25743a6d8cbe15eaac2b4d8c3bd309ffa5793f35584d862690a9d89725b72 packages/webui/src/public/chat.js
|
||||
1aa313f1602cf87a0ddac145bf58bbd7f1073c02009c65797e85418fdfdbd838 packages/webui/tests/chat-browser.test.mjs
|
||||
23de8678566f24a6c8bf0ca75b4550969015b06253586070e229f4eaf0bba2b4 packages/webui/tests/chat-sessions-access.test.mjs
|
||||
773538c0c6a26ab525aa30e588249ba4b4ee981129533db0348f2107da5dd1df packages/webui/tests/chat.test.mjs
|
||||
@@ -0,0 +1,17 @@
|
||||
c21f1bd9af6841126baf19f5c9926ca23ba8e56b9af342373d8e4fefcee63632 packages/bus/src/broker.mjs
|
||||
3957243d59e50eaa2326b88b9205ed8e8b5d33bde2ce1ef62e68f20e56283568 packages/bus/tests/user-principal.test.mjs
|
||||
0a920131a981c2a1c9b73428a998966f446babae4dfe7d10eebdd6455571c0c9 packages/cli/README.md
|
||||
96c4f93628964b6ea10fb87d5cf936f9ecf2e7286aa20c0914ae8b0fb32c47e7 packages/cli/src/cli.mjs
|
||||
1a30048032f5bb64419ae9716a1692978bc1a137220a094d1900298aca6ba1d7 packages/cli/src/session.mjs
|
||||
58888b7731f8009d4c3f8be2caf4445f13dd8f534e1633f6e2720b259fcc3f92 packages/cli/src/thread.mjs
|
||||
a32bfeecd8589d41b298317954fae172dc936ee0559ec37990e84ed03bd38b07 packages/cli/tests/session-cli.test.mjs
|
||||
5e7e1ec169392ee3065f8f47d19732ebbe8bdd90f9193581425b956e66da6b73 packages/plugins/channel/cli/index.mjs
|
||||
aa086c365c4a6b0eaf84359a20ef2b30e723013ada6573fb0133163d5bf98a74 packages/plugins/channel/webui/index.mjs
|
||||
143bf601badf5cc2b25cb24368ace2ec34840abec858d272af239d6a4ddcdecb packages/plugins/tests/channel.test.mjs
|
||||
6e743878e507be20d87ae10afdb01c105f6f4fab76d02410e065f3f20d18e597 packages/webui/src/chat-http.mjs
|
||||
7a5a18a9879fe7132f20a651c6927b007475571bd8fc45314eda9d6809288cea packages/webui/src/chat.mjs
|
||||
88dd353f5df0603ed1fd43dd543ef7b45af93fee72b6bb6a03a26c068202fbd3 packages/webui/src/public/chat.css
|
||||
68696f41a7335db23c0786537de76ff1202142518647d3d7b89b6b6503baf4f1 packages/webui/src/public/chat.js
|
||||
1aa313f1602cf87a0ddac145bf58bbd7f1073c02009c65797e85418fdfdbd838 packages/webui/tests/chat-browser.test.mjs
|
||||
23de8678566f24a6c8bf0ca75b4550969015b06253586070e229f4eaf0bba2b4 packages/webui/tests/chat-sessions-access.test.mjs
|
||||
773538c0c6a26ab525aa30e588249ba4b4ee981129533db0348f2107da5dd1df packages/webui/tests/chat.test.mjs
|
||||
@@ -0,0 +1,17 @@
|
||||
c21f1bd9af6841126baf19f5c9926ca23ba8e56b9af342373d8e4fefcee63632 packages/bus/src/broker.mjs
|
||||
3957243d59e50eaa2326b88b9205ed8e8b5d33bde2ce1ef62e68f20e56283568 packages/bus/tests/user-principal.test.mjs
|
||||
0a920131a981c2a1c9b73428a998966f446babae4dfe7d10eebdd6455571c0c9 packages/cli/README.md
|
||||
96c4f93628964b6ea10fb87d5cf936f9ecf2e7286aa20c0914ae8b0fb32c47e7 packages/cli/src/cli.mjs
|
||||
1a30048032f5bb64419ae9716a1692978bc1a137220a094d1900298aca6ba1d7 packages/cli/src/session.mjs
|
||||
58888b7731f8009d4c3f8be2caf4445f13dd8f534e1633f6e2720b259fcc3f92 packages/cli/src/thread.mjs
|
||||
a32bfeecd8589d41b298317954fae172dc936ee0559ec37990e84ed03bd38b07 packages/cli/tests/session-cli.test.mjs
|
||||
5e7e1ec169392ee3065f8f47d19732ebbe8bdd90f9193581425b956e66da6b73 packages/plugins/channel/cli/index.mjs
|
||||
aa086c365c4a6b0eaf84359a20ef2b30e723013ada6573fb0133163d5bf98a74 packages/plugins/channel/webui/index.mjs
|
||||
143bf601badf5cc2b25cb24368ace2ec34840abec858d272af239d6a4ddcdecb packages/plugins/tests/channel.test.mjs
|
||||
6e743878e507be20d87ae10afdb01c105f6f4fab76d02410e065f3f20d18e597 packages/webui/src/chat-http.mjs
|
||||
7a5a18a9879fe7132f20a651c6927b007475571bd8fc45314eda9d6809288cea packages/webui/src/chat.mjs
|
||||
88dd353f5df0603ed1fd43dd543ef7b45af93fee72b6bb6a03a26c068202fbd3 packages/webui/src/public/chat.css
|
||||
68696f41a7335db23c0786537de76ff1202142518647d3d7b89b6b6503baf4f1 packages/webui/src/public/chat.js
|
||||
12250a4f052489602182a49779fa67b49c7eed9d2f4c3ed7077306b2c360a344 packages/webui/tests/chat-browser.test.mjs
|
||||
23de8678566f24a6c8bf0ca75b4550969015b06253586070e229f4eaf0bba2b4 packages/webui/tests/chat-sessions-access.test.mjs
|
||||
773538c0c6a26ab525aa30e588249ba4b4ee981129533db0348f2107da5dd1df packages/webui/tests/chat.test.mjs
|
||||
@@ -0,0 +1,32 @@
|
||||
✖ 272 fallback: granted legacy route keeps default history, send and open without session controls (967.088999ms)
|
||||
✔ 272 fallback guard: granted explicit-session route refuses without default history or writes (780.341865ms)
|
||||
✔ 272 fallback guard: another catalog error never enables default-thread access (765.314522ms)
|
||||
✔ 272 fallback guard: owner route retains chooser and explicit default-session addressing (901.74851ms)
|
||||
ℹ tests 4
|
||||
ℹ suites 0
|
||||
ℹ pass 3
|
||||
ℹ fail 1
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3503.583341
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/webui/tests/chat-sessions-access.test.mjs:74:1
|
||||
✖ 272 fallback: granted legacy route keeps default history, send and open without session controls (967.088999ms)
|
||||
AssertionError [ERR_ASSERTION]: legacy granted-user thread must remain usable after session.list access-refused
|
||||
|
||||
false !== true
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/huey-system-manager-design/packages/webui/tests/chat-sessions-access.test.mjs:77:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: false,
|
||||
expected: true,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# Row 300 build notes (Launchpad): R6-N1, an HTTP-level test of the user-session option swap (#1807)
|
||||
|
||||
Brief: docs/plans/2026-10-23_r6-followup-rows.md § Brief: R6-N1. Reviewer Duckworth, gate owner Sage.
|
||||
Base c13c337e1 (refactor, after row 294 landed at d3059c2c3). Branch launchpad/row300.
|
||||
|
||||
## Change
|
||||
|
||||
Tests only. One new file, packages/webui/tests/user-option-swap.test.mjs. No product change and no
|
||||
existing assertion or fixture touched.
|
||||
|
||||
- **Gate stubbed open.** The file registers an in-thread load hook (`registerHooks`, as
|
||||
thread-claims.test and library-cli.test do through `--import`) keyed on the one URL
|
||||
`serve.mjs?gate=open`. On that URL alone it prepends `return true;` to `userRoute`'s body, then
|
||||
imports `startServer` from it. Every other import of serve.mjs, and every relative import inside
|
||||
the copy, is the real module. If the gate's signature line is not in serve.mjs exactly once, the
|
||||
hook throws. A renamed gate then fails loudly, instead of quietly testing the closed gate. The
|
||||
test also asserts that the copy's `userRoute` answers true for `POST /api/bus/message`.
|
||||
- **Setup.** This is granted-user.test.mjs's setup, cut to what the test needs: the fake OP, the
|
||||
in-process broker for `demo`, createSso with the access hook, and `userOptionsFor` over alice's
|
||||
transport. Every owner option is a stub that records its own name. The owner transport records
|
||||
`['transport', verb, args]`. One list, `owner`, holds both kinds of owner call.
|
||||
- **Positive control.** The owner signs in and posts `/api/bus/message`. Exactly
|
||||
`['option', 'send']` must be recorded, so the route reaches `send` and the count works.
|
||||
- **The check.** Alice signs in and walks G4's owner routes, copied, plus two chat reads. No answer
|
||||
may say `owner-only`, so the gate really is open. After each route, every owner record must be
|
||||
`['transport', 'plugins', {}]`, the read mos-1394 allowed. The message route must reach the user
|
||||
transport (`message.send`), and every transport opened is alice's.
|
||||
|
||||
Why a new file: granted-user.test.mjs's setup imports startServer statically. Passing the gate-open
|
||||
copy into it would change that fixture, and a fixture change goes to Mos. The route list and setup
|
||||
are copied, with a comment naming their source.
|
||||
|
||||
## Mutant
|
||||
|
||||
M12: serve.mjs:689 drops `send` from the user-session destructure, so a user session keeps the
|
||||
owner's `send`. Killed when "R6-N1" reports not ok. Expected failure: on `POST /api/bus/message`,
|
||||
the owner record holds `['option', 'send']`. run1/mutants.py applies it (anchor exactly once), runs
|
||||
the new file alone, restores with git checkout and checks the tree is clean.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves the manifest. On Sage's CLEAR: run1/gate.sh does npm ci (root and webui), the new
|
||||
file whole, the webui suite whole, then M12. Logs go to /mnt/storage/scratch/launchpad-r300/run1,
|
||||
outside the tree. Not run yet.
|
||||
|
||||
MANIFEST.sha256 lists the one new path.
|
||||
|
||||
## Run 1 (sage-3408 CLEAR, 12:05Z)
|
||||
|
||||
npm ci root and webui were green. The new file was red on its first run (mos-1272, placed by me):
|
||||
`TypeError: Cannot read properties of undefined (reading 'set-cookie')` in signIn. When I cut down
|
||||
granted-user.test.mjs's `call`, I dropped `headers: res.headers` from what it resolves, so the
|
||||
sign-in found no cookies. The fix puts `headers` back, the same as granted-user.test.mjs:40. No
|
||||
assertion changed. The new file then passed alone (1/0). The fix is a new commit, and the manifest
|
||||
is re-hashed. The gate resumes at new-file on the new head.
|
||||
|
||||
Run 1 resumed (TAG -r2) on db5122920, manifest db8be52ce895603e687985acfae29e53e3e8782ae945c3cded6cca36141bfa3b:
|
||||
|
||||
- new file whole: 1 pass, 0 fail.
|
||||
- webui suite whole: 375 pass, 0 fail, 0 skipped, 0 cancelled, 58.2 s (limit 600).
|
||||
- M12 killed. "R6-N1" reports not ok at :151: `POST /api/bus/message: ["option","send"]`, where
|
||||
`["transport","plugins",{}]` is expected. The restore left the tree clean.
|
||||
|
||||
Duckworth N1 (duckworth-0263, non-blocking): the setup passes no `upstream`, so the swap's
|
||||
`upstream = null` has no recorder here. The test proves the swap for the options it stubs. It
|
||||
proves nothing about `upstream`.
|
||||
@@ -0,0 +1 @@
|
||||
03b5d60f68ea2bddd779e1df19519855a5f01cc28ec9ddb26ee7c45fcfe75cf4 packages/webui/tests/user-option-swap.test.mjs
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 300 gate run 1 (brief R6-N1): npm ci (root and packages/webui), the new test file whole, the
|
||||
# webui suite whole, then mutant M12. Stops at the first red. FROM=<step> resumes at a step.
|
||||
set -u -o pipefail
|
||||
WT=/mnt/storage/scratch/launchpad-r300/wt
|
||||
OUT=$WT/agents/launchpad/work/queue-300/run1
|
||||
LOG=/mnt/storage/scratch/launchpad-r300/run1
|
||||
FROM=${FROM:-npm-root}; TAG=${TAG:-}
|
||||
mkdir -p $LOG
|
||||
export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/gXXXXXX)
|
||||
echo "start $(date -u +%H:%M:%SZ) FROM=$FROM TAG=$TAG TMPDIR $TMPDIR head $(git -C $WT rev-parse HEAD)" >> $LOG/summary.txt
|
||||
[ -z "$(git -C $WT status --porcelain)" ] || { echo "STOP worktree not clean" | tee -a $LOG/summary.txt; exit 9; }
|
||||
on=0
|
||||
step() { # name limit dir cmd...
|
||||
local name=$1 limit=$2 dir=$3; shift 3
|
||||
[ $on = 1 ] || [ $name = $FROM ] || return 0; on=1
|
||||
local t0=$(date +%s%N)
|
||||
( cd $dir && timeout -k 10 $limit "$@" ) > $LOG/$name$TAG.log 2>&1
|
||||
local rc=$?
|
||||
local ms=$(( ($(date +%s%N) - t0) / 1000000 )); local dt=$((ms/1000)).$(printf %03d $((ms%1000)))
|
||||
echo "$name$TAG rc=$rc seconds=$dt limit=$limit $(date -u +%H:%M:%SZ)" | tee -a $LOG/summary.txt
|
||||
[ $rc -eq 0 ] || { echo "STOP at $name$TAG" | tee -a $LOG/summary.txt; exit $rc; }
|
||||
}
|
||||
step npm-root 900 $WT npm ci --ignore-scripts
|
||||
step npm-webui 900 $WT/packages/webui npm ci --ignore-scripts
|
||||
step new-file 300 $WT/packages/webui node --test tests/user-option-swap.test.mjs
|
||||
step webui 600 $WT/packages/webui node --test tests/
|
||||
[ -z "$(git -C $WT status --porcelain)" ] || { echo "STOP worktree dirty after the suite" | tee -a $LOG/summary.txt; git -C $WT status --porcelain >> $LOG/summary.txt; exit 9; }
|
||||
echo "SUITES GREEN $(date -u +%H:%M:%SZ)" | tee -a $LOG/summary.txt
|
||||
python3 -B $OUT/mutants.py
|
||||
rc=$?; echo "MUTANTS rc=$rc $(date -u +%H:%M:%SZ)" | tee -a $LOG/summary.txt; exit $rc
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
# Row 300 mutant M12 (BUILD.md "Mutant"): serve.mjs keeps the owner's send for a user session. The
|
||||
# edit must match exactly once; the new file runs alone under timeout -k 10 300 with the TAP
|
||||
# reporter; git checkout restores and the tree must be clean. Killed = "R6-N1" reports "not ok".
|
||||
import json, os, re, subprocess, sys, time
|
||||
|
||||
WT = '/mnt/storage/scratch/launchpad-r300/wt'
|
||||
LOG = '/mnt/storage/scratch/launchpad-r300/run1'
|
||||
SWAP = "({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));"
|
||||
MUTANTS = [
|
||||
('M12', 'packages/webui/src/serve.mjs', SWAP,
|
||||
"({ bus, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));",
|
||||
('packages/webui', 'tests/user-option-swap.test.mjs'), 'R6-N1'),
|
||||
]
|
||||
|
||||
def git(*a):
|
||||
return subprocess.run(['git', '-C', WT, *a], capture_output=True, text=True, check=True).stdout
|
||||
|
||||
def main():
|
||||
if git('status', '--porcelain'): raise SystemExit('worktree not clean at start')
|
||||
summary, bad = [], 0
|
||||
for mid, path, old, new, (pkg, test), killer in MUTANTS:
|
||||
p = os.path.join(WT, path)
|
||||
try:
|
||||
s = open(p).read()
|
||||
if s.count(old) != 1: raise SystemExit(f'{mid}: anchor count {s.count(old)} in {path}')
|
||||
open(p, 'w').write(s.replace(old, new))
|
||||
diff = git('diff')
|
||||
t0 = time.time()
|
||||
r = subprocess.run(['timeout', '-k', '10', '300', 'node', '--test', '--test-reporter=tap', test],
|
||||
cwd=os.path.join(WT, pkg), capture_output=True, text=True)
|
||||
dt = round(time.time() - t0, 1)
|
||||
finally:
|
||||
git('checkout', '--', path)
|
||||
if git('status', '--porcelain'): raise SystemExit('worktree not clean after restore')
|
||||
open(f'{LOG}/{mid}.diff', 'w').write(diff)
|
||||
open(f'{LOG}/{mid}.tap', 'w').write(r.stdout + '\n# stderr\n' + r.stderr)
|
||||
res = {m.group(2): m.group(1) == 'ok' for m in re.finditer(r'^(ok|not ok) \d+ - (.*)$', r.stdout, re.M)}
|
||||
hit = [n for n in res if n.startswith(killer)]
|
||||
killed = r.returncode not in (0, 124, 137) and bool(hit) and all(not res[n] for n in hit)
|
||||
bad += not killed
|
||||
print(f'{mid} rc={r.returncode} seconds={dt} killed={killed} red={[n for n, ok in res.items() if not ok]}', flush=True)
|
||||
summary.append({'mutant': mid, 'rc': r.returncode, 'seconds': dt, 'killed': killed, 'killer': killer})
|
||||
json.dump(summary, open(f'{LOG}/mutants.json', 'w'), indent=1)
|
||||
sys.exit(1 if bad else 0)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,67 @@
|
||||
# Row 301 build notes (Launchpad): R6-N4, fold user-bus.mjs back into bus.mjs (#1808)
|
||||
|
||||
Brief: docs/plans/2026-10-23_r6-followup-rows.md § Brief: R6-N4. Reviewer Duckworth, gate owner Sage.
|
||||
Base e551cd846 (refactor after row 303). Branch launchpad/row301.
|
||||
|
||||
## Change
|
||||
|
||||
- packages/webui/src/bus.mjs: the user transport moves in beside humanCall, after `busReader`:
|
||||
`USER_CLI`, `userCall`, `busBusiness` and `userCaller`, byte for byte from user-bus.mjs (checked
|
||||
with diff), under a three-line header. One import is added, `fileURLToPath` from node:url.
|
||||
`readHostState`, `spawn`, `randomBytes` and `BusReadError` were already in bus.mjs. humanCall and
|
||||
busCaller are untouched.
|
||||
- packages/webui/src/user-bus.mjs: deleted.
|
||||
- packages/webui/src/cli.mjs: the two imports become one, from './bus.mjs'.
|
||||
- packages/webui/src/console-bus.mjs: a comment names bus.mjs instead of user-bus.mjs.
|
||||
- packages/webui/tests/granted-user.test.mjs:22-23: the two imports become one, from '../src/bus.mjs'.
|
||||
Only the import path changes. Mos ruled (mos-1449, via sage-3399) that this is covered by "update
|
||||
its imports" and is not an old-test change.
|
||||
|
||||
No other reference to user-bus.mjs remains under packages/. tools/agent-watch's "user-bus" means
|
||||
something unrelated. The 294 packet's mutants name the old file as history only.
|
||||
|
||||
## Re-pin
|
||||
|
||||
channel-proof.test.mjs:28 pinned packages/webui/src/bus.mjs at 15e8123f…3313. After the fold the
|
||||
file hashes to 39fcb27c82e917ef05e1a7a650d522c9f3c656bf8dbade87b8ddb3661b67ad6c. A pin change is an
|
||||
old-test change. Mos approved it under mos-1272 in mos-1449 (via sage-3399), as its own commit, with
|
||||
row 301 making its own re-pin window. Mos checked fd4b8c36e read-only: bus.mjs hashes to 39fcb27c,
|
||||
its diff removes no lines, user-bus.mjs from `export const USER_CLI` to the end is in it byte for
|
||||
byte, and nothing else imports user-bus.mjs. The second commit changes that one value and adds one
|
||||
comment line in the row 165/171/194 style.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves the manifest. channel-proof.test, the webui suite and the 294 user-transport
|
||||
tests (granted-user.test.mjs) run whole and green, on Sage's CLEAR.
|
||||
|
||||
MANIFEST.sha256 lists the changed paths. DELETED.txt names the one removed path.
|
||||
|
||||
## Run 1 (sage-3408 CLEAR, 12:08Z, head 04a179740)
|
||||
|
||||
npm ci root and webui green. channel-proof.test whole 2 pass 0 fail. The webui suite whole went
|
||||
374 tests, 372 pass, 2 fail, 0 skipped, so the gate stopped. granted-user.test whole, run after the
|
||||
stop to see whether the stop hid another red: 19 pass 0 fail. No file changed.
|
||||
|
||||
Both reds were in old tests: library-cli.test.mjs:10 (C2-04) and thread-claims.test.mjs:54 (row278
|
||||
Console adapter). Each failed with `SyntaxError: The requested module './bus.mjs' does not provide an
|
||||
export named 'busBusiness'` at cli.mjs:5. Each test spawns cli.mjs with a registerHooks load hook
|
||||
that replaces /webui/src/bus.mjs with a fixture exporting only BusReadError and busCaller. Before
|
||||
the fold, cli.mjs took busBusiness and userCaller from the real user-bus.mjs. Now they come from the
|
||||
stubbed bus.mjs, so cli.mjs fails to link. cli.mjs calls them only under --sso (:98, :103), and
|
||||
neither test passes --sso. A URL-keyed stub is not an import, so the "nothing else imports
|
||||
user-bus.mjs" check and the static reviews missed it. `git grep "webui/src/bus.mjs" --
|
||||
'packages/*/tests/*'` finds exactly channel-proof, library-cli and thread-claims.
|
||||
|
||||
## Fixture change (mos-1451 under mos-1272, via sage-3418)
|
||||
|
||||
Mos approved option A with one change and refused option B, a namespace import in cli.mjs. Each
|
||||
of the two busSource template strings (library-cli.test.mjs:17, thread-claims.test.mjs:58) gets
|
||||
this appended, and nothing else changes in either file:
|
||||
|
||||
export const busBusiness=()=>{throw Error('busBusiness outside --sso')};export const userCaller=()=>{throw Error('userCaller outside --sso')};
|
||||
|
||||
The stubs throw rather than return null, so a test that reaches them outside --sso fails loudly.
|
||||
The text has no backtick, `${` or backslash, so the template literal carries it unchanged. The
|
||||
manifest goes from five paths to seven. Sage's gate: webui 374/0, channel-proof 2/0,
|
||||
granted-user 19/0. Any other red stops the gate and goes to Mos.
|
||||
@@ -0,0 +1 @@
|
||||
deleted packages/webui/src/user-bus.mjs
|
||||
@@ -0,0 +1,7 @@
|
||||
db1f05e312b9bee7ece7df7df78047a2965024109a297cb8a24112135a6e5f04 packages/bus/tests/channel-proof.test.mjs
|
||||
39fcb27c82e917ef05e1a7a650d522c9f3c656bf8dbade87b8ddb3661b67ad6c packages/webui/src/bus.mjs
|
||||
0862b2b5bf6511d2c5deca1f318eee643edaee489c0ce8c83e57364f2f54a3d8 packages/webui/src/cli.mjs
|
||||
653d2087c0796a35c3884b7243d1adf8a78c52950a4924049317063f2d3c0974 packages/webui/src/console-bus.mjs
|
||||
34b6d052855369c8f7cf1faf8bab3c59ddf96ccd4c42731bd86a4e61e5e70640 packages/webui/tests/granted-user.test.mjs
|
||||
7e782c2b6b46b6b8b6e58cd3d148bf041d49e8fbf2cbbed51080cdb02fc11d89 packages/webui/tests/library-cli.test.mjs
|
||||
dd1e62d14a59bb8fe7cd4654dd1de8e518270c36d104ee356e165faee7f95a02 packages/webui/tests/thread-claims.test.mjs
|
||||
@@ -0,0 +1,8 @@
|
||||
# Lena
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 296 (FED R7, handed over from Quackerjack).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread db27faca) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `lena/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -0,0 +1,92 @@
|
||||
# Row 298 (#1804, Q228): the chief-of-staff grant scope
|
||||
|
||||
Owner Megavolt, reviewer Dewey. Assigned in sage-3584 (mos-1489), 2026-10-23 18:53Z.
|
||||
Base: origin/refactor 45dca69f6. Branch: megavolt/row298.
|
||||
Brief: docs/plans/2026-10-23_mvp-readiness-rows.md, "Brief: the chief-of-staff grant scope (Q228)".
|
||||
Decision: DECISIONS Q228 (jason-master-restart DECISIONS.md:2278). It replaces Mos's Q190 reading.
|
||||
|
||||
## What exists at 45dca69f6 (broker.mjs)
|
||||
|
||||
- Raise: `#raiseGrant` (:2558) is chief-only (`grant-chief-only`) and binds a grant body per method:
|
||||
gitea `#giteaGrantBinding` (:2665), host `#hostGrantBinding` (:2599), or host-enroll `enrollBinding`.
|
||||
The decision goes through `#dispatch(..., 'decision.raise', ...)`, routed to the human.
|
||||
- Approval: `#approvedGrant` (:1525) returns the binding only for a credential.mint routed to the
|
||||
human and closed with `approvalChoice` by the business's human, via 'cli', or via 'console' with
|
||||
the broker's own human.input (row 252).
|
||||
- Start: decision.resolve (:5653-5659) runs `#approvedGrant` after the close and, once the transaction
|
||||
commits, starts `#runEnrollment`, `#runHostGrant` or `#startGitea` with setImmediate.
|
||||
- Spend and records: each executor calls `#consumeGrant` and writes the 236 records (grant.call,
|
||||
grant.result, grant.step, grant.applied or grant.failed, gitea_grants or host_grants). Revoke
|
||||
goes through grants.revoke and hosts.revoke, as before.
|
||||
- Human-only settings: `#human(s)` (cli, outsideAgent) plus `#mayHuman`, as in veto.set (:4713).
|
||||
The organization file is checked whole by business.mjs `checkLaunch`, and a bad file refuses
|
||||
every read.
|
||||
|
||||
## Proposed design (waits on Sage's rulings, Q1-Q6 below)
|
||||
|
||||
D1. Storage. The scope gets its own key, `launch.grantScope`, in the organization file. The
|
||||
whole-file check doesn't refuse on it. A separate reader, `grantScope(document)`, returns the
|
||||
parsed scope or, when the key is missing or bad, the empty scope `{}`, plus a `fallback` flag.
|
||||
A bad value can't break every organization read, and it falls back to "everything needs
|
||||
Approve", which is the fail-closed direction. (Q1)
|
||||
D2. Shape. `{ gitea?: { agents: [id], projects: [id] }, host?: { agents: [id], hosts: [id],
|
||||
commands: [[argv prefix]] } }`. A request is in scope only if its method has an entry, its agent
|
||||
is listed, and every target is listed: each gitea project, the host, and each command prefix
|
||||
equal to a listed prefix. host-enroll is never in scope, because it adds a host to the registry.
|
||||
(Q2)
|
||||
D3. Writer. A new verb, `grants.scope.set { revision, scope }`, CLI-only for the human: `#human(s)`,
|
||||
then `#mayHuman`, with the verb added to LIFECYCLE_HUMAN_VERBS. The tester, the reader and every
|
||||
agent are refused. organization.set's ORGANIZATION_KEYS don't include it, so the chief's tool
|
||||
never reaches it, the same way instances.set and veto.set stay human-only. The write validates
|
||||
strictly and refuses a bad value; only the read falls back. Receipt: `grants.scope` with the old
|
||||
and new sha256. `grants.scope` reads the value for the human, the tester and the chief. (Q3)
|
||||
D4. In-scope raise. Inside the raise transaction, `#raiseGrant` checks the binding against the scope
|
||||
and, if it's in scope, closes the new decision at once: op resolved, choice approvalChoice, by
|
||||
`grant-scope`, via `scope`. It writes a `grant.scope.used` event carrying the scope sha256 and
|
||||
revision. `#approvedGrant` accepts via 'scope' only when that event exists for the close's seq
|
||||
and its sha256 matches the scope as read in the same transaction. After the commit, the same
|
||||
setImmediate starts the same executor, so 236's spend, records and revoke are unchanged. The
|
||||
decision is closed before any inbox read, so no inbox item ever shows. (Q4)
|
||||
D5. Out-of-scope raise: unchanged. The decision stays open, routed to the human, for the Console
|
||||
and CLI Approve.
|
||||
D6. Executor recheck. At the spend, `#consumeGrant` reruns the scope check for a via-'scope'
|
||||
approval. If the scope changed between raise and run and the grant is now out of scope, it
|
||||
fails with `grant.failed scope-changed` and the grant is spent. (Q5)
|
||||
|
||||
## Open questions for Sage
|
||||
|
||||
Q1. Storage: `launch.grantScope` in the organization file with a lenient reader (D1), or a separate
|
||||
file under the business directory?
|
||||
Q2. Shape: is per-method agent, project, host and command lists right? Should host-enroll stay
|
||||
out of scope always?
|
||||
Q3. "A scope change needs Approve": is the human's CLI verb itself the Approve (D3)? Or must the
|
||||
chief also be able to raise a scope change that Jason approves in the inbox (a new decision
|
||||
action, more code)? I recommend D3 alone. Should Console get a scope page in this row? I
|
||||
propose no.
|
||||
Q4. Recording an in-scope grant as a broker close via 'scope' with a scope event (D4), versus a
|
||||
decision that is never raised (a new table). D4 keeps 236's records and revoke unchanged.
|
||||
Q5. Scope narrowed between raise and run: fail scope-changed (D6), or honor the raise-time check?
|
||||
Q6. Chief enrollment ("Jason's Approve stays for the chief of staff's enrollment"): I read this
|
||||
as the chief's own instance and credentials, which the human's instances.set and the
|
||||
credential lifecycle already cover. So nothing new is needed in this row. Agree?
|
||||
|
||||
## Tests (access by brief; all on scratch stores, fake Gitea, fake host)
|
||||
|
||||
T1 agent, chief and tester callers of grants.scope.set are refused, and organization.set with
|
||||
grantScope is refused. T2 a bad or missing grantScope falls back to empty and the grant waits.
|
||||
T3 an in-scope gitea request issues with no open decision and no inbox item, and writes the 236
|
||||
records. T4 the same for a host request. T5 out of scope by agent, by project, or by command
|
||||
waits for Approve. T6 host-enroll always waits. T7 a scope change by the human's CLI writes a
|
||||
receipt; a scope change by any other path is refused. T8 an in-scope grant is logged and revoked
|
||||
with grants.revoke or hosts.revoke. T9 scope narrowed before run gives scope-changed.
|
||||
|
||||
## Mutants (brief)
|
||||
|
||||
M1 the scope check skipped (every chief raise auto-closes): killed by T5 and T6.
|
||||
M2 the fallback skipped (a bad value is used as-is or throws): killed by T2.
|
||||
M3 the `#human` check removed from grants.scope.set: killed by T1.
|
||||
|
||||
## Holds
|
||||
|
||||
Darkwing holds the slot: no push, no claim, no queue write, no node and no test run until Sage
|
||||
says so. Q188: no agent creates a grant; tests use scratch stores only.
|
||||
@@ -0,0 +1,205 @@
|
||||
# Row 306: a revoked run's late bus call stops it instead of exiting 22 (#1813, 295 N1)
|
||||
|
||||
Owner Morgana. Reviewer Dewey. Gate owner Sage. Brief: docs/plans/2026-10-23_gate-env-rows.md, third
|
||||
section (c16e943b9). Go: sage-3478 (Mos-1453 puts 306 next). Base: refactor fe4ee8c7b, after row 272
|
||||
(d33989230) and row 307 (d243f6b25). Every line below is as it stands at fe4ee8c7b.
|
||||
|
||||
## Design round 1 (no code yet), round 2 edits marked r2
|
||||
|
||||
### The window today
|
||||
|
||||
- `#revokeLost` (packages/bus/src/broker.mjs:1012-1029) ends each lost user thread's held claim in
|
||||
one transaction: `#claimEnd(..., 'revoke', null, 'access-revoked', by)` at :1022. The four
|
||||
callers are the timer `accessSweep` (:1039), a user's request (:4542), `access.reload` (:4561)
|
||||
and `threadsState` (:1107 and the access-expiry sweep at :1113).
|
||||
- The launcher learns of the loss from `threadsState`'s `revoked: true` row, records
|
||||
`thread-revoked` and sends SIGTERM (packages/cli/src/launcher.mjs:710-718).
|
||||
- A runner call between that claim end and the signal goes through `#agent` (:1064-1072). The
|
||||
run's latest claim row is now op `revoke`, so the check at :1069 fails. :1070 answers
|
||||
`launcher-released` only for a run in `#launcherReleased` (:431, row 302), and `not-holder`
|
||||
otherwise.
|
||||
- The runner's `ended()` (packages/harness/src/runner.mjs:488-500) counts `not-holder` as a gone
|
||||
session (`SESSION_GONE`, :55). It sets `code = EXIT.session` and the run exits 22 with
|
||||
`session-invalid`.
|
||||
|
||||
### D1: the broker remembers the revoked run in memory, the same way as #launcherReleased
|
||||
|
||||
- A new field next to `#launcherReleased` (:431): `#accessRevoked = new Set()`, keyed
|
||||
`business:run`, with a comment naming row 306.
|
||||
- `#revokeLost` adds each ended claim's `h.business + ':' + h.holder_run`. It adds them after the
|
||||
transaction at :1018-1026 commits, by iterating `ended`, so a transaction that throws leaves the
|
||||
Set unchanged. This is the only place the Set grows. No other `#claimEnd` caller adds to it:
|
||||
`assistant.stop` (:6078), `role.revoke`/`role.end` (:5333, :5338), `instance.fire` (:4780), the
|
||||
limit stop (:4949), `threadIdle` (:1133) and `landRelease` do not.
|
||||
- `#agent`'s refusal at :1070 becomes three-way, and only on the existing failure branch:
|
||||
`launcher-released` if the run is in `#launcherReleased`, else `thread-revoked` if it is in
|
||||
`#accessRevoked`, else `not-holder`. A run that still holds its claim never reaches the branch.
|
||||
The two sets don't overlap. A released run holds no claim for the sweep to end, and a revoked run
|
||||
holds none for `threadIdle` or `landRelease` to release (both refuse `not-holder` first, :1131 and
|
||||
:4237).
|
||||
- **Where the memory ends.** `endLaunch` deletes the key next to `#launcherReleased.delete(key)` at
|
||||
:1394. That is also where every capability of the run is dropped (:1395-1396), so after it no call
|
||||
of the run reaches `#agent`.
|
||||
- **The alternative I didn't take.** Derive the answer from the store: the run's latest claim row is
|
||||
op `revoke` with reason `access-revoked`. It would survive a broker restart. But `reason` is
|
||||
caller text at :4780, :5333 and :5338, so a human's `role.revoke` with reason `access-revoked`
|
||||
would turn that run's `not-holder` into `thread-revoked`, a stopped exit. `by` doesn't separate the
|
||||
cases either: `access.reload` passes the human's name (:4561). The Set can't be spoofed that way,
|
||||
and it matches row 302's shape. The cost is N1 below.
|
||||
- **Unchanged.** `role.claim` keeps its order: `launcher-released` (:5252), then `run-revoked` for a
|
||||
run with any revoke row in its session (:5257-5258), then `thread-revoked` from the file (:5259).
|
||||
`bindLaunch` keeps refusing `thread-revoked` for a new run of a lost thread (:1371). The limit
|
||||
stop's `not-holder` (:4952) is unchanged.
|
||||
|
||||
### D2: the runner maps thread-revoked from a call to stopped
|
||||
|
||||
- runner.mjs gets `THREAD_REVOKED = "thread-revoked"` next to `LAUNCHER_RELEASED` (:58), with a
|
||||
comment naming row 306.
|
||||
- In `ended()` (:491-500), `thread-revoked` takes the launcher-released branch:
|
||||
- it logs its own line, `runner: thread revoked by the access file`;
|
||||
- it sets `released = true` and returns true;
|
||||
- `code` stays `EXIT.stopped`.
|
||||
|
||||
`released` also skips the exit-time `role.release` (:596-602), which is right because the sweep
|
||||
already ended the claim.
|
||||
- The refusal of the run's first `role.claim` (:459-467) is unchanged. `thread-revoked` there stays
|
||||
`EXIT.claim` (21), because the brief maps it only "from a call" after the claim. Mostly
|
||||
`bindLaunch` refuses such a run before it starts.
|
||||
- `ended()` covers every call site in the loop: landing `state` (:521), `message.receive` (:543),
|
||||
the turn's `state` (:561) and the reply's `message.send`/`message.read` (:589). The harness's tool
|
||||
calls go through the tool server and return their error to the harness, as they do for
|
||||
`launcher-released` today. The run stops at its next own call.
|
||||
|
||||
### D3: docs
|
||||
|
||||
- packages/bus/README.md:328: "the run's next bus call refuses `not-holder`" becomes
|
||||
`thread-revoked`, until the run ends (row 306).
|
||||
- packages/harness/README.md:296: exit 0 adds `thread-revoked` next to `launcher-released`.
|
||||
- The `#revokeLost` comment at broker.mjs:1008 changes the same way as the bus README.
|
||||
|
||||
## Tests (red first under mos-1272)
|
||||
|
||||
### New file packages/bus/tests/revoke-window.test.mjs
|
||||
|
||||
It uses access-revoke.test.mjs's fixture shape, copied, because that file is an old test.
|
||||
|
||||
- **T1.** A run the timer sweep ended answers `thread-revoked` on `message.receive`, `state`,
|
||||
`message.read` and `message.send`, and each answer is logged as `action.refused` with code
|
||||
`thread-revoked`. Its `role.claim` still refuses `run-revoked`. The same holds when the trigger is
|
||||
a user's request or `access.reload`, one assert each, because all three go through `#revokeLost`.
|
||||
- **T2, the control.** In the same broker, after the sweep ended the first run:
|
||||
- a second user thread's run whose claim the human's `assistant.stop` ended answers `not-holder`;
|
||||
- a third run with a pending limit stop answers `not-holder` on its first `message.receive`, and
|
||||
then `not-holder` again on its next call (r2, Dewey C2). The first answer comes from
|
||||
`#limitStop` (:4952) and never reaches `#agent`, so only the second call checks the Set.
|
||||
|
||||
### New file packages/cli/tests/revoke-window.test.mjs
|
||||
|
||||
thread-revoke.test.mjs's `start()`, copied the way idle-stop-race.test.mjs copied thread-launch's,
|
||||
with the `threadsState` op replaced by the caller.
|
||||
|
||||
- **T3, the forcing test.**
|
||||
- Setup: a user thread A owned by alice and a human-owned thread B. Both get a message and a
|
||||
RESULT, so both runs hold.
|
||||
- Trigger: a hand edit drops alice's grant.
|
||||
- The hold: the replaced `threadsState` op calls `runtime.threadsState`. That call's sweep ends A's
|
||||
claim. When the rows include A as `revoked`, and before returning them to the launcher, the op
|
||||
has the human `assistant.stop` the instance's human-owned threads (B), then waits. It waits until
|
||||
A's runner has written its verdict line and the launch log has A's `end`, then the same for B.
|
||||
The launcher doesn't tick while a pass is running, so nothing signals either run while the op
|
||||
is held.
|
||||
- Asserts for A:
|
||||
- the verdict is `thread revoked by the access file`;
|
||||
- the end is `["stopped", 0, null]`;
|
||||
- the launch log has no `thread-revoked` event for A's run (r2, Dewey C1). The launcher
|
||||
removes A from the registry before it records `end` (packages/cli/src/launcher.mjs:643-646),
|
||||
so when the held op returns, `registry.running` at :705 finds nothing and :711-717 record
|
||||
and signal nothing. The runner stopped by itself before any SIGTERM;
|
||||
- the runner log has no `session stopped working` and no `role.release:`, and ends with
|
||||
`exiting 0`;
|
||||
- the store's `action.refused` codes for A include `thread-revoked` and not `not-holder`.
|
||||
- Asserts for B, the control:
|
||||
- the verdict is `session stopped working: not-holder`;
|
||||
- the end is `["session-invalid", 22, null]`;
|
||||
- B's codes include `not-holder`.
|
||||
- Every wait returns what it last saw, so a mutant fails on an assertion, not a timeout
|
||||
(sage-2707).
|
||||
|
||||
## Mutants
|
||||
|
||||
| # | Edit | Must turn red |
|
||||
|---|------|---------------|
|
||||
| M1 | `#agent` drops the `#accessRevoked` check: the failure answers `launcher-released` or `not-holder` | T1, T3 (A gets 22) |
|
||||
| M2 | runner `ended()` maps `thread-revoked` to `EXIT.session` (it moves into `SESSION_GONE`) | T3 (A exits 22) |
|
||||
| M3 | every ended thread claim marks its run revoked: `#claimEnd` adds `s.business:h.holder_run` to `#accessRevoked` when `h.thread` (anchor `if (h.thread)`, one hit at :6495) | T2 (both halves, the limit half on its second call), T3 (B gets thread-revoked) |
|
||||
|
||||
Each anchor must hit exactly once. The kit prints the count, as row 295's M8 receipt did.
|
||||
|
||||
## Expected first-run reds
|
||||
|
||||
- **New tests (mos-1272).** On a red-first run against fe4ee8c7b, T1 and T3 are red: A gets
|
||||
`not-holder` and exits 22. T2 is green on both sides. Any other first-run red in T1-T3 I place
|
||||
myself and report after.
|
||||
- **Old test that changes: Mos GO (mos-1463, via sage-3483).** In
|
||||
packages/bus/tests/access-revoke.test.mjs (row 295), `not-holder` becomes `thread-revoked` in the
|
||||
six asserts at :103, :120, :140, :171, :201 and :262, and the :95 title's "refuses not-holder"
|
||||
becomes "refuses thread-revoked". Nothing else in the file changes; `git diff --numstat` for it
|
||||
reads `7 7`. Dewey checks that in the manifest round. At fe4ee8c7b those are the only seven
|
||||
`not-holder` lines in the file.
|
||||
- **Checked and not expected to change.**
|
||||
- thread-revoke.test.mjs:180 asserts `["stopped", 0, null]` for a run that only the SIGTERM ends.
|
||||
If its runner's poll happened to land in the window, it would get exit 0 now, where today it
|
||||
would get 22, so the assert holds either way.
|
||||
- idle-stop-race.test.mjs's control is a human `assistant.stop`, not a revoke, so it stays
|
||||
`not-holder`/22. M3 would turn it red too.
|
||||
- A `git grep` of `packages/*/tests/*` finds `not-holder` after a revoke only in
|
||||
access-revoke.test.mjs.
|
||||
|
||||
## Gate (per the brief)
|
||||
|
||||
Dewey approves this design, then the manifest. On Sage's gate (mos-1455 D4, sage-3483/3484):
|
||||
- the new files and access-revoke.test.mjs whole;
|
||||
- the bus, cli, plugins, webui, business and harness suites;
|
||||
- every `scripts/test-*.sh` under `timeout -k 10` at three times its longest run (mos-1275);
|
||||
- M1-M3.
|
||||
|
||||
Every suite shows 0 failures. Any red follows mos-1272.
|
||||
|
||||
## N items
|
||||
|
||||
Both stay out of this row (sage-3483); Sage adds them to DEFERRED at landing. Dewey agrees on N1
|
||||
(dewey-0918).
|
||||
|
||||
- N1. The Set is lost on a broker restart. recover() rebinds an earlier host's run (:1366-1373),
|
||||
and if that run makes a call before the launcher's SIGTERM, it gets `not-holder` and exits 22, as
|
||||
every revoked run does today. Seeding the Set from the store at a rebind would need a mark only
|
||||
`#revokeLost` writes (D1's alternative shows `reason` and `by` aren't one). I'd leave it unless
|
||||
Dewey wants that mark.
|
||||
- N2. A revoked run that also has a pending limit stop gets `#limitStop`'s `not-holder` (:4572-4573,
|
||||
:4952) on `message.receive` and `state`, because that check runs before `#agent`. It's rare; I'd
|
||||
leave it and record it.
|
||||
|
||||
## Round 2 (dewey-0918 CHANGES C1-C3, sage-3483)
|
||||
|
||||
- C1: T3 asserts A has no `thread-revoked` launch-log event, in place of "end before the event".
|
||||
- C2: T2's limit-stopped run makes a second call, which carries M3.
|
||||
- C3: the gate lists the six suites and every scripts/test-*.sh with 0 failures; mos-1463's
|
||||
bounds for access-revoke.test.mjs are in "Expected first-run reds".
|
||||
- Cosmetic: threadIdle's `#claimEnd` is :1133.
|
||||
|
||||
## Build (dewey-0920 APPROVE r2, sage-3487)
|
||||
|
||||
- Red-first commit d38c5213a: packages/bus/tests/revoke-window.test.mjs (T1 two tests, T2) and
|
||||
packages/cli/tests/revoke-window.test.mjs (T3), no product code.
|
||||
- Build commit (this one): D1 in broker.mjs, D2 in runner.mjs, D3 in the two READMEs and the
|
||||
#revokeLost comment, and the mos-1463 edit to access-revoke.test.mjs (numstat 7 7).
|
||||
- Nothing has run yet; every run is slot work.
|
||||
|
||||
Expected first-run reds, on d38c5213a (new tests, fe4ee8c7b product):
|
||||
- bus revoke-window: both T1 tests red, A answers `not-holder` where `thread-revoked` is expected.
|
||||
T2 green.
|
||||
- cli revoke-window T3 red: A's verdict is `session stopped working: not-holder`, its end
|
||||
`session-invalid`/22.
|
||||
- access-revoke.test.mjs is unchanged at d38c5213a, so it stays green there.
|
||||
|
||||
On the build commit every file above, and every gate suite, is expected green.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Row 216 build notes (C2-05a, lookup index and queries, #1717)
|
||||
|
||||
Phase A only: code, tests and this file. No test, suite or mutant has been run. `node --check` passed on every changed `.mjs`.
|
||||
|
||||
- Branch: `researcher/row216`
|
||||
- Base: `d372102882d6858fa77af4aee453f866c886ad5c` (origin/refactor)
|
||||
- Design: `agents/gosalyn/work/case2/DESIGN.md` on origin/gosalyn/case2-design, section 3 (table at lines 625-631, bullets 633-641), 1.2-1.4, B12, D2
|
||||
- Brief: `docs/plans/2026-10-20_case2-rows.md`, C2-05a at lines 320-357
|
||||
|
||||
## Files
|
||||
|
||||
| Path | Status |
|
||||
|---|---|
|
||||
| `packages/library/src/index-build.mjs` | new |
|
||||
| `packages/library/src/query.mjs` | new |
|
||||
| `packages/library/src/index.mjs` | edit: 2 export lines added |
|
||||
| `packages/library/tests/query-fixture.mjs` | new, test helper |
|
||||
| `packages/library/tests/query.test.mjs` | new |
|
||||
| `packages/library/tests/index-build.test.mjs` | new |
|
||||
| `agents/researcher/work/queue-216/BUILD.md` | new (this file) |
|
||||
|
||||
No existing test or source file changed beyond `index.mjs`.
|
||||
|
||||
## Design rows and where each is tested
|
||||
|
||||
| Design row | Code | Tests (query.test.mjs unless noted) |
|
||||
|---|---|---|
|
||||
| `library_find` | `find` | find is bound to its universe; every result carries its scope; long list pages; large items page under the cap |
|
||||
| `library_get` | `get` | hybrid gets a source file; get lists visible links both ways; get pages its lines |
|
||||
| `library_search` | `search` | search returns scoped line hits; item that cannot fit alone is refused |
|
||||
| `library_manuscript` | `manuscript` | one chapter at most 400 lines; refused without chapter; hybrid reads a source chapter |
|
||||
| `library_universe` | `universeInfo` | universe info reports a hybrid's sources, constraints, notes; unknown universe refused |
|
||||
| Hybrid semantics (1.4) | `sees` | a hybrid sees its sources canon minus exclusions, no playground |
|
||||
| Unmapped never appears | index build + `sees` | unmapped files never appear (query.test); index holds mapped files only (index-build.test) |
|
||||
| Per-commit cache (3) | `indexFor` | same commit, path, mapping return one index; different path, mapping or commit get their own; eight-entry bound; failed read not kept; unchecked mapping refused (index-build.test) |
|
||||
| Rebuild time (3, C2-05a gate) | `indexFor` | 3,000-file rebuild, time reported with `t.diagnostic`, no threshold asserted |
|
||||
| REPLY_CAP parity | `QUERY_REPLY_CAP` | equals `REPLY_CAP` from harness tools.mjs |
|
||||
|
||||
## Choices where the design table is silent or differs
|
||||
|
||||
These are departures or gaps. Each is built as stated, so Sage can rule before Phase B. A ruling that changes one of them is a small edit.
|
||||
|
||||
1. **Paging arguments (gap a).** The table has no offset. `find` and `search` take an optional `offset` (a position in the filtered list, default 0). A reply carries `next`: the position of the next item, or `null`.
|
||||
2. **`library_get` has no universe (gap b).** Built with a required `universe`. Without it the reply could not be isolated. A missing file and a file the universe cannot see both return `null`, so a caller cannot tell them apart. `get` also takes an optional `from` line.
|
||||
3. **`get` over the cap (gap c).** Lines page by `from` with `next`. `links_out` and `links_in` come back whole on every page. If the frontmatter and links alone exceed the cap, the call is refused with `query-item-too-large`. A hub note with many backlinks may be refused. This needs a ruling: link paging, or accept the refusal.
|
||||
4. **`manuscript` without a chapter (gap d).** Refused with `query-chapter-required`. The design names no list form. A chapter name that matches two visible files is refused with `query-chapter-ambiguous`.
|
||||
5. **`search` excerpt (gap e).** The excerpt is the whole line, not a truncation. A line that alone exceeds the cap is refused. Truncating it would cut an item.
|
||||
6. **`exclude` form (gap f).** `checkMapping` accepts only a folder path for `exclude` (`folder()`), so an exclusion is a path prefix. There is no type or source form to build. Confirm.
|
||||
7. **Pin constraints.** `pin` does not hide any file. It is reported by `universeInfo` only. Confirm.
|
||||
8. **Playground through `get`.** `scope` applies to lists. `get` by an explicit path returns a playground file of the caller's own universe. Confirm.
|
||||
9. **Links.** A link into a file the universe cannot see is dropped from `links_out` and `links_in`, in line with N1. Links from a visible file that point into another universe are kept in the index (the reader keeps them under R6), and the query layer drops them for universes that cannot see them.
|
||||
10. **`find` matching.** Case-insensitive substring over the front matter `name` and the file name. An empty `query` matches every file in scope.
|
||||
11. **Cache key.** `repo`, `commit` and a sha256 of the canonical mapping. Keying by commit alone would return the wrong scopes after a mapping change. The cache holds eight entries, least recently used out. Failed reads are not kept.
|
||||
12. **`universeInfo`.** Reads only the checked mapping and the served commit. It builds no index.
|
||||
13. **Reply commit.** Every reply carries the index commit, which is the served commit passed in.
|
||||
|
||||
## Provisional refusal codes (pending Sage approval)
|
||||
|
||||
The brief says new refusal codes need Sage's approval first. These are provisional and must not be treated as settled:
|
||||
|
||||
- `query-argument-invalid`
|
||||
- `query-universe-unknown`
|
||||
- `query-chapter-required`
|
||||
- `query-chapter-ambiguous`
|
||||
- `query-item-too-large`
|
||||
|
||||
Reused, existing codes: `library-mapping-invalid`, plus the reader's `library-git`, `library-commit` through `indexFor`.
|
||||
|
||||
## Grep results (grep only, not a test run)
|
||||
|
||||
- Test references to the changed paths: `packages/library/tests/*.mjs` import `../src/index.mjs` and `../src/mapping.mjs`. `packages/bus/tests/library.test.mjs` and `library-sync.test.mjs` import the library index as a namespace. Neither enumerates its exports, so the two new export lines break no assertion.
|
||||
- `packages/cli/tests/stack-copy.test.mjs:52` checks that `packages/library/src/index.mjs` exists, and it still does.
|
||||
- 64-hex pins: none in `packages/library/tests`, none on `index.mjs`, `query.mjs` or `index-build.mjs`.
|
||||
- `scripts/test-*.sh`: no reference to the new modules.
|
||||
|
||||
## Flag for Sage
|
||||
|
||||
- `packages/library/tests/helpers.mjs` `repo()` builds its fixtures under `tmpdir()`, which is `/tmp`. The launcher rules forbid that. The new tests do not use it; they use `tests/query-fixture.mjs`, which builds under `/mnt/storage/scratch/tmp`. The existing helper should be fixed in a separate row, not here.
|
||||
|
||||
## Phase B commands (not run)
|
||||
|
||||
Run only after Sage sends "slot clear". Set the scratch TMPDIR first, per the launcher rule.
|
||||
|
||||
```
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-aa3cc98787bc5152c/packages/library
|
||||
export TMPDIR=/mnt/storage/scratch/tmp
|
||||
node --test tests/query.test.mjs tests/index-build.test.mjs
|
||||
node --test tests/*.test.mjs
|
||||
```
|
||||
|
||||
Also run `packages/bus/tests/library.test.mjs` and `library-sync.test.mjs`, since they import the index.
|
||||
|
||||
## Status
|
||||
|
||||
Not yet verified. Nothing has been tested and nothing is committed. The working tree holds the Phase A changes. AGENTS.md invariant 8 allows a commit only after the applicable suites are green, so the commit waits for Phase B. The dispatch asked for a local commit in Phase A; Sage to confirm that the commit waits for Phase B.
|
||||
|
||||
## Round 2 (2026-10-22, sage-2708, sage-2716): rulings applied, rebased onto aca16404c
|
||||
|
||||
Added after the text above; nothing above is edited. "Nothing is committed", "Provisional refusal codes",
|
||||
`query-chapter-required` and the tmpdir flag above were true when written and are superseded here.
|
||||
|
||||
- Branch `researcher/row216-r2`, one local commit on origin/refactor `aca16404c`. Row 268 (C2-01c) is in
|
||||
that base. Row 253 has not landed; the work rebases onto 253's tip before any gate. Not pushed, not run.
|
||||
- Rulings: `agents/sage/work/queue-216/RULINGS.md` at da9555ca2, rulings 1-13 and the tmpdir flag.
|
||||
- Drift check, d37210288..aca16404c against my paths: the library files that changed are `mapping.mjs`,
|
||||
`scope.mjs`, `README.md` and the mapping and scope tests. None is a file I add or edit. `query.mjs` reads
|
||||
the checked mapping through them, so the whole library package runs in phase B.
|
||||
|
||||
### What changed from phase A
|
||||
|
||||
| Ruling | Change in `query.mjs` | Test |
|
||||
|---|---|---|
|
||||
| 3 `get` over the cap | `links_out` and `links_in` page together with `links_from`; every reply carries `links_next`, `next` and the front matter. A page with lines left carries at least one line. Links take at most half the cap while lines remain. `query-item-too-large` is left for front matter alone or one line alone (or one link alone) | hub note with 900 backlinks pages and adds up; front matter alone and a line alone refused; bad `links_from` |
|
||||
| 4 `manuscript` no chapter | `chapter` optional: the first visible chapter in path order. Every reply carries `next_chapter`. `query-chapter-required` removed. Ambiguity still refuses | first chapter and `next_chapter`; ambiguous name refused |
|
||||
| 5 `search` long line | hit comes back `{path, line, excerpt: null, excerpt_too_long: true, scope}`, other hits still return | marked hit and unmarked hits in one reply |
|
||||
| 6 `exclude` | path-prefix reading only. The test names `EXCLUSION` and `EXCLUDED_PATHS` in `query-fixture.mjs`, so C2-15 (row 267) swaps two constants | exclusion hides from the hybrid and not from the source |
|
||||
| 8 hybrid `get` | unchanged code; test added | hybrid gets a source's playground file as `null`; the source gets it with `scope: "playground"` |
|
||||
| PRD 1.14 (row 268) | `universeInfo` reports `kind` from the mapping (`draft` included) | four tests below |
|
||||
|
||||
PRD 1.14 tests, one each: a lookup naming a `book` leaves `book: null` files out (find and search);
|
||||
`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 kind, name and prefixes.
|
||||
The levels fixture is `WITH_LEVELS` in `query.test.mjs`: harbor gets `Notes/Harbor/` as a universe-level
|
||||
playground prefix, and `idea` is a draft over `Drafts/New World/`.
|
||||
|
||||
`query-fixture.mjs` builds under `tmpdir()`, as `helpers.mjs` does (ruling "tmpdir flag"). The earlier
|
||||
flag about `helpers.mjs` building under `/tmp` is closed by that ruling: the gate sets `TMPDIR`.
|
||||
|
||||
Refusal codes now: `query-argument-invalid`, `query-universe-unknown`, `query-chapter-ambiguous`,
|
||||
`query-item-too-large`. All four approved in the rulings.
|
||||
|
||||
### Grep results (grep only, not a test run)
|
||||
|
||||
- `git grep` over `packages/*/tests/*` and `scripts/*` for `query.mjs` and `index-build`: no hit outside
|
||||
`packages/library/tests/query*.test.mjs` and `index-build.test.mjs`.
|
||||
- 64-hex pins naming a library path: none. Positive control: the same pathspec finds the sha256 lines in
|
||||
`packages/cli/tests/case1-hostcheck.test.mjs` and the `packages/library/src/index.mjs` existence check at
|
||||
`packages/cli/tests/stack-copy.test.mjs:52`.
|
||||
- Tests that read `packages/library/src` as a directory: `packages/cli/tests/stack-release-fixture.mjs:309`
|
||||
copies `packages/library/src` whole into a release fixture. It carries no file list, so two new files break
|
||||
no assertion. `stack-release.test.mjs` is not in the gate unless Sage wants it.
|
||||
- Tests that import the library index as a namespace: `packages/bus/tests/library.test.mjs` and
|
||||
`library-sync.test.mjs`. Neither enumerates exports, so the two export lines add no break. Both run
|
||||
whole in phase B.
|
||||
|
||||
### Run limits (fixed before any run)
|
||||
|
||||
Every node file runs as its own `setsid -w timeout -k 10 LIMIT node --test FILE`; nothing runs unbounded.
|
||||
|
||||
- Node test files: LIMIT 900 s. The library test files have no recorded time. 900 s is above the longest
|
||||
recorded whole-package run I know of (cli, 591 s, row 281 BUILD.md "Runs" step 3), so it holds the 3x rule
|
||||
for any single file. The first run records each file's time, and a file past 300 s gets a limit re-derived
|
||||
from it before the next run. `query.test.mjs` builds a 900-file repo and a 3,000-file index rebuild is in
|
||||
`index-build.test.mjs`; both are the files most likely to need it.
|
||||
- No shell script is in this row's gate, so no script limit applies.
|
||||
|
||||
Any timeout goes to Sage, then Mos, unplaced (mos-1275).
|
||||
|
||||
### Phase B (not run; Sage's CLEAR first, then a rebase onto 253's tip)
|
||||
|
||||
```
|
||||
export TMPDIR=/mnt/storage/scratch/tmp
|
||||
cd <worktree>/packages/library
|
||||
for f in tests/*.test.mjs; do setsid -w timeout -k 10 900 node --test "$f"; done
|
||||
cd ../bus
|
||||
for f in tests/library.test.mjs tests/library-sync.test.mjs; do setsid -w timeout -k 10 900 node --test "$f"; done
|
||||
cd ../cli
|
||||
setsid -w timeout -k 10 900 node --test tests/stack-copy.test.mjs
|
||||
```
|
||||
|
||||
Mutants I propose (my list, not read off the brief; each is meant to be killed by the named test, and none
|
||||
has been thrown; the kill claims are unverified until CLEAR):
|
||||
|
||||
- M-a: `sees()` returns true for a file of another universe: killed by "find is bound to its universe".
|
||||
- M-b: the playground filter dropped: killed by "playground appears only when asked for".
|
||||
- M-c: the hybrid reads a source's playground: killed by "a hybrid gets a source playground file as null".
|
||||
- M-d: `links_next` always null: killed by the hub paging test.
|
||||
- M-e: the book filter lets `book: null` through: killed by the PRD 1.14 book test.
|
||||
|
||||
### Status
|
||||
|
||||
Built, not run. `node --check` has not been run on this round's files (no node before the CLEAR); the
|
||||
phase A pass above covered the earlier text only. The commit is local and waits for no gate under Sage's
|
||||
sage-2716 instruction, which allows git and docs work only; push and claim wait for Sage's go.
|
||||
|
||||
## Round 3: rebase onto 45dca69f6, drift check, hash-pin grep (2026-10-23T18:53:43Z, sage-3576)
|
||||
|
||||
Appended; nothing above is edited. Git only; nothing run, nothing pushed. Branch researcher/row216-r4 holds the
|
||||
one row 216 commit on 45dca69f6 (origin/refactor, 10-23). Earlier base: 48e450ce8 (row 253).
|
||||
|
||||
- Rebase: clean. `git range-diff 48e450ce8..038089e5f 45dca69f6..researcher/row216-r4` shows the commit as `=`.
|
||||
- Landed in 48e450ce8..45dca69f6: 1175 files. Path overlap with this row's 7 paths: none. packages/library: no
|
||||
landed file. packages/harness/src/tools.mjs (imported by query-fixture.mjs or the tests): not landed.
|
||||
- Hash-pin grep (the item 216 owed): 64-hex in packages/library: none. Positive control: 364 files in
|
||||
packages/*/tests and scripts match 64-hex. Tests and scripts that name library files by path:
|
||||
bus/tests/library.test.mjs and library-sync.test.mjs and bus/src/runtime.mjs import library/src/index.mjs;
|
||||
business/src/business.mjs and its test import library/src/mapping.mjs (unchanged by this row);
|
||||
cli/tests/stack-copy.test.mjs, stack-release-fixture.mjs:310 and stack-release.test.mjs:1991 copy
|
||||
packages/library/src as a tree; scripts/stack-copy.sh:37 copies it; scripts/mosaic:41 runs survey.mjs. None pins
|
||||
a hash of index.mjs, index-build.mjs or query.mjs.
|
||||
- bus/src/runtime.mjs landed changes after 48e450ce8 and uses libraryModule by named members (ensureMirror, SLOT,
|
||||
readLibraryFile, libraryStatus, sync, LibraryError, SYNC_INTERVAL_MS) only; the landed diff touches none of them,
|
||||
and this row's two new index.mjs exports (query and index build) are not iterated by any caller.
|
||||
- Gate additions for phase B: run bus library.test.mjs and library-sync.test.mjs whole (they import index.mjs), and
|
||||
the cli stack-copy and stack-release tests that copy packages/library/src.
|
||||
|
||||
## Round 4: phase B on 4e5d28ab2, two test fixes for r5 (2026-10-23T20:20:51Z, sage-3702, mos-1528)
|
||||
|
||||
Appended; nothing above is edited.
|
||||
|
||||
Phase B receipts on 4e5d28ab2 (researcher-r216-kit/out-b1): library whole 114 pass, 2 fail; bus library.test and
|
||||
library-sync.test 12 pass; cli 266 pass. The 3,000-file rebuild took 3807 ms (the test, with fixture setup, 4070 ms;
|
||||
`t.diagnostic` in index-build.test.mjs). No row 216 mutant was required or thrown in that round.
|
||||
|
||||
The 2 library reds are test faults, not product faults. Both are placed as first-run reds in files this row adds
|
||||
(mos-1272); Mos ruled both fixes in mos-1528 and Sage relayed it in sage-3712.
|
||||
|
||||
- R4: `packages/library/tests/index-build.test.mjs`, test "a different repo path, mapping or commit gets its own
|
||||
index". Old: `writeFileSync(join(repo, 'Book One/Places/New.md'), …)` with no `Book One/Places` folder in FILES, so
|
||||
the write failed with ENOENT. New: one `mkdirSync(join(repo, 'Book One/Places'), { recursive: true })` before the
|
||||
write (import line gains `mkdirSync`). No assertion changes.
|
||||
- R5: `packages/library/tests/query.test.mjs`, test "a hybrid sees its sources canon, minus exclusions, and no
|
||||
playground". The assertion sorts the actual paths and compared them to an unsorted literal, so the literal had
|
||||
the wrong order (Characters before Chapters; Second World Characters before Chapters). New: only the expected
|
||||
literal is reordered into sorted order. The `.sort()` on the actual side stays, and the assertion still checks
|
||||
order. The set of seven paths is unchanged.
|
||||
- Product code: no edit.
|
||||
|
||||
Reruns owed after the r5 commit (run on Sage's CLEAR, after the 283 gate): index-build.test.mjs whole,
|
||||
query.test.mjs whole, library whole, and the mutants those two files kill (M-a to M-e above). Results are appended
|
||||
below as a new entry.
|
||||
|
||||
Run limit note: stack-release.test.mjs ran 644 s in the cli run. A per-file limit of 3 x 644 = 1932 s covers it.
|
||||
Sage's gates run cli as one whole-package `node --test` under 2100 s.
|
||||
|
||||
Carry notes from Filbert's static check (doc only, no change in r5): N1 and N2 stay recorded as he gave them. The
|
||||
`After` list in this file still names row 268; row 268 landed at c27207f6e.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Row 220: C2-10a, no raw session view (M7) and no raw Discord engine (M8)
|
||||
|
||||
Phase A. No test, suite or mutant has been run. Sage opens the slot.
|
||||
|
||||
- Owner: researcher seat (Haiku 5.5 subagent), dispatched by Sage under lead decision 129.
|
||||
- Reviewer: Filbert.
|
||||
- Branch: `researcher/row220`, from `origin/refactor` at d37210288.
|
||||
- Brief: `docs/plans/2026-10-20_case2-rows.md`, C2-10a (line 495) and "Rules every row keeps".
|
||||
- Design: `agents/gosalyn/work/case2/DESIGN.md` on `origin/gosalyn/case2-design`, sections 5.1 (M7, M8), 7.6, 8 (row list).
|
||||
|
||||
## Status
|
||||
|
||||
| Item | State |
|
||||
|---|---|
|
||||
| M7 reader side (`conversation-not-listed`) | Built. Tests written, not run. |
|
||||
| M7 production wiring (the caller that passes `isCreative`) | Not built. Outside the file list, and it needs a ruling (question 1). |
|
||||
| M8 `discord check` and `discord run` refusals | Not built. `packages/discord/src/cli.mjs` is untouched. Blocked on question 1 and question 2. |
|
||||
|
||||
Phase A is therefore partial. The row cannot pass its acceptance until Sage rules on the questions below.
|
||||
|
||||
## Files
|
||||
|
||||
- `packages/conversation/src/reader.mjs`: exports `NOT_LISTED`; `createReader` takes `isCreative` (default `() => false`); `catalogue()`, `open()` and `next()` hide a root for which it returns truthy.
|
||||
- `packages/conversation/tests/reader.test.mjs`: three new tests, inserted before the "unknown conversations" test.
|
||||
- `agents/researcher/work/queue-220/BUILD.md`: this file.
|
||||
|
||||
Nothing else is changed. `node_modules` in the worktree is a symlink to the canonical checkout and is ignored by git.
|
||||
|
||||
## Brief and design items mapped to tests
|
||||
|
||||
| Item | Test |
|
||||
|---|---|
|
||||
| M7: a creative session file under the root isn't listed | "M7: a creative seat's session is not listed and won't open by id; an assistant seat still lists" |
|
||||
| M7: and won't open by id (`conversation-not-listed`) | same test; second check of the open refusal in "M7: a creative session already open refuses its next page once the root is hidden" |
|
||||
| M7: an assistant session still lists | same test (the `sage` seat is listed and opens with its text) |
|
||||
| M7: an already-issued cursor stops once the root is hidden (my addition; the design's "won't open" implies it) | "M7: a creative session already open refuses its next page once the root is hidden" |
|
||||
| M7: refused rows and the unsupported placeholder of a creative root are hidden too (my addition; the design hides "creative sessions", and a refused row or placeholder names one) | "M7: a creative root's refused files and its unsupported placeholder are hidden as well" |
|
||||
| Default behavior without `isCreative` is unchanged | the existing reader suite; the default predicate returns false |
|
||||
| M8: `discord check` / `discord run` refuse `discord-engine-not-allowed` on a library business, and a `mosaic-bus` binding with `tools` | none (not built, question 1) |
|
||||
| M8: a pi binding on a business without a library still checks clean | none (not built, question 1) |
|
||||
|
||||
Every test's fixture text is `CREATIVE-MARKER` or a made-up assistant line. The M7 tests assert the marker is absent from the catalogue and refusal output.
|
||||
|
||||
## Choices made in phase A
|
||||
|
||||
- `isCreative(root)` is the reader's only input. The reader reads no business file. A truthy answer hides, so a doubtful answer fails closed. The predicate must depend on `root.seat` and `root.project` only, because a cursor is checked against the root object from an earlier listing.
|
||||
- Hidden check placed after the cursor's actor/purpose/conversation/branch checks in `next()`, and before the source is read.
|
||||
- `open()` checks hidden after the id resolves, so a creative id gets `conversation-not-listed`, and an unknown id still gets `unknown-conversation`.
|
||||
- `conversation-not-listed` is `reconcile: false`. The design names no reconcile flag. `unknown-conversation` uses true. Question 3.
|
||||
- The default predicate is `() => false`. Until a caller passes one, M7 does nothing. This is the open risk, and question 1 covers it.
|
||||
|
||||
## Grep over `packages/*/tests` and `scripts/test-*.sh` (static, phase A)
|
||||
|
||||
Changed path `packages/conversation/src/reader.mjs`:
|
||||
- `packages/conversation/tests/reader.test.mjs:14` imports `rootsFromSpecs, createReader, conversationId, cwdInProject, UNSUPPORTED_HARNESS`. It is the file under change.
|
||||
- `packages/control-board/tests/serve.test.mjs:26` imports `UNSUPPORTED_HARNESS`. That export is unchanged.
|
||||
- `packages/library/tests/sync.test.mjs:15` imports `packages/library/src/reader.mjs`, a different file. Not a hit.
|
||||
- `scripts/test-*.sh`: no reference to the conversation reader.
|
||||
- 64-hex pins naming this path: none. `packages/bus/tests/grant-migration.test.mjs:24` has a 64-hex value, but it is a DDL digest, not a pin on this path. That file matched only the word "reader" in a test title.
|
||||
|
||||
Changed path `packages/conversation/tests/reader.test.mjs`:
|
||||
- No other test or script names this file. It runs through `packages/conversation`'s `node --test tests/`.
|
||||
- No 64-hex value in the file.
|
||||
|
||||
Changed path `agents/researcher/work/queue-220/BUILD.md`: no pin.
|
||||
|
||||
`node --check` passed on both changed `.mjs` files.
|
||||
|
||||
## Phase B plan (not run)
|
||||
|
||||
The reader test uses `os.tmpdir()`. Set `TMPDIR` to a scratch dir first, per the rule against `/tmp`.
|
||||
|
||||
```
|
||||
export TMPDIR="$(mktemp -d /mnt/storage/scratch/tmp/r220XXXXXX)"
|
||||
cd packages/conversation && node --test tests/reader.test.mjs
|
||||
cd packages/conversation && node --test tests/
|
||||
cd packages/discord && node --test tests/
|
||||
cd packages/control-board && node --test tests/serve.test.mjs
|
||||
```
|
||||
|
||||
The first command is the touched file whole. The second and third are the conversation and discord suites whole, as the brief asks. The control-board serve test is not in the brief. It consumes `reader.mjs`, so I recommend it. Sage can drop it.
|
||||
|
||||
## Questions for Sage
|
||||
|
||||
1. **Business link (blocks M8 and the M7 wiring).** Neither rule can be built from this row's file list, because the inputs don't exist in the code:
|
||||
- A reader root carries `seat` (the spec's `agent`) and `project` (the repo directory name). It has no business id. Roots come from control-board scan specs (`packages/control-board/src/scan.mjs`, `discoverRepoAgents`) and seat registrations. Neither carries a definition.
|
||||
- A Discord binding has `seat` and no business key (`packages/discord/src/binding.mjs`, `TOP_KEYS`).
|
||||
- Creative status lives only in the business file: `roles.<instance>.definition` in `CREATIVE_DEFINITIONS` (`packages/business/src/vocabulary.mjs:176`). A library project is `projects.<id>.kind === "library"` (`packages/business/src/business.mjs:243`).
|
||||
- Creative thread sessions are stored at `<dataRoot>/threads/<business>/<thread>/session` (`packages/seat/src/session.mjs:59-64`), outside the `.pi/state` roots the board scans. M7 may therefore reach only creative seats that have a `.pi/state` session root. Please confirm.
|
||||
- The design does not define the mapping from a seat to a business.
|
||||
|
||||
Recommendation: add a required `business` key to the binding, validated against `businesses/<id>.json`. The board builds `isCreative` from `loadBusiness(id)`: a root is creative when its seat is a `roles` key whose definition is in `CREATIVE_DEFINITIONS`, and the business has a library project. This needs file scope for `packages/discord/src/binding.mjs` and `cli.mjs` (M8), and `packages/control-board/src/serve.mjs` (M7 wiring). None of those is on this row's list. I need that scope amendment and the mapping ruling before I build M8 or the wiring.
|
||||
|
||||
2. **M8 refusal code for a `mosaic-bus` binding with `tools`.** The design names `discord-engine-not-allowed` for the engine case and no code for the tools case. A new code goes to you first. My proposal is to reuse `discord-engine-not-allowed` with a message that names the tools key.
|
||||
|
||||
3. **`reconcile` on `conversation-not-listed`.** I chose `false`. Confirm or change.
|
||||
|
||||
4. **HTTP status for `conversation-not-listed`.** `REFUSAL_STATUS` in `packages/control-board/src/serve.mjs` has no entry, so the route returns 422. My proposal is 404, next to `unknown-conversation`. This needs that file on the scope list too.
|
||||
|
||||
Phase A stops here. On your "slot clear" I will run the phase B commands above. I will not build M8 or the wiring until question 1 is answered.
|
||||
@@ -0,0 +1,481 @@
|
||||
# Row 220 r2: phase A round 2 (M7, M8, business helper, REFUSAL_STATUS)
|
||||
|
||||
Phase A only. No node, npm, `node --check` or test run has happened in this round, and nothing is
|
||||
committed. Phase B (the suites) waits for the coordinator's "slot clear".
|
||||
|
||||
- Owner: researcher seat (Haiku 5.5 subagent), dispatched by Sage under lead decision 129.
|
||||
- Reviewer: Filbert.
|
||||
- Branch: `researcher/row220-r2`, from `origin/refactor` at da9555ca2. The r1 cherry-pick 3fa833715 is
|
||||
staged with `-n`.
|
||||
- Rulings: `agents/sage/work/queue-220/RULINGS.md` on origin/refactor (da9555ca2). Import-by-path is
|
||||
followed: no export line in `packages/business/src/index.mjs`, and `packages/discord/src/binding.mjs`
|
||||
is untouched.
|
||||
|
||||
## Coordinator rulings, applied
|
||||
|
||||
Round 1 answers (earlier packet): applied as before. Absent `businesses/` gives empty sets only on
|
||||
ENOENT; other failures throw. stack-copy test joins phase B. Webui fixtures pass `businessDir` (renamed `configDirectory` in round 2b below). Discord
|
||||
refusals exit 2.
|
||||
|
||||
Round 2 rulings (coordinator message, this round):
|
||||
|
||||
1. **Dangling symlink at `businesses/` refuses.** `loadSeatSets` calls `lstatSync(join(dir, "businesses"))`
|
||||
first. Only an lstat ENOENT (nothing at the path) returns empty sets. If something is there and
|
||||
`readdirSync` fails for any reason, including ENOENT through a dangling link, it throws `BusinessError`.
|
||||
Test added: "a businesses symlink to a missing target throws; no empty set". The absent-directory test is kept.
|
||||
2. **Sets are read once per public reader call.** `createReader` takes `creativeCheck`, a factory. Each
|
||||
`catalogue()`, `open()` and `next()` calls it once, then applies the returned predicate to every root.
|
||||
A load failure refuses `conversation-business-unreadable` for the whole call. `serve.mjs` exports
|
||||
`creativeCheck(dir = configDir())`, the factory, and `createServer` passes `creativeCheck(businessDir)`.
|
||||
Test added: "one catalogue calls the creative check once, however many roots it lists". It counts factory
|
||||
calls: 1 after `catalogue()`, 2 after one `open()`.
|
||||
3. **Phase B adds three test files:** `packages/webui/tests/tester-browser.test.mjs`,
|
||||
`tester-cli.test.mjs` and `proof-cli.test.mjs`, run whole (they reach `fixture.mjs` through
|
||||
`tester-fixture.mjs`). Phase B is 36 files: 33 plus these 3.
|
||||
4. **Scope additions approved** (coordinator round 2 ruling 4): `packages/control-board/tests/discord.test.mjs`
|
||||
line 123 and the five webui test files (`fixture.mjs`, `history-fixture.mjs`, `relaunch.test.mjs`,
|
||||
`return-flow.test.mjs`, `discord.test.mjs`). They stop tests reading the real config directory.
|
||||
5. **TMPDIR** for phase B is set by the coordinator under `/mnt/storage/scratch/tmp`. No code change.
|
||||
|
||||
## Discord exit code
|
||||
|
||||
Refusals from the library-seat check exit 2, the code the Discord CLI already uses for configuration and
|
||||
binding refusals.
|
||||
|
||||
- `packages/discord/src/errors.mjs` line 2: `// exitCode follows docs/TOOLS.md: 1 operation failed, 2 invalid data or configuration, 3 refused by a brake (STOP or a held binding; a supervisor must not retry), 4 usage.`
|
||||
- `packages/discord/src/errors.mjs` line 5: `constructor(message, exitCode = 2, details = undefined)`.
|
||||
- Config refusal precedent: `packages/discord/src/binding.mjs` line 67, `throw new DiscordError(\`config.dataRoot must be an absolute path: ${path}\`);`. No explicit code, so exit 2.
|
||||
- `checkLibraryEngine` in `packages/discord/src/cli.mjs` lines 115 to 122. Each refusal passes `, 2)`:
|
||||
`discord-business-unreadable` (115), `discord-engine-not-allowed` for a non-mosaic-bus engine (119),
|
||||
and `discord-engine-not-allowed` for a mosaic-bus binding that declares `tools` (122).
|
||||
- `grep -n ", 3)" packages/discord/src/cli.mjs` finds nothing. `grep -rn "status, 3" packages/discord/tests`
|
||||
finds only existing brake tests in recover.test.mjs and binding.test.mjs. Not changed.
|
||||
`library-seat.test.mjs` asserts 2 on all three refusals.
|
||||
|
||||
## Files (round 2, uncommitted)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/business/src/library-seats.mjs` | new: `loadSeatSets({ dir, rolesDir })` returns `{ librarySeats, creativeSeats }`; lstat-first (ruling 1); read per call |
|
||||
| `packages/business/tests/library-seats.test.mjs` | new: 7 tests (6 from r1 plus the dangling symlink) |
|
||||
| `packages/conversation/src/reader.mjs` | r1 plus r2: `NOT_LISTED`; `creativeCheck` factory option, called once per `catalogue()`, `open()` and `next()`; `catalogue()` wrapped so a refusal returns `{ ok: false }` |
|
||||
| `packages/conversation/tests/reader.test.mjs` | r1: 3 M7 tests, now on `creativeCheck`; r2: 1 spy test (one factory call per catalogue) |
|
||||
| `packages/control-board/src/serve.mjs` | `REFUSAL_STATUS`: `conversation-not-listed` 404, `conversation-business-unreadable` 503; `export creativeCheck(dir = configDir())` (factory); `createServer({ businessDir = configDir() })` wires `creativeCheck: creativeCheck(businessDir)` |
|
||||
| `packages/control-board/tests/creative-hidden.test.mjs` | new: 4 tests (3 from r1 plus known-and-unknown id both 503), switched to `creativeCheck` |
|
||||
| `packages/control-board/tests/serve.test.mjs` | pin test adds `NOT_LISTED` and `conversation-business-unreadable`; conversation fixture passes `businessDir` |
|
||||
| `packages/control-board/tests/discord.test.mjs` | line 123 passes `businessDir` (ruling 4) |
|
||||
| `packages/discord/src/cli.mjs` | `checkLibraryEngine` called from `prepare()`; exit 2 |
|
||||
| `packages/discord/tests/helpers.mjs` | `makeDeployment` creates an empty `businesses/` beside `config.json` |
|
||||
| `packages/discord/tests/library-seat.test.mjs` | new: 5 tests, all `--offline`, all asserting exit 2 |
|
||||
| `packages/webui/tests/fixture.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
| `packages/webui/tests/history-fixture.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
| `packages/webui/tests/relaunch.test.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
| `packages/webui/tests/return-flow.test.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
| `packages/webui/tests/discord.test.mjs` | `businessDir` on the board start (ruling 4) |
|
||||
|
||||
Not touched: `packages/business/src/index.mjs`, `packages/discord/src/binding.mjs`, `scripts/stack-copy.sh`.
|
||||
|
||||
## Brief item to test map
|
||||
|
||||
| Brief item | Test |
|
||||
|---|---|
|
||||
| Q1 helper: library and creative sets from business files | `business/tests/library-seats.test.mjs` "library seats come from library businesses..." |
|
||||
| Q1 a non-library business puts no seat in either set | `library-seats.test.mjs` "a business with no library project..." |
|
||||
| Q1 absent businesses directory gives empty sets | `library-seats.test.mjs` "an absent businesses directory gives empty sets" |
|
||||
| Q1 any other failure throws, no empty set | `library-seats.test.mjs` "a businesses path that is not a directory throws"; "a businesses symlink to a missing target throws"; "a business file that can't be read throws"; "a business file that is not valid JSON throws" |
|
||||
| Round 2 ruling 2: one read per public call | `conversation/tests/reader.test.mjs` "one catalogue calls the creative check once..." |
|
||||
| M7 hidden from catalogue, open and next | `control-board/tests/creative-hidden.test.mjs` "the check hides the creative seat..."; r1 reader tests (catalogue, open, next, symlink and placeholder) |
|
||||
| M7 second wall: thread store | `creative-hidden.test.mjs` "a thread store is not a root from a spec..." (asserts `rootsFromSpecs` drops it, then a root over it is hidden with 404 `conversation-not-listed`) |
|
||||
| M7 unreadable business refuses listing and open | `creative-hidden.test.mjs` "an unreadable business file refuses the listing and open with 503" |
|
||||
| Coordinator follow-up: known and unknown id both 503 when unreadable | `creative-hidden.test.mjs` "with an unreadable business file, a known and an unknown id both refuse with 503" |
|
||||
| M8 non-mosaic-bus engine on a library seat | `discord/tests/library-seat.test.mjs` "a library seat's binding on a non-mosaic-bus engine is refused" (exit 2, message names the engine) |
|
||||
| M8 mosaic-bus binding that declares tools | `library-seat.test.mjs` "...declares tools is refused, and the message names tools" (exit 2) |
|
||||
| M8 mosaic-bus, no tools, allowed | `library-seat.test.mjs` "...with no tools passes the engine rule" |
|
||||
| M8 unreadable business file | `library-seat.test.mjs` "an unreadable business file refuses check with discord-business-unreadable" (exit 2) |
|
||||
| M8 seat in no library business checks clean | `library-seat.test.mjs` "a binding on a seat that is in no library business checks clean" |
|
||||
| Q3 reconcile false | r1 reader test asserts `[false, "conversation-not-listed", false]` |
|
||||
| Q4 HTTP 404 and 503 | `creative-hidden.test.mjs` (status asserted); `serve.test.mjs` pin test |
|
||||
|
||||
## Choices (for Filbert to check)
|
||||
|
||||
1. **Absent versus unreadable `businesses/`.** Only nothing at the path reads as empty (lstat ENOENT). A
|
||||
dangling symlink, a file, or an unreadable directory throws. Coordinator ruling 1.
|
||||
2. **Sets are read once per public call.** `creativeCheck()` runs once in each of `catalogue()`, `open()` and
|
||||
`next()`. The returned predicate answers every root in that call. A catalogue with zero roots still
|
||||
loads the sets once, so an unreadable business file refuses even an empty list. Coordinator ruling 2.
|
||||
3. **Behavior change, kept on coordinator ruling (round 2 follow-up): an unknown id or cursor loads the sets
|
||||
first.** In `open()`, the check runs after `checkCaller` and before `resolveConversation`. In `next()`, it
|
||||
runs at the start of the try. So with an unreadable business file, a known id and an unknown id both give
|
||||
503 `conversation-business-unreadable`, where round 1 gave 404 `unknown-conversation` for the unknown id.
|
||||
Checking the id first would give 503 for a hidden creative id and 404 for an unknown one, which reveals
|
||||
whether the hidden id exists. Loading first answers every id the same way (fail closed). This reverses
|
||||
round 1 choice 3. Test: `creative-hidden.test.mjs` "with an unreadable business file, a known and an unknown id both refuse with 503".
|
||||
4. **Discord refusals use the `code: message` form and exit 2.** Exit 3 is for brake refusals (STOP or a held
|
||||
binding). A seat's engine or library membership is not a brake. The business CLI is not used as precedent.
|
||||
5. **The thread-store wall exists in part already.** `rootsFromSpecs` (`reader.mjs` line 44) drops any
|
||||
sessions directory that is not `<project>/.pi/state/<seat>/sessions`. The M7 check is the second wall, for
|
||||
roots built another way. The test checks both.
|
||||
|
||||
## Static checks (read-only; no node)
|
||||
|
||||
- **COPY_PATHS** (`scripts/stack-copy.sh` 29 to 41): `packages/business/src`, `packages/conversation/src`,
|
||||
`packages/discord/src` and `packages/library/src` are present. `packages/control-board/src` is not. COPY_PATHS
|
||||
is not changed.
|
||||
- **Stale names.** `grep -rn -E "isCreative|creativeRootPredicate" packages scripts` finds nothing after this round's edits.
|
||||
- **Board starts without `businessDir`.** Webui: all five direct starts pass it. Control board:
|
||||
- `serve.test.mjs` has seven single-line starts without it (lines 402, 445, 464, 483, 509, 528, 907) and four
|
||||
multi-line starts without it (114, 149, 229, 258). None of those tests reaches a conversation route.
|
||||
Every conversation route call in the file (lines 1027 to 1171) follows a start that passes `businessDir`
|
||||
(1006, 1075, 1124). Checked by line order.
|
||||
- `creative-hidden.test.mjs` builds `creativeCheck(f.dir)` on its own scratch directory. No default is used.
|
||||
- `discord.test.mjs` passes `businessDir` (line 123).
|
||||
- **Consumers of the two fixtures** (exact import strings, `from './fixture.mjs'`, `from './history-fixture.mjs'`):
|
||||
- Direct `fixture.mjs` importers, 31 test files: board-held, browser-edge, browser, bus-browser, bus,
|
||||
chat-browser, chat, discord, docs-browser, editor-browser, editor, instances-browser, limits-browser,
|
||||
limits-route, messages, preview, proof, relaunch, return-flow, routes-browser, send-browser, send, serve,
|
||||
settings-form, shell, sso-browser, sso, tester-listener, usage-browser, usage, views.
|
||||
- `history-fixture.mjs` importers, 2 test files: conversation, history-return-flow.
|
||||
- Total fixture consumers: 33. Plus `tester-browser`, `tester-cli` and `proof-cli` (through
|
||||
`tester-fixture.mjs`, ruling 3): 36 files in phase B.
|
||||
|
||||
## Phase B commands (after "slot clear" only)
|
||||
|
||||
TMPDIR is set by the coordinator under `/mnt/storage/scratch/tmp`. The commands below assume it. The new
|
||||
tests call `tmpdir()`, so an unset TMPDIR would put them under `/tmp`, which bwrap hides.
|
||||
|
||||
```sh
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/conversation && node --test tests/
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/discord && node --test tests/
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/business && node --test tests/
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/control-board && node --test tests/
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e && scripts/test-discord.sh
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/cli && node --test tests/stack-copy.test.mjs
|
||||
cd /mnt/storage/src/mosaic-stack/.claude/worktrees/agent-a1b36cb7769b3d00e/packages/webui && node --test tests/board-held.test.mjs tests/browser-edge.test.mjs tests/browser.test.mjs tests/bus-browser.test.mjs tests/bus.test.mjs tests/chat-browser.test.mjs tests/chat.test.mjs tests/conversation.test.mjs tests/discord.test.mjs tests/docs-browser.test.mjs tests/editor-browser.test.mjs tests/editor.test.mjs tests/history-return-flow.test.mjs tests/instances-browser.test.mjs tests/limits-browser.test.mjs tests/limits-route.test.mjs tests/messages.test.mjs tests/preview.test.mjs tests/proof.test.mjs tests/relaunch.test.mjs tests/return-flow.test.mjs tests/routes-browser.test.mjs tests/send-browser.test.mjs tests/send.test.mjs tests/serve.test.mjs tests/settings-form.test.mjs tests/shell.test.mjs tests/sso-browser.test.mjs tests/sso.test.mjs tests/tester-listener.test.mjs tests/usage-browser.test.mjs tests/usage.test.mjs tests/views.test.mjs tests/tester-browser.test.mjs tests/tester-cli.test.mjs tests/proof-cli.test.mjs
|
||||
```
|
||||
|
||||
The webui line is 36 files, run whole. `history-fixture.mjs` and `tester-fixture.mjs` run through their
|
||||
consumers. No test has been run. The expectations above come from reading the code.
|
||||
|
||||
## Open items
|
||||
|
||||
a. Phase B checks the dangling-symlink test on this platform (lstat then readdir ENOENT must throw).
|
||||
b. Phase B checks the spy count (1 after catalogue, 2 after one open). A failure here means a public call
|
||||
reads the factory more than once.
|
||||
c. Choice 3 is settled by the coordinator: keep 503 for unknown ids too. Phase B checks the new known-and-unknown test.
|
||||
d. The browser-based webui tests need a browser in this environment. A failure there would be environmental.
|
||||
Phase B will show which.
|
||||
e. Nothing here has been executed. Phase B checks every expectation, including the unreadable-file test
|
||||
(EACCES must throw), the root skip (uid 0 skips), and the webui consumers.
|
||||
|
||||
## Resolved
|
||||
|
||||
- Dangling symlink at `businesses/` (round 1 open item a, and round 1 question 1): refuses, ruling 1.
|
||||
- Tester and proof CLI tests (round 1 question 2): join phase B, ruling 3.
|
||||
- Per-root reads (round 1 open item b): replaced by one read per public call, ruling 2.
|
||||
|
||||
## Rebase onto aca16404c, run limits, index export (2026-10-22, sage-2708)
|
||||
|
||||
Added after the text above; nothing above is edited. "Nothing is committed" above was true when written.
|
||||
|
||||
- Commit `6f656abac` on `researcher/row220-r2`, one commit, rebased from da9555ca2 onto origin/refactor
|
||||
`aca16404c`. Row 253 has not landed; this rebases again onto 253's tip before any gate.
|
||||
- Drift check, da9555ca2..aca16404c against my 18 paths: one overlap, `packages/discord/src/cli.mjs`
|
||||
(the processing-control imports, rows 264 and neighbours). The conflict was the import block only. I
|
||||
kept both sides: the two landed `processing-control` imports and my `loadSeatSets` import. No landed hunk
|
||||
is reverted. `checkLibraryEngine` and its call in `prepare()` merged without a marker.
|
||||
- Board starts: no test added since da9555ca2 starts the control board or reads a conversation route
|
||||
without `businessDir`. `packages/webui/tests/shell.test.mjs` fakes the board and does not start it.
|
||||
- Two edits since the r2 text: (1) a missing space in `reader.mjs` `next()` (`held =typeof`) fixed;
|
||||
(2) `packages/business/src/index.mjs` gets `export { loadSeatSets } from "./library-seats.mjs";`. The text
|
||||
above says "no export line". RULINGS.md Q1 says the helper is "exported from the package index" and the
|
||||
scope amendment lists "its export line", so the file wins and the export is added. The two callers still
|
||||
import by path, so nothing needs the export today. Filbert: drop the line if you read the ruling otherwise.
|
||||
The export adds no test; `node --check` has not been run on it (no node before the CLEAR).
|
||||
|
||||
### Run limits (fixed before any run)
|
||||
|
||||
Every node file runs as its own `setsid -w timeout -k 10 LIMIT node --test FILE`; nothing runs unbounded.
|
||||
|
||||
- Node test files: LIMIT 900 s. I found no recorded per-file time for the conversation, discord, business,
|
||||
control-board or webui files. 900 s is above the longest recorded whole-package run I know of (cli, 591 s,
|
||||
row 281 BUILD.md "Runs" step 3), so it holds the 3x rule for any single file. The first run records each
|
||||
file's time; if a file passes 300 s, the limit is re-derived from it before the next run.
|
||||
- Shell scripts: `timeout -k 10 LIMIT bash scripts/test-X.sh`, LIMIT = 3x the longest recorded run plus
|
||||
120 s, taken from Megavolt's row 281 table (`agents/megavolt/work/queue-281/BUILD.md`, from Gander's
|
||||
`agents/gander/work/queue-278/SHELL-LIMITS.json`). A script with no limit does not run.
|
||||
|
||||
| Script | LIMIT | Script | LIMIT |
|
||||
|---|---|---|---|
|
||||
| test-auth | 126 s | test-land-work | 158 s |
|
||||
| test-conductor | 145 s | test-queue | 191 s |
|
||||
| test-config | 130 s | test-release | 157 s |
|
||||
| test-discord | 147 s | test-task | 231 s |
|
||||
| test-extension-package | 130 s | test-foundation | 240 s |
|
||||
|
||||
Any timeout goes to Sage, then Mos, unplaced (mos-1275).
|
||||
|
||||
## Round 2b: Filbert's early look and sage-2719 (2026-10-22T14:46Z)
|
||||
|
||||
Added after the text above; nothing above is edited. Where earlier text says `businessDir`, the parameter is
|
||||
now `configDirectory` (below). Second commit on `researcher/row220-r2`, on top of ea6dc9eff. Not pushed, not
|
||||
run; `node --check` has not been run on any of these files (no node before the CLEAR).
|
||||
|
||||
Rulings applied (sage-2719): Q1 keep the index export. C1, C2 and the fixture scope are changes.
|
||||
|
||||
### Fixture scope: the parameter is a config directory
|
||||
|
||||
`createServer` took `businessDir`, but `creativeCheck(dir)` reads `<dir>/businesses`, so every fixture that
|
||||
passed `join(root, 'businesses')` made the reader look in `<root>/businesses/businesses`. That read nothing
|
||||
(ENOENT, empty sets): isolation from the real config held by accident. Now:
|
||||
|
||||
- `createServer({ configDirectory = configDir() })` in `serve.mjs`. The parameter is not named `configDir`
|
||||
because the import of that name is the default's source.
|
||||
- Every fixture passes the config directory it owns: `configDirectory: root` in the five webui files,
|
||||
`f.root` in `control-board/tests/discord.test.mjs`, and `f.configDirectory` in `serve.test.mjs`.
|
||||
`conversationFixture()` and `replyFixture()` both return `configDirectory: root`. The seven reply starts
|
||||
that passed `f.businessDir` (undefined, falling back to the real `configDir()`) now pass a real value.
|
||||
- The dead `mkdirSync(join(root, 'businesses'))` lines in the five webui fixtures and in `serve.test.mjs`
|
||||
are removed. So is the one in `discord/tests/helpers.mjs` `makeDeployment`, whose comment said "a missing
|
||||
one refuses"; that was false (lstat ENOENT gives empty sets). The comment now says nothing there means no
|
||||
library seat. Tests that need a `businesses/` directory create it: `writeJson` does, and the two
|
||||
unreadable-file tests in `library-seat.test.mjs` call `mkdirSync` themselves.
|
||||
- Proof that the fixture's own business file is the one read: the new default-reader test (C2) hides `bean`
|
||||
and shows `pm`, and nothing else in that fixture says which is which.
|
||||
|
||||
### C1: M8 on `run`
|
||||
|
||||
`library-seat.test.mjs` gains three `run` tests: a raw engine, a mosaic-bus binding with `tools`, and an
|
||||
unreadable business file. Each asserts exit 2 and the code in stderr, and that `s.d.journalDir` does not
|
||||
exist (`ensureJournal` comes after `prepare()` in `run`, so nothing was started). `run()` calls
|
||||
`initializeControl(dataRoot)` first, because `run` stops at `assertProcessing` before `prepare()`, as the
|
||||
STOP test in `binding.test.mjs:203` does. There is no `run` test for a clean seat: that would go on to the
|
||||
network.
|
||||
|
||||
### C2: the default reader
|
||||
|
||||
`creative-hidden.test.mjs`, "the board's default reader hides a creative seat's session from the listing and
|
||||
the detail route". It calls `startServer` with no `conversationReader`, `configDirectory: f.dir`, a `bean`
|
||||
session and a `pm` session on disk, and drives HTTP: the listing holds `pm` only; bean's detail is 404
|
||||
`conversation-not-listed`; pm's detail is 200; after a broken business file lands the listing is 503
|
||||
`conversation-business-unreadable`.
|
||||
|
||||
### Mutants to throw (CLEAR only; none thrown; the kill claims are unverified)
|
||||
|
||||
| Id | Mutation | Meant to be killed by |
|
||||
|---|---|---|
|
||||
| M7-w | delete `creativeCheck: creativeCheck(configDirectory),` in `createServer` (`serve.mjs`) | the default-reader test, first assertion (the listing shows `bean`) |
|
||||
| M7-c | `hiddenCheck()` in `reader.mjs` open() returns `() => false` | reader.test "hides the creative seat" and the `creative-hidden` 404 asserts |
|
||||
| M7-o | `creativeCheck` catch returns an empty set instead of throwing | the 503 asserts in `creative-hidden` |
|
||||
| M8-p | delete `checkLibraryEngine(...)` in `prepare()` | the three `check` refusals and the three `run` refusals; one mutant covers both verbs because they share `prepare()` |
|
||||
| M8-e | `loadSeatSets` returns empty sets on any lstat error | `library-seats` ENOENT-vs-file tests |
|
||||
|
||||
### `refusalResponse` and `catalogue()`'s refusal (past "predicate wiring and REFUSAL_STATUS only")
|
||||
|
||||
Why it is needed: the reader's `catalogue()` now returns a refusal when the business check throws (M7: no
|
||||
partial list), and `conversationResponse` had no way to turn a catalogue refusal into a status. `open` and
|
||||
`next` already built `{ status: REFUSAL_STATUS[code] ?? 422, body: { error, refusal: { code, reconcile } } }`
|
||||
inline; that expression moved, character for character, into `refusalResponse(out)` so the listing could
|
||||
use it too.
|
||||
|
||||
No existing route's status or body changes:
|
||||
|
||||
- `open` and `next` refusals: the same expression, now behind a function. `git diff aca16404c..HEAD --
|
||||
packages/control-board/src/serve.mjs` shows the line as a move.
|
||||
- `/api/conversations` success: `{ status: 200, body: listing }` where `listing` is what the route returned
|
||||
before. `catalogue()` returned `ok: true` on every path before this row, so the new branch is reached only
|
||||
by the business refusal.
|
||||
- The "no parameters are accepted" 400 is untouched.
|
||||
|
||||
Phase B runs `serve.test.mjs`, `discord.test.mjs` and the conversation route tests whole, so Filbert can
|
||||
check this statement against a green run.
|
||||
|
||||
### Smaller changes from the early look
|
||||
|
||||
- Spy test: one `next()` call adds a third `creativeCheck` count (`reader.test.mjs`). The catalogue assert
|
||||
also checks `refusedRoots` is empty, in the same test and in the thread-store test in `creative-hidden`.
|
||||
- `library-seats.test.mjs` mode-000 test pins `BusinessError` and its message (`must be a regular,
|
||||
non-symbolic-link file`, from `readJsonFile`'s open failure).
|
||||
- "passes the engine rule" now asserts exit 0 and `offline check passed`. The expectation is unverified: if
|
||||
a mosaic-bus binding fails some other offline check, that is a first-run red to place (mos-1272).
|
||||
|
||||
### Carried, not changed here
|
||||
|
||||
- `conversation/src/controller.mjs:176` builds a reader with no `creativeCheck`. No `src` file imports it
|
||||
today; a live wiring must pass the check. Raised for Sage.
|
||||
- `discord run` reads the business file once at start. `engine` and `tools` are FIXED_KEYS, so only a
|
||||
business-file change while a connector runs goes unseen until the next start.
|
||||
- `creative-hidden` and `library-seats` use the repo `roles/` as the default `rolesDir`.
|
||||
|
||||
### Gate additions
|
||||
|
||||
Phase B list is unchanged: the same suites run whole, plus the files above are inside them. The drift check
|
||||
against `aca16404c..refactor`, including files these tests copy or read, repeats after the rebase onto 253.
|
||||
|
||||
## Round 2c: refusalResponse statement corrected from the diff, controller.mjs follow-up (2026-10-22T14:55Z, sage-2729)
|
||||
|
||||
Appended after Round 2b; nothing above is edited. This replaces Round 2b's refusalResponse statement, which I
|
||||
wrote from memory of the code. Source: `git diff aca16404c HEAD -- packages/control-board/src/serve.mjs` and
|
||||
`packages/conversation/src/reader.mjs`, read in full. No test has run.
|
||||
|
||||
### What the diff shows in serve.mjs
|
||||
|
||||
- `open` and `next` refusal: the object expression `{ status: REFUSAL_STATUS[out.refusal.code] ?? 422, body: {
|
||||
error: out.refusal.message, refusal: { code, reconcile } } }` moved unchanged into `refusalResponse(out)`.
|
||||
The two call sites reach it by `return refusalResponse(out)`. Status and body are identical for every code
|
||||
that reached it before.
|
||||
- `REFUSAL_STATUS` gains two keys, `conversation-not-listed` (404) and `conversation-business-unreadable`
|
||||
(503). Before, those codes did not exist, so no earlier response used them or changes. Every other key is
|
||||
untouched.
|
||||
- `/api/conversations` with a query string: the 400 `no parameters are accepted` line is untouched.
|
||||
- `/api/conversations` success: before, `{ status: 200, body: reader.catalogue() }`; now the same object
|
||||
when `listing.ok` (it is the same `listing`), so the 200 body is unchanged.
|
||||
- The one new response shape: `/api/conversations` can return a refusal (503 or the refusal's status) when
|
||||
the business check throws. That is the M7 behaviour, not a change to an existing route's response.
|
||||
|
||||
### What the diff shows in reader.mjs, past my Round 2b statement
|
||||
|
||||
`catalogue()` is now wrapped in `try { ... } catch (err) { return refuse(err); }`. `refuse` returns a refusal
|
||||
object for a `Refusal` and rethrows anything else (`reader.mjs:352-355`). So a non-Refusal error still
|
||||
propagates as before. A `Refusal` escaping the loop body would now come back as `{ ok: false }` where it
|
||||
used to throw. The loop already catches `Refusal` from `listSessionFiles` and from `summarize`, so the only
|
||||
`Refusal` that newly reaches the wrapper is the hidden check's. I have not confirmed that `listRoots()`
|
||||
(`rootsFromSpecs`, `loadRegistrations`) never throws a `Refusal`; if it does, that call now returns a
|
||||
refusal. Filbert's round check should read `rootsFromSpecs` for `Refusal` throws.
|
||||
|
||||
Round 2b said the old `catalogue()` "returned ok: true on every path". True for returns; it could throw.
|
||||
|
||||
### Named follow-up for DEFERRED (Sage lands it with 220; it does not block 220)
|
||||
|
||||
`packages/conversation/src/controller.mjs:176` builds a reader with no `creativeCheck`. No `src` file imports
|
||||
`controller.mjs` today, so no live path leaks. Whoever first wires `controller.mjs` live must pass
|
||||
`creativeCheck` (`creativeCheck(configDirectory)` from `control-board/src/serve.mjs`, or the reader's own
|
||||
equivalent) and add a test that a creative seat's sessions stay hidden through that path.
|
||||
|
||||
## Round 2d: can listRoots() throw a Refusal into catalogue()? No (2026-10-22T14:53Z, sage-2730)
|
||||
|
||||
Appended after Round 2c; closes the question Round 2c left open. Static read on 9a9f6dd54; nothing run.
|
||||
Filbert reached the same answer independently (filbert-220-r2c-static).
|
||||
|
||||
`catalogue()` calls `listRoots()` inside the new `try`. In production that is `serve.mjs:261`,
|
||||
`roots: () => rootsFromSpecs(specs, loadRegistrations(seatsDir).registrations)`. The only `Refusal` class
|
||||
the reader catches is the one in `conversation/src/safe-fs.mjs:21`.
|
||||
|
||||
- `rootsFromSpecs` (`conversation/src/reader.mjs:38-71`) calls `resolve`, `join`, `basename`, `Array.find`,
|
||||
`toTime` and `samePath`, and constructs no `Refusal`.
|
||||
- `toTime` (`conversation/src/pi.mjs:21-26`) returns a value or null and has no throw.
|
||||
- `samePath` (`seat/src/seat.mjs:261-269`) wraps `realpathSync` in try/catch and returns false.
|
||||
- `loadRegistrations` (`control-board/src/scan.mjs:272-292`) catches `SeatError` and rethrows anything
|
||||
else. It calls `readRegistration` (`seat/src/seat.mjs:200-216`), which throws `SeatError` only.
|
||||
- `grep -n "Refusal\|refuse(" packages/control-board/src/scan.mjs packages/seat/src/seat.mjs` finds
|
||||
nothing, and neither file imports `safe-fs.mjs` (checked with `grep -n "^import"`). A `Refusal` cannot
|
||||
originate in either.
|
||||
- Inside the loop, `listSessionFiles` is caught at `reader.mjs` (the `catch (err)` that rethrows
|
||||
non-Refusals) and `summarize` turns an `openSessionFile` Refusal into a `denied` row.
|
||||
|
||||
So the only `Refusal` that reaches the new catch is the hidden check's `conversation-business-unreadable`.
|
||||
Any other error still throws through `refuse()` (`reader.mjs:352-355`) exactly as before.
|
||||
|
||||
Therefore `/api/conversations` has no status change for a path that existed before this row: the old
|
||||
throw paths are unreachable by a `Refusal`. The one new response is the business refusal, already pinned by
|
||||
the creative-hidden 503 assertion and the reader catalogue-refusal test. No extra test is added.
|
||||
|
||||
The exception is an injected `roots` function that throws a `Refusal` itself; only a test could pass one,
|
||||
and none does.
|
||||
|
||||
Time correction (2026-10-22T14:51:01Z): the heading times of Round 2c (14:55Z) and Round 2d (14:53Z) were written before I read the clock. The commits were made at 14:49:40Z (9a9f6dd54) and 14:50:52Z (176ac262e). The heading of Round 2b (14:46Z) is also an estimate; its commit e4a204836 was made before 14:47:53Z.
|
||||
|
||||
## Round 4: rebase onto 45dca69f6, drift check (2026-10-23T18:53:43Z, sage-3576)
|
||||
|
||||
Appended; nothing above is edited. Git only; nothing run, nothing pushed. Branch researcher/row220-r4 holds the
|
||||
five row 220 commits on 45dca69f6 (origin/refactor, 10-23). Earlier base: 48e450ce8 (row 253).
|
||||
|
||||
- Rebase: clean. `git range-diff 48e450ce8..3ef6da90d 45dca69f6..researcher/row220-r4` shows all five commits as `=`,
|
||||
so no patch changed.
|
||||
- Landed in 48e450ce8..45dca69f6: 1175 files (47 packages/bus, 32 webui, 29 cli, 10 plugins, 9 seat, 4 harness,
|
||||
2 tasks, 7 scripts, BUILD-LOG.md, the rest agents/ and docs/).
|
||||
- Path overlap with this row's 19 paths: none.
|
||||
- Landed files in the packages these tests touch: webui (src/serve.mjs, src/cli.mjs, chat and bus files,
|
||||
tests). Row 220's webui test changes are five fixtures that start the control-board server with
|
||||
`configDirectory: root` and start the webui server with only `{ port, board }`; no landed webui file passes or reads
|
||||
configDirectory. The landed webui tests are not in this row's change set, but a whole webui run covers them.
|
||||
No landed file in control-board, conversation, discord or business.
|
||||
- Other users of the touched symbols (git grep on the r4 tree for createReader, rootsFromSpecs, creativeCheck,
|
||||
configDirectory, businessDir, loadSeatSets, checkLibraryEngine, conversationReader outside this diff):
|
||||
only conversation/src/controller.mjs:173-176 (the follow-up named in Round 2c) and the conversation README.
|
||||
- Hash pins: grep for 64-hex in packages/*/tests and scripts finds 364 files (positive control), and none pins a
|
||||
row 220 source file by hash. Path-string hits for packages/discord/src/cli.mjs (copy-service.test.mjs,
|
||||
scripts/test-discord.sh, discord-service.sh) are paths, not hashes.
|
||||
- Added to the gate list: `packages/discord/tests/helpers.mjs` is imported by 19 discord test files, and this row
|
||||
removed its empty `businesses` mkdir. Phase B must run the discord package whole (and `scripts/test-discord.sh`),
|
||||
not only library-seat.test.mjs.
|
||||
|
||||
## Round 5: phase B on 2bb146b28, R1 and R2 fixes (mos-1528) (2026-10-23T20:21:39Z, sage-3712)
|
||||
|
||||
Appended; nothing above is edited. The tree is 2bb146b28 plus the commits of branch researcher/row220-r5.
|
||||
|
||||
Phase B receipts on 2bb146b28 (researcher-r220-kit out-b1, out-b2, out-b3, out-mut, out-npm):
|
||||
|
||||
- discord 20 files, 200 pass; conversation 7 files, 187 pass; scripts/test-discord.sh rc=0, 82 passed.
|
||||
- control-board 7 files, 128 pass, 1 fail (serve.test.mjs, R2 below); business 26 files, 237 pass, 1 fail
|
||||
(library-seats.test.mjs, R1 below).
|
||||
- webui: first run 409 pass, 1 fail (editor-provenance.test.mjs). Cause: the esbuild in the worktree's
|
||||
node_modules resolved to the root copy (0.28.1) and not to packages/webui's own pin. Fix (R3, no repo change):
|
||||
`cd packages/webui && npm ci --ignore-scripts --no-audit --no-fund` in the worktree, output in out-npm/npm-ci.txt;
|
||||
esbuild is then 0.28.2. Rerun of all 65 webui files whole with MOSAIC_REQUIRE_EDITOR_BUILD=1: 65 of 65 rc=0, 410
|
||||
pass, 0 fail.
|
||||
|
||||
Fixes (both are test-only; src/ is unchanged from 2bb146b28; Mos ruled both in mos-1528):
|
||||
|
||||
- R1, `packages/business/tests/library-seats.test.mjs`, test "M7/M8: a business file that can't be read throws; no
|
||||
empty set". The fixture registers `rmSync(root)` as a `t.after` hook before the test registers its
|
||||
`chmodSync(file, 0o600)` restore, so on the way out the restore found the file already removed and threw ENOENT.
|
||||
Old: `t.after(() => chmodSync(file, 0o600));`. New: the same call inside try/catch that rethrows anything but
|
||||
ENOENT. The assertion is unchanged.
|
||||
- R2 (option A, mos-1528), `packages/control-board/tests/serve.test.mjs:1073`, test "conversation routes (F16): a
|
||||
foreign Host, a wrong port and a cross-origin Origin get 403 before the reader runs, with no CORS headers".
|
||||
Failure: the same-origin request at the end reached the route, the spy's `catalogue` returned the number
|
||||
`calls.push(...)` gives (1), and 220's `conversationResponse` reads `listing.ok`, so `refusalResponse(1)` threw on
|
||||
`out.refusal.code` and the route answered 500. Old text:
|
||||
`const spy = { catalogue: () => calls.push("catalogue"), open: () => calls.push("open"), next: () => calls.push("next") };`
|
||||
New text: `catalogue` records the call, then returns `{ ok: true, conversations: [], refusedRoots: [] }`; `open` and
|
||||
`next` are unchanged (the test never reaches them with a passing request). The assertions are unchanged: 200 for the
|
||||
same-origin request and `calls` equal to `["catalogue"]`. serve.mjs stays as 220 has it.
|
||||
Added test, same file before "every refusal code the reader can raise has an HTTP status": a reader whose
|
||||
`catalogue()` returns `{ ok: false, refusal }` gets `conversation-business-unreadable`'s status (503) and the body
|
||||
`{ error, refusal: { code, reconcile } }`. No such route test existed; the earlier claim that no route's status or
|
||||
body changed was wrong for a reader that returns a bare value, which the old spy did.
|
||||
|
||||
Mutants thrown on 2bb146b28 (out-mut; the base run for library-seats was red from R1, so a kill by that file is not
|
||||
shown there):
|
||||
|
||||
| Mutant | Result |
|
||||
| --- | --- |
|
||||
| M7-w | killed by control-board creative-hidden ("the board's default reader hides a creative seat's session…") |
|
||||
| M7-c | killed: creative-hidden 4 fail, conversation reader 3 fail |
|
||||
| M7-o | killed: creative-hidden 4 fail |
|
||||
| M8-p | killed: discord library-seat 6 fail |
|
||||
| M8-e | SURVIVED: no test failed other than the base red in library-seats; handled in the next entry |
|
||||
|
||||
Run limit: stack-release.test.mjs took 644 s. The per-file limit is 3 x 644 = 1932 s. Sage's gates run cli whole
|
||||
under 2100 s.
|
||||
|
||||
Reruns owed after r5 (run on Sage's CLEAR): the touched files whole (library-seats, serve, and the M8-e test),
|
||||
control-board whole, business whole, and every 220 mutant, M8-e included. Results are appended as a new entry.
|
||||
|
||||
## Round 5b: correction of the M8-e kill claim, and the test that kills it (2026-10-23T20:21:52Z, sage-3715)
|
||||
|
||||
Appended; nothing above is edited. This is a correction to the "Mutants to throw" table in this file (the M8-e
|
||||
row, around line 270), which says M8-e, "`loadSeatSets` returns empty sets on any lstat error", is killed by the
|
||||
`library-seats` ENOENT-vs-file tests. That claim is wrong. M8-e changes the lstat catch to
|
||||
`return { librarySeats: new Set(), creativeSeats: new Set() };` for every error code, so only a test that makes lstat
|
||||
fail with a code other than ENOENT can kill it. The file-as-businesses test and the dangling-symlink test I named
|
||||
make lstat succeed or fail with ENOENT, never another code, so they pass under M8-e. Phase B on 2bb146b28 showed
|
||||
the mutant surviving (out-mut, M8-e: no failure besides the base red).
|
||||
|
||||
Added in this commit, `packages/business/tests/library-seats.test.mjs`: "M7/M8: a config directory under a regular
|
||||
file throws (lstat fails with ENOTDIR); no empty set". The config path is a regular file, so
|
||||
`lstat(<file>/businesses)` fails with ENOTDIR; the test expects `BusinessError` matching "cannot list businesses".
|
||||
It is a new test in this row's own file, in its own commit, and droppable without touching R1 or R2.
|
||||
|
||||
The rerun of M8-e on r5 (the test must fail under it) is owed and runs on CLEAR; the result is appended as a new
|
||||
entry.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Row 304 / Vikunja 590 / Gitea #1811
|
||||
|
||||
Owner Rocko; reviewer Dewey; gate owner Sage. Base c16e943b90ef71ac96cfa5314bbea6040634d14d.
|
||||
Brief docs/plans/2026-10-23_gate-env-rows.md first section, approved exact
|
||||
one-line old-test change under mos-1442 point 1 / sage-3362.
|
||||
|
||||
265 is queued and has no active build, so 304 goes first. Slot requested;
|
||||
no Node, test, queue claim or push before Sage CLEAR.
|
||||
|
||||
Before the edit, run the unchanged whole claude-session.test.mjs with
|
||||
fixture-only HOME, CLAUDE_CONFIG_DIR and scripted API under an underscore
|
||||
TMPDIR and a hyphen TMPDIR. Expected: underscore 9/1 only at the unrestricted
|
||||
memory control (:298), hyphen 10/0. Record both raw results here. Stop for
|
||||
Sage if the installed CLI's mapping differs from [^A-Za-z0-9] -> '-'.
|
||||
|
||||
Only then replace /[/.]/g with /[^A-Za-z0-9]/g on the slug line. Every
|
||||
assertion and every other test line stays byte-identical. No product change.
|
||||
Acceptance is whole file green under mktemp, underscore, and space TMPDIR
|
||||
(if the CLI accepts space); old-regex mutant must fail the :298 assertion
|
||||
under underscore. Run scope and claim wait for Sage's slot grant.
|
||||
|
||||
Use short fixture TMPDIRs under /mnt/storage/scratch/tmp, process groups,
|
||||
explicit TAP and bounded runs. No Docker, browser, suites or live state.
|
||||
Keep token/config paths and real provider access out of every fixture.
|
||||
|
||||
## Evidence, sage-3366 (mos-1442 point 1)
|
||||
|
||||
queue next selected304; claim rev3521, rc0. Base test unchanged until both
|
||||
mapping probes finished. Correct restricted-check citation: :293, not :291.
|
||||
|
||||
| Run | TMPDIR | Exit | Pass/fail/skip |
|
||||
|---|---|---|---|
|
||||
| baseline-underscore | /mnt/storage/scratch/tmp/rk304_OV81Op | 1 | 9/1/0 |
|
||||
| baseline-hyphen | /mnt/storage/scratch/tmp/rk304-UnWJbm | 0 | 10/0/0 |
|
||||
| fixed-mktemp | /mnt/storage/scratch/tmp/rk304yw6LWT | 0 | 10/0/0 |
|
||||
| fixed-underscore | /mnt/storage/scratch/tmp/rk304_MyhpSs | 0 | 10/0/0 |
|
||||
| fixed-space | /mnt/storage/scratch/tmp/rk304 i5d7HA | 0 | 10/0/0 |
|
||||
| M1-old-regex | /mnt/storage/scratch/tmp/rk304_595gfj | 1 | 9/1/0 |
|
||||
|
||||
Baseline underscore and M1 both fail only the named unrestricted positive
|
||||
control at claude-session.test.mjs:298:10, ERR_ASSERTION, missing MARKER-MEMORY.
|
||||
Hyphen baseline is green. This confirms the expected installed-CLI mapping.
|
||||
Space is accepted and passes after the fix. All3 fixed whole-file runs30/0/0;
|
||||
baseline hyphen10/0/0; M1 one assertion kill (whole file9/1/0).0cancellations,
|
||||
0skips throughout. Tests11:05:01Z-11:05:36Z, no unexpected red or retry.
|
||||
Every other source byte, including all assertions, is unchanged, recorded in
|
||||
assertion-preservation.json. Fixed test sha2567690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9.
|
||||
Cwd node/chromium/claude sweep empty; no Docker/browser/suite/live activity.
|
||||
Sage's separate gate and Dewey's review still required; no gate claimed here.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"allOtherBytesUnchanged": true,
|
||||
"assertionLinesUnchanged": true,
|
||||
"baseSha256": "3a43f134d334e985e6dd50f641a7c1788de29beb86e2f9e2ac7f0ceaf924a8e0",
|
||||
"candidateSha256": "7690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
c16e943b90ef71ac96cfa5314bbea6040634d14d
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/packages/harness/tests/claude-session.test.mjs b/packages/harness/tests/claude-session.test.mjs
|
||||
index bf5cb2d29..d9c45e446 100644
|
||||
--- a/packages/harness/tests/claude-session.test.mjs
|
||||
+++ b/packages/harness/tests/claude-session.test.mjs
|
||||
@@ -269,7 +269,7 @@ test("claude adapter: a directory pin exits 2 before it runs; a pin with a space
|
||||
|
||||
test("claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do", { skip }, async (t) => {
|
||||
const { dir, workspace, env } = await session(t, []);
|
||||
- const slug = realpathSync(workspace).replace(/[/.]/g, "-");
|
||||
+ const slug = realpathSync(workspace).replace(/[^A-Za-z0-9]/g, "-");
|
||||
const plant = {
|
||||
"MARKER-USER": [join(env.HOME, ".claude", "CLAUDE.md"), join(env.CLAUDE_CONFIG_DIR, "CLAUDE.md")],
|
||||
"MARKER-PARENT": [join(dir, "CLAUDE.md")],
|
||||
@@ -0,0 +1 @@
|
||||
7690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9 packages/harness/tests/claude-session.test.mjs
|
||||
@@ -0,0 +1 @@
|
||||
packages/harness/tests/claude-session.test.mjs
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,14 @@
|
||||
START baseline-underscore 2026-10-23T11:05:01.371651+00:00 /mnt/storage/scratch/tmp/rk304_OV81Op
|
||||
END {"label": "baseline-underscore", "start": "2026-10-23T11:05:01.371651+00:00", "end": "2026-10-23T11:05:06.998843+00:00", "seconds": 5.627, "exit": 1, "counts": {"tests": 10, "pass": 9, "fail": 1, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304_OV81Op", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "3a43f134d334e985e6dd50f641a7c1788de29beb86e2f9e2ac7f0ceaf924a8e0", "skips": [], "log": "agents/rocko/work/queue-304/logs/baseline-underscore.tap"}
|
||||
START baseline-hyphen 2026-10-23T11:05:07.003823+00:00 /mnt/storage/scratch/tmp/rk304-UnWJbm
|
||||
END {"label": "baseline-hyphen", "start": "2026-10-23T11:05:07.003823+00:00", "end": "2026-10-23T11:05:12.829873+00:00", "seconds": 5.826, "exit": 0, "counts": {"tests": 10, "pass": 10, "fail": 0, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304-UnWJbm", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "3a43f134d334e985e6dd50f641a7c1788de29beb86e2f9e2ac7f0ceaf924a8e0", "skips": [], "log": "agents/rocko/work/queue-304/logs/baseline-hyphen.tap"}
|
||||
START fixed-mktemp 2026-10-23T11:05:12.834143+00:00 /mnt/storage/scratch/tmp/rk304yw6LWT
|
||||
END {"label": "fixed-mktemp", "start": "2026-10-23T11:05:12.834143+00:00", "end": "2026-10-23T11:05:18.560124+00:00", "seconds": 5.726, "exit": 0, "counts": {"tests": 10, "pass": 10, "fail": 0, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304yw6LWT", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "7690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9", "skips": [], "log": "agents/rocko/work/queue-304/logs/fixed-mktemp.tap"}
|
||||
START fixed-underscore 2026-10-23T11:05:18.564553+00:00 /mnt/storage/scratch/tmp/rk304_MyhpSs
|
||||
END {"label": "fixed-underscore", "start": "2026-10-23T11:05:18.564553+00:00", "end": "2026-10-23T11:05:24.340312+00:00", "seconds": 5.776, "exit": 0, "counts": {"tests": 10, "pass": 10, "fail": 0, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304_MyhpSs", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "7690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9", "skips": [], "log": "agents/rocko/work/queue-304/logs/fixed-underscore.tap"}
|
||||
START fixed-space 2026-10-23T11:05:24.344312+00:00 /mnt/storage/scratch/tmp/rk304 i5d7HA
|
||||
END {"label": "fixed-space", "start": "2026-10-23T11:05:24.344312+00:00", "end": "2026-10-23T11:05:30.170101+00:00", "seconds": 5.826, "exit": 0, "counts": {"tests": 10, "pass": 10, "fail": 0, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304 i5d7HA", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "7690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9", "skips": [], "log": "agents/rocko/work/queue-304/logs/fixed-space.tap"}
|
||||
START M1-old-regex 2026-10-23T11:05:30.173937+00:00 /mnt/storage/scratch/tmp/rk304_595gfj
|
||||
END {"label": "M1-old-regex", "start": "2026-10-23T11:05:30.173937+00:00", "end": "2026-10-23T11:05:36.100361+00:00", "seconds": 5.926, "exit": 1, "counts": {"tests": 10, "pass": 9, "fail": 1, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304_595gfj", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "3a43f134d334e985e6dd50f641a7c1788de29beb86e2f9e2ac7f0ceaf924a8e0", "skips": [], "log": "agents/rocko/work/queue-304/logs/M1-old-regex.tap"}
|
||||
BUILD_GREEN 2026-10-23T11:05:36.103508+00:00
|
||||
CWD_SWEEP []
|
||||
@@ -0,0 +1,99 @@
|
||||
TAP version 13
|
||||
# Subtest: claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
ok 1 - claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
---
|
||||
duration_ms: 810.176546
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: the hook alone blocks a path outside the workspace
|
||||
ok 2 - claude: the hook alone blocks a path outside the workspace
|
||||
---
|
||||
duration_ms: 503.543336
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a second turn resumes the first turn's session
|
||||
ok 3 - claude: a second turn resumes the first turn's session
|
||||
---
|
||||
duration_ms: 915.865915
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: --restricted is always passed
|
||||
ok 4 - claude adapter: --restricted is always passed
|
||||
---
|
||||
duration_ms: 4.807726
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
ok 5 - claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
---
|
||||
duration_ms: 15.271577
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
ok 6 - claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
---
|
||||
duration_ms: 5.309932
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
not ok 7 - claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
---
|
||||
duration_ms: 888.601244
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/rocko-r304/packages/harness/tests/claude-session.test.mjs:270:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
'MARKER-USER',
|
||||
'MARKER-PARENT',
|
||||
'MARKER-WS',
|
||||
- 'MARKER-MEMORY'
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0: 'MARKER-USER'
|
||||
1: 'MARKER-PARENT'
|
||||
2: 'MARKER-WS'
|
||||
3: 'MARKER-MEMORY'
|
||||
actual:
|
||||
0: 'MARKER-USER'
|
||||
1: 'MARKER-PARENT'
|
||||
2: 'MARKER-WS'
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r304/packages/harness/tests/claude-session.test.mjs:298:10)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: claude: a missing hook or MCP file refuses before claude starts
|
||||
ok 8 - claude: a missing hook or MCP file refuses before claude starts
|
||||
---
|
||||
duration_ms: 9.250346
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
ok 9 - claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
---
|
||||
duration_ms: 1764.786892
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
ok 10 - claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
---
|
||||
duration_ms: 871.328606
|
||||
type: 'test'
|
||||
...
|
||||
1..10
|
||||
# tests 10
|
||||
# suites 0
|
||||
# pass 9
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5870.59342
|
||||
@@ -0,0 +1,70 @@
|
||||
TAP version 13
|
||||
# Subtest: claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
ok 1 - claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
---
|
||||
duration_ms: 780.047981
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: the hook alone blocks a path outside the workspace
|
||||
ok 2 - claude: the hook alone blocks a path outside the workspace
|
||||
---
|
||||
duration_ms: 503.370776
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a second turn resumes the first turn's session
|
||||
ok 3 - claude: a second turn resumes the first turn's session
|
||||
---
|
||||
duration_ms: 848.822885
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: --restricted is always passed
|
||||
ok 4 - claude adapter: --restricted is always passed
|
||||
---
|
||||
duration_ms: 4.629499
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
ok 5 - claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
---
|
||||
duration_ms: 15.371187
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
ok 6 - claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
---
|
||||
duration_ms: 5.545444
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
ok 7 - claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
---
|
||||
duration_ms: 947.229815
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a missing hook or MCP file refuses before claude starts
|
||||
ok 8 - claude: a missing hook or MCP file refuses before claude starts
|
||||
---
|
||||
duration_ms: 8.467575
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
ok 9 - claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
---
|
||||
duration_ms: 1738.629452
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
ok 10 - claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
---
|
||||
duration_ms: 857.865121
|
||||
type: 'test'
|
||||
...
|
||||
1..10
|
||||
# tests 10
|
||||
# suites 0
|
||||
# pass 10
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5790.544695
|
||||
@@ -0,0 +1,100 @@
|
||||
TAP version 13
|
||||
# Subtest: claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
ok 1 - claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
---
|
||||
duration_ms: 748.693342
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: the hook alone blocks a path outside the workspace
|
||||
ok 2 - claude: the hook alone blocks a path outside the workspace
|
||||
---
|
||||
duration_ms: 499.864544
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a second turn resumes the first turn's session
|
||||
ok 3 - claude: a second turn resumes the first turn's session
|
||||
---
|
||||
duration_ms: 829.415272
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: --restricted is always passed
|
||||
ok 4 - claude adapter: --restricted is always passed
|
||||
---
|
||||
duration_ms: 4.710643
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
ok 5 - claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
---
|
||||
duration_ms: 14.085981
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
ok 6 - claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
---
|
||||
duration_ms: 4.998474
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
not ok 7 - claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
---
|
||||
duration_ms: 863.150735
|
||||
type: 'test'
|
||||
location: '/mnt/storage/scratch/rocko-r304/packages/harness/tests/claude-session.test.mjs:270:1'
|
||||
failureType: 'testCodeFailure'
|
||||
error: |-
|
||||
Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
[
|
||||
'MARKER-USER',
|
||||
'MARKER-PARENT',
|
||||
'MARKER-WS',
|
||||
- 'MARKER-MEMORY'
|
||||
]
|
||||
|
||||
code: 'ERR_ASSERTION'
|
||||
name: 'AssertionError'
|
||||
expected:
|
||||
0: 'MARKER-USER'
|
||||
1: 'MARKER-PARENT'
|
||||
2: 'MARKER-WS'
|
||||
3: 'MARKER-MEMORY'
|
||||
actual:
|
||||
0: 'MARKER-USER'
|
||||
1: 'MARKER-PARENT'
|
||||
2: 'MARKER-WS'
|
||||
operator: 'deepStrictEqual'
|
||||
stack: |-
|
||||
TestContext.<anonymous> (file:///mnt/storage/scratch/rocko-r304/packages/harness/tests/claude-session.test.mjs:298:10)
|
||||
process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
async Test.run (node:internal/test_runner/test:1409:7)
|
||||
async Test.processPendingSubtests (node:internal/test_runner/test:974:7)
|
||||
...
|
||||
# Subtest: claude: a missing hook or MCP file refuses before claude starts
|
||||
ok 8 - claude: a missing hook or MCP file refuses before claude starts
|
||||
---
|
||||
duration_ms: 8.695573
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
ok 9 - claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
---
|
||||
duration_ms: 1671.315421
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
ok 10 - claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
---
|
||||
duration_ms: 823.801717
|
||||
type: 'test'
|
||||
...
|
||||
1..10
|
||||
# tests 10
|
||||
# suites 0
|
||||
# pass 9
|
||||
# fail 1
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5552.702962
|
||||
@@ -0,0 +1,70 @@
|
||||
TAP version 13
|
||||
# Subtest: claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
ok 1 - claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
---
|
||||
duration_ms: 822.315273
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: the hook alone blocks a path outside the workspace
|
||||
ok 2 - claude: the hook alone blocks a path outside the workspace
|
||||
---
|
||||
duration_ms: 473.785584
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a second turn resumes the first turn's session
|
||||
ok 3 - claude: a second turn resumes the first turn's session
|
||||
---
|
||||
duration_ms: 834.093184
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: --restricted is always passed
|
||||
ok 4 - claude adapter: --restricted is always passed
|
||||
---
|
||||
duration_ms: 4.587717
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
ok 5 - claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
---
|
||||
duration_ms: 15.598032
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
ok 6 - claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
---
|
||||
duration_ms: 5.369426
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
ok 7 - claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
---
|
||||
duration_ms: 831.362199
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a missing hook or MCP file refuses before claude starts
|
||||
ok 8 - claude: a missing hook or MCP file refuses before claude starts
|
||||
---
|
||||
duration_ms: 8.237071
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
ok 9 - claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
---
|
||||
duration_ms: 1687.124498
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
ok 10 - claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
---
|
||||
duration_ms: 884.665674
|
||||
type: 'test'
|
||||
...
|
||||
1..10
|
||||
# tests 10
|
||||
# suites 0
|
||||
# pass 10
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5663.888591
|
||||
@@ -0,0 +1,70 @@
|
||||
TAP version 13
|
||||
# Subtest: claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
ok 1 - claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
---
|
||||
duration_ms: 740.530837
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: the hook alone blocks a path outside the workspace
|
||||
ok 2 - claude: the hook alone blocks a path outside the workspace
|
||||
---
|
||||
duration_ms: 484.250736
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a second turn resumes the first turn's session
|
||||
ok 3 - claude: a second turn resumes the first turn's session
|
||||
---
|
||||
duration_ms: 857.991709
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: --restricted is always passed
|
||||
ok 4 - claude adapter: --restricted is always passed
|
||||
---
|
||||
duration_ms: 5.19059
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
ok 5 - claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
---
|
||||
duration_ms: 16.380045
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
ok 6 - claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
---
|
||||
duration_ms: 5.47195
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
ok 7 - claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
---
|
||||
duration_ms: 869.204192
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a missing hook or MCP file refuses before claude starts
|
||||
ok 8 - claude: a missing hook or MCP file refuses before claude starts
|
||||
---
|
||||
duration_ms: 8.011154
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
ok 9 - claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
---
|
||||
duration_ms: 1760.816087
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
ok 10 - claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
---
|
||||
duration_ms: 951.447392
|
||||
type: 'test'
|
||||
...
|
||||
1..10
|
||||
# tests 10
|
||||
# suites 0
|
||||
# pass 10
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5778.476849
|
||||
@@ -0,0 +1,70 @@
|
||||
TAP version 13
|
||||
# Subtest: claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
ok 1 - claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist
|
||||
---
|
||||
duration_ms: 766.913918
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: the hook alone blocks a path outside the workspace
|
||||
ok 2 - claude: the hook alone blocks a path outside the workspace
|
||||
---
|
||||
duration_ms: 496.038436
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a second turn resumes the first turn's session
|
||||
ok 3 - claude: a second turn resumes the first turn's session
|
||||
---
|
||||
duration_ms: 839.816577
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: --restricted is always passed
|
||||
ok 4 - claude adapter: --restricted is always passed
|
||||
---
|
||||
duration_ms: 4.571987
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
ok 5 - claude adapter: MOSAIC_CLAUDE_BIN wins over PATH and must be an absolute executable
|
||||
---
|
||||
duration_ms: 17.17544
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
ok 6 - claude adapter: a directory pin exits 2 before it runs; a pin with a space runs
|
||||
---
|
||||
duration_ms: 5.448032
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
ok 7 - claude: CLAUDE.md files and auto-memory don't reach the model; without --restricted they do
|
||||
---
|
||||
duration_ms: 873.897762
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: a missing hook or MCP file refuses before claude starts
|
||||
ok 8 - claude: a missing hook or MCP file refuses before claude starts
|
||||
---
|
||||
duration_ms: 8.282957
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
ok 9 - claude: row 66 a reply with a tell gets the correction notice on the next turn, a clean reply gets none, and the counts read after
|
||||
---
|
||||
duration_ms: 1738.792709
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
ok 10 - claude: row 66 a broken lists.json gives no clean verdict, the operator is told once, and the session still answers
|
||||
---
|
||||
duration_ms: 863.908172
|
||||
type: 'test'
|
||||
...
|
||||
1..10
|
||||
# tests 10
|
||||
# suites 0
|
||||
# pass 10
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 5704.886282
|
||||
@@ -0,0 +1,16 @@
|
||||
0b487adf165fb6b83e9cba38fc49167c6ec54b66261d955052f7ee5a877fde07 BUILD.md
|
||||
a75846e9e3217b524ad09bb423033975da6c44074d8d74dc14193550ec1666b9 assertion-preservation.json
|
||||
5ddf1af4e2d23bc579e6a4005c2b355794dcc33fbd29c0421a6d1e9ca60f70ec base.txt
|
||||
85dc6f969a11d933b04cc7b2fe49199d745029779c66bf1815f549c51ec91568 build.patch
|
||||
a09b498872aaf4f3b3af9b297d839d4ddb6fdc57d7a01f05cc1a48200c30cfd4 candidate-manifest.sha256
|
||||
8588e890cdfadd6911fd3b473aad3ff826f6dd91a52e8d10aeb4a28f7929d995 changed-paths.txt
|
||||
37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570 cwd-sweep.json
|
||||
767c5c04bc6442bb6efc8de3bf86e5ef8d2bbdd90d20d72fd604d18e69290f91 driver.log
|
||||
f6f8a66ccc0c025782cc6932de9745720ca7574a0376d6122074275332abaf43 logs/M1-old-regex.tap
|
||||
dc290774a454c85033fdc8c71226f7cd56a07e52106b2868ca91ca5ea652ddca logs/baseline-hyphen.tap
|
||||
960114150fbf0a3069762d356b6cffe0068208a69e805bb0422de0da8825af35 logs/baseline-underscore.tap
|
||||
1d0836115cdf344e7cdcbe1ef9de7a3f2e57ca06a4cca42a07adb14ba82bd29a logs/fixed-mktemp.tap
|
||||
a69191da6fd3eb20ec99b92521df9418e9815b7879a159cc7b2abbd5b790261c logs/fixed-space.tap
|
||||
9776e07a392bc567609573a66a2a17619ded0863b8ee35cbc53482e42c58220b logs/fixed-underscore.tap
|
||||
3adeed328506abdeff0bb108ac16d306736e2caba1c8188d48173e5ea75b5c81 results.jsonl
|
||||
084a848241bf874a5873a881d2c47efd29deff7b6682947fc7738053556845d0 run-build.py
|
||||
@@ -0,0 +1,6 @@
|
||||
{"label": "baseline-underscore", "start": "2026-10-23T11:05:01.371651+00:00", "end": "2026-10-23T11:05:06.998843+00:00", "seconds": 5.627, "exit": 1, "counts": {"tests": 10, "pass": 9, "fail": 1, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304_OV81Op", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "3a43f134d334e985e6dd50f641a7c1788de29beb86e2f9e2ac7f0ceaf924a8e0", "skips": [], "log": "agents/rocko/work/queue-304/logs/baseline-underscore.tap"}
|
||||
{"label": "baseline-hyphen", "start": "2026-10-23T11:05:07.003823+00:00", "end": "2026-10-23T11:05:12.829873+00:00", "seconds": 5.826, "exit": 0, "counts": {"tests": 10, "pass": 10, "fail": 0, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304-UnWJbm", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "3a43f134d334e985e6dd50f641a7c1788de29beb86e2f9e2ac7f0ceaf924a8e0", "skips": [], "log": "agents/rocko/work/queue-304/logs/baseline-hyphen.tap"}
|
||||
{"label": "fixed-mktemp", "start": "2026-10-23T11:05:12.834143+00:00", "end": "2026-10-23T11:05:18.560124+00:00", "seconds": 5.726, "exit": 0, "counts": {"tests": 10, "pass": 10, "fail": 0, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304yw6LWT", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "7690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9", "skips": [], "log": "agents/rocko/work/queue-304/logs/fixed-mktemp.tap"}
|
||||
{"label": "fixed-underscore", "start": "2026-10-23T11:05:18.564553+00:00", "end": "2026-10-23T11:05:24.340312+00:00", "seconds": 5.776, "exit": 0, "counts": {"tests": 10, "pass": 10, "fail": 0, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304_MyhpSs", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "7690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9", "skips": [], "log": "agents/rocko/work/queue-304/logs/fixed-underscore.tap"}
|
||||
{"label": "fixed-space", "start": "2026-10-23T11:05:24.344312+00:00", "end": "2026-10-23T11:05:30.170101+00:00", "seconds": 5.826, "exit": 0, "counts": {"tests": 10, "pass": 10, "fail": 0, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304 i5d7HA", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "7690a4a945e45f5606633d36a3db9a160892be9d125d40351cd848f5bf3ea9c9", "skips": [], "log": "agents/rocko/work/queue-304/logs/fixed-space.tap"}
|
||||
{"label": "M1-old-regex", "start": "2026-10-23T11:05:30.173937+00:00", "end": "2026-10-23T11:05:36.100361+00:00", "seconds": 5.926, "exit": 1, "counts": {"tests": 10, "pass": 9, "fail": 1, "cancelled": 0, "skipped": 0}, "tmpdir": "/mnt/storage/scratch/tmp/rk304_595gfj", "head": "c16e943b90ef71ac96cfa5314bbea6040634d14d", "testSha256": "3a43f134d334e985e6dd50f641a7c1788de29beb86e2f9e2ac7f0ceaf924a8e0", "skips": [], "log": "agents/rocko/work/queue-304/logs/M1-old-regex.tap"}
|
||||
@@ -0,0 +1,66 @@
|
||||
from pathlib import Path
|
||||
import subprocess,os,signal,re,json,hashlib,time
|
||||
from datetime import datetime,timezone
|
||||
ROOT=Path(__file__).resolve().parents[4];P=Path(__file__).resolve().parent;O=P/'logs';O.mkdir(exist_ok=True)
|
||||
FILE='packages/harness/tests/claude-session.test.mjs';source=ROOT/FILE
|
||||
BASE='c16e943b90ef71ac96cfa5314bbea6040634d14d'
|
||||
old='realpathSync(workspace).replace(/[/.]/g, "-")';new='realpathSync(workspace).replace(/[^A-Za-z0-9]/g, "-")'
|
||||
original=source.read_bytes();assert original.decode().count(old)==1
|
||||
fixed=original.decode().replace(old,new,1).encode()
|
||||
rows=[];temps=[]
|
||||
def stamp():return datetime.now(timezone.utc).isoformat()
|
||||
def verify(data):
|
||||
assert subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip()==BASE
|
||||
assert source.read_bytes()==data
|
||||
|
||||
def run(label,template,data):
|
||||
verify(data);tmp=subprocess.check_output(['mktemp','-d',template],text=True).strip();temps.append(tmp)
|
||||
env=os.environ.copy();env.update(TMPDIR=tmp,DOCKER_HOST='unix:///mnt/storage/scratch/rocko-r304-dead.sock',NO_COLOR='1');env.pop('MOSAIC_GITEA_CREDENTIAL_FILE',None);env.pop('MOSAIC_LIVE_PROVIDER',None)
|
||||
args=['timeout','-k','10','180','node','--test','--test-reporter=tap',FILE];log=O/(label+'.tap');start=stamp();begin=time.monotonic()
|
||||
print('START',label,start,tmp,flush=True)
|
||||
with log.open('wb') as f:
|
||||
child=subprocess.Popen(args,cwd=ROOT,env=env,stdout=f,stderr=subprocess.STDOUT,start_new_session=True)
|
||||
try:rc=child.wait(timeout=195)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(child.pid,signal.SIGKILL);child.wait();rc=124
|
||||
finally:
|
||||
try:os.killpg(child.pid,signal.SIGTERM)
|
||||
except ProcessLookupError:pass
|
||||
text=log.read_text(errors='replace');counts={k:int(m[-1]) if (m:=re.findall(r'^# '+k+r' (\d+)\s*$',text,re.M)) else None for k in ['tests','pass','fail','cancelled','skipped']}
|
||||
row=dict(label=label,start=start,end=stamp(),seconds=round(time.monotonic()-begin,3),exit=rc,counts=counts,tmpdir=tmp,head=BASE,testSha256=hashlib.sha256(data).hexdigest(),skips=[x for x in text.splitlines() if re.search(r'^skip\b|# SKIP\b',x,re.I)],log=str(log.relative_to(ROOT)))
|
||||
rows.append(row)
|
||||
with (P/'results.jsonl').open('a') as f:f.write(json.dumps(row)+'\n')
|
||||
print('END',json.dumps(row),flush=True)
|
||||
assert rc not in [124,137,-9] and 'SyntaxError:' not in text,'invalid run '+label
|
||||
verify(data);return row,text
|
||||
|
||||
def red(label,template,data):
|
||||
r,t=run(label,template,data)
|
||||
assert r['exit']!=0 and r['counts']['tests']==10 and r['counts']['pass']==9 and r['counts']['fail']==1 and r['counts']['cancelled']==0 and r['counts']['skipped']==0,'unexpected red counts '+label
|
||||
names=re.findall(r'^not ok \d+ - (.*)$',t,re.M)
|
||||
assert len(names)==1 and "CLAUDE.md files and auto-memory" in names[0] and ':298:10' in t and 'ERR_ASSERTION' in t and 'MARKER-MEMORY' in t,'unexpected red '+label
|
||||
|
||||
def green(label,template,data):
|
||||
r,t=run(label,template,data)
|
||||
assert r['exit']==0 and r['counts']['tests']==10 and r['counts']['pass']==10 and r['counts']['fail']==0 and r['counts']['cancelled']==0 and r['counts']['skipped']==0,'unexpected mapping/refusal '+label
|
||||
|
||||
try:
|
||||
red('baseline-underscore','/mnt/storage/scratch/tmp/rk304_XXXXXX',original)
|
||||
green('baseline-hyphen','/mnt/storage/scratch/tmp/rk304-XXXXXX',original)
|
||||
# Both installed-CLI probes now confirm the approved mapping before editing.
|
||||
source.write_bytes(fixed)
|
||||
green('fixed-mktemp','/mnt/storage/scratch/tmp/rk304XXXXXX',fixed)
|
||||
green('fixed-underscore','/mnt/storage/scratch/tmp/rk304_XXXXXX',fixed)
|
||||
green('fixed-space','/mnt/storage/scratch/tmp/rk304 XXXXXX',fixed)
|
||||
source.write_bytes(original)
|
||||
try:red('M1-old-regex','/mnt/storage/scratch/tmp/rk304_XXXXXX',original)
|
||||
finally:source.write_bytes(fixed)
|
||||
verify(fixed);print('BUILD_GREEN',stamp(),flush=True)
|
||||
finally:
|
||||
found=[]
|
||||
for proc in Path('/proc').glob('[0-9]*'):
|
||||
try:
|
||||
name=(proc/'comm').read_text().strip();cwd=str((proc/'cwd').resolve())
|
||||
if any(x in name for x in ['node','chrom','claude']) and any(cwd==r or cwd.startswith(r+'/') for r in [str(ROOT),*temps]):found.append(dict(pid=int(proc.name),name=name,cwd=cwd))
|
||||
except (FileNotFoundError,PermissionError,ProcessLookupError):pass
|
||||
(P/'cwd-sweep.json').write_text(json.dumps(found,indent=2)+'\n');print('CWD_SWEEP',found,flush=True)
|
||||
@@ -12,6 +12,11 @@ export COMPOSE_PROJECT_NAME="$(basename "$(dirname "$G")")"
|
||||
trap 'docker network rm "${COMPOSE_PROJECT_NAME}_default" >/dev/null 2>&1 && echo "removed network ${COMPOSE_PROJECT_NAME}_default"' EXIT
|
||||
# A gate tree without the node_modules link fails cli tests with ENOENT (2026-10-13).
|
||||
[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; }
|
||||
# Gate rule (mos-1455 item 4, 2026-10-23): every landing gate runs the bus, cli, plugins, webui and business
|
||||
# suites whatever the row touches. The 270+271 gate skipped plugins, and its red sat on refactor unseen.
|
||||
for p in bus cli plugins webui business; do [ -d "$G/packages/$p/tests" ] || { echo "STOP no packages/$p/tests"; exit 8; }; done
|
||||
# mos-1453: a gate on the canonical node_modules must not let the webui editor rebuild test skip (Vikunja 597).
|
||||
export MOSAIC_REQUIRE_EDITOR_BUILD=1
|
||||
echo "base $(git rev-parse --short HEAD) dirty $(git status --short | grep -vc node_modules) DOCKER_HOST=${DOCKER_HOST:-unset}"
|
||||
run() { local name="$1" dir="$2" cmd="$3"
|
||||
(cd "$G/$dir" && bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
# PRD 1.17 draft: Case 2 story files are DOCX (Q235 to Q237, P1 to P6)
|
||||
|
||||
Sage, 2026-10-23, for mos-1366 item 1 and Vikunja 570. Draft 9. It is d8 plus Jason's Q41 answer O2 (DECISIONS Q239, mos-1467), which makes J-b yes, Webby's round 5 N15 to N17 (webby-0210) and Webby's round 6 B1, N19 and N20 (webby-0212) (see "d9 changes"). d8 is d7 plus Mos's d8 fix list mos-1443 (see "d8 changes"). d7 is d6 plus Mos's ruling mos-1379 on Fenton's 571 B2 (see "d7 changes"). d6 is d5 with Webby's round 3 (webby-0207) fixed: B1 to B3 and N10 to N13, with N14 left for Mos (see "d6 changes"). d5 folds in:
|
||||
- Mos's d4 check ruling mos-1372 (D1 to D14; D15 was dropped).
|
||||
- mos-1374, which replaces J-c.
|
||||
- The impact map mos-1368 (jason-master-restart d183246, `projects/mosaic-mvp/case2-docx-impact-map.md`: A1 to A16, critic items C1 to C6, items 2 to 7).
|
||||
- Mos's F4 measurement.
|
||||
|
||||
Review order (mos-1367 B5): Webby, then Mos, then Jason. Base: PRD 1.16, `docs/prd/mosaic-stack.md` blob a6db9b4a at 482e865ac.
|
||||
1.16 lands first (row 291), and this text is a new version on top of it. Nothing here is built or queued. The impact map's queue changes (A24, A57 to A59) go through `scripts/mosaic queue` after 1.17 is approved (C9).
|
||||
|
||||
Sources, verbatim in Mos's DECISIONS:
|
||||
- Q235, Jason's Q36 answer.
|
||||
- Q236: chapters are DOCX, Markdown for agent files and docs.
|
||||
- Q237: "It is correct for now (subject to future change)".
|
||||
- The P1 to P6 reading as restated to Jason in Q38.
|
||||
|
||||
## Settled points (mos-1367)
|
||||
|
||||
These stand as ruled. d5 changes only what mos-1372 changed.
|
||||
|
||||
- **O1. Where a converted chapter lives.** The DOCX lives in the writing store of
|
||||
O2 design D12, and the archived Markdown stays in the user's repository under a
|
||||
human-marked archive folder. Ruled yes in B1, on the condition that the text
|
||||
says plainly that a converted chapter leaves the Gitea-mirrored repository and
|
||||
lives in the stack's writing store. mos-1372 D7 and mos-1375 add a remote for
|
||||
the store, off the stack's host.
|
||||
- **O2. The unit of conversion.** One story file per import, refusing a file
|
||||
that is already converted. Ruled yes in B2: each file gets its own fidelity
|
||||
report, human confirmation, manifest and commit.
|
||||
- **O3. Whether conversion is a vocabulary action.** A human-only verb outside
|
||||
the agent vocabulary, with no role grant and no agent request path. Ruled yes
|
||||
in B3.
|
||||
- **O4. Reading archived Markdown.** A new `archived` scope. Lookups leave it out
|
||||
unless the caller asks, every answer marks it archived, and a canon citation
|
||||
can't name it. Ruled yes in B4.
|
||||
|
||||
## d3 choices after mos-1372
|
||||
|
||||
- **C1 is replaced by D1, keep-source-scope.** A DOCX keeps its source's scope:
|
||||
universe, book, and canon or playground. A playground DOCX follows the
|
||||
playground rules and is never returned as canon. The import accepts canon and
|
||||
playground story files. Type-folder files and supporting files stay Markdown
|
||||
(P4). This also settles the impact map's Q18, item 4. It leaves one narrowing
|
||||
for Jason, J-i. A playground DOCX can't become canon in 1.17.
|
||||
- **C2 stands.** The move keeps the file's path under the archive folder:
|
||||
`book1/ch3.md` goes to `<archive>/book1/ch3.md`. An archived file's universe
|
||||
and book are those of its old path.
|
||||
- **C3 is replaced by D3 and D14.** The archive commit is written by a new
|
||||
human-only bus step (Vikunja 571) on the C2-02 write plumbing and pushed through
|
||||
C2-03b. The order is the store commit and the remote's confirmation that it
|
||||
holds that commit, then the archive commit and its push, then publication to the
|
||||
lookups.
|
||||
- **N4 stands.** REQ-LIB-8 says "a real user's books". The PRD names no user
|
||||
anywhere in Case 2.
|
||||
|
||||
## Text changes
|
||||
|
||||
### Status and sources
|
||||
|
||||
In the Status bullet's first line, "- Status: approved, version 1.16." becomes
|
||||
"- Status: approved, version 1.17." Status gains, after the two 1.16 sentences at
|
||||
the end of the bullet: "Version 1.17 records Jason's rulings Q235 to Q237 of
|
||||
2026-10-23: Case 2 story files are DOCX, Markdown stays for agent files and
|
||||
documentation, and a human-started import converts a Markdown story file one way.
|
||||
It also adds REQ-LIB-9, the Console's DOCX editor, from Jason's rulings Q195, Q226
|
||||
and Q234. It changes what Case 2 requirements allow, so Mos puts the text to Jason
|
||||
as written, and it lands only after his yes (mos-1366, Vikunja 570)."
|
||||
|
||||
New source line, above the 1.16 one: "Version 1.17 source: Jason's Q235, answering
|
||||
Q36: 'How about we see a markdown file, we convert the markdown file to docx, then
|
||||
archive or mark the markdown file as converted. The new standard for files in the
|
||||
creating writing use case is to use docx to handle versioning and WYSIWYG. We
|
||||
eliminate the potential for confusion based on the files. Agents must understand
|
||||
which files are what format and where they live. The system must allow for import
|
||||
and conversion. We will not run the systems in parallel. There will be an
|
||||
irreversible conversion of the files.' His Q236 says: 'For creative writing
|
||||
chapters, we need to utilize docx as the standard. Markdown is acceptable for
|
||||
agent files, instructions, PRDs, documentation, etc. The creative writing files
|
||||
should be docx for familiarity for the user.' His Q237 says 'Q38: It is correct
|
||||
for now (subject to future change)', which confirms Mos's reading P1 to P6.
|
||||
REQ-LIB-9 rests on his Q195 request for 'a full WYSIWYG editor within Mosaic
|
||||
Stack', his Q226 answer 'F41: We want to move editing into the WYSIWYG in the
|
||||
WebUI. All changes are instantly committed and available to the agents.' and his
|
||||
Q234 answer 'Q37: yes' to the exact security header for the editor's frame. The
|
||||
storage follows the O2 design D10 to D12 as rewritten under Vikunja 571, at
|
||||
b99ca86b9 on `refactor`."
|
||||
|
||||
### Creative library, section lead
|
||||
|
||||
Replace "The library is the user's own writing repository." with "The library is
|
||||
the user's own writing repository plus the stack's writing store, which holds the
|
||||
DOCX story files (REQ-LIB-8, version 1.17)."
|
||||
|
||||
### REQ-LIB-1
|
||||
|
||||
Add, before "(Design 1.1": "The mapping also names one archive folder, set by the
|
||||
human, where converted Markdown story files go. A book's DOCX story files are in
|
||||
the stack's writing store, under the same universe and book (REQ-LIB-8)." Version
|
||||
tag, inside the closing parenthesis: "Version 1.15: supporting folders.)" becomes
|
||||
"Version 1.15: supporting folders. Version 1.17: the archive folder and the
|
||||
writing store.)"
|
||||
|
||||
### REQ-LIB-2
|
||||
|
||||
Replace "A file's scope comes from its path alone, prefixes first: a Markdown
|
||||
file under a supporting prefix is supporting, one under a playground prefix at
|
||||
any level is playground, any other Markdown file in a book folder is canon, and
|
||||
everything else is unmapped and never returned. A folder in both lists is
|
||||
supporting." with:
|
||||
|
||||
"A Markdown file's scope comes from its path and from past imports, prefixes
|
||||
first: a Markdown file under the archive folder is archived, one under a
|
||||
supporting prefix is supporting, a Markdown file at a converted file's old path is
|
||||
unmapped under REQ-LIB-8 but flagged in the format map, one under a playground
|
||||
prefix at any level is playground, any other Markdown file in a book folder is
|
||||
canon, and everything else is unmapped and never returned. A folder in both lists
|
||||
is supporting. The archive folder follows the same placement rules and may not
|
||||
overlap another prefix or a book folder. An archived file keeps the path it had
|
||||
under the archive folder, and its universe and book are those of that old path. A
|
||||
DOCX document in the writing store keeps its source's scope: the universe, the
|
||||
book if there is one, and canon or playground. Its source is the Markdown file it
|
||||
was converted from or, for one the human starts in the editor, the universe and
|
||||
book or the playground the human picks (REQ-LIB-8). A playground DOCX follows the
|
||||
playground rules and is never returned as canon. It stays playground after its
|
||||
draft universe is made real. In version 1.17 no promotion or mapping change makes
|
||||
a playground DOCX canon. A later version of this PRD says how one becomes canon."
|
||||
|
||||
Replace "Lookups return canon unless the caller asks for playground or
|
||||
supporting." with "Lookups return canon unless the caller asks for playground,
|
||||
supporting or archived. Every answer marks an archived file as archived and names
|
||||
the DOCX that replaced it."
|
||||
|
||||
Replace "so an agent can't cite a playground or supporting file as canon" with "so
|
||||
an agent can't cite a playground, supporting or archived file as canon".
|
||||
|
||||
In REQ-LIB-2 only, replace "The Console shows each cite's scope." with "The Console shows each cite's
|
||||
scope and format, and whether a cited Markdown file was converted."
|
||||
|
||||
Version tag, inside the closing parenthesis: "Version 1.15: supporting.)" becomes
|
||||
"Version 1.15: supporting. Version 1.17: archived scope and DOCX scope.)"
|
||||
|
||||
### REQ-LIB-4
|
||||
|
||||
Replace "The user commits and pushes with their own tools, today Obsidian with
|
||||
obsidian-git." with "The user commits and pushes Markdown with their own tools,
|
||||
today Obsidian with obsidian-git. A converted story file's current text is no
|
||||
longer in that repository, which keeps only its archived Markdown (REQ-LIB-8)."
|
||||
|
||||
Add, after "Bus writes and pushes come in later rows (C2-02, C2-03b).": "A DOCX
|
||||
story file is saved in the Console's editor, and each accepted save is one commit
|
||||
by the human in the stack's writing store (REQ-LIB-9). Each writing-store commit
|
||||
is also pushed to a remote off the stack's host, never by force-push. The remote
|
||||
is a setting only a human changes, and it starts unset. A repository on the
|
||||
stack's host never counts as that remote. A missing remote or a failed push never
|
||||
undoes a save, and the user sees each push that is pending or failed. Only the
|
||||
stack's host pushes to that remote, and no agent can reach it with any credential.
|
||||
Agents read the local commit."
|
||||
|
||||
Replace "Agents read only the mirror's served commit and never hold a copy of
|
||||
their own, so they see the user's push within about a minute." with "Agents read
|
||||
the user's repository only at the mirror's served commit, except a story file
|
||||
during its import under REQ-LIB-8. They read a DOCX story file only at a committed
|
||||
version in the writing store, under REQ-LIB-5. They never hold a copy of their
|
||||
own, so they see the user's Markdown push within about a minute and a DOCX save at
|
||||
their next lookup after the editor reports it as committed."
|
||||
|
||||
Version tag: "and the sync items.)" becomes "and the sync items. Version 1.17,
|
||||
with Q222 for the remote setting.)"
|
||||
|
||||
### REQ-LIB-5
|
||||
|
||||
Add, after "Each answer names the commit it read.": "Each answer also names the
|
||||
file's format and where it lives: its path, or its document id in the writing
|
||||
store. A DOCX story file is read from its latest committed save, never from
|
||||
unsaved text. A lookup that starts after the editor reports a save as committed
|
||||
returns that save or a later one, even while its push to the remote is pending or
|
||||
has failed, and the answer names the version. Get, search and manuscript read a
|
||||
DOCX story file as its extracted text with DOCX locators, never as lines. That
|
||||
text is the file with its tracked changes accepted. An agent reads a story file's
|
||||
comments, or its text with tracked deletions shown, only after a human turns on
|
||||
the matching setting. Both settings are off by default, and only a human changes
|
||||
them. The reader in this version can't read some things a DOCX story file can
|
||||
hold, such as a web link, a text box, a field or hidden text. The editor warns the
|
||||
human when an edit adds one, and the human can still save it. Until a later save
|
||||
takes it out, lookups give agents none of the text of the part that holds it,
|
||||
whether that is the chapter's body, a note or a comment. The manuscript lookup
|
||||
follows the book's chapter order across both formats (O2 design D10), and the next
|
||||
chapter it names is never an archived file. Links out and in resolve across both
|
||||
formats. The universe lookup returns the format map (REQ-LIB-8)."
|
||||
|
||||
Replace "The user sees them in Obsidian's graph and backlinks." with "The user sees
|
||||
a Markdown file's links in Obsidian's graph and backlinks. A DOCX story file's
|
||||
wikilinks stay clickable in the Console's editor (O2 design D11), and no graph
|
||||
shows them yet. The Console's Connections view lists the current links and
|
||||
backlinks of each story file the user may see, in both formats, with each one's
|
||||
scope and format."
|
||||
|
||||
Version tag: 'and "Connections are always visible".)' becomes 'and "Connections
|
||||
are always visible". Version 1.17, with Q222 for the two settings and Q239 for
|
||||
the Connections view.)'
|
||||
|
||||
### REQ-LIB-6
|
||||
|
||||
Replace "A reply may cite lines of the repository at a commit or pages the agent
|
||||
fetched, and the broker checks every citation before it stores the message." with
|
||||
"A reply may cite lines of a Markdown file at a commit, a place in a committed
|
||||
DOCX story file, or pages the agent fetched, and the broker checks every citation
|
||||
before it stores the message. A DOCX citation never calls a paragraph a line. It
|
||||
names the document id, the committed version and a DOCX locator. A citation that
|
||||
names an archived file as current is refused. The `requireCitation` setting below
|
||||
applies to Markdown and DOCX citations alike, and so does the rule for a citation
|
||||
marked as canon."
|
||||
|
||||
Add, after "the broker refuses such a write before any git call.": "The writing
|
||||
store and the archive folder are read-only to a creative agent as well, and the
|
||||
broker's path check treats the archive folder as story. No agent creates, edits,
|
||||
deletes, converts, archives or restores a story file in any format, and no agent
|
||||
writes a DOCX. No creative agent starts or undoes a conversion, and none asks for
|
||||
one through a tool or a decision it raises, as REQ-LIB-8 says. An agent may still
|
||||
suggest a conversion to the human in chat. Each creative role's contract states
|
||||
which files are DOCX and which are Markdown, and cites a DOCX story file by its
|
||||
locator, never by line."
|
||||
|
||||
Version tag, inside the closing parenthesis: "Version 1.15.)" becomes "Version
|
||||
1.15. Version 1.17.)"
|
||||
|
||||
### New REQ-LIB-8
|
||||
|
||||
This bullet goes after REQ-LIB-7 and before the "### Installer" heading.
|
||||
|
||||
"- **REQ-LIB-8.** Story files become DOCX. DOCX is the standard for chapters and
|
||||
the other story text in a Case 2 writing project, canon and playground alike. A
|
||||
story file becomes DOCX through a human's import (below), or when the human
|
||||
starts it in the editor. From then on DOCX is its only working format, edited in
|
||||
the Console's editor (REQ-LIB-9). A Markdown story file stays Markdown until its
|
||||
import. Files in a type folder (REQ-LIB-1), such as character and place notes,
|
||||
stay Markdown. So do supporting files, agent files, instructions, PRDs and
|
||||
documentation, including the character sheets, summaries and cross-links agents
|
||||
keep under REQ-LIB-6.
|
||||
- A converted story file's current text leaves the user's Gitea-mirrored
|
||||
repository. Its DOCX lives in the stack's writing store, on the stack's host,
|
||||
and not in the repository the user pushes or opens in Obsidian. Only its
|
||||
archived Markdown stays there.
|
||||
- An import converts one Markdown story file, canon or playground, outside every
|
||||
type folder (REQ-LIB-1), to a DOCX document in the writing store. It refuses
|
||||
an unmapped, supporting, archived or type-folder file, and a file that is
|
||||
already converted. Each import has its own fidelity report, the human's
|
||||
confirmation of that report, a manifest naming the source file and its commit,
|
||||
and one commit in the store. The DOCX keeps every wiki link of the source as
|
||||
written, and a lost link to another library file fails the fidelity report. A
|
||||
web link becomes plain text that keeps its label and address, and the fidelity
|
||||
report lists it for the human to confirm. A web link that can't be kept safely
|
||||
as plain text refuses the import.
|
||||
- The human can also start a new story file in the editor, in a universe and
|
||||
book or a playground the human picks. It is a DOCX document in the writing
|
||||
store from its first commit, with no Markdown copy, and it opens under the
|
||||
same gate as the import.
|
||||
- The same import marks the Markdown file converted, naming the document, and
|
||||
moves it to the archive folder under its old path (REQ-LIB-1, REQ-LIB-2).
|
||||
The mark and the move are one commit authored by the human in the user's
|
||||
repository. A new human-only bus step (Vikunja 571) on the C2-02 write
|
||||
plumbing writes it, and C2-03b pushes it. The move keeps the bytes and the
|
||||
git history, so it isn't `data.delete`.
|
||||
- The store commit comes first, and the writing store's remote must confirm it
|
||||
holds that commit. Then come the archive commit and its push, then publication
|
||||
to the lookups. Until publication the Markdown at its old path keeps its canon
|
||||
or playground scope. Agents read that file at the commit the import started
|
||||
from, even after the mirror's served commit reaches the archive commit or a
|
||||
later one. No lookup returns the file's archived copy before publication, even
|
||||
one that asks for archived files. A refusal or failure before the archive push
|
||||
leaves the Markdown unmoved and no DOCX that a lookup returns. A failure after
|
||||
the archive push leaves a pending import that the human sees and can resume,
|
||||
never a claim that nothing moved. An archive push whose result is unknown
|
||||
counts as a failure after the archive push until the stack reads the result
|
||||
back from the user's remote.
|
||||
- Each stage of an import writes a logged event. The import's record on the
|
||||
stack's host names the source file's path, commit and blob and the new
|
||||
document id, and each event points to that record.
|
||||
- After a conversion, a Markdown file at the converted file's old path is
|
||||
neither canon nor playground. Lookups leave it out, the format map flags it,
|
||||
and the human is told.
|
||||
- A new Markdown story file in a book folder that holds converted files stays
|
||||
canon until its import, unless it sits at a converted file's old path or under
|
||||
a playground prefix. The Console flags it and offers the import. Nothing
|
||||
converts it on its own.
|
||||
- A hybrid's exclusion that covers a converted file's old path also covers its
|
||||
DOCX document (REQ-LIB-3). An exclusion of a whole book folder also covers
|
||||
every DOCX document in that book, including one started in the editor. An
|
||||
exclusion can also name a DOCX document by its id.
|
||||
- Only a human starts an import or a new story file, from the CLI or a Console
|
||||
session signed in as that human. Both are human verbs outside the agent action
|
||||
vocabulary. Neither is added to `ACTIONS` in the business vocabulary or to the
|
||||
broker's always-gated list, including the import's action, `library.convert`.
|
||||
No role holds them, no agent can request them, and the hard-wait list in
|
||||
REQ-WAIT-1 doesn't change. In an organization with several humans, who may
|
||||
start them is a setting only a human changes, and the default is the owner
|
||||
only.
|
||||
- Conversion is one way, with no parallel run. Neither the Console nor any agent
|
||||
edits a converted story file as Markdown, and no DOCX edit syncs back to
|
||||
Markdown. The Console's Markdown save refuses every file under the archive
|
||||
folder. The user's own tools can still change the archived Markdown, and no
|
||||
such change reaches the DOCX. The old Markdown stays in git history, and
|
||||
nothing rewrites history. A promotion into a book folder (C2-19) runs the
|
||||
import for a Markdown story file or is refused. A promotion of a playground
|
||||
DOCX is refused in version 1.17.
|
||||
- Agents get a format map: for each mapped folder and story file, its format,
|
||||
scope and where it lives, archived files, flagged files and pending imports
|
||||
included.
|
||||
- The import and new story files ship closed. They open only after four things.
|
||||
The editor ships. The human has set the writing store's remote, and the stack
|
||||
has checked that it can push there. Agents can read committed DOCX story files
|
||||
through the lookups and cite them (REQ-LIB-5, REQ-LIB-6). Any earlier agent
|
||||
system that writes the user's files has stopped, which for Case 2 is the
|
||||
Hermes retirement (#423 step 5). So the import itself never cuts agents off
|
||||
from a chapter, the chapter's current text never lives only on the stack's
|
||||
host, and no second system writes the story. The first import of a real user's
|
||||
books waits for Jason's go.
|
||||
(Q235, Q236, Q237: P1 to P6. Q222 for the setting. O2 design D10 to D12.
|
||||
Version 1.17.)"
|
||||
|
||||
### New REQ-LIB-9
|
||||
|
||||
This bullet goes after REQ-LIB-8.
|
||||
|
||||
"- **REQ-LIB-9.** The Console's DOCX editor. The human edits a DOCX story file in
|
||||
the Console's editor, in a split pane with the agent chat alongside it. The
|
||||
human can pin an agent chat to a document. The human can select a word,
|
||||
right-click it, and choose a synonym from a thesaurus. Only a human saves. Saves
|
||||
happen on their own while the human types (Q226), at intervals only a human
|
||||
changes, and each accepted save is one commit with the human as author, pushed
|
||||
as REQ-LIB-4 says. A save made against an older version than the current one is
|
||||
refused. Agents never write a DOCX. Versioning is the editor's tracked changes
|
||||
plus one git commit per save. The editor's code is vendored and pinned, with a
|
||||
hash per file and the licenses that its independent security review accepted. At
|
||||
runtime the editor loads only files that the stack itself serves, and it
|
||||
requests nothing from any other host. The editor's frame sends exactly the
|
||||
content security policy Jason approved in Q234, and the WebUI shell's own header
|
||||
is unchanged. REQ-PROJ-3's project docs editor is unchanged. (Q195, Q226, Q234,
|
||||
Q236. O2 design D3, D4, D6, D8, D10, D13, D14, D16 and D22. Version 1.17.)"
|
||||
|
||||
### Version 1.17 notes
|
||||
|
||||
Added after the 1.16 notes:
|
||||
|
||||
"- Version 1.17:
|
||||
- Jason confirmed the reading 'for now (subject to future change)' (Q237). The
|
||||
rulings stand until he revises them.
|
||||
- The 10-27 Case 2 release ships on Markdown as built (P6). The 1.11 line 'The
|
||||
Case 2 MVP for 10-27 is the Markdown library (#416). The WYSIWYG editor (Q195,
|
||||
Q196) is an add-on that doesn't gate it.' still holds. Conversion opens after
|
||||
10-27, and only once REQ-LIB-8's four conditions are met. The editor ships,
|
||||
the writing store's remote is set and the stack has checked that it can push
|
||||
there, agents can read and cite committed DOCX story files through the
|
||||
lookups, and Hermes is retired, which is #423 step 5.
|
||||
- The 1.12 note says five bus write and Console editor rows, C2-02, C2-03b,
|
||||
C2-11a, C2-11b and C2-19, don't gate 10-27. Jason's Q226 F42 later moved
|
||||
C2-02, C2-03b and C2-11b into the 10-27 slice. The Case 2 design brings C2-11a
|
||||
in with them, because C2-11b needs it. C2-19 stays outside the slice. Mos's
|
||||
reading of F42 is that C2-02 and C2-03b now gate 10-27, because design 9.2
|
||||
puts them in 'the smallest slice that lets her start'. C2-11a and C2-11b are
|
||||
slice rows that still don't gate 10-27, because the editor is the Q195 add-on.
|
||||
The DOCX change in 1.17 leaves this slice as it is, and C2-11b's Markdown
|
||||
editor never edits a converted story file.
|
||||
- Q236 narrows Q176's editor choice to DOCX for story files (REQ-LIB-9). The
|
||||
1.12 note that the Case 2 design keeps Obsidian now holds only for Markdown
|
||||
files, including story files not yet imported. A DOCX story file is edited
|
||||
only in the Console's editor under REQ-LIB-9.
|
||||
- This replaces two parts of the O2 design at 25c09f19f. The superseded-notice
|
||||
proposal from Fenton's F1 in D12 is withdrawn, and the
|
||||
keep-Markdown-at-its-path model of D11 is replaced. Gyro's rewrite of the O2
|
||||
design for Vikunja 571, landed on `refactor` at b99ca86b9, carries the
|
||||
details, and where it differs from this text, this text holds:
|
||||
- the import and archive steps, the mark, and the new bus step;
|
||||
- links from supporting files to a moved chapter;
|
||||
- the DOCX reader;
|
||||
- the user's unpushed or later changes to an archived file or at its old path,
|
||||
and pushing and closing the file in Obsidian before each import;
|
||||
- the writing store's remote in W4 section 7.3, which replaces the no-remote
|
||||
and no-backup rule that W4 section 7.4 held before the rewrite, at
|
||||
062646487;
|
||||
- import events counted by a bus restore, and recovery when a bus restore or a
|
||||
restore of the whole host predates an import;
|
||||
- manifest order for the manuscript lookup and its next chapter;
|
||||
- `library.convert` kept out of `ACTIONS`, out of the hard-wait note and out
|
||||
of the pinned `GATED_ONLY` list in packages/business/src/vocabulary.mjs.
|
||||
- Agents read a converted story file through the Case 2 lookups under the same
|
||||
project and universe access as Markdown canon, in any thread or room. W4's
|
||||
earlier O2-only conversations, per-document grants and per-send sharing don't
|
||||
gate these lookups. The 571 rewrite replaced them in W4 sections 7.1 and 7.2.
|
||||
The creative tool list in REQ-LIB-6 doesn't change.
|
||||
- The six creative role contracts row 213 landed say 'Every item cites the
|
||||
lines of the user's material it is about'. A build row under 571 changes that
|
||||
line for DOCX story files (REQ-LIB-6).
|
||||
- A new Markdown file in a book folder is still canon in 1.17. The 1.17
|
||||
exception is a file at a converted file's old path, which REQ-LIB-2 makes
|
||||
unmapped.
|
||||
- Story files convert whether canon or playground. Type-folder and supporting
|
||||
files stay Markdown.
|
||||
- Measured 2026-10-23 at about 01:21Z: the Case 2 user's library repository
|
||||
tracks 213 files, 203 of them Markdown, and none is a .docx or .doc. It has no
|
||||
untracked files. The user's .doc files (Q196) are outside it. So the landed
|
||||
code has no DOCX there to report as unreadable at 10-27.
|
||||
- Text replaced in 1.17, kept here as in earlier notes:
|
||||
- Creative library lead, the 1.16 sentence: "The library is the user's own
|
||||
writing repository."
|
||||
- REQ-LIB-2, the 1.16 sentences: "A file's scope comes from its path alone,
|
||||
prefixes first: a Markdown file under a supporting prefix is supporting,
|
||||
one under a playground prefix at any level is playground, any other
|
||||
Markdown file in a book folder is canon, and everything else is unmapped
|
||||
and never returned. A folder in both lists is supporting." "Lookups return
|
||||
canon unless the caller asks for playground or supporting." "so an agent
|
||||
can't cite a playground or supporting file as canon" "The Console shows
|
||||
each cite's scope."
|
||||
- REQ-LIB-4, the 1.16 sentences: "The user commits and pushes with their own
|
||||
tools, today Obsidian with obsidian-git." "Agents read only the mirror's
|
||||
served commit and never hold a copy of their own, so they see the user's
|
||||
push within about a minute."
|
||||
- REQ-LIB-5, the 1.16 sentence: "The user sees them in Obsidian's graph and
|
||||
backlinks." A converted chapter's connections show in Obsidian only as they
|
||||
stood at conversion, and the Console's Connections view shows their
|
||||
current links.
|
||||
- REQ-LIB-6, the 1.16 sentence: "A reply may cite lines of the repository at
|
||||
a commit or pages the agent fetched, and the broker checks every citation
|
||||
before it stores the message."
|
||||
- REQ-LIB-1 only gains sentences.
|
||||
- REQ-LIB-3's text and REQ-LIB-7 are unchanged. REQ-LIB-8 adds one form of
|
||||
exclusion REQ-LIB-3 doesn't have: naming a DOCX document by its id. A path
|
||||
exclusion also covers DOCX documents, through a converted file's old path or a
|
||||
whole book folder. An archived file and a DOCX document keep a universe, and a
|
||||
book where their source has one, so every lookup still names one universe.
|
||||
REQ-PROJ-3, the Console editor for a project's spec and docs, is unchanged.
|
||||
C2-11b opens the library's Markdown files in that same editor.
|
||||
- In `packages/tasks/requirements.json` the titles of REQ-LIB-1, REQ-LIB-2 and
|
||||
REQ-LIB-4 change, and those of REQ-LIB-5 and REQ-LIB-6 stay. REQ-LIB-1 becomes
|
||||
'A human-set library maps the user's repository onto universes, books, types,
|
||||
playgrounds, supporting and archive folders'. REQ-LIB-2 becomes 'Scope comes
|
||||
from the path or a DOCX's source, lookups default to canon, and only canon
|
||||
files are ever cited as canon'. REQ-LIB-4 becomes 'Git holds every change. The
|
||||
bus fast-forwards a read-only mirror every 60 seconds, the one place agents
|
||||
read Markdown'. REQ-LIB-8 and REQ-LIB-9 add two ids, so the count goes from 85
|
||||
to 87. REQ-LIB-8 is titled 'Case 2 story files become DOCX by a human-only
|
||||
one-way import or in the editor; Markdown stays for notes and docs', and
|
||||
REQ-LIB-9 'The Console's DOCX editor: only a human saves, each save is one
|
||||
commit, and the editor code is vendored and pinned'.
|
||||
- Open points O1 to O4 of draft 1 were ruled by Mos in mos-1367 B1 to B4.
|
||||
- Sage files the build rows under Vikunja 570 and 571 after the rewrite is
|
||||
approved. This amendment adds the requirements only."
|
||||
|
||||
### Risks
|
||||
|
||||
The new lines go at the end of "## Risks and open questions", after the bullet
|
||||
that begins "Trusted mode lets an agent" and before the "## Milestones" heading.
|
||||
- "A DOCX file is a zip archive, so git can't show a line diff of a chapter.
|
||||
Review of a story change goes through the editor and the committed reader,
|
||||
not `git diff`. (Version 1.17.)"
|
||||
- "A converted chapter's DOCX is not in Obsidian's graph. Obsidian shows the
|
||||
chapter only through its archived Markdown, and no DOCX edit reaches that file's
|
||||
links. The DOCX's current links show in the Console's editor and its
|
||||
Connections view, and no graph shows them yet. (Version 1.17.)"
|
||||
- "Conversion can't be undone in place. Front matter, wiki link forms and
|
||||
unsupported blocks may not survive it, and the mirror can't refuse the user's
|
||||
push to an archived file. The controls are the fidelity report, the human's
|
||||
confirmation, an untouched original on refusal, and the kept archive.
|
||||
(Version 1.17.)"
|
||||
- "A restore of the stack's bus alone from a backup taken before an import drops
|
||||
the import's logged events, while the archive commit and the DOCX stay. A
|
||||
restore of the stack's whole host from such a backup also takes the writing
|
||||
store and the host's import records back to before the import. The archive
|
||||
commit stays in the user's Gitea repository and the DOCX stays on the writing
|
||||
store's remote, but the host keeps no record of the import. A bus restore counts
|
||||
import events, and the import's recovery covers both cases (Vikunja 571).
|
||||
(Version 1.17.)"
|
||||
|
||||
## d9 changes (Q239 and Webby round 5)
|
||||
|
||||
- J-b yes (Q239, mos-1467), in the five edits mos-1443 R3 set for a yes (Webby round 6 B1). R3.1: REQ-LIB-5's replacement gains the Connections view sentence after "no graph shows them yet.". R3.2: the graph Risks line ends with the editor and the Connections view. R3.3: the A6 line moves from "Not taken" under A6. R3.5: the REQ-LIB-5 note ends with the Connections view, which drops "until a Console view of their links exists" and "Q226 records that view as a later option". R3.4 is Gyro's design edit at 767bb62c7 (writing-editor.md:188-190, :240-244 and :1272, docx-citations.md:66), and it follows in his docs round. Beyond these and N21, nothing else in the PRD text changes.
|
||||
- N19 and N20 (Webby round 6): J-b's question text is in the past tense, and the line that the import doesn't wait for the view is cited as Mos's R3 default. Mos confirmed it at his J-b check (mos-1470).
|
||||
- N21 (Webby round 6, Mos yes in mos-1470): REQ-LIB-5's version tag cites Q239 for the Connections view beside Q222 for the two settings.
|
||||
- N15: the D7 landing line gives R8's wording beside the impact map's quote.
|
||||
- N16: the d7 changes line notes that R8 replaces "no agent credential reaches it" in d8.
|
||||
- N17: the REQ-LIB-2 Replace quote and the REQ-LIB-8 old-path bullet are rewrapped under 82 columns. Their words are unchanged.
|
||||
|
||||
## d8 changes (Mos's d8 fix list, check wf_63bba879-180)
|
||||
|
||||
- R1: REQ-LIB-4's tail covers DOCX saves, which agents see at their next lookup after the editor reports them as committed. The notes quote the whole 1.16 sentence. Source: nit/jason/F10.
|
||||
- R2: REQ-LIB-4's new title says the mirror is "the one place agents read Markdown". Source: decisions.n14.
|
||||
- R3: J-b asks whether 1.17 requires the Console Connections view, with Mos's recommendation of yes. Sources: mos1372/F9, jason/F2, anchors/F5.
|
||||
- R4: C2-02 and C2-03b gate 10-27, C2-11a and C2-11b are slice rows that don't, and "F42 wins" is gone. New J-k. Sources: anchors/F6, jason/F5.
|
||||
- R5: where the landed 571 design differs, this text holds. Sources: anchors/F8, adversarial/F8 item 1, jason/F8, critic/C6, G16's design follow-on.
|
||||
- R6: `library.convert` stays out of `ACTIONS`, and REQ-WAIT-1's hard-wait list doesn't change. Sources: gyro571/F1, adversarial/F8 item 3, critic/C6.
|
||||
- R7: a repository on the stack's host never counts as the writing store's remote, and Item 2 says so. Sources: mos1372/F2, adversarial/F1, gyro571/F2.
|
||||
- R8: "no agent credential reaches it" becomes "no agent can reach it with any credential". Source: Fenton's B1 under mos-1379.
|
||||
- R9: no playground DOCX becomes canon in 1.17, and a promotion of one is refused. New J-i. Sources: mos1372/F8, adversarial/F2.
|
||||
- R10: REQ-LIB-9 restates Q195's pin and right-click thesaurus, and its cite gains D4 and D22. Source: critic/C4.
|
||||
- R11: REQ-LIB-9's license bar is the licenses its independent security review accepted. New J-l. Source: critic/C3.
|
||||
- R12: no text for D16's narrow-window tabs. Mos records that reading in STATE. Sources: jason/F1, second verifier vote.
|
||||
- R13: no lookup returns a file's archived copy before publication. Source: adversarial/F5 part 3.
|
||||
- R14: a restore of the whole host from a backup taken before an import is a fourth recovery case. Source: adversarial/F6.
|
||||
- M1: REQ-LIB-4's writing-store sentences follow the later bus rows sentence and add the off-host remote rules and DOCX read timing. The note quotes both 1.16 sentences. Sources: mos1372/F2, adversarial/F1, gyro571/F2, anchors/F4, nit/jason/F10, adversarial/F5 part 1, Fenton B1.
|
||||
- M2: REQ-LIB-8's order bullet adds the remote's confirmation, the old-path read rule and "archive push". Sources: mos1372/F2 and the other remote findings, mos1372/F7, adversarial/F5 parts 2 to 4.
|
||||
- M3: REQ-LIB-8's gate bullet has four conditions, adding the remote. Sources: the remote findings, critic/C1 and C2 edit 2.
|
||||
- M4: the 1.17 note on when conversion opens restates the four conditions and drops W4b. Sources: jason/F5, the remote findings.
|
||||
- M5: Item 2 rewritten with the remote rules. Sources: the remote findings.
|
||||
- M6: the 1.12 gating note rewritten. Sources: anchors/F6, jason/F5.
|
||||
- M7: REQ-LIB-9 gains the split pane, the pin, the thesaurus, the license bar and the runtime load rule, and cites more of the design. Sources: jason/F1, critic/C4, critic/C3, nit/anchors/F12, nit/gyro571/F9.
|
||||
- M8: REQ-LIB-2, the one-way bullet and the C1 note keep a playground DOCX out of canon in 1.17. Sources: mos1372/F8, adversarial/F2.
|
||||
- M9: REQ-LIB-5 says what the reader can't read, and the import turns web links into plain text. New J-j. Sources: critic/C1, critic/C2.
|
||||
- M10: an exclusion of a whole book folder covers that book's DOCX documents. Source: adversarial/F3, with B5.
|
||||
- M11: the human-verbs sentence and the 571 list item name `library.convert` and REQ-WAIT-1. Sources: gyro571/F1, adversarial/F8 item 3, critic/C6.
|
||||
- M12: J-b rewritten. Sources: mos1372/F9, jason/F2, anchors/F5, gyro571/F3, critic/C6.
|
||||
- M13: J-d puts the agent read model to Jason as his call. Source: mos1372/F1.
|
||||
- M14: Status moves to version 1.17 and names REQ-LIB-9. Sources: anchors/F2, mos1372/F4, critic/C7.
|
||||
- M15: the 1.17 source line quotes Q235 in full (mos-1446) and the REQ-LIB-9 sources, at b99ca86b9. Sources: critic/C5, anchors/F9, jason/F8 pin part, nit/gyro571/F8, critic/C6 pin part.
|
||||
- M16: the 571 list lead names the two replaced design parts and the landed design. Sources: anchors/F9, anchors/F8, adversarial/F8 item 1, jason/F8, critic/C6.
|
||||
- Landing swap: Gyro's docs round for R5, R6 and R14 landed as b99ca86b9 (Fenton fenton-0015), so M15 and M16 cite b99ca86b9 in place of 767bb62c7, as the d8 fix list's header says. J-b keeps 767bb62c7 for its line numbers.
|
||||
- M17: W4 section numbers in the remote item and the lookups note. Sources: gyro571/F5, nit/adversarial/F11.
|
||||
- M18: the D7 landing line. Sources: gyro571/F5, the remote findings.
|
||||
- M19: a logged event for each import stage, in REQ-LIB-8 and A2. Source: gyro571/F7.
|
||||
- M20: the Risks line on restores adds the whole-host case. Sources: adversarial/F6, gyro571/F7.
|
||||
- M21: the 571 restore item adds the whole-host case. Source: adversarial/F6.
|
||||
- M22: the graph Risks line and the REQ-LIB-5 note name a Console view. Sources: anchors/F5, jason/F2.
|
||||
- M23: the d6 B3 line drops "F42 wins". Source: anchors/F6.
|
||||
- M24: new Markdown in a converted book, with the old-path and playground exceptions. Source: mos1372/F6.
|
||||
- M25: J-c's last sentence is Mos's reading. Source: mos1372/F10.
|
||||
- M26: J-h rewritten with the D13 defaults. Source: jason/F3.
|
||||
- M27: Item 4 rewritten. Sources: jason/F4, gyro571/F4, decisions.unmarked_folders.
|
||||
- M28: the "Not taken" note on unmarked folders. Sources: jason/F4, gyro571/F4, decisions.unmarked_folders.
|
||||
- M29: the import refuses an unmapped file. Sources: gyro571/F4, decisions.unmarked_folders.
|
||||
- M30: placement notes for REQ-LIB-8, REQ-LIB-9 and the Risks lines. Sources: anchors/F3, nit/anchors/F14.
|
||||
- M31: REQ-LIB-4's title and the D11 landing line. Source: decisions.n14.
|
||||
- M32: the Q236 note limits the 1.12 Obsidian note to Markdown files. Source: critic/C8.
|
||||
- M33: the exclusion note adds path exclusions of DOCX documents. Source: anchors/F7.
|
||||
- M34: A9 moves from "Mine" to the impact-map list. Source: decisions.exclusion_by_id.
|
||||
- M35: the D5 landing line. Sources: anchors/F10, mos1372/F5, nit/adversarial/F12.
|
||||
- M36: the D9 and D14 landing lines. Source: mos1372/F1.
|
||||
- M37: the O1 and C3 notes add the off-host remote and its confirmation. Sources: the remote findings.
|
||||
- M38: the REQ-PROJ-3 note. Source: nit/anchors/F11.
|
||||
- M39: REQ-LIB-5's freshness sentence keys on the editor reporting a save as committed. Source: nit/adversarial/F9.
|
||||
- M40: REQ-LIB-6 bars agent requests for a conversion and allows a suggestion in chat. Source: nit/jason/F9.
|
||||
- M41: REQ-LIB-2's opening and old-path rule, and the "Mine" note. Source: nit/adversarial/F10.
|
||||
- M42: REQ-LIB-8's lead sentence on Markdown story files. Source: nit/mos1372/F12.
|
||||
- M43: REQ-LIB-6's citation sentence. Source: nit/anchors/F13.
|
||||
- M44: REQ-LIB-5's version tag uses plain quotes. Source: nit/anchors/F15.
|
||||
- M45: reference codes B5, D12, D10 and B4 name their source. Source: nit/mos1372/F11.
|
||||
- Files the row would own: four files, with the id count going to 87. Sources: anchors/F1, adversarial/F4, decisions.n14 F4.
|
||||
|
||||
## d7 changes (mos-1379, Fenton's 571 B2)
|
||||
|
||||
- REQ-LIB-5 gains Mos's three sentences after the extracted-text sentence. The text agents read has tracked changes accepted. Comments, and text with tracked deletions shown, each wait for a setting only a human turns on, off by default. Its version tag cites Q222 for the two settings, which is the only text here beyond Mos's.
|
||||
- J-d gains Mos's sentence on comments and tracked deletions.
|
||||
- Fenton's B1 is design only. d6's "no agent credential reaches it" holds (R8 replaces it in d8), and the shared helper stays #431.
|
||||
|
||||
## d6 changes (Webby round 3, webby-0207)
|
||||
|
||||
- B1: titles for REQ-LIB-8 and REQ-LIB-9 in the requirements.json note and the file list. REQ-LIB-9 takes Webby's title. REQ-LIB-8 keeps mine, because Webby's ("...one-way import that archives the Markdown under its old path") leaves out D2's story files started in the editor.
|
||||
- B2: REQ-LIB-2 makes a Markdown file at a converted file's old path unmapped, ahead of the playground and canon rules, so a playground conversion is covered too. REQ-LIB-8's old-path bullet says "neither canon nor playground".
|
||||
- B3: the slice note names Q226 F42's three rows (C2-02, C2-03b, C2-11b). C2-11a joined the slice with C2-11b, because C2-11b needs it. Both are slice rows that don't gate 10-27, as design 9.2, case2-rows.md:1655 and the impact map's A57 say. The 'F42 wins' sentence is gone from the note in d8, so no queue change follows from it.
|
||||
- N10: the cite-scope replacement is scoped to REQ-LIB-2. The 1.14 note's copy stays.
|
||||
- N11: the notes say REQ-LIB-8 adds the by-id exclusion form REQ-LIB-3 doesn't have.
|
||||
- N12: J-h marks the autosave intervals for Jason.
|
||||
- N13: J-b names REQ-LIB-5's "Connections are always visible" tag and Q226's "another way to handle the visual brain map".
|
||||
- N14, not taken: REQ-LIB-4's title stays internal F3's text, as mos-1372 D11 ruled. Webby's alternative, "...and agents read Markdown from it alone" (120 chars), is Mos's call.
|
||||
|
||||
## Where each mos-1372 item landed
|
||||
|
||||
- D1: REQ-LIB-2's DOCX sentences, the REQ-LIB-8 lead and import bullet, C1 above, and the notes. "Playground files stay Markdown in 1.17" is gone.
|
||||
- D2: REQ-LIB-2's "for one the human starts in the editor" and the REQ-LIB-8 new-story-file bullet. The universe-and-book note is now true.
|
||||
- D3: the REQ-LIB-8 order bullet. This also settles N8.
|
||||
- D4: REQ-LIB-8's one-way bullet, which carries the archive refusal and consequence F2's text, and the 571 list item.
|
||||
- D5: it landed in REQ-LIB-2's "a Markdown file at a converted file's old path is unmapped" in Webby's B2 form, and in the REQ-LIB-8 old-path bullet.
|
||||
- D6: REQ-LIB-5's freshness sentences.
|
||||
- D7 and mos-1375: REQ-LIB-4's remote sentences, the remote step in REQ-LIB-8's order bullet, the fourth opening condition and the 1.17 note on when conversion opens, plus the 571 list item on W4 section 7.3, which replaces the no-remote rule of the old W4 section 7.4. The impact map's item 2 adds "only the stack's host pushes, and no agent credential reaches it", which R8 words in d8 as "no agent can reach it with any credential".
|
||||
- D8: the REQ-LIB-5 replacement, the import bullet's wiki-link sentence, the Risks line, the note quoting the old sentence, and "REQ-LIB-1 only gains sentences".
|
||||
- D9: the 1.17 note that agents read a converted story file through the Case 2 lookups, from the d4 check's consistency B2, and J-d, which puts the read model to Jason as his call.
|
||||
- D10: the d4 check's B5 sentence, in REQ-LIB-8.
|
||||
- D11: internal F3's clause in the REQ-LIB-4 section, and in the notes the title as Mos amended it for N14.
|
||||
- D12: the whole 1.16 sentence replaced, and quoted in full in the notes.
|
||||
- D13: consequence F1's text, in REQ-LIB-4 and REQ-LIB-8's first bullet.
|
||||
- D14: the row text from the d4 check's consistency B1, in the mark-and-move bullet and C3 above.
|
||||
|
||||
## What d5 adds beyond mos-1372 (for Webby and Mos to check)
|
||||
|
||||
From the impact map, folded as mos-1368 item 1 asked:
|
||||
- A1: the Console shows a cite's format and converted state (REQ-LIB-2).
|
||||
- A2: the logged events and the several-humans setting, default owner only, under Q222.
|
||||
- A3: the import is outside the vocabulary, the hard-wait list is unchanged, and the archive move isn't `data.delete`. The C6 code site is in the 571 list.
|
||||
- A4: the section lead and REQ-LIB-1's writing-store sentence.
|
||||
- A6: format and location in every answer, extracted text with DOCX locators, links across formats.
|
||||
- A6's named chapter-links view. D8 took B8's "no graph shows them yet", and J-b put the view to Jason. His O2 (Q239) takes it in d9 as REQ-LIB-5's Connections view (mos-1443 R3).
|
||||
- A7: read-only extends to the store and the archive, no agent action on a story file in any format, no agent DOCX write, and role contracts state the formats. C1's role contract line is a 571 build item.
|
||||
- A8: the DOCX cite shape, and a cite naming an archived file as current is refused.
|
||||
- A9: a hybrid sees a source's DOCX canon through REQ-LIB-2's source scope, and REQ-LIB-8 lets an exclusion name a DOCX document by its id. The id form lets the human exclude a single DOCX started in the editor, which has no old path. An archived file isn't canon, so no hybrid sees it.
|
||||
- A10: REQ-LIB-9, so the count is 87, not 86.
|
||||
- A11: the Hermes gate, from item 3.
|
||||
- A12: the C2-19 promotion sentence.
|
||||
- A13: the broker's path check treats the archive folder as story.
|
||||
- A14 and C5: two Risks lines.
|
||||
- A15: the flagged-new-file bullet. It sits beside the note from the d4 check's B4, "a new Markdown file in a book folder is still canon". The file stays canon and is offered for import.
|
||||
- A16: three titles change and two stay.
|
||||
- C2: manifest order for the manuscript lookup.
|
||||
- F3: the C2-02 gating note.
|
||||
- F4: Mos's measurement.
|
||||
|
||||
Mine, beyond either source:
|
||||
- REQ-LIB-2 opens "A Markdown file's scope comes from its path and from past imports". DOCX scope comes from the source, so "a file's" would be false.
|
||||
- REQ-LIB-9's autosave sentence. It is Mos's reading of F41's "instantly committed", with human-set intervals. The exact intervals stay in the design.
|
||||
|
||||
Not taken:
|
||||
- Impact map item 4's "files in unmarked folders are eligible one at a time", for folders outside every book folder and playground only. Their files are unmapped under REQ-LIB-2, so a DOCX made from one would have no universe or book to keep. The human maps such a folder into a book folder or a playground first, under REQ-LIB-1. A file in an unmarked folder inside a book folder or a playground is canon or playground, and the import takes it one file at a time.
|
||||
|
||||
## Marked for Jason (additions in the text he sees)
|
||||
|
||||
- J-a. Whether "We will not run the systems in parallel" applies per chapter or as one switch per book. His call. As written, it applies per chapter.
|
||||
- J-b. A converted chapter's DOCX is not in Obsidian's graph. Obsidian shows the chapter only through its archived Markdown, and no DOCX edit reaches that file's links. REQ-LIB-5 still cites two TARGET lines, "Visualization of data via Obsidian or equivalent is required" and "Connections are always visible". Mos's reading was that 1.17 as d8 wrote it met neither line for converted chapters. Their current links showed to the user only as clickable text inside the editor, and no graph or list showed them. Q226 leaves room for "another way to handle the visual brain map". The O2 design that landed for Vikunja 571 at 767bb62c7 held a Console Connections view and its tests as later work that waited for this answer. His call was whether 1.17 requires that view. As d8 was written, it did not. Mos recommended that it does, because impact map A6 says "Recommended: name the view" and TARGET says "Connections are always visible". Jason answered O2 (Q239): 1.17 requires the view. d9 writes it into REQ-LIB-5, its note and the Risks line, in the text of mos-1443 R3. The view comes with the conversion work after 10-27, and the 10-27 Case 2 release stays on Markdown (Q235 P6, Q237). That the import does not wait for the view is Mos's default in mos-1443 R3. Q239 doesn't address it, and Mos confirmed it at his J-b check (mos-1470).
|
||||
- J-c (mos-1374): "The store pushes to a remote (D7), and the Case 2 VM is in the nightly PBS backup. For a fixed restore point before her first real import, a manual backup of VM 1012 with PBS protect runs first. That step is reversible and needs no reboot." Mos's reading is that this sits inside the P5 go step, so it needs no new question.
|
||||
- J-d. How agents read a converted chapter, D9. His call. As written, an agent reads it through the ordinary Case 2 lookups in any thread or room, under the same project and universe access as Markdown canon. This is Mos's reading of F41 and REQ-LIB-7. The other choice is the model in the earlier O2 design, W4 sections 7.1 and 7.2. Under it, an agent reads a converted chapter only in a new writing conversation that holds one human and one agent, only after the business owner grants that agent access to that document, and by default only what the human attaches to each message. Comments and tracked deletions stay hidden from agents until a human turns on a setting for each. Markdown has neither, so the default keeps agents at the access Markdown gives them. Those two sentences are Mos's ruling mos-1379. A yes to the text as written settles it.
|
||||
- J-e. Versioning means the editor's tracked changes plus one git commit per save.
|
||||
- J-f. A converted chapter can be edited only in the Console while the stack is reachable. Obsidian no longer edits it offline.
|
||||
- J-g. The import takes Markdown only. Importing .doc files stays a separate task.
|
||||
- J-h. Autosave under REQ-LIB-9. Jason's Q226 F41 says "All changes are instantly committed and available to the agents." Mos's reading, recorded with that ruling, is "Each save is a commit at once, and agents read it at their next lookup." At the O2 design's D13 defaults, a save commits at most 2 seconds after a pause in typing or 15 seconds into steady typing, with no human action needed. Mos's reading in mos-1359 counts that delay as "instantly committed". Only a human changes the intervals. The PRD names no values, and the defaults stay in the design. Whether a delay of seconds fits his word "instantly" is his call, so this is marked for him.
|
||||
- J-i. In 1.17 a playground story that is DOCX can't become canon. That covers a playground Markdown file the human imports, a story the human starts in a playground in the editor, and the DOCX of a draft universe after it is made real. Today Melanie promotes a playground file by moving it into a book folder in Obsidian, and a DOCX in the writing store can't be moved that way. She can start a new story in the book and paste the text in, but it gets a new document id and the playground copy keeps the history. A later version adds DOCX promotion with C2-19, which comes after the slice.
|
||||
- J-j. Web links, text boxes and fields in a converted chapter. Jason's Q226 F41 says "All changes are instantly committed and available to the agents." The reader in this version can't read a web link, a text box, a field or hidden text. As written, the import turns each web link into plain text that keeps its label and address, and the human confirms that in the fidelity report. A web link that can't be kept safely as plain text refuses the import. If a later edit adds one of these, the save keeps it and the editor warns the human. Agents then get none of that part's text until a save takes it out.
|
||||
- J-k. The 1.12 note Jason approved says bus writes, C2-02 and C2-03b, don't gate 10-27. His Q226 "F42: ok" moved them into the 10-27 slice, and design 9.2 counts them in the smallest slice that lets Melanie start. Mos reads that as making them gate 10-27. The editor rows C2-11a and C2-11b still don't gate it, per his Q195 words "we need to get it moving as an add-on".
|
||||
- J-l. REQ-LIB-9 does not use REQ-PROJ-3's "permissive license" bar. It takes the licenses that the editor's independent security review accepted, because that review cleared fonts under the SIL Open Font License and the Ubuntu Font License, which not every reader counts as permissive. Mos's reading.
|
||||
|
||||
Marked as changes beyond P1 to P6, from the impact map (Q17, item 5):
|
||||
- A1 (archived scope and canon cites)
|
||||
- A2 (the conversion requirement, REQ-LIB-8)
|
||||
- A3 (outside the vocabulary, hard-wait list unchanged)
|
||||
- A6 (lookups across formats)
|
||||
- A7 (agent bounds)
|
||||
- A8 (DOCX cites)
|
||||
- Item 2 and mos-1375: each writing-store commit is pushed to a remote off the stack's host. Only a human sets it, it starts unset, and a repository on the stack's host doesn't count. Only the stack's host pushes to it, and no agent can reach it with any credential. Import and new story files stay closed until the remote is set and checked, and each import's archive push waits until the remote holds the DOCX commit. This keeps "Git is used for all changes -- local and remote" true for DOCX chapters.
|
||||
- Item 3 (the Hermes gate)
|
||||
- Item 4. Canon and playground story files convert one file at a time, including files in unmarked folders inside a book folder or a playground. Type-folder and supporting files stay Markdown. A story file outside every book folder and playground has no universe or book, so it converts only after a human maps its folder into a book folder or a playground. The mapping step is Mos's reading of P1 and Q30.
|
||||
|
||||
The F4 measurement goes with them.
|
||||
|
||||
## Files the row would own
|
||||
|
||||
The id count goes from 85 to 87. 1.16 owned three files because its count stayed at 85. Like 1.13, the last version that added ids, the row owns four files:
|
||||
- `docs/prd/mosaic-stack.md`
|
||||
- `packages/tasks/requirements.json`: the version 1.17, the two new ids REQ-LIB-8 and REQ-LIB-9 with their titles, and the changed titles of REQ-LIB-1, REQ-LIB-2 and REQ-LIB-4. The new ids go after REQ-LIB-7 and before REQ-INST-1, in the same order as the PRD's bullets.
|
||||
- `packages/tasks/tests/requirements.test.mjs`: the version pin 1.17, the id count of 87, and the duplicate-id message, which goes from `requirements[85].id: REQ-ROLE-1 twice` to `requirements[87].id: REQ-ROLE-1 twice`.
|
||||
- `packages/bus/tests/requirements.test.mjs`: the list length, which goes from 85 to 87.
|
||||
|
||||
The gate would match 1.16's:
|
||||
1. A reviewer approves the text.
|
||||
2. Jason says yes to the exact text through Mos.
|
||||
3. Both `requirements.test.mjs` files, in `packages/tasks/tests` and `packages/bus/tests`, and every `scripts/test-*.sh` that reads the PRD or `requirements.json` are green on Sage's gate.
|
||||
4. Mos gets the landing commit.
|
||||
@@ -0,0 +1,652 @@
|
||||
# PRD 1.17 draft: Case 2 story files are DOCX (Q235 to Q237, P1 to P6)
|
||||
|
||||
Sage, 2026-10-23, for mos-1366 item 1 and Vikunja 570. Draft 9. It is d8 plus Jason's Q41 answer O2 (DECISIONS Q239, mos-1467), which makes J-b yes, Webby's round 5 N15 to N17 (webby-0210) and Webby's round 6 B1, N19 and N20 (webby-0212) (see "d9 changes"). d8 is d7 plus Mos's d8 fix list mos-1443 (see "d8 changes"). d7 is d6 plus Mos's ruling mos-1379 on Fenton's 571 B2 (see "d7 changes"). d6 is d5 with Webby's round 3 (webby-0207) fixed: B1 to B3 and N10 to N13, with N14 left for Mos (see "d6 changes"). d5 folds in:
|
||||
- Mos's d4 check ruling mos-1372 (D1 to D14; D15 was dropped).
|
||||
- mos-1374, which replaces J-c.
|
||||
- The impact map mos-1368 (jason-master-restart d183246, `projects/mosaic-mvp/case2-docx-impact-map.md`: A1 to A16, critic items C1 to C6, items 2 to 7).
|
||||
- Mos's F4 measurement.
|
||||
|
||||
Review order (mos-1367 B5): Webby, then Mos, then Jason. Base: PRD 1.16, `docs/prd/mosaic-stack.md` blob a6db9b4a at 482e865ac.
|
||||
1.16 lands first (row 291), and this text is a new version on top of it. Nothing here is built or queued. The impact map's queue changes (A24, A57 to A59) go through `scripts/mosaic queue` after 1.17 is approved (C9).
|
||||
|
||||
Sources, verbatim in Mos's DECISIONS:
|
||||
- Q235, Jason's Q36 answer.
|
||||
- Q236: chapters are DOCX, Markdown for agent files and docs.
|
||||
- Q237: "It is correct for now (subject to future change)".
|
||||
- The P1 to P6 reading as restated to Jason in Q38.
|
||||
|
||||
## Settled points (mos-1367)
|
||||
|
||||
These stand as ruled. d5 changes only what mos-1372 changed.
|
||||
|
||||
- **O1. Where a converted chapter lives.** The DOCX lives in the writing store of
|
||||
O2 design D12, and the archived Markdown stays in the user's repository under a
|
||||
human-marked archive folder. Ruled yes in B1, on the condition that the text
|
||||
says plainly that a converted chapter leaves the Gitea-mirrored repository and
|
||||
lives in the stack's writing store. mos-1372 D7 and mos-1375 add a remote for
|
||||
the store, off the stack's host.
|
||||
- **O2. The unit of conversion.** One story file per import, refusing a file
|
||||
that is already converted. Ruled yes in B2: each file gets its own fidelity
|
||||
report, human confirmation, manifest and commit.
|
||||
- **O3. Whether conversion is a vocabulary action.** A human-only verb outside
|
||||
the agent vocabulary, with no role grant and no agent request path. Ruled yes
|
||||
in B3.
|
||||
- **O4. Reading archived Markdown.** A new `archived` scope. Lookups leave it out
|
||||
unless the caller asks, every answer marks it archived, and a canon citation
|
||||
can't name it. Ruled yes in B4.
|
||||
|
||||
## d3 choices after mos-1372
|
||||
|
||||
- **C1 is replaced by D1, keep-source-scope.** A DOCX keeps its source's scope:
|
||||
universe, book, and canon or playground. A playground DOCX follows the
|
||||
playground rules and is never returned as canon. The import accepts canon and
|
||||
playground story files. Type-folder files and supporting files stay Markdown
|
||||
(P4). This also settles the impact map's Q18, item 4. It leaves one narrowing
|
||||
for Jason, J-i. A playground DOCX can't become canon in 1.17.
|
||||
- **C2 stands.** The move keeps the file's path under the archive folder:
|
||||
`book1/ch3.md` goes to `<archive>/book1/ch3.md`. An archived file's universe
|
||||
and book are those of its old path.
|
||||
- **C3 is replaced by D3 and D14.** The archive commit is written by a new
|
||||
human-only bus step (Vikunja 571) on the C2-02 write plumbing and pushed through
|
||||
C2-03b. The order is the store commit and the remote's confirmation that it
|
||||
holds that commit, then the archive commit and its push, then publication to the
|
||||
lookups.
|
||||
- **N4 stands.** REQ-LIB-8 says "a real user's books". The PRD names no user
|
||||
anywhere in Case 2.
|
||||
|
||||
## Text changes
|
||||
|
||||
### Status and sources
|
||||
|
||||
In the Status bullet's first line, "- Status: approved, version 1.16." becomes
|
||||
"- Status: approved, version 1.17." Status gains, after the two 1.16 sentences at
|
||||
the end of the bullet: "Version 1.17 records Jason's rulings Q235 to Q237 of
|
||||
2026-10-23: Case 2 story files are DOCX, Markdown stays for agent files and
|
||||
documentation, and a human-started import converts a Markdown story file one way.
|
||||
It also adds REQ-LIB-9, the Console's DOCX editor, from Jason's rulings Q195, Q226
|
||||
and Q234. It changes what Case 2 requirements allow, so Mos puts the text to Jason
|
||||
as written, and it lands only after his yes (mos-1366, Vikunja 570)."
|
||||
|
||||
New source line, above the 1.16 one: "Version 1.17 source: Jason's Q235, answering
|
||||
Q36: 'How about we see a markdown file, we convert the markdown file to docx, then
|
||||
archive or mark the markdown file as converted. The new standard for files in the
|
||||
creating writing use case is to use docx to handle versioning and WYSIWYG. We
|
||||
eliminate the potential for confusion based on the files. Agents must understand
|
||||
which files are what format and where they live. The system must allow for import
|
||||
and conversion. We will not run the systems in parallel. There will be an
|
||||
irreversible conversion of the files.' His Q236 says: 'For creative writing
|
||||
chapters, we need to utilize docx as the standard. Markdown is acceptable for
|
||||
agent files, instructions, PRDs, documentation, etc. The creative writing files
|
||||
should be docx for familiarity for the user.' His Q237 says 'Q38: It is correct
|
||||
for now (subject to future change)', which confirms Mos's reading P1 to P6.
|
||||
REQ-LIB-9 rests on his Q195 request for 'a full WYSIWYG editor within Mosaic
|
||||
Stack', his Q226 answer 'F41: We want to move editing into the WYSIWYG in the
|
||||
WebUI. All changes are instantly committed and available to the agents.' and his
|
||||
Q234 answer 'Q37: yes' to the exact security header for the editor's frame. The
|
||||
storage follows the O2 design D10 to D12 as rewritten under Vikunja 571, at
|
||||
b99ca86b9 on `refactor`."
|
||||
|
||||
### Creative library, section lead
|
||||
|
||||
Replace "The library is the user's own writing repository." with "The library is
|
||||
the user's own writing repository plus the stack's writing store, which holds the
|
||||
DOCX story files (REQ-LIB-8, version 1.17)."
|
||||
|
||||
### REQ-LIB-1
|
||||
|
||||
Add, before "(Design 1.1": "The mapping also names one archive folder, set by the
|
||||
human, where converted Markdown story files go. A book's DOCX story files are in
|
||||
the stack's writing store, under the same universe and book (REQ-LIB-8)." Version
|
||||
tag, inside the closing parenthesis: "Version 1.15: supporting folders.)" becomes
|
||||
"Version 1.15: supporting folders. Version 1.17: the archive folder and the
|
||||
writing store.)"
|
||||
|
||||
### REQ-LIB-2
|
||||
|
||||
Replace "A file's scope comes from its path alone, prefixes first: a Markdown
|
||||
file under a supporting prefix is supporting, one under a playground prefix at
|
||||
any level is playground, any other Markdown file in a book folder is canon, and
|
||||
everything else is unmapped and never returned. A folder in both lists is
|
||||
supporting." with:
|
||||
|
||||
"A Markdown file's scope comes from its path and from past imports, prefixes
|
||||
first: a Markdown file under the archive folder is archived, one under a
|
||||
supporting prefix is supporting, a Markdown file at a converted file's old path is
|
||||
unmapped under REQ-LIB-8 but flagged in the format map, one under a playground
|
||||
prefix at any level is playground, any other Markdown file in a book folder is
|
||||
canon, and everything else is unmapped and never returned. A folder in both lists
|
||||
is supporting. The archive folder follows the same placement rules and may not
|
||||
overlap another prefix or a book folder. An archived file keeps the path it had
|
||||
under the archive folder, and its universe and book are those of that old path. A
|
||||
DOCX document in the writing store keeps its source's scope: the universe, the
|
||||
book if there is one, and canon or playground. Its source is the Markdown file it
|
||||
was converted from or, for one the human starts in the editor, the universe and
|
||||
book or the playground the human picks (REQ-LIB-8). A playground DOCX follows the
|
||||
playground rules and is never returned as canon. It stays playground after its
|
||||
draft universe is made real. In version 1.17 no promotion or mapping change makes
|
||||
a playground DOCX canon. A later version of this PRD says how one becomes canon."
|
||||
|
||||
Replace "Lookups return canon unless the caller asks for playground or
|
||||
supporting." with "Lookups return canon unless the caller asks for playground,
|
||||
supporting or archived. Every answer marks an archived file as archived and names
|
||||
the DOCX that replaced it."
|
||||
|
||||
Replace "so an agent can't cite a playground or supporting file as canon" with "so
|
||||
an agent can't cite a playground, supporting or archived file as canon".
|
||||
|
||||
In REQ-LIB-2 only, replace "The Console shows each cite's scope." with "The Console shows each cite's
|
||||
scope and format, and whether a cited Markdown file was converted."
|
||||
|
||||
Version tag, inside the closing parenthesis: "Version 1.15: supporting.)" becomes
|
||||
"Version 1.15: supporting. Version 1.17: archived scope and DOCX scope.)"
|
||||
|
||||
### REQ-LIB-4
|
||||
|
||||
Replace "The user commits and pushes with their own tools, today Obsidian with
|
||||
obsidian-git." with "The user commits and pushes Markdown with their own tools,
|
||||
today Obsidian with obsidian-git. A converted story file's current text is no
|
||||
longer in that repository, which keeps only its archived Markdown (REQ-LIB-8)."
|
||||
|
||||
Add, after "Bus writes and pushes come in later rows (C2-02, C2-03b).": "A DOCX
|
||||
story file is saved in the Console's editor, and each accepted save is one commit
|
||||
by the human in the stack's writing store (REQ-LIB-9). Each writing-store commit
|
||||
is also pushed to a remote off the stack's host, never by force-push. The remote
|
||||
is a setting only a human changes, and it starts unset. A repository on the
|
||||
stack's host never counts as that remote. A missing remote or a failed push never
|
||||
undoes a save, and the user sees each push that is pending or failed. Only the
|
||||
stack's host pushes to that remote, and no agent can reach it with any credential.
|
||||
Agents read the local commit."
|
||||
|
||||
Replace "Agents read only the mirror's served commit and never hold a copy of
|
||||
their own, so they see the user's push within about a minute." with "Agents read
|
||||
the user's repository only at the mirror's served commit, except a story file
|
||||
during its import under REQ-LIB-8. They read a DOCX story file only at a committed
|
||||
version in the writing store, under REQ-LIB-5. They never hold a copy of their
|
||||
own, so they see the user's Markdown push within about a minute and a DOCX save at
|
||||
their next lookup after the editor reports it as committed."
|
||||
|
||||
Version tag: "and the sync items.)" becomes "and the sync items. Version 1.17,
|
||||
with Q222 for the remote setting.)"
|
||||
|
||||
### REQ-LIB-5
|
||||
|
||||
Add, after "Each answer names the commit it read.": "Each answer also names the
|
||||
file's format and where it lives: its path, or its document id in the writing
|
||||
store. A DOCX story file is read from its latest committed save, never from
|
||||
unsaved text. A lookup that starts after the editor reports a save as committed
|
||||
returns that save or a later one, even while its push to the remote is pending or
|
||||
has failed, and the answer names the version. Get, search and manuscript read a
|
||||
DOCX story file as its extracted text with DOCX locators, never as lines. That
|
||||
text is the file with its tracked changes accepted. An agent reads a story file's
|
||||
comments, or its text with tracked deletions shown, only after a human turns on
|
||||
the matching setting. Both settings are off by default, and only a human changes
|
||||
them. The reader in this version can't read some things a DOCX story file can
|
||||
hold, such as a web link, a text box, a field or hidden text. The editor warns the
|
||||
human when an edit adds one, and the human can still save it. Until a later save
|
||||
takes it out, lookups give agents none of the text of the part that holds it,
|
||||
whether that is the chapter's body, a note or a comment. The manuscript lookup
|
||||
follows the book's chapter order across both formats (O2 design D10), and the next
|
||||
chapter it names is never an archived file. Links out and in resolve across both
|
||||
formats. The universe lookup returns the format map (REQ-LIB-8)."
|
||||
|
||||
Replace "The user sees them in Obsidian's graph and backlinks." with "The user sees
|
||||
a Markdown file's links in Obsidian's graph and backlinks. A DOCX story file's
|
||||
wikilinks stay clickable in the Console's editor (O2 design D11), and no graph
|
||||
shows them yet. The Console's Connections view lists the current links and
|
||||
backlinks of each story file the user may see, in both formats, with each one's
|
||||
scope and format."
|
||||
|
||||
Version tag: 'and "Connections are always visible".)' becomes 'and "Connections
|
||||
are always visible". Version 1.17, with Q222 for the two settings and Q239 for
|
||||
the Connections view.)'
|
||||
|
||||
### REQ-LIB-6
|
||||
|
||||
Replace "A reply may cite lines of the repository at a commit or pages the agent
|
||||
fetched, and the broker checks every citation before it stores the message." with
|
||||
"A reply may cite lines of a Markdown file at a commit, a place in a committed
|
||||
DOCX story file, or pages the agent fetched, and the broker checks every citation
|
||||
before it stores the message. A DOCX citation never calls a paragraph a line. It
|
||||
names the document id, the committed version and a DOCX locator. A citation that
|
||||
names an archived file as current is refused. The `requireCitation` setting below
|
||||
applies to Markdown and DOCX citations alike, and so does the rule for a citation
|
||||
marked as canon."
|
||||
|
||||
Add, after "the broker refuses such a write before any git call.": "The writing
|
||||
store and the archive folder are read-only to a creative agent as well, and the
|
||||
broker's path check treats the archive folder as story. No agent creates, edits,
|
||||
deletes, converts, archives or restores a story file in any format, and no agent
|
||||
writes a DOCX. No creative agent starts or undoes a conversion, and none asks for
|
||||
one through a tool or a decision it raises, as REQ-LIB-8 says. An agent may still
|
||||
suggest a conversion to the human in chat. Each creative role's contract states
|
||||
which files are DOCX and which are Markdown, and cites a DOCX story file by its
|
||||
locator, never by line."
|
||||
|
||||
Version tag, inside the closing parenthesis: "Version 1.15.)" becomes "Version
|
||||
1.15. Version 1.17.)"
|
||||
|
||||
### New REQ-LIB-8
|
||||
|
||||
This bullet goes after REQ-LIB-7 and before the "### Installer" heading.
|
||||
|
||||
"- **REQ-LIB-8.** Story files become DOCX. DOCX is the standard for chapters and
|
||||
the other story text in a Case 2 writing project, canon and playground alike. A
|
||||
story file becomes DOCX through a human's import (below), or when the human
|
||||
starts it in the editor. From then on DOCX is its only working format, edited in
|
||||
the Console's editor (REQ-LIB-9). A Markdown story file stays Markdown until its
|
||||
import. Files in a type folder (REQ-LIB-1), such as character and place notes,
|
||||
stay Markdown. So do supporting files, agent files, instructions, PRDs and
|
||||
documentation, including the character sheets, summaries and cross-links agents
|
||||
keep under REQ-LIB-6.
|
||||
- A converted story file's current text leaves the user's Gitea-mirrored
|
||||
repository. Its DOCX lives in the stack's writing store, on the stack's host,
|
||||
and not in the repository the user pushes or opens in Obsidian. Only its
|
||||
archived Markdown stays there.
|
||||
- An import converts one Markdown story file, canon or playground, outside every
|
||||
type folder (REQ-LIB-1), to a DOCX document in the writing store. It refuses
|
||||
an unmapped, supporting, archived or type-folder file, and a file that is
|
||||
already converted. Each import has its own fidelity report, the human's
|
||||
confirmation of that report, a manifest naming the source file and its commit,
|
||||
and one commit in the store. The DOCX keeps every wiki link of the source as
|
||||
written, and a lost link to another library file fails the fidelity report. A
|
||||
web link becomes plain text that keeps its label and address, and the fidelity
|
||||
report lists it for the human to confirm. A web link that can't be kept safely
|
||||
as plain text refuses the import.
|
||||
- The human can also start a new story file in the editor, in a universe and
|
||||
book or a playground the human picks. It is a DOCX document in the writing
|
||||
store from its first commit, with no Markdown copy, and it opens under the
|
||||
same gate as the import.
|
||||
- The same import marks the Markdown file converted, naming the document, and
|
||||
moves it to the archive folder under its old path (REQ-LIB-1, REQ-LIB-2).
|
||||
The mark and the move are one commit authored by the human in the user's
|
||||
repository. A new human-only bus step (Vikunja 571) on the C2-02 write
|
||||
plumbing writes it, and C2-03b pushes it. The move keeps the bytes and the
|
||||
git history, so it isn't `data.delete`.
|
||||
- The store commit comes first, and the writing store's remote must confirm it
|
||||
holds that commit. Then come the archive commit and its push, then publication
|
||||
to the lookups. Until publication the Markdown at its old path keeps its canon
|
||||
or playground scope. Agents read that file at the commit the import started
|
||||
from, even after the mirror's served commit reaches the archive commit or a
|
||||
later one. No lookup returns the file's archived copy before publication, even
|
||||
one that asks for archived files. A refusal or failure before the archive push
|
||||
leaves the Markdown unmoved and no DOCX that a lookup returns. A failure after
|
||||
the archive push leaves a pending import that the human sees and can resume,
|
||||
never a claim that nothing moved. An archive push whose result is unknown
|
||||
counts as a failure after the archive push until the stack reads the result
|
||||
back from the user's remote.
|
||||
- Each stage of an import writes a logged event. The import's record on the
|
||||
stack's host names the source file's path, commit and blob and the new
|
||||
document id, and each event points to that record.
|
||||
- After a conversion, a Markdown file at the converted file's old path is
|
||||
neither canon nor playground. Lookups leave it out, the format map flags it,
|
||||
and the human is told.
|
||||
- A new Markdown story file in a book folder that holds converted files stays
|
||||
canon until its import, unless it sits at a converted file's old path or under
|
||||
a playground prefix. The Console flags it and offers the import. Nothing
|
||||
converts it on its own.
|
||||
- A hybrid's exclusion that covers a converted file's old path also covers its
|
||||
DOCX document (REQ-LIB-3). An exclusion of a whole book folder also covers
|
||||
every DOCX document in that book, including one started in the editor. An
|
||||
exclusion can also name a DOCX document by its id.
|
||||
- Only a human starts an import or a new story file, from the CLI or a Console
|
||||
session signed in as that human. Both are human verbs outside the agent action
|
||||
vocabulary. Neither is added to `ACTIONS` in the business vocabulary or to the
|
||||
broker's always-gated list, including the import's action, `library.convert`.
|
||||
No role holds them, no agent can request them, and the hard-wait list in
|
||||
REQ-WAIT-1 doesn't change. In an organization with several humans, who may
|
||||
start them is a setting only a human changes, and the default is the owner
|
||||
only.
|
||||
- Conversion is one way, with no parallel run. Neither the Console nor any agent
|
||||
edits a converted story file as Markdown, and no DOCX edit syncs back to
|
||||
Markdown. The Console's Markdown save refuses every file under the archive
|
||||
folder. The user's own tools can still change the archived Markdown, and no
|
||||
such change reaches the DOCX. The old Markdown stays in git history, and
|
||||
nothing rewrites history. A promotion into a book folder (C2-19) runs the
|
||||
import for a Markdown story file or is refused. A promotion of a playground
|
||||
DOCX is refused in version 1.17.
|
||||
- Agents get a format map: for each mapped folder and story file, its format,
|
||||
scope and where it lives, archived files, flagged files and pending imports
|
||||
included.
|
||||
- The import and new story files ship closed. They open only after four things.
|
||||
The editor ships. The human has set the writing store's remote, and the stack
|
||||
has checked that it can push there. Agents can read committed DOCX story files
|
||||
through the lookups and cite them (REQ-LIB-5, REQ-LIB-6). Any earlier agent
|
||||
system that writes the user's files has stopped, which for Case 2 is the
|
||||
Hermes retirement (#423 step 5). So the import itself never cuts agents off
|
||||
from a chapter, the chapter's current text never lives only on the stack's
|
||||
host, and no second system writes the story. The first import of a real user's
|
||||
books waits for Jason's go.
|
||||
(Q235, Q236, Q237: P1 to P6. Q222 for the setting. O2 design D10 to D12.
|
||||
Version 1.17.)"
|
||||
|
||||
### New REQ-LIB-9
|
||||
|
||||
This bullet goes after REQ-LIB-8.
|
||||
|
||||
"- **REQ-LIB-9.** The Console's DOCX editor. The human edits a DOCX story file in
|
||||
the Console's editor, in a split pane with the agent chat alongside it. The
|
||||
human can pin an agent chat to a document. The human can select a word,
|
||||
right-click it, and choose a synonym from a thesaurus. Only a human saves. Saves
|
||||
happen on their own while the human types (Q226), at intervals only a human
|
||||
changes, and each accepted save is one commit with the human as author, pushed
|
||||
as REQ-LIB-4 says. A save made against an older version than the current one is
|
||||
refused. Agents never write a DOCX. Versioning is the editor's tracked changes
|
||||
plus one git commit per save. The editor's code is vendored and pinned, with a
|
||||
hash per file and the licenses that its independent security review accepted. At
|
||||
runtime the editor loads only files that the stack itself serves, and it
|
||||
requests nothing from any other host. The editor's frame sends exactly the
|
||||
content security policy Jason approved in Q234, and the WebUI shell's own header
|
||||
is unchanged. REQ-PROJ-3's project docs editor is unchanged. (Q195, Q226, Q234,
|
||||
Q236. O2 design D3, D4, D6, D8, D10, D13, D14, D16 and D22. Version 1.17.)"
|
||||
|
||||
### Version 1.17 notes
|
||||
|
||||
Added after the 1.16 notes:
|
||||
|
||||
"- Version 1.17:
|
||||
- Jason confirmed the reading 'for now (subject to future change)' (Q237). The
|
||||
rulings stand until he revises them.
|
||||
- The 10-27 Case 2 release ships on Markdown as built (P6). The 1.11 line 'The
|
||||
Case 2 MVP for 10-27 is the Markdown library (#416). The WYSIWYG editor (Q195,
|
||||
Q196) is an add-on that doesn't gate it.' still holds. Conversion opens after
|
||||
10-27, and only once REQ-LIB-8's four conditions are met. The editor ships,
|
||||
the writing store's remote is set and the stack has checked that it can push
|
||||
there, agents can read and cite committed DOCX story files through the
|
||||
lookups, and Hermes is retired, which is #423 step 5.
|
||||
- The 1.12 note says five bus write and Console editor rows, C2-02, C2-03b,
|
||||
C2-11a, C2-11b and C2-19, don't gate 10-27. Jason's Q226 F42 later moved
|
||||
C2-02, C2-03b and C2-11b into the 10-27 slice. The Case 2 design brings C2-11a
|
||||
in with them, because C2-11b needs it. C2-19 stays outside the slice. Mos's
|
||||
reading of F42 is that C2-02 and C2-03b now gate 10-27, because design 9.2
|
||||
puts them in 'the smallest slice that lets her start'. C2-11a and C2-11b are
|
||||
slice rows that still don't gate 10-27, because the editor is the Q195 add-on.
|
||||
The DOCX change in 1.17 leaves this slice as it is, and C2-11b's Markdown
|
||||
editor never edits a converted story file.
|
||||
- Q236 narrows Q176's editor choice to DOCX for story files (REQ-LIB-9). The
|
||||
1.12 note that the Case 2 design keeps Obsidian now holds only for Markdown
|
||||
files, including story files not yet imported. A DOCX story file is edited
|
||||
only in the Console's editor under REQ-LIB-9.
|
||||
- This replaces two parts of the O2 design at 25c09f19f. The superseded-notice
|
||||
proposal from Fenton's F1 in D12 is withdrawn, and the
|
||||
keep-Markdown-at-its-path model of D11 is replaced. Gyro's rewrite of the O2
|
||||
design for Vikunja 571, landed on `refactor` at b99ca86b9, carries the
|
||||
details, and where it differs from this text, this text holds:
|
||||
- the import and archive steps, the mark, and the new bus step;
|
||||
- links from supporting files to a moved chapter;
|
||||
- the DOCX reader;
|
||||
- the user's unpushed or later changes to an archived file or at its old path,
|
||||
and pushing and closing the file in Obsidian before each import;
|
||||
- the writing store's remote in W4 section 7.3, which replaces the no-remote
|
||||
and no-backup rule that W4 section 7.4 held before the rewrite, at
|
||||
062646487;
|
||||
- import events counted by a bus restore, and recovery when a bus restore or a
|
||||
restore of the whole host predates an import;
|
||||
- manifest order for the manuscript lookup and its next chapter;
|
||||
- `library.convert` kept out of `ACTIONS`, out of the hard-wait note and out
|
||||
of the pinned `GATED_ONLY` list in packages/business/src/vocabulary.mjs.
|
||||
- Agents read a converted story file through the Case 2 lookups under the same
|
||||
project and universe access as Markdown canon, in any thread or room. W4's
|
||||
earlier O2-only conversations, per-document grants and per-send sharing don't
|
||||
gate these lookups. The 571 rewrite replaced them in W4 sections 7.1 and 7.2.
|
||||
The creative tool list in REQ-LIB-6 doesn't change.
|
||||
- The six creative role contracts row 213 landed say 'Every item cites the
|
||||
lines of the user's material it is about'. A build row under 571 changes that
|
||||
line for DOCX story files (REQ-LIB-6).
|
||||
- A new Markdown file in a book folder is still canon in 1.17. The 1.17
|
||||
exception is a file at a converted file's old path, which REQ-LIB-2 makes
|
||||
unmapped.
|
||||
- Story files convert whether canon or playground. Type-folder and supporting
|
||||
files stay Markdown.
|
||||
- Measured 2026-10-23 at about 01:21Z: the Case 2 user's library repository
|
||||
tracks 213 files, 203 of them Markdown, and none is a .docx or .doc. It has no
|
||||
untracked files. The user's .doc files (Q196) are outside it. So the landed
|
||||
code has no DOCX there to report as unreadable at 10-27.
|
||||
- Text replaced in 1.17, kept here as in earlier notes:
|
||||
- Creative library lead, the 1.16 sentence: "The library is the user's own
|
||||
writing repository."
|
||||
- REQ-LIB-2, the 1.16 sentences: "A file's scope comes from its path alone,
|
||||
prefixes first: a Markdown file under a supporting prefix is supporting,
|
||||
one under a playground prefix at any level is playground, any other
|
||||
Markdown file in a book folder is canon, and everything else is unmapped
|
||||
and never returned. A folder in both lists is supporting." "Lookups return
|
||||
canon unless the caller asks for playground or supporting." "so an agent
|
||||
can't cite a playground or supporting file as canon" "The Console shows
|
||||
each cite's scope."
|
||||
- REQ-LIB-4, the 1.16 sentences: "The user commits and pushes with their own
|
||||
tools, today Obsidian with obsidian-git." "Agents read only the mirror's
|
||||
served commit and never hold a copy of their own, so they see the user's
|
||||
push within about a minute."
|
||||
- REQ-LIB-5, the 1.16 sentence: "The user sees them in Obsidian's graph and
|
||||
backlinks." A converted chapter's connections show in Obsidian only as they
|
||||
stood at conversion, and the Console's Connections view shows their
|
||||
current links.
|
||||
- REQ-LIB-6, the 1.16 sentence: "A reply may cite lines of the repository at
|
||||
a commit or pages the agent fetched, and the broker checks every citation
|
||||
before it stores the message."
|
||||
- REQ-LIB-1 only gains sentences.
|
||||
- REQ-LIB-3's text and REQ-LIB-7 are unchanged. REQ-LIB-8 adds one form of
|
||||
exclusion REQ-LIB-3 doesn't have: naming a DOCX document by its id. A path
|
||||
exclusion also covers DOCX documents, through a converted file's old path or a
|
||||
whole book folder. An archived file and a DOCX document keep a universe, and a
|
||||
book where their source has one, so every lookup still names one universe.
|
||||
REQ-PROJ-3, the Console editor for a project's spec and docs, is unchanged.
|
||||
C2-11b opens the library's Markdown files in that same editor.
|
||||
- In `packages/tasks/requirements.json` the titles of REQ-LIB-1, REQ-LIB-2 and
|
||||
REQ-LIB-4 change, and those of REQ-LIB-5 and REQ-LIB-6 stay. REQ-LIB-1 becomes
|
||||
'A human-set library maps the user's repository onto universes, books, types,
|
||||
playgrounds, supporting and archive folders'. REQ-LIB-2 becomes 'Scope comes
|
||||
from the path or a DOCX's source, lookups default to canon, and only canon
|
||||
files are ever cited as canon'. REQ-LIB-4 becomes 'Git holds every change. The
|
||||
bus fast-forwards a read-only mirror every 60 seconds, the one place agents
|
||||
read Markdown'. REQ-LIB-8 and REQ-LIB-9 add two ids, so the count goes from 85
|
||||
to 87. REQ-LIB-8 is titled 'Case 2 story files become DOCX by a human-only
|
||||
one-way import or in the editor; Markdown stays for notes and docs', and
|
||||
REQ-LIB-9 'The Console's DOCX editor: only a human saves, each save is one
|
||||
commit, and the editor code is vendored and pinned'.
|
||||
- Open points O1 to O4 of draft 1 were ruled by Mos in mos-1367 B1 to B4.
|
||||
- Sage files the build rows under Vikunja 570 and 571 after the rewrite is
|
||||
approved. This amendment adds the requirements only."
|
||||
|
||||
### Risks
|
||||
|
||||
The new lines go at the end of "## Risks and open questions", after the bullet
|
||||
that begins "Trusted mode lets an agent" and before the "## Milestones" heading.
|
||||
- "A DOCX file is a zip archive, so git can't show a line diff of a chapter.
|
||||
Review of a story change goes through the editor and the committed reader,
|
||||
not `git diff`. (Version 1.17.)"
|
||||
- "A converted chapter's DOCX is not in Obsidian's graph. Obsidian shows the
|
||||
chapter only through its archived Markdown, and no DOCX edit reaches that file's
|
||||
links. The DOCX's current links show in the Console's editor and its
|
||||
Connections view, and no graph shows them yet. (Version 1.17.)"
|
||||
- "Conversion can't be undone in place. Front matter, wiki link forms and
|
||||
unsupported blocks may not survive it, and the mirror can't refuse the user's
|
||||
push to an archived file. The controls are the fidelity report, the human's
|
||||
confirmation, an untouched original on refusal, and the kept archive.
|
||||
(Version 1.17.)"
|
||||
- "A restore of the stack's bus alone from a backup taken before an import drops
|
||||
the import's logged events, while the archive commit and the DOCX stay. A
|
||||
restore of the stack's whole host from such a backup also takes the writing
|
||||
store and the host's import records back to before the import. The archive
|
||||
commit stays in the user's Gitea repository and the DOCX stays on the writing
|
||||
store's remote, but the host keeps no record of the import. A bus restore counts
|
||||
import events, and the import's recovery covers both cases (Vikunja 571).
|
||||
(Version 1.17.)"
|
||||
|
||||
## d9 changes (Q239 and Webby round 5)
|
||||
|
||||
- J-b yes (Q239, mos-1467), in the five edits mos-1443 R3 set for a yes (Webby round 6 B1). R3.1: REQ-LIB-5's replacement gains the Connections view sentence after "no graph shows them yet.". R3.2: the graph Risks line ends with the editor and the Connections view. R3.3: the A6 line moves from "Not taken" under A6. R3.5: the REQ-LIB-5 note ends with the Connections view, which drops "until a Console view of their links exists" and "Q226 records that view as a later option". R3.4 is Gyro's design edit at 767bb62c7 (writing-editor.md:188-190, :240-244 and :1272, docx-citations.md:66), and it follows in his docs round. Beyond these and N21, nothing else in the PRD text changes.
|
||||
- N19 and N20 (Webby round 6): J-b's question text is in the past tense, and the line that the import doesn't wait for the view is cited as Mos's R3 default. Mos confirmed it at his J-b check (mos-1470).
|
||||
- N21 (Webby round 6, Mos yes in mos-1470): REQ-LIB-5's version tag cites Q239 for the Connections view beside Q222 for the two settings.
|
||||
- N15: the D7 landing line gives R8's wording beside the impact map's quote.
|
||||
- N16: the d7 changes line notes that R8 replaces "no agent credential reaches it" in d8.
|
||||
- N17: the REQ-LIB-2 Replace quote and the REQ-LIB-8 old-path bullet are rewrapped under 82 columns. Their words are unchanged.
|
||||
|
||||
## d8 changes (Mos's d8 fix list, check wf_63bba879-180)
|
||||
|
||||
- R1: REQ-LIB-4's tail covers DOCX saves, which agents see at their next lookup after the editor reports them as committed. The notes quote the whole 1.16 sentence. Source: nit/jason/F10.
|
||||
- R2: REQ-LIB-4's new title says the mirror is "the one place agents read Markdown". Source: decisions.n14.
|
||||
- R3: J-b asks whether 1.17 requires the Console Connections view, with Mos's recommendation of yes. Sources: mos1372/F9, jason/F2, anchors/F5.
|
||||
- R4: C2-02 and C2-03b gate 10-27, C2-11a and C2-11b are slice rows that don't, and "F42 wins" is gone. New J-k. Sources: anchors/F6, jason/F5.
|
||||
- R5: where the landed 571 design differs, this text holds. Sources: anchors/F8, adversarial/F8 item 1, jason/F8, critic/C6, G16's design follow-on.
|
||||
- R6: `library.convert` stays out of `ACTIONS`, and REQ-WAIT-1's hard-wait list doesn't change. Sources: gyro571/F1, adversarial/F8 item 3, critic/C6.
|
||||
- R7: a repository on the stack's host never counts as the writing store's remote, and Item 2 says so. Sources: mos1372/F2, adversarial/F1, gyro571/F2.
|
||||
- R8: "no agent credential reaches it" becomes "no agent can reach it with any credential". Source: Fenton's B1 under mos-1379.
|
||||
- R9: no playground DOCX becomes canon in 1.17, and a promotion of one is refused. New J-i. Sources: mos1372/F8, adversarial/F2.
|
||||
- R10: REQ-LIB-9 restates Q195's pin and right-click thesaurus, and its cite gains D4 and D22. Source: critic/C4.
|
||||
- R11: REQ-LIB-9's license bar is the licenses its independent security review accepted. New J-l. Source: critic/C3.
|
||||
- R12: no text for D16's narrow-window tabs. Mos records that reading in STATE. Sources: jason/F1, second verifier vote.
|
||||
- R13: no lookup returns a file's archived copy before publication. Source: adversarial/F5 part 3.
|
||||
- R14: a restore of the whole host from a backup taken before an import is a fourth recovery case. Source: adversarial/F6.
|
||||
- M1: REQ-LIB-4's writing-store sentences follow the later bus rows sentence and add the off-host remote rules and DOCX read timing. The note quotes both 1.16 sentences. Sources: mos1372/F2, adversarial/F1, gyro571/F2, anchors/F4, nit/jason/F10, adversarial/F5 part 1, Fenton B1.
|
||||
- M2: REQ-LIB-8's order bullet adds the remote's confirmation, the old-path read rule and "archive push". Sources: mos1372/F2 and the other remote findings, mos1372/F7, adversarial/F5 parts 2 to 4.
|
||||
- M3: REQ-LIB-8's gate bullet has four conditions, adding the remote. Sources: the remote findings, critic/C1 and C2 edit 2.
|
||||
- M4: the 1.17 note on when conversion opens restates the four conditions and drops W4b. Sources: jason/F5, the remote findings.
|
||||
- M5: Item 2 rewritten with the remote rules. Sources: the remote findings.
|
||||
- M6: the 1.12 gating note rewritten. Sources: anchors/F6, jason/F5.
|
||||
- M7: REQ-LIB-9 gains the split pane, the pin, the thesaurus, the license bar and the runtime load rule, and cites more of the design. Sources: jason/F1, critic/C4, critic/C3, nit/anchors/F12, nit/gyro571/F9.
|
||||
- M8: REQ-LIB-2, the one-way bullet and the C1 note keep a playground DOCX out of canon in 1.17. Sources: mos1372/F8, adversarial/F2.
|
||||
- M9: REQ-LIB-5 says what the reader can't read, and the import turns web links into plain text. New J-j. Sources: critic/C1, critic/C2.
|
||||
- M10: an exclusion of a whole book folder covers that book's DOCX documents. Source: adversarial/F3, with B5.
|
||||
- M11: the human-verbs sentence and the 571 list item name `library.convert` and REQ-WAIT-1. Sources: gyro571/F1, adversarial/F8 item 3, critic/C6.
|
||||
- M12: J-b rewritten. Sources: mos1372/F9, jason/F2, anchors/F5, gyro571/F3, critic/C6.
|
||||
- M13: J-d puts the agent read model to Jason as his call. Source: mos1372/F1.
|
||||
- M14: Status moves to version 1.17 and names REQ-LIB-9. Sources: anchors/F2, mos1372/F4, critic/C7.
|
||||
- M15: the 1.17 source line quotes Q235 in full (mos-1446) and the REQ-LIB-9 sources, at b99ca86b9. Sources: critic/C5, anchors/F9, jason/F8 pin part, nit/gyro571/F8, critic/C6 pin part.
|
||||
- M16: the 571 list lead names the two replaced design parts and the landed design. Sources: anchors/F9, anchors/F8, adversarial/F8 item 1, jason/F8, critic/C6.
|
||||
- Landing swap: Gyro's docs round for R5, R6 and R14 landed as b99ca86b9 (Fenton fenton-0015), so M15 and M16 cite b99ca86b9 in place of 767bb62c7, as the d8 fix list's header says. J-b keeps 767bb62c7 for its line numbers.
|
||||
- M17: W4 section numbers in the remote item and the lookups note. Sources: gyro571/F5, nit/adversarial/F11.
|
||||
- M18: the D7 landing line. Sources: gyro571/F5, the remote findings.
|
||||
- M19: a logged event for each import stage, in REQ-LIB-8 and A2. Source: gyro571/F7.
|
||||
- M20: the Risks line on restores adds the whole-host case. Sources: adversarial/F6, gyro571/F7.
|
||||
- M21: the 571 restore item adds the whole-host case. Source: adversarial/F6.
|
||||
- M22: the graph Risks line and the REQ-LIB-5 note name a Console view. Sources: anchors/F5, jason/F2.
|
||||
- M23: the d6 B3 line drops "F42 wins". Source: anchors/F6.
|
||||
- M24: new Markdown in a converted book, with the old-path and playground exceptions. Source: mos1372/F6.
|
||||
- M25: J-c's last sentence is Mos's reading. Source: mos1372/F10.
|
||||
- M26: J-h rewritten with the D13 defaults. Source: jason/F3.
|
||||
- M27: Item 4 rewritten. Sources: jason/F4, gyro571/F4, decisions.unmarked_folders.
|
||||
- M28: the "Not taken" note on unmarked folders. Sources: jason/F4, gyro571/F4, decisions.unmarked_folders.
|
||||
- M29: the import refuses an unmapped file. Sources: gyro571/F4, decisions.unmarked_folders.
|
||||
- M30: placement notes for REQ-LIB-8, REQ-LIB-9 and the Risks lines. Sources: anchors/F3, nit/anchors/F14.
|
||||
- M31: REQ-LIB-4's title and the D11 landing line. Source: decisions.n14.
|
||||
- M32: the Q236 note limits the 1.12 Obsidian note to Markdown files. Source: critic/C8.
|
||||
- M33: the exclusion note adds path exclusions of DOCX documents. Source: anchors/F7.
|
||||
- M34: A9 moves from "Mine" to the impact-map list. Source: decisions.exclusion_by_id.
|
||||
- M35: the D5 landing line. Sources: anchors/F10, mos1372/F5, nit/adversarial/F12.
|
||||
- M36: the D9 and D14 landing lines. Source: mos1372/F1.
|
||||
- M37: the O1 and C3 notes add the off-host remote and its confirmation. Sources: the remote findings.
|
||||
- M38: the REQ-PROJ-3 note. Source: nit/anchors/F11.
|
||||
- M39: REQ-LIB-5's freshness sentence keys on the editor reporting a save as committed. Source: nit/adversarial/F9.
|
||||
- M40: REQ-LIB-6 bars agent requests for a conversion and allows a suggestion in chat. Source: nit/jason/F9.
|
||||
- M41: REQ-LIB-2's opening and old-path rule, and the "Mine" note. Source: nit/adversarial/F10.
|
||||
- M42: REQ-LIB-8's lead sentence on Markdown story files. Source: nit/mos1372/F12.
|
||||
- M43: REQ-LIB-6's citation sentence. Source: nit/anchors/F13.
|
||||
- M44: REQ-LIB-5's version tag uses plain quotes. Source: nit/anchors/F15.
|
||||
- M45: reference codes B5, D12, D10 and B4 name their source. Source: nit/mos1372/F11.
|
||||
- Files the row would own: four files, with the id count going to 87. Sources: anchors/F1, adversarial/F4, decisions.n14 F4.
|
||||
|
||||
## d7 changes (mos-1379, Fenton's 571 B2)
|
||||
|
||||
- REQ-LIB-5 gains Mos's three sentences after the extracted-text sentence. The text agents read has tracked changes accepted. Comments, and text with tracked deletions shown, each wait for a setting only a human turns on, off by default. Its version tag cites Q222 for the two settings, which is the only text here beyond Mos's.
|
||||
- J-d gains Mos's sentence on comments and tracked deletions.
|
||||
- Fenton's B1 is design only. d6's "no agent credential reaches it" holds (R8 replaces it in d8), and the shared helper stays #431.
|
||||
|
||||
## d6 changes (Webby round 3, webby-0207)
|
||||
|
||||
- B1: titles for REQ-LIB-8 and REQ-LIB-9 in the requirements.json note and the file list. REQ-LIB-9 takes Webby's title. REQ-LIB-8 keeps mine, because Webby's ("...one-way import that archives the Markdown under its old path") leaves out D2's story files started in the editor.
|
||||
- B2: REQ-LIB-2 makes a Markdown file at a converted file's old path unmapped, ahead of the playground and canon rules, so a playground conversion is covered too. REQ-LIB-8's old-path bullet says "neither canon nor playground".
|
||||
- B3: the slice note names Q226 F42's three rows (C2-02, C2-03b, C2-11b). C2-11a joined the slice with C2-11b, because C2-11b needs it. Both are slice rows that don't gate 10-27, as design 9.2, case2-rows.md:1655 and the impact map's A57 say. The 'F42 wins' sentence is gone from the note in d8, so no queue change follows from it.
|
||||
- N10: the cite-scope replacement is scoped to REQ-LIB-2. The 1.14 note's copy stays.
|
||||
- N11: the notes say REQ-LIB-8 adds the by-id exclusion form REQ-LIB-3 doesn't have.
|
||||
- N12: J-h marks the autosave intervals for Jason.
|
||||
- N13: J-b names REQ-LIB-5's "Connections are always visible" tag and Q226's "another way to handle the visual brain map".
|
||||
- N14, not taken: REQ-LIB-4's title stays internal F3's text, as mos-1372 D11 ruled. Webby's alternative, "...and agents read Markdown from it alone" (120 chars), is Mos's call.
|
||||
|
||||
## Where each mos-1372 item landed
|
||||
|
||||
- D1: REQ-LIB-2's DOCX sentences, the REQ-LIB-8 lead and import bullet, C1 above, and the notes. "Playground files stay Markdown in 1.17" is gone.
|
||||
- D2: REQ-LIB-2's "for one the human starts in the editor" and the REQ-LIB-8 new-story-file bullet. The universe-and-book note is now true.
|
||||
- D3: the REQ-LIB-8 order bullet. This also settles N8.
|
||||
- D4: REQ-LIB-8's one-way bullet, which carries the archive refusal and consequence F2's text, and the 571 list item.
|
||||
- D5: it landed in REQ-LIB-2's "a Markdown file at a converted file's old path is unmapped" in Webby's B2 form, and in the REQ-LIB-8 old-path bullet.
|
||||
- D6: REQ-LIB-5's freshness sentences.
|
||||
- D7 and mos-1375: REQ-LIB-4's remote sentences, the remote step in REQ-LIB-8's order bullet, the fourth opening condition and the 1.17 note on when conversion opens, plus the 571 list item on W4 section 7.3, which replaces the no-remote rule of the old W4 section 7.4. The impact map's item 2 adds "only the stack's host pushes, and no agent credential reaches it", which R8 words in d8 as "no agent can reach it with any credential".
|
||||
- D8: the REQ-LIB-5 replacement, the import bullet's wiki-link sentence, the Risks line, the note quoting the old sentence, and "REQ-LIB-1 only gains sentences".
|
||||
- D9: the 1.17 note that agents read a converted story file through the Case 2 lookups, from the d4 check's consistency B2, and J-d, which puts the read model to Jason as his call.
|
||||
- D10: the d4 check's B5 sentence, in REQ-LIB-8.
|
||||
- D11: internal F3's clause in the REQ-LIB-4 section, and in the notes the title as Mos amended it for N14.
|
||||
- D12: the whole 1.16 sentence replaced, and quoted in full in the notes.
|
||||
- D13: consequence F1's text, in REQ-LIB-4 and REQ-LIB-8's first bullet.
|
||||
- D14: the row text from the d4 check's consistency B1, in the mark-and-move bullet and C3 above.
|
||||
|
||||
## What d5 adds beyond mos-1372 (for Webby and Mos to check)
|
||||
|
||||
From the impact map, folded as mos-1368 item 1 asked:
|
||||
- A1: the Console shows a cite's format and converted state (REQ-LIB-2).
|
||||
- A2: the logged events and the several-humans setting, default owner only, under Q222.
|
||||
- A3: the import is outside the vocabulary, the hard-wait list is unchanged, and the archive move isn't `data.delete`. The C6 code site is in the 571 list.
|
||||
- A4: the section lead and REQ-LIB-1's writing-store sentence.
|
||||
- A6: format and location in every answer, extracted text with DOCX locators, links across formats.
|
||||
- A6's named chapter-links view. D8 took B8's "no graph shows them yet", and J-b put the view to Jason. His O2 (Q239) takes it in d9 as REQ-LIB-5's Connections view (mos-1443 R3).
|
||||
- A7: read-only extends to the store and the archive, no agent action on a story file in any format, no agent DOCX write, and role contracts state the formats. C1's role contract line is a 571 build item.
|
||||
- A8: the DOCX cite shape, and a cite naming an archived file as current is refused.
|
||||
- A9: a hybrid sees a source's DOCX canon through REQ-LIB-2's source scope, and REQ-LIB-8 lets an exclusion name a DOCX document by its id. The id form lets the human exclude a single DOCX started in the editor, which has no old path. An archived file isn't canon, so no hybrid sees it.
|
||||
- A10: REQ-LIB-9, so the count is 87, not 86.
|
||||
- A11: the Hermes gate, from item 3.
|
||||
- A12: the C2-19 promotion sentence.
|
||||
- A13: the broker's path check treats the archive folder as story.
|
||||
- A14 and C5: two Risks lines.
|
||||
- A15: the flagged-new-file bullet. It sits beside the note from the d4 check's B4, "a new Markdown file in a book folder is still canon". The file stays canon and is offered for import.
|
||||
- A16: three titles change and two stay.
|
||||
- C2: manifest order for the manuscript lookup.
|
||||
- F3: the C2-02 gating note.
|
||||
- F4: Mos's measurement.
|
||||
|
||||
Mine, beyond either source:
|
||||
- REQ-LIB-2 opens "A Markdown file's scope comes from its path and from past imports". DOCX scope comes from the source, so "a file's" would be false.
|
||||
- REQ-LIB-9's autosave sentence. It is Mos's reading of F41's "instantly committed", with human-set intervals. The exact intervals stay in the design.
|
||||
|
||||
Not taken:
|
||||
- Impact map item 4's "files in unmarked folders are eligible one at a time", for folders outside every book folder and playground only. Their files are unmapped under REQ-LIB-2, so a DOCX made from one would have no universe or book to keep. The human maps such a folder into a book folder or a playground first, under REQ-LIB-1. A file in an unmarked folder inside a book folder or a playground is canon or playground, and the import takes it one file at a time.
|
||||
|
||||
## Marked for Jason (additions in the text he sees)
|
||||
|
||||
- J-a. Whether "We will not run the systems in parallel" applies per chapter or as one switch per book. His call. As written, it applies per chapter.
|
||||
- J-b. A converted chapter's DOCX is not in Obsidian's graph. Obsidian shows the chapter only through its archived Markdown, and no DOCX edit reaches that file's links. REQ-LIB-5 still cites two TARGET lines, "Visualization of data via Obsidian or equivalent is required" and "Connections are always visible". Mos's reading was that 1.17 as d8 wrote it met neither line for converted chapters. Their current links showed to the user only as clickable text inside the editor, and no graph or list showed them. Q226 leaves room for "another way to handle the visual brain map". The O2 design that landed for Vikunja 571 at 767bb62c7 held a Console Connections view and its tests as later work that waited for this answer. His call was whether 1.17 requires that view. As d8 was written, it did not. Mos recommended that it does, because impact map A6 says "Recommended: name the view" and TARGET says "Connections are always visible". Jason answered O2 (Q239): 1.17 requires the view. d9 writes it into REQ-LIB-5, its note and the Risks line, in the text of mos-1443 R3. The view comes with the conversion work after 10-27, and the 10-27 Case 2 release stays on Markdown (Q235 P6, Q237). That the import does not wait for the view is Mos's default in mos-1443 R3. Q239 doesn't address it, and Mos confirmed it at his J-b check (mos-1470).
|
||||
- J-c (mos-1374): "The store pushes to a remote (D7), and the Case 2 VM is in the nightly PBS backup. For a fixed restore point before her first real import, a manual backup of VM 1012 with PBS protect runs first. That step is reversible and needs no reboot." Mos's reading is that this sits inside the P5 go step, so it needs no new question.
|
||||
- J-d. How agents read a converted chapter, D9. His call. As written, an agent reads it through the ordinary Case 2 lookups in any thread or room, under the same project and universe access as Markdown canon. This is Mos's reading of F41 and REQ-LIB-7. The other choice is the model in the earlier O2 design, W4 sections 7.1 and 7.2. Under it, an agent reads a converted chapter only in a new writing conversation that holds one human and one agent, only after the business owner grants that agent access to that document, and by default only what the human attaches to each message. Comments and tracked deletions stay hidden from agents until a human turns on a setting for each. Markdown has neither, so the default keeps agents at the access Markdown gives them. Those two sentences are Mos's ruling mos-1379. A yes to the text as written settles it.
|
||||
- J-e. Versioning means the editor's tracked changes plus one git commit per save.
|
||||
- J-f. A converted chapter can be edited only in the Console while the stack is reachable. Obsidian no longer edits it offline.
|
||||
- J-g. The import takes Markdown only. Importing .doc files stays a separate task.
|
||||
- J-h. Autosave under REQ-LIB-9. Jason's Q226 F41 says "All changes are instantly committed and available to the agents." Mos's reading, recorded with that ruling, is "Each save is a commit at once, and agents read it at their next lookup." At the O2 design's D13 defaults, a save commits at most 2 seconds after a pause in typing or 15 seconds into steady typing, with no human action needed. Mos's reading in mos-1359 counts that delay as "instantly committed". Only a human changes the intervals. The PRD names no values, and the defaults stay in the design. Whether a delay of seconds fits his word "instantly" is his call, so this is marked for him.
|
||||
- J-i. In 1.17 a playground story that is DOCX can't become canon. That covers a playground Markdown file the human imports, a story the human starts in a playground in the editor, and the DOCX of a draft universe after it is made real. Today Melanie promotes a playground file by moving it into a book folder in Obsidian, and a DOCX in the writing store can't be moved that way. She can start a new story in the book and paste the text in, but it gets a new document id and the playground copy keeps the history. A later version adds DOCX promotion with C2-19, which comes after the slice.
|
||||
- J-j. Web links, text boxes and fields in a converted chapter. Jason's Q226 F41 says "All changes are instantly committed and available to the agents." The reader in this version can't read a web link, a text box, a field or hidden text. As written, the import turns each web link into plain text that keeps its label and address, and the human confirms that in the fidelity report. A web link that can't be kept safely as plain text refuses the import. If a later edit adds one of these, the save keeps it and the editor warns the human. Agents then get none of that part's text until a save takes it out.
|
||||
- J-k. The 1.12 note Jason approved says bus writes, C2-02 and C2-03b, don't gate 10-27. His Q226 "F42: ok" moved them into the 10-27 slice, and design 9.2 counts them in the smallest slice that lets Melanie start. Mos reads that as making them gate 10-27. The editor rows C2-11a and C2-11b still don't gate it, per his Q195 words "we need to get it moving as an add-on".
|
||||
- J-l. REQ-LIB-9 does not use REQ-PROJ-3's "permissive license" bar. It takes the licenses that the editor's independent security review accepted, because that review cleared fonts under the SIL Open Font License and the Ubuntu Font License, which not every reader counts as permissive. Mos's reading.
|
||||
|
||||
Marked as changes beyond P1 to P6, from the impact map (Q17, item 5):
|
||||
- A1 (archived scope and canon cites)
|
||||
- A2 (the conversion requirement, REQ-LIB-8)
|
||||
- A3 (outside the vocabulary, hard-wait list unchanged)
|
||||
- A6 (lookups across formats)
|
||||
- A7 (agent bounds)
|
||||
- A8 (DOCX cites)
|
||||
- Item 2 and mos-1375: each writing-store commit is pushed to a remote off the stack's host. Only a human sets it, it starts unset, and a repository on the stack's host doesn't count. Only the stack's host pushes to it, and no agent can reach it with any credential. Import and new story files stay closed until the remote is set and checked, and each import's archive push waits until the remote holds the DOCX commit. This keeps "Git is used for all changes -- local and remote" true for DOCX chapters.
|
||||
- Item 3 (the Hermes gate)
|
||||
- Item 4. Canon and playground story files convert one file at a time, including files in unmarked folders inside a book folder or a playground. Type-folder and supporting files stay Markdown. A story file outside every book folder and playground has no universe or book, so it converts only after a human maps its folder into a book folder or a playground. The mapping step is Mos's reading of P1 and Q30.
|
||||
|
||||
The F4 measurement goes with them.
|
||||
|
||||
## Files the row would own
|
||||
|
||||
The id count goes from 85 to 87. 1.16 owned three files because its count stayed at 85. Like 1.13, the last version that added ids, the row owns four files:
|
||||
- `docs/prd/mosaic-stack.md`
|
||||
- `packages/tasks/requirements.json`: the version 1.17, the two new ids REQ-LIB-8 and REQ-LIB-9 with their titles, and the changed titles of REQ-LIB-1, REQ-LIB-2 and REQ-LIB-4. The new ids go after REQ-LIB-7 and before REQ-INST-1, in the same order as the PRD's bullets.
|
||||
- `packages/tasks/tests/requirements.test.mjs`: the version pin 1.17, the id count of 87, and the duplicate-id message, which goes from `requirements[85].id: REQ-ROLE-1 twice` to `requirements[87].id: REQ-ROLE-1 twice`.
|
||||
- `packages/bus/tests/requirements.test.mjs`: the list length, which goes from 85 to 87.
|
||||
|
||||
The gate would match 1.16's:
|
||||
1. A reviewer approves the text.
|
||||
2. Jason says yes to the exact text through Mos.
|
||||
3. Both `requirements.test.mjs` files, in `packages/tasks/tests` and `packages/bus/tests`, and every `scripts/test-*.sh` that reads the PRD or `requirements.json` are green on Sage's gate.
|
||||
4. Mos gets the landing commit.
|
||||
@@ -0,0 +1,132 @@
|
||||
--- DRAFT-d8.md 2026-10-23 06:16:54.374694135 -0500
|
||||
+++ DRAFT-d9.md 2026-10-23 10:40:54.806553704 -0500
|
||||
@@ -1,6 +1,6 @@
|
||||
# PRD 1.17 draft: Case 2 story files are DOCX (Q235 to Q237, P1 to P6)
|
||||
|
||||
-Sage, 2026-10-23, for mos-1366 item 1 and Vikunja 570. Draft 8. It is d7 plus Mos's d8 fix list mos-1443 (see "d8 changes"). d7 is d6 plus Mos's ruling mos-1379 on Fenton's 571 B2 (see "d7 changes"). d6 is d5 with Webby's round 3 (webby-0207) fixed: B1 to B3 and N10 to N13, with N14 left for Mos (see "d6 changes"). d5 folds in:
|
||||
+Sage, 2026-10-23, for mos-1366 item 1 and Vikunja 570. Draft 9. It is d8 plus Jason's Q41 answer O2 (DECISIONS Q239, mos-1467), which makes J-b yes, Webby's round 5 N15 to N17 (webby-0210) and Webby's round 6 B1, N19 and N20 (webby-0212) (see "d9 changes"). d8 is d7 plus Mos's d8 fix list mos-1443 (see "d8 changes"). d7 is d6 plus Mos's ruling mos-1379 on Fenton's 571 B2 (see "d7 changes"). d6 is d5 with Webby's round 3 (webby-0207) fixed: B1 to B3 and N10 to N13, with N14 left for Mos (see "d6 changes"). d5 folds in:
|
||||
- Mos's d4 check ruling mos-1372 (D1 to D14; D15 was dropped).
|
||||
- mos-1374, which replaces J-c.
|
||||
- The impact map mos-1368 (jason-master-restart d183246, `projects/mosaic-mvp/case2-docx-impact-map.md`: A1 to A16, critic items C1 to C6, items 2 to 7).
|
||||
@@ -103,10 +103,11 @@
|
||||
|
||||
### REQ-LIB-2
|
||||
|
||||
-Replace "A file's scope comes from its path alone, prefixes first: a Markdown file
|
||||
-under a supporting prefix is supporting, one under a playground prefix at any level
|
||||
-is playground, any other Markdown file in a book folder is canon, and everything
|
||||
-else is unmapped and never returned. A folder in both lists is supporting." with:
|
||||
+Replace "A file's scope comes from its path alone, prefixes first: a Markdown
|
||||
+file under a supporting prefix is supporting, one under a playground prefix at
|
||||
+any level is playground, any other Markdown file in a book folder is canon, and
|
||||
+everything else is unmapped and never returned. A folder in both lists is
|
||||
+supporting." with:
|
||||
|
||||
"A Markdown file's scope comes from its path and from past imports, prefixes
|
||||
first: a Markdown file under the archive folder is archived, one under a
|
||||
@@ -191,10 +192,13 @@
|
||||
Replace "The user sees them in Obsidian's graph and backlinks." with "The user sees
|
||||
a Markdown file's links in Obsidian's graph and backlinks. A DOCX story file's
|
||||
wikilinks stay clickable in the Console's editor (O2 design D11), and no graph
|
||||
-shows them yet."
|
||||
+shows them yet. The Console's Connections view lists the current links and
|
||||
+backlinks of each story file the user may see, in both formats, with each one's
|
||||
+scope and format."
|
||||
|
||||
Version tag: 'and "Connections are always visible".)' becomes 'and "Connections
|
||||
-are always visible". Version 1.17, with Q222 for the two settings.)'
|
||||
+are always visible". Version 1.17, with Q222 for the two settings and Q239 for
|
||||
+the Connections view.)'
|
||||
|
||||
### REQ-LIB-6
|
||||
|
||||
@@ -273,8 +277,9 @@
|
||||
- Each stage of an import writes a logged event. The import's record on the
|
||||
stack's host names the source file's path, commit and blob and the new
|
||||
document id, and each event points to that record.
|
||||
- - After a conversion, a Markdown file at the converted file's old path is neither
|
||||
- canon nor playground. Lookups leave it out, the format map flags it, and the human is told.
|
||||
+ - After a conversion, a Markdown file at the converted file's old path is
|
||||
+ neither canon nor playground. Lookups leave it out, the format map flags it,
|
||||
+ and the human is told.
|
||||
- A new Markdown story file in a book folder that holds converted files stays
|
||||
canon until its import, unless it sits at a converted file's old path or under
|
||||
a playground prefix. The Console flags it and offers the import. Nothing
|
||||
@@ -413,8 +418,8 @@
|
||||
push within about a minute."
|
||||
- REQ-LIB-5, the 1.16 sentence: "The user sees them in Obsidian's graph and
|
||||
backlinks." A converted chapter's connections show in Obsidian only as they
|
||||
- stood at conversion, until a Console view of their links exists. Q226
|
||||
- records that view as a later option.
|
||||
+ stood at conversion, and the Console's Connections view shows their
|
||||
+ current links.
|
||||
- REQ-LIB-6, the 1.16 sentence: "A reply may cite lines of the repository at
|
||||
a commit or pages the agent fetched, and the broker checks every citation
|
||||
before it stores the message."
|
||||
@@ -451,8 +456,8 @@
|
||||
not `git diff`. (Version 1.17.)"
|
||||
- "A converted chapter's DOCX is not in Obsidian's graph. Obsidian shows the
|
||||
chapter only through its archived Markdown, and no DOCX edit reaches that file's
|
||||
- links. The DOCX's current links show only in the Console's editor until a
|
||||
- Console view of them exists. (Version 1.17.)"
|
||||
+ links. The DOCX's current links show in the Console's editor and its
|
||||
+ Connections view, and no graph shows them yet. (Version 1.17.)"
|
||||
- "Conversion can't be undone in place. Front matter, wiki link forms and
|
||||
unsupported blocks may not survive it, and the mirror can't refuse the user's
|
||||
push to an archived file. The controls are the fidelity report, the human's
|
||||
@@ -467,6 +472,15 @@
|
||||
import events, and the import's recovery covers both cases (Vikunja 571).
|
||||
(Version 1.17.)"
|
||||
|
||||
+## d9 changes (Q239 and Webby round 5)
|
||||
+
|
||||
+- J-b yes (Q239, mos-1467), in the five edits mos-1443 R3 set for a yes (Webby round 6 B1). R3.1: REQ-LIB-5's replacement gains the Connections view sentence after "no graph shows them yet.". R3.2: the graph Risks line ends with the editor and the Connections view. R3.3: the A6 line moves from "Not taken" under A6. R3.5: the REQ-LIB-5 note ends with the Connections view, which drops "until a Console view of their links exists" and "Q226 records that view as a later option". R3.4 is Gyro's design edit at 767bb62c7 (writing-editor.md:188-190, :240-244 and :1272, docx-citations.md:66), and it follows in his docs round. Beyond these and N21, nothing else in the PRD text changes.
|
||||
+- N19 and N20 (Webby round 6): J-b's question text is in the past tense, and the line that the import doesn't wait for the view is cited as Mos's R3 default. Mos confirmed it at his J-b check (mos-1470).
|
||||
+- N21 (Webby round 6, Mos yes in mos-1470): REQ-LIB-5's version tag cites Q239 for the Connections view beside Q222 for the two settings.
|
||||
+- N15: the D7 landing line gives R8's wording beside the impact map's quote.
|
||||
+- N16: the d7 changes line notes that R8 replaces "no agent credential reaches it" in d8.
|
||||
+- N17: the REQ-LIB-2 Replace quote and the REQ-LIB-8 old-path bullet are rewrapped under 82 columns. Their words are unchanged.
|
||||
+
|
||||
## d8 changes (Mos's d8 fix list, check wf_63bba879-180)
|
||||
|
||||
- R1: REQ-LIB-4's tail covers DOCX saves, which agents see at their next lookup after the editor reports them as committed. The notes quote the whole 1.16 sentence. Source: nit/jason/F10.
|
||||
@@ -535,7 +549,7 @@
|
||||
|
||||
- REQ-LIB-5 gains Mos's three sentences after the extracted-text sentence. The text agents read has tracked changes accepted. Comments, and text with tracked deletions shown, each wait for a setting only a human turns on, off by default. Its version tag cites Q222 for the two settings, which is the only text here beyond Mos's.
|
||||
- J-d gains Mos's sentence on comments and tracked deletions.
|
||||
-- Fenton's B1 is design only. d6's "no agent credential reaches it" holds, and the shared helper stays #431.
|
||||
+- Fenton's B1 is design only. d6's "no agent credential reaches it" holds (R8 replaces it in d8), and the shared helper stays #431.
|
||||
|
||||
## d6 changes (Webby round 3, webby-0207)
|
||||
|
||||
@@ -556,7 +570,7 @@
|
||||
- D4: REQ-LIB-8's one-way bullet, which carries the archive refusal and consequence F2's text, and the 571 list item.
|
||||
- D5: it landed in REQ-LIB-2's "a Markdown file at a converted file's old path is unmapped" in Webby's B2 form, and in the REQ-LIB-8 old-path bullet.
|
||||
- D6: REQ-LIB-5's freshness sentences.
|
||||
-- D7 and mos-1375: REQ-LIB-4's remote sentences, the remote step in REQ-LIB-8's order bullet, the fourth opening condition and the 1.17 note on when conversion opens, plus the 571 list item on W4 section 7.3, which replaces the no-remote rule of the old W4 section 7.4. The impact map's item 2 adds "only the stack's host pushes, and no agent credential reaches it".
|
||||
+- D7 and mos-1375: REQ-LIB-4's remote sentences, the remote step in REQ-LIB-8's order bullet, the fourth opening condition and the 1.17 note on when conversion opens, plus the 571 list item on W4 section 7.3, which replaces the no-remote rule of the old W4 section 7.4. The impact map's item 2 adds "only the stack's host pushes, and no agent credential reaches it", which R8 words in d8 as "no agent can reach it with any credential".
|
||||
- D8: the REQ-LIB-5 replacement, the import bullet's wiki-link sentence, the Risks line, the note quoting the old sentence, and "REQ-LIB-1 only gains sentences".
|
||||
- D9: the 1.17 note that agents read a converted story file through the Case 2 lookups, from the d4 check's consistency B2, and J-d, which puts the read model to Jason as his call.
|
||||
- D10: the d4 check's B5 sentence, in REQ-LIB-8.
|
||||
@@ -573,6 +587,7 @@
|
||||
- A3: the import is outside the vocabulary, the hard-wait list is unchanged, and the archive move isn't `data.delete`. The C6 code site is in the 571 list.
|
||||
- A4: the section lead and REQ-LIB-1's writing-store sentence.
|
||||
- A6: format and location in every answer, extracted text with DOCX locators, links across formats.
|
||||
+ - A6's named chapter-links view. D8 took B8's "no graph shows them yet", and J-b put the view to Jason. His O2 (Q239) takes it in d9 as REQ-LIB-5's Connections view (mos-1443 R3).
|
||||
- A7: read-only extends to the store and the archive, no agent action on a story file in any format, no agent DOCX write, and role contracts state the formats. C1's role contract line is a 571 build item.
|
||||
- A8: the DOCX cite shape, and a cite naming an archived file as current is refused.
|
||||
- A9: a hybrid sees a source's DOCX canon through REQ-LIB-2's source scope, and REQ-LIB-8 lets an exclusion name a DOCX document by its id. The id form lets the human exclude a single DOCX started in the editor, which has no old path. An archived file isn't canon, so no hybrid sees it.
|
||||
@@ -593,12 +608,11 @@
|
||||
|
||||
Not taken:
|
||||
- Impact map item 4's "files in unmarked folders are eligible one at a time", for folders outside every book folder and playground only. Their files are unmapped under REQ-LIB-2, so a DOCX made from one would have no universe or book to keep. The human maps such a folder into a book folder or a playground first, under REQ-LIB-1. A file in an unmarked folder inside a book folder or a playground is canon or playground, and the import takes it one file at a time.
|
||||
-- A6's named chapter-links view. D8 takes B8's "no graph shows them yet", and J-b puts the view to Jason.
|
||||
|
||||
## Marked for Jason (additions in the text he sees)
|
||||
|
||||
- J-a. Whether "We will not run the systems in parallel" applies per chapter or as one switch per book. His call. As written, it applies per chapter.
|
||||
-- J-b. A converted chapter's DOCX is not in Obsidian's graph. Obsidian shows the chapter only through its archived Markdown, and no DOCX edit reaches that file's links. REQ-LIB-5 still cites two TARGET lines, "Visualization of data via Obsidian or equivalent is required" and "Connections are always visible". Mos's reading is that 1.17 as written meets neither line for converted chapters. Their current links show to the user only as clickable text inside the editor, and no graph or list shows them. Q226 leaves room for "another way to handle the visual brain map". The O2 design that landed for Vikunja 571 at 767bb62c7 holds a Console Connections view and its tests as later work that waits for this answer. His call whether 1.17 requires that view. As written, it does not. Mos recommends that it does, because impact map A6 says "Recommended: name the view" and TARGET says "Connections are always visible".
|
||||
+- J-b. A converted chapter's DOCX is not in Obsidian's graph. Obsidian shows the chapter only through its archived Markdown, and no DOCX edit reaches that file's links. REQ-LIB-5 still cites two TARGET lines, "Visualization of data via Obsidian or equivalent is required" and "Connections are always visible". Mos's reading was that 1.17 as d8 wrote it met neither line for converted chapters. Their current links showed to the user only as clickable text inside the editor, and no graph or list showed them. Q226 leaves room for "another way to handle the visual brain map". The O2 design that landed for Vikunja 571 at 767bb62c7 held a Console Connections view and its tests as later work that waited for this answer. His call was whether 1.17 requires that view. As d8 was written, it did not. Mos recommended that it does, because impact map A6 says "Recommended: name the view" and TARGET says "Connections are always visible". Jason answered O2 (Q239): 1.17 requires the view. d9 writes it into REQ-LIB-5, its note and the Risks line, in the text of mos-1443 R3. The view comes with the conversion work after 10-27, and the 10-27 Case 2 release stays on Markdown (Q235 P6, Q237). That the import does not wait for the view is Mos's default in mos-1443 R3. Q239 doesn't address it, and Mos confirmed it at his J-b check (mos-1470).
|
||||
- J-c (mos-1374): "The store pushes to a remote (D7), and the Case 2 VM is in the nightly PBS backup. For a fixed restore point before her first real import, a manual backup of VM 1012 with PBS protect runs first. That step is reversible and needs no reboot." Mos's reading is that this sits inside the P5 go step, so it needs no new question.
|
||||
- J-d. How agents read a converted chapter, D9. His call. As written, an agent reads it through the ordinary Case 2 lookups in any thread or room, under the same project and universe access as Markdown canon. This is Mos's reading of F41 and REQ-LIB-7. The other choice is the model in the earlier O2 design, W4 sections 7.1 and 7.2. Under it, an agent reads a converted chapter only in a new writing conversation that holds one human and one agent, only after the business owner grants that agent access to that document, and by default only what the human attaches to each message. Comments and tracked deletions stay hidden from agents until a human turns on a setting for each. Markdown has neither, so the default keeps agents at the access Markdown gives them. Those two sentences are Mos's ruling mos-1379. A yes to the text as written settles it.
|
||||
- J-e. Versioning means the editor's tracked changes plus one git commit per save.
|
||||
@@ -0,0 +1,269 @@
|
||||
# Row 252 build notes: Console inbox Approve and Deny for grant requests
|
||||
|
||||
Seat: Scrooge. Reviewer: Webby. Issue #1753, Vikunja #435 (J2).
|
||||
Brief: `docs/plans/2026-10-21_console-inbox-approve.md` (blob b41676d06).
|
||||
Design: Gander's DESIGN.md at d218e14bd, approved by Webby at 5d8f0f4b5 (webby-0082) with manifest checks N1-N5.
|
||||
Base: aca16404c on branch `scrooge/row252`. Rebase onto the row 253 tip under a new branch name once 253 lands.
|
||||
|
||||
## Status
|
||||
|
||||
- Code, tests and docs are written. No test has been run yet, because Gander holds the gate slot for row 253 run 5 and Sage ruled no node until a CLEAR.
|
||||
- Everything in the Results section is empty until that run. Nothing below claims a pass.
|
||||
|
||||
## Q188
|
||||
|
||||
No agent creates a grant. Approve and Deny are human-only Console actions.
|
||||
|
||||
- Only a console cap reaches `decision.preview.console` and `decision.resolve.console`. A console cap reaches nothing else (console-decide `J1, Q188`).
|
||||
- The socket refuses a console proof for any other verb before the proof check (`server.mjs`).
|
||||
- A console cap binds only through `authenticateConsole`, which needs the console proof (section 2): the same-tree `console-cli.mjs` wrapper, its own nonce, a direct same-tree `serve` parent, and a configured owner subject from the spawn environment.
|
||||
- `recordEvent` keeps `human.input` reserved, so no agent writes the record that lets a console close start the executor (console-decide `recordEvent keeps human.input reserved`).
|
||||
- A `via console` close without its matching `human.input` approves nothing (console-decide `J14`).
|
||||
|
||||
## Manifest checks N1-N5
|
||||
|
||||
- **N1.** Every `console.sso` subject is an owner of the business. Name is display only.
|
||||
- `cli/src/config.mjs` carries `{provider, iss, sub}` per subject into the boot as `consoleOwners`. No sso means no key.
|
||||
- `bus/src/runtime.mjs` validates the list (array, at most 512, exactly three non-empty strings each) and passes it to `verifyPrincipal`.
|
||||
- Tests: cli config.test `Row 252 (Webby N1)` and principal.test.
|
||||
- **N2.** `human.input` is written in the same transaction, before `#approvedGrant`. The executor starts once, and a deny starts none.
|
||||
- Tests: console-decide `N2, J13` checks the order by seq and that the grant runs once. `J15` checks that a deny starts nothing.
|
||||
- **N3.** The close notice comes from `from_run 'human-console'`, and `public/bus.js` shows "via console". `landing` and `#protectedEnd` stay CLI-only.
|
||||
- Tests: console-decide `N2, J13` checks the notice. decide-browser `J17` checks the close-elsewhere line.
|
||||
- **N4.** `decision.resolve` is not a Console verb.
|
||||
- `CONSOLE_VERBS` is exactly the two console verbs. The webui serve `BUS_VERBS` read list has no decision verb, and webui `cli.mjs` has no `decision.` string. `consoleCall` refuses anything outside `CONSOLE_VERBS`.
|
||||
- Tests: console-decide `N4`, and decide-sso `N4`, which hits `/api/bus/decision.*` for 404 and covers the cli.mjs scan and the consoleCall refusals.
|
||||
- **N5.** `readProcess` keeps the five `MOSAIC_BUS_CONSOLE_*` keys in the env allowlist (`bus/src/human.mjs`). The human proof and its spawn are unchanged.
|
||||
|
||||
## Design choices and tradeoffs
|
||||
|
||||
- **Webui imports from the broker.** `webui/src/bus.mjs` imports `CONSOLE_VERBS` from `bus/src/broker.mjs`, which loads the broker module into the Console at import.
|
||||
- I chose one source for the verb list over a second copy that could drift.
|
||||
- If Webby prefers the copy, the alternative is a literal list plus a test pinning it equal to `CONSOLE_VERBS`.
|
||||
- **The Console transport shares humanCall's spawn.** `consoleCall` and `humanCall` share it as `transport()`, so `webui/src/bus.mjs` changed. The busCaller `business` default keeps `||` (the old falsy semantics), not `??`.
|
||||
- **Confirmations are held in memory in the serve process.** Their lifetime is 2 minutes, they are single use, and they bind the session, decision, choice and digest. The cap is a limit that never drops a live confirmation.
|
||||
- A restart loses all held confirmations. The user clicks again and gets a new preview. Nothing is written until the resolve.
|
||||
- **No retry anywhere.** A resolve whose bus reply is lost answers `outcome-unknown`, the confirmation is spent, and the page says "Nothing was sent again."
|
||||
|
||||
## Old tests changed (for Mos, per mos-1272)
|
||||
|
||||
Mos ruled both changes allowed in mos-1321 (17:12:28Z), each with one condition.
|
||||
- **shell.test condition:** reword the comment that opened "No Resolve control", since it is false once the row lands. It now says row 252's Approve and Deny is the one decision control, that its labels come from ACTIONS, and that the doesNotMatch keeps any literal label out of the markup. Webby checks the wording in review.
|
||||
- **channel-proof condition:** a header comment is not enough, because human.mjs changes what readProcess keeps. In the review, Webby reads the human.mjs and webui bus.mjs diffs and states two things:
|
||||
- the human proof's checks are unchanged
|
||||
- only the console verifier reads the five MOSAIC_BUS_CONSOLE_* keys, and each case is covered by a named test in console-decide or decide-sso
|
||||
- **Second row to land (252 or 239):** it recomputes the webui bus.mjs hash, adds its own header line, and its reviewer re-reads bus.mjs from the base to the combined tip.
|
||||
|
||||
- **`webui/tests/shell.test.mjs`**, write-boundary test. Allowed under mos-1321, with the comment reworded.
|
||||
- The `writes` list gains the new `fetch(url, ...)` in `decidePost`. It is first in file order (bus.js:362).
|
||||
- The `method:` count goes from 7 to 8.
|
||||
- A new assertion pins `decidePost`'s callers to `url` plus the two literal `/api/decisions/${enc(a.id)}/...` routes.
|
||||
- No existing entry was loosened.
|
||||
- **`bus/tests/channel-proof.test.mjs`**, sha256 re-pins with a header comment:
|
||||
- `bus/src/human.mjs` (N5 env keys) to bb13c146…c885.
|
||||
- `webui/src/bus.mjs` (consoleCall and `transport()`) to 701f009e…5272.
|
||||
- Any later edit to either file needs a re-pin.
|
||||
- `bus/tests/principal.test.mjs`, `webui/tests/tester-listener.test.mjs` and `cli/tests/config.test.mjs` have new tests appended only.
|
||||
|
||||
## Overlap with row 239 (Honker, b0c28366f, sage-2835)
|
||||
|
||||
- Row 239 also re-pins `channel-proof.test.mjs`:
|
||||
- `packages/webui/src/bus.mjs`, to 1eba9774…e5cf. That is the pin both rows change.
|
||||
- `packages/cli/src/transport.mjs`, which is 239 only.
|
||||
- `packages/bus/src/human.mjs` is a 252-only pin.
|
||||
- The two rows also share source files: `bus/src/broker.mjs`, `bus/src/runtime.mjs`, `webui/src/bus.mjs` and `webui/src/serve.mjs`.
|
||||
- Whichever row lands second rebases onto the first and recomputes the webui `bus.mjs` pin from the merged file. Both header comments are kept. It then reruns every touched test file whole.
|
||||
- Mos needs to see that re-pin (re-pins need Mos).
|
||||
|
||||
## Mutant plan (design section 6, J1-J17)
|
||||
|
||||
Each mutant is one edit to the named source. Run the killing test, then restore and compare the sha256 against the pre-mutant hash with `git status` clean apart from the build. Kill rules are written before the runs.
|
||||
|
||||
| J | Mutant | Source | Expected kill |
|
||||
|---|---|---|---|
|
||||
| J1 | eligibility from the request, `#consoleResolution` returns non-null for any decision | broker.mjs | console-decide `J1, J2` (ineligible) |
|
||||
| J2 | hardcode `approve = yes`, `deny = no` | broker.mjs | console-decide `J2` (go/stop, deny listed first) |
|
||||
| J3 | owner from name or header, skip the owner match in verifyPrincipal | principal.mjs | principal.test `console requires…` |
|
||||
| J4 | skip the serve-parent walk | principal.mjs | principal.test `console requires…` |
|
||||
| J5 | accept the human nonce namespace | principal.mjs | principal.test `console requires…` |
|
||||
| J6 | reuse the session from the confirmation, skip the live session check at resolve | decision-http.mjs | decision-http `session that ends`, decide-sso `listener gates` (new session) |
|
||||
| J7 | keep the console cap bound after the call (skip `release` for kind console) | server.mjs | **open**: no new test pins release for a console proof. Before the run, check whether an existing server test kills it. If not, add one and name it in the report. |
|
||||
| J8 | skip the same-origin check | decision-http.mjs | decision-http `gates`, decide-sso `listener gates` |
|
||||
| J9 | resolve without a confirmation token | decision-http.mjs | decision-http `a confirmation then a resolve`, `bad body` |
|
||||
| J10 | drop the choice, decision or session binding | decision-http.mjs | decision-http `binds the session, decision and choice` |
|
||||
| J11 | skip the digest, closed or business check | broker.mjs | console-decide `J11`, `N2, J13` (second answer closed) |
|
||||
| J12 | do not spend the confirmation | decision-http.mjs | decision-http `a second resolve refuses`, `confirmations… one use` |
|
||||
| J13 | write via cli, or omit the subject in human.input | broker.mjs | console-decide `N2, J13` |
|
||||
| J14 | `#approvedGrant` accepts via console without the human.input | broker.mjs | console-decide `J14` |
|
||||
| J15 | start the executor on any resolved choice | broker.mjs | console-decide `J15` and `J2` (stop starts nothing) |
|
||||
| J16 | `decide` true without sso or send | serve.mjs, public/bus.js | decide-sso `J16`, decide-browser `J16` |
|
||||
| J17 | retry on outcome-unknown or auto-submit after preview | public/bus.js | decide-browser `J17` (post count stays at 7) |
|
||||
|
||||
## Manifest
|
||||
|
||||
Source:
|
||||
- packages/bus/src/broker.mjs
|
||||
- packages/bus/src/client.mjs
|
||||
- packages/bus/src/console-cli.mjs (new)
|
||||
- packages/bus/src/human.mjs
|
||||
- packages/bus/src/principal.mjs
|
||||
- packages/bus/src/runtime.mjs
|
||||
- packages/bus/src/server.mjs
|
||||
- packages/cli/src/config.mjs
|
||||
- packages/webui/src/bus.mjs
|
||||
- packages/webui/src/cli.mjs
|
||||
- packages/webui/src/decision-http.mjs (new)
|
||||
- packages/webui/src/public/bus.js
|
||||
- packages/webui/src/serve.mjs
|
||||
|
||||
Tests:
|
||||
- packages/bus/tests/channel-proof.test.mjs (re-pins)
|
||||
- packages/bus/tests/console-decide.test.mjs (new)
|
||||
- packages/bus/tests/principal.test.mjs
|
||||
- packages/cli/tests/config.test.mjs
|
||||
- packages/webui/tests/decide-browser.test.mjs (new)
|
||||
- packages/webui/tests/decide-sso.test.mjs (new)
|
||||
- packages/webui/tests/decision-http.test.mjs (new)
|
||||
- packages/webui/tests/shell.test.mjs (old test changed, see above)
|
||||
- packages/webui/tests/tester-listener.test.mjs
|
||||
|
||||
Docs:
|
||||
- packages/webui/README.md
|
||||
- agents/scrooge/work/queue-252/BUILD.md (this file)
|
||||
|
||||
## Results
|
||||
|
||||
Not run. At the first CLEAR:
|
||||
1. Run each touched test file whole, with TMPDIR under /mnt/storage/scratch/tmp.
|
||||
2. Run the bus, webui and cli suites.
|
||||
3. Run the mutants above, then the drift check against refactor.
|
||||
|
||||
## Rebase onto fe4ee8c7b (sage-3482, 2026-10-23)
|
||||
|
||||
The old base aca16404c had drifted. Since then refactor changed 8 of the row's 24 paths, across 15 commits. I made a new branch, scrooge/row252-fe4ee8c7b, so scrooge/row252 22cc74645 is untouched and nothing was force-pushed. I rebased with local git, `-c core.hooksPath=/dev/null`, and no stash.
|
||||
|
||||
Commit map:
|
||||
- 4bd273bce -> 1891d36e4 (product and tests; six files conflicted)
|
||||
- ca4537cc9 -> 9e5980acd (BUILD.md; clean)
|
||||
- 22cc74645 -> 6fd206e80 (shell.test comment and BUILD.md; clean)
|
||||
|
||||
Conflicts, all in 4bd273bce, and how each was resolved:
|
||||
1. packages/bus/src/broker.mjs, verb lists. On the base, row 206 added the share verbs to USER_VERBS, and row 275 added VIEWER_VERBS, userLevel and BOARD_VERBS and removed the USER_LEVEL const. I kept the base's lists and dropped USER_LEVEL, because nothing on the base reads it. CONSOLE_VERBS goes in after USER_VERBS with its comment unchanged. The request() gate (`!!s.console !== CONSOLE_VERBS.includes(verb)`) auto-merged after the FED R4 access sweep and the user check. Its order relative to the user gate is the same as on the old base.
|
||||
2. packages/bus/src/runtime.mjs, startBroker. FED R4 (#1801) added `access.intervalMs` to the access check. I kept the consoleOwners validation and the frozen owners first, as in the row, and then the base's access check with intervalMs.
|
||||
3. packages/cli/src/config.mjs. FED R6 (#1800) added accessFor and `config.access`. I kept both: accessFor and then consoleOwners, and in bootConfig `config.access` and then `config.consoleOwners`. process.mjs passes the boot config to startBroker whole, so both keys reach it.
|
||||
4. packages/webui/src/cli.mjs. FED R6 moved the owner's bus options into consoleBus() (console-bus.mjs) and added userCaller and userOptions. I kept the base's form, added consoleCall to the bus.mjs import, and added decisionWrite to the declarations. I kept the row's `if (ssoOn && sendOn) decisionWrite = consoleCall(...)` after the consoleBus() call, and pass decisionWrite to startServer next to userOptions and federation.
|
||||
5. packages/webui/src/serve.mjs. FED R6 added granted-user sessions: userRoute gates them, and userOptions rebinds the options per request.
|
||||
- The signature now has both decisionWrite and userOptions/federation.
|
||||
- The session hunk keeps the base's signer and granted-user block. ssoSession is set only for a session with no `user`, which is the owner's. A granted user is refused at userRoute before the decision route anyway, because /api/decisions/* is not on userRoute's list. Under the new base this keeps the design section 2 rule that only the owner answers a decision.
|
||||
- /api/config keeps the base's `user` and `federation` keys. `decide` gains `!granted`, matching how the base gates `edit`, so a granted user's config never offers Approve and Deny.
|
||||
- The decisionWrite startup check and createConfirmations() auto-merged unchanged.
|
||||
6. packages/bus/tests/channel-proof.test.mjs, webui bus.mjs pin. Row 301 (#1808, mos-1449) moved the user transport into bus.mjs and re-pinned it to 39fcb27c. The row's consoleCall auto-merged on top. I kept row 301's comment and re-pinned to the merged blob's sha256, 604092cb706b152d458f299ff1071f6cd488774629012f96b672f7e9a16c0b2c. The human.mjs pin (bb13c146) auto-merged and matches the blob at the new tip. The row's existing header comment (Webby N5) still describes why bus.mjs is re-pinned. humanCall and transport() are unchanged.
|
||||
|
||||
Auto-merged without conflict: webui bus.mjs (consoleCall next to row 301's userCaller), webui README.md, bus client.mjs, human.mjs, principal.mjs, server.mjs and public/bus.js.
|
||||
|
||||
Two new-base reads I checked:
|
||||
- granted-user.test.mjs reads `/api/config` key by key and has no deepEqual on the whole object, so the extra `decide` key doesn't break it.
|
||||
- No other test pins one of the row's source files by sha256, apart from channel-proof.
|
||||
|
||||
Not run: no test, node or syntax check during this step. Sage's next CLEAR covers the push, the whole webui, bus and cli suites, every scripts/test-*.sh, and J1-J17. Plugins must show 0 failures (mos-1456/1462). J7 still needs a killer test. The serve.mjs `!granted` and ssoSession lines are new since the build, so they get mutants too. I'll add them to the J list at the run.
|
||||
|
||||
## Runs (sage-3488 CLEAR, 2026-10-23)
|
||||
|
||||
Gate tree: detached worktree of 3e6de9807 with the canonical node_modules linked, so
|
||||
`MOSAIC_REQUIRE_EDITOR_BUILD=1` is set (mos-1453). TMPDIR comes from mktemp under
|
||||
/mnt/storage/scratch/tmp. Each step runs under `timeout -k 10` (mos-1275):
|
||||
|
||||
| Step | Limit (s) |
|
||||
|---|---|
|
||||
| pkg bus, cli, plugins, webui, business (`node --test tests/*.test.mjs`) | 1800 each |
|
||||
| test-auth / conductor / config / discord / extension-package | 126 / 145 / 130 / 147 / 130 |
|
||||
| test-foundation / land-work / queue / release / task | 269 / 171 / 226 / 157 / 231 |
|
||||
| mutant single file | 300, chat-browser 600 |
|
||||
|
||||
The script limits are 3x the longest runs recorded in queue-253 (receipt 3b91cfdaf).
|
||||
|
||||
### First-run reds placed (mos-1272)
|
||||
|
||||
- pkg-bus at 3e6de9807: 902 pass, 1 fail. The failure is `N2, J13` in console-decide.test.mjs:180, a new
|
||||
test (0 human.input rows found, 1 expected). The cause is in the test, not the product. `events()` spread
|
||||
the parsed body after the row's `kind`, so an answer body's own `kind: 'answer'` overwrote
|
||||
`'human.input'` and the filter could never match. Three more filters on the same field had the same
|
||||
defect, :250 and :290 (count of human.input must be 0) and :252 (an action.refused exists). The two
|
||||
zero counts passed vacuously. Fix: the row's kind is also kept as `event`, set after the body, and all
|
||||
four filters read `event`. The deepEqual at :181 now names `event: 'human.input'`. The file then ran
|
||||
whole at 11 pass, 0 fail.
|
||||
- J7 (open in the plan): no existing test bound a console proof over the socket. The new test
|
||||
`J7: a console proof over the socket binds a cap for its one call and the reply releases it` in
|
||||
console-decide.test.mjs serves a real socket with authenticateConsole, makes one console call that
|
||||
answers and one that refuses (`decision-not-found`), and checks that both caps are released
|
||||
(`unauthenticated`). J7 is applied as `boundCap = kind === 'console' ? null : transient;` in server.mjs,
|
||||
which skips every release path for a console cap (finish, the finally, and socket close). It ran
|
||||
at 11 pass, 1 fail, failing only the new J7 test. KILLED. The restored server.mjs matched its pre-mutant sha256.
|
||||
- decide-sso.test.mjs (new) ran 2 pass, 2 fail before any mutant:
|
||||
- `J16`: its `{ send: null }` case reached `setup`, and startServer refused it, because a decision
|
||||
transport without `--send` never starts (serve.mjs guard). That case now asserts the refusal, and the
|
||||
`{ decisionWrite: null }` case still checks the read-only routes and `decide: false`.
|
||||
- `N4`: a POST to `/api/bus/decision.*` gets row 74's preview refusal (403 `read-only`, for every
|
||||
write without a route of its own) before any verb lookup. POST now asserts that refusal and GET
|
||||
still asserts 404. Either way the transport sees no call.
|
||||
- The file then ran whole at 4 pass, 0 fail.
|
||||
- Old-test reds, sent to Sage before any fix (status 2):
|
||||
- cli `access-boot.test.mjs` R6 B1-B3 come from this row. The fixture's SSO provider has no issuer,
|
||||
so `consoleOwners` maps `iss: undefined` and startBroker refuses the owner list
|
||||
(`invalid-host-request`). The file is 4/0 on fe4ee8c7b.
|
||||
- cli `compactions-launch.test.mjs` REQ-THREAD-2 T3, 5 tests, each "timed out waiting". They also fail
|
||||
0/5 on a fe4ee8c7b tree, so they are not this row's.
|
||||
- Correction (sage-3492): the compactions-launch reds were the environment, not a base red. The gate
|
||||
TMPDIR (`<gate>/tmp`) was long enough to break the launcher/bwrap paths. Alone in a short
|
||||
`mktemp -d /mnt/storage/scratch/tmp/scrXXXXXX` TMPDIR the file ran 5 pass, 0 fail, 0 skipped, and
|
||||
Sage's own gate ran it green. The file is unchanged.
|
||||
|
||||
### Rulings and further placements (sage-3492 to sage-3496)
|
||||
|
||||
- access-boot (Mos GO mos-1464, relayed in sage-3494): `access-boot.test.mjs:17`'s `authentik` provider
|
||||
gains `issuer: "https://idp.test"`, and nothing else changes. numstat against fe4ee8c7b reads 1 1.
|
||||
It is committed alone (b316e0a55). access.test.mjs:273 uses the same fixture but never reaches
|
||||
bootConfig, so it is untouched. The file ran 4 pass, 0 fail.
|
||||
- decide-browser.test.mjs (new) ran 0 pass, 3 fail before any mutant, then 1 pass, 2 fail after the
|
||||
first fix:
|
||||
- setup's `UPDATE decisions SET question=?` hit migration 3b's `decisions_no_update` trigger, because
|
||||
decisions is append-only. The test now writes its own open credential.mint grant decision, with the
|
||||
hostile question in the INSERT. It is the same two rows host-fixtures' grantDecision writes, and
|
||||
host-fixtures.mjs is unchanged.
|
||||
- `page()` visiting the URL it was already on was a same-document hash change, not a load. So J17's
|
||||
"closed elsewhere" step never re-read the bus, and the redirect test's second visit never redrew
|
||||
after signing out. `page()` now loads about:blank first, as editor-browser.test.mjs:216 does.
|
||||
- The file then ran whole at 3 pass, 0 fail (9e1601a20).
|
||||
- decide-sso J16 (sage-3492): the startServer refusal stays. The one config that starts without send
|
||||
also lacks a decision transport, since startServer refuses a transport without `--send`. That config,
|
||||
`{ decisionWrite: null, send: null }`, is now in the J16 loop and gets `decide: false` and the
|
||||
read-only routes. Webby rules in the manifest round.
|
||||
- J19 killer (new test in decide-sso.test.mjs): an access document with one granted user (alice). The
|
||||
listener has `--send`, a decision transport and a user transport. The owner's config says
|
||||
`decide: true`. Alice's says `user: true`, `send: true` and `decide: false`. Her POSTs to both
|
||||
decision steps get 403 `owner-only`, and the transport sees no call.
|
||||
- J18 is equivalent alone, because a granted session never reaches decisionHTTP. At serve.mjs:694 a
|
||||
session with `user !== undefined` meets `userRoute` before the decision branch. userRoute admits only
|
||||
`/api/chat*` and GETs of `/api/config` and the user files, never `/api/decisions/*`, so the 403
|
||||
`owner-only` comes before `ssoSession` is read. The `session.user === undefined` guard is a second
|
||||
layer behind that. The J19 test's owner-only POSTs pin the first layer.
|
||||
- J11c is equivalent. `s.human !== b.human` at broker.mjs:5604 can't hold for a console cap:
|
||||
- bindConsole (broker.mjs:1459) refuses `record.human !== b.human` with `human-required`.
|
||||
- Its cap carries `human: b.human` for its own business, and #cap freezes the session (:1094).
|
||||
- At resolve, `b` is `this.#business(s.business)` (:5324), the same business.
|
||||
- reloadBusiness refuses a document whose human differs (:517).
|
||||
So for any session with `s.console` set, the two humans are equal. Webby rules on it in the
|
||||
manifest round.
|
||||
- Mutants, part 2, on the tip test files (decide-sso 5/0, decide-browser 3/0 as the baseline).
|
||||
Every restore matched its pre-mutant sha256.
|
||||
- J16a: KILLED by decide-sso J16.
|
||||
- J16b: KILLED by decide-browser J16.
|
||||
- J17a: KILLED by decide-browser J17 and the redirect test.
|
||||
- J17b: KILLED by decide-browser J17.
|
||||
- J19: KILLED by decide-sso J19. granted-user stays 19/0 under it.
|
||||
- J18: SURVIVED decide-sso 5/0 and granted-user 19/0. It is equivalent alone, for the reason above.
|
||||
- Old webui stubs (Mos GO mos-1465, relayed in sage-3497): library-cli.test.mjs:17 and
|
||||
thread-claims.test.mjs:58 build a loader-hook stub of bus.mjs with no `consoleCall` export, and cli.mjs
|
||||
now imports it. Each busSource gains `export const consoleCall=()=>{throw Error('consoleCall outside --sso')};`
|
||||
in the neighbours' throwing form, and nothing else changes. numstat against fe4ee8c7b is 1 1 for each.
|
||||
Alone they ran 1/0 and 4/0. No mos-1316 item for compactions-launch: the whole cli rerun is the evidence.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Violet
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Mos under Q245 (mos-1506). Owns row 297 (the GitHub grant method, handed over from Bushroot).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread (thread 6229eb89) and works in its own worktree. It has no
|
||||
credentials of its own; it pushes only its own `violet/*` branches through the repository credential helper (mos-0981),
|
||||
never `refactor`, `main` or another seat's branch. Claude seats orchestrate and review (Q245). Registered by Sage on
|
||||
2026-10-23 so the queue can name the seat as row owner (store.mjs isSeatDir).
|
||||
@@ -1576,3 +1576,32 @@ are never rewritten or removed; corrections are new entries.
|
||||
- 2026-10-23T08:10:29Z | sage (Opus 5.5, 1ef1e4f8) | 299 gate and landing, 303 review, 219/295 rebases | 299 LANDED ffb2fedfb 08:03Z (done 3487, #1806 c31087); queue-commit 8a35ffadb (revs 3474-3487); 303 Duckworth approve rev 3480; requests posted as sage for 303 (c31082) and 299 (c31083); 219 full restart on bfa421b6c from 08:08Z (sage-3283)
|
||||
- 2026-10-23T10:48:40Z | sage (Opus 5.5, 1ef1e4f8) | 302/295/219 landings, Docker pool diag, 590/591 briefs, 272 rebase | 302 LANDED 3ed9fd754, 295 LANDED 2efd127ce, 219 LANDED 5fdfd7275 (#1720 c31110, P19 416 c913); test-release 11/3 from exhausted Docker pools, 18 networks removed (mos-1437/1438); queue-commit 90633c864 (revs 3495-3510, 3510 Huey's); 272 rebase and fallback ruled (sage-3357)
|
||||
2026-10-23T11:03:46Z | Gyro (Codex gpt-6.1-sol, mos-1444) | Vikunja571 PRD d8 docs alignment | R6 vocabulary boundary, mapped-folder eligibility and R14 whole-host recovery updated in parent/W4; static checks only, no tests or live changes. Fenton reviews; Sage lands.
|
||||
- 2026-10-23T11:47:32Z | filbert (Claude Opus 5.5, T3 048f3c63) | row 305 round 1 (#1812, Vikunja 591, Gizmoduck, candidate 7e8218a2e) | approve in 31127, rev 3534; static on git objects (scripts = 3bce852d5; traps kept, no compose down, ls failure fails, skip only on docker info); gate receipts read, not rerun: Gizmoduck run1 + Sage gate task 92/0, release 15/0, mutants a/b red, c survives
|
||||
2026-10-22 | Huey (Codex gpt-6.1-sol, cabd45ca) | Row 272 / 264e (#1777), sage-2796, huey/row272 on 2d0d407ba | CLI/Console session work in preparation; test execution/claim/push deferred under Gander's gate, no runtime or live change.
|
||||
2026-10-22 | Huey (cabd45ca) | Row 272 additive 264d contract gizmoduck-0272 at 78e14352f | Draft cancellation UI/CLI and terminal receipts added; tests authored but not run under sage-2816 gate, no commit/push/live change.
|
||||
2026-10-23 | Huey (cabd45ca) | Row 272 sage-3141 local rebase | Draft reapplied from 2d0d407ba onto b23783f39 on huey/row272-b23783f39; product merge clean, append-only log conflicts retained both sides. Landed 270/271 contracts checked; no Node/tests/claim/commit/push pending CLEAR.
|
||||
2026-10-23 | Huey (cabd45ca) | Row 272 sage-3145 draft preservation | Shared stash commit 07dc9d2f7 pinned to huey/row272-stash-keep; rebased draft committed locally by explicit Sage instruction before tests. Untested checkpoint only; claims/test/push CLEARs still pending.
|
||||
2026-10-23 | Huey (cabd45ca) | Row 272 Scrooge early static read on 850b22680 | B1 and N1/N2 follow-up drafted with new tests only; old assertions/shared stub unchanged. Static diff clean; no tests/commit/push pending CLEAR, round 1 remains frozen.
|
||||
2026-10-23 | Huey (cabd45ca) | Row 272 round 2, sage-3354/3357/3358/3361 | Rebased onto 5fdfd7275 on huey/row272-5fdfd7275, preserving 219/295 and approved old-test hunks; rev3510 in-progress. New fallback test ran RED first in Sage's slot: expected legacy failure, 3 guards pass; slot returned. Bounded fallback and extra guards/mutants prepared for Sage's full gate; no post-implementation run or green claim.
|
||||
|
||||
2026-10-23 | Huey (cabd45ca) | Row 272 round 3, mos-1455 / sage-3435 | Added empty role authority arrays to new CLI fixture and guarded null old at all three chat render sites; browser tests byte-identical. Same 17-path manifest.r3, no local execution or queue writes. Follow-up candidate awaits Scrooge delta review and Sage gate; gate run 1 remains RED.
|
||||
|
||||
2026-10-23 | Sage (1ef1e4f8) | Rows 272 and 307 landings, row 306 design, row 252 rebase check | 272 landed d33989230 and 307 landed d243f6b25, both on full five-suite gates with mutants killed. Row 306 design r2 approved by Dewey (c31197), build going under mos-1463. Row 252 rebase 3e6de9807 on fe4ee8c7b checked and Scrooge given the slot (sage-3488).
|
||||
2026-10-23 | Sage (1ef1e4f8) | Rows 291 and 306 landings, row 308 launch | 291 (PRD 1.16, #1796) landed 4874d1e83 and 306 (#1813) landed 1676ad0de..2d0318a44 by temp-index replay, patch-ids equal, both on all-suite gates (mos-1455, plugins 0 fail, 306 mutants M1-M3 killed). 306 N1/N2 in DEFERRED 9c8877bdf. Row 308 (PRD 1.17, #1815) briefed and launched to Gosalyn; slot passed to them for claim, push and in-review. Scrooge told to rebase 252 on 9c8877bdf. Mos told (sage-3515, sage-3520).
|
||||
2026-10-23T17:29:46Z | Gyro (Codex gpt-6.1-sol, sage-3542) | 571 REQ-LIB-5/Q239 Connections design amendment | Proposed W8 plus parent/W4 alignment; Mos build order kept distinct from import acceptance. Static docs checks only, no repo Node/tests/live action. Fenton review, Sage landing.
|
||||
2026-10-23T17:32:37Z | Gyro (Codex gpt-6.1-sol, sage-3544) | 571 Connections N1/N2 | W7 opening-order cross-references and human-view comment/deletion exclusion clarified. Static docs checks only; no repo Node/tests/live action. Delta to Fenton, Sage lands.
|
||||
2026-10-23T17:48:43Z | Scrooge (629b6cd3) | Row 252 Console inbox Approve and Deny (#1753) | Rebased onto 306 landing as 580037105 (range-diff all =), slot run bus 907/0 webui 410/0 cli 943/0, in-review revs 3608/3609, Webby r1 APPROVE, J1-J19 specs to Sage; Sage landed 45ea3fa0d (done rev 3616). Part 1 mutant results were never in BUILD.md; they went to Sage by T3 (scrooge-0252-mutant-specs).
|
||||
2026-10-23 | Sage (1ef1e4f8) | Rows 308 and 252 landings, 571 Connections design amendment, Vikunja 597 | 308 (PRD 1.17, #1815) landed 30e6bb680 and 252 (#1753) landed 45ea3fa0d (replay of 580037105 onto 30e6bb680, range-diff all =), each after a full gate on the landed tree (252 also J1-J19, J11c and J18 equivalent); Gyro's 571 amendment (Fenton APPROVE) landed e8bb13df8; Vikunja 570, 435 and 597 done, canonical webui esbuild 0.28.2; queue revs 3589-3616 committed 59f013247
|
||||
2026-10-23 | Sage (1ef1e4f8) | Vikunja 571 W build rows; rows 13, 42, 257 and 289 check | Filed the design's ten W rows as queue rows 309-318 (revs 3617-3626, owners set to unassigned 3627-3636, queue commit 293b42fee pushed); Vikunja 571 done (comment 994). Row 13's candidate fd72d2689 is on refactor, gate is Monday's row 7 ledger run; 42 paused under mos-1184; 257 post-v1; 289 waits on 214 and 288. Slot released 17:56Z.
|
||||
2026-10-23T21:08:32Z | Filbert (048f3c63) | Rows 220 and 216 round 2 verdicts, r5 static deltas, replay drift check | 220 APPROVE 739b868b6 (comment 31383 #1721, rev 3696), 216 APPROVE dae76ccc2 (comment 31384 #1717, rev 3697) on Researcher's r5 reruns and mutants and Sage's joint gate GREEN on fbe57a05b (sage-3748); static r5 deltas clean; 216 N3 (get returns a universe's own supporting files on 283's base) binds C2-05c; no tests run by me
|
||||
2026-10-23 | Gladstone (433058fc-9cea-46e3-8554-6ddefb312762, Codex gpt-6.1-sol) | #1816 / Vikunja 614, mos-1531 | Built hostcheck octal-mode fix and two bare-scratch bundle verifies on gladstone/hostcheck-item5 from 93694c518. Three append-only test blocks; Node/tests held by Sage-3738, untested push authorized sage-3740. Queue number pending; Morgana static review, Sage gate/landing.
|
||||
2026-10-23T21:58:38Z | Sage (1ef1e4f8) | Rows 220, 216, 320 landings and queue-commit (mos-1528, mos-1532/1534/1536/1537) | 220+216 joint gate GREEN 21:05:34Z on fbe57a05b, pushed with Filbert's SESSIONS line 582b86935; #1721 and #1717 closed. Row 320 (#1816, Gladstone F1-F6): faf55618e replayed onto 45e3ca7bd as 094124b4e (code patch-id equal, log appends in one final commit), Morgana APPROVE r2 c31403, full gate GREEN 21:53:00Z 0 fail, pushed 21:53Z, done rev 3717, #1816 closed c31413, Vikunja 614 done. queue-commit 604523b20 holds revs 3678-3717. Mos sent N=094124b4e to ops-01 (mos-1540).
|
||||
2026-10-23T22:06Z | Duckworth (Opus 5.5) | row 239 (#1745) round 1 on r4 e0cb1db2c (honker-0206, rev 3719) | APPROVE (static): r4 range-diff all =, 16 paths byte-equal to r3, e0cb1db2c BUILD.md only, 4 channel-proof pins match; record waits for Sage's comment id and gap (duckworth-0295) |
|
||||
|
||||
2026-10-23T19:13:00Z | Boyd (48a3ae38, Codex gpt-6.1-sol) | Row 298 (#1804, Q228) takeover from Megavolt under mos-1496 | Started on boyd/row298 at 45dca69f6; reading brief, D1-D6 and human-shell proof; awaiting Megavolt handoff. No Node/tests before Sage CLEAR, no queue writes.
|
||||
|
||||
2026-10-23T19:29:00Z | Boyd (48a3ae38) | Row 298 (#1804) targeted T1 lane B | Five whole files 29/0/0, ten mutants killed, source restored; reviewer delta prepared. Earlier startup timestamp corrected in Boyd BUILD.md. Full gate and queue/landing remain Sage's.
|
||||
|
||||
2026-10-23T22:41:07Z | Boyd (48a3ae38) | Row 298 rebase under sage-3794; row 321 review assigned | Rebased candidate on boyd/row298-r2 at base833ec1cc8; append-only log conflicts resolved preserving upstream, writer-lock revoke retained; no Node/tests before CLEAR. Row321 awaits pushed candidate/review request.
|
||||
2026-10-23T23:15Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row 324 / #1820 optional tracker design | New goldie/row324 worktree from approved860afc2b6, pinned brief f375f4e92, complete reader audit/plan. Existing required-key test contradicts Q247; requesting Mos exception before implementation. Tests held until CLEAR; row323 remains deferred until324 lands.
|
||||
2026-10-23T23:18Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row 324 / #1820 untested draft | Claim3759; optional tracker implementation, new loader/CLI/boot/Console tests, complete reader audit and prepared mutant. Mos-1556 permits sole existing required-key-list change. Syntax/diff checks pass; no tests before CLEAR. Untested push authorized sage-3836.
|
||||
@@ -40,8 +40,8 @@ install -d -o root -g root -m 0755 /etc/mosaic-stack/node-keys /opt/mosaic-stack
|
||||
cd "$(mktemp -d)"
|
||||
curl -fsSLO https://nodejs.org/dist/<v>/node-<v>-linux-x64.tar.xz
|
||||
curl -fsSLO https://nodejs.org/dist/<v>/SHASUMS256.txt
|
||||
curl -fsSLO https://nodejs.org/dist/<v>/SHASUMS256.txt.asc
|
||||
gpg --no-default-keyring --keyring /etc/mosaic-stack/node-keys/pubring.kbx --verify SHASUMS256.txt.asc SHASUMS256.txt
|
||||
curl -fsSLO https://nodejs.org/dist/<v>/SHASUMS256.txt.sig
|
||||
gpg --no-default-keyring --keyring /etc/mosaic-stack/node-keys/pubring.kbx --verify SHASUMS256.txt.sig SHASUMS256.txt
|
||||
grep " node-<v>-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c -
|
||||
echo "<pinned sha256> node-<v>-linux-x64.tar.xz" | sha256sum -c -
|
||||
tar -xJf node-<v>-linux-x64.tar.xz --no-same-owner
|
||||
@@ -189,7 +189,12 @@ First install:
|
||||
```sh
|
||||
# Copy the bundle in, then check it before anything reads it.
|
||||
echo "<bundle sha256> /root/<sha>.bundle" | sha256sum -c -
|
||||
git bundle verify /root/<sha>.bundle
|
||||
(
|
||||
bundle_check="$(mktemp -d)" || exit 1
|
||||
trap 'rm -rf "$bundle_check"' EXIT
|
||||
git init -q --bare "$bundle_check" &&
|
||||
git -C "$bundle_check" bundle verify /root/<sha>.bundle
|
||||
) || echo "STOP: bundle verify failed; do not continue"
|
||||
|
||||
# Root's checkout. stack-upgrade.sh and case2-unit.sh run from here, never from a copy.
|
||||
git clone -q -b refactor /root/<sha>.bundle /root/mosaic-stack
|
||||
@@ -206,7 +211,12 @@ Each upgrade, before `stack-upgrade.sh upgrade`:
|
||||
|
||||
```sh
|
||||
echo "<bundle sha256> /root/<sha>.bundle" | sha256sum -c -
|
||||
git bundle verify /root/<sha>.bundle
|
||||
(
|
||||
bundle_check="$(mktemp -d)" || exit 1
|
||||
trap 'rm -rf "$bundle_check"' EXIT
|
||||
git init -q --bare "$bundle_check" &&
|
||||
git -C "$bundle_check" bundle verify /root/<sha>.bundle
|
||||
) || echo "STOP: bundle verify failed; do not continue"
|
||||
git -C /root/mosaic-stack fetch -q /root/<sha>.bundle refs/heads/refactor:refs/remotes/origin/refactor
|
||||
git -C /root/mosaic-stack checkout -q --detach <sha>
|
||||
install -o mosaic -g mosaic -m 0600 /root/<sha>.bundle /srv/mosaic/incoming/<sha>.bundle
|
||||
@@ -231,13 +241,34 @@ When a run doesn't exit 0, send Mos the exit and the last lines of `/var/log/mos
|
||||
the old copy. The next run refuses `args-differ`, and a render from that file would put the old copy back.
|
||||
Move the file the result line names into place with `mv -f`. The two differ only in the `--copy` value.
|
||||
|
||||
After `case2-install.sh` succeeds, create the human CLI wrapper before running
|
||||
`stack-copy.sh check --units external`. Creating this wrapper is a separate live
|
||||
step requiring Mos's go (`scripts/stack-copy.sh:3`); the install does not create it. Run only on that go:
|
||||
|
||||
```sh
|
||||
sudo -u mosaic -H /bin/sh <<'MOSAIC_WRAPPER'
|
||||
cd / || exit 1
|
||||
mkdir -p -- "$HOME/.npm-global/bin" || exit 1
|
||||
"$HOME/src/mosaic-stack/scripts/stack-copy.sh" wrapper <sha> --create
|
||||
MOSAIC_WRAPPER
|
||||
```
|
||||
|
||||
`--create` refuses an existing wrapper; stop and review it rather than replacing
|
||||
it. The wrapper must name the same `<sha>` copy as the units.
|
||||
|
||||
Precheck lines: none. Root's checkout at `<sha>` and the stack's user's clone: precheck line missing.
|
||||
After install, `stack-copy.sh check --units external` reads the copy the units run.
|
||||
|
||||
The stack's user's `scripts/case2-install.sh` (C2-13b, design 10 step 2b) makes the first copy with
|
||||
`stack-copy.sh copy <sha>`. The copy runs `npm ci` from `package-lock.json`, which installs the pinned
|
||||
harness, so the host needs to reach the npm registry during the install. The install runs no
|
||||
`release.sh package` or `activate`.
|
||||
|
||||
## 7. The args file
|
||||
|
||||
`/etc/mosaic-stack/dev.args` holds the `case2-unit.sh` arguments, one per line, with the serve flags after a
|
||||
line holding `--`. I2's install writes it; ops-01 doesn't write it by hand. It must be root's, with no group
|
||||
line holding `--`. Root writes it at the unit render, from the values the go names. The stack's user's
|
||||
install never writes it: that user can't write `/etc`, and root reads nothing that user wrote. It must be root's, with no group
|
||||
or other write (`chmod 0644` or tighter). Before `--` it takes only `--user`, `--home`, `--business`,
|
||||
`--copy` (exactly once), `--node`, `--profile`, `--claude` and `--pm`. `stack-upgrade.sh` changes only the
|
||||
`--copy` line, and only after confirm passes.
|
||||
@@ -272,13 +303,37 @@ file with `stat -c '%U %a %n' PATH`, never `cat`. `<business>` is the host's bus
|
||||
|---|---|---|---|---|---|
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | `claude-session.env`, one line `ZAI_API_KEY=...` | 0600 | mosaic | library | `<path> sets ZAI_API_KEY` |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/` | the Console's proof file, the path the args file's `--proof-file` names | 0600 or 0400 | mosaic | library | none. `case2-unit.sh` refuses at render. Precheck line missing. |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | C2-13b's `library.sync` (not landed) |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/<business>/` | `library-gitea` | 0600 | mosaic | library | `case2-precheck.sh --business <business> --library-status`, after the bus is up: `library-last-error library-credential-missing` until it's placed, then `library <project> last fetched ...` |
|
||||
| `/srv/mosaic/.config/mosaic-dev/secrets/agents/<business>/<agent>/` | one file per service | 0600, dirs 0700 | mosaic | both | none. Filled only by `mosaic credentials issue`, or Approve after #436. Never by hand. |
|
||||
|
||||
The installer key, `<configDir>/secrets/hosts/<business>/<hostId>/installer`, is not a slot anyone places.
|
||||
Enrollment (section 8) writes it under the enrolling stack's config directory and deletes it when the host
|
||||
leaves.
|
||||
|
||||
## 9a. Notifier decision, before starting units
|
||||
|
||||
Make the explicit per-host decision before section 10: the default below is
|
||||
`binding: null`, so the bus runs without DMs. A named binding is a later per-host
|
||||
choice that needs Mos's go. The installer does not make this decision or write
|
||||
this file. Replace `<dataRoot>` with the configured data root and `<business>`
|
||||
with this host's business id. If the file already exists, stop and review the
|
||||
existing decision; this command refuses to overwrite it.
|
||||
|
||||
```sh
|
||||
sudo -u mosaic -H /bin/sh <<'MOSAIC_NOTIFY'
|
||||
cd / || exit 1
|
||||
umask 077
|
||||
notify_root='<dataRoot>/notify'
|
||||
notify_dir="$notify_root/<business>"
|
||||
mkdir -p -m 0700 -- "$notify_root" && chmod 0700 -- "$notify_root" || exit 1
|
||||
mkdir -p -m 0700 -- "$notify_dir" && chmod 0700 -- "$notify_dir" || exit 1
|
||||
(set -C; printf '%s\n' '{"notifyVersion": 1, "binding": null}' > "$notify_dir/notify.json") || exit 1
|
||||
chmod 0600 -- "$notify_dir/notify.json"
|
||||
MOSAIC_NOTIFY
|
||||
```
|
||||
|
||||
Precheck line: `ok notifier config <dataRoot>/notify/<business>/notify.json validates (directory 0700, file 0600)`.
|
||||
|
||||
## 10. Console start
|
||||
|
||||
None by hand. I2's `case2-unit.sh` enables both units, starts the bus, then the Console (Q189, mos-1122).
|
||||
|
||||
@@ -187,8 +187,10 @@ canon Markdown chapter takes its position by that rule or a human's committed or
|
||||
import preserves its chapter ID and position. Human reordering commits the manifest.
|
||||
library_manuscript and next_chapter use this order, never archived paths. Each
|
||||
answer/page names format, owned location/ID and captured commit. DOCX wikilinks stay clickable
|
||||
in the Console editor; no graph shows their current links yet. A Console Connections view
|
||||
and its tests are later work, waiting for Jason's J-b answer in PRD 1.17. Obsidian's archive
|
||||
in the Console editor, and no graph shows them yet. Under PRD 1.17 REQ-LIB-5 and Q239,
|
||||
the Console's Connections view lists the current links and backlinks of each story file
|
||||
the user may see, in both formats, with each one's scope and format. W8 owns that view
|
||||
and its tests. Obsidian's archive
|
||||
graph is historical, not a live DOCX graph. No graph implementation is in these W rows. New Story chooses universe/book or playground and gets
|
||||
its place in that manifest from its first DOCX commit, without an MD copy.
|
||||
|
||||
@@ -242,8 +244,8 @@ follow stable identities without an agent rewriting story links.
|
||||
Obsidian's old Markdown graph is historical after conversion. It does not track later DOCX
|
||||
edits, and it is not a parallel writing surface. The format map names current files and
|
||||
clickable editor links resolve across formats; it is not a graph view. No graph shows DOCX
|
||||
links yet. The later Connections view waits for Jason's J-b answer; it and its tests are
|
||||
outside the current W3/W7 scope. No Markdown export or Obsidian graph parity is promised. `.doc`/`.qxd` imports remain separate D23 work.
|
||||
links yet. W8 supplies REQ-LIB-5's Console Connections view: current links and backlinks
|
||||
of each story file the user may see, in both formats, with each one's scope and format. No Markdown export or Obsidian graph parity is promised. `.doc`/`.qxd` imports remain separate D23 work.
|
||||
|
||||
D12. Q235–Q237 withdraw the superseded-notice proposal and its open-Q36 boundary. Current
|
||||
Case 2 chapter reads after conversion use W4b's committed DOCX reader/citations. Agent
|
||||
@@ -579,12 +581,19 @@ runs Jason's layout agent nor automatically grants it access or sends book text
|
||||
|
||||
All rows follow BRIEF-TEMPLATE. They are not queue entries. S0 M1–M14 remain binding throughout W2/W6; source/target
|
||||
changes return to security review. W7 adds the policy conversion/routing row. S0 and the original W4 design have reviewed verdicts; this policy revision goes to Fenton.
|
||||
Their implementation gates remain separate. No code row W1/W2/W6/W3a/W3b/W3/W4b/W5/W7 enters the queue
|
||||
Their implementation gates remain separate. No code row W1/W2/W6/W3a/W3b/W3/W4b/W5/W7/W8 enters the queue
|
||||
before S0 PASS (recorded for this baseline, with M1–M14 still binding). Each also observes its explicit dependencies, source ownership, the CSP gate
|
||||
and the MVP gate-slot priority. Q177 permits chaining reviewed candidates, not overlapping
|
||||
owners. Sage assigns model-only seats. Each implementation reviewer is Opus 5.5 and did not
|
||||
author the code. Fenton reviews this design and can review code only when not its author.
|
||||
|
||||
Build order (mos-1470, Vikunja 571 comment 961): W7 first supplies reviewed conversion/format
|
||||
routing code with production import still closed, then W8 lands the Connections view, then
|
||||
W7's human opening step may open the first real chapter import. W8 is after W3/W4b and
|
||||
W7's closed routing candidate, before that W7 opening step. This is Mos's build-order rule,
|
||||
not a new REQ-LIB-8 condition or an addition to import acceptance. No converted real chapter
|
||||
exists without the view. O2 code remains after 10-27 and never ahead of MVP rows.
|
||||
|
||||
G1. Future code acceptance runs each touched test file independently and affected package
|
||||
suites on Sage's slot after MVP priority, plus required repository checks. Source builds need
|
||||
pinned tools, reproducible hashes, license/source manifests and independent review. Integration browser
|
||||
@@ -1258,6 +1267,7 @@ Independent conversion/parser/authorization review, writing/library/bus/harness/
|
||||
pinned-copy and sandbox closure tests, plus W4 A19/A20/A33–A35 and G1. Source archive, off-host acknowledged DOCX and
|
||||
format-map snapshot must match the new DOCX publication before completion. No real import
|
||||
opens before all D12 gates. The October 27 Markdown and supporting-doc fixtures still pass.
|
||||
The human opening step follows W8's landing (mos-1470, build order, not an acceptance condition).
|
||||
|
||||
### Handoffs
|
||||
|
||||
@@ -1265,6 +1275,83 @@ Owner sends commit/blob and fixture publication/crash evidence directly to the r
|
||||
Sage through T3. Reviewer sends verdict to owner/Sage. Copy Mos for the later first-real-book
|
||||
go or any proposed policy change, never ask an agent to execute that go.
|
||||
|
||||
## W8 Console Connections view for Markdown and DOCX story files
|
||||
|
||||
### Problem
|
||||
|
||||
PRD 1.17 REQ-LIB-5 at 30e6bb680, approved through Q239, requires the Console to list
|
||||
current links and backlinks of every story file the user may see, in both formats, with
|
||||
scope and format. Clickable editor wikilinks and Obsidian's historical archive graph do
|
||||
not supply this view.
|
||||
|
||||
### Owner and reviewer
|
||||
|
||||
- Owner: Sage assigns the seat, using Codex gpt-6.1-sol for cross-format lookup and authorization.
|
||||
- Reviewer: an independent Opus 5.5 seat assigned by Sage, not the author.
|
||||
- After: W3, W4b and W7's reviewed closed conversion/format-routing candidate; serial handoff
|
||||
from W7 for shared library/WebUI files and from C2-11a/11b and C2-10b for shared UI paths.
|
||||
- Before: W7's human opening step for the first real chapter import, under mos-1470. This
|
||||
orders builds, not REQ-LIB-8 acceptance, and does not change its four opening conditions.
|
||||
Post-10-27 only, with no gate slot ahead of MVP rows.
|
||||
|
||||
### Files owned
|
||||
|
||||
New: `packages/library/src/connections.mjs`, `packages/library/tests/connections.test.mjs`,
|
||||
`packages/webui/src/connections-http.mjs`,
|
||||
`packages/webui/src/public/{writing-connections.js,writing-connections.css}`,
|
||||
`packages/webui/tests/{writing-connections.test.mjs,writing-connections-browser.test.mjs}`.
|
||||
Existing or handed off from W7/W3: `packages/library/src/{index.mjs,links.mjs}`,
|
||||
`packages/webui/src/{serve.mjs,bus.mjs}`,
|
||||
`packages/webui/src/public/{index.html,writing-pane.js,writing-pane.css}`,
|
||||
`packages/webui/README.md`.
|
||||
|
||||
### What ships
|
||||
|
||||
A Console Connections control beside story navigation opens a keyboard-accessible list
|
||||
with Links and Backlinks sections for the selected story. Each item shows its title,
|
||||
scope and format, and opens the current authorized story through the existing resolver.
|
||||
It works for Markdown, DOCX and a partly converted book. It uses committed link metadata
|
||||
and W7's stable identities/format routing, never unsaved editor buffers or archive content.
|
||||
The response names the captured versions; after a committed save or conversion publication,
|
||||
a refresh uses the current authorized snapshot. An in-flight stale result cannot replace
|
||||
that refresh. An empty section says there are no visible links; it does not reveal hidden
|
||||
counts or titles. Unresolved links remain visibly unresolved and cannot open guessed paths.
|
||||
|
||||
A human-only Console read route uses existing authenticated project/universe/source access
|
||||
and exclusions. Authorize both endpoints of every displayed edge before returning titles,
|
||||
paths or counts. Backlinks cannot reveal another business, a denied universe, excluded
|
||||
story or archived source. Links inside comments or tracked-deletion text are not current
|
||||
story links, so the human Connections view never shows them and never depends on an agent
|
||||
setting. Final-view story links use the same committed extraction rules as W4b. Archive links are historical
|
||||
and excluded from this current-story view. No new agent grant, tool or conversation gate.
|
||||
|
||||
`connections.test.mjs` covers forward/backlinks across both formats, mixed-book identity
|
||||
preservation through conversion, current committed saves, scope/format labels, exclusions,
|
||||
archive/old-path refusal, unresolved targets and denial without metadata leaks. The HTTP/UI
|
||||
suite in `writing-connections.test.mjs` covers authentication, access revocation, response
|
||||
version binding, stale refresh rejection and refusal rendering. The browser fixture suite
|
||||
covers keyboard/focus, Links/Backlinks empty states, labels, cross-format navigation and
|
||||
refresh after save/publication. Fixtures only, never Melanie's text or a live stack.
|
||||
|
||||
### Out of scope
|
||||
|
||||
A node graph, Obsidian graph parity, editing links or story text, agent tools, private-book
|
||||
conversion, changes to REQ-LIB-8 acceptance, Q234's header, mos-1363 or S0 M1–M14.
|
||||
|
||||
### Gate
|
||||
|
||||
Independent Opus 5.5 review of access filtering and the human view, the named fixture suites
|
||||
and G1 on Sage's slot. Evidence shows both formats and mixed conversion state without
|
||||
archive or denied-source leakage. Sage records W8 landing before W7's first-real-import
|
||||
opening step per mos-1470; no import acceptance test or PRD condition is added here.
|
||||
|
||||
### Handoffs
|
||||
|
||||
Owner sends pushed commit/blob and fixture evidence directly to the reviewer and Sage via
|
||||
T3. Reviewer sends verdict to owner and Sage. Sage orders W8 landing before the W7 human
|
||||
opening step, and hands the shared files back before further W7 edits. Copy Mos for the
|
||||
first-real-book go or any proposed policy change.
|
||||
|
||||
## Engineering choices from mos-1368
|
||||
|
||||
This table resolves the map's engineering prompts; these are design decisions, not questions
|
||||
@@ -1282,7 +1369,7 @@ Later mos-1372/1375 override A32/A33's per-document/dedicated-session assumption
|
||||
| Q7 / A32/A33 | mos-1372 D9: ordinary Case 2 access in any thread/room, no per-document grant/share/dedicated session; existing supporting writes continue; marked for PRD 1.17 approval |
|
||||
| Q8 / A36 | W4b/W7: preserve Case 2 find/get/search/manuscript/universe tools via host committed-reader adapters, not fewer tools or raw ZIP access |
|
||||
| Q9 / A73 / C2 | W7: D10 manifest order, typed next_chapter, paragraph cursors bound to snapshot/access; both W4 byte cap and harness REPLY_CAP apply, never cut paragraphs or invent line equivalents |
|
||||
| Q10 / A22 | W3/W7 keep clickable cross-format editor links; no DOCX graph yet. Connections view/tests are later work, waiting on Jason's J-b answer; Obsidian archive graph is historical |
|
||||
| Q10 / A22 | W3/W7 keep clickable cross-format editor links; W8 supplies current links/backlinks for visible story files in both formats with scope/format (REQ-LIB-5, Q239); no DOCX graph yet; Obsidian archive graph is historical |
|
||||
| Q11 / A37 | W7: Console archive saves refuse; sync flags later archive edits or recreated old paths and excludes them from current canon; no remote-push refusal/protected-pattern guarantee |
|
||||
| Q12 / A21 | W7: reviewed markdown-it AST + first-party DOCX adapter; no new runtime converter; input cap 1 MiB default, W4 ZIP/XML validation, construct/token/extractor fidelity before archiving |
|
||||
| Q13 | W7 survey: synthetic construct corpus and aggregate-only count capability; no seat reads real text or claims a corpus census happened |
|
||||
@@ -1317,7 +1404,9 @@ renderer does not enable it. No change is imposed on the October27 release.
|
||||
|
||||
Alignment note: sage-3121 checks this revision against PRD 1.17 d6,
|
||||
SHA-256 `886b93c571f87bfbb2f27ee3113b6800a32d8f89196d95ace33566031bee380a`.
|
||||
Connections graph/tests wait for J-b; new non-old-path Markdown stays canon until import;
|
||||
That historical d6 alignment is superseded for Connections by landed PRD 1.17 at 30e6bb680:
|
||||
REQ-LIB-5/Q239 require W8's list view, ordered before real import under mos-1470.
|
||||
New non-old-path Markdown stays canon until import;
|
||||
archive folder is human-set in the library mapping with no default.
|
||||
|
||||
## Current verification and remaining gates
|
||||
@@ -1332,4 +1421,5 @@ append-only logs. No build, browser run, conversion, queue entry or live change
|
||||
Jason's format and CSP choices are settled by Q234–Q237. No question repeats Q1 or Q36.
|
||||
Before real conversion: Sage's Case 2 PRD amendment (Vikunja 570) needs Jason's approval;
|
||||
W4b/W7, Hermes-retired and post-10-27 gates must pass; Jason must explicitly give the first real Melanie-book
|
||||
conversion go. These are later authorization gates, not requests to convert anything now.
|
||||
conversion go. W8 lands before the human opening step under mos-1470 as build order, not
|
||||
an acceptance condition. These are later authorization gates, not requests to convert anything now.
|
||||
@@ -66,7 +66,11 @@ A Markdown file recreated at a converted old path is unmapped, neither canon nor
|
||||
excluded from lookups and flagged to the human. A new Markdown story at a different path in
|
||||
a converted book stays canon until import, flagged and offered for human import; no automatic
|
||||
conversion. Existing playground scope rules remain. Clickable DOCX editor links stay; no
|
||||
DOCX graph exists yet. A Connections graph and its tests wait for Jason's J-b answer.
|
||||
DOCX graph exists yet. PRD 1.17 REQ-LIB-5 and Q239 require the Console's Connections view
|
||||
to list the current links and backlinks of each story file the user may see, in both
|
||||
formats, with each one's scope and format. Parent design W8 owns the view and its tests.
|
||||
Mos-1470 orders its landing before W7's first-real-import opening step as build order,
|
||||
not an additional REQ-LIB-8 acceptance condition.
|
||||
|
||||
Every conversion runs the pinned extractor before archiving and refuses unsupported output.
|
||||
An independently reviewed importer preserves every wiki token and front-matter key.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Brief: a GitHub consumer for row 297's protected token callback
|
||||
|
||||
Sage, 2026-10-23, filed for Mos (mos-1525). Post-10-27.
|
||||
|
||||
## Brief: a GitHub consumer for Broker.githubWithToken
|
||||
|
||||
Row 297 builds the GitHub grant method and stops at the trusted host API
|
||||
`Broker.githubWithToken(business, agent, fn)` in `packages/bus/src/broker.mjs`.
|
||||
Nothing calls it yet: no git helper, mirror push or publication uses a GitHub
|
||||
token (violet BUILD.md, "Consumer gap for Sage's follow-up row").
|
||||
|
||||
This row adds the first consumer.
|
||||
|
||||
- It gets the token only through `githubWithToken`. It never reads the
|
||||
protected generation files, and never routes GitHub through the generic
|
||||
Gitea Credentials path, which has calendar-day dates and fork-specific
|
||||
identity.
|
||||
- It honors the callback's cancellation signal. The signal aborts on revoke and
|
||||
at the token safety deadline, so the consumer stops work and drops the token
|
||||
when it fires. No operation is retried with a token past its expiry.
|
||||
- No socket verb returns a token, and no consumer output, log or run record
|
||||
holds one.
|
||||
|
||||
Design comes first, with the consumer named (a git helper or mirror push), for
|
||||
Dewey's review. Tests cover: a revoke mid-call aborts the consumer; a call
|
||||
straddling the safety deadline aborts; an expired grant refuses before any
|
||||
network call; and no token text appears in output or the store.
|
||||
|
||||
Gate: Dewey approves the design and the manifest. The touched suites and every
|
||||
`scripts/test-*.sh` are green on Sage's gate, and mutants that drop the signal
|
||||
check or the expiry check are killed. Starts after row 297 is done.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Row 270's pi compaction reading moves behind the harness kind
|
||||
|
||||
Found during row 272's gate (#1777) on 10-23 (sage-3432). Mos ruled the move in mos-1455 item 3 and
|
||||
declined an EXCEPT entry. Vikunja 598.
|
||||
|
||||
## Brief: row 270's pi compaction reading moves behind the harness kind (Vikunja 598)
|
||||
|
||||
Gitea #1814. Owner Gizmoduck, who wrote row 270. Reviewer Duckworth, who reviewed it. Gate owner Sage.
|
||||
Base: refactor at this brief's commit. Design first, in `agents/gizmoduck/work/queue-307/DESIGN.md`.
|
||||
|
||||
`packages/plugins/tests/harness.test.mjs:192` (row 65) checks that no core source outside
|
||||
`packages/plugins` names a harness. It is red on refactor 1e4c1e6e9. Its `named` list is three lines
|
||||
of `packages/seat/src/compactions.mjs`:
|
||||
|
||||
- :232, where `readLedger` marks a ledger unsupported when a collection's harness isn't `"pi"`;
|
||||
- :283 and :287, where `collectCompactions` branches on `harness === "pi"`.
|
||||
|
||||
Row 270 (#1775, 8c28ee731) added them. The 270+271 gate didn't run the plugins suite.
|
||||
|
||||
REQ-PLUG-2 (`docs/prd/mosaic-stack.md:1001`) sends new work that calls out to a part of a plug-in kind
|
||||
through that kind from the start. Row 270 was new work, so its pi reading belongs in the pi harness
|
||||
module, not in core.
|
||||
|
||||
Change:
|
||||
|
||||
- The harness kind (`packages/plugins/src/kinds.mjs:52`) gains an export that collects a session
|
||||
store's compactions. A core change through `kinds.mjs` is in scope, as are its callers and its
|
||||
contract in `packages/plugins/README.md`. The design names the export, its arguments, its answer,
|
||||
and whether `interfaceVersion` moves.
|
||||
- pi's module owns the stream reading (`scanPi`, `PI_VERSION`, the Claude-history check) and answers
|
||||
what `collectCompactions` writes for pi today. claude-code's module answers harness-unsupported, as
|
||||
it does now.
|
||||
- Core keeps the ledger: `readLedger`, `compactionCount`, the O_APPEND write and the `collected` line.
|
||||
No core line names a harness. `readLedger`'s unsupported test reads a field the module's answer
|
||||
carries, not a harness name.
|
||||
- `packages/cli/src/launcher.mjs:312` calls through the kind. `packages/bus/src/runtime.mjs:282` and
|
||||
`broker.mjs` keep reading counts through `compactionCount`.
|
||||
- A ledger that row 270's code wrote gives the same count after the move, including the pi,
|
||||
claude-code and Claude-history cases. Nothing rewrites a ledger.
|
||||
|
||||
Tests, red first under mos-1272:
|
||||
|
||||
- a fixture ledger written by the current code reads the same count before and after the move, one
|
||||
per reason row 270 writes;
|
||||
- pi's module counts a fixture stream the way `scanPi` does now, with the read budget and stream cap;
|
||||
- a harness module without the export refuses at load (`packages/plugins/src/loader.mjs:61-64`).
|
||||
|
||||
`harness.test.mjs` stays unchanged and goes green. Any change to an existing test goes to Mos through
|
||||
Sage first. A new required export breaks every existing test fixture that builds a harness module, so
|
||||
DESIGN.md lists those fixtures (grep the tests for `interfaceVersion` and the harness exports). Those
|
||||
edits go to Mos with the design.
|
||||
|
||||
Mutants:
|
||||
|
||||
- core branches on a harness name again;
|
||||
- pi's module drops the Claude-history check;
|
||||
- `readLedger` treats every collection as supported.
|
||||
|
||||
Gate: Duckworth approves the design and then the manifest. On Sage's gate, the bus, cli, plugins,
|
||||
webui and business suites run green, plugins with no failures at all, along with every
|
||||
`scripts/test-*.sh`, and the mutants are killed.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Brief: Case 1/2 host fixes for the 433 P1 rerun
|
||||
|
||||
Sage, 2026-10-23, filed for Mos (mos-1532). Issue #1816, Vikunja 614. On the 10-27 path.
|
||||
|
||||
## Brief: hostcheck item 5, bundle verify, case2-unit @, stack-copy cwd
|
||||
|
||||
ops-01 found two defects in the 433 P1 Run A on VM 1013 (ops-0256). Row 240 (c515273db) introduced the first one.
|
||||
|
||||
1. `scripts/case1-hostcheck.sh:117` compares `dpkg-statoverride --list /usr/bin/bwrap` to the literal
|
||||
`root mosaic 0750 /usr/bin/bwrap`. dpkg 1.22.6 on Ubuntu 24.04 prints `750`, so item 5 FAILs on every
|
||||
correctly built host. The apt hook and mosaic-hostcheck.service then close bwrap and write DRIFT. The fake
|
||||
at `packages/cli/tests/case1-hostcheck.test.mjs:73` writes `0750`, so the suite missed it.
|
||||
2. `docs/guides/host-run-sheet.md:192` and `:209` run `git bundle verify` outside a repository, which fails on
|
||||
a fresh host.
|
||||
|
||||
The fix:
|
||||
- Item 5 accepts `750` and `0750` as the same mode for the statoverride line. Owner, group and path stay exact,
|
||||
and every other mode still fails. The stat check at :118 is unchanged.
|
||||
- The run sheet verifies the bundle inside an empty bare scratch repo, as `scripts/stack-copy.sh:91-93` does.
|
||||
A failure there prints a FAIL line and does not `exit` the interactive root shell (Morgana N1, sage-3743).
|
||||
|
||||
Tests are append-only blocks in case1-hostcheck.test.mjs (mos-1272 pre-approved by Mos: 0 lines deleted, the :73
|
||||
fake unchanged). They cover: a `750` statoverride passes; `0750` still passes; `755`, `0755`, `7500`, `075`, `00750`, a tab
|
||||
or doubled space, and a wrong owner or group fail. Any other change to an existing test goes to Mos first.
|
||||
|
||||
Scope add (mos-1534):
|
||||
3. F3: `scripts/case2-unit.sh:62` plain() rejects `@`, so the run sheet's required `--claude` path
|
||||
(host-run-sheet.md:85-89, `@anthropic-ai`) refuses on every host (ops-0258, section 10). plain() accepts `@`.
|
||||
The :61 comment doesn't list it as unsafe, so no requirement changes.
|
||||
4. F4: `scripts/stack-copy.sh` fails when run as mosaic from a cwd mosaic can't read: the sheet's `sudo -u mosaic`
|
||||
lines and stack-upgrade.sh AS_USER at :154 and :293. It works from any cwd.
|
||||
Specs and test terms are in mos-1534. Mos pre-approved append-only test blocks in case2-unit.test.mjs under mos-1272.
|
||||
One new N carries F1 to F4 for the #433 rerun.
|
||||
|
||||
Owner Gladstone. Reviewer Morgana. Gate: Morgana approves. Mutants are killed: dropping the `750` form, widening the
|
||||
match to any mode, rejecting `@` again in plain(), and the F4 cwd fix reverted. The touched test files run whole, and Sage's full landing gate runs every
|
||||
package suite and every `scripts/test-*.sh`. After landing, Mos sends ops-01 the new N for a full 433 P1 rerun.
|
||||
No hostcheck install on .53 or .54 until it lands.
|
||||
|
||||
Scope add (mos-1536, ops-0260):
|
||||
5. F5: on a host that follows the run sheet, `mosaic-bus@<business>` exits 3 because nothing writes
|
||||
`<dataRoot>/notify/<business>/notify.json` (bus cli.mjs:104-122 requires it, README :876-892), and case2-precheck
|
||||
has no line for it, so it passed with the bus down. The run sheet gains a step that writes binding `null` as the
|
||||
default, and case2-precheck gains a notify line with tests. Install does not write the file: the README wants an
|
||||
explicit decision. A named binding for .53 is a later per-host choice that needs Mos's go. Spec in mos-1536.
|
||||
Gate adds a mutant: the precheck notify line removed or passing on a missing file. #1816 carries F1 to F5.
|
||||
|
||||
Scope add (mos-1537, ops-0261):
|
||||
- F5 fixture: Mos OK under mos-1272 for the host(t) notify fixture in case2-unit.test.mjs and the world(t) one in
|
||||
case2-install.test.mjs (a 0700 notify/acme dir and a null notify.json, with no assertion changed or deleted).
|
||||
Conditions: the negative F5 tests build their host without the file; one append-only install test shows
|
||||
notify.json bytes and mode are unchanged after install; the fixture lines are their own follow-up commit.
|
||||
- Section 9a creates both `notify/` and `notify/<business>` at 0700 (ops-0261 measured a 755 parent).
|
||||
6. F6: sheet :235 runs `stack-copy.sh check`, which requires `$HOME/.npm-global/bin/mosaic` (stack-copy.sh:160-173).
|
||||
Nothing creates it: install runs only `copy` (:178), bootstrap has no .npm-global, and the sheet has no wrapper
|
||||
step. The sheet gains a step after install: as mosaic from cwd `/`, create the bin dir, then run
|
||||
`wrapper <sha> --create`, naming the Mos gate (stack-copy.sh:3). No requirement change. #1816 carries F1 to F6.
|
||||
|
||||
|
||||
## Brief: F7, the wrapper step's PATH and umask under sudo
|
||||
|
||||
Row: F7 for the 433 P1 rerun (ops-0271, mos-1544). Docs only. Builder: a Codex thread. Reviewer: a separate Codex
|
||||
thread (mos-1542). It can land with any row and is not N+1.
|
||||
|
||||
Defect, from ops-01 on 094124b4e: the run sheet's wrapper step (`docs/guides/host-run-sheet.md` :249-253) fails
|
||||
rc=127 `node: command not found`. `sudo -u mosaic -H /bin/sh` runs under sudo's `secure_path`, which drops
|
||||
`/opt/mosaic-stack/node-<v>/bin`, and `scripts/stack-copy.sh wrapper` reaches `scripts/instance.sh:78`, which calls
|
||||
bare `node`. The heredoc also sets no umask, so `$HOME/.npm-global/bin` came out 0775.
|
||||
|
||||
Change, inside the `MOSAIC_WRAPPER` heredoc only:
|
||||
|
||||
1. Before the `mkdir`, add `umask 077`, the same way the notify step does (:325).
|
||||
2. Before the `mkdir`, add `PATH=/opt/mosaic-stack/node-<v>/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH`, using the
|
||||
sheet's own PATH form (:69).
|
||||
3. Add one sentence above the block: the step runs under sudo's `secure_path`, so it sets its own PATH and umask.
|
||||
|
||||
Nothing else in the sheet changes, and no script changes. ops-01 is testing this exact form on 1013 (mos-1544).
|
||||
|
||||
Acceptance:
|
||||
|
||||
- The heredoc has `umask 077` and the PATH line before `mkdir`, with `<v>` written the same as the sheet's other
|
||||
node paths.
|
||||
- `git grep -n 'host-run-sheet' -- 'packages/*/tests/*' 'scripts/test-*.sh'` shows no test that reads the changed
|
||||
lines (positive control: a grep for a path a test does read). If a test does read them, stop and report.
|
||||
- BUILD.md under `agents/<seat>/work/queue-<row>/` records the diff and the grep.
|
||||
|
||||
F10, added to this row (ops-0274, mos-1548). Both are confirmed at 094124b4e.
|
||||
|
||||
- After a first install, the sheet has no `stack-release.sh adopt <business> --instance <i> --units external` step,
|
||||
so the first activate refuses `no-pointer` (`scripts/stack-release.sh:1297`, "no $POINTER; run adopt first"). Add
|
||||
that step where the sheet first moves from install to activate or upgrade. It runs on Mos's go, and Jason or Mos runs
|
||||
it (`stack-release.sh:14`).
|
||||
- The sheet doesn't say that the first `stack-upgrade.sh upgrade` after adopt needs `--no-rehearsal`. Adopt writes no
|
||||
backup, so without the flag the upgrade refuses `no-backup` (`scripts/stack-upgrade.sh:324-325`), and with the flag
|
||||
on a stack that has a backup it refuses `rehearsal-required`. Say so next to the upgrade command (sheet :210-223):
|
||||
only the first upgrade after adopt takes `--no-rehearsal`.
|
||||
|
||||
ops-01 is testing both on 1013 (mos-1548). F10 is also docs only. The grep in the acceptance list covers its lines too,
|
||||
and BUILD.md records them.
|
||||
|
||||
## Brief: F8 and F9, the library persona import runs without the clone's node_modules
|
||||
|
||||
Row: F8 and F9 for the 433 Run B rerun (ops-0273, mos-1546). Builder: a new Codex thread (mos-1542). Reviewer:
|
||||
Morgana (Claude), because the row touches the install path (mos-1542 rule 4). When it lands, ops-01 reruns Run B P1
|
||||
at that commit.
|
||||
|
||||
F8, confirmed at N = 094124b4e: `scripts/case2-install.sh:185` runs
|
||||
`node "$REPO/scripts/case2-persona-import.mjs"` from the clone (`REPO`, :38). `case2-persona-import.mjs:32` imports
|
||||
`yaml`, and a fresh clone has no `node_modules`, so the import fails `ERR_MODULE_NOT_FOUND`. The copy just made at
|
||||
`$COPIES/$COMMIT` (:178) has `node_modules` (mos-1546). ops-01 reproduced `ERR_MODULE_NOT_FOUND` from a checkout
|
||||
with no `node_modules`.
|
||||
|
||||
Fix F8 one of two ways, and say in BUILD.md which and why: run the importer from the copy
|
||||
(`$COPIES/$COMMIT/scripts/case2-persona-import.mjs`), or make node resolve its modules from the copy. The importer's
|
||||
relative import of `../packages/harness/src/bundle.mjs` must still resolve to the same commit's file. Whatever you
|
||||
pick has to work when the clone has no `node_modules` at all.
|
||||
|
||||
F9: :186 keeps only `tail -n 1` of the importer's output. For an uncaught node error that line is the
|
||||
node banner (`Node.js v24.22.1` on 1013, mos-1546), so the refusal hides the cause. Make the refusal carry the error line, for example the first line
|
||||
matching `Error` or `ERR_`, falling back to the last line. Keep it to one line. Don't put a full stack trace in the
|
||||
refusal.
|
||||
|
||||
Class check, same row: at N, `grep -n 'node "$REPO/' scripts/case2-install.sh scripts/case2-precheck.sh
|
||||
scripts/stack-copy.sh` finds only :185. Rerun that grep. Also list every clone-side `.mjs` those three scripts run
|
||||
that imports a package (a bare specifier, not `node:` or a relative path), and record the list in BUILD.md and the
|
||||
review request. Fix any other hit in this row only if it is the same defect, and say so.
|
||||
|
||||
Test: add a new, append-only test block to `packages/cli/tests/case2-install.test.mjs`. It runs the library persona
|
||||
import with no `node_modules` beside the script, and must fail at N. It needs no mos-1272 OK (mos-1546). Add a second
|
||||
assertion that the F9 refusal carries the error line, not `Node.js v...`. In BUILD.md, explain why the existing
|
||||
case2-install tests missed this. ops-01 thinks the checkout's `node_modules` hid it; check that.
|
||||
|
||||
Gate: Morgana approves the manifest. Every package suite (except queue) and every `scripts/test-*.sh` are green on
|
||||
Sage's gate, the new test fails on N, and a mutant that puts back `$REPO` at :185 is killed by the new test.
|
||||
@@ -0,0 +1,82 @@
|
||||
# PRD 1.17 brief: Case 2 story files are DOCX
|
||||
|
||||
Sage, 2026-10-23, for mos-1366 item 1, mos-1467 and mos-1470. This follows
|
||||
`docs/plans/BRIEF-TEMPLATE.md`. Gitea #1815, Vikunja 570.
|
||||
|
||||
## PRD 1.17 Case 2 story files are DOCX (Q235 to Q237, Q239)
|
||||
|
||||
### Problem
|
||||
|
||||
Jason's Q235 to Q237 make Case 2 story files DOCX, with Markdown kept for notes
|
||||
and docs. PRD 1.16 still says every story file is Markdown in the user's
|
||||
repository, seen in Obsidian. The 1.17 text is approved: Jason approved d8 at
|
||||
435a7458d in Q239 with O2, which adds the Console Connections view (mark J-b).
|
||||
d9 carries that view in Mos's mos-1443 R3 text. Webby approved d9 in rounds 7
|
||||
and 8 (webby-0214, webby-0217), and Mos passed the J-b check in mos-1470.
|
||||
|
||||
The approved text is `agents/sage/work/prd-1-17/DRAFT-d9.md` at 4607e15b8, blob
|
||||
sha256 02f28decad7f946f. Its base is PRD 1.16, `docs/prd/mosaic-stack.md` blob
|
||||
a6db9b4a at 482e865ac (row 291).
|
||||
|
||||
### Owner and reviewer
|
||||
|
||||
- Owner: Gosalyn.
|
||||
- Reviewer: Webby.
|
||||
- After: row 291 (PRD 1.16).
|
||||
|
||||
### Files owned
|
||||
|
||||
The four files in DRAFT-d9.md's "Files the row would own", as it lists them:
|
||||
- `docs/prd/mosaic-stack.md`.
|
||||
- `packages/tasks/requirements.json`: version 1.17, the titles of REQ-LIB-1,
|
||||
REQ-LIB-2 and REQ-LIB-4, and the new ids REQ-LIB-8 and REQ-LIB-9 after
|
||||
REQ-LIB-7 and before REQ-INST-1.
|
||||
- `packages/tasks/tests/requirements.test.mjs`: the version pin 1.17, the count
|
||||
87 (:21), and the duplicate-id message `requirements[87].id: REQ-ROLE-1 twice`
|
||||
(:77).
|
||||
- `packages/bus/tests/requirements.test.mjs`: the list length 87 (:48).
|
||||
|
||||
### What ships
|
||||
|
||||
- Every "Replace", "Add, after", "becomes" and "New" instruction in DRAFT-d9.md's
|
||||
PRD sections, applied to the 1.16 text word for word. Each quoted old text
|
||||
occurs once in a6db9b4a, except "The Console shows each cite's scope.", which
|
||||
d9 scopes to REQ-LIB-2. Webby checked those counts in round 6.
|
||||
- The 1.17 header, Status line, source lines and notes as d9 writes them.
|
||||
- requirements.json as d9's requirements.json note says: REQ-LIB-1, REQ-LIB-2 and
|
||||
REQ-LIB-4 take the new titles, REQ-LIB-5 and REQ-LIB-6 keep theirs, and
|
||||
REQ-LIB-8 and REQ-LIB-9 are added with the titles d9 gives, so the count is 87.
|
||||
REQ-LIB-4's title is 117 characters, under the 120 limit in `requirements.mjs`
|
||||
(mos-1443 R2).
|
||||
- Nothing from d9's review sections goes into the PRD. That covers "Settled
|
||||
points", the "d2 changes" through "d9 changes" lists, "What d5 adds", "Marked
|
||||
for Jason" and the impact map notes.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- The build rows for Vikunja 570 and 571, which Sage files after the landing.
|
||||
- The queue changes in the impact map (A24, A57 to A59), which go through
|
||||
`scripts/mosaic queue` (C9).
|
||||
- Gyro's design edit R3.4 at 767bb62c7.
|
||||
- Mos's build-order call (mos-1470): the Connections view lands before the first
|
||||
real chapter import opens. That belongs to Gyro's 571 rows, not the PRD.
|
||||
|
||||
### Gate
|
||||
|
||||
- Webby checks the candidate's PRD diff against DRAFT-d9.md, one instruction at a
|
||||
time, and checks that nothing else changed.
|
||||
- No new Jason question. Q239 approved the text, and a change beyond d9 would
|
||||
need a new version and his approval.
|
||||
- On Sage's gate:
|
||||
- tasks, run whole;
|
||||
- bus, run whole, because the broker loads requirements.json;
|
||||
- cli `stack-copy.test.mjs`;
|
||||
- every `scripts/test-*.sh`, under `timeout -k 10`.
|
||||
- Mos gets the landing commit (mos-1470).
|
||||
|
||||
### Handoffs
|
||||
|
||||
Every seat hands off directly (DECISIONS Q210, mos-1221). When the owner pushes a
|
||||
round or the reviewer posts a verdict, that seat messages the next seat's T3
|
||||
thread and Sage's with `mcp__t3__t3_send_message`. Mos has one step here, which
|
||||
is receiving the landing commit.
|
||||
@@ -644,3 +644,70 @@ Open follow-up (dewey-0913 N1). Row 219 (#1720, C2-09a) lands on 5fdfd7275 with
|
||||
## Row295 N1: a revoked run's late bus call exits 22, not stopped
|
||||
|
||||
Open follow-up (morgana-0167, sage-3318, sage-3360), to become a row. Row 295 (#1801) lands on 2efd127ce. Revocation ends a user's thread claim in one transaction, and the launcher then sends SIGTERM. A run that calls the bus between the claim end and the SIGTERM gets not-holder and exits 22, so its record reads error rather than stopped. Row 302's launcher-released answer covers only threadIdle and landRelease. The row gives a revoked claim the same shape: calls that need the claim answer thread-revoked until the run ends, and the runner maps that answer to stopped. It needs a forcing test that holds the window open and a mutant the test kills. Brief: docs/plans/2026-10-23_gate-env-rows.md; owner Morgana, reviewer Dewey, after row 272.
|
||||
|
||||
|
||||
|
||||
|
||||
## Row272 N1: users can't reach session verbs
|
||||
|
||||
Open follow-up (Huey's BUILD.md, sage-3357), to become a row. Row 272 (#1777) lands the session CLI and the Console chooser for owners. USER_VERBS still leaves out the session verbs, and no transport carries a user's session operation. A granted user's Console therefore falls back to the thread default when session.list answers access-refused on a route with no session named. A session link with an explicit session still refuses, and the session controls stay hidden. The row that adds user session grants (193/T5) adds the verbs to USER_VERBS under a grant check, and adds a test that a granted user lists, opens and clears only sessions on threads the grant covers. It removes the fallback once the grant exists.
|
||||
|
||||
## Row270 N6 rebound: the ledger and the session.list read are still unbounded after 272
|
||||
|
||||
Row 272 (#1777) lands without bounding the compaction ledger or the whole-file read on session.list, and it records no reason the 64 MiB collection cap is enough. N6 above stays open. It moves to the row after 307 (#1814, Vikunja 598), because 307 reshapes the compaction reading in packages/seat/src/compactions.mjs and both rows would edit readLedger. That row bounds the ledger or the read, with a test and a mutant, or records why the cap is enough.
|
||||
|
||||
## Row307 N1: row 270's store-unreadable doesn't stick
|
||||
|
||||
Duckworth, row 307 build round 1 (duckworth-0285, note d43690d0b). When a collection finds an unreadable
|
||||
session store header, it writes store-unreadable. The cursor then moves past the bad header, so the next
|
||||
collection never checks it again and the count reads as known with 0. The row 307 ledger test pins this
|
||||
behavior as it stands, so the move didn't change it. Bound to a follow-up row on row 270's
|
||||
store-unreadable path. The fix keeps the unreadable state until the header reads clean, or re-checks the
|
||||
header on every collection. Either way it's an old-test change and goes to Mos.
|
||||
|
||||
## Row306 N1: the revoke Set doesn't survive a broker restart
|
||||
|
||||
Morgana, row 306 BUILD.md (#1813, landed 2d0318a44; Dewey agrees, dewey-0918). `#accessRevoked` lives in
|
||||
memory. After a broker restart, recover() rebinds an earlier host's run, and if that run makes a bus call
|
||||
before the launcher's SIGTERM, it answers `not-holder` and the run exits 22, as every revoked run did before
|
||||
306. Seeding the Set at a rebind needs a mark only `#revokeLost` writes; `reason` and `by` on the ended
|
||||
claim aren't one (design D1's alternative). Bound to the next row that changes the broker's recover() path.
|
||||
|
||||
## Row306 N2: a pending limit stop answers before the revoke
|
||||
|
||||
Morgana, row 306 BUILD.md (#1813, landed 2d0318a44). A revoked run that also has a pending limit stop gets
|
||||
`#limitStop`'s `not-holder` on `message.receive` and `state`, because that check runs before `#agent`. It's
|
||||
rare and the run still stops. Bound to the next row that changes `#limitStop` or the order of those checks.
|
||||
|
||||
## Row226 N1: the install id regex is narrower than ID_PATTERN
|
||||
|
||||
Webby, row 226 round 1 (webby-0225, #1727, landed 661890e90). case2-install.sh:56 refuses a business id
|
||||
with `.` or `_`, which ID_PATTERN (vocabulary.mjs:199) and case2-precheck.sh:67 accept. Gosalyn confirmed
|
||||
it isn't deliberate (gosalyn-0208). It fails closed, so a real id with either character is refused, not
|
||||
mishandled. Bound to the next row that changes case2-install.sh: take the ID_PATTERN set, and add a test.
|
||||
|
||||
## Row226 N2: a first-run validate refusal leaves personas and the slot dir
|
||||
|
||||
Webby, row 226 round 1 (webby-0225). After a validate refusal on a first run, the staged personas and the
|
||||
empty token slot dir stay behind, and a re-run counts them as present. Bound to the same follow-up as N1:
|
||||
remove what this run created, as the business file path already does (install.sh:198/:202/:218).
|
||||
|
||||
## Row226 N3: the run sheet doesn't pin the clone to the copy's sha
|
||||
|
||||
Webby, row 226 round 1 (webby-0225). install.sh:217 validates with the checkout's scripts/mosaic, not the
|
||||
copy's, and host-run-sheet.md:237 never says the clone must be at `<sha>`. Bound to the same follow-up:
|
||||
a HEAD check in the install or one clause on the run sheet.
|
||||
|
||||
## Row226 N4: --library-status refuses after a 5 s lock wait
|
||||
|
||||
Webby, row 226 round 1 (webby-0225). If the bus holds the library lock for more than 5 s,
|
||||
`case2-precheck.sh --library-status` refuses with `ERROR library-sync-busy`. A re-run clears it. Recorded
|
||||
as a known operator step, no fix bound.
|
||||
|
||||
## Row283 N1: SURVEY-RUN.md shows the scopes line without supporting
|
||||
|
||||
Webby, row 283 round 1 (webby-0267, #1788, review note queue-283/REVIEW-manifest-b2a1f4092.md on
|
||||
webby/reviews c6ea27aab). docs/case2/SURVEY-RUN.md:96 documents the `--mapping` line as
|
||||
`scopes canon .., playground .., unmapped ..; indexed ..`, which predates the supporting scope 283 adds.
|
||||
Nothing reads the file (decision 137). Bound to the next docs commit that touches SURVEY-RUN.md: add the
|
||||
supporting count to that line.
|
||||
+38
-22
@@ -33,7 +33,7 @@ Gaps found while working go to `docs/plans/DEFERRED.md`, not here.
|
||||
|
||||
<!-- mosaic-queue:begin -->
|
||||
|
||||
Generated from `docs/plans/queue.json` revision 3530 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
Generated from `docs/plans/queue.json` revision 3732 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
|
||||
|
||||
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
@@ -252,18 +252,18 @@ Generated from `docs/plans/queue.json` revision 3530 by `queue render`. Do not e
|
||||
| 213 | Case 2 C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file | morgana; reviewers webby | #1714 | done | — | Webby approves the manifest. The business, bus and harness suites, `scripts/test-config.sh` and every `scripts/test-*.sh` are green on Sage's gate, and the `models` mutant is killed. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-08 creative roles, persona file and block, M1, M2, M6, M10, prompt passed by file | LANDED 361ba2387 (39 paths incl. 4 packet files). Patch-id f313e69b6f7e = c771a36f4..2b3e67e36; verify-commit ok, 35 manifest paths. Same combined gate as 195: business 200/0 bus 542/0 harness 140/0 cli 663/0, test-config 31/0 and all 10 scripts green, models mutant L1 killed. Webby N2 is row 235 acceptance (mos-1163). |
|
||||
| 214 | Case 2 C2-11a editor wikilink node, after measuring whether [[ round-trips | herb; reviewers filbert | #1715 | in-progress | — | Filbert approves the manifest. The webui suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-11a editor wikilink node, after measuring whether [[ round-trips | — |
|
||||
| 215 | Case 2 C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status | herb; reviewers filbert | #1716 | done | — | Filbert approves the manifest. The business, seat, cli, project, bus and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-04 the library entry in the business file, consumer skips, M9, mirror, library.file and library.status | LANDED e54e05463 on 6096b051b (sage-2094). Patch-id 2f68188b6c9d equals a36523713..dde3445ab; 118 paths, blobs and modes equal; tree 5b1613245 equals merge-tree. No path overlap with rows 66/243; no hash pins either way. Gate: Filbert APPROVE rev 2924; herb-0095 grant sage-2071 2634 pass 0 fail, 1 webui skip (editor rebuild needs esbuild 0.28.2, env, not a 215 path), accepted. |
|
||||
| 216 | Case 2 C2-05a lookup index and queries | researcher; reviewers filbert | #1717 | briefed | 268 done | Filbert approves the manifest. The library suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05a lookup index and queries | Rulings on phase A: agents/sage/work/queue-216/RULINGS.md (da9555ca2). r2 rebases on row 268's landing and adds the four PRD 1.14 playground tests. |
|
||||
| 217 | Case 2 C2-06 the citation guard, the page store, the runner texts and the retry | launchpad; reviewers webby | #1718 | briefed | 268 done | Webby approves the manifest. The library, bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-06 the citation guard, the page store, the runner texts and the retry | — |
|
||||
| 216 | Case 2 C2-05a lookup index and queries | researcher; reviewers filbert | #1717 | done | 268 done | Filbert approves the manifest. The library suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05a lookup index and queries | LANDED fbe57a05b (replay of r5 dae76ccc2 onto 8e2885d26, 3 commits, 0 patch-id mismatches), pushed 21:09Z with 582b86935. Sage joint gate GREEN 21:05:34Z on fbe57a05b, every package suite but queue and every scripts/test-*.sh green. Filbert APPROVE r2 c31384 rev 3697. N3 (get returns a universe own supporting files) binds the C2-05c row; N1 doc, N2 universeInfo echo and BUILD.md pronoun nit go to a later docs commit. |
|
||||
| 217 | Case 2 C2-06 the citation guard, the page store, the runner texts and the retry | beakley; reviewers webby | #1718 | briefed | 268 done, 283 done | Webby approves the manifest. The library, bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-06 the citation guard, the page store, the runner texts and the retry | — |
|
||||
| 218 | Case 2 C2-12 the setup sheet for Melanie and the survey run sheet | sage; reviewers webby | #1719 | done | — | Doc only. Webby approves it against C2-01's real flags. Before landing, grep `packages/*/tests` and `scripts/test-*.sh` to confirm no test reads either file. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-12 the setup sheet for Melanie and the survey run sheet | LANDED f8276c5eb (pushed, on Darkwing sessions b65f3a0e8). Webby APPROVE r1-r3 (revs 2848, 2856, 2860; r3 candidate 1a38baf5af2a = 89ba79d94). Patch-id 18ba1e40ad29 seat 5c6f73ed7..89ba79d94 equals landed b65f3a0e8..f8276c5eb; three blobs equal 89ba79d94. Gate: doc only, 0 test readers (control 3). Melanie handover waits on C2-06 and Jason (#419). |
|
||||
| 219 | Case 2 C2-09a one thread per creative instance | rocko; reviewers dewey | #1720 | done | — | Dewey approves the manifest. The bus, cli and webui suites are green on Sage's gate, along with every `scripts/test-*.sh`. Watch for hash pins on `bus.js` and `broker.mjs` (`packages/bus/tests/channel-proof.test.mjs` pins a webui file). (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-09a one thread per creative instance | LANDED 5fdfd7275 on refactor 10:40Z 10-23 (8 row commits cherry-picked onto 2efd127ce, range-diff all =, patch-ids match, tree e86a84a85 = gated merge 054da642f; top commit packet-only evidence). Gate manifest 4a2271dd: node 2523/0/2, ten shells 449/0, 24 mutants killed, audit ok (Rocko). Dewey approve rev 3507 c31108. N1 U6 persona label DEFERRED line follows. Slug fix Vikunja 590 and network leak 591 rows next. |
|
||||
| 220 | Case 2 C2-10a no raw session view (M7) and no raw Discord engine (M8) | researcher; reviewers filbert | #1721 | briefed | — | Filbert approves the manifest. The conversation and discord suites and `scripts/test-discord.sh` are green on Sage's gate, along with every `scripts/test-*.sh`. No test reaches a Discord server. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10a no raw session view (M7) and no raw Discord engine (M8) | Rulings on phase A: agents/sage/work/queue-220/RULINGS.md (da9555ca2). Scope adds packages/business/src/library-seats.mjs (imported by path, no index export), its test, and control-board serve.mjs. r2 building as a Haiku subagent. |
|
||||
| 220 | Case 2 C2-10a no raw session view (M7) and no raw Discord engine (M8) | researcher; reviewers filbert | #1721 | done | — | Filbert approves the manifest. The conversation and discord suites and `scripts/test-discord.sh` are green on Sage's gate, along with every `scripts/test-*.sh`. No test reaches a Discord server. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10a no raw session view (M7) and no raw Discord engine (M8) | LANDED fbe57a05b (replay of r5 739b868b6 onto 93694c518, 9 commits, 0 patch-id mismatches; tip 8e2885d26 for 220), pushed 21:09Z with 582b86935. Sage joint gate GREEN 21:05:34Z on fbe57a05b: every package suite but queue 0 fail 0 skipped, every scripts/test-*.sh rc 0. Filbert APPROVE r2 c31383 rev 3696. Researcher r5 reruns and mutants green. Filbert N1 (route test catalogue only) open, not blocking. |
|
||||
| 221 | Case 2 C2-20 Bean and Chet persona import, with a count-only dry run | gosalyn; reviewers webby | #1722 | done | — | Webby approves the manifest. The test file runs green on its own and the cli suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-20 Bean and Chet persona import, with a count-only dry run | LANDED 300766450, pushed. Cand 8f403d0d4, manifest 20f05e3a6ad5, 22 paths, verify-commit ok, patch-id dfbb754c6fb4 equals 62ddc96cc..8f403d0d4. Gate 11:07:50-11:19Z on b1fece6f0: test file alone 11/0, harness 140/0, cli 677/0, all scripts green. Brief Gate met: webby-0073 approve, test file alone, cli, scripts. No live run in the Gate; the import itself waits on Jason. N1 N2 N4 go to C2-20b, N3 to DEFERRED. |
|
||||
| 222 | Case 2 C2-03a fetch-only sync | megavolt; reviewers dewey | #1723 | done | — | Dewey approves the manifest. The library and bus suites are green on Sage's gate, along with every `scripts/test-*.sh`, and the mutant is killed. A search of the test run's output for the fixture token finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-03a fetch-only sync | sage-2024 on megavolt-0089: core.createObject=rename in the fetch argv is accepted as a brief departure, since C2-04 inspectMirror refuses nlink 2. Dewey checks it. The askpass env keys, export SAFE in project/src/git.mjs, bus/tests/library-sync.test.mjs and the SURVEY-RUN user line are accepted. The 5 s client timeout residual goes in BUILD.md and Dewey rules fix or defer. Slot after 197 proof 3 and the 249 gate. |
|
||||
| 223 | Case 2 C2-07 runner retry on a guard refusal, and the runner's notices | researcher; reviewers filbert | #1724 | blocked (from briefed): Q226: C2-07 folded into C2-06 (row 217). No work here; Jason parks it. | — | Filbert approves the manifest. The harness suite is green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-07 folded into C2-06 | — |
|
||||
| 224 | Case 2 C2-05b broker lookup verbs and typed tools | della; reviewers dewey | #1725 | in-progress | 216 done | Dewey approves the manifest. The bus and harness suites are green on Sage's gate, along with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05b broker lookup verbs and typed tools | — |
|
||||
| 225 | Case 2 C2-10b Console chat: replies, citations, write notices and the status line | researcher; reviewers dewey | #1726 | briefed | — | Dewey approves the manifest. The webui suite is green on Sage's gate, along with every `scripts/test-*.sh`. Grep for hash pins on `bus.js` and `serve.mjs` before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10b Console chat: replies, citations, write notices and the status line | — |
|
||||
| 226 | Case 2 C2-13 install script and precheck for the host | gosalyn; reviewers webby | #1727 | in-progress | 215 done, 221 done, 222 done, 230 done | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every `scripts/test-*.sh`. The run on the host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13b install script and library precheck | — |
|
||||
| 227 | Case 2 C2-14 end-to-end acceptance on a scratch root | launchpad; reviewers dewey | #1728 | briefed | — | Dewey runs the script and approves. It is green on Sage's gate with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-14 end-to-end acceptance on a scratch root | — |
|
||||
| 225 | Case 2 C2-10b Console chat: replies, citations, write notices and the status line | fethry; reviewers dewey | #1726 | briefed | 217 done | Dewey approves the manifest. The webui suite is green on Sage's gate, along with every `scripts/test-*.sh`. Grep for hash pins on `bus.js` and `serve.mjs` before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-10b Console chat: replies, citations, write notices and the status line | — |
|
||||
| 226 | Case 2 C2-13 install script and precheck for the host | gosalyn; reviewers webby | #1727 | done | 215 done, 221 done, 222 done, 230 done | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every `scripts/test-*.sh`. The run on the host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13b install script and library precheck | landed 661890e90 (replay of bfed0aa6c 893b4fd25 f3fabf51b onto 8352da88c, patch-ids equal, four blobs and modes equal to f3fabf51b) and fd3d2a99f (Darkwing 8a6321790 replay, mos-1519), tree 78fc5ad21 equals merge-tree. Webby APPROVE rev 3641. Sage gate GREEN 19:56Z on f3fabf51b: 17 package suites and 10 scripts, 0 fail. Webby N1-N4 DEFERRED e2b873390. #1727 closed. |
|
||||
| 227 | Case 2 C2-14 end-to-end acceptance on a scratch root | gizmoduck; reviewers dewey | #1728 | in-progress | — | Dewey runs the script and approves. It is green on Sage's gate with every `scripts/test-*.sh`. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-14 end-to-end acceptance on a scratch root | — |
|
||||
| 228 | Row A: ZAI_API_KEY passes through ENV_ALLOW to sessions, README line, pass-through test (XS, Haiku high) | researcher; reviewers webby | #1729 | done | — | Webby approves the manifest. The mutant is killed. The seat suite and the cli suite are green on Sage's gate with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § Brief: row A, ZAI_API_KEY in the session env allowlist | Landed 86249ac55 (pushed 07:25Z) on 59f817c32: the 3 manifest paths from cf18c42b9. Patch-id b6ffe31feb0d equals the candidate's; modes and blobs match; verify-commit ok (3 paths). Sage's combined 228+238 gate on c515273db plus both rows' 6 paths, 07:11:30Z to 07:23:34Z: seat 165/0, cli 640/0, all ten scripts/test-*.sh 585/0. |
|
||||
| 229 | Row B: per-instance pi agent dir for Chet's Codex login, design only, starts after 10-27 (#428) | gosalyn; reviewers webby | #1730 | briefed | — | A live check, with Jason's or Mos's go, that two clients on one ChatGPT login don't revoke each other's refresh tokens. No build row starts before it passes. Held until after 10-27. (sage) | `docs/plans/2026-10-20_case2-rows.md` § Brief: row B, a per-instance pi agent dir for Chet's Codex login (#428) | — |
|
||||
| 230 | Case 2 C2-13a unit render and host precheck (case2-unit.sh, case2-precheck.sh) | gosalyn; reviewers webby | #1732 | done | — | Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every scripts/test-*.sh. A run on a host is a separate window with Jason's go. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-13a unit render and host precheck | Landed c8bc5cf4c on refactor (base 390ea5261), pushed. Patch-id f4b49eb68997 matches git diff 722136772..96768766c on the 5 manifest paths (manifest fbb0d7e42c67, Webby approve r2 rev 2563). packages/cli/README.md 3-way with row 199, hunks apart. Gosalyn gate on 96768766c: cli 541/0, scripts 429/0, mutants 10/10. Merged tree check: case2-unit 30/0, host 42/0. |
|
||||
@@ -275,7 +275,7 @@ Generated from `docs/plans/queue.json` revision 3530 by `queue render`. Do not e
|
||||
| 236 | #436b Gitea grant path build: seat accounts and tokens through an approved grant, schema 3j-3k (design r5 1be535aca, due 10-27 at risk; live waits for Jason's section 8 approval and bootstrap step) | megavolt; reviewers dewey | #1742 | done | 210 done, 234 done | Dewey approves the manifest. The bus, business and cli suites and every scripts/test-*.sh are green on Sage's gate. Section 9's mutants are killed, N9's restart test included. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: #436b Gitea grant path, build (schema 3j-3k) | Landed bd5f11ded on refactor, a fast-forward on be2b008cc. It is 08840f3c1 rebased past 194 (commit 1 unions only, 2-7 =) plus 2 stale-pin fixes and a gitea-not-configured raise test (sage-2559). Dewey approve on manifest 0b00269b. Megavolt gate under Sage CLEAR: 2c green, bus 765, business 228, cli 832, ten scripts rc=0, mutants 40/40; skips named, env-only. Overlap 13/257 clean. Schema 3j-3k. #1742 closed. |
|
||||
| 237 | Row 199 follow-up N1: one decision publishes once (publish-busy or #spent recheck) | megavolt; reviewers duckworth | #1743 | done | 199 done | Duckworth approves the manifest. The bus suite and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 199 follow-up N1, one decision publishes once | Landed d7d943199 on refactor (local, push pending), patch-id c5f4beb4 = e42415935, manifest bb777cb0 blobs checked. Sage gate 01:31-01:44Z on 816d48b03+4 paths: M1 killed (bus 1/13, cli 1/8), every package suite and scripts/test-*.sh green (bus 478, cli 564). |
|
||||
| 238 | row 200 follow-up N1: the sso-config hint carries --instance NAME (#1744) | launchpad; reviewers webby | #1744 | done | 200 done | Webby approves the manifest; stack-release.test.mjs whole, cli suite and every scripts/test-*.sh green on Sage's gate; the mutant killed (sage) | `docs/plans/2026-10-20_stack-release-sso.md` § Brief: row 200 follow-up N1, the sso-config hint names the instance | Landed d49b1f2bd (pushed 07:25Z) on 86249ac55: the 3 manifest paths from 4c90099ea. Patch-id b9d0bb0ee3c7 equals the candidate's; modes and blobs match; verify-commit ok (3 paths). Sage's combined 228+238 gate on c515273db plus both rows' 6 paths, 07:11:30Z to 07:23:34Z: seat 165/0, cli 640/0, all ten scripts/test-*.sh 585/0. Mutant (sso-config drops --instance) killed, survived none. Round 1 N1 stays on the #1744 carry list. |
|
||||
| 239 | Row 194 follow-up: the credential revoke runs under the writer's lock, refusals after a revoke say so, and a stale-revision fire test (duckworth-0074, mos-1158) | honker; reviewers duckworth | #1745 | in-progress | 194 done | Duckworth approves the manifest. The bus, business, cli and webui suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-lifecycle.md` § Brief: row 194 follow-up, the revoke under the writer's lock and a fire test | — |
|
||||
| 239 | Row 194 follow-up: the credential revoke runs under the writer's lock, refusals after a revoke say so, and a stale-revision fire test (duckworth-0074, mos-1158) | honker; reviewers duckworth | #1745 | done | 194 done | Duckworth approves the manifest. The bus, business, cli and webui suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-lifecycle.md` § Brief: row 194 follow-up, the revoke under the writer's lock and a fire test | LANDED e0cb1db2c (ff from 546366f4a) 22:24:58Z 10-23. Duckworth APPROVE c31422 rev 3723. Honker slot: touched files 65/65, mutants 9/9. Sage full gate GREEN 22:24:24Z: all package suites 0 fail (bus 909, cli 976, plugins 119, webui 411) and every scripts/test-*.sh rc 0; skips are queue canonical-root and task live-provider only. Overlap: 298 shares broker.mjs, runtime.mjs, instance-settings.mjs and rebases. N+1 for 433 P2/P3 (mos-1540). |
|
||||
| 240 | Installer I4: hostcheck, the bwrap profile check, pins and boot unit, from r6 B2 (MVP, Q187, Vikunja #432; split from I3) | darkwing; reviewers filbert | #1746 | done | 233 done | Filbert approves the manifest. The test file, the cli suite and every scripts/test-*.sh are green on Sage's gate, with the mutants killed. (sage) (sage) | `docs/plans/2026-10-20_installer-rows.md` § I4 hostcheck: the bwrap profile check, pins and boot unit | Landed c515273db (pushed 06:57Z) on da3051ccc: the 9 manifest paths from ad1b23ab2. Patch-id 48e376efe8a4 equals the candidate's; modes and blobs match; verify-commit ok (9 paths). Sage's gate on 060a17b54 plus the 9 paths, 06:45:05Z to 06:56:21Z: cli 639/0, all ten scripts/test-*.sh 585/0. Darkwing's r2: singles 37/0, M1-M8 8/8. Filbert N1 and N2 in DEFERRED (da3051ccc); N3 by design. |
|
||||
| 241 | Row 237 follow-up: one decision lands once, a two-ticket broker land test and three mutants on the landing guards (megavolt-0069, sage-1651, P19 441) | megavolt; reviewers duckworth | #1747 | done | 237 done | Duckworth approves the manifest. The bus, seat and cli suites and every scripts/test-*.sh are green on Sage's gate, and the three mutants are killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 237 follow-up, one decision lands once, with tests at the broker | Landed 5d4a61776 on refactor, pushed (base e65a3ab4d). Blobs from 3e3ed303e on 2 paths; patch-id a1649f80 equals git diff f5753c0e8..3e3ed303e on those paths; ls-tree modes equal; manifest e8266864 verifies on the landed files. Sage slot on bbf3b1ee8: M1-M3 killed; gate 15 packages 2797/0, 10 script suites 429/0. Duckworth approve 29870 rev 2628. |
|
||||
| 242 | Row 237 follow-up N1: a retry after publish-timeout pushes once; hold the decision or kill and wait for the push, a slow-push retry test and a mutant (duckworth-0069, P19 440) | megavolt; reviewers duckworth | #1748 | done | 237 done | Duckworth approves the manifest. The bus and cli suites and every scripts/test-*.sh are green on Sage's gate, and the mutant is killed. (sage) (sage) | `docs/plans/2026-10-20_agent-credentials.md` § Brief: row 237 follow-up N1, a retry after publish-timeout pushes once | Landed 5fb736b92 on refactor, pushed (base 96bac588f). Blobs from 635c10924 on 3 paths; patch-id ceb741ee equals git diff 8fede5fc6..635c10924 on those paths; ls-tree modes equal; manifest 1fbfab6c verifies on the landed files. Megavolt M1/M2 killed on Sage's slot; Sage gate 15 packages 2826/0, 10 script suites 429/0. Duckworth approve 29889 rev 2639. |
|
||||
@@ -288,7 +288,7 @@ Generated from `docs/plans/queue.json` revision 3530 by `queue render`. Do not e
|
||||
| 249 | row 235 follow-up: the grant undo records hostLineLeft when install never returned, since the remote add can outlive an abort or timeout (mos-1166) | gizmoduck; reviewers duckworth | #1751 | done | — | Duckworth approves the manifest. On Sage's gate the bus suite and the whole touched test files are green, and a mutant that drops the new catch branch is killed. (sage) | `docs/plans/2026-10-20_host-credentials.md` § Machine grant row b follow-up: the undo can't prove the remote add has ended | LANDED 9de5a57ab on refactor, pushed with 7b9737953 (DEFERRED N1). Range-diff: 8a04f4aa4..05595794b vs 8ca064158..9de5a57ab, same 3 paths, patch-id 99551b122e66 on both. Gate sage-2026 14:53Z: grant-executor whole 39/0, bus 598/0, no stub left, C1 (drop :1025) killed by G8 and both G23. Duckworth approve c30070. |
|
||||
| 250 | K12 (cohort.test.mjs) reads cgroup.events after cgroup.kill under load: print the shim's reason, then fix the cause at the source (Vikunja #450) | gizmoduck; reviewers filbert | #1755 | done | 197 done | reviewer approves the manifest; conversation and control-board suites whole on Sage's slot (sage) | `docs/plans/2026-10-21_k12-cohort-events.md` § Brief: K12 reads cgroup.events after cgroup.kill under load | LANDED 1b6158ce2 on refactor 19:42Z: candidate c1a858975 on 3b0aef995, manifest eca0b8e2, Filbert APPROVE rev 2957 (#1755 c30130). patch-id 29786caa = candidate; blob diff empty; tree 59f1e4781 = merge-tree. Gate on Sage slot sage-2109 (gizmoduck-0150): P1/P2/M1 as predicted, conversation whole 3x 183/183 plus concurrent pair, control-board 124/124. No drift on paths since base. |
|
||||
| 251 | T27 (host-run.test.mjs:453) lists no identities right after ssh-add -t 1: print timing and agent state, then fix the cause at the source (Vikunja #453) | darkwing; reviewers dewey | #1756 | done | 197 done | reviewer approves the manifest; bus suite whole on Sage's slot (sage) | `docs/plans/2026-10-21_t27-ssh-add-lifetime.md` § Brief: T27 lists no identities right after ssh-add -t 1 | LANDED d37210288 on b79ad5f2c: candidate 27f01ef63 manifest f2bdd684, patch-id 440d2442c976 equal, blob diff empty, tree matches merge-tree. Dewey r2 approve rev 2996 (c30168). Gate: bus suite whole in a clean worktree, 627/627 at 21:30:33Z on Sage's slot. Overlap check vs 194/198/244/253/254 branches: none. DEFERRED sleep-orphan entry follows as a doc commit. |
|
||||
| 252 | Console inbox Approve and Deny for grant requests (#435, J2) | scrooge; reviewers webby | — | in-progress | — | Webby approves DESIGN.md then manifest; webui, bus, cli suites and every test-*.sh green on Sage's gate; DESIGN mutants killed; REQ-WEB-2 PRD amendment landed first (sage) | `docs/plans/2026-10-21_console-inbox-approve.md` § Gate | mos-1193. Chained on row 196's candidate (Q177): design starts now on Gander's row 196 build; code rebases onto 196's landing. Vikunja #435. |
|
||||
| 252 | Console inbox Approve and Deny for grant requests (#435, J2) | scrooge; reviewers webby | #1753 | done | — | Webby approves DESIGN.md then manifest; webui, bus, cli suites and every test-*.sh green on Sage's gate; DESIGN mutants killed; REQ-WEB-2 PRD amendment landed first (sage) | `docs/plans/2026-10-21_console-inbox-approve.md` § Gate | Landed 45ea3fa0d (replay of Scrooge 580037105 onto 30e6bb680, range-diff all =, 10 commits). Webby APPROVE c31229 rev 3612. Sage gate on 45ea3fa0d: every package suite 0 fail 0 skipped (bus 907, cli 943, webui 410), every scripts/test-*.sh rc 0, J1-J19 all killed except J11c and J18 (equivalent per BUILD.md), all restores match. Old-test numstat 1 1 vs fe4ee8c7b held. |
|
||||
| 253 | F27: case2-unit.sh and case1-accept.sh move to --tester-socket and refuse --tester-port with stack-release's migration wording; no .53/.54 copy with 196 until this lands (mos-1198 item 4) | gander; reviewers duckworth | #1758 | done | 196 done | reviewer approves DESIGN.md then the manifest; cli and webui suites whole and every scripts/test-*.sh on Sage's slot; mutants per DESIGN.md; static check covers scripts and tests (sage) | `docs/plans/2026-10-21_tester-socket-scripts.md` § Brief: case2-unit.sh and case1-accept.sh move to --tester-socket (F27) | Landed 48e450ce8 on refactor (fast-forward from aca16404c, 22 commits; landed sha = candidate, so range-diff and patch-id are identical by construction). Manifest 591020a1 (7 paths) rechecked against the commit: 0 mismatches. Gate: run 5 whole, cli 55 files, webui 58 files, all 10 scripts/test-*.sh green; M1-M40 killed 40/40. Duckworth APPROVE round 1, Gitea #1758 comment 30696. |
|
||||
| 254 | Library sync follow-up: client timeout, STATUS codes, README (222 deferrals, #1759) | megavolt; reviewers dewey | #1759 | done | 196 done | Dewey approves the manifest; bus, library and webui suites whole and every scripts/test-*.sh green on Sage's slot; the timeout and STATUS mutants killed (sage) | `docs/plans/2026-10-21_library-sync-followup.md` § Gate | Landed 331597709 (candidate b31300d79 blobs and modes on 41b0cf755; patch-id 96216216 equal, range-diff differs only in the message) plus doc eb9672c91 (Dewey N1 README sentence closed; N2 four 502 codes to DEFERRED, binding the Console library routes row). Merged-tree rerun on Sage's slot 23:44-23:55Z: library 77/0, bus 628/0, webui 305/0 with 1 skip, cli 749/0. Megavolt's gate megavolt-0102 covers scripts and both mutants. |
|
||||
| 255 | Bundled Pocket ID for users with no OIDC provider, design (Q203, #1760) | launchpad; reviewers webby | — | queued | 252 done | Webby approves DESIGN.md; design only, build rows follow after Sage checks it against Q203, O6, REQ-WEB-2 and REQ-INST-1 (sage) | `docs/plans/2026-10-21_bundled-pocket-id.md` § Gate | Vikunja 458, Gitea #1760, mos-1202 item 2. REQ-INST-2 lands in PRD 1.10 as post-v1 (mos-1204). Design only: no install, no download, no .53/.54, dragon-lin keeps Authentik. |
|
||||
@@ -308,7 +308,7 @@ Generated from `docs/plans/queue.json` revision 3530 by `queue render`. Do not e
|
||||
| 269 | 264b T1 item 7: per-session native store, workspace and single-writer fence | gizmoduck; reviewers duckworth | #1774 | done | 264 done | Duckworth approves this slice's manifest. Its design tests run whole plus every touched suite on Sage's slot, and its mutants are killed (RULINGS Q1). Tests T1 run half, T2 workspace and path mutants; whichever of 191 and 264b lands second wires 191's admission checks (Q5). (sage) | `docs/plans/2026-10-21_t1-sessions.md` § T1 item 7: independent sessions, compaction counts and clear context | Landed efcc9ea8d fast-forward on 94c33a43c (13 commits, 12 paths, manifest 455c12ee). r3 is a carry of r2 aafcc857c, range-diff = (Duckworth 0194, c30756, rev 3307). Gate r3 18:37-18:50Z all green, 0 skips: bus 834, seat 172, cli 869, case2-unit 43, singles 38. M1-M12 kills carry. Re-pin needs the Console restart by Jason or Mos right after the host. A 264a rollback refuses a v2 sessions.json, see BUILD.md Rollback. |
|
||||
| 270 | 264c T1 item 7: compaction count, exact for Pi | gizmoduck; reviewers duckworth | #1775 | done | 269 done | Duckworth approves this slice's manifest. Its design tests run whole plus every touched suite on Sage's slot, and its mutants are killed (RULINGS Q1). Test T3 Pi half; Claude shows unknown with the Vikunja 466 reason (Q6). (sage) | `docs/plans/2026-10-21_t1-sessions.md` § T1 item 7: independent sessions, compaction counts and clear context | LANDED 7328da8ca (with 271, mos-1351) 02:13Z 10-23 on 767bb62c7: 33 commits cherry-picked from gizmoduck/row270-271-r4 49cee846e (base 25c09f19f), range-diff all '=', 40 paths identical to 49cee846e. Manifest f4174458 (15 paths) matches the tip. Duckworth APPROVE c30930 rev 3389. C2 run 5 GREEN on 53406f3e4: bus 873, seat 202, cli 905, ten scripts, 38/38 mutants killed. #1775 closes. |
|
||||
| 271 | 264d T1 item 7: clear context through row 194's lifecycle stop | gizmoduck; reviewers duckworth | #1776 | done | 194 done, 269 done | Duckworth approves this slice's manifest. Its design tests run whole plus every touched suite on Sage's slot, and its mutants are killed (RULINGS Q1). Tests T4 and T6; rebased onto 194's landing with no 194 code copied (Q4). (sage) | `docs/plans/2026-10-21_t1-sessions.md` § T1 item 7: independent sessions, compaction counts and clear context | LANDED 7328da8ca (with 270, mos-1351) 02:13Z 10-23: tip 2b964a082 is in the 33-commit pick, range-diff all '='. Manifest 3ab1f5c8 (33 paths): 29 match the tip; BUILD.md(270), bus README, launcher.mjs and compactions.mjs moved under 270 and match 270's f4174458. Duckworth APPROVE rev 3380. Combined C2 run 5 GREEN, 271 M1-M14 killed. #1776 closes. |
|
||||
| 272 | 264e T1 item 7: mosaic session CLI and Console chooser | huey; reviewers scrooge | #1777 | in-review, round 2, request posted | 264 done | Duckworth approves this slice's manifest. Its design tests run whole plus every touched suite on Sage's slot, and its mutants are killed (RULINGS Q1). Tests T5 legacy single human and T7 Console half; controls for unlanded verbs refuse with a reason (Q1, Q7). (sage) | `docs/plans/2026-10-21_t1-sessions.md` § T1 item 7: independent sessions, compaction counts and clear context | — |
|
||||
| 272 | 264e T1 item 7: mosaic session CLI and Console chooser | huey; reviewers scrooge | #1777 | done | 264 done | Duckworth approves this slice's manifest. Its design tests run whole plus every touched suite on Sage's slot, and its mutants are killed (RULINGS Q1). Tests T5 legacy single human and T7 Console half; controls for unlanded verbs refuse with a reason (Q1, Q7). (sage) | `docs/plans/2026-10-21_t1-sessions.md` § T1 item 7: independent sessions, compaction counts and clear context | LANDED d33989230: 5 row commits replayed onto 1e629c892, every non-log patch-id equal, candidate paths = bea0c8e1c. Scrooge APPROVE rev 3579 c31184. Sage round 4 gate: bus, cli, plugins, webui, business; plugins red only the known base red harness.test.mjs:192 on compactions.mjs:232/283/287 (row 270), allowed by mos-1456, fixed under row 307 Vikunja 598; M1-M6 and F1-F4 killed. DEFERRED: 272 N1 user session verbs, 270 N6 rebound. |
|
||||
| 273 | Q175 phase 2: stack-release.sh activate --send and --no-sso (F39, mos-1246) | darkwing; reviewers filbert | #1778 | done | — | Filbert approves the manifest. stack-release.test.mjs and stack-release-external.test.mjs run whole plus scripts/test-release.sh on Sage's slot, and the brief's mutants are killed. (sage) | `docs/plans/2026-10-22_activate-send-nosso.md` § Brief: activate --send and --no-sso | landed 0bbf0f6d1 03:35:55Z, FF of 378a76b85; rebase of cac7d0f67, 6 range-diff pairs =, patch-ids equal, manifest e1031023 unchanged. Gate by Darkwing on 0bbf0f6d1: stack-release.test 218/218, external 25/25, test-release.sh rc 0, T58 c0 pass c1 fail, M1-M7 killed. Filbert APPROVE rev 3111 c30308, carry c30319. Docs 5365adaef (TOOLS.md N1 + BUILD-LOG). #1778. |
|
||||
| 274 | FED R3: broker user principal (#414) | morgana; reviewers dewey | #1779 | done | — | Dewey approves the manifest. The bus and harness suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-22_federation-r3.md` § Brief: FED R3, the broker user principal | LANDED 4c485c63b ff from 47a35bf43 (6 Morgana commits). Dewey APPROVE c30487 on 4c485c63b (manifest 00047b52, 13 paths). Gate 10:09-10:22Z: 23 touched files 0 fail, bus 787, harness 151, business 228, cli 832, seat 167, 10 scripts rc=0 (skips test-queue 1, test-task 2), 22/22 mutants. M3x wall code not-holder (kit comment said human-required). r5 f58cfc361 red, void. Overlap 13/257 clean. |
|
||||
| 275 | 193b1b: viewer verbs and board refusals (also after FED R4 when filed) | morgana; reviewers duckworth | #1780 | done | 205 done, 274 done | Duckworth approves the manifest. The bus suite and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-20_human-users-build.md` § Brief: 193b1b, viewer verbs and board refusals | Landed 3410df7c3 on refactor (fast-forward from 48e450ce8, 3 commits; landed sha = candidate, so range-diff and patch-id are identical by construction). Manifest 39847b15 (4 paths) blobs match the commit. Gate: whole files 7/22/6, bus 834, cli 861, harness 151, business 231, seat 167, 0 fail; 10 scripts/test-*.sh rc 0 with 3 named skips; M6 and M10 killed. Duckworth APPROVE round 1, Gitea #1780 comment 30717. |
|
||||
@@ -319,30 +319,46 @@ Generated from `docs/plans/queue.json` revision 3530 by `queue render`. Do not e
|
||||
| 280 | Bus tests: the scratch sshd proves it owns its port (194 gate follow-up, MVP, mos-1270) | rocko; reviewers duckworth | #1785 | done | — | Duckworth approves the manifest. Both files run whole, the bus suite is green on Sage's gate, the 30-run repeat has no timeout, and M1 is killed. Row 194 b17's full gate then runs on top. Any red goes to Mos (mos-1270). (sage) | `docs/plans/2026-10-22_sshd-port-race.md` § Brief: the scratch sshd's port is ours before a test uses it | Landed b0fd28b6f 07:57:30Z, ff from da5d3651d: b765f5cb8 (Duckworth approve rev 3153, manifest 27806c05) + slim packet. Sage gate bus 656/0/0, ge 41, hr 26 (c30431). Repeat 2010/2010 under cli 1588/1588, no timeout; M1/M2 killed 4/4. Full evidence d6c924764 on rocko/row280-da5d-20261022. N2-N4 carry in BUILD.md. 194 b17 rebases on b0fd28b6f. |
|
||||
| 281 | A fire or a delete revokes the instance's host and Gitea grants (194/236 follow-up, MVP, mos-1271) | megavolt; reviewers dewey | #1786 | done | 236 done | Dewey approves the design, then the manifest. Bus, business, cli and every touched test file run whole, plus the Docker gitea-grant test and every scripts/test-*.sh, green on Sage's gate. M1-M4 killed. Lands before .53 Case 1 hires a granted instance. Red goes to Sage, then Mos. (sage) | `docs/plans/2026-10-22_fire-revokes-grants.md` § Brief: no grant outlives its instance | LANDED e479521d3 ff on 385a7194c (17 commits, manifest b73d4c81). Slot on 7979fb41e green: bus 819, business 231, cli 837, dewey-bus 76 with gitea-grant-docker run, shells green under 3x limits, M1-M22 killed. M1 hang fixed in b4a (mos-1273). Skips: test-task row 244 live-provider pair, test-queue canonical root. DEFERRED: N1 host-unknown hint verb, N2 fire tool text, to the follow-up row. Dewey APPROVE c30614. |
|
||||
| 282 | PRD REQ-LIB-6 rewritten to Q223 and Q226 (mos-1320 item 8) | gosalyn; reviewers webby | #1787 | done | — | Webby approves against the ruling. requirements.test.mjs and every scripts/test-*.sh that reads the PRD or requirements.json are green on Sage's gate. Jason gets a report. (sage) | `docs/plans/2026-10-20_case2-rows.md` § PRD REQ-LIB-6 rewritten to Q223 and Q226 (mos-1320 item 8) | Landed 647424442 on 86a665bdf: 1841f7d4e and 6b5b06819 rebased, patch-ids equal (4e50b4b6, 16a7a02f), range-diff =, tree 4f30ed10a = Gosalyn's gated trial tree. Webby APPROVE rev 3308 c30764 on 6b5b06819. Gate 19:06Z on the tip tree: bus+tasks requirements and tasks verbs 37/0/0, 0 skips; no scripts/test-*.sh reads the PRD (control ok). PRD 1.15. 1.16 is row 291, waits for Jason. |
|
||||
| 283 | Case 2 C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12) | honker; reviewers webby | #1788 | briefed | 268 done, 282 done | Webby approves the manifest. The library and business suites are green on Sage's gate, along with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12) | — |
|
||||
| 283 | Case 2 C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12) | honker; reviewers webby | #1788 | done | 268 done, 282 done | Webby approves the manifest. The library and business suites are green on Sage's gate, along with every scripts/test-*.sh. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-01d supporting folders, the supporting scope, the editor field and supportingPath (M12) | LANDED 8d2856ee3 (replay of 0bf308dba onto 59226f878, 13 patch-ids equal) plus DEFERRED N1 93694c518. Webby APPROVE r1 c31338 rev 3677, r2 c31342 rev 3683 (mos-1528 G1 mapping.test:52). Sage full gate GREEN 20:37:16Z: all package suites but queue plus every scripts/test-*.sh, 0 fail, library 98/0, webui npm ci, editor build required. supporting.test first-run red placed (mos-1272, 54d9e07bb). #1788 closed c31358. Live unchanged 45dca69f6. |
|
||||
| 284 | Case 2 C2-05c the supporting scope and blob in the lookups | della; reviewers dewey | #1789 | briefed | 224 done, 283 done | Dewey approves the manifest. Library, bus and harness suites and every scripts/test-*.sh green on Sage's gate. Grep packages/*/tests for the scope enum pin and ['canon', 'playground'] before in-review; update each. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-05c the supporting scope and blob in the lookups (sage-2833) | — |
|
||||
| 285 | Case 2 C2-08b role changes for agent writes and the chief's read grant | scrooge; reviewers webby | #1790 | briefed | 213 done, 291 done | Webby approves the manifest. Business suite, test-config.sh and every scripts/test-*.sh green on Sage's gate. Grep for CREATIVE_KINDS and 64-hex pins of changed role files. Sage checks it against mos-1309 item 5. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-08b role changes for agent writes and the chief's read grant (mos-1309 item 5) | — |
|
||||
| 286 | Case 2 C2-03b push and per-seat credentials for the library mirror (with R4, mos-1323) | ludwig; reviewers dewey | #1791 | briefed | 222 done | Dewey approves the manifest. Library and bus suites and every scripts/test-*.sh green on Sage's gate, mutant killed. A search of the run output for the fixture token finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-03b push and per-seat credentials for the library mirror | — |
|
||||
| 287 | Case 2 C2-21 the grant executor for a library project | megavolt; reviewers dewey | #1792 | briefed | 226 done, 285 done | Dewey approves the manifest. Bus suite, Gitea Docker test, install test and every scripts/test-*.sh green on Sage's gate. A search of the run output for the fixture admin and seat tokens finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-21 the grant executor for a library project | — |
|
||||
| 288 | Case 2 C2-02 agent writes, the notice, revert and her Console saves | honker; reviewers webby | #1793 | briefed | 215 done, 282 done, 283 done, 284 done, 285 done, 286 done | Webby approves the manifest. Library, bus and harness suites and every scripts/test-*.sh green on Sage's gate, mutants killed. A search of the run output for the fixture tokens finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-02 agent writes, the notice, revert and her Console saves | — |
|
||||
| 288 | Case 2 C2-02 agent writes, the notice, revert and her Console saves | bubba; reviewers webby | #1793 | briefed | 215 done, 282 done, 283 done, 284 done, 285 done, 286 done | Webby approves the manifest. Library, bus and harness suites and every scripts/test-*.sh green on Sage's gate, mutants killed. A search of the run output for the fixture tokens finds nothing. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-02 agent writes, the notice, revert and her Console saves | A2 revert contract (morgana-0199, sage-3596): library.revert is a human verb. Args {project, commit}, strict keys; project is the business.projects key (the chat repo link), not a UUID. Result {project, path, reverted, commit}. Refusals: 409 stale-base, and 409 library-revert-not-agent for an agent caller. Owner Bubba under Q245 (mos-1506). |
|
||||
| 289 | Case 2 C2-11b Console library pages and her saves (Haiku subagent, decision 129) | researcher; reviewers filbert | #1794 | briefed | 214 done, 288 done | Filbert approves the manifest. The webui suite and every scripts/test-*.sh are green on Sage's gate. Grep for hash pins on bus.js and serve.mjs before in-review. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-11b Console library pages and her saves | — |
|
||||
| 290 | Case 2 C2-12b the setup sheet rewrite for free chat, supporting folders and saves | louie; reviewers webby | #1795 | briefed | 217 done, 218 done, 285 done, 291 done | Doc only. Webby approves it against the ruling. Before landing, grep packages/*/tests and scripts/test-*.sh to confirm no test reads the file. (sage) | `docs/plans/2026-10-20_case2-rows.md` § C2-12b the setup sheet rewrite for free chat, supporting folders and saves | — |
|
||||
| 291 | PRD 1.16 library.write routes under REQ-LIB-6 (mos-1328) | gosalyn; reviewers webby | #1796 | in-review, round 1, request posted | 282 done | Webby approves the text. Jason says yes to the exact text (Mos asks him, mos-1328 step 3); no landing before that. requirements.test.mjs and every scripts/test-*.sh that reads the PRD or requirements.json are green on Sage's gate. Mos gets the landing commit. (sage) | `docs/plans/2026-10-22_prd-1-16-brief.md` § PRD 1.16 library.write routes under REQ-LIB-6 (mos-1328) | — |
|
||||
| 291 | PRD 1.16 library.write routes under REQ-LIB-6 (mos-1328) | gosalyn; reviewers webby | #1796 | done | 282 done | Webby approves the text. Jason says yes to the exact text (Mos asks him, mos-1328 step 3); no landing before that. requirements.test.mjs and every scripts/test-*.sh that reads the PRD or requirements.json are green on Sage's gate. Mos gets the landing commit. (sage) | `docs/plans/2026-10-22_prd-1-16-brief.md` § PRD 1.16 library.write routes under REQ-LIB-6 (mos-1328) | Landed 4874d1e83 on e909ef51a: 482e865ac replayed, patch-id equal (cef7de74), range-diff =. Webby APPROVE c30848 on 482e865ac; Jason approved the text in Q229. Gate 16:15Z on the tip tree: every package suite but queue, 0 fail 0 skipped (tasks 100, bus 891, cli 941, plugins 119, webui 392, business 231), every scripts/test-*.sh rc 0 (queue-root and row 244 skips only). PRD 1.16. 1.17 is row 308. |
|
||||
| 292 | stack-release.sh config_checks runs the target loader on any config (row 270 rollback hazard) | gizmoduck; reviewers duckworth | #1797 | done | 203 done, 270 done, 271 done | Duckworth approves the manifest. The touched test files run whole, the cli suite and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-22_config-checks-any-key-brief.md` § stack-release.sh config_checks runs the target loader on any config (row 270 rollback hazard) | Landed 6aab979e6 (cherry-pick of c902a767d..90be6cccd, range-diff all =, patch-id a45c9ba4 equal). Tested head 0d0021454: stack-release.test 224/224, cli suite r2 red only compactions-launch:229 session-invalid 22 (idle-stop race, Vikunja 588, row 302), r3 rerun 910/910 under mos-1411, ten shells green, M1-M9 killed. Duckworth APPROVE rev 3433 on 2fa7ae17. |
|
||||
| 293 | 574: maintenance-free test fixture commits (Vikunja 574) | gander; reviewers filbert | #1805 | done | — | Filbert approves. The four whole files, then the seat and cli suites, are green on Sage's gate. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: 574, maintenance-free test fixture commits (Vikunja 574) | LANDED e51d11640 03:55Z 10-23 (cherry-pick of gander b23783f39..a9cae2bd6 onto 464010fbe; range-diff 6/6 equal, range patch-id equal). Filbert APPROVE c30982 cdda41f5; run 2 4 files 153/0, seat 202/0, cli 905/0, 312 flagged/0 maintenance children. N1: lifecycle.test needs TMPDIR of 50 bytes or less. Residual: queue store.mjs, discord git.mjs, harness tools.mjs unflagged. #1805 closed c31013, P19 574 c888. |
|
||||
| 294 | FED R6: Console granted-user sign-in, user-cli transport, peer links | launchpad; reviewers duckworth | #1800 | done | — | Duckworth approves the manifest. The webui, cli and bus suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R6, Console granted-user sign-in | landed d3059c2c3 on refactor 07:04Z: cherry-pick of c902a767d..24b745447 onto 09d0d6faf, range-diff 11 =, patch-id 8371405a equal. Duckworth approve c31053 rev 3469 on a1ff5d67, C1/C2 met. Run 4 on 8616b9a4f from npm ci: webui 365, cli 909, bus 873, 10 scripts, M1-M11 killed. Merged tree: stack-release-external 25, stack-upgrade 21, stack-release 223/224 (R9 transient, row 1810) then 224/224 rerun under mos-1422. Follow-ups rows 300, 301. Closes #1800. |
|
||||
| 295 | FED R4: revocation | morgana; reviewers dewey | #1801 | done | — | Dewey approves the manifest. The bus, cli and business suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R4, revocation | LANDED 2efd127ce on refactor 10:10Z 10-23 (9 commits cherry-picked onto 07802dc57, range-diff all =, patch-ids match, HEAD~1 tree a86592ffc = gated tree, top commit BUILD.md doc-only). Gate d68b4300d manifest e94c9aeb: 13 whole files, bus 886/0, business 231/0, cli 924/0, harness 151/0, all test-*.sh rc 0, M1-M16 killed, M8 per mos-1436. Dewey approve rev 3500 c31102. Re-pin precheck mos-1436 cond 11 binds. |
|
||||
| 296 | FED R7: scratch two-instance acceptance | quackerjack; reviewers filbert | #1802 | briefed | 190 done, 294 done, 295 done | Filbert approves the manifest. The script test and every scripts/test-*.sh are green on Sage's gate, and no unit, port or socket of the script's outlives the run. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R7, scratch two-instance acceptance | — |
|
||||
| 297 | GitHub grant method (Bushroot's half of #436b) | bushroot; reviewers dewey | #1803 | briefed | 198 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the GitHub grant method (Bushroot's half of #436b) | — |
|
||||
| 298 | Q228: chief-of-staff grant scope | megavolt; reviewers dewey | #1804 | briefed | 252 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the chief-of-staff grant scope (Q228) | — |
|
||||
| 296 | FED R7: scratch two-instance acceptance | lena; reviewers filbert | #1802 | briefed | 190 done, 294 done, 295 done | Filbert approves the manifest. The script test and every scripts/test-*.sh are green on Sage's gate, and no unit, port or socket of the script's outlives the run. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: FED R7, scratch two-instance acceptance | — |
|
||||
| 297 | GitHub grant method (Bushroot's half of #436b) | violet; reviewers dewey | #1803 | briefed | 198 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the GitHub grant method (Bushroot's half of #436b) | — |
|
||||
| 298 | Q228: chief-of-staff grant scope | boyd; reviewers dewey | #1804 | in-review, round 1, request posted | 252 done | Dewey approves the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the mutants are killed. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: the chief-of-staff grant scope (Q228) | — |
|
||||
| 299 | 573: the P10 hop band (Vikunja 573) | gander; reviewers filbert | #1806 | done | 293 done | Filbert approves. The whole file under load at least 3 times, then the cli suite, are green on Sage's gate. (sage) | `docs/plans/2026-10-23_mvp-readiness-rows.md` § Brief: 573, the P10 hop band (Vikunja 573) | landed ffb2fedfb 08:03Z on 15e688cc0 via Filbert SESSIONS c1f910b4c; 5 commits = gander/row299-gate-277ace0b 63db96d8b (range-diff 5/5 =, tree differs only BUILD-LOG/SESSIONS); manifest 277ace0b sha256 -c OK. Gate green on ffa93b054: case1 whole 3x 53/0/0, cli whole 917/0/0 (59 files), P10 inner 2.27 outer 4.01, M1-M6 killed (gander-0189). Filbert approve rev 3485 c31084; N1 M1 1.23 s margin. |
|
||||
| 300 | R6-N1: HTTP-level test of the user-session option swap (294 N1) | launchpad; reviewers duckworth | #1807 | briefed | 294 done | Duckworth approves the manifest; new test file whole, webui suite, mutant M12 killed (sage) | `docs/plans/2026-10-23_r6-followup-rows.md` § Brief: R6-N1, an HTTP-level test of the user-session option swap | — |
|
||||
| 301 | R6-N4: fold user-bus.mjs back into bus.mjs (294 N4) | launchpad; reviewers duckworth | #1808 | briefed | 294 done | Duckworth approves the manifest; channel-proof.test, webui suite and 294 user-transport tests whole and green; re-pin only with Mos go (sage) | `docs/plans/2026-10-23_r6-followup-rows.md` § Brief: R6-N4, fold user-bus.mjs back into bus.mjs | — |
|
||||
| 300 | R6-N1: HTTP-level test of the user-session option swap (294 N1) | launchpad; reviewers duckworth | #1807 | done | 294 done | Duckworth approves the manifest; new test file whole, webui suite, mutant M12 killed (sage) | `docs/plans/2026-10-23_r6-followup-rows.md` § Brief: R6-N1, an HTTP-level test of the user-session option swap | Landed on refactor as 9d929911c..07315e651 (from launchpad/row300 a00b25b37, range-diff equal), pushed 5b846b048 12:28Z. Duckworth approved r2 (c31158, rev 3559). Sage gate on bcf3c1dac: user-option-swap 1/0, webui 375/0 with pinned esbuild and MOSAIC_REQUIRE_EDITOR_BUILD=1, M12 killed at :151. Run 1 red placed under mos-1272 (headers). N1 deferred: board proxy upstream has no recorder. |
|
||||
| 301 | R6-N4: fold user-bus.mjs back into bus.mjs (294 N4) | launchpad; reviewers duckworth | #1808 | done | 294 done | Duckworth approves the manifest; channel-proof.test, webui suite and 294 user-transport tests whole and green; re-pin only with Mos go (sage) | `docs/plans/2026-10-23_r6-followup-rows.md` § Brief: R6-N4, fold user-bus.mjs back into bus.mjs | Landed on refactor as bb031ec84..5b846b048 (from launchpad/row301 acc97fb9c, range-diff equal), pushed 12:28Z. Re-pin of webui bus.mjs to 39fcb27c is its own commit 59f0e44f4 (mos-1449); fixture stubs per mos-1451. Duckworth approved r2 (c31159, rev 3560). Sage gate: channel-proof 2/0, webui 374/0 pinned esbuild, granted-user 19/0. Live unchanged; 301 makes its own re-pin window. |
|
||||
| 302 | Idle-stop race: an idle-released run's last call ends it session-invalid/22 (Vikunja 588) | gizmoduck; reviewers dewey | #1809 | done | 292 done | Dewey approves the design, then the manifest; forcing test, compactions-launch.test whole, bus/cli/harness suites, all scripts/test-*.sh; fix-drop mutant dies (sage) | `docs/plans/2026-10-23_idle-stop-race-row.md` § Brief: the idle-stop race (Vikunja 588) | Landed 3ed9fd754 08:49Z 10-23: Sage cherry-picked 0e30a85fa..2a8281a6f (7 commits) onto 1ac655005; range-diff all equal, patch-ids match, tree equals gated merge 783602e06 plus packet-only 2a8281a6f files. Gate run 2 green (bus 876, harness 151, cli 918, ten test-*.sh, M1-M5 M7-M9 killed). Dewey approve rev 3492 c31092. mos-1430 fixed launcher-released.test:89. mos-1411 rerun allowance ends. |
|
||||
| 303 | R9 shows its output when rehearse makes no directory (Vikunja 589) | launchpad; reviewers duckworth | #1810 | in-review, round 1, request posted | 294 done | Duckworth approves the diff; stack-release.test.mjs whole and green on Sage's gate, in a gap (sage) | `docs/plans/2026-10-23_r9-guard-row.md` § Brief: R9 shows its output when rehearse makes no directory (Vikunja 589) | — |
|
||||
| 304 | claude-session test builds the memory slug the way the CLI does (Vikunja 590) | rocko; reviewers dewey | #1811 | in-review, round 1, request posted | — | Dewey approves the diff; claude-session.test.mjs whole and green 3x (mktemp, underscore, space TMPDIRs) on Sage's gate, in a gap; old-regex mutant red at :298 (sage) | `docs/plans/2026-10-23_gate-env-rows.md` § Brief: claude-session test builds the memory slug the way the CLI does (Vikunja 590) | — |
|
||||
| 305 | Shell suites remove the compose network they create (Vikunja 591) | gizmoduck; reviewers filbert | #1812 | in-review, round 1, request posted | — | Filbert approves the diff; test-release.sh and test-task.sh whole and green on Sage's gate, in a gap, project network absent after each run; dropped-rm mutant red (sage) | `docs/plans/2026-10-23_gate-env-rows.md` § Brief: shell suites remove the compose network they create (Vikunja 591) | — |
|
||||
| 306 | A revoked run's late bus call stops it instead of exiting 22 (295 N1) | morgana; reviewers dewey | #1813 | briefed | 272 done | Dewey approves the design, then the manifest; bus, harness and cli suites and every scripts/test-*.sh green on Sage's gate (sage) | `docs/plans/2026-10-23_gate-env-rows.md` § Brief: a revoked run's late bus call stops it instead of exiting 22 (295 N1) | — |
|
||||
| 303 | R9 shows its output when rehearse makes no directory (Vikunja 589) | launchpad; reviewers duckworth | #1810 | done | 294 done | Duckworth approves the diff; stack-release.test.mjs whole and green on Sage's gate, in a gap (sage) | `docs/plans/2026-10-23_r9-guard-row.md` § Brief: R9 shows its output when rehearse makes no directory (Vikunja 589) | Landed e551cd846 from launchpad 88ffecaf7, patch-id 559c802b equal, test blob e936808b matches the manifest. Gate: Duckworth approve c31079; Sage gate on c13c337e1 plus the test blob, stack-release.test.mjs whole 224/0 rc 0 in 595 s, alnum mktemp TMPDIR, default networks 6 before and 6 after. No overlap with in-review 272, 304, 305. |
|
||||
| 304 | claude-session test builds the memory slug the way the CLI does (Vikunja 590) | rocko; reviewers dewey | #1811 | done | — | Dewey approves the diff; claude-session.test.mjs whole and green 3x (mktemp, underscore, space TMPDIRs) on Sage's gate, in a gap; old-regex mutant red at :298 (sage) | `docs/plans/2026-10-23_gate-env-rows.md` § Brief: claude-session test builds the memory slug the way the CLI does (Vikunja 590) | Landed a863015a5 from rocko 5e8354ebb, range-diff equal, patch-id 808fa765. Gate: Dewey approve c31129 rev 3533; Sage gate on e551cd846 with file 7690a4a9, claude 2.1.309: claude-session.test.mjs whole 10/0 under mktemp, underscore and space TMPDIRs; old-regex mutant under underscore 9/1 at :298. Gates may drop the alnum TMPDIR rule (mos-1432 ends here). |
|
||||
| 305 | Shell suites remove the compose network they create (Vikunja 591) | gizmoduck; reviewers filbert | #1812 | done | — | Filbert approves the diff; test-release.sh and test-task.sh whole and green on Sage's gate, in a gap, project network absent after each run; dropped-rm mutant red (sage) | `docs/plans/2026-10-23_gate-env-rows.md` § Brief: shell suites remove the compose network they create (Vikunja 591) | Landed 353d65f57..d52d9f1f2 from gizmoduck 541dee2b7..7e8218a2e, range-diff all equal, on top of filbert 2c289f6f0. Gate: Filbert approve c31127 rev 3534; Sage gate on e551cd846 with task 7d6fbf67, release b29850ce: test-task 92/0, test-release 15/0, network check OK; mutants a and b red in both, c a known survivor; own mutant networks removed, none left. Gates stop the manual network rm (mos-1438 ends here). |
|
||||
| 306 | A revoked run's late bus call stops it instead of exiting 22 (295 N1) | morgana; reviewers dewey | #1813 | done | 272 done | Dewey approves the design, then the manifest; bus, harness and cli suites and every scripts/test-*.sh green on Sage's gate (sage) | `docs/plans/2026-10-23_gate-env-rows.md` § Brief: a revoked run's late bus call stops it instead of exiting 22 (295 N1) | Landed 2d0318a44 on 4874d1e83: 33d3844a0..683833205 replayed, 4 patch-ids equal (a1960f36 55fadbc3 af1a3d88 35cf81a3), range-diff all =. Dewey APPROVE c31206 rev 3596 on 683833205. access-revoke.test numstat 7 7 (mos-1463). Sage gate 16:33Z: every package suite 0 fail 0 skipped (bus 894, cli 942, plugins 119, webui 392, business 231, harness 151), every scripts/test-*.sh rc 0, M1-M3 each killed on every named file, restores equal. N1/N2 to DEFERRED next. |
|
||||
| 307 | Row 270's pi compaction reading moves behind the harness kind (REQ-PLUG-2, Vikunja 598) | gizmoduck; reviewers duckworth | #1814 | done | — | Duckworth approves the design, then the manifest; bus, cli, plugins (0 failures), webui and business suites, every scripts/test-*.sh green and the mutants killed on Sage's gate (sage) | `docs/plans/2026-10-23_harness-kind-compactions.md` § Brief: row 270's pi compaction reading moves behind the harness kind (Vikunja 598) | LANDED d243f6b25: 6 row commits replayed onto 8dc132bec, every non-log patch-id equal, candidate paths = 6fdfe3790 (manifest 17c9f8d9). Duckworth APPROVE rev 3586 c31189 round 1, C2 met. Sage gate: bus, cli 0 skipped, plugins fully green incl harness.test.mjs:192, webui, business, seat, every scripts/test-*.sh; M1-M4 killed. P19 598 commented. DEFERRED: 307 N1 store-unreadable not sticky. N3 doc-only BUILD.md red list one short, next BUILD.md change. |
|
||||
| 308 | PRD 1.17 Case 2 story files are DOCX (Q235 to Q237, Q239) | gosalyn; reviewers webby | #1815 | done | 291 done | Webby checks the PRD diff against DRAFT-d9.md at 4607e15b8 one instruction at a time and that nothing else changed. No new Jason question (Q239 approved the text). tasks whole, bus whole, cli stack-copy.test and every scripts/test-*.sh green on Sage's gate. Mos gets the landing commit (mos-1470). (sage) | `docs/plans/2026-10-23_prd-1-17-brief.md` § PRD 1.17 Case 2 story files are DOCX (Q235 to Q237, Q239) | Landed 30e6bb680: Gosalyn 45fe7b3ce replayed onto 7defb58e9, patch-id 4658c831 equal, range-diff =, four blobs equal. Webby r1 APPROVE c31224 rev 3606, d9 at 4607e15b8 applied word for word. Sage gate 17:19Z: every package suite 0 fail 0 skipped (tasks 100, bus 894, cli 942, plugins 119, webui 392), every scripts/test-*.sh rc 0. Mos gets the sha (mos-1470). 570/571 build rows next. |
|
||||
| 309 | 571 W1: reviewed local thesaurus data (post-10-27) | unassigned | — | queued | — | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W1 reviewed local thesaurus data | — |
|
||||
| 310 | 571 W2: WordCraft source import and reproducible restricted web build (post-10-27) | unassigned | — | queued | — | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W2 WordCraft source import and reproducible restricted web build | — |
|
||||
| 311 | 571 W3a: bus pins and human-only editor settings (post-10-27) | unassigned | — | queued | — | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W3a bus pins and human-only editor settings | — |
|
||||
| 312 | 571 W3b: human DOCX store and instant commits (post-10-27) | unassigned | — | queued | 311 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W3b human DOCX store and instant commits | — |
|
||||
| 313 | 571 W6: owned browser bridge and editor-only route (post-10-27) | unassigned | — | queued | 309 done, 310 done, 312 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W6 owned browser bridge and editor-only route | — |
|
||||
| 314 | 571 W3: full editor pane, navigation, pins and right-click synonyms (post-10-27) | unassigned | — | queued | 311 done, 312 done, 313 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W3 full editor pane, navigation, pins and right-click synonyms | — |
|
||||
| 315 | 571 W4b: committed reader, citation resolver and typed tools (post-10-27) | unassigned | — | queued | 314 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W4b committed reader, citation resolver and typed tools | — |
|
||||
| 316 | 571 W5: read-only flags, citations and chat suggestions (post-10-27) | unassigned | — | queued | 315 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W5 read-only flags, citations and chat suggestions | — |
|
||||
| 317 | 571 W7: human one-way chapter import and Case 2 format routing, production import closed; the first real import opens only after W8 lands (post-10-27, mos-1470) | unassigned | — | queued | 314 done, 315 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W7 human one-way chapter import and Case 2 format routing | — |
|
||||
| 318 | 571 W8: Console Connections view for Markdown and DOCX story files, before W7's first real import (post-10-27, mos-1470) | unassigned | — | queued | 314 done, 315 done, 317 done | Independent Opus 5.5 reviewer, not the author, approves the manifest; the brief section's Gate and G1 pass on Sage's slot: touched test files whole, affected package suites green, named mutants killed. Post-10-27; MVP rows keep slot priority (sage) | `docs/plans/2026-10-21_q195-writing-editor.md` § W8 Console Connections view for Markdown and DOCX story files | fenton-0017 carry: connections.test.mjs must test that links inside comments and tracked deletions are excluded. W8 follows W7's closed routing candidate and lands before W7's first real import opens (mos-1470) |
|
||||
| 319 | GitHub consumer for row 297's protected githubWithToken callback, honoring expiry and cancellation (post-10-27) | unassigned; reviewers dewey | — | queued | 297 done | Dewey approves the design and the manifest. The touched suites and every scripts/test-*.sh are green on Sage's gate, and the signal and expiry mutants are killed. (sage) | `docs/plans/2026-10-23_github-consumer-brief.md` § Brief: a GitHub consumer for Broker.githubWithToken | — |
|
||||
| 320 | Case 1/2 host fixes for the 433 P1 rerun: hostcheck item 5 mode 750, run sheet bundle verify, case2-unit --claude @, stack-copy unreadable cwd, notify.json step and precheck, mosaic wrapper step (10-27 path) | gladstone; reviewers morgana | #1816 | required; done | — | Morgana approves. Touched test files run whole (case1-hostcheck, case2-unit, case2-install, stack-copy) and the 8 named mutants are killed; Sage's full landing gate runs every package suite and every scripts/test-*.sh. After landing Mos sends ops-01 the new N. (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: hostcheck item 5, bundle verify, case2-unit @, stack-copy cwd | LANDED 094124b4e 21:53Z (replay of faf55618e onto 45e3ca7bd, code paths patch-id equal, log appends in one final commit). F1-F6 hostcheck item 5, bundle verify, case2-unit scoped paths and cwd, precheck notify decision, run sheet 9a notify dirs 0700, wrapper step. Morgana APPROVE r2 c31403 rev 3715. Full gate GREEN 21:53:00Z, 0 fail (mos-1453/1455/1456). Vikunja 614 done. 433 P1 reruns on this N (mos-1532). |
|
||||
| 321 | F7 and F10: host run sheet wrapper step sets umask 077 and the node PATH under sudo secure_path; adopt step after a first install; --no-rehearsal on the first upgrade after adopt (433, ops-0271/0274); docs only | gladstone; reviewers boyd | #1817 | required; briefed | — | Boyd (Codex) approves. A git grep with a positive control shows no test reads the changed run sheet lines; BUILD.md records the diff and the grep. Doc only, lands with any row (mos-1544). (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: F7, the wrapper step's PATH and umask under sudo | — |
|
||||
| 322 | F8/F9: case2-install.sh runs the library persona import from the stack copy, and its refusal carries the error line (433 Run B, ops-0273) | goldie; reviewers morgana | #1818 | required; briefed | — | Morgana approves (install path). The new case2-install test fails on 094124b4e and passes on the candidate; a mutant restoring REPO at :185 is killed; Sage's full landing gate runs every package suite and scripts/test-*.sh. Then ops-01 reruns Run B P1. (sage) | `docs/plans/2026-10-23_host-fixes-1816-brief.md` § Brief: F8 and F9, the library persona import runs without the clone's node_modules | — |
|
||||
|
||||
<!-- mosaic-queue:end -->
|
||||
|
||||
|
||||
+6768
-110
File diff suppressed because it is too large.
Load diff
+399
-44
@@ -1,6 +1,6 @@
|
||||
# PRD: Mosaic Stack
|
||||
|
||||
- Status: approved, version 1.15. Jason approved draft 0.4 unchanged as
|
||||
- Status: approved, version 1.17. Jason approved draft 0.4 unchanged as
|
||||
1.0 on 2026-10-05 (lead decision 61). They approved version 1.1, the
|
||||
Organization requirements, in decision 03130d08-ddce-480d-a601-e59f64744fc3
|
||||
on 2026-10-14. They approved version 1.2, the chief of staff, the
|
||||
@@ -44,7 +44,44 @@
|
||||
as 1.11 (mos-1233, mos-1238, mos-1241, vikunja:19/464). Version 1.15
|
||||
rewrites REQ-LIB-6 to Jason's Q223 and Q226 rulings and adds supporting
|
||||
folders to REQ-LIB-1 and REQ-LIB-2. Mos ordered it in mos-1320 item 8,
|
||||
and Jason gets a report.
|
||||
and Jason gets a report. Version 1.16 routes the new action
|
||||
`library.write` under REQ-LIB-6 (REQ-WAIT-3). A change to the hard waits
|
||||
needs Jason's approval under REQ-WAIT-2, so Mos puts the text to him as
|
||||
written, and it lands only after his yes (mos-1328, mos-1329). Version
|
||||
1.17 records Jason's rulings Q235 to Q237 of 2026-10-23: Case 2 story
|
||||
files are DOCX, Markdown stays for agent files and documentation, and a
|
||||
human-started import converts a Markdown story file one way. It also
|
||||
adds REQ-LIB-9, the Console's DOCX editor, from Jason's rulings Q195,
|
||||
Q226 and Q234. It changes what Case 2 requirements allow, so Mos puts
|
||||
the text to Jason as written, and it lands only after his yes (mos-1366,
|
||||
Vikunja 570).
|
||||
- Version 1.17 source: Jason's Q235, answering Q36: 'How about we see a
|
||||
markdown file, we convert the markdown file to docx, then archive or
|
||||
mark the markdown file as converted. The new standard for files in the
|
||||
creating writing use case is to use docx to handle versioning and
|
||||
WYSIWYG. We eliminate the potential for confusion based on the files.
|
||||
Agents must understand which files are what format and where they live.
|
||||
The system must allow for import and conversion. We will not run the
|
||||
systems in parallel. There will be an irreversible conversion of the
|
||||
files.' His Q236 says: 'For creative writing chapters, we need to
|
||||
utilize docx as the standard. Markdown is acceptable for agent files,
|
||||
instructions, PRDs, documentation, etc. The creative writing files
|
||||
should be docx for familiarity for the user.' His Q237 says 'Q38: It is
|
||||
correct for now (subject to future change)', which confirms Mos's
|
||||
reading P1 to P6. REQ-LIB-9 rests on his Q195 request for 'a full
|
||||
WYSIWYG editor within Mosaic Stack', his Q226 answer 'F41: We want to
|
||||
move editing into the WYSIWYG in the WebUI. All changes are instantly
|
||||
committed and available to the agents.' and his Q234 answer 'Q37: yes'
|
||||
to the exact security header for the editor's frame. The storage follows
|
||||
the O2 design D10 to D12 as rewritten under Vikunja 571, at b99ca86b9 on
|
||||
`refactor`.
|
||||
- Version 1.16 source: Webby's review webby-0164 B2, from Scrooge's row
|
||||
285. Version 1.15 lets creative agents write in supporting folders, but
|
||||
the write action stayed a hard wait under REQ-WAIT-3, and REQ-WAIT-2
|
||||
refuses every role file that grants one. Mos ruled in mos-1328 and
|
||||
mos-1329 that the routing is its own version. The bounds come from
|
||||
REQ-LIB-6 and the Case 2 design's ruling after r11, f6e3e7f20 on
|
||||
`gosalyn/case2-design`.
|
||||
- Version 1.15 source: Jason's Q223, answering Q29: "The agents are not
|
||||
allowed to write the prose into the writing projects. Agents are
|
||||
allowed to CRUD character sheets, info summaries, cross-link info, and
|
||||
@@ -706,6 +743,14 @@ Added in version 1.5 (Q167).
|
||||
`host.sudo` was added as a hard wait and routes under REQ-CRED-4 since
|
||||
version 1.8.
|
||||
|
||||
`library.write` was added as a hard wait and routes under REQ-LIB-6
|
||||
since version 1.16. It is granted within-role to the six creative roles
|
||||
only, and only for Markdown files under the supporting folders a human
|
||||
marks. It never writes a story file. A write also needs the writing
|
||||
agent's own live Gitea grant on the library project. Each write is one
|
||||
commit by that agent's own Gitea account with a trailer naming the
|
||||
agent, and the user sees a notice for it and can revert it.
|
||||
|
||||
### Messages
|
||||
|
||||
- **REQ-MSG-1.** Messages are addressed to a role. They are append-only
|
||||
@@ -1178,7 +1223,8 @@ design `docs/plans/2026-10-21_t1-sessions.md`, 2c843d190 on `huey/t1-sessions-de
|
||||
Added in version 1.12 (Q176 Case 2, Q211). Drafted from the Case 2
|
||||
design r11, fb20a61b4 on `gosalyn/case2-design`, approved by Webby in
|
||||
82f50d127, with the rulings recorded after r11 up to 624212842. The
|
||||
library is the user's own writing repository.
|
||||
library is the user's own writing repository plus the stack's writing
|
||||
store, which holds the DOCX story files (REQ-LIB-8, version 1.17).
|
||||
|
||||
- **REQ-LIB-1.** A library project maps the user's writing repository
|
||||
onto universes, books, entity types such as character and place, the
|
||||
@@ -1187,9 +1233,12 @@ library is the user's own writing repository.
|
||||
human changes it, and an agent's organization write that changes it
|
||||
is refused. So
|
||||
`universe.book.{character,place,...}` reads as universe, book folder,
|
||||
type folder. (Design 1.1 and M9. Q176 Case 2 "Data is stored in
|
||||
little-endian organizational scheme". Version 1.15: supporting
|
||||
folders.)
|
||||
type folder. The mapping also names one archive folder, set by the
|
||||
human, where converted Markdown story files go. A book's DOCX story
|
||||
files are in the stack's writing store, under the same universe and book
|
||||
(REQ-LIB-8). (Design 1.1 and M9. Q176 Case 2 "Data is stored in
|
||||
little-endian organizational scheme". Version 1.15: supporting folders.
|
||||
Version 1.17: the archive folder and the writing store.)
|
||||
- **REQ-LIB-2.** Playgrounds are never confused with canon. A playground
|
||||
has one of three levels, set by where its prefix sits. A book-level
|
||||
prefix lies inside one of its universe's book folders, and its files
|
||||
@@ -1204,18 +1253,34 @@ library is the user's own writing repository.
|
||||
its own universe, when two prefixes overlap, when a draft has books or
|
||||
no prefix, and when a hybrid names a draft as a source. Supporting
|
||||
prefixes, the folders agents may write in (REQ-LIB-6), follow the same
|
||||
placement rules. A file's scope comes from its path alone, prefixes
|
||||
first: a Markdown file under a supporting prefix is supporting, one
|
||||
under a playground prefix at any level is playground, any other
|
||||
Markdown file in a book folder is canon, and everything else is
|
||||
unmapped and never returned. A folder in both lists is supporting.
|
||||
Lookups return canon unless the caller asks for playground or
|
||||
supporting. A citation marked as canon must name a canon file, so an
|
||||
agent can't cite a playground or supporting file as canon. The Console
|
||||
shows each cite's scope. (Design 1.2, 5.2 and 7.4, the playground
|
||||
ruling, 51fdfb7e2, and the ruling after r11. Q176 Case 2, the
|
||||
playground items, TARGET.md:92-94. Version 1.14: the levels and the
|
||||
draft. Version 1.15: supporting.)
|
||||
placement rules. A Markdown file's scope comes from its path and from
|
||||
past imports, prefixes first: a Markdown file under the archive folder
|
||||
is archived, one under a supporting prefix is supporting, a Markdown
|
||||
file at a converted file's old path is unmapped under REQ-LIB-8 but
|
||||
flagged in the format map, one under a playground prefix at any level is
|
||||
playground, any other Markdown file in a book folder is canon, and
|
||||
everything else is unmapped and never returned. A folder in both lists
|
||||
is supporting. The archive folder follows the same placement rules and
|
||||
may not overlap another prefix or a book folder. An archived file keeps
|
||||
the path it had under the archive folder, and its universe and book are
|
||||
those of that old path. A DOCX document in the writing store keeps its
|
||||
source's scope: the universe, the book if there is one, and canon or
|
||||
playground. Its source is the Markdown file it was converted from or,
|
||||
for one the human starts in the editor, the universe and book or the
|
||||
playground the human picks (REQ-LIB-8). A playground DOCX follows the
|
||||
playground rules and is never returned as canon. It stays playground
|
||||
after its draft universe is made real. In version 1.17 no promotion or
|
||||
mapping change makes a playground DOCX canon. A later version of this
|
||||
PRD says how one becomes canon. Lookups return canon unless the caller
|
||||
asks for playground, supporting or archived. Every answer marks an
|
||||
archived file as archived and names the DOCX that replaced it. A
|
||||
citation marked as canon must name a canon file, so an agent can't cite
|
||||
a playground, supporting or archived file as canon. The Console shows
|
||||
each cite's scope and format, and whether a cited Markdown file was
|
||||
converted. (Design 1.2, 5.2 and 7.4, the playground ruling, 51fdfb7e2,
|
||||
and the ruling after r11. Q176 Case 2, the playground items,
|
||||
TARGET.md:92-94. Version 1.14: the levels and the draft. Version 1.15:
|
||||
supporting. Version 1.17: archived scope and DOCX scope.)
|
||||
- **REQ-LIB-3.** Universes stay separate. Every lookup names one universe,
|
||||
and its results never leave it. A draft universe (REQ-LIB-2) counts as
|
||||
a universe here. It has no canon, so a lookup in a draft asks for
|
||||
@@ -1231,23 +1296,62 @@ library is the user's own writing repository.
|
||||
A pin doesn't stop the change. The continuity agent reads the human's other
|
||||
constraints and cites them. (Design 1.4 and 3. Q176 Case 2, the
|
||||
crossover items.)
|
||||
- **REQ-LIB-4.** Git holds every change. The user commits and pushes with
|
||||
their own tools, today Obsidian with obsidian-git. The bus keeps a
|
||||
- **REQ-LIB-4.** Git holds every change. The user commits and pushes
|
||||
Markdown with their own tools, today Obsidian with obsidian-git. A
|
||||
converted story file's current text is no longer in that repository,
|
||||
which keeps only its archived Markdown (REQ-LIB-8). The bus keeps a
|
||||
mirror of the remote, fetched with a read-only token every 60 seconds
|
||||
and before any lookup when the last fetch is older than that. It only
|
||||
fast-forwards. A force-push is held as `library-diverged` until a human
|
||||
accepts it. Agents read only the mirror's served commit and never hold
|
||||
a copy of their own, so they see the user's push within about a minute.
|
||||
Bus writes and pushes come in later rows (C2-02, C2-03b). (Design D5,
|
||||
D6, 7.1, 7.2 and 7.5. Q176 Case 2 "Git is used for all changes -- local
|
||||
and remote" and the sync items.)
|
||||
accepts it. Agents read the user's repository only at the mirror's
|
||||
served commit, except a story file during its import under REQ-LIB-8.
|
||||
They read a DOCX story file only at a committed version in the writing
|
||||
store, under REQ-LIB-5. They never hold a copy of their own, so they see
|
||||
the user's Markdown push within about a minute and a DOCX save at their
|
||||
next lookup after the editor reports it as committed. Bus writes and
|
||||
pushes come in later rows (C2-02, C2-03b). A DOCX story file is saved in
|
||||
the Console's editor, and each accepted save is one commit by the human
|
||||
in the stack's writing store (REQ-LIB-9). Each writing-store commit is
|
||||
also pushed to a remote off the stack's host, never by force-push. The
|
||||
remote is a setting only a human changes, and it starts unset. A
|
||||
repository on the stack's host never counts as that remote. A missing
|
||||
remote or a failed push never undoes a save, and the user sees each push
|
||||
that is pending or failed. Only the stack's host pushes to that remote,
|
||||
and no agent can reach it with any credential. Agents read the local
|
||||
commit. (Design D5, D6, 7.1, 7.2 and 7.5. Q176 Case 2 "Git is used for
|
||||
all changes -- local and remote" and the sync items. Version 1.17, with
|
||||
Q222 for the remote setting.)
|
||||
- **REQ-LIB-5.** Agents check the library through typed lookup tools:
|
||||
find, get, search, manuscript and universe. Each answer names the
|
||||
commit it read. Get returns a file's links out and links in, so the
|
||||
connections are visible to agents. The user sees them in Obsidian's
|
||||
graph and backlinks. (Design 3 and 7.1. Q176 Case 2 "The agents have
|
||||
tools to check character, place, and other data", the visualization
|
||||
item and "Connections are always visible".)
|
||||
find, get, search, manuscript and universe. Each answer names the commit
|
||||
it read. Each answer also names the file's format and where it lives:
|
||||
its path, or its document id in the writing store. A DOCX story file is
|
||||
read from its latest committed save, never from unsaved text. A lookup
|
||||
that starts after the editor reports a save as committed returns that
|
||||
save or a later one, even while its push to the remote is pending or has
|
||||
failed, and the answer names the version. Get, search and manuscript
|
||||
read a DOCX story file as its extracted text with DOCX locators, never
|
||||
as lines. That text is the file with its tracked changes accepted. An
|
||||
agent reads a story file's comments, or its text with tracked deletions
|
||||
shown, only after a human turns on the matching setting. Both settings
|
||||
are off by default, and only a human changes them. The reader in this
|
||||
version can't read some things a DOCX story file can hold, such as a web
|
||||
link, a text box, a field or hidden text. The editor warns the human
|
||||
when an edit adds one, and the human can still save it. Until a later
|
||||
save takes it out, lookups give agents none of the text of the part that
|
||||
holds it, whether that is the chapter's body, a note or a comment. The
|
||||
manuscript lookup follows the book's chapter order across both formats
|
||||
(O2 design D10), and the next chapter it names is never an archived
|
||||
file. Links out and in resolve across both formats. The universe lookup
|
||||
returns the format map (REQ-LIB-8). Get returns a file's links out and
|
||||
links in, so the connections are visible to agents. The user sees a
|
||||
Markdown file's links in Obsidian's graph and backlinks. A DOCX story
|
||||
file's wikilinks stay clickable in the Console's editor (O2 design D11),
|
||||
and no graph shows them yet. The Console's Connections view lists the
|
||||
current links and backlinks of each story file the user may see, in both
|
||||
formats, with each one's scope and format. (Design 3 and 7.1. Q176 Case
|
||||
2 "The agents have tools to check character, place, and other data", the
|
||||
visualization item and "Connections are always visible". Version 1.17,
|
||||
with Q222 for the two settings and Q239 for the Connections view.)
|
||||
- **REQ-LIB-6.** The creative roles are ideas, research, proofreader,
|
||||
fact-checker and continuity, plus a companion role that the user's own
|
||||
named agents run as. Agents never write the story. A creative agent may
|
||||
@@ -1255,22 +1359,37 @@ library is the user's own writing repository.
|
||||
folders a human marks (REQ-LIB-1), such as character sheets, summaries,
|
||||
cross-link notes and research notes. Every other path in the user's
|
||||
repository is read-only to it, chapters and unmarked folders included,
|
||||
and the broker refuses such a write before any git call. Each agent
|
||||
write is one commit by that agent's own Gitea account, with a trailer
|
||||
naming the agent. The user sees a notice for it and can revert it. Its
|
||||
tools are the lookups, a message to the human and the two write tools,
|
||||
and research and companion also fetch web pages through the broker.
|
||||
Agents talk with the user in free text. A reply may cite lines of the
|
||||
repository at a commit or pages the agent fetched, and the broker
|
||||
checks every citation before it stores the message. A setting,
|
||||
`requireCitation`, makes every reply carry a citation, except the
|
||||
runner's fixed notices for no answer, a failed turn, no answer passing
|
||||
the citation check and an answer too long to send. Only the human
|
||||
changes it, and it is off by default. The user sees only stored
|
||||
and the broker refuses such a write before any git call. The writing
|
||||
store and the archive folder are read-only to a creative agent as well,
|
||||
and the broker's path check treats the archive folder as story. No agent
|
||||
creates, edits, deletes, converts, archives or restores a story file in
|
||||
any format, and no agent writes a DOCX. No creative agent starts or
|
||||
undoes a conversion, and none asks for one through a tool or a decision
|
||||
it raises, as REQ-LIB-8 says. An agent may still suggest a conversion to
|
||||
the human in chat. Each creative role's contract states which files are
|
||||
DOCX and which are Markdown, and cites a DOCX story file by its locator,
|
||||
never by line. Each agent write is one commit by that agent's own Gitea
|
||||
account, with a trailer naming the agent. The user sees a notice for it
|
||||
and can revert it. Its tools are the lookups, a message to the human and
|
||||
the two write tools, and research and companion also fetch web pages
|
||||
through the broker. The write tools need the action `library.write`,
|
||||
which routes under this requirement within the bounds REQ-WAIT-3 names
|
||||
(version 1.16). Agents talk with the user in free text. A reply may cite
|
||||
lines of a Markdown file at a commit, a place in a committed DOCX story
|
||||
file, or pages the agent fetched, and the broker checks every citation
|
||||
before it stores the message. A DOCX citation never calls a paragraph a
|
||||
line. It names the document id, the committed version and a DOCX
|
||||
locator. A citation that names an archived file as current is refused.
|
||||
The `requireCitation` setting below applies to Markdown and DOCX
|
||||
citations alike, and so does the rule for a citation marked as canon. A
|
||||
setting, `requireCitation`, makes every reply carry a citation, except
|
||||
the runner's fixed notices for no answer, a failed turn, no answer
|
||||
passing the citation check and an answer too long to send. Only the
|
||||
human changes it, and it is off by default. The user sees only stored
|
||||
messages, on every channel. (Design 4, 5.2 and the ruling after r11.
|
||||
Q223 and Q226. Q180 for Bean and Chet. Q176 Case 2, the agent list,
|
||||
"User sets strict guardrails for agent capabilities" and TARGET.md:101
|
||||
"Agents NEVER write prose for the user". Version 1.15.)
|
||||
"Agents NEVER write prose for the user". Version 1.15. Version 1.17.)
|
||||
- **REQ-LIB-7.** The user talks to several creative agents in one chat. A
|
||||
room holds one thread per creative agent in a project. A message goes
|
||||
to every agent in the room, or only to the agents named by @tags at its
|
||||
@@ -1278,6 +1397,110 @@ library is the user's own writing repository.
|
||||
the threads as one timeline, and each agent sees only its own thread.
|
||||
(Design D4 and 6, row C2-09b. Q176 Case 2 "converse with N agents in
|
||||
one chat session with @tagged messages".)
|
||||
- **REQ-LIB-8.** Story files become DOCX. DOCX is the standard for chapters and
|
||||
the other story text in a Case 2 writing project, canon and playground alike. A
|
||||
story file becomes DOCX through a human's import (below), or when the human
|
||||
starts it in the editor. From then on DOCX is its only working format, edited in
|
||||
the Console's editor (REQ-LIB-9). A Markdown story file stays Markdown until its
|
||||
import. Files in a type folder (REQ-LIB-1), such as character and place notes,
|
||||
stay Markdown. So do supporting files, agent files, instructions, PRDs and
|
||||
documentation, including the character sheets, summaries and cross-links agents
|
||||
keep under REQ-LIB-6.
|
||||
- A converted story file's current text leaves the user's Gitea-mirrored
|
||||
repository. Its DOCX lives in the stack's writing store, on the stack's host,
|
||||
and not in the repository the user pushes or opens in Obsidian. Only its
|
||||
archived Markdown stays there.
|
||||
- An import converts one Markdown story file, canon or playground, outside every
|
||||
type folder (REQ-LIB-1), to a DOCX document in the writing store. It refuses
|
||||
an unmapped, supporting, archived or type-folder file, and a file that is
|
||||
already converted. Each import has its own fidelity report, the human's
|
||||
confirmation of that report, a manifest naming the source file and its commit,
|
||||
and one commit in the store. The DOCX keeps every wiki link of the source as
|
||||
written, and a lost link to another library file fails the fidelity report. A
|
||||
web link becomes plain text that keeps its label and address, and the fidelity
|
||||
report lists it for the human to confirm. A web link that can't be kept safely
|
||||
as plain text refuses the import.
|
||||
- The human can also start a new story file in the editor, in a universe and
|
||||
book or a playground the human picks. It is a DOCX document in the writing
|
||||
store from its first commit, with no Markdown copy, and it opens under the
|
||||
same gate as the import.
|
||||
- The same import marks the Markdown file converted, naming the document, and
|
||||
moves it to the archive folder under its old path (REQ-LIB-1, REQ-LIB-2).
|
||||
The mark and the move are one commit authored by the human in the user's
|
||||
repository. A new human-only bus step (Vikunja 571) on the C2-02 write
|
||||
plumbing writes it, and C2-03b pushes it. The move keeps the bytes and the
|
||||
git history, so it isn't `data.delete`.
|
||||
- The store commit comes first, and the writing store's remote must confirm it
|
||||
holds that commit. Then come the archive commit and its push, then publication
|
||||
to the lookups. Until publication the Markdown at its old path keeps its canon
|
||||
or playground scope. Agents read that file at the commit the import started
|
||||
from, even after the mirror's served commit reaches the archive commit or a
|
||||
later one. No lookup returns the file's archived copy before publication, even
|
||||
one that asks for archived files. A refusal or failure before the archive push
|
||||
leaves the Markdown unmoved and no DOCX that a lookup returns. A failure after
|
||||
the archive push leaves a pending import that the human sees and can resume,
|
||||
never a claim that nothing moved. An archive push whose result is unknown
|
||||
counts as a failure after the archive push until the stack reads the result
|
||||
back from the user's remote.
|
||||
- Each stage of an import writes a logged event. The import's record on the
|
||||
stack's host names the source file's path, commit and blob and the new
|
||||
document id, and each event points to that record.
|
||||
- After a conversion, a Markdown file at the converted file's old path is
|
||||
neither canon nor playground. Lookups leave it out, the format map flags it,
|
||||
and the human is told.
|
||||
- A new Markdown story file in a book folder that holds converted files stays
|
||||
canon until its import, unless it sits at a converted file's old path or under
|
||||
a playground prefix. The Console flags it and offers the import. Nothing
|
||||
converts it on its own.
|
||||
- A hybrid's exclusion that covers a converted file's old path also covers its
|
||||
DOCX document (REQ-LIB-3). An exclusion of a whole book folder also covers
|
||||
every DOCX document in that book, including one started in the editor. An
|
||||
exclusion can also name a DOCX document by its id.
|
||||
- Only a human starts an import or a new story file, from the CLI or a Console
|
||||
session signed in as that human. Both are human verbs outside the agent action
|
||||
vocabulary. Neither is added to `ACTIONS` in the business vocabulary or to the
|
||||
broker's always-gated list, including the import's action, `library.convert`.
|
||||
No role holds them, no agent can request them, and the hard-wait list in
|
||||
REQ-WAIT-1 doesn't change. In an organization with several humans, who may
|
||||
start them is a setting only a human changes, and the default is the owner
|
||||
only.
|
||||
- Conversion is one way, with no parallel run. Neither the Console nor any agent
|
||||
edits a converted story file as Markdown, and no DOCX edit syncs back to
|
||||
Markdown. The Console's Markdown save refuses every file under the archive
|
||||
folder. The user's own tools can still change the archived Markdown, and no
|
||||
such change reaches the DOCX. The old Markdown stays in git history, and
|
||||
nothing rewrites history. A promotion into a book folder (C2-19) runs the
|
||||
import for a Markdown story file or is refused. A promotion of a playground
|
||||
DOCX is refused in version 1.17.
|
||||
- Agents get a format map: for each mapped folder and story file, its format,
|
||||
scope and where it lives, archived files, flagged files and pending imports
|
||||
included.
|
||||
- The import and new story files ship closed. They open only after four things.
|
||||
The editor ships. The human has set the writing store's remote, and the stack
|
||||
has checked that it can push there. Agents can read committed DOCX story files
|
||||
through the lookups and cite them (REQ-LIB-5, REQ-LIB-6). Any earlier agent
|
||||
system that writes the user's files has stopped, which for Case 2 is the
|
||||
Hermes retirement (#423 step 5). So the import itself never cuts agents off
|
||||
from a chapter, the chapter's current text never lives only on the stack's
|
||||
host, and no second system writes the story. The first import of a real user's
|
||||
books waits for Jason's go.
|
||||
(Q235, Q236, Q237: P1 to P6. Q222 for the setting. O2 design D10 to D12.
|
||||
Version 1.17.)
|
||||
- **REQ-LIB-9.** The Console's DOCX editor. The human edits a DOCX story file in
|
||||
the Console's editor, in a split pane with the agent chat alongside it. The
|
||||
human can pin an agent chat to a document. The human can select a word,
|
||||
right-click it, and choose a synonym from a thesaurus. Only a human saves. Saves
|
||||
happen on their own while the human types (Q226), at intervals only a human
|
||||
changes, and each accepted save is one commit with the human as author, pushed
|
||||
as REQ-LIB-4 says. A save made against an older version than the current one is
|
||||
refused. Agents never write a DOCX. Versioning is the editor's tracked changes
|
||||
plus one git commit per save. The editor's code is vendored and pinned, with a
|
||||
hash per file and the licenses that its independent security review accepted. At
|
||||
runtime the editor loads only files that the stack itself serves, and it
|
||||
requests nothing from any other host. The editor's frame sends exactly the
|
||||
content security policy Jason approved in Q234, and the WebUI shell's own header
|
||||
is unchanged. REQ-PROJ-3's project docs editor is unchanged. (Q195, Q226, Q234,
|
||||
Q236. O2 design D3, D4, D6, D8, D10, D13, D14, D16 and D22. Version 1.17.)
|
||||
|
||||
### Installer
|
||||
|
||||
@@ -1750,6 +1973,118 @@ latest-by date and a hard target (Q176).
|
||||
ruling after r11 and in the Q226 briefs of
|
||||
`docs/plans/2026-10-20_case2-rows.md`. Sage files the rows under
|
||||
Vikunja 416. This amendment adds the requirements only.
|
||||
- Version 1.16:
|
||||
- REQ-WAIT-3 gains one paragraph, the `library.write` routing and its
|
||||
bounds. REQ-LIB-6 gains one sentence naming the action. No other
|
||||
requirement text changes.
|
||||
- `packages/tasks/requirements.json` changes its version to 1.16 and
|
||||
nothing else. The titles stay and the id count stays 85.
|
||||
- The commit that adds `library.write` to `ROUTED` in the business
|
||||
vocabulary and the broker is C2-08b, row 285. It lands only after this
|
||||
version.
|
||||
- Version 1.17:
|
||||
- Jason confirmed the reading 'for now (subject to future change)' (Q237). The
|
||||
rulings stand until he revises them.
|
||||
- The 10-27 Case 2 release ships on Markdown as built (P6). The 1.11 line 'The
|
||||
Case 2 MVP for 10-27 is the Markdown library (#416). The WYSIWYG editor (Q195,
|
||||
Q196) is an add-on that doesn't gate it.' still holds. Conversion opens after
|
||||
10-27, and only once REQ-LIB-8's four conditions are met. The editor ships,
|
||||
the writing store's remote is set and the stack has checked that it can push
|
||||
there, agents can read and cite committed DOCX story files through the
|
||||
lookups, and Hermes is retired, which is #423 step 5.
|
||||
- The 1.12 note says five bus write and Console editor rows, C2-02, C2-03b,
|
||||
C2-11a, C2-11b and C2-19, don't gate 10-27. Jason's Q226 F42 later moved
|
||||
C2-02, C2-03b and C2-11b into the 10-27 slice. The Case 2 design brings C2-11a
|
||||
in with them, because C2-11b needs it. C2-19 stays outside the slice. Mos's
|
||||
reading of F42 is that C2-02 and C2-03b now gate 10-27, because design 9.2
|
||||
puts them in 'the smallest slice that lets her start'. C2-11a and C2-11b are
|
||||
slice rows that still don't gate 10-27, because the editor is the Q195 add-on.
|
||||
The DOCX change in 1.17 leaves this slice as it is, and C2-11b's Markdown
|
||||
editor never edits a converted story file.
|
||||
- Q236 narrows Q176's editor choice to DOCX for story files (REQ-LIB-9). The
|
||||
1.12 note that the Case 2 design keeps Obsidian now holds only for Markdown
|
||||
files, including story files not yet imported. A DOCX story file is edited
|
||||
only in the Console's editor under REQ-LIB-9.
|
||||
- This replaces two parts of the O2 design at 25c09f19f. The superseded-notice
|
||||
proposal from Fenton's F1 in D12 is withdrawn, and the
|
||||
keep-Markdown-at-its-path model of D11 is replaced. Gyro's rewrite of the O2
|
||||
design for Vikunja 571, landed on `refactor` at b99ca86b9, carries the
|
||||
details, and where it differs from this text, this text holds:
|
||||
- the import and archive steps, the mark, and the new bus step;
|
||||
- links from supporting files to a moved chapter;
|
||||
- the DOCX reader;
|
||||
- the user's unpushed or later changes to an archived file or at its old path,
|
||||
and pushing and closing the file in Obsidian before each import;
|
||||
- the writing store's remote in W4 section 7.3, which replaces the no-remote
|
||||
and no-backup rule that W4 section 7.4 held before the rewrite, at
|
||||
062646487;
|
||||
- import events counted by a bus restore, and recovery when a bus restore or a
|
||||
restore of the whole host predates an import;
|
||||
- manifest order for the manuscript lookup and its next chapter;
|
||||
- `library.convert` kept out of `ACTIONS`, out of the hard-wait note and out
|
||||
of the pinned `GATED_ONLY` list in packages/business/src/vocabulary.mjs.
|
||||
- Agents read a converted story file through the Case 2 lookups under the same
|
||||
project and universe access as Markdown canon, in any thread or room. W4's
|
||||
earlier O2-only conversations, per-document grants and per-send sharing don't
|
||||
gate these lookups. The 571 rewrite replaced them in W4 sections 7.1 and 7.2.
|
||||
The creative tool list in REQ-LIB-6 doesn't change.
|
||||
- The six creative role contracts row 213 landed say 'Every item cites the
|
||||
lines of the user's material it is about'. A build row under 571 changes that
|
||||
line for DOCX story files (REQ-LIB-6).
|
||||
- A new Markdown file in a book folder is still canon in 1.17. The 1.17
|
||||
exception is a file at a converted file's old path, which REQ-LIB-2 makes
|
||||
unmapped.
|
||||
- Story files convert whether canon or playground. Type-folder and supporting
|
||||
files stay Markdown.
|
||||
- Measured 2026-10-23 at about 01:21Z: the Case 2 user's library repository
|
||||
tracks 213 files, 203 of them Markdown, and none is a .docx or .doc. It has no
|
||||
untracked files. The user's .doc files (Q196) are outside it. So the landed
|
||||
code has no DOCX there to report as unreadable at 10-27.
|
||||
- Text replaced in 1.17, kept here as in earlier notes:
|
||||
- Creative library lead, the 1.16 sentence: "The library is the user's own
|
||||
writing repository."
|
||||
- REQ-LIB-2, the 1.16 sentences: "A file's scope comes from its path alone,
|
||||
prefixes first: a Markdown file under a supporting prefix is supporting,
|
||||
one under a playground prefix at any level is playground, any other
|
||||
Markdown file in a book folder is canon, and everything else is unmapped
|
||||
and never returned. A folder in both lists is supporting." "Lookups return
|
||||
canon unless the caller asks for playground or supporting." "so an agent
|
||||
can't cite a playground or supporting file as canon" "The Console shows
|
||||
each cite's scope."
|
||||
- REQ-LIB-4, the 1.16 sentences: "The user commits and pushes with their own
|
||||
tools, today Obsidian with obsidian-git." "Agents read only the mirror's
|
||||
served commit and never hold a copy of their own, so they see the user's
|
||||
push within about a minute."
|
||||
- REQ-LIB-5, the 1.16 sentence: "The user sees them in Obsidian's graph and
|
||||
backlinks." A converted chapter's connections show in Obsidian only as they
|
||||
stood at conversion, and the Console's Connections view shows their
|
||||
current links.
|
||||
- REQ-LIB-6, the 1.16 sentence: "A reply may cite lines of the repository at
|
||||
a commit or pages the agent fetched, and the broker checks every citation
|
||||
before it stores the message."
|
||||
- REQ-LIB-1 only gains sentences.
|
||||
- REQ-LIB-3's text and REQ-LIB-7 are unchanged. REQ-LIB-8 adds one form of
|
||||
exclusion REQ-LIB-3 doesn't have: naming a DOCX document by its id. A path
|
||||
exclusion also covers DOCX documents, through a converted file's old path or a
|
||||
whole book folder. An archived file and a DOCX document keep a universe, and a
|
||||
book where their source has one, so every lookup still names one universe.
|
||||
REQ-PROJ-3, the Console editor for a project's spec and docs, is unchanged.
|
||||
C2-11b opens the library's Markdown files in that same editor.
|
||||
- In `packages/tasks/requirements.json` the titles of REQ-LIB-1, REQ-LIB-2 and
|
||||
REQ-LIB-4 change, and those of REQ-LIB-5 and REQ-LIB-6 stay. REQ-LIB-1 becomes
|
||||
'A human-set library maps the user's repository onto universes, books, types,
|
||||
playgrounds, supporting and archive folders'. REQ-LIB-2 becomes 'Scope comes
|
||||
from the path or a DOCX's source, lookups default to canon, and only canon
|
||||
files are ever cited as canon'. REQ-LIB-4 becomes 'Git holds every change. The
|
||||
bus fast-forwards a read-only mirror every 60 seconds, the one place agents
|
||||
read Markdown'. REQ-LIB-8 and REQ-LIB-9 add two ids, so the count goes from 85
|
||||
to 87. REQ-LIB-8 is titled 'Case 2 story files become DOCX by a human-only
|
||||
one-way import or in the editor; Markdown stays for notes and docs', and
|
||||
REQ-LIB-9 'The Console's DOCX editor: only a human saves, each save is one
|
||||
commit, and the editor code is vendored and pinned'.
|
||||
- Open points O1 to O4 of draft 1 were ruled by Mos in mos-1367 B1 to B4.
|
||||
- Sage files the build rows under Vikunja 570 and 571 after the rewrite is
|
||||
approved. This amendment adds the requirements only.
|
||||
|
||||
## Risks and open questions
|
||||
|
||||
@@ -1788,6 +2123,26 @@ latest-by date and a hard target (Q176).
|
||||
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).
|
||||
- A DOCX file is a zip archive, so git can't show a line diff of a chapter.
|
||||
Review of a story change goes through the editor and the committed reader,
|
||||
not `git diff`. (Version 1.17.)
|
||||
- A converted chapter's DOCX is not in Obsidian's graph. Obsidian shows the
|
||||
chapter only through its archived Markdown, and no DOCX edit reaches that file's
|
||||
links. The DOCX's current links show in the Console's editor and its
|
||||
Connections view, and no graph shows them yet. (Version 1.17.)
|
||||
- Conversion can't be undone in place. Front matter, wiki link forms and
|
||||
unsupported blocks may not survive it, and the mirror can't refuse the user's
|
||||
push to an archived file. The controls are the fidelity report, the human's
|
||||
confirmation, an untouched original on refusal, and the kept archive.
|
||||
(Version 1.17.)
|
||||
- A restore of the stack's bus alone from a backup taken before an import drops
|
||||
the import's logged events, while the archive commit and the DOCX stay. A
|
||||
restore of the stack's whole host from such a backup also takes the writing
|
||||
store and the host's import records back to before the import. The archive
|
||||
commit stays in the user's Gitea repository and the DOCX stays on the writing
|
||||
store's remote, but the host keeps no record of the import. A bus restore counts
|
||||
import events, and the import's recovery covers both cases (Vikunja 571).
|
||||
(Version 1.17.)
|
||||
|
||||
## Milestones
|
||||
|
||||
|
||||
@@ -226,6 +226,8 @@ an operator inspect content and delivery evidence after an uncertain result.
|
||||
| `hosts.remove` | `{host}`; the human or the tester. Deletes the record, writes `grant.host.removed` and removes the installer key. A `host_grants` record for the host is `host-in-use`, a guarded event naming a host line left on the host `host-line-left`. |
|
||||
| `hosts.revoke` | `{agent,host,reason}`; the human, the business's assistant, or the tester when `bus.tester` is exactly `true` (row 235, #1741, r6 section 6). Anyone else gets `revoke-refused`. The tester check reads `bus.tester` again at the call, `tester-dev-only`. One transaction deletes the `host_grants` record and writes `grant.revoked` with `hostLine:"pending"`. With no await between, the key leaves memory and its file goes, and a `host.run` or executor in flight aborts. Then `remove <user> <tag>` runs on the host and `grant.revoke.closed` records `hostLine:"removed"` or `hostLineLeft`. A revoke with no record still runs, with `granted:false`. Returns `{agent,host,tag,granted,keyFile,hostLine or hostLineLeft}`. A restart with a pending marker runs the remove once the bus is up; `hosts.status` lists its tag under `revoking`. |
|
||||
| `hosts.sweep` | `{host}`; the human or the tester (r6 X3). Runs `list <user>` and removes each listed tag this business recorded for the host in a `grant.step` or `grant.revoked` that no live grant holds. Another business's line and an unrecorded `mosaic-` line stay. Writes `grant.host.swept {host,removed,left}`; `hostLineLeft` then counts from it. Returns `{host,removed,hostLineLeft}`. |
|
||||
| `grants.scope` | `{}`; the proven human CLI only (row 298, #1804, Q228). Returns the organization revision, effective scope and fallback flag. `launch.grantScope` missing or invalid covers nothing. |
|
||||
| `grants.scope.set` | `{revision,scope}`; the proven human CLI only, itself Approve for the scope change. Agents, testers, readers and Console caps refuse. Strict scope shape; optimistic organization revision, durable organization receipt, and `grant.scope` with human proof and old/new scope digests. General organization setters cannot write this setting. Scope matches per-method agent and every project, or agent/host and exact command arrays; host enrollment is excluded. In-scope requests by `launch.by` close via `scope` with protected `grant.scope.used` evidence, and run the existing grant executor without per-grant Approve. The raise returns the closed decision view to the chief; this shortcut sends no separate decision-close message. Outside scope waits as before. Scope narrowing before spend fails `scope-changed`; every issued grant keeps its records and revoke path. Empty default ships; the live value is Jason's to set. |
|
||||
| `grants.status` | `{service:"gitea"}`; the human, the tester or the chief (row 236, #1742, #436b r5). The admin slot's url, account and rotate-by date with `rotationDue`, `adminTokenLeft`, the `gitea_users` and `gitea_grants` records, approved decisions not yet run (`spentAtNextStart`), grants `running` or `settlePending`, agents held as `tokenUnknown`, the `serviceLeft` entries a sweep retries, and revokes still `revoking`. No token. |
|
||||
| `grants.run` | `{decision}`; the human only. Starts the Gitea executor of a `credential.mint` decision with a gitea grant the human said yes to, as the yes itself does (X1). Step 0's refusals come back with no record: `not-authorized`, `decision-consumed`, `grant-token-unknown`, `grant-busy`, `gitea-not-configured`. Returns `{decision,agent,started:true}`; the outcome is a `grant.applied` or `grant.failed` and a notice. |
|
||||
| `grants.revoke` | `{service:"gitea",agent,reason}`; the human, the business's assistant, or the tester when `bus.tester` is exactly `true`. The token leaves memory and its file goes, an executor in flight aborts, then one transaction deletes the `gitea_grants` record and writes `grant.revoked` with the token ids to delete. The deletes run with the admin token after C1 to C4, and `grant.revoke.closed` records `serviceLeft`, a list. `credentials.revoke AGENT gitea` and `credentials.delete` do the same for a stack-made user; delete also deletes every token of the user and sets `prohibit_login` (fire). |
|
||||
@@ -325,8 +327,9 @@ starts such a thread and sends a running one SIGTERM.
|
||||
Revocation (row 295, #1801, FED R4, federation design 3.7). A user thread is lost when no grant
|
||||
in the access file covers it at collaborator level, so a downgrade to viewer is a loss. One
|
||||
transaction ends the held claim of every session of every lost thread, op `revoke`, reason
|
||||
`access-revoked`; the run's next bus call refuses `not-holder`, and a new run of the thread
|
||||
refuses `thread-revoked` at `bindLaunch` and at `role.claim`. A rebind of an earlier host's run is
|
||||
`access-revoked`; the run's bus calls refuse `thread-revoked` until it ends (row 306, so the runner
|
||||
stops rather than exit as a lost session), and a new run of the thread refuses `thread-revoked` at
|
||||
`bindLaunch` and at `role.claim`. A rebind of an earlier host's run is
|
||||
allowed, so the launcher's recovery can end it. Four triggers end the claims, each with its `by`:
|
||||
the runtime's timer, every 30 s (`access.intervalMs` sets it in process, as `librarySync.intervalMs`
|
||||
does), which reads the file whatever its stamp (`access-timer`); a user's request or a
|
||||
|
||||
+287
-40
@@ -4,7 +4,7 @@ import { initializeControl, processingStatus } from '../../processing-control/sr
|
||||
import { NAME as HARNESS_NAME } from '../../plugins/src/kinds.mjs';
|
||||
import { loadRequirements } from '../../tasks/src/requirements.mjs';
|
||||
import { MERGE, LandingRefusal, readLanding } from './landing.mjs';
|
||||
import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers } from '../../business/src/vocabulary.mjs';
|
||||
import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers, grantScopeOf, grantInScope } from '../../business/src/vocabulary.mjs';
|
||||
|
||||
// Kept closed while S1 lands; its vocabulary is the integration authority.
|
||||
export const ACTIONS = Object.freeze([
|
||||
@@ -137,6 +137,9 @@ export const TESTER_VERBS = Object.freeze(['message.send', 'message.receive', 'm
|
||||
// collaborator grant, so a viewer gets access-refused on each.
|
||||
export const USER_VERBS = Object.freeze(['project.list', 'thread.create', 'thread.list', 'thread.open', 'thread.send', 'thread.messages', 'names',
|
||||
'thread.share', 'thread.unshare', 'thread.shares']);
|
||||
// Row 252 (#1753, design sections 2-3): the only verbs a Console owner cap reaches. Every other verb
|
||||
// refuses it human-required before its own checks, and these refuse every other cap.
|
||||
export const CONSOLE_VERBS = Object.freeze(['decision.preview.console', 'decision.resolve.console']);
|
||||
// Row 275 (193b1b, #1780, row 193 design 4.3 and 9): the user verbs a viewer grant reaches. They
|
||||
// read, so a live grant at any level covers them. thread.create, thread.open and thread.send need a
|
||||
// collaborator grant on the project and instance, so a viewer gets access-refused on each.
|
||||
@@ -287,6 +290,9 @@ export function writeGrantEvent(store, at, business, body, subject = null) {
|
||||
);
|
||||
return eid;
|
||||
}
|
||||
// Row 298 (#1804, Q228): the by of a close the chief's grant scope makes, with via 'scope'. No session
|
||||
// closes as it, and #approvedGrant takes it only with the grant.scope.used record of that close.
|
||||
const SCOPE_BY = 'grant-scope';
|
||||
export const HOSTS_VERBS = Object.freeze(['hosts.status', 'hosts.check', 'hosts.remove', 'hosts.revoke', 'hosts.sweep']);
|
||||
// Row 236 (#1742, #436b r5): the Gitea grant path's verbs. run and admin.* are the human's alone.
|
||||
export const GRANTS_VERBS = Object.freeze([
|
||||
@@ -409,6 +415,7 @@ export class Broker {
|
||||
#revokeUndo = [];
|
||||
#testerOn;
|
||||
#roleFile;
|
||||
#grantScope;
|
||||
// Row 236 (#436b): giteaGrant and giteaKeys from gitea-grant.mjs, or null, and the remote halves of
|
||||
// section 5's restart rows and of revokes a restart cut short, for reconcileGitea.
|
||||
#giteaGrant;
|
||||
@@ -429,14 +436,19 @@ export class Broker {
|
||||
// landRelease release and then signal, so the run's own calls in between answer launcher-released,
|
||||
// not not-holder, and the runner exits stopped instead of as a lost session.
|
||||
#launcherReleased = new Set();
|
||||
// Row 306 (#1813): runs whose claim #revokeLost ended, by business:run, until endLaunch. The launcher
|
||||
// signals only after its threadsState op returns, so the run's own calls in between answer
|
||||
// thread-revoked, not not-holder, and the runner exits stopped instead of as a lost session.
|
||||
#accessRevoked = new Set();
|
||||
#clock = 0;
|
||||
#processing;
|
||||
// credentialStatus is the runtime's metadata-only Credentials.status(); the broker never gets use().
|
||||
constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, library = null, usage = null, limits = null, harnessOn = null, credentialVerbs = null, hostGrant = null, hostKeys = null, processing = null, testerOn = null, roleFile = null, giteaGrant = null, giteaKeys = null, access = null, accessStamp = null, compactions = null }) {
|
||||
constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, library = null, usage = null, limits = null, harnessOn = null, credentialVerbs = null, hostGrant = null, hostKeys = null, processing = null, testerOn = null, grantScope = null, roleFile = null, giteaGrant = null, giteaKeys = null, access = null, accessStamp = null, compactions = null }) {
|
||||
if (!object(businesses) || !Object.keys(businesses).length) fail('invalid-business');
|
||||
// Row 194 b15 (sage-2501): the function-argument checks come before anything reads the store.
|
||||
if (harnessOn !== null && typeof harnessOn !== 'function') throw new TypeError('harnessOn must be a function');
|
||||
if (testerOn !== null && typeof testerOn !== 'function') throw new TypeError('testerOn must be a function');
|
||||
if (grantScope !== null && typeof grantScope !== 'function') throw new TypeError('grantScope must be a function');
|
||||
if (roleFile !== null && typeof roleFile !== 'function') throw new TypeError('roleFile must be a function');
|
||||
if (access !== null && typeof access !== 'function') throw new TypeError('access must be a function');
|
||||
if (accessStamp !== null && typeof accessStamp !== 'function') throw new TypeError('accessStamp must be a function');
|
||||
@@ -474,6 +486,9 @@ export class Broker {
|
||||
// sets bus.tester to exactly true; the bus runtime reads it live, so a hand edit counts at the next
|
||||
// tester lifecycle call and at the take. Without it the broker's own copy decides.
|
||||
this.#testerOn = testerOn ?? ((business) => this.#business(business).vars?.['bus.tester'] === true);
|
||||
// Row 298 (#1804, Q228): grantScope(business) reads {value, revision} atomically from the
|
||||
// organization file. Without a configured reader no scope can authorize a grant.
|
||||
this.#grantScope = grantScope ?? (() => ({ value: undefined, revision: null }));
|
||||
// Row 194 (#1692, D2.3): roleFile(name) loads role definition `name` from the roles directory as it
|
||||
// stands, for the hire checks. Without it a hire refuses not-configured.
|
||||
this.#roleFile = roleFile;
|
||||
@@ -1005,10 +1020,10 @@ export class Broker {
|
||||
covers(g, { business: t.business, project: t.project, instance: t.instance }, now, { level: 'collaborator' }));
|
||||
}
|
||||
// Design 3.7: on a loss, one transaction ends the held claim of every session of every user thread
|
||||
// the access file no longer covers, in every business. The run's next bus call refuses not-holder
|
||||
// (F10), and a later claim refuses thread-revoked. Runs only when the read is new: a forced read,
|
||||
// a stamp it hasn't swept, or no stamp at all. `doc` is a read the caller just made, so a request
|
||||
// reads once. Returns the ended claims.
|
||||
// the access file no longer covers, in every business. The run's bus calls refuse thread-revoked
|
||||
// until it ends (row 306), and a later claim refuses thread-revoked. Runs only when the read is
|
||||
// new: a forced read, a stamp it hasn't swept, or no stamp at all. `doc` is a read the caller just
|
||||
// made, so a request reads once. Returns the ended claims.
|
||||
#revokeLost(by, force = false, doc = this.#accessDoc(force)) {
|
||||
const stamp = this.#accessSeen.stamp;
|
||||
if (!force && stamp !== null && stamp === this.#accessSwept) return [];
|
||||
@@ -1023,6 +1038,7 @@ export class Broker {
|
||||
ended.push(h);
|
||||
}
|
||||
});
|
||||
for (const h of ended) this.#accessRevoked.add(h.business + ':' + h.holder_run);
|
||||
this.#accessSwept = stamp;
|
||||
return ended;
|
||||
}
|
||||
@@ -1067,7 +1083,8 @@ export class Broker {
|
||||
if (s.human) fail('agent-required');
|
||||
const h = this.#own(s);
|
||||
if (h?.op !== 'claim' || (s.thread ? h.generation !== this.#runGeneration(s) : h.holder_run !== s.run))
|
||||
fail(this.#launcherReleased.has(s.business + ':' + s.run) ? 'launcher-released' : 'not-holder');
|
||||
fail(this.#launcherReleased.has(s.business + ':' + s.run) ? 'launcher-released'
|
||||
: this.#accessRevoked.has(s.business + ':' + s.run) ? 'thread-revoked' : 'not-holder');
|
||||
return h;
|
||||
}
|
||||
// Row 264a (#1769, T2): the generation a thread run holds, its latest claim's in its session, or
|
||||
@@ -1392,6 +1409,7 @@ export class Broker {
|
||||
if (exitCode !== null && !Number.isSafeInteger(exitCode)) fail('invalid-request');
|
||||
this.#ended.add(key);
|
||||
this.#launcherReleased.delete(key);
|
||||
this.#accessRevoked.delete(key);
|
||||
for (const [cap, s] of this.#sessions)
|
||||
if (!s.human && !s.reader && s.business === record.business && s.run === record.run) this.#sessions.delete(cap);
|
||||
return this.#store.transaction(() => {
|
||||
@@ -1449,6 +1467,26 @@ export class Broker {
|
||||
if (!this.#userGrants(s).length) fail('access-revoked');
|
||||
return this.#cap(s);
|
||||
}
|
||||
// Row 252 (#1753, design section 2): the trusted Console transport invokes this only after
|
||||
// verifyPrincipal checked the console-cli wrapper, its webui `serve` parent and the signed-in owner.
|
||||
// The cap is the business human's with via 'console' and the owner subject. request() lets it reach
|
||||
// CONSOLE_VERBS only, and #human refuses it, so no cli verb accepts it.
|
||||
bindConsole(record) {
|
||||
keys(record, ['business', 'human', 'subject'], ['business', 'human', 'subject']);
|
||||
const b = this.#business(record.business);
|
||||
const x = record.subject;
|
||||
if (record.human !== b.human || !object(x) || Object.keys(x).length !== 3 ||
|
||||
!['provider', 'iss', 'sub'].every((k) => typeof x[k] === 'string' && x[k].length > 0 && x[k].length <= 512))
|
||||
fail('human-required');
|
||||
return this.#cap({
|
||||
business: record.business,
|
||||
human: b.human,
|
||||
via: 'console',
|
||||
console: Object.freeze({ provider: x.provider, iss: x.iss, sub: x.sub }),
|
||||
role: null,
|
||||
run: null,
|
||||
});
|
||||
}
|
||||
bindReader({ business }) {
|
||||
this.#business(business);
|
||||
return this.#cap({ business, reader: true, role: null, run: null, human: false });
|
||||
@@ -1497,10 +1535,127 @@ export class Broker {
|
||||
const raise = this.#raised(business, decision);
|
||||
if (d?.action !== 'credential.mint' || d.route_to !== 'human' || !raise?.grant) return null;
|
||||
const r = this.#closed(decision);
|
||||
if (r?.op !== 'resolved' || r.choice !== raise.approvalChoice || r.via !== 'cli' || r.by !== this.#business(business).human)
|
||||
return null;
|
||||
if (r?.op !== 'resolved' || r.choice !== raise.approvalChoice) return null;
|
||||
// Row 298 (#1804, Q228, sage-3594 Q4): a scope close counts only with its own grant.scope.used,
|
||||
// which only #scopeClose writes, under the grant actor.
|
||||
if (r.via === 'scope') return r.by === SCOPE_BY && this.#scopeUsed(business, decision, r.seq, raise.grant) ? raise.grant : null;
|
||||
if (r.by !== this.#business(business).human) return null;
|
||||
// Row 252 (#1753, design section 3, J14): a console close counts only with its own human.input,
|
||||
// which only the broker writes (recordEvent reserves the kind).
|
||||
if (!(r.via === 'console' ? this.#consoleInput(business, decision, r) : r.via === 'cli')) return null;
|
||||
return raise.grant;
|
||||
}
|
||||
// Row 298: the grant.scope.used record of scope close seq. Gitea and host grants only.
|
||||
#scopeUsed(business, decision, seq, grant) {
|
||||
if (grant.method !== 'gitea' && grant.method !== 'host') return false;
|
||||
const row = this.#store.get(
|
||||
`SELECT body FROM events WHERE business=? AND ${GRANT_EVIDENCE} AND json_extract(body,'$.action')='grant.scope.used'
|
||||
AND json_extract(body,'$.grantDecision')=? AND json_extract(body,'$.decisionSeq')=? AND json_extract(body,'$.method')=? LIMIT 1`,
|
||||
business, decision, seq, grant.method,
|
||||
);
|
||||
if (!row) return false;
|
||||
const evidence = JSON.parse(row.body);
|
||||
const checked = grantScopeOf(evidence.scope);
|
||||
// Verify the immutable raise-time snapshot, not today's scope: scope changes do not
|
||||
// invalidate already issued grants. The executor rechecks today's scope before spend.
|
||||
return !checked.fallback && typeof evidence.revision === 'string' && /^[a-f0-9]{64}$/.test(evidence.revision) &&
|
||||
evidence.agent === grant.agent && evidence.scopeSha256 === sha256(JSON.stringify(checked.scope)) &&
|
||||
grantInScope(checked.scope, grant);
|
||||
}
|
||||
// Row 298: the live organization snapshot. Missing/bad scope covers nothing. An unreadable
|
||||
// organization file or missing revision cannot authorize a scope close either.
|
||||
#scopeNow(business) {
|
||||
try {
|
||||
const snapshot = this.#grantScope(business);
|
||||
return { ...grantScopeOf(snapshot?.value), revision: snapshot?.revision ?? null };
|
||||
} catch {
|
||||
return { ...grantScopeOf(undefined), revision: null };
|
||||
}
|
||||
}
|
||||
|
||||
// Row 298 (D4, sage-3594 Q2/Q4): after a gitea or host raise, in its transaction. When the raiser is
|
||||
// launch.by, the decision went to the human and the grant is inside the scope, the broker closes it
|
||||
// with the approval choice, via 'scope', writes grant.scope.used and starts the executor, as the
|
||||
// human's Approve does. The spend, the records and the revoke are row 236's and 235's, unchanged.
|
||||
// Anything else is left in the inbox. launch.by is a hand edit, so a chief-of-staff instance the
|
||||
// tester or an instances.set adds never raises in scope (Q6).
|
||||
#scopeClose(s, view) {
|
||||
if (s.role !== this.#business(s.business).launch?.by) return view;
|
||||
const d = this.#store.get('SELECT id,route_to FROM decisions WHERE business=? AND id=?', s.business, view.id);
|
||||
if (d?.route_to !== 'human' || this.#closed(d.id)) return view;
|
||||
const raise = this.#raised(s.business, d.id);
|
||||
const { scope, revision } = this.#scopeNow(s.business);
|
||||
if (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision)) return view;
|
||||
if (!raise?.grant || !grantInScope(scope, raise.grant)) return view;
|
||||
const seq = Number(
|
||||
this.#store.run(
|
||||
'INSERT INTO decision_events(decision,at,op,by,choice,note,via) VALUES(?,?,?,?,?,?,?)',
|
||||
d.id, this.#now(), 'resolved', SCOPE_BY, raise.approvalChoice, null, 'scope',
|
||||
).lastInsertRowid,
|
||||
);
|
||||
this.#grantEvent(s.business, {
|
||||
action: 'grant.scope.used', grantDecision: d.id, decisionSeq: seq, method: raise.grant.method,
|
||||
agent: raise.grant.agent, scope, revision, scopeSha256: sha256(JSON.stringify(scope)),
|
||||
}, d.id);
|
||||
// As the resolve's: each executor rechecks all of this itself, so a rollback starts nothing.
|
||||
if (raise.grant.method === 'host') setImmediate(() => this.#runHostGrant(s.business, d.id));
|
||||
if (raise.grant.method === 'gitea') setImmediate(() => this.#startGitea(s.business, d.id));
|
||||
return this.#decisionView(this.#decision(s, d.id));
|
||||
}
|
||||
// Row 298 (D6, sage-3594 Q5): at the spend, a scope close needs its grant still inside the scope.
|
||||
// A narrowed scope ends it scope-changed, and the human's Approve is the way back.
|
||||
#scopeRecheck(business, decision, g) {
|
||||
if (this.#closed(decision)?.via === 'scope' && !grantInScope(this.#scopeNow(business).scope, g)) fail('scope-changed');
|
||||
}
|
||||
// Row 252: the console answer's human.input for close row r. Same decision, same seq, same choice,
|
||||
// no role or run, and a three-string owner subject.
|
||||
#consoleInput(business, decision, r) {
|
||||
const e = this.#store.get(
|
||||
`SELECT body FROM events WHERE business=? AND kind='human.input' AND subject=? AND actor_role IS NULL AND actor_run IS NULL
|
||||
AND json_extract(body,'$.via')='console' AND json_extract(body,'$.decisionSeq')=? ORDER BY seq LIMIT 1`,
|
||||
business,
|
||||
decision,
|
||||
r.seq,
|
||||
);
|
||||
const x = e && JSON.parse(e.body);
|
||||
return (
|
||||
!!x &&
|
||||
x.kind === 'answer' &&
|
||||
x.decision === decision &&
|
||||
x.op === 'resolved' &&
|
||||
x.choice === r.choice &&
|
||||
object(x.subject) &&
|
||||
['provider', 'iss', 'sub'].every((k) => typeof x.subject[k] === 'string' && x.subject[k].length > 0)
|
||||
);
|
||||
}
|
||||
// Row 252 (design section 1): a Console-eligible grant request, or null. Open is the caller's
|
||||
// check. Eligible is route_to human, action credential.mint, a raise with a validated grant binding
|
||||
// and exactly two distinct option keys, one of them approvalChoice. Approve is that key and deny is
|
||||
// the other. The summary carries only the binding's method, agent, host and target projects. The
|
||||
// digest binds the id, business, options, approval key and binding, so any change reads stale.
|
||||
#consoleResolution(d) {
|
||||
if (d.route_to !== 'human' || d.action !== 'credential.mint') return null;
|
||||
const raise = this.#raised(d.business, d.id);
|
||||
if (!object(raise?.grant) || typeof raise.grant.method !== 'string') return null;
|
||||
const options = JSON.parse(d.options);
|
||||
if (options.length !== 2 || options[0].key === options[1].key) return null;
|
||||
const approve = options.find((o) => o.key === raise.approvalChoice);
|
||||
if (!approve) return null;
|
||||
const deny = options.find((o) => o.key !== raise.approvalChoice);
|
||||
const g = raise.grant;
|
||||
const grant = {
|
||||
method: g.method,
|
||||
...(typeof g.agent === 'string' ? { agent: g.agent } : {}),
|
||||
...(typeof g.host === 'string' ? { host: g.host } : {}),
|
||||
...(Array.isArray(g.target?.projects) ? { projects: g.target.projects.filter((p) => typeof p === 'string') } : {}),
|
||||
};
|
||||
return {
|
||||
approve: { key: approve.key, text: approve.text },
|
||||
deny: { key: deny.key, text: deny.text },
|
||||
grant,
|
||||
digest: sha256(JSON.stringify([d.id, d.business, options, raise.approvalChoice, raise.grant])),
|
||||
};
|
||||
}
|
||||
// r6 X1: the executor's own spend. The caller owns the transaction. The consume record is the only
|
||||
// guarded body with `decision` (#436b section 7), so #spent counts it.
|
||||
#consumeGrant(business, decision, method) {
|
||||
@@ -1684,6 +1839,7 @@ export class Broker {
|
||||
}
|
||||
// r6 X2 step 1's checks after the spend, R4's binding again. Throws the code that ends the grant.
|
||||
#hostGrantRecheck(business, decision, g) {
|
||||
this.#scopeRecheck(business, decision, g);
|
||||
if (!this.#hostGrant || !this.#hostKeys) fail('host-not-configured');
|
||||
const definition = this.#hostDefinition(business, g.agent);
|
||||
if (this.#hostGrant.definitionSha256(definition) !== g.definitionSha256) fail('decision-mismatch');
|
||||
@@ -2374,11 +2530,13 @@ export class Broker {
|
||||
} catch (e) {
|
||||
const code = e instanceof BusError ? e.code : 'outcome-unknown';
|
||||
const list = out.grants.map((g) => g.grant).join(', ') || 'none';
|
||||
// Row 239 (#1745): a step B refusal after the writer's revoke ran.
|
||||
const tokens = code.endsWith('-revoked') ? ' Its credential tokens may already be revoked.' : '';
|
||||
this.#store.transaction(() => {
|
||||
this.#event(sc, 'action.allowed', { action: 'fire.failed', job, step, code }, name);
|
||||
this.#humanNotice(
|
||||
sc.business,
|
||||
`Fire of ${name}: job ${job} revoked its grants (${list}) but stopped at step ${step} (${code}). The instance is still firing. Run the fire again once ${code} is cleared.`,
|
||||
`Fire of ${name}: job ${job} revoked its grants (${list}) but stopped at step ${step} (${code}).${tokens} The instance is still firing. Run the fire again once ${code} is cleared.`,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -2484,7 +2642,7 @@ export class Broker {
|
||||
if (!this.#giteaGrant) fail('gitea-not-configured');
|
||||
const g = this.#giteaGrant.giteaGrantCheck(a.grant, this.#now().slice(0, 10));
|
||||
return this.#store.transaction(() => {
|
||||
const result = this.#dispatch(s, 'decision.raise', a, null, this.#giteaGrantBinding(s.business, g));
|
||||
const result = this.#scopeClose(s, this.#dispatch(s, 'decision.raise', a, null, this.#giteaGrantBinding(s.business, g)));
|
||||
this.#secretCheck(result);
|
||||
return result;
|
||||
});
|
||||
@@ -2493,7 +2651,7 @@ export class Broker {
|
||||
if (a.grant?.method === 'host') {
|
||||
const g = this.#hostGrant.hostGrantCheck(a.grant, this.#now().slice(0, 10));
|
||||
return this.#store.transaction(() => {
|
||||
const result = this.#dispatch(s, 'decision.raise', a, null, this.#hostGrantBinding(s.business, g));
|
||||
const result = this.#scopeClose(s, this.#dispatch(s, 'decision.raise', a, null, this.#hostGrantBinding(s.business, g)));
|
||||
this.#secretCheck(result);
|
||||
return result;
|
||||
});
|
||||
@@ -2615,6 +2773,7 @@ export class Broker {
|
||||
}
|
||||
// Section 4 step 1's checks after the spend. Throws the code that ends the grant.
|
||||
#giteaRecheck(business, decision, g) {
|
||||
this.#scopeRecheck(business, decision, g);
|
||||
const { b, role, definition } = this.#giteaRole(business, g.agent);
|
||||
if (this.#giteaGrant.definitionSha256(definition) !== g.definitionSha256) fail('decision-mismatch');
|
||||
if (this.#giteaGrant.identitySha256(role, b.projects) !== g.identitySha256) fail('decision-mismatch');
|
||||
@@ -2765,10 +2924,22 @@ export class Broker {
|
||||
} catch {}
|
||||
return code;
|
||||
}
|
||||
// Step 1, one transaction: the spend and the rechecks. A refusal rolls the spend back.
|
||||
// Step 1, one transaction: spend and recheck. scope-changed ends spent (D6);
|
||||
// other refusals retain the existing rollback behavior.
|
||||
let checked;
|
||||
try {
|
||||
checked = this.#store.transaction(() => this.#giteaRecheck(business, decision, this.#consumeGrant(business, decision, 'gitea')));
|
||||
checked = this.#store.transaction(() => {
|
||||
const consumed = this.#consumeGrant(business, decision, 'gitea');
|
||||
try {
|
||||
return this.#giteaRecheck(business, decision, consumed);
|
||||
} catch (e) {
|
||||
// D6: scope narrowing is a terminal, spent failure. Other recheck
|
||||
// refusals retain row 236's transaction/rollback behavior.
|
||||
if (!(e instanceof BusError) || e.code !== 'scope-changed') throw e;
|
||||
this.#giteaFailed(business, decision, agent, e.code);
|
||||
return { scopeChanged: true };
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
const code = e instanceof BusError ? e.code : 'gitea-failed';
|
||||
if (code !== 'not-authorized' && code !== 'decision-consumed') {
|
||||
@@ -2778,6 +2949,7 @@ export class Broker {
|
||||
}
|
||||
return code;
|
||||
}
|
||||
if (checked.scopeChanged) return 'scope-changed';
|
||||
return await this.#giteaSteps(business, decision, g, signal, log, api, meta, checked, stop);
|
||||
} finally {
|
||||
this.#giteaKeys.release(business, agent, signal);
|
||||
@@ -4544,6 +4716,9 @@ export class Broker {
|
||||
grants = this.#userGrants(s, doc);
|
||||
if (!grants.length) fail('access-revoked');
|
||||
}
|
||||
// Row 252 (design section 2, N4): a console cap reaches the two console verbs only, and nothing
|
||||
// else reaches them. decision.resolve stays off the list.
|
||||
if (!!s.console !== CONSOLE_VERBS.includes(request.verb)) fail('human-required');
|
||||
const args = request.args ?? {};
|
||||
if (!object(args)) fail('invalid-request');
|
||||
this.#secretCheck(args);
|
||||
@@ -4585,6 +4760,8 @@ export class Broker {
|
||||
if (CREDENTIAL_VERBS.includes(request.verb)) return this.#requestCredential(s, request.verb, structuredClone(args));
|
||||
if (request.verb === 'instance.hire' || request.verb === 'instance.fire')
|
||||
return this.#chiefLifecycle(s, request.verb, structuredClone(args));
|
||||
if (request.verb === 'grants.scope' || request.verb === 'grants.scope.set')
|
||||
return this.#grantScopeVerb(s, request.verb, structuredClone(args));
|
||||
if (["limits", "limits.set", "organization.set", "organization.revision", "plugins", "plugins.set", "instances", "instances.set", "routes", "routes.set", "veto.set"].includes(request.verb)) {
|
||||
// Row 123 (#1617, REQ-COS-1, REQ-LIMIT-1): organization.set is limits.set for a role whose
|
||||
// file lists organization.change. It takes the same keys, runs the same write path and
|
||||
@@ -4682,6 +4859,34 @@ export class Broker {
|
||||
throw this.#refused(s, e, request);
|
||||
}
|
||||
}
|
||||
// Row 298 (#1804, Q228, sage-3594 Q3): the chief's grant scope, read and written by the human at the
|
||||
// cli alone, and the write is the human's Approve of the change. #human is the one check, so an
|
||||
// agent, the chief, the tester and a reader refuse human-required (the Console's cap never gets
|
||||
// here). bindHuman binds only the business's human. The writer is the organization writer's
|
||||
// grantScope; ORGANIZATION_KEYS doesn't name grantScope, so organization.set and limits.set can't
|
||||
// write it. Outside the store transaction, as the organization branch.
|
||||
#grantScopeVerb(s, verb, a) {
|
||||
this.#human(s);
|
||||
if (!this.#organization) fail('not-configured');
|
||||
if (verb === 'grants.scope') keys(a, []);
|
||||
else keys(a, ['revision', 'scope'], ['revision', 'scope']);
|
||||
let result;
|
||||
try {
|
||||
result = this.#organization(s.business, verb, a, s.human);
|
||||
} catch (e) {
|
||||
this.#organizationFail(e, verb);
|
||||
}
|
||||
this.#secretCheck(result);
|
||||
// The change's record: the human proof bindHuman checked, and the old and new scopes' sha256.
|
||||
// The writer's changes.jsonl entry holds the values.
|
||||
if (verb === 'grants.scope.set')
|
||||
this.#store.transaction(() => this.#grantEvent(s.business, {
|
||||
action: 'grant.scope', by: s.human, via: s.via, outsideAgent: s.outsideAgent === true,
|
||||
oldSha256: sha256(JSON.stringify(result.old)), newSha256: sha256(JSON.stringify(result.scope)),
|
||||
transaction: result.transaction ?? null,
|
||||
}));
|
||||
return result;
|
||||
}
|
||||
// Row 199 (#1697, design section 6). The token of an issue comes in args from the human's stdin; the
|
||||
// secret check above has refused one the bus already holds, a refusal records its code alone, and
|
||||
// nothing returned carries it. Outside the store transaction, as the organization branch.
|
||||
@@ -4845,6 +5050,9 @@ export class Broker {
|
||||
// The bus code for an organization writer's refusal. Anything the writer didn't refuse by name is
|
||||
// outcome-unknown: the write may have committed.
|
||||
#organizationFail(e, verb) {
|
||||
// Row 239 (#1745): the writer marks an error raised after a fire's or a delete's revoke ran, so
|
||||
// the human reads that the tokens may already be revoked. Without the mark, today's code.
|
||||
if (e.revoked) fail(e.code === "revision-conflict" ? "revision-conflict-revoked" : "outcome-unknown-revoked");
|
||||
if (e.code === "revision-conflict") fail("revision-conflict");
|
||||
if (e.code?.startsWith("organization-")) fail(e.code);
|
||||
if (e.code === "invalid-plugins" || e.code === "unknown-plugin") fail(e.code);
|
||||
@@ -5074,6 +5282,7 @@ export class Broker {
|
||||
const s = this.#session(cap);
|
||||
try {
|
||||
if (s.user) fail('access-refused');
|
||||
if (s.console) fail('human-required');
|
||||
this.#processingWork();
|
||||
keys(request, ['verb', 'args'], ['verb']);
|
||||
if (!TASK_VERBS.includes(request.verb)) fail('unknown-verb');
|
||||
@@ -5112,6 +5321,7 @@ export class Broker {
|
||||
let args;
|
||||
try {
|
||||
if (s.user) fail('access-refused');
|
||||
if (s.console) fail('human-required');
|
||||
keys(request, ['verb', 'args'], ['verb']);
|
||||
this.#processingWork();
|
||||
if (!verb) fail('unknown-verb');
|
||||
@@ -5171,6 +5381,7 @@ export class Broker {
|
||||
let a, entry, timeout, bound;
|
||||
try {
|
||||
if (s.user) fail('access-refused');
|
||||
if (s.console) fail('human-required');
|
||||
this.#processingWork();
|
||||
keys(request, ['verb', 'args'], ['verb']);
|
||||
if (!HOST_VERBS.includes(request.verb)) fail('unknown-verb');
|
||||
@@ -5494,15 +5705,38 @@ export class Broker {
|
||||
}
|
||||
return this.#decisionView(this.#decision(s, did));
|
||||
}
|
||||
// Row 252 (#1753, design section 3): the Console's read of one grant request before its
|
||||
// confirmation. It returns the two answers, a safe grant summary and the digest the answer must
|
||||
// carry. Anything not eligible refuses, so the browser never decides eligibility (J1).
|
||||
case 'decision.preview.console': {
|
||||
keys(a, ['id'], ['id']);
|
||||
identifier(a.id);
|
||||
if (!s.console || s.human !== b.human) fail('human-required');
|
||||
const d = this.#decision(s, a.id);
|
||||
if (this.#closed(d.id)) fail('decision-closed');
|
||||
const c = this.#consoleResolution(d);
|
||||
if (!c) fail('decision-not-eligible');
|
||||
return { id: d.id, business: d.business, question: d.question, ...c };
|
||||
}
|
||||
case 'decision.resolve.console':
|
||||
case 'decision.resolve':
|
||||
case 'decision.seen':
|
||||
case 'decision.withdraw': {
|
||||
keys(a, ['id', 'choice', 'note'], ['id']);
|
||||
const viaConsole = verb === 'decision.resolve.console';
|
||||
if (viaConsole) keys(a, ['id', 'choice', 'digest'], ['id', 'choice', 'digest']);
|
||||
else keys(a, ['id', 'choice', 'note'], ['id']);
|
||||
identifier(a.id);
|
||||
if (a.note !== undefined) string(a.note);
|
||||
const d = this.#decision(s, a.id);
|
||||
if (this.#closed(d.id)) fail('decision-closed');
|
||||
if (verb === 'decision.withdraw') {
|
||||
// Row 252 (design section 3): the Console answer rechecks eligibility, the digest and the choice
|
||||
// in this transaction. It answers one of the two keys, never a key the browser names (J2).
|
||||
if (viaConsole) {
|
||||
if (!s.console || s.human !== b.human) fail('human-required');
|
||||
const c = this.#consoleResolution(d);
|
||||
if (!c) fail('decision-not-eligible');
|
||||
if (a.digest !== c.digest || ![c.approve.key, c.deny.key].includes(a.choice)) fail('decision-stale');
|
||||
} else if (verb === 'decision.withdraw') {
|
||||
if (s.human) {
|
||||
this.#human(s);
|
||||
} else {
|
||||
@@ -5514,32 +5748,39 @@ export class Broker {
|
||||
this.#agent(s);
|
||||
if (s.role !== d.route_to) fail('not-resolver');
|
||||
}
|
||||
const op = verb === 'decision.resolve' ? 'resolved' : verb === 'decision.seen' ? 'seen' : 'withdrawn';
|
||||
const op = verb === 'decision.resolve' || viaConsole ? 'resolved' : verb === 'decision.seen' ? 'seen' : 'withdrawn';
|
||||
if (op === 'resolved' && !JSON.parse(d.options).some((o) => o.key === a.choice))
|
||||
fail('invalid-choice');
|
||||
this.#store.run(
|
||||
'INSERT INTO decision_events(decision,at,op,by,choice,note,via) VALUES(?,?,?,?,?,?,?)',
|
||||
d.id,
|
||||
this.#now(),
|
||||
op,
|
||||
by,
|
||||
op === 'resolved' ? a.choice : null,
|
||||
a.note ?? null,
|
||||
s.human ? 'cli' : 'broker',
|
||||
const seq = Number(
|
||||
this.#store.run(
|
||||
'INSERT INTO decision_events(decision,at,op,by,choice,note,via) VALUES(?,?,?,?,?,?,?)',
|
||||
d.id,
|
||||
this.#now(),
|
||||
op,
|
||||
by,
|
||||
op === 'resolved' ? a.choice : null,
|
||||
a.note ?? null,
|
||||
s.console ? 'console' : s.human ? 'cli' : 'broker',
|
||||
).lastInsertRowid,
|
||||
);
|
||||
// Row 234 (#1740, r6 X1): the human's cli approve of a grant mint starts the executor once this
|
||||
// transaction commits. It rechecks all of this itself, so a rollback starts nothing.
|
||||
const grant = op === 'resolved' ? this.#approvedGrant(s.business, d.id) : null;
|
||||
if (grant?.method === 'host-enroll') setImmediate(() => this.#runEnrollment(s.business, d.id));
|
||||
if (grant?.method === 'host') setImmediate(() => this.#runHostGrant(s.business, d.id));
|
||||
if (grant?.method === 'gitea') setImmediate(() => this.#startGitea(s.business, d.id));
|
||||
// Row 252 (N2): the human.input comes before #approvedGrant, which needs a console answer's
|
||||
// record to start the executor. Its body names the owner subject and the close's seq.
|
||||
if (s.human)
|
||||
this.#event(
|
||||
s,
|
||||
'human.input',
|
||||
{ kind: 'answer', decision: d.id, op, choice: a.choice ?? null },
|
||||
s.console
|
||||
? { kind: 'answer', decision: d.id, op, choice: a.choice, via: 'console', subject: s.console, decisionSeq: seq }
|
||||
: { kind: 'answer', decision: d.id, op, choice: a.choice ?? null },
|
||||
d.id,
|
||||
);
|
||||
// Row 234 (#1740, r6 X1): the human's cli approve of a grant mint starts the executor once this
|
||||
// transaction commits. It rechecks all of this itself, so a rollback starts nothing. Row 252: a
|
||||
// Console approve does too. A deny closes the decision with the other key and starts nothing.
|
||||
const grant = op === 'resolved' ? this.#approvedGrant(s.business, d.id) : null;
|
||||
if (grant?.method === 'host-enroll') setImmediate(() => this.#runEnrollment(s.business, d.id));
|
||||
if (grant?.method === 'host') setImmediate(() => this.#runHostGrant(s.business, d.id));
|
||||
if (grant?.method === 'gitea') setImmediate(() => this.#startGitea(s.business, d.id));
|
||||
// Row 115 (#1609): a close by anyone but the raising run tells the raising role, routed like message.send's.
|
||||
// Row 133 (#1627): no messaging check here. decision.raise refused a route that can't message the
|
||||
// raiser, definitions change only with a pinned copy and arbiters only through the business
|
||||
@@ -5566,7 +5807,7 @@ export class Broker {
|
||||
this.#now(),
|
||||
s.business,
|
||||
s.human ? 'human' : s.role,
|
||||
s.human ? 'human-cli' : s.run,
|
||||
s.console ? 'human-console' : s.human ? 'human-cli' : s.run,
|
||||
d.raised_by_role,
|
||||
'DECISION',
|
||||
null,
|
||||
@@ -5576,7 +5817,8 @@ export class Broker {
|
||||
);
|
||||
this.#route(s.business, mid, d.raised_by_role, session);
|
||||
}
|
||||
return grant ? { id: d.id, op, grant: 'started' } : { id: d.id, op };
|
||||
const answer = viaConsole ? { id: d.id, op, choice: a.choice } : { id: d.id, op };
|
||||
return grant ? { ...answer, grant: 'started' } : answer;
|
||||
}
|
||||
case 'message.send': {
|
||||
keys(a, ['to', 'body', 'class', 'in_reply_to', 'corrects', 'decision'], ['to', 'body']);
|
||||
@@ -5607,13 +5849,13 @@ export class Broker {
|
||||
)
|
||||
fail('tester-reply-required');
|
||||
// FED R3 (design 3.6 item 7, B6): a run answers a user only in reply to that user's message
|
||||
// in its own thread, the thread the user owns. Anything else, another user's message or one
|
||||
// outside the thread included, refuses.
|
||||
// in its own session of the thread the user owns (264e, deferred B6). A sibling
|
||||
// session, another user or another thread cannot supply that citation.
|
||||
if (to === 'user') {
|
||||
const m = id(a.in_reply_to) && s.thread
|
||||
? this.#store.get(
|
||||
"SELECT m.from_run FROM messages m JOIN message_threads mt ON mt.message=m.id JOIN threads t ON t.id=mt.thread WHERE m.id=? AND m.business=? AND m.from_role='user' AND mt.thread=? AND t.owner=m.from_run",
|
||||
a.in_reply_to, s.business, s.thread)
|
||||
"SELECT m.from_run FROM messages m JOIN message_threads mt ON mt.message=m.id JOIN threads t ON t.id=mt.thread WHERE m.id=? AND m.business=? AND m.from_role='user' AND mt.session=? AND t.owner=m.from_run",
|
||||
a.in_reply_to, s.business, s.session)
|
||||
: null;
|
||||
if (!m) fail('user-reply-required');
|
||||
}
|
||||
@@ -5932,7 +6174,12 @@ export class Broker {
|
||||
s.business,
|
||||
s.human || s.reader ? 'human' : s.role,
|
||||
)
|
||||
.map((d) => this.#decisionView(d));
|
||||
.map((d) => {
|
||||
// Row 252: a grant request the Console can answer carries its two answers. The Console
|
||||
// shows buttons from this, and the answer path rechecks it at the bus.
|
||||
const c = s.human || s.reader ? this.#consoleResolution(d) : null;
|
||||
return c ? { ...this.#decisionView(d), console: c } : this.#decisionView(d);
|
||||
});
|
||||
if (verb === 'agents') {
|
||||
const claims = this.#store.all(
|
||||
"SELECT * FROM role_claims c WHERE business=? AND op='claim' AND seq=(SELECT max(seq) FROM role_claims WHERE business=c.business AND role=c.role) ORDER BY role",
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { connect } from 'node:net';
|
||||
import { BusError, PROJECT_SAVE_VERB, PROJECT_SAVE_REQUEST_MAX } from './broker.mjs';
|
||||
export class Client {
|
||||
constructor({ path, cap, human, tester, user, timeout = 5000 }) {
|
||||
constructor({ path, cap, human, tester, user, console: owner, timeout = 5000 }) {
|
||||
this.path = path;
|
||||
this.cap = cap;
|
||||
this.human = human;
|
||||
this.tester = tester;
|
||||
this.user = user;
|
||||
this.console = owner;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
call(verb, args = {}) {
|
||||
const request =
|
||||
JSON.stringify({
|
||||
...(this.human ? { human: this.human } : this.tester ? { tester: this.tester } : this.user ? { user: this.user } : { cap: this.cap }),
|
||||
...(this.human ? { human: this.human } : this.tester ? { tester: this.tester } : this.user ? { user: this.user } : this.console ? { console: this.console } : { cap: this.cap }),
|
||||
verb,
|
||||
args,
|
||||
}) + '\n';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Row 252 (#1753, design section 2): the Console owner's transport shim, spawned only by the webui
|
||||
// `cli.mjs serve` process for one preview or one answer. Not a second product CLI. The Console sets
|
||||
// the nonce, the business and the signed-in (provider, iss, sub) in the spawn environment, and the
|
||||
// bus reads them back from this process's initial environment. Nothing respawns here, so the
|
||||
// Console stays the direct parent. Input is a verb and its args only: a body that names a business,
|
||||
// a subject or any other key is refused, never merged.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { Client } from './client.mjs';
|
||||
import { readProcess } from './human.mjs';
|
||||
import { CONSOLE_VERBS } from './broker.mjs';
|
||||
const env = process.env;
|
||||
try {
|
||||
if (!/^[a-f0-9]{64}$/.test(env.MOSAIC_BUS_CONSOLE_NONCE ?? '')) throw Object.assign(Error(), { code: 'human-required' });
|
||||
const request = JSON.parse(readFileSync(0, 'utf8'));
|
||||
if (
|
||||
!request ||
|
||||
typeof request !== 'object' ||
|
||||
Array.isArray(request) ||
|
||||
Object.keys(request).some((k) => !['verb', 'args'].includes(k)) ||
|
||||
!CONSOLE_VERBS.includes(request.verb)
|
||||
)
|
||||
throw Error();
|
||||
const me = readProcess(process.pid);
|
||||
const client = new Client({
|
||||
path: process.argv[2],
|
||||
console: {
|
||||
business: env.MOSAIC_BUS_CONSOLE_BUSINESS,
|
||||
provider: env.MOSAIC_BUS_CONSOLE_PROVIDER,
|
||||
iss: env.MOSAIC_BUS_CONSOLE_ISS,
|
||||
sub: env.MOSAIC_BUS_CONSOLE_SUB,
|
||||
pid: me.pid,
|
||||
startTime: me.startTime,
|
||||
nonce: env.MOSAIC_BUS_CONSOLE_NONCE,
|
||||
},
|
||||
});
|
||||
const result = await client.call(request.verb, request.args ?? {});
|
||||
process.stdout.write(JSON.stringify(result) + '\n');
|
||||
} catch (e) {
|
||||
process.stderr.write((/^[a-z-]{1,64}$/.test(e.code ?? '') ? e.code : 'invalid-request') + '\n');
|
||||
process.exitCode = 2;
|
||||
}
|
||||
@@ -10,6 +10,10 @@ const MARKERS = [
|
||||
'CODEX_THREAD_ID',
|
||||
'PI_AGENT_DIR',
|
||||
];
|
||||
// Row 252 (#1753, Webby N5): the Console wrapper's nonce and the subject its spawn bound. The
|
||||
// verifier reads each from the wrapper's initial environment, never from its proof alone.
|
||||
export const CONSOLE_ENV = Object.freeze(['MOSAIC_BUS_CONSOLE_NONCE', 'MOSAIC_BUS_CONSOLE_BUSINESS', 'MOSAIC_BUS_CONSOLE_PROVIDER', 'MOSAIC_BUS_CONSOLE_ISS', 'MOSAIC_BUS_CONSOLE_SUB']);
|
||||
const NONCE_KEYS = ['MOSAIC_BUS_CLI_NONCE', 'MOSAIC_BUS_USER_NONCE', ...CONSOLE_ENV];
|
||||
const refuse = () => {
|
||||
throw new BusError('human-required');
|
||||
};
|
||||
@@ -31,7 +35,7 @@ export function readProcess(pid, { readFile = readFileSync, stat = lstatSync } =
|
||||
for (const entry of environment.split('\0')) {
|
||||
const i = entry.indexOf('=');
|
||||
const key = entry.slice(0, i);
|
||||
if (['MOSAIC_BUS_CLI_NONCE', 'MOSAIC_BUS_USER_NONCE'].includes(key) || MARKERS.includes(key)) env[key] = entry.slice(i + 1);
|
||||
if (NONCE_KEYS.includes(key) || MARKERS.includes(key)) env[key] = entry.slice(i + 1);
|
||||
}
|
||||
return {
|
||||
pid,
|
||||
|
||||
@@ -5,8 +5,8 @@ import { verifyTester, testerNonce } from './tester.mjs';
|
||||
|
||||
// Trusted transport API. Identity is held only by this connection's transient session.
|
||||
// User mapping and live grant authorization belong to R3, not to this verifier.
|
||||
export function verifyPrincipal(request, { peer, paths, businesses, launches = [], readProcess = defaultRead, readTesterNonce = testerNonce }) {
|
||||
const kinds = ['human', 'tester', 'user'].filter(k => request?.[k]);
|
||||
export function verifyPrincipal(request, { peer, paths, businesses, launches = [], readProcess = defaultRead, readTesterNonce = testerNonce, consoleOwners = [] }) {
|
||||
const kinds = ['human', 'tester', 'user', 'console'].filter(k => request?.[k]);
|
||||
if (kinds.length !== 1 || request.cap) throw new BusError('invalid-envelope');
|
||||
const kind = kinds[0], proof = request[kind];
|
||||
const error = kind === 'tester' ? 'tester-required' : 'human-required';
|
||||
@@ -16,6 +16,7 @@ export function verifyPrincipal(request, { peer, paths, businesses, launches = [
|
||||
const { business } = verifyTester(proof, { cliPath: paths.tester, launches, readProcess, readNonce: readTesterNonce });
|
||||
return { kind: 'tester', business, id: 'tester' };
|
||||
}
|
||||
if (kind === 'console') return verifyConsole(proof, { peer, paths, businesses, launches, readProcess, consoleOwners });
|
||||
let checked = proof, parent;
|
||||
if (kind === 'user') {
|
||||
if (!proof || typeof proof.user !== 'string' || !proof.user ||
|
||||
@@ -42,3 +43,38 @@ export function verifyPrincipal(request, { peer, paths, businesses, launches = [
|
||||
if (!id) throw new BusError('unknown-business');
|
||||
return { kind: 'owner', business, id };
|
||||
}
|
||||
|
||||
// Row 252 (#1753, design section 2): the Console owner. The same-tree console-cli.mjs wrapper, its own
|
||||
// nonce namespace and the human walk, with the exact same-tree webui `cli.mjs serve` as its direct
|
||||
// parent, read again after the walk as the user kind does. Business and (provider, iss, sub) must be
|
||||
// the wrapper's initial spawn environment and one of the checked console.sso subjects the runtime
|
||||
// was started with (Webby N1: every configured subject is an owner). The Console's mapping is trusted
|
||||
// as row 196 permits; the bus does not exchange an ID token itself.
|
||||
const SUBJECT_KEYS = ['provider', 'iss', 'sub'];
|
||||
const SUBJECT_ENV = { business: 'MOSAIC_BUS_CONSOLE_BUSINESS', provider: 'MOSAIC_BUS_CONSOLE_PROVIDER', iss: 'MOSAIC_BUS_CONSOLE_ISS', sub: 'MOSAIC_BUS_CONSOLE_SUB' };
|
||||
function verifyConsole(proof, { peer, paths, businesses, launches, readProcess, consoleOwners }) {
|
||||
const refuse = () => { throw new BusError('human-required'); };
|
||||
if (!proof || typeof proof !== 'object' ||
|
||||
Object.keys(proof).some(k => !['pid', 'startTime', 'nonce', 'business', ...SUBJECT_KEYS].includes(k)) ||
|
||||
SUBJECT_KEYS.some(k => typeof proof[k] !== 'string' || !proof[k] || proof[k].length > 512)) refuse();
|
||||
if (typeof paths.consoleCli !== 'string' || typeof paths.console !== 'string') refuse();
|
||||
const { provider, iss, sub, ...checked } = proof;
|
||||
let child, parent;
|
||||
try {
|
||||
child = readProcess(proof.pid);
|
||||
parent = readProcess(child.ppid);
|
||||
if (parent.uid !== peer.uid || parent.argv[1] !== resolve(paths.console) || parent.argv[2] !== 'serve') refuse();
|
||||
for (const [key, name] of Object.entries(SUBJECT_ENV)) if (child.env?.[name] !== proof[key]) refuse();
|
||||
} catch { refuse(); }
|
||||
const { business } = verifyHuman(checked, { cliPath: paths.consoleCli, launches, readProcess, nonceKey: 'MOSAIC_BUS_CONSOLE_NONCE' });
|
||||
try {
|
||||
const again = readProcess(proof.pid), up = readProcess(parent.pid);
|
||||
if (again.ppid !== parent.pid || again.startTime !== child.startTime || up.startTime !== parent.startTime ||
|
||||
up.ppid !== parent.ppid || up.uid !== parent.uid || up.argv[1] !== parent.argv[1] || up.argv[2] !== 'serve') refuse();
|
||||
for (const [key, name] of Object.entries(SUBJECT_ENV)) if (again.env?.[name] !== proof[key]) refuse();
|
||||
} catch { refuse(); }
|
||||
if (!Array.isArray(consoleOwners) || !consoleOwners.some(o => o && o.provider === provider && o.iss === iss && o.sub === sub)) refuse();
|
||||
const id = businesses[business]?.human;
|
||||
if (!id) throw new BusError('unknown-business');
|
||||
return { kind: 'console', business, id, via: 'console', subject: { provider, iss, sub } };
|
||||
}
|
||||
@@ -106,7 +106,15 @@ export async function startBroker({
|
||||
// Without it no user binds. R6 and R8 pass it; the live boot config doesn't yet. FED R4 (#1801):
|
||||
// intervalMs, the revocation timer's period (design 3.7, 30 s), in-process only like librarySync's.
|
||||
access = null,
|
||||
// Row 252 (#1753, design section 2): the checked console.sso subjects, [{provider, iss, sub}], each an
|
||||
// owner of the business (Webby N1). The boot config carries them; no socket verb sets them. Empty, no
|
||||
// Console owner binds and the terminal owner is unchanged.
|
||||
consoleOwners = [],
|
||||
}) {
|
||||
if (!Array.isArray(consoleOwners) || consoleOwners.length > 512 || consoleOwners.some((o) => o === null || typeof o !== 'object' || Array.isArray(o) ||
|
||||
Object.keys(o).length !== 3 || ['provider', 'iss', 'sub'].some((k) => typeof o[k] !== 'string' || !o[k] || o[k].length > 512)))
|
||||
throw new BusError('invalid-host-request');
|
||||
const owners = Object.freeze(consoleOwners.map((o) => Object.freeze({ provider: o.provider, iss: o.iss, sub: o.sub })));
|
||||
if (access !== null && (typeof access?.file !== 'string' || access.context === null || typeof access.context !== 'object'
|
||||
|| (Object.hasOwn(access, 'intervalMs') && !(Number.isSafeInteger(access.intervalMs) && access.intervalMs > 0))))
|
||||
throw new BusError('invalid-host-request');
|
||||
@@ -153,11 +161,11 @@ export async function startBroker({
|
||||
const levels=organization?await harnessLevels():()=>null;
|
||||
const settings=organization?launchSettings({...organization,sessions:()=>readSessions(dataRoot),levels}):null;
|
||||
// Row 194 (#1692, brief item 8, row 199 D13): a fire or a delete revokes the agent's credentials
|
||||
// after the writer's checks and before its write. Fire revokes each service of the entry and keeps
|
||||
// the store directory, delete calls remove (sage-1593). A service the running bus doesn't know (an
|
||||
// agent created since the start) has nothing in memory, so it goes on, and then no token or tmp
|
||||
// file may remain in the agent's store directory (sage-1592). Any failure stops the verb with the
|
||||
// file unchanged.
|
||||
// after the writer's checks and before its write, under the writer's lock (row 239). Fire revokes
|
||||
// each service of the entry and keeps the store directory, delete calls remove (sage-1593). A
|
||||
// service the running bus doesn't know (an agent created since the start) has nothing in memory,
|
||||
// so it goes on, and then no token or tmp file may remain in the agent's store directory
|
||||
// (sage-1592). Any failure stops the verb with the file unchanged.
|
||||
const revokeFailed = (message) => new OrganizationError('credential-revoke-failed', message);
|
||||
function revoke(business, agent, how, services) {
|
||||
const instance = business + '/' + agent;
|
||||
@@ -315,6 +323,8 @@ export async function startBroker({
|
||||
if(verb==='instances')return instances.read(id,actor);
|
||||
// Row 65 (mos-0840): the Console's routing table.
|
||||
if(verb==='routes')return settings.routes(id);
|
||||
// Row 298 (#1804, Q228): the chief's grant scope as the file stands, the fallback shown.
|
||||
if(verb==='grants.scope')return instances.grantScopeRead(id);
|
||||
// Row 194 (#1692, V5): a delete's checks at the verb, with no write and no reload.
|
||||
if(verb==='instances.check')return instances.checkDelete(id,args.instance,actor);
|
||||
// Row 194 b6 (webby-0094 B1): a fire's checks against the file, before step A.
|
||||
@@ -326,6 +336,8 @@ export async function startBroker({
|
||||
else if(verb==='plugins.set')result=plugins.set(id,args,actor);
|
||||
// Row 194 (#1692, V6): the human's keep and noHire lists, through the instance writer.
|
||||
else if(verb==='veto.set')result=instances.veto(id,args,actor);
|
||||
// Row 298: the human's grant scope, through the instance writer.
|
||||
else if(verb==='grants.scope.set')result=instances.grantScope(id,args,actor);
|
||||
// Row 194 (D2.1, D2.2): the chief's hire and fire, with the reason in the change context.
|
||||
else if(verb==='instance.hire')result=instances.hire(id,args,{actor,context});
|
||||
else if(verb==='instance.fire')result=instances.fire(id,{name:args.name},{actor,context});
|
||||
@@ -357,6 +369,13 @@ export async function startBroker({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// Row 298 (#1804, Q228): one read binds launch.grantScope to the organization revision.
|
||||
// A missing organization reader or unreadable file cannot authorize an automatic grant.
|
||||
grantScope: (id) => {
|
||||
if (!settings) return { value: undefined, revision: null };
|
||||
const { document, revision } = instances.writer.read(id);
|
||||
return { value: document.launch?.grantScope, revision };
|
||||
},
|
||||
// Row 194 (#1692, D4.1, B8): bus.tester as the file sets it now; an unreadable file is off.
|
||||
testerOn: (id) => {
|
||||
try {
|
||||
@@ -408,6 +427,7 @@ export async function startBroker({
|
||||
tester: fileURLToPath(new URL('./tester-cli.mjs', import.meta.url)),
|
||||
user: fileURLToPath(new URL('./user-cli.mjs', import.meta.url)),
|
||||
console: fileURLToPath(new URL('../../webui/src/cli.mjs', import.meta.url)),
|
||||
consoleCli: fileURLToPath(new URL('./console-cli.mjs', import.meta.url)),
|
||||
};
|
||||
const path = join(store.directory, 'broker.sock');
|
||||
// The existing host executor owns SSH and its key lifecycle. Combine its revocation
|
||||
@@ -439,6 +459,11 @@ export async function startBroker({
|
||||
const p = verifyPrincipal({ user: proof }, { peer, paths: principalPaths, businesses, launches: launchRecords });
|
||||
return broker.bindUser({ business: p.business, user: p.id });
|
||||
},
|
||||
// Row 252 (#1753): a fresh proof per connection, a cap for the Console verbs only.
|
||||
authenticateConsole: (proof, peer) => {
|
||||
const p = verifyPrincipal({ console: proof }, { peer, paths: principalPaths, businesses, launches: launchRecords, consoleOwners: owners });
|
||||
return broker.bindConsole({ business: p.business, human: p.id, subject: p.subject });
|
||||
},
|
||||
});
|
||||
// Row 235 (r6 X3): the remote undo of interrupted host grants, once the bus is up, so a start doesn't
|
||||
// wait on the network.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createServer } from 'node:net';
|
||||
import { lstatSync, chmodSync, unlinkSync } from 'node:fs';
|
||||
import { BusError, TASK_VERBS, WEB_VERBS, HOST_VERBS, HOSTS_VERBS, GRANTS_VERBS, PROJECT_FILE_VERBS, LIBRARY_VERBS, PROJECT_SAVE_VERB, PROJECT_SAVE_REQUEST_MAX } from './broker.mjs';
|
||||
import { BusError, TASK_VERBS, WEB_VERBS, HOST_VERBS, HOSTS_VERBS, GRANTS_VERBS, PROJECT_FILE_VERBS, LIBRARY_VERBS, PROJECT_SAVE_VERB, PROJECT_SAVE_REQUEST_MAX, CONSOLE_VERBS } from './broker.mjs';
|
||||
import { inspectInterpreter } from './interpreter.mjs';
|
||||
import { createPeerReader } from './peercred.mjs';
|
||||
const LIMIT = 65536;
|
||||
@@ -13,8 +13,9 @@ const SAVE_VERB_AT_START = new RegExp(`"verb":"${PROJECT_SAVE_VERB.replaceAll('.
|
||||
// the web verbs reach it, and without one they refuse web-not-configured (or web-not-granted).
|
||||
// `host` (row 210) is the trusted host.run executor (host-run.mjs), or null: host.run then refuses
|
||||
// host-not-configured.
|
||||
// A request carries exactly one of cap, human (row S4) and tester (row 110) proof.
|
||||
export async function serve({ broker, path, authenticateHuman = null, authenticateTester = null, authenticateUser = null, peerReader = null, timeout = 5000, tasks = null, web = null, host = null }) {
|
||||
// A request carries exactly one of cap, human (row S4), tester (row 110), user (FED R3) and console
|
||||
// (row 252) proof.
|
||||
export async function serve({ broker, path, authenticateHuman = null, authenticateTester = null, authenticateUser = null, authenticateConsole = null, peerReader = null, timeout = 5000, tasks = null, web = null, host = null }) {
|
||||
try {
|
||||
lstatSync(path);
|
||||
throw new BusError('socket-exists');
|
||||
@@ -64,15 +65,17 @@ export async function serve({ broker, path, authenticateHuman = null, authentica
|
||||
!r ||
|
||||
Array.isArray(r) ||
|
||||
typeof r !== 'object' ||
|
||||
Object.keys(r).some((k) => !['cap', 'human', 'tester', 'user', 'verb', 'args'].includes(k)) ||
|
||||
Object.keys(r).some((k) => !['cap', 'human', 'tester', 'user', 'console', 'verb', 'args'].includes(k)) ||
|
||||
typeof r.verb !== 'string' ||
|
||||
[r.cap, r.human, r.tester, r.user].filter(Boolean).length !== 1
|
||||
[r.cap, r.human, r.tester, r.user, r.console].filter(Boolean).length !== 1
|
||||
)
|
||||
throw new BusError('invalid-envelope');
|
||||
let cap = r.cap;
|
||||
const kind = r.human ? 'human' : r.tester ? 'tester' : r.user ? 'user' : null;
|
||||
const kind = r.human ? 'human' : r.tester ? 'tester' : r.user ? 'user' : r.console ? 'console' : null;
|
||||
// Row 252 (#1753): a Console owner proof reaches its two verbs only, refused before any proof check.
|
||||
if (kind === 'console' && !CONSOLE_VERBS.includes(r.verb)) throw new BusError('human-required');
|
||||
if (kind) {
|
||||
const authenticate = kind === 'human' ? authenticateHuman : kind === 'tester' ? authenticateTester : authenticateUser;
|
||||
const authenticate = kind === 'human' ? authenticateHuman : kind === 'tester' ? authenticateTester : kind === 'user' ? authenticateUser : authenticateConsole;
|
||||
if (!authenticate) throw new BusError(kind === 'tester' ? 'tester-required' : 'human-required');
|
||||
socket.pause();
|
||||
let peer;
|
||||
|
||||
@@ -92,7 +92,7 @@ function setup(t, { runs = true, grants = [grant(), grant({ user: 'bob' })] } =
|
||||
|
||||
const withoutAlice = (gs) => gs.filter((g) => g.user !== 'alice');
|
||||
|
||||
test('R4: the timer sweep after a hand edit ends only the lost user\'s run; its next call refuses not-holder', (t) => {
|
||||
test('R4: the timer sweep after a hand edit ends only the lost user\'s run; its next call refuses thread-revoked', (t) => {
|
||||
const { b, code, edit, claim, A } = setup(t);
|
||||
edit(withoutAlice);
|
||||
const ended = b.accessSweep();
|
||||
@@ -100,7 +100,7 @@ test('R4: the timer sweep after a hand edit ends only the lost user\'s run; its
|
||||
assert.deepEqual({ ...ended[0], session: undefined }, { business: 'demo', thread: A, session: undefined, run: 'a-run' });
|
||||
assert.equal(typeof ended[0].session, 'string');
|
||||
assert.deepEqual(claim('a-run'), { op: 'revoke', reason: 'access-revoked', by: 'access-timer' });
|
||||
assert.equal(code('a', 'message.receive'), 'not-holder', 'the ended run');
|
||||
assert.equal(code('a', 'message.receive'), 'thread-revoked', 'the ended run');
|
||||
assert.equal(code('b', 'message.receive'), null, 'another user\'s run stays');
|
||||
assert.equal(code('h', 'message.receive'), null, 'the human\'s run stays');
|
||||
assert.deepEqual([claim('b-run').op, claim('h-run').op], ['claim', 'claim']);
|
||||
@@ -117,7 +117,7 @@ test('R4: an expiry with no write and no request ends the run at the next timer
|
||||
assert.equal(code('bob', 'thread.list'), null);
|
||||
assert.equal(claim('a-run').op, 'claim');
|
||||
assert.deepEqual(b.accessSweep().map((h) => h.run), ['a-run']);
|
||||
assert.equal(code('a', 'message.receive'), 'not-holder');
|
||||
assert.equal(code('a', 'message.receive'), 'thread-revoked');
|
||||
assert.equal(code('b', 'message.receive'), null);
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ test('R4: another user\'s request after a hand edit ends the lost user\'s run, a
|
||||
assert.equal(code('a', 'message.receive'), null, 'an agent request is not a trigger');
|
||||
assert.equal(code('bob', 'thread.list'), null);
|
||||
assert.deepEqual(claim('a-run'), { op: 'revoke', reason: 'access-revoked', by: 'access-change' });
|
||||
assert.equal(code('a', 'message.receive'), 'not-holder');
|
||||
assert.equal(code('a', 'message.receive'), 'thread-revoked');
|
||||
assert.equal(code('b', 'message.receive'), null);
|
||||
assert.equal(code('h', 'message.receive'), null);
|
||||
});
|
||||
@@ -168,7 +168,7 @@ test('R4: access.reload ends the lost runs at once for the human; the tester, a
|
||||
assert.deepEqual(ended.map((h) => [h.thread, h.run]), [[A, 'a-run']]);
|
||||
assert.equal(typeof ended[0].session, 'string');
|
||||
assert.deepEqual(claim('a-run'), { op: 'revoke', reason: 'access-revoked', by: 'jason' });
|
||||
assert.equal(code('a', 'message.receive'), 'not-holder');
|
||||
assert.equal(code('a', 'message.receive'), 'thread-revoked');
|
||||
assert.equal(code('b', 'message.receive'), null);
|
||||
assert.deepEqual(b.accessSweep(), []);
|
||||
});
|
||||
@@ -198,7 +198,7 @@ test('R4: threadsState after an expiry with no write and no sweep ends the claim
|
||||
const a = row(A);
|
||||
assert.deepEqual([a.revoked, a.holder], [true, null]);
|
||||
assert.deepEqual(claim('a-run'), { op: 'revoke', reason: 'access-revoked', by: 'access-expiry' });
|
||||
assert.equal(code('a', 'message.receive'), 'not-holder');
|
||||
assert.equal(code('a', 'message.receive'), 'thread-revoked');
|
||||
assert.deepEqual([row(B).holder, claim('b-run').op], ['b-run', 'claim']);
|
||||
// Ended once: the next call writes no second revoke.
|
||||
row(A);
|
||||
@@ -259,7 +259,7 @@ test('R4 runtime: a hand edit ends the run at the first timer period, intervalMs
|
||||
t.mock.timers.tick(ACCESS_SWEEP_MS - 1);
|
||||
assert.equal(receive(), null, 'before the first period');
|
||||
t.mock.timers.tick(1);
|
||||
assert.equal(receive(), 'not-holder', 'at the first period');
|
||||
assert.equal(receive(), 'thread-revoked', 'at the first period');
|
||||
// After close the timer is gone: the sweep is never called again.
|
||||
let sweeps = 0;
|
||||
b.accessSweep = () => void sweeps++;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
// bus.mjs for six library-* STATUS entries; no proof or transport change.
|
||||
// Row 236 (#1742) re-pins human-cli.mjs for the same 130 s wait on the grants verbs; no proof or
|
||||
// transport change.
|
||||
// Row 252 (#1753, Webby N5) re-pins human.mjs, whose readProcess also keeps the five
|
||||
// MOSAIC_BUS_CONSOLE_* keys for the Console wrapper's proof, and webui bus.mjs for consoleCall,
|
||||
// which shares humanCall's spawn as transport(). The human proof and its spawn are unchanged.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -15,9 +18,10 @@ import { provedRuntime } from "../../cli/tests/channel-fixture.mjs";
|
||||
import { Client } from "../src/client.mjs";
|
||||
|
||||
const baselines = {
|
||||
"packages/bus/src/human.mjs": "0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079",
|
||||
"packages/bus/src/human.mjs": "bb13c146717cdc46d75d157177e400edac4d3e10f14fe511027a25fd44cec885",
|
||||
"packages/bus/src/human-cli.mjs": "271b9781d73cf31cc562a56180e3ebcd725b5fde148a83347a201fdef2fc28af",
|
||||
"packages/cli/src/transport.mjs": "5129c44b4d9f642e825344df65c9bfb4bac4fc8d6b25343b42d39853dbd40774",
|
||||
// Row 239 (#1745): EXIT outcome-unknown-revoked 1; no proof or transport change.
|
||||
"packages/cli/src/transport.mjs": "662642a8ce739fcb6c188ad07125a94e008c8a305eced127c8041d472c56e55b",
|
||||
// Row 165 (#1662): seven STATUS entries for the role instance routes (organization-instance-exists,
|
||||
// organization-unknown-instance, organization-credentials-out-of-scope, organization-launch-not-configured,
|
||||
// organization-no-change, organization-project-mismatch, organization-unresolvable-instance), no proof or
|
||||
@@ -25,7 +29,10 @@ const baselines = {
|
||||
// Row 171 (#1668): project-name-is-repo maps to 409; no proof or transport change.
|
||||
// Row 194 (#1692, brief item 7, sage-1610 a): organization-tracker-required 400 and
|
||||
// organization-tracker-bot-taken 409; no proof or transport change.
|
||||
"packages/webui/src/bus.mjs": "15e8123ffdbb6d067e8a516f5823efdb5de3c869963764f27cd14bb9e4ce3313",
|
||||
// Row 301 (#1808, mos-1449): the user transport moved from user-bus.mjs into bus.mjs; humanCall unchanged.
|
||||
// Row 239 (#1745): outcome-unknown-revoked joins OPEN (duckworth-0165 C1), and STATUS gets
|
||||
// revision-conflict-revoked 409 and outcome-unknown-revoked 503; no proof or transport change.
|
||||
"packages/webui/src/bus.mjs": "e3f52172054676b7982eda420ce051854d404c3f0f81834c3f81138bd91c2a89",
|
||||
};
|
||||
const repoFile = path => new URL("../../../" + path, import.meta.url);
|
||||
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
// Row 252 (#1753, Gander's design approved by Webby at 5d8f0f4b5, N1-N5): the broker side of the
|
||||
// Console's Approve and Deny for a grant request. A console cap, made by bindConsole after the
|
||||
// transport checked the wrapper and the signed-in owner, reaches only decision.preview.console and
|
||||
// decision.resolve.console, and no other cap reaches those. The answer records via 'console' and a
|
||||
// human.input that names the owner, written before the grant check (N2), and only that record lets
|
||||
// an approve start the executor (J13, J14). Q188: no agent caller answers. The grant uses row 234's
|
||||
// host-enroll raise with a fake keyscan and fake enroll and check steps, so nothing connects anywhere.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { Broker, CONSOLE_VERBS } from '../src/broker.mjs';
|
||||
import { Client } from '../src/client.mjs';
|
||||
import { serve } from '../src/server.mjs';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { hostGrant, fingerprint } from '../src/host-grant.mjs';
|
||||
import { grantDecision } from './host-fixtures.mjs';
|
||||
|
||||
const options = [{ key: 'yes', text: 'Yes, enroll it' }, { key: 'no', text: 'No' }];
|
||||
const SUBJECT = { provider: 'fake-oidc', iss: 'https://idp.test', sub: 'owner-1' };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const keyLine = (dir, name) => {
|
||||
execFileSync('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', 'x', '-f', join(dir, name)]);
|
||||
return readFileSync(join(dir, name + '.pub'), 'utf8').trim().split(' ').slice(0, 2).join(' ');
|
||||
};
|
||||
const refusesSync = (fn, code) => assert.throws(fn, (e) => (assert.equal(e.code, code), true));
|
||||
|
||||
function setup(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'c252-'));
|
||||
const store = new Store(root);
|
||||
t.after(() => {
|
||||
store.close();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const hostKeys = [keyLine(root, 'hk1'), keyLine(root, 'hk2')];
|
||||
const configDir = join(root, 'config');
|
||||
const real = hostGrant({ configDir, runRoot: join(configDir, 'secrets', 'run'), scan: async () => hostKeys });
|
||||
const enrolls = [];
|
||||
const hg = {
|
||||
...real,
|
||||
enroll: async (...args) => (enrolls.push(args), real.enroll(...args)),
|
||||
check: async (...args) => ({ installerFingerprint: args[2].installer_fingerprint, exit: 0 }),
|
||||
};
|
||||
const business = {
|
||||
id: 'demo',
|
||||
human: 'jason',
|
||||
arbiters: { technical: 'pm', delivery: 'pm' },
|
||||
roles: {
|
||||
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
},
|
||||
vars: { 'bus.tester': true },
|
||||
};
|
||||
const make = () => new Broker({ store, businesses: { demo: business }, hostGrant: hg });
|
||||
const broker = make();
|
||||
const caps = {};
|
||||
for (const role of ['chief', 'pm']) {
|
||||
caps[role] = broker.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi', address: role });
|
||||
broker.request(caps[role], { verb: 'role.claim' });
|
||||
}
|
||||
caps.human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
caps.tester = broker.bindTester({ business: 'demo' });
|
||||
caps.reader = broker.bindReader({ business: 'demo' });
|
||||
caps.console = broker.bindConsole({ business: 'demo', human: 'jason', subject: SUBJECT });
|
||||
const grant = { method: 'host-enroll', host: 'web1', address: '192.0.2.10', port: 22, user: 'jarvis', sudo: true, fingerprints: hostKeys.map(fingerprint) };
|
||||
const call = (who, verb, args = {}) => broker.request(caps[who], { verb, args });
|
||||
const raise = (extra = {}) =>
|
||||
call('chief', 'decision.raise', { action: 'credential.mint', question: 'Enroll web1?', options, recommendation: 'yes', blocking: false, grant, ...extra });
|
||||
const events = () =>
|
||||
store
|
||||
.all("SELECT seq, kind, actor_role, actor_run, subject, body FROM events WHERE business='demo' ORDER BY seq")
|
||||
// The row's subject is `about`, since a console answer's body has its own `subject`. The row's kind
|
||||
// is `event`, set after the body, since an answer's body has its own `kind`.
|
||||
.map((e) => ({ seq: e.seq, kind: e.kind, actor_role: e.actor_role, actor_run: e.actor_run, about: e.subject, ...JSON.parse(e.body), event: e.kind }));
|
||||
const grantEvents = (decision) => events().filter((e) => e.actor_run === 'grant' && (e.decision === decision || e.grantDecision === decision));
|
||||
const settle = async (decision) => {
|
||||
for (let i = 0; i < 250; i++) {
|
||||
if (grantEvents(decision).some((e) => ['grant.host.enrolled', 'grant.failed'].includes(e.action))) return;
|
||||
await sleep(20);
|
||||
}
|
||||
throw new Error('enrollment did not end');
|
||||
};
|
||||
const closes = (decision) => store.all('SELECT seq, op, by, choice, via FROM decision_events WHERE decision=? ORDER BY seq', decision);
|
||||
return { store, broker, make, caps, call, raise, events, grantEvents, settle, closes, enrolls, grant };
|
||||
}
|
||||
|
||||
test('N4: the console verbs are exactly the preview and the console resolve, and decision.resolve is not one', () => {
|
||||
assert.deepEqual([...CONSOLE_VERBS], ['decision.preview.console', 'decision.resolve.console']);
|
||||
assert.equal(CONSOLE_VERBS.includes('decision.resolve'), false);
|
||||
assert.ok(Object.isFrozen(CONSOLE_VERBS));
|
||||
});
|
||||
|
||||
test('bindConsole takes only the business human and a three-string owner subject', (t) => {
|
||||
const f = setup(t);
|
||||
const bind = (record) => () => f.broker.bindConsole(record);
|
||||
refusesSync(bind({ business: 'demo', human: 'someone', subject: SUBJECT }), 'human-required');
|
||||
refusesSync(bind({ business: 'demo', human: 'jason', subject: { ...SUBJECT, name: 'Jason' } }), 'human-required');
|
||||
refusesSync(bind({ business: 'demo', human: 'jason', subject: { provider: 'p', iss: 'i' } }), 'human-required');
|
||||
refusesSync(bind({ business: 'demo', human: 'jason', subject: { ...SUBJECT, sub: '' } }), 'human-required');
|
||||
refusesSync(bind({ business: 'demo', human: 'jason', subject: { ...SUBJECT, sub: 'x'.repeat(513) } }), 'human-required');
|
||||
refusesSync(bind({ business: 'demo', human: 'jason', subject: { ...SUBJECT, sub: 7 } }), 'human-required');
|
||||
refusesSync(bind({ business: 'demo', human: 'jason', subject: 'owner-1' }), 'human-required');
|
||||
assert.throws(bind({ business: 'demo', human: 'jason', subject: SUBJECT, via: 'cli' }));
|
||||
assert.throws(bind({ business: 'demo', human: 'jason' }));
|
||||
assert.throws(bind({ business: 'other', human: 'jason', subject: SUBJECT }));
|
||||
assert.deepEqual(f.broker.identity(f.caps.console), { business: 'demo', role: null, run: null, human: 'jason' });
|
||||
});
|
||||
|
||||
test('J1, Q188: only a console cap reaches the console verbs, and a console cap reaches nothing else', async (t) => {
|
||||
const f = setup(t);
|
||||
const d = await f.raise();
|
||||
const preview = f.call('console', 'decision.preview.console', { id: d.id });
|
||||
const args = { id: d.id, choice: 'yes', digest: preview.digest };
|
||||
// Agents, the cli human, the tester and the reader are refused before the verb runs.
|
||||
for (const who of ['chief', 'pm', 'human', 'tester', 'reader']) {
|
||||
refusesSync(() => f.call(who, 'decision.preview.console', { id: d.id }), 'human-required');
|
||||
refusesSync(() => f.call(who, 'decision.resolve.console', args), 'human-required');
|
||||
}
|
||||
// N4: the console cap answers nothing through decision.resolve, and reaches no other verb.
|
||||
for (const [verb, a] of [
|
||||
['decision.resolve', { id: d.id, choice: 'yes' }],
|
||||
['decision.seen', { id: d.id }],
|
||||
['decision.withdraw', { id: d.id }],
|
||||
['decision.raise', { action: 'credential.mint', question: 'q', options, recommendation: 'yes', blocking: false, grant: f.grant }],
|
||||
['inbox', {}],
|
||||
['message.send', { to: 'chief', body: 'hi' }],
|
||||
['role.claim', {}],
|
||||
])
|
||||
refusesSync(() => f.call('console', verb, a), 'human-required');
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
assert.equal(f.enrolls.length, 0);
|
||||
});
|
||||
|
||||
test('preview: the two real answers, a grant summary and the digest, for an open grant request only', async (t) => {
|
||||
const f = setup(t);
|
||||
const d = await f.raise();
|
||||
const p = f.call('console', 'decision.preview.console', { id: d.id });
|
||||
assert.equal(p.id, d.id);
|
||||
assert.equal(p.business, 'demo');
|
||||
assert.match(p.question, /^Enroll web1\?/);
|
||||
assert.deepEqual(p.approve, { key: 'yes', text: 'Yes, enroll it' });
|
||||
assert.deepEqual(p.deny, { key: 'no', text: 'No' });
|
||||
assert.deepEqual(p.grant, { method: 'host-enroll', host: 'web1' });
|
||||
assert.match(p.digest, /^[a-f0-9]{64}$/);
|
||||
// Read twice, the digest is the same; a preview writes nothing.
|
||||
assert.equal(f.call('console', 'decision.preview.console', { id: d.id }).digest, p.digest);
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
refusesSync(() => f.call('console', 'decision.preview.console', { id: d.id, choice: 'yes' }), 'invalid-request');
|
||||
refusesSync(() => f.call('console', 'decision.preview.console', { id: 'no-such-decision' }), 'decision-not-found');
|
||||
// The inbox row says the same, for the human and the reader. The agent-facing views do not.
|
||||
for (const who of ['human', 'reader']) {
|
||||
const row = f.call(who, 'inbox').find((r) => r.id === d.id);
|
||||
assert.deepEqual(row.console, { approve: p.approve, deny: p.deny, grant: p.grant, digest: p.digest });
|
||||
}
|
||||
});
|
||||
|
||||
test('J1, J2: a decision that is not an eligible grant request refuses both console verbs', async (t) => {
|
||||
const f = setup(t);
|
||||
const grant = { method: 'host', agent: 'pm', host: 'web1', commands: ['uptime'], rotateBy: '2099-01-01' };
|
||||
const ineligible = [
|
||||
grantDecision(f.store, { action: 'deploy', grant, close: null }),
|
||||
grantDecision(f.store, { route: 'pm', grant, close: null }),
|
||||
grantDecision(f.store, { grant: undefined, close: null }),
|
||||
];
|
||||
for (const id of ineligible) {
|
||||
refusesSync(() => f.call('console', 'decision.preview.console', { id }), 'decision-not-eligible');
|
||||
refusesSync(() => f.call('console', 'decision.resolve.console', { id, choice: 'yes', digest: '0'.repeat(64) }), 'decision-not-eligible');
|
||||
assert.equal(f.call('human', 'inbox').find((r) => r.id === id)?.console, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
test('N2, J13: a Console approve records via console and the owner, before the executor starts, and the grant runs once', async (t) => {
|
||||
const f = setup(t);
|
||||
const d = await f.raise();
|
||||
const { digest } = f.call('console', 'decision.preview.console', { id: d.id });
|
||||
const r = f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest });
|
||||
assert.deepEqual(r, { id: d.id, op: 'resolved', choice: 'yes', grant: 'started' });
|
||||
const [close] = f.closes(d.id);
|
||||
assert.deepEqual({ ...close, seq: undefined }, { seq: undefined, op: 'resolved', by: 'jason', choice: 'yes', via: 'console' });
|
||||
const input = f.events().filter((e) => e.event === 'human.input' && e.about === d.id);
|
||||
assert.equal(input.length, 1);
|
||||
assert.deepEqual(
|
||||
{ ...input[0], seq: undefined },
|
||||
{ seq: undefined, kind: 'answer', actor_role: null, actor_run: null, about: d.id, decision: d.id, op: 'resolved', choice: 'yes', via: 'console', subject: SUBJECT, decisionSeq: close.seq, event: 'human.input' },
|
||||
);
|
||||
await f.settle(d.id);
|
||||
assert.equal(f.enrolls.length, 1);
|
||||
const spend = f.grantEvents(d.id).find((e) => e.operation === 'grant.host-enroll');
|
||||
assert.ok(spend && input[0].seq < spend.seq, 'the human.input precedes the executor spend');
|
||||
assert.ok(f.grantEvents(d.id).some((e) => e.action === 'grant.host.enrolled'));
|
||||
// N3: the close notice to the raiser comes from the human through the Console.
|
||||
const notices = f.store.all("SELECT from_run, body FROM messages WHERE business='demo' AND decision=? AND class='DECISION' AND from_role='human'", d.id);
|
||||
assert.equal(notices.length, 1);
|
||||
const [notice] = notices;
|
||||
assert.equal(notice.from_run, 'human-console');
|
||||
assert.match(notice.body, /resolved by jason: yes \(Yes, enroll it\)/);
|
||||
// A second answer of either kind finds it closed, and nothing starts again.
|
||||
refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest }), 'decision-closed');
|
||||
refusesSync(() => f.call('console', 'decision.preview.console', { id: d.id }), 'decision-closed');
|
||||
refusesSync(() => f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' }), 'decision-closed');
|
||||
await sleep(50);
|
||||
assert.equal(f.enrolls.length, 1);
|
||||
assert.equal(f.closes(d.id).length, 1);
|
||||
});
|
||||
|
||||
test('J15: a Console deny closes with the other key and starts nothing', async (t) => {
|
||||
const f = setup(t);
|
||||
const d = await f.raise();
|
||||
const { digest } = f.call('console', 'decision.preview.console', { id: d.id });
|
||||
const r = f.call('console', 'decision.resolve.console', { id: d.id, choice: 'no', digest });
|
||||
assert.deepEqual(r, { id: d.id, op: 'resolved', choice: 'no' });
|
||||
assert.deepEqual(f.closes(d.id).map((c) => [c.op, c.choice, c.via]), [['resolved', 'no', 'console']]);
|
||||
await sleep(100);
|
||||
assert.equal(f.enrolls.length, 0);
|
||||
assert.deepEqual(f.grantEvents(d.id), []);
|
||||
assert.deepEqual(f.make().reconcileGrants(), []);
|
||||
});
|
||||
|
||||
test('J2: approve and deny come from approvalChoice and the options, not a fixed yes and no', async (t) => {
|
||||
const f = setup(t);
|
||||
// Deny listed first, so a build that takes options[0] as approve fails too.
|
||||
const keyed = { options: [{ key: 'stop', text: 'Stop' }, { key: 'go', text: 'Go ahead' }], approvalChoice: 'go', recommendation: 'go' };
|
||||
const denied = await f.raise({ ...keyed, question: 'Enroll web1? (deny)' });
|
||||
const p = f.call('console', 'decision.preview.console', { id: denied.id });
|
||||
assert.deepEqual([p.approve, p.deny], [{ key: 'go', text: 'Go ahead' }, { key: 'stop', text: 'Stop' }]);
|
||||
for (const choice of ['yes', 'no']) refusesSync(() => f.call('console', 'decision.resolve.console', { id: denied.id, choice, digest: p.digest }), 'decision-stale');
|
||||
assert.deepEqual(f.call('console', 'decision.resolve.console', { id: denied.id, choice: 'stop', digest: p.digest }), { id: denied.id, op: 'resolved', choice: 'stop' });
|
||||
await sleep(100);
|
||||
assert.equal(f.enrolls.length, 0);
|
||||
const approved = await f.raise({ ...keyed, question: 'Enroll web1? (approve)' });
|
||||
const { digest } = f.call('console', 'decision.preview.console', { id: approved.id });
|
||||
assert.deepEqual(f.call('console', 'decision.resolve.console', { id: approved.id, choice: 'go', digest }), { id: approved.id, op: 'resolved', choice: 'go', grant: 'started' });
|
||||
await f.settle(approved.id);
|
||||
assert.equal(f.enrolls.length, 1);
|
||||
});
|
||||
|
||||
test('J11: a stale digest or a key outside the two refuses decision-stale and writes nothing', async (t) => {
|
||||
const f = setup(t);
|
||||
const d = await f.raise();
|
||||
const { digest } = f.call('console', 'decision.preview.console', { id: d.id });
|
||||
refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest: 'f'.repeat(64) }), 'decision-stale');
|
||||
refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'maybe', digest }), 'decision-stale');
|
||||
refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes' }), 'invalid-request');
|
||||
refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest, note: 'x' }), 'invalid-request');
|
||||
// Another decision's digest does not answer this one.
|
||||
const other = await f.raise({ question: 'Enroll web1 again?' });
|
||||
const od = f.call('console', 'decision.preview.console', { id: other.id }).digest;
|
||||
assert.notEqual(od, digest);
|
||||
refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest: od }), 'decision-stale');
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
assert.equal(f.events().filter((e) => e.event === 'human.input').length, 0);
|
||||
// A refusal is recorded, as for every verb.
|
||||
assert.ok(f.events().some((e) => e.event === 'action.refused' && e.code === 'decision-stale'));
|
||||
// The decision closed by the cli meanwhile: the console answer finds it closed.
|
||||
f.call('human', 'decision.resolve', { id: d.id, choice: 'no' });
|
||||
refusesSync(() => f.call('console', 'decision.resolve.console', { id: d.id, choice: 'yes', digest }), 'decision-closed');
|
||||
});
|
||||
|
||||
test('J14: a via console close with no matching human.input approves nothing', (t) => {
|
||||
const f = setup(t);
|
||||
const grant = { method: 'host-enroll', host: 'web2', address: '192.0.2.11', port: 22, user: 'jarvis', sudo: true, fingerprints: [] };
|
||||
const cli = grantDecision(f.store, { grant });
|
||||
const bare = grantDecision(f.store, { grant: { ...grant, host: 'web3' }, close: { op: 'resolved', by: 'jason', choice: 'yes', via: 'console' } });
|
||||
const wrong = grantDecision(f.store, { grant: { ...grant, host: 'web4' }, close: { op: 'resolved', by: 'jason', choice: 'yes', via: 'console' } });
|
||||
const good = grantDecision(f.store, { grant: { ...grant, host: 'web5' }, close: { op: 'resolved', by: 'jason', choice: 'yes', via: 'console' } });
|
||||
const seqOf = (id) => f.store.get('SELECT seq FROM decision_events WHERE decision=?', id).seq;
|
||||
const input = (id, body, role = null) =>
|
||||
f.store.run(
|
||||
"INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,'human.input',?,?,?,?)",
|
||||
'e-' + id + '-' + Math.random(), '2026-10-22T00:00:00.000Z', 'demo', role, role && role + '-run', id, JSON.stringify(body),
|
||||
);
|
||||
const answer = (id) => ({ kind: 'answer', decision: id, op: 'resolved', choice: 'yes', via: 'console', subject: SUBJECT, decisionSeq: seqOf(id) });
|
||||
// `bare` has no human.input at all. `wrong` has records that each break one part: another seq, choice, empty subject, an agent actor.
|
||||
input(wrong, { ...answer(wrong), decisionSeq: seqOf(wrong) + 1000 });
|
||||
input(wrong, { ...answer(wrong), choice: 'no' });
|
||||
input(wrong, { ...answer(wrong), subject: { ...SUBJECT, sub: '' } });
|
||||
input(wrong, answer(wrong), 'chief');
|
||||
input(good, answer(good));
|
||||
// reconcileGrants ends every approved, unspent host-enroll: exactly the cli one and the good console one.
|
||||
assert.deepEqual(f.make().reconcileGrants().sort(), [cli, good].sort());
|
||||
assert.equal(f.enrolls.length, 0);
|
||||
assert.equal(f.store.get("SELECT count(*) AS n FROM events WHERE business='demo' AND actor_run='grant' AND json_extract(body,'$.decision') IN (?,?)", bare, wrong).n, 0);
|
||||
});
|
||||
|
||||
test('recordEvent keeps human.input reserved, so no agent forges a console answer', async (t) => {
|
||||
const f = setup(t);
|
||||
const d = await f.raise();
|
||||
const forged = { kind: 'human.input', subject: d.id, body: { kind: 'answer', decision: d.id, op: 'resolved', choice: 'yes', via: 'console', subject: SUBJECT, decisionSeq: 1 } };
|
||||
for (const who of ['chief', 'pm']) refusesSync(() => f.broker.recordEvent(f.caps[who], forged), 'reserved-event');
|
||||
refusesSync(() => f.broker.recordEvent(f.caps.console, forged), 'agent-required');
|
||||
assert.equal(f.events().filter((e) => e.event === 'human.input').length, 0);
|
||||
});
|
||||
|
||||
// J7: the socket transport binds a console proof's cap for its one call only. The reply and a refusal
|
||||
// both release it, so the owner cap can't outlive the request that proved it.
|
||||
test('J7: a console proof over the socket binds a cap for its one call and the reply releases it', async (t) => {
|
||||
const f = setup(t);
|
||||
const d = await f.raise();
|
||||
const dir = mkdtempSync(join(tmpdir(), 'c252s-'));
|
||||
const bound = [];
|
||||
const server = await serve({
|
||||
broker: f.broker,
|
||||
path: join(dir, 'bus.sock'),
|
||||
authenticateConsole: () => (bound.push(f.broker.bindConsole({ business: 'demo', human: 'jason', subject: SUBJECT })), bound.at(-1)),
|
||||
});
|
||||
t.after(async () => {
|
||||
await server.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
const client = new Client({ path: join(dir, 'bus.sock'), console: { fixture: true } });
|
||||
const preview = await client.call('decision.preview.console', { id: d.id });
|
||||
assert.equal(preview.id, d.id);
|
||||
await assert.rejects(client.call('decision.preview.console', { id: 'nope' }), { code: 'decision-not-found' });
|
||||
assert.equal(bound.length, 2);
|
||||
for (const cap of bound) refusesSync(() => f.broker.identity(cap), 'unauthenticated');
|
||||
});
|
||||
@@ -133,8 +133,9 @@ async function setup(t) {
|
||||
},
|
||||
};
|
||||
// The fake organization writer. A fire delists the name and a delete drops the role, each with a
|
||||
// reload, as the runtime reloads after a commit. `fireError` makes the fire's write refuse.
|
||||
const org = { log: [], fireError: null };
|
||||
// reload, as the runtime reloads after a commit. `fireError` makes the fire's write refuse, and
|
||||
// `deleteError` the delete's (row 239).
|
||||
const org = { log: [], fireError: null, deleteError: null };
|
||||
const sw = { state: { version: 1, halted: false, generation: GENERATION } };
|
||||
let broker;
|
||||
const organization = (business, op, args) => {
|
||||
@@ -146,6 +147,7 @@ async function setup(t) {
|
||||
return { revision: REV, transaction: 't-' + org.log.length };
|
||||
}
|
||||
if (op === 'instances.set') {
|
||||
if (org.deleteError) throw org.deleteError;
|
||||
delete doc.roles[args.instance];
|
||||
doc.launch.instances = doc.launch.instances.filter((n) => n !== args.instance);
|
||||
broker.reloadBusiness('demo', doc);
|
||||
@@ -952,3 +954,44 @@ test("row 281 T14 (N1, sage-2653): a host removed under an undecided decision st
|
||||
assert.ok(!f.doc.launch.instances.includes('coder'));
|
||||
assert.equal(f.grantEvents().filter((e) => e.action === 'grant.revoke.refused').length, 1);
|
||||
});
|
||||
|
||||
// Row 239 (#1745): the writer marks an error raised after its own revoke ran (revoked: true), and the
|
||||
// bus names it, so the human reads that the tokens may already be revoked. Unmarked, today's code.
|
||||
test("row 239 T4: a refusal after the writer's revoke is named -revoked, in the job's fire.failed and notice, a synchronous fire and a delete", async (t) => {
|
||||
const f = await setup(t);
|
||||
await f.approveHost('coder', 'web1');
|
||||
f.org.fireError = Object.assign(new Error('refused'), { code: 'revision-conflict', revoked: true });
|
||||
const out = f.fire('coder');
|
||||
await f.broker.grantsIdle();
|
||||
const [failed] = f.allowed('fire.failed');
|
||||
assert.deepEqual([failed.body.job, failed.body.step, failed.body.code], [out.job, 'B', 'revision-conflict-revoked']);
|
||||
const [message] = f.inbox();
|
||||
assert.ok(message.includes('(revision-conflict-revoked). Its credential tokens may already be revoked. The instance is still firing.'), message);
|
||||
// helper has no grant, so its fire writes synchronously and answers with the code.
|
||||
await rejects(() => f.fire('helper'), 'revision-conflict-revoked');
|
||||
// Any other marked error: the write may have landed.
|
||||
f.org.fireError = Object.assign(new Error('EIO: i/o error'), { code: 'EIO', revoked: true });
|
||||
await rejects(() => f.fire('helper'), 'outcome-unknown-revoked');
|
||||
f.org.fireError = Object.assign(new Error('refused'), { code: 'revision-conflict' });
|
||||
await rejects(() => f.fire('helper'), 'revision-conflict');
|
||||
f.org.fireError = null;
|
||||
f.org.deleteError = Object.assign(new Error('refused'), { code: 'revision-conflict', revoked: true });
|
||||
assert.equal(f.remove('helper').code, 'revision-conflict-revoked');
|
||||
f.org.deleteError = Object.assign(new Error('EIO: i/o error'), { code: 'EIO', revoked: true });
|
||||
assert.equal(f.remove('helper').code, 'outcome-unknown-revoked');
|
||||
f.org.deleteError = Object.assign(new Error('refused'), { code: 'revision-conflict' });
|
||||
assert.equal(f.remove('helper').code, 'revision-conflict');
|
||||
});
|
||||
|
||||
test("row 239 T4b: an unmarked step B refusal keeps its code, and the notice says nothing of the tokens", async (t) => {
|
||||
const f = await setup(t);
|
||||
await f.approveHost('coder', 'web1');
|
||||
f.org.fireError = Object.assign(new Error('refused'), { code: 'revision-conflict' });
|
||||
const out = f.fire('coder');
|
||||
await f.broker.grantsIdle();
|
||||
const [failed] = f.allowed('fire.failed');
|
||||
assert.deepEqual([failed.body.job, failed.body.code], [out.job, 'revision-conflict']);
|
||||
const [message] = f.inbox();
|
||||
assert.ok(message.includes('(revision-conflict). The instance is still firing.'), message);
|
||||
assert.ok(!message.includes('may already be revoked'), message);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// Row 298: the real runtime routes the human scope verb to the durable
|
||||
// organization writer. Everything lives under a scratch config/data root.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { startBroker } from '../src/runtime.mjs';
|
||||
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
|
||||
|
||||
// Break caught: missing runtime setter routing or losing the organization receipt.
|
||||
test('row 298 runtime: human scope approval writes the file and organization history at the supplied revision', async t => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'scope-runtime-'));
|
||||
let runtime;
|
||||
t.after(async () => { await runtime?.close(); rmSync(root, { recursive: true, force: true }); });
|
||||
const rolesDir = rolesCopy(root);
|
||||
const file = writeJson(join(root, 'businesses/demo.json'), businessDoc(root, 'demo'));
|
||||
mkdirSync(join(root, 'data'), { mode: 0o700 });
|
||||
runtime = await startBroker({ dataRoot: join(root, 'data'),
|
||||
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
|
||||
roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } },
|
||||
organization: { dir: root, rolesDir, system: systemFor(root) },
|
||||
});
|
||||
const human = runtime.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const call = (verb, args = {}) => runtime.broker.request(human, { verb, args });
|
||||
const before = call('grants.scope');
|
||||
assert.deepEqual(before.scope, {});
|
||||
const scope = { gitea: { agents: ['coder'], projects: ['stack'] } };
|
||||
const saved = call('grants.scope.set', { revision: before.revision, scope });
|
||||
assert.deepEqual(JSON.parse(readFileSync(file, 'utf8')).launch.grantScope, scope);
|
||||
assert.deepEqual(call('grants.scope').scope, scope);
|
||||
assert.notEqual(saved.revision, before.revision);
|
||||
const entries = readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
||||
assert.equal(entries.at(-1).state, 'committed');
|
||||
assert.equal(entries.at(-1).actor, 'jason');
|
||||
assert.equal(entries.at(-1).changes[0].path, 'launch.grantScope');
|
||||
const bytes = readFileSync(file, 'utf8');
|
||||
assert.throws(() => call('grants.scope.set', { revision: before.revision, scope: {} }), e => e.code === 'revision-conflict');
|
||||
assert.equal(readFileSync(file, 'utf8'), bytes);
|
||||
});
|
||||
@@ -0,0 +1,359 @@
|
||||
// Row 298 (#1804, Q228): the chief of staff's grant scope at the broker. The human's grants.scope and
|
||||
// grants.scope.set (sage-3594 Q3), the in-scope close via 'scope' with grant.scope.used (Q4), the
|
||||
// spend recheck's scope-changed (Q5), and the out-of-scope raise that still waits for the human.
|
||||
// Gitea grants run against the fake Gitea in fake-gitea-api.mjs on 127.0.0.1; host grants stop at the
|
||||
// spend, as no host key store is configured, and nothing connects to a host. Every token is a fake the
|
||||
// fake made and none is printed. The organization writer is a fake here; the real one is
|
||||
// packages/business/tests/grant-scope.test.mjs.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { Broker, GRANT_EVIDENCE, writeGrantEvent } from '../src/broker.mjs';
|
||||
import { Credentials } from '../src/credentials.mjs';
|
||||
import { giteaGrant, giteaKeys } from '../src/gitea-grant.mjs';
|
||||
import { hostGrant, grantKeys, fingerprint } from '../src/host-grant.mjs';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { checkGrantScope, grantScopeOf } from '../../business/src/vocabulary.mjs';
|
||||
import { fakeGiteaApi } from './fake-gitea-api.mjs';
|
||||
import { hostRecord, grantDecision } from './host-fixtures.mjs';
|
||||
|
||||
const USER = 'demo-coder';
|
||||
const ENTRY = { address: '192.0.2.10', port: 22, user: 'jarvis', hostKeys: ['ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHostKeyForTests'] };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const sha256 = (x) => createHash('sha256').update(x).digest('hex');
|
||||
const dayFrom = (n) => new Date(Date.now() + n * 86400000).toISOString().slice(0, 10);
|
||||
const options = [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }];
|
||||
const none = { authority: { withinRole: ['message.send'], crossRole: [] } };
|
||||
const DEFINITIONS = {
|
||||
'chief-of-staff': { roleVersion: 2, credentials: [] },
|
||||
pm: { roleVersion: 2, credentials: [] },
|
||||
coder: { roleVersion: 2, credentials: [{ service: 'gitea' }] },
|
||||
ops: { roleVersion: 2, credentials: [{ service: 'host' }] },
|
||||
};
|
||||
const GITEA = { gitea: { agents: ['coder'], projects: ['app'] } };
|
||||
const HOST = { host: { agents: ['ops'], hosts: ['web1'], commands: [['systemctl', 'status', 'nginx']] } };
|
||||
const REVISION = 'a'.repeat(64);
|
||||
async function until(fn, ms = 10000) {
|
||||
const end = Date.now() + ms;
|
||||
while (!fn()) {
|
||||
if (Date.now() > end) throw new Error('timed out');
|
||||
await sleep(20);
|
||||
}
|
||||
}
|
||||
const rejects = (p, code) => assert.rejects(Promise.resolve().then(p), (e) => (assert.equal(e.code, code), true));
|
||||
|
||||
// opts.scope: the value grantScope answers, or a function called at each read.
|
||||
async function setup(t, opts = {}) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'g298-'));
|
||||
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
const gitea = await fakeGiteaApi(t, join(dir, 'tls'));
|
||||
gitea.addUser('acme');
|
||||
gitea.addRepo('acme', 'app');
|
||||
gitea.addRepo('acme', 'lib');
|
||||
const configDir = join(dir, 'cfg');
|
||||
mkdirSync(configDir, { mode: 0o700 });
|
||||
const storeRoot = join(configDir, 'secrets', 'agents');
|
||||
mkdirSync(join(dir, 'data'), { mode: 0o700 });
|
||||
const store = new Store(join(dir, 'data'));
|
||||
const credentials = new Credentials({ references: {}, storeRoot, dataRoot: join(dir, 'data') });
|
||||
t.after(() => {
|
||||
credentials.close();
|
||||
store.close();
|
||||
});
|
||||
const business = {
|
||||
id: 'demo',
|
||||
human: 'jason',
|
||||
arbiters: { technical: 'pm', delivery: 'pm' },
|
||||
roles: {
|
||||
chief: { definition: 'chief-of-staff', ...none },
|
||||
// A second chief-of-staff instance, as an instances.set or the tester could add. It isn't launch.by.
|
||||
chief2: { definition: 'chief-of-staff', ...none },
|
||||
pm: { definition: 'pm', ...none },
|
||||
coder: { definition: 'coder', ...none, identity: { name: 'Coder', email: '[email protected]', gitea: { url: gitea.url, user: USER } }, publish: ['app', 'lib'] },
|
||||
ops: { definition: 'ops', ...none },
|
||||
},
|
||||
definitions: structuredClone(DEFINITIONS),
|
||||
projects: Object.fromEntries(['app', 'lib'].map((p) => [p, { remote: `${gitea.url}/acme/${p}` }])),
|
||||
launch: { by: 'chief', instances: ['pm', 'coder', 'ops'] },
|
||||
vars: { 'bus.tester': true },
|
||||
};
|
||||
const grantApi = giteaGrant({ configDir, ca: gitea.ca, timeoutMs: 5000 });
|
||||
grantApi.writeAdmin('demo', gitea.adminToken, { url: gitea.url, account: 'mosaic-grant', rotateBy: dayFrom(60), setAt: '2026-10-21T00:00:00.000Z' });
|
||||
// The fake organization writer: the scope in a box, checked as the real writer checks it.
|
||||
const box = { value: opts.scope };
|
||||
const orgCalls = [];
|
||||
const organization = (id, verb, args, actor) => {
|
||||
orgCalls.push([verb, actor]);
|
||||
if (verb === 'grants.scope') return { business: id, revision: REVISION, ...grantScopeOf(box.value) };
|
||||
if (verb === 'grants.scope.set') {
|
||||
const scope = checkGrantScope(args.scope);
|
||||
if (scope === null) throw Object.assign(new Error('scope is not a grant scope'), { code: 'invalid-request' });
|
||||
const old = grantScopeOf(box.value).scope;
|
||||
box.value = scope;
|
||||
return { business: id, scope, old, transaction: 'tx-298' };
|
||||
}
|
||||
return { revision: REVISION };
|
||||
};
|
||||
// Keep the real key generation/store/records; replace only remote host calls.
|
||||
const hostLines = new Set();
|
||||
const hostApi = {
|
||||
...hostGrant({ configDir, runRoot: join(configDir, 'secrets', 'run') }),
|
||||
keyscan: async () => ENTRY.hostKeys,
|
||||
install: async (_business, _host, _record, tag) => { hostLines.add(tag); },
|
||||
verify: async () => {},
|
||||
remove: async (_business, _host, _record, tag) => { hostLines.delete(tag); },
|
||||
};
|
||||
const broker = new Broker({
|
||||
store,
|
||||
businesses: { demo: business },
|
||||
secretCheck: (value) => credentials.assertClean(value),
|
||||
giteaGrant: grantApi,
|
||||
giteaKeys: giteaKeys({ credentials, storeRoot }),
|
||||
hostGrant: hostApi,
|
||||
hostKeys: opts.hostKeys ? grantKeys({ credentials, storeRoot }) : null,
|
||||
organization,
|
||||
grantScope: () => ({ value: typeof box.value === 'function' ? box.value() : box.value, revision: Object.hasOwn(opts, 'revision') ? opts.revision : REVISION }),
|
||||
});
|
||||
const caps = {};
|
||||
for (const role of ['chief', 'chief2', 'pm', 'ops']) {
|
||||
caps[role] = broker.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi', address: role });
|
||||
broker.request(caps[role], { verb: 'role.claim' });
|
||||
}
|
||||
caps.human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
caps.tester = broker.bindTester({ business: 'demo' });
|
||||
caps.reader = broker.bindReader({ business: 'demo' });
|
||||
caps.console = broker.bindConsole({ business: 'demo', human: 'jason', subject: { provider: 'p', iss: 'https://idp.example.test', sub: 's' } });
|
||||
hostRecord(store, { host: 'web1', entry: ENTRY });
|
||||
const call = (who, verb, args = {}) => broker.request(caps[who], { verb, args });
|
||||
const raiseGitea = (over = {}, who = 'chief') =>
|
||||
call(who, 'decision.raise', {
|
||||
action: 'credential.mint', question: 'Grant coder Gitea?', options, recommendation: 'yes', blocking: false,
|
||||
grant: { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30), ...over },
|
||||
});
|
||||
const raiseHost = (over = {}, who = 'chief') =>
|
||||
call(who, 'decision.raise', {
|
||||
action: 'credential.mint', question: 'Grant ops web1?', options, recommendation: 'yes', blocking: false,
|
||||
grant: { method: 'host', agent: 'ops', host: 'web1', commands: [['systemctl', 'status', 'nginx']], rotateBy: dayFrom(30), ...over },
|
||||
});
|
||||
const grantEvents = () =>
|
||||
store.all(`SELECT body FROM events WHERE business='demo' AND ${GRANT_EVIDENCE} ORDER BY seq`).map((e) => JSON.parse(e.body));
|
||||
const ended = (decision) => grantEvents().findLast((e) => e.grantDecision === decision && ['grant.applied', 'grant.failed'].includes(e.action));
|
||||
const closes = (decision) => store.all('SELECT * FROM decision_events WHERE decision=? ORDER BY seq', decision);
|
||||
const spent = (decision) => grantEvents().filter((e) => e.decision === decision && e.operation?.startsWith('grant.')).map((e) => e.operation);
|
||||
const record = () => store.get("SELECT * FROM gitea_grants WHERE business='demo' AND agent='coder'");
|
||||
return { store, broker, caps, box, orgCalls, call, raiseGitea, raiseHost, grantEvents, ended, closes, spent, record, gitea, hostLines };
|
||||
}
|
||||
|
||||
test('row 298 (Q3): only the human at the cli reads or sets the scope; agents, the tester, a reader and the Console refuse', async (t) => {
|
||||
const f = await setup(t);
|
||||
for (const verb of ['grants.scope', 'grants.scope.set']) {
|
||||
const args = verb === 'grants.scope' ? {} : { revision: REVISION, scope: GITEA };
|
||||
for (const who of ['chief', 'chief2', 'pm', 'tester']) await rejects(() => f.call(who, verb, args), 'human-required');
|
||||
await rejects(() => f.call('reader', verb, args), 'read-only');
|
||||
await rejects(() => f.call('console', verb, args), 'human-required');
|
||||
}
|
||||
assert.deepEqual(f.orgCalls, []);
|
||||
assert.equal(f.box.value, undefined);
|
||||
assert.deepEqual(f.grantEvents().filter((e) => e.action === 'grant.scope'), []);
|
||||
});
|
||||
|
||||
test('row 298 (Q1): organization.set and limits.set refuse a grantScope key, and nothing is written', async (t) => {
|
||||
const f = await setup(t);
|
||||
await rejects(() => f.call('human', 'limits.set', { revision: REVISION, grantScope: GITEA }), 'invalid-request');
|
||||
await rejects(() => f.call('chief', 'organization.set', { revision: REVISION, grantScope: GITEA }), 'invalid-request');
|
||||
assert.deepEqual(f.orgCalls, []);
|
||||
assert.equal(f.box.value, undefined);
|
||||
});
|
||||
|
||||
test('row 298 (Q3): the human verb writes the scope and a grant.scope record with the human proof and both sha256', async (t) => {
|
||||
const f = await setup(t);
|
||||
assert.deepEqual(await f.call('human', 'grants.scope'), { business: 'demo', revision: REVISION, scope: {}, fallback: false });
|
||||
const r = await f.call('human', 'grants.scope.set', { revision: REVISION, scope: GITEA });
|
||||
assert.deepEqual(r.scope, GITEA);
|
||||
assert.deepEqual(f.orgCalls, [['grants.scope', 'jason'], ['grants.scope.set', 'jason']]);
|
||||
const changes = f.grantEvents().filter((e) => e.action === 'grant.scope');
|
||||
assert.deepEqual(changes, [{
|
||||
action: 'grant.scope', by: 'jason', via: 'cli', outsideAgent: true,
|
||||
oldSha256: sha256(JSON.stringify({})), newSha256: sha256(JSON.stringify(GITEA)), transaction: 'tx-298',
|
||||
}]);
|
||||
await rejects(() => f.call('human', 'grants.scope.set', { revision: REVISION, scope: { gitea: {} } }), 'invalid-request');
|
||||
await rejects(() => f.call('human', 'grants.scope.set', { revision: REVISION }), 'invalid-request');
|
||||
await rejects(() => f.call('human', 'grants.scope.set', { revision: REVISION, scope: {}, extra: 1 }), 'invalid-request');
|
||||
assert.equal(f.grantEvents().filter((e) => e.action === 'grant.scope').length, 1);
|
||||
});
|
||||
|
||||
test('row 298 (Q4): an in-scope Gitea grant is issued with no inbox item, logged with 236 records, and revocable', async (t) => {
|
||||
const f = await setup(t, { scope: GITEA });
|
||||
const d = await f.raiseGitea();
|
||||
const [close] = f.closes(d.id);
|
||||
assert.deepEqual([close.op, close.by, close.choice, close.via], ['resolved', 'grant-scope', 'yes', 'scope']);
|
||||
assert.ok(f.grantEvents().some(e => e.action === 'grant.scope.used' && e.grantDecision === d.id), 'scope close must have protected evidence');
|
||||
const inbox = await f.call('human', 'inbox');
|
||||
assert.ok(!JSON.stringify(inbox).includes(d.id), 'the decision is not in the human inbox');
|
||||
await until(() => f.ended(d.id));
|
||||
await f.broker.giteaIdle();
|
||||
assert.equal(f.ended(d.id).action, 'grant.applied');
|
||||
const used = f.grantEvents().find((e) => e.action === 'grant.scope.used');
|
||||
assert.deepEqual(used, {
|
||||
action: 'grant.scope.used', grantDecision: d.id, decisionSeq: close.seq, method: 'gitea', agent: 'coder', scope: GITEA, revision: REVISION, scopeSha256: sha256(JSON.stringify(GITEA)),
|
||||
});
|
||||
assert.deepEqual(f.spent(d.id), ['grant.gitea']);
|
||||
assert.equal(f.record().decision, d.id);
|
||||
// The spend is row 236's: a second start finds it consumed.
|
||||
assert.equal(await f.broker.runGiteaGrant('demo', d.id), 'decision-consumed');
|
||||
const out = await f.call('human', 'grants.revoke', { service: 'gitea', agent: 'coder', reason: 'test' });
|
||||
assert.deepEqual(out.serviceLeft, []);
|
||||
assert.ok(f.grantEvents().some((e) => e.action === 'grant.revoked' && e.agent === 'coder'));
|
||||
assert.equal(f.record(), undefined);
|
||||
});
|
||||
|
||||
test('row 298: an out-of-scope Gitea grant waits for the human, whose Approve still issues it', async (t) => {
|
||||
const f = await setup(t, { scope: GITEA });
|
||||
const d = await f.raiseGitea({ target: { projects: ['app', 'lib'] } });
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
assert.ok(JSON.stringify(await f.call('human', 'inbox')).includes(d.id));
|
||||
assert.deepEqual(f.grantEvents().filter((e) => e.action === 'grant.scope.used'), []);
|
||||
await f.call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
|
||||
await until(() => f.ended(d.id));
|
||||
await f.broker.giteaIdle();
|
||||
assert.equal(f.ended(d.id).action, 'grant.applied');
|
||||
});
|
||||
|
||||
test('row 298: with no scope, a bad scope or an unreadable file, an in-scope-looking grant waits', async (t) => {
|
||||
const bad = { gitea: { agents: ['coder'], projects: ['app'], note: 'not a key' } };
|
||||
for (const scope of [undefined, bad, () => { throw new Error('unreadable'); }]) {
|
||||
const f = await setup(t, { scope });
|
||||
const d = await f.raiseGitea();
|
||||
assert.deepEqual(f.closes(d.id), [], String(scope));
|
||||
assert.deepEqual(f.grantEvents().filter((e) => e.action === 'grant.scope.used'), []);
|
||||
}
|
||||
});
|
||||
|
||||
test('row 298 (Q6): a chief-of-staff instance that is not launch.by never raises in scope', async (t) => {
|
||||
const f = await setup(t, { scope: GITEA });
|
||||
const d = await f.raiseGitea({}, 'chief2');
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
});
|
||||
|
||||
test('row 298 (Q5): a scope narrowed between the raise and the spend ends the grant scope-changed', async (t) => {
|
||||
let reads = 0;
|
||||
const f = await setup(t, { scope: () => (reads++ === 0 ? GITEA : {}) });
|
||||
const d = await f.raiseGitea();
|
||||
assert.equal(f.closes(d.id)[0].via, 'scope');
|
||||
await until(() => f.ended(d.id));
|
||||
await f.broker.giteaIdle();
|
||||
assert.deepEqual([f.ended(d.id).action, f.ended(d.id).code], ['grant.failed', 'scope-changed']);
|
||||
assert.equal(f.record(), undefined);
|
||||
assert.deepEqual(f.gitea.tokens(USER) ?? [], []);
|
||||
assert.deepEqual(f.spent(d.id), ['grant.gitea']);
|
||||
assert.equal(await f.broker.runGiteaGrant('demo', d.id), 'decision-consumed');
|
||||
});
|
||||
|
||||
test('row 298 (Q2): an in-scope host grant is spent with no Approve; ls never covers lsblk, nor a scope command extra arguments', async (t) => {
|
||||
const scope = { host: { agents: ['ops'], hosts: ['web1'], commands: [['ls'], ['systemctl', 'status', 'nginx']] } };
|
||||
const f = await setup(t, { scope });
|
||||
const d = await f.raiseHost();
|
||||
assert.equal(f.closes(d.id)[0].via, 'scope');
|
||||
await until(() => f.ended(d.id));
|
||||
// The spend is row 235's. No host key store is configured, so the recheck after it ends here.
|
||||
assert.deepEqual(f.spent(d.id), ['grant.host']);
|
||||
assert.deepEqual([f.ended(d.id).action, f.ended(d.id).code], ['grant.failed', 'host-not-configured']);
|
||||
for (const commands of [[['lsblk']], [['ls', '-l']], [['systemctl', 'restart']]]) {
|
||||
const w = await f.raiseHost({ commands });
|
||||
assert.deepEqual(f.closes(w.id), [], JSON.stringify(commands));
|
||||
}
|
||||
});
|
||||
|
||||
test('row 298 (Q5): a host scope narrowed before the spend ends scope-changed', async (t) => {
|
||||
let reads = 0;
|
||||
const f = await setup(t, { scope: () => (reads++ === 0 ? HOST : {}) });
|
||||
const d = await f.raiseHost();
|
||||
assert.equal(f.closes(d.id)[0].via, 'scope');
|
||||
await until(() => f.ended(d.id));
|
||||
assert.deepEqual([f.ended(d.id).action, f.ended(d.id).code], ['grant.failed', 'scope-changed']);
|
||||
assert.deepEqual(f.spent(d.id), ['grant.host']);
|
||||
});
|
||||
|
||||
// Break caught: accepting scope closes with no evidence or unbound evidence.
|
||||
test('row 298 Q4: missing scope evidence or a bad scope digest cannot authorize execution', async (t) => {
|
||||
const f = await setup(t, { scope: GITEA });
|
||||
const good = { scope: GITEA, scopeSha256: sha256(JSON.stringify(GITEA)), revision: REVISION };
|
||||
for (const evidence of [
|
||||
null,
|
||||
{ ...good, scopeSha256: '0'.repeat(64) },
|
||||
{ ...good, revision: null },
|
||||
{ ...good, scope: { gitea: 'all' } },
|
||||
{ ...good, agent: 'other' },
|
||||
{ ...good, method: 'host' },
|
||||
{ ...good, decisionSeq: 999999 },
|
||||
]) {
|
||||
const grant = { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30) };
|
||||
const id = grantDecision(f.store, { grant, close: { op: 'resolved', by: 'grant-scope', choice: 'yes', via: 'scope' } });
|
||||
if (evidence) writeGrantEvent(f.store, new Date().toISOString(), 'demo', {
|
||||
action: 'grant.scope.used', grantDecision: id, decisionSeq: f.closes(id)[0].seq,
|
||||
method: 'gitea', agent: 'coder', ...evidence,
|
||||
}, id);
|
||||
assert.equal(await f.broker.runGiteaGrant('demo', id), 'not-authorized');
|
||||
assert.deepEqual(f.spent(id), []);
|
||||
assert.equal(f.record(), undefined);
|
||||
}
|
||||
// Positive control: the same fixture with valid scope evidence reaches the
|
||||
// next grant check. Its intentionally absent definition binding refuses there.
|
||||
const id = grantDecision(f.store, {
|
||||
grant: { method: 'gitea', agent: 'coder', target: { projects: ['app'] }, rotateBy: dayFrom(30) },
|
||||
close: { op: 'resolved', by: 'grant-scope', choice: 'yes', via: 'scope' },
|
||||
});
|
||||
writeGrantEvent(f.store, new Date().toISOString(), 'demo', {
|
||||
action: 'grant.scope.used', grantDecision: id, decisionSeq: f.closes(id)[0].seq,
|
||||
method: 'gitea', agent: 'coder', ...good,
|
||||
}, id);
|
||||
assert.equal(await f.broker.runGiteaGrant('demo', id), 'decision-mismatch');
|
||||
assert.deepEqual(f.spent(id), []);
|
||||
assert.equal(f.record(), undefined);
|
||||
});
|
||||
|
||||
// Break caught: host grants bypassing records or losing the ordinary revoke path.
|
||||
test('row 298 T4/T8: an in-scope host grant issues, records and revokes through the existing executor', async (t) => {
|
||||
const f = await setup(t, { scope: HOST, hostKeys: true });
|
||||
const d = await f.raiseHost();
|
||||
assert.equal(f.closes(d.id)[0].via, 'scope');
|
||||
assert.ok(!JSON.stringify(await f.call('human', 'inbox')).includes(d.id));
|
||||
await until(() => f.ended(d.id));
|
||||
await f.broker.grantsIdle();
|
||||
assert.equal(f.ended(d.id).action, 'grant.applied');
|
||||
assert.equal(f.store.get("SELECT decision FROM host_grants WHERE agent='ops'").decision, d.id);
|
||||
assert.equal(f.hostLines.size, 1);
|
||||
await f.call('human', 'grants.scope.set', { revision: REVISION, scope: {} });
|
||||
// Historical approval still passes #hostAccess after scope is narrowed;
|
||||
// only the absent command executor refuses this call, before any sudo decision.
|
||||
await rejects(() => f.broker.requestHost(f.caps.ops,
|
||||
{ verb: 'host.run', args: { host: 'web1', argv: ['systemctl', 'status', 'nginx'] } }, null), 'host-not-configured');
|
||||
await f.call('human', 'hosts.revoke', { agent: 'ops', host: 'web1', reason: 'test' });
|
||||
assert.equal(f.store.get("SELECT decision FROM host_grants WHERE agent='ops'"), undefined);
|
||||
assert.equal(f.hostLines.size, 0);
|
||||
assert.ok(f.grantEvents().some(e => e.action === 'grant.revoked' && e.agent === 'ops'));
|
||||
});
|
||||
|
||||
// Break caught: a new host enrollment entering the scope shortcut.
|
||||
test('row 298 T6: host enrollment stays open for human Approve even with both methods in scope', async (t) => {
|
||||
const f = await setup(t, { scope: { ...GITEA, ...HOST } });
|
||||
const d = await f.call('chief', 'decision.raise', {
|
||||
action: 'credential.mint', question: 'Enroll host?', options, recommendation: 'yes', blocking: false,
|
||||
grant: { method: 'host-enroll', host: 'web2', address: ENTRY.address, port: ENTRY.port,
|
||||
user: ENTRY.user, sudo: true, fingerprints: ENTRY.hostKeys.map(fingerprint) },
|
||||
});
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
assert.ok(JSON.stringify(await f.call('human', 'inbox')).includes(d.id));
|
||||
assert.deepEqual(f.grantEvents().filter(e => e.action === 'grant.scope.used'), []);
|
||||
});
|
||||
|
||||
// Break caught: using a scope without a verifiable organization revision.
|
||||
test('row 298: scope without an organization revision never closes automatically', async (t) => {
|
||||
const f = await setup(t, { scope: GITEA, revision: null });
|
||||
const d = await f.raiseGitea();
|
||||
assert.deepEqual(f.closes(d.id), []);
|
||||
assert.deepEqual(f.spent(d.id), []);
|
||||
});
|
||||
@@ -46,3 +46,52 @@ test('human leaf identity is reread after walking the parents',()=>{
|
||||
c.readProcess=pid=>{const p=read(pid);if(pid===50&&n++>0)p.startTime='101';return p;};
|
||||
assert.throws(()=>verifyPrincipal({human:proof},c),/human-required/);
|
||||
});
|
||||
|
||||
// Row 252 (#1753, design section 2, J3-J5): the Console owner. The same-tree console-cli wrapper with
|
||||
// its own nonce, a direct same-tree `serve` parent, the subject from the wrapper's spawn environment,
|
||||
// and that subject one of the configured owners (N1).
|
||||
const owner={provider:'fake-oidc',iss:'https://idp.test',sub:'owner-1'};
|
||||
function consoleContext() {
|
||||
const c=context('user');c.paths={...paths,consoleCli:'/tree/console-cli.mjs'};c.consoleOwners=[{provider:'other',iss:'https://idp.test',sub:'owner-2'},owner];
|
||||
Object.assign(c.table.get(50),{argv:['node','/tree/console-cli.mjs'],env:{MOSAIC_BUS_CONSOLE_NONCE:nonce,MOSAIC_BUS_CONSOLE_BUSINESS:'demo',MOSAIC_BUS_CONSOLE_PROVIDER:owner.provider,MOSAIC_BUS_CONSOLE_ISS:owner.iss,MOSAIC_BUS_CONSOLE_SUB:owner.sub}});
|
||||
return c;
|
||||
}
|
||||
const consoleProof={...proof,...owner};
|
||||
test('console requires its own wrapper, nonce and serve parent, and a configured owner from the spawn env',()=>{
|
||||
assert.deepEqual(verifyPrincipal({console:consoleProof},consoleContext()),{kind:'console',business:'demo',id:'owner',via:'console',subject:owner});
|
||||
// J3: the subject is the spawn's, never the body's alone, and must be a configured owner.
|
||||
for(const change of [{sub:'owner-2'},{iss:'https://evil.test'},{provider:'other'}])
|
||||
assert.throws(()=>verifyPrincipal({console:{...consoleProof,...change}},consoleContext()),/human-required/);
|
||||
const unlisted=consoleContext();unlisted.consoleOwners=[];
|
||||
assert.throws(()=>verifyPrincipal({console:consoleProof},unlisted),/human-required/);
|
||||
for(const extra of [{name:'Jason'},{human:'owner'},{user:'owner'},{sub:''},{sub:'x'.repeat(513)}])
|
||||
assert.throws(()=>verifyPrincipal({console:{...consoleProof,...extra}},consoleContext()),/human-required/);
|
||||
// J4: the parent is the same-tree serve, direct, and not an agent.
|
||||
for(const change of [{argv:['bash']},{argv:['node','/other/console.mjs','serve']},{argv:['node',paths.console,'other']},{env:{CODEX_THREAD_ID:'agent'}},{uid:uid+1}]) {
|
||||
const c=consoleContext();Object.assign(c.table.get(40),change);
|
||||
assert.throws(()=>verifyPrincipal({console:consoleProof},c),/human-required/);
|
||||
}
|
||||
const launched=consoleContext();launched.launches=[{pid:40,startTime:'90'}];
|
||||
assert.throws(()=>verifyPrincipal({console:consoleProof},launched),/human-required/);
|
||||
// J5: another nonce namespace, or the wrapper path of another kind, refuses.
|
||||
for(const env of [{MOSAIC_BUS_CLI_NONCE:nonce},{MOSAIC_BUS_USER_NONCE:nonce}]) {
|
||||
const c=consoleContext();const t=c.table.get(50);t.env={...t.env,...env};delete t.env.MOSAIC_BUS_CONSOLE_NONCE;
|
||||
assert.throws(()=>verifyPrincipal({console:consoleProof},c),/human-required/);
|
||||
}
|
||||
for(const argv of [['node',paths.human],['node',paths.user],['node','/other/console-cli.mjs']]) {
|
||||
const c=consoleContext();c.table.get(50).argv=argv;
|
||||
assert.throws(()=>verifyPrincipal({console:consoleProof},c),/human-required/);
|
||||
}
|
||||
// The Console's own nonce does not answer as the cli human or the user.
|
||||
assert.throws(()=>verifyPrincipal({human:proof},consoleContext()),/human-required/);
|
||||
assert.throws(()=>verifyPrincipal({user:{...proof,user:'owner'}},consoleContext()),/human-required/);
|
||||
// Each piece is read again after the walk: a parent or a spawn subject that changed refuses.
|
||||
for(const [pid,mutate] of [[40,p=>{p.startTime='91';}],[50,p=>{p.env={...p.env,MOSAIC_BUS_CONSOLE_SUB:'owner-2'};}],[50,p=>{p.ppid=41;}]]) {
|
||||
const c=consoleContext(),read=c.readProcess;let n=0;
|
||||
// The last read of each pid is verifyConsole's own reread: 40 is read 3 times, 50 is read 4 times.
|
||||
c.readProcess=x=>{const p=read(x);if(x===pid&&n++===(pid===40?2:3))mutate(p);return p;};
|
||||
assert.throws(()=>verifyPrincipal({console:consoleProof},c),/human-required/);
|
||||
}
|
||||
for(const peer of [{pid:51,uid,gid:uid},{pid:50,uid:uid+1,gid:uid}])assert.throws(()=>verifyPrincipal({console:consoleProof},{...consoleContext(),peer}),/human-required/);
|
||||
assert.throws(()=>verifyPrincipal({console:consoleProof,human:proof},consoleContext()),/invalid-envelope/);
|
||||
});
|
||||
@@ -33,7 +33,7 @@ test('Broker keeps the sandbox client import closure: only the listed imports, n
|
||||
// Row 236 (#1742, #436b) reads giteaName and parseGiteaUrl for the Gitea grant path.
|
||||
// FED R3 r5 (row 274, #414) reads covers, a user's grant check, from it.
|
||||
// Row 219 reads THREAD_DEFINITIONS from the same vocabulary for creative thread admission.
|
||||
"import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers } from '../../business/src/vocabulary.mjs';",
|
||||
"import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers, grantScopeOf, grantInScope } from '../../business/src/vocabulary.mjs';",
|
||||
]);
|
||||
assert.doesNotMatch(source, /\bimport\s*\(/);
|
||||
assert.doesNotMatch(readFileSync(new URL('../src/landing.mjs', import.meta.url), 'utf8'), /\bimport\b/);
|
||||
|
||||
Loaded 100 of 219 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user