Skip to content

merge: integrate all six supercluster lanes, prove the installed artifact [SC-INT] - #79

Closed
AetherAI3 wants to merge 16 commits into
mainfrom
supercluster/integration-release-gate
Closed

merge: integrate all six supercluster lanes, prove the installed artifact [SC-INT]#79
AetherAI3 wants to merge 16 commits into
mainfrom
supercluster/integration-release-gate

Conversation

@AetherAI3

Copy link
Copy Markdown
Owner

What this is

The six feature lanes composed onto one head, to answer the question none of them can answer alone: do they break each other?

Nothing here is merged to main. This is a branch that exists to be proven, and to be thrown away once the lanes land individually.

Merge results

order lane result
1 SC-A0 skills & health clean
2 SC-A2 project context clean
3 SC-A3 review / ship clean (COMMANDS.md auto-merged)
4 SC-A4 local brain clean
5 SC-A1 tool runtime clean
6 SC-A5 usage continuity conflict — resolved below

Conflict — src/commands/chat.ts, the local branch of runTurn

lane A1 threads the AbortSignal into runLocalTurn so Ctrl+C reaches a local turn
lane A5 marks the session local-unmetered so usage is not reported as zero spend
chosen both, A5's label first so an abort arriving immediately still finds the session correctly classified
reason the changes are independent; taking either side alone silently drops a shipped contract
proof test/chat_local_abort.test.ts (A1) and test/usage_cap.test.ts (A5) both pass on this head

A defect integration found that no lane could

SC-A5's cap gate was duplicated.

Its patch anchor matched both runTurn and runCloudTurn, so the cap check and the turn-id allocation were written into each. Every turn ran checkUvtCap() twice and burned two ids from the counter.

Not a correctness bug — settleTurn keys off the id runCloudTurn actually uses, and a doubled gate returns the same verdict. But it is duplicated control flow on a spend boundary, and SC-A5's own PR body describes the gate as living in runCloudTurn.

Removed the runTurn copy. The gate now exists once, in runCloudTurn — which is also the only billable path, since local turns are unmetered and must not be gated.

Every lane was individually green and CI-clean. This was invisible until they were composed. That is what this lane is for.

Repository metadata (finding A0-F7)

package.json repository/bugs, the README CI badge and links, CONTRIBUTING.md and docs/PRODUCTION_OPERATIONS.md pointed at DBarr3/aether-agent.

Verified before editing, not assumed. gh repo view resolves DBarr3/aether-agent, DBarr3/Unlimited-Context-LLM and DBarr3/aethercloud all to their AetherAI3 equivalents — so all three are canonically AetherAI3 today and the old paths only redirect.

Deliberately not rewritten:

  • _loopstate/ audit artifacts and dated docs/specs/ design records — historical records of what was true when written.
  • NOTICE.md:5 — the author's personal GitHub profile, not a repository reference.

Gates on the integrated head

command result
npm run typecheck exit 0
npm test 1054 pass / 0 fail
npm run verify:production exit 0 — ok:true, 518 files, 2343944 bytes
npm pack aether-agents-0.1.0.tgz, 518 files, 563150 bytes

1054 accounts for every lane exactly: 922 on clean 41a7e261, plus 84 (A0.1) + 1 (CodeQL regression) + 10 (A0.2) + 6 (A2) + 12 (A3) + 5 (A4) + 4 (A1) + 10 (A5). No test was lost or double-counted in the merge.

Installed-artifact proofs

Run against the packed tarball installed into a temp prefix, not the source tree.

# proof result
1 aether --version 0.1.0
2 aether --help renders
3 aether skills list 6 built-in skills, resolved from dist/src/skills/builtin inside the package
4 aether doctor --json schema 2, 24 checks, all 6 new skill/instruction checks present
5 doctor payload contains a boolean-shaped verified false — PR #71's competing schema-v2 contract is absent from the shipped artifact, not merely unused
6 doctor payload contains a not-checked axis true — main's contract is live in the packed build
7 support-bundle canary seeded aek_live_CANARY_… not present; no aek_ token shape anywhere in the 25600-byte tar
8 skill trust lifecycle untrusted → trust → trustedflip one bytechanged, and doctor reports warning: 1 project skill(s) untrusted or changed
9 non-TTY trust refused without --yes — printed the digest and permissions, recorded nothing. Fails closed.

Proof 8 is the digest-bound trust contract demonstrated end to end on the installed package: a single byte of SKILL.md invalidates prior trust.

