Skip to content

feat: durable media output history, one safe opener, and aether doctor v2 - #66

Merged
AetherAI3 merged 5 commits into
mainfrom
feat/reliability-lane
Aug 14, 2026
Merged

feat: durable media output history, one safe opener, and aether doctor v2#66
AetherAI3 merged 5 commits into
mainfrom
feat/reliability-lane

Conversation

@AetherAI3

Copy link
Copy Markdown
Owner

What this fixes

Three defects that share a shape: state you can lose without being told.

output open 101 was ambiguous. vision.ts wrote index: entries.length + 1, and retention trims the array to 100 — so the 101st generation and every one after it all claimed 101. The resolver returned whichever came first.

A corrupt history looked like an empty one. readLog() was catch { return [] }. Parse failure, disk failure and "no generations yet" were indistinguishable.

The index could be lost outright. saveLog() was a bare writeFileSync over the only copy — no lock, no fsync, no backup. Two Agent turns writing at once silently dropped one side; an interrupted write could truncate everything.

Opening anything went through a shell. execSync(`${cmd} "${entry.filepath}"`) for files, cmd /c start "" <url> for URLs. A filename containing ", & or a backtick was a command-injection primitive. The Windows file path was also simply broken — start is a cmd builtin, not an executable.

aether doctor reported one axis. "The backend URL is well-formed" and "the backend answered just now" both rendered as a green pass, and an unrun check showed skip beside real passes.

Implementation map

Area Files
Shared primitives src/core/durable_store.ts (new), src/core/opener.ts (new)
Media history v2 src/core/media_history.ts (new), src/core/media_history_store.ts (new), src/core/vision.ts, src/core/browser.ts
Command surfaces src/commands/output.ts, src/commands/media.ts, src/commands/slash_media.ts
Doctor v2 src/core/health.ts (new), src/core/diagnostics.ts, src/core/doctor_live.ts (new), src/core/doctor_repair.ts (new), src/commands/doctor.ts
Injectable for the receipt proof src/core/custody.ts (optional trailing path param; every existing call site unchanged)

Media history v2

Root document is versioned: schemaVersion, generation, nextSequence, updatedAt, entries[]. Each entry carries an artifactId (UUID, generated once, never changes) and a sequence — a persistent monotonic alias serialised as a decimal string, so output open 285 stays convenient without the counter ever being derived from entries.length. Trimming happens after allocation, so retention can never lower the next reference.

Migration. v1 arrays migrate on first read. An index that is a positive integer and unique across the whole legacy set keeps its value; duplicate, missing, zero, negative and non-integer indexes are reallocated above the survivors in stable chronological order. Repairs land before the document is committed, so the resolver never picks a first match. Repeated migrations of the same input are deterministic.

Durability. Every write is one locked transaction: acquire an owner-stamped cross-process lock → read the best valid generation → allocate under the lock → validate → write a same-directory temp → fsync → stage a backup of the previous generation → atomic rename → re-read and confirm the committed generation → release in finally. The temp is always a sibling of the target, because a temp on another volume turns rename into copy+delete.

Recovery. Reads try primary → backup → rebuild from the output directory, and return a state (ok / migrated / recovered-backup / rebuilt / degraded) with a warning the CLI prints above results. An unreadable primary is preserved as .corrupt.<timestamp>; a document with a newer schemaVersion is left strictly alone and refuses both append and clear. Rebuilt entries derive stable IDs from file facts, so re-running a rebuild does not mint a fresh duplicate set, and they are labelled recovered with prompt and model left empty rather than invented.

Resolution. Sequence → full artifact ID → unique ID prefix → unique filename. Ambiguity lists its candidates instead of guessing.

Safe opener

One implementation for files and URLs, shared by media, auth login and github connect. Validates first — http/https only, no embedded credentials, files must exist and be regular files or directories — then hands the target to explorer.exe / open / xdg-open as an argument array with shell: false. Paths are resolved to absolute, which also removes the leading-dash case. doctor --live proves this exact code path.

Doctor v2

Three axes per check. An axis nobody exercised reports not-checked, never a pass; verified now can only be stamped from the current run; a surface this build genuinely lacks (Actions dispatch, Predator) reports n/a with the reason.

Agent transport
  configured     yes
  reachable      yes
  verified now   yes · 15:42:08

Fast is strictly read-only — no network, model, session, opener launch, credential refresh or write. New checks: media index, opener (validated and command-selected without spawning), GitHub, Protocol-C receipt storage.

--live runs the real proof: catalog fetch; dev session; sequence-numbered frames checked for strict monotonicity; pause → resume → steer, in that order, carrying the run nonce; a sandboxed tool write/read/compare/delete confined to a doctor temp dir regardless of what the server asks for; clean session close; a browser open confirmed by a loopback callback; GitHub identity; branch freshness; MCP broker; Protocol-C receipt round trip.

No billed probe. The create request carries purpose: "doctor" and max_uvt: 0. The loop runs only if the server echoes purpose: "doctor" with billable: false; anything else is closed immediately and reported unproven with the reason. Every usage/done frame is accounted, and a run that was billed — or left a session open — reports spend.none as an error.

Branch freshness never fetches. ls-remote plus local object queries. When origin has a commit this checkout has never seen, it says so rather than fetching to find out. A test asserts no mutating git verb is ever invoked.

MCP. A tool is called only when it declares itself readOnly and doctorSafe. No such annotation exists on the broker today, so the honest result is "reachable, and here is why no tool proof was possible".

--fix is a closed allowlist: create missing state dirs, tighten Aether-owned permissions, remove provably abandoned locks and transaction temps, rebuild the media index preserving the corrupt original, prune worktree metadata git already reports gone. It prints exact scope / action / risk / reversibility / backup and changes nothing without --yes; --dry-run shows the plan and stops. Each repair re-checks its own precondition at apply time, so a plan built against a stale report cannot force a mutation. The forbidden set is exported as data so every exclusion has a negative test.

--deep keeps its original read-only meaning and points at --live.

Threat cases covered

  • filename containing " & ` $( ) ' and spaces cannot spawn a second process — asserted on the argument array, not a rendered string
  • leading-dash filename cannot reach xdg-open as a flag
  • URL schemes outside http/https rejected; embedded credentials rejected
  • bare Windows drive path is not misclassified as a URL (new URL("C:\\x") parses with protocol c:)
  • cross-volume rename avoided: the temp is always a sibling of the target
  • the doctor sandbox tool call cannot touch anything outside its temp root
  • doctor output and JSON redact token / JWT / AWS / Slack / private-key shapes and URL query strings; a test asserts the stored credential never appears in a live report
  • --fix has a negative test per forbidden operation

Tests

npm run typecheck
npm test

922 pass, 0 fail (886 on the base commit; the two --deep network tests were replaced by read-only and live equivalents). New files: test/durable_store.test.ts, test/opener.test.ts, test/media_history.test.ts, test/media_history_store.test.ts, test/doctor_repair.test.ts, test/doctor_live.test.ts.

Notable coverage: 250 generations at retention 100 (no reference reused, nextSequence survives every trim); four concurrent writer processes spawned via real execFileSync children (no duplicate sequences, no lost entries, no leftover lock or temp); crash injection at every persistence transition; corrupt-primary → backup → rebuild → degraded; future-schema refusal; deterministic duplicate-101 migration; all seven branch-freshness states; billed-run and orphan-session detection; sandbox cleanup.

CI (.github/workflows/ci.yml) runs ubuntu-latest and windows-latest, covering the atomic-replace, locking, opener and path behaviour on both.

Migration and rollback

No action on upgrade. output open <number> keeps working; v1 logs migrate on first read and stay visible. Rollback is asymmetric: an older binary reads a v2 document as an unparseable array and would report an empty history. The v2 file is not destroyed, but a downgrade should be paired with restoring .genlog.json.bak or letting the rebuild path repopulate from disk.

