twinlane 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. twinlane-0.4.1/CITATION.cff +9 -0
  2. twinlane-0.4.1/DESIGN.md +152 -0
  3. twinlane-0.4.1/LICENSE +21 -0
  4. twinlane-0.4.1/LIMITS.md +141 -0
  5. twinlane-0.4.1/MANIFEST.in +5 -0
  6. twinlane-0.4.1/METHODOLOGY.md +23 -0
  7. twinlane-0.4.1/PKG-INFO +483 -0
  8. twinlane-0.4.1/README.md +468 -0
  9. twinlane-0.4.1/examples/briefs/first-change.md +9 -0
  10. twinlane-0.4.1/examples/briefs/first-review.md +10 -0
  11. twinlane-0.4.1/examples/demo/run.py +78 -0
  12. twinlane-0.4.1/pyproject.toml +27 -0
  13. twinlane-0.4.1/setup.cfg +4 -0
  14. twinlane-0.4.1/skills/twinlane/SKILL.md +116 -0
  15. twinlane-0.4.1/tests/adversarial_check.py +212 -0
  16. twinlane-0.4.1/tests/fake_claude.py +106 -0
  17. twinlane-0.4.1/tests/fake_codex_cli.py +42 -0
  18. twinlane-0.4.1/tests/fake_codex_run.py +97 -0
  19. twinlane-0.4.1/tests/fixtures/app_server_ratelimits.json +10 -0
  20. twinlane-0.4.1/tests/fixtures/app_server_ratelimits_full.json +93 -0
  21. twinlane-0.4.1/tests/fixtures/claude_transcript.jsonl +14 -0
  22. twinlane-0.4.1/tests/fixtures/claude_transcript_usage.jsonl +3 -0
  23. twinlane-0.4.1/tests/fixtures/codex_rollout.jsonl +17 -0
  24. twinlane-0.4.1/tests/fixtures/codex_rollout_current.jsonl +13 -0
  25. twinlane-0.4.1/tests/fixtures/codex_rollout_usage.jsonl +1 -0
  26. twinlane-0.4.1/tests/fixtures/jobs_codex_limit/events.jsonl +2 -0
  27. twinlane-0.4.1/tests/fixtures/jobs_codex_limit/result.txt +2 -0
  28. twinlane-0.4.1/tests/fixtures/jobs_stream_limit.jsonl +4 -0
  29. twinlane-0.4.1/tests/fixtures/jobs_stream_ok.jsonl +5 -0
  30. twinlane-0.4.1/tests/fixtures/oauth_usage.json +8 -0
  31. twinlane-0.4.1/tests/fixtures/rollout_sample.jsonl +4 -0
  32. twinlane-0.4.1/tests/git_fixtures.py +175 -0
  33. twinlane-0.4.1/tests/guard_review_b1.txt +82 -0
  34. twinlane-0.4.1/tests/guard_review_b2.txt +110 -0
  35. twinlane-0.4.1/tests/guard_review_b3.txt +46 -0
  36. twinlane-0.4.1/tests/guard_review_b4.txt +93 -0
  37. twinlane-0.4.1/tests/guard_review_b5.txt +112 -0
  38. twinlane-0.4.1/tests/run_suite.py +547 -0
  39. twinlane-0.4.1/tests/suite_driver.py +105 -0
  40. twinlane-0.4.1/tests/test_admission.py +206 -0
  41. twinlane-0.4.1/tests/test_archive.py +311 -0
  42. twinlane-0.4.1/tests/test_archive_races.py +266 -0
  43. twinlane-0.4.1/tests/test_baton.py +1407 -0
  44. twinlane-0.4.1/tests/test_bestof.py +429 -0
  45. twinlane-0.4.1/tests/test_board.py +1194 -0
  46. twinlane-0.4.1/tests/test_bootstrap_isolation.py +119 -0
  47. twinlane-0.4.1/tests/test_cli_dispatch.py +137 -0
  48. twinlane-0.4.1/tests/test_codex_runner.py +99 -0
  49. twinlane-0.4.1/tests/test_common.py +1918 -0
  50. twinlane-0.4.1/tests/test_coordination.py +844 -0
  51. twinlane-0.4.1/tests/test_detach.py +1072 -0
  52. twinlane-0.4.1/tests/test_docs.py +190 -0
  53. twinlane-0.4.1/tests/test_gc_safety.py +123 -0
  54. twinlane-0.4.1/tests/test_guard.py +815 -0
  55. twinlane-0.4.1/tests/test_guard_parity.py +424 -0
  56. twinlane-0.4.1/tests/test_hook.py +311 -0
  57. twinlane-0.4.1/tests/test_host.py +94 -0
  58. twinlane-0.4.1/tests/test_hostload.py +30 -0
  59. twinlane-0.4.1/tests/test_install.py +398 -0
  60. twinlane-0.4.1/tests/test_jobs.py +3951 -0
  61. twinlane-0.4.1/tests/test_land.py +1007 -0
  62. twinlane-0.4.1/tests/test_meter.py +1693 -0
  63. twinlane-0.4.1/tests/test_meter_stale_ok.py +316 -0
  64. twinlane-0.4.1/tests/test_no_private_strings.py +74 -0
  65. twinlane-0.4.1/tests/test_overload.py +1605 -0
  66. twinlane-0.4.1/tests/test_p0b.py +866 -0
  67. twinlane-0.4.1/tests/test_p0c.py +714 -0
  68. twinlane-0.4.1/tests/test_patrol.py +985 -0
  69. twinlane-0.4.1/tests/test_preserving_codex_start.py +577 -0
  70. twinlane-0.4.1/tests/test_preserving_start.py +630 -0
  71. twinlane-0.4.1/tests/test_process_safety.py +417 -0
  72. twinlane-0.4.1/tests/test_projects.py +200 -0
  73. twinlane-0.4.1/tests/test_public_config.py +58 -0
  74. twinlane-0.4.1/tests/test_resume.py +500 -0
  75. twinlane-0.4.1/tests/test_route.py +474 -0
  76. twinlane-0.4.1/tests/test_run_suite.py +628 -0
  77. twinlane-0.4.1/tests/test_sessions.py +826 -0
  78. twinlane-0.4.1/tests/test_ship.py +596 -0
  79. twinlane-0.4.1/tests/test_slot_lock_wait.py +363 -0
  80. twinlane-0.4.1/tests/test_strict_windows_detach.py +49 -0
  81. twinlane-0.4.1/tests/test_support.py +82 -0
  82. twinlane-0.4.1/tests/test_test_support.py +64 -0
  83. twinlane-0.4.1/tests/test_wip.py +416 -0
  84. twinlane-0.4.1/tests/test_worktrees.py +460 -0
  85. twinlane-0.4.1/tests/test_writer_admission.py +1463 -0
  86. twinlane-0.4.1/tests/test_writer_explain_cli.py +176 -0
  87. twinlane-0.4.1/twinlane/__init__.py +8 -0
  88. twinlane-0.4.1/twinlane/__main__.py +4 -0
  89. twinlane-0.4.1/twinlane/archive.py +332 -0
  90. twinlane-0.4.1/twinlane/baton.py +2371 -0
  91. twinlane-0.4.1/twinlane/bestof.py +807 -0
  92. twinlane-0.4.1/twinlane/board.py +1138 -0
  93. twinlane-0.4.1/twinlane/capacity.py +40 -0
  94. twinlane-0.4.1/twinlane/cli.py +205 -0
  95. twinlane-0.4.1/twinlane/codex_runner.py +161 -0
  96. twinlane-0.4.1/twinlane/common.py +2568 -0
  97. twinlane-0.4.1/twinlane/context.py +116 -0
  98. twinlane-0.4.1/twinlane/coordination.py +439 -0
  99. twinlane-0.4.1/twinlane/envhandoff.py +296 -0
  100. twinlane-0.4.1/twinlane/guard.py +2599 -0
  101. twinlane-0.4.1/twinlane/hook.py +269 -0
  102. twinlane-0.4.1/twinlane/host.py +106 -0
  103. twinlane-0.4.1/twinlane/hostload.py +406 -0
  104. twinlane-0.4.1/twinlane/inbox.py +211 -0
  105. twinlane-0.4.1/twinlane/install.py +572 -0
  106. twinlane-0.4.1/twinlane/jobs.py +4450 -0
  107. twinlane-0.4.1/twinlane/land.py +1298 -0
  108. twinlane-0.4.1/twinlane/lanes.py +241 -0
  109. twinlane-0.4.1/twinlane/meter.py +1547 -0
  110. twinlane-0.4.1/twinlane/observations.py +136 -0
  111. twinlane-0.4.1/twinlane/patrol.py +652 -0
  112. twinlane-0.4.1/twinlane/policy.json +67 -0
  113. twinlane-0.4.1/twinlane/projects.py +295 -0
  114. twinlane-0.4.1/twinlane/resume.py +333 -0
  115. twinlane-0.4.1/twinlane/route.py +205 -0
  116. twinlane-0.4.1/twinlane/sessions.py +715 -0
  117. twinlane-0.4.1/twinlane/ship.py +1004 -0
  118. twinlane-0.4.1/twinlane/skill/SKILL.md +136 -0
  119. twinlane-0.4.1/twinlane/watch.py +163 -0
  120. twinlane-0.4.1/twinlane/watchdog.py +107 -0
  121. twinlane-0.4.1/twinlane/wip.py +526 -0
  122. twinlane-0.4.1/twinlane/worktrees.py +1141 -0
  123. twinlane-0.4.1/twinlane/writer_admission.py +781 -0
  124. twinlane-0.4.1/twinlane.egg-info/PKG-INFO +483 -0
  125. twinlane-0.4.1/twinlane.egg-info/SOURCES.txt +126 -0
  126. twinlane-0.4.1/twinlane.egg-info/dependency_links.txt +1 -0
  127. twinlane-0.4.1/twinlane.egg-info/entry_points.txt +2 -0
  128. twinlane-0.4.1/twinlane.egg-info/top_level.txt +1 -0
