All notable changes to guild-cli will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to the versioning policy described in docs/POLICY.md.
Add entries by dropping a file under .changelog/next/ (see .changelog/README.md). The release script collects them into a versioned block at npm run changelog:release -- <ver>.
-
changelog-releasenow also bumps theversionfield inpackage.jsonandpackage-lock.json(root +packages[""]) to the release version, in the same step that rewritesCHANGELOG.md. Previously the script only touched the changelog, so a release had to remember to bump the manifests by hand — and the 0.7.0 release didn't, leavingpackage-lock.jsonat0.6.0until CI's version-drift guard caught it (#442). The bump is a byte-stable text replacement (only the version line(s) move), and--dry-run/changelog:previewreports the would-bump without writing..changelog/README.mddocuments the new release step and the annotatedvX.Y.Ztag convention. (#441/#442 follow-up.) -
changelog-releasenow refuses to run when.changelog/next/holds a file with an unrecognized category, instead of warning and silently leaving it behind. Adocs-foo.md(nodocscategory exists) used to bewarned-and-skipped while the release still exited 0 — the entry was dropped on the floor and the orphan lingered for every future release ("silence reads as success"). The script now collects such files as orphans and exits 1 with a per-file list and the fix (rename to a valid category, or delete — docs-only changes have no fragment). The same guard fires under--dry-run/changelog:preview, so a mis-categorized fragment surfaces before release..changelog/README.mddocuments that docs-only changes get no fragment. (#441, surfaced by the 0.7.0 release dogfood.)
ctx supersede <old-id> --fact "..."— the first phase-2 ctx verb. ctx records are immutable on save, so a correction is not an in-place edit:supersederecords a new fact whosesupersedesfield points back at the one it replaces. The old record is never mutated, so the ledger keeps both and the supersession is reconstructable from the forward-only link alone.ctx listnow folds superseded facts out by default (showing the current head of each chain) and gains--allto keep every fact, marking the superseded ones;ctx show <old-id>stays readable and resolves the reverse link at read time assuperseded_by, an array of successor ids (empty while current, more than one when two independent corrections fork the same fact — both are reported rather than silently picking one). A chain is allowed (C supersedes B supersedes A) and stays acyclic — every link points strictly backward to an id that already existed, and the domain rejects a self-supersession outright; since the newest fact can never be superseded, a non-empty store always keeps at least one current head. Superseding an id with no record is a recoverable not-found (a correction must point at something real). Thesupersedeskey is omitted from an ordinary record's YAML, so phase-1 records round-trip byte-for-byte. Remaining phase-2 verbs:fork/chain/status.
Add entries by dropping a file under .changelog/next/ (see .changelog/README.md). The release script collects them into a versioned block at npm run changelog:release -- <ver>.
-
ctxOKF hardening — collision-safe import + export overwrite guard. Two follow-ups from the OKF round-trip review (#431): (1) on import, a foreignidthat collides with an existing record but carries different prose is now reallocated a fresh id instead of being dropped — the idempotent skip is gated on a prose match, so a distinct observation reusing thectx-YYYY-MM-DD-NNNnamespace is never silently lost (records-outlive-writers). The incumbent is read on demand only on a collision, so the common path stays a single id-set check. (2)ctx exportrefuses a non-empty target directory unless--force, so it can't silently clobber an unrelated tree. -
ctx importtags type-less docsokf:none. OKF requires atypeon every concept; import stays tolerant (a frontmatter-less ortype-empty.mdstill records — its body is the fact) but now tags such a docokf:noneinstead of letting it pass as a plain Fact. A stray non-concept file in a bundle (a README, a note) is therefore auditable after the fact viactx list --tag okf:none, rather than being indistinguishable from a real guild fact. The marker isnone, notuntyped: a real type literally named "Untyped" slugs tookf:untyped, so usinguntypedfor the missing-type marker would collide the two and defeat the audit;noneis not a plausible OKF concept type and stays unambiguous. A document with a usable type is unchanged (Factstays tag-clean; other types keep theirokf:<type>provenance tag). Also clarifies, in help and the docs, that prose dedup matches on a trim + whitespace-collapse only — case and punctuation are significant (it catches a markdown re-wrap, not a hand-edited copy). -
ctx <phase-2 verb>now gives a roadmap-aware message. Typing a verb that the help / docs name as "arriving in phase 2" (fork/supersede/show/list/chain/status) used to produce the same bareunknown verba typo gets. It now says the verb is planned for phase 2, not yet implemented and points at the phase-1 surface (record/export/import). Genuine typos still get the did-you-mean suggestion. Surfaced by dogfooding ctx from a user's perspective. -
gate complete(from approved) andgate deny(from executing) now redirect by verb name, completing the verb-shape redirect family alongside the existingfail-from-pending/approved and theexecute-from-pending cases. Scoped (per a dev-substrate agora play) to the two transitions with a clean single-verb bridge:gate completeon an approved request → namesgate execute(start the work first), recovery{verb:"execute"}.gate denyon an executing request → namesgate fail(deny is the pending cancel path; once work has started, fail is the cancel), recovery{verb:"fail"}; the dry-run path redirects identically. Deliberately left on the domain's state-name hint:complete-on-pending (recovery is multi-step approve→execute→complete, no single verb) anddeny-on-approved (no clean cancel verb exists from approved). State vocabulary stays in the domain; the verb hint is an interface concern.
-
gate executeon a pending request now redirects togate approveby name. The domain rejectspending → executingwith a state-name hint ("valid next states from pending: approved, denied"), leaving a caller who skipped approve to translate "approved" back into a verb.reqExecutenow pre-checks the pending state and throws aRecoverableErrornaming the bridge directly — proseerror:line plus a structurederror.recovery: {verb:"approve", args:{id}, …}for JSON consumers. This mirrors the existinggate fail-from-pending (→deny) andgate fail-from-approved (→execute) redirects; the dry-run path redirects identically rather than throwing the bare domain hint. State vocabulary stays in the domain (RequestState.ts); the verb hint stays an interface concern. -
gate boot/gate status/gate doctor: the misconfigured-cwd warning no longer asserts "not a fresh start". On a genuine fresh clone (the dogfood substrate'sguild.config.yamlis local-only, so a fresh checkout always lands here) the block read "likely wrong cwd, not a fresh start" while boot's own→ next:hint saidgate register— a self-contradiction. The block now reads "either the wrong cwd, or a fresh start here" and offers thegate register --name <you>escape hatch alongside the cd-elsewhere fix, aligning both surfaces. (This also closes theformatContentRootDisclosurearm oftrap_silent_fallback_loses_signal: a fallback disclosure now carries a recoverynext:cue instead of reading as noise.) -
README
30 seconds: install entry now 3-tier + shell-portable GUILD_ACTOR. R19 first-impression dogfood (yuki / asteria / miki) surfaced two doc papercuts that the prior block left to the reader:- The leading
npm installlooked clone-first by default, withnpx gate/npm i -gburied in a blockquote — newcomers repeatedly missed the "use inside an existing repo" path. The block now presents three explicit install styles (A clone, B drop-in dev-dep, C one-off npx) so the reader picks before running anything. - The
export GUILD_ACTOR=line was bash-shaped only; fish and PowerShell readers had to mentally re-translate. The exports for all three shells now sit inline as comments under the POSIX form.
- The leading
-
README config warning: "no
guild.config.yaml? no problem" is now leading prose, not the overcautious "pick a name distinct fromhost_names:" warning the dogfood read as gatekeeping. The substrate already handles the config-less path with a clearnotice: config: none — cwd used as fallback rootline; the README now says that before getting into the host-name collision detail, which is now framed as "if you do add a config" rather than "before you start". -
AGENT.md: explicit "humans welcome" preface. R19 dogfood (yuki) found the "AI-agent-first" framing read as a membership tier to first-time human readers. The preface now reframes it as "documentation density choice, not membership tier" — humans and AI share a content_root, share a trail, share the same verbs. -
README:
gate fast-trackis now glossed where the "30 seconds" loop first uses it. The quick-start told newcomers to rungate fast-trackbut the verb was absent from the "the whole tool is six verbs" list and never defined — so the loop you're told to run and the loop you're shown didn't line up. An inline comment plus a one-line note now explain that fast-track collapses request → approve → execute for the self-flow case, while the six verbs remain the full deliberation loop. -
README: the Passages note says why agora / devil / ctx run via
node ./bin/<name>.mjs. Onlygateandguildare registered asbincommands inpackage.json, so the alpha passages aren't on PATH even after a global install — the README now states that rather than leaving the reader to infer it. -
Split
tests/interface/ax.test.ts(1549 lines, 56 tests — the largest test file) following the #421 exemplar. The four largest cohesive sections move into focused, well-named files sharing one fixture module:_axHelpers.ts— the sharedbootstrap+runGate+rid/todayaxSuggest.test.ts—gate suggesttight-loop behavioraxVerbsAvailableNow.test.ts—boot.verbs_available_nowdiscoveryaxVoiceCalibration.test.ts— Two-Persona Devil voice memoryaxThank.test.ts—gate thankappreciation primitiveax.test.ts(trimmed, 648 lines) keeps the remaining AX affordances (suggested_next routing, JSON error envelope, board/show surfaces, advisory semantics, transcript, --plain, --dry-run, the text footer). Same 56 tests, no behavior change — navigability only. (boot.test.ts, the other 1.4k-line file, is the remaining follow-up.)
-
Split
tests/interface/boot.test.ts(1444 lines, 42 tests — the last of the three oversized files, after #421/Request and #422/ax). Its seven helpers (bootstrap/runGate/escapeRegex/bootstrapWithMembers/registerMember/makeRequestWithTarget/makeRequestSessioned, plus theGATEpath) were interleaved through the file; they're now centralized in_bootHelpers.tsand the tests split by concern:boot.test.ts(trimmed) — JSON-shape stability, actor/role, misconfigured-cwd, content_root_health, content-root disclosure, tailbootReviewedAuthored.test.ts— the reviewed-authored surfacebootOverlap.test.ts—active_overlapping_targets+ same-actor parallel-session detection (#234 / #249)bootWarnings.test.ts— the C3 silent-fallback warnings Same 42 tests, no behavior change. With this the three big test files (Request 1158, ax 1549, boot 1444) are all decomposed.
-
gate boot/gate suggest: newsuggested_next_reasonfield. Whensuggested_nextisnullbut the substrate has open (pending/approved/executing) requests that don't name the caller as executor, the field carries a one-line explanation — closing the asteria-dogfood gap wherestatus.pending.total: 1next tosuggested_next: nullread as a substrate bug for a host who approves fromgate list --state pendingrather than through the suggest ladder. Field isnullwhen silence is genuine. Text mode renders the reason inline as(nothing urgent — <reason>)/→ (no suggestion — <reason>). -
gate register --name <n>: host-collision error front-loads the fix. The "pick a different--name" path is now the lead recovery hint; "remove fromhost_names:" is parenthetical. Lines up with the asteria observation that a first-time user hittingnao(a defaulthost_namesreservation) was reading the longer guidance first. README30 secondsblock also picks up a one-liner:<you>should be distinct fromhost_names:(default reservations:eris,nao). -
Per-group test scripts.
package.jsongainstest:domain,test:application,test:infrastructure,test:interface,test:passages, andtest:e2e— each builds then runs only that subtree vianode tests/run.mjs dist/tests/<group>(the runner already accepted a root argument). Iterating on one area no longer requires the full ~1.8k-test suite. The defaultnpm testis unchanged. -
Split the largest domain test file.
tests/domain/Request.test.ts(1158 lines, 69 tests) is broken into focused files sharing one fixture module:RequestId.test.ts(id format),RequestSlices294.test.ts(the self-contained #294 per-executor slice-closure block), and the trimmedRequest.test.ts(core lifecycle / serialization / actor stamps), with_requestHelpers.tsholding the shared clock + builder. Same 69 tests, no behavior change — just navigability. (Exemplar for the remaining large files;ax.test.tsis left untouched here to avoid colliding with an in-flight change.)
-
gate bootcross_passage — oldest-paused alarm. Each passage's orientation summary now carriesoldest_suspended_age_daysandoldest_suspended_cliff(null when nothing is paused). A baresuspendedcount reads the same whether the oldest pause is two hours or two months old, so a long-forgotten thread hides inside the count; surfacing the oldest pause's age plus its one-line cliff turns boot into a forgotten-thread alarm. Text mode renders an↳ oldest paused Nd ago: <cliff>line under the passage when the oldest pause is at least a day old. Found by dogfood: an agora play sat suspended 39 days with its conclusion intact, because re-entry only ever saw the count. Closes the other half of records-outlive-writers — records must also be findable on re-entry, not merely countable. -
ctx list/ctx show— phase-1 read-side. Facts can now be read back without grep.ctx listprints recorded facts newest-first (id, author, timestamp, tags, snippet);--tag prefix:valuefilters by an exact tag and--by <m>by author.ctx listwith no records andctx listwith a filter that matches nothing give distinct messages.ctx show <id>prints one fact in full; a well-formed but absent id raises a not-found that namesctx listas the recovery (text hint + structurederror.recoveryin JSON), and a malformed id fails at the domain boundary.list/showleave the phase-2 verb set. Surfaced as the strongest pull toward phase 2 while dogfooding ctx. -
ctx export/ctx import— Open Knowledge Format (OKF) round-trip. ctx facts can now be projected to and recorded from OKF bundles (a directory of<id>.mdfiles: YAML frontmatter + fact prose, plus generatedindex.md/log.mdviews).ctx export <dir>writes the bundle;ctx import <dir>records it back. Guild-authored bundles round-trip losslessly — id, timestamp, author and tags survive, and a re-import is idempotent (existing ids skip). Foreign bundles import tolerantly: nested subtrees are walked, bare/prefixed tags are coerced toprefix:value(bare →topic:), a non-Facttype is preserved as anokf:<type>provenance tag, missing authors fall back to--by, and empty/unparseable documents are reported as skipped rather than failing the import. Prose dedup is on by default — a fact whose prose (matched on a trim + whitespace-collapse, so case and punctuation are significant) is already recorded (under any id, or earlier in the same bundle) is skipped, so even an id-less foreign bundle re-imported is a no-op;--allow-duplicatesopts out. OKF is an interchange projection (principle 11), not a storage change — the on-disk substrate stays YAML. -
gate show/gate chain/gate wave-status: not-found errors now honor--format json(#408). Pre-#408 these surfaces emittednot found: <id>\n try 'gate list' or 'gate tail' ...as free text even when the caller requested--format json— tool-use agents that pipegate show <id> --format jsoninto a JSON parser tripped on the prose. A newnotFoundEnvelopehelper insrc/interface/shared/notFoundHint.tsrenders the same information through the existing envelope shape used bywhoamiand the write-verb error path (issue #194 lineage):{ok:false, error:{kind:'not_found', entity, id, message, hint}}. Text and--plainformats are unchanged — the asymmetry the PR fixes is "format flag was ignored", not "format default changed". Unknown-command andlorenot-found are out of scope (separate contract decisions; tracked in #408 discussion).
-
after:reviewhooks now receive the appended review onctx.extra.review. The review handler firedfireAfterHookwithout theextraargument, soctx.extrawasundefinedforafter:reviewsubscribers — contradicting the contract documented indocs/plugin-schema.md("before:review/after:reviewsetextra.review"). A hook readingctx.extra.review.verdictto route reject/concern verdicts silently sawundefined. The handler now passes the terminal (just-appended) review as{ review }, and a regression test intests/interface/hookPluginLoader.test.tsasserts the hook receiveslense/verdict/comment/by. (before:reviewcarrying its own payload is tracked separately — it fires before the append, so the documented "appended review" shape needs a distinct decision.) -
bin entries: a missing dependency is no longer misreported as an unbuilt
dist/. On a fresh clone wheretschad run (sodist/exists) butnpm installhad not (sonode_modulesis absent), the bareyamlimport failed and everygate/guild/agora/devil/ctxinvocation printeddist/ not built (or out of date) … npm run build— pointing the operator at a rebuild when the actual fix is installing deps. Node phrases the failure asCannot find package 'yaml' imported from <dist importer>, so the old/dist/message scan misclassified it. The 5 inlined catch-blocks now delegate to a sharedbin/_lib/handleDistLoadError.mjsthat distinguishes a bare-specifier dependency miss (→dependency '<pkg>' is not installed … npm install) from a genuine missing/staledist/(→ unchanged build message + transitive-miss hint). New unit tests intests/interface/distLoadError.test.tspin both paths. -
members/: identity resolution chain hardened (#407). Two related gaps in themembers/actor identity boundary, surfaced by a first-impression dogfood:- An empty (0-byte) or malformed
members/<name>.yamlno longer promotes<name>to a registered actor. Previouslyexists()was filename-only, sotouch members/ghost.yamlwas enough forgate fast-track --from ghost ...to slip pastassertActorwhilewhoamialready classifiedghostasunknown— write verbs and the read-side identity surface disagreed. Both surfaces now follow the same parse + hydrate contract. - The internal
name:field ofmembers/<filename>.yaml, if present, must match the filename stem. A divergence (e.g.members/alice.yamlcontainingname: leysia) is now flagged as malformed byhydrateand the record is rejected — neitheralicenorleysiaresolves to member status from such a file. Previously yaml.name silently won, letting a write-access adversary (or careless operator) promote arbitrary names tomemberby editing the yaml internals without renaming the file. SECURITY.md"Invariants enforced in code" gains an "Identity resolution chain" entry documenting theGUILD_ACTORenv / filename / yaml.name resolution rules now enforced.- Three new tests under
tests/infrastructure/hydrateErrorSurface.test.tscover the empty-file path, the divergence path, and the regression guard that properly-registered actors (with or without an explicit yaml.name) continue to work transparently.
- An empty (0-byte) or malformed
-
gate --helpnow advertises thelistfilter as--executor <m>, matching the runtime. The BASE catalog line showedgate list … [--executors a[,b,...]](plural, copied fromgate request's multi-executor flag), but thelistverb accepts only--executor(singular — "match waves naming this one executor") and rejects--executorswith "unknown flag". An agent copying the advertised flag from--helphit a wall — a help-text ↔ runtime drift (principle 10 /trap_help_text_drift_on_new_verb). Help line corrected; a regression test pins the help and the runtime KNOWN_FLAGS to the same singular flag. -
gate list --state <bogus>now enumerates the valid states instead of dead-ending. It errored with a bareInvalid state: <x>and no valid set, while sibling unknown-flag errors list their valid flags — an asymmetric touch-feel that violated the "error + recovery path" house style.reqListnow validates--stateat the interface boundary and, on a miss, names the full CLI-valid set:pending, approved, executing, completed, failed, denied, all(built from the domain'sREQUEST_STATES∪ the interface-onlyallsugar, so it can't drift from the enum) plus a note that omitting--stateuses the verb's default. The app-layer guard inRequestUseCases.listByStatestays as defense-in-depth. State vocabulary lives in the domain; the CLI-facing hint is an interface concern — same split as the deny/execute verb redirects. Pinned bytests/interface/listInvalidState.test.ts. -
Docs + touch-feel:
gate pending's filter surface is now accurate and self-directing.docs/verbs.mdhad lumpedgate listandgate pendingtogether as accepting--from / --executor / --auto-review / --for, butpendingis the lean "--for me" shortcut — its known flags are{for, format}and it rejects the richer filters (surfaced by a two-persona review red-teaming the #426/#427 docs work, which had reasoned aboutlistin isolation). The doc now describeslist's full filter set andpending's narrow one separately, pointing richer pending filtering atgate list --state pending. And the runtime matches the advice:gate pending --executor <m>(or--from/--auto-review) no longer dead-ends at "valid flags: --for, --format" — it appendsto filter pending by author/executor/reviewer, use: gate list --state pending …. (rejectUnknownFlagsgained an optionalhint; onlypendingpasses one, so every other verb's error is unchanged.) Pinned bytests/interface/pendingFilterFlags.test.ts. -
Transition-redirect errors now carry
error.codeagain, not justerror.recovery.RecoverableError(thrown by the verb-shape redirects:execute-from-pending → approve,complete-from-approved → execute,deny-from-executing → fail,fail-from-pending/approved) reworded the message away from the domain's "Illegal state transition" phrasing, soderiveErrorCode's prose scan stopped classifying it and the JSON envelope emittedrecoverybut acode: undefined. An agent branching onerror.codewas blind to exactly the errors carrying the richest recovery hint (surfaced dogfooding the JSON surface as an orchestrator).RecoverableErrornow carries acode(defaultillegal_transition, overridable) and the envelope emits it — so every redirect error classifies asillegal_transitionand ships a dispatchablerecovery.not_foundand the plain transition errors are unchanged. -
gate suggest/gate bootno longer undercount pending when a self-wave is open. A request the actor both authored and is the executor of (a self-wave) is deliberately kept off the suggestion ladder — its only open transition is a self-approve, whichactionableTransitionswon't nudge. ButderiveSuggestedNextNullReasonskipped those requests entirely, so the null-reason's pending tally read one lower thanboot'squeues: pendingline for the same substrate (e.g. queues showedpending=2while the reason said "1 pending"). The two surfaces now reconcile: the reason carries a dedicated self-wave clause ("N pending request(s) you authored also name you as executor (self-wave) —gate suggestdoes not nudge self-approve …"). This also closes a silent-gap sub-case where a lone self-wave pending produced no reason at all (the not-mine total was zero) whilequeues: pending=1showed it — the exact "silence reads as a bug" failure the reason field was added to prevent. -
Write-lifecycle not-found errors now carry the same discovery hint the read verbs already emit.
gate approve/deny/execute/complete/failon a mistyped or stale id threwRequest not found: <id>straight through the shared error envelope with no pointer to recovery — whilegate show <id>(and the other read verbs) had carriedtry 'gate list' or 'gate tail'since the not-found-hint work. The two not-found surfaces disagreed; the write side was the worse touch-feel.emitErrorEnvelopenow performs the wire-up sweep its own doc anticipated: a recognizedRequest not foundgains the prose hint line plus, under--format json, anerror.hintfield and a structurederror.recovery({verb: "list", …}) —error.messagestays clean so existing parsers are unaffected. Both the dry-run (plainError) and real (DomainError) not-found paths are covered. Sibling passages (agora / devil / ctx) that share the envelope are untouched: the hint is attached only for therequestentity, never stapled onto an unrelatedPlay not found/Review not found.
Add entries by dropping a file under .changelog/next/ (see .changelog/README.md). The release script collects them into a versioned block at npm run changelog:release -- <ver>.
-
BREAKING (v0.6 cut, closes #239):
--executor(singular) and theexecutorJSON alias are removed. The multi-executor surface introduced in #230 carried both forms for a v0.6 deprecation window; 330 commits later the singular form is gone:gate request --executor <m>→ usegate request --executors <m>gate fast-track --executor <m>→ usegate fast-track --executors <m>(omit for self-execute)gate issues promote <id> --executor <m>→--executors <m>gate show --format json | jq .executor→ read.executors[].nameCreateRequestInput.executor: string(domain/application) → useexecutors: readonly string[](the only form accepted as input) Hydrate is unchanged: YAML records written before #230 with the legacyexecutor: <string>field still load viaYamlRequestRepository.hydrate(records-outlive-writers per principle 04). Only the input surface is removed.
-
Request.toRenderJSON()removed. It was a back-compat shim that copiedtoJSON()and added the deprecatedexecutorkey; callers (gate show / list / board, dry-run preview) now calltoJSON()directly. The render-vs-persistence split was there to keep the deprecated alias from polluting on-disk records — with the alias gone, the separation has no reason to exist.
If you have shell scripts using --executor <m>, replace with
--executors <m> (same value, plural flag). If you have tooling
parsing gate show --format json | jq .executor, switch to
.executors[].name — the field has been executors[] (an array
of {name, status} objects since #294) for the entire v0.5.x
range; the singular alias was the only thing keeping the old
shape compatible.
agent-loop / SubAgent callers already use --executors as of
PR #382 (the executors-singular-to-plural-in-examples sweep), so
no action is required there.
-
Advisory fields classified ambient-by-design now name that status at the declaration site (#344 audit close-out). Touched 4 fields per the 2026-05-15 audit:
Issue.severity,Issue.area,Request.sourceAgoraPlay,Request.template(representing the template / templateVersion / gateRequiredAcknowledged trio). Each gets a one-paragraph comment naming the audit, its consumer (display-only or cold-reader audit), and why no further consumer is needed.schema.tsdescription forfrom-agorasoftened — it previously promisedgate chain-style navigation that did not ship. -
gate decisionsno longer mis-attributes slice-fail/complete reasons to theexecutingrow. Issue surfaced by asteria's dogfood run (2026-05-16, finding D1): runninggate fail <id> --by X --reason "msg"produced adecisionspayload where the fail row carried the cascade auto-note (wave failed (any-fail- wave-fail)) while the execute row carried the actor's--reasontext. Root cause: per-#294 slice-closure writes a secondexecutingstatus_log stamp (with the slice note) that the dedupe walk collapsed into the execute row via "later wins". Fix: detect slice-close stamps (executing entry whoseatmatches the executor record'scompleted_at) and re-kind them to fail/complete; skip wave-cascade entries whose auto-notes match the domain-emitted patterns fromRequest.ts:1060. -
gate transcriptprose reads naturally on slice-closure waves. Same root cause as the decisions fix (eris touch-feel 2026-05-16 finding 4.4). Before:Asteria moved it to executing (90ms later). Asteria moved it to executing (87ms later) (note: "slice done"). Asteria moved it to completed (0ms later) (note: "wave completed (all slices closed)").After:Asteria moved it to executing (90ms later). Asteria closed their slice as completed (87ms later) (note: "slice done"). The wave completed (all slices closed) (0ms later).Cold readers see one judgement per executor, plus the wave-level consequence as subject-less prose (no actor credited with writing the auto-text). -
gate failon an approved wave now surfaces thegate executebridge. asteria finding B1: pending → fail returned a RecoverableError with agate denyrecovery hint and the JSON envelope'serror.recoveryfield; approved → fail returned the domain layer's rawIllegal state transition: approved → failedwith no recovery shape at all. Same prose + structured-recovery discipline now applies — JSON consumers can dispatch theexecutemove without pattern-matching on state-machine prose. -
gate helpis now a sugar alias forgate --help. The barehelpwas previously rejected asunknown command, costing one tool-call round-trip for the universal "what does this CLI do?" reflex (asteria finding A1). Aligns withgate --help/-h. -
boot.session_id_source: "unset"replacesnull. Enum is now['flag', 'env', 'unset']. asteria finding A2 — null required readers to know whether it meant "not present yet" or "error". The explicit"unset"token is principle 10 (schema- as-contract / output specificity) applied at the value layer. Schema enum updated; tests adjusted. -
gate lore show <number>resolves to the slug. Numeric input (gate lore show 11) now matches the unique principle/trap with that number prefix (11-ai-first-human-as-projection). Cold readers reach for the number first; the slug-only requirement cost a round-trip (eris touch-feel finding 4.6). Ambiguous or non-numeric input falls through to the previous "not found" hint unchanged. -
--note/--reason/--action/ stake-note fields now declaremaxLengthingate schema. Free-form text fields (MAX_TEXT = 4096) and stake fields (MAX_STAKE_NOTE = 80) ship their domain caps via the schema envelope so AI consumers pre-validate before composing long payloads. asteria finding F1 ("--notecap not advertised → silent truncate risk on long handoff messages"). Applied to gate's primary write verbs (approve, deny, execute, complete, fail, claim, witness); others inherit the same caps but advertise them less prominently — sweep-out is a follow-up. -
gate reviewhelp example uses--note(the canonical flag) instead of--comment(the deprecated alias). Drift surfaced by eris touch-feel finding 4.5: the same verb runs a deprecation notice when--commentis used, but the canonical example still showed it as the first surface. Aligned. -
Lore broken citations removed. Principle 12 cited
substrate/agora/plays/whole-repo-review/2026-05-04-001.yaml(dir doesn't exist in repo); principle 04 citedalexandria/orientation/PHILOSOPHY.md(dir doesn't exist). Both were dangling references — the irony of principle 04 (records-outlive-writers) carrying a citation that didn't outlive its writer was not lost. Principle 12 kept the three-voice framing but removed the file pin; principle 04 lost the alexandria bullet entirely (asteria findings 1 & 2). -
Docs and
gate --helpexamples now consistently use--executors a[,b,c]rather than the deprecated--executor <m>. The singular flag is still accepted as a back-compat alias (removed at v0.7 per #239). Filter-sidegate list --executor <name>keeps its semantically-singular spelling — only the create-side examples (request / fast-track / issues promote) have been swept. -
gate nextsetup-failure errors now flow through the structured JSON error envelope instead of plain-texterror: <msg>on stderr. Three sites (GUILD_ACTOR not set / actor not registered / --confirm but verb needs extra args) used to writeprocess.stderr.write('error: ...')+return 1, bypassing the outer-catch'semitErrorEnvelopehelper. JSON consumers got plain text where every other gate verb gives them{"ok":false,"error":{"message":"...","field":"...","code":"..."}}. After this PR, the first two throwDomainError(field='GUILD_ACTOR')and route through the standard envelope; the third (post-plan-emit failure) emits the plan withdispatched: falseand confines the prose hint to text mode. -
gate next --confirmon a verb-needing-args now emitsdispatched: falsein the plan envelope (wasdispatched: true— misleading, since nothing was dispatched). Consumers branching ondispatchedto decide retry semantics were getting false positives.Follow-up to #400's success-path notice gating: same eris-first cleanup applied to error paths.
-
mcp/moved toexamples/mcp/. The directory held a worked example of MCP integration (gate_mcp.py) plus two example hook plugins (mcp/plugins/{doc-check,self-loop-check}.mjs) — none of it is core substrate, so its top-level position misled cold readers about what was load-bearing. The move brings the repo root one step closer to the 11 → 8 dir target named in #385. Voice-budget test allowlist and principle 03 reference paths updated to match. -
Shared
--formatvalidator (src/interface/shared/parseFormat.ts) replaces 55 inlineif format !== json && format !== textblocks across gate / agora / devil / ctx handlers. Drift surface collapses to a single source of truth; the validation message ("--format must be 'json' or 'text', got: <raw>") is now uniform across passages (previously several handlers had the reversed-order "text or json" variant). Helper throwsDomainError(field='format')so JSON-mode callers now receive the structured envelope ({"ok":false,"error":{"field":"format","code":"validation_error",…}}) instead of plain text — consistent with how every OTHER error in the same handler renders. Single-format verbs (gate boot,gate schema,gate status, etc.) pass'json'as the default viaparseFormat(args, 'json'). Net: −134 LOC across 55 files. -
notice: wrote <path>stderr line is suppressed in JSON mode foragora new,agora play,devil open. The path and config-file are already in the stdout JSON envelope (where_written+config_file); re-emitting them on stderr was pure context pollution for AI consumers — every successful create burned two extra lines of tool-result context for information the caller already had structured. Text-mode readers still get the disclosure (the stdout✓ createdline does not carry the path).ctx recordalready had the notice correctly gated inside its text-modeelsebranch. -
Error prefix unified to
error: <msg>acrossgate next,gate self-pattern,gate decisions. Three handlers used a passage-verb-named prefix (gate next: GUILD_ACTOR is not set…), which on thethrow new Errorpath produced the doubled prologueerror: gate self-pattern: …once the outer-catch added its ownerror:envelope. Machine consumers that grep for^error:now match every error from every verb; prose hints retain thenext: <command>recovery line below for the human reader. -
Four post-2026-05-15-touch-feel polish fixes. None blocked any current workflow; each closes a small papercut surfaced during the end-of-day dogfood pass.
-
bin/_lib/checkDistFreshness.mjs— stale-dist warning now names "after switching worktrees on this repo" alongside the existing "after agit pull" cause. Worktree exit followed by./bin/gate.mjsin the parent is a regular trigger and the prior hint missed it. -
GuildConfig.load()—GUILD_CONFIG=""(set-but-empty) now emits a one-line stderr nudge before falling back to walk-up. The previous behavior silently treated empty as unset, which is the footgun mode where the caller thinks they're clearing the override but is in fact letting walk-up decide the substrate. -
gate swarm-statustext rendering — waves with no executors render on a single line (<id> [state] from=<from> (no executors)) rather than emitting a separate indented(no executors assigned)sub-line. Substrates dominated by pre-#230 records (or freshly- filed pending waves) no longer present visually heavy two-line blocks per wave. -
gate swarm-statussummary hint — whenactive_waves > 0butdistinct_executors == 0, a one-line hint surfaces(no executor-stamped activity — likely pre-#230 records or freshly-filed pending). The previous summary line read as "swarm picture" at face value, misleading the reader for the legacy-substrate case.
-
-
Success-path stderr notices now suppressed in JSON mode for
gate approve,gate execute,gate review,gate thank. The notices (self-approve / executor-assignment mismatch / self-review /--commentdeprecation hint / self-thank) carry information that JSON consumers can already detect structurally from the envelope (by/from/executors[].namefields), so re-emitting prose on stderr was pure context pollution for AI consumers. Text-mode readers still get the disclosure unchanged.gate approve --by XwhereX == request.from, profile-featureselfApprove: warn→ notice (text only)gate execute --by XwhereXis not in the assigned executors list → notice (text only); message updated from--executor records intentto--executors records intent(singular flag was removed in #398)gate review --by XwhereX == request.from→ ⚠ self-review (text only)gate review --comment <s>→ deprecation hint (text only)gate thank X --to X→ self-thank notice (text only)
Audit follow-up to #397 (which handled
notice: wrote <path>in agora new/play, devil open). Same eris-first principle: stderr on success paths should not carry prose that's already in the JSON envelope.Out of scope (kept as-is):
gate messageself-message / inactive-recipient notices: this verb has no JSON output mode at all, so stderr is the only signal channel.gate nextsetup-failure notices: those are error paths, not success-path notices; they need a separate parity refactor (route throughemitErrorEnvelopefor JSON consumers) covered by a future PR.errorEnvelope.tsJSON-on-stderr write: that's the contract (JSON consumers parse stderr for the envelope), not pollution.(explain: ...): opt-in via--explain, caller asked for it.⟶ <voice>: text-mode-only ornament by design (#382).
-
Test runner default concurrency raised from 4 → 8 (override via
TEST_CONCURRENCY=<N>). The suite is subprocess-spawn-bound (~119 of 173 testsspawnSynca freshgate.mjs), not CPU-bound, so oversubscription pays even on the 4-vCPU GitHububuntu-latestrunner. Local measurement on a 10-core mac: 4 → 513s, 8 → ~310s, 12 → 224s, 20 → 159s. The doc-comment block intests/run.mjscarries the table so future tuning has a baseline. -
tools/lore-scope.shmoved toscripts/lore-scope.sh. Top-leveltools/held a single shell script that paralleledscripts/without a meaningful distinction. Merging removes a one-script directory and brings the root listing down 1 (part of #385's cold-reader discoverability pass). Test file movedtests/tools/→tests/scripts/to match. Doc refs inlore/README.md,docs/glossary.md, and source comments updated. -
writeFormat.ts:parseFormatre-export shim removed. PR #397 introducedsrc/interface/shared/parseFormat.tsas the canonical parser and left a single-line re-export inwriteFormat.tsso the 9 write-side handlers that pullparseFormatfrom there didn't need touching in the same PR. This PR finishes the move: those 9 handlers now import from../../shared/parseFormat.jsdirectly, the shim is gone. Single import path for one symbol — no divergence risk if either side changes signature.
-
.changelog/next/fragment system replaces concurrent writes toCHANGELOG.md's[Unreleased]block. Each PR drops one file at.changelog/next/<category>-<slug>.md; at release timenpm run changelog:release -- <version>collects fragments, groups by category, and rewrites the[Unreleased]block. Per-PR filenames make textual conflicts impossible — the previous "append to end of section" convention still produced duplicate### Fixedsub-headings when PRs raced. See.changelog/README.mdfor the format. -
gate decisions --limit <N>truncates the rendered decision list to the most-recent N entries after sort.totals.entries_countedcontinues to reflect pre-truncation total so callers can detect whether more decisions existed past the cap. Siblinggate voices --limitalready had this; aligning the flag surface removes a cross-verb inconsistency. -
gate swarm-status— cross-wave director / participant view (#346). Closes the principle-14 loop: composeswave-statusacross all active waves into one envelope so a director never has to chain 1 + N + N×M sub-reads to compose the swarm picture. Returns waves (with per-executor freshness bands per #309), distinct-executor count, and a flatalerts[]array surfacingstale_executor/overlapping_target/attribution_risk. Dual scope flags:--orchestrating <actor>(director-centric, "what swarm am I conducting?") and--for <actor>(participant-centric, "what swarm am I part of?"). GUILD_ACTOR env defaults toorchestrating=$GUILD_ACTORwithscope.for_source="env"reported in the payload. Sibling ofgate wave-status(per-wave) andgate decisions(per-actor history). 8 tests undertests/interface/swarmStatus346.test.ts. -
GUILD_CONFIGenv var override for cross-tree substrate access (#308 Layer A). When set to an absolute path,gateskips the cwd walk-up and uses the namedguild.config.yamldirectly. Unblocks the "worktree at , substrate at " case where the orchestrator's substrate is not in the worktree's git ancestry — most notably Claude Code SubAgents running in.claude/worktrees/agent-X/against a parent substrate that lives outside the repo tree. Resolution priority:GUILD_CONFIGenv >findConfig(cwd)walk-up > cwd fallback. A nonexistent path errors loudly rather than silently falling back (silent fallback here would let a SubAgent write to a stale substrate without realising). -
Claude Code
PostToolUse→gate witnessexample wiring shipped underexamples/plugins/harness-wirings/claude-code/. Surfaces SubAgent tool-use as throttled witness updates on the parent wave sogate wave-status/gate swarm-statusshow fresh activity without polling. Source 3 of the #308 Layer A bundle (per principle 15 routing: harness-specific wirings live inexamples/, not in core). -
Principle 15 promoted: "Plugins are the default extension surface; core owns substrate identity." Names the long-implicit order asymmetry between core and plugin routing — plugin-first for surface / style / cross-cutting; core only for substrate identity (domain semantics, lifecycle, principle-11 director-axis reads). Promotion observations: #308 design re-route (core daemon → plugin composition) and the voice plugin cluster (#377–#383) shipping
VoicePluginas a fourth plugin type rather than as a core feature. Glossary count updated 14 → 15.
-
bin/*.mjsentries now catch unhandled errors and render them in the standarderror: <msg>envelope instead of raw Node stack traces. Surfaced byGUILD_CONFIG=/nonexistentprinting anat file:///…/bin/gate.mjs:54:1 { field: 'GUILD_CONFIG' }stack with aNode.js v23.6.1footer — unprofessional for what is user error. The shared helperbin/_lib/handleMainError.mjsdetects DomainError-shaped throws (presence of.fieldproperty) and prints cleanerror: <msg>+ exit 1; unexpected internal throws still surface their stack with a<bin>: internal error (please file an issue …)preamble. Fired from gate / guild / agora / devil / ctx — all five entry-points. -
GUILD_CONFIG=""stderr nudge now fires exactly once per process instead of twice.GuildConfig.load()is called multiple times in one invocation (top-level + at least one plugin loader); the prior implementation re-emitted on each. Module-private one-shot guardemptyGuildConfigNudgeFiredpreserves per-process emission semantics so a fresh shell still gets the warning, but a single CLI run no longer doubles it. -
gate boot --by <actor>(and--asalias) now resolves identity for one invocation, overridingGUILD_ACTOR. Pre-fix the muscle-memorygate boot --by erisbounced withunknown flag: --bybecause boot only consulted env. Lifecycle verbs all take--by; aligning boot removes the cross-verb surprise. -
gate voices(no positional) now emits a per-actor utterance index instead of a bare usage error. Pre-fix the no-arg invocation returnedUsage: gate voices <name>with no way to discover which actors had utterances to walk — a silent-fallback signal-loss against the discovery question (trap_silent_fallback_loses_signal). Index counts both authored requests and reviews, labels each rowmember/host/historical, and sorts by activity desc.
gate tail 0no longer falsely claims the content_root is empty. Pre-fix,gate tail 0on a rich substrate rendered the same(no utterances on this content_root yet)line as a truly-empty root — a false claim about source state (trap_silent_fallback_loses_signal). The empty-result branch now disambiguates:(0 utterances requested)when the caller asked for zero, the original message only when source is actually empty.gate tailrejects extra positional arguments instead of silently dropping them. Pre-fix,gate tail 3 extrareturned the first 3 utterances and threwextraon the floor — the same fail-open shape thatrejectUnknownFlagsexists to prevent for flags. The rejection now fires symmetrically for positionals, naming the unexpected values in the error message.gate voices <typo>now surfaces a "did you typo the name?" diagnostic instead of returning identical empty output to a registered-but-quiet actor. Pre-fix, the two cases were shape-indistinguishable — a silent-fallback signal-loss named bylore/traps/trap_silent_fallback_loses_signal.md. Text mode adds a 3-line hint pointing atgate tail/gate register; JSON mode wraps the output in{utterances: [], _meta: {actor_unknown: true}}only on the typo branch. Registered members and hosts with no utterances keep the pre-fix shape unchanged (array form for JSON consumers, plain empty-line for text).gate bootno longer raises the 7-line "inbox-enrichment-failed" warning when the actor is a host. Hosts have no inbox by design (MessageUseCases.inboxthrows "hosts do not have inboxes"). Before this fix, every host-side boot recapped that fact as a warning block, inverting principle 09 (orientation-disclosure: surface surprising cases, stay quiet otherwise). The fact is already conveyed byrole: 'host'in the payload. Inbox enrichment now skips on the host path; the warning still fires for an unknown actor (typo diagnostic), so the no-inbox surface remains useful where it actually disambiguates.gate list/gate pending/gate boardno longer emit the "filtered by GUILD_ACTOR=..." stderr disclosure in JSON mode. That disclosure exists so a human reading text output knows why their result set is implicitly scoped. JSON consumers already receive the same fact on stdout as_meta.filter.for_source: 'GUILD_ACTOR'(or_meta.filter.sourceon board), so the stderr line is structurally redundant; emitting it on every JSON invocation crosses the chronic-noise threshold named bylore/traps/trap_chronic_noise_blindness.md. Text mode keeps the line because that's the only surface humans see.gate bootno longer steers the author of an executing wave towardgate complete --by <author>when a different actor is the named executor. The actionable-transition predicate previously matchedexecuting-mineon eitherr.hasExecutor(actor) || r.from === actor, so a wave authored by eris with--executor mikisurfaced to eris with→ next: gate complete <id> --by eris. Dispatch then failed ("eris is not in this wave's executors (miki)"). The author fallback is now gated onexecutors.length === 0(legacy / self-execute records that never populated executors); when the list names someone else, the wave does not appear on the author's actionable ladder. Observed 2026-05-13 on a drained test fixture (req 2026-05-08-0012).gate failon a pending request redirects togate denyby name. The domain still rejects the transition with the state-name hint (valid next states from pending: approved, denied) — that's the right shape for the domain layer — but the interface now pre-checks state and surfacesgate deny <id> --by <m> --reason <s>directly, so the next verb is one line away instead of a translation step. Pairs with the help-tier promotion below.gate schemanow declaresinbox mark-readas a subcommand on theinboxentry. Pre-fix, the schema'ssummaryclaimed "mark-read as subcommand" but theinput.propertiescarried nosubcommandfield — an MCP orchestrator reading the structured contract saw no way to invokegate inbox mark-read. The prose and the structured contract disagreed (trap_help_text_drift_on_new_verb). Pattern aligns with the existingdoctor/issues/templates/loreschema entries which carry asubcommandenum.gate list/gate pendingtext mode no longer silently truncates long actions and no longer corrupts table layout on multi-line actions. Pre-fix,printSummaryusedString(j['action']).slice(0, 60): (a) it cut at exactly 60 chars with no indicator (trap_silent_fallback_loses_signal— a 500-char action and a 60-char action rendered identically), (b) it kept embedded\nin the slice, so the second line shifted to column 0 and broke the columnar layout, and (c).sliceon a UTF-16 boundary risked cleaving a surrogate pair. The fix collapses\r\n\tto a U+21B5 ↵ marker first, then runs the existingtruncateCodePoints(..., 60)helper (appends..., splits on code points). JSON consumers continue to see the full action verbatim — truncation is a text-mode-only concern.bin/ctx.mjsis now tracked as executable (mode 100755). Pre-fix, the file was shipped with mode 100644 since #143 (2026-05-04) —./bin/ctx.mjsfrom a clean checkout failed with "permission denied." Invocation vianpm run ctxornode bin/ctx.mjsworked (which is how every test path reaches it), so the regression hid in the gap between shell-invoked and node-invoked entry points. The four sibling dispatchers (agora/devil/gate/guild) were already 100755. A new test (binEntryExecutableBit.test.ts) pins the invariant viagit ls-files -sso the next bin entry can't regress in the same way.
gate denyis now part of BASE help (profile=standard). Pre-fix it was tier=extra, visible only viagate --help --all— yetdeniedwas listed in BASE help's terminal states block. A cold-session caller searching for "cancel a pending request" hitgate failfirst (illegal pending → failed) and only founddenyafter invoking--all. The verb is now surfaced symmetrically withapprove/complete/fail.examples/agent-voices/README.mdcross-references the underlying lore. Adds an "Anchoring principles" section linking principle 08 (voice-as-doctrine) and principle 07 (perception-not-judgement) so a cold reader sees why this content_root exists in the shape it does, not just how to use it. Also surfaces the new boot/lore/--explainaffordances under "Everything is gate voices readable."--explainregistered forgate voicesandgate tail. These read verbs now emit a one-line orientation on stderr when called with--explain, joiningboot/list/show/chain/lore list/lore show. JSON contract unchanged.handlers/request.tssplit into three files by passage role. The original 1656-line module mixed creation, read, lifecycle, and fast-track handlers in one place; it has been split intorequest.ts(create + fast-track, 605 lines),requestReads.ts(list/pending/show + formatters, 582 lines), andrequestLifecycle.ts(approve/ deny/execute/complete/fail + helpers, 533 lines). The dispatcher inindex.tsnow imports each set from its dedicated module. No behavior change — every handler's signature, output, and lifecycle remain byte-identical; the existing test suite passes unchanged (1677/1677).board.tsand the test re-export inindex.tsupdated to importformatReviewMarkers/computeReviewMarkerWidthfrom the newrequestReads.ts;issues.tsstill importsparseExecutorsListfromrequest.ts(unchanged path).handlers/boot.tssplit into four files by concern. The original 1661-line module mixed payload types, derivation logic, text rendering, and the dispatcher. It has been split intobootTypes.ts(250 lines, leaf module owning every shape),bootActionable.ts(650 lines, all derivation — actionable transitions, suggested_next, verbs_available_now, overlap detection, computeLastAuthoredWriteAt),bootRender.ts(320 lines, text mode renderer + cross-passage fan-out), andboot.tsitself (370 lines — bootCmd orchestrator only). No behavior change — full test suite passes unchanged (1677/1677). External consumers (suggest.ts,tests/interface/boot.test.ts) keep their original import paths via re-exports ofderiveBootSuggestedNext,BootSuggestedNext, andcomputeLastAuthoredWriteAtfromboot.ts. The leaf-module type layout was chosen specifically to avoid the circular-import shape that would otherwise emerge from "boot.ts holds bootCmd that needs derivation that needs the payload shape that lives in boot.ts."
-
gate bootladder thickening — lore stats + alternative next steps. Two surface improvements to the orientation payload:lore_statsis now part of the boot payload (JSON:{ available, principles, traps }; text: alore: N principles, M traps (gate lore list)line belowqueues:). Cold AI agents learn thatgate loreexists at orientation time, without having to grep the verb catalog.- Alternative ladder in text mode: when
verbs_available_now.actionablecarries more than one entry, the renderer surfaces up to two siblings as→ or:lines beneath the primary→ next:. Previously the catalog was JSON-only — callers reading text saw a single suggestion and had to round-trip through--format jsonto see what else they could do. The primary is de-duped from the ladder via verb + id match. JSON shape unchanged.
-
Universal
--explainflag on read verbs. Callers append--explainto a registered read verb to receive a one-line orientation message on stderr describing what the verb returns and which related verbs to reach for. Stdout output is unchanged, so--explaincomposes with--format jsonand shell pipelines — orientation is a sibling channel. Initial coverage:gate boot,gate list,gate pending,gate show,gate chain,gate lore list,gate lore show. Adding more verbs is a two-line change (an entry inEXPLAIN_MESSAGES+ amaybeEmitExplain(args, verb)call afterrejectUnknownFlags). Shaped by principle 09 (orientation- disclosure): instead of waiting for friction to teach, callers can ask for orientation up front at the cost of one stderr line. -
lore/traps/trap_eris_first_override_of_ai_first.md— public trap disclosure. Names the structural pattern where a dogfooder-of-one substrate can silently override its declared AI-agent-first axis through private preference. Establishes that a forward-looking audit is being run (without exposing the audit contents publicly) and treats accumulated weight as the signal to re-evaluate principle 11. One-way link to principles 11 / 02 / 04; principle 11 itself is not edited — the trap is the watcher, the principle is the watched. -
gate loreverb — package-shipped doctrine reader. Lets agents browse principles and traps from inside the substrate without needing to know thelore/directory layout. Subcommands:list(with--type,--applies-to,--relevant-untilfilters) andshow <name>(full markdown body, or--format jsonfor the structured entry). Reads<packageRoot>/lore/principles/*.mdand<packageRoot>/lore/traps/*.md; no per-content_root tier (lore is authored via PR, not by a CLI write). The--applies-tofilter matchesapplies_to:frontmatter on principles; entries without an explicit scope are universal (all) and surface regardless. The--relevant-untilfilter classifies trap frontmatter ascurrent(indefinite or future-dated),expired(past-dated), orindefinite(literal only). -
gate issues show <id>— per-id issue reader. Sibling ofgate show <id>(request) andagora show <id>(play); closes the asymmetry whereissueshad list/add/note/transition but no per-id detail view. Supports--format text|json. Text renders the full body (no list-row truncation) plus a notes block; JSON returns the full issuetoJSON(). Callers wired into muscle memory forgate show <id>no longer bounce when reaching for the issue equivalent. -
gate chain --format text|json. Previously text-only by design — the asymmetry vs every other read verb madechainan outlier for pipeline writers. JSON shape:{root, forward: {issues, requests}, inbound: {issues, requests}}, each item carryingid / found / bidirectional / summary / cross_passage. -
Cross-passage chain resolution (gate → agora). Request-shaped ids (
YYYY-MM-DD-NNN) that miss the gate request store are now probed against the agora play store. Found plays render as→ agora play (game=<slug>, <state>)instead of the previous bare(referenced but not found). Multi-game match (same play id in multiple games) surfaces every candidate. The JSON shape carries the matches under each item'scross_passage.matches[]. -
gate issues <unknown-sub>suggests the closest valid sub. Previously the error was bareunknown issues sub: X; readers reaching forissues showgot no orientation. Now the error includes adid you mean 'gate issues <best>'?hint when the typo has ≥50% overlap with a known sub, plus the full catalog inline. -
gate decisionsverb — director-axis decision audit. Surfaces the approve / deny / execute / complete / fail transitions an actor authored within a window. Decision-shaped sibling ofgate voices(review-shaped) andgate lense-stats(lense-shaped). Defaults--fortoGUILD_ACTORso a baregate decisionsfrom the calling actor's shell answers "what have I decided in the last 7 days?" Dedupes by(request_id, transition)— the #294 slice- closure path adds an extraexecutingstamp ongate complete --by Xwhich would otherwise inflate raw counts. -
gate self-patternverb — actor behavioral bias surface. Aggregates an actor's substrate footprint over a window: decision counts (approve/deny/execute/complete/fail), review verdict ratio (ok / concern / reject), top review lense,approve_rate(approve / approve+deny),ok_rate(ok / total reviews). For the full lense breakdown the verb hints atgate lense-stats --for <actor>rather than duplicating it. Defaults--fortoGUILD_ACTOR. Read-only; no schema change — composes from existingstatus_log+reviewsdata. -
gate lense-statsverb — lense rotation diagnostic (closes #305) (#305). Counts review entries per lense over a window (--since 7ddefault), highlights the most-frequent and least-frequent lense, and surfaces anext:hint when one lense dominates 3× the bottom. -
Per-executor freshness for
gate wave-status(closes #309) (#309). Stale judgment is now per-executor, derived from each executor's most recent attributable activity. Wave-level(stale)in the footer now derives from "all executors stale" (wave_stale_effective). -
gate review-context <id>verb — depth-aware reviewer bundle (closes #310) (#310, builds on #221). Returns the bundle a reviewer needs to drive behaviour from substrate state:action,reason,target,executors,depth,recommended_lenses(shallow→[Logic];standard→ 6-lens default;deep→ all 10 + memory MCP + state-machine + cross-check),prior_reviews. Closes the substrate-to-reviewer signal loop: the--depthfield was previously decorative — set on the wave but read by nobody. Also declareswave-statusinverbs.tsREAD_VERBS, closing a pre-existing gap. -
error.recoverystructured next-step in the JSON envelope. Throw sites with an obvious "use verb X instead" answer can now carry the recovery in a machine-readable slot:error.recovery: { verb: string, args: Record<string,string>, reason: string }. AI-agent consumers dispatch from this directly instead of parsing the proseerror:line. Text-mode runs are unaffected — the prose hint remains the human surface; the structured form is its additive sibling. Wired today:gate fail <pending-id>carries{verb: 'deny', args: {id}, reason: '...'}. Future throw sites can opt-in by throwing the newRecoverableError(exported fromshared/errorEnvelope.ts) instead ofError. Shape mirrorsBootSuggestedNext(verb/args/reason) so orchestrators that already consumeboot.suggested_nextcan reuse the same dispatch path. First step of the "AI-agent- first delight" cluster — seememory/eris_first_overrides.mddefault-pattern section for the design rubric. -
gate next— one-call read-and-dispatch of the top actionable verb. Composes boot's actionable ladder with verb dispatch so an agent loop can chaingate boot && gate next --confirmto drain its queue one transition at a time. Without--confirm, the verb is a read: it prints the plan (verb / args / reason / can_auto_dispatch / command) and exits 0 (or 2 when nothing is actionable — the loop terminator). With--confirm, it dispatches via subprocess so the actioned verb runs through its normal code path (rejectUnknownFlags, hooks, write guards) and propagates the exit code. Auto-dispatch is gated on whether the verb needs only--by(complete/execute/approve/show); verbs needing extra args (review/deny/fail) refuse with a "needs additional args" message and the caller-facing command line to invoke manually. The canonical agent loop iswhile gate next --confirm; do :; done. Second step of the "AI-agent-first delight" cluster (seememory/eris_first_overrides.mddefault-pattern section); pure shape, no eris-specific content.
-
executorsfield in JSON / YAML changed shape for any mutated multi-executor request. Prior to #294 the field serialized as a flat array of name strings:"executors": ["a", "b"]. After the first slice closure (or for any request created post-merge), the field serializes as an array of objects:"executors": [{"name": "a", "status": "completed", "completed_at": "...", "note": "..."}, {"name": "b", "status": "pending"}]. Records that have never seen a slice closure (legacy pre-#294 files that are read-then-resaved without mutation) still emit the legacy flat shape — byte-stable round-trip is preserved by a sentinel-rule (status: 'unknown'on every entry collapses the serializer to the legacy form).Migration for JSON consumers (
jqpipelines, dashboards, third-party scripts):- jq '.executors[]' + jq '.executors[] | (.name // .)'
The same expression works against both legacy flat records and post-#294 structured records.
-
Per-executor slice closure on multi-executor requests (closes #294 — PR-A2 schema migration) (#294, design lock at #304).
gate complete --by Xandgate fail --by Xare now per-slice operations on multi-executor waves. Each executor's slice has its ownstatus(pending/completed/failed/unknown) andcompleted_atstamp; the wave-level state transitions only when every assigned executor's slice is terminal (any-fail-wave-fail is the phase-1 default — PR-C will replace it with template-bound policy via #235 phase 2).- Domain (Request entity):
executorsgetter signature unchanged (readonly MemberName[], 55+ existing call sites keep working). New surface:executorRecordsgetter, plusexecutorStatus(name)andcompleteSlice/failSlicemethods.Request.complete(by)auto-routes tocompleteSlicewhenhasExecutor(by)is true; falls back to direct wave transition otherwise (pre-#294 record compat). Note: the design doc (#304) sketched a rename fromexecutorstoexecutorNames; PR-A2 declined to follow that and kept the existing getter intact to avoid 55+ call-site sweeps. Will revisit if a structural reason for the rename surfaces. - Persistence: byte-stable round-trip is preserved by a
sentinel rule — when every executor record has
status: 'unknown'(= legacy hydrate of a pre-#294 record), the serializer emits the legacy flat array form (executors: [a, b]). Any structural mutation (a singlecompleteSlicecall) flips the record to structured-form output on the next save; the migration is one-way per the design doc. - Hydrate tolerance: three input shapes are accepted —
structured array of
{name, status, ...}, legacy flat[name, name], single-stringexecutor: name. The first two are tested via existing fixtures; legacy records that never re-save stay byte-identical forever. - Interface (
gate complete/gate fail): a slice-closing call that leaves other executors open emitsslice closed: <id> by <by>plus a list of remaining open executors with their current status and a "next: each remaining executor must rungate complete --by <name>to terminate the wave" hint. When the call closes the last slice, the historicalcompleted: <id>output is preserved. - Typo safety (miki concern #1 from #304 review): if a wave
has assigned executors and the
--byactor is not among them, the verb refuses with exit 1 instead of silently stamping the wave terminal. Closes the multi-executor wave-fail-on-typo accident path. gate wave-statuspivot: reads structured form directly. Per-executor lines surface[pending]/[completed]/[failed]/[?](legacy unknown). Witness-inference (v1) remains as the freshness fallback until #309 lands per- executor freshness.- 22 new tests across domain + interface layers; 1509/1509 pass.
- Domain (Request entity):
-
Template registry — two-tier resolution with built-in templates shipped (closes #302) (#302). Wave-brief templates now resolve from two sources, with the user override always shadowing the built-in of the same name:
- Built-in — packaged with guild-cli at
<packageRoot>/templates/wave-brief/. Five templates ship out of the box (single-impl,parallel-impl,research-wave,verification,compare-and-ratify) — a fresh install / public-repo clone now sees a populatedgate templates listinstead of an empty registry. - User override — per-instance customization at
<content_root>/data/guild/templates/wave-brief/(unchanged path). A file there with the sametemplate_nameas a built-in replaces the built-in entirely for that instance (no merge; override wins).
gate templates list(text) tags each entry with its source ([built-in]/[content_root]).gate templates list --format jsonaddssourceper entry and_meta.builtin_dir/_meta.builtin_exists.- Structural cleanup: the previous one-off
data/guild/templates/wave-brief/at the repo top — which was a<content_root>convention path masquerading as a tracked distribution asset — moves totemplates/wave-brief/(a proper packaged asset).package.jsonfiles:addstemplatesso npm publish ships them. The top-leveldata/directory no longer holds tracked content. - Backwards-compatible: existing instances with content_root overrides keep working unchanged; the override path is unchanged and still wins precedence.
- Built-in — packaged with guild-cli at
-
gate resume --with-doctor [--auto-repair]— surface substrate health at session re-entry (closes #306) (#306). New flags on the existinggate resumeverb that fold agate doctorsummary into the resume payload (doctor.findings,doctor.summary,doctor.is_clean). With--auto-repair, quarantineable findings are processed inline via the repair use case and the outcome is reported underdoctor.auto_repair. Default behavior is unchanged — without--with-doctorthe JSON shape and text prose are byte-identical to pre-#306. -
gate flow-suggest— advisory verb for picking a flow shape (closes #307) (#307). New read verb:gate flow-suggest --severity <low|med|high> --area <s> [--scope <s>] [--format json|text]. Maps the (severity, area, scope) tuple to a recommended flow —fast-track,direct-pr, orfull-request— and returns the reason plus alternative options. Pure advisory: no substrate writes, no state. Rule engine inapplication/request/flowSuggest.ts; future v2 can wrap it with aguild.config.yamloverride without touching the CLI surface. -
gate wave-status <id>— per-executor in-flight slice status (closes #295) (#295). New read verb that composes per-executor view from existing substrate fields (witness notes + claim state + status_log timestamps) — no schema change needed for v1. Sibling ofgate boot's cross-request overlap surface (#234): boot is actor-axis cross-wave, wave-status is wave-axis cross-actor.- Works on any state (pending / approved / executing / completed / failed / denied).
- Single-executor renders a compact one-line form so the verb is not useless for the common case.
- Multi-executor renders a per-executor block with witness note + claim state + last attributable write.
- Age-threshold disambiguation (per #295 acceptance):
< 5 minsuppresses any "no progress" warning (fresh wave — witnesses may simply be incoming);5-30 minneutral note;≥ 30 minno attributable activity surfaces⚠ stale — no in-flight progress note recorded— the ceremony-swarm failure mode (a wave with executors named who never landed any substrate-side activity). - Forward-compatible with #294: v1 uses witness-inference; when
#294 ships structured
executors[].status, this verb pivots to read that field directly without breaking the JSON shape. gate schemalists the verb undercategory: 'read'with full input/output schema; the keys snapshot test surfaces the addition forward-compatibly.
gate bootnow surfaces enrichment failures viawarnings: string[](combo C3 / silent-fallback-loses-signal). Pre-fix, four enrichment paths (issues count, inbox unread, unresponded concerns, content_root_health probe) caught their own errors silently —// may not be configured — non-fatal— so a broken issue repository or a partly-quarantined inbox passed through invisibly:status.open_issues = 0looked identical whether there were genuinely zero issues or the repo was unreadable. Devil's concern2 on PR #105 (agent-first-session 2026-04-16-0001) flagged this; 2026-05-10 dogfood (substrate/agora/plays/eris- dogfood-0510/) re-surfaced it. Newwarnings: string[]field onBootPayload(sorted into the keys snapshot per 0.x stability); each silent catch now pushes a descriptive single-line entry includinge.message. Empty array = clean case (no extra noise). Text format adds a⚠ N warning(s) raised...block when non-empty with a one-line disclaimer naming which downstream values may be inaccurate. Three regression tests pin: clean-empty / broken-repo- surfaces / message-includes-error-detail.
-
hook bus extended for session-boundary events (#290 — closes #290) (#290). The Phase 1 hook bus (#259) was request-shaped only —
HookContext.requestwas a non-optionalRequestand the event union covered only the six request-lifecycle verbs plus review. Phase 2 verbs (gate rest/gate wake/gate farewell, #261-#263) now firebefore:/after:hooks via a newctx.sessionEvent: SessionEvent | undefinedfield alongside the existingctx.request: Request | undefined. Exactly one of the two is populated per invocation — picked by which verb fired the hook. The event union grew six entries (before:/after:rest,wake,farewell). Abefore:veto on a session-boundary event blocks the YAML write, so a vetoedgate restleaves no record on disk (substrate untouched). Theafter:fires post-save with the final id allocated. Existing plugins that readctx.request.Xdirectly continue to work for their subscribed events; multi-axis plugins discriminate on which subject is populated. Migration guidance indocs/POLICY.md§ "Hook subject migration"; updatedexamples/plugins/hooks/audit-log.mjsshows the branching pattern;policy-no-self-approve.mjsgot a defensive null-check for forward-compat. Plugin schema doc updated with aSessionEventreference section. -
plugin schema doc drift detection (#283 — closes #283) (#283). New CI test
tests/docs/pluginSchemaDocSync.test.tsenforces sync betweendocs/plugin-schema.mdand the actualRequest/Reviewclass surfaces. Adding aRequestgetter without updating the doc fails CI with an actionable error pointing at the section to update; removing or renaming a getter without updating the doc fails the same way. Staler.Xreferences in theextra.reviewdoc section are also caught. The doc carries an## Intentionally undocumented Request gettersallowlist for internal-only getters (loadedVersion/currentVersion/mutationSeq); new getters must land in either the doc tables OR the allowlist — silently skipping is not allowed. Implementation reads the.d.tsemitted bytsc(no extra dependencies) and skips method calls (e.g.request.toJSON()) since the v1 contract is getter-only.
-
gate fast-tracknow fires lifecycle hooks (#279) (#279). Pre-fix,reqFastTrackcalled the use cases directly (create → approve → execute → complete) and skipped every handler-level hook fire. Audit-log plugins (after:approve,after:execute,after:complete) observed zero events for self-flow waves; policy plugins (before:approve, ...) were silently bypassed — the exact path those policies were designed to govern. Post-fix: each of the three sub-transitions runs its before/after hooks identically to the multi-step path. A before-hook veto aborts the chain and leaves the substrate in the pre-veto state (no synthetic "fast-track aborted" state — same contract as the multi-step path). Three new regression tests pin the fires + veto semantics. Discovered via dogfood walkingexamples/plugins/. -
strict-mode review hydrate (#134 H2 hotfix) — review records written under
gate.strict_lenses: true(using a bundled devil catalog lense likeinjection) failed re-read bygate show/gate listbecause the hydrate path only knew the (narrower)config.lensesallowed-set.Review.restorenow uses a permissive lense parser (parseLenseLoose): the allowed-set check fires only on fresh writes, not on re-reads. Records-outlive-writers — a historical record's lense was already validated at write time, and re-validating against a possibly-changed policy would erase the audit trail. This also closes a pre-existing latent bug where removing a lense fromconfig.lenseswould silently drop older reviews from list output.
-
opt-in strict lense vocabulary for
gate review(#134 H2 — closes #134) (#274). New top-levelgate.strict_lenses: boolconfig (defaultfalse, permanently opt-in). Whenfalse, gate review's allowed-lense set comes fromlenses:inguild.config.yaml— byte-identical to pre-H2. Whentrue, the allowed set is the unified devilComposedLenseCatalog(bundled defaults + content_root extensions from G under<content_root>/devil/lenses/*.yaml). Composes with G: teams that registeredteam-perf.yaml/team-a11y.yamletc. get gate-side vocabulary enforcement on the full custom catalog without touching devil's coverage discipline.gate review --helpreflects whichever source is load-bearing for the run. Coverage gating (devil's "conclude requires every catalog lense touched") is NOT propagated — strict mode is vocabulary enforcement only. Default is permanently opt-in: we do not flip totrueat v1.0 or any future cut. -
per-content_root lense extension loader for devil-review (#134 G) (#134).
<content_root>/devil/lenses/<name>.yamlnow extends the bundled lense catalog. Each YAML file follows the same shape asLense.createinput (name/title/description/ingest_sources?/delegate?/examples?) and is loaded byComposedLenseCatalogon top ofBundledLenseCatalog.- extend-only, hard-error on collision. A name collision between
a content_root extension and a bundled lense raises
LenseCollisionat startup. Silent override would make older review records ambiguous to re-read (records-outlive-writers). The fix is to pick a distinct extension name (e.g.security-strict). - provenance pinned at write time. Every
Lensecarriessource: 'bundled' | 'extension'; entry records persistlense_source: extensionwhen the lense was loaded from content_root (omitted for the bundled-default common case to keep YAML terse). A future bundled v2 that adds the same name cannot retroactively reinterpret a recorded entry — the record disambiguates itself. - doctor-friendly failure modes. Malformed YAML and schema
validation failures route through
onMalformedrather than crashing startup, sogate doctorcan surface them. devil schema --format jsonnow exposessourceper lense so consumers can distinguish bundled vs extension entries.- H2 (gate-side
gate.strict_lensesopt-in mode) is the next slice and lands as a separate PR; this ship is loader + record annotation only.
- extend-only, hard-error on collision. A name collision between
a content_root extension and a bundled lense raises
--executor(singular) ongate request/gate fast-track(#239). Explicit--executor <name>now emits a stderr notice pointing at--executors <name>and announcing v0.7.0 removal. The implicit--executorfallback ingate fast-track(defaulting to--fromwhen neither flag is given) stays silent — only user-supplied values trigger the notice. Behavior is otherwise unchanged through the v0.6 cycle; the deprecated surface is removed at v0.7.0 cut along with theexecutorJSON alias on rendering. Notice introduced for v0.6.0; removal scheduled for v0.7.0.
- friction bundle — flag aliases / lense help / self-approve discoverability /
stderr warnings
(#228).
Four touch-feel improvements that descend to the standard profile
(sibling to #233 self_approve under swarm):
gate reviewnow accepts--noteas the canonical comment flag (parity with the six other write verbs — approve / deny / execute / complete / fail / fast-track).--commentis preserved as a deprecated alias for back-compat; passing both is rejected as mutually exclusive, and using the alias surfaces a stderr deprecation notice (stderr only, so JSON consumers stay clean).gate review --helpdynamically lists the lenses resolved fromguild.config.yamlrather than the four domain defaults — so a project that registeredsecurity/perf/a11ysees them in help, not just in the post-error hint. Implemented via a new optionalextrasfield onHelpRequestedso other verbs can surface their own dynamic info without bespoke renderers.gate requestemits asuggested_nextline mentioninggate fast-trackwhenever the author and the (sole) executor coincide — the self-wave case where the standard pending → approve → execute → complete dance is overkill. Cross- actor waves keep the standard hint without the fast-track nudge.- dist staleness warning is verified to ride stderr (was already
correct in
bin/_lib/checkDistFreshness.mjs—process.stderr.write, notconsole.log); the new test pins the contract so a regression that flips to stdout is caught.
-
gate request --from-agora <play_id>bridges agora play cliff/invitation into request action/reason (#232). Surfaces the substrate-side link from agora (suspended-conversation passage) into gate (request passage) without forcing the operator to copy/paste prose between two CLIs. Lift policy:action ← invitation(the "what to do next" half of the cliff),reason ← cliff(the "what was happening" half); either flag may still be passed explicitly to override its corresponding lift while the other axis stays bridged. The play id is stamped structurally onRequest.sourceAgoraPlay(YAML keysource_agora_play) sogate chain-style walks see the agora→gate edge without scanning free-form text.--game <slug>disambiguates cross-game play-id collisions (each agora game sequences plays independently per day); passing--gamewithout--from-agorais refused as a flag-shaped error rather than silently ignored. Refusal cases each carry an actionablenext:hint: concluded plays (terminal — open a fresh play or file without--from-agora); playing-but-never-suspended plays (no cliff to bridge —agora suspendfirst); not-found play ids (surfaces the agora content_root the bridge consulted, so a multi-root operator sees which tree rejected the lookup); invalid id shape (names the YYYY-MM-DD-NNN format). Profile-agnostic — works identically understandardandswarm. Byte-stable persistence:source_agora_playis omitted from YAML when unset (every plaingate requestand every pre-#232 record); hydrate tolerates the absence by leaving the field undefined.gate showrenderssource_agora_play: <id>in text mode (next tocreated:) and exposes the same key under the JSON payload. Mutually exclusive with--template(#235) at the interface layer — both supply action/reason defaults, so combining them is rejected with a flag-shaped error rather than silently picking one. -
**
feat(gate templates): wave-brief template registry — list / show / --template skeleton expansion ([#235](https://github.com/eris-ths/guild-cli/issues/235))**. Adds two read subverbs (gate templates list/gate templates show) over a per-instance template SOT at<content_root>/data/guild/templates/wave-brief/, and threadsgate request --templatethrough to stamp three new fields on the request record (template,template_version,gate_required_acknowledged). The skeleton expansion populates--action/--reasonfrom the template's frontmatter when the caller omits them; explicit overrides win and the template stamp survives. Templates are markdown files with YAML frontmatter (template_name/template_version/intended_use/gate_required); malformed frontmatter routes through the conventionalonMalformedwarn-and-skip path so one bad file doesn't take the registry offline. The publicguild-clirepo does NOT ship templates — each guild instance keeps its own brief catalogue undercontent_root, and a missing dir is the legitimate empty-registry case (thelistsurface emits a single-line advisory rather than failing). Profile=swarm gating for parallel-executor waves is in stub form for this phase: whenexecutors.length > 1is supplied without--template, a stderr notice points at the registry andgate templates list, but the request still lands. Enforcement (refuse without--templatefor parallel waves) is the follow-up issue. Pre-#235 records lack all three template fields and round-trip clean (records-outlive-writers, principle 04). Schema declares the newtemplatesverb under categoryreadwith asubcommanddiscriminator; the schema-drift detector treats it as a subcommand umbrella alongsideissuesandinbox. Mutually exclusive with--from-agora` (#232) at the interface layer (both supply action/reason defaults). -
features.self_approve: { allowed | warn | forbidden }— profile-gated self-approve policy ongate approve(#233). Tri-state config gates the case where--bymatches the request author. Three states (not a boolean) because failure modes differ:allowedpasses silently for deployments that actively rely on self-approve,warnpasses with a stderr notice pointing at fast-track (the historical default),forbiddenrefuses with exit 1 and an actionable error naming three recovery paths (gate fast-track, another actor approving, or switching profile / settingself_approve: warn). Profile defaults:warnunderstandard(preserves current behaviour),forbiddenunderswarm(parallel waves require bias-checked approvals — same shape asworktree_required_for_parallel). An explicitfeatures.self_approvealways overrides the profile default so a deployment can opt in/out without flipping the whole profile. Malformed values (non-string, or string outside the enum) surface viaonMalformedand fall back to the profile default rather than rejecting the config — same conservative read pattern other optional fields use.fast-trackis unaffected (orthogonal verb, never gated). Non-self approve is also unaffected regardless of profile/policy — the gate is self-only.- Self-detection is now performed on the canonical actor
representation (case-fold + trim, via
MemberName) rather than the raw CLI argument. Pre-fix, daily typing habits like--by ALICEor--by 'alice 'slipped past the swarmforbiddengate even though the persisted record collapsed both toalice— a critical bypass surfaced by Asteria during cross-review. The same normalization is applied to all handler-side self-detection sites (review's self-review warning, thank's self-thank notice, message's self-message advisory) and to the# verb id: invoked by X on behalf of Ysurface line, so whitespace no longer leaks into the audit trail.
- Self-detection is now performed on the canonical actor
representation (case-fold + trim, via
-
gate witness <id> --by <actor>/gate unwitness <id> --by <actor>for non-exclusive cross-session observation (#244, #226 phase 2). Companion verb toclaim(phase 1): where claim is "I'm working on this right now, do not double-stake" (exclusive, refuses on conflict), witness is "I'm watching this" (non-exclusive — multiple actors can witness simultaneously, never refuses on conflict with a claim or another witness). Re-witness by the same actor is a no-op (idempotent — the array doubles as a set ordered by first registration).unwitnessremoves only the caller's own witness, refusing on a foreign actor (no kick semantics in this primitive). State guard: witness allowed on pending/approved/executing (the live race window — passive observation of in-progress work is legitimate); terminal states refuse and auto-reset existing witnesses to[]on the transition. Pre-#244 records hydrate as no witnesses and YAML stays byte-stable (thewitnesses:field is omitted when empty).gate showsurfaceswitnesses: a, b, cbelow the claim line in text mode, and exposes the structuredwitnessesarray in JSON mode (omitted when empty in both surfaces).- Hydrate dedup: hand-edited YAML with duplicate
witnessesentries is collapsed to first-occurrence on load (matching the domain's set-by-first-registration semantic) and the migration surfaces viaonMalformedrather than passing silently — without this, a singleunwitnesswould leave duplicate names visible onshow.- Terminal-aware
unwitnesserror: when invoked on a closed record (completed/failed/denied) where auto-reset has already cleared the list, the error message names the auto-release behavior and signals "no action needed", distinguishing a benign cleanup pass from a real misnamed--bytypo (which keeps the original phrasing on pending/approved/executing).
- Terminal-aware
- Hydrate dedup: hand-edited YAML with duplicate
-
gate claim <id> --by <actor>for cross-session stake (#226 phase 1). Two concurrent main-session agents that independently pick up the same id (substrate-experiment 5) can now mediate the race by claiming the request before working on it. Same-actor re-claim is a no-op (idempotent —claimed_atis NOT bumped, the timestamp is a "since when" stamp, not a heartbeat); a different actor's claim is refused while one is held; the claim auto-releases when the request reaches a terminal state (completed/failed/denied). Pre-#226 records hydrate as unclaimed and YAML stays byte-stable (the field is omitted entirely when null). Witness/explicit-release verbs are deferred to a follow-up issue — phase 1 ships claim only.
-
witness/unwitness/claimconcurrency safety (#244 Devil REJECT root cause). Pre-fix: these verbs mutated state without appending tostatus_log/reviews/thanks, so the optimistic- lock token (sum of those three array lengths) was non-monotonic across them. Two concurrentgate witnesscalls both passed the lock check on identical pre-state and last-writer-wins atomic rename silently dropped one of the two writes — Devil's repro showed 4 parallel witnesses landing exactly 1 entry, parallelunwitnesslosing the loser, andclaim ⊥ witnessmixed races destroying the #244 core promise that "witness coexists with any claim". Fix: eachclaim/witness/unwitnessmutation (and every per-actor terminal auto-reset) bumps a newmutation_seqcounter on Request, andcomputeVersionincludes it in the optimistic-lock token; concurrent writers now throwRequestVersionConflictand the use case (RequestUseCases.{claim,witness,unwitness}) retries the load → mutate → save loop on conflict (bounded, with stagger). Idempotent re-claim / re-witness by the same actor does NOT bump (no-op, save skipped). Per-actor accounting on terminal auto-reset means a frontier collapsing "claim + 3 witnesses" contributes+4so the seq delta remains a faithful "how many actors were mediating" signal. YAML byte-stable:mutation_seqis omitted when 0, so pre-#244 records and never-mediated post-fix records round-trip identically. -
darwin path-comparison flakes in
tests/{interface,passages}/(#238). Twelve tests acrossboot,doctor,register, andagora new/playasserted on absolute paths derived frommkdtempSync(os.tmpdir())vianew RegExp(escapeRegex(root)). On macOS,os.tmpdir()returns/var/folders/...but child processes resolve their cwd through the/var → /private/varsymlink, so subprocess output names the/private/var/...form and the regex^...$anchors miss. Fix: wrapmkdtempSync(...)results inrealpathSyncat the test bootstrap so the root used in assertions matches what spawned subprocesses emit. Test-infra only — no production code touched. -
optionalOptionrejects bare flags (--depth,--executor,--state, ... with no value). Pre-fix, an optional flag passed without a value silently fell through to the env fallback / undefined. Surfaced in v0.5 dogfood:gate request --depth(intending to set the value but forgetting to type it) silently created a request with no depth. Same fail-open classrequireOptionalready protects against;optionalOptionnow mirrors the shape:error: Missing --depth value. (If your value begins with "--", use --depth=<value> or place "-- <value>" after the other flags.)Affects every callsite —
--executor,--target,--state,--game,--note, etc — uniformly. Bare-flag short-circuits the env fallback path so a typo can't be silently overridden by ambient GUILD_ACTOR. -
Inbox text-mode surfaces
(unread, expects response). Phase 1 of #220 (PR #222) stampedexpects_responseon inbox YAML and surfaced it viagate boot'ssuggested_next, but the inbox text rendering only said(unread)for both flagged and unflagged broadcasts. A human scanning the text inbox couldn't tell them apart without re-running with--format json. Now the marker shows inline:1. [...] broadcast from nao (unread, expects response) audit needed 2. [...] broadcast from nao (unread) FYI onlyRead state suppresses the marker (Phase 1 ack proxy: read drains the surface). Same principle-09 disclosure shape — visible at the surface that emits it, in both text and JSON.
-
gate show --format textlabel rename:executor:→executors:(#230). text mode の label が常に複数形になる (単数 wave でもexecutors: miki)。 人間スクリプト (grep '^executor:',sed 's/executor: //', etc.) を破壊するため BREAKING として明示。- Before:
executor: miki(単数 wave のとき) /executor: miki, leysia(複数のとき、暫定 v0.5 形式は未存在) - After:
executors: miki/executors: miki, leysia(uniformly plural) - Migration: 新 label に追従するか、構造化 consumer は
--format jsonのexecutorsarray key を使うこと。永続化 YAML 側の field rename (executor→executors) は同じ release で起きるが、read-only の legacy hydrate により旧 record は透過に load される (詳細は docs/storage-format.md §Request の Hydrate tolerance)。
- Before:
-
gate list(no--state) now defaults to--state all(#218, closes #217). Pre-fix the no-flag call exited 1 with a hint that disclosed the--stateenum + theallsugar. The hint was good but the exit-1-on-muscle-memory-call posture was inconsistent with every sibling list verb (agora list,devil list,gate issues listall "just work" without flags). A v0.5 dogfood pass surfaced this as the last remaining touch-feel asymmetry across the four passages.- Before:
gate list→ exit 1, hint on stderr. - After:
gate list→ exit 0, every request across every state, identical togate list --state all. - Migration: callers that explicitly passed
--state pending(or any other state) are unaffected. Callers relying on the exit-1 + hint behaviour (likely none — this was a discovery affordance, not an API) should migrate togate list --state pendingif they wanted the pending-only triage view, orgate statusfor the count snapshot. The hint itself moves togate list --help(the runnable-example surface from PR #163), out of the hot path.
- Before:
-
gate request --executors a,b,crecords multiple executors per request (#230). parallel-impl wave 等で複数の executor を attribution として 正しく保存できるようになる。各要素は member name regex (/^[a-z][a-z0-9_-]{0,31}$/) で個別検証、重複・空文字は loud に reject。--executor単数 flag は後方互換 alias として継続するが、--executorsと同時指定すると排他エラー。永続化 YAML はexecutors: [<name>, ...](array) で書かれ、単数 wave でもexecutors: [miki]の形を取る (uniformity)。pre-#230 のexecutor: <single>record は read 時に透過 hydrate され、 次回 save 時に新形式へ rewrite される (in-place migration なし)。 substrate-experiment 実験 6 で実証された並列実装 attribution race を構造的に解く修正。 -
gate issues promote --executors a,b,csymmetric with request (Devil concern follow-up from #230). promote 経路でも複数 executor を一発で記録できる。--executor単数 alias は同様に継続。 -
gate request --depth shallow|standard|deepreviewer-depth advisory (#221 phase 1 substrate slot). Substrate-experiment 実験 2 で発見した sharp edge への対応: Devil agent が常時最大深度で grep + 検証するため、極小実装に 対しオーバーレビューが構造的に発生していた (PR #207 の事例)。--depthflag をgate requestに opt-in で追加し、enum[shallow, standard, deep]で reviewer 側 (Devil agent) に advisory を渡せるようにする。shallow: surface 点検、grep 自走しないstandard: 現行と等価 (default、互換)deep: arch / threat-model まで掘る- substrate slot のみ: agent 側の prompt 3 段階化は operator/agent setup 側で対応する (本 repo の外)。 principle 02 (advisory-not-directive) のとおり、reviewer は depth を尊重する義務を負わず、substrate は signal を carry するだけ。
- byte-stable:
--depth省略時は YAML にdepth:フィールド を書かない。pre-#221 record と round-trip-byte-同等 (principle 04: records-outlive-writers)。 - hydrate tolerance: 既存 record ですべての older 形 (
depth:なし) が clean に load される test。 - Schema input + drift detector update + advisory-framing description (principle 02 / 10)。
-
gate whoamihonours--format json|textwith a typed payload. whoami was the lone read-shape verb without--formatsupport — every sibling (status/board/list/show/voices/tail/doctor) accepted both formats, but whoami declared only--limitand was text-only. That meant the principle-09actor source: ...disclosure was reachable to agents only via regex parsing of prose. JSON now emits{actor, role, display_name, actor_source, recent_utterances}with the schema'soutputproperties fleshed out to match (previously declared as{type: "object"}, contributing to the schema-as-contract gap that principle 10 names). Error path (missingGUILD_ACTOR) returns the standardok: falseenvelope in JSON mode so orchestrators don't switch parsers based on outcome. whoami joins the format-symmetry CASES; new focused JSON-path tests cover env / file / missing-actor / invalid-format. -
bin entries set stdio blocking before dispatch. Pipe-targeted
process.stdout.writeis async; on--format jsonpayloads above ~8 KB (notablygate schemaonce whoami's output shape was fleshed out, but every verb is exposed to the same race) the trailingprocess.exit(code)would cut the tail of the JSON envelope, surfacing asUnexpected end of JSON inputin spawn-based tests and non-tty consumers. All 5 bin entries (gate/agora/devil/ctx/guild) now flipprocess.stdout._handle.setBlocking(true)(and stderr) before dispatch — tty writes are already synchronous so this is a no-op on interactive runs. Latent bug surfaced by the whoami schema-as-contract fix above pushinggate schemapast the threshold. -
Format-symmetry contract test (principle 11 enforcement). A v0.5 dogfood pass found that
gate statusandgate doctorsilently fell back to text mode for unknown--formatvalues (the inverse symmetry — every--format-aware verb must reject unknown values uniformly — had no CI guard). New test runs both--format jsonand--format textagainst a representative set of read verbs (gate status / board / schema / doctor / tail, agora list / schema, devil list / schema), asserts both succeed and produce parseable output, plus a negative pass that pins rejection of--format yaml. Two handler fixes shipped together:gate statusandgate doctornow validate--formatat entry (matches the existing pattern used by board / list / schema). -
Schema ↔ handler flag-coverage CI test.
<cli> schema --verb <name>is the agent-dispatch reflection layer; the handler's*_KNOWN_FLAGSset is what the CLI actually accepts viarejectUnknownFlags. There was no CI guard against the two drifting. New test enumerates every verb across the 5 passages via<cli> schema --format json, parses the handler's known-flag set from the unknown-flag error message, and asserts set equality (after filtering positionals — those carry adescription: 'positional; ...'marker in devil's schema and are absent from agora's). Sub-dispatchers (gate issues,gate message) are skipped: their schema models asubcommandenum rather than a flag set. A floor-check verifies at least 3 verbs per CLI passed the comparison so a regression in the unknown-flag error shape doesn't silently empty the loop. -
gate whoamidiscloses actor source provenance. WhenGUILD_ACTOR(env) and.guild-actor(file) both produce a valid identity, the resolved actor looks the same on the surface but the path that produced it carried different signals (env from shell vs. file dropped into the tree by a colleague). Newactor source: GUILD_ACTOR (env)/actor source: .guild-actor (file)line underyou are Xcloses the orientation gap. Sibling helperresolveGuildActorWithSource()exposes the source as a typed field for other surfaces that want it. Mirrors principle 09 (orientation-disclosure) — when two valid configurations produce the same value, name which one was used.
-
gate show --format text: labelexecutor:→executors:(#230). uniformity: 単数 wave でも複数形executors: mikiで表示される (record の arity に関わらず)。grep / sed で^executor:を expecting する 外部スクリプトはexecutors:に追従が必要 — 移行先はexecutors:を grep するか、--format jsonのexecutorsarray key を使う。BREAKING 節にも明示。 -
gate show --format jsonaddsexecutors: <array>key (#230; canonical shape). 既存のexecutor: <string>key も back-compat として first-of-list 値を 1 リリース継続 emit する (consumer の段階 移行のため)。executorJSON key は v0.7 で削除予定。 consumer はexecutorsarray に migrate すること。 -
gate list --executor <name>matches against the array. (#230). 1 人でも array に含まれていれば match。flag 名は--executor単数のまま (filter は単一 name 検索なので、複数指定の必要なし)。 -
Actor-flag missing-arg errors unified across agora / devil / ctx to the #164
requireOptionshape. A v0.5 dogfood sweep found 16 callsites still using the pre-#164 pattern (optionalOption+ manual--by required (or set GUILD_ACTOR)stderr write) — the gate sweep had been thorough but agora / devil / ctx kept the older bespoke message. Now every actor-flag error readsMissing --by <m> (or set GUILD_ACTOR).regardless of which passage emitted it. Net deletion of ~90 lines (onerequireOptioncall replaces six lines of manual handling). Tests asserting the prior/--by required/shape updated to/Missing --by/.
-
Partial-dist-staleness detector at bin entry. The existing guard at #139/#140 catches "dist not built at all" but not "dist exists, but lags src after a
git pull". A v0.5 dogfood pass hit the lag case —agora lastreturnedunknown verbbecause dist held the pre-#179 dispatcher. New shared helperbin/_lib/checkDistFreshness.mjs(plain ESM, lives outsidedist/to avoid the circular trap) compares newest src/*.tsmtime against newestdist/src/*.jsmtime, with a 2-second grace for fs clock skew. Emits a single stderr notice and continues — staleness is dev-loop friction, not a correctness failure (a stale dist often runs fine for the verbs that didn't change). Silent when src/ is missing (installed-via-npm shape). Wired into all 5 bin entries (gate / guild / agora / devil / ctx). -
agora list --state allanddevil list --state allaccept the cross-passage sugar. A v0.5 dogfood pass surfaced a touch-feel asymmetry:gate list --state allandgate issues list --state allboth acceptallas sugar for "every state, no filter" (#170), but the siblingagora listanddevil listerrored:agora: error: --state must be one of playing|suspended|concluded, got: all devil: error: review state must be one of open, concluded, got: allSame friction class #170 closed for
gate— muscle memory between sibling list verbs across passages was breaking on theagora/devilboundary. Both now accept--state alland treat it as "no filter" at the interface layer; the domain enums are unchanged (the sugar is a CLI affordance, not a state). agora's invalid-state error now also names|allso the option is discoverable from the failure itself. -
gate boot --since <ISO-timestamp>delta surface. Token-cost lever for long sessions: agents that re-boot mid-flow now pass the previous boot'slast_activity(or any ISO-8601 UTC timestamp the substrate would emit) and receivetail,your_recent, andinbox_unreadfiltered to entries strictly newer than the cutoff. Validation is strict — the value must matchYYYY-MM-DDTHH:MM:SS(.sss)?Zso the echoedsincefield is always directly comparable to substrate timestamps; malformed input is rejected with anext:hint pointing at the previous boot'slast_activityas the canonical chain value. Two invariants preserved across the filter: (1)status.inbox_unreadSCALAR reflects the TRUE unread count, not the filtered slice, so the orientation counter never undercounts; (2)last_activityitself is NOT filtered, so the next boot can chain--since=$last_activitywithout a second read. AI-agent-first delight cluster #2; pure shape (rubric tier-1), no eris-specific content. (#37x) -
gate complete --cliff <s>forward-pointing close note + bootpast_cliffssurface. Zeigarnik continuity for gate: a completing actor can stamp "next agent should..." prose onto the terminal request, andgate bootresurfaces those cliffs the next session underpast_cliffsfor the authoring / executing actor. Mirrors agora'scliff/invitationsemantic across passages but ports only the forward half —--notealready covers "what happened," so the asymmetry is intentional. v1 scope: completed-only (fail/deny carry forward intent in their reasons already). Persistence: cliff lands on the terminalstatus_logentry and projects to the top-levelclifffield on the JSON envelope (mirroring thecompletion_noteprojection pattern). Hydrate is strict — a cliff stamped on a non-completed entry is dropped viaonMalformedrather than silently accepted. Boot surface:past_cliffslists the actor's newest 5 cliff-stamped closures (authored OR executed), filterable by--since, null when no actor is resolved (global-view boot has no self to attach "past selves" to). AI-agent-first delight cluster #6; pure shape (rubric tier-1), no eris-specific content. Text-mode boot adds apast selves left these cliffs:section that renders only when entries exist (principle 13 voice budget). (#37x) -
Voice plugin infrastructure: ornamental-voice surface as the second dogfood validation of principle 15. Introduces a third plugin kind (
plugins.voices) alongside the existing verb/hook plugins. A voice plugin attaches OPTIONAL personality narration to write-verb JSON envelopes via a new_meta.voicefield, distinct from the doctrinal voice held in handlers (principle 08 — that voice is intentionally NOT pluggable). Two-layer model preserved by construction: doctrinal voice carries lore inmessage/suggested_next.reason/ schema descriptions; ornamental voice carries personality and CANNOT replace doctrinal prose. Stripping_meta.voicefrom a pipeline loses zero information — that invariant is what keeps the two layers honest. Activation:GUILD_VOICE=<name>env picks one loaded plugin by name; unset or no match → no_metafield emitted (silent miss, never error). Plugin shape:{name, verbs: {<verb>: [{when, template}, ...]}}withwhen ∈ {default, cliff_present, cliff_absent}and{id} / {action} / {by} / {cliff}template variables sourced from the substrate (voice cannot invent facts). v1 scope:gate completefires voice on wave-terminal completion only; slice-only closes do not emit voice (narrating a slice as "completed" would be a false claim). Trust model: sharesplugins.trusted: trueconsent gate with verb/hook plugins. Doctor surfaces voice plugin load failures asarea: 'plugin'findings, same channel as the other plugin kinds. Text-mode renders ornamental voice on stderr ((voice: …)line) so JSON-piped consumers stay clean. (#37x — cluster #1) -
Voice plugin extension: all write verbs now fire ornamental voice. Sibling PR to the infrastructure landing — voice templates may now be authored against
approve/deny/execute/complete/fail/review(the full write-verb surface). Template variables{note},{verdict},{lense},{comment}join the existing{id}/{action}/{by}/{cliff};{by}resolves to the just-appended review's reviewer onreview, the terminal status_log entry's actor everywhere else (matches a human reader's intuition: "by" means the actor of the salient event). Newwhenpredicates:verdict_ok/verdict_concern/verdict_reject(review verb),with_note/without_note(every write verb).failmirrorscomplete's honesty rule — voice fires on wave-terminal only; slice-only closures are not narrated (per-slice failure is not the wave failing). Doctrinal voice (message/suggested_next.reason) unchanged across every verb — the augment-not-replace invariant carries from PR 1 unchanged. (#37x — cluster #1 continuation) -
gate schema --voice <name>voice-flavored description overlay. Voice plugin shape gains an optionalschemasection carrying per-verbsummary+ per-flaginput.<flag>description overrides;gate schema --voice <plugin-name>overlays them onto the doctrinal schema before emitting. Augment-only — any field a voice does NOT override falls through verbatim, so principle 08 (voice-as-doctrine, doctrinal voice held in handlers) is preserved as the substrate floor. Unknown voice name → silent miss (no error, no overlay), mirroring the_meta.voicesemantic on write envelopes. Activation is per-invocation (a flag, not env) so a reader can switch voices without exportingGUILD_VOICE— useful for AI agents that want to inspect what eris's schema reads like before committing to that voice for the whole shell. (#37x — cluster #5, second half of the voice-plugin landing pair) -
gate voicemode-switch verb + 4-layer voice resolution. Lowers the burden of switching between voice plugins ("今この気分") from "edit env / config" down to a one-keystroke verb. Resolution order (least → most specific):config.voice.default < <content_root>/.guild-voice file < GUILD_VOICE env < per-invocation --voice flag. The new.guild-voicefile (single-line voice name) is the cwd-stable middle tier — written bygate voice <name>, cleared bygate voice off, inspected by baregate voice. Introspect output surfaces the active layer and emits a hint when a higher-priority layer is masking the file ("$ unset GUILD_VOICE"). New optionalvoice.default: <name>inguild.config.yamlprovides the deployment-baseline lowest tier. Set is permissive on whether the named voice is currently loaded — mirrors the silent-miss contract established by_meta.voiceon write envelopes. Lock-exempt (the marker file is config-shaped, not substrate-data-shaped, so the content-root lock would be over-conservative). (#37x — eris-first refinement, follow-up to the AI-agent-first delight cluster) -
Voice plugin
essentialssection +gate --help --essentialscuration. Voice plugin gains an optionalessentials: {verbs: [...], note?: string}section carrying a per-mode curated verb list — the verbs the voice (and the operator wearing it) considers load-bearing for daily work.gate --help --essentialsreads the active voice (resolved via the 4-layer order shipped alongsidegate voice) and renders only those verbs, orthogonal to the profile-driven BASE/COORDINATION/EXTRA tiering. The banner names the curating voice and surfaces the optionalnoteso readers see whose curation they're looking through. Silent fallback when no voice is active or the active voice has no essentials section —--helpfalls back to the profile tier. Mode IS the curation:gate voice devilswaps to a devil-mode essentials (review / deny / fail emphasis) in one keystroke. (#37x — eris-first refinement PR-D, builds on thegate voicelever) -
gate bootrefinement:past_cliffsvoice re-rendering +--since-last-minesugar. Twogate bootupgrades bundled as one PR — both refine the boot text-mode surface for an eris-first Zeigarnik experience.Voice plugin gains an optional
read.past_cliffs: {header?, entry?}section. When the active voice (resolved via the 4-layer order from #380) carries either template, boot's text-modepast_cliffssection re-renders through it:headermay use{count},entrymay use{id}/{action}/{cliff}/{closed_by}/{closed_at}. Doctrinal voice fallback when no voice / no read section / no template — the augment-not-replace invariant carries from the write side (principle 08 unchanged). JSON mode is untouched; structuredpast_cliffspreserves the data shape for orchestrators that compose their own narration. The 「過去の私からの手紙」 framing this section was always trying to surface now actually reads as a letter when an eris voice is wearing it.Boot gains
--since-last-mine: sugar for--since=<actor's last authored write>. Internally resolves viacomputeLastAuthoredWriteAt(GUILD_ACTOR)and applies the same delta-filter semantic shipped in #375. Mutually exclusive with--since(usage error to pass both). RequiresGUILD_ACTOR; without one, errors with anext:hint. Actor with no prior writes → cutoff stays null and boot returns the full snapshot (correct first-time-here behavior). Removes the agent-side read-last-activity-then-pass-it-back-as-ISO loop ─ the substrate owns the resolution. (#37x — eris-first refinement PR-B + PR-C bundled) -
Voice polish bundle: dogfood-driven 4-fix pack. Touch-feel pass through the cluster shipped in #380-#382 surfaced four refinements; bundled here so they ship together.
P0
gate voicemasking hint fires only when an env source is winning. The pre-fix code emitted "higher-priority layer in effect" on any non-file resolution includingconfig, butconfigis the BOTTOM layer — nothing was masking it, so the hint was misleading. Hint now keyed tosource === 'env'only.P1
gate fast-trackfires ornamental voice on itscompletesegment. v1 wired voice throughgate completedirect only; fast-track is the daily-use shortcut and was silent on the most common write surface. Same template (verbs.complete) fires on both paths now — eris's voice doesn't go quiet on the chained flow.P2 text-mode voice marker changes from
(voice: …)to⟶ …(U+27F6). The parenthetical form read as a debug label during dogfood; the arrow signals "voiced echo of what just happened" without the hedge. stderr-only as before, so JSON-piped consumers stay unaffected.P3
gate --help --essentials --compactrenders one usage line per verb instead of the full multi-line entry. Multi-line was defensible for tier rendering (BASE/COORDINATION/EXTRA where the audience is "everyone") but contradicted the essentials framing ("the verbs I reach for, at a glance").--compactis opt-in; bare--essentialskeeps the existing multi-line output, and--compactwithout--essentialsis a noop on the tier surface.Existing voice envelope assertions updated for the new marker. (#37x — eris-first refinement polish PR, dogfood-driven)
0.5.0 — 2026-05-06
substrate self-improvement wave (#207 / #208) closes the loop: guild-cli's own
gatesubstrate was used to design and ship two guild-cli improvements — meta-experiment that doubled as a v0.5 hardening pass.
gate bootsurfaces authored requests with new reviews (#207). Adds a fifthActionableKindreviewed-authored(PRIORITY=4) lifted via a derivedlastAuthoredWriteAt(actor)boundary aggregatingstatus_log[].at ∪ reviews[].at ∪ thanks[].atper actor. Zero persistence, READ/WRITE invariants intact, host/member treated uniformly (string actor space). Designed entirely via the guild-cli substrate atdata/guild/(Noir v1→Devil reject→Noir v2→Devil concern→Noir v3→Devil ratify→Miki impl).<write-verb> --helpbypasses the lock (#208). Detectsargs.options.helpafterparseArgsand routes dispatch aroundwithEntryLockso help is never gated by a contended lock. Closes #200.- Wave 8 (#155 lock landing) + envelope contract (#194 / #205) — full notes below; v0.5 ships the locking substrate, the JSON error envelope contract across all four entries, and the handler-internal catch fix that closes the envelope gap left by #204.
Cross-process write serialization (#155 — landed via #193 as a single squash that combined PR-A and PR-B; docs follow as PR-C). Replaces "serialize at the caller" with
withGuildLock, a content-root-wide single-writer mutex enforced by per-entry middleware across all five CLIs (gate,guild,agora,devil,ctx). The optimistic per-record CAS is retained as an in-process safety net.Convergence path: design (Eris + Noir) → devil-review surfaced 5 ship-blockers (entry coverage, host self-attestation,
buildContainerinvariant, verb-set SOT,doctorchicken-and-egg) → all accepted → implementation (Miki) → real-machine validation (Asteria, 14 scenarios) → final-gate devil pass (zero ship-blockers, 6 follow-ups filed: #194 / #195 / #196 / #197 / #200 / #201).
withGuildLock+withEntryLocklock primitive and middleware (#155, #193).${contentRoot}/.guild-lockviaO_CREAT | O_EXCL, JSON metadata payload,unlinkinfinally. Three reclaim branches: dead pid (kill 0→ESRCHwith ancestor-pid safety valve), pre-boot lock (os.uptime()-derived), andGUILD_LOCK_MAX_AGE_MSenv cap. Engaged by per-entry middleware that consults a per-passageverbs.tsexporting three sets (READ_VERBS/WRITE_VERBS/LOCK_EXEMPT_VERBS); decision order is EXEMPT → READ → acquire-lock (unknown verbs are fail-safe to the lock side). Cross-passage race is exercised in CI bytests/integration/lock/cross-passage-race.test.ts(spawn-based with a shared barrier file viaGUILD_LOCK_TEST_BARRIER, no-op in production).buildContainerinvariant pin tests on each passage (tests/infrastructure/{gate,agora,devil,ctx}Container.test.ts) catch any regression that would put a write side-effect ahead of the lock.LockBusyError(DomainError subclass) — JSON envelope codelock_busyon the gate path; non-gate parity tracked in #194.SECURITY.md§ Concurrent writes rewritten to describe guildLock as the serialization boundary, with the threat-model caveat (writers with content_root access are out of scope) and links to the six follow-up issues.docs/storage-format.md§ Cross-process serialization — on-disk contract for.guild-lock(path, payload shape, reclaim rules, test barrier env) so the format is documented alongside the rest of the substrate.
Touch-feel campaign (5 PRs, 1 design issue). A P3 dogfood pass with fresh-agent eyes surfaced the same friction class repeatedly: "the user knows something is wrong / done / different but not what to do next." Each PR fixes one layer of that gap; together they compound — by the final dogfood, every error / not-found / state mismatch / list-shape touch point answers the next-move question inline. No architectural changes; substrate, domain, and persistence contracts unchanged.
-
Verb-help shows a runnable usage example. (#163) PR #148 made
--helpa universal escape valve and printed each verb's flag catalog. What was still missing was a runnable invocation.gate request --helpnow reads:gate request: --action, --auto-review, --executor, --format, --from, --reason, --target, --with e.g. gate request --action "<what>" --reason "<why>" see `gate --help` for the full verb catalog.Examples live in
src/interface/shared/verbExamples.ts, keyed bycli → verb → string(same-named verbs across CLIs need scoping so anagora list --helpexample doesn't render agate ...command). Coverage tests pin the (cli, verb) pairs that exist; an invariant test asserts every example begins with its own verb name to catch copy/paste typos. -
requireOptionerrors name the value shape and the env fallback. (#164) Pre-fix:error: Missing --from. --from required— the second sentence repeated the flag name without saying what value to put there or thatGUILD_ACTORwould have satisfied the call. Post-fix:error: Missing --from <m> (or set GUILD_ACTOR). error: Missing --persona <red-team|author-defender|mirror>. error: Missing --severity <low|med|high>.The third argument is renamed
usage→shape(the value placeholder); the env hint(or set <env>)is composed in one place and propagates to every actor-flag callsite (~22 of them). All ~50 callsites swept to structured shapes — free text passes"...", members<m>, enums spell their option set inline. -
assertTransitionlists valid next states. (#167) Pre-fix:error: Illegal state transition: pending → completed. Post-fix:error: Illegal state transition: pending → completed. valid next states from pending: approved, denied.For terminal states (completed/failed/denied), the message reads
X is terminal — no further transitions are allowedinstead of emptyvalid next:(which would have been more confusing than the rejection itself). State names are the only domain vocabulary added — verb hints belong in the interface layer and intentionally stay out ofRequestState.ts. -
not found: <id>gains a per-entity discovery hint. (#167) Pre-fix:not found: 2026-05-05-9999. Post-fix:not found: 2026-05-05-9999 try 'gate list' or 'gate tail' to see existing requests.Centralised in
notFoundMessage(entity, id). Three call shapes: request →gate list/gate tail; issue →gate issues list; member →guild list. Wired intogate show,summarize,transcript,why,chain,issues note,guild show. -
gate executenotice when actor differs from--executor. (#169 for #168)--executorrecords intent, not access — anyone with substrate access may rungate execute. Pre-fix, a fresh agent reading--executor bobreasonably interpreted it as a gate; when alice ran execute on bob's assignment, the substrate captured both but the surface gave no signal. Post-fix:notice: alice executed request 2026-05-05-0001 (assigned to bob); --executor records intent, not access.Mirrors the shape of the self-approve notice on
gate approve. Silent when--executorwas never set or matches the actor. AGENT.md cross-links #168 for the design rationale (recorded so future readers see the deliberate choice, not an oversight). -
gate list --state allis sugar for "every state, no filter." (#170) Pre-fix,gate list --state allerroredInvalid state: allwhile the siblinggate issues list --state allalready accepted it — broke muscle memory between the two list verbs. The hint shown when--stateis omitted now mentions| alland includes agate list --state allexample line. -
guildverbs honour--helplike every other CLI. (#166) PR #148 rolled out the universalHelpRequested/rejectUnknownFlags/renderVerbHelpmechanism across gate / agora / devil / ctx, but the operator-helperguildwas out of scope. The 4guildverbs (list/show/new/validate) now honour--helpwith the same shape andguild new --bogusno longer hardcodesguildin the unknown-flag error prefix. -
agora/ctx/devilget the same did-you-mean asgate. (#173) Typo'd verbs across the three passages now surface the closest matches inline rather than dumping the full HELP. Same Levenshtein shape gate already used. Also drops theagora — v0 skeletonlabel from agora's HELP block (alpha v1 has shipped). -
--versionreports a parseable version across all 5 CLIs. (#174) Pre-fix,gate --versionandguild --versionprintedguild-cli <X.Y.Z>whileagora/ctx/devil --versionprinted status strings without a version number — a shell that didagora --version | grep <version>was silently broken. Each passage now prints<cli> (under guild-cli <X.Y.Z>) — <status>so the version is grep-able and the status remains visible. Also:agora newsuggested_nextpoints atplay(the natural next step) instead oflist. -
agora newdefaults--kind sandboxand--titleto the slug;agora move's next-hint stops repeating on every move. (#177)agora new --slug <s>now works without--kindor--title, matching the most common opening shape (Sandbox plays without ceremony). And thenext: agora move ... | agora suspend ...block that previously printed after every successful move was noise once a play got going —moveis now a quiet✓line.
-
Sanitize absolute paths in CLI error messages. (#172, closes #153) Errors from
safeFsand friends carried the full resolved path (e.g./Users/alice/work/proj/substrate/requests/pending/foo.yaml), incidentally leaking home-directory and checkout-location info into logs / screenshots / bug reports. Each CLI's main() catch now collapses the configuredcontentRootprefix to the literal<content_root>token viasanitizeError(new pure function insrc/interface/shared/sanitizeError.ts). The structural tail is preserved so debugging is not impaired. Paths outside contentRoot (/etc,/tmp, user-input absolute paths) are intentionally not sanitized — the threat model is single-user, trusted-environment. -
Close
agora newcollision-error sanitizer hole. (#175) Follow-up to #172. TheGameSlugCollisionbranch returned 1 directly from the handler instead of throwing, bypassing main()'s catch wheresanitizeErrorruns — so itsAt: <path>line still leaked the absolute substrate path. ApplysanitizeErrorat the handler level so this alternate egress matches the contract every other error-path emission already holds.
- DomainError trailing
(field)tag andDomainError:prefix dropped from human-readable error output across all five dispatchers (gate / agora / devil / ctx / guild). (#170) Pre-fix:error: Invalid lense: "bogus" ... (lense)— the trailing tag was design intent at one point ("name which flag was bad") but read as redundant for flag-aligned fields and as debug noise for domain-internal fields ((state),(sequence)). TheDomainError:class-name prefix on agora/devil/ctx/guild also leaked an internal class name. JSON envelopes are unchanged:error.fieldstill surfaces in--format jsonso orchestrators retain the structured info.
Picks up where the touch-feel campaign closed. With
gate/agora/devil/ctxall in active use, the cross-passage orientation surface (gate boot) finally caught up with the 4-passage reality, and agora grew two read-only sugar verbs to answer the daily-use questions ("which play am I in?", "what was I about to do?") that the v1 core didn't directly address.
-
agora lastandagora cliffsugar verbs. (#179) Two read-only verbs on top of the v1 core.agora lastreturns the actor's most recent open play (noagora list+ copy required);agora cliff <id>peeks the closing cliff/invitation of a suspended play without resuming. Both preserve agora's JSON envelope contract. -
gate bootcross_passage now surfaces ctx (4th passage). (#184) ctx records existed on the substrate butbootonly reported agora and devil activity. AddedctxOrientationprovider; ctx in phase 1 is record-only (no transitions), soopenis the count andlast_stateis'recorded'. -
agora list --state suspendedsorts by most-recent suspension. (#182) Among suspended plays, the one suspended most recently shows up first — id-desc (creation date) was the wrong axis when the question is "what's been hibernating, and for how long?". Other states preserve the id-desc default. Re-sort happens in the handler; substrate contract unchanged. -
devil entrylense/persona miss includes catalog + did-you-mean. (#185) Typingdevil entry --lense bogusnow lists the 12 lenses + a Levenshtein "did you mean: --lense X?" hint on near-matches. Same for--persona(6 personas). Hint on stderr — JSON consumers reading stdout unaffected. -
gate bootdefault--taillowered 10 → 5 (principle 13). (#181)bootis the hot-path bootstrap verb; default tail of 10 produced ~250-line JSON. Override via--tail <N>unchanged. Codifies principle 13 "affordance density follows verb shape" added in #180. -
Doc sweep: 4-passage architecture reflected across README/AGENT/etc. (#178) Audit of cross-passage references caught 4 docs still framed around 3 passages. Updated to the current shape (gate / agora / devil / ctx).
A focused pass to defendable v1 surface: a touch-feel friction that surfaced during a fresh-eyes review of the latest agora shape, three v1-prep items from the external deep-dive review (#153 sibling cluster), and one CI-signal-reliability bug.
-
agora lastnext-hint includes--game <slug>;list --state suspendedtext shows suspension timestamps. (#186) Surfaced by a 2026-05-05 main-merge dogfood pass. Bare play-ids collide across games, soagora last'snext:hint produced a call that errored on the very next line. Hint is now--game-qualified, state-aware (playing → move/suspend, suspended → resume/cliff, concluded → no hint).list --state suspendedsurfaces theattimestamp so the post-#182 sort axis is visible. -
Devil-review consistency follow-up:
--gamequalifier across play / suspend / resume next-hints. (#188) Devil review on #186 caught that the--gamefix landed only inagora lastwhile the same hint shape lived unqualified in three other verbs. Same logic applies — extended consistently. Also documentsconcluded-state intent inlast.tsand the serial-execution dependency ofwithCleanCwd(from #187). -
Test isolation:
.guild-actorfile leak in develop-cut PRs. (#187, closes #183) Three env-unset tests inparseArgs.test.tssilently failed in CI when the PR branch was cut fromdevelop— that branch tracks.guild-actorat repo root for dogfood ergonomics, and PR branches inherit it;resolveGuildActor()reads both env AND file, so deletingGUILD_ACTORalone didn't isolate the call. Wrapped affected tests in awithCleanCwd()helper. Regression test pins both the leak observability and the fix. -
docs/storage-format.md— explicit on-disk YAML contract. (#189, closes #157) POLICY.md declared the YAML shapes "stable surface" but never described them. New doc covers all 7Yaml*Repositoryadapters: layout, fields, hydrate tolerance, versioning expressions, backward-compat rules table. Cross-linked from POLICY.md / AGENT.md / README.md. Out of scope: format changes themselves (separate concern, requires minor bump).
- Defense-in-depth prototype-pollution guard in
parseYamlSafe. (#190, closes #154) Hydrate layer was leaning onyaml-lib's promise that__proto__lands as a literal key. Added independent guard at theparseYamlSafechokepoint: walks the parsed tree, drops__proto__/constructor/prototypeliteral keys at every nesting level, rebuilds plain objects viaObject.create(null). NoYaml*Repository.tschanges needed — bracket-index reads work transparently. SECURITY.md known-hardening item now marked mitigated, sibling shape to #153.
- #168 —
--executorrecords intent, not access (closed by #169). Issue preserved in the substrate as the design-rationale anchor.
0.4.0 — 2026-05-03
Three-passage release. Two new CLI passages land alongside
gate:agora(second passage — play / narrative / exploration; suspend and resume as first-class primitives), anddevil-review(third passage — security-backstop multi-persona scrutiny that composes with/ultrareview/ Claude Security / supply-chain-guard). The three-passage architecture is named inlore/principles/11-ai-first -human-as-projection.md; the dispatch shorthand (gate=判断 / agora=探索 / devil=守備) is now in README / AGENT / per-passage READMEs;docs/playbook.mdcollects the cross-passage combos (including the bug-killing recipe). Two example content_roots are preserved as substrate (examples/three-passages-framing/shows agora's full propose→suspend→resume→conclude arc).
-
docs/playbook.md(#131) — practical guide for combininggate/agora/devilon real work. Each section is a recipe: 6 gate-only patterns, 4 agora-only, 4 devil-only, 4 combos (including the bug-killing flow asissue → agora → gate (+ devil if security)), and 8 tips for AI agents. Linked from AGENT.md (top callout), README.md (depth ladder), and docs/concepts-for-newcomers.md (Where-to-go-next). -
判断 / 探索 / 守備 framing (#130) — single-kanji + English shorthand for the three passages added to README / README.ja / AGENT / docs/concepts-for-newcomers + per-passage READMEs. Per principle 11 (AI-first), the framing is a dispatch tool, not a metaphor — AI agents recognize the shape and route to the matching passage.
examples/three-passages-framing/preserves the agora play that captured the framing decision as a real artifact (concluded in #132). -
agora's
src/passages/agora/README.mdrewrote post-merge (was stale "v0 skeleton" claim from snapshot phase; now reflects the v1 surface). Newsrc/passages/devil/README.mdparallel to agora's. Post-merge cleanup pass (#128) removed stale "snapshot / landing soon / v0 scaffold" markers across docs and code.
-
devil-review
--from scgingest: SCG now runtime-enforced as a mandatory delegate. (#126 decision C; e-001 from devil-on-devil dogfood) Previously, the supply-chain lense's "mandatory delegate to SCG" claim in design docs was not actually enforced at runtime —devil ingest --from scgaccepted any well-formed JSON matching the strict v0 shape, with no check that the input came from a real SCG run. A reviewer could fabricate averdict: CLEARJSON and satisfy the supply-chain lense gate without SCG ever running, defeating the floor-raising design intent.Post-fix,
--from scgprobes for thescgcommand onPATHviawhich scg(POSIX) orwhere scg(Windows) before proceeding. If scg is not found, the verb refuses with a structured error pointing at the install link (eris-ths/supply-chain-guard) and naming the design decision (#126 C) and the dogfood finding (e-001) for substrate-as-audit-trail.ultrareview / claude-security ingest paths are unchanged; only the scg path adds the binary check (it's the only source with a named delegate in the lense schema).
Tests: a new test pins the refusal under an empty PATH environment, plus a paired test pins that
--from ultrareviewis unaffected. Existing scg-ingest tests now install a fakescgshim in a tmpdir and prepend it to PATH for the spawned devil process — testing the substrate without requiring SCG to be installed in CI.
coherencelense added to devil-review's bundled catalog (12th lense, bird's-eye-as-feature). Surfaced as a methodology gap by mirror persona's e-014 synthesis during a post-merge devil-on-devil dogfood: the lense-coverage gate enforces lense-by-lense thinking but cannot detect cross-lense coherence drift; bird's-eye review had no first-class verb. Promoted to a lense so the audit posture itself is auditable. Catches: doc/code drift, naming inconsistencies between sibling verbs, contradictions between findings under different lenses, architectural-posture observations. Reviewers can now writekind=finding/kind=resistance/kind=synthesisentries on thecoherencelense to record the bird's-eye observations a single-lense audit cannot reach. Theconcludelense-coverage gate now requires an entry oncoherence(or an explicitkind=skipwith reason) alongside the other 11 — explicit-skip is the substrate-honest way to declare "we did not bird's-eye on this review."
-
devil— third passage under guild (alpha, security backstop). (#127) A multi-persona, lense-enforced review surface that composes with single-pass tools (Anthropic/ultrareview, Claude Security, supply-chain-guard) rather than replacing them. Designed to raise the security knowledge floor for code reviewed by authors who haven't met OWASP top 10 — not to guarantee protection, but to keep the dialogue honest when a finding is dismissed.Complete v1 surface from #126: 11 verbs.
devil open <ref> --type <pr|file|function|commit>— start a review session against a target.devil entry <rev-id>— append a hand-rolled entry. Per-kind validation:kind=findingrequires--severityAND--severity-rationale(the friction that forces exploitability-context reasoning, Claude Security style);kind=gateis reserved fordevil ingest.devil list— enumerate reviews (filter by--stateand--target-type).devil show <rev-id>— full detail (entries / suspensions / resumes / conclusion); JSON form isreview.toJSON(), same shape as the on-disk YAML.devil dismiss <rev-id> <entry-id> --reason <r> [--note "..."]— mark a finding-entry dismissed with a structured reason (one of:not-applicable | accepted-risk | false-positive | out-of-scope | mitigated-elsewhere). Refuses re-dismiss and refuses dismiss-after-conclude.devil resolve <rev-id> <entry-id> [--commit <sha>]— mark a finding-entry resolved, optionally citing the commit that landed the fix (resolved_by_commitbecomes part of the substrate).devil suspend <rev-id> --cliff "..." --invitation "..."— record a cliff/invitation pause on a thread of the review. Softer than agora's suspend: does NOT block other entries; just records re-entry context.devil resume <rev-id> [--note "..."]— pick up the most recent un-paired suspension. Surfaces the closing cliff/invitation in the response.devil ingest <rev-id> --from <ultrareview|claude-security|scg> <input>— append entries from an automated source's output. Strict v0 input JSON shapes per source; logs each invocation tore_run_history. SCG ingest produces onekind=gateentry on thesupply-chainlense; ultrareview / claude-security produce Nkind=findingentries.devil conclude <rev-id> --synthesis "<prose>" [--unresolved e-001,e-002,...]— terminal state transition. Verdict-less close (nook|concern|reject); synthesis prose is required. Lense-coverage gate: every lense in the catalog needs at least one entry (akind: skipentry counts when its text declares why) — silent skipping defeats the floor-raising design.devil schema [--verb <name>]— principle 10 dispatch contract for the passage.
Substrate path:
<content_root>/devil/reviews/<rev-id>.yaml(rev-id format:rev-YYYY-MM-DD-NNN, sequence per content_root per day).v1 lense catalog (11):
injection,injection-parser,path-network,auth-access,memory-safety,crypto,deserialization,protocol-encoding(Claude Security's 8 categories) pluscomposition,temporal,supply-chain(devil-specific). Thesupply-chainlense delegates to supply-chain-guard (mandatory; hard-error if SCG is unavailable — the floor- raising design refuses silent skip on supply chain per #126 decision C).v1 persona catalog (6): three hand-rolled —
red-team(adversarial framing strict),author-defender(articulate the author's framing + assumptions),mirror(read both, surface contradictions and shared blind spots) — plus three ingest-only personas attributable only viadevil ingest:ultrareview-fleet,claude-security,scg-supply-chain-gate.devil entryrefuses to attribute hand-rolled entries to ingest-only personas (PersonaIsIngestOnly).Entry kinds:
finding/assumption/resistance/skip/synthesis/gate(the last reserved for ingest paths).assumptionandresistanceare the load-bearing kinds for the design intent —assumptionmakes trust-assumptions explicit so they can be contested (--addresses);resistanceholds verdict-less concern across re-entry without forcing it took|concern|reject.Architecture in practice:
- principle 11 (AI-first): every verb's JSON envelope is the
agent contract (snake_case,
where_written,config_file,suggested_next); text mode is a projection. Stale-prose detector applied at the schema level (lessons from #121 extended). - principle 10 (schema as contract):
devil schemaadvertises every implemented verb's input + output. - principle 09 (orientation disclosure): every write verb
emits the same
notice: wrote <abs> (config: <abs>)line shape asgate registerandagora new. - principle 04 (records outlive writers): all state mutations are append-only at the array level (entries / suspensions / resumes / re_run_history). Status mutation on findings (dismiss/resolve) replaces the entry value at the same id slot — append-only at the array level preserved.
State machine (intentionally thinner than agora's):
open ──── conclude ────▶ concluded (terminal)No
suspendedstate — suspend/resume cycles are append-only history and do not block other entries. Multiple reviewers can be working simultaneously; the cliff/invitation records re-entry context only. (Contrast agora's Play, where suspend genuinely blocks moves.)Optimistic CAS via
DevilReviewVersionConflicton every appending operation;saveConclusionuses state-CAS (open→concluded);replaceEntry(used by dismiss / resolve) carries CAS on entries.length AND the targeted id. The CAS is sequential not atomic — it catches the load-then-act-then-write race that AI agents naturally produce when re-entering between sessions, but does NOT prevent two simultaneous writer processes from both passing the check (last-write-wins under true OS concurrency). Trust assumption named in the repository docstring: "one CLI process at a time per content_root."Strict v0 ingest input JSON shapes (per source) are documented in
src/passages/devil/interface/handlers/ingest.ts. Real-world adapter shims that translate actual upstream tool output into those shapes are out of scope for the in-tree passage and would land as separate utilities.Design rationale, three-source landscape comparison (
/ultrareviewvs Claude Security vs SCG), the seven shapes single-pass review structurally misses, and decisions A–E (naming / lense catalog / SCG mandatory / severity_rationale required / gate entry kind) live in issue #126.Sister project (devil-review's
supply-chainlense's delegate target): eris-ths/supply-chain-guard — its 8-stage Devil Gate framework is a reference implementation of the adversarial-gate-sequence shape devil-review's ownkind: gateentry was designed to ingest.Per-content_root custom lense override loader (
<content_root>/devil/lenses/<name>.yaml) is intentionally out of scope for v1. The catalog interface (LenseCatalog) is the seam; v1 ships the bundled defaults adapter only.~5,500 LOC, ~140 contract tests across 14 test files. Built iteratively on
snapshot/devil-review(12 + 5 commits) through three dogfood passes — self-review ofYamlDevilReviewRepository.tssurfaced the lense-coverage gate gap (e-006) before merge, and the cumulative skip-with-reason audit posture (e-014) was named as a side-effect of the gate's design.
-
agora
suggested_next.reasonno longer carries stale "(when implemented)" / "(... lands in subsequent commits)" prose. (#121) agora landed one verb per commit, and each commit's reason string was written when the next verb was genuinely future. Subsequent commits made those verbs real but didn't rewrite the older reason strings. Post-fix,agora newandagora movereason text reads as post-implementation. Regression test intests/passages/agora/suggestedNextContract.test.tsscans every agora verb'ssuggested_next.reasonfor known stale phrases — guards against drift returning.Surfaced by another Claude instance playing agora interactively with nao (see #117). Principle 09 (orientation disclosure) adjacent — surface drifted from substrate.
-
agora
suggested_next.args.byis no longer pre-filled with the just-acted actor. (#122) Pre-fix,agora play / move / suspend / resumeall setsuggested_next.args.byto the calling actor, implicitly recommending same-actor continuation. For Sandbox plays with multi-actor alternation (e.g., AI agent + human, or two AI personas), this biased the orchestrator toward "same person continues" — counter to the rhythm those plays take.Post-fix,
args.byis omitted from all four.argscarries only the load-bearing field (play_id); the orchestrator (or human + AI pair) decides who acts next per their own alternation model. Per principle 11 (AI-first, human as projection): agora's substrate doesn't carry policy about who runs verbs.Behavior change for any orchestrator that read
args.bywithout questioning. The verb name (suggested_next.verb) andargs.play_idare unchanged and remain the load-bearing recommendation.Regression test pins the absence of
byfor play / move / suspend / resume going forward.
-
agora— second passage under guild (alpha). A play / narrative surface alongsidegate's request-lifecycle / review surface. Built on the container/passage architecture (PR #116) withgate's substrate (safeFs, parseYamlSafe, GuildConfig, MemberName, parseArgs) reused without modification.Verbs (8 + schema):
agora new— create a Game definition (Quest or Sandbox)agora play— start a play session against a Gameagora move— append a move (with optimistic CAS)agora suspend— pause with cliff + invitation prose (the design pivot; substrate-side Zeigarnik effect)agora resume— pick up; surfaces the closing cliff/invitation in success output so the re-entering instance reads what was paused on without a separate queryagora conclude— terminal state (playing or suspended → concluded; drift-away outcome valid)agora list— enumerate games + plays (filterable)agora show <slug-or-play-id>— detail view; for plays renders full move + suspension/resume history paired by indexagora schema— principle 10 dispatch contract for the passage
Substrate path:
<content_root>/agora/games/<slug>.yamland<content_root>/agora/plays/<game-slug>/<play-id>.yaml(per-game subdirs scope plays to their definition; each game has its own sequence counter).Architecture in practice:
- principle 11 (AI-first): every verb's JSON envelope is the
agent contract (snake_case,
where_written,config_file,suggested_next); text mode is a projection - principle 10 (schema as contract):
agora schemaadvertises every verb's input + output; mechanical drift detector test enforces input-side equivalence (tests/passages/agora/schema.test.ts) - principle 09 (orientation disclosure): every write verb emits
the same
notice: wrote <abs> (config: <abs>)line shape asgate register - principle 04 (records outlive writers): all state mutations are append-only; suspensions and resumes are separate arrays paired by index; multi-cycle history preserved
State machine:
playing ── move ──────▶ playing ── suspend ───▶ suspended ── conclude ──▶ concluded (terminal) suspended ── resume ──▶ playing ── conclude ──▶ concluded (drift-away outcome)All state-changing appends use optimistic CAS (
PlayVersionConflict) — re-entering instances detect concurrent appenders rather than silently overwrite.Design rationale (Zeigarnik / rabbit tracker translation for AI agents, three-axis convergence of AI-first + gamification + human learning) lives in issue #117. Designed via the kiri/noir/mira three-voice review pattern; the pivot of
suspend / resumeas first-class primitives was surfaced by mira's mirror role.~3500 LOC, 62 contract tests across 8 test files.
-
lore/principles/11-ai-first-human-as-projection.md. Names the order-asymmetry every existing principle has been enacting without declaring: design decisions start from "is this AI- natural?", not from "is this human-friendly?" The substrate belongs to the agent's cognition; human-facing ergonomics are a projection layer assembled outside the substrate. Order is substrate → projection, never projection → substrate.Unlike principles 09 and 10 (which were waited-on for a third instance to surface the pattern), 11 was named immediately on recognition — it had been the latent stance the project chose for every prior decision (
gateJSON-default, snake_case JSON, schema as contract, stderr-only notices, explicit flags refusing human-default-intuition, etc.) but had no written rule pushing back when "let's make it more human-friendly" PRs would chip the substrate. With 11 named, the question becomes structural: is the friendlier version a projection, or a substrate change? Projections welcome; substrate changes for human-friendliness not.Cross-referenced from principle 10 (schema-as-contract is one concrete face of 11). Will become the upstream rule for agora design (issue #117) so passage primitives don't re-litigate AI-first per primitive.
-
Schema/KNOWN_FLAGS drift detector test. Mechanical CI enforcement of principle 10's input-side obligation: every flag the runtime accepts must appear in the schema, and vice versa. PRs #103 (
--dry-run), #105 (--with-calibration), #111 (tail--format) all hit drift after-the-fact during fresh-agent dogfood; this test catches it at PR time.The test parses each handler's
*_KNOWN_FLAGSdeclaration and therejectUnknownFlags(args, X, 'verb-name')calls via static regex (no source export needed — KNOWN_FLAGS stays internal), builds a verb→flag-set map, and compares to the livegate schemaoutput. Subcommand umbrellas (issues,inbox) are skipped because the schema collapses multi-handler verbs into a single entry — aligning that requires a schema-model change rather than drift fixing. Thelist/pendingindirection (one function with conditional const choice) is handled via an explicitINDIRECT_VERB_TO_CONSTallowlist in the test.8 existing drift instances fixed in the same PR, surfaced by running the detector against main:
fail/review/transcript:iddescription didn't start withpositional;so the detector treated it as a flag. Updated the sharedidStrconst +transcript's inline description to mark them positional.thank.for: was usingidStr(which is now positional) as a flag-typed id reference. Inlined a non-positional description forfor.show: schema lacked--plainand--fields(which the runtime accepted). Added with descriptions naming the agent-facing shell-composition use case.message/broadcast: schema lacked--type. Added.repair: schema lacked--format. Added.
Going forward, a PR adding a flag to
KNOWN_FLAGSwithout the matchingschema.input.propertiesentry fails at CI. -
lore/principles/10-schema-as-contract.md. Names the rule three input-side drift PRs (#103 dry-run, #105 with-calibration, #111 tail format) and ~10 read verbs with bare output schemas had been making necessary without articulating:gate schemais the agent dispatch contract, in BOTH directions. Input side: every flag the runtime accepts must appear inschema.input.properties. Output side: every field the runtime emits must appear inschema.output(no bare{ type: 'array' }/{ type: 'object' }placeholders). This principle is structurally one level UP from principle 09 — without schema-as-contract, "boot's missing-disclosure was a contract bug" reduces to "it was annoying"; with it, the asymmetry between schema claim and runtime delivery IS the bug. Surfaced via the kiri-author / noir-devil / mira-mirror three- voice review session in the dogfood sandbox. Mira's mirror role flagged that 09 was implicitly leaning on a foundation that wasn't named. -
gate tailandgate voicesoutput schemas fleshed out (first instance of principle 10's output obligation). Pre-fix, both verbs declaredoutput: { type: 'array' }with noitems— an MCP wiring saw "an array of something" and had to discover the utterance shape empirically. Post-fix, a sharedutteranceArraySchemadescribes every utterance kind (authored / review / thank), every shared field (kind,at,request_id), every kind-specific field (from/completion_note/withfor authored;by/lense/verdict/commentfor review;to/reasonfor thank), and the optionalinvoked_byproxy field. All snake_case (post-#109). Field documentation names which kind each field applies to so a JSON-Schema-driven consumer reads the discriminated-union semantics without needingoneOf(the JsonSchema subset gate uses doesn't include it). Tracked follow-ups for the remaining bare-output read verbs (status,whoami,show,chain,list,pending,repair,issues *) are listed in principle 10. -
Runtime-validates-schema test for tail + voices. Mira's design suggestion (review on req
2026-05-01-0001): the schema becomes unforgeable when the actual JSON output is validated against the declared schema in CI. If the runtime emits a shape that doesn't match the schema, EITHER the runtime regressed OR the schema is wrong; either way, CI catches it at the boundary that matters (what an MCP wiring would actually receive). Hand-rolled draft-07-subset validator (no new dependencies); validator self-tests its own failure modes so the suite can't silently pass with a buggy validator. Same approach extends to the other read verbs as their schemas are fleshed in follow-ups.
-
Schema/KNOWN_FLAGS drift detector test (input side). PRs #103, #105, #111 each added a flag to a verb's
KNOWN_FLAGSbut had to add the matchingschema.input.propertiesentry separately. A CI-level test that compares each handler'sKNOWN_FLAGSagainst its schema entry is the natural enforcement vehicle. Queued as the next mechanical follow-up to principle 10 — the principle file names this explicitly. -
Output schemas for the remaining bare-typed read verbs.
status,whoami,show,chain,list,pending,repair, andissues *still declareoutput: { type: 'object' }/{ type: 'array' }. Each is mechanical (read the runtime, translate to JsonSchema, add a runtime-validates test). Done in clusters where multiple verbs share a shape:show/list/pendingshare theRequestshape;status/boardshare theStatusSummaryshape.
-
lore/principles/09-orientation-disclosure.md. Names the rule three empirical PRs (#108 register stderr notice, #110 boot text disclosure, this PR's doctor disclosure) had been re-deriving instance-by-instance: a verb whose output is a count / finding / status about a content_root must let the operator verify the content_root identity in surprising cases, and stay quiet otherwise. The conditional emission is the whole design — voice budget says we don't burn a line on the 99% case where cwd === content_root and config sits where expected. Surfaced via the kiri-author / noir-devil / mira-mirror review session in the dogfood sandbox; mira's mirror role (paraphrase + meta-question) flagged that we'd been ad-hoc'ing the same pattern across PRs without naming the rule. Pinning it so the next opt-in (status? board?) doesn't re-litigate. -
gate doctordiscloses the resolved content_root + config when surprising. Carries the principle 09 pattern to doctor. Pre-fix,gate doctorreported "members 1 total, 0 malformed" with no signal about WHICH content_root those numbers applied to. An operator running doctor from a subdir of an active guild had no clue the diagnostic walked up to a parent. Post- fix, doctor emits the canonical line shape ONLY when surprising:- Subdir of an active guild →
content root: /abs (config: /abs/guild.config.yaml) - No-config fallback (cwd as implicit root, has data) →
content root: /abs (config: none — cwd used as fallback root)Suppressed whenmisconfigured_cwdalready fires (no-config + no-data, the bigger warning owns disclosure) so the operator sees exactly one disclosure surface at a time.--summarymode also discloses;--format jsonis unaffected (text-only per principle 09 boundary). New shared formatterformatContentRootDisclosureininternal.tsso a third caller doesn't copy-paste.
- Subdir of an active guild →
-
gate tail --format jsoncloses the asymmetry that lefttailas the lone read verb without JSON output. Sibling verbs (voices,list,board,show,status,whoami,inbox,issues list) all support--format json|text; tail was bare-text-only. Empty stream emits[](not an error envelope) sojqpipelines don't have to branch on "is this an array or an error". Utterance keys are snake_case (request_id/invoked_by/completion_note/deny_reason/failure_reason) inheriting the post-#109 contract — shipping the rename first means tail's JSON debuts in the right shape and never goes through a transient camelCase phase. Thegate schemaentry for tail now advertises bothlimitandformatflags so MCP wirings discover them. A typo on the format value (--format josn) rejects loudly rather than silently degrading. Devil-reviewed in design sandbox (2026-05-01-0001).
-
gate boot --format textsurfacescontent root: <path> (config: <path>)when the situation is surprising. Sibling to the PR #108 register-notice fix: closes the silent parent- config-pickup gap on the READ side. Pre-fix,gate boot --format textshowed neitherconfig_filenorresolved_content_root(both fields existed in the JSON envelope but the text rendering omitted them), so an agent who ran boot for orientation got no path signal even when the cwd was a subdir of an active guild and gate had silently walked up to a parent'sguild.config.yaml.Post-fix, the orientation block emits one line only when the situation is surprising — voice budget is preserved by staying silent at the alignment case (cwd === resolved content_root, config present and discovered). Two trigger cases:
- Subdir of an active guild (
cwd != resolved_content_root) —content root: /abs/path (config: /abs/path/guild.config.yaml) - No config found, cwd used as fallback (
config_file === nulland there's data) —content root: /abs/path (config: none — cwd used as fallback root)
Suppressed when
misconfigured_cwdalready fired (no-config + no-data, the bigger warning takes over) so the disclosure is surfaced exactly once. JSON envelope gainscwd_outside_content_rootboolean for orchestrators reading the structured contract. Phrasing matches PR #108's(config: ...)segment for cross- verb recognition. Devil-reviewed (2026-05-01-0001/0002); v2 absorbed the voice-budget concern (D1) by gating the emission on the surprising cases. - Subdir of an active guild (
-
gate voices/gate tailJSON:request_id/invoked_by/completion_note/deny_reason/failure_reason(was:requestId/invokedBy/completionNote/denyReason/failureReason). The voices stream is the project's highest-traffic JSON surface and was the lone camelCase outlier; every other JSON surface (gate show,gate inbox,gate status,gate registerJSON envelope,gate boot.hints, etc.) was already snake_case (created_at,read_at,display_name,auto_review,status_log,where_written,config_file). The mismatch made cross-tool consumers carry a translation layer for one stream only.Single-cycle cut per 0.x policy (same shape as the
displayName → display_namerename in PR #102) — no dual-emit phase. Fresh- agent dogfood (post-PR #107) surfaced the inconsistency; devil- reviewed (2026-05-01-0001/0002) to confirm scope: rename the TS fields too so the text-path readers (renderUtteranceinvoices.ts, thetailsummary block inboot.ts, the bilingualresume.ts) stay consistent with the JSON. Thegate schemadeclarations did not advertise the voices output shape, so no schema update was needed.Downstream JSON parsers (MCP wirings, ad-hoc scripts) that read these fields must update key names. Records on disk are unchanged — this is purely an output-shape rename, not a storage migration.
-
gate registersurfaces the absolute path it wrote, on both stderr (humans) and JSON (orchestrators). Closes the silent parent-config-pickup gap a fresh-agent dogfood surfaced. Pre- fix, runninggate register --name newcomerfrom a subdir of an active guild silently walked up the tree, found the parent'sguild.config.yaml, and wrote<parent>/members/newcomer.yamlwith no signal — the agent had no clue their YAML landed in someone else's repo. Same gap hit no-config-found cases (cwd used as the implicit content_root).Post-fix,
gate registeremits one stderr notice on success:notice: wrote /abs/path/members/<name>.yaml (config: /abs/path/guild.config.yaml)When no config was discovered the second segment becomes
config: none — cwd used as fallback root, naming the implicit default explicitly. The JSON success envelope gainswhere_written(absolute path of the saved file) andconfig_file(absolute path ofguild.config.yamlin use, ornull) so MCP consumers parse structured fields rather than scraping stderr.Symmetric on
--dry-run: the preview header now shows the absolute path (was: relativemembers/<name>.yaml) and the stderr notice fires withwould write(notwrote) so the preview is not less honest about location than the real write. Notice does NOT fire on error paths (collision, host-name reservation, validation) — pinned by test. Devil-reviewed (2026-05-01-0001/0002); v2 absorbed the JSON-contract, dry-run-symmetry, and error-boundary concerns. -
gate doctorsurfaces unrecognized .yaml files and unexpected subdirectories underrequests/. Pre-fix,listByState's regex filter (^\d{4}-\d{2}-\d{2}-\d{3,4}\.yaml$) silently dropped off-pattern entries — abad.yamlinrequests/pending/, a2026-05-01-7.yaml(wrong digit count), anoops-dir/subdirectory under<state>/, or even a properly-named2026-05-01-9999.yamlplaced atrequests/root (wrong directory level) — all stayed there forever andgate doctorreported the root as clean. Two new finding kinds:unrecognized_file(off- pattern .yaml in any record location) maps to quarantine in repair (gate ignores them anyway; moving toquarantine/is safe and reversible).unrecognized_directory(subdir under<state>/or non-state directory at requests/ root) maps to no-op — contents are unknown and quarantining a tree is invasive; the operator must inspect first. Boundary: only.yamlfiles and directories are flagged;notes.txt,README.md,.gitkeepand other repo artifacts are intentionally ignored. Devil- reviewed (2026-05-01-0001/0002). -
gate doctorextends the unrecognized-file scan toissues/andmembers/. Same shape of fix as the requests-side scan, for the same class of bug. Pre-fix, ani-bogus.yamlinissues/(typo'd id), a capitalised-prefixI-2026-05-01-0001.yaml, or — most painfully — anAlice.yamlinmembers/(uppercase first letter) were silently dropped by each repository's listAll regex (^i-\d{4}-\d{2}-\d{2}-\d{3,4}\.yaml$for issues,^[a-z][a-z0-9_-]{0,31}\.yaml$for members) andgate doctorreported a clean root while the member was missing fromgate list. Now each off-pattern.yamlsurfaces asunrecognized_file(→ quarantine in repair) and any subdirectory underissues/ormembers/surfaces asunrecognized_directory(→ no-op in repair, contents unknown and quarantining a tree is invasive). The four common member-name typos covered explicitly by tests: uppercase first letter, leading digit, leading underscore, name >32 chars. Boundary unchanged: only.yamlfiles and subdirectories are flagged;notes.txt,README.md,.gitkeepignored.Internal cleanup absorbed (devil-review concern D1 from
2026-05-01-0001): each repo now exports a single*_FILE_PATTERNconstant that bothlistAll(filters records) andlistUnrecognizedFiles(surfaces non-matches) consume, so the two paths cannot drift. Pre-fix,YamlRequestRepositoryduplicated the same regex literal across two methods — latent drift bug surfaced by the design sandbox and fixed at the same time. The shared port type renamedUnrecognizedRequestFile→UnrecognizedRecordEntryand moved to its own module (src/application/ports/UnrecognizedRecordEntry.ts); 5 callsites updated mechanically. Devil-reviewed (2026-05-01-0001/0002).
-
Cross-area unrecognized-file scan. A file with the wrong area's pattern sitting in the wrong area (e.g., an
i-2026-05-01-0001.yamlleft undermembers/, oralice.yamlleft underissues/) is still invisible because each scan only walks its own area and only flags files that don't match its OWN pattern. The misplaced file matches the member pattern so the issues-area scan ignores it, and vice versa. Out of scope for this PR — the cross-area decision depends on whether such files should auto-relocate or just be flagged, which is a policy choice this PR doesn't have a basis for. Recorded so a future reader who hits "I have an i-prefix file in members/ and doctor doesn't see it" finds the trail. Devil-reviewed concern D3 from2026-05-01-0001. -
Voice calibration:
verdict=concern + state=failedcounts as aligned. The source-code header insrc/interface/gate/voices.tsdocumented v1 alignment rules includingverdict=concern + state=failed → aligned (you flagged it, it broke), but the implementation had// verdict === 'concern' intentionally excludedand dropped ALL concern verdicts from both counts. Reviewers who usedconcernas their primary signal got zero calibration credit and registered asuncalibratedforever. Doc-code drift; the code now matches the documented rules:concern + failed → aligned,concern + completed → soft(excluded). Thereject + completed → overruledcase keeps its existing "counted as missed" semantic, and a comment now names the choice (the alternative — "the risk you flagged was reviewed and held" — isn't separately observable from the record alone, so we pick the conservative work-as-evidence read). Devil-reviewed (2026-05-01-0001/0002). -
gate schema: voices entry declares--with-calibrationwith a description that names the JSON-only semantic honestly: the flag opts into the{utterances, calibration}JSON object shape (default: bare array), but text mode emits the calibration footer regardless of this flag. Pre-fix the runtime accepted the flag viaKNOWN_FLAGSbut the schema didn't declare it, and the text-mode behaviour was undocumented — fresh agents reading the schema saw an undocumented flag and a hidden text-mode override. -
gate review: empty editor body aborts with a context-aware error. Pre-fix, when the user opened the editor (no--comment/positional/stdin) and saved without writing,readCommentViaEditorreturned an empty string and the caller's genericreview comment is required (use --comment <s>, …, or run interactively so $EDITOR opens)error fired — but the "run interactively" hint was misleading because the user had just done so. Post-fix the editor flow throws its own message:editor returned an empty review body; aborting. Re-run and write content above the scissors line, or use --comment / --comment - / a positional.Surfaces the empty effort at the layer that produced it; matches
git commit's "empty message aborts" semantic. The function docstring (which already named this throw, but pre-fix the implementation only honored the editor-failure cases) is now honest. Devil-reviewed (2026-05-01-0001/0002); the user- facing message dropped its "matches git commit behavior" rationale — that justification belongs in the source comment, not in the recovery instructions. -
gate schemadeclaresdry-runon every write verb that accepts it. Pre-fix the schema entries for approve / deny / execute / complete / fail / review / thank did NOT declaredry-runas an input property — so MCP wirings readinggate schemasaw a tool surface strictly less capable than the runtime (which accepted--dry-runvia KNOWN_FLAGS).registerdeclared it but as a string; the parser treats it as a boolean flag with optional=true/=falsesuffix. Now all eight verbs share a singledryRunFielddeclaration ({type: 'boolean'}) pointing at the preview-envelope contract. Fresh-agent dogfood surfaced; devil-reviewed (2026-05-01-0001/0002) to extend the fix to register so the asymmetry doesn't just move. -
gate <verb> --dry-run --format textemits a one-line stderr notice naming why the format is fixed. The dry-run envelope (dry_run / verb / would_transition / preview) has no useful text rendering, so stdout stays JSON regardless of--format. Pre-fix: silent JSON when--format textwas passed. Post-fix:# --dry-run preview is structured (json envelope); --format text would lose dry_run/verb/would_transition.Suppressed for--format json(pipelines stay clean).
--dry-runcoverage on creation/annotation verbs. Ten verbs do not yet support the preview envelope:request,fast-track,message,broadcast,issues add,issues note,issues start/defer/resolve/reopen,issues promote. The help text scopes "any write verb above" to the approve/deny/execute/complete/fail/review/thank set, so the current state is asymmetry-by-design rather than a bug; expanding to the creation/annotation set requires either handler-side preview construction or use-case-side dry-run flag plumbing. Recorded here so a future reader who hitsgate request --dry-run: unknown flagfinds the trail rather than silence.
- Member YAML:
displayName→display_nameon save. Aligns the one camelCase field on disk with the rest of the project's snake_case convention (auto_review,read_at,status_log,created_at,invoked_by). The hydrate path (YamlMemberRepository) already accepts both forms; new writes always use snake_case. Existing member YAMLs in users' content_roots keep loading. Bundled fixtures (examples/quick-start,examples/dogfood-session,examples/agent-voices) migrated mechanically. Single-cycle cut per 0.x policy — no dual-emit phase. Devil-reviewed (2026-05-01-0001/0002).
-
gate whoamisurfacesdisplay_name. When a member YAML carries adisplay_name, the orientation line now readsyou are noir — Noir (Critic) (member)rather than the pre-fixyou are noir (member)which hid the chosen presentation. Em- dash separator follows the pattern other surfaces use to compose name/label pairs; the trailing role-in-parens is unchanged. When no display_name exists, the line stays in its original concise form. -
gate inbox --format json+ self/inactive message advisories. Three friction points on the messages surface a fresh-agent dogfood surfaced. (1)gate inbox --format jsonnow emits an array of inbox-entry objects with snake_case keys ({from, to, type, text, at, read, read_at?, read_by?, invoked_by?, related?}). Optional fields are OMITTED when undefined (matchesgate showJSON convention). (2)gate message --from X --to X(self-message) emits a stderr notice — same shape as the existing self-approve notice. The act is allowed and recorded; the writer sees the edge they crossed. (3)gate message --to <inactive>emits a stderr notice naming the consequence ("the message landed in their inbox but they may not be reading it").gate broadcastalready filters inactive recipients; the DM path was silent — asymmetric. The notice closes the asymmetry without making a policy choice (deliver-or-block) the PR doesn't have a basis for. Devil-reviewed (2026-05-01-0001/0002in design sandbox); v2 absorbed phrasing trims and the omit-when- undefined JSON shape rule. -
gate issues listJSON output +--state all+ bare-issues hint. Closes four discoverability gaps a fresh-agent dogfood surfaced. (1)--format jsonnow emits an array of nested issue objects (notes preserved as a sub-array, not flattened as in text format). (2)--state allreturns every state in one call; previously a reader had to invoke list four times. (3) When--stateis omitted, a stderr hint discloses the implicit open-only filter and names the open-vs-active distinction withgate status's count:# filtered to state=open; status counts open+in_progress (active) — --state to override. (4) A baregate issues(no subcommand) used to silently fall through to list; it now emits a short hint at the most-common subcommands and exits 1, mirroring howgate listhandles missing--state. The list semantics (worklist = open) and the status semantics (triage = open+in_progress) intentionally differ — the difference is exposed at the surface that produces the count, not papered over by aligning them. Devil-reviewed (2026-05-01-0001/0002in design sandbox). -
lore/principles/08-voice-as-doctrine.md+ voice-budget audit. Names the principle that the tool's prose —suggested_next.reason, schema descriptions, footers, finding messages — is the running embodiment of lore, the substrate by which 02 (advisory not directive), 03 (legibility costs), and 07 (perception not judgement) reach readers who do not openlore/. The companion testtests/interface/voiceBudget.test.tsenumerates named pedagogical phrases ("all first-class", "DETECTOR, not an enforcer", "advisory — override freely", and four others), each with a budget and an allowed-files list. New occurrences fail the test untilVOICE_BUDGETis updated with rationale in the same commit. The test is a detector for proliferation; paraphrase escapes it (LLM-difficult to detect verbatim) and that limitation is named in the test header.CONTRIBUTING.md(new) documents the workflow. No source-code behavior changes — this is a discipline added at the CI surface, not the runtime surface. -
gate unresponded(read verb). Surfaces concern/reject verdicts on the actor's authored or pair-made requests that have no follow-up record yet. Thin wrapper over the sameUnrespondedConcernsQuerythat drivesgate resume's concerns surface, so the two cannot diverge. Default actor is GUILD_ACTOR;--for <m>and--max-age-days <N>override. Naming aligns with the underlying detector's "unresponded" semantic — explicitly NOTgate concerns(which would suggest "all concerns" rather than "concerns without follow-up"). The detector is deliberately coarse (existence-only follow-up detection);gate chain <id>walks the actual references when the reader wants to verify whether a follow-up addresses anything specific. Surfaces the gap that bit first-time agents: aconcernverdict on a completed request was not visible from any read path short of re-runninggate resume. (refines 2026-05-01-0003) -
gate showadds a concern marker line. A binary existence signal —no concerns recorded/concern recorded — walk gate chain ...— sits next to the existingchain hintline. Existence-language deliberately, NOT a count: counting ("3 concerns, 1 follow-up") would invite the reader to play a "drive the number down" game (principle 03 — performance-for-the-record). The original design called for a 3-state marker (concern + inbound / concern + no inbound / no concerns); resolving inbound presence at show time would require an async repository scan, so the shipped form punts that resolution togate chain <id>(named inline in the marker text). The reader gets the same perception affordance with one extra command rather than via formatRequestText becoming async. -
gate bootaddsverbs_available_now.requires_other_actor. A sibling array toactionable(which keeps its existing flat shape — additive change). Each entry names{verb, id, candidates, reason}: a verb that exists on the actor's record but cannot be dispatched by them as themselves. Surfaces blockers (e.g. "your pending request needs approval by host X") so the actor sees WHY their queue isn't moving without parsingsuggested_nextprose.candidatesis a list, not a single name, so a content_root with N hosts (or zero) doesn't have to embed a "first host" assumption in the payload. The host-self case is filtered out (a host who authored a pending request sees it underactionablevia pending-as-executor, not underrequires_other_actor). -
SuggestedNext.actor_resolved: boolean. New field on the write-response, boot, and resumesuggested_nextpayloads. True iffargs.byis absent or matches the calling actor (GUILD_ACTOR); false when the suggestion names a different actor. Lets an orchestrator branch (escalate / hand off vs. dispatch as self) without parsing the verb's--byagainst the env. Hint, not gate — the underlying verb still validates--byat the boundary. Schema description names the discipline so a reason-skipping loop is structurally redirected. -
Concern advisory on completed-with-concern reviews. When a completed request carries a
concern/rejectreview (and its auto-reviewer, if any, has recorded),suggested_nextreturns achainwalk advisory rather than null: verb is read-only (chain) to avoid embedding a dispute-resolution flow in the tool's voice; reason names follow-up paths AND explicitly lists "leaving as-is, conversing it out, or letting it fade — all first-class." The absence of action stays structurally legitimate. Replaces the prior null-after-review behavior for the concern case only —okverdicts still close the arc cleanly withnull. -
gate transcriptends with aConcerns recordedsection. Bare enumeration ofconcern/rejectverdicts in the request, pointing atgate chain <id>for reference resolution. No status language ("still open", "addressed by"), no severity — the tool surfaces existence; status judgement stays with the reader (principle 07). -
FsInboxNotification.post/markReadretry once onInboxVersionConflict. When a concurrent writer advances the on-disk version between our read and the CAS check, the first attempt throws; the retry re-reads the now-advanced file and commits on top. Safe because post is append-only (order may shift but side-effects don't duplicate) and markRead is idempotent (already-read entries stay read). Two consecutive conflicts still bubble up so a three-or-more simultaneous-writer scenario surfaces to the caller instead of looping forever. Closes devil's C4 on hiroba2026-04-22-0002. (PR #84) -
Issue repository now uses atomic write + optimistic-lock CAS.
YamlIssueRepository.savewrites viawriteTextSafeAtomicand rejects concurrent mutations with a newIssueVersionConflict(parallel toRequestVersionConflictandInboxVersionConflict). Closes the asymmetry where Request and Inbox were locked but Issue was last-writer-wins — which had self-defeated the state_log append-only invariant when twogate issues resolve / notecalls raced. Version counter =state_log.length + notes.length(monotonic, append-only domain operations). Legacy issues still hydrate cleanly; the first save after upgrade follows the new path. -
gate repairandgate doctorjoin the strict-flag rejection set. Typos likegate repair --aply(which used to silently stay in dry-run) orgate doctor --summry(which used to show the full report instead of the summary) now error with the valid flag list. Brings these two verbs into parity with the write-verb suite shipped earlier.
gate issues resolve/defer/start/reopennow require--by <m>(orGUILD_ACTOR). Issue state transitions now append to astate_log: [{state, by, at, invoked_by?}]array (parallel to Request'sstatus_log), and the transition cannot be recorded without knowing who performed it. Migration: add--by <name>to any scripted issue state-transition invocation;GUILD_ACTORcontinues to work as fallback.Issue.setState(next)→Issue.setState(next, by, invokedBy?)at the domain level;IssueUseCases.setState(id, state)→setState(id, state, by, invokedBy?). Legacy issue YAML (nostate_log) hydrates cleanly as[]so existing records open fine — the audit trail starts from the first post-upgrade transition. (PR #81)
- Issue state transitions now produce an append-only
state_log. Every resolve / defer / start / reopen records one entry{state, by, at, invoked_by?}(mirrors Requeststatus_log), max 100 entries per issue. Forensics for flapping (open → resolved → open → resolved) previously collapsed to only-the-final-state; state_log preserves the transition history. Empty arrays are omitted fromtoJSONso byte-identical YAML output survives issues that haven't transitioned. (PR #81, Sec H3) - Strict unknown-flag rejection extended to all write verbs.
rejectUnknownFlags(previously opted-in bygate tailonly) now runs in every write verb:register,request,approve,deny,execute,complete,fail,fast-track,review,thank,message,broadcast,inbox,inbox mark-read, and allissuessubcommands. Typos likegate register --catgeory X,gate request --executr noir,gate thank --reasn "..."now error with a list of valid flags instead of silently doing the wrong thing. (PR #81, Sec H1) - Inbox writes are atomic with optimistic-lock (
InboxVersionConflict).FsInboxNotification.postandmarkReadnow usewriteTextSafeAtomicand maintain a monotonicversion: Ncounter for compare-and-swap. Concurrent writers that would previously last-writer-wins (silently dropping a message or a read flag) now surface anInboxVersionConflictthe caller can retry. Exported fromapplication/ports/NotificationPort.tsalongside the existingRequestVersionConflict. Legacy files withoutversionhydrate as 0 so first post-upgrade save proceeds without a false conflict. (PR #81, Sec H2) - Shared
sanitizeTexthelper atsrc/domain/shared/sanitizeText.ts. Replaces four hand-written near-duplicates (Request, Issue, Review, MessageUseCases). Options:{maxLen, requireNonEmpty?, trim?}. Behavior is preserved per call site (Review keeps its existingtrim: false/requireNonEmpty: falsedrift intentionally, flagged as a named follow-up). New policy changes (e.g. emoji / BOM handling) can now land in one place instead of four. (PR #81, Refactor H1) actionableTransitions()as single source of truth in boot.deriveBootSuggestedNextandderiveVerbsAvailableNowboth consume one predicate set (executing-mine,unreviewed-mine,approved-for-me,pending-as-executor) with declared priority, instead of hand-coding the same four predicates in each function. Byte-identical output; future state additions are guided by TypeScript's exhaustiveness check onActionableKind. (PR #81, Refactor H2)gate show <id> --format textnow prints a chain-hint footer. The footer scansaction/reason/completion_note/deny_reason/failure_reason/status_log[].note/reviews[].commentfor full-id references (YYYY-MM-DD-NNN...) and reports either"chain hint: no outbound id references detected"or the list of referenced ids. Read-time surfacing only — the write path is untouched, so writers stay free-form while readers can see at a glance whethergate chain <id>will return anything. Short-form(0004)is intentionally not detected (that's the case the hint is warning about). Self-ids are excluded. See also the expanded paragraph indocs/verbs.md. (PR #72)- Strict unknown-flag rejection helper, with
gate tailas the pilot caller.rejectUnknownFlags(args, known, verb)lives insrc/interface/shared/parseArgs.ts. Verbs opt in individually;gate tailnow errors with a clear message (and lists the valid flags for the verb) instead of silently ignoring a typo likegate tail --from noir. Other verbs migrate in follow-up PRs — the opt-in model is deliberate so existing invocations don't break en masse. (PR #73)
- README minimized; the repo's top-level surface is the entrance,
and the entrance should be small. README dropped from 478 to ~96
lines: the depth ladder + a one-line link to
lore/are the load- bearing parts. The verb cookbook, configuration block, file-layout diagram, state-machine diagrams, "what this tool does NOT do" list, and tests-detail block were redundant againstAGENT.md/docs/verbs.mdand now defer to those files. Top-level git tree shrank from 22 to 18 entries:POLICY.md→docs/POLICY.md,guild.config.yaml.example+members.example/→examples/quick-start/,scripts/run-tests.mjs→tests/run.mjs. GitHub-conventional files (README.md,LICENSE,SECURITY.md,CHANGELOG.md,.github/) untouched.lore/stays at top so ls-ing the repo surfaces the load-bearing thinking next to the code, not buried underdocs/. Doc / code refs follow the moves.
save()no longer throws spuriousRequestVersionConflictwhenreviewscarries non-object entries. Class-closure follow-up to the priorstatus_logfix below.hydrate()silently drops review entries that aren't objects (a loose-shape input rare in normal write paths but possible from hand-edited or imported YAML); the earlierreadVersion()patch only filteredstatus_log, leaving the same drift onreviews. Both arrays now share oneisObjectEntryguard so the structural symmetry is visible at the call site, and adding a future skip rule on either side forces the corresponding filter here. Surfaced by a noir-lens devil review on the prior fix that asked whether "any hydrate skip rule ↔ counter mismatch" was closed as a class, not just at one site. Regression test injects a non-object review entry alongside a real one and verifiesaddThank+save()no longer raiseVersionConflict.save()no longer throws spuriousRequestVersionConflicton records carrying legacy statelessstatus_logentries.hydrateskips status_log rows whosestatefield is missing (an older format wrote review notes that way), butreadVersionwas counting those rows from the raw YAML — soloadedVersion(from the hydrated aggregate) laggedmaxOnDisk(from the raw count) by exactly the number of skipped entries. Every save() on such a record then threwRequestVersionConflicteven when no concurrent writer existed. Surfaced bygate thankagainst any reviews ≥ 1 request whose status_log carried a legacy review-note row — the verb only touchesthanks[], so the version drift wasn't masked by a real status_log/reviews delta. Same shape as the earlier "Custom lenses no longer break listAll-backed read verbs" fix (read-path skip rule out of sync with raw count). One-line behavior fix; regression test injects the legacy shape and runsaddThank→save()round-trip.unresponded_concernsno longer counts pre-dating mentions as follow-ups.UnrespondedConcernsQuery.hasFollowUpwas checking "does any authored record mention this id?" without a temporal guard. Concrete failure: v1 denied with reason mentioning v2.id ("refile as v2"), then v2 reviewed with a concern — the earlier v1 deny's mention of v2.id falsely counted as a follow-up, so the concern was hidden from resume. With the guard, a referring record is only counted as a follow-up if itscreated_atpost-dates the latest concern on the request. Surfaced by a review-mode dogfood where concerns were legitimately open but invisible.- Positional
-now triggers the stdin sentinel ongate review,gate issues note, andgate issues add. Previously--comment -/--text -read stdin but writing the sentinel as a trailing positional (gate review ... --verdict X - <<EOF) stored the literal"-"as the body — a natural shape that silently dropped the heredoc. Positional-now reads stdin the same way as the explicit flag forms. gate chaindedupes bidirectional mentions and marks them with↔. Two records that mention each other in their text used to appear in BOTH "referenced" and "referenced by" sections — the same record rendered twice. Now bidirectional refs appear once, in the forward section, prefixed with↔to signal mutual reference. One-way refs stay as before.
gate issues promotewrites a structuredpromoted_fromfield on the created request. The default--action/--reasontemplates mention the source issue id textually, and chain picks that up via its text-scan. But both flags can be overridden — in that narrow case the textual link disappears and chain would lose the connection. The newpromoted_from: <issue-id>field on the request carries the tool-generated link independent of text content, so chain walks it as a separate-from-text reference path regardless of overrides. The field is omitted on non-promoted requests so existing YAML stays byte-identical;gate show --format textrenders it on a dedicated line;chaindedupes against text-mention hits so default-promote output doesn't surface the same issue twice. Added as the first example of a tool-generated structured relationship (alongsideexecutor,auto_review,with) — user-authored references still use the general text-mention channel. (#47 neighborhood.)
- Custom lenses (configured in
guild.config.yaml) no longer breaklistAll-backed read verbs.findByIdcorrectly passedconfig.lensesto the hydrator, butlistByStatedid not — so a review with a custom lens (e.g.rational,emotional) wrote fine, showed fine viagate show, but thengate chain/gate voices/gate tailhit hydrate failure with "Invalid lense" and silently dropped the record. Surfaced by dogfooding gate in solo-journal mode with arational / emotional / future-self / skepticlens set. One-line fix at the call site. gate message --text -andgate broadcast --text -now read from stdin (med, silent data loss regression).gate issues note --text -worked.gate request --reason -/deny/fail/review --comment -all worked. Butmessageandbroadcastsilently stored the literal-as the body. Heredoc-piped handoff notes dropped their content with no error to show for it — a direct hit on the "the record is the truth" invariant. Ported the same--text -sentinel asissues note.
gate issues addnow accepts--text <s>/--text -(low, symmetry). Pre-fix the verb accepted text only as the positional argument, while the siblinggate issues notehad all three routes. Users who built muscle memory onnotebounced offadd. Now symmetrical:--text <s>inline,--text -for stdin, positional remains as the backward- compat legacy form. Missing-text error lists all three routes and carries the same POSIX escape hint (--text=<value>/--separator) asissues notewhen a flag value begins with--.
gate board— "what's in flight" view. New read verb that answers "what's happening right now" in one call, grouping pending + approved + executing rows under per-state headers. Surfaces the question thatgate statusgave counts for andgate list --state <s>gave single-state contents for, without requiring three commands to see the whole board. Terminal states (completed / failed / denied) and issues are out of scope: "in flight" means "someone could still act on this." Filters mirrorgate list:--for <m>narrows each section to rows naming that actor; GUILD_ACTOR is applied implicitly when--foris omitted (same pattern, same stderr notice). Empty sections still render their header so the board shape stays stable across calls. JSON emits{ pending: [...], approved: [...], executing: [...] }with a stable key set — consumers can rely on all three arrays being present even when empty.
gate register --name <x>now rejectsxalready inhost_names. The existing guards blocked--category hostand duplicate-member names but missed the third way to create the same collision: registering a plain member whose name is already a host inguild.config.yaml. Post-fix, all three entry points end up at the same invariant "a single name cannot be both a host and a member," which is what downstream verbs assume when they resolve an actor's role. Error names both remediation paths (pick a different --name, or remove fromhost_names:).
gate resumedocuments its same-actor scope and points atgate bootwhen nothing is waiting. Surfaced by a handoff dogfood: a newcomer running resume as a first command saw "Nothing is waiting" and walked away, not realizing their inbox had 3 unread + they were named as --with on two pending requests. resume's scope is "same-actor continuation" by design; the orientation lens is boot. Pinned that boundary in three places — schema summary,gate --helpentry, and the module JSDoc — and added a fallback line to the empty-path prose (en + ja) so a newcomer who ran resume as part of a handoff learns to try boot. No functional change to what resume computes; this is a scope-clarification polish.- User-facing errors no longer leak the
DomainError:class prefix. Errors from the domain layer used to come through the CLI aserror: DomainError: Request not found: ... (id). TheDomainError:prefix was pure noise for the end user — theerror:cue is the universal CLI "this failed" marker, and naming the internal class on top added no information. Dropped. The field suffix ((id),(from), etc.) stays, because that actually names which flag was bad. - Chain documentation refreshed to match bidirectional walk.
gate schema --verb chainpreviously read "walk cross-references one hop from id" — stale since #45 added inbound refs. LLM tool layers consuming the schema would have built their wrapper docs off the one-sided summary. Updated schema summary,gate --helpentry, and the chain module's JSDoc so every surface names the forward-and-inbound behavior consistently.
gate chain <id>walks inbound references too. Previously only forward: chain scanned the root's own text and listed the ids it mentioned. Chain in reverse (who mentions ME) was silent, so an issue promoted to a request could be followed request→issue but not issue→request. Now renders up to four sections —referenced issues,referenced requests,referenced by issues,referenced by requests— joined by standard tree glyphs. O(N) scan over the corpus; typical content_root size makes this cheap.
gate chainsees issue notes.gatherIssueTextnow includes every note body in addition to the immutabletextfield, so a cross-reference added post-hoc as a note is visible to chain the same way it is to show/list. Same-shape fix that joins with the inbound walk — an issue that got a late "see 2026-04-18-0003" note is now reachable from both directions.gate chainempty-state phrasing. Updated from "no cross- referenced records in action/reason/notes/reviews" (one-sided) to "no cross-referenced records; nothing references this either" (bidirectional), matching the new semantic.
invoked_bysurfaces on authored utterances ingate voices/gate tail/gate resume. #43 stamped the creator's invoker ontostatus_log[0], but the read paths for authored utterances (vs review utterances, handled in #41) still hid it. The gap surfaced by dogfooding:GUILD_ACTOR=claude gate request --from erisshowed up ongate showbut vanished ongate voices erisand in resume's "Your last voice was authoring req=X" prose. Now:gate voices/gate tailrender[invoked_by=<actor>]on the authored header (same shape as the review branch);gate resumeprose (en + ja) appends(invoked by <actor>)/(<actor> が代行);AuthoredUtterancegains an optionalinvokedBy, lifted fromstatus_log[0].invoked_byat collection time;RequestJSONgains an optionalstatus_logprojection so voices can read it without pulling in the full domain object. Same-actor creation is untouched.
gate show --format textpads the state column instatus_log. Ragged widths (pending7ch vsexecuting9ch) made theby Xcolumn shift per row. Padded per-render to the max state length in this log, so logs that never reached executing/completed stay compact.
-
invoked_byextended to every proxy-eligible write verb. Previously scoped to the five transitions +review+fast-track(#39). Dogfooding surfaced the gap —GUILD_ACTOR=X gate request --from Ywas silently attributed to Y with no audit trail of X. The invariant now applies to:gate request— initialpendingstatus_log entry stampsinvoked_bygate issues add— issue-levelinvoked_byon the first-frame recordgate issues note— per-noteinvoked_bygate issues promote— the created request's initial status_log entrygate message— per-notification envelopegate broadcast— every fan-out envelope Each verb also emits the standard one-line stderr delegation notice. Omitted from YAML wheninvokedByequals the nominal actor, so same-actor invocations stay byte-identical.gate issues listrenders[invoked_by=<actor>]on the issue header and on each note;gate inboxshows it alongside the sender on every incoming message.
A shared helper pair (
deriveInvokedBy+emitInvokedByNotice) lets creation paths (id unknown until save) defer the stderr notice until after the record is allocated. The existingresolveInvokedBywrapper is unchanged for same-id call sites. -
gate bootemitssuggested_nextfor pre-onboarding shapes. When the caller has no identity yet, boot now prescribes a concrete first action instead of handing back a silent empty payload. Three branches:GUILD_ACTORunset, no members on this content_root →register(fresh root, new agent);GUILD_ACTORunset, members exist →export GUILD_ACTOR=<...>with a short list of existing member names (returning user);GUILD_ACTORset but unknown to the guild →register --name <that-name>(they already picked a handle; just file the record). Registered members and hosts getsuggested_next: null— boot has no unambiguous next action for them. Text format renders a→ next:line with the exact shell command to paste.
- Multi-line values in
gate show --format text/gate voices/gate tailalign continuation lines with the value column. Long--reasonheredocs used to lose their indent on the second line onward, which broke the visual grouping of the field. Applied toaction,reason,completion_note,deny_reason, andfailure_reasonvia a sharedpushMultilineFieldhelper so all three read paths stay in lockstep.
invoked_bysurfaces ingate voices,gate tail, andgate resume. Previouslygate show <id>was the only read path that renderedinvoked_by, so an AI agent that ghost-wrote ten operations on a member's behalf was invisible in the streams where people actually read their activity. voices and tail now append[invoked_by=<actor>]to review lines when the proxy differs fromby; resume'slast_transitionJSON emitsinvoked_by(snake_case, matchingstatus_logon the wire) and the restoration prose names the proxy ("invoked by claude" / "claude が代行"). Same-actor invocations stay unchanged — no clutter for the common case.gate listwithout--stateprints a tighter hint. The prior version listed the state enum twice (once inline, once on a "States:" line); collapsed to one listing, and trimmed the "For X, use" phrasing so the hint is 3 lines instead of 5.
-
Values beginning with
--can now be passed to every flag. The arg parser refuses to consume a next-token that starts with--(it can't tell a literal from a genuine next flag without per-flag metadata), which made notes likegate issues note <id> --by eris --text "--reason - 実装済"silently drop the value. Same shape affected every STDIN-accepting verb (gate request --reason,gate deny --reason,gate fail --reason,gate review --comment,gate issues note --text) whenever the literal itself started with--. The fix is twofold:- POSIX
--end-of-options separator. After a bare--, every remaining token becomes positional, even if it starts with--:gate issues note <id> --by eris -- "--reason - foo". - Clearer errors. When a value-expecting flag lands as boolean
(the surface symptom of this ambiguity), the error now names the
two escape valves —
--key=<value>and-- <value>— so the user isn't left staring at "text is required" after they did pass text.
--key=<value>always worked and is pinned with a regression test.gate --helpgains a short "Values beginning with--" section. - POSIX
-
invoked_byon status_log and reviews. WhenGUILD_ACTORdiffers from the explicit--by(an AI agent acting on a human's behalf), write verbs (approve/deny/execute/complete/fail/review/fast-track) recordinvoked_by: <GUILD_ACTOR>on the status_log entry (or review) and print a one-line delegation notice to stderr. The on-record actor (by) still wins for attribution;invoked_bypreserves the delegation so "eris approved" and "an AI approved on eris's behalf" stop being indistinguishable in YAML. Same pattern as inboxread_by. Omitted whenbyand the invoker agree (no YAML clutter for the self-invocation common case).gate show --format textrenders[invoked_by=<actor>]inline on the matching log entry or review header. -
gate issues note <id>. Append-only annotation for existing issues. The originalseverity/area/textstay immutable by design (Two-Persona Devil: the first-frame record is preserved, not overwritten) — but the understanding of an issue evolves, and without a notes mechanism users had to spawn a whole new issue that referenced the old one just to say "sev should be med in hindsight" or "not reproducible on macOS". Notes take--by <m>,--text <s>,--text -(STDIN), or a positional; they appear under the parent issue ingate issues listas└ note by <who> at <when>: <text>. No edit, no delete — still append-only. -
read_byon inbox mark-read. Mark-read now records the actor that ran the command alongsideread_at, so audits can distinguish "sentinel acknowledged this" from "eris marked it read on sentinel's behalf" (--for <other>). WhenGUILD_ACTORdiffers from the inbox owner, stderr surfaces a# mark-read by <actor> on behalf of <owner>line so the delegation is visible in the session transcript.gate inboxdisplay now shows(read <at> by <actor>)when read_by differs from the owner; identical reads stay formatted as before. -
--reason -reads from STDIN.gate request,gate deny,gate fail, andgate fast-tracknow accept--reason -the same waygate review --comment -already did. Long multi-line reasons can come from$(cat reason.txt)or a heredoc without shell quoting gymnastics.--note -on deny/fail works too for muscle-memory parity.
-
gate listwithout--statepoints atstatus. The error that used to readMissing --statenow spells out the list-vs-status distinction:statusfor counts across every state,list --state <s>for the contents of one. First-time users who reach forgate listto "see everything" get the right verb in one hop instead of reading--helptwice. Schema entry gets the same clarification. -
gate reviewwarns on self-review. When--byequals the request author, a⚠ self-reviewline prints to stderr. The review still lands (history may legitimately need self-annotations), but the Two-Persona Devil frame expects a different voice, and the warning makes the choice visible in the transcript instead of silently laundering it into YAML. -
gate bootreportscontent_root_health. boot payload gainshints.content_root_healthwithmalformed_count, a per-area breakdown (members/requests/issueswith totals and malformed counts), and afix_hintnaming the exact two commands to reach for when anything is malformed (gate doctorto inspect,gate doctor --format json | gate repair --applyto quarantine). Text output adds a corresponding warning block whenmalformed_count > 0; clean roots stay silent. Catches test leftovers and schema-drifted records in the orientation moment, so the recurring hydration warning on every subsequent verb becomes a named one-command fix instead of background noise. The malformed probe is wrapped in try/catch so a failing diagnostic can never break boot.
- Message errors surface guild-flow hints. Four error paths
around
gate message/gate inboxnow name a concrete next verb instead of just rejecting:send --to <host>→ "hosts don't have inboxes; share a request / fast-track / issue instead, host can read via tail/voices".send --to <unknown>→ "not registered;gate register --name <raw>(or check the spelling)".inbox --for <host>→ "hosts observe viagate tail/gate voices/gate list, not their own inbox".inbox --for <unknown>→ register hint, same as send. Each is vertical-formatted (same shape as severity/verdict/lense) so the guidance is scannable. No domain change; existing substring assertions in downstream tests continue to match because the canonical phrase is preserved as a prefix.
gate register— one-shot member registration. Writesmembers/<name>.yamlwithout the newcomer having to hand-author YAML, figure out the schema frommembers.example/, or risk a typo. Category defaults toprofessional(the right bucket for most agents), aliases accepted (pro,prof,member→professional,assigned→assignee,try/tryout→trial).--dry-runpreviews the YAML without touching disk, showing the canonical category so what you see is what gets written. Already-existing names fail loudly rather than silently overwriting.--category hostis rejected — hosts are declared inguild.config.yamldirectly, not registered at runtime. JSON output mirrors the write-response shape withsuggested_next: { verb: "boot", ... }pointing the orchestrator at the next obvious step.assertActorsurfaces the register hint. When an unknown actor is passed to--from/--by/--executor/ etc, the error now includesgate register --name <raw>as a concrete way out. This is the onboarding loop's keystone: newcomers hitting the wall learn the one-command unlock from the error itself.MemberCategoryaccepts aliases with a vertical-format error. Same pattern as severity/verdict — interface-layer convenience, domain invariant unchanged. The rejection error walks the canonical set and the alias table in a scannable table and gently suggests "professional" as the default for most agents.- Concept doc gains "30-second first touch".
docs/concepts-for-newcomers.mdnow leads with the three commands a newcomer needs to exist in a content_root:register→boot→fast-track. Everything else is positioned as "what unlocks as you keep using it." - AGENT.md session-start block now shows
gate registeras the first-time step before the recurringboot/resumeloop, so an AI agent's first read of the quick reference includes the registration path. gate schemalistsregisteras a first-class verb so LLM tool layers can invoke it without out-of-band knowledge.
parseVerdictaccepts grammatical and muscle-memory aliases.approve/approved/pass/lgtm/yes→ok,concerned/concerning/worried/warn→concern,rejected/block/blocked/veto→reject. Case-insensitive after trim. The canonical 3 values and theVerdicttype are unchanged — interface-layer convenience for reviewers (especially AI agents) who reach for the grammatical adjective (concerned) before the noun (concern). Rejection error lists both canonical values and accepted aliases.- Concept map for newcomers.
docs/concepts-for-newcomers.md— a 30-second mental map from Jira / Linear / ADR / PR review / Slack to the guild-cli vocabulary. Elevator pitch, "coming from" table, quick vocabulary list, core loop diagram, and the one thing most newcomers miss.README.mdlinks this as the first stop before the command surface.
- Layered documentation signposts. The README now has a
"How much of this do I need to read?" table naming every doc
with the time cost and when it's enough.
AGENT.mdsays upfront "you don't need to read all of this to be productive" and names which sections are essential.docs/verbs.mdnow opens with "you probably don't need this yet — come back when a verb surprises you." The intent is zero reading pressure at each layer; you keep going only if the value warrants it. suggested_nextdescription softened. Schema and inline comments now make explicit that the field is a convenience hint for orchestrators — safe to ignore if you have other plans. The lifecycle does not demand progression along the suggested axis.
parseIssueSeverityaccepts common aliases.medium,mid,crit,hi,lo, and single-letter shortcuts (l/m/h/c) now normalize to the canonical 4 values. Matching is case- insensitive after trim. The canonical set (low | med | high | critical) is unchanged — this is interface-layer convenience for muscle memory from Jira/Linear/GitHub. The rejection error now lists both the canonical values and the accepted aliases so a first-time user who typedmediumand got rejected learns the extension without reading source.parseLenseerror points atguild.config.yaml. When a domain-specific lense (security,perf, ...) is rejected because the config didn't opt into it, the error now names the exact extension path (lenses:inguild.config.yaml). The extension mechanism has always existed; this just surfaces it in the moment of friction.AGENT.mddocuments domain-specific lenses. Examplelenses: [..., security, perf, a11y]with an explanation that the four defaults are meta-perspectives and domain lenses layer on top.gate boothintsfield — misconfigured-cwd detection. boot payload gainshints: { misconfigured_cwd, config_file, resolved_content_root }.misconfigured_cwdistrueiff noguild.config.yamlwas found up the tree AND the fallback content_root is empty — the concrete signature of "wrong cwd", distinct from an intentional fresh start (config present, 0 data). Text output surfaces a fix hint with the resolved path. Purely additive to the boot payload contract. Motivation: AI agents reading.mcp.jsonoften assumeGATE_CONTENT_ROOTworks for direct CLI invocation (it doesn't — only the MCP wrapper sets subprocesscwd), then hit cryptic "no such member" errors on the next verb. SeeAGENT.md§ Troubleshooting.GuildConfig.configFile. New readonly field onGuildConfig: absolute path of the loadedguild.config.yaml, ornullwhencwdwas used as a fallback root. Lets callers tell "fresh start" apart from "misconfigured cwd".
AGENT.md§ Troubleshooting. Walks through the "no such member" trap:cwdfallback semantics, that no config-resolution env var is read by the CLI as of v0.3.x, and three workarounds (cd, wrapper, symlink).
- Pair-mode Layer 1 —
Request.with.gate requestandgate fast-trackaccept--with <n1>[,<n2>...]to record dialogue partners during the formation of a request. Surfaces ongate show(with: eris),gate voices/tail(authored (with eris)), andgate resumeprose ("shaped with eris" / 「eris と 一緒に」). Partners go through the same actor validation as other--by/--from/--executorfields. Author-self is silently dropped from the list. Layers 2 (durable kinship on Member) and 3 (config policy) are intentionally deferred — they'll be added when real use surfaces the demand. gate resume— picking up where the last session ended. Reads the content_root from the actor's perspective and composes a restoration prompt: last utterance, last lifecycle step, open loops (executing / awaiting_execution / pending_review / unreviewed_completion), suggested_next, and a prose narrative. The prose is deterministic (no LLM call inside the tool) — templated from the same facts the structured fields carry. RequiresGUILD_ACTOR; resume is inherently first-person.examples/agent-voices/— a content_root where agents leave reflections. Seed "survey" requests curated by a host; each agent addslense=userreviews as voice on each theme.gate voices <agent> --lense userreplays a single agent's arc across all themes. README is inside the directory; one quiet pointer in AGENT.md. Not linked from the top-level README — discovery is for agents who look.gate boot— single-command session orientation. Returns identity + status + tail + your recent utterances + inbox unread as one JSON payload. Replaces the three-verbstatus+whoami+tailrecipe.GUILD_ACTORis optional (global view if unset).--format jsonon every write verb.request,approve,deny,execute,complete,fail,review, andfast-tracknow return{ok, id, state, message, suggested_next}. Thesuggested_nextfield is derived deterministically from the post-mutation state so orchestrators can parse it straight into the next tool call.suggested_nextisnullat terminal states. Multi-host content roots omitbyand list candidates inreasonrather than silently nominating a host. Review suggestions intentionally omitverdict— rubber-stamping is the exact failure mode the Two-Persona loop exists to prevent.gate schema— JSON Schema introspection. Draft-07 catalogue of every verb's inputs and outputs. Primary consumer: LLM tool layers. A CI test (schema drift) pins the VERBS list againstindex.tsdispatch so silent drift is impossible.
writes via .tmp-<pid>-<rand>-<basename> + rename() so readers never
observe a torn or partial YAML. Temp files are cleaned up on failure.
- Optimistic lock on save.
Request.loadedVersionsnapshots the total mutation count (status_log.length + reviews.length) at load time. If the on-disk total has grown before save, the repo throwsRequestVersionConflictinstead of overwriting. Catches both transition races (concurrentapprove/execute) and review races (concurrentaddReview, which does not touchstatus_log). gate deny/gate failaccept--note <s>or--reason <s>in addition to the legacy positional argument. Aligns muscle memory withapprove/execute/complete.
- Closure notes have a single source of truth. The domain no
longer writes
completion_note/deny_reason/failure_reasonas separate Request fields; they are derived attoJSON()time fromstatus_log[-1].note. External shape is unchanged — the top-level keys are still emitted for backward compatibility with consumers of the YAML / JSON output. On next save, the status_log entry is authoritative: if a legacy file has the two disagreeing, the top-level value is dropped. Hydration warns viaonMalformedwhen disagreement is detected. host_namesare validated viaMemberName.of()at config-load time. Entries that were previously accepted but could collide with path-traversal, shell metachars, or reserved names now fail loudly withInvalid host_names entry ....findByIdnow scans every state directory and dedupes by total mutation count, so a file mid-transition (present under two dirs between atomic-write and old-file-unlink) deterministically returns the newer representation. The previous first-hit-wins behavior could return the stale pending/ file while the newer approved/ file also existed.gateMCP (mcp/gate_mcp.py) stops mixing stderr into stdout.--format jsonoutput is no longer corrupted by[stderr] ...suffixes. Stderr is forwarded to the host's stderr for observability.
0.3.0 — 2026-04-16
gate statusverb — agent orientation command. Returns pending/approved/executing counts, open issues, unread inbox, and last activity timestamp. Default output is JSON (agent-first);--format textfor human-readable. RespectsGUILD_ACTORand--forfor actor-scoped summaries.- Configurable lenses via
guild.config.yaml. Thelensesfield accepts a list of strings (e.g.[devil, layer, cognitive, user, security]). Defaults to the four built-in lenses when omitted.parseLense()validates against the configured set at runtime. gate repairverb — intervention layer paired withgate doctor. Consumesgate doctor --format jsonfrom stdin (or--from-doctor <path>) and either prints the proposed plan (default--dry-run) or executes it (--apply). Quarantine is the only action: malformed records (top_level_not_mapping,hydration_error,yaml_parse_error) are moved to<content_root>/quarantine/<ISO-timestamp>/<area>/<basename>.duplicate_idandunknownfindings are no-op (data safety: automatic resolution risks data loss).textandjsonoutput formats. The--applypath is idempotent (already-moved sources are skipped, not errored). Path safety is enforced viarealpathSynccanonicalization on both content_root and source — symlink-escape is closed structurally. Closesi-2026-04-15-0026(partial: quarantine path; field-level patch repair tracked separately).CHANGELOG.mdandPOLICY.md— versioning promise and change history.- Doctor plugin system via
guild.config.yaml. Thedoctor.pluginsfield accepts a list of ES module paths. Each plugin exports a function returning additionalDiagnosticFinding[]. Plugin errors become findings (never crash doctor). Enables domain-specific health checks without modifying the core CLI. guild --version/gate --version(alias-v) — printguild-cli <version>and exit 0.
OnMalformedport signature widened from(msg: string) => voidto(source: string, msg: string) => void. Thesourceis the absolute filesystem path of the offending file. This makes the intervention contract type-enforced rather than convention-pinned (previously the path was carried as a prefix of the message string). All three YAML repositories (YamlMemberRepository,YamlRequestRepository,YamlIssueRepository) now pass the absolute source path explicitly.DiagnosticFindinggains a newreadonly source: stringfield, surfaced ingate doctortext/json output. Closesi-2026-04-15-0025.
gate voicesdefault output is now JSON, matchinggate show. Text output is still available via--format text. This aligns with the agent-first design: machine-readable by default, human-readable on request. Existing scripts that parsegate voicestext output must add--format textto preserve behavior.
- Sequence ceiling: Request and Issue ids now use 4-digit
sequences (
YYYY-MM-DD-NNNN/i-YYYY-MM-DD-NNNN), raising the per-UTC-day ceiling from 999 to 9999. The loader accepts both 3- and 4-digit forms; existing content roots continue to work without migration. Generation always produces 4 digits. Regex patterns inchaincross-references, file filters, andnextSequenceparsers all widened accordingly.
- Refactor:
src/interface/gate/index.tssplit intohandlers/{request,review,read,issues,messages}.tsplushandlers/internal.tsfor shared helpers.index.tsis now 158 lines (was 1206) and contains only routing + HELP. Behavior unchanged;formatReviewMarkersandcomputeReviewMarkerWidthare re-exported fromindex.tsfor backward-compat with existing test imports. - Data-loss visibility: malformed YAML records (requests, issues, members, and individual
status_logentries) no longer disappear silently.GuildConfignow carries anonMalformed: (msg: string) => voidcallback, defaulting toprocess.stderr.write("warn: ..."), and every hydrate code path routes skipped records through it with source path + id hint + cause. Tests inject a collecting spy; production users see warnings on stderr.
.github/workflows/ci.yml— typecheck + test on Node 20 / 22.
0.1.0 — 2026-04-14
Initial alpha release. Extracted from the private THS (Three Hearts Space) eris-guild instance and generalized into a public OSS for AI-agent-first team coordination.
- Member management:
guild new | list | show | validate. - Categories:
core | professional | assignee | trial | special | host. host_namesconfig support — non-member actors allowed in--from/--by.
- Request lifecycle:
request → approve → execute → complete(withdeny/failbranches). gate request --from --action --reason --executor --target --auto-review.gate fast-track— one-shotpending → completedfor self-contained work.gate show <id> --format json|textwith time deltas on status/review entries.gate list --state <s>andgate pendingwith--for / --from / --executor / --auto-reviewfilters.
gate review <id> --by <m> --lense <devil|layer|cognitive|user> --verdict <ok|concern|reject>.- Review comment via positional arg,
--comment <s>, or--comment -for STDIN. - Review markers in
gate list/gate pending(✓ ! x ?per lens).
gate tail [N]— unified recent activity stream across all actors.gate voices <name> [--lense --verdict --limit --format]— cross-cutting actor history.gate whoami— session-start orientation viaGUILD_ACTORenv var.gate chain <id>— one-hop cross-reference walk across free-text fields.
gate issues add | list | resolve | defer | start | reopen.gate issues promote <id>— lift issue into new request with cross-reference.- Issue state machine:
open ↔ in_progress ↔ deferred → resolved.
gate message --from --to --text— per-recipient inbox append.gate broadcast --from --text— fan-out with delivery report.gate inbox --for <m> [--unread]— read with unread filtering.gate inbox mark-read [N]— audit-stamped read marker.
GUILD_ACTORenvironment variable — default for--from / --by / --for.- Explicit flags always override;
GUILD_ACTOR= <cmd>for one-off unset. - stderr hint on read-side commands when env var fills in
--for.
- YAML-only persistence. No daemon, no database, no network.
- Path safety via
safeFs(base directory resolve + symlink rejection). - Text sanitization (ASCII control strip, 4 KB action/reason cap, 2 KB issue cap).
- DoS caps: 1000 directory listings, 50 reviews per request, 100 status log entries per request, 500 inbox messages per member.
RequestRepository.listAllwith purededupeRequestsByIdfor concurrent-transition TOCTOU mitigation.
- Comprehensive README with bilingual (EN/JP) AI-agent onboarding section.
examples/dogfood-session/— full content_root generated by this tool tracking its own implementation.SECURITY.md— threat model, enforced invariants, known hardening items.
--auto-reviewis not auto-dispatched; completion prints a ready-to-run review template.- No dashboard generator (raw YAML is the UI).
- No state-transition lock;
saveNewis race-safe (O_EXCL) butsavehas last-writer-wins semantics. - Sequence ceiling 999 per UTC day (ID format
YYYY-MM-DD-NNN).
0.2.0 — 2026-04-15
Stabilization pass: diagnostic/repair verbs, cross-platform fixes, and the infrastructure to make 0.3.0's agent-first features possible.
gate doctorverb — read-only diagnostic scan of the content_root. Reports malformed YAML (parse failures, hydration errors, top-level non-mapping), duplicate ids, and per-area totals. Text and JSON output formats. Closesi-2026-04-14-0015. (#14)gate repairverb — intervention layer paired withgate doctor. Consumesgate doctor --format jsonoutput and quarantines malformed records to<content_root>/quarantine/<ISO-timestamp>/<area>/.--dry-run(default) previews the plan;--applyexecutes. Idempotent — already-moved sources are skipped. Path safety viarealpathSynccanonicalization. Closesi-2026-04-15-0025,i-2026-04-15-0026. (#15)guild --version/gate --version(alias-v) — printguild-cli <version>and exit 0. (#9)CHANGELOG.mdandPOLICY.md— versioning promise and change history. (#8).github/workflows/ci.yml— typecheck + test on Node 20 / 22. (#7)
- Sequence ceiling: Request and Issue ids now use 4-digit sequences
(
YYYY-MM-DD-NNNN/i-YYYY-MM-DD-NNNN), raising the per-UTC-day ceiling from 999 to 9999. Loader accepts both 3- and 4-digit forms; generation always produces 4 digits. (#12) OnMalformedcallback:GuildConfignow carriesonMalformed: (msg: string) => void, defaulting to stderr. Every hydrate path routes skipped records through it. (#11)
- Refactor:
src/interface/gate/index.tssplit intohandlers/{request,review,read,issues,messages}.tsplushandlers/internal.ts.index.tsis now 158 lines (was 1206). Behavior unchanged. (#10)
- Cross-platform: Windows path separators,
EDITORfallback tonotepadon win32, richer error messages with hints. (#21) - Diagnostic: YAML parse errors now surface via
onMalformedinstead of silently skipping. (#17) - Sort: Numeric-aware id ordering for mixed 3/4-digit sequences. (#13)
- README extracted verb deep-dives to
docs/verbs.md(803→458 lines). (#16) - README reflects 0.2.0 status and test surface. (#19)
- POLICY.md: MemberName ASCII-only rationale + diagnostic/repair partial stability. (#20)
- README: tighten redundancy, clarify scope and requirements. (#22)
- CI lockfile sync, version drift guard. (#23)