The doctor JSON report is now schemaVersion: 2; scripts should read the three axes rather than a single status field.

Docs

COMMANDS.md doctor section rewritten; the /output row documents the new reference forms. Patch notes in RELEASE_NOTES.md and docs/releases/2026-08-14.md, indexed in docs/releases/README.md.

Known gap, stated rather than hidden

--live's agent-loop probes report unproven against today's server, because no non-billable doctor session exists yet. The client contract is implemented and documented (purpose: "doctor" + max_uvt: 0 out, purpose: "doctor" + billable: false back); the server half is an AETHER-CLOUD change (api/routes/agent_dev_session_routes.py) and is deliberately not in this PR. Until it lands, those checks say why they could not be proven instead of showing green.

…rm opener

`aether output open 101` could resolve to any of dozens of artifacts. The v1
index wrote `index: entries.length + 1`, and retention trims the array to 100,
so the 101st generation and every one after it all claimed index 101. A parse
failure returned `[]`, making a corrupt history indistinguishable from "no
generations yet". The whole file was rewritten with a bare writeFileSync, so an
interrupted write could lose every entry, and two Agent turns writing at once
would silently drop one side.

Schema v2 gives every artifact a UUID that never changes plus a persistent
monotonic `sequence` alias that survives trimming, so the convenient numeric
reference stays convenient without ever being reused. The counter is stored, not
derived from the retained window.

Writes now run one locked transaction: acquire an owner-stamped cross-process
lock, read the best valid generation, allocate under the lock, validate, write a
same-directory temp, fsync, stage a backup of the previous generation, rename
atomically, then re-read and confirm the committed generation before reporting
success. A crash at any phase leaves either the old or the new generation intact.

Reads recover in order — primary, backup, rebuild from the output directory —
and every degraded outcome returns a visible state (`recovered-backup`,
`rebuilt`, `degraded`) with a warning the CLI prints above the results. An
unreadable index is preserved as `.corrupt.<stamp>` instead of being overwritten,
and a document written by a newer Aether is left strictly alone.

Opening moves to one argument-array implementation shared by media, `auth login`
and `github connect`. The two old paths each built a shell string — vision.ts ran
execSync(`${cmd} "${filepath}"`) and browser.ts spawned `cmd /c start "" <url>` —
so a filename containing a quote, `&` or a backtick was a command-injection
primitive. Targets are validated first (http/https only, no embedded credentials,
files must exist), then handed to explorer.exe/open/xdg-open with shell disabled.

- v1 indexes migrate on read; duplicate 101s are repaired deterministically
  before the document is committed, so the resolver never picks a first match
- ambiguous references now report their candidates instead of guessing
- 53 new tests: identity, retention, migration, four-writer cross-process
  concurrency, crash injection at every persistence transition, future-schema
  refusal, and a filename-injection regression
…answers

`aether doctor` reported one axis — pass / warn / fail / skip. That made "the
backend URL is well-formed" and "the backend answered just now" both read as a
green pass, which is the exact confusion a health command exists to remove. A
`skip` for an unrun check sat next to real passes in the same summary line.

Every check now answers three questions independently: is it configured, is it
reachable, and was it verified during this run. An axis nobody exercised reports
`not-checked`, never a pass, and a `verified now` timestamp can only come from
the current run. A surface this build genuinely does not have — Actions dispatch,
Predator — reports `n/a` with the reason, instead of a green tick for something
that does not exist.

Four checks are new: the media output index (schema, retained count, next
reference, recovery state), the opener (validated and command-selected without
spawning anything), GitHub identity, and Protocol-C receipt persistence.

Fast mode is now explicitly read-only: no network call, no model call, no session,
no opener launch, no credential refresh, no write. `--deep` keeps meaning exactly
what it meant and points at the new mode rather than silently becoming something
that mutates.

