dreamteamer 0.20.0 → 0.22.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.
@@ -0,0 +1,235 @@
1
+ # worktrees — a second checkout of this workspace, and the one verb that brings its records home
2
+
3
+ A **linked worktree** is a second working directory on the same repository: its own files, its own
4
+ branch, one shared object store. A session can work in one without touching the tree you are looking
5
+ at. `dt install` makes such a checkout ready; `dt land` brings its records back.
6
+
7
+ The whole design turns on one asymmetry. Two sessions writing records in the SAME directory is
8
+ conflict-BLIND — the second `dt set` on a record silently wins and nobody is told. Two sessions in
9
+ two worktrees make that same collision a **visible landing failure**, and the tree you were not
10
+ looking at is left byte-identical while you decide.
11
+
12
+ | the question | read |
13
+ |---|---|
14
+ | what a worktree costs to make ready | making one ready |
15
+ | add, list, get, rm | the four verbs |
16
+ | bringing the records home | landing |
17
+ | it refused — why | the refusals |
18
+ | it stopped on a conflict | the merge policy |
19
+ | a harness that cuts worktrees for you | hooks |
20
+ | `npm` is not on the hook's PATH | the shim |
21
+ | the other four harnesses | per harness |
22
+
23
+ ## making one ready
24
+
25
+ ```
26
+ dt install # in the worktree — engine, .env, local assets, git modules, compile, post-step
27
+ ```
28
+
29
+ A fresh worktree has no `node_modules`, no `.env`, no compiled runtime and no local assets. `dt
30
+ install` is idempotent and prints a board of what it found and what it did; run it again and every
31
+ line says *already*.
32
+
33
+ Three things it does that a bare `npm install` cannot:
34
+
35
+ - **`.env` is LINKED from the primary — but only when the worktree lies under the primary's root.**
36
+ A worktree elsewhere on the disk gets no credentials unless `--link-env` is passed for that run,
37
+ and the board says which capabilities are degraded without them. Credentials are not widened by
38
+ accident: a harness that keeps its worktrees under `~` gets an honest, credential-free checkout.
39
+ - **Declared local assets are linked, not copied.** `dreamteamer.local-assets` in a module's or the
40
+ workspace's `package.json` names gitignored heavy folders — model weights, browser profiles — and
41
+ the worktree gets a symlink to the primary's copy. A 1.5 GB download does not happen twice.
42
+ - **`dreamteamer.postinstall` runs LAST.** Deliberately not npm's `scripts.postinstall`: npm fires
43
+ that on every install, before the engine has compiled, and in contexts where a slow environment
44
+ step must not run at all.
45
+
46
+ ⚠ **A worktree of a workspace running a SHADOWED engine inherits the shadow.** If
47
+ `node_modules/dreamteamer` or a `git_modules/<name>` entry is a symlink, `dt add worktrees` mirrors
48
+ that link into the new tree — so the copy compiles with the same compiler as the tree it was cut
49
+ from. Real clones under `git_modules/` are per-checkout working state and are restored by `install`,
50
+ never linked.
51
+
52
+ ## the four verbs
53
+
54
+ ```
55
+ dt add worktrees --name <n> [--path <dir>] [--base <ref>] [--temp]
56
+ dt list worktrees [--json]
57
+ dt get worktrees/<name|path> [--json]
58
+ dt rm worktrees/<name|path> [--force]
59
+ ```
60
+
61
+ A worktree is **observed, never stored** — every row comes from `git worktree list` plus two cheap
62
+ reads. There is no record to drift from the truth.
63
+
64
+ `add` cuts the tree on branch `worktree-<n>`, runs `install` in it, and prints its absolute path
65
+ **last**, which is what a creation hook echoes. `--temp` is the sandbox form: detached, under
66
+ `.worktrees/.tmp-<rand>/<n>` inside the primary root, for a caller that will remove it.
67
+
68
+ ⚠ **`rm` REFUSES by default, and names why.** A worktree holds two things the primary cannot see:
69
+ records written but not committed, and commits not landed. `git worktree remove` knows about
70
+ neither. A detached worktree's commits are reachable from nothing but the HEAD about to be deleted,
71
+ so the guard measures orphanhood rather than branch-ahead-ness — and if that measurement cannot be
72
+ taken, it refuses rather than assuming zero.
73
+
74
+ ## landing
75
+
76
+ ```
77
+ dt land worktrees/<name|path> [--keep] [--dry-run] [--branch <n>] [--json]
78
+ ```
79
+
80
+ Run it **from the primary**. It rebases a COPY of the worktree's branch onto the primary branch,
81
+ fast-forwards, recompiles, and retires the worktree. `--dry-run` prints the plan and mutates
82
+ nothing. `--keep` leaves the worktree standing on the landed tip.
83
+
84
+ What it prints when it works:
85
+
86
+ ```
87
+ ✔ landed worktrees/w onto main: 1 commit(s)
88
+ notes: 1 record(s)
89
+ managed block regenerated: CLAUDE.md
90
+ primary recompiled
91
+ worktree and branch removed
92
+ ```
93
+
94
+ The order matters and each step exists for a measured reason:
95
+
96
+ 1. **One lock**, owned by the engine, at `<common-dir>/dreamteamer-land.lock`. A lock whose process
97
+ is gone is reclaimed out loud; a live one is refused with its pid and age.
98
+ 2. **A copy.** `land/<name>` is created from the worktree's branch and rebased in a throwaway
99
+ detached worktree. The branch you are standing on is never touched until the fast-forward
100
+ succeeds — which is what makes a failed landing leave every tree byte-identical.
101
+ 3. **Recompile on the copy's tip**, then commit the regenerated harness blocks pathspec-scoped.
102
+ `.dreamteamer/` is gitignored, so a fresh checkout has no runtime and `check` would otherwise
103
+ answer "no compiled runtime" for every landing.
104
+ 4. **`dt check` on the rebased tree.** A failure stops here: `land/<name>` is KEPT for inspection
105
+ and the message says the branch is rebased and NOT landed. (A compile *error* deletes the copy;
106
+ a check *failure* keeps it. Both are safe — the difference is whether there is anything to look
107
+ at.)
108
+ 5. **Fast-forward**, asked of `git merge-base --is-ancestor` rather than of git's English. If the
109
+ primary moved underneath, the copy is re-rebased and retried, three times, then refused.
110
+ 6. **`npm ci` in the primary** if the range changed `package-lock.json`, then **compile** there.
111
+ 7. **Retire** — but only after re-measuring. See the warning below.
112
+
113
+ ⚠ **A worktree that CHANGED during the landing is kept, not destroyed.** Cleanliness is measured
114
+ when the landing starts; a session inside that worktree can write a record while the rebase runs.
115
+ So the tree is measured again immediately before the destructive step, and if it changed it survives
116
+ with its own line: `worktree kept at <path> — it changed during the landing (<n> path(s)); land
117
+ again to pick them up`. Exit 0 — the landing itself succeeded. The window is milliseconds, not zero:
118
+ **a harness hook must never delete a dirty worktree on its own.**
119
+
120
+ `--json` carries `kept`, read from the disk after the work rather than inferred from the flags that
121
+ went in, so a script can tell a retired worktree from one that survived.
122
+
123
+ ⚠ **`--keep` recompiles the kept worktree, and a compile is a write.** The tree is reset to what
124
+ landed and then compiled, so if that compile produces a block even one byte different from the one
125
+ just committed, the worktree is left with a modified tracked `CLAUDE.md` — and the NEXT `dt land` of
126
+ it refuses with `1 uncommitted change(s)`. That is an ordinary system write, not a fault: commit it
127
+ in the worktree, or land without `--keep`.
128
+
129
+ ## the refusals
130
+
131
+ Each is printed under `✖ cannot land worktrees/<name>:` and each names its own fix:
132
+
133
+ | it says | it means |
134
+ |---|---|
135
+ | `it is the primary checkout` | you named the tree you are standing in |
136
+ | `it is DETACHED (no branch)` | give it one — the message prints both spellings, including `--branch <name>` |
137
+ | `<n> dirty record(s) — dt commit them first` | the worktree has records written but not published |
138
+ | `<n> uncommitted change(s)` | ordinary git dirt in the worktree |
139
+ | `branch <b> has an upstream (<u>)` | a pushed branch is not rebased; land it by hand |
140
+ | `the primary has <n> pending record write(s)` | ⚠ read from the primary's STORE, not from `git status` — a landing would sweep another session's unpublished records into it |
141
+ | `the primary has uncommitted changes to <n> file(s) this branch also touches` | resolve them first |
142
+ | `another land holds the lock (pid <p>, <age>)` | wait, or remove the lock if that process is gone |
143
+ | `nothing to land` | the branch is already on the primary branch |
144
+
145
+ `--branch <name>` must name the worktree it is applied to, and it never mutates under `--dry-run`
146
+ (the plan prints `would switch …` instead).
147
+
148
+ ## the merge policy
149
+
150
+ On a conflict, every conflicted path is classified and the classes are treated differently:
151
+
152
+ | class | how it is known | what happens |
153
+ |---|---|---|
154
+ | **the generated instruction block** | the path is exactly `CLAUDE.md`, `AGENTS.md` or `GEMINI.md`, AND every conflict hunk lies entirely between the block's begin/end markers | the primary's block wins, the rebase continues, and the block is regenerated once at the end |
155
+ | **anything else** | — | `rebase --abort`; the paths are listed grouped by collection; exit 1; **both trees exactly as they were** |
156
+
157
+ ⚠ **Matched by exact ROOT path, never by basename.** A nested `docs/CLAUDE.md` is a hand-written
158
+ file the compiler never regenerates; taking the primary's side of it would silently discard someone's
159
+ prose. And a hunk that touches the markers themselves, or lies outside the block, is an ordinary
160
+ conflict — the classifier errs toward surfacing.
161
+
162
+ There is no union merge. An append-only file is not append-only in practice: rows get corrected in
163
+ place, and two branches each appending would keep both. A log conflict is an ordinary conflict,
164
+ resolved by hand.
165
+
166
+ The Cursor rules file is a **whole-file generated output** with no block in it, so there is nothing
167
+ to take a side of; `init` gitignores `.cursor/` anyway. A workspace that un-ignores it gets an
168
+ ordinary conflict there and regenerates the file.
169
+
170
+ ## hooks
171
+
172
+ A harness can cut a worktree, or start a session in one, and call the engine at that moment. Print
173
+ the configuration for this workspace:
174
+
175
+ ```
176
+ dt install --print-adapters
177
+ ```
178
+
179
+ Three entry points, each reading the harness's JSON payload from **stdin**:
180
+
181
+ - `dt install --hook` — a session started. Makes this checkout ready and, in a linked worktree,
182
+ ends the board with the line that tells the session how to finish: *this is worktree `<name>` of
183
+ `<primary>`; before you finish, `dt commit` your records and tell the operator to run `dt land
184
+ worktrees/<name>`.*
185
+ - `dt add worktrees --hook` — a worktree is being created. Takes the name from the payload, places
186
+ the tree under `.worktrees/<name>`, and prints the path last. It is a FORM: `--path`, `--base` and
187
+ `--temp` are refused with it, rather than accepted and ignored.
188
+ - `dt land --hook --dry-run` — a worktree is being removed. Reports whether it would land, and never
189
+ removes anything. `worktree_path` is required in the payload; a path that is not a registered
190
+ worktree is a quiet exit 0, since the harness may have unregistered it already.
191
+
192
+ ⚠ **`--hook` refuses a terminal.** Reading stdin from a TTY blocks forever with no output, which in
193
+ a hook is indistinguishable from a hang; so it says `--hook reads the harness's JSON on stdin —
194
+ nothing is piped` and exits.
195
+
196
+ ## the shim
197
+
198
+ ⚠ **A hook's shell reads no startup file.** A hook runs as `sh -c` / `bash -c`, and neither reads a
199
+ profile — only an interactive or login shell does. So a hook line beginning with `npm` or `npx` can
200
+ fail with *command not found* in an environment where the same command works perfectly in the
201
+ operator's terminal, and a hook's stderr is nobody's problem. No profile edit can fix this; a profile
202
+ is per-machine, per-shell and outside the workspace, which is the opposite of what a committed
203
+ adapter needs to be.
204
+
205
+ The engine therefore ships `bin/dt-hook.sh`, a POSIX shell shim that resolves node absolutely:
206
+ `$DREAMTEAMER_NODE`, then `command -v node`, then the highest `~/.nvm/versions/node/*/bin/node`,
207
+ then `/opt/homebrew/bin`, then `/usr/local/bin` — and when none resolves it fails LOUDLY onto
208
+ stdout, which the harness adds to the session's context, rather than dying silently.
209
+
210
+ ⚠ **Resolving `npm` beside node is not enough.** npm's own shebang is `#!/usr/bin/env node`, so an
211
+ absolutely-resolved npm still dies with `env: node: No such file or directory` at exit 127 when the
212
+ child's PATH has no node on it. The engine puts node's own directory at the front of the PATH it
213
+ hands to any child that needs npm. Measured on a real worktree, not reasoned.
214
+
215
+ ## per harness
216
+
217
+ Only Claude Code's adapter ships today. The rest are documented as they are measured — the
218
+ `SessionStart` contract is the same shim line for all of them, and the two things that stay
219
+ per-harness are the **per-directory trust prompt** and **removal**, which only some harnesses hook
220
+ at all.
221
+
222
+ | harness | creation | session start | removal |
223
+ |---|---|---|---|
224
+ | Claude Code | `WorktreeCreate` — replaces git's own logic; your hook cuts the tree and prints its path | `SessionStart`, no matcher, `cwd` in the payload | `WorktreeRemove` — dry-run only; never delete a dirty worktree from a hook |
225
+ | Codex · Gemini CLI · Cursor · pi | not yet shipped | not yet shipped | not hooked, or not documented |
226
+
227
+ ## common mistakes
228
+
229
+ | mistake | what happens |
230
+ |---|---|
231
+ | running `dt land` from inside the worktree being landed | works, but the primary is where the verb belongs — it is the tree being changed |
232
+ | deleting a worktree with `git worktree remove` | git checks a dirty tree and stops there; it knows nothing about unpublished records or orphaned commits. `dt rm worktrees/<n>` does |
233
+ | expecting a sandbox to see uncommitted work | a worktree is cut from HEAD. What is not committed is not there — the engine-ships-first rule in miniature |
234
+ | putting `npm` at the front of a hook line | works in a terminal, fails in the hook. Use `dt install --print-adapters` and paste what it gives you |
235
+ | landing a pushed branch | refused. Rebasing a branch someone else may have is not the engine's call |