Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ec8a48e
docs: keep design notes separate from tracked work
arthware-dev Jul 31, 2026
f635a39
test: add an offline gate agents can run unattended
arthware-dev Jul 31, 2026
4b551c6
fix: read the Matrix server name from config instead of hardcoding it
arthware-dev Jul 31, 2026
db12576
feat(test): switch the AI backend between local, mock, and external
arthware-dev Jul 31, 2026
bfeb12b
feat(cli): add stack doctor to diagnose a broken instance
arthware-dev Jul 31, 2026
452612c
docs: make module tests the testing standard
arthware-dev Jul 31, 2026
c57af19
fix: use canonical core__STACKER_BOT_PASSWORD in messages setup
arthware-dev Jul 31, 2026
8b8fe7e
test: stop the stacker-bot credential test writing to the live instance
arthware-dev Jul 31, 2026
da653f4
fix: keep the stacker bot logged in when messages setup re-runs
arthware-dev Jul 31, 2026
912d7a0
test: prove against real Synapse that a password PUT ends the session
arthware-dev Jul 31, 2026
3163168
fix: stop stack doctor reporting image defaults as config drift
arthware-dev Jul 31, 2026
e9a9de7
chore: add branch-status to tell stale branches from live ones
arthware-dev Jul 31, 2026
1fce73a
feat(agent): let the agent search the vault and read family profiles
arthware-dev Jul 31, 2026
c4c0f95
fix: let a stacklet with only optional containers finish setup
arthware-dev Jul 31, 2026
a5fb078
fix: make stack memory person work on a normal install
arthware-dev Jul 31, 2026
d2e4cc6
fix: make the agent's vault tools actually work
arthware-dev Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ If you might do both, load both. They are short on purpose.

## Approach (universal)