@@ -0,0 +1,9 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use twinlane, please cite this release."
3
+ title: "twinlane"
4
+ version: 0.4.1
5
+ date-released: 2026-10-27
6
+ authors:
7
+ - name: twinlane contributors
8
+ repository-code: https://github.com/mmelqadi/twinlane
9
+ license: MIT
@@ -0,0 +1,152 @@
1
+ # Design
2
+
3
+ ## Observation cost and read-only status
4
+
5
+ Each command/poll shares one invocation-local job index from a single directory scan.
6
+ Specs and writer identities are resolved once per distinct job/path and released at the
7
+ end of the invocation. Lane activity is indexed by baton and writer once, with lifecycle
8
+ mtimes checked before opening old specs. No age filter weakens live-worker custody.
9
+ Bound transcript lookup/context estimates and session provenance are shared within a poll.
10
+ Status dispatches before directory initialization and uses only cached meter evidence;
11
+ effective job state is reported separately from the persisted receipt status.
12
+
13
+ ## Waiting, owners and wake delivery
14
+
15
+ Batons store `waiting_on` (`lead`, `user`, `job:<id>`, `lane:<slug>`, `none`) and
16
+ `waiting_since`, an ISO timestamp changed only when the dependency changes. Legacy
17
+ batons default to `none`. Job ids and lane slugs use the existing validators. A user wait
18
+ reuses the yes/no `--ask` and inbox path; answers are evidence, never execution authority.
19
+ Board reads rank lead/user waits oldest first and count observed idle lanes without writes.
20
+
21
+ `run --owner SLUG` validates an existing baton before allocation, merges the new job into
22
+ its job index and sets its dependency. Every terminal receipt persistence path notifies the
23
+ owner. Under the baton lock, completion records `job_done` with id, state, exit code and
24
+ receipt path. It changes a matching job dependency (or `none`) to its bound lane, otherwise
25
+ lead. Other dependencies are preserved. A per-job notification marker and history check
26
+ make repeated finalization idempotent; notification failures log without changing the job.
27
+ Before publishing a terminal receipt, the runner writes `owner-pending.json` with the
28
+ owner slug. Completion writes `owner-notified.json` and removes the pending intent.
29
+ Runner re-entry, wait, result and the writable inbox CLI repair pending completions;
30
+ metadata-preserving re-entry still refuses terminal-job mutation. Status, board and watch
31
+ never repair. Collection discovers per-job notices independently of the capped job index.
32
+ WIP admission is acquired before baton locks. Collection notices compare the dependency
33
+ and its timestamp with the completion marker and the existing result-collected marker.
34
+
35
+ `watch` reads finalized receipts, baton/history snapshots, cached meter windows and bounded
36
+ tails of identifiable bound sessions. There are no per-poll subprocesses, network requests,
37
+ model calls or refreshes. It watches Claude 5h/7d and Codex weekly 80%/85% crossings and
38
+ estimated bound context crossing 70%; missing estimates are skipped. Meter thresholds use
39
+ a high-water value per reset window, so decreases do not trigger or re-arm a wake.
40
+ Each poll sees only the latest meter/context observation. A threshold excursion that
41
+ rises and falls between observations is not detected or reconstructed. Context crossing
42
+ keys include the baton slug and session id, so shared sessions wake each bound lane.
43
+
44
+ Each validated watcher name owns `state/watch-<name>.json`. On startup it delivers retained
45
+ events absent from that cursor, including events predating the first invocation. Names
46
+ are independent. Output is printed and flushed before the atomic cursor write: a crash
47
+ can replay output, so delivery is at least once. No-event polls persist observation
48
+ baselines but acknowledge no events. Only the cursor file is written. Retention is bounded
49
+ by available receipts and baton history; this is a local wake mechanism, not an event broker.
50
+ Coordinating sessions run watch in the background and re-arm after each wake; worker guards
51
+ refuse it before cursor access. Defaults are a 5-second interval and a 3300-second timeout.
52
+
53
+ `capacity.py` provides pure weekly-pace advice: percent used divided by elapsed fraction
54
+ of a seven-day window. Only a measured lower Codex pace advises Codex first; ties and
55
+ unknown readings advise Claude first with the reason, naming missing meters. This line
56
+ is advisory and never replaces policy admission.
57
+
58
+ ## Lanes, human questions and checkpoints
59
+
60
+ `wip.py` merges jobs, batons and recent interactive sessions by `common.writer_key`, the checkout toplevel.
61
+ Count and creation are serialized under the WIP lock. Writer rows without a known identity retain their lane.
62
+ The versioned `state/wip-queue.json` contains retry argv, never prompt bodies or approvals. A next operation
63
+ prints the retry command; it never dispatches and leaves the queue intact while full unless forced.
64
+
65
+ Jobs count as admission classifies them (`write`, or any profile but `intel`). A job releases its lane
66
+ only when its receipt is terminal and `jobs._writer_job_busy(id)` is false, the writer-directory
67
+ admission rule: a provably dead engine frees it even when the runner's identity is unreadable and its
68
+ status file is unfinished. A just-admitted job (no pid, no receipt) and a row without an id retain their lane.
69
+
70
+ Setting `wip.claims_command` to a non-empty argv list selects claims mode: writer jobs and claimed
71
+ worktrees count; batons and activity-window sessions do not. Because a baton holds no lane there, baton
72
+ commands (`baton new|set|snap`, `handoff`) skip writer admission and never run the provider. A null or empty
73
+ command keeps the default activity mode. Claims merge by `common.writer_key` and respect `wip.count_main_checkout`.
74
+
75
+ The provider prints `{"version": 1, "claims": [{"worktree": "<absolute path>", "id": "<opaque string>",
76
+ "age_s": null}]}`. Ages may also be finite numbers in seconds. Collection runs the argv
77
+ without a shell, stdin or pipes: stdout goes to a temporary file read up to 1 MiB (`output too large`
78
+ beyond), stderr is discarded, and on timeout the provider's process tree is killed with a bounded wait, so
79
+ a descendant holding inherited handles cannot stall admission under the WIP lock. Claims are fetched once
80
+ per command observation.
81
+
82
+ `wip.claims_timeout_s` defaults to 10. Spawn errors, timeouts, nonzero exits and invalid JSON, versions,
83
+ claim shapes or paths fail closed with `writer custody unknown: claims provider failed: <reason>`,
84
+ which names the config file to remove `wip.claims_command` from. Only writer admission fails: `board`
85
+ and `status` show the writer count as `?` with a signal, and read-only (intel) runs never enter admission.
86
+ Reasons are short fixed labels; provider output and arguments are never included in the error.
87
+
88
+ `lanes.py` derives stale from progress, local git activity, job activity and bound sessions. Mechanical
89
+ snapshots preserve the progress clock. Parked batons release their slot until explicitly resumed; after the
90
+ park timeout the board asks whether to close or resume. Only an explicit human/lead close writes closed.
91
+ Landing records landed, the count and verified status outside the integrator lock, including unverified landings.
92
+
93
+ The inbox ranks blocking questions, waiting questions and legacy approvals, collapsing duplicate legacy text.
94
+ Its header reports every hidden item. Answers take only the baton lock and append to `holds.answers`, never
95
+ `approvals_granted`; an inbox yes is never a live-write approval. ATTENTION contains inbox and stale work.
96
+ SIGNALS retains the P0 usage and process warnings. Board collection takes no locks and writes no state.
97
+
98
+ Supervisor discovery is memoized and bounded; failure falls back to transcript observations. Notification,
99
+ Stop and Codex notify write atomic local lane-state files, never contact a service and never store Codex
100
+ message bodies. Supervisor observations take precedence over hook state. Provenance never confers authority.
101
+
102
+ `wt prune --merged-only` and its thin `wt gc` alias retain shallow/deep blockers, interactive usage,
103
+ locks and immediate rechecks. Unique patches plus unique merge commits keep a branch; landed ledger tips
104
+ can identify squash landings. The merged minimum age defaults to one hour; `wt.behind_max` defaults to 50.
105
+ Removal unlinks dependencies first, restores links if a folder remains, and uses `git branch -d` only.
106
+ Worktree creation is gated by WIP. Configured dependencies must be ready and links resolve within allowed roots.
107
+ The optional post-land archive applies these same checks to one just-landed path, outside the integrator lock.
108
+
109
+ `context.py` uses the latest per-request usage, never cumulative billing totals. Window precedence is exact
110
+ model policy, engine policy, then the Codex rollout window. Every display carries `~` and `estimate: true`;
111
+ missing denominators remain unknown. PreCompact binds by session id, checkpoints up to three batons with
112
+ bounded lock time, preserves `progress_at`, and never guesses by cwd. Worker callbacks return first.
113
+ Install previews a redacted unified settings diff, preserves unrelated keys/events and requires `--yes`.
114
+ Codex notify configuration is printed for manual installation, never edited.
115
+
116
+ twinlane keeps its state under a configurable data directory. `state/` stores cached usage and locks, `jobs/` stores per-attempt inputs and receipts, `batons/` stores task context, and `bin/` is reserved for local helpers. The packaged policy is merged with a user JSON override at read time.
117
+
118
+ `meter.py` normalizes Claude OAuth usage and Codex app-server limits. Each reading carries source and age; missing evidence remains unknown. `route.py` is a pure decision function over a task kind, meter snapshot, and policy. `jobs.py` launches workers, records JSON events and final text, and tries the other engine only after a usage-limit result. `codex_runner.py` calls `codex exec --json` directly and writes the adapter evidence contract.
119
+
120
+ `baton.py` records goals, worktree state, decisions, holds, and next actions. `board.py` reads cached records without starting workers. `sessions.py` discovers vendor session files for forwarding. `hook.py` warns at the limit boundary; `guard.py` checks tool calls inside Claude workers. The advisory hook fails open, so a hook failure leaves the user's prompt usable. The guard fails closed inside a job: if it cannot load it exits 2 (a blocking PreToolUse result), and a scan that cannot finish denies. It is a heuristic over command text, not a sandbox, and an obfuscated command can still pass, so the worker's permission profile and the Codex sandbox remain necessary.
121
+
122
+ Liveness is three-valued: `common.pid_state` returns alive, dead or unknown, and a process whose creation time cannot be read is unknown, never lost. On Windows a process this token cannot open (for example a SYSTEM service that now holds a recycled pid) has its creation time read from a `NtQuerySystemInformation` process snapshot, trusted only when the same snapshot gives the current process the creation time `GetProcessTimes` gives it; a different time is dead, and without a trusted snapshot it stays unknown. A job keeps a `child-starting.json` marker from just before its engine starts until the engine is reaped, and a `survivor.json` when an engine outlives its kill, so a crash between those writes and the receipt still reads `orphaned` or `unknown`. On Windows, `--bg` runners start through WMI with the caller's environment handed over in an owner-only file, and `--bg` is refused when that start is impossible; POSIX runners use a new session. Windows tree kills verify creation times and terminate through the same pinned handles; snapshot parsing bounds the full fixed header and declared thread payload within each entry, and query growth is bounded.
123
+
124
+ Windows descendant membership uses each child's handle to read its recorded parent PID and monotonic process sequence number (`NtQueryInformationProcess`, classes 0 and 92), requiring the sequence to exceed its verified parent's while retaining all ancestor handles until termination. An orphan predating a reused parent PID fails this rule regardless of wall-clock rollback or recovery; creation FILETIMEs remain identity checks, never ancestry ordering evidence. An unavailable sequence query, mismatched parent, or inaccessible intermediate refuses that branch and returns `False` after attempting only the verified root and independently proven branches.
125
+
126
+ A job's `result.json` has one writer: the holder of the runner role (`runner.lock`, taken with O_EXCL; a lock naming a dead process is stale). `twinlane stop` writes `stop.requested` and kills the recorded tree, but writes `stopped` only after it takes the runner role itself, which a live runner never yields; it re-reads the receipt inside the role and keeps any terminal receipt. While a runner may still be alive, stop exits 4 (`stop_requested`) and that runner records the outcome itself: `stopped`, or `done` when the work had already finished. A background refusal takes the same role, and a runner that starts later finds the receipt and exits.
127
+
128
+ `jobs.codex_credit_refusal` runs before every Codex attempt and refuses paid credits for every model unless `codex.allow_credits` is true; a write job also reserves its worktree so two writers cannot share one.
129
+
130
+ A stale writer claim with a terminal receipt can be released when the runner and recorded child are provably DEAD, without attempt exit evidence. For an existing process whose creation identity is unreadable, the PID-reuse exception instead requires a DEAD runner and a `done` or `failed` receipt with a nonempty attempts list: the terminal final attempt has a matching integer exit code, a nonempty post-wait events path, no survivor, and an integer `n` equal to the recorded `status.json` `attempt`. Engines must match when both are present. Missing attempt numbers, unknown existence, a matching live child, survivors and launch markers block this exception. The exit evidence covers only the recorded child; descendants are covered by the survivor marker. Admission holds `writers.lock`, rechecks a pending status repair under `status.lock`, and refuses if that recheck is rejected. Repair returns `repaired`, `already-final` or `rejected`; unsuccessful repair never grants writer clearance.
131
+
132
+ ## Project commands and the trust boundary
133
+
134
+ `projects.py` resolves a repository to its main checkout (the first `git worktree list` entry, confirmed by asking that checkout which worktrees it owns, because a linked worktree's `.git` file is writable by whoever works in it). Settings for `land`, `ship`, `wt` and `test` come only from the `projects` map of the user config, keyed by that path, and never from repository content. A worker in a checkout therefore cannot rewrite the gate, land command or ship command that judges its own work. Commands are argv lists with named placeholders replaced in one pass; there is no shell, unknown keys and placeholders are errors, and values with line breaks are refused. A `.bat` or `.cmd` target receives only plain values, because `cmd.exe` re-parses its arguments. State for each project lives under `state/<feature>/<repo_key>/`.
135
+
136
+ ## Land queue and lock
137
+
138
+ `land.py` keeps `queue.json`, `holder.json` and an append-only `ledger.jsonl` per project, and serialises every read-modify-write behind one OS file lock. The integrator lock is a lease held by the running `land run` process (a token plus a heartbeat). Staleness alone never takes it: a live holder is respected, a provably dead one is broken only by an explicit `--break --reason`, and an unjudgeable one (another host, an unreadable process identity) is never broken. Landing is fast-forward only; the gate runs on the tree that will land, and a gate that changes `HEAD` or the tree is refused. The ledger records each landing's exact range, which is what `ship plan` later reads. A file that exists but cannot be parsed is an error, never an empty queue, so a write cannot silently drop entries.
139
+
140
+ ## Ship manifests and receipts
141
+
142
+ `ship plan` hashes the bytes a checkout of the source commit would produce (git attributes are read from that commit) and records files, deletions, commits, excluded commits and flags for foreign and superseded changes. The manifest is accepted by `ship run` only in the exact canonical bytes `plan` writes, and `run` works on a private copy keyed by its sha256. `ship run` is a dry run that writes a receipt; `--execute` crosses a different boundary than the plan: it needs a fresh, unused receipt for the same manifest hash and command, a confirm code (the first 8 hex digits of the hash), a manifest that still matches the repository, and a re-hash just before the command starts. twinlane runs only the configured command; it has no deploy logic of its own.
143
+
144
+ ## Prune safety
145
+
146
+ `wt prune` is a dry run first. It keeps a worktree if anything uses it, if it is dirty, if its branch is not merged, if it holds submodules or links it cannot classify, or if it is itself a link. Junctions and symlinks inside a worktree are unlinked first, after recording their targets and without following them, because some Windows Git builds descend into a junction and delete what it points at; a link that will not go, or a vanished target outside the worktree, stops the prune. Branches are deleted with `git branch -d`, never `-D`, so git itself refuses an unmerged branch. Each action is appended to `prune.jsonl`.
147
+
148
+ ## Resume
149
+
150
+ `resume.py` re-runs a lost job's request as a continuation. It never runs on its own, selects only jobs whose status is exactly `lost`, and stops at the first refusal. `host.py` compares the job's start with the machine's boot time to name a restart as the likely cause; a missing boot time gives `unknown`, never a guess. A `resumed.json` marker prevents a second resume.
151
+
152
+ Exit codes: 0 success, 2 command usage, 3 refused, 4 still running, 5 failed, 6 usage limit, 127 missing executable.
twinlane-0.4.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammed Elqadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,141 @@
1
+ # Limits
2
+
3
+ - Job admission lock contention waits up to `jobs.lock_wait_s` (default 1800 seconds / 30 minutes),
4
+ with jitter and `waiting for slot lock` / `waiting for writer lock` in status and the board.
5
+ Expiry reports how long the lock stayed busy and the last observed holder PID and age, if available.
6
+ One bound covers a whole streak of lock waits and restarted scans (restarts do not reset it); after three restarted scans in a row the next attempt probes inside the lock so the oldest job always decides. The lead command `release_stale_writers` (`wt doctor --fix`, patrol) still fails after 30 s.
7
+ Holder sidecars are best-effort diagnostics, never grounds to remove a lock. OS locks still release
8
+ when the holder dies. Slot admission probes processes outside the lock and rereads admission files
9
+ inside it; new identities cause a retry. Ordinary non-admission lock timeouts remain unchanged.
10
+
11
+ - Codex meter timeouts or closed output/no reply get one additional app-server exchange with a 30 s timeout (`APP_SERVER_RETRY_TIMEOUT_S`), before the rollout fallback; the default exchange budget is 15 + 30 s, plus bounded process cleanup. Missing binaries, protocol errors and unsupported requests are not retried. Only dispatch admission (`route`, `run`, best-of and start/failover checks) can use the last genuine `app-server` or `rollout` Codex section after an unavailable live read. Its original `reading_at` must be younger than `meter.stale_ok_max_age_s` (default 1800 s / 30 minutes), capped by `codex.stale_reading_max_age_s`. Every counted window needs a known percentage and a future reset; the section must be `ok` or `caution` under current thresholds and never on credits. Missing, unreadable, malformed, expired, reset, handoff/stop/out/unknown or previously carried sections are refused as before. A carried section is labelled `meter stale-ok (age Nm)` with `stale_ok` and `stale_ok_age_s`; it never refreshes its original stamp or replaces `state/meter.json`. Actual failed observations are stored separately in `state/meter-codex-failure.json`, so explicit meter commands and the board show the failure with only a labelled fallback-availability note. The 120 s cache TTL and `--fresh` semantics are unchanged. Claude's OAuth meter has no stale-ok fallback; its failures have a different cause. Post-job usage observations also never substitute stale-ok evidence.
12
+
13
+ - The writer cap coordinates one twinlane home on one machine. It counts distinct worktrees, not model calls or processes. Interactive activity comes from recent transcript markers and is an estimate; unreadable live job metadata occupies its own lane. Starting a CLI outside twinlane is not mechanically prevented. Queue entries contain metadata, never executable prompts, and `wip next` grants no slot or authority. `wip pump` claims a request (`pumping_at`) before it starts and the request leaves the queue only when the job of the run it started exists on disk; a claim older than five minutes is left for a human (`wip next`), never relaunched blind. A run started inside a job object that ends its children can therefore die with its parent; its request stays claimed rather than being lost. If a stall kill does not report that the whole tree is gone, the job is not retried and its worktree claim is held by `tree-unresolved.json` until a person confirms with `twinlane stop JOB --tree-gone`; twinlane cannot tell which processes survived and kills none of them from that marker. On Windows each runner attempt starts its engine suspended, assigns a kill-on-close Job Object and then resumes it; zero ActiveProcesses after TerminateJobObject is the only confirmation. Descendants stay contained even after an intermediate parent exits. Escaping descendants remain a limit for callers without that job handle (cross-process stop, or assignment failure such as forbidden nesting): snapshot cleanup is best effort and reports tree exit unconfirmed. An unconfirmed runner kill retains custody. The attempt handle closes after the result is read; first the job is terminated and must report zero active processes, otherwise (or if an interrupt cuts the drain short) the job keeps its worktree claim (`tree-unresolved.json`, or the pid-less launch marker when that cannot be written) like an unconfirmed stall kill. If the job object cannot be assigned (for example a host that forbids nesting), the attempt still runs but nothing can prove its descendants gone, so unresolved-tree custody is written BEFORE the engine is resumed (a killed runner or a `stop` cannot lose it) and every such attempt ends with the claim held until a person confirms; if that marker cannot be written the engine is not started; an unresolved tree also stops a usage-limit failover from starting a second engine. On macOS and Linux no stall kill is ever reported confirmed (the packaged Codex runner starts `codex exec` in its own session, and a Claude worker may start a helper in its own session): the process group is still killed, but the job is not retried and the claim is held until a person runs `twinlane stop JOB --tree-gone`, so the automatic stall requeue only works on Windows. It re-runs a queued `run` command exactly as typed (it holds the path of the brief, not its text) through full admission again, only for a request younger than a day whose brief file still exists.
14
+ - Host load is advisory. CPU is averaged over `hostload.cpu_window_s` (default 20 s), so a spike shorter than that cannot hold the queue, and a real overload shorter than that is noticed late. It is read from CPU, free-RAM and process counters; desktop-heap exhaustion has no cheap counter, so it shows up only as a stalled worker, which the watchdog handles. The watchdog judges progress by file changes, so a worker that is busy but silent for longer than `watchdog.stall_s` is killed and requeued once.
15
+ - Stale is an inactivity projection, not proof of completion. Batons are never auto-closed; landing marks landed and explicit closure remains a human/lead action. Inbox answers are retained as context on that baton and never transfer live-write authority across sessions or engines. A lane resumes only through fresh admission.
16
+ - Context is an estimate from the latest request record in a bounded transcript tail. Claude input and cache tokens are summed; Codex uses `last_token_usage`, never cumulative totals. Missing usage or a missing model-window configuration stays unknown. The packaged context-window map is intentionally empty; verified Codex event windows can supply the denominator. The 70/85 flags are advisory and do not stop a model.
17
+ - `startedBy` is explicit recorded provenance; missing provenance stays unknown. `UNMANAGED` is a recent session outside direct job receipts, explicit baton bindings and the configured lead family. It does not imply that the session is malicious or still running. Child sessions are visible and are not automatically trusted because their parent is tracked.
18
+ - `wt gc` is a dry run by default and uses prune's existing ownership, age, lock, link and recheck protections. A zero-unique-patch result permits cleanup of a cherry-picked equivalent branch; its branch ref is kept if `git branch -d` refuses deletion. Post-land archive defaults to a suggestion; apply is an explicit user-config choice and retains prune safety. Configured dependency checks cannot prove that an application runs correctly.
19
+ - PreCompact snaps up to three batons explicitly bound by session id. A matching cwd alone grants no binding. It never calls a model, refreshes usage, creates a Git checkpoint or installs itself. The advisory hook remains fail-open if snapshotting fails.
20
+
21
+ - Claude's OAuth usage endpoint is undocumented and may change. The meter sends the locally stored token only to `api.anthropic.com` and refuses redirects. Unknown or stale readings block dispatch for that engine.
22
+ - Codex usage comes from its app-server interface and local session evidence. These formats may change; unknown readings stay unknown.
23
+ - Served-model reporting differs by vendor. A Codex receipt can only show the requested model when the CLI does not report the served one.
24
+ - Windows has the most complete detach behaviour. POSIX uses a new process session and process-group termination; Windows job-object details are unavailable there.
25
+ - On Windows, `--bg` runners start through WMI. Surviving a desktop app quit was shown with real quit-and-reopen tests on one tested Windows machine and build; that is not a general guarantee. Sign-out, shutdown, sleep and reboot are untested, and survival says nothing about whether the task succeeds.
26
+ - A job whose runner cannot be identified (for example from inside a restricted-token sandbox) reads `unknown`; twinlane will not stop it, so check it from the host.
27
+ - The Codex app-server meter currently finds only the Windows Codex desktop app. It copies the app-server build into `<home>/bin`, keeping at most 3 copies of about 434 MB each (about 1.3 GB total). On Linux/macOS, set `TWINLANE_CODEX_BIN` or rely on rollout files; otherwise the Codex meter reads **unknown**, and parallel/sweep routes fail safe to Claude or refuse.
28
+ - When the Codex app-server exchange fails, the meter falls back to the newest local rollout reading. `--no-rollout` or `meter.codex_rollout_fallback: false` turns that off; Codex then reads **unknown**.
29
+ - `twinlane install` registers an advisory hook for interactive sessions. The blocking guard only acts inside twinlane-dispatched workers; interactive sessions are NOT guarded. The guard is a heuristic over shell text, not a sandbox: it does not read script files, and an obfuscated command can still pass. Code workers must still run in worktrees with the vendor's own permissions and sandbox.
30
+ - Built and tested against the `codex exec --json` interface (flags `--json --model --config model_reasoning_effort=... --sandbox --cd --output-last-message`); older Codex CLI releases may not accept current model names; run `codex --version` and set your models in config.
31
+ - Model ids are defaults as of 2026-10; set your own in config.
32
+ - Credits and extra usage are checked only when a job starts. A long Codex job can still reach paid credits if your account allows it; twinlane passes no option that enables credits. Nothing in twinlane stops a running Codex job that crosses into credits partway. Check the receipt and meter before continuing.
33
+ - Removing secret-named environment variables from code workers does not remove secrets a script reads from files such as a repository `.env`. It also removes `CLAUDE_CODE_OAUTH_TOKEN`, so a Claude code worker on a setup that logs in only through that variable fails to authenticate; intel workers keep it.
34
+ - Outside-path checks fail closed: a `..` or home path named by any command that is not on the guard's short read-only list is denied, even when it is only read (`python ../tools/x.py`, `sort ../x > out.txt`); read or run it from a read-only command or from inside the worktree. A command is read-only per call: one with a write or run option is read-only only without it (`git` read subcommands without `--output` or `grep -O`, `find` without actions, `fd` without `-x`/`-X`, `rg` without `--pre`, `xxd` with one file operand, `tree` without `-o`/`-R`, `file` without `-C`), and `less`, `ack` and `ag` are not read-only at all. A path the shell computes when it runs (`$(...)`, backticks, `${X:-..}`, `${PWD%/*}`) in a command that is not read-only is denied, and so is a brace expansion over 256 words; bash `$'...'` quoting is decoded first. Code jobs allow only known tools: MCP tools, a write or shell call without a known job worktree, and a call without a string path or command are denied. What still passes: an ABSOLUTE path given to a command the guard does not know as a writer, outside an output option (`chmod 777 /abs/x`); a target taken from a config file or an environment variable; a plain `$VAR` given to a command the guard does not know as a writer; and anything a script file does. Codex workers also run under Codex's own `workspace-write` sandbox; Claude workers rely on the guard plus Claude Code's own permissions.
35
+ - Archive extractors are allowlisted: tar and bsdtar run only with options known to keep member names inside the destination (unknown options, `-P`, `--absolute-names`, `--transform`, `-s` and command hooks are denied in any getopt abbreviation), unzip only with plain letter options (no `-:`/`-^`), 7z without `-spf`/`-snl`/`-snh`; extraction by `cpio`, `jar`, `pax -r` and `python -m tarfile -e` is denied. A tar `-T`/`--files-from` list is denied unless `--verbatim-files-from` comes first, because GNU tar reads its lines as options. Setting `TAR_OPTIONS`, `TAR_READER_OPTIONS`, `TAR_WRITER_OPTIONS`, `TAPE`, `UNZIP`, `UNZIPOPT`, `ZIPOPT` or `ZIP` is denied in any syntax the guard reads, including names joined from quotes, escapes or `+`; an uppercase `UNZIP`, `ZIP` or `TAPE` anywhere in a command is refused too (`grep UNZIP` needs `grep -i unzip`); and `export`, `declare`, `env`, `setx`, `Set-Item env:` or `SetEnvironmentVariable` with a variable NAME built at run time (`export "$n=x"`, `$'..'`, `[char]`) is denied, because the name cannot be read. `git fetch`, `git clone` and `git ls-remote` may contact any host, so the URL itself can carry data out.
36
+ - After a timeout or interrupt kill, twinlane checks the process it started. On macOS and Linux the packaged Codex runner starts `codex exec` in its own session, so a Codex engine that outlives its runner is not detected; on Windows the attempt Job Object contains the tree; if assignment fails, verified snapshot members are killed best effort and tree exit stays unconfirmed.
37
+ - The guard hook runs with the Python interpreter recorded at install time. If that interpreter is removed (for example a rebuilt virtual environment), the hook cannot start and Claude Code does not block the call; rerun `twinlane install`.
38
+ - The guard is a deny-list and defense in depth, not a sandbox, and a deny-list cannot be complete: a new tool, an unmodelled spelling or a script file can pass. Real confinement comes from elsewhere: workers that hold no push or deploy credentials, a worker that runs only in its own worktree, and a lead who reviews every diff and reruns the gates. Treat a guard denial as a guardrail, and a guard pass as no evidence of safety. `guard.deny_commands` and `guard.deny_text` add your own rules; they do not make the guard complete.
39
+ - The guard acts only inside twinlane-dispatched workers. A `guard` policy section that is not an object, or cannot be read, denies every call of the job (fail closed); a typo in `deny_text` therefore stops the job instead of weakening it.
40
+ - `twinlane land` serialises landings through one lock per repository and lands fast-forward only. It trusts the project's gate to be a real check; with no `land.gate` it lands ungated and says so. A holder whose liveness cannot be judged (another host, an unreadable process identity) is never broken, so such a lock needs a person to look at it. The queue and ledger are local files: they coordinate sessions on one machine's state directory, not machines.
41
+ - `twinlane ship` never deploys by itself. It writes a manifest and runs only the command you configured in your user config, after a fresh dry-run receipt and a confirm code. It cannot tell whether that command is safe, and the manifest lists files and commits, not whether the release is correct. Manifest hashes are computed on the machine that plans, so line-ending or filter settings can make them differ across machines.
42
+ - `twinlane wt prune` never deletes an unmerged branch (`git branch -d`, never `-D`) and keeps any worktree that is dirty, in use, linked or unclassifiable; the cost is that some stale worktrees stay until you remove them yourself. Link handling covers Windows junctions and symlinks; an unusual reparse point is kept, not removed.
43
+ - `twinlane resume` continues only jobs whose status is exactly `lost`, only on request, once each. A continuation is a new attempt with the original brief and any recorded context; it does not restore a running conversation or undo partial changes. The restart cause is advisory: it compares times and can be `unknown`.
44
+ - `run --best-of` spends one run of usage per contestant and never fails over between engines. `compare` shows evidence; it does not choose a winner or merge anything.
45
+ - Per-project commands run with your user's rights and no shell. twinlane refuses a string command, an unknown placeholder, a value with a line break and, for `.bat`/`.cmd` targets, any value `cmd.exe` would interpret, but it does not sandbox the command itself.
46
+
47
+ - WIP sees only jobs, batons and local supervisor/transcript activity in this home. Sessions with no observable activity are invisible. Transcript activity uses file times; `wip.count_main_checkout` controls main-checkout lanes.
48
+ - An inbox yes is never a live-write approval. Answers are recorded separately from grants. Context numbers labelled `~` are estimates from the newest bounded usage record.
49
+ - Merged-only pruning keeps a one-hour minimum age except the optional archive of a just-landed worktree. It never deletes an unmerged branch.
50
+ - A landed-unverified range is never shippable. Re-gating is possible only when the lane worktree still sits on the landed commit. twinlane never deploys by itself.
51
+
52
+ - `twinlane patrol` is a file-only, one-pass report by default (exit 0 even with findings).
53
+ It reads cached engine meters and host-load samples without refreshing them, and writes only
54
+ `state/patrol.json` and `state/patrol.jsonl`. The cache-only host reader never measures or spawns.
55
+ Missing, older-than-five-minute or unusable host counters mean host status `unknown`: the overload
56
+ count is retained and an existing overload finding stays stale until usable evidence returns.
57
+ Liveness observations never spawn `ps`: POSIX uses `/proc` or signal zero, keeping unreadable
58
+ identities `unknown` and conservatively counted as running. This also applies to the file-only board.
59
+ Custody snapshots take no lock and create no file; only a missing folder is complete and empty.
60
+ Unreadable folders or claim files are incomplete evidence: `patrol_error` retains prior custody
61
+ findings as stale, without authorizing release. Actual release still locks and revalidates claims.
62
+ No model is called.
63
+ `--fix` permits only `wip.pump_quietly()` and `jobs.release_stale_writers()`; pumping can launch stored
64
+ requests through full admission. Engine workers may read patrol, but cannot run `patrol --fix`.
65
+ All other findings are report only: unresolved trees require a person to verify they are gone before
66
+ `twinlane stop JOB --tree-gone`; lost jobs point at `twinlane resume` and are never auto-resumed.
67
+ `queue_ready` reports engine and host eligibility only; file-only readers cannot verify writer capacity.
68
+ Only a confirmed `--fix` pump launch produces `queue_stalled`; a refusal is `queue_waiting` with the
69
+ pump's own reason, retained on subsequent reports until evidence changes. `queue_claimed` is never
70
+ replayed by patrol; a claim older than ten minutes (or with an unreadable timestamp) needs a person.
71
+ A claim missing its timestamp defers the whole pump, since its replay guard cannot recognize it.
72
+ Slot-leak checking is skipped: slots are derived from job liveness, with no separate cheap lease reader.
73
+ `worktrees.doctor` is skipped because repository and dependency probes exceed the file-only pass budget.
74
+ Each collector has a bounded wait within a roughly ten-second pass; slow or failed collectors become
75
+ `patrol_error`, and unknown evidence does not resolve earlier findings. An overdue read is not duplicated
76
+ on the next pass. Remedies check the pass deadline before making changes; an interrupted pump can leave
77
+ a claimed request for manual inspection. The existing remedies retain their own admission and custody rules.
78
+ `--loop SECONDS` requires at least 30 seconds; `--max-runs N` bounds the number of passes.
79
+ `--quiet` suppresses unchanged output; opened, resolved and hourly reminder findings are printed.
80
+ JSON always includes every current finding and its `new` flag. Ages and changing host measurements
81
+ (`detail`) are separate from stable condition identity, preserving quiet output and hourly reminders.
82
+ History retains the last 2000 opened/resolved/fixed events. Unreadable patrol state freezes
83
+ state/history writes and fixes until it can be read or repaired; prior evidence is not overwritten.
84
+ Configure `patrol.queue_stall_s` (600) and `patrol.person_wait_s` (1800); overload needs three consecutive
85
+ measured passes above the configured host-load threshold; unknown evidence does not reset the count.
86
+ A quiet job with the watchdog disabled asks for inspection and offers `twinlane stop JOB`.
87
+ Patrol is advisory and never kills or stops a job.
88
+
89
+
90
+ ### Patrol reader completeness
91
+
92
+ Incomplete evidence never resolves a finding. Each failed source emits `patrol_error`; earlier findings
93
+ that depend on it stay open with `stale=True`, `safe_fix=False`, including a finding emitted again with
94
+ the same key. Policy failures invalidate all dependent checks; details invalidate board checks, and
95
+ board/slots/host failures invalidate queue checks. Runtime waits also depend on the board's live-job
96
+ classification and queue evidence, despite their stored source being details. No error authorizes a fix.
97
+ A genuinely missing optional file/directory can be complete and empty; a denied parent is an error,
98
+ never absence.
99
+
100
+ The table covers every reader consumed by `patrol.collect()`. "Strict" means only FileNotFoundError
101
+ is accepted as absence; other access/I/O failures and invalid JSON/object types raise. The underlying
102
+ readers remain tolerant for other callers unless stated otherwise.
103
+
104
+ | Reader / evidence | Unreadable directory | Unreadable file | Malformed file | Patrol handling |
105
+ | --- | --- | --- | --- | --- |
106
+ | Policy + optional config (`load_policy`); host/watchdog/patrol settings | Strict open raises (config is checked without isfile) | Strict open raises; loader SystemExit is captured | Strict JSON/object raises; loader failure captured | policy error; all prior sources stale. Settings functions only transform this policy; invalid optional scalar settings keep documented defaults. |
107
+ | Board meters cache | Strict open raises | Raw reader returns None; patrol strict open raises | Strict parse requires engine objects and parseable read_at plus each engine's reading_at, including missing timestamps | board error; board, host, queue and runtime-wait findings stale; no pumping. Missing cache means engines unknown. |
108
+ | Board job directory scan (`observations.job_ids`) | scandir raises; missing directory is empty | Entry is_dir/stat errors propagate | Non-job directory names ignored | board/details/slots errors; their dependents stale. |
109
+ | Board job rows (`job/status/result.json`), runner.pid, survivor/child/tree markers | Strict patrol inputs raise | Raw JSON/text can return None/empty; strict patrol inputs raise | Strict JSON/object/status or runner identity validation raises; board problems also become errors | board error; prior job findings stale; details/slots independently validate these inputs. |
110
+ | Board batons (`collect_batons`, file_only) | Explicit listdir raises; missing directory is empty | Row reports problems | Row reports problems; invalid wait/inbox timestamps flag incomplete | board error; prior human waits stale. Non-patrol enumeration retains tolerant glob behavior. |
111
+ | Board inbox/attention + durable owner notices | Job scan raises | Raw notice read returns None; patrol strict input raises | Raw notice ignored; patrol strict JSON/object raises; malformed baton holds become row problems | board error; prior human waits stale. Inbox ranking and attention projection perform no extra evidence reads. Collected-marker stat is strict. |
112
+ | WIP queue (`wip.queue` equivalent) | Strict open raises | Raw read can default to empty; patrol strict open raises | Strict JSON, object, version, rows, identity and timestamp checks raise | queue error; prior queue and runtime-wait findings stale. Queue absence is complete and empty. |
113
+ | Queue launchability (`wip.launchable`) | Strict cwd/brief stat raises, including absolute legacy briefs without cwd | Permission/I/O stat failure raises; no brief contents read | Malformed argv/paths cause reader error or explicit unlaunchable reason | queue error + stale on read failure; genuinely gone brief/cwd is an explicit wait. |
114
+ | Host cache (`hostload.cached`) | Strict open raises | Raw reader returns None; patrol strict open raises | Invalid JSON raises; invalid timestamp/counters return None/unusable; any missing counter is partial | hostload error; prior overload and queue findings stale. Missing/stale cache is unknown; no sampling. Disabled host checks need no measurement. |
115
+ | Slots (`jobs.running_count`) | Strict job scan/inputs raise | Strict job inputs raise rather than accepting tolerant None | Strict inputs raise; non-integer/negative returned counts raise | slots error; prior queue findings stale. Native liveness stays non-spawning and conservative. |
116
+ | Read-only custody snapshot: writers directory, claims, receipt/status, runner identity, marker stats | listdir/stat/open raise; missing writers directory is empty | Strict open raises | Strict JSON/object/receipt status/runner identity raises | custody error; prior custody findings stale, no release. Missing receipt without a live status is incomplete; unknown identity needs existing finalized/grace proof. Release still locks and revalidates independently. |
117
+ | Job details: job/status + containment markers, output mtimes | Strict inputs/stat raise | Raw helpers use None/False; patrol uses strict inputs/stat | Strict JSON/object/status raises; log contents are not parsed | details error; prior unresolved/quiet-job/queue findings stale. Missing logs use job start time; watchdog settings come only from policy. |
118
+
119
+ The dependency audit below includes indirect invalidation. Failure of any named collector keeps
120
+ both re-emitted and prior matching findings stale, disables their fixes, and emits no resolved event.
121
+ The single parametrized reader-failure matrix exercises every source/dependency pair, including
122
+ runtime waits through board AND queue and legacy requests through their absolute brief path.
123
+
124
+ | Finding source / kinds | Required collect readers |
125
+ | --- | --- |
126
+ | board: job_orphaned, job_unknown, job_lost, job_long, waiting_on_person | policy, board, details |
127
+ | hostload: host_overload | policy, hostload, board, details |
128
+ | queue: queue_ready, queue_waiting, queue_claimed, queue_stalled, engine_blocked | policy, queue, board, slots, hostload, details |
129
+ | custody: custody_stale | policy, custody |
130
+ | details: custody_unresolved | policy, details |
131
+ | details: runtime queue_waiting | policy, details, board, queue (also slots and hostload through queue invalidation) |
132
+
133
+ Patrol errors identify the failed reader in the current pass; they never authorize fixes.
134
+ A failed whole-pass collector retains every prior finding as stale.
135
+
136
+ Patrol's own previous-state reader runs before collect: unreadable or malformed state emits a state
137
+ error and freezes state/history writes and fixes. It cannot reconstruct unreadable findings, so it retains
138
+ the stored observation for recovery instead of replacing it with an empty one. History is bookkeeping,
139
+ not evidence for finding resolution; its existing read/write errors emit a history error. Verdict text,
140
+ board signals and display-only activity stats do not decide patrol findings. Worktree/session/provider
141
+ readers are excluded from the file-only board.
@@ -0,0 +1,5 @@
1
+ include LICENSE README.md LIMITS.md DESIGN.md METHODOLOGY.md CITATION.cff
2
+ recursive-include skills *.md
3
+ recursive-include twinlane *.json
4
+ recursive-include tests *.py *.json *.jsonl *.txt
5
+ recursive-include examples *
@@ -0,0 +1,23 @@
1
+ # Operating method
2
+
3
+ Keep one lead for a task. The lead writes a small goal and owns the worktree, chooses a task kind, and checks the board before dispatch. Workers on either engine return evidence and changes; the lead reviews the diff and runs the final checks.
4
+
5
+ Use a read-only worker for investigation and a code worker only where edits are intended. Cross-vendor review is useful for risky changes and disagreements. Stop when the evidence converges on the stated goal; do not repeat a completed review just to alternate vendors.
6
+
7
+ At 75% usage, narrow new work; between 75% and 85%, `twinlane route` sends new work to the other engine when it is below 75% and usable. At 80%, update the baton and prepare a handoff brief. At 85%, stop new nonurgent work on that engine and move it to the other engine if its reading is usable. Never treat unknown usage as free allowance, turn on paid extra usage, or spend Codex credits. A handoff transfers context, not an approval for live actions.
8
+
9
+ ## Lead and integrator lanes
10
+
11
+ Workers do not land or ship. Each worker finishes in its own worktree on its own branch and reports evidence; the lead reviews the diff and reruns the project's gate itself rather than trusting a worker's "done".
12
+
13
+ One session is the integrator for a repository. It enqueues finished branches with `twinlane land enqueue SLUG`, checks `twinlane land status`, and lands them one at a time with `twinlane land run`, after a `--dry-run` if anything looks off. Because the gate runs on the tree that will land and a landing is fast-forward only, a branch that falls behind goes back to its worker or is rebased with `--rebase` when its range is linear. Two integrators never run at once: the lock refuses the second, and a stale lock is broken only when its holder is provably dead, with a reason on record.
14
+
15
+ Release is a separate, targeted step. After landing, `twinlane ship plan SLUG` writes a manifest of exactly that landing's files; read its foreign and superseded flags before passing `--allow-foreign` or `--allow-superseded`. Run `twinlane ship run --manifest PATH` as a dry run, review it, and only then `--execute --confirm CODE`. Ship the landing you reviewed, not everything on the branch. Nothing in this lane needs a worker to hold deploy credentials.
16
+
17
+ Keep the tree tidy at task boundaries: `twinlane wt doctor`, then `twinlane wt prune` as a dry run before `--yes`. After a reboot or an app quit, `twinlane resume` lists lost jobs; continue one only if its work is still wanted. Use `run --best-of` and `compare` for a decision that deserves two independent attempts, and accept that it costs the usage of both.
18
+
19
+ The lead loop is `twinlane board` ? `twinlane inbox` ? `twinlane wip` ?
20
+ `twinlane wt prune --merged-only` (preview). Ask one yes/no question instead of marking work blocked.
21
+ Bind the active session to its baton with `--bind-session SESSION_ID`; an inbox yes is never a live-write
22
+ approval. Park waiting work explicitly and close stale or landed batons yourself after checking the evidence.
23
+ Use `twinlane land card --baton SLUG` before enqueueing. Review source, tests, integration and release separately.