gitoriaLog in with ident

antcolony

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitc613d26bc613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mrec613d26b/README.md

129.1 KB

  1. # antcolony-scheduler
  2. The colony's deterministic part (ticket **antcolony#1**, concept: byrodin
  3. `/CONTAINERS/projects/antcolony/docs/scheduler-agent.md` + `README.md`). It reads and writes
  4. **tickets.worldapi.org only through its JSON API** and holds no state of its own.
  5. **v0 (mission 015)** = build-order step 2: a command-line program run BY HAND that replaces the
  6. architect's manual loop (`tools/t.py`). **Agent v0 (mission 016, ticket antcolony#2)** = `work`: one
  7. headless Claude Code worker per call, run by hand — no daemon, heartbeat, WebSocket or quota control yet.
  8. **Controller + cycle (mission 019, antcolony#1 build order 4 part 1)** = after a schema-valid report a second
  9. one-shot Claude session checks it (`verdict: pass|fail`); posting follows the verdict; `cycle` = next → work →
  10. controller → post, once.
  11. **Daemon (mission 020, build order 4 part 2)** = `run`: the loop — leases that expire (heartbeat), Claude quota reserve,
  12. parking + `claude --resume`, a port range per session, clean SIGTERM. Still ONE host, no WebSocket / agent split.
  13. **Scheduler + agents per host (mission 035, antcolony#14)** = `run --serve` (scheduler, starts no worker) + `agent` (per host, runs the
  14. workers, owns quota + ports); HTTPS request/answer because Hybriel has no WebSocket client (section "Scheduler + agents per host").
  15. **Relations, questions, rejections (mission 022, antcolony#1)** = blocked / parent tickets are not work; every report
  16. question becomes its own creator-question ticket (parent = the ticket); a rejected ticket is work again, its brief
  17. starts with the rejection (section "Relations, questions, rejections" below; `lib/relations.hl`).
  18. **Readable ticket texts, weekly limit (mission 023, antcolony#1)** = everything the scheduler writes into tickets follows
  19. antcolony README "Ticket texts are written for the creator" (≤ 5 short lines, plain words, ONE short machine line last —
  20. section "Ticket texts" below); the report gets `result` + `test` (they ARE the comment); `run` starts nothing while the
  21. weekly usage is above `--week-limit` (84 %); hand-filed `Question…` children count as creator questions.
  22. **Finalize step, budget in the brief, salvage (mission 025, antcolony#1)** = a worker that runs out of steps / time
  23. without a report gets its SAME Claude session resumed once to write the report (then controller → post as usual);
  24. the brief states the step budget; `./colony salvage <run>` does the same for a finished run (section "Finalize step +
  25. salvage" below). Resumed sessions report CUMULATIVE cost — the scheduler now counts only the new part.
  26. **Continue, clean up, cheaper finalize (mission 026, antcolony#1)** = the report has `complete` (true only if the goal is
  27. reached AND what the creator tests is really in place); `complete: false` → "Half done: …; the next worker continues." +
  28. back to `open`, never `awaiting creator`; the brief of a ticket with an earlier colony session starts with "Previous
  29. attempt" (how it ended, result, open, work copy, report path); after every worker / finalize / controller part the
  30. scheduler stops what it left LISTENING on its port range (started during that part, never others); the finalize resume
  31. has no `--disallowedTools` (prompt cache: real finalize $0.03 instead of $1.05); `salvage` takes a lease like `work`
  32. (section "Mission 026" below).
  33. **After the first live batch (mission 027, antcolony#1)** = `result` ≤ 120 characters, `test` = 2–4 steps ≤ 90 characters
  34. shown as a NUMBERED list, written for the creator on the LIVE site (never a work copy / local address); a **deploy gate**:
  35. a project with `live` gets "Built — goes live with the next deploy." and stays `in progress` until `./colony deployed
  36. <project> <n>` (`./colony ready` lists them); new report key `decided` (small choices → one "Decided: …" bullet), ≤ 3
  37. short questions; every brief carries the antcolony "Conventions for all apps" as TEXT and the project's own tickets; a
  38. refused report gets ONE fix step (section "Mission 027" below).
  39. **Permanent operation (mission 028, antcolony#1)** = `./colony run --live` as the Docker container `antcolony-scheduler`
  40. on Loreana (`restart: unless-stopped`, starts with the system, no systemd unit); section "Operations" below.
  41. **Librarian (mission 029, antcolony#21)** = `./colony librarian`: the creator's own words from tickets (+ the architect's
  42. chat seed once) → one model step per new text → `DECISIONS.md` per project (by topic, current only; superseded →
  43. `DECISIONS-archive.md`) and `templates/decisions-global.md`; every brief carries them after the conventions; the librarian
  44. runs FIRST in every `run` iteration and before `brief`/`work`/`cycle`; UI projects' briefs name the layout conventions
  45. (section "Librarian" below).
  46. **Lease expiry race (mission 031, antcolony#1)** = the daemon never acts on its survey alone: before it expires a lease or
  47. resumes a parked session it RE-READS the ticket and writes nothing unless it is STILL `in progress` with the same lease; a
  48. lease ENDS with `colony-deployed:`, the session's own report / verdict / give-back, or any state change — such a ticket is
  49. never expired (section "Mission 031" below).
  50. **Keywords in comments (antcolony#17)** = the creator's short commands, code only (no model): `/hold`, `/prio`, `/confirm`,
  51. `/reject <why>` as the FIRST word of a comment by the creator (`COLONY_CREATOR_NAMES`); `run` handles them at the start of
  52. every iteration (no quota needed), by hand `./colony keywords [--post]` (dry run lists them; section "Keywords" below).
  53. **Status + daily summary (antcolony#19)** = `./colony status [--write]` (per project: tickets by state, what waits for the creator)
  54. and `./colony digest [--post]` (one short summary of what waits for the creator); both code only, built from the tickets —
  55. section "Status and daily summary" below.
  56. **Small local models (antcolony#20)** = the librarian's routine decision (is a creator text a decision, for which project) can
  57. run on a small model on a local llama-server instead of Claude — section "Small local models" below.
  58. ## Operations (mission 028) — the container `antcolony-scheduler` on Loreana
  59. **What it is**: `docker-compose.yml` here. A busybox container bind-mounts the host's `/` at `/host`, `docker/pivot.sh`
  60. (the only root part) makes its mounts private, `pivot_root`s into the host filesystem and drops to **uid 1000 (mre)**
  61. with all of mre's groups → `docker/entrypoint.sh` → `./colony run [--live]`. So the scheduler and every worker see the
  62. host's toolchain unchanged: `claude` (`~/.local/bin/claude` + login in `~/.claude`), node, google-chrome-stable (with its
  63. sandbox), rsync, ssh, `/media/STORAGE/projects`, `~/.config/antcolony/tickets-token`. Host network, host PID namespace
  64. (`lib/cleanup.sh` reads `/proc` + `ss`), cap `SYS_ADMIN` + `seccomp=unconfined` (needed for `pivot_root` and Chrome's
  65. user namespaces; uid 1000 has no effective capabilities). Why not an image with the tools installed: Chrome (AUR), node
  66. and claude versions would drift from the host, and Chrome's sandbox would still need the same security options.
  67. Why not `chroot`: the kernel refuses user namespaces to chrooted processes → Chrome's sandbox fails.
  68. ```bash
  69. cd /media/STORAGE/projects/antcolony-scheduler
  70. cp .env.example .env && $EDITOR .env # once: settings (creator 2026-09-24: COLONY_RESERVE=100, COLONY_WEEK_LIMIT=100)
  71. docker compose up -d # start (creates/recreates the container; also after every .env change)
  72. docker stop antcolony-scheduler # stop (see "Stop" below); stays stopped, also after a reboot
  73. docker start antcolony-scheduler # start again with the same settings (parked sessions resume)
  74. docker logs -f antcolony-scheduler # RUN / ITER / SESSION END lines (json-file, 3 × 10 MB)
  75. tail -f logs/colony.log # the same lines with a UTC time, rotates at 10 MB, keeps colony.log.1 … .5
  76. docker compose ps # state; `docker inspect -f '{{.RestartCount}}' antcolony-scheduler`
  77. ```
  78. - **Settings**: env in `docker-compose.yml`, overridden by `.env` (template `.env.example`, all commented with the
  79. defaults): `COLONY_LIVE` (1 = `--live`), `COLONY_RESERVE` (→ `COLONY_QUOTA_RESERVE`, 70), `COLONY_WEEK_LIMIT` (84),
  80. `COLONY_RUN_INTERVAL` (60), `COLONY_PARALLEL` (1), `COLONY_MODEL`, `COLONY_MAX_TURNS`, `COLONY_WORK_TIMEOUT`,
  81. `COLONY_CONTROLLER_*`, `COLONY_MAX_BUDGET_USD`, `COLONY_PORT_POOL`, `COLONY_PORTS_PER_SESSION`, `COLONY_RUN_ARGS` (extra
  82. `run` options), `COLONY_STOP_FINISH_SECONDS` (20), `COLONY_LOG_MAX_BYTES` / `COLONY_LOG_KEEP`. Change → `docker compose
  83. up -d` (a plain `docker start` keeps the old values). The first log lines print every `COLONY_*` value in use.
  84. - **Stop**: docker sends ONE SIGTERM; the entrypoint maps it onto the wrapper's two signals: 1st TERM → **finish** (nothing
  85. new starts; idle → stops in ~1 s); still a session running after `COLONY_STOP_FINISH_SECONDS` (20 s) → 2nd TERM →
  86. **park** (worker/controller killed, `colony-parked` marker, ticket stays leased; took < 1 s in the test) → exit.
  87. `stop_grace_period: 75s` covers it and stays below systemd's 90 s stop timeout of docker.service at shutdown. A
  88. `docker kill` / SIGKILL takes the running session with it (no park; the lease expires after `--lease-ttl`).
  89. - **Reboot**: docker is enabled at boot; `unless-stopped` starts the container again unless it was stopped by hand
  90. (`docker stop`). At shutdown docker stops it like `docker stop` → parked; after the boot `run` resumes the parked
  91. sessions (`claude --resume`). If the scheduler process dies, the container exits and docker restarts it.
  92. - **One-off `colony` commands next to the daemon**: either on the host as mre (same filesystem, same tools) —
  93. `COLONY_TOKEN_FILE=~/.config/antcolony/tickets-token ./colony leases --live` — or inside the container with ITS settings
  94. (docker exec lands in the host filesystem because of the pivot; numeric user only, no supplementary groups):
  95. `docker exec -u 1000:1000 -e HOME=/home/mre -e PATH=/home/mre/.local/bin:/usr/bin:/bin -w
  96. /media/STORAGE/projects/antcolony-scheduler antcolony-scheduler ./colony leases --live`. Don't run `work`/`cycle` by hand
  97. on a ticket the daemon may pick (leases protect in-progress tickets only).
  98. - **Ports**: default pool 8700-8799; on Loreana 8765, 8766, 8787, 8795 belong to other services (2026-09-24) — a session
  99. given 8760-8769 / 8780-8789 / 8790-8799 may find ports busy.
  100. - **Test instance** (never live): `.scratch/m028/dc.sh up -d | stop | down` = the same compose file + `.scratch/m028/compose.test.yml`
  101. (container `antcolony-scheduler-m028`, own tickets 8752 + ident 8751 from `node .scratch/m028/own-tickets.mjs 6`, fake claude,
  102. pool 8770-8779, logs in `.scratch/m028/logs/`); section "Tests of the container" below.
  103. ## Small local models (antcolony#20) — routine decisions without Claude
  104. Which routine decisions call a model today: only the **librarian** (`classify`: decision or not, project, topic, reading).
  105. "Which ticket next" (`next`) and keywords (`/hold`, `/confirm` …) are code, no model; the **controller** (does the report
  106. hold?) stays on Claude — it is the check on the worker, not a routine call.
  107. - **Switch**: `COLONY_LIBRARIAN_MODEL=local` (or `local:<alias>` = the model name sent to the server; `--model` / `--librarian-model`)
  108. — default stays `sonnet`. The same prompt goes to an OpenAI-compatible `POST <COLONY_LOCAL_URL>/chat/completions`
  109. (default `http://127.0.0.1:8080/v1`, `COLONY_LOCAL_TIMEOUT` 120 s) with `response_format: json_schema` = `templates/librarian.schema.json`,
  110. temperature 0 — the server enforces the schema. The call files are `librarian/calls/<event>-local-{prompt.md,result.json}`.
  111. - **Escalation, never a silent wrong answer**: the local answer passes the same `checkAnswer` as Claude's (project allowed, topic, verbatim quote,
  112. known `supersedes`). Server unreachable / not 200 / answer cut off / checks refuse → the same text goes to Claude
  113. (`COLONY_LIBRARIAN_FALLBACK`, default `sonnet`, `off` = no fallback → counts as "no answer", retried next pass). Cost of a local answer: $0.
  114. - **The server is not started by the scheduler**: one llama-server per GPU (concept: exactly one on the Arc at a time) belongs to the host's
  115. agent/operator; the scheduler only needs the URL. `tests` : `local-test.hl` (root, so `./templates` resolves) runs one canned text through
  116. `classify`: `COLONY_HOME=$PWD COLONY_LOCAL_URL=http://127.0.0.1:PORT/v1 bin/hybriel local-test.hl -- DIR local off`.
  117. - **Measured** (this host, CPU only — the box sees no GPU; 184 real librarian prompts with Claude's answers as reference): see STATUS.
  118. ## Librarian (mission 029, ticket antcolony#21) — the creator's decisions in one place per project
  119. Why: a worker sees its ticket, the concept and the TITLES of other tickets; the creator's answers and decisions in
  120. comments never reached it (gitoria#3 asked about "shared components live in layouts.worldapi.org", decided long before).
  121. The librarian collects **the creator's own words** and every brief carries them.
  122. **Where the files go** (mission 030): `<dev folder>/DECISIONS.md` on this host, else the code folder, else
  123. `librarian/projects/<p>/DECISIONS.md` — and ALWAYS `librarian/projects/<p>/` for a project with `"workers": false`
  124. (hybriel: its own process in its own repo; the librarian never writes into it). The hybriel file lives in
  125. `librarian/projects/hybriel/DECISIONS.md`.
  126. **Sources** (only texts BY THE CREATOR — never the architect's or the scheduler's). Creator 2026-09-24: "the librarian should
  127. be enough. i have to adopt to the workflow" → **tickets only** (+ the chat seed once); no chat transcripts.
  128. 1. tickets (READ only, `GET /api/tickets` + each ticket updated since the last pass): comments, state-change texts (incl.
  129. answers on `Question…` tickets — the question + the history before it go to the model as context), the subject +
  130. summary of tickets the creator opened, his edits. The creator = the event's `author` is one of `COLONY_CREATOR_NAMES`
  131. (default `Caramboleyo,creator`; `creator` = the free-text author of events from before the login). **The tickets API
  132. shows display names only, no user ids** — see STATUS "open".
  133. 2. the architect's chat SEED (decisions made in chat, once):
  134. `./colony librarian --post-files --seed /media/STORAGE/projects/antcolony-docs/docs/global-decisions-seed.md` — no
  135. model call; `## Topic` headings, bullets `- "quote" · "quote" · reading: …` (wrapped lines joined with one space), every
  136. `"…"` part is a quote, the text before the first quote is kept as context, the reading is marked as the ARCHITECT's;
  137. a bullet without a quote is skipped (printed); idempotent (sha256 of the quotes).
  138. **One model step per new text** (`lib/librarian.hl` classify): `claude -p --output-format json --json-schema
  139. templates/librarian.schema.json --model sonnet --max-turns 4 --permission-mode default --permission-prompts none
  140. --strict-mcp-config --tools "" --no-session-persistence`, cwd `librarian/`, prompt = `templates/librarian.md` + the
  141. input as JSON (the text, the ticket, the history before it, the allowed projects = registry + the ticket's project +
  142. `all`, the existing topics, every current entry). Answer: `{decision, project, topic, quote, means, supersedes, why}`.
  143. **The code decides what is stored**: project must be allowed, topic 1–40 characters; `means` = ONE line (longer than
  144. 300 characters → clipped + note); `quote` must be `""` (the whole text) or a VERBATIM excerpt (checked character for
  145. character — else the whole text is stored, `note: the excerpt … is not verbatim`); `supersedes` only ids of current
  146. entries. The creator's words always come from the ticket, never from the model. A refused answer is retried once (next
  147. pass), no answer up to 3 times, then given up (`--retry-refused` takes the refused ones again; `--redo EVENT[,EVENT]`
  148. classifies texts again and removes the librarian's own entry for them — for a wrong reading, not a creator correction).
  149. No model call when nothing is new. A bare "yes"/"no" answers the ticket's SUBJECT (prompt rule, after the first live pass
  150. read "yes" on ident#13 as the summary's suggestion).
  151. **Files** (rendered from `librarian/decisions.json`, never edited by hand — header says so; rewritten only when their
  152. text changes):
  153. - per project `DECISIONS.md` in its **dev folder** when that is on this host, else its **code folder** on this host
  154. (antcolony → the scheduler folder), else `librarian/projects/<name>/DECISIONS.md` (a project without a registry file);
  155. `all` → `templates/decisions-global.md` (`COLONY_DECISIONS_GLOBAL`).
  156. - CURRENT state only, by TOPIC (the model picks a short topic, reusing existing ones), `## Contents` at the top, newest
  157. first inside a topic. Entry: `- <date> · **"<the creator's exact words>"** · [<project #n — subject>](<ticket url>)`
  158. (seed: `· source: architect session 2026-09-23/24, chat`) + ` - means (the librarian's reading, not the creator's
  159. words): …` (seed: `the architect's reading`). Multi-line texts as a blockquote.
  160. - a correction ("i never said …") → the model lists the entries it supersedes → they move to `DECISIONS-archive.md`
  161. (`decisions-global-archive.md`) beside it, kept with quote + link + `superseded on <date> by <id> — "<new quote>"`.
  162. **In every brief** (`brief.hl`, after "Conventions for all apps", before "Where"): `## Decisions of the creator — a
  163. decision here overrides anything else; ask only if it is not decided here` · both file paths · "Search both files … before
  164. you ask a question" · `### For all projects` + `### For <project>`: the whole file while it has ≤ 150 lines
  165. (`COLONY_DECISIONS_FULL_LINES`), else its contents list + only the topic sections that share a word with the ticket
  166. (subject + summary; words ≥ 4 letters without stop words, compared by their first 5 letters, against the topic name and
  167. the entries' "means" lines) + "This file is long … Search the whole file `<path>` before you ask anything". Read-first
  168. item 5 names the decisions; item 6 (registry `"ui": true`: tickets, ident, gitoria) names the creator's markup/CSS rules
  169. `/media/STORAGE/projects/antcolony-docs/docs/layouts-conventions.md` (`COLONY_LAYOUTS_CONVENTIONS`).
  170. **ORDER — the librarian always runs BEFORE a brief is built** (a decision the creator wrote in ANY ticket a moment ago is
  171. in DECISIONS.md when the brief is assembled):
  172. - `run`: every iteration starts with ONE librarian pass and WAITS for it (`daemon.hl` tick → `librarianPass` → only then
  173. `iterate`: leases, candidates, quota, starts). A pass that failed or left a creator text unfiled (e.g. `--librarian-max`
  174. reached, a text without an answer yet) → `ITER … · librarian: n creator text(s) not filed yet → no start this iteration`
  175. (or `librarian failed (…) → no start this iteration`) — nothing starts until the texts are filed / given up (≤ 3 tries).
  176. - by hand: `brief`, `work`, `cycle` await one pass first (`librarianFirst`, scheduler.hl); not for `--dry-run` (no model
  177. call there) or `--librarian off`. A pass that fails / finds the lock taken says so (`librarian: brief goes on WITHOUT a
  178. finished librarian pass (…)`) and the command goes on with the files as they are.
  179. - Remaining gap: a text written while the pass runs or after it (seconds) reaches the NEXT iteration's briefs.
  180. - **Usage limit (mission 034, antcolony#25)** — see "Usage limit" under `run`: the quota is read BEFORE every librarian
  181. pass of `run` (at or above the start threshold → no librarian call, no start); a librarian call that still hits the limit
  182. prints `→ USAGE LIMIT — <Claude's text> · resets <time | at an unknown time> → the pass stops, the text stays unfiled (no
  183. try counted)` and `run` pauses. By hand (`brief`/`work`/`cycle`): `librarian: the Claude usage limit is reached (resets …)
  184. — n creator text(s) not filed; <cmd> goes on with the files as they are`.
  185. ```bash
  186. ./colony librarian # dry run: the new creator texts; no model call, nothing written
  187. ./colony librarian --post-files [--since 2026-09-24|ms] [--max N] [--model M] [--retry-refused] [--redo EVENT,EVENT]
  188. ./colony librarian --post-files --seed FILE [--seed-source TEXT]
  189. ./colony librarian --render # all files again from librarian/decisions.json (no tickets, no model)
  190. python3 -m json.tool librarian/state.json | head # cursorMs (newest ticket updatedMs read; stays while texts are left), floorMs (--since), done, tries
  191. python3 -c "import json;[print(e['id'],e['status'],e['project'],e['topic'],e['quotes']) for e in json.load(open('librarian/decisions.json'))['entries']]"
  192. ```
  193. Output: `librarian: n new creator text(s) …`, per text `LIBRARIAN TEXT <ref> · <what> · <author> · <when> · "<text>"` →
  194. `LIBRARIAN USAGE: …` → `→ DECISION <id> · <project> · <topic> · quote "…" · means: …` / `→ supersedes <id>` /
  195. `→ not a decision — …` / `→ REFUSED (k of 2 tries) — …` / `→ NO ANSWER (k of 3 tries) — …` / `→ note: …`;
  196. `LIBRARIAN FILE <path> (n current · <where>)`; `LIBRARIAN: … classified …`; `LIBRARIAN COST: $x for n model call(s)`.
  197. `librarian: BUSY — …` = another pass holds `librarian/lock` (stale after 15 min — a pass killed mid-way blocks starts
  198. of `run` for at most that long). In `run` the pass is quiet when nothing is new; `--librarian off` / `COLONY_LIBRARIAN=off`,
  199. `--librarian-model`, `--librarian-max` (0 = all). The `RUN:` line says `librarian on, first in every iteration (sonnet)`.
  200. **The live container runs the old code until it is restarted** (`docker compose up -d`) — until then run the librarian by
  201. hand (as mre on Loreana, see "Operations"; no token needed, it only reads; `CLAUDE_CONFIG_DIR=/home/mre/.claude-colony` =
  202. the colony's own Claude subscription like the container).
  203. ## Scheduler + agents per host (mission 035, ticket antcolony#14; concept docs/scheduler-agent.md §1–§3)
  204. **Two programs instead of one.** `./colony run` alone is still the all-in-one loop of missions 020–034 (one host, starts its
  205. workers itself — the live container on Loreana runs like this until the architect switches it). New:
  206. | | command | where | does |
  207. |---|---|---|---|
  208. | scheduler | `./colony run --serve PORT` | Byrodin, next to tickets | picks work, assembles the brief, leases/expiry, librarian; **starts no worker**, holds no data (tickets only) |
  209. | agent | `./colony agent --scheduler URL` | every host that has dev folders (Loreana, Byrodin) | runs the workers the scheduler sends, owns quota + ports of its host, reports back |
  210. **Connection (why HTTP, not a WebSocket).** The concept says the agent "connects OUT over one WebSocket". Hybriel has no
  211. WebSocket client (hybriel#109, open) and a JS bridge is forbidden, so the agent asks and the scheduler answers with plain
  212. HTTPS: every `--poll` s (default 5) `POST <scheduler>/agent/sync`, JSON, `Authorization: Bearer <agent token>`
  213. (`COLONY_AGENT_TOKEN_FILE`, a secret both sides read; scheduler and agent refuse to start without it; a wrong / missing token → 401,
  214. other path → 404, bad JSON → 400):
  215. - agent → `{ host, parallel, accepting, why, running: [{ref, session, folder, phase, ports}], events: [not yet acked] }`
  216. - scheduler → `{ ack: [event ids], commands: [{kind: start|resume, project, number, session, folder, brief}] }`
  217. Same effects as the socket: the agent reconnects by itself; a dropped connection breaks nothing — sessions keep running (their lease
  218. heartbeat and their report go to tickets directly), `SESSION END` / `refused` events are buffered until the scheduler acks them
  219. (delivered after the reconnect, printed once — ids dedupe); a command sent twice is ignored (session id); a command whose answer
  220. was lost is sent again at the next round. When hybriel#109 is fixed only `agent.hl` (client) and `AgentServer.hl` (server) change.
  221. **Capabilities (antcolony#15).** Each agent announces what its computer offers: `--capabilities gpu,chrome,deploy` /
  222. `COLONY_CAPABILITIES` (comma list, lower-cased; default nothing) — sent in every sync, shown in `AGENT: H connected · offers …`.
  223. A project's registry file may say `"needs": ["chrome", "gpu"]`; the scheduler starts a NEW ticket of it only on a host whose
  224. agent offers every need, otherwise the iteration says `P#n waits — H does not offer: gpu` and the ticket stays open. Names are
  225. free words (nothing is detected automatically). The single-host `run` takes the same `--capabilities` for its own host. A parked
  226. session's resume is not checked (it already started there). Test: `tests/e2e-agent.mjs` (project `gp` needs a gpu, `ag` needs chrome).
  227. **Scheduler side** (`daemon.hl`, `AgentServer.hl`; `--serve PORT`, `--serve-host` (default 127.0.0.1 — behind TLS/nginx on Byrodin),
  228. `--agent-ttl` (30 s), no `--once`): the state of the agents (host, last sync, parallel, accepting, running) lives in memory only —
  229. each sync rebuilds it, so a restart of the scheduler needs no recovery. A candidate is a ticket whose registry `dev.host` has an
  230. ONLINE agent that is `accepting` with a free slot; otherwise `waits — no agent online on H` / `agent H is not accepting work (why)` /
  231. `has all N slot(s) busy`. A start = the scheduler builds the brief (`brief.hl`, with the creator's decisions it holds; ports are the
  232. marker `@AGENT@`), queues `start`, sends it with the agent's next sync. A parked session on host H is resumed by H's agent
  233. (`resume`; the mission-031 re-read before it is unchanged). Lines: `AGENT: H connected · parallel n · …`, `AGENT: H is OFFLINE (no sync
  234. for S s) — n session(s) were on it; their leases in tickets decide` (the workers keep renewing; a lease nobody renews expires as
  235. before → back to `open`), `SESSION END <ref> <session> · <outcome> · cost … · on H · ports …`, `ITER … · agents: H 1/2, …`. A stop
  236. (SIGTERM) ends the scheduler at once: sessions keep running on their agents.
  237. **Agent side** (`agent.hl`; `--scheduler URL` / `COLONY_SCHEDULER_URL`, `--poll` / `COLONY_AGENT_POLL`, `--parallel`, `--reserve`,
  238. `--week-limit`, `--port-pool`, `--ports-per-session`, `--quota-every` / `COLONY_AGENT_QUOTA_EVERY` (60 s), the worker options of `run`;
  239. needs `COLONY_TOKEN_FILE` — its workers lease and post to tickets — and `COLONY_AGENT_TOKEN_FILE`): it runs the very same `work`
  240. (lease, worker, controller, post) with the brief it was sent (`work --brief-file`; the ticket checks still run on this host and
  241. its own port range replaces the marker). Quota is the agent's: `claude -p /usage` at most every `--quota-every` s while a slot is free;
  242. at/above the reserve, above the weekly limit, reached limit, or a session parked on the usage limit → `accepting: false` + why
  243. (`AGENT: not accepting work — …`, `LIMIT: …`), a command that arrives anyway is refused (event `refused`). Ports: its own pool, one
  244. disjoint range per session. Stop (via the `colony` wrapper, like `run`): 1st signal → accepts nothing, sessions finish, `AGENT STOPPED`;
  245. 2nd → they are parked. Every line starts with `AGENT` / `SESSION END` / `LIMIT` / `work:`.
  246. **What each host needs**: this folder (code + `projects/` registry + `templates/`), `bin/hybriel` with `plugins/http1` (server) and `fetch`,
  247. the tickets token, the agent token; agent hosts additionally `claude` and the dev folders. The scheduler host needs `claude` only for the
  248. librarian (`COLONY_LIBRARIAN=off` where there is none — the decisions then stop updating).
  249. **Not decided by this ticket / open**: the container files for Byrodin (scheduler) and per-host agent containers — the architect's deploy;
  250. the librarian still runs on the scheduler host (needs Claude there); the quota is a shared per-account view since antcolony#16 (next section).
  251. ## Claude quota per account, shared across computers (antcolony#16; concept §3 "Claude quota per account, not per host")
  252. The quota belongs to the Claude ACCOUNT. Hosts that are logged in to the same account name it the same:
  253. `./colony agent --account NAME` / `COLONY_CLAUDE_ACCOUNT` (default = the host's own name = its own account, nothing shared; nothing
  254. is detected automatically). Every sync now also carries `account`, `quota` (the agent's last `/usage` reading: percent, weekly,
  255. resetsMs, `at`) and `limitUntil` (its usage-limit pause, if any). The scheduler pools them per account (`daemon.hl` accountBlock):
  256. - the FRESHEST reading of any online agent of the account counts — a host that has not read the quota itself starts on another
  257. host's reading (no reading, or one older than 5 min → `the quota is not read yet` / `older than 5 min` → waits);
  258. - same rules as one host: 5 h ≥ `--reserve`, week > `--week-limit` (or unknown), 100 % reached → the ticket waits
  259. (`P#n waits — Claude account A: quota 5 h 80% ≥ reserve 70%`);
  260. - a usage limit hit on ANY host of the account holds every host of it (`… usage limit reached until T (hit on H)`);
  261. - one new start per account at a time: after a start (or resume) no further start on that account until a reading taken
  262. `--account-settle` s (default 60, `COLONY_ACCOUNT_SETTLE`) AFTER it is in — otherwise two hosts read 69 % at once and both start.
  263. Also within one iteration (`… a start on it (T) is not in the quota reading yet — shared by H1, H2`).
  264. The ITER line lists `account A 12% week 41% [h1+h2]`; the `AGENT: H connected` line names the account.
  265. Disconnected agent: the concept's "conservative fixed share" needs nothing extra — an agent starts only what the scheduler sends
  266. and refuses on its own reading (reserve/week/limit) too, so a host without the scheduler starts nothing new.
  267. Test: `tests/e2e-agent.mjs` section 5b (hand-made syncs of two hosts on one account; +5 checks).
  268. ## Run
  269. ```bash
  270. cd /media/STORAGE/projects/antcolony-scheduler # (Loreana; Hybriel 1 binary vendored)
  271. ./colony next [--post]
  272. ./colony brief <project> <n> [--out FILE] [--ports A-B] [--session ID]
  273. ./colony report <file> [--post]
  274. ./colony work <project> <n> [--post] [--dry-run] [--model M] [--max-turns N] [--permission-mode MODE]
  275. [--timeout SECONDS] [--max-budget-usd X] [--ports A-B] [--session ID]
  276. [--controller-model M] [--controller-max-turns N] [--controller-timeout S] [--max-fails N]
  277. [--finalize-max-turns N] [--finalize-timeout S]
  278. ./colony check <runs/session dir> [--post] [--again] [controller options]
  279. ./colony salvage <runs/session dir> [--post] [--finalize-max-turns N] [--finalize-timeout S] [--ports A-B] [controller options]
  280. ./colony cycle [--post] [--dry-run] [work options]
  281. ./colony run [--once] [--interval S] [--parallel K] [--reserve P] [--week-limit P] [--port-pool A-B] [--ports-per-session N]
  282. [--lease-ttl S] [--heartbeat S] [--quota-claude BIN] [--live] [work options]
  283. ./colony leases [--expire] [--live]
  284. ./colony ready # mission 027: tickets built on a project with a live site, waiting for the deploy
  285. ./colony deployed <project> <n> # mission 027: after the deploy — readable comment + awaiting creator
  286. ./colony librarian [--post-files] [--since D|ms] [--max N] [--model M] [--retry-refused] [--redo EVENT,…] # mission 029 (section "Librarian")
  287. ./colony librarian --post-files --seed FILE | --render
  288. ```
  289. `./colony` = `bin/hybriel scheduler.hl -- "$@"` — the `--` is needed: the hybriel binary refuses any
  290. `--option` after the script unless it follows `--` (Hybriel issue, STATUS). Output is plain text;
  291. **the exit status is always 0** (Hybriel has no way to set it) — read the first word: `NEXT:`,
  292. `brief:`, `REPORT OK` / `REPORT REFUSED`, `POSTED` / `POST REFUSED` / `POST FAILED`, `work: REFUSED`,
  293. `WORK DONE` / `WORK OK` / `WORK FAILED` (last line of `work`; `CHECK …` for `check`), `VERDICT: PASS|FAIL`,
  294. `CYCLE COST:` + `CYCLE END —` / `CYCLE DRY RUN` / `CYCLE —` (last lines of `cycle`); `run`: `RUN:`, `ITER`,
  295. `SESSION END`, `PARKED —`, `RUN STOPPED —`; `leases`: `LEASE` / `LEASES:`; finalize (mission 025): `work: FINALIZE —`,
  296. `FINALIZE USAGE:`, `FINALIZE COST:`, `work: FINALIZE FAILED —`; `salvage`: `salvage: REFUSED —`, `SALVAGE DONE` / `SALVAGE OK` /
  297. `SALVAGE FAILED`, `SALVAGE COST:`, `SALVAGE END —`; mission 026: `WORK HALF DONE` / `CHECK HALF DONE` / `SALVAGE HALF DONE`
  298. (a passed report with `complete: false`), `work|finalize|controller: CLEANUP — stopped pid … / skipped pid … / nothing
  299. listening on A-B`, `salvage: <ref> leased until …`.
  300. ### next
  301. Reads every ticket (`GET /api/tickets`), applies the rules and prints `NEXT: <project>#<n>` plus why
  302. the others wait:
  303. * **eligible** = state `open` + the project has a registry file + that file names a `concept` + it
  304. does not say `"workers": false` (such a project — hybriel — is listed "takes no workers", no question);
  305. v0 priority = **oldest** (stored `createdMs`, ties → lower number).
  306. * not eligible, listed with the reason: no registry file / empty concept (`→ question for the
  307. creator`), `in progress` (= leased — v0 has no agent, the state is the lease), `on hold`,
  308. **`blocked by rel#2 (open)`** (any blocker not `confirmed`), **`a parent (n child tickets, k confirmed) — not
  309. work itself, its children are`** (mission 022; the scheduler's own creator-question children don't count).
  310. * **`rejected`** with a rejection newer than the last colony session → a candidate again (same rules), marked
  311. `— REJECTED by the creator <when> (newer than the last colony session) → rework` (mission 022).
  312. * `awaiting creator` / `confirmed` / other `rejected` are only counted.
  313. * `--post`: every project with open tickets but no metadata / no concept gets ONE creator question
  314. (ticket in project `antcolony`, state `awaiting creator`; idempotent via `source`
  315. `colony:no-metadata:<p>` / `colony:no-concept:<p>` → a re-run prints `HAVE`, writes nothing).
  316. ### brief
  317. Writes a worker brief for one ticket (default `briefs/<project>-<n>.md`; `--out` relative to the
  318. cwd). Refused for a project without metadata or concept. Parts, in the order of the architect's
  319. `missions/*.md`: [**`REJECTED by the creator: <reason>`** FIRST while a rejection is unanswered — mission 022] ·
  320. header (ticket URL, session id, UTC time) · Read first (ticket, **concept path**,
  321. project README/STATUS, antcolony README) · Where (dev/code/live folders, deploy note, **port range**,
  322. default `8700-8749` or `COLONY_PORTS`) · **Relations** (parent / children — creator questions marked / blocked by —
  323. not confirmed = **blocking** / blocks; mission 022) · **Hybriel block** (`templates/hybriel-block.md`, only if
  324. `dependsOn` has `hybriel`) · **open tickets of every project it depends on** (all not confirmed /
  325. rejected) · **rules** (`templates/rules.md`) · **report section** = the JSON schema
  326. (`templates/report.md`, filled with ticket, session, known projects) · the **ticket + history**:
  327. tickets' Markdown read view (`Accept: text/markdown`, tickets#6) or, when tickets does not serve
  328. it, rendered from the JSON. Session id: `--session` or generated `s-<UTC yyyymmddThhmm>-<6 hex>`.
  329. ### report
  330. Validates a worker's JSON report (schema: concept §4) and prints EVERY problem with its path, e.g.
  331. `verified[0]: 'output' is missing (every verified entry needs command AND output)`:
  332. all 12 keys `ticket session result complete test done verified open issues questions running refused` required
  333. (mission 027: + `decided`, optional here, required in the schema — see "Mission 027" for the tightened limits)
  334. (mission 026: `complete` = true / false; lists may
  335. be `[]`, not null), no unknown keys at any level, **`result`** = one line ≤ 200 characters, **`test`** = 1–4 steps,
  336. each one line ≤ 200 characters (mission 023 — `result` + `test` are what the creator reads), `ticket` = `<project>#<n>`, strings non-empty,
  337. `verified[]` = `{claim, command, output}`, `issues[]` = `{project, subject, repro, observed,
  338. expected, blocks: bool}`, `running[]` = `{what, url, pid: number, log}`. An `issues[].project` not in
  339. the registry is a **note**, not a refusal (→ creator question on `--post`, concept: "else → creator's
  340. inbox"). `--post` (only for a valid report):
  341. 1. the ticket must exist; a comment with `colony-report: <session> · sha256 <hash>` already there →
  342. same hash: nothing is written again; different hash: **refused** (one session, one report);
  343. 2. each issue → a ticket in its project (summary: link back, Repro / Observed / Expected, "Blocks"
  344. when `blocks`), source `colony-report:<session>:issue:<i>`; unknown project → creator question
  345. (`antcolony`, `awaiting creator`, source `colony-report:<session>:question:<i>`, parent = the ticket);
  346. an issue with `blocks: true` → the ticket gets it as a **blocker** (`POST …/blocked-by`, once — mission 022);
  347. 2b. (mission 022) each `questions` entry → its own ticket in the TICKET's project: subject `Question (<ticket>):
  348. <title>` (antcolony#23: the entry is `{ title, text }`, the worker writes the short title, ≤ 80 chars; an old plain string still works, its first line is the title), summary = the question + where it came from, `awaiting creator`, parent = the ticket,
  349. source `colony-report:<session>:questions:<i>` (`HAVE` on a re-post);
  350. 3. ONE short comment on the ticket (mission 023, section "Ticket texts"): `result` · `**Test:** 1) … 2) …` · at most 3
  351. detail bullets picked by the code (questions filed → problems filed elsewhere → first `open` entry → running →
  352. refused) · the machine line `colony-report: <S> · sha256 <12 hex>[ · controller pass]`. `done`, `verified` etc. are
  353. NOT in the ticket — the full report stays in `runs/<S>/report.json` (+ `verdict.json`);
  354. 4. ticket → `awaiting creator` (state text "Ready for you to test — see the last comment."; never `confirmed`), only
  355. if the comment was new and the ticket is not already awaiting / confirmed / rejected.
  356. Re-posting the same report writes nothing (sources dedupe tickets, the marker dedupes the comment).
  357. A run that failed half-way can simply be repeated.
  358. ### work (agent v0)
  359. Runs ONE worker on one ticket, on the host that holds the project's dev folder (`dev.host` in the
  360. registry must equal this host — `/etc/hostname`, case-insensitive, or `COLONY_HOST`):
  361. 1. **refuses before any write** (`work: REFUSED — …`): no token file, project without metadata /
  362. concept / with `"workers": false`, ticket not `open` (in progress = leased; `rejected` is accepted when its
  363. rejection is newer than the last colony session — mission 022), blocked / a parent (`… is not eligible: blocked
  364. by …`), dev folder on another
  365. host or missing, `--permission-mode bypassPermissions` (only `auto acceptEdits default dontAsk
  366. plan manual`), bad `--max-turns` / `--timeout`, `runs/<session>` exists;
  367. 2. writes `runs/<session>/brief.md` (the `brief` text) and `session.json`;
  368. 3. **lease**: ticket → `in progress` with the text `session <id> started on <host>` + the marker line
  369. `colony-lease: <id> · host <h> · started · ms <expiry>` (one state event, text "A worker started on this ticket."); a heartbeat renews
  370. it while worker + controller run (see "Leases");
  371. 4. runs, in the dev folder, `claude -p --output-format json --json-schema <templates/report.schema.json>
  372. --model M --max-turns N --permission-mode MODE --permission-prompts none --strict-mcp-config
  373. --session-id <uuid>` with the brief on stdin (`umask 077`, `COLONY_TOKEN_FILE` unset for the worker);
  374. `--timeout` kills it;
  375. 5. on exit: `claude-result.json` (raw stdout), `claude-stderr.log`, `report.json` (= Claude's
  376. `structured_output`), prints `USAGE: $cost · turns · s · tokens … · model · subtype · claude session`,
  377. keeps cost/usage/turns/permission modes/denials/outcome in `session.json`; then the `report`
  378. validation (+ report `ticket`/`session` must be this session's);
  379. 5b. (mission 025) **out of steps (`error_max_turns`) or time (`--timeout`)** → the **finalize step** (section "Finalize
  380. step + salvage") resumes the same Claude session once to write the report; its report goes on to 6./7. like any other;
  381. 6. **no valid report** (no structured output e.g. max turns, bad JSON, timeout, validation refused — after the finalize
  382. step, if there was one) → ticket back to `open` with the reason (+ validation problems, run folder) → `WORK FAILED`;
  383. 7. a valid report → the **controller** (below) → `--post` posts **by the verdict**. Without `--post`:
  384. `WORK OK — valid report, controller: pass|fail, NOT posted`, ticket stays `in progress` → post later
  385. with `./colony check runs/<session> --post` (reuses `verdict.json`). `./colony report
  386. runs/<session>/report.json --post` still posts WITHOUT a controller (architect's override).
  387. `--dry-run`: every refusal check + the brief (GETs only), then `work: DRY RUN — would lease … run …`;
  388. nothing written, no Claude session.
  389. ### The controller (mission 019; concept §1 + §2 "Report handling")
  390. A second one-shot Claude Code session, started by `work` right after a schema-valid report and BEFORE
  391. anything is posted, in the SAME dev folder, with the worker's safety settings (`--permission-mode`
  392. auto by default, never bypass, `--permission-prompts none`, `--strict-mcp-config`, no
  393. `COLONY_TOKEN_FILE`) plus `--disallowedTools Edit,Write,NotebookEdit` (it only reads and re-runs).
  394. - **Input** (stdin, kept as `runs/<session>/controller-prompt.md`): `templates/controller.md` (what to
  395. check: every `verified` entry — re-run where cheap and safe, compare outputs; every `done` entry
  396. against the files; flag claims without evidence; don't re-run writes / deploys / POSTs), the report,
  397. the worker's session as a compact transcript (text, tool calls, tool results from the verbose
  398. `claude-result.json`, each clipped, whole ≤ 80 000 characters), the brief.
  399. - **Answer** (`--json-schema templates/verdict.schema.json`): `{ ticket, session, verdict: pass|fail,
  400. findings: [{ claim, about: verified|done|other, status: ok|false|no evidence|not checked, check }],
  401. summary }` → `runs/<session>/verdict.json`; `controller-result.json`, `controller-stderr.log`;
  402. `session.json.controller` = cost, usage, turns, verdict, `effective`, `overruled`. Prints
  403. `CONTROLLER USAGE: …` and `VERDICT: PASS|FAIL` + one line per finding.
  404. - **The code decides**: `lib/controller.hl` validates the verdict; a `pass` with any `false` / `no
  405. evidence` finding counts as **fail** (`overruled`, said in the comment).
  406. - **pass** → `report --post` as before, the machine line ends with `· controller pass` (mission 023: summary + findings
  407. stay in `verdict.json`), `awaiting creator`, issues filed.
  408. - **fail** → ONE comment `## Controller: FAIL — worker session … (fail k of N)`: summary, findings,
  409. run folder, the checked report in `<details>` (its issues are NOT filed), marker
  410. `colony-verdict: <session> · fail · k of N` → ticket back to `open` (the next worker reads it in the history).
  411. Mission 023: the fail comment is SHORT (what happened + ≤ 3 false / no-evidence findings, "Not true: …" / "Not
  412. shown: …"); the checked report and all findings stay in `runs/<S>/`.
  413. - **Nth fail** (k ≥ N; N = `--max-fails` / `COLONY_MAX_FAILS`, default 2; k = distinct sessions with a
  414. fail marker in the ticket's comments) → a creator question (`antcolony`, `awaiting creator`, source
  415. `colony-verdict:<session>:question`) instead, the comment links it, ticket → `on hold` (`next`
  416. never picks it; the creator sets it back to `open`).
  417. - Idempotent: a session's fail comment is written once (`HAVE`); pass = the report's own marker.
  418. - **No verdict** (controller timed out / no structured output / verdict refused) → nothing posted,
  419. ticket stays `in progress`: `WORK OK, CONTROLLER FAILED — … Retry: ./colony check <dir> --post`.
  420. ### run (mission 020) — the loop
  421. `./colony run` (ALWAYS through `./colony`: the wrapper catches the signals). Refuses the live tickets server
  422. (`COLONY_TICKETS_URL` unset or host `tickets.worldapi.org`) unless `--live`. Every `--interval` s (default 60) + once at
  423. the start, ONE iteration (`lib/daemon.hl`):
  424. 1. `survey` + the lease of every `in progress` ticket: expired colony lease → ticket back to `open` with a comment
  425. (`colony-lease-expired: <session>`); a parked session of THIS host whose resume time is over → resume candidate; a
  426. live lease → skipped (a restarted `run` never starts a second session on it); `in progress` WITHOUT a colony lease
  427. (architect missions, v0 leases) → listed, never expired; mission 031: a lease that has ENDED → listed, never expired,
  428. and the expiry / the resume RE-READ the ticket right before they write (section "Mission 031");
  429. 2. candidates: due resumes first, then eligible `open` tickets whose registry `dev.host` is this host (oldest first);
  430. a ticket whose dev folder is in use by a running session of this run waits (`same dev folder`);
  431. 3. free slots = `--parallel` (default 1) − running; nothing to start / no slot → no quota probe (mission 034: with the
  432. librarian on, the probe runs FIRST in every iteration — see "Usage limit" below — and its reading is reused here);
  433. 4. **quota**: `claude -p /usage` (below) — `percent ≥ --reserve` (default 70) of the 5 h window → no start
  434. (resumes too); probe failure → no start; **weekly** (mission 023, creator): `weekly_all` percent **above**
  435. `--week-limit` (default 84) or missing in the answer → no start (`… week 85% > week limit 84% → no start (waiting: …)`
  436. / `week UNKNOWN → no start`); `--reserve 100` / `--week-limit 100` switch the check off (`5 h reserve off (100%)`,
  437. `week limit off (100%)` in the ITER line — for the first live runs after a quota reset); mission 034: a REACHED limit
  438. (5 h or week at 100 %) holds every start also with the checks off (`… → the 5 h limit is reached (100%) → no start`);
  439. 5. start: a **port range** per session from `--port-pool` (default 8700-8799) in blocks of `--ports-per-session`
  440. (default 10; disjoint while running; `--parallel` > blocks → refused) → `work … --post --session S --ports A-B`
  441. (brief says the range; worker + controller get env `COLONY_PORTS`, `COLONY_PORT_FROM`, `COLONY_PORT_TO`) or the
  442. resume. `run` always posts (by the verdict).
  443. 6. ONE line: `ITER <n> <time> · quota 5% of 5 h (resets …), week 41% < reserve 70% · running 1/1 · started d1#1
  444. (s-…, ports 8780-8789) · expired … · skipped: …`. A finished session: `SESSION END <ref> <session> · posted |
  445. sent back | question | parked | no valid report | … · cost $x (worker $a + controller $b) · ports … freed`.
  446. **Stop** (Hybriel has no signal handlers): the wrapper runs the scheduler with `setsid` (a terminal's Ctrl-C reaches
  447. only the wrapper), traps SIGTERM/SIGINT and writes `$COLONY_STOP_FILE` (a `mktemp` name in `$TMPDIR`/`/tmp`,
  448. removed on exit); the run checks it twice a second. **1st signal** → `RUN: stop requested`: nothing new starts,
  449. running sessions FINISH (post), then `RUN STOPPED`. **2nd signal** → the running worker/controller processes are
  450. killed and **parked** (`PARKED — … the scheduler run was stopped`, resume = now) → the next `run` resumes them.
  451. `--once` = one iteration, wait for what it started, exit. A SIGKILLed / crashed `run` takes its sessions with it:
  452. hl:proc sets `PR_SET_PDEATHSIG = SIGTERM` on every child (plugins/proc/proc.zig:305) — the lease then expires.
  453. Wrapper prints `colony run: wrapper pid <p>, scheduler pid <q>, stop file <f>` (kill the WRAPPER pid).
  454. ### Usage limit (mission 034, ticket antcolony#25; `daemon.hl` tick / pauseForLimit, `claude.hl` rateLimitOf)
  455. Why (live 2026-09-25 19:58): the 5 h limit was reached (a worker parked "resets 8:50pm"), yet EVERY iteration ran the
  456. librarian first: its model call came back empty (`LIBRARIAN USAGE: $0 · 1 turns · 0.466 s · tokens 0 · success`, `1 without
  457. an answer`), the text stayed unfiled → `librarian: 1 creator text(s) not filed yet → no start` — every minute until the reset.
  458. The answer's shape (`librarian/calls/0muha3gdyxbp-result.json`, kept in `.scratch/m034/livecheck/`): `rate_limit_event`
  459. status `rejected` (resetsAt), a SYNTHETIC assistant message with `error: "rate_limit"` + "You've hit your session limit ·
  460. resets 8:50pm (Europe/Vienna)", then a result with subtype **`success`**, `is_error: true`, `api_error_status: 429`, 0 tokens,
  461. $0, no structured output. The librarian never looked for the limit.
  462. 1. **Quota FIRST** (the creator: "the usage command is model independent? it always works"): with the librarian on, every
  463. iteration starts with `claude -p /usage` (local, $0) BEFORE the librarian pass. At or above the start threshold
  464. (`--reserve` of the 5 h window, `--week-limit` of the week; a REACHED limit = 100 % always counts, also with the checks
  465. off) → no librarian call and no start this iteration; the ITER line has the reading with the reset time and
  466. `librarian not run: quota at or above the start threshold (5 h 75% ≥ reserve 70% | the 5 h limit is reached (100%) | week …)`.
  467. A failed probe / unknown week → the librarian runs (starts are held as before). The reading is reused for the start
  468. check (one probe per iteration) unless the pass made model calls (then read again right before a start).
  469. 2. **Safety net — a call that still hits the limit**: `rateLimitOf` recognises the live shape (and also a synthetic
  470. `error: "rate_limit"` message alone, a 429 without is_error, "session limit" / "weekly limit"). The librarian pass stops
  471. at that text (no try counted, text stays unfiled) and reports `limited` + the reset time; a worker / controller parked on
  472. the limit reports it the same way (`parkRun` → `limited`, `resetMs` when known). `run` then remembers "limited until":
  473. `LIMIT: usage limit reached — librarian and starts paused until <ISO> · <who hit it>` (ONE line); until then no librarian
  474. pass, no quota probe, no start or resume, the iterations are silent (leases are still looked after; a line only if one
  475. was expired); a later limit with a later reset → `LIMIT: the pause is extended until …`. After it: `LIMIT: resumed — the
  476. pause until <ISO> is over; the quota is read first, then the librarian and starts` (ONE line) → normal iteration.
  477. 3. No reset time known → paused 15 min (`… (no reset time known → 15 min, then try once)`), then the quota is read first.
  478. 4. Parked sessions resume as before (their resume time = the reset; after the pause the iteration resumes them).
  479. The pause lives in the running process only (a restarted `run` starts with the quota probe, which sees the limit).
  480. ### Leases (mission 020, `lib/lease.hl`)
  481. tickets is the only state, so a lease is marker text: the `→ in progress` state event of `work` (above), heartbeat
  482. comments `Still working.` + `colony-lease: <S> · host <H> · renewed · ms <expiry>` every `--heartbeat` s (default
  483. ttl/2) while the session runs, `… · resumed (park k) …` on resume, and on a park
  484. `colony-parked: <S> · host <H> · phase worker|controller · claude <uuid> · resume after <ISO> · resume-ms <ms> · lease
  485. until <ISO> · ms <resume + ttl>`. The LAST marker of the lease's session decides; past `ms` the lease is dead.
  486. `--lease-ttl` default **7200 s, heartbeat ttl/2 = 1 h** (mission 023: "heartbeats as few as possible" — a session under
  487. 1 h writes none, the default worker 1 h + controller 15 min at most one; was 3600 / ttl/3 = 3 per hour; a crash puts the
  488. ticket back to `open` ≤ 2 h later). Park marker since mission 023: `colony-parked: <S> · host <H> · phase … · resume-ms
  489. <ms> · ms <expiry>` (the Claude session id lives in `session.json`); older markers with `lease until` / `claude` /
  490. `resume after` parts are still parsed. `./colony leases` lists every in-progress ticket's lease; `--expire` gives the dead ones back.
  491. ### Mission 031 (ticket antcolony#1): lease expiry never touches a finished ticket (`lease.hl` endOf / stillLeased / expireLease)
  492. Why (live 2026-09-24 21:05Z, `logs/colony.log` ITER 45): ident.worldapi.org#19 was built (`ready to deploy`), the architect
  493. deployed it and ran `./colony deployed ident.worldapi.org 19` (comment `colony-deployed: S` at 21:05:18.683, → `awaiting
  494. creator` 146 ms later). The daemon's iteration had read the ticket list while it was still `in progress`; its lease (session
  495. s-20260924T1852-496f93, dead since 20:52) was no longer "built, waiting for the deploy" (the deployed marker voids that),
  496. so `leaseOf` returned the old dead lease and `expireLease` wrote → `open` "the worker stopped answering" 1.9 s after the
  497. deploy. The finished ticket left the creator's inbox and a second worker ran on it (s-20260924T2108-d3b3f0, changed nothing).
  498. Same iteration: ident#15 was in the same gap and only escaped because its lease was still alive.
  499. * **A lease ENDS** (`leaseOf` kind `ended`, never expired) when after its `→ in progress` event: the ticket changed state
  500. (to anything but `in progress`, by anyone), or a `colony-deployed:` line appears (any session), or the lease's OWN session
  501. wrote `colony-report:` (not the `· ready to deploy` one — that is kind `ready`), `colony-verdict:`, `colony-run:` or
  502. `colony-lease-expired:`. `run`: `skipped: … <ref> in progress, but the lease of S has ended (<why>) → never expired`;
  503. `leases`: `LEASE <ref> — the lease of session S has ended (<why>): never expires`. A ticket left like that (e.g.
  504. `deployed` wrote its comment but not the state) is completed by running `./colony deployed <p> <n>` again (HAVE + state).
  505. * **Re-read before acting**: `expireLease` (used by `run` and `leases --expire`) and the resume of a parked session (`run`)
  506. first GET the ticket again (`stillLeased`) and act only if it is STILL `in progress` with the SAME lease (session,
  507. expiry, parked state) and still dead (expire) — else NOTHING is written and the reason is printed:
  508. `lease: NOT expired <ref> (session S): re-read right before — it is "awaiting creator" now → nothing written`, in the ITER
  509. line `NOT expired <ref> (session S, lease ended …): re-read right before — <why> → nothing written`; resume: `NOT resumed
  510. <ref> (S): …`; `leases --expire`: `… EXPIRED at … → NOT expired, re-read right before: <why> (nothing written)`. Reasons:
  511. `it is "<state>" now`, `the lease of S has ended: …`, `another session holds it now (…)`, `the lease of S changed (now
  512. until …)` (a heartbeat / park after the survey), `it is built now, waiting for the deploy`, `could not re-read it (…)`.
  513. * **Remaining gap**: re-read and write are two requests a few ms apart; tickets has no conditional state change
  514. (e.g. `POST …/state {state, expect: "in progress"}`) — that would close it (question for the creator, STATUS).
  515. * **Test hook** `COLONY_TEST_HOOK_URL` (tests only, never set live): right before the re-read the scheduler GETs
  516. `<url>?action=expire|resume&project=…&number=…&session=…` and waits for the answer — the e2e changes the ticket there,
  517. which reproduces the race deterministically. Unset = no request.
  518. ### Quota + parking (mission 020, `lib/quota.hl`, `work.hl` parkRun / runResume)
  519. **Reading**: `claude -p /usage --output-format json --verbose --model haiku --max-turns 1 --permission-mode plan
  520. --permission-prompts none --strict-mcp-config` (`--quota-claude` / `COLONY_QUOTA_CLAUDE`, default the worker's
  521. binary). It is a LOCAL command of Claude Code 2.1.280: **$0, 0 turns, 0 tokens, ~1–4 s** (proved 2026-09-24,
  522. `.scratch/m020/usage-probe/`); the assistant message carries `usage_report.rate_limits.limits[]` with
  523. `kind: session` → `percent`, `resets_at` (fallback: the text `Current session: N% used`). The account = whatever
  524. `claude` is logged in as on this host (per host = per account today). A probe that cost > 0 is printed (`PROBE COST`).
  525. Every session's verbose output also has `rate_limit_event`s (`unifiedWindows.five_hour.utilization`) — kept in
  526. `session.json.rateLimit`.
  527. **Evicted by the limit** (a `rate_limit_event` with status `rejected`, or an error result with
  528. `api_error_status 429` / "hit your limit" / "usage limit"; mission 034: also a synthetic `error: "rate_limit"` message,
  529. "session limit" / "weekly limit"): the session is **parked**, not given back — comment
  530. `**Parked** — Claude usage limit (…)` + `colony-parked` marker, ticket stays `in progress`, `session.json.parked`
  531. = { phase, reason, resumeMs, count }. Resume time = the event's `resetsAt`, else `…|<epoch>` in the text, else
  532. now + 1 h; `run` resumes only on the host that parked (the Claude session lives there) and only below the reserve.
  533. **Resume**: phase `worker` → `claude -p --resume <claudeSession>` (same flags, no `--session-id`) with
  534. `runs/<S>/resume-<k>.md` (continue, the NEW port range, the report's ticket + session); the earlier output is kept as
  535. `claude-result.parked-<k>.json` (+ stderr) and the controller sees all parts; phase `controller` (stopped after a
  536. valid report, or the controller hit the limit) → a new controller session on the kept `report.json`. Costs of all
  537. parts are summed (`costEarlier`, `controllerCostEarlier`).
  538. ### Mission 027 (ticket antcolony#1): after the first live batch — readable test steps, deploy gate, fewer questions
  539. Why (batch 2026-09-24: ident#2, tickets#12, gitoria#2, all PASS, $6.36): the Test steps were one run-on line pointing at
  540. work copies on Loreana; "Ready for you to test" was posted before anything was deployed (the creator can only test on the
  541. live site); too many / too long questions, some trivial, one already decided (only in a ticket, not in a doc the worker
  542. could read — workers on Loreana cannot read Byrodin's antcolony README at all: the gitoria worker tried ssh and was refused).
  543. * **Limits** (`report.hl` validate; told in `templates/report.md` + the schema descriptions): `result` one line ≤ 120;
  544. `test` 2–4 steps, one line ≤ 90 each, no `.scratch`, and on a project with `live` no `localhost` / `127.0.0.1` / `0.0.0.0`;
  545. `questions` (antcolony#24: no count limit), each ≤ 3 non-empty lines and ≤ 300 characters; `decided` = list of one-line strings ≤ 120 (key OPTIONAL
  546. in the validation so older reports stay valid; REQUIRED in `report.schema.json` so workers give it). The brief's report
  547. part says where the creator tests: `on the live site <live.url> (after the deploy …)` or, without `live`, `on the real
  548. result (… never a work copy)`.
  549. * **Numbered Test list**: every comment with steps renders `**Test:**` + `1. …` lines (was `**Test:** 1) … 2) …` on one line).
  550. The README "≤ 5 short lines" limit is counted WITHOUT the step lines (the `**Test:**` header counts as one) — e2e
  551. `readableLines`.
  552. * **`decided`** → ONE detail bullet `Decided: a; b.` (after questions + problems filed, before "Not done").
  553. * **Deploy gate** (`report.hl` deployGate/readyCommentOf, `lib/deploy.hl`, `lease.hl` readyOf): a passed, complete report on a
  554. project whose registry has `live` → questions / issues filed as usual, then ONE comment `Built — goes live with the next
  555. deploy.` + `colony-report: S · sha256 H · controller pass · ready to deploy`; the ticket stays `in progress` (from `open` /
  556. `rejected` → `in progress` "Built — waiting for the next deploy."). Outcome `ready to deploy` (`WORK DONE — x built, waiting
  557. for the deploy …`, `SESSION END … · ready to deploy`). `readyOf(events)` = the last ready marker's session unless a later
  558. `colony-deployed: S`, a lease of ANOTHER session or a state change away from `in progress` voids it; `leaseOf` → kind
  559. `ready` (never expires; `run` notes "built, waiting for the deploy"; `leases`: `LEASE x — built, waiting for the deploy
  560. (session S): never expires`); `next` lists it `built — waiting for the deploy (session S; ./colony ready)`.
  561. - `./colony ready` (GETs only): `READY <ref> "<subject>" — built by session S · live <url> · deploy: <note> · after the
  562. deploy: ./colony deployed <p> <n> · <url>` + `READY: n ticket(s) waiting for a deploy`.
  563. - `./colony deployed <project> <n>` (token; on the host that holds `runs/<S>/report.json`, `COLONY_RUNS_DIR`): refused when
  564. the ticket waits for no deploy or the report is not on this host (nothing written). Else ONE comment `Now live on <site>
  565. — <result>` · `**Test:**` + numbered steps · ≤ 3 details (question / problem links found by their `source`
  566. `colony-report:S:…`, Decided, Not done …) · `colony-deployed: S · sha256 H`, then `in progress → awaiting creator` ("Ready
  567. for you to test — see the last comment."). Again → `HAVE`, nothing written (a missing state change is repaired).
  568. Last line `DEPLOYED — <ref> (session S, live on <site>)[: nothing new]`.
  569. - Projects without `live` (gitoria research, the e2e's dummy) → `awaiting creator` at once, as before.
  570. * **Brief** (`brief.hl`): after "Read first" the section `## Conventions for all apps — already decided by the creator, don't
  571. ask about them` = `templates/conventions.md` (a COPY of antcolony README "Conventions for all apps" + "Design: colors" — keep
  572. in step by hand); `## Tickets of <project> — decisions may already be there` (every other ticket of the project, all
  573. states, newest first, ≤ 40; an answered / confirmed question is followed by the creator's own comments on it — antcolony#22) before the Budget; "Where" names the live site as the place the creator tests; the concept line
  574. says "list it under `decided`". `templates/rules.md`: small choices → `decided`, ask only what changes what gets built.
  575. * **One fix step** (`work.hl` finish/fixPrompt; concept §2 "reject incomplete reports back to the worker (one retry)"): a
  576. report the validation refuses → the SAME Claude session is resumed once like the finalize step (`--resume`, same flags,
  577. `--finalize-max-turns`, `runs/<S>/finalize.md` = `# FIX YOUR REPORT — the scheduler refused it` + the problems + "do not
  578. continue, change nothing" + ticket/session + `colony-finalize: S`) → valid → controller → post; refused again → back to open
  579. once ("its report was incomplete"). `work: FINALIZE — the worker had its report refused (…); resuming claude session …
  580. once to FIX it`. `session.json.finalize.why = "had its report refused"`. `--finalize-max-turns 0` switches it off too.
  581. Salvage treats such a run as "did not run out of steps or time".
  582. * **Controller** (`templates/controller.md` 3): on a live project the steps describe the live site after the deploy — check
  583. them against the code, never open the live site; a work-copy / local step is `false`.
  584. * Tests: section "Tests of mission 027" below. Pre-027 code: `.scratch/m027/orig/`.
  585. ### Mission 026 (ticket antcolony#1): finished or not, "Previous attempt", cleanup, cheaper finalize, salvage lease
  586. Why (first live run, ident.worldapi.org#2): the salvaged report said "built in a work copy, not copied in, test unfinished,
  587. docs not updated" — posting it as "Ready for you to test" was wrong; the worker left two dev servers running (8700/8701);
  588. the finalize resume cost $1.05 because it missed the prompt cache.
  589. * **`complete`** (report key, boolean, `report.hl` + `templates/report.schema.json` + `templates/report.md`): true ONLY if
  590. the goal is reached AND what the creator should test is in place (copied into the app, docs updated). The controller
  591. checks it (`templates/controller.md` 3b: a `true` whose work is only in a work copy → `false` finding → fail).
  592. **`complete: false` + valid report + pass** → `postReport` writes ONE comment `Half done: <result>; the next worker
  593. continues.` + `- Still open: <open[0]> (and n more)` + questions / problems filed / running / refused (≤ 3 bullets, no
  594. Test line) + machine line `colony-report: S · sha256 … · controller pass · half done`, then the state `in progress →
  595. open` ("Back to open: half done — the next worker continues."; any other state is left alone). Questions and issues are
  596. filed as usual. Outcome `half done` (`WORK HALF DONE — …`, `SESSION END … · half done`). A half-done report does NOT
  597. answer a rejection (`relations.hl`): the next brief still starts with it. Half-done sessions are not counted as
  598. controller fails (no limit on half-done rounds yet).
  599. * **"Previous attempt"** (`lib/previous.hl`, in the brief after the intro, before "Read first"): only when the ticket's
  600. history has a colony lease event (`colony-lease: S` — work or salvage — or v0 `session S started on H`). The LAST such
  601. session: how it ended (last marker of S: `colony-report` [· half done], `colony-verdict … fail`, `colony-run` (no
  602. report), `colony-lease-expired`, `colony-parked`), and from `runs/<S>/` on THIS host (`COLONY_RUNS_DIR` /
  603. `$COLONY_HOME/runs`): its result (+ "not complete"), up to 5 `open` items, its work copy = every `.scratch/<name>` named
  604. in the report (done / open / verified commands / running logs / result; relative → the dev folder, nested ones dropped,
  605. trailing `.`/`,` stripped), its report + run folder paths; no report → session.json's outcome + the run folder. Run
  606. folder on another host → says so. Example (e2e): `.scratch/m026/briefs/brief-m026-rej.md`.
  607. * **Cleanup** (`claude.hl` runClaude/cleanupPorts + `lib/cleanup.sh`): every worker / resumed / finalize / controller
  608. session gets `COLONY_SESSION=<S>` in its env; when the part exits (any outcome — report, no report, timeout kill, park)
  609. `bash lib/cleanup.sh <from> <to> <since-ms> <S>` stops each process that LISTENS (TCP, `ss -ltnpH`) on the session's
  610. range AND started > 50 ms after the part began (start = now − (uptime − /proc starttime); btime+starttime was up to 1 s
  611. off) AND belongs to this user AND is not an ancestor of the script (the scheduler) AND does not carry ANOTHER
  612. `COLONY_SESSION`. SIGTERM, ≤ 5 s, then SIGKILL. Every stopped / skipped process is printed (`work: CLEANUP — stopped pid
  613. P · port N · started <ISO> · SIGTERM · <cmd>`) and kept in `session.json.cleanup[{part, at, lines}]` /
  614. `session.json.controller.cleanup`. Not stopped: children that do not listen (e.g. a `node --watch` parent), processes
  615. of other users (no pid visible), anything outside the range. `rules.md` tells the worker.
  616. * **Cheaper finalize**: the finalize resume has NO `--disallowedTools` any more (it changed the tool list = the cached
  617. prefix); the "change nothing" instruction (now "do not use the Edit/Write tools"), `auto` mode and the rest stay. Real
  618. run (2026-09-24, `.scratch/m026/real-work.txt`): Sonnet worker `--max-turns 2` → out of steps ($0.1866, cache write
  619. 41 438) → finalize resume **$0.0301, cache read 83 909 / write 1 259**, changed no file (mtimes) — vs. mission 025's
  620. finalize with the flag: $1.0528, cache read 0 (207 k context). The controller keeps `--disallowedTools` (a new session).
  621. * **Salvage lease**: `salvage` now needs the token always and a ticket in state `open`; it leases it (`open → in progress`,
  622. text `A worker started on this ticket: it writes the report of an earlier session.` + `colony-lease: S · host H ·
  623. salvage · ms N`, heartbeat) → posted by the verdict, or no report → the lease goes back (`→ open`, the worker's reason,
  624. `SALVAGE FAILED — … → <ref> back to open`); without `--post` the ticket stays leased (`./colony check <dir> --post`).
  625. * **The real salvaged run of ident.worldapi.org#2** predates `complete`: to post it as half done, a COPY with
  626. `"complete": false` was posted to OWN tickets (`.scratch/m026/halfdone-real.sh` → `halfdone-real.txt`,
  627. `halfdone-real-ticket.md`). For the live post the architect adds `"complete": false` to
  628. `runs/s-20260924T1527-525807/report.json` (changes its sha256; no comment of it exists live yet) and runs `./colony check
  629. runs/s-20260924T1527-525807 --post` (reuses verdict.json).
  630. ### Finalize step + salvage (mission 025, ticket antcolony#1; `work.hl` startFinalize / runSalvage)
  631. Why: the first live cycle (ident.worldapi.org#2, 2026-09-24) — the Sonnet worker did the work (6 gates green in its dev copy),
  632. then hit `--max-turns 120` before copying back + reporting → `error_max_turns`, no report, $4.67 lost.
  633. * **Budget in the brief**: section `## Budget — leave room for the report` (before `## Rules`): "Steps: at most N … **Write
  634. your report before step R**" (R = N − min(10, ⌊N/3⌋), ≥ 1: 120 → 110, 40 → 30, 12 → 8), the time limit (`--timeout`),
  635. and "running short: stop, report done / verified / open". N / time = the `--max-turns` / `--timeout` (or env) of `work`,
  636. `brief`, `run`.
  637. * **Finalize** (inside `work` / a resumed `run` session): the worker ended with subtype `error_max_turns`, or was killed by
  638. `--timeout`, and no park applies → `runs/<S>/claude-result.json` → `claude-result.before-finalize.json` (+ stderr), then
  639. `claude -p --resume <claude session> --output-format json --json-schema templates/report.schema.json --model <same>
  640. --max-turns <--finalize-max-turns, 8> --permission-mode <same> --permission-prompts none --strict-mcp-config` (mission
  641. 026: `--disallowedTools Edit,Write,NotebookEdit` removed — prompt cache) in the dev folder, stdin `runs/<S>/finalize.md` ("STOP WORKING — write your
  642. report now": do not continue, change nothing, report done/verified/open honestly, ticket + session, last line
  643. `colony-finalize: <S>`). Timeout `--finalize-timeout` (default 600 s, never more than the worker's `--timeout`).
  644. Valid report → controller → post by the verdict (the controller prompt shows "Part 1 — the worker, until it ran out" +
  645. "Part 2 — the finalize step"). No report again → `work: FINALIZE FAILED — …`, then the normal give-back (`open`, ONE
  646. state event, the WORKER's reason — "it ran out of steps / time"). Nothing about the finalize step is written to the
  647. ticket. `--finalize-max-turns 0` = off (old behaviour). ONE finalize per session.
  648. * `session.json.finalize` = { why, workerReason, workerSubtype, workerTurns, workerCostUsd, maxTurns, timeoutSeconds,
  649. started, ended, costUsd, numTurns, subtype, outcome (`valid report` / `report refused by the validation` / `no report: …`) };
  650. `costEarlier` = the worker part, `costUsd` = the finalize part.
  651. * **Cost of a resumed session (found on the real salvage)**: Claude Code 2.1.280 reports the WHOLE session's cost in
  652. `total_cost_usd` / `modelUsage` of a `--resume` call (cumulative: 5.7198 = worker 4.6670 + resume 1.0528; `usage` is
  653. per call). `finish` now takes `total − meta.claudeCostSeen` for any resumed part (finalize AND mission 020's parked
  654. resume — that one used to count the first part twice) and prints `FINALIZE COST:` / `RESUME COST: $x for this part
  655. (Claude reports $y for the whole session so far)`.
  656. * **`./colony salvage <runs/S> [--post] [--ports A-B] [--finalize-max-turns N] [--finalize-timeout S] [controller options]`**:
  657. the finalize step for a FINISHED run: refused unless the worker ran out of steps (claude-result subtype
  658. `error_max_turns`) or time (`outcome` "timed out after"), the run is on THIS host (the Claude session lives there), no
  659. finalize yet, no report.json, not parked; `--post` needs the token and a ticket that is not `in progress` / `confirmed`
  660. (posts to `COLONY_TICKETS_URL`!). Mission 026: takes a LEASE (token always, ticket must be `open`) — see "Mission 026". Controller gets an extra
  661. line: "SALVAGED run — re-run NOTHING that writes …, only read". Valid report + verdict → with `--post` posted by the
  662. verdict like `check`, else `SALVAGE OK — … NOT posted`; later `./colony check runs/<S> --post` posts it (reuses
  663. verdict.json — that is how the architect posts a salvage to the live tickets). No report → `SALVAGE FAILED`, nothing
  664. written to tickets. Last lines `SALVAGE COST: finalize $a + controller $b (the worker before: $c; whole session now $d)`,
  665. `SALVAGE END — <ref>: <outcome>`.
  666. * Controller transcript (mission 025): a transcript over 80 000 characters now keeps its first 30 000 AND its last
  667. 50 000 characters (was: only the start — the end of a long session, where the final checks are, was lost).
  668. ### check
  669. `./colony check runs/<session> [--post] [--again]`: the controller for a finished `work` run (valid
  670. `report.json` + `session.json` needed). Uses an existing `verdict.json` (no new session) unless
  671. `--again`; then, with `--post`, posts by the verdict (same as `work`). Last line `CHECK DONE / OK /
  672. FAILED …`.
  673. ### cycle
  674. `./colony cycle [--post] [--dry-run] [work/controller options]`: ONE pass for ONE ticket —
  675. 1 `next` (the oldest eligible ticket whose registry `dev.host` is THIS host; eligible tickets of other
  676. hosts are printed as skipped; `next --post`'s creator questions are NOT filed by `cycle`) → 2 `work` →
  677. 3 controller → 4 post by the verdict (only with `--post`) → `CYCLE COST: $sum (worker $a + controller
  678. $b)` and `CYCLE END — <ref>: <outcome>` (posted / sent back / question / no verdict / verdict …, not
  679. posted / no valid report). `--dry-run` stops before any Claude session (and before the lease): nothing
  680. written. Options after `cycle` go to `work` unchanged.
  681. **Permissions (tested 2026-09-24, Claude Code 2.1.280 on Loreana).** Default `--permission-mode auto`
  682. (the classifier lets a worker edit and run commands in its folder, blocks production actions), with
  683. `--permission-prompts none` (anything that would ask is denied, shows in `permission_denials`). Never
  684. bypass. **Haiku is allowed as worker model** (creator 2026-09-24; nothing refuses it), the default stays Sonnet + auto.
  685. **But auto mode needs a model that supports it: Haiku does NOT** — Claude then silently falls back
  686. to `default` (debug log: `auto mode disabled: model claude-haiku-4-5… does not support auto mode`), and
  687. with prompts off the worker can edit/run nothing. `work` prints `WARNING — asked for permission mode
  688. "auto", Claude reported "default"` (it reads the system messages). So the default model is `sonnet`
  689. (cheapest with auto). Loreana's `~/.claude/settings.json` has `defaultMode: bypassPermissions` —
  690. `work` always passes `--permission-mode` explicitly. `--output-format json` gives a message LIST on
  691. Loreana (`verbose: true` in the user settings); `work` takes the `result` entry (or a lone object).
  692. Cost seen: Sonnet, 4 turns, trivial file task: $0.18; Haiku 1 turn: $0.03–0.06 (≈28 k tokens of system
  693. prompt; `--strict-mcp-config` halves it by dropping the user's MCP servers).
  694. ### Relations, questions, rejections (mission 022, `lib/relations.hl`; concept §2 "Creator questions are tickets", "Rejections are work")
  695. Needs tickets' relations (tickets#4, mission 017: `parent`, `children`, `blockedBy`, `blocks` on every row). A tickets
  696. without them (no `children` key) is tolerated: no blocking, no parents, no links; the brief says "serves no relations".
  697. * **Blocked**: any `blockedBy` entry whose state is not `confirmed` (also `awaiting creator`, `rejected`) → not eligible.
  698. * **Parent**: a ticket with children is not work — except children that are creator questions: the scheduler's own
  699. (`source` starts with `colony` and contains `:question`: report questions, unknown-project issues, the Nth controller
  700. fail) and (mission 023) the architect's hand-filed ones: subject starts with the WORD `Question` (`Question: …`,
  701. `Question (x#1): …`, `Questions …` — not `Questionnaire …`). Without that exception a ticket that ever asked a question
  702. could never be reworked.
  703. * **Questions**: see `report --post` 2b. Every creator question the scheduler files (`fileQuestionIn` in
  704. `lib/tickets.hl`) is idempotent by `source`, sets `awaiting creator` only on a new ticket or one that never had a
  705. state event (repairs a half-done run, never undoes the creator), and sets the parent only while there is none.
  706. `next --post`'s project-level questions (no metadata / no concept) stay in `antcolony` without a parent.
  707. * **Rejections**: the last `→ rejected` event vs. the last colony session start (= `work`'s lease event: `→ in
  708. progress` with a `colony-lease:` marker, or v0's `session <S> started on <H>`). Newer → eligible (`next`, `cycle`,
  709. `run`, `work`). The brief starts with it while no `colony-report:` comment came after it (so a rework that ended
  710. without a valid report still carries it):
  711. `REJECTED by the creator: <the rejection's text>` · who/when · "The creator's comments since the last session"
  712. (comments by the rejecting user after the last session start before the rejection, quoted) · **Build what was
  713. described, not a workaround; if impossible, stop and explain** · `---` · then the normal brief.
  714. The lease moves `rejected → in progress` (Colony may: only `→ confirmed/rejected` is creator-only).
  715. * **A question ticket is NEVER work, in no state** (mission 030, antcolony#1). The pre-030 live run treated rejected
  716. question tickets ("already answered") as rework: ident#17 once, gitoria#3 twice, gitoria#4 five times. Now a ticket that
  717. is a creator question (`isCreatorQuestion`: colony question `source`, or subject starting with the word `Question`) is:
  718. `next` / `cycle` / `run` → listed `a question for the creator — the librarian takes its answer (<state>) — never work`
  719. (open, rejected, on hold; checked BEFORE the rework-on-rejection logic, not counted as "rejected"); `brief` / `work` →
  720. `REFUSED — <ref> "<subject>" is a question for the creator — the librarian takes its answer (state …); never work — no
  721. brief, no worker` (nothing written); `salvage` → REFUSED; a session PARKED on one (pre-030) → `run` does not resume it
  722. (`<ref> parked, but a question for the creator … → not resumed`); leased in progress → `next` adds `…: never resumed`.
  723. Its answer reaches workers only through the librarian (DECISIONS.md in every brief). Not guarded: `ready` / `deployed`
  724. (a question that a pre-030 session left "built — waiting for the deploy", e.g. ident#17, is still listed there).
  725. ## Ticket texts (mission 023)
  726. antcolony README "Ticket texts are written for the creator" — hard limits: ≤ 5 short lines, plain words, what is done /
  727. what to do first. Every text the scheduler writes = plain lines + at most ONE machine line, always the last
  728. (tickets' Markdown shows raw HTML as typed, so no `<!-- -->` / `<details>`). All of them (e2e examples:
  729. `.scratch/m023/texts-2.md`):
  730. | when | text (machine line last) |
  731. |---|---|
  732. | lease (`→ in progress`) | `A worker started on this ticket.` · `colony-lease: S · host H · started · ms N` |
  733. | heartbeat (comment) | `Still working.` · `colony-lease: S · host H · renewed · ms N` |
  734. | parked (comment) | `Paused: the Claude usage limit is reached — work continues after 2026-09-24 18:19 UTC.` (or `the scheduler was stopped`) · `colony-parked: …` |
  735. | resumed (comment) | `Work continues after a pause.` · `colony-lease: S · host H · resumed (park k) · ms N` |
  736. | no valid report (`→ open`) | `Back to open: the worker stopped without a usable result (it ran out of steps / time, its report was incomplete, it ended without a result) — the next worker tries again.` · `colony-run: S · host H · no report` (reason + validation problems in `session.json`) |
  737. | lease expired (`→ open`) | `Back to open: the worker stopped answering (its computer crashed or went offline) — a new worker will pick it up.` · `colony-lease-expired: S · host H` |
  738. | report (comment) | `<result>` · `**Test:**` + `1. …` lines (m027; was `**Test:** 1) … 2) …`) · ≤ 3 bullets (`A question for you: [alpha #6](…)`, `Problems found elsewhere: [hybriel #3](…) (blocks this ticket)`, `Not done: …`, `Still running: …`, `The safety check refused n actions.`) · `colony-report: S · sha256 <12 hex> · controller pass` |
  739. | report (`→ awaiting creator`) | `Ready for you to test — see the last comment.` |
  740. | report on a project with `live` (m027, comment) | `Built — goes live with the next deploy.` · `colony-report: S · sha256 <12 hex> · controller pass · ready to deploy` |
  741. | same, ticket was `open` (m027, `→ in progress`) | `Built — waiting for the next deploy.` |
  742. | `deployed` (m027, comment) | `Now live on <site> — <result>` · `**Test:**` · `1. …` (2–4) · ≤ 3 bullets (question / problems / `Decided: …` / Not done …) · `colony-deployed: S · sha256 <12 hex>` |
  743. | `deployed` (m027, `→ awaiting creator`) | `Ready for you to test — see the last comment.` |
  744. | half done (m026, comment) | `Half done: <result>; the next worker continues.` · `- Still open: …` · ≤ 2 more bullets · `colony-report: S · sha256 <12 hex> · controller pass · half done` |
  745. | half done (m026, `in progress → open`) | `Back to open: half done — the next worker continues.` |
  746. | salvage lease (m026, `→ in progress`) | `A worker started on this ticket: it writes the report of an earlier session.` · `colony-lease: S · host H · salvage · ms N` |
  747. | controller fail (comment) | `The result did not hold up when it was checked — back to open for another try.` (overruled: `could not be confirmed`; Nth: `failed its check 2 times — on hold until you answer [antcolony #4](…)`) · ≤ 3 `Not true: …` / `Not shown: …` · `colony-verdict: S · fail · k of N[ · overruled]` |
  748. | controller fail (state) | `Back to open: the check failed (1 of 2 tries).` / `On hold: the check failed 2 times — waiting for your answer.` |
  749. | creator question: report | subject `Question (alpha#3): <first line ≤ 100>` · summary = the question + `Asked while working on [alpha #3](…). **Answer** with a comment here — the next worker reads it.` |
  750. | creator question: Nth fail | `Question (dummy#6): the work failed its check 2 times — how should it go on?` · what happened · `**What to do:** …` · `- Last check: …` |
  751. | creator question: unknown project | `Question (alpha#3): where should "<issue>" go?` · `A problem found while working on … belongs to "zeta", a project the colony does not know.` · `**Answer:** …` · Repro / Observed / Expected |
  752. | creator question: `next --post` | `Question: project beta has no written concept yet — can we write one?` / `… is not set up for the colony yet — where does its code live?` · reason · `**What to do:** …` · `- Waiting: [beta #1](…)` (≤ 3 links) |
  753. | any question (`→ awaiting creator`) | `Waiting for your answer.` |
  754. | issue in another project | subject = the worker's · `Found while working on [alpha #3](…) — it blocks that ticket.` · `**Repro:**` / `**Observed:**` / `**Expected:**` |
  755. The worker is told the limits in `templates/report.md`; `report.hl` refuses a `result` / `test` that breaks them. Free
  756. text written by workers (`open`, questions, issue repro) is clipped to one line where the code quotes it.
  757. ## Config (env)
  758. | Var | Default | |
  759. |---|---|---|
  760. | `COLONY_TICKETS_URL` | `https://tickets.worldapi.org` | tickets base URL |
  761. | `COLONY_TOKEN_FILE` | — | file holding a tickets API token (`tkt_…`); needed only for `--post`; never printed |
  762. | `COLONY_PROJECTS_DIR` | `projects/` beside `scheduler.hl` | registry: one `<name>.json` per project |
  763. | `COLONY_INBOX_PROJECT` | `antcolony` | where creator questions go |
  764. | `COLONY_DIGEST_HOUR` / `COLONY_STATUS_DIR` | `7` / `status` | antcolony#19: UTC hour of the daily summary (`off` = never) / where `STATUS.md` is written |
  765. | `COLONY_PORTS` | `8700-8749` | default port range for briefs |
  766. | `COLONY_SERVE` / `--serve` | — | mission 035: the scheduler proper — port of the agent door (no worker of its own); `COLONY_SERVE_HOST` (127.0.0.1), `COLONY_AGENT_TTL` (30 s) |
  767. | `COLONY_AGENT_TOKEN_FILE` | — | mission 035: the secret scheduler and agents share (both refuse to start without it) |
  768. | `COLONY_SCHEDULER_URL` / `--scheduler` | — | mission 035: the agent's scheduler, e.g. `https://…` |
  769. | `COLONY_AGENT_POLL` / `COLONY_AGENT_QUOTA_EVERY` | `5` / `60` | agent: seconds between syncs / between quota reads |
  770. | `COLONY_USER_AGENT` | `antcolony-scheduler/0` | sent on every request (Cloudflare refuses script defaults) |
  771. | `COLONY_HOME` | set by `./colony` | absolute path of this folder (`work` refuses without it) |
  772. | `COLONY_HOST` | `/etc/hostname` | this host's name, compared with the registry's `dev.host` |
  773. | `COLONY_RUNS_DIR` | `runs/` beside `scheduler.hl` | `work`'s per-session files |
  774. | `COLONY_SANDBOX` | `on` | `off` = sessions run with host access (only the tests' fake claude needs it) — see "Sandbox" |
  775. | `COLONY_SANDBOX_RO` | the docs folder | extra read-only paths in the box, `:`-separated |
  776. | `COLONY_MODEL` / `--model` | `sonnet` | worker model; `haiku` is allowed, but has no auto mode (see Permissions) |
  777. | `COLONY_MAX_TURNS` / `--max-turns` | `40` | |
  778. | `COLONY_PERMISSION_MODE` / `--permission-mode` | `auto` | never `bypassPermissions` (refused) |
  779. | `COLONY_WORK_TIMEOUT` / `--timeout` | `3600` | seconds, then the worker is killed |
  780. | `COLONY_MAX_BUDGET_USD` / `--max-budget-usd` | — | passed to `claude --max-budget-usd` when set |
  781. | `COLONY_CLAUDE` / `--claude` | `claude` | the Claude Code binary (tests: `tests/fake-claude.mjs`) |
  782. | `COLONY_CONTROLLER_CLAUDE` / `--controller-claude` | = the worker's | the controller's binary (tests: a fake worker + the real controller) |
  783. | `COLONY_CONTROLLER_MODEL` / `--controller-model` | `sonnet` | |
  784. | `COLONY_CONTROLLER_MAX_TURNS` / `--controller-max-turns` | `20` | |
  785. | `COLONY_CONTROLLER_TIMEOUT` / `--controller-timeout` | `900` | seconds, then the controller is killed (= no verdict) |
  786. | `COLONY_MAX_FAILS` / `--max-fails` | `2` | the Nth controller fail on a ticket → creator question + `on hold` |
  787. | `COLONY_FINALIZE_MAX_TURNS` / `--finalize-max-turns` | `8` | mission 025: steps of the finalize step; `0` = no finalize step |
  788. | `COLONY_FINALIZE_TIMEOUT` / `--finalize-timeout` | `600` (≤ the worker's `--timeout`) | seconds, then the finalize step is killed |
  789. | `COLONY_LEASE_TTL` / `--lease-ttl` | `7200` | seconds a lease lives without a heartbeat (`work`, `cycle`, `run`) |
  790. | `COLONY_HEARTBEAT` / `--heartbeat` | ttl / 2 | seconds between heartbeat comments |
  791. | `COLONY_RUN_INTERVAL` / `--interval` | `60` | `run`: seconds between iterations |
  792. | `COLONY_PARALLEL` / `--parallel` | `1` | `run`: sessions at once |
  793. | `COLONY_QUOTA_RESERVE` / `--reserve` | `70` | `run`: no start at/above this % of the 5 h window; `100` = never blocks |
  794. | `COLONY_WEEK_LIMIT` / `--week-limit` | `84` | `run`: no start (resumes too) while the WEEKLY usage is ABOVE this % or unknown; `100` = never blocks (mission 023) |
  795. | `COLONY_QUOTA_CLAUDE` / `--quota-claude` | = `--claude` | `run`: binary for the `/usage` probe |
  796. | `COLONY_PORT_POOL` / `--port-pool` | `8700-8799` | `run`: ports handed out to sessions |
  797. | `COLONY_PORTS_PER_SESSION` / `--ports-per-session` | `10` | `run`: size of one session's range |
  798. | `COLONY_STOP_FILE` | set by `./colony run` | the signal channel wrapper → scheduler (don't set by hand) |
  799. | `COLONY_SESSION` | set by the scheduler for every Claude session part | mission 026: the colony session id; the cleanup never stops a process carrying ANOTHER one (don't set by hand) |
  800. | `COLONY_LIBRARIAN` / `--librarian` | `on` | mission 029: the librarian runs first in every `run` iteration and before `brief`/`work`/`cycle`; `off` = never |
  801. | `COLONY_LIBRARIAN_MODEL` / `--librarian-model` (`librarian`: `--model`) | `sonnet` | the librarian's model |
  802. | `COLONY_LIBRARIAN_MAX` / `--librarian-max` (`librarian`: `--max`) | `0` (= all) | creator texts classified per pass; texts left over → `run` starts nothing that iteration |
  803. | `COLONY_LIBRARIAN_CLAUDE` / `--librarian-claude` | = `--claude` | the librarian's Claude binary (tests: the fake) |
  804. | `COLONY_LIBRARIAN_TIMEOUT`, `COLONY_LIBRARIAN_MAX_TURNS` | `300`, `4` | per model call |
  805. | `COLONY_LIBRARIAN_DIR` | `librarian/` beside scheduler.hl | state.json, decisions.json, calls/, lock, projects/<name>/DECISIONS.md (fallback) |
  806. | `COLONY_DECISIONS_GLOBAL` | `templates/decisions-global.md` | the decisions for all projects (+ `-archive.md` beside it) |
  807. | `COLONY_CREATOR_NAMES` | `Caramboleyo,creator` | the creator's display names in tickets (exact, case-sensitive) |
  808. | `COLONY_DECISIONS_FULL_LINES` | `150` | a decisions file up to this many lines goes into the brief whole, a longer one by matching topics |
  809. | `COLONY_LAYOUTS_CONVENTIONS` | `/media/STORAGE/projects/antcolony-docs/docs/layouts-conventions.md` | named in briefs of `"ui": true` projects |
  810. | `COLONY_TEST_HOOK_URL` | — | mission 031, TESTS ONLY: GET before the re-read of an expiry / resume (the e2e reproduces the race with it); never set live |
  811. Relative paths in env/arguments are resolved against the cwd.
  812. `projects/` here is THE project registry (single source since 2026-09-24; one `<name>.json` each:
  813. `name, dev {host, folder}, concept, dependsOn, code?, live?, deploy?, note?, workers?, ui?` — mission 029: `ui: true` =
  814. an app with pages/styles → its briefs name the layout conventions).
  815. Live use needs the "Colony" identity + token (created by the creator, concept "Decided" 5); until
  816. then the architect's token file works: `COLONY_TOKEN_FILE=/root/.config/antcolony/tickets-token`.
  817. ## Test
  818. ```bash
  819. COLONY_E2E_PORT_BASE=8750 node tests/e2e.mjs # ~4 min, 343 checks (mission 034: + 13); 330 checks (mission 031: + 10); 320 checks (mission 030: + 10); 310 checks (mission 029: + 44 librarian); before: 266 checks (234 up to mission 026 − 3 moved + 35 mission 027); ident base+1, tickets base+2
  820. # (base 8700–8797); `work` sessions get base+3..base+9 (COLONY_PORTS — m026: the cleanup stops leftovers
  821. # there, so it must be a range only the e2e uses), `run` workers 8780-8799, salvage 8770-8779
  822. COLONY_E2E_PORT_BASE=8750 COLONY_E2E_TICKETS_DIR=/media/STORAGE/projects/tickets.worldapi.org/.scratch/dev-m012 node tests/e2e.mjs
  823. # 222 checks — a tickets WITHOUT the Markdown read view and WITHOUT relations: JSON fallback,
  824. # relation checks skipped (the e2e prints `tickets serves relations …: false`)
  825. ```
  826. The whole process against its OWN instances: tickets' and ident's code are COPIED (no `.env`,
  827. storage, sessions) to `.scratch/e2e/` and started there (ident via tickets' `tests/identkit.mjs`,
  828. mail to a sink). User "Colony" logs in via ident's code exchange, sets its name, makes an API token
  829. (→ `.scratch/e2e/colony-token`). A registry fixture (`alpha` concept + deps hybriel/beta, `beta`
  830. no concept, `gamma` no file, `delta` no deps, `hybriel`, `antcolony`) and 11 seeded tickets. Then
  831. `./colony` runs as a user would (cwd `.scratch/e2e`) and every outcome is read back over the API:
  832. `next` picks alpha#3 and explains each other ticket; `next --post` files 2 questions, a second run
  833. leaves the whole store identical (snapshot of every ticket + event); `brief` contains all 16 parts,
  834. refusals (no concept, no metadata, 404, bad ports); `report` refuses 17 kinds of bad reports with the
  835. exact message; `report --post` → exactly comment + state on alpha#3, hybriel#3 issue, antcolony
  836. question; again → store identical; different report same session → refused; second session on an
  837. awaiting ticket → comment only. Logs: `.scratch/e2e/{tickets,ident}.log`. Nothing stays running.
  838. **Test isolation (mission 030)** — tests never depend on the live configuration. The Hybriel interpreter loads the `.env`
  839. BESIDE THE ENTRY SCRIPT (`scheduler.hl`; not the cwd; the real environment wins, an empty variable counts as set — probe
  840. `.scratch/m030/envprobe/`). Run from this folder, the e2e's `./colony` calls got the live `.env` (max turns 120, week limit,
  841. token file, CLAUDE_CONFIG_DIR) → 6 failures. Now `tests/e2e.mjs` copies this folder WITHOUT `.env`, `.scratch`, `runs`,
  842. `sessions`, `briefs`, `logs`, `librarian`, `projects` to `.scratch/e2e/app/` and runs every `./colony` from there (chosen
  843. over "set every setting explicitly": a copy also covers keys added to `.env` later); every call gets a defined environment
  844. (inherited `COLONY_*` / `FAKE_*` dropped except `COLONY_E2E_*`) and — unless `COLONY_E2E_REAL=1` — a `claude` guard first on
  845. PATH that refuses and logs to `.scratch/e2e/claude-guard.log` (last check: never called). Prove it both ways:
  846. `node tests/e2e.mjs` here (live `.env` present) and in a copy without `.env` — both must be fully green.
  847. ### Tests of mission 035 (own file `tests/e2e-agent.mjs`, FAKE claude only, 39 checks, ~3 min; `COLONY_E2E_PORT_BASE=8710 node tests/e2e-agent.mjs`)
  848. Own tickets + ident (like e2e.mjs; ident = base+1, tickets = base+2, scheduler door = base+3, the agent's port pool base+4…base+9), work dir
  849. `.scratch/e2e-agent/`. Covers: scheduler alone (no agent → the ticket waits, no Claude call); the door (401 wrong / no token, 404, 400);
  850. agent connects → the ticket is worked in the AGENT's runs folder and posted (controller pass), the brief carries the agent's port range and
  851. no marker, the scheduler ran no Claude; scheduler killed mid-session → the worker still posts, the agent buffers the end and delivers it once
  852. after the reconnect, nothing runs twice; quota 80 % (agent side) → not accepting, the start waits, then resumes at 5 %; agent stop → OFFLINE
  853. on the scheduler; the four refusals. Both suites need a tickets/ident copy that logs in (`COLONY_E2E_TICKETS_DIR` / `COLONY_E2E_IDENT_DIR`).
  854. ### Tests of mission 034 (in `tests/e2e.mjs`, FAKE claude only, 13 checks; block "mission 034" at the end of mission 029)
  855. Project `lim` (dev folder `.scratch/e2e/limdev`, host e2e-host; every other open ticket put on hold first), the m029
  856. librarian folder, `FAKE_USAGE_FILE` `.scratch/e2e/m034-usage`, `FAKE_LIBRARIAN_MODE_FILE` `.scratch/e2e/m034-librarian-mode`
  857. (re-read per call — the test switches the librarian's answer while `run` runs), `FAKE_LOG_DIR` `.scratch/e2e/m034-calls`
  858. (every probe with its time). fake-claude.mjs librarian mode `ratelimit` = the LIVE answer's shape (resetsAt = now +
  859. `FAKE_RATELIMIT_RESET`), `ratelimit-noreset` = without the rate_limit_event and the 429. Outputs `.scratch/e2e/m034-*.txt`.
  860. (A) quota 100 % / 75 % (reserve 70) / 100 % with `--reserve 100 --week-limit 100`: `run --once` → no librarian call, no
  861. start, ITER line with the reset time and `librarian not run: …`, one probe. (B) quota 5 %, the librarian's call answers the
  862. limit (reset in 8 s) → `→ USAGE LIMIT … resets <that time>`, the pass stops, `0 without an answer`; ONE `LIMIT: … paused
  863. until <reset>`; during the pause NO librarian call, NO probe, no start, no ITER line; ONE `LIMIT: resumed`; then the SAME
  864. text is filed (tries = 1) and lim#1 is started → posted. (C) the limit without a reset time → recognised, paused 15 min,
  865. the next pass files the text (tries = 1). (D) a WORKER parked on the limit (reset 8 s) → `LIMIT: … paused until <its
  866. reset> · lim#3 <session> was parked on it`; a creator text written during the pause gets no call until `LIMIT: resumed`,
  867. then it is filed and the parked session is resumed (`--resume`) → posted.
  868. Changed old checks (behaviour changed on purpose): m020 "parked: …" now expects the ONE `LIMIT:` line instead of ITER lines
  869. "parked until" during the pause; m023 "both checks off" runs at 99 % (100 % = limit reached now holds starts); m029 "I"
  870. holds starts by an UNKNOWN week instead of 5 h 99 % ≥ reserve 50 % (that would now skip the librarian).
  871. Regression proof: the pre-034 code (`.scratch/m034/orig/lib`) with the new tests → 13 FAILED (`.scratch/m034/e2e-origcode-newtests.txt`).
  872. ### Tests of mission 031 (in `tests/e2e.mjs`, FAKE claude only, 10 checks; block "mission 031" after the m027 deploy gate)
  873. Own registry `.scratch/e2e/projects-m031/` with only project `lr` (live site `https://lr.example`, dev folder on e2e-host),
  874. `COLONY_LIBRARIAN=off`. (A) `work` on lr with `--lease-ttl 2` → ready to deploy; after 2.5 s the FIRST half of `deployed`
  875. is posted by hand (the comment with `colony-deployed: S`, no state change — the 146 ms gap of the live case, frozen) →
  876. `leases --expire` and `run --once` list "the lease of S has ended (deployed …)", write nothing, start nothing; `deployed`
  877. then completes it (HAVE, → awaiting creator). (B–E) one `run --once` with `COLONY_TEST_HOOK_URL` = an HTTP server of the e2e
  878. on base+8 (8758), four lr tickets leased by hand: B dead lease — the hook moves it to `awaiting creator` (the live case) →
  879. "NOT expired … it is "awaiting creator" now", only that event; C dead lease — the hook posts a heartbeat renewal → "NOT expired
  880. … the lease of S changed"; D dead lease, hook does nothing → still expired (control); E parked + due (runs/s-m031-race-e/
  881. session.json) — the hook moves it to `awaiting creator` → "NOT resumed", no claude call, no lease comment. Then `leases
  882. --expire` with the hook putting C on hold → "NOT expired … it is "on hold" now". Run outputs `.scratch/e2e/run-logs-m031/`.
  883. **Proof the test catches the bug**: the same e2e against the pre-031 code + ONLY the hook call (`.scratch/m031/orighook/`)
  884. → 322 passed / 8 failed (`.scratch/m031/e2e-orighook.txt`): A expired → open and `run` started a SECOND worker on it; B
  885. `expired lr#2 … → open` after the move to awaiting creator (the live bug); C expired despite the renewal; E resumed.
  886. ### Tests of mission 030 (in `tests/e2e.mjs`, FAKE claude only, 10 checks; block "mission 030" at the end of mission 022)
  887. Isolation (copy without .env; the guard never called). Questions in project `rej`: the report's question rej#q0 REJECTED by
  888. the creator ("already answered" — newer than any session, the pre-030 rework trigger), a hand-filed `Question: …` left OPEN,
  889. one ON HOLD → `next` lists all three "a question for the creator — the librarian takes its answer (<state>) — never work",
  890. `0 rejected`; `work` ×3 and `brief` ×2 REFUSED, store snapshot unchanged, no run folder, no fake-claude call; `cycle --post`
  891. → "nothing to do"; `run --once` → "nothing to start"; a session PARKED on a question (lease + park markers written by the
  892. test, `runs/s-m030-parked-q/session.json`) → `run --once` does not resume it; `next` shows it "…: never resumed";
  893. `salvage` of a max-turns run on the question → REFUSED, nothing written. m029's project `lib` (`workers: false`) now gets
  894. its DECISIONS files in the librarian folder and NOTHING in its dev folder (3 m029 checks changed accordingly).
  895. Logs: `.scratch/m030/e2e-*.txt` (see STATUS).
  896. ### Tests of `work` (in `tests/e2e.mjs`)
  897. With a FAKE claude (`tests/fake-claude.mjs`, no quota; answers like Claude 2.1.280 incl. the verbose
  898. message list): registry `dummy` (dev folder `.scratch/e2e/dummy`, host `e2e-host`) + `omega`
  899. (`"workers": false`); `next` lists omega "takes no workers"; 7 refusals write nothing; a good run
  900. → lease event text, comment, `awaiting creator`, claude's cwd = dev folder, brief on stdin, every flag
  901. (no bypass), no `COLONY_TOKEN_FILE` in the worker, all run files, `session.json` cost/usage, `USAGE`
  902. line; no structured output → `open` + reason; invalid report → `open` + problem; `--timeout 2` on a
  903. hanging worker → killed, `open`, nothing left running; without `--post` → stays `in progress`, then
  904. `report … --post`.
  905. ### Tests of the controller / check / cycle (mission 019, in `tests/e2e.mjs`)
  906. The fake claude is ALSO the controller (it sees the verdict schema in `--json-schema`; `FAKE_CONTROLLER_MODE`
  907. pass | fail | noevidence | nostructured | hang, `FAKE_CONTROLLER_LOG`); in `pass` it really looks at the files
  908. the report `cat`s. Checked: the controller runs after the worker in the dev folder with every flag (verdict
  909. schema, `--disallowedTools Edit,Write,NotebookEdit`, no bypass, no token), its stdin = `controller-prompt.md`
  910. = instructions + report + transcript (the worker's tool call/result) + brief; `verdict.json`,
  911. `session.json.controller`; pass → the comment's controller section, marker order. **fail**: fake worker
  912. `falseclaim` (report claims `missing.txt`) → `VERDICT: FAIL`, findings comment, issues NOT filed, → `open`;
  913. `check --post` again → uses verdict.json, store unchanged; **2nd fail** → creator question naming both
  914. sessions, ticket `on hold`, `next` skips it; **missing evidence**: controller says pass + a `no evidence`
  915. finding → the code fails it (overruled); `--max-fails 1`; **no verdict** → stays `in progress`, then
  916. `check --post` → controller → posted; **cycle**: nothing on another host, `--dry-run` (no Claude, no run
  917. folder, store unchanged), `cycle --post` → every step + `CYCLE COST: $0.0168 (worker $0.0123 + controller
  918. $0.0045)`. 119 checks (118 with the m012 tickets).
  919. ### Tests of mission 029 — the librarian (in `tests/e2e.mjs`, FAKE librarian model only, 44 checks; block "mission 029" before the m023 sweep)
  920. `tests/fake-claude.mjs`: a call whose `--json-schema` has `"supersedes"` is the LIBRARIAN — it reads the prompt's JSON input and
  921. answers by markers in the creator's text: `#not` / "test" → not a decision, `#global` → `all`, `#project:<p>`, `#topic:<a-b>`,
  922. `#excerpt:"…"` (verbatim), `#paraphrase` (a quote NOT in the text), `#longmeans` (a 2-line reading of ~400 characters),
  923. `i never said "X"` → supersedes the entries quoting X; `FAKE_LIBRARIAN_MODE` good | nostructured | badproject | hang;
  924. `FAKE_LIBRARIAN_LOG` (argv, cwd, input per call). Project `lib` (dev folder `.scratch/e2e/libdev` on e2e-host, `workers:
  925. false`, `ui: true`), project `lord` (a ticket `run` can start), creator texts by the e2e's CREATOR (display name `Creator`,
  926. `COLONY_CREATOR_NAMES=Creator`), `--since` = the block's start; every e2e call has its own `COLONY_LIBRARIAN_DIR` /
  927. `COLONY_DECISIONS_GLOBAL` under `.scratch/e2e/` (never the real `librarian/` or `templates/`).
  928. Checked: dry run (lists 8, no call, nothing written); first pass (8 calls, argv `--tools ""`, mode default, no session, cwd
  929. `librarian/`, no token; quote = the API text exactly; topics + contents; not-a-decision and Colony's text in no file;
  930. `#global` → the global file; verbatim excerpt alone / paraphrase → whole text + note; "yes" on a question ticket with the
  931. question as link + context; a ticket the creator opened (subject + summary); multi-line → blockquote; newest first; every
  932. registered project's file); nothing new → no call, no file changed; correction → archive; refused answer → retried once →
  933. given up → `--retry-refused`; no answer → retried; long reading → one line, clipped to 300; "i dont care" on two questions
  934. → two entries; `--redo`; brief (section after the conventions, before Where; both paths; whole short files; UI rules for
  935. `ui: true` only; a LONG file (`COLONY_DECISIONS_FULL_LINES=5`) → contents + only the matching topic + the search note);
  936. **ORDER**: `brief` files a decision written a moment ago before building (in the brief), `work --dry-run` / `--librarian
  937. off` call no model, `run --once` files the new text BEFORE the ITER line that starts a worker and that worker's
  938. `runs/<S>/brief.md` has it, a pass with a text left over (`--librarian-max 1`, 2 new) → "not filed yet → no start this
  939. iteration", the next iteration files it and starts (brief has both); `run` quiet when nothing is new, `--librarian off`, bad
  940. value refused; seed (refused without --post-files, 4 entries, wrapped quote joined, architect's reading, item without quote
  941. skipped, HAVE on re-run); lock → BUSY (and `brief` goes on, saying so); `--render` identical; nothing written to tickets.
  942. Logs: `.scratch/m029/e2e-fake-final.txt` (310/310 = 266 + 44; the one changed old check: the Hybriel block path, changed by
  943. the architect after mission 027 — the baseline before this mission was 265/266 because of it: `.scratch/m029/e2e-baseline.txt`),
  944. `e2e-fake-m012.txt` (m012 tickets). Pre-029 code: `.scratch/m029/orig/`.
  945. ### Tests of the container (mission 028, by hand, FAKE claude + own tickets; outputs in `.scratch/m028/*.txt`)
  946. ```bash
  947. cd /media/STORAGE/projects/antcolony-scheduler/.scratch/m028
  948. setsid nohup node own-tickets.mjs 6 > own-tickets.log 2>&1 & # own ident 8751 + tickets 8752, tiny#1..6; kill -TERM it afterwards
  949. ./dc.sh up -d; docker logs -f antcolony-scheduler-m028 # A: RUN + ITER lines, tiny#1..6 leased → fake worker → PASS → posted
  950. ./t.sh states # → all `awaiting creator`
  951. ./test-stop-park.sh # B+C: docker stop during a 60 s session → finish → 20 s → PARKED (stop took 21 s, nothing left); docker start → resumed → posted
  952. ./test-finish-kill.sh # D: stop during a short session → it finishes + posts, no park (9 s); E: docker kill → no process left;
  953. # F: kill -9 of the scheduler → container exits → docker restarts it (RestartCount 1)
  954. ./test-exec.sh # G: docker exec as uid 1000 = host fs; REAL claude --version + `claude -p /usage` (cost 0, 0 turns); ./colony leases
  955. ./dc.sh down # remove the test container; kill -TERM $(cat own-tickets.pid)
  956. ```
  957. Processes checked with `ps`/`/proc/<pid>/status`: entrypoint, scheduler and fake claude run as uid 1000, groups of mre,
  958. CapEff 0. Log rotation: `COLONY_LOG_MAX_BYTES=6000` in the test → `colony.log`, `.1`, `.2`.
  959. ### Tests of mission 027 (in `tests/e2e.mjs`, FAKE claude only, 35 new checks; 3 old ones removed/moved)
  960. `tests/fake-claude.mjs`: `FAKE_DECIDED` (the report's `decided`), `FAKE_CLAUDE_MODE=longresult` (result 124 characters + a test
  961. step on `.scratch/` → refused). Registry fixture `alpha` has `live` (https://alpha.example) → alpha is the deploy-gate project;
  962. new project `lv` (host e2e-host, live https://lv.example). Checked: 10 new report refusals (result 121, 1 step, step 91, step on
  963. `.scratch`, step on 127.0.0.1 for a live project, 4 questions, a 4-line question, a 301-character question, `decided` on two
  964. lines / not a list) + a report without `decided` accepted; brief: the conventions section (after "Read first"), the
  965. shared-components decision, "Tickets of alpha" (newest first, without the ticket itself), where to test (live URL / real
  966. result), the new limits, `decided`; **deploy gate via `report --post`** (alpha#3): open → in progress "Built — waiting for the
  967. next deploy.", ONE-line comment + `· ready to deploy`, `next` "built — waiting for the deploy", `ready` lists exactly it,
  968. `leases` "never expires", `deployed` refused without the run folder / on a ticket that waits for nothing, then the deployed
  969. comment (numbered steps, question / problems / Decided bullets, machine line) + awaiting creator, again → nothing new, `ready`
  970. empty; **deploy gate via `work`** (lv: worker → controller → `WORK DONE — lv#1 built, waiting for the deploy`), lease ttl 2 s
  971. long past → `leases --expire` / `run --once` leave it alone, `deployed` → awaiting creator; **fix step**: refused report →
  972. `FIX YOUR REPORT` resume of the same session → posted with the Decided bullet; refused again → open once; `--finalize-max-turns
  973. 0` → no fix step. Changed on purpose: the good report's test steps use the live URL; alpha#3 checks now go through
  974. ready/deployed (the comment-link checks moved to the deployed comment); the dummy comment's Test steps are a numbered list;
  975. `readableLines` does not count numbered step lines; refusal texts for 2–4 steps / 120 characters.
  976. Logs: `.scratch/m027/e2e-fake-3.txt` (266/266), `e2e-fake-m012.txt` (254/254, m012 tickets); texts `.scratch/m027/texts.md`, briefs
  977. `.scratch/m027/briefs/` (incl. `live-tickets-12.md`: a brief built from the LIVE tickets, GETs only). Pre-027 code
  978. `.scratch/m027/orig/`, baseline before the change `.scratch/m027/e2e-baseline.txt` (234/234).
  979. ### Tests of mission 026 (in `tests/e2e.mjs`, FAKE claude only, 19 checks; block "mission 026" after mission 022)
  980. `tests/fake-claude.mjs`: `FAKE_COMPLETE=false`; `FAKE_LEAVE_SERVER=worker,finalize,controller` + `FAKE_SERVER_LOG` (detached
  981. node servers after 300 ms: worker → own on COLONY_PORT_FROM, another session's (`COLONY_SESSION=s-foreign-e2e`) on +2, a
  982. SIGTERM-ignoring one on +3; finalize → +5; controller → +4); a SIMULATED prompt cache (a resume with another
  983. `--disallowedTools` than the session's first call: cache read 0 / write 20000 / 9× cost). Checked: report refuses a missing /
  984. non-boolean `complete`; `complete:false` → REPORT OK "NOT complete", PASS, `WORK HALF DONE`, ticket = lease + the half-done
  985. comment (exact text, ≤ 5 lines, question linked, no Test line) + `→ open`; `next` lists it; its brief has "Previous
  986. attempt" (session, HALF DONE, result, open items, work copy `<dev>/.scratch/dev-m026`, report path); a fresh ticket's brief
  987. has none; the next worker's stdin has it → complete → awaiting creator; the section then names the LAST session ("2 colony
  988. sessions"); a rejected rework that ends half done → open, next brief still starts with the rejection + Previous attempt;
  989. after a no-report session: "stopped without a usable report", run files. **Cleanup**: the e2e's own server started BEFORE
  990. the session (skipped, alive), the worker's own (stopped, SIGTERM), the stubborn one (SIGKILL), another session's (skipped,
  991. alive), the controller's (stopped after the controller), the finalize step's, a timed-out worker's; session.json cleanup
  992. lines; the e2e kills the skipped ones itself, nothing of ours listens afterwards. **Cheaper finalize (fake model)**: old
  993. flags $0.1107 / cache read 0 vs. new $0.0123 / cache read 20000. m025 checks changed on purpose: finalize argv now WITHOUT
  994. `--disallowedTools`; salvage → lease + give-back / lease + comment + awaiting creator. Logs: `.scratch/m026/e2e-fake-5.txt`
  995. (234/234), `e2e-fake-m012.txt` (222/222), texts `.scratch/m026/texts.md`, briefs `.scratch/m026/briefs/`. Pre-026 code:
  996. `.scratch/m026/orig/`. Real run: `.scratch/m026/real.sh` (own tickets 8753/8754 via `own-tickets.mjs`, Sonnet worker
  997. `--max-turns 2` + FAKE controller) → `real-work.txt`, run folder `.scratch/m026/real-runs/`, `show.py <run dir>` prints
  998. the transcript + cache numbers.
  999. ### Tests of mission 025 (in `tests/e2e.mjs`, FAKE claude only, 28 checks)
  1000. `tests/fake-claude.mjs`: a `--resume` whose stdin has `colony-finalize:` is the finalize call (`FAKE_FINALIZE_MODE` good |
  1001. nostructured | badreport | hang, default = the worker's mode; `FAKE_FINALIZE_LOG`; kind `finalize` in `FAKE_LOG_DIR`);
  1002. it keeps each Claude session's total cost in `.fake-claude-sessions/` of its cwd (`FAKE_STATE_DIR`) and reports it
  1003. cumulatively on `--resume` like the real Claude; `hang` writes the worker's file before hanging. Checked: brief Budget
  1004. section (120 → 110, 40 → 30, 7 → 5, minutes); **out of steps → finalize → report → controller PASS → posted** (lease +
  1005. comment + awaiting creator only; finalize argv: `--resume <the worker's session>`, same schema/model/mode, `--max-turns 8`,
  1006. `--disallowedTools Edit,Write,NotebookEdit`, no `--session-id`, no bypass, no token; finalize.md text; run files; costs
  1007. $0.0123 + $0.0123 with Claude's cumulative $0.0246 not double counted; controller sees both parts); **finalize fails too →
  1008. back to open ONCE** (2 events, the worker's reason); **out of time** → finalize (2 s) → posted; finalize hangs too → killed,
  1009. open, nothing left running; `--finalize-max-turns 0` = old behaviour, bad values refused; **salvage**: 5 refusals
  1010. (ended well, report refused, other host, no token, no folder) write nothing, `--post` on an `in progress` ticket refused,
  1011. failed finalize → `SALVAGE FAILED`, store identical, 2nd salvage refused; a salvage `--post --ports 8770-8779` → finalize →
  1012. controller (SALVAGED note) → posted (`open → awaiting creator`, no lease), SALVAGE COST line, ports in both env; `check
  1013. --post` afterwards → nothing new. Logs: `.scratch/m025/e2e-fake-4.txt` (215/215), `e2e-fake-m012.txt` (203/203).
  1014. ### Tests of mission 023 (in `tests/e2e.mjs`, FAKE claude only)
  1015. Readable texts: every check that asserted an old text now asserts the NEW text exactly (lease, heartbeat, park, resume,
  1016. give-back, expiry, report comment, awaiting-creator state, fail comment + state, all creator questions, issue summaries);
  1017. 6 new report refusals (`result` on two lines / > 200 characters / missing, `test` with 5 or 0 steps, a step on two lines);
  1018. the report comment has result + Test first, ≤ 3 bullets, no verified commands, the machine line last and short. **Sweep**
  1019. at the end: every text the scheduler wrote in the whole store (92 in the last run; seeds skipped) has ≤ 5 lines the
  1020. creator reads, at most ONE machine line and it is the last, none of the old jargon, every filed creator question's subject
  1021. starts with "Question"; all texts are dumped to `.scratch/e2e/texts.md` (copy: `.scratch/m023/texts-2.md`). Weekly
  1022. limit: week 85 % → `no start`, no weekly number → `week UNKNOWN → no start`, `--week-limit 101` refused, `--reserve 100
  1023. --week-limit 100` at 5 h 100 % and no weekly number → starts (fake `/usage` reads `FAKE_WEEK_FILE`, `none` = no weekly
  1024. limit). Hand-filed question: a child `Question (rel#5): which colour?` (no colony source) does not make rel#5 a parent,
  1025. the brief lists it as creator question; a child `Questionnaire page` does. Haiku: `work --model haiku` runs (not refused).
  1026. ### Tests of mission 022 (in `tests/e2e.mjs`, FAKE claude only, ~25 checks)
  1027. The e2e now starts tickets with a CREATOR (`[email protected]` in our ident; its per-app id =
  1028. `TICKETS_CREATOR_IDENTITY`; logs in as "Creator", token `CTOKEN`, helper `capi`) — only the creator rejects/confirms.
  1029. Checked: Colony may not reject (403); `report --post` of `good()`: alpha#3 blocked by hybriel#3 (link event), the
  1030. question "Is small enough?" → alpha ticket `Question (alpha#3): …`, awaiting creator, parent alpha#3, linked in the
  1031. comment, the zeta question has parent alpha#3; project `rel` (host loreana): rel#1 blocked by rel#2 → `next` "blocked
  1032. by rel#2 (open)", rel#3 with child rel#4 → "a parent …", `work` refuses both (store unchanged), brief Relations
  1033. sections (blocking / parent / children), blocker `awaiting creator` still blocks, confirmed by the creator → rel#1
  1034. eligible; project `rej` (host e2e-host): fake worker with 2 questions (`FAKE_QUESTIONS`) → 2 tickets (subjects,
  1035. summary, parent, links), re-post (`check --post`, `report --post`) → HAVE, store identical; creator comment + Colony
  1036. comment + rejection → `next` "REJECTED … → rework" (2 question children don't make it a parent), brief starts with
  1037. the rejection + the creator's comment only + the rule; `work` → `rejected → in progress` → the worker's stdin starts
  1038. with it → posted; afterwards the brief has no rejection; 2nd rejection → only the comments since the last session;
  1039. a failed rework → open, next brief still starts with it; 3rd rejection → `run --once` picks it → posted.
  1040. Briefs of the last run: `.scratch/m022/briefs/` (`.scratch/e2e/` is wiped by every e2e).
  1041. ### Tests of `run` (mission 020, in `tests/e2e.mjs`, FAKE claude only, 25 checks, ~40 s)
  1042. Projects `d1`, `d2` (host e2e-host, own dev folders), worker port pool 8780-8799; `tests/fake-claude.mjs` also answers
  1043. `-p /usage` (percent from `FAKE_USAGE_FILE`, re-read per call), `FAKE_CLAUDE_MODE=ratelimit` (rejected
  1044. rate_limit_event + "You've hit your limit", 429; `--resume` then answers `good`), `FAKE_CLAUDE_SLEEP`, `FAKE_LOG_DIR`
  1045. (one JSON per call: argv, pid, start/end, `COLONY_PORTS*`). Checked: live URL (unset / any spelling) → refused;
  1046. `--parallel 3` > pool → refused; quota 80% → `no start`, store unchanged, probe flags (plan, never bypass); `--reserve
  1047. 90` → starts, ports 8780-8789 in brief + env of worker AND controller; **crash**: SIGKILL of the scheduler while a
  1048. 60 s worker runs (the worker dies with it, PDEATHSIG) → restart skips the live lease → after ttl 6 s `expired … → open`
  1049. with the reason → ONE new session → posted; alpha#1 (no colony lease) never expired; SIGTERM idle → stops; `leases`;
  1050. **SIGTERM while running** → finishes + posts, the other open ticket not started; **2nd SIGTERM** → worker killed,
  1051. PARKED (marker with the Claude session id), next `run --once` → `claude -p --resume <id>` → posted; **usage limit** →
  1052. PARKED until the reset, quota 90% holds the resume, 10% → resumed → posted (cost of both parts summed); **`--parallel
  1053. 2`** → d1 + d2 started in ONE iteration with 8780-8789 / 8790-8799, really overlapping in time, the third ticket in
  1054. d1's folder waits; one `ITER` line per iteration; nothing left running. Run outputs: `.scratch/e2e/run-logs/*.txt` (wiped by every e2e; last copy `.scratch/m020/run-logs-2/`).
  1055. Last runs: `.scratch/m020/e2e-fake-2.txt` 144/144, `e2e-fake-m012.txt` 143/143 (`COLONY_E2E_PORT_BASE=8760`).
  1056. **Real Claude** (costs quota — 3 sessions, ≈ $0.37): `COLONY_E2E_REAL=1 COLONY_E2E_PORT_BASE=8760 node tests/e2e.mjs`
  1057. adds (briefs get ports 8770–8779): the open dummy tickets are parked (`on hold`), then ONE `cycle --post`
  1058. with a Sonnet worker (`--max-turns 12`) creating `hello.txt` and a Sonnet controller (`--controller-max-turns
  1059. 10`) → the controller re-ran the `cat`, PASS, report comment with the controller section, `awaiting
  1060. creator`, both sessions in mode `auto`; then a FAKE worker claiming `missing.txt` checked by the REAL
  1061. controller → FAIL (both findings `false`), back to `open`. 124 checks. `COLONY_E2E_REAL_MAXTURNS=1` adds
  1062. mission 016's Haiku `--max-turns 1` run (no report → `open`, no controller). Last real run (2026-09-24):
  1063. `.scratch/m019/e2e-real.txt`, run folders kept in `.scratch/m019/real-runs/` (`.scratch/e2e/` is wiped by
  1064. every run) — worker $0.087 + controller $0.175 (pass cycle), controller $0.104 (fail).
  1065. ## Files
  1066. | | |
  1067. |---|---|
  1068. | `colony` | wrapper (`--`); for `run`: setsid + SIGTERM/SIGINT → stop file |
  1069. | `scheduler.hl` | entry, dispatch |
  1070. | `lib/next.hl`, `lib/brief.hl`, `lib/report.hl`, `lib/work.hl` | the commands (rules in their headers); `work.hl` also has `check` and `cycle` |
  1071. | `lib/sandbox.hl` | the bubblewrap box of every Claude session + the deploy mark (README "Sandbox"); `tests/e2e-sandbox.mjs`, `tests/sandbox-mark.hl` |
  1072. | `lib/controller.hl` | the controller: prompt, transcript, verdict validation, posting by the verdict |
  1073. | `lib/status.hl` | antcolony#19: `status`, `digest`, the daily summary tick of `run`; `tests/e2e-status.mjs` |
  1074. | `lib/daemon.hl` | `run` (the loop, slots, ports, stop) and `leases` |
  1075. | `lib/lease.hl` | lease markers, parsing, expiry, heartbeat; mission 031: `endOf` (a lease that ended), `stillLeased` (re-read before acting) |
  1076. | `lib/quota.hl` | the `/usage` probe + its parser (ISO time → ms) |
  1077. | `lib/claude.hl` | one headless Claude session (spawn, timeout, output parsing, usage) — worker + controller |
  1078. | `lib/tickets.hl` | the API client (UA, token, `fileQuestion`) |
  1079. | `lib/registry.hl` | project metadata |
  1080. | `lib/util.hl` | sort / list / path helpers (Hybriel workarounds) |
  1081. | `lib/previous.hl` | mission 026: the brief's "Previous attempt" (last colony session, how it ended, its report / work copy) |
  1082. | `lib/deploy.hl` | mission 027: `ready` + `deployed` (the deploy gate's commands) |
  1083. | `templates/conventions.md` | mission 027: COPY of antcolony README "Conventions for all apps" (+ colours) — every brief carries it as text |
  1084. | `lib/cleanup.sh` | mission 026: stops what a session part left listening on its port range (called by `claude.hl` runClaude) |
  1085. | `lib/relations.hl` | mission 022: blocked / parent rules, creator-question children, rejection vs. last session, the brief's rejection + Relations parts |
  1086. | `lib/jsoncheck.hl` | copy of tickets' JSON syntax check (hybriel#6: JSON.parse cannot fail softly) |
  1087. | `templates/` | brief parts: Hybriel block (copy of antcolony `missions/TEMPLATE-hybriel-block.md`), rules, report schema; `report.schema.json` (JSON Schema for `claude --json-schema`, keep in step with `report.hl`); `controller.md` (the controller's instructions), `verdict.schema.json` (keep in step with `controller.hl`) |
  1088. | `runs/<session>/` | `work`'s files: brief.md, claude-result.json, claude-stderr.log, report.json, session.json; the controller's controller-prompt.md, controller-result.json, controller-stderr.log, verdict.json (0600/0700); mission 025: finalize.md, claude-result.before-finalize.json, claude-stderr.before-finalize.log |
  1089. | `projects/` | the project registry |
  1090. | `tests/e2e.mjs` | the test; `tests/fake-claude.mjs` the claude stand-in |
  1091. | `docker-compose.yml`, `.env.example` | mission 028: the permanent container `antcolony-scheduler` + its settings template (a `.env` is never committed) |
  1092. | `docker/pivot.sh` | mission 028: root part in the busybox container — private mounts, `pivot_root` into the host fs, drop to uid 1000 |
  1093. | `docker/entrypoint.sh` | mission 028: uid 1000 main process — mre's environment, `./colony run [--live]`, docker stop → finish/park, file log |
  1094. | `logs/colony.log` | mission 028: the container's rotating file log (`.1` … `.5`) |
  1095. | `lib/librarian.hl` | mission 029: the librarian — sources (tickets, seed), the model step + the code's checks, rendering, the brief's decisions part, `librarianFirst` |
  1096. | `templates/librarian.md`, `templates/librarian.schema.json` | mission 029: the librarian's instructions + answer schema (keep in step with `librarian.hl` checkAnswer) |
  1097. | `templates/decisions-global.md` (+ `-archive.md`) | mission 029: GENERATED — the creator's decisions for all projects (every brief carries it) |
  1098. | `librarian/` | mission 029: `decisions.json` (all entries, the source of every DECISIONS file), `state.json`, `calls/<event>-{prompt.md,result.json,stderr.log}`, `lock`, `projects/<name>/DECISIONS.md` for projects without a folder on this host |
  1099. Vendored Hybriel: `bin/hybriel` + `plugins/{core,crypto,data,fetch,fs,http,proc,time}` copied from
  1100. tickets.worldapi.org 2026-09-24 (sha256 `c51d163d…5805`, hybriel e565176b — see tickets' README).
  1101. ## Keywords (antcolony#17) — `lib/keywords.hl`
  1102. The creator writes a command as the first word of a comment; the scheduler acts and (for `/prio` and refusals) answers with a
  1103. comment that ends in `colony-keyword: <event id> · <keyword>` — that marker is the only memory (nothing else is stored; an
  1104. event with the marker in its history is done). Only comments whose author is in `COLONY_CREATOR_NAMES` count.
  1105. - `/hold` → state `on hold` ("On hold, as you asked.").
  1106. - `/prio high|normal|low|-9..9` → the last valid `/prio` of the ticket decides; `next` / `run` pick the highest priority
  1107. first, then the oldest (`next` lists `priority high` per ticket). Not understood → one refusing comment. Tickets has no
  1108. priority field, so the history is the store.
  1109. - `/confirm`, `/reject <why>` (the reason = the rest of the comment = the state text, so a rework brief quotes it). Tickets
  1110. lets ONLY the creator confirm/reject (403 for the Colony token) — the scheduler needs the creator's token in
  1111. `COLONY_CREATOR_TOKEN_FILE` (a tickets API token of the creator, `~/.config/antcolony/`, 0600). Without it the scheduler answers
  1112. once "I cannot confirm for you … use the button" and changes nothing. `/reject` without a reason is refused in a comment.
  1113. - A state keyword is ignored when a state change came after it (not tickets' own automatic `answered` after the creator's
  1114. comment) or the ticket is already in that state; a keyword that is not the first word is text.
  1115. - `run` keeps an in-memory `updatedMs` per ticket and re-reads only changed tickets; `keywords` (CLI) reads all.
  1116. - Test: `COLONY_E2E_PORT_BASE=8700 node tests/e2e-keywords.mjs` (own ident + tickets, 15 checks). If the current ident code of
  1117. the dev folder is mid-change, point `COLONY_E2E_IDENT_DIR` at an older ident copy (e.g. `.scratch/m028/own/ident/ident-code`).
  1118. ## New ticket states and roles (antcolony#27; tickets#20) — `lib/tickets.hl`
  1119. tickets#20 renamed the states: `awaiting creator` → `review` (a creator question → `pending`), `confirmed` → `done`,
  1120. `rejected` → `reopened`, `in progress` → `progress`, `on hold` → `pending` (`canceled` is new; `answered` is gone).
  1121. **One vocabulary inside, translated at the edge.** All scheduler logic keeps the old names; `tickets.hl` is the only file that
  1122. knows both:
  1123. - everything READ (`getJson`, and the answer of `postJson`) has its `state` / `to` / `from` turned into the old names
  1124. (`normalizeStates`) — an old server is left as it is;
  1125. - everything WRITTEN is turned into what the server speaks: `serverIsNew()` asks `GET /api/projects` once (`states` has `review`);
  1126. new server → new names (`writeState`), old server → old names. A built ticket → `review`; a question ticket (`fileQuestionIn`) →
  1127. `pending`; `/hold` → `pending`; `/confirm` → `done`; `/reject` → `reopened`; the lease → `progress`. A `reopened` ticket is read as
  1128. `rejected`, so it is picked up like a rejected one ("Rejections are work"); `done` counts as `confirmed`; `review` / `pending` are
  1129. not work.
  1130. - **Roles.** The scheduler's token (and the architect's) is a project member with the role `edit` (may move a ticket to any
  1131. state); `use` may only move open ⇄ review. Somebody with the role `admin` must add it: `POST /api/projects/<p>/members
  1132. { user: "<name>", role: "edit" }`.
  1133. - **The creator = the project's admin**, not a name: `adminsOf(project)` reads the members (`GET /api/projects/<p>`, 5 min cache),
  1134. `isCreatorEvent` decides for the librarian (creator texts) and the keywords (`/prio` `/hold` `/confirm` `/reject`). An event is
  1135. matched by user id when the API shows one (`userId`), else by the admin's display name — the tickets API never shows the ident
  1136. short id (it stays server side). A server without members (old tickets) → `COLONY_CREATOR_NAMES` as before.
  1137. - Test: `COLONY_E2E_PORT_BASE=8714 COLONY_E2E_IDENT_DIR=… node tests/e2e-states.mjs` (own tickets #20 copy) — never together with
  1138. another e2e (the full suite kills listeners on 8710–8719).
  1139. ## Sandbox — each worker in its own sealed box (antcolony#18) — `lib/sandbox.hl`
  1140. Every Claude session the scheduler starts (worker, its finalize / resume step, the controller) runs inside a
  1141. **bubblewrap** box (`/usr/bin/bwrap`), built from an allowlist — not the whole host filesystem any more:
  1142. | in the box | how |
  1143. |---|---|
  1144. | the system: `/usr /bin /lib /lib64 /sbin /etc /opt /sys`, `/proc` + `/dev` of its own, private empty `/tmp /var /run` | read-only (+ `/run/systemd/resolve` for DNS) |
  1145. | home = an empty tmpfs with only `~/.local/bin`, `~/.local/share/claude`, `~/.hybriel`, `~/.gitconfig` (ro) and the Claude login folder (`CLAUDE_CONFIG_DIR`, else `~/.claude` + `~/.claude.json`, rw) | no `~/.config/antcolony` (tokens), `~/.ssh`, shell history, other projects |
  1146. | the project's own dev folder | read-write; `.env*` files (not `.env.*.example`) replaced by an empty file |
  1147. | the dev folders of the projects it `dependsOn` (this host only) + paths its concept names + the docs folder (`COLONY_SANDBOX_RO`) | read-only |
  1148. | the controller additionally | the run folder, read-only |
  1149. | the scheduler's own folder (project antcolony) | `runs/ logs/ sessions/ briefs/` are empty inside — no sight of other projects' briefs and reports |
  1150. Own process namespace (`--unshare-pid`), dies with the scheduler (`--die-with-parent`); `COLONY_TOKEN_FILE`,
  1151. `COLONY_CREATOR_TOKEN_FILE`, `COLONY_AGENT_TOKEN_FILE`, `COLONY_STOP_FILE` are removed from the environment. The **network is
  1152. shared** (Claude API, the tickets copy, dev servers on the session's ports) — not restricted. Chrome, node, git, the project's
  1153. own `bin/hybriel` work inside.
  1154. **Deploy rights only for tickets marked for deploy.** A ticket is marked by the line `colony-deploy: yes` in its opening
  1155. text or in a comment of the creator. Only then the box also holds the project's `live` folder (if it is on this host) read-write
  1156. and `~/.ssh` read-only. The mark is read when the session starts and kept in `session.json` (`deploy`); a resume / finalize
  1157. step uses the same box; the controller never gets it. (Deploys are still the architect's job — this is the door for the day an
  1158. agent may deploy.)
  1159. No box possible (bwrap missing) → `work` REFUSES before it leases the ticket; a finalize / resume step whose box cannot be
  1160. built runs `/usr/bin/false` (never a session with host access). `./colony box <project> [--deploy] [--folder F]` prints the
  1161. box's command prefix, one argument per line — `"$(./colony box p)"`-style: run `<prefix> sh -c 'ls /media/STORAGE/projects'`
  1162. to see what a worker of `p` sees. Tested live: bwrap works as uid 1000 inside the `antcolony-scheduler` container
  1163. (`docker exec --user 1000 … bwrap …`, no extra capabilities needed).
  1164. Test: `node tests/e2e-sandbox.mjs` (19 checks, no tickets, no Claude, ~10 s). The other suites set `COLONY_SANDBOX=off`
  1165. themselves (their fake claude lives outside any box); `tests/e2e.mjs` has one block "antcolony#18" that runs a whole `work`
  1166. (worker + controller, fake claude) with the box ON.
  1167. ## Status and daily summary (antcolony#19) — `lib/status.hl`
  1168. Built from the tickets, never written by hand (concept: "STATUS is generated from tickets"; timer "daily digest").
  1169. - `./colony status [--write]` — Markdown: a table per project (open / in progress / awaiting creator / on hold / rejected /
  1170. confirmed; `answered` counts with awaiting creator), what waits for the creator, what is being worked on. `--write` also writes
  1171. `status/STATUS.md` (`COLONY_STATUS_DIR`, default `status/` beside the scheduler; regenerated, never edited).
  1172. - `./colony digest [--post]` — the daily summary: **what waits for the creator** = tickets in `awaiting creator`, split
  1173. into questions to answer and built work to check (max 8 listed each, then "and N more" + a count per project), plus one line
  1174. "working on N, M in the queue". Dry run prints; `--post` files it as ONE ticket in the inbox project (`COLONY_INBOX_PROJECT`),
  1175. subject `Daily summary <UTC day> — N tickets wait for you`, source `colony:digest:<day>` (once per day, also after a restart;
  1176. the creator's inbox is the only delivery channel until ident's daily mail exists), and writes `status/STATUS.md`. The summary
  1177. tickets themselves are never counted as waiting.
  1178. - `run` files it once a day: first iteration after `COLONY_DIGEST_HOUR` (UTC hour, default `7`; `off` = never), log line
  1179. `DIGEST <day> — N waiting · filed|already filed <ticket>`. No quota, no model.
  1180. - Test: `COLONY_E2E_PORT_BASE=8710 node tests/e2e-status.mjs` (own tickets + ident, 12 checks).
  1181. ### Only real questions reach the creator (antcolony#24)
  1182. The check after each job (the controller) is the only filter for a worker's `questions` — there is no count limit any more
  1183. (`lib/report.hl` no longer refuses more than 3). The verdict has a new list `questions`: one ruling
  1184. `{ index, ruling, reason, answer }` per question of the report (refused when one is missing, `lib/controller.hl` `validateVerdict`):
  1185. `ask` (changes what gets built, nothing decided answers it → its own creator ticket as before) · `decided` / `trivial` / `internal`
  1186. (dropped, no ticket; `answer` = the decision, added to the report's `decided` line) · `packed` (more than one decision → the verdict
  1187. becomes a fail, `decide`, the next worker splits it). `filterQuestions` applies the rulings before `postReport`; `templates/controller.md`
  1188. tells the check to search the brief's decisions first. Test: `bin/hybriel tests/questions-test.hl`; `tests/fake-claude.mjs` answers
  1189. `ask` for all (`FAKE_QUESTION_RULINGS=ask,decided,…` to vary). A report posted by hand (`report --post`) is not filtered.
  1190. ## A ticket waiting for a creator question (antcolony#30)
  1191. A ticket with a creator question (child ticket) that is not answered (`confirmed`) or refused (`rejected`) is not work: `next` lists it as "waiting for the creator's answer on <question>", `work` refuses it. Without an open question a half-done ticket continues as before; a newer rejection outranks a pending question.
  1192. Safety net: more than `COLONY_MAX_STARTS_PER_HOUR` (default 4; 0 = off) session starts on one ticket within an hour → not eligible until the hour has passed.

Branches

Latest commits

  • c613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mre
  • 9062978ctracker: worker box sees /media/STORAGE/projects/old-tracker read-only (tracker#2 source data)mre
  • 7f9660eeState of 2026-09-27, before the move to gitoriamre