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 +120 -0
- package/CHANGELOG.md +266 -0
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/README.zh.md +151 -0
- package/SECURITY.md +56 -0
- package/cordis.patch.yml +30 -0
- package/docs/DESIGN.md +783 -0
- package/docs/TASKS.md +164 -0
- package/lib/config.js +126 -0
- package/lib/engine-face.js +328 -0
- package/lib/git-port.js +773 -0
- package/lib/index.js +402 -0
- package/lib/merge-queue.js +832 -0
- package/lib/naming.js +107 -0
- package/lib/repo-gate.js +202 -0
- package/lib/state-store.js +512 -0
- package/lib/tools/worktree-cleanup.js +127 -0
- package/lib/tools/worktree-create.js +212 -0
- package/lib/tools/worktree-list.js +234 -0
- package/lib/tools/worktree-merge.js +396 -0
- package/lib/tools/worktree-queue.js +330 -0
- package/lib/tools/worktree-status.js +194 -0
- package/lib/worktree-service.js +673 -0
- package/package.json +55 -0
- package/scripts/link-harness-dsh-tools.sh +95 -0
- package/scripts/lint.js +142 -0
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)
|