@erclx/aitk 3.48.0 → 3.48.1
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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/git-worktree/REQUIREMENT.md +2 -1
- package/claude/skills/git-worktree/SKILL.md +9 -2
- package/package.json +1 -1
- package/scripts/lib/sandbox-path.sh +31 -2
- package/scripts/lib/ui.sh +9 -3
- package/src/commands/sandbox.ts +1 -24
- package/src/sandbox/tree.ts +47 -0
|
@@ -14,7 +14,7 @@ Without this skill, linked worktrees accumulate past the point where any of them
|
|
|
14
14
|
- Resolve merge state per branch from the pull request first, and fall back to local ancestry when no pull request exists
|
|
15
15
|
- Match the ancestry fallback against the branch name alone. `git branch --merged` decorates the current branch and every branch checked out in a linked worktree, which is the whole set this skill enumerates.
|
|
16
16
|
- Remove the worktree and its local branch together, since either one left alone is the state the skill exists to prevent
|
|
17
|
-
- Exclude
|
|
17
|
+
- Exclude five kinds of row from the remove set: the main root, the current session's worktree, any dirty tree, any worktree registered from outside the path prefix `claude-worktree` creates under, and any worktree a live session is occupying
|
|
18
18
|
- Give every skipped row a one-word reason, so the skip is a decision the user can overturn rather than a silence
|
|
19
19
|
- Pick exactly one mode. Listing and removing are different requests and inferring both from one invocation removes worktrees the user meant to read about.
|
|
20
20
|
|
|
@@ -22,6 +22,7 @@ Without this skill, linked worktrees accumulate past the point where any of them
|
|
|
22
22
|
|
|
23
23
|
- Enter or create a worktree. The description states the boundary so the model routes entry elsewhere rather than discovering it here.
|
|
24
24
|
- Remove a worktree with uncommitted work, whatever its merge state
|
|
25
|
+
- Remove a worktree a live session is occupying, whatever its merge state
|
|
25
26
|
- Remove the worktree the session is currently running in
|
|
26
27
|
- Emit anything after the result line
|
|
27
28
|
|
|
@@ -55,6 +55,10 @@ Determine current: the row whose `path` equals `git rev-parse --show-toplevel`.
|
|
|
55
55
|
|
|
56
56
|
Determine provenance: a non-main row is `foreign` when its `path` does not start with `<MAIN_ROOT>/.claude/worktrees/`. That prefix is the folder `claude-worktree` creates under, a convention of that skill rather than a fact this one owns. A tree an operator registered by hand anywhere else on disk reads as `foreign`. Step 1 has already marked the main row `main`, so it never reaches this test.
|
|
57
57
|
|
|
58
|
+
Determine occupancy: run `aitk sessions list --json` once for the whole enumeration, never once per row. Resolve each enumerated row's `path` and each live session's `worktree` field with `realpath` before comparing, since a session registered from a second clone of this repository reports a path under that clone rather than under `MAIN_ROOT`, and a raw string compare would hold nothing back. A row is `occupied` when a resolved `worktree` from any live session equals its resolved `path`. A resolved session `worktree` outside `MAIN_ROOT` names a different checkout, so it clears no row here and marks none as occupied.
|
|
59
|
+
|
|
60
|
+
The roster is unreadable when the command exits non-zero, when its JSON carries a `reason` field instead of a populated `sessions` array, or when `sessions` is non-empty but no entry carries a `worktree` key at all, which is the shape an older binary prints and cannot be told apart from a roster answering that nothing is occupied. `list` leaves the Notes column silent on that failure rather than changing behavior beyond it. `cleanup` refuses instead, stated in its own section below.
|
|
61
|
+
|
|
58
62
|
## `list` mode
|
|
59
63
|
|
|
60
64
|
Print the enumeration as a table, then stop. `list` has no final command.
|
|
@@ -64,7 +68,7 @@ Print the enumeration as a table, then stop. `list` has no final command.
|
|
|
64
68
|
| -- | ---------------------------------- | ----------------- | -------- | ------ | -------- |
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
Notes column shows the first that applies of `current`, `foreign`, `dirty`, or empty. Show paths relative to `MAIN_ROOT` (`.claude/worktrees/<name>`). A `foreign` row sits outside `MAIN_ROOT`, so print its path in full.
|
|
71
|
+
Notes column shows the first that applies of `current`, `occupied`, `foreign`, `dirty`, or empty. Show paths relative to `MAIN_ROOT` (`.claude/worktrees/<name>`). A `foreign` row sits outside `MAIN_ROOT`, so print its path in full.
|
|
68
72
|
|
|
69
73
|
After the table, append a one-line hint:
|
|
70
74
|
|
|
@@ -73,15 +77,18 @@ After the table, append a one-line hint:
|
|
|
73
77
|
|
|
74
78
|
## `cleanup` mode
|
|
75
79
|
|
|
80
|
+
If Enumeration marked the roster unreadable, stop: `❌ Session roster unreadable, so occupancy cannot be checked. Resolve the aitk sessions list failure, then re-run cleanup.` Hold every non-main row rather than computing the remove set from the other five tests alone, since removing a tree a live session holds is the failure this skill exists to prevent and a sweep that removes nothing costs one re-run.
|
|
81
|
+
|
|
76
82
|
From the enumeration, include a worktree in the remove set when all hold:
|
|
77
83
|
|
|
78
84
|
- Not the main row.
|
|
79
85
|
- Not the current session's worktree.
|
|
86
|
+
- Not occupied by a live session.
|
|
80
87
|
- Not a `foreign` tree.
|
|
81
88
|
- State is `merged` or `merged (local)`.
|
|
82
89
|
- Working tree is clean.
|
|
83
90
|
|
|
84
|
-
Every other non-main row goes to the skip set with a one-word reason, the first that applies in this order: `current`, `foreign`, `dirty`, `open`, `closed`, `unmerged`. A `foreign` tree that is also dirty reports `foreign`, since `dirty` is a state the operator can clear and `foreign` is not. Leading with the transient reason invites a commit or stash that changes nothing, after which the row reports `foreign` and holds back anyway.
|
|
91
|
+
Every other non-main row goes to the skip set with a one-word reason, the first that applies in this order: `current`, `occupied`, `foreign`, `dirty`, `open`, `closed`, `unmerged`. A `foreign` tree that is also dirty reports `foreign`, since `dirty` is a state the operator can clear and `foreign` is not. Leading with the transient reason invites a commit or stash that changes nothing, after which the row reports `foreign` and holds back anyway.
|
|
85
92
|
|
|
86
93
|
### Preview
|
|
87
94
|
|
package/package.json
CHANGED
|
@@ -10,15 +10,44 @@
|
|
|
10
10
|
# reports no writes at all. `scripts/eval/run.sh` keeps its fixture outside the
|
|
11
11
|
# repository for the same reason.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
13
|
+
# Mints a short random per-run identifier the first time it is asked for, then
|
|
14
|
+
# holds it in AITK_SANDBOX_RUN_ID for the rest of this process. A direct call
|
|
15
|
+
# (not `$(...)`) exports it into the caller's own shell, which is what lets
|
|
16
|
+
# `run.sh` mint once and have manage-sandbox.sh and the check it shells out to
|
|
17
|
+
# both inherit the same id as ordinary children. A call already carrying the
|
|
18
|
+
# variable, inherited from such a parent, reuses it rather than minting a new one.
|
|
19
|
+
#
|
|
20
|
+
# Twin of `mintSandboxRunId` in `src/commands/sandbox.ts`.
|
|
21
|
+
mint_sandbox_run_id() {
|
|
22
|
+
if [ -z "${AITK_SANDBOX_RUN_ID:-}" ]; then
|
|
23
|
+
AITK_SANDBOX_RUN_ID="$(date +%s)-$$-$RANDOM"
|
|
24
|
+
fi
|
|
25
|
+
export AITK_SANDBOX_RUN_ID
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# The base every per-run tree nests under, with no run id appended. Split out
|
|
29
|
+
# of `resolve_sandbox_dir` so a caller that needs to recognize any run's tree,
|
|
30
|
+
# such as `require_project_root` in `scripts/lib/ui.sh`, tests against this
|
|
31
|
+
# prefix instead of a single resolved path that changes on every call.
|
|
32
|
+
sandbox_dir_prefix() {
|
|
33
|
+
printf '%s/aitk/sandbox\n' "${XDG_STATE_HOME:-$HOME/.local/state}"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Twin of `sandboxTree` in `src/commands/sandbox.ts`. The exec boundary rules out
|
|
14
37
|
# a shared constant, so a change to the default lands on both sides.
|
|
38
|
+
#
|
|
39
|
+
# A bare fall-through with no per-run component is one path per machine, so
|
|
40
|
+
# two sessions resolving the default at once would provision over each other
|
|
41
|
+
# with neither told. `mint_sandbox_run_id` is what makes two such sessions
|
|
42
|
+
# land on two different trees rather than one.
|
|
15
43
|
resolve_sandbox_dir() {
|
|
16
44
|
if [ -n "${AITK_SANDBOX_DIR:-}" ]; then
|
|
17
45
|
printf '%s\n' "$AITK_SANDBOX_DIR"
|
|
18
46
|
return 0
|
|
19
47
|
fi
|
|
20
48
|
|
|
21
|
-
|
|
49
|
+
mint_sandbox_run_id
|
|
50
|
+
printf '%s-%s\n' "$(sandbox_dir_prefix)" "$AITK_SANDBOX_RUN_ID"
|
|
22
51
|
}
|
|
23
52
|
|
|
24
53
|
# Collapses repeated separators, folds `.` and `..` segments, and strips every
|
package/scripts/lib/ui.sh
CHANGED
|
@@ -115,9 +115,15 @@ guard_root() {
|
|
|
115
115
|
require_project_root() {
|
|
116
116
|
local GREEN RED YELLOW WHITE GREY NC
|
|
117
117
|
set_palette 2
|
|
118
|
-
local sandbox
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
local sandbox in_sandbox=0
|
|
119
|
+
if [ -n "${AITK_SANDBOX_DIR:-}" ]; then
|
|
120
|
+
sandbox="$AITK_SANDBOX_DIR"
|
|
121
|
+
[[ "$PWD" == "$sandbox" || "$PWD" == "$sandbox"/* ]] && in_sandbox=1
|
|
122
|
+
else
|
|
123
|
+
sandbox="$(sandbox_dir_prefix)"
|
|
124
|
+
[[ "$PWD" == "$sandbox" || "$PWD" == "$sandbox"/* || "$PWD" == "$sandbox"-* ]] && in_sandbox=1
|
|
125
|
+
fi
|
|
126
|
+
if [ "$in_sandbox" -eq 1 ]; then
|
|
121
127
|
echo -e "${GREY}┌${NC}" >&2
|
|
122
128
|
log_error "Execution restricted: Command cannot be run from inside the sandbox environment."
|
|
123
129
|
fi
|
package/src/commands/sandbox.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
2
|
import { join } from 'node:path'
|
|
4
3
|
import type { Command } from 'commander'
|
|
5
4
|
import { execScript } from '@/exec'
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
type RunEnvelope,
|
|
23
22
|
type Verdict,
|
|
24
23
|
} from '@/sandbox/expect'
|
|
24
|
+
import { sandboxTree } from '@/sandbox/tree'
|
|
25
25
|
import {
|
|
26
26
|
frameError,
|
|
27
27
|
intro,
|
|
@@ -64,29 +64,6 @@ function reportAbsentScenarioTree(): boolean {
|
|
|
64
64
|
return true
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* The provisioned tree, as opposed to `SANDBOX_DIR` above, which holds the
|
|
69
|
-
* scenario scripts. It sits outside the toolkit worktree so the toolkit's own
|
|
70
|
-
* `CLAUDE.md` stays off the ancestor chain of the session `run.sh` spawns with
|
|
71
|
-
* cwd here.
|
|
72
|
-
*
|
|
73
|
-
* Twin of `resolve_sandbox_dir` in `scripts/lib/sandbox-path.sh`. The exec
|
|
74
|
-
* boundary rules out a shared constant, so a change to the default lands on both
|
|
75
|
-
* sides.
|
|
76
|
-
*/
|
|
77
|
-
function sandboxTree(): string {
|
|
78
|
-
const override = process.env.AITK_SANDBOX_DIR
|
|
79
|
-
if (override !== undefined && override !== '') return override
|
|
80
|
-
|
|
81
|
-
const state = process.env.XDG_STATE_HOME
|
|
82
|
-
const base =
|
|
83
|
-
state !== undefined && state !== ''
|
|
84
|
-
? state
|
|
85
|
-
: join(homedir(), '.local', 'state')
|
|
86
|
-
|
|
87
|
-
return join(base, 'aitk', 'sandbox')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
67
|
/**
|
|
91
68
|
* Holds fixture content for scenarios rather than scenarios of its own.
|
|
92
69
|
* Twin of the `-not -name fixtures` filter in `scripts/manage-sandbox.sh`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mints a short per-run identifier the first time it is asked for and holds it
|
|
7
|
+
* in `AITK_SANDBOX_RUN_ID` for the rest of this process, so a script that
|
|
8
|
+
* spawns a child inheriting `process.env` — `run.sh` calling `manage-sandbox.sh`
|
|
9
|
+
* and then `aitk sandbox check` — resolves the same tree in every one of them.
|
|
10
|
+
* A process that already carries the variable, inherited from such a parent,
|
|
11
|
+
* reuses it rather than minting a new one.
|
|
12
|
+
*
|
|
13
|
+
* Twin of `mint_sandbox_run_id` in `scripts/lib/sandbox-path.sh`.
|
|
14
|
+
*/
|
|
15
|
+
export function mintSandboxRunId(): string {
|
|
16
|
+
const existing = process.env.AITK_SANDBOX_RUN_ID
|
|
17
|
+
if (existing !== undefined && existing !== '') return existing
|
|
18
|
+
|
|
19
|
+
const id = randomBytes(4).toString('hex')
|
|
20
|
+
process.env.AITK_SANDBOX_RUN_ID = id
|
|
21
|
+
return id
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The provisioned tree's path, split out of `src/commands/sandbox.ts` so the
|
|
26
|
+
* per-run default is unit-testable on its own rather than only through the
|
|
27
|
+
* command's registration.
|
|
28
|
+
*
|
|
29
|
+
* Twin of `resolve_sandbox_dir` in `scripts/lib/sandbox-path.sh`. The exec
|
|
30
|
+
* boundary rules out a shared constant, so a change to the default lands on
|
|
31
|
+
* both sides. The fall-through used to be one path per machine, so two
|
|
32
|
+
* sessions each resolving the default at once provisioned over each other
|
|
33
|
+
* with neither told; `mintSandboxRunId` gives the path a per-run component
|
|
34
|
+
* instead, which is what makes two such sessions land on two different trees.
|
|
35
|
+
*/
|
|
36
|
+
export function sandboxTree(): string {
|
|
37
|
+
const override = process.env.AITK_SANDBOX_DIR
|
|
38
|
+
if (override !== undefined && override !== '') return override
|
|
39
|
+
|
|
40
|
+
const state = process.env.XDG_STATE_HOME
|
|
41
|
+
const base =
|
|
42
|
+
state !== undefined && state !== ''
|
|
43
|
+
? state
|
|
44
|
+
: join(homedir(), '.local', 'state')
|
|
45
|
+
|
|
46
|
+
return join(base, 'aitk', `sandbox-${mintSandboxRunId()}`)
|
|
47
|
+
}
|