dsh-plugin-worktrees 0.1.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 ADDED
@@ -0,0 +1,120 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI agents (DeepSeek Harness agents, Claude Code, Codex, …)
4
+ working in this repository.
5
+
6
+ ## What this is
7
+
8
+ `dsh-worktrees` — a DeepSeek Harness (Cordis) plugin providing git worktree
9
+ parallel write isolation plus serial merge integration: six new-name tools
10
+ (`worktree_create / list / status / merge / queue / cleanup`), a serial
11
+ merge queue into a session integration branch (`merge --no-ff`), conflict
12
+ scenes retained for out-of-band resolution, crash reconciliation at apply
13
+ time, and a double-confirmation cleanup. The core engine is ported from
14
+ task-weaver's `packages/workspaces/`; the design record is
15
+ [docs/DESIGN.md](docs/DESIGN.md) and the task breakdown is
16
+ [docs/TASKS.md](docs/TASKS.md).
17
+
18
+ ## Commands
19
+
20
+ ```bash
21
+ npm install # install dependencies
22
+ npm run setup:peer # symlink the RUNNING harness's @deepseek-ai/dsh-tools
23
+ npm test # node:test suite (295 tests; real local git fixtures, no network)
24
+ npm run lint # node --check every module + the git-ingress audit
25
+ ```
26
+
27
+ Never add a test that requires a network, a remote, or credentials — the
28
+ suite must stay green on a bare runner. When migrating code from
29
+ task-weaver, do not weaken migrated assertions; new cases add coverage,
30
+ they don't change old semantics.
31
+
32
+ ## Repo layout
33
+
34
+ ```
35
+ cordis.patch.yml # bundle patch: ONE insert row (id: worktrees) — no disables
36
+ lib/
37
+ index.js # apply(): config, peer self-check, state load, reconcile, tool registration
38
+ config.js # zod strict config (§8.1 table; unknown keys fail loudly)
39
+ git-port.js # the ONLY git ingress: runGit + NodeGitPort + porcelain parsers
40
+ state-store.js # one JSON file: atomic 0600 tmp+rename writes, __proto__ guard
41
+ repo-gate.js # realpath + session-cwd/workspace/allowedRoots admission (fail closed)
42
+ naming.js # sanitizeBranch / branchName / worktreePath / repoIdFromRoot (FNV-1a)
43
+ worktree-service.js # create/list/status/cleanup/findActiveByTask (task-weaver workspace-service, worktree mode only)
44
+ merge-queue.js # enqueue/drain/applyOne + collect + cancel/retry/resolve
45
+ engine-face.js # the worktreesEngine service face (§10.1 DAG seam: four-key enqueue, five-state drain, create/findActiveByTask)
46
+ tools/ # one module per model-facing tool (6 modules)
47
+ scripts/ # lint.js (syntax + git-discipline audit), link-harness-dsh-tools.sh
48
+ test/ # node:test suite (real local git fixtures)
49
+ docs/DESIGN.md # the architecture record — read before changing semantics
50
+ docs/TASKS.md # task breakdown and acceptance criteria
51
+ ```
52
+
53
+ ## Safety red lines (non-negotiable)
54
+
55
+ All ten are binding (DESIGN §12); they were decided before implementation
56
+ and every one of them is mechanically or structurally enforced:
57
+
58
+ 1. **The GitPort is the only git ingress.** Every git call goes through
59
+ `lib/git-port.js` (`spawn("git", argvArray, {shell:false})`) — argv is
60
+ never a shell string. `scripts/lint.js` enforces this statically: no
61
+ module under `lib/` other than `git-port.js` may contain a
62
+ process-spawning call (`spawn(` / `spawnSync(` / `exec(` / `execSync(` /
63
+ `execFile(` / `execFileSync(`), real forms only (a `name(` after the
64
+ method name; `child_process` imports and string mentions do not trip).
65
+ 2. **Ref/branch-name injection defence.** Caller-controllable refs are
66
+ always resolved with `rev-parse --verify --end-of-options`; branch names
67
+ (task slugs AND explicitly supplied integration branches — at create,
68
+ at the merge override, at the engine-seam enqueue, and re-vetted at
69
+ apply time) are pre-vetted with `check-ref-format --branch`;
70
+ merge/delete positional refs sit AFTER the `--` end-of-options
71
+ terminator.
72
+ 3. **Path discipline.** `node:path.join`/`resolve` + `realpath`
73
+ normalisation everywhere — never string concatenation or `URL.pathname`.
74
+ Worktree physical paths land only under `worktreeRoot` (constructive
75
+ guarantee; the caller cannot inject a leaf directory).
76
+ 4. **The repo gate fails closed.** A repo root must realpath inside the
77
+ union of registered workspaces, the session-cwd subtree, and explicit
78
+ `allowedRoots`. There is NO any-root switch; the unknown is always
79
+ `repo_not_registered`.
80
+ 5. **Cleanup never deletes unmerged work.** While the source branch head is
81
+ not an ancestor of the integration branch head — OR the integration
82
+ branch does not resolve (never bootstrapped) while the worktree holds
83
+ commits past its recorded baseCommit — removal requires
84
+ `force && acknowledge` — two INDEPENDENT booleans simultaneously and
85
+ explicitly true. No single parameter or default ever releases it.
86
+ 6. **Merge never auto-pushes.** The GitPort structurally has no
87
+ push/fetch/clone method; the README and the tool descriptions state that
88
+ integration results stay on the local integration branch for human
89
+ review and push.
90
+ 7. **The conflict scene is untouchable.** A conflicted job's integration
91
+ worktree and source branch are retained; reconciliation and `retry`
92
+ never delete them. Only an explicit cleanup (past the protection check)
93
+ or a post-`resolve` explicit action may clear the scene.
94
+ 8. **Tests use zero network and zero remotes.** Fixtures are local
95
+ `git init` only; no case may push/fetch/clone (CI must stay green on a
96
+ bare runner).
97
+ 9. **Config is zod-strict.** Unknown keys fail loudly (family convention);
98
+ `engines >= 18` — no `Promise.withResolvers` (hand-written deferreds).
99
+ 10. **Single-writer invariant.** Writes to `state.json` happen only inside
100
+ the engine's serial chain or a tool's single-step critical section; the
101
+ plugin offers no second write entrance (the dag-orchestrator goes
102
+ through the `worktreesEngine` service face, which wraps the SAME
103
+ engine/tool singletons — DESIGN §10/§10.1).
104
+
105
+ ## Engineering discipline
106
+
107
+ - `node:test` only — no real CLIs, no keys, no network (family red line).
108
+ - A migration never weakens assertions; ports stay line-for-line equivalent
109
+ except where DESIGN explicitly requires a rename or an extension, and
110
+ original comment semantics are preserved.
111
+ - README.md and README.zh.md are updated **together** — section-for-section
112
+ aligned whenever the config surface, tools, install flow, or the safety
113
+ boundaries change.
114
+ - Every user-visible change gets a `CHANGELOG.md` entry.
115
+ - Keep `lib/index.js` thin: assembly only; tools live in `lib/tools/`, one
116
+ module per tool; engines are constructor-injected (`{ git, store,
117
+ config }`) and never import host services.
118
+ - `apply()` must return `undefined` (the loader treats a non-undefined
119
+ return as a disposable and fails the boot with `TypeError: Invalid
120
+ effect`).
package/CHANGELOG.md ADDED
@@ -0,0 +1,266 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-08-18
9
+
10
+ First release. `dsh-worktrees` gives the DeepSeek Harness **git worktree
11
+ parallel write isolation + serial merge integration**: one isolated worktree
12
+ per parallel task (dedicated branch `dsh-wt/<session-short>/<task>`), a
13
+ serial merge queue into a session integration branch, and conflict scenes
14
+ that are retained for out-of-band resolution. The core engine is ported from
15
+ task-weaver's `packages/workspaces/` (migration map: DESIGN §9); the plugin
16
+ pattern follows dsh-plugin-subagents.
17
+
18
+ ### Documentation (2026-08-18) — README rewrite
19
+
20
+ - Both READMEs (`README.md` / `README.zh.md`) rewritten in plain language,
21
+ still section-for-section aligned. New shape: what the plugin gives you →
22
+ why (parallel write agents racing in one checkout) → install (npm name
23
+ plus local-checkout path) with an expected result → quick start showing
24
+ the create/delegate/merge/resolve loop → the six tools → the five merge
25
+ outcomes → conflicts in one paragraph → safety features in user terms →
26
+ trimmed configuration table → boundaries & troubleshooting → references
27
+ & credits. Internal vocabulary (DESIGN codes, semver prerelease arcana,
28
+ test counts) was dropped from the READMEs.
29
+
30
+ ### Changed (2026-08-18)
31
+
32
+ - npm package renamed to `dsh-plugin-worktrees`: the unscoped name
33
+ `dsh-worktrees` was taken on npm by an unrelated third-party package
34
+ (different author, different functionality). The dsh bundle id in
35
+ `cordis.patch.yml` stays `worktrees`; only the npm distribution name
36
+ changed. First npm release: 0.1.0.
37
+
38
+ ### CI (2026-08-18)
39
+
40
+ - windows-latest leg DEFERRED: all Windows legs failed 42
41
+ git-path-sensitive assertions (worktree-listing path shapes, engine-face
42
+ record visibility) that pass on macOS/ubuntu. Removed from the matrix
43
+ until Windows path semantics are implemented and validated;
44
+ `timeout-minutes: 15` added as a runaway guard.
45
+ - Added gitleaks secret scanning: `.github/workflows/gitleaks.yml` (full
46
+ history scan on every push/PR) and `.pre-commit-config.yaml` for local
47
+ commits; matches the gateway-provider repo's setup plus a `.gitleaks.toml`
48
+ allowlist for the `dag/<case>/integration` test-fixture branch names the
49
+ generic-api-key entropy heuristic flags.
50
+ ### Added
51
+
52
+ - **Six-tool family** (all new global-layer names — no official tool take
53
+ over, no preset adaptation, zero host patches):
54
+ - `worktree_create` — isolated worktree per parallel task; base ref always
55
+ resolved to a concrete commit oid; returns the path plus a delegation
56
+ hint pointing the subagent tool's `cwd` parameter at it.
57
+ - `worktree_list` — managed worktrees with the `orphaned` flag and a
58
+ merge-queue summary; `include_integration` also lists conflict-retained
59
+ integration worktrees.
60
+ - `worktree_status` — live git status (HEAD, dirty, per-file changes,
61
+ `ahead_of_base`) plus the worktree's merge-job projection.
62
+ - `worktree_merge` — auto-collect uncommitted work, enqueue, and serially
63
+ drain the branch queue; five-state return (`succeeded` / `conflicted` /
64
+ `queued` / `no_changes` / `failed`), with a held branch surfaced as the
65
+ `active_job_exists` return state (a business state, never a throw).
66
+ - `worktree_queue` — queue inspection (`list` + `branch_holders`) and the
67
+ `cancel` / `retry` / `resolve` commands (idempotent on terminal states;
68
+ invalid transitions fail loudly with `invalid_job_state`).
69
+ - `worktree_cleanup` — remove a worktree and its branch; unmerged work is
70
+ protected behind the `force` AND `acknowledge` double confirmation.
71
+ - **Serial merge queue** (`lib/merge-queue.js`, ported from task-weaver
72
+ `merge-queue.ts`): per-branch promise chains replace exclusive leases
73
+ (strict serialisation inside one process), `merge --no-ff` replaces
74
+ cherry-pick (multi-commit branches integrate losslessly), one-active
75
+ invariant per repo+integration branch, monotonic `orderIndex`, terminal
76
+ job history pruned to `retainJobHistory` (200).
77
+ - **Conflict scene retention**: on a conflict the integration worktree is
78
+ kept with the markers in place (the job holds its branch until
79
+ out-of-band resolution); `retry` re-queues while leaving the old scene
80
+ for the operator; nothing but an explicit, protection-checked cleanup
81
+ deletes it.
82
+ - **Crash reconciliation** at `apply()` time, before any tool registration:
83
+ vanished worktrees marked, applying jobs failed with provenance (a live
84
+ partial integration worktree retained and annotated for inspection),
85
+ conflicted jobs kept verbatim, orphan `.integration/` worktrees
86
+ best-effort removed — marking only, no destructive deletes.
87
+ - **Repo gate, fail closed** (`lib/repo-gate.js`): realpath canonicalisation
88
+ then admission only inside the session-cwd subtree, a registered
89
+ workspace, or explicit `allowedRoots`; no any-root switch.
90
+ - **GitPort as the only git ingress** (`lib/git-port.js`, ported from
91
+ task-weaver): argv-array `spawn("git", …, {shell:false})`, per-command
92
+ timeouts with SIGKILL, dual-stream 8 MiB caps, `--verify
93
+ --end-of-options` ref resolution, `check-ref-format --branch` validation,
94
+ `--` terminators on positional refs — and structurally **no push / fetch /
95
+ clone** (no auto-push by construction).
96
+ - **State store** (`lib/state-store.js`): one JSON file, atomic tmp+rename
97
+ writes with owner-only `0600` mode and a `__proto__`-key guard.
98
+ - **zod-strict config** (`lib/config.js`): unknown keys fail loudly at
99
+ apply time; every default resolved centrally.
100
+ - **Peer discipline**: `npm run setup:peer` symlinks the running harness's
101
+ `@deepseek-ai/dsh-tools`; `apply()` self-checks the single-instance
102
+ invariant (module-level Symbol probe) and fails loudly with the re-link
103
+ hint.
104
+ - **Bundle patch** (`cordis.patch.yml`): one insert row (`id: worktrees`,
105
+ `name: dsh-worktrees`) — new-name tools are naturally visible on the
106
+ global layer, nothing to disable.
107
+ - **Lint** (`scripts/lint.js`): `node --check` every module plus a static
108
+ git-discipline audit — `lib/` may spawn only from `git-port.js`, with
109
+ self-tests pinning the audit.
110
+ - **Test suite**: 295 tests, `node --test`, real local git fixtures only —
111
+ zero network, zero remotes, green on a bare runner.
112
+
113
+ ### Fixed (audit round 1)
114
+
115
+ - **P1-B — integration-branch name injection.** An explicitly supplied
116
+ `integration_branch` (create-time and the `worktree_merge` override)
117
+ reached `git branch <name> <startPoint>` / `git merge` unvalidated; a
118
+ flag-shaped value (`-m`) renamed `main` away to a 40-char oid (reproduced
119
+ by the `p1b` probe). Every entry point now pre-vets the name with
120
+ `git check-ref-format --branch` and rejects loudly with
121
+ `invalid_integration_branch` (naming the value and the legal-branch-name
122
+ rules); the merge queue re-vets at apply time as defense in depth, and
123
+ the engine seam vets the DAG four-key enqueue too.
124
+ - **P1-C — pre-first-merge bare cleanup deleted unmerged work.** The
125
+ cleanup protection gate was skipped entirely while the integration branch
126
+ did not resolve (never bootstrapped), so a worktree holding commits past
127
+ its base could be bare-cleaned — directory, branch, and commit
128
+ reachability gone (reproduced by the `p1c` probe). When the integration
129
+ branch does not resolve, the gate now compares the worktree head against
130
+ the recorded `baseCommit`: anything past base is unmerged by definition
131
+ and triggers the same `cleanup_protected` double confirmation
132
+ (`force` AND `acknowledge`). A worktree with no commits past base stays
133
+ bare-cleanable. `worktree_cleanup`'s description documents both arms.
134
+ - **P2 — `autoCollect: false` + a dirty tree reported a misleading
135
+ succeeded.** `collect` returned `ok` with the old head, the merge then
136
+ integrated that old head, and the uncommitted files were never
137
+ integrated. The outcome is now the explicit `dirty_not_collected`
138
+ terminal: `worktree_merge` returns
139
+ `state:'failed', error:'dirty_not_collected: …'` with the remediation
140
+ hint (commit the work or enable `autoCollect`), and no job is created.
141
+ - **P3 — the `worktreesEngine` seam's drain projected a misleading
142
+ `succeeded` for a dirty worktree** (the DAG gap twin of P2). The DAG
143
+ four-key `enqueue → drain` path resolved `sourceHead` to the worktree's
144
+ current head and integrated it, so a worktree holding uncommitted changes
145
+ (never in that head) was reported `succeeded` — the same "success
146
+ illusion" the tool layer's `dirty_not_collected` stops. The seam's drain
147
+ projection now re-checks the integrated source worktrees' clean state
148
+ (reusing the GitPort's `git.status` porcelain judgment) and maps a dirty
149
+ source `succeeded` → `failed` / `dirty_not_collected` with the remediation
150
+ hint (commit the work first, or drive it through the tool layer's
151
+ `autoCollect`). The DAG consumer already handles a `failed` DrainOutcome
152
+ (transient `dag.merge_failed` retry), so the five-state contract is
153
+ unchanged. A missing worktree record or a status flake fails open.
154
+
155
+ ### Changed
156
+
157
+ - **`mergeTimeoutMs` is now effective.** The key was validated but
158
+ discarded (`void config.mergeTimeoutMs`). It now budgets the merge step
159
+ specifically: `mergeNoFf` accepts a per-call `timeoutMs` override, the
160
+ queue passes the configured budget down, and a killed merge lands on the
161
+ normal hard-failure path (scene cleaned, queue continues) — separate
162
+ from the general 15s `gitTimeoutMs` so big-repo merges survive.
163
+
164
+ ### Added (audit round 1)
165
+
166
+ - **`worktreesEngine` service face** (`lib/engine-face.js`, DESIGN §10.1):
167
+ `apply()` now provides `ctx.provide('worktreesEngine', …)` over the SAME
168
+ service/queue/store singletons the tools use (red line 10 — single
169
+ writer), adapting the frozen dsh-dag-orchestrator consumer contract:
170
+ - `service.create({task, repoRoot, baseRef?, origin:'dag',
171
+ correlationId})` — repoKey derived provider-side (repoIdFromRoot),
172
+ `origin`/`correlationId` persisted on the worktree record (optional
173
+ fields, old state.json files stay valid);
174
+ - `service.findActiveByTask(repoRoot, task)` — the DAG re-dispatch reuse
175
+ probe (returns the active record with its correlationId, the
176
+ ownership-gate evidence);
177
+ - `queue.enqueue({worktreeId, integrationBranch, origin, correlationId})`
178
+ — the DAG four-key form; git facts (repoKey/repoRoot/sourceBranch/
179
+ sourceHead) resolved server-side from the worktree record, the
180
+ integration branch check-ref-format vetted (P1-B), and idempotent
181
+ while a job is active (a DAG retry re-polls, never re-stacks);
182
+ - `queue.drain(repoKey, integrationBranch)` — the five-state
183
+ DrainOutcome (`succeeded`/`conflicted`/`failed`/`no_changes`/`queued`
184
+ + `queued_ahead`) projected on top of the internal drain; the
185
+ tool-layer `{drained}|{blockedBy}` contract is unchanged.
186
+ - **Tests for every fix point**: integration-branch rejection at create /
187
+ merge-override / engine-seam / applyOne choke point; the P1-C protection
188
+ arm (+ the intentional semantics flip of the old "no protection target"
189
+ case, and the still-clearable no-commits case); `dirty_not_collected`;
190
+ `mergeTimeoutMs` budget wiring; and the full engine-face chain
191
+ (create→findActiveByTask→enqueue four-key→drain five-state) over real
192
+ git fixtures, modelled on the DAG side's frozen acceptance shapes.
193
+
194
+ ### Fixed (audit round 2 — cleanup/branch-probe tightening; all fail-closed, no semantic loosening)
195
+
196
+ - **P2 — the P1-C protection arm ignored uncommitted changes.** README
197
+ promised "clearable bare only with no commits past base AND no uncommitted
198
+ work in the branch probe", but the implementation compared only commits.
199
+ A worktree with a clean head but a dirty tree could be bare-cleaned and the
200
+ uncommitted work lost. The P1-C arm now runs `git status --porcelain` on
201
+ the worktree: any change (or a porcelain flake) → `cleanup_protected`
202
+ (`force` AND `acknowledge` required), message naming "uncommitted changes
203
+ present". The README claim is now true.
204
+ - **P3 — the P1-C head probe actually failed OPEN.** The comment said
205
+ "cannot PROVE absence of work: fail closed", but an unresolvable
206
+ `worktreeHead` (`null`) fell through to "not ahead" and allowed a bare
207
+ cleanup. It now fails `cleanup_protected` directly with a
208
+ "head is unresolvable" message.
209
+ - **P3 — divergent history could bare-delete unique commits.** The P1-C
210
+ "ahead" test was `isAncestor(base, head)`, mapping BOTH "head behind base"
211
+ (safe) and "head forked off onto unrelated history" (unique work) to
212
+ clearable. The test is now `head !== base && !isAncestor(head, base)` —
213
+ only a head strictly behind base (or at base) is clearable; a divergent,
214
+ rebased, or unrelated head is protected.
215
+ - **`ensureBranch` existence probe now uses `--end-of-options`** (consistency
216
+ with `resolveRef` / AGENTS.md red line 2; `validateBranch` already makes the
217
+ flag-shaped case unreachable upstream).
218
+ - **Engine-seam idempotence never widens when `integrationBranch` is
219
+ omitted.** The `enqueue` idempotence scan guarded the branch comparison
220
+ with `integrationBranch !== undefined`, so an omitted field skipped the
221
+ branch dimension and could return an active job for a DIFFERENT branch on
222
+ the same worktree. The scan now always compares against the effective
223
+ branch key (`integrationBranch ?? record.integrationBranch`).
224
+ - **`record.repoRoot` is now realpath-canonicalised on the engine-path
225
+ create too.** The tool path stores the canonical root (via repo-gate);
226
+ the engine path persisted the raw argument, so a symlinked/`..` engine
227
+ caller stored a value that could not string-match a canonical
228
+ `findActiveByTask` argument. create now realpath-normalises before
229
+ persisting (falling back to the raw value on realpath failure, preserving
230
+ prior behaviour), keeping tool- and engine-created records consistent.
231
+ - **Explicit symlinked-repoRoot regression pin added to the test suite.**
232
+ Previously the realpath-normalisation above was verified only INDIRECTLY
233
+ on macOS (where tmpdir itself is a symlinked path); on Linux CI that
234
+ coverage silently disappeared. `test/worktree-service.test.js` now drives
235
+ `create` through a deliberately symlinked repoRoot on any platform and
236
+ asserts the persisted `record.repoRoot`, the `store.repos` entry, and
237
+ `findActiveByTask(canonicalRoot, task)` all use the canonical form.
238
+
239
+ ### Docs (2026-08-18, README completion ahead of the GitHub publish)
240
+
241
+ README.md + README.zh.md, updated together (section-for-section aligned):
242
+
243
+ - **Compatibility banner** (both languages): compatible with DSH
244
+ `0.1.0-rc.7` (npm latest) and `0.1.0-rc.6`; `peerDependencies:
245
+ ^0.1.0-rc.6` satisfies rc.7 under semver (same-version-tuple prerelease
246
+ rule; `0.1.1-rc.x` would not). Verified against rc.7: `defineTool` /
247
+ `TOOL_RUNTIME_SCHEDULER` exports intact, the new `DefineToolOptions`
248
+ fields all optional, 303/303 green with peers linked to rc.7.
249
+ - **The per-call `cwd` caveat stated fully** (Why section + the
250
+ composition walkthrough lead + the Install bullet, en+zh): the official
251
+ DSH runtime's `SubagentStartRequest` has no `cwd` field, and the
252
+ unpatched rc.6/rc.7 runtime silently drops a per-call cwd (the child
253
+ inherits the parent's working directory) — the composition's
254
+ `subagent(cwd: P)` step is ineffective without the patches. The
255
+ walkthrough now requires dsh-plugin-subagents **with its
256
+ `patches/install.sh` run**; the Install bullet spells out that the
257
+ installer must be re-run after every dsh upgrade (an upgrade reinstalls
258
+ the pristine runtime), and that both patches apply verbatim onto rc.7
259
+ anchors.
260
+ - **Cross-repo links absolutified** for the standalone GitHub repos
261
+ (dsh-plugin-subagents / dsh-dag-orchestrator →
262
+ `https://github.com/Luck9Star/<repo>`; the old `../dsh-plugin-subagents`
263
+ relative paths 404 outside the monorepo layout).
264
+ - **Test count refreshed**: 295 → 303 (the suite grew; both language
265
+ versions).
266
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-worktrees contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # dsh-worktrees
2
+
3
+ **English** | [简体中文](README.zh.md)
4
+
5
+ > Runs on DeepSeek Harness (dsh) `0.1.0-rc.6` / `0.1.0-rc.7` · Node ≥ 18 · git on PATH · MIT
6
+
7
+ **Parallel agents without file fights.** Each task gets its own directory —
8
+ a git worktree on its own branch — so several agents can edit code at the
9
+ same time without stepping on each other. When a task finishes, its branch
10
+ is merged back **one at a time** through a serial queue into a reviewable
11
+ integration branch. If two tasks touched the same lines, the conflict scene
12
+ is kept intact for you — nothing is auto-resolved or force-pushed.
13
+
14
+ Six tools, zero host patches, no tool-name takeovers — install it and the
15
+ `worktree_*` family is visible to every session.
16
+
17
+ ## Why
18
+
19
+ Dispatch several write-task subagents in parallel and they stomp on each
20
+ other: everyone edits the same working directory, every file is a race.
21
+ The classical fix is git worktrees — this plugin operationalizes them for
22
+ agent work:
23
+
24
+ - `worktree_create("api-types")` → a fresh checkout at
25
+ `~/.dsh/worktrees/<repo>/<session>/api-types`, branch
26
+ `dsh-wt/<session>/<api-types>`.
27
+ - Delegate agents there with `cwd` (via
28
+ [dsh-plugin-subagents](https://github.com/Luck9Star/dsh-plugin-subagents))
29
+ — or work there yourself.
30
+ - `worktree_merge(id)` → uncommitted work is auto-committed, then the branch
31
+ merges into `dsh-wt/integration/<session>` — strictly one merge at a time,
32
+ so integrations can't conflict with each other mid-flight.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ # 1. Install into your dsh profile
38
+ dsh plugin --profile web add dsh-plugin-worktrees # or: add /path/to/local/checkout
39
+
40
+ # 2. Restart and open a NEW session
41
+ dsh --profile web
42
+ ```
43
+
44
+ **Expected result:** a new session exposes `worktree_create`, `worktree_list`,
45
+ `worktree_status`, `worktree_merge`, `worktree_queue`, `worktree_cleanup`.
46
+
47
+ > **Local checkout instead of npm?** Run `npm install && npm run setup:peer`
48
+ > inside the repo first (avoids a second copy of `dsh-tools`, which crashes
49
+ > every tool call), then `dsh plugin --profile web add "$(pwd)"`.
50
+
51
+ > **Want agents to work inside the worktrees?** The stock harness silently
52
+ > drops a subagent's `cwd`. Install
53
+ > [dsh-plugin-subagents](https://github.com/Luck9Star/dsh-plugin-subagents)
54
+ > and run its `patches/install.sh` (re-run after every dsh upgrade).
55
+
56
+ ## Quick start
57
+
58
+ The full loop — two parallel tasks, one clean merge, one conflict:
59
+
60
+ ```jsonc
61
+ worktree_create({ task: "api-types" }) // → { id: id1, path: P1, branch, base_commit, … }
62
+ worktree_create({ task: "docs-refresh" }) // → { id: id2, path: P2, … }
63
+
64
+ // parallel work — each agent (or you) edits inside its own path
65
+ subagent({ prompt: "Implement the new API types.", cwd: P1 })
66
+ subagent({ prompt: "Refresh the docs.", cwd: P2 })
67
+
68
+ worktree_merge({ worktree_id: id1 })
69
+ // → { state: "succeeded", integrated_commit, integration_branch }
70
+
71
+ worktree_merge({ worktree_id: id2 })
72
+ // → { state: "conflicted", conflict_files: [...],
73
+ // integration_worktree: "<kept scene>", resolution_hint: "…" }
74
+
75
+ // resolve by hand inside the kept worktree (markers are in place), then:
76
+ worktree_queue({ action: "list" })
77
+ worktree_queue({ action: "resolve", job_id: "…" }) // releases the integration branch
78
+ worktree_cleanup({ worktree_id: id1 }) // remove worktree + branch
79
+ ```
80
+
81
+ ## The six tools
82
+
83
+ | Tool | What it does |
84
+ | --- | --- |
85
+ | `worktree_create` | New worktree + dedicated branch for a task. Returns `path` (hand it to `cwd`), `branch`, `base_commit`, `integration_branch`. |
86
+ | `worktree_list` | List worktrees for a repo (optional: include retained conflict scenes), plus a merge-queue summary. |
87
+ | `worktree_status` | One worktree: HEAD, dirty state, per-file changes, ahead-of-base, current merge job. |
88
+ | `worktree_merge` | Auto-commit uncommitted work, then enqueue + merge into the integration branch. Five possible outcomes (below). |
89
+ | `worktree_queue` | Inspect the queue; `resolve` / `retry` / `cancel` a job. |
90
+ | `worktree_cleanup` | Remove a worktree (and its branch, unless `keep_branch`). Refuses to destroy unmerged work without double confirmation. |
91
+
92
+ ### Merge outcomes
93
+
94
+ | `state` | Meaning |
95
+ | --- | --- |
96
+ | `succeeded` | Merged; `integrated_commit` on the integration branch. |
97
+ | `conflicted` | Conflicts. The integration worktree is **kept** with markers in place; the job holds the branch until you `resolve` or `retry`. |
98
+ | `queued` | Enqueued behind other merges (`queued_ahead` tells you how many). |
99
+ | `no_changes` | Clean tree at base — nothing to integrate (normal, not an error). |
100
+ | `failed` | Hard failure (e.g. dirty tree with `autoCollect: false` → `dirty_not_collected`). |
101
+
102
+ ### Conflicts, the short version
103
+
104
+ Nothing is auto-resolved, nothing is rebased, nothing is force-pushed. A
105
+ conflicted merge keeps the scene (a retained worktree with conflict markers),
106
+ tells you the files, and **blocks that integration branch** until you:
107
+ edit the files inside the kept worktree and `worktree_queue(action:
108
+ "resolve")`, or `action: "retry"` to re-queue the merge, or abandon.
109
+
110
+ ## Safety, built in
111
+
112
+ - **Unmerged work is hard to lose.** Cleanup of a worktree whose commits
113
+ never made it to the integration branch requires **two** independent
114
+ confirmations (`force: true` **and** `acknowledge: true`); otherwise it
115
+ refuses with evidence. A merged worktree cleans up without ceremony.
116
+ - **Repo gate, fail closed.** Worktrees are only created inside the session
117
+ cwd subtree, registered workspaces, or `allowedRoots` — there is no
118
+ any-root switch. Branch names are validated by git itself.
119
+ - **Local only.** The plugin can merge but never push/fetch/clone — results
120
+ stay on the local integration branch for a human to review and push.
121
+ - **Dirty trees are handled.** `autoCollect: true` (default) commits
122
+ uncommitted work (including untracked files) before merging. Turn it off
123
+ and a dirty tree stops the merge with a clear error instead.
124
+ - **Restart-safe.** On startup, worktrees whose repo/path vanished are
125
+ marked `orphaned`, in-flight merges are failed with a note — marking only,
126
+ no destructive deletes.
127
+
128
+ ## Configuration
129
+
130
+ Optional — everything below has a working default. Keys live on the plugin's
131
+ row in your profile's `cordis.patch.yml`; unknown keys fail loudly at startup.
132
+
133
+ | Key | Default | Meaning |
134
+ | --- | --- | --- |
135
+ | `worktreeRoot` | `~/.dsh/worktrees/` | Root directory for all task worktrees. |
136
+ | `maxWorktrees` | `16` | Cap on non-terminal worktrees across repos. |
137
+ | `defaultBaseRef` | `HEAD` | What new task branches start from (always resolved to a concrete commit). |
138
+ | `autoCollect` | `true` | Auto-commit uncommitted work before merging. |
139
+ | `gitTimeoutMs` / `mergeTimeoutMs` | `15000` / `120000` | Per-git-command / merge-step timeouts. |
140
+ | `allowedRoots` | `[]` | Extra repo roots the gate admits. |
141
+ | `requireWorkspaceRegistration` | `true` | Also admit registered workspaces (disable to rely on session-cwd + `allowedRoots` only). |
142
+ | `statePath` | `~/.dsh/dsh-worktrees/state.json` | State file (atomic, owner-only writes). |
143
+ | `retainJobHistory` | `200` | Terminal merge-job records kept. |
144
+ | `register.*` | `true` | Per-tool switches. |
145
+
146
+ ## Works well with
147
+
148
+ - [dsh-plugin-subagents](https://github.com/Luck9Star/dsh-plugin-subagents)
149
+ — its per-call `cwd` is the join point: pass `worktree_create`'s `path`
150
+ straight to the subagent and it writes inside the worktree.
151
+ - [dsh-dag-orchestrator](https://github.com/Luck9Star/dsh-dag-orchestrator)
152
+ — this plugin exposes a `worktreesEngine` service the orchestrator probes
153
+ for, enabling `worktree:` task isolation and `merge` nodes in DAGs with
154
+ zero extra wiring.
155
+
156
+ ## Boundaries & troubleshooting
157
+
158
+ - **One repo, one dsh session for merges.** The queue is an in-process
159
+ chain; two hosts merging the same repo concurrently is not supported.
160
+ - **`cwd` ignored?** Stock harness drops it — install dsh-plugin-subagents
161
+ and run its `patches/install.sh`.
162
+ - **`active_job_exists` error** — an earlier conflicted job holds the
163
+ integration branch; `worktree_queue(action: "list")`, then `resolve` or
164
+ `retry` it.
165
+ - **Every tool call dies with `Cannot read properties of undefined
166
+ (reading 'prepare')`** — local checkout without `npm run setup:peer`;
167
+ re-run it after `npm install` here or a dsh upgrade.
168
+
169
+ ## Development
170
+
171
+ ```sh
172
+ npm install && npm run setup:peer # link the running harness's peers
173
+ npm test # node --test, real local git fixtures — no network or remotes
174
+ npm run lint
175
+ ```
176
+
177
+ Design record: [docs/DESIGN.md](docs/DESIGN.md).
178
+
179
+ ## References & credits
180
+
181
+ - **git worktree** — the mechanism everything here is built on.
182
+ - **task-weaver** (`packages/workspaces/`) — the core engine (git port,
183
+ merge queue, conflict-scene retention) is ported from it.
184
+ - [dsh-plugin-subagents](https://github.com/Luck9Star/dsh-plugin-subagents)
185
+ and [dsh-dag-orchestrator](https://github.com/Luck9Star/dsh-dag-orchestrator)
186
+ — the companion plugins this is designed to compose with.
187
+
188
+ ## Security
189
+
190
+ See [SECURITY.md](SECURITY.md).
191
+
192
+ ## License
193
+
194
+ [MIT](LICENSE)