Known limits — what this head does NOT prove

  • Linux and Windows CI have not run on this branch yet. The 1054-test result is a local Windows run. Per-lane CI is green on all six PRs (ubuntu + windows + CodeQL + analyze + supply-chain), but this composed head needs its own matrix before anything is claimed cross-platform.
  • npm run smoke still fails on cloud turn — HTTP 401, expired local session token. Verified pre-existing and operator-owned; it needs aether auth login, not a code change.
  • Canaries not run: denied-mutation (1), cancellation with a live child process (2), reconnect replay (3), project freshness against a live remote (4), review-and-ship with a fake gh (5), usage cap through a real reconnect (8), local-vs-Ollama transcript parity (9). These need a live brain, a real remote, or a fake gh harness.
  • This branch is not the merge path. The lanes should merge individually in the stated order; this head exists to prove they compose.

Merge order

#72 SC-A0 → #73 SC-A2 → #74 SC-A3 → #75 SC-A4 → #77 SC-A1 → #78 SC-A5

#78 will need the chat.ts rebase resolved as above, and the duplicated cap gate removed — it is a real fix, not a merge artifact, and should be applied to #78 itself rather than left to integration.

PR A0.1 of the SC-A0 Skills & Health integration rescue.

Recovers the skill and instruction subsystems from PR #71 (a868f7d) onto
current main (41a7e26) without importing PR #71's parallel doctor engine.

PR #71 branched at b98ef26 (2026-08-12) and never saw PR #66 (27c100d,
2026-08-14), which landed its own doctor v2 on main. Both declare doctor
schema version 2 with incompatible payloads: main's health.ts models
configured/reachable/verified as Axis objects with a "not-checked" state,
PR #71's contracts.ts models them as plain booleans. A boolean cannot
express "not checked", so in fast mode — which performs no network I/O —
every remote axis would have to report false, which is indistinguishable
from checked-and-failed. main's contract is both newer and safer, so it
stays canonical and PR #71's is dropped rather than merged.