`--fix` is a closed allowlist, not a repair agent. It prints the exact scope,
action, risk, reversibility and backup behaviour of every planned repair, and
changes nothing without `--yes`. `--dry-run` shows the plan and stops. Each repair
re-checks its own precondition at apply time, so a plan built against a stale
report cannot force a mutation. The forbidden set — credential rotation, UVT
spend, model invocation, source edits, git ref mutation, Actions dispatch,
Predator runs, MCP write tools — is exported as data so every exclusion has a
negative test.

- allowlist: create missing state dirs, tighten Aether-owned permissions, remove
  provably abandoned locks and transaction temps, rebuild the media index
  preserving the corrupt original, prune worktree metadata git already reports gone
- `--live` is declared and refuses with an explanation. The session, opener-callback
  and receipt proofs are not built yet, and reporting fast-mode results as verified
  would be the false green this change removes
- 18 tests: axis defaults, ordering, redaction, n/a honesty, --deep contacting
  nothing, allowlist closure, live-lock survival, evidence preservation, and a
  negative test per forbidden operation
…ipts

`--live` was declared but refused, because reporting fast-mode results as
verified is exactly the false green this command exists to remove. It now runs a
real proof, and still refuses to claim anything it did not exercise.

The agent sequence is driven serially — create, frames, pause, resume, steer,
sandboxed tool round trip, close — because a parallel version could not tell a
missing acknowledgement from a racing one. Frame sequence numbers are checked
for strict monotonicity, the steer carries the run nonce, and the tool call is
answered by a write/read/compare/delete confined to a doctor-owned temp
directory regardless of what the server asked for.

No billed health probe. The create request carries purpose:"doctor" and
max_uvt:0, and the loop runs only if the server echoes purpose:"doctor" with
billable:false. Anything else is closed immediately and reported as unproven
with the reason, rather than risking a probe that costs the user money. Every
usage and done frame is accounted, and a run that was billed — or that left a
session open — reports spend.none as an error.

The opener proof serves a page on loopback and waits for that page to call back
with the run nonce, so a process that spawns but renders nothing is not a pass.
Headless reports skipped, never verified.

Branch freshness compares local HEAD to the remote tip using ls-remote plus
local object queries only. When origin has a commit this checkout has never
seen, it says so instead of fetching to find out — a health command must not
mutate the repository. A test asserts no mutating git verb is ever invoked.

The MCP probe calls a tool only when the tool declares itself readOnly and
doctorSafe. No such annotation exists on the broker today, so the honest result
is "reachable, and here is why no tool proof was possible" rather than guessing
which tool looks harmless.

The Protocol-C proof persists a receipt, reads it back, verifies the commitment,
and proves a replay is de-duplicated — through the production
appendCustody/readCustodyLog path, but pointed at a doctor sandbox so the user's
real receipt log is never written to. custody.ts gained an optional trailing
path parameter for this; every existing call site is unchanged.

- adds --no-ui for headless boxes, and --only to narrow a live run
- every temp file lives under one sandbox directory, removed in finally
- 22 new tests: sequence monotonicity, all seven branch states, billed-run and
  orphan-session detection, unacknowledged control, missing doctor-safe tool,
  signed-out, sandbox cleanup, and a no-credential-leak assertion
- docs: COMMANDS.md doctor section rewritten, patch notes for 2026-08-14
…n exit

server.close() only stops accepting new connections. A browser holding the
loopback proof page open keeps the handle — and therefore the process — alive
after the probe has already answered.
@AetherAI3
AetherAI3 merged commit 27c100d into main Aug 14, 2026
5 checks passed
@AetherAI3
AetherAI3 deleted the feat/reliability-lane branch August 14, 2026 18:42
AetherAI3 added a commit that referenced this pull request Aug 19, 2026
…upport bundle [SC-A0.1 + A0.2a] (#72)

* feat(skills): skill runtime and instruction resolver foundation

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)

* fix(instructions): escape every glob metacharacter, not just the first

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

* feat(capabilities): capability matrix and redacted support bundle

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.

* feat(doctor): skill and instruction health checks on the three-axis contract

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

* feat(cli): aether skills, capabilities and support-bundle, with packaged 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.

* chore: drop .aether/skills.lock.json committed by mistake

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.
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