@autosk/worktree 0.1.0 → 0.1.2
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/README.md +21 -13
- package/package.json +2 -3
- package/src/index.ts +59 -19
package/README.md
CHANGED
|
@@ -22,8 +22,11 @@ autosk.registerWorkflow({
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Each isolated task runs in its own checkout so concurrent tasks never collide on
|
|
25
|
-
the working tree. The engine calls `acquire` before scheduling
|
|
26
|
-
returned `cwd` becomes `ctx.cwd`) and `
|
|
25
|
+
the working tree. The engine calls `acquire` before scheduling each session (its
|
|
26
|
+
returned `cwd` becomes `ctx.cwd`) and `reap` only on a terminal transition
|
|
27
|
+
(`done`/`cancel`). This provider has no live env to stop, so it **omits**
|
|
28
|
+
`release` entirely — keeping the checkout on disk across sibling/human-park steps
|
|
29
|
+
is exactly the absence of teardown.
|
|
27
30
|
|
|
28
31
|
## Behaviour
|
|
29
32
|
|
|
@@ -35,22 +38,27 @@ either stack resolves to the same place:
|
|
|
35
38
|
branch = autosk/<task-id>
|
|
36
39
|
```
|
|
37
40
|
|
|
38
|
-
- **acquire** — allocates the per-task worktree on branch
|
|
39
|
-
(off `HEAD`), or re-uses it when a prior step kept it. A
|
|
40
|
-
re-allocated on the *existing* branch (v1 "missing worktree
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
- **acquire** (ensure-ready) — allocates the per-task worktree on branch
|
|
42
|
+
`autosk/<task-id>` (off `HEAD`), or re-uses it when a prior step kept it. A
|
|
43
|
+
**missing** dir is re-allocated on the *existing* branch (v1 "missing worktree
|
|
44
|
+
auto-recovery"). Idempotent and re-entered per step. Returns
|
|
45
|
+
`{ cwd, meta: { branch, projectRoot } }`.
|
|
46
|
+
- **(no `release`)** — sibling step and human-park keep the dir on disk
|
|
47
|
+
untouched, so the next `acquire` re-uses the same checkout. There is nothing to
|
|
48
|
+
quiesce, so the method is omitted.
|
|
49
|
+
- **reap** (destroy-on-terminal, done / cancel) — keyed by `(projectRoot,
|
|
50
|
+
taskId)`, so it works with no live handle (engine terminal, a manual
|
|
51
|
+
`done`/`cancel` after a park, or crash recovery). Removes the worktree dir but
|
|
43
52
|
**preserves** the `autosk/<task-id>` branch (so the work survives for review /
|
|
44
|
-
merge).
|
|
45
|
-
|
|
46
|
-
the next step re-uses the same checkout.
|
|
53
|
+
merge). With `force:false` it refuses to discard uncommitted changes and
|
|
54
|
+
reports `{ removed:false, dirty:true }`; `force:true` removes regardless.
|
|
47
55
|
|
|
48
56
|
### Failure handling
|
|
49
57
|
|
|
50
58
|
The provider **only throws descriptive messages** — the engine wraps them
|
|
51
|
-
(`isolation_acquire_failed: …` on acquire, `
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
(`isolation_acquire_failed: …` on acquire, `isolation_reap_failed: …` on a
|
|
60
|
+
terminal reap) and parks the task to `human`. It never parks or formats those
|
|
61
|
+
prefixes itself. Throw cases:
|
|
54
62
|
|
|
55
63
|
- **non-git root** — `not a git repository: <root>` (the project root isn't a
|
|
56
64
|
git repo).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autosk/worktree",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Shipped autoskd v2 isolation provider: per-task git worktree isolation (worktreeIsolation()) attachable to any workflow.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wierdbytes",
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
"keywords": [
|
|
17
17
|
"autosk",
|
|
18
18
|
"autosk-extension",
|
|
19
|
-
"
|
|
20
|
-
"isolation"
|
|
19
|
+
"autosk-isolation"
|
|
21
20
|
],
|
|
22
21
|
"type": "module",
|
|
23
22
|
"publishConfig": {
|
package/src/index.ts
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* `@autosk/worktree` — the shipped `worktreeIsolation()` provider (plan §3.5).
|
|
3
3
|
*
|
|
4
4
|
* Ports v1's per-task git worktree isolation onto the v2
|
|
5
|
-
* {@link IsolationProvider} contract. The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `release`
|
|
10
|
-
*
|
|
5
|
+
* {@link IsolationProvider} contract. The engine
|
|
6
|
+
* (`daemon/core/src/engine/session.ts`) calls `acquire` before scheduling an
|
|
7
|
+
* isolated session (idempotently re-used across the run's steps) and `reap` on a
|
|
8
|
+
* terminal transition (`done`/`cancel`). A worktree has nothing to "stop", so
|
|
9
|
+
* this provider omits `release` entirely — keeping the dir across a step→step or
|
|
10
|
+
* a human-park IS the absence of teardown. FAILURES ARE WRAPPED BY THE ENGINE
|
|
11
|
+
* (`acquire` throw → `isolation_acquire_failed: <msg>`, `reap` throw →
|
|
12
|
+
* `isolation_reap_failed: <msg>`), so this provider just throws descriptive
|
|
13
|
+
* messages — it never parks or formats those prefixes itself.
|
|
11
14
|
*
|
|
12
15
|
* Deterministic mapping (byte-identical to the v1 Go/Rust slug so a worktree
|
|
13
16
|
* allocated by either stack resolves to the same place):
|
|
@@ -22,7 +25,7 @@ import { createHash } from "node:crypto";
|
|
|
22
25
|
import { mkdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
23
26
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
24
27
|
|
|
25
|
-
import type { IsolationHandle, IsolationProvider } from "@autosk/sdk";
|
|
28
|
+
import type { IsolationHandle, IsolationProvider, IsolationReapResult } from "@autosk/sdk";
|
|
26
29
|
|
|
27
30
|
/** Options for {@link worktreeIsolation}. */
|
|
28
31
|
export interface WorktreeIsolationOptions {
|
|
@@ -42,7 +45,7 @@ export const WORKTREE_TAG = "worktree";
|
|
|
42
45
|
/** Provider-internal bookkeeping carried on every {@link IsolationHandle}. */
|
|
43
46
|
interface WorktreeMeta extends Record<string, unknown> {
|
|
44
47
|
branch: string;
|
|
45
|
-
/** The canonical project root
|
|
48
|
+
/** The canonical project root the worktree was checked out from. */
|
|
46
49
|
projectRoot: string;
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -50,9 +53,9 @@ interface WorktreeMeta extends Record<string, unknown> {
|
|
|
50
53
|
* Builds the shipped worktree {@link IsolationProvider}. Attach it to a workflow
|
|
51
54
|
* via `isolation: worktreeIsolation()` (plan §3.6). `acquire` allocates (or
|
|
52
55
|
* re-uses / re-allocates) the per-task worktree and hands the engine its path as
|
|
53
|
-
* `ctx.cwd`; `
|
|
54
|
-
* `autosk/<task>` branch
|
|
55
|
-
*
|
|
56
|
+
* `ctx.cwd`; `reap` removes the dir on a terminal transition but PRESERVES the
|
|
57
|
+
* `autosk/<task>` branch. There is no `release`: the dir is simply kept across
|
|
58
|
+
* step→step and human-park (the env is reused on the next `acquire`).
|
|
56
59
|
*/
|
|
57
60
|
export function worktreeIsolation(opts: WorktreeIsolationOptions = {}): IsolationProvider {
|
|
58
61
|
const gitBin = opts.gitBin ?? "git";
|
|
@@ -95,15 +98,11 @@ export function worktreeIsolation(opts: WorktreeIsolationOptions = {}): Isolatio
|
|
|
95
98
|
return { cwd: path, meta };
|
|
96
99
|
},
|
|
97
100
|
|
|
98
|
-
async
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (!terminal) return;
|
|
102
|
-
const meta = (handle.meta ?? {}) as Partial<WorktreeMeta>;
|
|
103
|
-
const path = handle.cwd;
|
|
104
|
-
const canon = meta.projectRoot ? canonRoot(meta.projectRoot) : "";
|
|
101
|
+
async reap({ projectRoot, taskId }, { force }): Promise<IsolationReapResult> {
|
|
102
|
+
const canon = canonRoot(projectRoot);
|
|
103
|
+
const path = pathFor(canon, taskId, opts.home);
|
|
105
104
|
await ensureGitAvailable(gitBin);
|
|
106
|
-
|
|
105
|
+
return cleanupTerminal(gitBin, canon, path, force);
|
|
107
106
|
},
|
|
108
107
|
};
|
|
109
108
|
}
|
|
@@ -172,6 +171,47 @@ async function verifyHealthy(gitBin: string, canon: string, path: string): Promi
|
|
|
172
171
|
}
|
|
173
172
|
}
|
|
174
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The terminal-cleanup core behind {@link IsolationProvider.reap} (session-free,
|
|
176
|
+
* keyed by identity).
|
|
177
|
+
*
|
|
178
|
+
* Removes the worktree dir while PRESERVING its branch, gated on `force`:
|
|
179
|
+
* `force:false` leaves a dirty checkout in place and reports `{dirty:true}` so
|
|
180
|
+
* the caller can warn; `force:true` removes it regardless. A vanished/absent dir
|
|
181
|
+
* is a no-op (`{removed:false}`); a stranded dir that is not a healthy checkout
|
|
182
|
+
* cannot be "dirty" in any recoverable sense and is reaped.
|
|
183
|
+
*/
|
|
184
|
+
async function cleanupTerminal(
|
|
185
|
+
gitBin: string,
|
|
186
|
+
canon: string,
|
|
187
|
+
path: string,
|
|
188
|
+
force: boolean,
|
|
189
|
+
): Promise<IsolationReapResult> {
|
|
190
|
+
if (!existsPath(path)) {
|
|
191
|
+
// Nothing on disk; best-effort clear of any stale git registration.
|
|
192
|
+
if (canon !== "" && (await isGitRepo(gitBin, canon))) await pruneWorktrees(gitBin, canon);
|
|
193
|
+
return { removed: false, dirty: false };
|
|
194
|
+
}
|
|
195
|
+
const { dirty, detail } = await worktreeDirty(gitBin, path);
|
|
196
|
+
if (dirty && !force) return { removed: false, dirty: true, detail };
|
|
197
|
+
await onTerminal(gitBin, canon, path);
|
|
198
|
+
return { removed: true, dirty, detail: dirty ? detail : undefined };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Reports whether the checkout at `path` has uncommitted changes (modified,
|
|
203
|
+
* staged, OR untracked — anything `git status --porcelain` surfaces). A path
|
|
204
|
+
* that is not a healthy worktree (status errors) reads as NOT dirty: there is no
|
|
205
|
+
* recoverable working state to protect, so the caller may reap the stranded dir.
|
|
206
|
+
*/
|
|
207
|
+
async function worktreeDirty(gitBin: string, path: string): Promise<{ dirty: boolean; detail: string }> {
|
|
208
|
+
const r = await runGit(gitBin, path, ["status", "--porcelain", "--untracked-files=all"]);
|
|
209
|
+
if (r.code !== 0) return { dirty: false, detail: "" };
|
|
210
|
+
const lines = r.stdout.split("\n").filter((l) => l.trim() !== "");
|
|
211
|
+
if (lines.length === 0) return { dirty: false, detail: "" };
|
|
212
|
+
return { dirty: true, detail: `${lines.length} uncommitted file(s)` };
|
|
213
|
+
}
|
|
214
|
+
|
|
175
215
|
/** Removes the worktree dir on a terminal transition while PRESERVING its branch. */
|
|
176
216
|
async function onTerminal(gitBin: string, canon: string, path: string): Promise<void> {
|
|
177
217
|
// git itself broken / no project root → still try to reap the on-disk dir.
|