Five principles. The first four are distilled from [Andrej Karpathy's observations on LLM coding pitfalls](https://github.com/multica-ai/andrej-karpathy-skills); the fifth is classic separation of concerns. Apply to every change, every role. **Tradeoff:** these bias toward caution over speed. For trivial tasks (typos, obvious one-liners), use judgment.
Six principles. The first four are distilled from [Andrej Karpathy's observations on LLM coding pitfalls](https://github.com/multica-ai/andrej-karpathy-skills); the fifth is classic separation of concerns; the sixth is what we test and why. Apply to every change, every role. **Tradeoff:** these bias toward caution over speed. For trivial tasks (typos, obvious one-liners), use judgment.

### 1. Think before acting
Don't assume. Don't hide confusion. Surface tradeoffs.
Expand Down Expand Up @@ -62,6 +62,16 @@ Put each responsibility with the component that owns the resource or contract, a
- **Split by kind of work.** Pure transformation → its own module, no I/O (unit-testable). I/O and orchestration → the bot/CLI. Domain schema → the producer of that domain.
- **Before adding a handler, ask "is this this component's concern, or just convenient here?"** If the logic is specific to another producer, keep the generic seam and push the specifics back to where they belong. Convenience is not a reason to couple.

### 6. Module tests over unit tests
A module test exercises one coherent piece of functionality from the outside, as a client of it would. Its job is to state the *intent* and pin the *expected behaviour* at the time of writing, so both survive every later refactor. This is the single most valuable thing we produce: implementations get rewritten, intent does not.

- **Write from the caller's side.** Drive the module through its public surface. If a test reaches for a private helper, it is testing how the code works instead of what it promises, and it will break on refactors that broke nothing.
- **Read like good API documentation.** Name the behaviour, not the function. Say why the case matters. A new reader should learn what the module is *for* from its tests alone.
- **Prefer real collaborators.** Real stacklets, real Synapse, a real model via `stacktests ai local`. Mock only what you cannot run, and only at an external boundary.
- **Beware self-confirming tests.** A test written alongside the code it covers proves the two agree, not that either is right. When the fixture encodes the same assumption as the implementation, both pass and reality still disagrees. Assert against an external contract (a spec, a service's real response, an invariant we promise) rather than a restatement of the code.
- **Delete tests that only mirror the implementation.** If it could only fail when someone deliberately changes their mind, it is costing tokens and buying nothing.
- **demo-rig and e2e sit on top.** Module tests carry the intent; the rig lanes prove the wiring holds between real containers. Neither replaces the other.

## Universal non-negotiables

Apply to every role, every session.
Expand Down
7 changes: 5 additions & 2 deletions docs/agent/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,11 @@ Profile details live in [../../tests/README.md](../../tests/README.md).

Testing rules:

- **Module tests first, and they are the point.** See [AGENTS.md § 6](../../AGENTS.md). Test one coherent piece of functionality from the outside, as a client of it would, so the test states the intent and pins the expected behaviour for every later refactor. Write it to read like API documentation: name the behaviour, say why the case matters. demo-rig and e2e sit on top and prove the wiring; they do not replace this.
- **Do not write tests that mirror the implementation.** A test written next to the code it covers proves the two agree, not that either is right. Assert against something external: a spec, a real service's response, an invariant we promise. If a test could only fail when someone changes their mind, delete it.
- **Behavioural TDD: RED then GREEN.** Write the failing test that captures the behaviour you want; make it pass with the smallest change; then refactor.
- **Blackbox at the module boundary.** Test what a module promises through its public surface. Mock only external interfaces (network endpoints, the LLM), and only when truly required.
- **Prefer a real model over a stubbed one.** `tests/integration/stacktests ai local` points the rig at a self-hosted endpoint: real answers, no cost per call, only slower. A green run against a stub proves the wiring, not the behaviour.
- **Docker integration tests when warranted.** If a change crosses a container boundary or depends on a real service's behaviour, add a test under `tests/integration/`.
- **Tests run against real stacklets and real hooks.** No parallel test-only compose files.
- **Use real Synapse via the `messages` stacklet.** No handwritten Matrix mocks.
Expand All @@ -189,7 +192,7 @@ Testing rules:
- **Test helpers do one thing.** Add a parameter only when a second test needs it - not preemptively.
- **`tests/integration/eval/` is opt-in** (slow, real model). Excluded from `pytest tests/` by `norecursedirs`.
- **Write tests before fixing.** No duct tape.
- **Before running integration / e2e tests, ASK.** They collide with the user's running instance.
- **The rig is shared, not off-limits.** This repo root is the Simpsons dev instance, not anyone's real famstack, so agents may run the rig lanes. Ports are fixed, so exactly one run at a time: check nothing else is mid-run before starting. `tests/integration/stacktests help` lists which subcommands are autonomous, shared, or destructive.

## Code style

Expand Down Expand Up @@ -235,7 +238,7 @@ Pre-tag gate, in order. A published tag is never moved; anything missed here shi
## Pre-1.0 conventions

- Invariant changes (marker semantics, field renames, contract shifts) get coherent commits - each stands alone for revert.
- Cleanup backlog lives at `docs/cleanup-backlog.md`. Items there have a reason; surface them when adjacent code is touched.
- Actionable work lives on the tracker board, one card each, every card carrying a verification gate. Design notes - decisions, rejected dead ends, known-but-unresolved tensions - live at `docs/design-notes.md`; surface them when adjacent code is touched. If a note grows a "do this next", move it to a card and leave the reasoning behind.
- Don't add backwards-compatibility shims, feature flags for one-shot migrations, or renamed `_unused` vars.

## What NOT to do
Expand Down
129 changes: 0 additions & 129 deletions docs/cleanup-backlog.md

This file was deleted.

50 changes: 50 additions & 0 deletions docs/design-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Design notes

Decisions and dead ends worth remembering. Surface them when adjacent code
is touched (see `docs/agent/dev.md`, Pre-1.0 conventions).

**This file is not a task list.** Anything actionable lives on the tracker
board, where it carries a verification gate and an owner. What stays here is
the material a card can't hold: why a shape was chosen, what was tried and
rejected, and which tensions are known but not yet resolved. If an entry
below ever grows a "do this next", move it to a card and leave the reasoning.

## Wiki freshness (curator shipped 2026-06-11)

The curator sidecar ships the first two freshness tiers: debounced
incremental rebuilds (persons + home) and the nightly full sweep. The
third tier (chat-triggered rebuild) is a card.

- **Realtime is NOT a requirement.** The mirror is realtime; the wiki
is a derived view. The nightly sweep makes the incremental person
mapping merely *helpful*, never load-bearing - worst case for a
mapping miss is "stale until tonight". Don't grow the incremental
heuristics; grow the deriver instead.
- **Page update strategy - unresolved, and the tension is known
(2026-06-11):** full regeneration resamples page quality (a good page
can regress on the next sweep); evolving the existing page accumulates
errors that self-cite (the "Bartley [5]" finding). Most promising
middle: a fact-checking pass - "page + sources, fix what the sources
don't support, touch nothing else" - anchored to ground truth while
preserving good prose. Likely CLI shape then: `wiki` = update/check,
`wiki rebuild` = fresh full generation.
- **Rejected runtime homes, don't re-litigate:** host daemons (no launchd
surface), quartz container (node image; "the wiki never writes"),
bot-runner service concept (one consumer), bot-runner image reuse
(the curator uses 2 of its 10 deps; slim image won).

## Surviving upstream drift: the `wait_task` pattern

When Paperless-ngx 3.0 redesigned its task API, the fix that held up was
absorbing *both* response shapes in a single parser and covering both
offline: `PaperlessAPI.wait_task` (`stacklets/docs/bot/pipeline.py`) plus
`TestWaitTask` in `test_pipeline.py`.

Worth copying whenever an upstream service changes a contract. One parser,
both shapes, proved in the `unit` lane - it turns a version migration into
a contained task instead of a rewrite, and it means the old version keeps
working while the new one is evaluated.

The corollary is the reason it was needed: **an unpinned image is a
scheduled outage.** `:latest` plus watchtower rolled Paperless from 2.x to
3.0.2 unattended and broke filing across the whole e2e suite.
40 changes: 40 additions & 0 deletions lib/stack/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from pathlib import Path

from . import docker
from . import doctor
from .commands import COMMANDS
from .prompt import ORANGE, TEAL, GREEN, RED, DIM, BOLD, RESET
from .stack import Stack
Expand Down Expand Up @@ -827,6 +828,42 @@ def handle_status(stck, args):
print_status(result)


def handle_doctor(stck, args):
"""Diagnose the instance: what is wrong, and what to type to fix it.

`status` answers "is it up?". When it isn't, this answers "why?" —
the checks live in doctor.py as pure rules; everything here is the
I/O they need.
"""
preferred = stck._cfg("core", "runtime", "orbstack")
docker.init_runtime(preferred)

stacklets = sorted(s["id"] for s in stck.discover())
findings = doctor.diagnose(
stacklets,
stck.env,
docker.containers_for,
docker.container_env,
docker.image_env,
)

print()
if not findings:
print(f" {GREEN}✓{RESET} {doctor.summarise(findings)}\n")
return

for finding in findings:
mark = f"{RED}✗{RESET}" if finding.is_error else f"{ORANGE}⚠{RESET}"
print(f" {mark} {BOLD}{finding.title}{RESET}")
print(f" {DIM}{finding.detail}{RESET}")
print(f" {TEAL}{finding.fix}{RESET}\n")

print(f" {doctor.summarise(findings)}\n")
# Exit non-zero on errors so an agent or script can gate on it.
if any(f.is_error for f in findings):
sys.exit(1)


def handle_list(stck, args):
print_list(stck.list(), stck)

Expand Down Expand Up @@ -1225,6 +1262,7 @@ def _plugin_help(module_path: str):
"down": handle_down,
"destroy": handle_destroy,
"status": handle_status,
"doctor": handle_doctor,
"list": handle_list,
"config": handle_config,
"env": handle_env,
Expand All @@ -1246,6 +1284,7 @@ def _plugin_help(module_path: str):
]),
("Info", [
("list", "Show all stacklets and their status"),
("doctor", "Diagnose problems and print how to fix them"),
("config", "Print stack.toml configuration"),
("config admin", "Print tech admin credentials"),
("env <stacklet>", "Print rendered environment variables"),
Expand Down Expand Up @@ -1331,6 +1370,7 @@ def main():
)
sub.add_parser("init")
sub.add_parser("status")
sub.add_parser("doctor")
sub.add_parser("list")
p = sub.add_parser("config")
config_sub = p.add_subparsers(dest="config_action")
Expand Down
Loading
Loading