@tyroneross/build-loop 0.35.0 → 0.36.0
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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +67 -14
- package/README.md +208 -299
- package/agents/advisor.md +2 -0
- package/agents/alignment-checker.md +2 -0
- package/agents/api-assessor.md +3 -1
- package/agents/architecture-scout.md +2 -0
- package/agents/assessment-orchestrator.md +2 -0
- package/agents/build-orchestrator.md +13 -8
- package/agents/database-assessor.md +107 -3
- package/agents/design-contract-specialist.md +2 -0
- package/agents/fact-checker.md +2 -0
- package/agents/fix-critique.md +26 -5
- package/agents/frontend-assessor.md +3 -1
- package/agents/implementer.md +2 -0
- package/agents/independent-auditor.md +2 -0
- package/agents/mock-scanner.md +2 -0
- package/agents/optimize-runner.md +2 -0
- package/agents/overfitting-reviewer.md +2 -0
- package/agents/performance-assessor.md +3 -1
- package/agents/plan-critic.md +2 -0
- package/agents/promotion-reviewer.md +2 -0
- package/agents/recurring-pattern-detector.md +4 -1
- package/agents/retrospective-synthesizer.md +14 -0
- package/agents/root-cause-investigator.md +39 -2
- package/agents/scope-auditor.md +2 -0
- package/agents/security-reviewer.md +3 -0
- package/agents/self-improvement-architect.md +2 -0
- package/agents/synthesis-critic.md +2 -0
- package/agents/transcript-pattern-miner.md +2 -0
- package/agents/ui-validator.md +2 -0
- package/assets/build-loop-plugin-icon.png +0 -0
- package/bin/build-loop-install.js +234 -0
- package/commands/run.md +1 -1
- package/hooks/git/pre-commit +40 -0
- package/hooks/git/pre-push +35 -10
- package/hooks/hooks.json +42 -5
- package/hooks/pre-edit-architecture.sh +2 -2
- package/hooks/route-guard.sh +15 -0
- package/hooks/route_guard.py +221 -0
- package/hooks/session-start-extensions.sh +10 -0
- package/hooks/session-start-git-hooks.sh +6 -1
- package/hooks/session-start-rally-point.sh +23 -1
- package/hooks/test_session_start_rally_version.sh +123 -0
- package/package.json +7 -2
- package/plugin-artifacts/codex/.codex-plugin/plugin.json +1 -1
- package/plugin-artifacts/codex/AGENTS.md +67 -14
- package/plugin-artifacts/codex/README.md +208 -299
- package/plugin-artifacts/codex/assets/build-loop-plugin-icon.png +0 -0
- package/plugin-artifacts/codex/references/backlog-system.md +285 -0
- package/plugin-artifacts/codex/references/capability-routing.md +49 -0
- package/plugin-artifacts/codex/references/coordination-rules.md +271 -2
- package/plugin-artifacts/codex/references/memory-systems.md +25 -2
- package/plugin-artifacts/codex/references/memory.md +14 -0
- package/plugin-artifacts/codex/references/model-tier-mapping.md +45 -6
- package/plugin-artifacts/codex/references/phase-1-assess.md +19 -1
- package/plugin-artifacts/codex/references/phase-2-plan.md +9 -1
- package/plugin-artifacts/codex/references/phase-3-execute.md +1 -1
- package/plugin-artifacts/codex/references/phase-4-review.md +20 -1
- package/plugin-artifacts/codex/references/phase-5-iterate.md +4 -3
- package/plugin-artifacts/codex/references/phase-6-learn.md +2 -0
- package/plugin-artifacts/codex/references/verify-dispatch.md +85 -0
- package/plugin-artifacts/codex/skills/build-loop/SKILL.md +19 -10
- package/plugin-artifacts/codex/skills/build-loop/fallbacks.md +1 -1
- package/plugin-artifacts/codex/skills/build-loop/references/capability-routing.md +49 -0
- package/plugin-artifacts/codex/skills/build-loop/references/experiment-results-template.md +101 -0
- package/plugin-artifacts/codex/skills/build-loop/references/memory.md +14 -0
- package/plugin-artifacts/codex/skills/build-loop/references/output-style.md +98 -5
- package/plugin-artifacts/codex/skills/build-loop/references/phase-1-assess.md +19 -1
- package/plugin-artifacts/codex/skills/build-loop/references/phase-2-plan.md +9 -1
- package/plugin-artifacts/codex/skills/build-loop/references/phase-3-execute.md +1 -1
- package/plugin-artifacts/codex/skills/build-loop/references/phase-4-review.md +20 -1
- package/plugin-artifacts/codex/skills/build-loop/references/phase-5-iterate.md +4 -3
- package/plugin-artifacts/codex/skills/build-loop/references/phase-6-learn.md +2 -0
- package/plugin-artifacts/codex/skills/build-loop/references/verify-dispatch.md +85 -0
- package/scripts/build_codex_plugin_artifact.py +7 -0
- package/scripts/embed_backend.py +17 -13
- package/scripts/lessons_index/query.py +1 -1
- package/scripts/memory_graph/__init__.py +2 -2
- package/scripts/sync_navgator_lessons.py +4 -4
- package/skills/api-registry-bridge/SKILL.md +1 -1
- package/skills/auto-decision-capture/SKILL.md +4 -3
- package/skills/auto-finding-capture/SKILL.md +119 -0
- package/skills/build-loop/SKILL.md +19 -10
- package/skills/build-loop/fallbacks.md +1 -1
- package/skills/build-loop/references/capability-routing.md +49 -0
- package/skills/build-loop/references/experiment-results-template.md +101 -0
- package/skills/build-loop/references/memory.md +14 -0
- package/skills/build-loop/references/output-style.md +98 -5
- package/skills/build-loop/references/phase-1-assess.md +19 -1
- package/skills/build-loop/references/phase-2-plan.md +9 -1
- package/skills/build-loop/references/phase-3-execute.md +1 -1
- package/skills/build-loop/references/phase-4-review.md +20 -1
- package/skills/build-loop/references/phase-5-iterate.md +4 -3
- package/skills/build-loop/references/phase-6-learn.md +2 -0
- package/skills/build-loop/references/verify-dispatch.md +85 -0
- package/skills/debug-loop/SKILL.md +4 -2
- package/skills/debugging-memory/SKILL.md +24 -12
- package/skills/{debugging/assess/SKILL.md → debugging-memory/references/assess.md} +6 -13
- package/skills/{debugging/memory/SKILL.md → debugging-memory/references/search.md} +10 -17
- package/skills/{debugging/store/SKILL.md → debugging-memory/references/store.md} +5 -13
- package/skills/debugging-memory/references/subagent-integration.md +3 -3
- package/skills/defenseclaw-bridge/SKILL.md +1 -1
- package/skills/{loop-builder → focused-loop-builder}/SKILL.md +6 -6
- package/skills/ibr-bridge/SKILL.md +1 -1
- package/skills/knowledge/SKILL.md +13 -2
- package/skills/{knowledge-review/SKILL.md → knowledge/references/review-mode.md} +8 -20
- package/skills/logging-tracer/SKILL.md +20 -4
- package/skills/model-bakeoff/SKILL.md +44 -0
- package/skills/model-tiering/SKILL.md +55 -20
- package/skills/optimize/SKILL.md +2 -2
- package/skills/prd-bridge/SKILL.md +1 -1
- package/skills/recursive-retrospective/SKILL.md +46 -0
- package/skills/recursive-retrospective/references/01-retrospective.md +136 -0
- package/skills/recursive-retrospective/references/02-learning-capture.md +72 -0
- package/skills/recursive-retrospective/references/03-judge.md +45 -0
- package/skills/research/SKILL.md +1 -1
- package/skills/root-cause-analysis/SKILL.md +42 -0
- package/skills/security-methodology/SKILL.md +10 -1
- package/skills/security-scan/SKILL.md +55 -0
- package/skills/spec-writing/SKILL.md +42 -1
- package/skills/sync-skills/SKILL.md +8 -7
- package/commands/assess.md +0 -41
- package/commands/compose-handoff.md +0 -113
- package/commands/debug.md +0 -20
- package/commands/debugger-detail.md +0 -34
- package/commands/debugger-scan.md +0 -15
- package/commands/debugger-status.md +0 -20
- package/commands/debugger.md +0 -32
- package/commands/optimize-run.md +0 -31
- package/commands/promote-experiment.md +0 -129
- package/commands/rally-point.md +0 -301
- package/commands/research-run.md +0 -22
- package/commands/review-knowledge.md +0 -16
- package/commands/self-review.md +0 -51
- package/commands/setup-memory.md +0 -32
- package/commands/start-prd.md +0 -57
- package/commands/test.md +0 -34
- package/commands/verify-plan.md +0 -39
- package/skills/debugging/debug-loop/SKILL.md +0 -231
- package/skills/logging-tracer-bridge/SKILL.md +0 -70
- /package/skills/{loop-builder → focused-loop-builder}/presets/active-project-evidence.yaml +0 -0
- /package/skills/{loop-builder → focused-loop-builder}/presets/generic-artifact-loop.yaml +0 -0
- /package/skills/{loop-builder → focused-loop-builder}/presets/presentation-audit.yaml +0 -0
- /package/skills/{loop-builder → focused-loop-builder}/presets/research-synthesis.yaml +0 -0
- /package/skills/{loop-builder → focused-loop-builder}/presets/source-ingestion-raw-data-audit.yaml +0 -0
- /package/skills/{loop-builder → focused-loop-builder}/references/spec-format.md +0 -0
- /package/skills/{loop-builder → focused-loop-builder}/scripts/loop_builder.py +0 -0
|
Binary file
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
|
|
3
|
+
# Backlog System — host-agnostic, multi-repo deferred-work tracker
|
|
4
|
+
|
|
5
|
+
A backlog is the durable list of wanted-but-not-now work for a repo: deferred
|
|
6
|
+
debt, blocked infra, known fixes, pending decisions. This system makes that list
|
|
7
|
+
**host-neutral** (any coding agent — Claude, Codex, or other — reads and writes
|
|
8
|
+
it the same way), **filesystem-first** (grep + frontmatter, no DB/vector/graph),
|
|
9
|
+
and **aggregatable** across all of one user's repos via their personal memory.
|
|
10
|
+
|
|
11
|
+
## Why it is shaped this way
|
|
12
|
+
|
|
13
|
+
Five research-grounded principles drive the design (from the memory-architecture
|
|
14
|
+
guide; Letta filesystem-memory benchmark; "don't over-engineer"):
|
|
15
|
+
|
|
16
|
+
1. **Canonical truth vs derived index.** The items in `items/` are the truth.
|
|
17
|
+
`INDEX.md` is a *generated, rebuildable* view — never hand-edited.
|
|
18
|
+
2. **Metadata-rich items.** Each item carries provenance, evidence,
|
|
19
|
+
supersession links, and a TTL (`review_by`) so it can be audited and aged.
|
|
20
|
+
3. **Filesystem-first retrieval.** Reading = `cat BACKLOG.md` → `INDEX.md`, then
|
|
21
|
+
`grep` over `items/*.md` frontmatter. No index server, no embeddings.
|
|
22
|
+
4. **Lifecycle ops.** Formation (`new`) → consolidation (`sync` archives
|
|
23
|
+
done/dropped, flags stale) → retrieval (`list`, grep) → forgetting
|
|
24
|
+
(archive, never delete).
|
|
25
|
+
5. **Governance Level 3.** Every write is logged, timestamped, attributable —
|
|
26
|
+
git history + `created`/`updated` stamps + archive-not-delete provide it.
|
|
27
|
+
|
|
28
|
+
## Two scopes (do not conflate)
|
|
29
|
+
|
|
30
|
+
| Scope | Path | Lifetime | Shared? |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| **Per-repo backlog** | `<repo>/.build-loop/backlog/` | Travels with the repo; committed | Team-shareable (in the repo) |
|
|
33
|
+
| **Personal memory mirror** | `build-loop-memory/projects/<slug>/backlog/` | Per-user, aggregated across ALL the user's repos | Private to the user |
|
|
34
|
+
|
|
35
|
+
The sync is **one-way**: per-repo backlog → personal memory. The per-repo store
|
|
36
|
+
is the source of truth; the memory mirror is a cross-repo read view for the user
|
|
37
|
+
who downloaded build-loop.
|
|
38
|
+
|
|
39
|
+
## Structure
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
<repo>/
|
|
43
|
+
├── BACKLOG.md # repo-root pointer → .build-loop/backlog/INDEX.md
|
|
44
|
+
└── .build-loop/backlog/
|
|
45
|
+
├── README.md # what this dir is + how to use it
|
|
46
|
+
├── .gitattributes # INDEX.md merge=ours (resolve by regeneration)
|
|
47
|
+
├── INDEX.md # DERIVED — regenerated by `sync`; do not edit
|
|
48
|
+
├── items/<ID>.md # CANONICAL truth — one file per item
|
|
49
|
+
└── archive/<ID>.md # done/dropped items (never deleted)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`<ID>` = `<PROJSLUG>-<AREA>-<token>` — e.g. `ATOM-SEARCH-mt3k9p7q`. The prefix is
|
|
53
|
+
derived from the repo basename; the **token** is a coordination-free,
|
|
54
|
+
collision-resistant suffix: 8 Crockford-base32 digits of millisecond time
|
|
55
|
+
(so `ls items/` still roughly sorts by creation) followed by 5 digits of
|
|
56
|
+
`os.urandom` entropy (so the ID is unique across agents, machines, branches, and
|
|
57
|
+
clones with **no shared counter**). Lowercase throughout.
|
|
58
|
+
|
|
59
|
+
**Why not the old sequential `-NNN`?** A per-area zero-padded counter is a
|
|
60
|
+
single-writer assumption. Two agents in independent worktrees/clones (Claude on
|
|
61
|
+
branch A, Codex on branch B) each read their OWN highest-NNN and both minted the
|
|
62
|
+
SAME `...-001/002/003` for DIFFERENT items — on `git merge` the identical
|
|
63
|
+
filenames/IDs collided or silently lost an item. The token removes the shared
|
|
64
|
+
counter entirely. The readability trade-off (IDs gain a ~13-char token suffix
|
|
65
|
+
instead of `-001`) is paid back by `INDEX.md`, which is the human-ordered
|
|
66
|
+
navigation view; the items themselves are addressed by token.
|
|
67
|
+
|
|
68
|
+
**Legacy `-NNN` items still read.** Items minted before this change (and any
|
|
69
|
+
hand-authored `-NNN`) remain valid: the ID-shape regex accepts BOTH a token
|
|
70
|
+
suffix and a legacy `\d{3,}` suffix, and no parser assumes a numeric tail.
|
|
71
|
+
Existing IDs are never rewritten — only NEW items get tokens.
|
|
72
|
+
|
|
73
|
+
## Item schema (frontmatter on every `items/<ID>.md`)
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
id: ATOM-SEARCH-001
|
|
77
|
+
title: <one line>
|
|
78
|
+
status: open # open | in-progress | blocked | deferred | done | dropped
|
|
79
|
+
priority: P2 # P0..P3
|
|
80
|
+
type: debt # feature | fix | debt | infra | decision | cleanup | research
|
|
81
|
+
area: search # free-ish tag; the grep + filter routing key
|
|
82
|
+
entities: [pg_trgm, keyword-leg]
|
|
83
|
+
gated: db-migration # none | prod-deploy | db-migration | infra | product-decision
|
|
84
|
+
provenance: { source: followup, ref: <path/commit/PR#/chat-date> }
|
|
85
|
+
evidence: [<commit/PR/retro/followup paths>]
|
|
86
|
+
supersedes: null
|
|
87
|
+
superseded_by: null
|
|
88
|
+
created: 2026-06-16
|
|
89
|
+
updated: 2026-06-16
|
|
90
|
+
review_by: 2026-07-16 # TTL — surfaced by `sync` when passed
|
|
91
|
+
owner: unassigned
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Body sections: `## Context`, `## Acceptance`, `## Notes`.
|
|
95
|
+
|
|
96
|
+
Dates are caller-stamped (`--today YYYY-MM-DD` or the `BACKLOG_TODAY` env var,
|
|
97
|
+
falling back to the system clock) so the tool runs in harnesses where the clock
|
|
98
|
+
is not callable.
|
|
99
|
+
|
|
100
|
+
## Lifecycle / sync contract
|
|
101
|
+
|
|
102
|
+
`scripts/backlog.py sync --repo <path>` does three things, in order:
|
|
103
|
+
|
|
104
|
+
1. **Consolidate.** Move every `status: done` / `status: dropped` item from
|
|
105
|
+
`items/` to `archive/`. Never deletes. Flags items whose `review_by` has
|
|
106
|
+
passed as stale. Warns about `.build-loop/{followup,issues,proposals}` files
|
|
107
|
+
not yet referenced by any item's `evidence`/`provenance.ref` (scattered work
|
|
108
|
+
not yet triaged into the backlog).
|
|
109
|
+
2. **Regenerate INDEX.** Rebuild `INDEX.md` deterministically from the remaining
|
|
110
|
+
items' frontmatter — grouped status → area → priority, with a compact table,
|
|
111
|
+
a summary (active count, open P0/P1, stale count, gated count), and a stale
|
|
112
|
+
call-out. For a FIXED item set re-rendering is **byte-identical** (no
|
|
113
|
+
wall-clock in the body), so `sync` is safe to re-run and diff-clean. The INDEX
|
|
114
|
+
write is atomic (temp file + `os.replace`), so a concurrent `sync` by two
|
|
115
|
+
agents never leaves a torn file for a reader. **Duplicate-ID guard
|
|
116
|
+
(defense in depth):** if two items ever share an `id` (a bad merge, a
|
|
117
|
+
hand-edit), `sync` surfaces them LOUDLY in a `## ⚠ Duplicate IDs` section
|
|
118
|
+
(id, count, all titles, all paths) instead of silently rendering one — so a
|
|
119
|
+
collision that did happen is visible.
|
|
120
|
+
3. **Mirror to personal memory.** Copy active item files (+ a mirror INDEX) into
|
|
121
|
+
`build-loop-memory/projects/<slug>/backlog/`. Each mirror file is written
|
|
122
|
+
atomically (temp file + `os.replace`), so a parallel session never observes a
|
|
123
|
+
half-written file; last-writer-wins (the per-repo backlog is the truth).
|
|
124
|
+
Best-effort: if the memory root is absent or unwritable, sync still succeeds
|
|
125
|
+
and reports `mirror.skipped`.
|
|
126
|
+
|
|
127
|
+
### INDEX.md merge handling
|
|
128
|
+
|
|
129
|
+
`INDEX.md` is fully derived, so two agents that both regenerate it WILL conflict
|
|
130
|
+
on `git merge`. The scaffold (via `adopt` AND any backlog materialisation by
|
|
131
|
+
`new`/`sync`) drops a `.build-loop/backlog/.gitattributes` with:
|
|
132
|
+
|
|
133
|
+
```gitattributes
|
|
134
|
+
INDEX.md merge=ours
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
so git resolves the conflict by keeping one side instead of inserting conflict
|
|
138
|
+
markers. **On an `INDEX.md` conflict, run `backlog.py sync` — `items/` is the
|
|
139
|
+
truth and the INDEX regenerates deterministically.** Item files in `items/`
|
|
140
|
+
don't conflict once IDs are unique (token suffix → separate paths), so both
|
|
141
|
+
branches' new items survive a merge.
|
|
142
|
+
|
|
143
|
+
## How ANY agent uses it (host-neutral)
|
|
144
|
+
|
|
145
|
+
**Read** (no tools required — just the filesystem):
|
|
146
|
+
1. `cat <repo>/BACKLOG.md` → follow the pointer to `INDEX.md`.
|
|
147
|
+
2. `cat <repo>/.build-loop/backlog/INDEX.md` for the grouped view.
|
|
148
|
+
3. `grep -l 'area: search' <repo>/.build-loop/backlog/items/*.md` to find items
|
|
149
|
+
by any frontmatter field; read the matching item bodies.
|
|
150
|
+
|
|
151
|
+
**Write** (via the pure-stdlib CLI — works under bare `python3`, no host SDK):
|
|
152
|
+
```bash
|
|
153
|
+
# create
|
|
154
|
+
python3 scripts/backlog.py new --repo <path> --area search --type debt \
|
|
155
|
+
--title "pg_trgm for keyword leg" --priority P1 --gated db-migration \
|
|
156
|
+
--provenance-source followup --provenance-ref .build-loop/followup/x.md \
|
|
157
|
+
--today 2026-06-16
|
|
158
|
+
# edit the item body / frontmatter by hand in items/<ID>.md
|
|
159
|
+
# then regenerate + mirror
|
|
160
|
+
python3 scripts/backlog.py sync --repo <path> --today 2026-06-16
|
|
161
|
+
# read
|
|
162
|
+
python3 scripts/backlog.py list --repo <path> --status open
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The CLI emits structured JSON (machine-readable) on `new`/`sync`; `list` prints
|
|
166
|
+
a text table (or JSON with `--json`). The host LLM reasons over the structured
|
|
167
|
+
data + the convention — it never calls a vendor API.
|
|
168
|
+
|
|
169
|
+
## Relationship to existing build-loop surfaces
|
|
170
|
+
|
|
171
|
+
- `.build-loop/issues/` — current-run bugs (short-lived). A persistent issue is
|
|
172
|
+
triaged INTO the backlog (`new`), then the issue file is referenced as
|
|
173
|
+
`evidence`/`provenance.ref` so `sync` stops warning about it.
|
|
174
|
+
- `.build-loop/followup/` — iterate-cap overflow. Same triage path.
|
|
175
|
+
- `templates/backlog-item.md` + `build-loop-memory/projects/<slug>/backlog.md`
|
|
176
|
+
(the legacy single-table durable backlog) remain valid for build-loop's own
|
|
177
|
+
self-work tracking; this per-item system is the general, multi-repo surface
|
|
178
|
+
and writes its mirror to the `backlog/` *subdir* (no clobber of `backlog.md`).
|
|
179
|
+
|
|
180
|
+
## Phase 1 Assess integration
|
|
181
|
+
|
|
182
|
+
`scripts/context_bootstrap.py` surfaces a compact backlog line at Phase 1 Assess
|
|
183
|
+
when `.build-loop/backlog/INDEX.md` exists: open P0/P1 count, past-`review_by`
|
|
184
|
+
stale count, and gated count. Cheap (reads the generated INDEX summary only) and
|
|
185
|
+
non-fatal when the file is absent.
|
|
186
|
+
|
|
187
|
+
It also surfaces a one-time **discoverability nudge** (`backlog_discoverability`)
|
|
188
|
+
when there's something to act on:
|
|
189
|
+
|
|
190
|
+
- **Adoptable** — the repo has scattered work in `.build-loop/{followup,issues}`
|
|
191
|
+
but no `INDEX.md` yet → *"Backlog available — run `backlog.py adopt
|
|
192
|
+
--dry-run --repo .`"*.
|
|
193
|
+
- **Won't travel** — `.build-loop/backlog/` exists but is gitignored → *"Backlog
|
|
194
|
+
won't travel — run `adopt --apply` to fix `.gitignore`"*.
|
|
195
|
+
|
|
196
|
+
Both are silent when there's nothing to surface, and neither ever raises.
|
|
197
|
+
|
|
198
|
+
## Download & upgrade safety
|
|
199
|
+
|
|
200
|
+
This is the contract that makes the backlog safe to **download** with a fresh
|
|
201
|
+
build-loop and **upgrade** in place over an existing one. The guiding rule:
|
|
202
|
+
*every adoption/upgrade step is additive and reversible; nothing the user
|
|
203
|
+
already had is moved, deleted, or rewritten in a lossy way.*
|
|
204
|
+
|
|
205
|
+
**1. Additive / never-destructive.** Adoption never touches the existing work
|
|
206
|
+
surfaces. `.build-loop/followup/`, `.build-loop/issues/`, `.build-loop/proposals/`,
|
|
207
|
+
and the personal `build-loop-memory` store are read-only inputs to `adopt` — an
|
|
208
|
+
imported item *links* to its source (`provenance.ref` + `evidence[]` +
|
|
209
|
+
`imported_from`); the source file stays exactly where it was as the inbox /
|
|
210
|
+
provenance record. `sync` likewise only ever archives (`done`/`dropped` →
|
|
211
|
+
`archive/`) and regenerates the derived `INDEX.md`; it never deletes a live item.
|
|
212
|
+
|
|
213
|
+
**2. Lazy / opt-in scaffolding.** A repo gets backlog dirs and pointer files
|
|
214
|
+
*only* when the user runs `adopt` (or `new`). A downloaded build-loop does not
|
|
215
|
+
litter every repo with an empty `backlog/` on first run — the structure
|
|
216
|
+
materialises on demand. `adopt --dry-run` (the default) reports exactly what it
|
|
217
|
+
*would* create without writing anything, so the user previews before opting in.
|
|
218
|
+
|
|
219
|
+
**3. The `.gitignore` dependency — and how `adopt` fixes it.** `.build-loop/` is
|
|
220
|
+
gitignored by default (confirmed in both `build-loop` and `sample-app`). That is
|
|
221
|
+
correct for transient run state — but it means the backlog would **not commit
|
|
222
|
+
and therefore would not travel** with the repo (no team-sharing, lost on a fresh
|
|
223
|
+
clone). `adopt` detects this by inspecting `.gitignore` (pure text, no git call)
|
|
224
|
+
and, on `--apply`, appends un-ignore rules so the durable backlog escapes the
|
|
225
|
+
broad ignore:
|
|
226
|
+
|
|
227
|
+
```gitignore
|
|
228
|
+
# build-loop backlog (added by `backlog.py adopt` — keep so the backlog travels)
|
|
229
|
+
!.build-loop/backlog/
|
|
230
|
+
!.build-loop/backlog/**
|
|
231
|
+
!BACKLOG.md
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
It reports this loudly (`gitignore.was_ignored: true`, `added: [...]`) and is
|
|
235
|
+
idempotent — re-running never appends the block twice. If `.build-loop/` is not
|
|
236
|
+
ignored, `adopt` leaves `.gitignore` untouched.
|
|
237
|
+
|
|
238
|
+
**4. Idempotent, dry-run-first migration.** `adopt` defaults to a read-only
|
|
239
|
+
dry-run; `--apply` executes. Re-running `--apply` is a no-op: each imported item
|
|
240
|
+
records `imported_from: <source path>`, and a re-run reads that field across
|
|
241
|
+
`items/` **and** `archive/` and skips any source already imported. No duplicate
|
|
242
|
+
items, no churned `.gitignore`. Verified contract: `adopt --apply` twice → the
|
|
243
|
+
second run's diff (items + `.gitignore`) is empty.
|
|
244
|
+
|
|
245
|
+
**5. Schema-version tolerance (forward/back compatible).** Every item carries
|
|
246
|
+
`schema_version` (current = `1`). The reader (`read_item`) is deliberately
|
|
247
|
+
tolerant: it **defaults missing known fields** and **preserves unknown fields**.
|
|
248
|
+
An item written by an *older* build-loop (no `schema_version`) reads back with
|
|
249
|
+
defaults filled in; an item written by a *newer* build-loop (carrying fields
|
|
250
|
+
this version doesn't know) reads back without error and with those fields intact.
|
|
251
|
+
So a backlog written by one build-loop version always reads on another — neither
|
|
252
|
+
a downgrade nor an upgrade corrupts or rejects it. Bump `SCHEMA_VERSION` only for
|
|
253
|
+
a contract change a reader must actively know about; the tolerant reader absorbs
|
|
254
|
+
purely-additive field changes without a bump.
|
|
255
|
+
|
|
256
|
+
**6. `build-loop-memory` is the per-user cross-repo long-term store.** The
|
|
257
|
+
per-repo backlog travels *with the repo* (committed, team-shareable). `sync`
|
|
258
|
+
additionally mirrors active items one-way into
|
|
259
|
+
`build-loop-memory/projects/<slug>/backlog/` — the user's private, cross-repo
|
|
260
|
+
aggregate view. The mirror writes to the `backlog/` **subdir**, so it never
|
|
261
|
+
collides with the legacy single-table `backlog.md`, with `lessons/`, or with
|
|
262
|
+
`decisions/` in that project's memory lane. The mirror is additive: its only
|
|
263
|
+
delete path is pruning orphaned item mirrors (ID-shaped stems whose source item
|
|
264
|
+
was removed), and it never touches a hand-dropped note. If the memory root is
|
|
265
|
+
absent or unwritable, `sync` still succeeds and reports `mirror.skipped`.
|
|
266
|
+
|
|
267
|
+
**Concurrency note (multi-agent) — two distinct collision classes.**
|
|
268
|
+
|
|
269
|
+
1. **Distributed (cross-worktree/clone/machine).** Independent checkouts share no
|
|
270
|
+
filesystem and no counter, so IDs must be collision-free *with zero
|
|
271
|
+
coordination*. The token suffix (time-ordered + `os.urandom`) provides exactly
|
|
272
|
+
that: two agents in separate worktrees minting in the same area produce
|
|
273
|
+
DISJOINT ID sets, so a `git merge` of their `items/` trees lands every item
|
|
274
|
+
with nothing clobbered. This is the bug the token scheme fixes — the retired
|
|
275
|
+
sequential counter collided here.
|
|
276
|
+
2. **Same-filesystem concurrent processes.** N `new`/`adopt` processes on ONE
|
|
277
|
+
filesystem still race on the create. That is handled (kept from the prior fix)
|
|
278
|
+
by the `O_EXCL` atomic create: exactly one process can create a given path;
|
|
279
|
+
the rest re-mint a fresh token and retry, bounded. So 8 parallel `new` calls
|
|
280
|
+
in one area yield 8 distinct files, zero loss.
|
|
281
|
+
|
|
282
|
+
Together these make the backlog safe as a shared multi-agent work surface across
|
|
283
|
+
both separate checkouts AND concurrent processes — not just a single-writer file.
|
|
284
|
+
The INDEX and memory-mirror writes are atomic (temp + `os.replace`) so a reader
|
|
285
|
+
never sees a torn derived file.
|
|
@@ -25,6 +25,55 @@ Phase 1 runs `node ${CLAUDE_PLUGIN_ROOT}/skills/build-loop/detect-plugins.mjs` a
|
|
|
25
25
|
| Modular systems pack | Phases 1-4 | Read `references/modular-systems-pack.md`; partition files/tasks MECE; prefer modular scalable boundaries unless an exception is documented |
|
|
26
26
|
| Codex subagent adapter | Phase 3 (Execute, Codex only) | Read `references/codex-subagents.md`; use `templates/codex-worker-prompt.md` for authorized Codex workers |
|
|
27
27
|
|
|
28
|
+
### Spec/Plan author router (intent-driven, ordered)
|
|
29
|
+
|
|
30
|
+
This router selects the single skill that AUTHORS the plan/spec/task graph for this run. It is a recommendation that DRIVES Phase 2 — recorded into `state.json.intent.spec_router`, consumed by Phase 2 Plan, never a hard block.
|
|
31
|
+
|
|
32
|
+
**Scope — author selection only.** `prd-bridge` is NOT in this router. It is mandatory in Phase 1 step 10 and only *consumes* an existing PRD (`docs/prd-*.md` → `state.json.prd`); it never authors. PRD-context is orthogonal to author selection — keep them separate.
|
|
33
|
+
|
|
34
|
+
**Governing rule**: select on intent + goal + context, not on a trigger-word match. A skill's word-overlap activation is not enough to call it (this is why `prd-builder` over-fired during build-loop runs). The router is the intent-side selection; it pairs with the activation-side guard (prd-builder's negative-trigger scope, which excludes in-flight changes and active build-loop runs).
|
|
35
|
+
|
|
36
|
+
Signals (all set during Phase 1 Assess):
|
|
37
|
+
|
|
38
|
+
- `run_active` — is a build-loop run active? (`true` for any `/build-loop:run` or build-orchestrator dispatch.)
|
|
39
|
+
- `plan_status` — `no-plan` | `plan-valid`, from the Phase 2 plan-exists gate (`.build-loop/plan.md` absent/empty + last `plan-verify` result).
|
|
40
|
+
- `intent_kind` — `PRD-author` | `build-plan` | `task-graph` | `none`, the LLM's read of what this run needs authored.
|
|
41
|
+
- `code_exists` — does the repo have substantive existing code? (Assess maps architecture: new/empty repo → false, existing repo → true.)
|
|
42
|
+
- `goal_scope` — `new-app` | `existing-app-change` | `in-build-task-breakdown`.
|
|
43
|
+
|
|
44
|
+
The rows are ORDERED — first match wins, so exactly one author is selected and no case matches two. The final default row makes the table exhaustive: every `(run_active, plan_status, intent_kind)` combination matches exactly one row, so nothing falls through.
|
|
45
|
+
|
|
46
|
+
| # | Match condition (first true wins) | route_type / action | Author selected | Why |
|
|
47
|
+
|---|---|---|---|---|
|
|
48
|
+
| 1 | `intent_kind == none` — nothing to author (Q&A, status, trivial, read-only, or advisory-context-only) | `noop` (call nothing) | — (`skill: null`) | no plan/spec/task graph needed |
|
|
49
|
+
| 2 | `run_active == false` AND greenfield PRD authoring is explicitly intended (new app with no code, OR `/start-prd` requested for a project) | `call` (or `recommend` outside an interactive session) | `prd-builder` | greenfield authoring + interactive intake; honors prd-builder's negative trigger — never fires inside an active run |
|
|
50
|
+
| 3 | `run_active == true` AND `plan_status == no-plan` | `call` | `build-loop:spec-writing` | author the in-build, non-interactive, gated plan for the orchestrator |
|
|
51
|
+
| 4 | `run_active == true` AND `plan_status == plan-valid` | `call` | `build-loop:writing-plans` (external) | turn the accepted plan into the task / dependency graph |
|
|
52
|
+
| 5 (default) | else (no earlier row matched) — the canonical case is `run_active == false` AND `intent_kind` is `build-plan` or `task-graph`: a direct, out-of-run invocation of plan authoring on an existing repo | `recommend` | `build-loop:spec-writing` | recommend, not auto-call: there is no active run to drive, so surface the author for the lead to run if it chooses |
|
|
53
|
+
|
|
54
|
+
The order is the contract: row 1 short-circuits before any author runs; row 2 is the only PRD author and is fenced off from active runs; rows 3 and 4 are mutually exclusive on `plan_status`; row 5 is the exhaustive `else` default that catches every remaining combination — its canonical case is an out-of-run `build-plan`/`task-graph` intent, but as the final row it also absorbs any residual (e.g. an out-of-run non-greenfield `PRD-author`) so no signal combination ever falls through. The table extends to any future author skill — add a row at the right precedence above the default, key it on the same signals, and the system still selects exactly one and records why.
|
|
55
|
+
|
|
56
|
+
`build-loop:writing-plans` (row 4) is the **external** `writing-plans` skill from the superpowers plugin — it is not vendored in this repo (`skills/writing-plans/SKILL.md` does not exist here). When it is absent, fall back to the inline "write a structured plan directly" path in the §"Core loop skills/assets" table above.
|
|
57
|
+
|
|
58
|
+
> Future refinement (do not over-build now): for monorepos, a `target_code_exists` signal (does the *target sub-path* already have code, vs the repo as a whole) would refine row 2's greenfield test. Out of scope until a monorepo case demands it.
|
|
59
|
+
|
|
60
|
+
**Phase 1 record (the consumable contract)**. Phase 1 writes the matched decision into `state.json.intent.spec_router` so a Codex lead (no `Skill()` auto-activation) can consume it deterministically:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"route_type": "author",
|
|
65
|
+
"action": "call" | "recommend" | "noop",
|
|
66
|
+
"skill": "build-loop:spec-writing" | "build-loop:writing-plans" | "prd-builder" | null,
|
|
67
|
+
"fallback": "<fallbacks.md section or inline guidance> | null",
|
|
68
|
+
"matched_row": 1 | 2 | 3 | 4 | 5,
|
|
69
|
+
"signals": { "run_active": true, "plan_status": "no-plan", "intent_kind": "build-plan", "code_exists": true, "goal_scope": "existing-app-change" }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`action: "noop"` → `skill: null`, Phase 2 authors nothing from the router (terminal). `action: "recommend"` → `skill` carries the recommended author name (so the lead knows what to run if it chooses); Phase 2 surfaces the recommendation in the report and does NOT auto-call. `action: "call"` → `skill` is non-null and Phase 2 calls it. The three actions are distinct: `call` auto-invokes, `recommend` surfaces-only, `noop` skips.
|
|
74
|
+
|
|
75
|
+
**Phase 2 read (the load-bearing wire)**. Phase 2 Plan READS `state.json.intent.spec_router` and acts on `action`/`skill` — it does NOT independently re-decide which author to call. See `references/phase-2-plan.md` step 0.
|
|
76
|
+
|
|
28
77
|
### Phase quick reference
|
|
29
78
|
|
|
30
79
|
| # | Phase | Purpose | Sub-steps / key actions |
|
|
@@ -51,6 +51,8 @@ post(
|
|
|
51
51
|
|
|
52
52
|
**Channel scope (worktree- and clone-independent):** resolve the channel through `scripts/rally_point/discovery_bridge.resolve(workdir)`. Native `agent-rally-point` discovery returns the canonical shared channel (currently `~/.agent-rally-point/apps/<repo-id>/`). The embedded build-loop fallback also defaults to `~/.agent-rally-point/apps/<slug>/`, where `slug` comes from `git rev-parse --git-common-dir` via `scripts/rally_point/channel_paths.app_slug(cwd)`. The main checkout, every worktree, and every clone of the same canonical repo share ONE channel. Different canonical repos get different channel directories (cross-repo isolation).
|
|
53
53
|
|
|
54
|
+
**Rally's REAL CLI surface (the only commands build-loop shells out to).** Rally exposes `enter`, `say <kind>`, `whoami`, `room`, `next`, `recent`, `stop <session|name|tool>`, `sessions [--reap]`, `migrate-legacy`, `mission`, `version`, and `check`. It has NO `setup`, NO `post`, NO `start`, and NO `replay` — those were anticipated but never shipped, and a discovery tier that gated on them was dead code (removed). A write goes out as `rally say <kind> --json …` (build-loop's `post()` helper translates to this); identity/channel info comes from `rally whoami --json` (`repo_root`/`repo_id`/`worktree`/`build_id`/`cwd`), NOT a `setup` probe. The surface-acceptance check (`discovery_bridge._rally_binary_supports_required_surface`) is pinned to a real rally binary's `--help` by `scripts/test_discovery_bridge.py::RequiredSurfacePinnedToRealRallyTests`, so it can never silently drift back to a phantom surface. When adding a rally call, read the binary's real `--help` first; do not assume a command exists.
|
|
55
|
+
|
|
54
56
|
**Anti-pattern (silent no-op):**
|
|
55
57
|
|
|
56
58
|
```python
|
|
@@ -77,6 +79,27 @@ Threats this model does NOT cover (out of scope by design): a hostile process ru
|
|
|
77
79
|
|
|
78
80
|
---
|
|
79
81
|
|
|
82
|
+
## Evidence boundary (Rally is not a verifier)
|
|
83
|
+
|
|
84
|
+
Rally records are peer-authored coordination metadata. They can tell an agent
|
|
85
|
+
what another agent claimed, handed off, reviewed, blocked, or released, and they
|
|
86
|
+
can point to artifacts worth inspecting. They do not prove the artifact, code,
|
|
87
|
+
package, tag, release, or remote state is correct.
|
|
88
|
+
|
|
89
|
+
Before making a factual claim about repo or release state, check the authoritative
|
|
90
|
+
surface directly:
|
|
91
|
+
|
|
92
|
+
- Code and docs: working tree, `git diff`, file contents, and tests.
|
|
93
|
+
- Package/version surface: manifests, package tests, dry-run pack/publish output,
|
|
94
|
+
and release-surface verifier scripts.
|
|
95
|
+
- Remote release state: GitHub/npm/GitHub Packages API or public pages, not a
|
|
96
|
+
Rally `release` or `artifact` record.
|
|
97
|
+
|
|
98
|
+
A Rally `artifact`, `release`, `resolve`, `review_artifact`, or `next` record is
|
|
99
|
+
a routing signal. It may identify what to inspect; it is never the inspection.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
80
103
|
## Cheap detection at step boundaries
|
|
81
104
|
|
|
82
105
|
**Poll `coordination_status.py` BEFORE any step-boundary decision.** Costs ~100 tokens; prevents stale-state recommendations that cost full plan rewrites (~5K tokens).
|
|
@@ -150,9 +173,255 @@ any of the above. Memory citation:
|
|
|
150
173
|
|
|
151
174
|
**Orphaned-lane absorption:** when a lane assigned to an idle peer is **local and reversible** (commits, doc/agent edits, dead-code or dead-key trims, version bumps, test updates), the live agent **absorbs it** — does the work itself, then records in the report `absorbed <peer>'s idle lane: <what> [<evidence>]`. Do **not** block a release, a finish, or "done" on an idle peer's local lane — that is the same manufactured wait as a turn-length stop (see `skills/build-loop/SKILL.md` §"Keep going until done"). Only surface/hold a lane that is genuinely **peer-exclusive**: needs the other vendor's model (true cross-vendor review), the peer's environment/credentials, or an irreversible action only that peer is authorized to take. Coordination is cooperative, not a dependency that can deadlock the live agent.
|
|
152
175
|
|
|
176
|
+
## Recency decay & size-scaled lead/ownership auto-reclaim
|
|
177
|
+
|
|
178
|
+
A single coordination policy governs message aging and stale-claim reclaim,
|
|
179
|
+
mirrored from the canonical Rust implementation (agent-rally-point). Tunables
|
|
180
|
+
live under `coordinationPolicy` in `.build-loop/config.json` (defaults shown):
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"coordinationPolicy": {
|
|
185
|
+
"half_life_hours": 48,
|
|
186
|
+
"archive_floor_weight": 0.05,
|
|
187
|
+
"reclaim_small_minutes": 30,
|
|
188
|
+
"reclaim_large_minutes": 120
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- **Recency decay (listing order + archive).** Every coordination change gets a
|
|
194
|
+
weight `0.5 ** (age_hours / half_life_hours)` (default half-life 48h). The
|
|
195
|
+
status / recent-changes listing orders fresh-first by weight and EXCLUDES any
|
|
196
|
+
change whose weight has fallen below the archive floor (default `0.05`, ≈14d).
|
|
197
|
+
Archived changes are losslessly retrievable with `--include-archived`
|
|
198
|
+
(`coordination_status.py --include-archived`), which also folds back any
|
|
199
|
+
physically-rotated `changes.jsonl.<date>` logs. Decay applies only to the
|
|
200
|
+
historical change stream — never to the live direct-message inbox or to active
|
|
201
|
+
state. Fails OPEN: a change with a malformed `ts` is treated as fresh.
|
|
202
|
+
- **Size-scaled lead/ownership auto-reclaim.** A lead lease (`rally/lead.json`)
|
|
203
|
+
whose `lease_until` has passed is auto-reclaimable by the next `claim_lead`.
|
|
204
|
+
The lease WINDOW scales with the claimed work size: a small (single-file /
|
|
205
|
+
effort XS·S) claim expires after `reclaim_small_minutes` (default 30m); a
|
|
206
|
+
large (multi-file / coarse / effort M·L·XL) claim after `reclaim_large_minutes`
|
|
207
|
+
(default 2h). Pass `work_size`/`effort`/`owns` to `claim_lead`; with NO size
|
|
208
|
+
signal the lease window stays the historical `renew_every_minutes` cadence
|
|
209
|
+
(backward-compatible). An auto-reclaim posts a durable `lead-reclaim` record
|
|
210
|
+
naming who reclaimed, the prior owner, and the reason (`stale-by-timeout`).
|
|
211
|
+
- **Preserved invariants.** Reclaim stays race-safe (the `rally/lead.lock`
|
|
212
|
+
fcntl lock is untouched) and FAIL-CLOSED: a present incumbent lease whose
|
|
213
|
+
`lease_until` is unparseable is NEVER auto-reclaimed (we refuse rather than
|
|
214
|
+
reclaim on a timestamp we cannot trust). An empty seat is still freely
|
|
215
|
+
claimable.
|
|
216
|
+
- **Policy is Rust-only; the Python math is an in-process helper.** Reclaim and
|
|
217
|
+
reap DECISIONS are made by the Rust binary (facade in `reaper.py` /
|
|
218
|
+
`leadership.py`, fail-loud below full capability). `decay.py` survives only as
|
|
219
|
+
pure in-process math the lead-lease window sizing needs (`recency_weight`,
|
|
220
|
+
`reclaim_timeout_seconds`, `classify_work_size`); it is NOT a behavioral mirror
|
|
221
|
+
of `decay.rs` and is no longer double-pinned against a cross-repo golden
|
|
222
|
+
fixture. The status/recent-changes LISTING still applies the decay weight in
|
|
223
|
+
Python over build-loop's own change-log so an old on-PATH binary cannot
|
|
224
|
+
un-decay the listing (pinned by `scripts/test_coordination_decay_invariant.py`);
|
|
225
|
+
that is a presentation concern, distinct from the now-Rust-only reclaim/reap
|
|
226
|
+
actuation.
|
|
227
|
+
|
|
228
|
+
This complements (does not replace) the >10-minute idle absorption rule above:
|
|
229
|
+
idle-absorption handles local reversible lanes a quiet peer left open; the lease
|
|
230
|
+
timeout governs the formal lead/ownership role handover.
|
|
231
|
+
|
|
232
|
+
## In-room stale-state reaper (actuator) — RUST-ONLY via a fail-loud facade
|
|
233
|
+
|
|
234
|
+
The sections above define WHEN records become stale. Physically removing them
|
|
235
|
+
(reaping) is **Rust-only**. There is no Python reaper sweep — the prior Python
|
|
236
|
+
parity mirror (presence/claim-index/lead deletion, double-pinned against golden
|
|
237
|
+
fixtures) was RETIRED in the Rust-rally facade migration. A Python process
|
|
238
|
+
deleting coordination records the Rust binary owns is the exact shadow
|
|
239
|
+
implementation that is worse than no coordination.
|
|
240
|
+
|
|
241
|
+
**Canonical actuator (Rust):** the `rally` binary's reaper (`rally sessions
|
|
242
|
+
--reap`, or `rally doctor --reap-stale` on newer builds). Dry-run by default;
|
|
243
|
+
`--reap`/`--apply` physically removes over-TTL presence, claims, and leads.
|
|
244
|
+
|
|
245
|
+
**Facade (`scripts/rally_point/reaper.py`).** `reap_channel(channel, workdir,
|
|
246
|
+
apply=…)` resolves coordination capability via `discovery_bridge` and then:
|
|
247
|
+
- **full capability** (a real binary owns the channel — `repo-local-rally-cli`
|
|
248
|
+
/ `fetched-binary` / env-override / path-binary / python-import) → shells
|
|
249
|
+
`rally sessions --reap` and surfaces the result (`capability_level: full`,
|
|
250
|
+
`deferred_to_rust: false`).
|
|
251
|
+
- **below full** (degraded-breadcrumb or unavailable) → REFUSES. It reaps
|
|
252
|
+
nothing and returns a capability-marked report (`deferred_to_rust: true`,
|
|
253
|
+
`coordination_unavailable: <reason>`). A degraded session must never reap a
|
|
254
|
+
peer it cannot prove is dead.
|
|
255
|
+
|
|
256
|
+
CLI: `python3 scripts/rally_point/reaper.py --workdir <path> [--apply] [--json]`.
|
|
257
|
+
|
|
258
|
+
**Capability field on every coordination envelope.** Every facade return and
|
|
259
|
+
`DiscoveryEnvelope` carries `capability_level` (`full` / `degraded-breadcrumb` /
|
|
260
|
+
`unavailable`) + a `coordination_unavailable` reason. The single source of truth
|
|
261
|
+
is `scripts/rally_point/capability.py`; `FULL_ONLY_OPERATIONS`
|
|
262
|
+
(claim/release/reclaim/lead/reap/liveness/before_write/checkpoint) are permitted
|
|
263
|
+
only at full capability.
|
|
264
|
+
|
|
265
|
+
**Degraded breadcrumb path (the ONLY thing a sub-full session may write).** When
|
|
266
|
+
no binary is available but the host is supported, a session may write
|
|
267
|
+
capability-marked presence/handoff *breadcrumb* facts so a later full-capability
|
|
268
|
+
peer (or a human) sees it existed. It must NOT — and structurally cannot — claim
|
|
269
|
+
ownership, reclaim, infer liveness, reap, or imply before-write protection.
|
|
270
|
+
|
|
271
|
+
**Unsupported-host = loud no-coordination.** A host with no fetchable pinned-
|
|
272
|
+
binary asset (Intel macOS, musl/Alpine, exotic arch) resolves to
|
|
273
|
+
`capability_level: unavailable` (`coordination_unavailable: unsupported_host`).
|
|
274
|
+
The facade is a loud no-op there — NEVER a policy mirror.
|
|
275
|
+
|
|
276
|
+
**Remaining Python guards** (the destructive paths that survive as in-process
|
|
277
|
+
guards, all FAIL-CLOSED): `presence.reap_stale` physically unlinks only at full
|
|
278
|
+
capability; `leadership` reclaim (taking a peer's lease) is full-only, while
|
|
279
|
+
seeding an EMPTY lead seat and self-relinquishing one's OWN seat stay breadcrumb-
|
|
280
|
+
class. The in-process `decay.py` / `liveness.py` are now pure window/weight math
|
|
281
|
+
helpers (lead-lease sizing, adaptive presence window), not behavioral mirrors.
|
|
282
|
+
|
|
283
|
+
**Codex parity.** A codex session emits the same presence record claude does, via
|
|
284
|
+
the `.codex/hooks.json` `SessionStart` hook that calls `session_probe.py --tool
|
|
285
|
+
codex`, so it ages and decays identically — now enforced by the single Rust
|
|
286
|
+
reaper both tools share, not by a cross-language golden fixture.
|
|
287
|
+
|
|
288
|
+
**Session-end self-release (primary).** Both tool hooks release the agent's Rally
|
|
289
|
+
file-claims at turn completion (`Stop` event) so peers immediately see the agent's
|
|
290
|
+
absence and the claims do not leak past Stop. The two hosts reach this differently
|
|
291
|
+
because their session models differ:
|
|
292
|
+
|
|
293
|
+
- **Codex** emits `rally stop codex` (`.codex/hooks.json` Stop). `codex` resolves to
|
|
294
|
+
Codex's managed `rally-*` tmux session, so that one call both self-kills the
|
|
295
|
+
session (it can never become a detached orphan) AND releases the session's claims.
|
|
296
|
+
- **Claude Code** is normally NOT a `rally run`-managed session, so `rally stop
|
|
297
|
+
claude_code` fails (`unknown managed session`). The Claude Stop hook therefore
|
|
298
|
+
releases claims via the portable primitive instead: `scripts/stop_closeout.py`
|
|
299
|
+
(`release_my_claims`, called from `hooks/closeout.sh stop`) enumerates this tool's
|
|
300
|
+
open claims with `rally room --tool claude_code --json` and releases each by event
|
|
301
|
+
id with `rally say release --tool claude_code --ref <event-id>`. It fires on EVERY
|
|
302
|
+
Stop (a stopped session is no longer editing), is advisory + fail-open (`command -v
|
|
303
|
+
rally` guarded; swallows all errors; exit 0 always), and is capped per Stop so a
|
|
304
|
+
backlog can't make the hook run long. History: before this, the Claude hook
|
|
305
|
+
released only the run IDENTITY, never the file claims, so `claude_code` auto-claims
|
|
306
|
+
accreted unbounded (112→127 observed). Added 2026-06-29.
|
|
307
|
+
|
|
308
|
+
**Reaper = backstop, not primary (NON-DEFAULT).** The reaper that expires abandoned
|
|
309
|
+
claims/presence (`rally sessions --reap`, faced by `scripts/rally_point/reaper.py`)
|
|
310
|
+
is Rust-only and dry-run/manual by default — it is NOT turned on. Self-release above
|
|
311
|
+
is the primary mechanism. The reaper exists ONLY to catch the dead: sessions that
|
|
312
|
+
crash (SIGKILL / 529 / network drop) before their Stop hook fires, so self-release
|
|
313
|
+
never ran. Do not enable it as a default-on sweep; run it deliberately as the
|
|
314
|
+
explicit backstop for crash-orphaned claims.
|
|
315
|
+
|
|
316
|
+
## Adaptive multi-signal liveness (squad-projection decay + tmux orphan reaper)
|
|
317
|
+
|
|
318
|
+
Fixed staleness cutoffs are replaced by liveness that ADAPTS to each session's
|
|
319
|
+
planned heartbeat cadence and weighs four signals. The liveness DECISION (reap /
|
|
320
|
+
self-exit) is Rust-only via the facade; `scripts/rally_point/liveness.py` retains
|
|
321
|
+
only the in-process window/verdict MATH the presence squad-projection uses,
|
|
322
|
+
verified by its own inline unit tests (`test_liveness.py`). The cross-repo golden
|
|
323
|
+
fixture `liveness_vectors.json` and its `_provenance.json` drift entry were
|
|
324
|
+
RETIRED in the Rust-rally migration — there is no longer a byte-identical parity
|
|
325
|
+
contract to maintain across the two codebases.
|
|
326
|
+
|
|
327
|
+
**Adaptive cadence.** A session declares its beat via `planned_heartbeat_secs`
|
|
328
|
+
(presence record) or `renew_every_minutes` (lead.json); undeclared → the default
|
|
329
|
+
cadence. Staleness is RELATIVE: `window = planned_interval * MISS_MULTIPLIER + GRACE`.
|
|
330
|
+
Defaults `DEFAULT_CADENCE_SECS=300`, `MISS_MULTIPLIER=6`, `GRACE_SECS=60` →
|
|
331
|
+
a 5-min cadence is stale at ~31 min (≈6 missed beats); a 5-hour cadence not until
|
|
332
|
+
~30 h. Tunable via `.rally/config.json` `coordination{}` (Rust) /
|
|
333
|
+
`.build-loop/config.json` `coordinationPolicy{}` (Python):
|
|
334
|
+
`default_cadence_secs`, `miss_multiplier`, `grace_secs` (+ `RALLY_*` env in Rust).
|
|
335
|
+
Legacy `heartbeat_minutes` is honored as a cadence source for backward compat.
|
|
336
|
+
|
|
337
|
+
**Four signals — LIVE if ANY is fresh within the adaptive window:**
|
|
338
|
+
(a) heartbeat/presence `last_seen`; (b) inject/ack (a `receipt`/`wake`/`handoff`
|
|
339
|
+
naming the session); (c) forward code progress (the session's worktree branch HEAD
|
|
340
|
+
MOVED since the last poll — Rust compares the two newest presence facts' shas, the
|
|
341
|
+
Python reaper compares a cached `branch_head_sha`); (d) declared active work (a
|
|
342
|
+
live claim or authored mission/handoff).
|
|
343
|
+
|
|
344
|
+
**Two fail-directions, each on the safe side:**
|
|
345
|
+
- **Squad VISIBILITY projection** (Rust `snapshot_from_facts_with_policy`) is
|
|
346
|
+
FAIL-OPEN. A squad whose four signals are ALL provably stale is DROPPED from the
|
|
347
|
+
default `rally room` view; `--include-archived` restores it (mirrors the message
|
|
348
|
+
archive model). A `Live` OR `Unknown` (any absent/unparseable signal) verdict
|
|
349
|
+
KEEPS the squad visible — hiding a still-alive peer could cause the very
|
|
350
|
+
write-collision this system prevents.
|
|
351
|
+
- **Reaper REMOVAL** (presence-file unlink, claim/lead) stays FAIL-CLOSED — never
|
|
352
|
+
remove on a signal it cannot trust.
|
|
353
|
+
|
|
354
|
+
**tmux orphan reaper.** `rally sessions --reap` also detects DETACHED `rally-*`
|
|
355
|
+
tmux sessions whose last activity is past the adaptive window and which are not
|
|
356
|
+
tracked as managed sessions, kills them, and tombstones the reap (closing the gap
|
|
357
|
+
where `--reap` saw 0 of the real detached orphans). Attached sessions (a human is
|
|
358
|
+
looking) are never killed.
|
|
359
|
+
|
|
360
|
+
## Zombie-tmux prevention — three layers over ONE liveness model
|
|
361
|
+
|
|
362
|
+
Root cause of accreted zombie tmux sessions: rally `exec`s the agent, so a session
|
|
363
|
+
auto-closes when its agent EXITS, but agents that never exit (a disabled autonomy
|
|
364
|
+
poller, idle detached panes) leave the session forever — tmux has no native
|
|
365
|
+
idle/lifetime timeout. The fix is three layers, all reusing the single
|
|
366
|
+
`liveness::is_live` 4-signal model and the adaptive window above. NONE adds a
|
|
367
|
+
fixed/brute-force idle clock; lifetime follows real liveness/ownership.
|
|
368
|
+
|
|
369
|
+
**Layer 1 — completion-scoped self-exit (prevent at source).** `rally
|
|
370
|
+
self-exit-check --tool <self>` is a stateless re-check: a task-scoped session that
|
|
371
|
+
holds NO active claims AND for which `rally next` is non-actionable for a SUSTAINED
|
|
372
|
+
streak self-kills its own `rally-*` tmux session, so `exec` auto-closes it. The
|
|
373
|
+
streak (default 2 consecutive empty re-checks, `liveness::DEFAULT_SELF_EXIT_STREAK`)
|
|
374
|
+
is persisted in the session's OWN tmux env (`RALLY_SELFEXIT_STREAK`, dies with the
|
|
375
|
+
session — no new filesystem surface) so a brief lull between claims never exits
|
|
376
|
+
mid-task. **Opt-out:** `--persistent` short-circuits to "never self-exit" for a
|
|
377
|
+
deliberately-long-lived session. The existing `rally stop` self-kill remains the
|
|
378
|
+
explicit-completion path; Layer 1 adds the implicit "work done" path. Decision is
|
|
379
|
+
the shared `liveness::completion_self_exit_eligible(work_resolved,
|
|
380
|
+
next_empty_streak, required_streak, persistent_optout)`.
|
|
381
|
+
|
|
382
|
+
**Layer 2 — event-driven liveness-lease safety net.** `rally enter` (a new agent
|
|
383
|
+
joining) opportunistically sweeps detached `rally-*` orphan tmux sessions via the
|
|
384
|
+
SAME reaper Layer-3 logic, in addition to `rally sessions --reap`. Best-effort and
|
|
385
|
+
fail-open: it runs AFTER presence (so the entering agent's own session is in the
|
|
386
|
+
guard set), never blocks the enter path, and never raises. A live / parent-alive
|
|
387
|
+
session is never reaped. No daemon/cron (those would themselves need worktree
|
|
388
|
+
isolation). Both the enter sweep and `sessions --reap` call ONE shared actuator
|
|
389
|
+
(`sweep_orphan_tmux`).
|
|
390
|
+
|
|
391
|
+
**Layer 3 — parent-lifecycle binding.** At launch (`tmux_start_command`) the new
|
|
392
|
+
session's env is stamped with `RALLY_PARENT_PID=<launcher pid>` in the SAME atomic
|
|
393
|
+
`tmux new-session -e` call. The reaper reads it back (`show-environment`), probes
|
|
394
|
+
`kill -0 <pid>` (no new crate dependency — the repo keeps a zero-extra-dep
|
|
395
|
+
contract), and feeds the result to the shared `liveness::reapable(liveness,
|
|
396
|
+
parent_alive)`. This targets the exact failure mode here (autonomy poller died →
|
|
397
|
+
its child sessions orphaned).
|
|
398
|
+
|
|
399
|
+
**The single reaper-eligibility authority** is `liveness::reapable` (mirrored
|
|
400
|
+
Rust↔Python, asserted by the byte-identical `liveness_vectors.json` `reapable_cases`):
|
|
401
|
+
|
|
402
|
+
| liveness | parent_alive | reapable | rationale |
|
|
403
|
+
|----------|--------------|----------|-----------|
|
|
404
|
+
| Live | any | NO | any of 4 signals fresh → independently live |
|
|
405
|
+
| Unknown | any | NO | fail-closed: untrustworthy signals |
|
|
406
|
+
| Stale | alive | NO | stale by signals but a live parent may re-drive it (conservative) |
|
|
407
|
+
| Stale | dead | YES | the Layer-3 orphan target |
|
|
408
|
+
| Stale | none (no info)| YES | window criterion ALONE — fail-safe degradation |
|
|
409
|
+
|
|
410
|
+
**Fail-safe directions (binding):**
|
|
411
|
+
- A session making code progress / heartbeating on cadence / recently injected /
|
|
412
|
+
holding a live plan is NEVER reaped — that's `Live` → not reapable, regardless of
|
|
413
|
+
parent state.
|
|
414
|
+
- The control NEVER reaps on the parent criterion ALONE: parent-dead reaps only a
|
|
415
|
+
session that is ALSO `Stale` by the 4-signal liveness.
|
|
416
|
+
- Missing/unparseable parent info (`parent_alive = None`) degrades to the
|
|
417
|
+
liveness-window criterion alone (`Stale → reap`), preserving the pre-Layer-3
|
|
418
|
+
orphan-window behavior exactly — never reaped *because* the parent is unknown.
|
|
419
|
+
- `kill -0` failing for any reason other than "no such process" (e.g. EPERM) reads
|
|
420
|
+
ALIVE (a live-but-unsignalable process is never treated as dead).
|
|
421
|
+
|
|
153
422
|
## Idle-agent self-selection (rally facilitates, the agent decides)
|
|
154
423
|
|
|
155
|
-
**Rally is a facilitator, not an orchestrator.** It exposes room state (`rally room` / `rally next`), file-level deconfliction (`rally check before-write --path P`), and claims/handoffs. It does **not** assign
|
|
424
|
+
**Rally is a facilitator, not an orchestrator or verifier.** It exposes room state (`rally room` / `rally next`), file-level deconfliction (`rally check before-write --path P`), and claims/handoffs. It does **not** assign work, pick work, or verify code/release truth. A waiting agent runs this decision tree itself and chooses — the agent's LLM reasons over Rally's surfaced coordination records. This keeps coordination decentralized: no single point that hands out tasks (which would be a failure site and a bottleneck).
|
|
156
425
|
|
|
157
426
|
When an agent is idle and `rally next` returns no actionable item, walk the tree top-down, stop at the first match:
|
|
158
427
|
|
|
@@ -162,7 +431,7 @@ When an agent is idle and `rally next` returns no actionable item, walk the tree
|
|
|
162
431
|
4. **All coding candidates are claimed or conflicted** → do read-only research or assessment that helps and has zero file conflict (simplification scans of untouched areas, duplication/test-gap audits, docs the room needs).
|
|
163
432
|
5. **Nothing fits, or the only work left is risky/deferred/peer-exclusive** → stay idle and say so; do not start risky/deferred work, do not touch another session's claimed paths.
|
|
164
433
|
|
|
165
|
-
The tree is the guideline;
|
|
434
|
+
The tree is the guideline; Rally supplies coordination records (claims, collisions, pending items) each branch needs. Two same-tool agents running it independently land on different work because claim-first + `check before-write` makes the first claimant win and the second re-select — no central referee required.
|
|
166
435
|
|
|
167
436
|
## Coordination reliability (verify the room before trusting it)
|
|
168
437
|
|