Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde5b84964 | ||
|
|
b6c3129d2e | ||
|
|
c25f5b8dae |
No files matched your search
@@ -5411,3 +5411,9 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r
|
||||
2026-10-23T17:29:46Z | Gyro Vikunja571 sage-3542 Connections amendment | Branch gyro/editor-connections-design cut exactly from landed PRD1.17 30e6bb680; read REQ-LIB-5 in that PRD. Replaced stale J-b waits in parent and W4. Proposed W8 owns current authorized Markdown/DOCX links/backlinks list, labels, versioned refresh, leakage refusals and fixture tests. Build order under mos-1470/comment961 is W7 closed routing candidate, W8 landing, then W7 human first-real-import opening. No REQ-LIB-8 condition or acceptance change. Q234/mos-1363/S0 M1-M14 and post-10-27/MVP priority fixed. Static diff, twelve brief sections and append-only checks pass. No repo Node, tests, build, browser, queue mutation or live action. Fenton reviews after authorized docs branch push; Sage lands on approval.
|
||||
|
||||
2026-10-23T17:32:37Z | Gyro sage-3544 fenton-0016 N1/N2 delta | Before: W7 Gate/final real-conversion list omit the W8 build-order cross-reference and W8 human-view filtering cites agent settings. After: both locations explicitly put W8 landing before human opening as mos-1470 build order, not acceptance; comment/tracked-deletion links are not current story links and never appear in the human Connections view regardless of agent settings. New commit atop approved 5af060fb5, no amend. Static diff and append-only checks pass. Docs only, no repo Node/tests/live action. Fenton checks only this delta.
|
||||
|
||||
2026-10-23 | Beakley (88b97f65) | Row 217 #1718 takeover, before implementation | Clean base 45dca69f6 on beakley/row217. Recovering Launchpad handoff for citation guard, live settings, page store and runner retry. Acceptance is C2-06/design 5.2 plus sage-3595/3599/3602. Tracking agents/beakley/work/queue-217/BUILD.md; no product changes or Node/tests; Sage CLEAR required.
|
||||
|
||||
2026-10-23 | Beakley (88b97f65) | Row 217 #1718 untested draft checkpoint | Continued Launchpad e25ef0373; added runner retry/text/note implementation, pure protocol constants and sage-3640 approved single read-only mount, CLI documentation and new acceptance test files. Static checks caught missing projects compatibility, async request mutation and Git pathspec matching; bounded fixes and regression tests authored. git diff --check clean. No Node/test/mutant executed; Sage holds CLEAR. 283/216/285 dependency checks and all acceptance execution remain open. Packet agents/beakley/work/queue-217/MANIFEST.md.
|
||||
|
||||
2026-10-23 | Beakley (88b97f65) | Row 217 early-look follow-up, Untested | Webby 0939c0682 E1 adds ordinary mallory human-swap test; E2 adds stored trailer-derived agent/file event evidence; E6 creative-sender matrix and E7 runtime readRaw tests authored. Sage-3668/3670 accepts direct writer substitution because setter keys exclude replyLimits (organization.set exists, correcting review premise), and requires distinct availability refusal for E4. Real held-lock/socket and runner-reason regressions plus M5 authored before that production fix; expected RED awaits Sage CLEAR. No Node/tests/mutants, no old-test edits, no green claim.
|
||||
@@ -0,0 +1,157 @@
|
||||
# Row 217 — C2-06 citation guard, page store and runner retry
|
||||
|
||||
Owner: Beakley (88b97f65-97db-4f05-8c6e-081f92bc3501), assigned by Mos Q245.
|
||||
Issue: #1718. Reviewer: Webby. Branch: `beakley/row217`.
|
||||
Brief: `docs/plans/2026-10-20_case2-rows.md`, C2-06.
|
||||
Status: Untested draft on Launchpad handoff `e25ef0373`; targeted execution awaits Sage CLEAR.
|
||||
|
||||
## Plan and boundaries
|
||||
|
||||
Continue Launchpad's implementation from its handed-off ref. Complete the guard,
|
||||
live reply limits and M11 writer fence, broker page store and check evidence,
|
||||
CLI setter and runner retry/note behavior against the brief and design 5.2.
|
||||
Supply acceptance tests and mutants, then request Sage's test slot and Webby's
|
||||
review. Sage owns queue movement, gate and landing. Push only `beakley/*`, add
|
||||
commits without amend/force or attribution lines. No credential reads.
|
||||
|
||||
## Standing rulings received from Sage (sage-3610)
|
||||
|
||||
- sage-3595: stub supporting scope until row 283 lands, then rebase onto it and
|
||||
add real-fixture tests. Landing follows 283. `runtime.mjs` wiring approved.
|
||||
- Check evidence belongs in an `action.allowed` event in the same transaction
|
||||
as message insertion; no new event kind. Read the library at the cited commit.
|
||||
- In library businesses only, JSON over 32,768 characters gets the
|
||||
`request-too-large` retry. `role.claim` adds `library` only for a library business.
|
||||
- Store `check.citations` entries with `scope`, `agent`, and `file` (the file's
|
||||
last commit at the cited commit). On `thread.messages`, add `last` at the served
|
||||
tip, one git call per distinct path per read. Scope/agent are broker evidence.
|
||||
- sage-3599: optional `BusError.reason`, guard refusals only, capped at 300
|
||||
characters. Add bus server/client to manifest. Reasonless envelopes stay
|
||||
byte-identical.
|
||||
- `render.mjs` obsolete notice hits belong to row 225.
|
||||
- Node and tests require Sage CLEAR (sage-3610 supersedes the initial permission
|
||||
for targeted new tests). Full suites, webui builds and gate require shared slot.
|
||||
- Existing test edits require Mos GO through Sage; list every file and numstat
|
||||
below before seeking that approval.
|
||||
|
||||
## Recovery and coordination
|
||||
|
||||
Initial base `45dca69f6`, clean. `scripts/mosaic queue next beakley` refused:
|
||||
linked worktrees cannot run the queue. Reported to Sage/Mos in beakley-0001;
|
||||
Sage will change ownership at a queue gap. No queue files changed.
|
||||
Read AGENTS.md, agents/README.md, C2-06 brief, design 5.2 and the row 225 BUILD
|
||||
contract. Requested Launchpad handoff in beakley-0002. Its header timestamp was
|
||||
mistakenly typed ahead of the actual clock; sequence key identifies the message.
|
||||
|
||||
## Existing test changes
|
||||
|
||||
None yet. No tests or Node commands run.
|
||||
|
||||
|
||||
## Handoff adopted and independent work (2026-10-23)
|
||||
|
||||
Fast-forwarded `beakley/row217` to Launchpad `e25ef0373` on `45dca69f6`.
|
||||
The full original handoff/rulings remain at
|
||||
`agents/launchpad/work/queue-217/BUILD.md`. No handoff code was rebuilt from scratch.
|
||||
|
||||
Implemented the runner path: role.claim library switch; exact fixed texts; note
|
||||
inside JSON text with citations retained and no JSON clip; fixed texts defer
|
||||
notes; one same-session retry for guard codes and both existing size caps;
|
||||
retry fallback chosen from the second refusal; separate usage turn id for retry.
|
||||
Outside library businesses the existing send path remains intact.
|
||||
|
||||
Added the CLI README usage paragraph. New acceptance tests cover library parsing,
|
||||
real Git reachability/quotes/scope/origin, settings fallback, M11 direct writer
|
||||
and serialization, CLI dispatch, broker insert/check atomicity, live settings,
|
||||
page bounds/lifecycle, socket reasons, per-path last reads, request snapshots,
|
||||
and runner RESULT/typed send_message, fixed texts, long JSON and unslop behavior.
|
||||
These are authored evidence only: none has executed.
|
||||
|
||||
Static defects corrected locally: a missing projects object now means no library;
|
||||
the guard sends its captured request arguments after the asynchronous check;
|
||||
origin/last Git logs use literal pathspecs; a web quote can match any retained
|
||||
fetch of the cited URL. Each has new-file regression coverage.
|
||||
|
||||
## Scope additions and cross-row contract
|
||||
|
||||
- `packages/library/src/reply-protocol.mjs`: shared GUARD_CODES/FIXED_TEXTS,
|
||||
import-free, so runner imports do not reach host Git modules. guard.mjs
|
||||
reexports them to preserve Launchpad's API. Fethry confirmed no renderer import
|
||||
is needed when the renderer simply displays fixed texts as ordinary text.
|
||||
- `packages/seat/src/sandbox.mjs`: one RUNNER_FILES entry for that pure file,
|
||||
approved by sage-3640. No other mount change. The existing sandbox.test.mjs
|
||||
import-closure test must pass unchanged; seat joins the gate list.
|
||||
- Runtime hunks remain exactly Launchpad's import and library guard/limits/last
|
||||
adapters, plus library.limits.set dispatch. Drift checks wait on 216 and 285.
|
||||
- Status contract confirmed with Fethry (beakley-0004, fethry-0009): nested
|
||||
`replyLimits: {requireCitation, citationsPerReply, fallback: [keys],
|
||||
problem: "reply-limits-unreadable" | null}`. Check entries remain ordered
|
||||
`scope/agent/file`, plus `last` on reads; no changed boolean.
|
||||
|
||||
## Verification and remaining acceptance
|
||||
|
||||
- Static `git diff --check`: clean so far. No Node, suite, test or mutant run.
|
||||
- Existing test file edits: none (numstat 0/0 for every pre-existing test).
|
||||
All new test files are listed in manifest.r1.sha256; no Mos approval requested.
|
||||
- `mutants.py` is authored for isolated HEAD archives with green controls:
|
||||
M1 remove both writer fences; M2 compare incoming human; M3 split evidence
|
||||
from insert; M4 drop client reason. Not executed.
|
||||
- Still required: Sage CLEAR for targeted runs; fix first-run failures; supporting
|
||||
scope acceptance against row 283; 216 index/readLibrary fixture comparison;
|
||||
285/runtime drift check; full library/business/bus/cli/harness/seat suites and
|
||||
every scripts/test-*.sh on Sage's gate; mutant execution; Webby approval.
|
||||
- Before in-review, perform brief's obsolete-code/key search and report outside
|
||||
ownership hits to Sage. render.mjs/render.test.mjs hits belong to 225.
|
||||
- No queue changes, protected branch pushes, gate claims or acceptance claims.
|
||||
|
||||
Static obsolete-code/key search completed: only packages/library/src/render.mjs:6 and packages/library/tests/render.test.mjs:58 match no-cited-answer; both belong to row 225 per standing ruling. No owned-path hit. Repeat before in-review after dependency changes.
|
||||
|
||||
|
||||
## Webby early look 0939c0682 / webby-0249 and sage-3668/3670
|
||||
|
||||
This remains an untested draft. The follow-up adds tests only; production E4
|
||||
changes wait for the held-lock regression's first RED under Sage CLEAR.
|
||||
|
||||
- E1: added an ordinary actor `mallory` that sets both `human` to itself and
|
||||
`replyLimits`. The existing role/tester actors did not distinguish M2 because
|
||||
the actor-shape exclusions rejected them independently of the human comparison.
|
||||
M2 is still unrun; no kill claimed.
|
||||
- E2: added a real bus event assertion for `agent: "acme/scribe"` from a file's
|
||||
Mosaic-Agent trailer. The cited tip includes a later unrelated commit, so the
|
||||
event must store the file commit rather than the library tip. thread.messages
|
||||
must show `last === file` for that unchanged path. Supporting scope still waits
|
||||
for row 283's real fixture, as ruled.
|
||||
- E3: sage-3668 accepts direct writer.write tests under chief and tester actors
|
||||
as substitutes for the brief's setter cases (brief lines 483-491 at base
|
||||
45dca69f6). Correction to the early-look premise: organization.set DOES exist,
|
||||
broker.mjs:4701/4709, but ORGANIZATION_KEYS excludes replyLimits; instances.set
|
||||
also cannot carry it. Direct writes exercise the shared M11 boundary those
|
||||
setters cannot reach with that input. Sage confirmed the accurate reason in
|
||||
sage-3670; the unrelated actor case remains required.
|
||||
- E4: traced readSyncState -> withLibraryLock -> library-sync-busy after five
|
||||
seconds -> servedMirror catch-all -> null served -> misleading citation-invalid.
|
||||
New real-flock/socket test first proves the cite valid, holds the lock, asserts
|
||||
the exact citation-unavailable reason the agent receives, verifies no insert,
|
||||
and checks fixed fallback delivery and recovery after unlock. Additional tests
|
||||
distinguish malformed sync state from absent/new mirrors, and assert that the
|
||||
actual runner retry request contains that reason. These tests are expected RED
|
||||
until the fix. Planned production change: only an absent mirror or legitimate
|
||||
empty state yields null; other failures become GuardError citation-unavailable;
|
||||
bodies without path cites do not require the mirror lock. Add the code to
|
||||
GUARD_CODES so the runner sends the agent that reason on its one retry. Per
|
||||
sage-3670, existing four fallback bodies can remain only if that delivery is
|
||||
verified. M5 restores catch-all behavior; authored, not run.
|
||||
- E5: first-library-project selection is a Launchpad handoff implementation
|
||||
choice, not a validated rule that a business can only have one library.
|
||||
Cites into another library remain a limitation. No mapping/schema rule added.
|
||||
- E6: the broker's full guard table now runs as both coder and research.
|
||||
- E7: new citation-runtime.test.mjs starts the real runtime, reads limits from
|
||||
the real organization file, then changes, corrupts and removes that fixture
|
||||
file and checks library.status without replacing the library adapter.
|
||||
|
||||
No Node, tests or mutants ran. No test that predated row 217 changed.
|
||||
|
||||
|
||||
## Webby follow-up 192049c98 / webby-0258
|
||||
|
||||
E1, E2, E6 and E7 closed statically; no executed acceptance claimed. E3 correction and E5 limitation accepted. E4 tests accepted statically; real RED then production fix/green remain on hold for Sage CLEAR, with supporting scope waiting on 283. F1: citation-helpers status now uses its mutable served value, matching guard/last after setServed instead of reporting the original fixture commit. Test-only fixture correction, unrun. manifest.r3.sha256 records the updated helper; no production or old-test changes.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Row 217 draft manifest — Untested
|
||||
|
||||
Issue #1718, Case 2 C2-06. Base `45dca69f6`, continued from Launchpad
|
||||
`e25ef0373`. This is a checkpoint for static review, not an in-review or
|
||||
acceptance claim. `manifest.r1.sha256` freezes the product and test files.
|
||||
|
||||
## Product scope
|
||||
|
||||
| Path | Change |
|
||||
| --- | --- |
|
||||
| packages/library/src/guard.mjs | Schema and seven checks, real cited-commit reads, origins and latest path commits |
|
||||
| packages/library/src/limits.mjs | Pure defaults, validation and conservative unreadable-file fallback |
|
||||
| packages/library/src/reply-protocol.mjs | Import-free guard codes and four runner fixed texts |
|
||||
| packages/library/src/index.mjs | Guard and limits exports |
|
||||
| packages/business/src/business.mjs | Permit any replyLimits value without widening another schema |
|
||||
| packages/business/src/organization.mjs | M11 fence on input and serialized output, human setter, raw live reader |
|
||||
| packages/bus/src/broker.mjs | Async pre-insert guard, atomic existing-kind check event, per-session pages, settings verb/status, optional library claim flag and read-time last evidence |
|
||||
| packages/bus/src/runtime.mjs | Inject guard/live limits/last and dispatch human setter (sage-3595) |
|
||||
| packages/bus/src/server.mjs | Await guarded sends and thread reads; optional guard reason (sage-3599) |
|
||||
| packages/bus/src/client.mjs | Preserve optional capped reason (sage-3599) |
|
||||
| packages/harness/src/runner.mjs | Library texts, one retry, JSON note/clip behavior and retry usage tracking |
|
||||
| packages/seat/src/sandbox.mjs | Exactly one read-only RUNNER_FILES addition for the pure protocol (sage-3640) |
|
||||
| packages/cli/src/limits.mjs | Human limits replies file command |
|
||||
| packages/cli/README.md | Reply settings usage |
|
||||
|
||||
No dependency, SQL schema, event kind, role authority or credential changes.
|
||||
The additional sandbox file carries only constants and has no imports.
|
||||
|
||||
## New tests and fixtures
|
||||
|
||||
| Path | Acceptance focus |
|
||||
| --- | --- |
|
||||
| packages/library/tests/guard.test.mjs | Seven table checks, cap boundaries, real Git origins/reachability/literal names, web quotes and limits fallback |
|
||||
| packages/business/tests/reply-limits.test.mjs | Human-only M11, old-human comparison, dotted agent identity, serialization, invalid values and raw reads |
|
||||
| packages/bus/tests/citation-helpers.mjs | Shared made-up repository and real broker fixture |
|
||||
| packages/bus/tests/citation-guard.test.mjs | Refusal insertion counts, transaction failure, live settings/status, page bounds, socket envelopes, file/last and async snapshot |
|
||||
| packages/cli/tests/reply-limits.test.mjs | Revision read, exact setter args and usage refusals |
|
||||
| packages/harness/tests/fixtures/citation-adapter.mjs | Scripted local harness, typed calls and unslop events |
|
||||
| packages/harness/tests/citation-runner.test.mjs | Every RESULT refusal and typed send_message, one retry, both size caps, plain multibyte answers, fixed texts and notes, old non-library behavior |
|
||||
|
||||
No existing test changed. Existing role.claim pins and the seat import-closure
|
||||
test stay untouched. Existing test numstat: none.
|
||||
|
||||
## Review focus and outstanding evidence
|
||||
|
||||
The check event and message insert must roll back together. Scope/agent/file
|
||||
come from Git; `last` is read once per distinct path per read. Guard refusals
|
||||
alone have a reason, bounded at 300 characters without quote or URL content.
|
||||
Captured arguments must be the ones inserted after async validation. Every
|
||||
plain or JSON length refusal shares the same single retry budget.
|
||||
|
||||
All tests and four isolated mutants are unrun pending Sage CLEAR. The supporting
|
||||
scope tests require row 283; the index-set comparison requires row 216; runtime
|
||||
drift must be checked against 216/285 before in-review. Landing follows 283.
|
||||
Full gate: library, business, bus, cli, harness and seat, plus every
|
||||
scripts/test-*.sh. Webby reviews the final tested manifest; Sage owns the gate,
|
||||
queue and landing. No push to a protected branch is authorized.
|
||||
|
||||
|
||||
## Early-look follow-up packet (manifest.r2.sha256)
|
||||
|
||||
Tests-only follow-up to Webby 0939c0682. Adds ordinary-actor M2 discrimination,
|
||||
stored agent/file evidence at an unrelated tip, creative-sender table coverage,
|
||||
and real runtime readRaw coverage. New citation-availability.test.mjs and runner
|
||||
availability case are intentionally unrun regressions for E4/sage-3668/3670.
|
||||
Production availability handling is not implemented yet: run that targeted RED
|
||||
on CLEAR before applying its fix, then rerun the same cases. M5 restores the
|
||||
catch-all. This packet is not ready for a green gate or manifest approval.
|
||||
@@ -0,0 +1,21 @@
|
||||
68c05644d29188b897e2d4695d8e7025e143d424d69a91bc07bc950f589d34bf packages/bus/src/broker.mjs
|
||||
ad3c686a089f67f3c89610937c6fc9b32367cea5d9735125d89bef042082d4e8 packages/bus/src/client.mjs
|
||||
49e01e5e73cc3e830ad48a1b6b13fe4d06a38950f17847dc049b75209d6d3f0a packages/bus/src/runtime.mjs
|
||||
66b26d131a19f4f411686427274936850c1ff0aed11e9b281ae4aadcf4b289fc packages/bus/src/server.mjs
|
||||
29114051a9bc5271e48978a9552d64b51b0ac4810f94a1f98940bf617b148df8 packages/bus/tests/citation-guard.test.mjs
|
||||
dc2d5836f10f591fc174aaf9587a3bff793b24ac29efe9ab68c6a9243d1d1c2c packages/bus/tests/citation-helpers.mjs
|
||||
825da818ba3cf45a4837f0f15a9c2cdba55a3d0bbb4479d55236f35799427c6f packages/business/src/business.mjs
|
||||
dbd79a4dfc926620d034b540f98f39336d81e4c79f214424c3cf19250c1d1f8d packages/business/src/organization.mjs
|
||||
bec04a894bc2af000548283ed7719e4de1c6d56688f42238a113390e9b2d3fbe packages/business/tests/reply-limits.test.mjs
|
||||
1f1bea250f694a2aae9b9dc856b3c3424ceca153ccf826c99ccb98e6ea9cd394 packages/cli/README.md
|
||||
2f291a71f490f2741edada7a3e3788500f8976a08eb8ab1411f6c5982f778260 packages/cli/src/limits.mjs
|
||||
f38af2d46d5f5f6388f599912507b94b87a0bf9dcc2104bb2187c38ee35118d5 packages/cli/tests/reply-limits.test.mjs
|
||||
6ad72b4245e0a66e32725941900c1ece64c2e5de2fb73d65c8e042b4f3a691a6 packages/harness/src/runner.mjs
|
||||
ec7ef0cbf493217eb9229895cae2fe7ef603f41913840e59a73220df387c81ca packages/harness/tests/citation-runner.test.mjs
|
||||
330bc71bf6c7e64d3ddb70440903d2d9acf7878d44a49d7e30c2352ee4c807ec packages/harness/tests/fixtures/citation-adapter.mjs
|
||||
0365114cf5773cbaa9fba196fbd1e773b2ec5ad2300c40231c357ca58884b9e3 packages/library/src/guard.mjs
|
||||
aaa8f7ba60a729a88a6ef57b3d5ff5a69bcf0e17b60e7db51a8dc5a697627b6c packages/library/src/index.mjs
|
||||
3e04fa25a64334d33ffbec704205b32cd01992f2d341e908f933a345199beee0 packages/library/src/limits.mjs
|
||||
c9e682eff87e822647921288d7fce0ebdc7b09f0a6dfdd6528f10249ae26e81b packages/library/src/reply-protocol.mjs
|
||||
dd056409712113eb9c1b70e29b21022f5b30df6ffbfb2658e293edd6ec7195c7 packages/library/tests/guard.test.mjs
|
||||
2f3ec99129d16394d4458e252b9cba4e38ff66cd9393e3889f8afc3140e15a8e packages/seat/src/sandbox.mjs
|
||||
@@ -0,0 +1,23 @@
|
||||
68c05644d29188b897e2d4695d8e7025e143d424d69a91bc07bc950f589d34bf packages/bus/src/broker.mjs
|
||||
ad3c686a089f67f3c89610937c6fc9b32367cea5d9735125d89bef042082d4e8 packages/bus/src/client.mjs
|
||||
49e01e5e73cc3e830ad48a1b6b13fe4d06a38950f17847dc049b75209d6d3f0a packages/bus/src/runtime.mjs
|
||||
66b26d131a19f4f411686427274936850c1ff0aed11e9b281ae4aadcf4b289fc packages/bus/src/server.mjs
|
||||
cdd0304ec25ef1ad3e31c52502fa46f2c80bd77c8d3c2e13f8ed4b5328f342c9 packages/bus/tests/citation-availability.test.mjs
|
||||
8dc344b8a905c58d544b031ca84adb7eb869d9de88eb8e6b0fc1d9190702a38a packages/bus/tests/citation-guard.test.mjs
|
||||
62d8a55945d4b700e22c8c49b6420a5e25719d266901a2ad20c72514735e65e9 packages/bus/tests/citation-helpers.mjs
|
||||
168dcac99d9bfdfdf5ff0eaa2c0fff91595b351b4d0092dc299532c9c32869f0 packages/bus/tests/citation-runtime.test.mjs
|
||||
825da818ba3cf45a4837f0f15a9c2cdba55a3d0bbb4479d55236f35799427c6f packages/business/src/business.mjs
|
||||
dbd79a4dfc926620d034b540f98f39336d81e4c79f214424c3cf19250c1d1f8d packages/business/src/organization.mjs
|
||||
4d2eb11beaa80175e524af1b95c81e7164990fba1d3e1754f5ba06eee390a14f packages/business/tests/reply-limits.test.mjs
|
||||
1f1bea250f694a2aae9b9dc856b3c3424ceca153ccf826c99ccb98e6ea9cd394 packages/cli/README.md
|
||||
2f291a71f490f2741edada7a3e3788500f8976a08eb8ab1411f6c5982f778260 packages/cli/src/limits.mjs
|
||||
f38af2d46d5f5f6388f599912507b94b87a0bf9dcc2104bb2187c38ee35118d5 packages/cli/tests/reply-limits.test.mjs
|
||||
6ad72b4245e0a66e32725941900c1ece64c2e5de2fb73d65c8e042b4f3a691a6 packages/harness/src/runner.mjs
|
||||
a5ef1b65230239a7baf6d0e8f2b8a51c0516348c7c4339a55b3f0a1a6ff531ba packages/harness/tests/citation-runner.test.mjs
|
||||
330bc71bf6c7e64d3ddb70440903d2d9acf7878d44a49d7e30c2352ee4c807ec packages/harness/tests/fixtures/citation-adapter.mjs
|
||||
0365114cf5773cbaa9fba196fbd1e773b2ec5ad2300c40231c357ca58884b9e3 packages/library/src/guard.mjs
|
||||
aaa8f7ba60a729a88a6ef57b3d5ff5a69bcf0e17b60e7db51a8dc5a697627b6c packages/library/src/index.mjs
|
||||
3e04fa25a64334d33ffbec704205b32cd01992f2d341e908f933a345199beee0 packages/library/src/limits.mjs
|
||||
c9e682eff87e822647921288d7fce0ebdc7b09f0a6dfdd6528f10249ae26e81b packages/library/src/reply-protocol.mjs
|
||||
dd056409712113eb9c1b70e29b21022f5b30df6ffbfb2658e293edd6ec7195c7 packages/library/tests/guard.test.mjs
|
||||
2f3ec99129d16394d4458e252b9cba4e38ff66cd9393e3889f8afc3140e15a8e packages/seat/src/sandbox.mjs
|
||||
@@ -0,0 +1,23 @@
|
||||
68c05644d29188b897e2d4695d8e7025e143d424d69a91bc07bc950f589d34bf packages/bus/src/broker.mjs
|
||||
ad3c686a089f67f3c89610937c6fc9b32367cea5d9735125d89bef042082d4e8 packages/bus/src/client.mjs
|
||||
49e01e5e73cc3e830ad48a1b6b13fe4d06a38950f17847dc049b75209d6d3f0a packages/bus/src/runtime.mjs
|
||||
66b26d131a19f4f411686427274936850c1ff0aed11e9b281ae4aadcf4b289fc packages/bus/src/server.mjs
|
||||
cdd0304ec25ef1ad3e31c52502fa46f2c80bd77c8d3c2e13f8ed4b5328f342c9 packages/bus/tests/citation-availability.test.mjs
|
||||
8dc344b8a905c58d544b031ca84adb7eb869d9de88eb8e6b0fc1d9190702a38a packages/bus/tests/citation-guard.test.mjs
|
||||
8a2d4b636f8d4f926c484254c33ecf7b7e4722acef3250d2730593984d5ac06b packages/bus/tests/citation-helpers.mjs
|
||||
168dcac99d9bfdfdf5ff0eaa2c0fff91595b351b4d0092dc299532c9c32869f0 packages/bus/tests/citation-runtime.test.mjs
|
||||
825da818ba3cf45a4837f0f15a9c2cdba55a3d0bbb4479d55236f35799427c6f packages/business/src/business.mjs
|
||||
dbd79a4dfc926620d034b540f98f39336d81e4c79f214424c3cf19250c1d1f8d packages/business/src/organization.mjs
|
||||
4d2eb11beaa80175e524af1b95c81e7164990fba1d3e1754f5ba06eee390a14f packages/business/tests/reply-limits.test.mjs
|
||||
1f1bea250f694a2aae9b9dc856b3c3424ceca153ccf826c99ccb98e6ea9cd394 packages/cli/README.md
|
||||
2f291a71f490f2741edada7a3e3788500f8976a08eb8ab1411f6c5982f778260 packages/cli/src/limits.mjs
|
||||
f38af2d46d5f5f6388f599912507b94b87a0bf9dcc2104bb2187c38ee35118d5 packages/cli/tests/reply-limits.test.mjs
|
||||
6ad72b4245e0a66e32725941900c1ece64c2e5de2fb73d65c8e042b4f3a691a6 packages/harness/src/runner.mjs
|
||||
a5ef1b65230239a7baf6d0e8f2b8a51c0516348c7c4339a55b3f0a1a6ff531ba packages/harness/tests/citation-runner.test.mjs
|
||||
330bc71bf6c7e64d3ddb70440903d2d9acf7878d44a49d7e30c2352ee4c807ec packages/harness/tests/fixtures/citation-adapter.mjs
|
||||
0365114cf5773cbaa9fba196fbd1e773b2ec5ad2300c40231c357ca58884b9e3 packages/library/src/guard.mjs
|
||||
aaa8f7ba60a729a88a6ef57b3d5ff5a69bcf0e17b60e7db51a8dc5a697627b6c packages/library/src/index.mjs
|
||||
3e04fa25a64334d33ffbec704205b32cd01992f2d341e908f933a345199beee0 packages/library/src/limits.mjs
|
||||
c9e682eff87e822647921288d7fce0ebdc7b09f0a6dfdd6528f10249ae26e81b packages/library/src/reply-protocol.mjs
|
||||
dd056409712113eb9c1b70e29b21022f5b30df6ffbfb2658e293edd6ec7195c7 packages/library/tests/guard.test.mjs
|
||||
2f3ec99129d16394d4458e252b9cba4e38ff66cd9393e3889f8afc3140e15a8e packages/seat/src/sandbox.mjs
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run only on Sage CLEAR. Mutate isolated HEAD archives; never edit the checkout.
|
||||
|
||||
A green control run is required before each negative result counts as a kill.
|
||||
Usage: python3 agents/beakley/work/queue-217/mutants.py OUTPUT_DIRECTORY
|
||||
"""
|
||||
from pathlib import Path
|
||||
import io
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(sys.argv[1]).resolve()
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
WRITER = "packages/business/src/organization.mjs"
|
||||
BROKER = "packages/bus/src/broker.mjs"
|
||||
CLIENT = "packages/bus/src/client.mjs"
|
||||
ATOMIC = "if (check) this.#event(s, 'action.allowed', { action: 'message.checked', message: mid, check }, mid, checkEventId(mid));"
|
||||
REENTER = "return this.#reenter(cap, s, { verb: 'message.send', args }, check);"
|
||||
SPLIT = """const result = this.#reenter(cap, s, { verb: 'message.send', args }, check);
|
||||
if (check) this.#store.transaction(() => this.#event(s, 'action.allowed', { action: 'message.checked', message: result.id, check }, result.id, checkEventId(result.id)));
|
||||
return result;"""
|
||||
MUTANTS = [
|
||||
("M1-writer-fence", [(WRITER, " replyLimitsFence(old, document, actor);", ""),
|
||||
(WRITER, " replyLimitsFence(old, written, actor);", "")],
|
||||
["packages/business/tests/reply-limits.test.mjs"]),
|
||||
("M2-incoming-human", [(WRITER, "actor === old.human", "actor === document.human")],
|
||||
["packages/business/tests/reply-limits.test.mjs"]),
|
||||
("M3-split-check-transaction", [(BROKER, ATOMIC, ""), (BROKER, REENTER, SPLIT)],
|
||||
["packages/bus/tests/citation-guard.test.mjs"]),
|
||||
("M5-hide-library-unavailable", [("packages/library/src/guard.mjs",
|
||||
"refuse('citation-unavailable', 'The library is busy or unavailable. Retry the answer when the library is available.');",
|
||||
"served = null;")], ["packages/bus/tests/citation-availability.test.mjs"]),
|
||||
("M4-drop-client-reason", [(CLIENT, "new BusError(r.error, typeof r.reason === 'string' ? r.reason.slice(0, 300) : undefined)", "new BusError(r.error)")],
|
||||
["packages/bus/tests/citation-guard.test.mjs"]),
|
||||
]
|
||||
|
||||
archive = subprocess.check_output(["git", "archive", "HEAD", "packages", "roles", "scripts", "tools", "docs", "package.json", "RELEASE"], cwd=ROOT)
|
||||
summary = []
|
||||
for name, changes, tests in MUTANTS:
|
||||
with tempfile.TemporaryDirectory(prefix="row217-mutant-") as directory:
|
||||
work = Path(directory)
|
||||
with tarfile.open(fileobj=io.BytesIO(archive)) as source:
|
||||
source.extractall(work, filter="data")
|
||||
modules = ROOT / "node_modules"
|
||||
if modules.exists():
|
||||
os.symlink(modules.resolve(), work / "node_modules", target_is_directory=True)
|
||||
cmd = ["node", "--test", *tests]
|
||||
with (OUT / f"{name}-control.log").open("w") as log:
|
||||
control = subprocess.run(cmd, cwd=work, stdout=log, stderr=subprocess.STDOUT, timeout=120)
|
||||
if control.returncode:
|
||||
raise SystemExit(f"{name}: control RED, no mutant result claimed")
|
||||
for file, before, after in changes:
|
||||
path = work / file
|
||||
text = path.read_text()
|
||||
if text.count(before) != 1:
|
||||
raise SystemExit(f"{name}: expected exactly one mutation anchor in {file}")
|
||||
path.write_text(text.replace(before, after))
|
||||
with (OUT / f"{name}.log").open("w") as log:
|
||||
mutant = subprocess.run(cmd, cwd=work, stdout=log, stderr=subprocess.STDOUT, timeout=120)
|
||||
if mutant.returncode == 0:
|
||||
raise SystemExit(f"{name}: SURVIVED")
|
||||
summary.append(f"{name}: killed (control green, mutant exit {mutant.returncode})")
|
||||
(OUT / "summary.txt").write_text("\n".join(summary) + "\n")
|
||||
print(summary[-1], flush=True)
|
||||
@@ -1593,3 +1593,5 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-23T17:48:43Z | Scrooge (629b6cd3) | Row 252 Console inbox Approve and Deny (#1753) | Rebased onto 306 landing as 580037105 (range-diff all =), slot run bus 907/0 webui 410/0 cli 943/0, in-review revs 3608/3609, Webby r1 APPROVE, J1-J19 specs to Sage; Sage landed 45ea3fa0d (done rev 3616). Part 1 mutant results were never in BUILD.md; they went to Sage by T3 (scrooge-0252-mutant-specs).
|
||||
2026-10-23 | Sage (1ef1e4f8) | Rows 308 and 252 landings, 571 Connections design amendment, Vikunja 597 | 308 (PRD 1.17, #1815) landed 30e6bb680 and 252 (#1753) landed 45ea3fa0d (replay of 580037105 onto 30e6bb680, range-diff all =), each after a full gate on the landed tree (252 also J1-J19, J11c and J18 equivalent); Gyro's 571 amendment (Fenton APPROVE) landed e8bb13df8; Vikunja 570, 435 and 597 done, canonical webui esbuild 0.28.2; queue revs 3589-3616 committed 59f013247
|
||||
2026-10-23 | Sage (1ef1e4f8) | Vikunja 571 W build rows; rows 13, 42, 257 and 289 check | Filed the design's ten W rows as queue rows 309-318 (revs 3617-3626, owners set to unassigned 3627-3636, queue commit 293b42fee pushed); Vikunja 571 done (comment 994). Row 13's candidate fd72d2689 is on refactor, gate is Monday's row 7 ledger run; 42 paused under mos-1184; 257 post-v1; 289 waits on 214 and 288. Slot released 17:56Z.
|
||||
|
||||
2026-10-23 | Beakley (88b97f65, Codex gpt-6.1-sol) | Row 217 #1718 takeover under Q245 | Registered beakley/row217; read brief/design and standing Sage rulings; awaiting Launchpad ref before implementation. Queue next refused linked worktree; reported to Sage. Untested, no Node/test run pending CLEAR.
|
||||
@@ -5196,7 +5196,7 @@ export class Broker {
|
||||
}
|
||||
// C2-06: the business's library project, the first in file order, or null.
|
||||
#libraryProject(business) {
|
||||
return Object.entries(this.#business(business).projects).find(([, p]) => p.kind === 'library')?.[0] ?? null;
|
||||
return Object.entries(this.#business(business).projects ?? {}).find(([, p]) => p.kind === 'library')?.[0] ?? null;
|
||||
}
|
||||
// C2-06 (design 5.2): every agent session's message to the human in a business with a library project
|
||||
// is guarded, whatever the sender's definition.
|
||||
@@ -5230,7 +5230,7 @@ export class Broker {
|
||||
const Guard = this.#library?.GuardError;
|
||||
throw this.#refused(s, Guard && e instanceof Guard ? new BusError(e.code, e.reason) : e, request);
|
||||
}
|
||||
return this.#reenter(cap, s, request, check);
|
||||
return this.#reenter(cap, s, { verb: 'message.send', args }, check);
|
||||
}
|
||||
// C2-06 (sage-3595, morgana-0200): each path cite of a checked row gains `last`, the file's last commit
|
||||
// at the served tip, one git call per distinct path. Rows without `check` are unchanged.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// E4/sage-3668: real flock contention must not turn a valid cite into an invalid-cite refusal.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { ensureMirror, mirrorPath, SYNC_STATE_FILE, withLibraryLock } from '../../library/src/mirror.mjs';
|
||||
import { guardReply } from '../../library/src/guard.mjs';
|
||||
import { git } from '../../library/tests/helpers.mjs';
|
||||
import { Client } from '../src/client.mjs';
|
||||
import { serve } from '../src/server.mjs';
|
||||
import { citationWorld, citedBody, fixtureCite } from './citation-helpers.mjs';
|
||||
|
||||
test('C2-06 held library lock gives the agent citation-unavailable and its retry reason', { timeout: 25000 }, async t => {
|
||||
const f = citationWorld(t), dataRoot = join(f.root, 'data'); mkdirSync(dataRoot, { mode: 0o700 });
|
||||
const context = { business: f.business, project: 'books', dataRoot };
|
||||
await ensureMirror(context); const mirror = mirrorPath(context);
|
||||
git(f.repo, '-c', 'protocol.file.allow=always', 'push', '-q', mirror, 'HEAD:refs/heads/served');
|
||||
writeFileSync(join(mirror, SYNC_STATE_FILE), JSON.stringify({ version: 1, branch: 'main', served: f.commit,
|
||||
lastFetch: null, lastError: null, diverged: null, problems: [] }), { mode: 0o600 });
|
||||
f.adapter.guard = (ctx, body, opts) => guardReply({ ...ctx, dataRoot }, body, opts);
|
||||
f.call(f.agent, 'role.claim');
|
||||
const socket = join(f.root, 'availability.sock'), server = await serve({ broker: f.broker, path: socket });
|
||||
t.after(() => server.close()); const client = new Client({ path: socket, cap: f.agent, timeout: 15000 });
|
||||
const args = { to: 'human', body: citedBody([fixtureCite(f.commit)]) };
|
||||
await client.call('message.send', args); // Establish that the cite and real mirror are valid.
|
||||
const before = f.store.get('SELECT count(*) n FROM messages').n;
|
||||
await withLibraryLock(mirror, async () => {
|
||||
await assert.rejects(client.call('message.send', args), e => {
|
||||
assert.equal(e.code, 'citation-unavailable');
|
||||
assert.equal(e.reason, 'The library is busy or unavailable. Retry the answer when the library is available.');
|
||||
return true;
|
||||
});
|
||||
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, before);
|
||||
// A fixed fallback carries no library cite and must not need the contested lock.
|
||||
await client.call('message.send', { to: 'human', body: '(no answer passed the citation check)' });
|
||||
});
|
||||
await client.call('message.send', args); // No lock mutation or lost valid cite after release.
|
||||
});
|
||||
|
||||
test('C2-06 malformed sync state is availability failure, never a fabricated invalid cite', async t => {
|
||||
const f = citationWorld(t), dataRoot = join(f.root, 'data'); mkdirSync(dataRoot, { mode: 0o700 });
|
||||
const context = { business: f.business, project: 'books', dataRoot };
|
||||
await ensureMirror(context); const mirror = mirrorPath(context);
|
||||
writeFileSync(join(mirror, SYNC_STATE_FILE), '{broken', { mode: 0o600 });
|
||||
await assert.rejects(guardReply(context, citedBody([fixtureCite(f.commit)]), {
|
||||
limits: f.limits(), definition: 'coder', pages: [],
|
||||
}), { code: 'citation-unavailable' });
|
||||
});
|
||||
|
||||
test('C2-06 no mirror or freshly initialized empty mirror still means no served citation', async t => {
|
||||
const f = citationWorld(t), dataRoot = join(f.root, 'data'); mkdirSync(dataRoot, { mode: 0o700 });
|
||||
const context = { business: f.business, project: 'books', dataRoot };
|
||||
const check = () => guardReply(context, citedBody([fixtureCite(f.commit)]), { limits: f.limits(), definition: 'coder', pages: [] });
|
||||
await assert.rejects(check(), { code: 'citation-invalid' });
|
||||
await ensureMirror(context); await assert.rejects(check(), { code: 'citation-invalid' });
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { connect } from 'node:net';
|
||||
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Client } from '../src/client.mjs';
|
||||
import { serve } from '../src/server.mjs';
|
||||
import { FIXED_TEXTS, GuardError } from '../../library/src/guard.mjs';
|
||||
import { git } from '../../library/tests/helpers.mjs';
|
||||
import { organizationWriter, setReplyLimits } from '../../business/src/organization.mjs';
|
||||
import { businessDoc, rolesCopy, writeJson } from '../../business/tests/helpers.mjs';
|
||||
import { resolveReplyLimits } from '../../library/src/limits.mjs';
|
||||
import { citationWorld, citedBody, fixtureCite, guardCases } from './citation-helpers.mjs';
|
||||
|
||||
for (const definition of ['coder', 'research']) test(`C2-06 ${definition} send_message guard covers every table refusal; inserts nothing`, async t => {
|
||||
const f = citationWorld(t, { definition }); f.call(f.agent, 'role.claim');
|
||||
writeFileSync(f.settings, JSON.stringify({ replyLimits: { requireCitation: true } }));
|
||||
for (const [code, body] of guardCases(f.commit)) {
|
||||
const count = f.store.get('SELECT count(*) n FROM messages').n;
|
||||
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body }), e => e.code === code && e.reason.length <= 300);
|
||||
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, count, code);
|
||||
assert.equal(f.store.all("SELECT * FROM events WHERE json_extract(body,'$.action')='message.checked'").length, 0);
|
||||
}
|
||||
const sent = await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit)]) });
|
||||
const row = f.store.get("SELECT body,subject FROM events WHERE json_extract(body,'$.action')='message.checked'");
|
||||
assert.equal(row.subject, sent.id);
|
||||
assert.deepEqual(JSON.parse(row.body).check, { citations: [{ scope: 'canon', agent: null, file: f.commit }] });
|
||||
});
|
||||
|
||||
test('C2-06 stored check carries the file author trailer at an unrelated cited tip', async t => {
|
||||
const f = citationWorld(t, { definition: 'research' }); f.call(f.agent, 'role.claim');
|
||||
const path = 'Book One/Characters/Mara Vell.md';
|
||||
writeFileSync(join(f.repo, path), readFileSync(join(f.repo, path), 'utf8') + 'A fixture note.\n');
|
||||
git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'Fixture note\n\nMosaic-Agent: acme/scribe');
|
||||
const file = git(f.repo, 'rev-parse', 'HEAD');
|
||||
writeFileSync(join(f.repo, 'Inbox Note.md'), 'Unrelated fixture.\n'); git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'unrelated');
|
||||
const cited = git(f.repo, 'rev-parse', 'HEAD'); f.setServed(cited);
|
||||
assert.notEqual(cited, file);
|
||||
const sent = await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(cited)]) });
|
||||
const event = f.store.get("SELECT body FROM events WHERE subject=? AND json_extract(body,'$.action')='message.checked'", sent.id);
|
||||
assert.deepEqual(JSON.parse(event.body).check, { citations: [{ scope: 'canon', agent: 'acme/scribe', file }] });
|
||||
const [row] = await f.call(f.human, 'thread.messages', { thread: f.thread.id });
|
||||
assert.deepEqual(row.check.citations, [{ scope: 'canon', agent: 'acme/scribe', file, last: file }]);
|
||||
});
|
||||
|
||||
test('C2-06 check evidence and insert are atomic when storing evidence fails', async t => {
|
||||
const f = citationWorld(t); f.call(f.agent, 'role.claim');
|
||||
f.store.run(`CREATE TRIGGER fixture_check_refused BEFORE INSERT ON events
|
||||
WHEN json_extract(NEW.body,'$.action')='message.checked' BEGIN SELECT RAISE(ABORT,'fixture check failure'); END`);
|
||||
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit)]) }));
|
||||
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, 0);
|
||||
});
|
||||
|
||||
test('C2-06 live limits change both directions; unreadable file keeps checks and exact fixed texts', async t => {
|
||||
const f = citationWorld(t); f.call(f.agent, 'role.claim');
|
||||
const send = body => f.call(f.agent, 'message.send', { to: 'human', body });
|
||||
await send('free');
|
||||
writeFileSync(f.settings, JSON.stringify({ replyLimits: { requireCitation: true } }));
|
||||
await assert.rejects(send('free'), { code: 'citation-missing' });
|
||||
writeFileSync(f.settings, JSON.stringify({ replyLimits: { requireCitation: false } })); await send('free');
|
||||
for (const invalid of ['{broken', 'null', '[]']) {
|
||||
writeFileSync(f.settings, invalid);
|
||||
const before = f.store.get('SELECT count(*) n FROM messages').n;
|
||||
await assert.rejects(send('free'), { code: 'citation-missing' });
|
||||
await assert.rejects(send(citedBody(Array(21).fill(fixtureCite(f.commit)))), { code: 'citations-over-limit' });
|
||||
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, before);
|
||||
for (const text of Object.values(FIXED_TEXTS)) await send(text);
|
||||
assert.equal((await f.call(f.human, 'library.status', { project: 'books' })).replyLimits.problem, 'reply-limits-unreadable');
|
||||
}
|
||||
rmSync(f.settings);
|
||||
await assert.rejects(send('free'), { code: 'citation-missing' });
|
||||
});
|
||||
|
||||
test('C2-06 page store is session-local, caps 20 fetches and 200 KiB, drops at disconnect', async t => {
|
||||
const f = citationWorld(t, { definition: 'research' }); f.call(f.agent, 'role.claim');
|
||||
const fetch = async (n, text = `page ${n}`) => f.broker.requestWeb(f.agent, { verb: 'web.fetch', args: { url: `https://fixture.invalid/${n}` } },
|
||||
{ handle: async () => ({ result: { text }, summary: { outcome: 'ok' } }) });
|
||||
const send = (n, quote) => f.call(f.agent, 'message.send', { to: 'human', body: citedBody([{ url: `https://fixture.invalid/${n}`, quote }]) });
|
||||
await fetch(0); await send(0, 'page 0');
|
||||
for (let n = 1; n <= 20; n++) await fetch(n);
|
||||
await assert.rejects(send(0, 'page 0'), { code: 'citation-unfetched' }); await send(1, 'page 1');
|
||||
await fetch(21, 'x'.repeat(200 * 1024) + 'AFTER_CAP');
|
||||
await send(21, 'xxxx'); await assert.rejects(send(21, 'AFTER_CAP'), { code: 'citation-unfetched' });
|
||||
f.broker.disconnect(f.agent);
|
||||
f.broker.endLaunch({ business: 'acme', run: 'coder-run', reason: 'fixture', exitCode: 0 });
|
||||
const replacement = f.broker.bindLaunch({ ...f.launch, run: 'next-run' });
|
||||
f.call(replacement, 'role.claim');
|
||||
await assert.rejects(f.call(replacement, 'message.send', { to: 'human', body: citedBody([{ url: 'https://fixture.invalid/21', quote: 'xxxx' }]) }), { code: 'citation-unfetched' });
|
||||
});
|
||||
|
||||
test('C2-06 transport passes safe capped guard reason; ordinary refusal envelope is byte-identical', async t => {
|
||||
const f = citationWorld(t, { guard: async () => { throw new GuardError('citation-invalid', 'guard reason '.repeat(50)); } });
|
||||
f.call(f.agent, 'role.claim'); const path = join(f.root, 'socket');
|
||||
const server = await serve({ broker: f.broker, path }); t.after(() => server.close());
|
||||
const client = new Client({ path, cap: f.agent });
|
||||
await assert.rejects(client.call('message.send', { to: 'human', body: 'fixture' }), e => e.code === 'citation-invalid' && e.reason === ('guard reason '.repeat(50)).slice(0, 300));
|
||||
const raw = await new Promise((resolve, reject) => {
|
||||
const socket = connect(path); let text = ''; socket.on('error', reject); socket.on('data', b => { text += b; }); socket.on('end', () => resolve(text));
|
||||
socket.on('connect', () => socket.write(JSON.stringify({ cap: f.agent, verb: 'unknown-fixture' }) + '\n'));
|
||||
});
|
||||
assert.equal(raw, '{"ok":false,"error":"unknown-verb"}\n');
|
||||
});
|
||||
|
||||
test('C2-06 non-library businesses retain role.claim shape and unguarded free sends', async t => {
|
||||
const f = citationWorld(t, { library: false });
|
||||
assert.deepEqual(f.call(f.agent, 'role.claim'), { role: 'coder', run: 'coder-run' });
|
||||
assert.ok(f.call(f.agent, 'message.send', { to: 'human', body: 'free' }).id);
|
||||
});
|
||||
|
||||
test('C2-06 thread messages carry file/last evidence with one read per distinct path', async t => {
|
||||
const f = citationWorld(t, { definition: 'research' }); f.call(f.agent, 'role.claim');
|
||||
const path = 'Book One/Characters/Mara Vell.md';
|
||||
await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit), fixtureCite(f.commit)]) });
|
||||
await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit)]) });
|
||||
const rows = await f.call(f.human, 'thread.messages', { thread: f.thread.id });
|
||||
assert.equal(rows.length, 2); assert.deepEqual(f.reads, [path]);
|
||||
for (const row of rows) for (const c of row.check.citations) {
|
||||
assert.equal(c.file, f.commit); assert.equal(c.last, f.commit); assert.equal(c.agent, null);
|
||||
}
|
||||
writeFileSync(join(f.repo, path), 'A changed fixture.\n'); git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'changed fixture');
|
||||
const changed = git(f.repo, 'rev-parse', 'HEAD'); f.setServed(changed); f.reads.length = 0;
|
||||
const newer = await f.call(f.human, 'thread.messages', { thread: f.thread.id });
|
||||
assert.deepEqual(f.reads, [path]); assert.equal(newer[0].check.citations[0].file, f.commit); assert.equal(newer[0].check.citations[0].last, changed);
|
||||
});
|
||||
|
||||
test('C2-06 guarded request snapshot cannot change between check and insert', async t => {
|
||||
let finish;
|
||||
const f = citationWorld(t, { guard: () => new Promise(r => { finish = r; }) }); f.call(f.agent, 'role.claim');
|
||||
const request = { verb: 'message.send', args: { to: 'human', body: 'checked fixture' } };
|
||||
const sent = f.broker.request(f.agent, request); request.args.body = 'unchecked replacement';
|
||||
finish(null); await sent;
|
||||
assert.equal(f.store.get("SELECT body FROM messages WHERE from_role='coder'").body, 'checked fixture');
|
||||
});
|
||||
|
||||
test('C2-06 library.limits.set is human-only and invalid settings leave file and receipts unchanged', async t => {
|
||||
let writer;
|
||||
const f = citationWorld(t, { organization: (id, verb, args, actor) => {
|
||||
assert.equal(verb, 'library.limits.set'); return setReplyLimits(writer, id, args, actor);
|
||||
} });
|
||||
const dir = join(f.root, 'config'), file = writeJson(join(dir, 'businesses/acme.json'), businessDoc(f.root));
|
||||
writer = organizationWriter({ dir, rolesDir: rolesCopy(f.root) });
|
||||
f.adapter.replyLimits = () => resolveReplyLimits(writer.readRaw('acme'));
|
||||
f.call(f.agent, 'role.claim');
|
||||
await f.call(f.human, 'library.limits.set', { revision: writer.read('acme').revision, replyLimits: { requireCitation: true } });
|
||||
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body: 'free' }), { code: 'citation-missing' });
|
||||
const before = readFileSync(file), log = readFileSync(writer.logFile('acme'));
|
||||
const args = { revision: writer.read('acme').revision, replyLimits: { citationsPerReply: 0 } };
|
||||
for (const [cap, code] of [[f.human, 'invalid-request'], [f.agent, 'human-required'], [f.tester, 'tester-refused']]) {
|
||||
assert.throws(() => f.call(cap, 'library.limits.set', args), { code });
|
||||
assert.deepEqual(readFileSync(file), before); assert.deepEqual(readFileSync(writer.logFile('acme')), log);
|
||||
}
|
||||
assert.throws(() => f.call(f.human, 'library.limits.set', { ...args, replyLimits: { invented: true } }), { code: 'invalid-request' });
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// C2-06: real broker, real Git reads and fixture-only library. Reused by runner acceptance tests.
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { Broker } from '../src/broker.mjs';
|
||||
import { checkReply, GuardError, libraryReader } from '../../library/src/guard.mjs';
|
||||
import { THREAD_DEFINITIONS } from '../../business/src/vocabulary.mjs';
|
||||
import { resolveReplyLimits } from '../../library/src/limits.mjs';
|
||||
import { mapping, repo, MARKER } from '../../library/tests/helpers.mjs';
|
||||
|
||||
export const citedBody = citations => JSON.stringify({ text: 'A fixture answer.', citations });
|
||||
export const fixtureCite = (commit, extra = {}) => ({ path: 'Book One/Characters/Mara Vell.md', commit, lines: [9, 9], quote: 'Grey eyes.', ...extra });
|
||||
|
||||
export function citationWorld(t, { definition = 'coder', library = true, guard: overrideGuard, organization = null, sendAuthority = true } = {}) {
|
||||
const r = repo(t), root = r.root, state = join(root, 'bus'); mkdirSync(state, { mode: 0o700 });
|
||||
const store = new Store(state); t.after(() => store.close());
|
||||
const map = mapping(), settings = join(root, 'business.json');
|
||||
writeFileSync(settings, JSON.stringify({ replyLimits: { requireCitation: false, citationsPerReply: 20 } }));
|
||||
const business = { id: 'acme', human: 'jason', vars: { 'bus.tester': true },
|
||||
...(library ? { projects: { books: map } } : {}),
|
||||
arbiters: { technical: 'coder', delivery: 'coder' },
|
||||
roles: { coder: { definition, web: true, authority: { withinRole: sendAuthority ? ['message.send'] : [], crossRole: [] } } } };
|
||||
const limits = () => {
|
||||
try { return resolveReplyLimits({ readable: true, document: JSON.parse(readFileSync(settings, 'utf8')) }); }
|
||||
catch { return resolveReplyLimits({ readable: false }); }
|
||||
};
|
||||
let served = r.commit;
|
||||
const reads = [];
|
||||
const adapter = {
|
||||
GuardError,
|
||||
replyLimits: limits,
|
||||
guard: overrideGuard ?? ((context, body, opts) => checkReply(body, {
|
||||
limits: opts.limits, mapping: map, library: libraryReader(r.repo, served, map),
|
||||
web: { allowed: ['companion', 'research'].includes(opts.definition), pages: opts.pages },
|
||||
})),
|
||||
last: async (context, paths) => {
|
||||
const out = new Map(); for (const path of new Set(paths)) { reads.push(path); out.set(path, await libraryReader(r.repo, served, map).last(path)); } return out;
|
||||
},
|
||||
readFile: async () => ({}), status: async () => ({ project: 'books', served }),
|
||||
};
|
||||
const broker = new Broker({ store, businesses: { acme: business }, library: adapter, organization });
|
||||
const human = broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const project = THREAD_DEFINITIONS.includes(definition)
|
||||
? broker.request(human, { verb: 'project.create', args: { name: 'Citation fixture' } }) : null;
|
||||
const thread = project
|
||||
? broker.request(human, { verb: 'thread.create', args: { project: project.id, name: 'Fixture thread', instance: 'coder' } }) : null;
|
||||
const launch = { business: 'acme', role: 'coder', run: 'coder-run', harness: 'pi', ...(thread ? { thread: thread.thread ?? thread.id } : {}) };
|
||||
const agent = broker.bindLaunch(launch);
|
||||
const tester = broker.bindTester({ business: 'acme' });
|
||||
const call = (cap, verb, args = {}) => broker.request(cap, { verb, args });
|
||||
return { ...r, root, store, business, broker, agent, human, tester, settings, limits, call, adapter, reads, launch, thread, setServed: commit => { served = commit; } };
|
||||
}
|
||||
|
||||
export function guardCases(commit) {
|
||||
const good = fixtureCite(commit);
|
||||
return [
|
||||
['reply-invalid', JSON.stringify({ text: 'x', citations: [], invented: true })],
|
||||
['citation-missing', 'Uncited fixture answer.'],
|
||||
['citations-over-limit', citedBody(Array(21).fill(good))],
|
||||
['citation-invalid', citedBody([{ ...good, quote: 'wrong fixture quote' }])],
|
||||
['playground-cited-as-canon', citedBody([{ ...good, path: 'Book One/Playground/What If.md', lines: [4, 4], quote: MARKER, as: 'canon' }])],
|
||||
['citation-cross-universe', citedBody([good, { ...good, path: 'Second World/Chapters/01.md', lines: [1, 1], quote: MARKER }])],
|
||||
['citation-unfetched', citedBody([{ url: 'https://fixture.invalid/page', quote: 'no fetch' }])],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// E7: exercise startBroker's real live readRaw adapter, rather than replacing the library adapter.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { startBroker } from '../src/runtime.mjs';
|
||||
import { validateBusinessDocument } from '../../business/src/business.mjs';
|
||||
import { businessDoc, rolesCopy, writeJson } from '../../business/tests/helpers.mjs';
|
||||
import { mappingEntry } from '../../library/tests/helpers.mjs';
|
||||
|
||||
test('C2-06 runtime reads changed and unreadable reply limits without restarting', async t => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'citation-runtime-')); t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const doc = businessDoc(root), rolesDir = rolesCopy(root), dir = join(root, 'config'), dataRoot = join(root, 'data');
|
||||
doc.projects.books = mappingEntry(); doc.replyLimits = { requireCitation: false, citationsPerReply: 3 };
|
||||
const file = writeJson(join(dir, 'businesses/acme.json'), doc);
|
||||
const business = validateBusinessDocument(doc, file, { rolesDir }); mkdirSync(dataRoot, { mode: 0o700 });
|
||||
const runtime = await startBroker({ dataRoot, businesses: { acme: business }, organization: { dir, rolesDir } });
|
||||
t.after(() => runtime.close());
|
||||
const human = runtime.broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const status = () => runtime.broker.request(human, { verb: 'library.status', args: { project: 'books' } });
|
||||
assert.deepEqual((await status()).replyLimits, { requireCitation: false, citationsPerReply: 3, fallback: [], problem: null });
|
||||
doc.replyLimits = { requireCitation: true, citationsPerReply: 7 }; writeJson(file, doc);
|
||||
assert.deepEqual((await status()).replyLimits, { requireCitation: true, citationsPerReply: 7, fallback: [], problem: null });
|
||||
writeFileSync(file, '{broken');
|
||||
assert.deepEqual((await status()).replyLimits, { requireCitation: true, citationsPerReply: 20, fallback: [], problem: 'reply-limits-unreadable' });
|
||||
rmSync(file);
|
||||
assert.equal((await status()).replyLimits.problem, 'reply-limits-unreadable');
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { organizationWriter, setReplyLimits } from '../src/organization.mjs';
|
||||
import { validateBusinessDocument } from '../src/business.mjs';
|
||||
import { businessDoc, rolesCopy, tmp, writeJson } from './helpers.mjs';
|
||||
|
||||
function fixture(t, human = 'jason') {
|
||||
const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
const doc = businessDoc(root); doc.human = human; doc.replyLimits = { requireCitation: false, citationsPerReply: 20 };
|
||||
const dir = join(root, 'config'), file = writeJson(join(dir, 'businesses/acme.json'), doc), rolesDir = rolesCopy(root);
|
||||
const writer = organizationWriter({ dir, rolesDir });
|
||||
const next = structuredClone(doc); next.vars['tracker.pollSeconds'] = 61;
|
||||
writer.write('acme', next, { revision: writer.read('acme').revision, actor: human });
|
||||
return { root, file, writer, rolesDir, log: writer.logFile('acme') };
|
||||
}
|
||||
|
||||
function refused(f, actor, change) {
|
||||
const before = readFileSync(f.file), log = readFileSync(f.log), { document, revision } = f.writer.read('acme');
|
||||
change(document);
|
||||
assert.throws(() => f.writer.write('acme', document, { revision, actor }), { code: 'organization-reply-limits-human-only' });
|
||||
assert.deepEqual(readFileSync(f.file), before); assert.deepEqual(readFileSync(f.log), log);
|
||||
}
|
||||
|
||||
test('C2-06 M11 human replacement works; omitted settings return to defaults', t => {
|
||||
const f = fixture(t);
|
||||
setReplyLimits(f.writer, 'acme', { revision: f.writer.read('acme').revision, replyLimits: { requireCitation: true } }, 'jason');
|
||||
assert.deepEqual(f.writer.read('acme').document.replyLimits, { requireCitation: true });
|
||||
setReplyLimits(f.writer, 'acme', { revision: f.writer.read('acme').revision, replyLimits: {} }, 'jason');
|
||||
assert.deepEqual(f.writer.read('acme').document.replyLimits, {});
|
||||
});
|
||||
|
||||
test('C2-06 M11 agents and tester cannot change settings, including by changing human', t => {
|
||||
const f = fixture(t);
|
||||
for (const actor of ['chief-of-staff.chief', 'tester', 'coder.coder'])
|
||||
refused(f, actor, d => { d.replyLimits.requireCitation = true; });
|
||||
refused(f, 'chief-of-staff.chief', d => { d.human = 'chief-of-staff.chief'; d.replyLimits.requireCitation = true; });
|
||||
const dotted = fixture(t, 'coder.coder');
|
||||
refused(dotted, 'coder.coder', d => { d.replyLimits.citationsPerReply = 1; });
|
||||
});
|
||||
|
||||
test('C2-06 M11 serialization cannot replace settings after the first fence', t => {
|
||||
const f = fixture(t);
|
||||
refused(f, 'chief-of-staff.chief', d => {
|
||||
Object.defineProperty(d.replyLimits, 'toJSON', { value: () => ({ requireCitation: true }) });
|
||||
});
|
||||
});
|
||||
|
||||
// E1: unlike tester/role actors, mallory passes the other actor-shape exclusions.
|
||||
// Comparing document.human instead of old.human must fail this case.
|
||||
test('C2-06 M11 an unrelated actor cannot appoint itself human and change reply limits', t => {
|
||||
const f = fixture(t);
|
||||
refused(f, 'mallory', d => { d.human = 'mallory'; d.replyLimits.requireCitation = true; });
|
||||
});
|
||||
|
||||
test('C2-06 invalid setter values preserve the business and changes.jsonl', t => {
|
||||
const f = fixture(t), before = readFileSync(f.file), log = readFileSync(f.log);
|
||||
for (const replyLimits of [{ citationsPerReply: 0 }, { citationsPerReply: 1001 }, { requireCitation: 'true' }, { invented: true }, []]) {
|
||||
assert.throws(() => setReplyLimits(f.writer, 'acme', { revision: f.writer.read('acme').revision, replyLimits }, 'jason'), { code: 'invalid-request' });
|
||||
assert.deepEqual(readFileSync(f.file), before); assert.deepEqual(readFileSync(f.log), log);
|
||||
}
|
||||
});
|
||||
|
||||
test('C2-06 validator accepts hand-edited settings; raw reader fails closed on invalid files', t => {
|
||||
const f = fixture(t), doc = f.writer.read('acme').document;
|
||||
for (const replyLimits of ['true', 1, null, [true], {}, { requireCitation: 'true' }, { citationsPerReply: 0 }]) {
|
||||
const next = { ...doc, replyLimits };
|
||||
assert.doesNotThrow(() => validateBusinessDocument(next, f.file, { rolesDir: f.rolesDir }));
|
||||
writeJson(f.file, next); assert.deepEqual(f.writer.readRaw('acme').document.replyLimits, replyLimits);
|
||||
}
|
||||
for (const bad of ['{broken', 'null', '[]', '1']) {
|
||||
writeFileSync(f.file, bad); assert.deepEqual(f.writer.readRaw('acme'), { readable: false });
|
||||
}
|
||||
rmSync(f.file); assert.deepEqual(f.writer.readRaw('acme'), { readable: false });
|
||||
});
|
||||
@@ -959,6 +959,12 @@ writer adds `since`, the time the limit was set, and keeps it on later saves.
|
||||
A patch that sends a different `since` is refused. Only these paths are token
|
||||
limits; the agent limits above are never read as one, whatever their names.
|
||||
|
||||
`mosaic limits replies --file LIMITS [--business ID]` replaces the citation settings
|
||||
through the human-only `library.limits.set` verb. LIMITS is a JSON object with optional
|
||||
`requireCitation` (boolean, default false) and `citationsPerReply` (integer 1–1000,
|
||||
default 20). Omitted keys use their defaults; unknown keys and invalid values refuse.
|
||||
The next library reply uses the new settings without restarting the bus.
|
||||
|
||||
`mosaic limits reset WORD... [--business ID]` moves one token limit's `since`
|
||||
to now, so its count restarts. The path is one word per level, as the plain
|
||||
output prints it: `mosaic limits reset tokens models anthropic claude-opus-5-5`.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { limitsCommand } from '../src/limits.mjs';
|
||||
|
||||
test('C2-06 CLI replies reads revision and sends only the JSON settings to the human verb', async t => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'reply-cli-')); t.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
const file = join(dir, 'settings.json'); writeFileSync(file, JSON.stringify({ requireCitation: true, citationsPerReply: 7 }));
|
||||
const calls = [], output = [];
|
||||
await limitsCommand(['replies', '--file', file, '--business', 'acme'], { stdout: { write: s => output.push(s) } }, parsed => {
|
||||
assert.equal(parsed.values['--business'], 'acme');
|
||||
return async (verb, args) => { calls.push([verb, args]); return verb === 'limits' ? { revision: 'r1' } : { revision: 'r2' }; };
|
||||
});
|
||||
assert.deepEqual(calls, [['limits', {}], ['library.limits.set', { revision: 'r1', replyLimits: { requireCitation: true, citationsPerReply: 7 } }]]);
|
||||
assert.match(output.join(''), /r2/);
|
||||
});
|
||||
|
||||
test('C2-06 CLI replies requires a file and refuses tester writes before a bus call', async () => {
|
||||
for (const args of [['replies'], ['replies', '--tester', '--file', 'unused'], ['replies', '--file', 'unused', '--models', 'unused']])
|
||||
await assert.rejects(limitsCommand(args, { stdout: { write() {} } }, () => { throw Error('bus touched'); }), { exitCode: 4 });
|
||||
});
|
||||
@@ -38,6 +38,7 @@ import { createServer, connect } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { Client } from "../../bus/src/client.mjs";
|
||||
import { GUARD_CODES, FIXED_TEXTS } from "../../library/src/reply-protocol.mjs";
|
||||
import { fetchReview, landWork, pushWorking } from "./git-tools.mjs";
|
||||
|
||||
export const EXIT = Object.freeze({ stopped: 0, founder: 20, claim: 21, session: 22, broker: 23, usage: 2 });
|
||||
@@ -458,8 +459,9 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||
return EXIT.founder;
|
||||
}
|
||||
const client = new Client({ path: session.brokerSocket, cap, timeout: 30_000 });
|
||||
let library = false;
|
||||
try {
|
||||
await client.call("role.claim");
|
||||
library = (await client.call("role.claim"))?.library === true;
|
||||
} catch (e) {
|
||||
// No answer is not a refusal: the broker is down or the outcome is unknown.
|
||||
if (e.code === "outcome-unknown") {
|
||||
@@ -582,9 +584,60 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
|
||||
if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`);
|
||||
try {
|
||||
if (m.class !== "RESULT") {
|
||||
const body = turn.ok ? turn.text || "(the session gave no answer)" : `The turn failed: ${turn.reason}`;
|
||||
await client.call("message.send", { to: m.from_role, body: clip(body) + unslop.note(), class: "RESULT", in_reply_to: m.id });
|
||||
unslop.sent();
|
||||
if (!library) {
|
||||
const body = turn.ok ? turn.text || "(the session gave no answer)" : `The turn failed: ${turn.reason}`;
|
||||
await client.call("message.send", { to: m.from_role, body: clip(body) + unslop.note(), class: "RESULT", in_reply_to: m.id });
|
||||
unslop.sent();
|
||||
} else {
|
||||
const send = async (answer) => {
|
||||
const raw = answer.ok ? answer.text || FIXED_TEXTS.empty : FIXED_TEXTS.failed;
|
||||
const fixed = Object.values(FIXED_TEXTS).includes(raw);
|
||||
let body = raw;
|
||||
if (!fixed) {
|
||||
let value;
|
||||
try { value = JSON.parse(raw); } catch {}
|
||||
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
||||
// Malformed objects still reach the guard and its reply-invalid refusal.
|
||||
if (typeof value.text === "string") value.text += unslop.note();
|
||||
body = JSON.stringify(value);
|
||||
} else body = clip(raw) + unslop.note();
|
||||
}
|
||||
try {
|
||||
await client.call("message.send", { to: m.from_role, body, class: "RESULT", in_reply_to: m.id });
|
||||
if (!fixed) unslop.sent();
|
||||
} catch (error) {
|
||||
// The existing broker body cap is smaller than the socket's 64 KiB cap.
|
||||
// Both follow the one-retry rule, without changing non-library sends.
|
||||
if (error.code === "invalid-request" && body.length > 32768) {
|
||||
error = Object.assign(new Error("request-too-large"), { code: "request-too-large" });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
try { await send(turn); }
|
||||
catch (error) {
|
||||
if (!GUARD_CODES.includes(error.code) && error.code !== "request-too-large") throw error;
|
||||
log(`runner: reply to ${m.id} refused: ${error.code}${error.reason ? `: ${error.reason}` : ""}`);
|
||||
const reason = error.code === "request-too-large"
|
||||
? "The message exceeds the 64 KiB request limit or the 32768-character body limit. Send a shorter answer."
|
||||
: error.reason ?? "The reply did not pass the citation check.";
|
||||
++n;
|
||||
try { await client.call("usage.begin", { turn: `${m.id}:retry` }); }
|
||||
catch (e) { log(`runner: usage baseline unavailable: ${e.code ?? "failed"}`); }
|
||||
const retry = await runTurn({ session, request: `${block ?? ""}\n${turnRequest(m)}\n\nYour answer was refused with ${error.code}: ${reason}\nReply again in this same session.`, n, env: turnEnv, onChild: (c) => (child = c) });
|
||||
unslop.read();
|
||||
try { await client.call("usage.end", { turn: `${m.id}:retry` }); }
|
||||
catch (e) { log(`runner: usage outcome unavailable: ${e.code ?? "failed"}`); }
|
||||
if (stopping) break loop;
|
||||
if (!retry.ok) log(`runner: retry turn ${n} for message ${m.id} failed: ${retry.reason}`);
|
||||
try { await send(retry); }
|
||||
catch (again) {
|
||||
if (!GUARD_CODES.includes(again.code) && again.code !== "request-too-large") throw again;
|
||||
log(`runner: retry to ${m.id} refused: ${again.code}`);
|
||||
await send({ ok: true, text: again.code === "request-too-large" ? FIXED_TEXTS.tooLong : FIXED_TEXTS.uncited });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await client.call("message.read", { id: m.id });
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { serve } from '../../bus/src/server.mjs';
|
||||
import { FIXED_TEXTS } from '../../library/src/reply-protocol.mjs';
|
||||
import { GuardError } from '../../library/src/guard.mjs';
|
||||
import { citationWorld, citedBody, fixtureCite, guardCases } from '../../bus/tests/citation-helpers.mjs';
|
||||
|
||||
const RUNNER = new URL('../src/runner.mjs', import.meta.url).pathname;
|
||||
const ADAPTER = new URL('./fixtures/citation-adapter.mjs', import.meta.url).pathname;
|
||||
async function until(fn) {
|
||||
const stop = Date.now() + 15000;
|
||||
while (Date.now() < stop) { const value = fn(); if (value) return value; await new Promise(r => setTimeout(r, 15)); }
|
||||
throw Error('fixture timed out');
|
||||
}
|
||||
|
||||
async function setup(t, stepsFor, { library = true, requireCitation = true, sendAuthority = true, guard } = {}) {
|
||||
const f = citationWorld(t, { library, sendAuthority, guard }), runDir = join(f.root, 'run'), bundle = join(runDir, 'bundle');
|
||||
mkdirSync(bundle, { recursive: true }); mkdirSync(join(f.root, 'ws'));
|
||||
const steps = stepsFor(f.commit), brokerSocket = join(f.root, 'broker.sock');
|
||||
writeFileSync(f.settings, JSON.stringify({ replyLimits: { requireCitation } }));
|
||||
const server = await serve({ broker: f.broker, path: brokerSocket });
|
||||
writeFileSync(join(runDir, 'fixture-steps.json'), JSON.stringify(steps));
|
||||
const adapter = join(runDir, 'adapter.sh'); writeFileSync(adapter, `exec '${process.execPath}' '${ADAPTER}'\n`);
|
||||
writeFileSync(join(bundle, 'prompt.md'), 'Fixture only.\n');
|
||||
writeFileSync(join(bundle, 'tools.json'), JSON.stringify([{ name: 'send_message', verb: 'message.send', action: 'message.send' }]));
|
||||
writeFileSync(join(bundle, 'policy.json'), JSON.stringify({ needs: [], credentials: [] }));
|
||||
writeFileSync(join(runDir, 'session.json'), JSON.stringify({ business: 'acme', instance: 'coder', run: 'coder-run', harness: 'pi',
|
||||
provider: 'fixture', model: 'fixture', brokerSocket, workspace: join(f.root, 'ws'), sessionDir: join(runDir, 'session'), adapter,
|
||||
bundle: { prompt: join(bundle, 'prompt.md'), tools: join(bundle, 'tools.json'), policy: join(bundle, 'policy.json') },
|
||||
toolSocket: join(runDir, 'tools.sock'), turnMarker: join(runDir, 'turn.done'), turnEnd: 'marker', harnessEnv: {}, pollInterval: 20, turnTimeout: 10 }));
|
||||
const child = spawn(process.execPath, [RUNNER, runDir], { stdio: ['pipe', 'ignore', 'pipe'], env: { PATH: process.env.PATH } });
|
||||
let log = ''; child.stderr.on('data', b => { log += b; }); child.stdin.on('error', () => {}); child.stdin.end(JSON.stringify({ cap: f.agent }) + '\n');
|
||||
const exited = once(child, 'exit');
|
||||
t.after(async () => { child.kill('SIGTERM'); await exited; await server.close(); });
|
||||
await until(() => f.store.get("SELECT 1 FROM role_claims WHERE role='coder' AND op='claim'"));
|
||||
const ask = async () => {
|
||||
const sent = await f.call(f.human, 'message.send', { to: 'coder', body: 'Fixture question.', class: 'REQUEST' });
|
||||
const reply = await until(() => f.store.get('SELECT * FROM messages WHERE in_reply_to=?', sent.id));
|
||||
await until(() => f.store.get("SELECT 1 FROM deliveries WHERE message=? AND op='read'", sent.id));
|
||||
return reply;
|
||||
};
|
||||
return { ...f, runDir, ask, log: () => log, count: () => Number(readFileSync(join(runDir, 'fixture-count'), 'utf8')) };
|
||||
}
|
||||
|
||||
for (const code of ['reply-invalid', 'citation-missing', 'citations-over-limit', 'citation-invalid', 'playground-cited-as-canon', 'citation-cross-universe', 'citation-unfetched']) {
|
||||
test(`C2-06 RESULT ${code} retries once, delivers only the valid answer and names reason`, async t => {
|
||||
const f = await setup(t, commit => [{ text: guardCases(commit).find(c => c[0] === code)[1] }, { text: citedBody([fixtureCite(commit)]) }]);
|
||||
const reply = await f.ask(); assert.equal(reply.body, citedBody([fixtureCite(f.commit)])); assert.equal(f.count(), 2);
|
||||
assert.equal(f.store.get("SELECT count(*) n FROM messages WHERE from_role='coder'").n, 1);
|
||||
const requests = readFileSync(join(f.runDir, 'fixture-requests.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
||||
assert.match(requests[1], new RegExp(code)); assert.match(requests[1], /refused with/);
|
||||
assert.match(f.log(), new RegExp(code));
|
||||
});
|
||||
}
|
||||
|
||||
test('C2-06 RESULT two guard refusals use the citation fixed text and exactly one retry', async t => {
|
||||
const f = await setup(t, () => [{ text: 'uncited' }, { text: 'uncited again' }]);
|
||||
assert.equal((await f.ask()).body, FIXED_TEXTS.uncited); assert.equal(f.count(), 2);
|
||||
});
|
||||
|
||||
for (const size of [34000, 70000]) {
|
||||
test(`C2-06 RESULT JSON ${size} characters retries with a shorter valid answer`, async t => {
|
||||
const f = await setup(t, commit => [{ text: JSON.stringify({ text: 'x'.repeat(size), citations: [fixtureCite(commit)] }) }, { text: citedBody([fixtureCite(commit)]) }]);
|
||||
assert.equal((await f.ask()).body, citedBody([fixtureCite(f.commit)])); assert.equal(f.count(), 2);
|
||||
assert.match(readFileSync(join(f.runDir, 'fixture-requests.jsonl'), 'utf8'), /64 KiB/);
|
||||
});
|
||||
}
|
||||
|
||||
test('C2-06 RESULT two oversized JSON bodies use the length fixed text with citations required', async t => {
|
||||
const f = await setup(t, commit => Array(2).fill({ text: JSON.stringify({ text: 'x'.repeat(70000), citations: [fixtureCite(commit)] }) }));
|
||||
assert.equal((await f.ask()).body, FIXED_TEXTS.tooLong); assert.equal(f.count(), 2);
|
||||
});
|
||||
|
||||
test('C2-06 multibyte plain bodies also receive the one request-size retry', async t => {
|
||||
const f = await setup(t, commit => [{ text: '界'.repeat(32000) }, { text: citedBody([fixtureCite(commit)]) }]);
|
||||
assert.equal((await f.ask()).body, citedBody([fixtureCite(f.commit)])); assert.equal(f.count(), 2);
|
||||
});
|
||||
|
||||
test('C2-06 two multibyte plain bodies use the length fallback', async t => {
|
||||
const f = await setup(t, () => Array(2).fill({ text: '界'.repeat(32000) }));
|
||||
assert.equal((await f.ask()).body, FIXED_TEXTS.tooLong); assert.equal(f.count(), 2);
|
||||
});
|
||||
|
||||
test('C2-06 RESULT a length refusal then citation refusal shares one retry and citation fallback', async t => {
|
||||
const f = await setup(t, commit => [{ text: JSON.stringify({ text: 'x'.repeat(70000), citations: [fixtureCite(commit)] }) }, { text: 'uncited' }]);
|
||||
assert.equal((await f.ask()).body, FIXED_TEXTS.uncited); assert.equal(f.count(), 2);
|
||||
});
|
||||
|
||||
test('C2-06 fixed empty/failed replies defer unslop note until the next cited reply', async t => {
|
||||
const f = await setup(t, commit => [{ text: '', unslop: true }, { fail: true }, { text: citedBody([fixtureCite(commit)]) }]);
|
||||
assert.equal((await f.ask()).body, FIXED_TEXTS.empty); assert.equal((await f.ask()).body, FIXED_TEXTS.failed);
|
||||
const reply = JSON.parse((await f.ask()).body);
|
||||
assert.deepEqual(reply.citations, [fixtureCite(f.commit)]); assert.match(reply.text, /unslop/i); assert.equal(f.count(), 3);
|
||||
});
|
||||
|
||||
test('C2-06 cited JSON above the old 32000 clip retains its citations and text', async t => {
|
||||
const f = await setup(t, commit => [{ text: JSON.stringify({ text: 'x'.repeat(32200), citations: [fixtureCite(commit)] }) }]);
|
||||
const reply = JSON.parse((await f.ask()).body); assert.equal(reply.text.length, 32200); assert.deepEqual(reply.citations, [fixtureCite(f.commit)]); assert.equal(f.count(), 1);
|
||||
});
|
||||
|
||||
test('C2-06 a flagged cited answer keeps the note inside text and citations whole', async t => {
|
||||
const f = await setup(t, commit => [{ text: citedBody([fixtureCite(commit)]), unslop: true }]);
|
||||
const reply = JSON.parse((await f.ask()).body);
|
||||
assert.deepEqual(reply.citations, [fixtureCite(f.commit)]); assert.match(reply.text, /unslop/i); assert.equal(f.count(), 1);
|
||||
});
|
||||
|
||||
test('C2-06 send_message typed tool runs every guard code before the valid RESULT', async t => {
|
||||
const f = await setup(t, commit => [{ text: citedBody([fixtureCite(commit)]), tools: guardCases(commit).map(([, body]) => ({ to: 'human', body })) }]);
|
||||
await f.ask(); const results = readFileSync(join(f.runDir, 'fixture-tools.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
||||
assert.equal(results.length, 7); results.forEach((r, i) => { assert.equal(r.ok, false); assert.match(r.error, new RegExp(guardCases(f.commit)[i][0])); });
|
||||
assert.equal(f.store.get("SELECT count(*) n FROM messages WHERE from_role='coder'").n, 1);
|
||||
});
|
||||
|
||||
test('C2-06 outside library the old failed text, clip and unslop behavior hold', async t => {
|
||||
const f = await setup(t, () => [{ fail: true }, { text: 'x'.repeat(40000), unslop: true }], { library: false });
|
||||
assert.match((await f.ask()).body, /^The turn failed:/);
|
||||
const reply = (await f.ask()).body; assert.match(reply, /answer cut at 32000 characters/); assert.match(reply, /unslop/i); assert.equal(f.count(), 2);
|
||||
});
|
||||
|
||||
test('C2-06 outside library a refused send keeps the existing no-retry behavior', async t => {
|
||||
const f = await setup(t, () => [{ text: 'answer without send authority' }], { library: false, sendAuthority: false });
|
||||
await f.call(f.human, 'message.send', { to: 'coder', body: 'Fixture question.', class: 'REQUEST' });
|
||||
await until(() => /runner: reply to .* failed:/.test(f.log()));
|
||||
assert.equal(f.count(), 1); assert.equal(f.store.get("SELECT count(*) n FROM messages WHERE from_role='coder'").n, 0);
|
||||
assert.equal(existsSync(join(f.runDir, 'fixture-requests.jsonl')), true);
|
||||
});
|
||||
|
||||
|
||||
// E4: the agent must receive the availability reason in its actual retry turn,
|
||||
// rather than being told only that the source citation was invalid.
|
||||
test('C2-06 the retry turn delivers citation-unavailable reason to the agent', async t => {
|
||||
let sends = 0;
|
||||
const reason = 'The library is busy or unavailable. Retry the answer when the library is available.';
|
||||
const f = await setup(t, commit => [{ text: citedBody([fixtureCite(commit)]) }, { text: citedBody([fixtureCite(commit)]) }], {
|
||||
guard: async () => { if (sends++ === 0) throw new GuardError('citation-unavailable', reason); return null; },
|
||||
});
|
||||
await f.ask(); assert.equal(f.count(), 2);
|
||||
const requests = readFileSync(join(f.runDir, 'fixture-requests.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
||||
assert.equal(requests[1].includes(`citation-unavailable: ${reason}`), true);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// A fixture-only scripted harness. No provider or real credential is used.
|
||||
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { callTool } from '../../src/tools.mjs';
|
||||
|
||||
const root = dirname(process.env.MOSAIC_TURN_MARKER), countFile = join(root, 'fixture-count');
|
||||
const n = existsSync(countFile) ? Number(readFileSync(countFile, 'utf8')) : 0;
|
||||
const steps = JSON.parse(readFileSync(join(root, 'fixture-steps.json'), 'utf8'));
|
||||
const step = steps[n] ?? { fail: true };
|
||||
writeFileSync(countFile, String(n + 1));
|
||||
appendFileSync(join(root, 'fixture-requests.jsonl'), JSON.stringify(process.env.MOSAIC_REQUEST) + '\n');
|
||||
if (step.unslop) appendFileSync(process.env.MOSAIC_UNSLOP_LOG, JSON.stringify({ ev: 'flagged', findings: [{ source: 'fixture', id: 'fixture' }] }) + '\n');
|
||||
for (const args of step.tools ?? []) {
|
||||
let result;
|
||||
try { result = { ok: true, result: await callTool(process.env.MOSAIC_TOOL_SOCKET, 'send_message', args) }; }
|
||||
catch (e) { result = { ok: false, error: e.message }; }
|
||||
appendFileSync(join(root, 'fixture-tools.jsonl'), JSON.stringify(result) + '\n');
|
||||
}
|
||||
if (step.fail) process.exit(3);
|
||||
process.stdout.write(step.text ?? '');
|
||||
writeFileSync(process.env.MOSAIC_TURN_MARKER, 'done\n');
|
||||
@@ -8,17 +8,8 @@ import { LibraryError } from './problems.mjs';
|
||||
import { mirrorPath, readSyncState } from './mirror.mjs';
|
||||
import { plain } from './limits.mjs';
|
||||
|
||||
export const GUARD_CODES = Object.freeze(['reply-invalid', 'citation-missing', 'citations-over-limit', 'citation-invalid',
|
||||
'playground-cited-as-canon', 'citation-cross-universe', 'citation-unfetched']);
|
||||
|
||||
// The runner's four fixed texts in a library business. The guard accepts exactly these bodies when
|
||||
// requireCitation is true; an agent that sends one gains nothing it couldn't say anyway.
|
||||
export const FIXED_TEXTS = Object.freeze({
|
||||
empty: '(the session gave no answer)',
|
||||
failed: '(the turn failed)',
|
||||
uncited: '(no answer passed the citation check)',
|
||||
tooLong: '(the answer was too long to send)',
|
||||
});
|
||||
import { FIXED_TEXTS } from './reply-protocol.mjs';
|
||||
export { GUARD_CODES, FIXED_TEXTS } from './reply-protocol.mjs';
|
||||
|
||||
// Web cites are for these definitions only (design 5.2).
|
||||
export const WEB_CITE_DEFINITIONS = Object.freeze(['research', 'companion']);
|
||||
@@ -107,9 +98,9 @@ export async function checkReply(body, { limits, mapping, library, web }) {
|
||||
if (!Object.hasOwn(c, 'url')) return;
|
||||
if (!web.allowed) refuse('citation-unfetched', `citation ${i + 1}: only research and companion cite a url`);
|
||||
const quote = squash(c.quote);
|
||||
const page = web.pages.find(p => p.url === c.url || p.finalUrl === c.url);
|
||||
if (!page) refuse('citation-unfetched', `citation ${i + 1}: this session fetched no such url through the bus`);
|
||||
if (!quote || !squash(page.text).includes(quote)) refuse('citation-unfetched', `citation ${i + 1}: the quote isn't in the url's kept text`);
|
||||
const pages = web.pages.filter(p => p.url === c.url || p.finalUrl === c.url);
|
||||
if (!pages.length) refuse('citation-unfetched', `citation ${i + 1}: this session fetched no such url through the bus`);
|
||||
if (!quote || !pages.some(page => squash(page.text).includes(quote))) refuse('citation-unfetched', `citation ${i + 1}: the quote isn't in the url's kept text`);
|
||||
});
|
||||
const out = [];
|
||||
for (const [i, c] of citations.entries())
|
||||
@@ -150,7 +141,7 @@ export function libraryReader(mirror, served, mapping) {
|
||||
async origin(commit, path) {
|
||||
if (!mirror) return { agent: null, file: null };
|
||||
try {
|
||||
const [file, trailer = ''] = (await git(mirror, ['log', '-1', '--format=%H%x00%(trailers:key=Mosaic-Agent,valueonly)', commit, '--', path])).split('\0');
|
||||
const [file, trailer = ''] = (await git(mirror, ['--literal-pathspecs', 'log', '-1', '--format=%H%x00%(trailers:key=Mosaic-Agent,valueonly)', commit, '--', path])).split('\0');
|
||||
return { agent: trailer.trim() || null, file: SHA.test(file) ? file : null };
|
||||
} catch { return { agent: null, file: null }; }
|
||||
},
|
||||
@@ -158,7 +149,7 @@ export function libraryReader(mirror, served, mapping) {
|
||||
async last(path) {
|
||||
if (!mirror || !served) return null;
|
||||
try {
|
||||
const value = await git(mirror, ['log', '-1', '--format=%H', served, '--', path]);
|
||||
const value = await git(mirror, ['--literal-pathspecs', 'log', '-1', '--format=%H', served, '--', path]);
|
||||
return SHA.test(value) ? value : null;
|
||||
} catch { return null; }
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// C2-06: shared pure protocol constants for the host guard and sandboxed runner.
|
||||
export const GUARD_CODES = Object.freeze(['reply-invalid', 'citation-missing', 'citations-over-limit', 'citation-invalid',
|
||||
'playground-cited-as-canon', 'citation-cross-universe', 'citation-unfetched']);
|
||||
|
||||
// The runner's four fixed texts in a library business. The guard accepts exactly these bodies when
|
||||
// requireCitation is true; an agent that sends one gains nothing it couldn't say anyway.
|
||||
export const FIXED_TEXTS = Object.freeze({
|
||||
empty: '(the session gave no answer)',
|
||||
failed: '(the turn failed)',
|
||||
uncited: '(no answer passed the citation check)',
|
||||
tooLong: '(the answer was too long to send)',
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { checkReply, GuardError, libraryReader, parseReply, FIXED_TEXTS } from '../src/guard.mjs';
|
||||
import { resolveReplyLimits, checkReplyLimits } from '../src/limits.mjs';
|
||||
import { BASE, MARKER, mapping, repo, git } from './helpers.mjs';
|
||||
|
||||
const defaults = resolveReplyLimits({ readable: true, document: {} });
|
||||
const body = citations => JSON.stringify({ text: 'A fixture answer.', citations });
|
||||
const cite = (commit, extra = {}) => ({ path: 'Book One/Characters/Mara Vell.md', commit, lines: [9, 9], quote: 'Grey eyes.', ...extra });
|
||||
const context = r => ({ limits: defaults, mapping: mapping(), library: libraryReader(r.repo, r.commit, mapping()), web: { allowed: true, pages: [] } });
|
||||
const refusal = code => e => e instanceof GuardError && e.code === code && typeof e.reason === 'string' && e.reason.length <= 300;
|
||||
|
||||
test('C2-06 reply schema: free bodies, exact objects and safe refusal reasons', async t => {
|
||||
const c = context(repo(t));
|
||||
for (const text of ['plain', 'null', '42', '[]', '"text"', '{broken']) {
|
||||
assert.equal(parseReply(text).cited, false);
|
||||
assert.equal(await checkReply(text, c), null);
|
||||
}
|
||||
assert.deepEqual(await checkReply(body([]), c), { citations: [] });
|
||||
for (const value of [{}, { text: 'x', citations: [], scope: 'canon' }, { text: 1, citations: [] },
|
||||
{ text: 'x', citations: {} }, { text: 'x', citations: [{ url: 'https://invalid.test', quote: 'x', agent: 'forged' }] },
|
||||
{ text: 'x', citations: [cite('a'.repeat(40), { as: 'supporting' })] }])
|
||||
await assert.rejects(checkReply(JSON.stringify(value), c), refusal('reply-invalid'));
|
||||
});
|
||||
|
||||
test('C2-06 citation setting and all four exact fixed texts', async t => {
|
||||
const c = context(repo(t)); c.limits = { ...defaults, requireCitation: true };
|
||||
await assert.rejects(checkReply('plain', c), refusal('citation-missing'));
|
||||
await assert.rejects(checkReply(body([]), c), refusal('citation-missing'));
|
||||
for (const text of Object.values(FIXED_TEXTS)) {
|
||||
assert.equal(await checkReply(text, c), null);
|
||||
await assert.rejects(checkReply(text + ' ', c), refusal('citation-missing'));
|
||||
}
|
||||
});
|
||||
|
||||
test('C2-06 default citation cap passes 20 and refuses 21, even with optional citations', async t => {
|
||||
const r = repo(t), c = context(r);
|
||||
assert.equal((await checkReply(body(Array(20).fill(cite(r.commit))), c)).citations.length, 20);
|
||||
await assert.rejects(checkReply(body(Array(21).fill(cite(r.commit))), c), refusal('citations-over-limit'));
|
||||
});
|
||||
|
||||
test('C2-06 real Git citation scope, reachability, lines and normalized quotes', async t => {
|
||||
const r = repo(t), c = context(r);
|
||||
assert.equal((await checkReply(body([cite(r.commit, { quote: 'Grey\n eyes.' })]), c)).citations[0].scope, 'canon');
|
||||
for (const extra of [{ path: 'Inbox Note.md', lines: [1, 1], quote: MARKER },
|
||||
{ path: 'Book One/Board.canvas' }, { path: 'Book One/Characters/Missing.md' },
|
||||
{ commit: '0'.repeat(40) }, { commit: git(r.repo, 'rev-parse', 'HEAD^{tree}') },
|
||||
{ lines: [999, 999] }, { quote: 'not in her fixture' }])
|
||||
await assert.rejects(checkReply(body([cite(r.commit, extra)]), c), refusal('citation-invalid'));
|
||||
// A future commit exists locally but is not reachable from the served commit.
|
||||
writeFileSync(join(r.repo, 'Inbox Note.md'), 'new\n'); git(r.repo, 'add', '-A'); git(r.repo, 'commit', '-qm', 'future');
|
||||
await assert.rejects(checkReply(body([cite(git(r.repo, 'rev-parse', 'HEAD'))]), c), refusal('citation-invalid'));
|
||||
assert.equal((await checkReply(body([cite(r.commit)]), c)).citations[0].scope, 'canon');
|
||||
});
|
||||
|
||||
test('C2-06 playground cites pass but cannot state canon', async t => {
|
||||
const r = repo(t), c = context(r), p = cite(r.commit, { path: 'Book One/Playground/What If.md', lines: [4, 4], quote: MARKER });
|
||||
assert.equal((await checkReply(body([p]), c)).citations[0].scope, 'playground');
|
||||
await assert.rejects(checkReply(body([{ ...p, as: 'canon' }]), c), refusal('playground-cited-as-canon'));
|
||||
});
|
||||
|
||||
test('C2-06 independent universes refuse; a hybrid and only its sources pass', async t => {
|
||||
const r = repo(t, { ...BASE, 'Crossover/Chapters/01.md': 'A hybrid fixture.\n' }), c = context(r);
|
||||
const other = cite(r.commit, { path: 'Second World/Chapters/01.md', lines: [1, 1], quote: MARKER });
|
||||
await assert.rejects(checkReply(body([cite(r.commit), other]), c), refusal('citation-cross-universe'));
|
||||
c.mapping = mapping(e => { e.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'] }; return e; });
|
||||
c.library = libraryReader(r.repo, r.commit, c.mapping);
|
||||
const hybrid = cite(r.commit, { path: 'Crossover/Chapters/01.md', lines: [1, 1], quote: 'hybrid fixture' });
|
||||
assert.equal((await checkReply(body([hybrid, cite(r.commit), other]), c)).citations.length, 3);
|
||||
});
|
||||
|
||||
test('C2-06 web cites require allowed definition, this session fetch and kept quote', async t => {
|
||||
const c = context(repo(t)), web = { url: 'https://fixture.invalid/page', quote: 'kept text' };
|
||||
await assert.rejects(checkReply(body([web]), c), refusal('citation-unfetched'));
|
||||
c.web.pages = [{ url: web.url, finalUrl: 'https://fixture.invalid/final', text: 'The kept\n text is here.' }];
|
||||
assert.deepEqual(await checkReply(body([web]), c), { citations: [{ scope: 'web', agent: null }] });
|
||||
assert.equal((await checkReply(body([{ ...web, url: c.web.pages[0].finalUrl }]), c)).citations.length, 1);
|
||||
c.web.pages.push({ url: web.url, text: 'A later fetch.' });
|
||||
assert.equal((await checkReply(body([{ ...web, quote: 'later fetch' }]), c)).citations.length, 1);
|
||||
await assert.rejects(checkReply(body([{ ...web, quote: 'discarded bytes' }]), c), refusal('citation-unfetched'));
|
||||
c.web.allowed = false;
|
||||
await assert.rejects(checkReply(body([web]), c), refusal('citation-unfetched'));
|
||||
});
|
||||
|
||||
test('C2-06 file origin is the path commit and trailer, while last follows served changes', async t => {
|
||||
const r = repo(t), path = 'Book One/Characters/Mara Vell.md';
|
||||
writeFileSync(join(r.repo, path), 'Agent fixture.\n'); git(r.repo, 'add', '-A');
|
||||
git(r.repo, 'commit', '-qm', 'Note\n\nMosaic-Agent: acme/scribe');
|
||||
const file = git(r.repo, 'rev-parse', 'HEAD');
|
||||
writeFileSync(join(r.repo, 'Inbox Note.md'), 'Unrelated.\n'); git(r.repo, 'add', '-A'); git(r.repo, 'commit', '-qm', 'unrelated');
|
||||
const cited = git(r.repo, 'rev-parse', 'HEAD'), reader = libraryReader(r.repo, cited, mapping());
|
||||
assert.notEqual(cited, file);
|
||||
assert.deepEqual(await reader.origin(cited, path), { agent: 'acme/scribe', file });
|
||||
assert.equal(await reader.last(path), file);
|
||||
writeFileSync(join(r.repo, path), 'Human fixture.\n'); git(r.repo, 'add', '-A'); git(r.repo, 'commit', '-qm', 'human');
|
||||
assert.notEqual(await libraryReader(r.repo, git(r.repo, 'rev-parse', 'HEAD'), mapping()).last(path), file);
|
||||
});
|
||||
|
||||
test('C2-06 origin uses literal paths even when a filename contains a glob', async t => {
|
||||
const path = 'Book One/Characters/Mara*.md';
|
||||
const r = repo(t, { ...BASE, [path]: 'Literal fixture.\n' }), original = r.commit;
|
||||
writeFileSync(join(r.repo, 'Book One/Characters/Mara Vell.md'), 'Other fixture.\n'); git(r.repo, 'add', '-A');
|
||||
git(r.repo, 'commit', '-qm', 'Other path\n\nMosaic-Agent: acme/other');
|
||||
const reader = libraryReader(r.repo, git(r.repo, 'rev-parse', 'HEAD'), mapping());
|
||||
assert.deepEqual(await reader.origin(git(r.repo, 'rev-parse', 'HEAD'), path), { agent: null, file: original });
|
||||
assert.equal(await reader.last(path), original);
|
||||
});
|
||||
|
||||
test('C2-06 reply limits fall back independently without coercion or inherited keys', () => {
|
||||
for (const value of ['true', 1, null, [true], {}]) {
|
||||
const r = resolveReplyLimits({ readable: true, document: { replyLimits: { requireCitation: value, citationsPerReply: 5 } } });
|
||||
assert.equal(r.requireCitation, false); assert.equal(r.citationsPerReply, 5); assert.deepEqual(r.fallback, ['requireCitation']);
|
||||
}
|
||||
for (const value of [0, 1001, 2.5, true, '5']) {
|
||||
const r = resolveReplyLimits({ readable: true, document: { replyLimits: { requireCitation: true, citationsPerReply: value } } });
|
||||
assert.equal(r.requireCitation, true); assert.equal(r.citationsPerReply, 20); assert.deepEqual(r.fallback, ['citationsPerReply']);
|
||||
}
|
||||
for (const value of [null, [], 1, 'settings', undefined]) {
|
||||
const r = resolveReplyLimits({ readable: true, document: { replyLimits: value } });
|
||||
assert.equal(r.requireCitation, false); assert.equal(r.citationsPerReply, 20);
|
||||
}
|
||||
assert.equal(resolveReplyLimits({ readable: true, document: { replyLimits: Object.create({ requireCitation: true }) } }).requireCitation, false);
|
||||
for (const document of [undefined, null, [], 'invalid']) assert.equal(resolveReplyLimits({ readable: true, document }).requireCitation, true);
|
||||
assert.equal(resolveReplyLimits({ readable: false }).requireCitation, true);
|
||||
assert.deepEqual(checkReplyLimits({ requireCitation: true, citationsPerReply: 1000 }), { requireCitation: true, citationsPerReply: 1000 });
|
||||
for (const value of [{ unknown: 1 }, { requireCitation: 'true' }, { citationsPerReply: 0 }, null, []]) assert.equal(checkReplyLimits(value), null);
|
||||
});
|
||||
@@ -60,6 +60,7 @@ export const RUNNER_FILES = Object.freeze([
|
||||
"packages/bus/src/landing.mjs",
|
||||
"packages/business/src/vocabulary.mjs",
|
||||
"packages/plugins/src/kinds.mjs",
|
||||
"packages/library/src/reply-protocol.mjs",
|
||||
"packages/processing-control/src/state.mjs",
|
||||
"packages/tasks/src/requirements.mjs",
|
||||
].map((p) => join(REPO, p)));
|
||||
|
||||
Reference in new issue
Block a user