@mu-crew/mu 1.6.0 → 3.0.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.md +6 -5
- package/README.md +31 -17
- package/dist/cli.js +3582 -2633
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +265 -137
- package/dist/index.js +3128 -2486
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +71 -45
- package/docs/ROADMAP.md +20 -105
- package/docs/USAGE_GUIDE.md +62 -41
- package/docs/VISION.md +11 -21
- package/docs/VOCABULARY.md +33 -40
- package/docs/img/tui-dashboard.png +0 -0
- package/docs/specs/2026-09-28-agent-state-from-murmur.md +333 -0
- package/docs/specs/2026-09-29-task-substates.md +247 -0
- package/package.json +1 -1
- package/skills/mu/REMOTE_WORKERS.md +31 -38
- package/skills/mu/SKILL.md +26 -25
- package/docs/.m-reflow-5f7exa_b.md +0 -509
- package/docs/.m-reflow-5v_034rw.md +0 -545
- package/docs/.m-reflow-7yaw8ubc.md +0 -513
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Task substates: classification without stranded dependents
|
|
2
|
+
|
|
3
|
+
Status: implemented 2026-09-29 (approved 2026-09-29; see [Deviations during implementation](#deviations-during-implementation))
|
|
4
|
+
Date: 2026-09-29
|
|
5
|
+
Supersedes the notes-only convention from `9e3c47b` ("tasks: reduce lifecycle to three states").
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`9e3c47b` removed `REJECTED` and `DEFERRED`. That fixed a real problem and created a new one.
|
|
10
|
+
|
|
11
|
+
The real problem: each old status mixed two independent facts into one value.
|
|
12
|
+
|
|
13
|
+
| old status | lifecycle phase | why | effect on dependents |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `REJECTED` | ended | won't do | still blocked |
|
|
16
|
+
| `DEFERRED` | not active | parked | still blocked |
|
|
17
|
+
|
|
18
|
+
Neither satisfied a `blocks` edge, so rejecting or deferring a task silently stranded its dependents. The guard against that refused the transition unless `--cascade` was given. `--cascade` then stamped the whole subtree, which was too aggressive (`bug_cascade_reject_too_aggressive`).
|
|
19
|
+
|
|
20
|
+
The new problem: with only `OPEN | IN_PROGRESS | CLOSED`, mu cannot tell "done" from "abandoned", and cannot keep a task out of the scheduler without closing it. The "why" lives only in free-text notes, which no view can filter on.
|
|
21
|
+
|
|
22
|
+
## Approach
|
|
23
|
+
|
|
24
|
+
Split the two facts into two columns.
|
|
25
|
+
|
|
26
|
+
- **`status`** is the lifecycle phase. It alone decides edge satisfaction: `CLOSED` satisfies a `blocks` edge, anything else does not. Unchanged.
|
|
27
|
+
- **`substate`** qualifies the status. It never touches edge semantics.
|
|
28
|
+
- **Blocked / ready** stay derived from edges in views. They are never stored.
|
|
29
|
+
|
|
30
|
+
Rule: **store intent, derive graph facts.** Only what a person or agent decides is stored.
|
|
31
|
+
|
|
32
|
+
| status | substates | default |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `OPEN` | `todo`, `parked` | `todo` |
|
|
35
|
+
| `IN_PROGRESS` | `active` | `active` |
|
|
36
|
+
| `CLOSED` | `done`, `wontfix`, `duplicate`, `superseded` | `done` |
|
|
37
|
+
|
|
38
|
+
`substate` is never null. Every status has one named default, so an absent value never carries meaning.
|
|
39
|
+
|
|
40
|
+
Consequences:
|
|
41
|
+
|
|
42
|
+
- `CLOSED/wontfix` (and any other close) unblocks dependents. The close reports which dependents it unblocked. Nothing gets stranded, so no cascade is needed.
|
|
43
|
+
- `OPEN/parked` leaves the scheduler: it is excluded from `ready` and `next`, and `claim` refuses it without `--force`. It stays in `goals`, because tracks are built from goals and a parked goal must not make its subtree vanish; a track whose non-closed tasks are all parked is marked parked instead. Its dependents stay ordinarily blocked, and surfaces say "blocked by parked X".
|
|
44
|
+
- A closed blocker now means "stop waiting", not "the prerequisite work exists". `mu task wait` and `close --if-ready` fire on any `CLOSED/*` blocker. This is intended, and it is why every non-`done` close prints the dependents it unblocked.
|
|
45
|
+
|
|
46
|
+
## Key decisions
|
|
47
|
+
|
|
48
|
+
| # | Decision | Why |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| D1 | Two columns (`status` + `substate`), not more statuses. | Keeps a single rule for edge semantics; classification can grow without touching the DAG. |
|
|
51
|
+
| D2 | Parked is an `OPEN` substate, not a status. | The dependency is still real: B still waits on A. Only scheduling changes. |
|
|
52
|
+
| D3 | Non-`done` closes unblock dependents. | Blocking caused the stranding problem last time. The report keeps the effect visible. |
|
|
53
|
+
| D4 | `substate` is never null; each status has a named default. | An absent value must not carry meaning. |
|
|
54
|
+
| D5 | Valid pairs are enforced by a lookup table plus a composite foreign key `DEFERRABLE INITIALLY DEFERRED`. | Enforces the rule across both columns; tolerates sync's one-field-at-a-time writes, because the check runs at commit; adding a value is an `INSERT`, not a table rebuild. Verified in SQLite (see Appendix A). |
|
|
55
|
+
| D6 | Drop the `status` CHECK. The lookup table is the single list. | Two lists would drift. |
|
|
56
|
+
| D7 | Every lifecycle op writes `status` and `substate` together. | Both fields carry the same HLC, so per-field last-write-wins cannot split a pair from our own ops. |
|
|
57
|
+
| D8 | Apply resolves the pair before writing: a status-only op, or an unknown substate from a newer peer, falls back to the status default. The original payload stays in `ops`. | The foreign key fails the whole commit. Version skew must not break sync; the foreign key is there to catch our own bugs. |
|
|
58
|
+
| D9 | Non-`done` close and `park` require `--why`, stored as an ordinary note in the same transaction. | Keeps the rationale, the one good part of the notes-only convention. |
|
|
59
|
+
| D10 | Start with `parked` as the only non-default `OPEN` substate. No `--until`. | YAGNI. mu has no daemon to run the automatic un-park. Adding `triage` later is one seeded row. |
|
|
60
|
+
| D11 | One shared legacy mapping: `REJECTED → CLOSED/wontfix`, `DEFERRED → OPEN/parked`. | Replaces `normalizeTaskStatus` (currently folds both onto `OPEN`). Apply, rebuild, undo and migration all use it, so recovery happens on every path. |
|
|
61
|
+
| D12 | Migrate v10 → v11 through `scripts/migrate.ts`, not in `openDb`. | Keeps the "production never migrates in place" rule. SQLite cannot add a foreign key to an existing table, so a new target is required anyway. |
|
|
62
|
+
|
|
63
|
+
## Schema (v11)
|
|
64
|
+
|
|
65
|
+
```sql
|
|
66
|
+
CREATE TABLE IF NOT EXISTS task_substates (
|
|
67
|
+
status TEXT NOT NULL,
|
|
68
|
+
substate TEXT NOT NULL,
|
|
69
|
+
is_default INTEGER NOT NULL DEFAULT 0 CHECK (is_default IN (0, 1)),
|
|
70
|
+
PRIMARY KEY (status, substate)
|
|
71
|
+
);
|
|
72
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_substates_one_default
|
|
73
|
+
ON task_substates (status) WHERE is_default = 1;
|
|
74
|
+
|
|
75
|
+
-- tasks gains:
|
|
76
|
+
substate TEXT NOT NULL,
|
|
77
|
+
FOREIGN KEY (status, substate) REFERENCES task_substates (status, substate)
|
|
78
|
+
DEFERRABLE INITIALLY DEFERRED
|
|
79
|
+
-- and loses: CHECK (status IN ('OPEN', 'IN_PROGRESS', 'CLOSED'))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `applySchema` seeds the pairs with `INSERT OR IGNORE`, before any task write.
|
|
83
|
+
- `CURRENT_SCHEMA_VERSION = MIN_ACCEPTED_SCHEMA_VERSION = 11`.
|
|
84
|
+
- `EXPECTED_TABLES` gains `task_substates` (11 entries).
|
|
85
|
+
- Views:
|
|
86
|
+
- `ready`: add `AND t.substate <> 'parked'`.
|
|
87
|
+
- `blocked`: unchanged.
|
|
88
|
+
- `goals`: unchanged (see Consequences: tracks mark parked instead).
|
|
89
|
+
- `SchemaTooOldError.errorNextSteps` names `scripts/migrate.ts`.
|
|
90
|
+
- New `SchemaTooNewError` (exit 4): `openDb` refuses a DB newer than `CURRENT_SCHEMA_VERSION`. v10 has no such guard, so a v10 binary opening a v11 DB fails only when it writes (NOT NULL / FK). The guard protects v12 and later; for v11 the operator must switch every binary before the swap (see Restoring destroyed workstreams).
|
|
91
|
+
- `substate` has no column DEFAULT, so a writer that doesn't know about it fails loudly instead of guessing.
|
|
92
|
+
|
|
93
|
+
`src/tasks/status.ts` mirrors the table:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
export const TASK_SUBSTATES = {
|
|
97
|
+
OPEN: ["todo", "parked"],
|
|
98
|
+
IN_PROGRESS: ["active"],
|
|
99
|
+
CLOSED: ["done", "wontfix", "duplicate", "superseded"],
|
|
100
|
+
} as const satisfies Record<TaskStatus, readonly string[]>;
|
|
101
|
+
export const DEFAULT_SUBSTATE = { OPEN: "todo", IN_PROGRESS: "active", CLOSED: "done" } as const;
|
|
102
|
+
export function resolvePair(status: string, substate: unknown): { status: TaskStatus; substate: TaskSubstate };
|
|
103
|
+
export function mapLegacyStatus(value: string): { status: TaskStatus; substate: TaskSubstate } | null;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The seed rows are generated from `TASK_SUBSTATES`. A test asserts that the seeded table and the map are identical.
|
|
107
|
+
|
|
108
|
+
## Sync and ops
|
|
109
|
+
|
|
110
|
+
- `capture.ts` `CAPTURED_COLUMNS.tasks` and `apply.ts` `APPLIABLE_COLUMNS.tasks` gain `substate`.
|
|
111
|
+
- Every lifecycle verb writes `{status, substate}` in one op payload.
|
|
112
|
+
- `applyTaskPut` projects the payload in this order:
|
|
113
|
+
1. Map legacy status values through `mapLegacyStatus`. This can set both fields.
|
|
114
|
+
2. Apply per-field LWW as today.
|
|
115
|
+
3. Read the resulting row pair. If it is invalid, write the default substate for the status. The repair is deterministic, so every peer converges on the same row without an extra op.
|
|
116
|
+
- `undo.ts` `restoreRow` uses the same mapping and repair.
|
|
117
|
+
- v10 peers drop `substate` as an unknown field and keep working. Status-only ops from v10 peers are repaired by step 3.
|
|
118
|
+
|
|
119
|
+
## CLI and SDK surface
|
|
120
|
+
|
|
121
|
+
| Command | Effect |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `mu task close <id> [--as done\|wontfix\|duplicate\|superseded] [--why "..."]` | `CLOSED/<as>`; default `done`. `--why` required unless `done`. Prints unblocked dependents when `--as` is not `done`. |
|
|
124
|
+
| `mu task park <id> --why "..."` | `OPEN/todo → OPEN/parked`. Refuses `IN_PROGRESS` (next step: `release` first) and `CLOSED` (next step: `open` first). Idempotent on `OPEN/parked`. |
|
|
125
|
+
| `mu task unpark <id>` | `OPEN/parked → OPEN/todo`. No-op on other pairs. |
|
|
126
|
+
| `mu task open <id>` | `→ OPEN/todo` from any pair. |
|
|
127
|
+
| `mu task claim <id>` | `→ IN_PROGRESS/active`. Refuses `OPEN/parked` without `--force`. |
|
|
128
|
+
| `mu task release <id>` | `IN_PROGRESS/active → OPEN/todo`. `--reopen` from `CLOSED/*` also gives `OPEN/todo`. |
|
|
129
|
+
| `mu task list --substate <name>` | Filter. Composes with `--status`. |
|
|
130
|
+
| `mu task show`, `list`, `tree`, `next`, `mu state` | Render `STATUS/substate` when the substate is not the default, for example `OPEN/parked` or `CLOSED/wontfix`. Show the bare status for defaults. JSON always carries both fields. |
|
|
131
|
+
|
|
132
|
+
- `mu task show` blocked output names the blocker's substate: "blocked by X (OPEN/parked)".
|
|
133
|
+
- SDK exports: `TaskSubstate`, `TASK_SUBSTATES`, `DEFAULT_SUBSTATE`, `parkTask`, `unparkTask`, and `closeTask(…, { as, why })`. `TaskRow` gains `substate`.
|
|
134
|
+
- Tracks: a track whose non-closed tasks are all parked is marked parked and is not counted as available parallel work.
|
|
135
|
+
|
|
136
|
+
## TUI
|
|
137
|
+
|
|
138
|
+
The TUI only reads data; it has no commands that change tasks. Only `recent.tsx` references `closeTask`, and only to describe it. So the TUI work is display, filtering and yank strings.
|
|
139
|
+
|
|
140
|
+
| Surface | File | Change |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| Colours | `src/cli/format.ts` `colorStatus`, `inkColorForStatus` | Take the pair. `OPEN/parked` gray; `CLOSED/done` green; other `CLOSED/*` red/dim. `InkColor` gains `gray` and `red` again. |
|
|
143
|
+
| Status label | `list-row.tsx`, `all-tasks.tsx:243`, `task-detail.tsx`, DAG `renderForest` | Render `STATUS/substate` for non-defaults, sharing one formatting helper with the CLI. |
|
|
144
|
+
| Filter strip | `use-status-filter.tsx`, `keymap-spec.ts` | Keep `o/i/c`. Add `p`, which toggles parked tasks within `OPEN`, and `w`, which toggles non-`done` closes within `CLOSED`. Default: all visible. Update the `DAG_HINTS` / `ALL_TASKS_HINTS` clusters and the help pane; `tui-help-overlay.test.ts` enforces that they match. |
|
|
145
|
+
| Ready card | `cards/ready.tsx:72` | Parked tasks drop out via the view. Empty-state text: mention parked when some exist, e.g. "(no ready tasks) 3 parked". |
|
|
146
|
+
| Blocked card / popup | `cards/blocked.tsx`, `popups/blocked.tsx` | `stillGating` unchanged (not `CLOSED`). Annotate gating blockers that are parked: "← X (parked)". |
|
|
147
|
+
| Tracks popup | `popups/tracks.tsx` `statusRank` | Order: `IN_PROGRESS`, `OPEN/todo`, `OPEN/parked`, `CLOSED/*`. Mark all-parked tracks. |
|
|
148
|
+
| Recent card | `cards/recent.tsx` | Show the close substate: "closed (wontfix)". |
|
|
149
|
+
| Yank | `popups/ready.tsx` `yankCommandForTask` | `OPEN/parked` → `mu task unpark …`. `CLOSED/*` → `mu task open …` (unchanged). |
|
|
150
|
+
| Glyphs | `src/glyphs.ts` | Add a `parked` glyph for dense rows. The status text stays the source of truth. |
|
|
151
|
+
|
|
152
|
+
## Migration (`scripts/migrate.ts`)
|
|
153
|
+
|
|
154
|
+
Extend the existing sidecar to target v11. It already accepts v7–v9; add v10. The default output suffix becomes `.v11.db`.
|
|
155
|
+
|
|
156
|
+
**v10 → v11.** Use `rebuildInto` as the v9 path does: copy the full ops log, then replay it through the v11 apply path. Recovery mostly comes for free, because the legacy payloads are still in the log:
|
|
157
|
+
|
|
158
|
+
1. Ops carrying `REJECTED` / `DEFERRED` project through `mapLegacyStatus` to `CLOSED/wontfix` / `OPEN/parked`.
|
|
159
|
+
2. Normal LWW decides the result. If a later op wrote the status, that later write wins and nothing is recovered, which is the "later change wins" rule.
|
|
160
|
+
When looking for the last status writer, ignore ops with intent `undo` or `migrate.substate`. An undo restore replays an older value; it is not a new decision.
|
|
161
|
+
3. **Fallback from `MIGRATION:` notes.** If a task has a `MIGRATION: previous status was X` note, currently projects as `OPEN/todo`, and no status-writing op is newer than the note's `created_at`, append one `migrate.substate` op setting the mapped pair.
|
|
162
|
+
4. Carry machine-local rows (agents, workspaces, owners) exactly as the v9 path does.
|
|
163
|
+
|
|
164
|
+
**v7–v9 → v11.** Unchanged, except that the target is v11. The shared mapping now yields substates directly. Stop appending `MIGRATION:` notes: the substate carries the information.
|
|
165
|
+
|
|
166
|
+
**Report.** In addition to the current output, list every recovered task with its old and new pair, and for each `CLOSED/wontfix` recovery, the dependents it unblocked.
|
|
167
|
+
|
|
168
|
+
`scripts/README.md`: rename the section to "v7–v10 to v11", update the recipe paths, and document the recovery rules above.
|
|
169
|
+
|
|
170
|
+
## Restoring destroyed workstreams
|
|
171
|
+
|
|
172
|
+
Run this after the v11 swap, using ordinary `mu undo`, so the undo ops sync and appear in `mu log`. `restoreRow` uses the shared mapping, so restored tasks come back as `OPEN/parked` or `CLOSED/wontfix`, not as `OPEN`.
|
|
173
|
+
|
|
174
|
+
A tombstone written after the v10 migration may hold the already-folded `OPEN`, not the legacy value, so mapping alone recovers nothing. The migration's recovery pass (steps 1–3 above) is therefore a shared function, `recoverLegacySubstates(db, workstream)`. After restoring, run it on each restored workstream with `npx tsx scripts/migrate.ts --recover <db> -w <ws>`. It uses the same last-status-writer rule and writes `migrate.substate` ops.
|
|
175
|
+
|
|
176
|
+
Current live DB (read-only check, 2026-09-29):
|
|
177
|
+
|
|
178
|
+
- 69 tasks with legacy statuses in history, all in 11 destroyed workstreams: `feedback`, `gchatui`, `gchatui-node` (2 destroy groups), `infer-rs`, `modelbridge` (teardown), `mufeedback`, `mufeedback-v03`, `multimachine`, `roadmap-v0-2`, `surface-audit`, `tui-impl`.
|
|
179
|
+
- 42 workstreams in total are tombstoned; only 3 are live.
|
|
180
|
+
- Dry runs of `mu undo 6f3d12e7` and `mu undo f34e2141` on v10 both plan full restores.
|
|
181
|
+
|
|
182
|
+
Scope: all 42 tombstoned workstreams, not only the 11 with legacy tasks.
|
|
183
|
+
|
|
184
|
+
Recipe:
|
|
185
|
+
|
|
186
|
+
1. For each destroy/teardown group, oldest first, run `mu undo <group>` (dry run). Record conflicts. A workstream destroyed more than once (e.g. `gchatui-node`) needs its groups undone newest first.
|
|
187
|
+
2. Apply with `--yes`. Do not use `--force`; resolve any superseded groups by hand.
|
|
188
|
+
3. Run `mu doctor --deep`, then `mu task list --substate parked` and `--substate wontfix` per workstream, and compare against the migration report.
|
|
189
|
+
|
|
190
|
+
## Testing
|
|
191
|
+
|
|
192
|
+
- **Schema:** fresh DB seeds pairs; invalid pair, unknown value, and deleting a pair that is in use all fail; per-field writes inside one transaction commit; one default per status.
|
|
193
|
+
- **Map parity:** `TASK_SUBSTATES` equals the seeded rows.
|
|
194
|
+
- **Lifecycle:** each verb's resulting pair; `park` refuses `IN_PROGRESS` and `CLOSED` with next steps; `claim` on parked refuses without `--force`; `--why` is required; the unblocked-dependents report is correct.
|
|
195
|
+
- **Views:** parked tasks are excluded from `ready` and stay in `goals`; a dependent of a parked task is in `blocked`; a dependent of `CLOSED/wontfix` is `ready`.
|
|
196
|
+
- **Apply:** status-only op (v10 peer) repairs to the default; unknown substate falls back; legacy payloads map; the same op sequence converges on two DBs.
|
|
197
|
+
- **Undo:** restoring a tombstoned legacy task yields the mapped pair; undoing a park restores the prior pair.
|
|
198
|
+
- **Migration:** v10 fixtures with (a) a legacy op and no later status op → recovered; (b) a later `task.open` → not recovered; (c) a note but truncated ops → fallback fires; (d) v9 source → v11 directly. `mu doctor --deep` reports zero drift on the target.
|
|
199
|
+
- **TUI:** filter-key parity test; `yankCommandForTask` for parked; colour and label helper cases; ready empty-state text.
|
|
200
|
+
|
|
201
|
+
## Out of scope
|
|
202
|
+
|
|
203
|
+
- Task kinds (bug / feature / chore) and free-form labels.
|
|
204
|
+
- `--until` automatic un-parking.
|
|
205
|
+
- `triage` / `needs-info` substates (one seeded row each, later).
|
|
206
|
+
- Substates for `IN_PROGRESS` beyond `active`.
|
|
207
|
+
- Blocking semantics for any closed substate.
|
|
208
|
+
|
|
209
|
+
## Resolved questions
|
|
210
|
+
|
|
211
|
+
1. **Restore scope:** restore all 42 tombstoned workstreams, then run legacy recovery on each.
|
|
212
|
+
2. **Park on `IN_PROGRESS`:** refuse. The owner must `release` first. One verb, one transition.
|
|
213
|
+
3. **TUI keys:** `p` and `w` accepted.
|
|
214
|
+
|
|
215
|
+
## Implementation checklist
|
|
216
|
+
|
|
217
|
+
1. `status.ts`: `TASK_SUBSTATES`, `DEFAULT_SUBSTATE`, `resolvePair`, `mapLegacyStatus`; delete `normalizeTaskStatus`.
|
|
218
|
+
2. `db.ts`: v11 schema, seed, view changes, `EXPECTED_TABLES`, `SchemaTooOldError` next steps.
|
|
219
|
+
3. `capture.ts` / `apply.ts` / `undo.ts`: carry `substate`, apply-time mapping and repair.
|
|
220
|
+
4. Lifecycle verbs and SDK: `close --as/--why`, `park`, `unpark`, claim guard, unblocked-dependents report.
|
|
221
|
+
5. CLI rendering, `--substate` filter, JSON fields, tracks marking.
|
|
222
|
+
6. TUI changes per the table above.
|
|
223
|
+
7. `scripts/migrate.ts`: v10 → v11 path, `recoverLegacySubstates`, `--recover` mode, report; `scripts/README.md`.
|
|
224
|
+
8. Docs: `VOCABULARY.md` (substate, park, resolution values), `skills/mu/SKILL.md`, `CHANGELOG.md` (Breaking: v11; Added: substates).
|
|
225
|
+
9. Operator step: migrate the live DB, then restore workstreams per the recipe.
|
|
226
|
+
|
|
227
|
+
## Deviations during implementation
|
|
228
|
+
|
|
229
|
+
Recorded after tasks 1–7 landed. Where this section and the text above disagree, this section describes the code.
|
|
230
|
+
|
|
231
|
+
1. **Substate is resolved from the log, not by per-field LWW** (task 3). [Sync and ops](#sync-and-ops) step 2–3 described per-field LWW plus a row-only repair. That diverges by arrival order: a repair that reads only the row bakes the fallback into it, and a later status change keeps the fallback on one peer and the real substate on another. `repairTaskPair(db, rowId, pending?)` in `src/apply.ts` instead takes the substate from the newest op in the log that writes one (an explicit substate, a legacy status implying its mapped pair, or a status-only op implying the status default), then resolves it against the row's current status. The result is a pure function of the op set, so every peer converges and the repair records no op. `applyOp` wraps each task put in `db.transaction` so the deferred FK sees a complete pair. `normalizeTaskStatus` and `RETIRED_STATUSES` are deleted.
|
|
232
|
+
2. **A second `updated_at`-only task op may share the group** (tasks 4, 5). `close --why` and `park` insert a note, and the note's parent touch writes a separate task op carrying only `updated_at` when the millisecond clock ticks between the two writes. It sits in the same group, so undo and log rendering are unaffected. Tests count ops that carry `status` or `substate`, not all task ops.
|
|
233
|
+
3. **Ready empty-state text is shorter** (task 6): `(no ready tasks) N parked · the rest are blocked or closed`, truncated at the card edge. The longer text in [TUI](#tui) wrapped over the card's border. The Recent card shows the pair (`CLOSED/wontfix`) rather than "closed (wontfix)". `inkColorForStatus` is removed in favour of `inkColorForPair`; `loadFullDag` gained an `include` predicate so the root module does not import the TUI filter.
|
|
234
|
+
4. **Migration and drift** (task 7). `src/drift.ts` compares `substate`, so `mu doctor --deep` sees substate drift. The v7/v8/v9 paths no longer write `MIGRATION:` notes. The migration report lists a third source, `replay`, for pairs the apply path derived during replay, alongside `ops` and `note` for captured `migrate.substate` recoveries. Recovery candidates are `OPEN/todo` tasks only, and a task that already has a `migrate.substate` op is skipped, which makes `--recover` idempotent. The v10 path turned out to need captured recovery only for the undo-hidden and note-only cases; replay already maps the rest.
|
|
235
|
+
5. **`SchemaTooNewError`** (task 2). `openDb` refuses a DB whose `schema_version` is newer than `CURRENT_SCHEMA_VERSION` (exit 4), before `applySchema`. Without it, a v10 binary would open a v11 DB and fail at commit on its first write. `tasks.substate` has no `DEFAULT`, so an old INSERT fails loudly rather than silently choosing a substate.
|
|
236
|
+
|
|
237
|
+
## Appendix A: foreign key verification
|
|
238
|
+
|
|
239
|
+
Tested in SQLite with `PRAGMA foreign_keys=ON` and the deferred composite foreign key:
|
|
240
|
+
|
|
241
|
+
| test | result |
|
|
242
|
+
|---|---|
|
|
243
|
+
| Change `status`, then `substate`, in two `UPDATE`s inside one transaction | committed |
|
|
244
|
+
| `CLOSED` + `parked` | rejected |
|
|
245
|
+
| unknown substate `bogus` | rejected |
|
|
246
|
+
| delete an allowed pair that a task uses | rejected |
|
|
247
|
+
| `ALTER TABLE … ADD CONSTRAINT FOREIGN KEY` on an existing table | syntax error (hence D12) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mu-crew/mu",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A persistent, observable crew of pi agents running in one tmux or herdr session per workstream, coordinated through a built-in task DAG.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -10,7 +10,8 @@ the traps below it are the part that costs time.
|
|
|
10
10
|
**If you read one thing:** on a session-capped host, never leave an
|
|
11
11
|
attach pane open — it holds the only ssh channel and silently breaks
|
|
12
12
|
`git fetch`, `murmur collect` and every other ssh. Attach to look, then
|
|
13
|
-
`mu agent close`. See § Never leave an attach pane open.
|
|
13
|
+
`mu agent close`. See § Never leave an attach pane open. This guidance
|
|
14
|
+
concerns interactive attachment, not mu's automatic agent-state reads.
|
|
14
15
|
|
|
15
16
|
---
|
|
16
17
|
|
|
@@ -19,8 +20,8 @@ attach pane open — it holds the only ssh channel and silently breaks
|
|
|
19
20
|
**The pane is local, the process is remote.** `mu agent spawn
|
|
20
21
|
--command 'ssh <host> -t "..."'` starts an ordinary tmux pane whose
|
|
21
22
|
foreground process happens to be ssh. Everything mu does is
|
|
22
|
-
pane-shaped, so `mu agent send`, `mu agent read`,
|
|
23
|
-
|
|
23
|
+
pane-shaped, so `mu agent send`, `mu agent read`, and the reaper work
|
|
24
|
+
unchanged across the network. murmur reports the remote process's agent state.
|
|
24
25
|
|
|
25
26
|
**One orchestrator DB; panes may be remote.** All state stays on your
|
|
26
27
|
machine. Do not run a second mu on the host to "coordinate": ownership
|
|
@@ -65,8 +66,8 @@ the only ssh channel**. While it is open, `git fetch`, `git push`,
|
|
|
65
66
|
`Permission denied (keyboard-interactive)` error.
|
|
66
67
|
|
|
67
68
|
**Attach to look, then close immediately.** `mu agent close <name>`
|
|
68
|
-
detaches without stopping a detached-tmux agent.
|
|
69
|
-
|
|
69
|
+
detaches without stopping a detached-tmux agent. mu reads agent state
|
|
70
|
+
through murmur automatically; use mule for orchestrator commands because
|
|
70
71
|
its separate ControlPath cannot contend with the attach pane. See
|
|
71
72
|
§ When the host limits concurrent sessions for diagnosis and the
|
|
72
73
|
remote-agent setup.
|
|
@@ -140,39 +141,30 @@ Local and remote agents mix freely in one workstream. The DAG, tracks,
|
|
|
140
141
|
— task status is a row in YOUR database, written by you, so a wait on
|
|
141
142
|
it is exact.
|
|
142
143
|
|
|
143
|
-
**Agent
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
**Agent state comes from murmur.** Its extension runs inside pi on the
|
|
145
|
+
host and reports state without reading the nested tmux screen. mu reads
|
|
146
|
+
murmur's remote rows and caches them for 10 seconds. Remote rows can lag
|
|
147
|
+
by murmur's collect floor (30 seconds ± 10 seconds) plus that cache;
|
|
148
|
+
their `since` value is the row's `updated_at`, so idle and stall age is
|
|
149
|
+
approximate.
|
|
148
150
|
|
|
149
151
|
| waiting on | remote? |
|
|
150
152
|
| --- | --- |
|
|
151
153
|
| `mu task wait` (task status) | exact — a DB poll, once something closes the task |
|
|
152
154
|
| the reaper | fires, but see below |
|
|
153
|
-
| stall detection |
|
|
154
|
-
| `mu agent wait --first` |
|
|
155
|
+
| stall detection | exact state from murmur; approximate age |
|
|
156
|
+
| `mu agent wait --first` | exact state from murmur |
|
|
155
157
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
`
|
|
159
|
-
`--timeout`; see `mu task wait --help` for stall controls.
|
|
158
|
+
Without murmur, remote agent state is `unknown`. `mu agent wait` does not
|
|
159
|
+
fire on `unknown`, and `unknown` never counts as a task stall. Run
|
|
160
|
+
`mu doctor` for the missing-source reason.
|
|
160
161
|
|
|
161
|
-
The reaper is right for a
|
|
162
|
+
The reaper is right for a direct spawn — the connection dying really
|
|
162
163
|
does kill that agent — and wrong for a detached-tmux one, where the
|
|
163
164
|
agent outlives the ssh but mu reaps the task anyway. See § A dropped
|
|
164
165
|
connection reaps the task but NOT the commit.
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
extension runs inside pi ON THE HOST and pushes state, so nothing is
|
|
168
|
-
scraped and no ssh hop distorts it:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
murmur collect && murmur status --json # activity is host-reported
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
That is the division worth remembering: mu owns the work, murmur owns
|
|
175
|
-
what the agent is doing.
|
|
167
|
+
The division is: mu owns the work; murmur reports what the agent is doing.
|
|
176
168
|
|
|
177
169
|
### On step 2 — the note is load-bearing
|
|
178
170
|
|
|
@@ -361,17 +353,18 @@ respawn.
|
|
|
361
353
|
|
|
362
354
|
## mu and murmur, and what you lose without it
|
|
363
355
|
|
|
364
|
-
[murmur](https://github.com/mu-crew/murmur)
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
remote recipe and `git fetch` collection
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
356
|
+
[murmur](https://github.com/mu-crew/murmur) provides agent state for
|
|
357
|
+
mu's tmux backend. Without it, mu reports agent state as `unknown`.
|
|
358
|
+
The DAG, claims, task completion waits, workspaces, spawn, send, read,
|
|
359
|
+
the reaper, this remote recipe, and `git fetch` collection still work.
|
|
360
|
+
Agent-state consumers such as `mu agent wait`, idle flags, and task-stall
|
|
361
|
+
detection wait rather than treating `unknown` as completion or a stall.
|
|
362
|
+
|
|
363
|
+
murmur also provides the tmux status pills, `prefix+a`, attachment hints,
|
|
364
|
+
and `murmur peer list` for host reachability. The division is: **mu owns
|
|
365
|
+
the work; murmur reports what an agent is doing.** murmur never places
|
|
366
|
+
work. Its interfaces used by mu are in murmur's
|
|
367
|
+
[stable contract](https://github.com/mu-crew/murmur/blob/main/ARCHITECTURE.md#contract).
|
|
375
368
|
|
|
376
369
|
| question | ask |
|
|
377
370
|
| --- | --- |
|
package/skills/mu/SKILL.md
CHANGED
|
@@ -32,12 +32,12 @@ both. `--json` exists on every verb:
|
|
|
32
32
|
- **agent** — named worker in a pane (you may be one).
|
|
33
33
|
- **mux** — the multiplexer mu drives: tmux, or herdr. One per
|
|
34
34
|
invocation. `mu doctor` names the active one; `MU_MUX` forces it.
|
|
35
|
-
Spawn, send,
|
|
36
|
-
|
|
35
|
+
Spawn, send, and read work on both. Agent state comes from murmur
|
|
36
|
+
on tmux and from herdr on herdr; `mu agent kick` is Linux-only on herdr.
|
|
37
37
|
- **task** — DAG node with mandatory `impact` (1–100) and
|
|
38
|
-
`effort_days`.
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
`effort_days`. Shown as status/substate: `OPEN/todo|parked`,
|
|
39
|
+
`IN_PROGRESS/active`, `CLOSED/done|wontfix|duplicate|superseded`.
|
|
40
|
+
Any `CLOSED/*` satisfies `--blocked-by`.
|
|
41
41
|
- **claim / release** — atomic take/clear of `tasks.owner`.
|
|
42
42
|
- **note** — append-only task context; survives sessions.
|
|
43
43
|
- **track** — independent DAG subtree; don't spawn more agents than
|
|
@@ -98,8 +98,9 @@ that a refusal with `--strict-staleness`.
|
|
|
98
98
|
### Remote agents
|
|
99
99
|
|
|
100
100
|
Agents can run on another machine: the PANE is local, the PROCESS is
|
|
101
|
-
remote (`--command 'ssh <host> -t "..."'`), so `send`, `read`,
|
|
102
|
-
|
|
101
|
+
remote (`--command 'ssh <host> -t "..."'`), so `send`, `read`, and the
|
|
102
|
+
reaper keep working unchanged. murmur reports the remote agent's state.
|
|
103
|
+
**One orchestrator
|
|
103
104
|
DB; panes may be remote** — never run a second mu on the host, since
|
|
104
105
|
`tasks.owner_id` is an FK into the machine-local `agents` table and a
|
|
105
106
|
remote mu could not claim your tasks anyway. You create the remote
|
|
@@ -158,7 +159,7 @@ Every turn:
|
|
|
158
159
|
`mu task claim <id> -w <ws> --for <agent> --evidence "..."`.
|
|
159
160
|
If no task exists, `mu task add` first; include initial context with
|
|
160
161
|
`--note 'REPRO: ...\nSCOPE: ...'` when the title alone is not
|
|
161
|
-
enough. Agent
|
|
162
|
+
enough. Agent state is runtime observation; task ownership is durable and
|
|
162
163
|
waitable.
|
|
163
164
|
4. Send terse instructions: task id, files/notes to read, workspace
|
|
164
165
|
path, validation command, scope guards, task note contract.
|
|
@@ -173,6 +174,7 @@ Every turn:
|
|
|
173
174
|
6. `mu task wait ... --first --on-stall exit --json`.
|
|
174
175
|
7. Cherry-pick the closed worker's **new** commit(s), verify the MERGE
|
|
175
176
|
(see below), return control. Do not barrier or loop in shell.
|
|
177
|
+
Only `CLOSED/done` ships; other closes: read the reason note.
|
|
176
178
|
8. Repeat from `mu state`.
|
|
177
179
|
|
|
178
180
|
## Dispatch rules that prevent real failures
|
|
@@ -210,8 +212,8 @@ Every turn:
|
|
|
210
212
|
conflict. Refresh workspaces between waves.
|
|
211
213
|
- Cross-workstream wait/claim uses qualified refs. The owner stays in its own
|
|
212
214
|
workstream; only task ownership crosses.
|
|
213
|
-
- For an idle worker,
|
|
214
|
-
`MU_IDLE_THRESHOLD_MS` defaults to 5m.
|
|
215
|
+
- For an idle worker, read the pane to learn why it stopped, then answer,
|
|
216
|
+
retry, or release its task. `MU_IDLE_THRESHOLD_MS` defaults to 5m.
|
|
215
217
|
- `mu agent kick` targets a wedged foreground subprocess. It refuses to signal
|
|
216
218
|
the wrapping CLI; close the agent if that is what must stop.
|
|
217
219
|
- Use `mu agent send`, not raw mux input: mu preserves literal text and confirms
|
|
@@ -231,6 +233,10 @@ Every turn:
|
|
|
231
233
|
5 timeout, 6 pane died.
|
|
232
234
|
- **`task close --if-ready`** no-ops until every blocker is CLOSED; bare
|
|
233
235
|
`task release` reopens IN_PROGRESS.
|
|
236
|
+
- **`task close --as wontfix --why ...`** unblocks dependents (listed in
|
|
237
|
+
the output). To keep dependents waiting, `task park --why` instead: parked
|
|
238
|
+
leaves `next`, and `claim` refuses it without `--force`. Park refuses
|
|
239
|
+
IN_PROGRESS — `task release` first.
|
|
234
240
|
- **For waits use `task wait`, not `log --tail`.** `--kind` is the operator's
|
|
235
241
|
log-ledger channel; `--intent` is what mu recorded.
|
|
236
242
|
- **`mu undo`** bare lists undoable actions with group ids; `<group>` previews;
|
|
@@ -291,24 +297,18 @@ Convention: `pi_mini` / `pi` / `pi_big`. Use mini for probing,
|
|
|
291
297
|
modest for build/edit/refactor, big for design/review/incidents.
|
|
292
298
|
Discover model strings with `pi --list-models [fuzzy-search]`.
|
|
293
299
|
|
|
294
|
-
## Reaper and
|
|
300
|
+
## Reaper and agent state
|
|
295
301
|
|
|
296
302
|
If an agent pane dies, or `mu agent close` kills it mid-task, owned
|
|
297
303
|
IN_PROGRESS tasks revert to OPEN with a `[reaper]` note and `task
|
|
298
304
|
reap` op. No manual release after crashes.
|
|
299
305
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
**
|
|
305
|
-
|
|
306
|
-
on the host rather than scraping a pane, and it answers across machines.
|
|
307
|
-
Optional and strictly additive: nothing here needs it, and the seam is
|
|
308
|
-
the env vars `mu agent spawn` already injects (`MU_MANAGED_AGENT`,
|
|
309
|
-
`MU_AGENT_NAME`, `MU_WORKSTREAM`), which murmur reads to mark a pane as
|
|
310
|
-
crew. **mu owns the work; murmur owns what an agent is doing.** See
|
|
311
|
-
[REMOTE_WORKERS.md](REMOTE_WORKERS.md) § mu and murmur.
|
|
306
|
+
Agent state comes from [murmur](https://github.com/mu-crew/murmur) on
|
|
307
|
+
tmux and from herdr on herdr. `unknown` means no state source; run
|
|
308
|
+
`mu doctor` for the reason. mu needs murmur only for agent state:
|
|
309
|
+
tasks, claims, workspaces, spawn, send, read, and task completion waits
|
|
310
|
+
work without it. **mu owns the work; murmur reports what an agent is
|
|
311
|
+
doing.** See [REMOTE_WORKERS.md](REMOTE_WORKERS.md) § mu and murmur.
|
|
312
312
|
|
|
313
313
|
For high-stakes decisions:
|
|
314
314
|
|
|
@@ -337,7 +337,8 @@ mu task note <id> "FILES: ...\nDECISION: ...\nVERIFIED: ..."
|
|
|
337
337
|
mu task close <id> --evidence "tests pass: ..." # LAST action
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
Skipping close makes the orchestrator's wait hang.
|
|
340
|
+
Skipping close makes the orchestrator's wait hang. Won't do it:
|
|
341
|
+
`close --as wontfix --why "..."`.
|
|
341
342
|
|
|
342
343
|
## Follow-on prompts
|
|
343
344
|
|
|
@@ -359,7 +360,7 @@ Sending to a BUSY agent is not delayed — that input queues normally.
|
|
|
359
360
|
|
|
360
361
|
## Guardrails
|
|
361
362
|
|
|
362
|
-
Task ownership outranks
|
|
363
|
+
Task ownership outranks runtime agent state. Coordinate through task notes and the
|
|
363
364
|
activity log. Keep edges within one workstream, role-name agents, and reserve
|
|
364
365
|
the `mu_` task-id prefix. Give workers bounded paths and commands; use `mu
|
|
365
366
|
agent kick` if a subprocess wedges.
|