Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Gated routine runner

A small organisation keeps its work in a folder tree. One folder per project, a task list in each, a few shared files at the root. Some of the work is done by people in sessions, some by unattended runs on a schedule. Without a protocol three things go wrong, quietly. Two writers land in the same project and the slower one wins; a run does something irreversible because the task said so; a run finds something out and nobody who works next ever sees it.

This repository is the protocol, as code, with a comparison run that counts how often each of those things happens with and without it.

Everything here is invented. The projects atlas, birch and cedar, the actors run-a, run-b and session-m, and every task text exist only in example.py. No dependency beyond the Python standard library.

Run it

python -m venv .venv
.venv/Scripts/activate        # Windows
# source .venv/bin/activate   # Linux, macOS
pip install -e ".[dev]"

python -m runner compare                  # ungated vs gated, on a fresh example
python -m runner --root org init          # write the example organisation
python -m runner --root org run run-a     # one scheduled run
python -m runner --root org status        # leases and pending decisions
python -m runner --root org decide <id> approve person
python -m runner --root org run run-a     # executes what was approved
pytest                                    # 16 tests

What a run does

acquire ──► work inside the project ──► complete ──► next project ──► integrate
   │              │                        │                              │
   │ skip once    │ reversible: do it      │ hashes of touched files      │ handover.md
   │ if leased    │ irreversible: propose  │ stop if anything else moved  │ situation.md
   │ stop if      │   a decision, mark ?   │ release                      │ under the
   │ expired      │ approved earlier: do   │                              │ root lease
  1. Acquire. One lease file per project, created with O_CREAT | O_EXCL, so two actors cannot both succeed. A project that is leased is skipped once and recorded as skipped. A lease whose heartbeat is older than the catalogue's lease_seconds is not taken over. The run stops and a person runs recover after looking. Silent takeover is how two writers end up in one folder.
  2. Work. Tasks are lines in tasks.md, - [ ] verb: text. A reversible verb is executed (here: a line in log.md). A verb listed as irreversible in catalog.json (send, publish, delete, pay) is never executed by the run that meets it. It is appended to decisions.jsonl as pending and the task is marked [?].
  3. Complete. The run compares the project folder against the snapshot it took at acquire. Only files the run itself touched may differ. If anything else changed under its lease, the run stops fail-closed and leaves the lease on disk, so the next person sees who was there. On success it records the changed files with before/after SHA-256 in completions/<run>.jsonl and releases the lease.
  4. Integrate. After the last project, under a separate root lease, the run compiles handover.md from every completion on disk (not only its own) and rewrites situation.md. Project work never touches root files; only the integrator does, one at a time.

A session (session open <actor> <project>) takes the same lease, so runs skip that project until session close. A decision (decide <id> approve|reject <actor>) is executed by a later run that owns the project, and the record names who approved it. A run never executes a decision it proposed itself, even if it was approved in between; the proposing run's id is stored with the decision and checked.

The comparison, measured

python -m runner compare builds the example twice and starts the same three actors at the same moment on each. Two scheduled runs and one person who opens atlas, edits one task text, and saves. The ungated actors do what an ordinary script does: read the task list, work through it, write it back, execute whatever the task says. Then everything is counted from the files, not from what the actors report.

variant                                  | irreversible | executed without | executed after | tasks done | session edit | left in  | still | findings in | decisions
                                         | tasks        | decision         | decision       | twice      | survived     | progress | open  | handover    | pending
ungated                                  | 4            | 8                | 0              | 7-12       | no           | 0        | 0     | 0           | 0
gated, after the first runs              | 4            | 0                | 0              | 0          | yes          | 0        | 0-4   | 5-8         | 3-4
gated, after decisions and 1-2 more runs | 4            | 0                | 3              | 0          | yes          | 0        | 0     | 8           | 0

Reading the ungated row: every one of the four irreversible tasks was executed twice, once per run, with no record of anyone deciding. Between seven and all twelve tasks were done twice, depending on how far the first run got before the second read the file (12 on Linux, 7 on Windows in our runs). The person's edit was overwritten by a run that had read the file before the edit. There is no handover, so the eight findings live only in three log files.

Reading the gated rows: nothing irreversible ran without a decision. Three decisions were approved and executed by a later run; the fourth (delete) was rejected and the task stays marked [?]. The person's edit survived because the runs skipped atlas while the session held it. All eight findings are in handover.md.

The middle row varies between invocations, and the table says so. Which actor wins atlas first depends on thread timing: if the session gets it, both runs skip it and its four tasks stay open until the next run; if a run gets it, the session waits. Both outcomes are the protocol working. The final row is stable; tests/test_compare.py asserts it.

Where the code deviates from the design

docs/design.md was written first. Two things changed while building:

  • The design said a run checks that nothing outside its scope changed. That is wrong when two runs legitimately work different projects at the same time. The check is now: nothing inside the leased scope changed except what this run touched. The root files are protected by the root lease instead.
  • The rule "never execute a decision in the run that proposed it" was first implemented by comparing timestamps. With one-second resolution, a decision proposed and approved within the same second as the next run's start was skipped wrongly; the comparison run caught it (one execution where three were expected). The decision now carries the proposing run's id, and that is what is checked.

What this does not show

  • Real work. Executing a task means appending a line. Executing an approved irreversible task means writing a file to outbox/. The runner is about who may do what and when, not about the doing.
  • A network. One machine, one filesystem. The lease is a file; the atomic create that makes it safe holds for local filesystems and not reliably for network shares.
  • Journalled writes. The per-project lease here is deliberately simpler than a global write journal. That mechanism, with its own comparison run, is in concurrent-file-writes; this repository sits one level above it and does not repeat it.
  • Scheduling. Nothing here starts runs on a timer; the caller does.
  • Model calls. The runs are plain Python. The protocol is what a model driven run would have to obey, and it is testable without one.

Layout

src/runner/
  store.py     catalogue, task lines, decision log, hashes
  lease.py     per-project lease files: acquire, heartbeat, release, recover
  gates.py     propose, decide, record execution
  run.py       the run, the integrator, sessions
  example.py   the invented organisation
  compare.py   the ungated actors, the measuring, the two scripts
  cli.py       init / run / session / decide / recover / status / compare
docs/design.md the design as written before the code
tests/
  test_lease.py    refusal, expiry, keys, recover
  test_run.py      gating, skipping, execution by a later run, fail-closed stop
  test_compare.py  the final row of the table

On AI assistance

This repository was written with AI assistance. The protocol comes from an operating setup I run; the design decisions, the choice of what to measure and the review of the result are mine. The two deviations above came out of running the comparison, not out of getting it right first.

Licence

MIT. See LICENSE.

About

Scheduled runs on a shared folder tree: per-project leases, a decision gate in front of irreversible actions, and a handover for whoever works next. Measured against the same runs without the protocol. Standard library only, 16 tests.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages