Skip to content

Commit fc13fd5

Browse files
committed
AI: workspace 2 - toolkit
1 parent dcb443e commit fc13fd5

File tree

3 files changed

+211
-3
lines changed

3 files changed

+211
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
**/target/
66
build
77
*~
8-
.idea/
8+
.idea/
9+
.agent-tools/

agents.just

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
consolidate workspace_id start_change_id end_change_id:
2-
bash -x scripts/agent-workspace.sh run {{workspace_id}} --workflow consolidate -- just --justfile agents.just --set workspace_id={{workspace_id}} consolidate-inner {{start_change_id}} {{end_change_id}}
2+
scripts/agent-workspace.sh run {{workspace_id}} --workflow consolidate -- just --justfile .agent-tools/agents.just --set workspace_id={{workspace_id}} consolidate-inner {{start_change_id}} {{end_change_id}}
33

44
consolidate-inner start_change_id end_change_id:
55
#!/usr/bin/env sh
@@ -40,6 +40,9 @@ workspace-shell workspace_id:
4040
workspace-clean workspace_id:
4141
scripts/agent-workspace.sh clean {{workspace_id}}
4242

43+
workspace-sync-tools workspace_id:
44+
scripts/agent-workspace.sh sync-tools {{workspace_id}}
45+
4346
questions-for-pm rev='@':
4447
#!/usr/bin/env sh
4548
CURRENT_CHANGE=`jj log -r @ --template 'change_id' --no-graph`

scripts/agent-workspace.sh

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Usage:
88
agent-workspace.sh status [<workspace-id>]
99
agent-workspace.sh shell <workspace-id>
1010
agent-workspace.sh clean <workspace-id>
11+
agent-workspace.sh sync-tools <workspace-id>
1112
USAGE
1213
}
1314

@@ -32,6 +33,9 @@ cache_root_default=${XDG_CACHE_HOME:-"$HOME/.cache"}
3233
workspace_root=${AI_WORKSPACES_ROOT:-"$cache_root_default/ai-workspaces"}
3334
workspace_repo_root="$workspace_root/$repo_slug"
3435

36+
tools_source_root="${AGENT_TOOLS_SOURCE:-$repo_root}"
37+
tools_relative_paths=("agents.just" "rules" "scripts")
38+
3539
sanitise_workspace_id() {
3640
local id="$1"
3741
[[ "$id" =~ ^[A-Za-z0-9._-]+$ ]] || fail "workspace id '$id' contains invalid characters"
@@ -48,6 +52,108 @@ metadata_path_for() {
4852
echo "$(workspace_path_for "$workspace_id")/.agent-workflow.json"
4953
}
5054

55+
compute_tools_hash() {
56+
python - "$tools_source_root" "${tools_relative_paths[@]}" <<'PY'
57+
import hashlib
58+
import os
59+
import sys
60+
from pathlib import Path
61+
62+
root = Path(sys.argv[1]).resolve()
63+
paths = sys.argv[2:]
64+
65+
hasher = hashlib.sha256()
66+
67+
def add_file(path: Path):
68+
rel = path.relative_to(root)
69+
hasher.update(str(rel).encode('utf-8'))
70+
hasher.update(b'\0')
71+
with path.open('rb') as fh:
72+
while True:
73+
chunk = fh.read(65536)
74+
if not chunk:
75+
break
76+
hasher.update(chunk)
77+
78+
if not paths:
79+
print('')
80+
raise SystemExit(0)
81+
82+
for rel in paths:
83+
src = root / rel
84+
if not src.exists():
85+
print(f"missing:{rel}", file=sys.stderr)
86+
raise SystemExit(1)
87+
if src.is_file():
88+
add_file(src)
89+
else:
90+
for file_path in sorted(src.rglob('*')):
91+
if file_path.is_file():
92+
add_file(file_path)
93+
94+
print(hasher.hexdigest())
95+
PY
96+
}
97+
98+
copy_tools_payload() {
99+
local dest="$1"
100+
python - "$tools_source_root" "$dest" "${tools_relative_paths[@]}" <<'PY'
101+
import shutil
102+
import sys
103+
from pathlib import Path
104+
105+
root = Path(sys.argv[1]).resolve()
106+
dest = Path(sys.argv[2]).resolve()
107+
paths = sys.argv[3:]
108+
109+
for rel in paths:
110+
src = root / rel
111+
if not src.exists():
112+
raise SystemExit(f"tool path missing: {rel}")
113+
target = dest / rel
114+
if src.is_dir():
115+
shutil.copytree(src, target, dirs_exist_ok=True)
116+
else:
117+
target.parent.mkdir(parents=True, exist_ok=True)
118+
shutil.copy2(src, target)
119+
PY
120+
}
121+
122+
prepare_tools_copy() {
123+
local workspace_path="$1"
124+
local force_copy="${2:-false}"
125+
126+
local tools_dest="$workspace_path/.agent-tools"
127+
TOOLS_COPY_PATH="$tools_dest"
128+
129+
local desired_hash
130+
desired_hash=$(compute_tools_hash) || fail "failed to hash tool sources"
131+
TOOLS_VERSION="$desired_hash"
132+
133+
local version_file="$tools_dest/.version"
134+
local current_hash=""
135+
if [[ -f "$version_file" ]]; then
136+
current_hash=$(cat "$version_file")
137+
fi
138+
139+
if [[ "$force_copy" == "true" ]]; then
140+
current_hash=""
141+
fi
142+
143+
if [[ "$current_hash" != "$desired_hash" ]]; then
144+
case "$tools_dest" in
145+
"$workspace_path"/*) ;;
146+
*) fail "tool copy path outside workspace: $tools_dest" ;;
147+
esac
148+
rm -rf "$tools_dest"
149+
mkdir -p "$tools_dest"
150+
copy_tools_payload "$tools_dest"
151+
printf '%s\n' "$desired_hash" > "$version_file"
152+
else
153+
mkdir -p "$tools_dest"
154+
fi
155+
}
156+
51157
ensure_workspace() {
52158
local workspace_id="$1"
53159
local workspace_path="$2"
@@ -75,7 +181,8 @@ update_metadata() {
75181

76182
STATUS="$status" TIMESTAMP="$timestamp" WORKSPACE_ID="$WORKSPACE_ID" REPO_ROOT="$repo_root" \
77183
WORKSPACE_PATH="$WORKSPACE_PATH" WORKFLOW_NAME="${WORKFLOW_NAME:-}" COMMAND_JSON="$COMMAND_JSON" \
78-
DIRENV_ALLOWED="$DIRENV_ALLOWED" BASE_CHANGE="${BASE_CHANGE:-}" \
184+
DIRENV_ALLOWED="$DIRENV_ALLOWED" BASE_CHANGE="${BASE_CHANGE:-}" TOOLS_SOURCE="${TOOLS_SOURCE:-}" \
185+
TOOLS_COPY="${TOOLS_COPY:-}" TOOLS_VERSION="${TOOLS_VERSION:-}" \
79186
python - "$metadata_path" <<'PY'
80187
import json
81188
import os
@@ -91,6 +198,9 @@ workflow_name = os.environ.get("WORKFLOW_NAME") or None
91198
direnv_allowed = os.environ.get("DIRENV_ALLOWED", "false").lower() == "true"
92199
command = json.loads(os.environ.get("COMMAND_JSON", "[]"))
93200
base_change = os.environ.get("BASE_CHANGE") or None
201+
tools_source = os.environ.get("TOOLS_SOURCE") or None
202+
tools_copy = os.environ.get("TOOLS_COPY") or None
203+
tools_version = os.environ.get("TOOLS_VERSION") or None
94204
95205
try:
96206
with open(path, "r", encoding="utf-8") as fh:
@@ -121,6 +231,21 @@ if base_change is None:
121231
else:
122232
data["base_change"] = base_change
123233
234+
if tools_source is None:
235+
data.pop("tools_source", None)
236+
else:
237+
data["tools_source"] = tools_source
238+
239+
if tools_copy is None:
240+
data.pop("tools_copy", None)
241+
else:
242+
data["tools_copy"] = tools_copy
243+
244+
if tools_version is None:
245+
data.pop("tools_version", None)
246+
else:
247+
data["tools_version"] = tools_version
248+
124249
with open(path, "w", encoding="utf-8") as fh:
125250
json.dump(data, fh, indent=2)
126251
fh.write("\n")
@@ -192,6 +317,8 @@ run_subcommand() {
192317
(cd "$workspace_path" && jj edit "$base_change")
193318
fi
194319

320+
prepare_tools_copy "$workspace_path"
321+
195322
local direnv_status="false"
196323
if [[ "$use_direnv" == "true" ]]; then
197324
command -v direnv >/dev/null 2>&1 || fail "direnv is required but not installed"
@@ -202,6 +329,7 @@ run_subcommand() {
202329
direnv_status="true"
203330
fi
204331
DIRENV_ALLOWED="$direnv_status"
332+
TOOLS_SOURCE="$tools_source_root"
205333

206334
COMMAND_JSON=$(python - <<'PY' "${cmd[@]}"
207335
import json
@@ -210,6 +338,7 @@ print(json.dumps(sys.argv[1:]))
210338
PY
211339
)
212340

341+
TOOLS_COPY="$TOOLS_COPY_PATH"
213342
update_metadata "$metadata_path" "running"
214343

215344
local exit_code
@@ -220,12 +349,18 @@ PY
220349
AGENT_WORKSPACE_PATH="$workspace_path" \
221350
AGENT_WORKSPACE_METADATA="$metadata_path" \
222351
AGENT_WORKSPACE_REPO_ROOT="$repo_root" \
352+
AGENT_TOOL_COPY_ROOT="$TOOLS_COPY_PATH" \
353+
AGENT_TOOLS_VERSION="$TOOLS_VERSION" \
354+
AGENT_TOOLS_SOURCE="$tools_source_root" \
223355
direnv exec . "${cmd[@]}"
224356
else
225357
AGENT_WORKSPACE_ID="$workspace_id" \
226358
AGENT_WORKSPACE_PATH="$workspace_path" \
227359
AGENT_WORKSPACE_METADATA="$metadata_path" \
228360
AGENT_WORKSPACE_REPO_ROOT="$repo_root" \
361+
AGENT_TOOL_COPY_ROOT="$TOOLS_COPY_PATH" \
362+
AGENT_TOOLS_VERSION="$TOOLS_VERSION" \
363+
AGENT_TOOLS_SOURCE="$tools_source_root" \
229364
"${cmd[@]}"
230365
fi
231366
)
@@ -361,6 +496,72 @@ clean_subcommand() {
361496
fi
362497
}
363498

499+
sync_tools_subcommand() {
500+
if [[ $# -ne 1 ]]; then
501+
fail "sync-tools requires a workspace id"
502+
fi
503+
504+
local workspace_id
505+
workspace_id=$(sanitise_workspace_id "$1")
506+
local workspace_path
507+
workspace_path=$(workspace_path_for "$workspace_id")
508+
509+
[[ -d "$workspace_path" ]] || fail "workspace '$workspace_id' does not exist"
510+
511+
if ! jj workspace root --workspace "$workspace_id" >/dev/null 2>&1; then
512+
fail "workspace '$workspace_id' is not registered with jj"
513+
fi
514+
515+
prepare_tools_copy "$workspace_path" "true"
516+
517+
local metadata_path
518+
metadata_path=$(metadata_path_for "$workspace_id")
519+
520+
local prev_status="idle"
521+
local prev_workflow=""
522+
local prev_command="[]"
523+
local prev_direnv="false"
524+
local prev_base=""
525+
526+
if [[ -f "$metadata_path" ]]; then
527+
local -a meta_info=()
528+
mapfile -t meta_info < <(python - "$metadata_path" <<'PY'
529+
import json
530+
import sys
531+
532+
path = sys.argv[1]
533+
with open(path, 'r', encoding='utf-8') as fh:
534+
data = json.load(fh)
535+
536+
print(data.get('status', 'idle'))
537+
print(data.get('workflow') or '')
538+
print(json.dumps(data.get('command', [])))
539+
print('true' if data.get('direnv_allowed') else 'false')
540+
print(data.get('base_change') or '')
541+
PY
542+
)
543+
if [[ ${#meta_info[@]} -ge 5 ]]; then
544+
prev_status="${meta_info[0]}"
545+
prev_workflow="${meta_info[1]}"
546+
prev_command="${meta_info[2]}"
547+
prev_direnv="${meta_info[3]}"
548+
prev_base="${meta_info[4]}"
549+
fi
550+
fi
551+
552+
WORKSPACE_ID="$workspace_id"
553+
WORKSPACE_PATH="$workspace_path"
554+
METADATA_PATH="$metadata_path"
555+
WORKFLOW_NAME="$prev_workflow"
556+
BASE_CHANGE="$prev_base"
557+
COMMAND_JSON="$prev_command"
558+
DIRENV_ALLOWED="$prev_direnv"
559+
TOOLS_SOURCE="$tools_source_root"
560+
TOOLS_COPY="$TOOLS_COPY_PATH"
561+
TOOLS_VERSION="$TOOLS_VERSION"
562+
update_metadata "$metadata_path" "$prev_status"
563+
}
564+
364565
main() {
365566
if [[ $# -lt 1 ]]; then
366567
usage
@@ -384,6 +585,9 @@ main() {
384585
clean)
385586
clean_subcommand "$@"
386587
;;
588+
sync-tools)
589+
sync_tools_subcommand "$@"
590+
;;
387591
*)
388592
usage
389593
fail "unknown subcommand '$subcommand'"

0 commit comments

Comments
 (0)