This commit lands only the part of PR #71 that main has no equivalent of,
and which turned out to apply to main unmodified:

  src/core/skills/*        schema, digest, lock, trust, discovery, loader,
                           resolver, policy, bounds, eval, session, settings,
                           context packet, permission vocabulary
  src/core/instructions/*  AGENTS.md discovery and resolution with provenance
  src/core/why_log.ts      capability explanation log

Capabilities, support bundle, and the skills/instructions doctor checks
follow in A0.2; CLI wiring and packaged skill assets follow in A0.3.

The full conflict matrix, including the four textual conflicts and the
decision record for each contested surface, is in
_loopstate/LOOP-01/sc-a0-2026-08-19/AUDIT-ARTIFACT.md.

Gates at this commit, run against this exact tree:
  npm run typecheck   exit 0
  npm test            1006 pass / 0 fail  (baseline on 41a7e26 was 922/0)
CodeQL js/incomplete-sanitization (high) on PR #72, at
src/core/instructions/instruction_resolver.ts:18 — the metacharacter escape
in globToRegExp used a non-global regex.

Not exploitable as written: `char` is `glob[index]`, always a single code
unit, so there is never a second occurrence to miss. But the safety of that
escape rests on an invariant nothing in the function states, and a future
change to a multi-character token would silently widen every glob's match
set. Fixed rather than dismissed, since the fix is free.

Adds a regression test that pins the property rather than the implementation:
a glob containing . + ( ) | { } [ ] $ ^ must match itself literally and must
not match a decoy path, while ** and * keep working.

Mutation-checked: replacing the escape with a bare `pattern += char` fails
the new test with "unescaped . would match axts"; restoring it passes.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            1007 pass / 0 fail
PR A0.2 of the SC-A0 Skills & Health integration rescue, part 1 of 2.

Recovers PR #71's capability matrix and support bundle onto main:

  src/core/capabilities.ts            capability matrix
  src/generated/agent_capabilities.ts packaged offline fallback snapshot
  src/core/support_bundle.ts          redacted, self-verifying bundle
  src/core/redaction.ts               shared redaction vocabulary
  src/core/tar.ts                     dependency-free tar writer

capabilities, redaction, and tar applied unmodified. support_bundle needed a
two-line port: it called PR #71's doctorReportV2, which this rescue drops in
favour of main's diagnosticReport (see the conflict matrix in
_loopstate/LOOP-01/sc-a0-2026-08-19/AUDIT-ARTIFACT.md, decision C1).

The swap is a strict improvement for the bundle. PR #71's report modelled
configured/reachable/verified as plain booleans, so a fast-mode bundle — which
performs no network I/O — had to serialize verified:false for every remote
axis, indistinguishable from checked-and-failed. main's HealthReport carries
per-axis "not-checked", so an unexercised probe stays visibly unexercised in
the artifact a user sends to support.

Still owed for A0.2 part 2: porting PR #71's skills and instructions checks
(the only two it has that main lacks) from its CheckSpec onto main's
CheckOutcome, and the safe-repair reconciliation.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            1017 pass / 0 fail  (1007 before this commit)
  npm run smoke       3 pass / 3 skip / 1 fail

The smoke failure is `cloud turn` HTTP 401 "Invalid or expired session token".
Verified pre-existing: the identical failure reproduces at 1521199 with this
commit's changes stashed. It is an expired local credential requiring
`aether auth login`, not a code regression, and no file in this commit is on
the cloud-turn path. Recorded as an operator-owned gate, not a pass.

Bundle safety is covered by the ported suites, both passing here: a seeded
canary secret is rejected rather than shipped, and an interrupted generation
leaves no misleading "complete" artifact behind.
…ontract

PR A0.2 of the SC-A0 Skills & Health integration rescue, part 2 of 2.

Ports the only two checks PR #71 has that main lacks — skills and instructions
— from PR #71's CheckSpec onto main's DiagnosticCheckSpec/CheckOutcome, and
wires them into fast mode. Six checks land:

  skills.index            store integrity and index errors
  skills.lock             lockfile presence and drift
  skills.trust            project skills untrusted or changed
  skills.evals            declared eval manifests
  instructions.graph      source count and parse warnings
  instructions.conflicts  detected conflicts, by topic

This is a shape translation, not a rewrite. PR #71 modelled a result as a
status plus a detail string with configured/reachable/verified as plain
booleans; each axis here carries its own state and evidence.

The translation is where the contract decision earns its keep. These checks
touch only the filesystem, so `reachable` is n/a — there is no remote to
reach — rather than a pass borrowed from a probe that never ran. Under PR
#71's booleans there was no way to say that: `reachable: false` would have
read as unreachable. `verified` is a genuine yes/no because these checks do
exercise the files they report on during the run.

The axis mapping lives in one helper (`localOutcome`) rather than being
repeated per check, so a future check cannot quietly claim a `verified` it
did not earn.

Extends, rather than relaxes, the frozen check-ID inventory in
test/diagnostics.test.ts. That assertion exists to catch unintended drift;
this drift is intended, so the expected list grows and the deep-equal stays
exact. Adds an assertion that both new categories report `reachable: "na"`,
so a later change cannot silently upgrade them to a pass.

Live proof against the built CLI at this commit — `aether doctor --json`:

  skills.index            cfg=yes reach=na ver=yes  0 skill(s) indexed
  skills.lock             cfg=yes reach=na ver=yes  no project skills, no lock required
  skills.trust            cfg=yes reach=na ver=yes  no project skills awaiting trust review
  skills.evals            cfg=yes reach=na ver=yes  no skills discovered
  instructions.graph      cfg=yes reach=na ver=yes  0 instruction source(s), no parse warnings
  instructions.conflicts  cfg=yes reach=na ver=yes  no instruction conflicts detected

The emitted payload contains no boolean-shaped `verified` field, confirming
PR #71's competing schema-v2 contract is absent rather than merely unused.

"0 skill(s) indexed" is truthful, not a defect: the built-in skill assets are
not packaged until A0.3.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            1017 pass / 0 fail
…ged assets

PR A0.3 of the SC-A0 Skills & Health integration rescue.

Makes the work in A0.1 and A0.2 reachable. Until this commit the skill runtime
existed but no user could invoke it.

  aether skills <subcommand>   list, show, check, trust, lock
  aether capabilities          capability contract, with --available
  aether support-bundle        redacted metadata-only diagnostic archive

Built-in skill assets (SKILL.md, skill.json, evals) ship as data, which tsc
does not emit, so `build` now runs scripts/copy-skill-assets.js after compile.
The existing `files: ["dist/src"]` entry already carries them into the tarball;
verified below rather than assumed.

Deliberately NOT taken from PR #71:

  * Its cli_registry entry rewriting `doctor [--deep]` to `[--network] [--fix]`.
    `--deep` stays read-only per decision C2 in the conflict matrix; aliasing it
    to a network-performing mode would silently change what an existing habit
    does. `--network` is a separate, explicitly named mode when it lands.
  * Its main.ts doctor flags (--network, --schema, --category, --failed), which
    belong to the doctor engine this rescue drops. main's doctor parses its own
    argv, so it needs none of them.
  * Its `aether agent --skill` / `--no-skills` flags and dev-session skill
    context. Those touch code.ts, chat.ts, brain_cloud.ts and stream.ts, which
    SC-A1, SC-A4 and SC-A5 own. Deferred rather than raced.

COMMANDS.md gains real sections, not just index entries. The parity test only
asserts the index, but an index pointing at nothing is a doc that lies by
omission. Every subcommand documented was then invoked to confirm it exists:

  skills list   -> 6 built-ins listed
  skills show   -> aether/ship@1.0.0 — Ship
  skills check  -> ok
  skills trust  -> "aether/ship is a builtin skill — already trusted"
  skills lock   -> locked 0 project skills

Live proof at this commit, against the built CLI:

  aether skills list        6 built-in skills, scope and trust shown
  aether capabilities       contract v1, falls back to the packaged snapshot and
                            states that it did so, and why
  aether support-bundle     25600-byte tar written, sha256 reported
  aether doctor --json      skills.index now reports "6 skill(s) indexed"
                            (it read 0 before the assets were packaged)

Gates at this commit:
  npm run typecheck        exit 0
  npm test                 1017 pass / 0 fail
  npm run verify:production exit 0 — ok:true, 518 packed files, 2315934 bytes
  npm pack --dry-run       18 dist/src/skills/builtin/** entries present

The COMMANDS.md parity test failed first on this change, correctly: the CLI
registry had grown and the doc had not. Fixed by documenting the commands, not
by relaxing the assertion.
This file was written into the worktree by running `aether skills lock` while
verifying that the documented subcommand actually exists. It is a byproduct of
that check, not source, and it was swept in by `git add -A` in the previous
commit.

`aether skills lock` is a project-scoped command: the lockfile belongs to
whichever repository a user runs it in, generated on demand. Shipping this
repo's own lockfile would pin an empty project skill set into the package for
no reason.
…lind

Lane SC-A2, slice 1 of the project-continuity work.

`aether agent --repo owner/name` reused an existing local mirror on the
strength of one `existsSync` check:

    if (existsSync(join(dir, ".git"))) return { dir, cloned: false };

There is no `git fetch` anywhere in this codebase, so that mirror was never
refreshed. `createWorktree` then branched off it with no start-point argument,
meaning a task could silently begin from a tip that was current days ago, and
`prCreateHint` would go on to invite a PR from it.

The mirror path is derived from the slug alone
(~/.aether-agent/repos/<owner>-<name>), so the remote was never validated
either: any directory sitting at that path was accepted as the requested repo.

Adds `refreshMirror`, called by `ensureLocalClone` before an existing mirror is
returned. It guarantees three things:

  1. the mirror's origin really is the repo that was asked for — compared
     through parseRepoSpec so https/ssh/.git/trailing-slash forms normalize
     rather than needing a second, subtly different URL parser
  2. the mirror is fetched, and the resulting tip is reported
  3. when the fetch cannot happen — offline, auth expired, remote gone — the
     result is "unknown" with the git error attached. It never degrades to
     "fresh" as a convenience

Read-only with respect to the user's tree: it runs remote get-url, fetch and
rev-parse. Never checkout, reset, clean, merge, pull or rebase. A test asserts
each of those six verbs is absent from the recorded argv.

Auth stays the user's own git/gh configuration, inherited from the environment.
A test asserts no Aether credential shape (aek_, Authorization,
http.extraheader, GIT_ASKPASS, x-access-token) reaches the git argv.

`ensureLocalClone` now takes an injected Runner, defaulting to the existing
`defaultRunner()` from worktree.ts. It previously called spawnSync directly and
was therefore untestable; this is the same seam the gated-worktree flow already
uses, so no second runner abstraction is introduced.

The user-facing line stops rounding off. "(reusing local clone)" was equally
true of a mirror last fetched a week ago:

    ⎇ repo octocat/hello-world (fetched) @ a1b2c3d
    ⎇ repo octocat/hello-world (NOT REFRESHED — Could not resolve host: github.com)
      ! this worktree will branch off whatever the mirror already had;
        its base is not known to match the remote.

Written test-first: the six tests were added and confirmed failing to compile
against the missing export before `refreshMirror` existed.

Mutation-checked: rewriting every `state: "unknown"` to `state: "fresh"` fails
"a failed fetch reports unknown, never fresh" (11 pass / 1 fail); restoring
gives 12 / 12.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            928 pass / 0 fail  (922 on clean 41a7e26)

Noted, not fixed here: test/diagnostics.test.ts "a hanging backend cannot stall
the fast report" failed once under full-suite load at 1818ms and passes in
isolation at 82ms. Its 50ms budget is load-sensitive. Pre-existing fragility,
unrelated to this change, left for the lane that owns that file.
…s lying about HEAD

Lane SC-A3, slice 1 of the review-and-ship rail.

Three defects in two commands that discard the user's uncommitted work, and
which shipped with no tests at all.

1. `/rollback [n]` accepted a count that did nothing.

   The count was parsed, range-checked to 1..50, and then never referenced
   again — every invocation reverted the entire working tree, whatever number
   was passed. The usage line advertised "revert last n filesystem changes",
   a capability that does not exist: nothing in this codebase records per-step
   filesystem state.

   There is no checkpoint journal to hang a real `[n]` on, so this takes the
   other option the contract allows and drops the argument. Passing one now
   prints what it used to do and refuses, rather than silently ignoring it.
   `/revert step-3` likewise stops saying "coming soon" for something no code
   is working toward.

2. Both commands claimed changes were "restored to last commit". They were not.

   `git checkout -- <path>` restores from the INDEX. With anything staged, the
   content that comes back is the staged content — so the message was false
   exactly when the user had staged work they might have been counting on.

   Both commands now check the index and say which it was, and `/rollback`
   prints the command that does go all the way back to HEAD. The confirmation
   prompt names the same target as the completion line.

3. Neither command worked from a subdirectory.

   Detection was `existsSync(join(cwd, ".git"))`, which reports "not a git
   repository" for every subdirectory of one — where a REPL usually sits.
   Now `git rev-parse --show-toplevel`.

Also: `git checkout --` becomes `git restore --`. Both restore from the index,
but restore only ever touches files, so it cannot be talked into switching
branches by a crafted pathspec.

Adds a testability seam. Both functions take an optional GitToolDeps
{ cwd, git } defaulting to the real runner. It reuses the existing GitRunner
from git_commit_guard.ts rather than introducing a third runner abstraction —
this repo already has two, which recon flagged as a hazard. The three call
sites in slash.ts are unchanged.

First behavioural coverage for these commands — 12 tests, previously zero:

  the count is refused, and refusing mutates nothing
  usage text no longer promises per-change undo
  "last commit" is never claimed while the index is populated
  "last commit" is claimed only when nothing is staged
  untracked files are never removed (no `clean` in any argv)
  declining the prompt issues no mutation
  a clean tree issues no mutation
  a subdirectory of a repo is still in the repo
  outside a repo it refuses and mutates nothing
  /revert passes the path after `--`, proven with a leading-dash filename
  /revert on an untracked file refuses and mutates nothing

Mutation-checked: forcing restoreTargetLabel to always answer "last commit"
fails both truthfulness tests (10 pass / 2 fail); restoring gives 12 / 12.

Registry and COMMANDS.md updated to match — `/rollback` no longer documents an
argument, and both entries state the index-versus-HEAD behaviour rather than
leaving the reader to discover it.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            934 pass / 0 fail  (922 on clean 41a7e26)

Not addressed here, and still open in this lane: hunk-level staging, the
commit and PR rail, and stage_diff.ts still parsing human `git diff --stat`
output with a regex.
…op faking steer

Lane SC-A4, slice 1 of local-brain parity.

Three ways the local Ollama brain quietly disagreed with the host.

1. Tool results were not correlated to tool calls.

   The pending result was a single anonymous resolver and the id parameter was
   literally discarded:

       sendToolResult(_id: string, result: ToolResult): void {
         if (this.pending) { ... }
       }

   Any result satisfied whichever call happened to be waiting. A result
   carrying the wrong id was indistinguishable from the right one, and a
   duplicate vanished with no trace. The loop awaits calls serially so only one
   is ever in flight today, which is what kept this from corrupting runs — but
   nothing enforced that, and nothing would have reported it if it broke.

   Now keyed by id. An unknown id, or a second result for a call already
   settled, emits an error event instead of advancing the loop on a result it
   never asked for. close() drains every outstanding waiter rather than one.

   Also fixes an ordering fragility: the waiter is now registered BEFORE the
   tool_call event is emitted. Registering afterwards only worked because the
   consumer resumes on a microtask — a host that replied synchronously would
   have found no waiter at all.

2. The advertised tool schemas were hand-written and told the model nothing.

       parameters: { type: "object", properties: {}, additionalProperties: true }

   Argument names, types, required-ness and bounds were never advertised. The
   only description of the real shapes lived in free-text strings that nothing
   kept in step with TOOL_DEFINITIONS, and validateToolDefinitionCoverage only
   checks name-set equality, not schema fidelity.

   ollamaToolSchemas() now generates them from TOOL_DEFINITIONS: per-argument
   types, maxLength from maxBytes, integer min/max, an accurate required set,
   and additionalProperties:false — the host rejects unknown arguments, so
   advertising them as allowed only invited a refusal. A test asserts the
   advertised argument set, required set and bounds match the validator for
   every tool, so the two cannot drift.

3. control() accepted pause/resume/steer and did nothing.

   It returned normally, so the host believed the instruction landed. A dropped
   steer then reads to the user as the model ignoring them. This brain runs a
   single-pass loop with no interruption point and genuinely cannot honour
   these, so it now says so in a visible monologue rather than reporting a
   success it did not deliver.

Tests: 5 added — unknown id rejected, duplicate does not advance the loop
twice, close() cannot strand a waiter, control() is visibly honest, and
schemas match TOOL_DEFINITIONS argument-for-argument.

Mutation-checked: restoring the old behaviour (fall back to any waiter when the
id is unknown) fails "a tool result for an unknown id is rejected" — 8 pass /
1 fail; restoring gives 9 / 9.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            927 pass / 0 fail  (922 on clean 41a7e26)

Not addressed here, and still open in this lane: chat.ts drops the AbortSignal
on local turns so Ctrl+C is inert until the turn ends; tool_executor.ts uses
blocking spawnSync with no process-group cleanup, so a timed-out test leaves
its children running; and chat.ts re-implements the host loop rather than
sharing code.ts's. The last two are SC-A1's surface and are deliberately not
raced here.
Lane SC-A1, slice 1 of the cancellable tool runtime.

The REPL builds an AbortController per turn and aborts it on Ctrl+C. runTurn
accepted that signal and then dropped it on the local branch:

    const backend = await resolveBackend(ctx);
    if (backend === "local") {
      await runLocalTurn(ctx, prompt);   // signal not passed
      return;
    }

So Ctrl+C did nothing to a local turn. The abort fired and nothing was
listening; the turn ran to completion regardless. Only the cloud path was ever
cancellable.

runLocalTurn now takes the signal and closes the brain on abort. close() is
what unblocks a loop parked on a tool result, so an abort arriving mid-turn is
observed rather than waiting the turn out. The listener is registered before
the loop starts, so a signal that is already aborted is honoured instead of
starting work that was cancelled before it began.

An aborted turn returns rather than throwing. The user asked for the stop; it
is not a failed turn and must not be reported as one.

Adds a LocalTurnDeps seam ({ brain, exec }) mirroring the one smoke.ts already
uses, so the abort path is testable without an Ollama server, a child process
or real tool execution. runLocalTurn becomes exported for the same reason.

Tests: 4 added, driving a brain that emits one tool_call and then parks exactly
as the real one does while awaiting sendToolResult — abort reaches the brain, an
already-aborted signal stops the turn, an aborted turn does not reject, and a
turn with no signal still completes.

Mutation-checked, and the failure mode is the interesting part: restoring the
old behaviour does not fail the tests, it HANGS them. The runner is killed by
timeout with exit 124, having produced no TAP summary at all, because the turn
never settles. Restored, the same run exits 0. That hang is precisely what a
user experienced when they pressed Ctrl+C.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            926 pass / 0 fail  (922 on clean 41a7e26)

Scope note. This makes a local turn cancellable BETWEEN steps — during a model
request, or while parked awaiting a tool result. It does not interrupt a tool
already executing: tool_executor.ts still uses blocking spawnSync with no
AbortSignal, so Ctrl+C during a long `run_tests` is still not observed until
that command returns, and the command's children are still orphaned on timeout.

Fixing that requires spawn() with process-group cleanup, which requires
ToolExecutor.run to become async, which requires finalVerify to become async —
it is the synchronous ground-truth gate (verify_gate.ts:71, called from
code.ts:371, with 13 tests pinning its behaviour). That is a deliberate,
separate slice rather than something to graft onto this one.
…wn spend as zero

Lane SC-A5, slice 1 of truthful usage and continuity.

/limit was a control that enforced nothing.

`uvtSpent` was only ever written as `= 0` — at construction, in purge(), and
on snapshot restore. No usage frame ever incremented it. `checkUvtCap()` had
zero callers anywhere in the tree. So:

  * `/limit 50000` printed "agent will pause and ask permission if ceiling
    hit". Nothing paused. Nothing checked. The cap was never consulted.
  * `/limit` reported "spent: 0" and a 0% bar for every session, whatever it
    had actually cost, because the number was a constant.
  * The field comment claimed it was "read from custody log". Nothing read it.

Three changes.

1. Usage is measured, or it is unknown. Never zero by default.

   `uvtObserved: number | null` replaces the always-zero counter. null means no
   authoritative frame has arrived, which is not the same as a measured zero
   and is no longer rendered as one. Nothing estimates UVT from token counts —
   only the server's own number is recorded.

   `uvtSpent` survives as a getter for the HUD, which needs a number, and is
   documented as reporting 0 when the answer is unknown so that anything which
   must tell those apart reads `uvtObserved`.

2. Turns settle once, by id.

   The terminal frame carries the turn total and a reconnect can replay it, so
   `settleTurn(turnId, uvt)` is keyed rather than accumulated blindly. A
   replayed done frame is ignored; distinct turns accumulate.

3. The cap is checked before a billable turn starts, and says what it is.

   `runCloudTurn` consults `checkUvtCap()` before doing anything and refuses to
   start when the observed spend has reached the cap. Two states deliberately
   do not trip it: a local brain (Aether meters nothing, so it is marked
   unmetered rather than zero-spend) and a session with no observed usage
   (there is no evidence the cap was reached, and guessing either blocks free
   work or waves through expensive work).

   The wording is now accurate about what it can and cannot do:

     no further turn will START once the server-reported spend reaches it.
     a turn already in flight may still complete and be billed.
     this is a local stop only — your plan and balance are unchanged.

A design change came out of the mutation pass rather than the plan. `remaining`
was a number, and an unmeasured session reported the full cap as headroom —
which is the same false zero in a different costume: it tells the user their
whole budget is intact when in truth none of it has been counted. The first
mutation run did not fail any test, which is what exposed it. `remaining` is now
`number | null`, null when unmeasured, and a test pins it.

Tests: 10 added. Unknown is not zero; a duplicate done frame does not
double-count; distinct turns accumulate; an unknown session is never reported
as capped; the cap trips on reaching it; no cap never caps; local sessions are
labelled unmetered; purge returns to unknown rather than to zero; and
unmeasured headroom is null rather than the full cap.

Mutation-checked, both guards: restoring the false-zero headroom and removing
the replay dedupe fails two tests (8 pass / 2 fail); restoring gives 10 / 10.

Gates at this commit:
  npm run typecheck   exit 0
  npm test            932 pass / 0 fail  (922 on clean 41a7e26)

Known limits. The HUD still renders `uvtUsed: reg.uvtSpent`, so it shows 0 for
an unknown session — hud.ts is SC-INT's surface and is deliberately not touched
here. Session-cap persistence across resume, and the cross-workspace rejection
that goes with it, are a later slice. The cap is enforced on the REPL cloud
path; `aether agent` has its own loop and is not yet gated.
…ory metadata

Lane SC-INT. Composes the six feature lanes onto one head and proves they do
not break each other. Nothing here is merged to main.

Merged in the stated integration order:

  SC-A0  supercluster/a0-skills-health-integration   clean
  SC-A2  supercluster/a2-project-context             clean
  SC-A3  supercluster/a3-review-ship                 clean (COMMANDS.md auto-merged)
  SC-A4  supercluster/a4-local-brain                 clean
  SC-A1  supercluster/a1-tool-runtime                clean
  SC-A5  supercluster/a5-usage-continuity            CONFLICT, resolved below

Conflict 1 — src/commands/chat.ts, runTurn local branch.

  lane A1  threads the AbortSignal into runLocalTurn so Ctrl+C reaches a local turn
  lane A5  marks the session local-unmetered so usage is not reported as zero spend
  chosen   both, A5's label first so an abort arriving immediately still finds the
           session correctly classified
  reason   the two changes are independent; neither supersedes the other, and
           taking either side alone silently drops a shipped contract
  proof    test/chat_local_abort.test.ts (A1) and test/usage_cap.test.ts (A5) both
           pass on this head

Defect found by integration, not by any lane.

  SC-A5's cap gate was written against a patch anchor that matched BOTH runTurn
  and runCloudTurn, so the gate and the turn-id allocation were duplicated into
  each. Every turn ran checkUvtCap twice and burned two ids from the counter.
  Not a correctness bug — settleTurn keys off the id runCloudTurn actually uses,
  and a doubled gate returns the same verdict — but it is duplicated control
  flow on a spend boundary, and SC-A5's own PR body describes the gate as living
  in runCloudTurn.

  Removed the runTurn copy. The gate now exists once, in runCloudTurn, which is
  also the only billable path: local turns are unmetered and must not be gated.

  This is exactly what this lane is for. Every lane was individually green and
  the duplication was invisible until they were composed.

Repository metadata (finding A0-F7 from the intake artifact).

  package.json repository and bugs, the README CI badge and links, CONTRIBUTING
  and docs/PRODUCTION_OPERATIONS pointed at DBarr3/aether-agent.

  Verified rather than assumed before editing: gh repo view resolves
  DBarr3/aether-agent, DBarr3/Unlimited-Context-LLM and DBarr3/aethercloud all
  to their AetherAI3 equivalents, so all three are canonically AetherAI3 today
  and the old paths only redirect.

  Deliberately NOT rewritten: _loopstate/ audit artifacts and dated docs/specs
  design records, which are historical records of what was true when written,
  and NOTICE.md line 5, which is the author's personal profile URL rather than a
  repository reference.

Gates on the integrated head:

  npm run typecheck   exit 0
  npm test            1054 pass / 0 fail

  1054 accounts for every lane exactly: 922 on clean 41a7e26, plus 84 (A0.1)
  + 1 (CodeQL regression) + 10 (A0.2) + 6 (A2) + 12 (A3) + 5 (A4) + 4 (A1)
  + 10 (A5). No test was lost or double-counted in the merge.
@AetherAI3

Copy link
Copy Markdown
Owner Author

Update — cross-platform matrix landed, and the A5 fix moved to its own lane

CI on 2b2d3cc: 5/5 green — Test (ubuntu-latest), Test (windows-latest), CodeQL, analyze, supply-chain.

That closes the largest known limit in the description above. The composed head is now proven on both platforms in CI, not only by a local Windows run.

The duplicated cap gate has moved

The description records that SC-A5's cap gate was duplicated into both runTurn and runCloudTurn, and that the fix belonged on #78 rather than here. That has been done — d817f62 on supercluster/a5-usage-continuity.

Worth recording why it matters beyond the tidy-up: the test suite could not see it. #78 is 932/932 both before and after the fix. Duplicated control flow on a spend boundary returns the same verdict twice, so nothing failed. It was visible only when six independently-green lanes were composed and read side by side.

Re-verified on this head after merging the fix back: checkUvtCap now appears exactly once, and the suite is unchanged at 1054 / 0.

Still not proven

  • Seven canaries remain unrun — denied mutation, live cancellation with a child process, reconnect replay, freshness against a live remote, ship with a fake gh, cap across a real reconnect, and local-vs-Ollama transcript parity. Each needs a live brain, a real remote, or a harness that does not exist yet.
  • npm run smoke still fails on cloud turn (HTTP 401, expired local token). Operator-owned; needs aether auth login, not a code change.

This branch remains proof, not a merge path. The lanes should still land individually in the stated order.

@AetherAI3

Copy link
Copy Markdown
Owner Author

Superseded by the lanes themselves. All six lanes plus the de-flake are now merged to main individually (#82, #72, #73, #74, #75, #77, #78), each with CI green against the main it actually landed on.

This branch was never a merge path — it existed to prove the lanes compose, and it did its job: it caught a duplicated cap gate that was invisible to 932 passing tests and green per-lane CI on both platforms. That fix landed on #78 where it belonged.

Closing rather than merging, as stated in the description.

@AetherAI3 AetherAI3 closed this Aug 19, 2026
@AetherAI3
AetherAI3 deleted the supercluster/integration-release-gate branch August 19, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant