Red CI fixed, wind-down decision layer, and the ruled 0.2.1 work - #16
Merged
Conversation
…irst effect test_agent_exit_while_detached_releases_immediately broke out of its poll the instant active.current() went None, then immediately asserted the grace timer had been popped. But release() clears the slot FIRST and only then awaits transport.end() plus an executor hop before _deferred_release's finally pops _pending — so the assert raced that tail and lost twice on loaded CI runners (runs 33849778047, 33821410455) while always winning locally. The previous de-flake (iteration count -> deadline) could not fix this because it rebudgeted time instead of reordering observables. Poll for the pop itself — the last effect in the release path — so every assert runs against a settled lifecycle, the same philosophy _disconnect already documents. The test still fails when the liveness poll is broken: mutation-verified by forcing _transport_alive() -> True, which turns exactly this test red (the 30s grace window cannot expire inside the 15s deadline).
get_by_role(name=...) is a substring match, so 'Generate' also matched the
Today view's empty-state CTA 'Generate content →' — a strict-mode violation
on any machine with no decks, which is CI's state (run 33857350882) and
rarely a dev machine's. Data-dependent, not flaky.
exact=True resolves each sidebar button uniquely; the CI failure log itself
shows Playwright naming the sidebar button as
get_by_role('button', name='Generate', exact=True) alongside the CTA, and
the file already uses that exact locator for the same button elsewhere.
The sidebar SVG icons carry no accessible text, so the exact names are the
span labels. Verified: just test-browser-smoke, 20/20.
… Python
page.wait_for_function does not await a returned Promise: the Promise
object itself is truthy, so every 'async () => ...' predicate passed on
its first poll no matter what it would have resolved to. Verified
empirically — wait_for_function('async () => false') returns instantly.
That silent no-op is how phase 5 of the body-double journey went red on
main (run 33859082268): its 'wait until the server holds both notes' guard
never waited, and the active_total read raced the second POST on a loaded
runner. Six call sites carried the same defect — one had already failed,
five were latent.
Add _env.await_async_predicate, which drives the predicate through
page.evaluate (which DOES await) on a Python-side deadline, and convert
all six sites. Positive control per the campaign's trap #1: an
always-false predicate times out, an eventually-true one passes at the
flip. Sync predicates keep using wait_for_function, which is cheaper.
Verified: test_body_double_journey 13 passed; test_session_recovery_journey
+ test_ghostty_dev_terminal 40 passed, 2 skipped.
…d merge MATCH (R-92)
Two defects in one line. The join carries a content column on BOTH sides
(messages_fts.content and messages.content), so the unqualified
'content MATCH ?' raised 'ambiguous column name: content' on every call —
R-22b then correctly re-raised it, so get_study_history failed outright.
And even qualified, the shape OR'd one MATCH constraint per keyword, which
FTS5 refuses ('unable to use function MATCH in the requested context'), so
the multi-keyword path — the normal path for every caller — was broken
either way.
Replace it with ONE qualified MATCH whose FTS5 query string ORs the
keywords, each double-quoted as a phrase: several study terms carry spaces
('window functions', 'lake formation') and unquoted they would parse as
loose AND'd terms and match scattered occurrences. An empty keyword list
now returns [] without opening the DB, where it previously built
'WHERE ()' — a syntax error.
Tests build the REAL agent-session-tools schema.sql (the ambiguity only
exists in the real shape) and were verified red against the old code:
single keyword, OR'd multi-keyword, phrase-not-scattered-terms, and the
empty-list guard.
…nd-down --json
Layer 1 of the acceptance-harness plan (WD-1..WD-3). The protocol used to
ask the agent to derive the one second-brain offer from brain status flags,
which left two of the three acceptance gate checks human-only. The decision
now lives in code and the agent reads it as one JSON object:
{channel, offer, sentence, reason}.
Two rules, deliberately not one conjunction (design council D1):
- publish: configured AND supports_publish. Vault writability is NOT part
of the rule — available is a runtime condition the publish itself reports.
- xtiles: provider == 'xtiles' AND an 'xtiles' connector attached in this
session, passed by the caller via --connector because connector state is
per-session — which is also why this is a subcommand, not a field on
brain status. One conjunction would have made the xtiles channel
permanently false, since XtilesStageOneBackend sets supports_publish=False
on purpose.
No command field in the payload: nothing guarantees a plan id at wind-down,
and a half-filled command string is worse than none (council ruling).
WD-1 (test_wind_down_decision.py) pins all 8 truth-table rows against the
pure function; WD-2 (test_wind_down_cli.py) re-runs every row through the
real CLI in a subprocess with a real config file, byte-compares the
sentence, and writes the wind-down-truth-table.json artefact; WD-3 extends
the docs pin to a third location — the CLI's own constants — and adds the
xTiles sentence pin (skill <-> CLI). The protocol and the skill shrink to:
run the command; if offer is true say sentence verbatim once, else say
nothing about second brains.
Mutation-verified: inverting truth-table row 7 turns the row-7 tests red;
drifting one byte of the protocol sentence turns the pin tests red.
Import boundary preserved: wind_down.py imports no provider module
(test_second_brain_optionality.py still green). 110 tests across the six
touched files pass; pyright clean.
…rulings ARBITRATION Q2 rows 1-4 plus addendum items N1-N4 and N6, with the replacement sentences the review wrote out. Row 1 (the BLOCKER-class honesty defect): 'three tested prompts' and 'written for Claude Code, which is where it was tested' both overstated the evidence. The prompts have now been exercised end to end exactly once, by a person, in Kiro CLI 2.21.0 — not Claude Code — so the guide and core.py now say precisely that, using the sentence from the owner's filled checklist. Row 2 / C5: the planner prompt becomes the P1b wording the owner verified — a Markdown tile, not a task, because a tile is the only shape that returns a URL and the only one the live UI check has validated. The guide names which prompt produces a URL. Row 3: the offer-once claim is hedged to what the evidence supports: the skill instructs it; no recorded session shows it yet. Row 4: harness wording narrowed (paths verified against vendor docs; behaviour verified per the run), and the OpenCode skill link is dropped from installers.py — the hub is already on OpenCode's global search path, and nothing verified it de-duplicates two hits by name. N1: the guide and the skill now say the next action and due reviews are the learner's whole study state, not one plan's. N2: the project prompt stops promising a board view or collection-page refreshes; the connector's resource types bite before any tier does. N3: the wind-down prompt skips the Review task when nothing is due instead of inventing a date, and the guide carries the R-93 sentence: the xTiles learning record is not written back into the plan. N4 (as corrected): planner tiles can be removed through the connector; pages and projects are deleted in the UI by hand. N6: the permission-prompt sentence is attributed to xTiles, with what you see depending on the assistant. 76 tests across the docs/protocol/installer files pass; mkdocs builds.
…_plan_learning (R-93)
LearningRecord was constructed in exactly one place in the package: the
Markdown parser. So a record existed only if the learner typed it into the
plan document by hand — and for an xTiles learner following the wind-down
prompt, the session's learning record existed only in xTiles, inverting
ADR-0010 (the second brain became the sole store, the plan stopped being
the record of what was learned). Found as N8 in the 2026-09-04 review.
The writer follows the arbitrated design exactly:
- parse -> append a LearningRecord -> save_plan, never an append of raw
Markdown: save_plan re-renders through render_plan, so the on-disk shape
('### LR-0004 — Title') stays the renderer's business and the projection
and template guards keep pinning it unchanged.
- numbering is max(existing)+1, so a superseded record keeps its number and
gaps are never reused.
- re-running with the same title and body is a byte-level no-op (the vault
writer's idempotence rule), which makes agent retries safe.
- one guard the council did not ask for but the parser demands: H1-H3 lines
in a body are refused, because _split_sections/_subsection_items would
re-parse them as new sections or records on the next load (and
_subsection_items ignores code fences), silently corrupting the document.
Surfaces: studyloop plan record <plan-id> --title T [--body|--body-file]
[--status] [--json], and a record_plan_learning MCP tool for the mentor at
wind-down. The guide's wind-down prompt and the xTiles skill now record
into the plan FIRST, then offer the xTiles projection — the page is a
projection of a record the plan already has, never the only copy.
ADR-0010 clause 1 is amended to the rule the code obeys (per
FINDING-adr-0010-already-inaccurate.md): the original 'nothing in a
backend, the CLI or an agent protocol writes the plan file' was untrue the
day it shipped — five plan commands write it by design. The amended clause:
nothing in the SECOND-BRAIN layer writes it; studyloop plan … is its only
writer, through render_plan.
16 new tests (store/CLI/MCP); 120 across the touched surface pass; pyright
clean.
O1 — no directory is ever created outside the vault. mkdir(parents=True) ran BEFORE the containment re-check, so an ancestor swapped for a symlink between projection_path and the write created directories on the far side of the link before the file write was refused. Containment is now checked on the nearest EXISTING ancestor before mkdir (resolve() on unbuilt directories cannot see where a hostile link sends children), re-checked after mkdir as before, and once more immediately before os.replace — narrowing the accepted TOCTOU window to the rename itself. The new test asserts the property that matters: nothing appears outside the vault, not merely that an error was raised. O4 — the dry run previews the replace warning. classify_write returned WRITTEN for an owned-and-changed note, so --dry-run said 'would write' where the real publish then said 'written + replaced your edits' — the one warning added so a learner is told BEFORE losing text was invisible in the preview. classify_write now returns REPLACED, plan_dry_run carries the 'would replace your edits in …' warning, and journey beat 10 asserts the preview warns (and that the dry run itself does not touch the edit). O7 — an unreadable mode refuses the replace. The 0o644 fallback on any OSError meant a note the learner had locked to 0o600 could be silently rewritten world-readable if one stat failed. A vanished file still gets the 0o644 new-file default (the inode-identity check refuses the race); any other stat failure on an existing note now refuses with the reason. All three ruled MINOR-with-small-fix by ARBITRATION Q3. 95 tests across the writer/backend/CLI files pass; 35 journey tests pass; pyright clean.
Makes 'a connector named xtiles is attached' REAL for the transcript acceptance tests without a network, an account or an OAuth flow: a stdio server named xtiles exposing the three write tools the prompts select between (create_tasks, create_tiles_from_markdown_in_my_planner, create_view_from_markdown), appending every call to a JSON artefact. Deliberately dumb — no validation, no state, no response fidelity beyond the one thing the prompts check (the planner-tile tool returns a URL). Anything smarter would be a second xTiles implementation for tests to accidentally depend on. Two teeth worth naming. The stub REFUSES to run without a call-log path: a stub that ran unlogged would pass WD-5's silence checks vacuously, the exact positive-control trap (D4) this suite exists to avoid. And 'reaches no network' is enforced two ways — stdio transport by construction, plus an AST scan that goes red if a network-capable import creeps in. WD-4 drives the real handshake with the official SDK client: server name 'xtiles' (the name IS the skill's gate), exactly the three tools, every call logged in order, appends never overwrites. Mutation-verified: making _log_call silently drop a call turns both log tests red.
… the next cut (Q5) 0.2.0 shipped the whole second-brain layer while its change sat at 0/19 tasks, unarchived, with a proposal still selling the withdrawn Obsidian-CLI adapter — an agent following docs/contributing.md found no capability spec and instructions to build something three families cut. Ruling: archive- and-reconcile, never archive fiction (ARBITRATION Q5). Reconciled BEFORE archiving: proposal.md's What Changes and Risks now match design D4/D12 (adapter withdrawn; the four keys it used are refused with a naming error); tasks.md records what actually happened — shipped items ticked, the adapter and daily_note struck, the per-lane verifier items replaced by the P2 council that actually reviewed the merged diff, and the owner prompt run ticked against its filled checklist (run 2026-09-04, Kiro CLI 2.21.0). Then openspec archive second-brain, which wrote the second-brain capability spec into openspec/specs/ and applied the two deltas. ADR-0010 is now Accepted (it shipped), here and in the index. The guard, so this cannot recur: check-release-consistency.py gains a --release mode wired into release-check via the new release-consistency-shipped recipe (NOT preflight — open changes are legal during a cycle; only shipping one is not). It fails when any directory under openspec/changes/ (archive/ excluded) has commits since the last tag and is neither archived nor carrying an explicit 'deferred: <reason>' in its .openspec.yaml, and it validates archive entries added since the last tag with the openspec CLI (soft-skip when absent, same convention as spec-check; scoped to NEW archives because a July archive predating the guard has unticked tasks nobody has evidence to reconcile). The always-on ADR check fails any ADR present in the last tag that still says Proposed. Positive controls, per this campaign's trap #1: setting ADR-0010 back to Proposed turns the always-on check red (proving it would have fired on 0.2.0); restore turns it green. The shipped-changes guard's control run follows in the next verification step, since it needs a committed change.
…fix archive-name parse The Justfile half of the Q5 guard commit (missed there: the tracked file is 'Justfile', and the add used the lowercase name). release-check now runs release-consistency-shipped — the --release superset — while preflight keeps the plain recipe, since open changes are legal during a cycle. Also fixes validate_new_archives' name extraction (parts[3], not parts[2] — which read 'archive' as the change name) and records the positive controls run against the finished guard: a committed, undeferred open change turned the gate red with the change named; adding 'deferred: <reason>' to its .openspec.yaml turned it green with the reason printed; the newly archived 2026-09-04-second-brain validates.
…ng .text pyright: call_tool content is a union (Text/Image/Audio/ResourceLink/ EmbeddedResource); assert the type discriminant first, which is also the stronger test.
The gate resolves bare-name helpers from a test's source; a method on the
test class ('self._run') is an attribute call it cannot follow, so 'plan
record' read as uncovered. The invocation now lives in a module-level
helper the gate can see.
There was a problem hiding this comment.
🟡 Changes recommended
The Unreleased changelog currently contradicts the new “record learning into the plan first” behavior and should be corrected for accurate public documentation.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR resolves previously red CI by fixing flaky/incorrect Playwright waits and accessibility selectors, and it adds a new “wind-down decision” layer plus a plan learning-record writer so end-of-session behavior can be driven by deterministic JSON (CLI + MCP) and pinned documentation.
Changes:
- Add
studyloop brain wind-down --jsondecision command +decide_wind_down()(with tests pinning truth-table rows and pinned offer sentences across code/docs/skills). - Add
studyloop plan record+record_plan_learningMCP tool (idempotent parse→append→save via renderer) and related tests. - Strengthen release gates and second-brain safety: new release-consistency guards (ADR status + OpenSpec shipped/archive checks), Obsidian writer TOCTOU containment hardening, and docs/spec/ADR updates to match ruled wording.
File summaries
| File | Description |
|---|---|
| scripts/check-release-consistency.py | Adds release-check mode guards for ADR status staleness and OpenSpec “shipped but unarchived/undeferred” changes + validates newly archived changes. |
| Justfile | Switches release-check to run the new release-consistency shipped-mode superset. |
| packages/studyloop/tests/test_xtiles_stub_server.py | Adds tests for the stub xtiles MCP server (handshake/tools/logging + no-network import scan). |
| packages/studyloop/tests/_xtiles_stub_server.py | Introduces a stdio-only stub MCP server named xtiles with JSON call logging. |
| packages/studyloop/src/studyloop/second_brain/wind_down.py | New pure decision module with pinned offer sentences and JSON payload shape. |
| packages/studyloop/tests/test_wind_down_decision.py | Pins the wind-down truth table as a pure function and asserts “two rules, not one conjunction”. |
| packages/studyloop/tests/test_wind_down_cli.py | Pins CLI output (python -m studyloop … brain wind-down --json) to the same truth-table contract and writes an evidence artifact. |
| packages/studyloop/src/studyloop/cli/_brain.py | Adds studyloop brain wind-down command producing decision JSON/human-form output. |
| packages/studyloop/tests/test_second_brain_docs.py | Pins publish offer sentence across docs/protocol/CLI constant and pins xTiles offer sentence to the skill. |
| packages/studyloop/tests/test_second_brain_agent_protocol.py | Updates protocol contract test to require brain wind-down --json (not brain status --json) and retains flag explanation checks. |
| agents/shared/wind-down-protocol.md | Updates Phase 1 second-brain offer flow to delegate to the decision command and handle xTiles via the skill. |
| agents/skills/studyloop-xtiles-wind-down/SKILL.md | Updates skill gate to use the decision command and adds “record into plan first” rule + pins offer sentence in delimited markers. |
| agents/skills/studyloop-xtiles-wind-down/references/harnesses.md | Updates harness path reference to reflect hub-served OpenCode behavior. |
| packages/studyloop/src/studyloop/installers.py | Removes OpenCode per-harness xTiles skill link; documents why (hub already on global search path). |
| packages/studyloop/tests/test_install_agent_contracts.py | Updates installer contract expectations for OpenCode hub-served behavior (no per-harness link). |
| docs/second-brain.md | Updates xTiles guide wording to match observed behavior, documents limits, and adds the wind-down decision command. |
| packages/studyloop/src/studyloop/planning/store.py | Adds record_learning() writer: idempotent learning-record append with heading-guard validation. |
| packages/studyloop/src/studyloop/planning/init.py | Exposes record_learning in the planning public API. |
| packages/studyloop/src/studyloop/cli/_plan.py | Adds studyloop plan record CLI command with JSON output and --body-file support. |
| packages/studyloop/src/studyloop/mcp/tools.py | Adds record_plan_learning MCP tool mapping errors to ToolError. |
| packages/studyloop/tests/test_plan_record.py | Adds unit/CLI/MCP coverage for record-learning writer, numbering, idempotence, and validation. |
| packages/studyloop/src/studyloop/history/search.py | Fixes FTS query (qualified MATCH + OR inside query string + empty-keywords fast path). |
| packages/studyloop/tests/test_history.py | Adds tests over the real export schema to prevent ambiguous-column and multi-MATCH regressions (R-92). |
| packages/studyloop/tests/test_web_smoke_browser.py | Uses exact=True for accessible-name smoke checks to avoid substring strict-mode collisions on CI. |
| packages/studyloop/tests/e2e/_env.py | Adds await_async_predicate() helper to correctly poll async JS predicates. |
| packages/studyloop/tests/e2e/test_session_recovery_journey.py | Replaces wait_for_function(async …) with await_async_predicate() and adjusts timeouts. |
| packages/studyloop/tests/e2e/test_ghostty_dev_terminal.py | Replaces wait_for_function(async …) with await_async_predicate() and adjusts timeouts. |
| packages/studyloop/tests/e2e/test_body_double_journey.py | Replaces multiple async wait_for_function usages with await_async_predicate() and improves “what” diagnostics. |
| packages/studyloop/tests/test_session_ws_grace.py | Fixes flake by polling settled lifecycle (pending release pop) rather than first observable effect. |
| packages/studyloop/src/studyloop/second_brain/obsidian_writer.py | Hardens containment checks (pre-mkdir + post-mkdir + pre-replace) and refuses unreadable-mode replaces; improves dry-run classification via REPLACED. |
| packages/studyloop/src/studyloop/second_brain/obsidian.py | Makes dry-run preview replacement warnings consistent with real publish warnings. |
| packages/studyloop/tests/test_obsidian_writer.py | Adds regression tests for containment-before-mkdir, dry-run replace warning classification, and unreadable-mode refusal. |
| packages/studyloop/tests/journeys/test_obsidian_learners_week.py | Adds journey assertion that dry-run previews “would replace your edits” warning without performing replacement. |
| packages/studyloop/src/studyloop/second_brain/core.py | Docstring wording tweak (“supplied prompts”). |
| docs/adr/README.md | Marks ADR-0010 as Accepted. |
| docs/adr/0010-second-brains-are-projections.md | Updates ADR-0010 status to Accepted and amends clause 1 wording to match actual plan-writer rule. |
| openspec/specs/second-brain/spec.md | Adds spec for the second-brain capability at the OpenSpec layer. |
| openspec/specs/configuration-and-secrets/spec.md | Adds spec requirement for parsing second_brain into config with one-line errors. |
| openspec/specs/cli-surface/spec.md | Adds spec requirement for lazy brain registration and --json command behavior. |
| openspec/changes/second-brain/tasks.md | Removes active change tasks (archived instead). |
| openspec/changes/archive/2026-09-04-second-brain/tasks.md | Archives reconciled implementation task list per review ruling. |
| openspec/changes/archive/2026-09-04-second-brain/specs/second-brain/spec.md | Stores archived spec snapshot for the change. |
| openspec/changes/archive/2026-09-04-second-brain/specs/configuration-and-secrets/spec.md | Stores archived config spec snapshot for the change. |
| openspec/changes/archive/2026-09-04-second-brain/specs/cli-surface/spec.md | Stores archived CLI-surface spec snapshot for the change. |
| openspec/changes/archive/2026-09-04-second-brain/proposal.md | Updates archived proposal to reflect CLI-adapter withdrawal + retired key refusal. |
| openspec/changes/archive/2026-09-04-second-brain/design.md | Adds archived design write-up including decision table and withdrawn adapter rationale. |
| openspec/changes/archive/2026-09-04-second-brain/.openspec.yaml | Adds archived change metadata. |
| CHANGELOG.md | Adds Unreleased notes for new commands/guards and xTiles guide updates (contains one inconsistency noted in PR comment). |
| agents/manifest.json | Updates manifest hashes/dates for changed agent/protocol/skill artifacts. |
Review details
- Files reviewed: 44/49 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+39
to
+44
| also now says that the next action and due reviews sent to xTiles reflect your | ||
| whole study history (they are not plan-scoped), that per-write permission | ||
| prompts are xTiles' statement about its connector and what you see depends on | ||
| your assistant, that the xTiles learning record is not written back into the | ||
| plan document, and that planner tiles can be removed through the connector | ||
| while pages and projects must be deleted in the xTiles interface by hand. |
…s it caught
Layer 3, plan item 6. Claude Code headless (the one harness with attested
flags) through the LiteLLM gateway - no vendor credential; the key is read
from the proxy's own config at call time, enters the child env only, and is
never printed or passed in argv. Opt in with "just gate-checks"; artefacts
(per-state transcripts, summary.json, tool-routing.json) land under
gitignored reviews/.
The three gate checks, graded on real transcripts with the arbitrated
assertions and nothing stronger: sentence at most once across the whole
transcript; silence = no pinned sentence and no second-brain vocabulary in
assistant prose WHILE the D4 positive control holds (the transcript must
show that "studyloop brain wind-down" actually ran, or a crashed run would
pass by absence); a declined offer leaves the stub log empty - with the
xtiles tools ALLOWED, so declining was a choice, not an inability. 3/3 for
the offer-once-then-decline state; one pre-response infra retry, flakes
counted in the artefact, never smoothed.
Capturing found four real defects; each is fixed here and visible in the
transcript evidence:
1. Protocol violation - the harness narrated the step ("Running the second
brain check now"). The protocol now opens step 5 with the silence rule
and names narration a violation.
2. Fabricated connector flag - given a literal "--connector xtiles" example,
the model passed it with NO server attached, manufacturing an offer. The
protocol now grounds the flag in the mcp__<server>__ tool names the
session can actually see, and calls it a statement of fact.
3. D2 exactly as the council predicted - the hermetic HOME hid the skill and
"Unknown skill: studyloop-xtiles-wind-down" killed an offer turn. The
world now runs the REAL "studyloop install agents" into the isolated
home, which also exercises the installer for free.
4. A gateway bug - LiteLLM's anthropic-passthrough adapter (Bedrock invoke
path) dies on extended-thinking blocks ("Content block is not a text
block"), deterministic on thinking-length prompts. Worked around with
MAX_THINKING_TOKENS=0 in the harness env; proxy upgrade is the real fix.
Final capture: 6/6 pass, 0 flakes. WD-6's observation (records, never
gates): the P1b planner wording selected
xtiles_create_tiles_from_markdown_in_my_planner - matching the owner's hand
run. Spend recorded in the ledger: est $1.50, actual $4.45; the overrun is
Claude Code's ~30k-token own system prompt per call plus five debug
re-runs, both now named in the estimate file so the next one models them.
[message restored 2026-09-04: the original -m string was double-quoted and
its backticked command names were command-substituted by the shell, which
ran a paid gate-checks capture mid-commit and pasted pytest output into
this message. The accidental run's evidence is preserved under
reviews/2026-09-04-gate-checks/evidence/gate-checks-capture2-accidental/.]
…y naming it An accidental extra capture (see below) caught a third violation shape the first two rules missed: after "No thanks.", the mentor said "The learner declined, so no xTiles write happens" — naming the declined service in the very turn that was supposed to close the subject. The protocol now lists it alongside the other two named violations and states the rule positively. Re-captured after the change: S3 offer-once/decline/silence 3/3, 0 flakes. The accidental capture itself is a lesson recorded here so it is not repeated: the previous commit ran `just gate-checks` mid-commit because its message was passed with -m "..." (double quotes) and contained backticked command names — zsh command-substituted them, executing a paid live run and pasting pytest output into the committed message. That mangled message is in the pushed history of this branch; evidence of the run is preserved under reviews/…/gate-checks-capture2-accidental/. Commit messages with backticks go in single quotes or a file from now on.
…s wrappers (Q5 step 3) EARLY WARNING, NOT ENFORCEMENT, and labelled so in every file: the hard gate stays `just release-check` + CI. The hook exists so a release action is questioned at the keyboard instead of failing twenty minutes later — and per the arbitration's own caution, a hook that cannot be shown to block is never described as enforcement. Verification state is recorded per harness in the script header: Kiro's PreToolUse exit-2 block is verified against the harness's own hook documentation; Claude Code's is vendor-documented, unattested here; Codex's events were verified against the vendor page in the 2026-09-04 review. One body (scripts/openspec-gate.py, tool call as JSON on stdin), the same hub pattern as the xTiles skill; the wrappers are committed (force-added past the harness-dir gitignore rules, the same convention as openspec/) at .kiro/hooks/openspec-gate.json, .claude/settings.json and .codex/hooks.json so contributors inherit them. The gate IMPORTS the real release guard from check-release-consistency.py rather than re-implementing it — two implementations of "what counts as shipped" is how a hook and a release gate come to disagree. Behaviour: `git tag` / `prepare-release` are blocked (exit 2) when an openspec change with commits since the last tag is neither archived nor deferred; `git commit` gets a one-line warning and is NEVER blocked — open changes are legal during a cycle; anything else exits 0 with no subprocess spawned. UserPromptSubmit gets a terse reminder only when the guard would fail. The hook fails OPEN on its own errors: a broken early warning must never block work the hard gate would allow. Tests drive the script in a subprocess against fixture repos carrying the REAL consistency script: block-on-tag, warn-on-commit, deferred unblocks, garbage stdin fails open, and a wrapper-drift pin that all three wrappers call the one body. Full preflight green: 4790 passed, 4 skipped.
NetDevAutomate
force-pushed
the
fix/red-ci-tests
branch
from
September 5, 2026 10:49
7506af8 to
40664ef
Compare
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
Version 0.2.1 in both pyprojects (and the lockfile), the CHANGELOG Unreleased section rolled into [0.2.1] - 2026-09-05, and a real release note in releases/v0.2.1.md summarising the review-driven work: the wind-down decision command, the learning-record writer, the xTiles wording corrections, the Obsidian writer hardening, the history-search fix, and the acceptance suite with its release guards. Cut per the Q6 ruling: from a green main (PR #16, all 16 checks), with the tag to follow only once GitHub CI is green on THIS sha — the release gate (release-consistency --release) passes locally, including the new shipped-changes guard and the newly archived change's validation. Full default suite on this sha: 4790 passed, 4 skipped.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
Red CI fixed, wind-down decision layer, and the ruled 0.2.1 work
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
Version 0.2.1 in both pyprojects (and the lockfile), the CHANGELOG Unreleased section rolled into [0.2.1] - 2026-09-05, and a real release note in releases/v0.2.1.md summarising the review-driven work: the wind-down decision command, the learning-record writer, the xTiles wording corrections, the Obsidian writer hardening, the history-search fix, and the acceptance suite with its release guards. Cut per the Q6 ruling: from a green main (PR #16, all 16 checks), with the tag to follow only once GitHub CI is green on THIS sha — the release gate (release-consistency --release) passes locally, including the new shipped-changes guard and the newly archived change's validation. Full default suite on this sha: 4790 passed, 4 skipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Works through the acceptance-harness plan (
reviews/2026-09-04-acceptance-harness/PLAN.md) items 1–5 and 7, plus the ordered docs rulings and the sub-hour fixes fromreviews/HANDOFF.md§4._pendingpop), not the first observable; mutation-verified against a broken liveness pollexact=True— "Generate" also substring-matched the empty-state CTA, which renders exactly on deck-less CI machineswait_for_functionnever awaits async predicates (a returned Promise is truthy on the first poll) — six call sites were silent no-ops; all now go through_env.await_async_predicate, which has its own positive controltopic_frequency's FTS query was doubly broken (ambiguouscontent, multiple MATCH constraints);get_study_historyworks again, with tests over the real export schemastudyloop brain wind-down --json [--connector NAME]+ WD-1 (8-row truth table), WD-2 (subprocess, byte-identical sentence, artefact), WD-3 (three-location sentence pin + the xTiles skill pin). Two rules, not one conjunction (D1)studyloop plan record+record_plan_learningMCP tool; parse → append →save_plan; idempotent; ADR-0010 clause 1 amended to the rule the code obeys; wind-down records into the plan firstxtilesMCP server + WD-4 (refuses to run unlogged; AST no-network scan; mutation-verified)release-checkgains the shipped-changes guard and the always-on ADR-status check, both shown to fireTesting
just preflightexit 0 — 4783 passed, 4 skipped (lint, pyright, JS, docs, release-consistency, spec-check all green)just e2eexit 0 — 503 passed, 20 skippedNot in this PR
just gate-checks) — spends model money and needs owner credentialsrelease-check