Compare commits

..
Author SHA1 Message Date
jason.woltje 1e629c892d queue: revs 3565-3571: row 272 round 3 moves on 72690cae7 (huey 3565-3567) and request (sage 3569-3570, comment 31174); row 307 added and briefed (#1814, Vikunja 598, mos-1455 item 3; sage 3568, 3571); queue only 2026-10-23 08:08:15 -05:00
jason.woltje 84de3fbca3 plans: row 270 pi compaction reading moves behind the harness kind (#1814, Vikunja 598, mos-1455 item 3, Gizmoduck, reviewer Duckworth); gate template runs all five suites and sets MOSAIC_REQUIRE_EDITOR_BUILD=1 (mos-1455 item 4, mos-1453) 2026-10-23 08:02:15 -05:00
jason.woltje 1e4c1e6e94 queue: revs 3539-3564: rows 300 and 301 round 1 claims and in-review moves (launchpad 3539-3544), round 1 requests (sage 3545-3548), round 2 moves (launchpad 3549-3554), round 2 requests (sage 3555-3558), Duckworth approve records 300 (3559) and 301 (3560), landing notes and done at 07315e651 and 5b846b048 (sage 3561-3564); queue only 2026-10-23 07:33:26 -05:00
jason.woltje 5b846b048a launchpad: row 301 (#1808): library-cli and thread-claims bus.mjs fixtures export throwing busBusiness and userCaller stubs (mos-1451 under mos-1272); BUILD.md repaired with run 1 and the ruling; manifest seven paths; unrun 2026-10-23 07:26:44 -05:00
jason.woltje 59f0e44f47 launchpad: row 301 (#1808): channel-proof re-pins webui bus.mjs at 39fcb27c for the user transport fold, humanCall unchanged (mos-1449 under mos-1272); unrun 2026-10-23 07:26:44 -05:00
jason.woltje bb031ec840 launchpad: row 301 (#1808): R6-N4, user-bus.mjs folded into bus.mjs beside humanCall, unchanged; channel-proof re-pin held for Mos's go; unrun 2026-10-23 07:26:44 -05:00
jason.woltje 07315e6514 launchpad: row 300 (#1807): run 1 gate results in BUILD.md (1/0, webui 375/0, M12 killed) and Duckworth's N1; packet only 2026-10-23 07:26:44 -05:00
jason.woltje fb04cbc431 launchpad: row 300 (#1807): run 1 first-run red placed (mos-1272), the test's call helper resolves headers again as granted-user.test does; manifest re-hashed 2026-10-23 07:26:44 -05:00
jason.woltje 9d929911c8 launchpad: row 300 (#1807): R6-N1, an HTTP-level test of the user-session option swap with the userRoute gate stubbed open by a load hook, and mutant M12; unrun 2026-10-23 07:26:44 -05:00
jason.woltje bcf3c1dacf queue: revs 3531-3538: row 303 landing note and done at e551cd846 (sage 3531/3532); row 304 approve record (dewey 3533), row 305 approve record (filbert 3534); row 304 landing note and done at a863015a5 (sage 3535/3536); row 305 landing note and done at d52d9f1f2 (sage 3537/3538); queue only 2026-10-23 06:54:46 -05:00
jason.woltje d52d9f1f2f row 305 (#1812): gate run 1 evidence (sage-3374): both suites green, mutants a and b killed in both, c a known survivor 2026-10-23 06:49:49 -05:00
jason.woltje 830f3d8bff row 305 (#1812): BUILD.md notes the SIGKILL leak and its cleanup (Filbert N1) 2026-10-23 06:49:49 -05:00
jason.woltje e48d602165 row 305 (#1812): BUILD.md with the trace, change, gate and the three mos-1445 mutants 2026-10-23 06:49:49 -05:00
jason.woltje cabcb81d49 row 305 (#1812): each suite's network check also requires the network present under the run's project before the remove (mos-1445) 2026-10-23 06:49:49 -05:00
jason.woltje 353d65f577 row 305 (#1812, Vikunja 591) draft: per-run COMPOSE_PROJECT_NAME, exit trap removes that one network, one absent-network check per suite; for Mos through Sage before the build (mos-1272) 2026-10-23 06:49:49 -05:00
jason.woltje 2c289f6f0d SESSIONS: filbert row 305 round 1 approve (c31127, rev 3534) 2026-10-23 06:47:32 -05:00
Rocko a863015a51 test: match Claude memory slug normalization under mos-1442 2026-10-23 06:46:02 -05:00
jason.woltje e551cd8466 launchpad: row 303 (#1810): R9 asserts the rehearsal directory is set before the placeholders check joins it, so a null shows the rehearsal output (mos-1422, Vikunja 589) 2026-10-23 06:41:07 -05:00
jason.woltje c13c337e16 queue: revs 3521-3530: rows 304 and 305 claimed (rocko 3521, gizmoduck 3522); row 305 in-review on 7e8218a2e (gizmoduck 3523), request failed without a token (gizmoduck 3524), sage's request posted c31126 (3525/3526); row 304 in-review on 5e8354ebb (rocko 3527), request failed without a token (rocko 3528), sage's request posted c31128 (3529/3530); queue only 2026-10-23 06:27:36 -05:00
jason.woltje 435a7458d3 sage: PRD 1.17 draft 8 (343cff6f), d7 plus Mos's d8 fix list mos-1443 (R1-R14, M1-M45, J-i to J-l) and the mos-1446 choices (M10 default, M15 quotes Q235 in full, M40 both sentences); M15 and M16 cite b99ca86b9. Webby APPROVE round 5 webby-0210, no B items; N15-N17 carry to d9 2026-10-23 06:21:31 -05:00
jason.woltje b99ca86b9c docs: align editor conversion boundary and host restore with PRD d8 2026-10-23 06:08:20 -05:00
jason.woltje 0e057e05eb queue: revs 3511-3520: rows 304, 305 and 306 added and briefed (sage 3511-3516); row 272 round 2 in-review on b44332a93 (huey 3517), request failed without a token (huey 3518), sage's request posted c31120 (3519/3520); queue only (sage) 2026-10-23 06:02:42 -05:00
47 changed files with 4694 additions and 109 deletions

No files matched your search

+2
View File
@@ -5387,3 +5387,5 @@ Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-s
Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault run minus a separate baseline run, and a loaded whole-suite run went red. Mos approved direct timing from host /proc/uptime stamps with ambient load only (mos-1409). After: rebased onto 80793d824 as ffa93b054 (range-diff 3/3 =, manifest 277ace0b unchanged). Gate 07:26Z-07:50Z: case1 whole 3x 53/0/0, cli whole 917/0/0 (59 files, load 1.54-7.17), P10 inner 2.27 s, outer 4.01 s, M1-M6 killed. M4's planned guard return then exact outer-reason failure counts as its kill (sage-3271, mos-1426). Filbert APPROVE c31084 rev 3485, N1 M1 margin 0.02 s. 299 LANDED ffb2fedfb 08:03Z, done rev 3487, #1806 closed c31087; queue commit 8a35ffadb (revs 3474-3487). Row 303: Duckworth APPROVE c31079 rev 3480 with C1, the whole-file gate. Rows 219 (bfa421b6c) and 295 r2 (b16cef1d0) rebases: Dewey static clean. Corrections: Launchpad and Gander have no token, and the CLI refuses another seat's token file, so I posted both requests as sage (c31082, c31083). My sage-3265 wording led Duckworth to poll queue show into Gander's slot (07:20Z-07:26Z); I ruled it ambient load (sage-3274).
2026-10-23T10:48:40Z | Sage, rows 302, 295 and 219 landings, the Docker pool diagnostic, rows for Vikunja 590 and 591 | Before: refactor 1ac655005. 302 (#1809, Gizmoduck, Dewey) LANDED 3ed9fd754 08:49Z: 7 commits onto 1ac655005, range-diff all =, tree = gated merge 783602e06; queue commit 07802dc57 (revs 3488-3494). Row 219's gate went 11/3 in test-release.sh: release activate's health check failed because Docker could not create sage-diag1437_default, since all predefined address pools were used up. Under mos-1437 I reproduced it on clean 07802dc57 (11/3, the same stderr). I then removed 18 unused gate _default networks, and the rerun went 14/0 with the run-task marker ok. Mos accepted the removal and ruled that a probe that can create something waits for the go and a removal waits for the report (mos-1438). 295 (#1801, Morgana, Dewey) round 2 gate on d68b4300d (manifest e94c9aeb) was green. It LANDED 2efd127ce 10:10Z: 9 commits onto 07802dc57, HEAD~1 tree = gated a86592ffc. Done rev 3502, #1801 c31105, P19 414 c911. 219 (#1720, Rocko, Dewey): the restart on the post-295 merge 054da642f (manifest 4a2271dd, my mos-1439 call) passed node 2523/0/2 and shells 449/0, with 24 mutants killed. It LANDED 5fdfd7275 10:40Z (8 row commits plus packet-only evidence, tree e86a84a85 = gated). Done rev 3509, #1720 c31110, P19 416 c913. Queue commit 90633c864 (revs 3495-3510). Brief docs/plans/2026-10-23_gate-env-rows.md names rows for 590 (claude-session slug) and 591 (compose network cleanup), and DEFERRED.md gets 219 N1 (U6). Row 272 (Huey) conflicts with 5fdfd7275 in code, so it moves back to in-progress for a rebase. Its new fallback is bounded to a session.list access-refused on an omitted-session route (sage-3357). Corrections: my network create probe ran before Mos's go, and the removal ran before the report. My first slot-order line put the diag start at 09:44Z; it was about 09:41Z. My 219 push piped its output through grep, but the after-fetch confirmed origin. Queue commit 90633c864 also carries Huey's rev 3510 (272 back to in-progress), and its message names only 3495-3509.
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.
+66
View File
@@ -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
+31
View File
@@ -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()
+67
View File
@@ -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
+26
View File
@@ -0,0 +1,26 @@
# Row 303 build notes (Launchpad): R9 shows its output when rehearse makes no directory (#1810)
Brief: docs/plans/2026-10-23_r9-guard-row.md (Vikunja 589). Reviewer Duckworth, gate owner Sage.
Base 80793d824 (refactor after row 294 landed at d3059c2c3). Branch launchpad/row303.
## Change
One added line in packages/cli/tests/stack-release.test.mjs, R9 only, at :2178:
assert.ok(r.dir!==null, r.out+r.err);
It sits right after R9's exit-code check and right before the `includes` check whose message builds
`join(r.dir,'placeholders')`. rehearse() (:2040) sets `dir` to null when the output has no
`rehearsal directory:` line, so the guard fails first and its message carries the rehearsal's
output and stderr, including the FAIL line that stopped it. This is a test-only change to a
pre-existing test, approved under mos-1272 and mos-1316 (mos-1422 point 2). It adds an assertion
and changes none. No other line in R9 or any other test changes. The lines the brief names (:2058,
:2113, :2126, :2267, :2280) put r.dir in a template, where a null prints as `null` and the message
still carries r.out, so they get no guard.
MANIFEST.sha256 lists the one changed path, hashed from the commit that adds this file.
## Gate
Duckworth approves the diff. stack-release.test.mjs runs whole and green on Sage's gate, in a gap.
Not run yet.
@@ -0,0 +1 @@
e936808bb12361acf164b675fe40ead416720dfd48c5d51ea5bfee1e1893cc33 packages/cli/tests/stack-release.test.mjs
+49
View File
@@ -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"
}
+1
View File
@@ -0,0 +1 @@
c16e943b90ef71ac96cfa5314bbea6040634d14d
+13
View File
@@ -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 @@
[]
+14
View File
@@ -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"}
+66
View File
@@ -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=$?
+638
View File
@@ -0,0 +1,638 @@
# 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:
- 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."
Version tag: 'and "Connections are always visible".)' becomes 'and "Connections
are always visible". Version 1.17, with Q222 for the two settings.)'
### 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, until a Console view of their links exists. Q226
records that view as a later option.
- 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 only in the Console's editor until a
Console view of them exists. (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.)"
## 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, 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".
- 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.
- 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.
- 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-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.
+747
View File
@@ -0,0 +1,747 @@
--- DRAFT-d7.md 2026-10-22 20:44:39.909606789 -0500
+++ DRAFT.md 2026-10-23 06:16:54.370223847 -0500
@@ -1,12 +1,12 @@
# 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 7. It 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 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:
- 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 (B5): Webby, then Mos, then Jason. Base: PRD 1.16, `docs/prd/mosaic-stack.md` blob a6db9b4a at 482e865ac.
+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:
@@ -19,11 +19,12 @@
These stand as ruled. d5 changes only what mos-1372 changed.
-- **O1. Where a converted chapter lives.** The DOCX lives in the O2 writing store
- (D12), and the archived Markdown stays in the user's repository under a
+- **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 adds a remote for the store.
+ 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.
@@ -40,15 +41,16 @@
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) without a narrowing for
- Jason.
+ (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, then the archive commit and its
- push, then publication to the lookups.
+ 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.
@@ -56,19 +58,33 @@
### Status and sources
-Status gains, after the 1.16 sentence: "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 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)."
+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, his Q236 '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.', and his Q237 'Q38: It is correct for now (subject to future change)',
-confirming Mos's reading P1 to P6. The storage follows the O2 design D10 and D12,
-25c09f19f, as rewritten under Vikunja 571."
+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
@@ -92,20 +108,22 @@
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 alone, 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
-(REQ-LIB-8), 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."
+"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,
@@ -126,89 +144,110 @@
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). 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 the human sets, never by force-push, and the
-user sees a push that is pending or failed. Only the stack's host pushes to that
-remote, and no agent credential reaches it. Agents read the local commit."
-
-Replace "Agents read only the mirror's served commit and never hold a copy of their
-own" with "Agents read the user's repository only at the mirror's served commit,
-read a DOCX story file only at a committed version in the writing store
-(REQ-LIB-5), and never hold a copy of their own". The rest of that sentence, ", so
-they see the user's push within about a minute.", is unchanged.
+longer in that repository, which keeps only its archived Markdown (REQ-LIB-8)."
-Version tag: "and the sync items.)" becomes "and the sync items. Version 1.17.)"
+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 a save is acknowledged returns that save
-or a later one, 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
+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 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)."
+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."
-Version tag: "and \"Connections are always visible\".)" becomes "and \"Connections
-are always visible\". Version 1.17, with Q222 for the two settings.)"
+Version tag: 'and "Connections are always visible".)' becomes 'and "Connections
+are always visible". Version 1.17, with Q222 for the two settings.)'
### 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
+"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. `requireCitation` and a citation
-marked as canon work the same way for both kinds."
+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, requests or undoes a conversion
-(REQ-LIB-8). 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."
+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). Until its import a story file stays
- Markdown. 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.
+ 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 a supporting, archived or type-folder file, and a file that is
+ - 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 fails the fidelity report.
+ 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
@@ -219,61 +258,81 @@
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, then the archive commit and its push, then
- publication to the lookups. Until publication the Markdown stays canon at
- its old path, and agents read the source at the commit the import started
- from. A refusal or failure before the push leaves the Markdown unmoved and
- no DOCX that a lookup returns. A failure after the push leaves a pending
- import that the human sees and can resume, never a claim that nothing moved.
- - Each import writes one logged event naming the source file's path, commit
- and blob and the new document id.
+ - 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. The Console flags it and offers the import. Nothing
+ 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 can also name a DOCX document by its
- id.
+ 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, so no role holds them, no agent can request them, and
- REQ-WAIT-2's list is unchanged. 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
+ 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.
+ 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 three things.
- The editor ships. 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 agents never lose the current chapter, and no
- second system writes the story. The first import of a real user's books
- waits for Jason's go.
+ - 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, on a page of its own. 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
+ 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
- its license and a hash per file, and nothing loads at runtime. 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 D10 and D14. Version 1.17.)"
+ 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
@@ -285,40 +344,52 @@
- 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, after W4b, and after Hermes is retired (#423 step 5).
- - The 1.12 note says bus writes and the Console editor (C2-02, C2-03b, C2-11a,
- C2-11b, C2-19) don't gate 10-27. Jason's Q226 F42 later moved three of them,
- C2-02, C2-03b and C2-11b, into the 10-27 slice, and that later ruling holds.
- C2-11a and C2-19 still don't gate 10-27. Where a plan or queue record names
- a different set, F42 wins, and the record is corrected through its own
- process.
- 1.17 changes neither, 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).
- - This replaces the superseded-notice proposal in O2 design D12 (Fenton F1),
- which is withdrawn, and the keep-Markdown-at-its-path model of D11. Gyro's
- rewrite of the O2 design (Vikunja 571) carries the details:
+ 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, with W4 7.4's no-remote and no-backup rule taken
- out;
- - import events counted by a stack restore, and recovery when a restore
- predates an 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;
- - the import verb kept out of `ACTIONS` (packages/business/src/vocabulary.mjs,
- the hard-wait note and the pinned `GATED_ONLY` list).
+ - `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
- O2-only conversations, per-document grants and per-send sharing (W4 7.1, 7.2)
- don't gate these lookups, and the 571 rewrite brings W4 into line. The
- creative tool list in REQ-LIB-6 doesn't change.
+ 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.
+ - 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
@@ -336,52 +407,130 @@
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 sentence: "The user commits and pushes with their own
- tools, today Obsidian with obsidian-git." and the clause "Agents read only
- the mirror's served commit and never hold a copy of their own".
+ - 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, until a WebUI view exists (Q226).
+ stood at conversion, until a Console view of their links exists. Q226
+ records that view as a later option.
- 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. 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 project docs editor, is not Case 2 and is
- unchanged.
+ 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 and agents read
- Markdown only there'. 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'.
+ 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
-New lines:
+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.)"
-- "Converted chapters leave Obsidian's graph. Their links show only in the
- editor until a graph view exists. (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.)"
- "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 stack restore from a backup taken before an import drops the import's
- logged event, while the archive commit and the DOCX stay. Restore counts
- import events, and the import's recovery covers this case (Vikunja 571).
+- "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.)"
+## 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.
@@ -392,7 +541,7 @@
- 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 doesn't gate 10-27. F42 wins where case2-rows.md:1284-1285 or the impact map's A57 says otherwise.
+- 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.
@@ -405,57 +554,61 @@
- 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: REQ-LIB-2's "unless its path is a converted file's old path" and the REQ-LIB-8 old-path bullet.
+- 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: REQ-LIB-4's remote sentences, plus the 571 list item that takes out W4 7.4's rule. 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".
- 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: B2's note.
-- D10: B5's sentence, in REQ-LIB-8.
-- D11: internal F3's clause and title, in the REQ-LIB-4 section and the notes.
+- 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: B1's row text, in the mark-and-move bullet and C3 above.
+- 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 event and the several-humans setting, default owner only (Q222).
+- 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.
- 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 B4's "a new Markdown file in a book folder is still canon" note: the file stays canon and is offered for import.
+- 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 alone". DOCX scope comes from the source, so "a file's" would be false.
-- REQ-LIB-8's exclusion-by-id sentence. A DOCX started in the editor has no old path, so without it a hybrid couldn't exclude one.
+- 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". D1 names canon and playground story files only, and an unmapped file has no scope for a DOCX to keep. If Mos wants unmarked folders in, the human would map them first.
+- 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. Converted chapters leave Obsidian's graph, and 1.17 has no other graph. REQ-LIB-5 still carries its "Connections are always visible" tag, and Q226 leaves room for "another way to handle the visual brain map". As written, that tag holds for Markdown files and for links inside the editor, and a graph of DOCX links waits for that other way.
-- 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." This belongs to the P5 go step, so no new question.
-- J-d. The agent read model in D9. A converted chapter is read through the ordinary Case 2 lookups in any thread or room, not through W4's per-document grants. Mos's reading of F41 and REQ-LIB-7. 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 (mos-1379).
+- 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-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 (REQ-LIB-9) runs at intervals only a human changes. F41 says saves are "instantly committed". The intervals are text beyond that, so they are marked for him. The exact values stay in the design.
+- 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)
@@ -464,21 +617,22 @@
- A6 (lookups across formats)
- A7 (agent bounds)
- A8 (DOCX cites)
-- Item 2 (the remote only the stack's host pushes)
+- 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 convert, type folders stay Markdown)
+- 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
-As with 1.16, the row owns three files:
+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, the two new ids with their titles (REQ-LIB-8 and REQ-LIB-9), and the changed titles of REQ-LIB-1, REQ-LIB-2 and REQ-LIB-4.
-- `packages/tasks/tests/requirements.test.mjs`: the version pin and the id count of 87.
+- `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. `requirements.test.mjs` and every `scripts/test-*.sh` that reads the PRD or `requirements.json` are green on Sage's gate.
+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.
+2
View File
@@ -1575,3 +1575,5 @@ are never rewritten or removed; corrections are new entries.
- 2026-10-23T07:59:57Z | filbert (Claude Opus 5.5, T3 048f3c63) | row 299 manifest round 1 (#1806, Vikunja 573, Gander, manifest 277ace0b on ffa93b054) | approve in 31084, rev 3485; static on git objects (blobs = reviewed 1ec2b9765, product = base 80793d824); gate receipts 63db96d8b read, not rerun: case1 53/0 x3, cli 917/0 (59 files), M1-M6 killed; N1 M1 margin 0.02 s
- 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
+15 -6
View File
@@ -265,7 +265,9 @@ After each file's conversion publication, the authorized format map is a typed h
sourceCommit,conversionCommit}`. kind is chapter or supporting-document; current chapter
format is docx, supporting format markdown; state is current or converted-archive. scope distinguishes canon, playground, supporting
and archived without format-based promotion. Type-folder entity notes remain Markdown;
an unmarked-folder story is eligible for human selection, one file at a time (mos-1368 Q18). location
a story file in an unmarked folder inside a book or playground is eligible for human selection,
one file at a time. A story file outside every book and playground is unmapped and converts
only after the human maps its folder, per PRD 1.17 REQ-LIB-8 and mos-1368 Q18. location
is an opaque owned location, never a host path/capability. Publish only entries the caller's
live scopes allow, no title/path leak from denied documents. Agents get this map through
reviewed library/universe lookup results and normal authorized Case 2 context so they know formats
@@ -321,8 +323,9 @@ amendment, W4b PASS and live committed-reader/citation readiness, W7 reviewed im
release, Hermes retirement (#423 step 5, with its old write/store access removed), verified
host-only off-host durability and an authenticated human start. `library.convert` is a
human-only CLI/Console verb run through the host async processing adapter, outside agent
vocabulary/role grants and off the read-verb list. Its action remains a hard wait under
REQ-WAIT-2/3 until the approved PRD says otherwise; it never joins ROUTED silently.
vocabulary/role grants and off the read-verb list. It is not in ACTIONS or ROUTED and never
reaches the broker. A role file naming an action outside the vocabulary is already refused.
REQ-WAIT-1 and its hard-wait test remain unchanged (mos-1444 R6).
`editor.conversionHumans` is human-only, default business owner only. Folder setting/mapping
have one authoritative archive-prefix value; the library mapping and editor cannot disagree.
Archive prefixes cannot overlap playground/supporting prefixes. Conversion preserves bytes
@@ -1155,7 +1158,7 @@ The source archive and private DOCX repository cannot publish atomically through
`packages/bus/tests/{writing-save.test.mjs,chapter-format.test.mjs,schema.test.mjs,conversion-events.test.mjs}`,
`packages/harness/src/tools.mjs`, `packages/harness/tests/chapter-format.test.mjs`,
`packages/business/src/{vocabulary.mjs,business.mjs}`,
`packages/business/tests/{vocabulary.test.mjs,business.test.mjs,hard-waits.test.mjs}`,
`packages/business/tests/{vocabulary.test.mjs,business.test.mjs}`,
`scripts/mosaic`, `packages/cli/src/library.mjs`, `packages/cli/tests/library-convert.test.mjs`,
`roles/{companion,continuity,fact-checker,ideas,proofreader,research}.md`,
`packages/webui/src/{writing-http.mjs,library-http.mjs,cli.mjs,serve.mjs,bus.mjs}`,
@@ -1214,6 +1217,11 @@ bus restore. Test the third case explicitly: convert, then rollback --restore a
backup, losing the bus conversion event while archive and DOCX commits remain. The normal
counted-event approval/loss protocol applies; reconcile immutable commits/journal with new
recovery events before lookups resume. Never revive Markdown or replay authorization grants.
The fourth recovery case restores the whole host from a backup taken before an import,
losing its writing store and host import records as well as bus events. Rebuild the host
record from the import manifests on the writing store's off-host remote and the archive
commit in Gitea. Chapter lookups refuse until recovery proves one current version.
Restoring VM 1012 boots it, so Jason is present under Q185 (mos-1444 R14).
Old release capability preflight must refuse converted projects it cannot interpret.
Update the six creative .md role contracts' obsolete 'Every item cites the lines' text:
@@ -1222,8 +1230,9 @@ source format/scope from the map, archived never canon, and no story/conversion
still use only their approved lookups/message/supporting-write tools. Schema/role expansion
waits for the approved PRD, and existing role JSON never grants library.convert. Expose the
human CLI/Console conversion verb through runtime/processing.work and async readback, not
the agent tool list. Hard-wait tests are at business/tests/hard-waits.test.mjs:22; the new
action remains GATED_ONLY unless the approved PRD explicitly changes it.
the agent tool list. library.convert is absent from ACTIONS and ROUTED; host processing
never sends it to the broker. Role vocabulary validation already refuses an unknown action.
REQ-WAIT-1 and packages/business/tests/hard-waits.test.mjs remain unchanged (mos-1444 R6).
The importer lives at `packages/writing/src/import-markdown.mjs`, owns a reviewed AST mapping
from vendored markdown-it to the WordCraft DOCX model, and validates source token/metadata,
+16 -6
View File
@@ -15,8 +15,9 @@ from the reviewed baseline except their replaced per-document-scope wording.
Q235–Q237 P1–P6 and mos-1372 D1/D2 settle the design basis: canon and playground story
files become DOCX, retaining universe/book and source scope. Type-folder notes, agent files,
instructions, PRDs, character sheets, summaries and supporting documentation stay Markdown.
An unmarked-folder story is eligible; the human selects one story file at a time. New
stories start directly as DOCX in the human-selected universe/book or playground, without a
A story file in an unmarked folder inside a book or playground is eligible, and the human
selects one story file at a time. An unmapped story file converts only after the human maps
its folder. New stories start directly as DOCX in the human-selected universe/book or playground, without a
Markdown precursor, under the same production-opening gate as import.
The October 27 release stays Markdown. Production import/new-story opening waits for W4b,
@@ -24,7 +25,9 @@ W7, the post-10-27 release, the approved Case 2 PRD/brief amendment, Hermes reti
(#423 step 5) and configured off-host durability. The first real use on Melanie's books
also waits for Jason's scoped go and mos-1374's protected manual VM1012 backup before the
first real import, handled by its authorized human operator, not this design seat. All starts are human-only; `library.convert` has no role
grant or agent request path and stays a hard wait unless an approved PRD changes it.
grant or agent request path. It is not in ACTIONS or ROUTED; the human host processing
adapter never sends it to the broker. A role file naming an action outside the vocabulary
is already refused. REQ-WAIT-1 and its hard-wait test remain unchanged (mos-1444 R6).
Nothing in this document authorizes conversion, new private story creation or deployment.
Q234's exact frame CSP, mos-1363 and S0 M1–M14 remain unchanged implementation gates.
@@ -592,9 +595,10 @@ join stack-release COUNTED and T46 exactly once, so post-backup conversion event
to --events and the existing restore approval/counting behavior. No new private conversation,
per-document grant or per-send share tables are created for D9.
Three recovery cases are required: (1) crash after durable DOCX staging but before archive
Four recovery cases are required: (1) crash after durable DOCX staging but before archive
push, (2) crash after archive push before publication, and (3) rollback --restore from a
pre-conversion bus backup while archive commits and O2 documents remain. The source is pinned
pre-conversion bus backup while archive commits and O2 documents remain, and (4) a whole-host
restore from a backup taken before an import. The source is pinned
until final publication in cases1/2; after archive push state is visibly pending, never
'original untouched'. Case3 drops conversion rows only through the counted/approved restore
path. The durable host conversion journal/format registry is not restored from that older bus
@@ -606,6 +610,12 @@ pending operation to the human. Do not replay chapter text into an old index or
Incompatible old binaries cannot activate converted projects through the supported controller;
raw privileged rollback tampering is outside the guarantee. Never rewrite git/run history.
Case4 also loses the local writing store and host import records. Recovery rebuilds the
host record from the import manifests on the writing store's off-host remote and the
archive commit in Gitea. Chapter lookups refuse until it proves one current version;
missing or inconsistent remote evidence cannot revive old Markdown as current. Restoring
VM 1012 boots it, so Jason is present under Q185 (mos-1444 R14).
Hermes retirement (#423 step5) is a production-opening prerequisite, with old story-write and
store-read access removed before any conversion/new-story operation. W7 checks its retirement
receipt and host capability preflight; no design seat retires services or accesses Melanie's
@@ -644,7 +654,7 @@ These are required future fixture gates, not implementation results.
| A24 | Full page and oversized paragraph | end before overflow; first oversized row refuses without text; no unreachable subrange claim |
| A25 | Every read/write/tool surface uses existing Case 2 isolation | same document ID across business/universe or replayed cursor refuses |
| A26 | Host-only off-host remote and per-save push | unset/local-only/helper-readable/unexpected remote refuses opening; real helper denial tested; push outage visible, local next lookup still fresh |
| A27 | Conversion restore accounting and durable journal reconciliation | pre-conversion --restore loses counted row but cannot revive Markdown; no lost-event concealment |
| A27 | Conversion restore accounting and durable journal reconciliation | pre-conversion bus --restore or whole-host restore reconciles remote manifests and archive commit; lookups refuse until one current version is proven; no lost-event concealment |
| A28 | Reopened M1 supports same-thread DOCX-read then supporting-note write | any room/project-authorized caller works; no per-document/share token; story/archive write refuses |
| A29 | Readiness is reviewed runtime/extractor plus approved PRD, not caller fields | no request/go/setting can assert release or Hermes retirement |
| A30 | Canonical schemas and complete selections | unknown keys, inconsistent ranges, xmlId null mismatch and noncanonical story IDs refuse |
@@ -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.
+9 -5
View File
@@ -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 3510 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
Generated from `docs/plans/queue.json` revision 3571 by `queue render`. Do not edit between the markers; change the queue with `scripts/mosaic queue`.
| # | Piece | Owner | Issues | State | After | Gate | Brief | Note |
|---|---|---|---|---|---|---|---|---|
@@ -308,7 +308,7 @@ Generated from `docs/plans/queue.json` revision 3510 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-progress | 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 | in-review, round 3, 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 | — |
| 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. |
@@ -336,10 +336,14 @@ Generated from `docs/plans/queue.json` revision 3510 by `queue render`. Do not e
| 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) | — |
| 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) | — |
| 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 | 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) | — |
| 307 | Row 270's pi compaction reading moves behind the harness kind (REQ-PLUG-2, Vikunja 598) | gizmoduck; reviewers duckworth | #1814 | briefed | — | 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) | — |
<!-- mosaic-queue:end -->
+2077 -20
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -25,7 +25,8 @@ 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.
"packages/webui/src/bus.mjs": "39fcb27c82e917ef05e1a7a650d522c9f3c656bf8dbade87b8ddb3661b67ad6c",
};
const repoFile = path => new URL("../../../" + path, import.meta.url);
@@ -2175,6 +2175,7 @@ test('R9 mos-0963: a role reference left outside placeholders/, or left on an en
ownScript(e,'for(const [name,r] of Object.entries(b.roles??{})){strip(r?.vars);swap(name,r?.credentials)}','for(const [name,r] of Object.entries(b.roles??{})){strip(r?.vars);if(name!=="pm")swap(name,r?.credentials)}');
const r=await rehearse(e,B,'--backup',e.act,'--port',await freePort(),'--timeout','3');
assert.equal(r.code,1,r.out+r.err);
assert.ok(r.dir!==null, r.out+r.err);
assert.ok(r.out.includes(`FAIL credential-ref-live: pm gitea is ${tok}, not a file in ${join(r.dir,'placeholders')}\n`),r.out);
assert.deepEqual(r.names.slice(-3),['FAIL credential-ref-live','PASS backup-unchanged','PASS live-config-unchanged']);
assert.equal(existsSync(rh(e,'host-args.log')),false);assert.equal(existsSync(rh(e,'console-args.log')),false);
@@ -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")],
+58
View File
@@ -10,6 +10,7 @@
import { spawn } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { HUMAN_CLI } from '../../cli/src/transport.mjs';
import { readHostState } from '../../cli/src/host.mjs';
import { views } from '../../bus/src/views.mjs';
@@ -72,6 +73,63 @@ export function busCaller({ dataRoot, socket, business = null, env = process.env
}
export const busReader = options => views({ call: busCaller(options) });
// FED R6 (#1800): a granted user's bus transport, R3's user-cli.mjs (#414, design 3.6). Same
// protocol as humanCall: the request on stdin, JSON on stdout, or a bus error code as the last
// `^[a-z-]{1,64}$` line of stderr. Row 301 (#1808) moved it here from user-bus.mjs.
export const USER_CLI = fileURLToPath(new URL('../../bus/src/user-cli.mjs', import.meta.url));
// The Console sets the user nonce itself, so user-cli.mjs doesn't re-exec and the broker sees the
// Console (`cli.mjs serve`) as its parent (principal.mjs). The human nonce is dropped, so this child
// can never pass as human-cli, and it only ever runs USER_CLI.
export function userCall({ socket, business, user, env = process.env, timeoutMs = 30000, maxBytes = 8 * 1024 * 1024 }) {
const { MOSAIC_BUS_CLI_NONCE, ...rest } = env;
return (verb, args = {}) => new Promise((resolve, reject) => {
let child;
try {
child = spawn(process.execPath, [USER_CLI, socket], { env: { ...rest, MOSAIC_BUS_USER_NONCE: randomBytes(32).toString('hex') }, stdio: ['pipe', 'pipe', 'pipe'] });
} catch { return reject(new BusReadError('outcome-unknown', 'the bus transport did not start')); }
const out = [], err = [];
let size = 0, settled = false;
const done = (fn, value) => { if (!settled) { settled = true; clearTimeout(timer); fn(value); } };
const timer = setTimeout(() => { child.kill('SIGKILL'); done(reject, new BusReadError('outcome-unknown', 'the bus transport did not finish in time')); }, timeoutMs);
child.stdout.on('data', chunk => {
size += chunk.length;
if (size > maxBytes) { child.kill('SIGKILL'); return done(reject, new BusReadError('response-too-large')); }
out.push(chunk);
});
child.stderr.on('data', chunk => { if (err.length < 64) err.push(chunk); });
child.on('error', () => done(reject, new BusReadError('outcome-unknown', 'the bus transport did not start')));
child.on('close', status => {
if (status === null) return done(reject, new BusReadError('outcome-unknown', 'the bus transport did not finish'));
if (status !== 0) {
const code = Buffer.concat(err).toString('utf8').trim().split('\n').reverse().find(l => /^[a-z-]{1,64}$/.test(l)) ?? 'invalid-response';
return done(reject, new BusReadError(code));
}
try { done(resolve, JSON.parse(Buffer.concat(out).toString('utf8'))); }
catch { done(reject, new BusReadError('invalid-response')); }
});
child.stdin.on('error', () => {}); // A child that exits early reports through 'close'.
child.stdin.end(`${JSON.stringify({ business, user, verb, args })}\n`);
});
}
// The business is the --business flag, or else the live bus host's, read on every call, as busCaller does.
export function busBusiness({ dataRoot, business = null }) {
return () => {
if (business) return business;
let state;
try { state = readHostState(dataRoot); } catch { throw new BusReadError('no-bus-host', 'the bus host state file is unreadable'); }
if (!state?.live) throw new BusReadError('no-bus-host', 'no bus host is running and no --business was given');
return state.business;
};
}
// One granted user's caller, for that user's session only.
export function userCaller({ dataRoot, socket, business = null, user, env = process.env, timeoutMs = 30000 }) {
const which = busBusiness({ dataRoot, business });
return async (verb, args) => userCall({ socket, business: which(), user, env, timeoutMs })(verb, args);
}
// A send's error, sorted. The broker writes in one transaction, so any code it
// returns means nothing was written. A transport that lost the answer, or an
// answer that can't be read, leaves the outcome open: the message may exist.
+1 -2
View File
@@ -2,8 +2,7 @@
// MOSAIC_TESTER_SOCKET_V1: private tester ingress, no tester TCP.
import { createChat } from './chat.mjs';
import { startServer, previewOptions, proofOptions, DEFAULT_BOARD } from './serve.mjs';
import { busCaller } from './bus.mjs';
import { busBusiness, userCaller } from './user-bus.mjs';
import { busCaller, busBusiness, userCaller } from './bus.mjs';
import { consoleBus, userOptionsFor, agentCaller, TESTER_AGENT_VERBS } from './console-bus.mjs';
import { consoleAccess } from './access.mjs';
import { accessContextFor } from '../../cli/src/access.mjs';
+1 -1
View File
@@ -1,6 +1,6 @@
// FED R6 (#1800): the Console's bus-backed options for startServer, built over one transport
// `call`. cli.mjs builds them once over the human transport. A granted user's session gets the same
// set over its own user transport (user-bus.mjs userCaller), so no route a user reaches holds a
// set over its own user transport (bus.mjs userCaller), so no route a user reaches holds a
// human-transport object, and the broker decides every verb the user sends (design 3.1 D4).
import { createChat } from './chat.mjs';
import { views } from '../../bus/src/views.mjs';
-63
View File
@@ -1,63 +0,0 @@
// FED R6 (#1800): a granted user's bus transport, R3's user-cli.mjs (#414, design 3.6). Kept apart
// from bus.mjs so the human transport's reviewed bytes stay as row 164 froze them
// (bus/tests/channel-proof.test.mjs). Same protocol as humanCall: the request on stdin, JSON on
// stdout, or a bus error code as the last `^[a-z-]{1,64}$` line of stderr.
import { spawn } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { readHostState } from '../../cli/src/host.mjs';
import { BusReadError } from './bus.mjs';
export const USER_CLI = fileURLToPath(new URL('../../bus/src/user-cli.mjs', import.meta.url));
// The Console sets the user nonce itself, so user-cli.mjs doesn't re-exec and the broker sees the
// Console (`cli.mjs serve`) as its parent (principal.mjs). The human nonce is dropped, so this child
// can never pass as human-cli, and it only ever runs USER_CLI.
export function userCall({ socket, business, user, env = process.env, timeoutMs = 30000, maxBytes = 8 * 1024 * 1024 }) {
const { MOSAIC_BUS_CLI_NONCE, ...rest } = env;
return (verb, args = {}) => new Promise((resolve, reject) => {
let child;
try {
child = spawn(process.execPath, [USER_CLI, socket], { env: { ...rest, MOSAIC_BUS_USER_NONCE: randomBytes(32).toString('hex') }, stdio: ['pipe', 'pipe', 'pipe'] });
} catch { return reject(new BusReadError('outcome-unknown', 'the bus transport did not start')); }
const out = [], err = [];
let size = 0, settled = false;
const done = (fn, value) => { if (!settled) { settled = true; clearTimeout(timer); fn(value); } };
const timer = setTimeout(() => { child.kill('SIGKILL'); done(reject, new BusReadError('outcome-unknown', 'the bus transport did not finish in time')); }, timeoutMs);
child.stdout.on('data', chunk => {
size += chunk.length;
if (size > maxBytes) { child.kill('SIGKILL'); return done(reject, new BusReadError('response-too-large')); }
out.push(chunk);
});
child.stderr.on('data', chunk => { if (err.length < 64) err.push(chunk); });
child.on('error', () => done(reject, new BusReadError('outcome-unknown', 'the bus transport did not start')));
child.on('close', status => {
if (status === null) return done(reject, new BusReadError('outcome-unknown', 'the bus transport did not finish'));
if (status !== 0) {
const code = Buffer.concat(err).toString('utf8').trim().split('\n').reverse().find(l => /^[a-z-]{1,64}$/.test(l)) ?? 'invalid-response';
return done(reject, new BusReadError(code));
}
try { done(resolve, JSON.parse(Buffer.concat(out).toString('utf8'))); }
catch { done(reject, new BusReadError('invalid-response')); }
});
child.stdin.on('error', () => {}); // A child that exits early reports through 'close'.
child.stdin.end(`${JSON.stringify({ business, user, verb, args })}\n`);
});
}
// The business is the --business flag, or else the live bus host's, read on every call, as busCaller does.
export function busBusiness({ dataRoot, business = null }) {
return () => {
if (business) return business;
let state;
try { state = readHostState(dataRoot); } catch { throw new BusReadError('no-bus-host', 'the bus host state file is unreadable'); }
if (!state?.live) throw new BusReadError('no-bus-host', 'no bus host is running and no --business was given');
return state.business;
};
}
// One granted user's caller, for that user's session only.
export function userCaller({ dataRoot, socket, business = null, user, env = process.env, timeoutMs = 30000 }) {
const which = busBusiness({ dataRoot, business });
return async (verb, args) => userCall({ socket, business: which(), user, env, timeoutMs })(verb, args);
}
+1 -2
View File
@@ -19,8 +19,7 @@ import { startServer, userRoute } from '../src/serve.mjs';
import { createSso, SESSION_COOKIE } from '../src/sso.mjs';
import { consoleAccess } from '../src/access.mjs';
import { consoleBus, userOptionsFor } from '../src/console-bus.mjs';
import { BusReadError } from '../src/bus.mjs';
import { USER_CLI, userCall } from '../src/user-bus.mjs';
import { BusReadError, USER_CLI, userCall } from '../src/bus.mjs';
import { close, fixture } from './fixture.mjs';
import { browser } from './browser.mjs';
import { fakeOidc } from './helpers/fake-oidc.mjs';
+1 -1
View File
@@ -14,7 +14,7 @@ test('C2-04 CLI: both listener callback pairs dispatch library verbs with their
// The loader supplies a fixture bus and captures callbacks instead of
// opening an HTTP listener. Production imports and routes are unchanged.
const serveSource = "export const DEFAULT_BOARD='http://127.0.0.1:9';export const previewOptions=()=>null;export const proofOptions=()=>null;export async function startServer(o){if(typeof o.libraryFile!=='function'||typeof o.libraryStatus!=='function')throw Error('missing library callbacks');await o.libraryFile({project:'books',path:'Book One/A.md'});await o.libraryStatus({project:'books'});return {address:()=>({port:0}),close:()=>{}};}";
const busSource = `import {appendFileSync} from 'node:fs';export class BusReadError extends Error{};export const busCaller=o=>async(verb,args)=>{appendFileSync(${JSON.stringify(log)},JSON.stringify({verb,args,cli:o.cli??${JSON.stringify(HUMAN_CLI)}})+'\\n');return {};};`;
const busSource = `import {appendFileSync} from 'node:fs';export class BusReadError extends Error{};export const busCaller=o=>async(verb,args)=>{appendFileSync(${JSON.stringify(log)},JSON.stringify({verb,args,cli:o.cli??${JSON.stringify(HUMAN_CLI)}})+'\\n');return {};};export const busBusiness=()=>{throw Error('busBusiness outside --sso')};export const userCaller=()=>{throw Error('userCaller outside --sso')};`;
writeFileSync(hook, `import {registerHooks} from 'node:module';
registerHooks({load(url,context,next){
if(url.endsWith('/webui/src/serve.mjs'))return {format:'module',shortCircuit:true,source:${JSON.stringify(serveSource)}};
+1 -1
View File
@@ -55,7 +55,7 @@ test('row278 Console adapter calls argument-free thread.claims through the norma
const root = tmp(t, 'claims-cli-'), f = fixture(root), log = join(root, 'calls.jsonl'), hook = join(root, 'hooks.mjs');
writeFileSync(log, '');
const serveSource = `export const DEFAULT_BOARD='http://127.0.0.1:9';export const previewOptions=()=>null;export const proofOptions=()=>null;export async function startServer(o){await o.bus['thread.claims']();return {address:()=>({port:0}),close:()=>{}};}`;
const busSource = `import {appendFileSync} from 'node:fs';export class BusReadError extends Error{};export const busCaller=o=>async(verb,...args)=>{appendFileSync(${JSON.stringify(log)},JSON.stringify({verb,args,tester:!!o.cli})+'\\n');return [];};`;
const busSource = `import {appendFileSync} from 'node:fs';export class BusReadError extends Error{};export const busCaller=o=>async(verb,...args)=>{appendFileSync(${JSON.stringify(log)},JSON.stringify({verb,args,tester:!!o.cli})+'\\n');return [];};export const busBusiness=()=>{throw Error('busBusiness outside --sso')};export const userCaller=()=>{throw Error('userCaller outside --sso')};`;
writeFileSync(hook, `import {registerHooks} from 'node:module';registerHooks({load(url,context,next){
if(url.endsWith('/webui/src/serve.mjs'))return {format:'module',shortCircuit:true,source:${JSON.stringify(serveSource)}};
if(url.endsWith('/webui/src/bus.mjs'))return {format:'module',shortCircuit:true,source:${JSON.stringify(busSource)}};
@@ -0,0 +1,155 @@
// R6-N1 (#1807, row 294 N1): the user-session option swap in serve.mjs, driven through HTTP.
// granted-user.test.mjs G4 stops every owner route at the userRoute gate, so it never reaches the
// swap behind it. Here serve.mjs loads with that gate stubbed open (a load hook keyed on one query
// string; the product file is unchanged), and every owner route a user session might try must reach
// no owner option. The owner transport may carry only the plugins read mos-1394 allowed.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { request } from 'node:http';
import { registerHooks } from 'node:module';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../../bus/src/store.mjs';
import { Broker } from '../../bus/src/broker.mjs';
import { createSso, SESSION_COOKIE } from '../src/sso.mjs';
import { consoleAccess } from '../src/access.mjs';
import { userOptionsFor } from '../src/console-bus.mjs';
import { BusReadError } from '../src/bus.mjs';
import { close } from './fixture.mjs';
import { fakeOidc } from './helpers/fake-oidc.mjs';
const SERVE = new URL('../src/serve.mjs', import.meta.url).href, OPEN = `${SERVE}?gate=open`;
const GATE = 'export function userRoute(path, method) {';
registerHooks({
load(url, context, next) {
const r = next(url, context);
if (url !== OPEN) return r;
const source = String(r.source);
// A renamed gate fails here rather than leaving the test on the closed gate.
if (source.split(GATE).length !== 2) throw new Error('serve.mjs: the userRoute gate is not there exactly once');
return { ...r, source: source.replace(GATE, `${GATE} return true;`) };
},
});
const { startServer, userRoute } = await import(OPEN);
const HOST = 'console.example.test', ORIGIN = `https://${HOST}`;
function call(port, { method = 'GET', path = '/', headers = {}, body } = {}) {
return new Promise((resolve, reject) => {
const length = body === undefined ? {} : { 'content-length': Buffer.byteLength(body) };
const req = request({ host: '127.0.0.1', port, method, path, headers: { host: HOST, ...length, ...headers } }, res => {
let text = '';
res.on('data', c => (text += c));
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: text }));
});
req.on('error', reject);
req.end(body);
});
}
const setCookies = r => [r.headers['set-cookie'] ?? []].flat();
const writeHeaders = cookie => ({ cookie, origin: ORIGIN, 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' });
// As granted-user.test.mjs setup: the fake OP, a broker for `demo`, createSso with the access hook,
// counting owner stubs, and consoleBus over alice's transport for her session. Only startServer
// differs: it is the gate-open copy.
async function setup(t) {
const op = await fakeOidc();
t.after(() => op.close());
const iss = op.issuer('a');
const root = mkdtempSync(join(tmpdir(), 'webui-option-swap-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const doc = { accessVersion: 1, revision: 1, users: [{ id: 'alice', home: 'local', subjects: [{ provider: 'a', sub: 'alice-sub' }] }],
grants: [{ id: 'g-1', user: 'alice', business: 'demo', projects: ['*'], instances: ['assistant'], level: 'collaborator' }] };
const load = () => doc;
const broker = new Broker({
store,
businesses: {
demo: {
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: {},
roles: {
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } },
},
definitions: { 'chief-of-staff': {}, assistant: { messaging: JSON.parse(readFileSync(new URL('../../../roles/assistant.json', import.meta.url), 'utf8')).messaging } },
},
},
access: load,
});
const owner = [], user = [], opened = [];
const userTransport = name => async (verb, args = {}) => {
user.push(verb);
try { return broker.request(broker.bindUser({ business: 'demo', user: name }), { verb, args }); }
catch (e) { throw new BusReadError(e.code ?? 'invalid-response'); }
};
const ownerCall = async (verb, args) => { owner.push(['transport', verb, args]); return { plugins: { channel: ['webui'] } }; };
const userOptions = userOptionsFor({ owner: ownerCall, send: true, transport: name => { opened.push(name); return userTransport(name); } });
const lines = [];
const config = { publicOrigin: ORIGIN, providers: [{ id: 'a', issuer: iss.issuer, clientId: iss.clientId, clientSecret: { slot: 'a' }, algs: ['RS256'], subjects: [{ sub: 'owner-sub', name: 'jason' }] }] };
const sso = await createSso(config, { secrets: new Map([['a', iss.clientSecret]]), fetch: op.fetch, log: l => lines.push(l), users: consoleAccess({ load, business: () => 'demo' }) });
t.after(() => sso.close());
// Every owner option is a stub that records its own name.
const stub = name => async () => { owner.push(['option', name]); return name === 'send' ? { id: 'msg-0001' } : [{ verb: name }]; };
const options = {
bus: Object.fromEntries(['inbox', 'tasks', 'agents', 'trail', 'decisions', 'names', 'instances', 'routes', 'thread.claims'].map(v => [v, stub(`bus.${v}`)])),
send: stub('send'), settingsWrite: stub('settingsWrite'), projectFiles: stub('projectFiles'), projectFile: stub('projectFile'), projectSave: stub('projectSave'),
nameWrite: stub('nameWrite'), assistantStop: stub('assistantStop'), instancesWrite: stub('instancesWrite'), routesWrite: stub('routesWrite'), agentWrite: stub('agentWrite'),
processing: { read: stub('processing.read'), halt: stub('processing.halt'), resume: stub('processing.resume') },
reads: Object.fromEntries(['settings', 'business', 'busStatus', 'queue', 'row'].map(v => [v, stub(`reads.${v}`)])),
chat: Object.fromEntries(['projects', 'threads', 'messages', 'send', 'open', 'createProject', 'createThread'].map(v => [v, stub(`chat.${v}`)])),
};
const web = await startServer({ port: 0, board: 'http://127.0.0.1:9', sso, log: l => lines.push(l), ...options, userOptions });
t.after(() => close(web));
return { op, iss, owner, user, opened, lines, port: web.address().port };
}
async function signIn(env, sub) {
const start = await call(env.port, { path: '/auth/login' });
assert.equal(start.status, 303);
const cookie = setCookies(start)[0].split(';')[0];
const location = start.headers.location;
const g = env.op.grant(env.iss, location, { sub });
const q = new URLSearchParams({ code: g.code, state: new URL(location).searchParams.get('state') });
const r = await call(env.port, { path: `/auth/callback?${q}`, headers: { cookie } });
const session = setCookies(r).find(c => c.startsWith(`${SESSION_COOKIE}=`) && !c.includes('Max-Age=0'))?.split(';')[0];
assert.ok(session, `${sub}: no session cookie (${r.status})`);
return session;
}
// granted-user.test.mjs G4's owner routes, plus the chat routes a user session reaches anyway.
const ROUTES = [
['GET', '/api/bus/inbox'], ['GET', '/api/bus/tasks'], ['GET', '/api/bus/names'], ['GET', '/api/bus/thread.claims'],
['POST', '/api/bus/message', { to: 'coder', body: 'hi' }],
['GET', '/api/settings'], ['POST', '/api/settings', { revision: 'x' }],
['POST', '/api/name', { role: 'assistant', name: 'x' }], ['POST', '/api/assistant/stop', {}],
['POST', '/api/instances', { instance: 'x' }], ['POST', '/api/routes', { routes: [] }],
['POST', '/api/agent/launch', { instance: 'x' }], ['GET', '/api/agent/request?id=x'],
['GET', '/api/processing'], ['POST', '/api/processing/halt', {}],
['GET', '/api/project-files?project=p'], ['GET', '/api/project-file?project=p&path=a'], ['POST', '/api/project-file', { project: 'p' }],
['GET', '/api/board'], ['GET', '/api/conversations'], ['POST', '/api/seen', {}], ['POST', '/api/reply', {}],
['GET', '/api/queue'], ['GET', '/api/queue/1'], ['GET', '/api/business'], ['GET', '/api/bus-status'], ['GET', '/api/task-fields'],
['GET', '/api/chat/projects'], ['GET', '/api/chat/instances'],
['GET', '/app.js'], ['GET', '/bus.js'], ['GET', '/agents.js'], ['GET', '/editor.js'], ['GET', '/shared/app.css'], ['GET', '/index.html'],
];
const go = (env, session, [method, path, value]) => call(env.port, { method, path, headers: value === undefined ? { cookie: session } : writeHeaders(session), body: value === undefined ? undefined : JSON.stringify(value) });
test('R6-N1 with the userRoute gate open, a user session reaches no owner option over HTTP', async t => {
// The stub took: the gate-open copy lets an owner route through.
assert.equal(userRoute('/api/bus/message', 'POST'), true);
const env = await setup(t);
// Positive control: the owner's session reaches the owner's send on this route.
const ownerSession = await signIn(env, 'owner-sub');
const sent = await go(env, ownerSession, ['POST', '/api/bus/message', { to: 'coder', body: 'hi' }]);
assert.deepEqual(env.owner, [['option', 'send']], `the owner's send did not run: ${sent.status} ${sent.body}`);
env.owner.length = 0;
const session = await signIn(env, 'alice-sub');
for (const route of ROUTES) {
const r = await go(env, session, route);
assert.doesNotMatch(r.body, /owner-only/, `${route[0]} ${route[1]}: the gate is still closed`);
// Only the plugins read with {}, as granted-user.test.mjs G9 pins for the chat.
for (const entry of env.owner) assert.deepEqual(entry, ['transport', 'plugins', {}], `${route[0]} ${route[1]}: ${JSON.stringify(entry)}`);
}
assert.ok(env.user.includes('message.send'), 'the message route never reached the user transport');
assert.ok(env.opened.length > 0 && env.opened.every(name => name === 'alice'));
});