@seanyao/roll 2026.527.1 → 2026.528.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/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/bin/roll +448 -4
- package/package.json +1 -1
- package/skills/roll-design/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ roll loop on # let AI work through the backlog (optional)
|
|
|
69
69
|
| Configuration (env vars) | [guide/en/configuration.md](guide/en/configuration.md) | [guide/zh/configuration.md](guide/zh/configuration.md) |
|
|
70
70
|
| Skill selection guide | [guide/en/skills.md](guide/en/skills.md) | [guide/zh/skills.md](guide/zh/skills.md) |
|
|
71
71
|
| Slides (deck generator) | [guide/en/slides.md](guide/en/slides.md) | [guide/zh/slides.md](guide/zh/slides.md) |
|
|
72
|
+
| Test isolation (`roll test` + Tart VM) | [guide/en/test-isolation.md](guide/en/test-isolation.md) | [guide/zh/test-isolation.md](guide/zh/test-isolation.md) |
|
|
72
73
|
| Cross-machine sync | [guide/en/loop.md#cross-machine-sync](guide/en/loop.md#cross-machine-sync) | [guide/zh/loop.md#跨机器同步](guide/zh/loop.md#%E8%B7%A8%E6%9C%BA%E5%99%A8%E5%90%8C%E6%AD%A5) |
|
|
73
74
|
| Pricing (cost visibility) | [guide/en/pricing.md](guide/en/pricing.md) | [guide/zh/pricing.md](guide/zh/pricing.md) |
|
|
74
75
|
| FAQ (troubleshooting) | [guide/en/faq.md](guide/en/faq.md) | [guide/zh/faq.md](guide/zh/faq.md) |
|
package/bin/roll
CHANGED
|
@@ -4,7 +4,7 @@ set -euo pipefail
|
|
|
4
4
|
# Roll — AI Agent Convention Manager
|
|
5
5
|
# Single source of truth for how all AI coding agents behave.
|
|
6
6
|
|
|
7
|
-
VERSION="2026.
|
|
7
|
+
VERSION="2026.528.1"
|
|
8
8
|
ROLL_HOME="${ROLL_HOME:-${HOME}/.roll}"
|
|
9
9
|
ROLL_CONFIG="${ROLL_HOME}/config.yaml"
|
|
10
10
|
ROLL_GLOBAL="${ROLL_HOME}/conventions/global"
|
|
@@ -70,6 +70,11 @@ ai_tool_name() {
|
|
|
70
70
|
# Antigravity (agy) reuses ~/.gemini/ from the deprecated Gemini CLI for
|
|
71
71
|
# its config dir, so a literal `gemini` basename now identifies agy.
|
|
72
72
|
[[ "$bn" == "gemini" ]] && bn="agy"
|
|
73
|
+
# FIX-126: Kimi upstream renamed its CLI to kimi-code and its config dir
|
|
74
|
+
# to ~/.kimi-code/; map both old and new basenames to the canonical
|
|
75
|
+
# "kimi" agent identifier so downstream argv / config / sync paths stay
|
|
76
|
+
# uniform across the upgrade.
|
|
77
|
+
[[ "$bn" == "kimi-code" ]] && bn="kimi"
|
|
73
78
|
echo "$bn"
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -266,6 +271,7 @@ _ensure_config_entries() {
|
|
|
266
271
|
"ai_claude:~/.claude|CLAUDE.md|CLAUDE.md"
|
|
267
272
|
"ai_agy:~/.gemini|GEMINI.md|GEMINI.md"
|
|
268
273
|
"ai_kimi:~/.kimi|AGENTS.md|AGENTS.md"
|
|
274
|
+
"ai_kimi_code:~/.kimi-code|AGENTS.md|AGENTS.md"
|
|
269
275
|
"ai_codex:~/.codex|AGENTS.md|AGENTS.md"
|
|
270
276
|
"ai_cursor:~/.cursor|.cursor-rules|.cursor-rules"
|
|
271
277
|
"ai_trae:~/.trae|user_rules.md|project_rules.md"
|
|
@@ -490,6 +496,7 @@ _install_local() {
|
|
|
490
496
|
ai_claude: ~/.claude|CLAUDE.md|CLAUDE.md
|
|
491
497
|
ai_gemini: ~/.gemini|GEMINI.md|GEMINI.md
|
|
492
498
|
ai_kimi: ~/.kimi|AGENTS.md|AGENTS.md
|
|
499
|
+
ai_kimi_code: ~/.kimi-code|AGENTS.md|AGENTS.md
|
|
493
500
|
ai_codex: ~/.codex|AGENTS.md|AGENTS.md
|
|
494
501
|
ai_cursor: ~/.cursor|.cursor-rules|.cursor-rules
|
|
495
502
|
ai_trae: ~/.trae|user_rules.md|project_rules.md
|
|
@@ -782,7 +789,7 @@ cmd_setup() {
|
|
|
782
789
|
esac
|
|
783
790
|
}
|
|
784
791
|
|
|
785
|
-
local _ai_dirs="$HOME/.claude:$HOME/.gemini:$HOME/.kimi:$HOME/.codex:$HOME/.cursor:$HOME/.trae:$HOME/.config/opencode:$HOME/.openclaw:$HOME/.pi:$HOME/.deepseek:$HOME/.qwen"
|
|
792
|
+
local _ai_dirs="$HOME/.claude:$HOME/.gemini:$HOME/.kimi:$HOME/.kimi-code:$HOME/.codex:$HOME/.cursor:$HOME/.trae:$HOME/.config/opencode:$HOME/.openclaw:$HOME/.pi:$HOME/.deepseek:$HOME/.qwen"
|
|
786
793
|
|
|
787
794
|
_run_setup_step "$ROLL_HOME" _install_local "$force"
|
|
788
795
|
_record "$(_state_to_marker "$_ROLL_SETUP_STATE")" "Install templates & conventions to ~/.roll"
|
|
@@ -1928,6 +1935,18 @@ PY
|
|
|
1928
1935
|
return 0
|
|
1929
1936
|
fi
|
|
1930
1937
|
|
|
1938
|
+
# FIX-125: cycle-context tripwire. Apply phase below runs launchctl unload
|
|
1939
|
+
# and rm against ${HOME}/Library/LaunchAgents/<plist> (bin/roll:1957-1958).
|
|
1940
|
+
# From inside a loop cycle this would mutate the host's launchd domain
|
|
1941
|
+
# using another project's identity. Doc-only offboards (no plists) stay
|
|
1942
|
+
# allowed so cycles can still call offboard for non-launchd cleanup.
|
|
1943
|
+
if [ "${#plists[@]}" -gt 0 ] && _loop_in_cycle; then
|
|
1944
|
+
err "Refusing to unload launchd plists from inside a loop cycle (FIX-125)."
|
|
1945
|
+
echo " Run 'roll offboard --confirm' from a terminal outside the cycle," >&2
|
|
1946
|
+
echo " or pause the loop first: 'roll loop pause'." >&2
|
|
1947
|
+
return 1
|
|
1948
|
+
fi
|
|
1949
|
+
|
|
1931
1950
|
# Apply. Guard every loop with a count check — `set -u` upstream makes
|
|
1932
1951
|
# naked `"${arr[@]}"` over an empty array a hard error on bash 5.0.
|
|
1933
1952
|
echo "$(msg offboard.applying_offboard)"
|
|
@@ -3133,9 +3152,20 @@ _agent_argv() {
|
|
|
3133
3152
|
*) _AGENT_ARGV=(claude -p "$prompt") ;;
|
|
3134
3153
|
esac ;;
|
|
3135
3154
|
kimi)
|
|
3155
|
+
# FIX-126: Kimi upstream renamed binary from kimi-cli → kimi-code.
|
|
3156
|
+
# Prefer the new name when present; fall back through legacy names
|
|
3157
|
+
# so users mid-upgrade keep working until they reinstall.
|
|
3158
|
+
local _kimi_bin
|
|
3159
|
+
if command -v kimi-code >/dev/null 2>&1; then
|
|
3160
|
+
_kimi_bin=kimi-code
|
|
3161
|
+
elif command -v kimi-cli >/dev/null 2>&1; then
|
|
3162
|
+
_kimi_bin=kimi-cli
|
|
3163
|
+
else
|
|
3164
|
+
_kimi_bin=kimi
|
|
3165
|
+
fi
|
|
3136
3166
|
case "$mode" in
|
|
3137
|
-
interactive) _AGENT_ARGV=(
|
|
3138
|
-
*) _AGENT_ARGV=(
|
|
3167
|
+
interactive) _AGENT_ARGV=("$_kimi_bin" "$prompt") ;;
|
|
3168
|
+
*) _AGENT_ARGV=("$_kimi_bin" --quiet -p "$prompt") ;;
|
|
3139
3169
|
esac ;;
|
|
3140
3170
|
deepseek)
|
|
3141
3171
|
# deepseek has the same argv shape in both modes (positional prompt).
|
|
@@ -4417,6 +4447,397 @@ cmd_agent() {
|
|
|
4417
4447
|
esac
|
|
4418
4448
|
}
|
|
4419
4449
|
|
|
4450
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4451
|
+
# ISOLATION — pluggable adapter for running tests in an isolated environment
|
|
4452
|
+
# (US-ISO-001). Phase 1 supports two providers: `none` (default — direct host
|
|
4453
|
+
# execution) and `tart` (US-ISO-002 — macOS VM). The dispatcher reads
|
|
4454
|
+
# .roll/local.yaml's `test_isolation.type` and routes to
|
|
4455
|
+
# `_isolation_<type>_<method>`. See .roll/features/engineering-infrastructure/
|
|
4456
|
+
# dev-vm-isolation-plan.md for the full interface contract.
|
|
4457
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4458
|
+
|
|
4459
|
+
_ISOLATION_SUPPORTED_TYPES="none tart"
|
|
4460
|
+
|
|
4461
|
+
# Read test_isolation.type from .roll/local.yaml. Falls back to "none" when
|
|
4462
|
+
# the file or key is missing. Uses python3+yaml for nested-key parsing,
|
|
4463
|
+
# matching the parser used by cmd_offboard.
|
|
4464
|
+
_isolation_get_type() {
|
|
4465
|
+
local val=""
|
|
4466
|
+
if [[ -f .roll/local.yaml ]] && command -v python3 >/dev/null 2>&1; then
|
|
4467
|
+
val=$(python3 - <<'PY' 2>/dev/null
|
|
4468
|
+
import sys
|
|
4469
|
+
try:
|
|
4470
|
+
import yaml
|
|
4471
|
+
except ImportError:
|
|
4472
|
+
sys.exit(0)
|
|
4473
|
+
try:
|
|
4474
|
+
data = yaml.safe_load(open(".roll/local.yaml")) or {}
|
|
4475
|
+
except Exception:
|
|
4476
|
+
sys.exit(0)
|
|
4477
|
+
section = data.get("test_isolation")
|
|
4478
|
+
if isinstance(section, dict):
|
|
4479
|
+
t = section.get("type")
|
|
4480
|
+
if isinstance(t, str) and t:
|
|
4481
|
+
print(t)
|
|
4482
|
+
PY
|
|
4483
|
+
)
|
|
4484
|
+
fi
|
|
4485
|
+
if [[ -z "$val" ]]; then
|
|
4486
|
+
val="none"
|
|
4487
|
+
fi
|
|
4488
|
+
printf '%s\n' "$val"
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
# Dispatch an isolation-adapter method to the configured provider.
|
|
4492
|
+
# Usage: _isolation_dispatch <method> [args...]
|
|
4493
|
+
# Methods: init / provision / exec / status / reset / destroy
|
|
4494
|
+
_isolation_dispatch() {
|
|
4495
|
+
local method="$1"; shift || true
|
|
4496
|
+
if [[ -z "$method" ]]; then
|
|
4497
|
+
err "isolation: missing method"
|
|
4498
|
+
echo " usage: _isolation_dispatch <init|provision|exec|status|reset|destroy> [args...]" >&2
|
|
4499
|
+
return 1
|
|
4500
|
+
fi
|
|
4501
|
+
|
|
4502
|
+
# Resolve provider; emit a fallback-INFO line only when the config file is
|
|
4503
|
+
# missing (so an explicit `type: none` stays quiet). Goes to stderr so the
|
|
4504
|
+
# actual dispatch output (e.g. exec stdout) stays clean.
|
|
4505
|
+
local type; type=$(_isolation_get_type)
|
|
4506
|
+
if [[ "$type" = "none" ]] && [[ ! -f .roll/local.yaml ]]; then
|
|
4507
|
+
info "isolation: no test_isolation config, falling back to type=none (host)" >&2
|
|
4508
|
+
fi
|
|
4509
|
+
|
|
4510
|
+
# Reject unknown types up front so the error names the provider, not the
|
|
4511
|
+
# missing function — this is the difference between "you typed it wrong"
|
|
4512
|
+
# and "the adapter is broken".
|
|
4513
|
+
local supported_ok=0 t
|
|
4514
|
+
for t in $_ISOLATION_SUPPORTED_TYPES; do
|
|
4515
|
+
[[ "$type" = "$t" ]] && supported_ok=1
|
|
4516
|
+
done
|
|
4517
|
+
if (( ! supported_ok )); then
|
|
4518
|
+
err "isolation: unknown type '$type' in .roll/local.yaml"
|
|
4519
|
+
echo " supported types: ${_ISOLATION_SUPPORTED_TYPES// /, }" >&2
|
|
4520
|
+
return 1
|
|
4521
|
+
fi
|
|
4522
|
+
|
|
4523
|
+
local fn="_isolation_${type}_${method}"
|
|
4524
|
+
if ! declare -F "$fn" >/dev/null 2>&1; then
|
|
4525
|
+
err "isolation: provider '$type' has no '${method}' implementation"
|
|
4526
|
+
return 1
|
|
4527
|
+
fi
|
|
4528
|
+
"$fn" "$@"
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
# ── `none` adapter (default — direct host execution) ──────────────────────
|
|
4532
|
+
# init / provision / destroy are no-ops; exec runs the command in the host
|
|
4533
|
+
# shell unchanged; status is always 'ready'; reset is a benign no-op
|
|
4534
|
+
# (US-ISO-004 will print an explanatory message when invoked via roll test).
|
|
4535
|
+
_isolation_none_init() { return 0; }
|
|
4536
|
+
_isolation_none_provision() { return 0; }
|
|
4537
|
+
_isolation_none_exec() { "$@"; }
|
|
4538
|
+
_isolation_none_status() { echo "ready"; return 0; }
|
|
4539
|
+
_isolation_none_reset() {
|
|
4540
|
+
# US-ISO-004 AC: type=none has nothing to reset; print explanation but
|
|
4541
|
+
# exit 0 (not a failure — host execution is already as clean as it gets).
|
|
4542
|
+
info "isolation type 'none' has nothing to reset (host execution is stateless)" >&2
|
|
4543
|
+
return 0
|
|
4544
|
+
}
|
|
4545
|
+
_isolation_none_destroy() { return 0; }
|
|
4546
|
+
|
|
4547
|
+
# ─── reset lock (US-ISO-004) ──────────────────────────────────────────────
|
|
4548
|
+
# A single lockfile under .roll/ prevents two `roll test --reset` runs from
|
|
4549
|
+
# racing, and forces concurrent `roll test` test-execution paths to bail
|
|
4550
|
+
# fast rather than blocking on a half-rebuilt VM. --where is read-only and
|
|
4551
|
+
# deliberately bypasses the lock.
|
|
4552
|
+
_isolation_reset_lock_path() {
|
|
4553
|
+
echo ".roll/.iso-reset.lock"
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
_isolation_reset_lock_held() {
|
|
4557
|
+
[[ -f "$(_isolation_reset_lock_path)" ]]
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4560
|
+
# Returns 0 if the caller now holds the lock; 1 if someone else does.
|
|
4561
|
+
_isolation_reset_acquire_lock() {
|
|
4562
|
+
local lock; lock=$(_isolation_reset_lock_path)
|
|
4563
|
+
if [[ -f "$lock" ]]; then
|
|
4564
|
+
return 1
|
|
4565
|
+
fi
|
|
4566
|
+
mkdir -p "$(dirname "$lock")"
|
|
4567
|
+
echo "$$" > "$lock"
|
|
4568
|
+
return 0
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
_isolation_reset_release_lock() {
|
|
4572
|
+
rm -f "$(_isolation_reset_lock_path)"
|
|
4573
|
+
}
|
|
4574
|
+
|
|
4575
|
+
# ── `tart` adapter (US-ISO-002 — macOS Apple Silicon VM via Tart) ─────────
|
|
4576
|
+
# Test override hooks (used by unit tests; default values keep prod stable):
|
|
4577
|
+
# _TART_VM_NAME — VM identifier (default: roll-dev-test)
|
|
4578
|
+
# _TART_BASE_IMAGE — OCI base image (default: cirruslabs macos-tahoe-base)
|
|
4579
|
+
# _TART_SSH_USER — SSH user inside the VM (default: admin)
|
|
4580
|
+
|
|
4581
|
+
_isolation_tart_vm_name() { printf '%s\n' "${_TART_VM_NAME:-roll-dev-test}"; }
|
|
4582
|
+
_isolation_tart_base_image() { printf '%s\n' "${_TART_BASE_IMAGE:-ghcr.io/cirruslabs/macos-tahoe-base:latest}"; }
|
|
4583
|
+
_isolation_tart_ssh_user() { printf '%s\n' "${_TART_SSH_USER:-admin}"; }
|
|
4584
|
+
|
|
4585
|
+
_isolation_tart_check_platform() {
|
|
4586
|
+
if [[ "$(uname)" != "Darwin" ]] || [[ "$(uname -m)" != "arm64" ]]; then
|
|
4587
|
+
err "Tart 仅支持 Apple Silicon macOS"
|
|
4588
|
+
err "Tart only supports Apple Silicon macOS"
|
|
4589
|
+
return 1
|
|
4590
|
+
fi
|
|
4591
|
+
return 0
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
_isolation_tart_check_binary() {
|
|
4595
|
+
if ! command -v tart >/dev/null 2>&1; then
|
|
4596
|
+
err "tart binary not found"
|
|
4597
|
+
err " install via: brew install cirruslabs/cli/tart"
|
|
4598
|
+
return 1
|
|
4599
|
+
fi
|
|
4600
|
+
return 0
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
# Returns 0 with the VM name on stdout when the VM is in `tart list`,
|
|
4604
|
+
# returns 1 silently otherwise. Caller decides what to do.
|
|
4605
|
+
_isolation_tart_vm_present() {
|
|
4606
|
+
local name; name=$(_isolation_tart_vm_name)
|
|
4607
|
+
tart list 2>/dev/null | awk -v n="$name" '$1 == n { found=1 } END { exit !found }'
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4610
|
+
# Returns the VM's IP on stdout when reachable; exit non-zero when the VM
|
|
4611
|
+
# is stopped or `tart ip` fails for any other reason.
|
|
4612
|
+
_isolation_tart_ip() {
|
|
4613
|
+
local name; name=$(_isolation_tart_vm_name)
|
|
4614
|
+
local ip; ip=$(tart ip "$name" 2>/dev/null) || return 1
|
|
4615
|
+
[[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
|
|
4616
|
+
printf '%s\n' "$ip"
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
# Status state machine — see dev-vm-isolation-plan.md §4.
|
|
4620
|
+
# Returns one of: not-installed | stopped | running | ready
|
|
4621
|
+
_isolation_tart_status() {
|
|
4622
|
+
_isolation_tart_check_platform >/dev/null 2>&1 || { echo "not-installed"; return 0; }
|
|
4623
|
+
command -v tart >/dev/null 2>&1 || { echo "not-installed"; return 0; }
|
|
4624
|
+
_isolation_tart_vm_present || { echo "not-installed"; return 0; }
|
|
4625
|
+
local ip
|
|
4626
|
+
if ! ip=$(_isolation_tart_ip); then
|
|
4627
|
+
echo "stopped"
|
|
4628
|
+
return 0
|
|
4629
|
+
fi
|
|
4630
|
+
# VM up. Is it provisioned? A trivial SSH probe is the cheapest check.
|
|
4631
|
+
local user; user=$(_isolation_tart_ssh_user)
|
|
4632
|
+
if ssh -o BatchMode=yes -o ConnectTimeout=3 -o StrictHostKeyChecking=no \
|
|
4633
|
+
"${user}@${ip}" "true" >/dev/null 2>&1; then
|
|
4634
|
+
echo "ready"
|
|
4635
|
+
else
|
|
4636
|
+
echo "running"
|
|
4637
|
+
fi
|
|
4638
|
+
return 0
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
# init: ensure the base image is cloned into our VM slot. Idempotent —
|
|
4642
|
+
# `tart clone` is skipped when the VM already exists.
|
|
4643
|
+
_isolation_tart_init() {
|
|
4644
|
+
_isolation_tart_check_platform || return 1
|
|
4645
|
+
_isolation_tart_check_binary || return 1
|
|
4646
|
+
local name; name=$(_isolation_tart_vm_name)
|
|
4647
|
+
if _isolation_tart_vm_present; then
|
|
4648
|
+
return 0
|
|
4649
|
+
fi
|
|
4650
|
+
local img; img=$(_isolation_tart_base_image)
|
|
4651
|
+
tart clone "$img" "$name"
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
# provision: ensure runtime deps are installed inside the VM. Idempotent —
|
|
4655
|
+
# brew install no-ops for already-installed packages. Requires the VM to
|
|
4656
|
+
# be running with SSH responsive (caller's responsibility, usually exec).
|
|
4657
|
+
_isolation_tart_provision() {
|
|
4658
|
+
_isolation_tart_check_platform || return 1
|
|
4659
|
+
_isolation_tart_check_binary || return 1
|
|
4660
|
+
local ip; ip=$(_isolation_tart_ip) || { err "tart provision: VM not running"; return 1; }
|
|
4661
|
+
local user; user=$(_isolation_tart_ssh_user)
|
|
4662
|
+
ssh -o BatchMode=yes -o StrictHostKeyChecking=no \
|
|
4663
|
+
"${user}@${ip}" "brew list bats >/dev/null 2>&1 || brew install bats-core; \
|
|
4664
|
+
brew list node >/dev/null 2>&1 || brew install node; \
|
|
4665
|
+
brew list bash >/dev/null 2>&1 || brew install bash"
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
# exec: run the command inside the VM. Auto-starts the VM if it's stopped.
|
|
4669
|
+
# Mounts the host worktree at /Volumes/My Shared Files/roll (Tart virtiofs).
|
|
4670
|
+
_isolation_tart_exec() {
|
|
4671
|
+
_isolation_tart_check_platform || return 1
|
|
4672
|
+
_isolation_tart_check_binary || return 1
|
|
4673
|
+
local name; name=$(_isolation_tart_vm_name)
|
|
4674
|
+
local ip
|
|
4675
|
+
if ! ip=$(_isolation_tart_ip); then
|
|
4676
|
+
# VM stopped — start it in the background with the repo mounted.
|
|
4677
|
+
local repo_root; repo_root="$(pwd -P)"
|
|
4678
|
+
tart run --dir="roll:${repo_root}" "$name" >/dev/null 2>&1 &
|
|
4679
|
+
# Wait up to ~30s for IP to come up.
|
|
4680
|
+
local i=0
|
|
4681
|
+
while (( i < 30 )); do
|
|
4682
|
+
if ip=$(_isolation_tart_ip); then break; fi
|
|
4683
|
+
sleep 1
|
|
4684
|
+
i=$((i + 1))
|
|
4685
|
+
done
|
|
4686
|
+
[[ -n "${ip:-}" ]] || { err "tart exec: VM failed to start in 30s"; return 1; }
|
|
4687
|
+
fi
|
|
4688
|
+
local user; user=$(_isolation_tart_ssh_user)
|
|
4689
|
+
ssh -o BatchMode=yes -o StrictHostKeyChecking=no "${user}@${ip}" "$@"
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
# reset: stop, delete, re-clone from base image, then re-provision.
|
|
4693
|
+
# Target: ≤90s (caller's perception); actual depends on tart clone speed.
|
|
4694
|
+
# Clone is called directly (not via init) so the sequence is unconditional —
|
|
4695
|
+
# tart's own "VM exists" check still no-ops re-clone if delete didn't take.
|
|
4696
|
+
_isolation_tart_reset() {
|
|
4697
|
+
_isolation_tart_check_platform || return 1
|
|
4698
|
+
_isolation_tart_check_binary || return 1
|
|
4699
|
+
local name; name=$(_isolation_tart_vm_name)
|
|
4700
|
+
local img; img=$(_isolation_tart_base_image)
|
|
4701
|
+
tart stop "$name" 2>/dev/null || true
|
|
4702
|
+
tart delete "$name" 2>/dev/null || true
|
|
4703
|
+
tart clone "$img" "$name" || return 1
|
|
4704
|
+
_isolation_tart_provision || true # provision may fail mid-reset; surface
|
|
4705
|
+
# via subsequent status check.
|
|
4706
|
+
}
|
|
4707
|
+
|
|
4708
|
+
# destroy: stop + delete. Doesn't rebuild.
|
|
4709
|
+
_isolation_tart_destroy() {
|
|
4710
|
+
_isolation_tart_check_platform || return 1
|
|
4711
|
+
_isolation_tart_check_binary || return 1
|
|
4712
|
+
local name; name=$(_isolation_tart_vm_name)
|
|
4713
|
+
tart stop "$name" 2>/dev/null || true
|
|
4714
|
+
tart delete "$name" 2>/dev/null || true
|
|
4715
|
+
return 0
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
# ─── cmd_test ────────────────────────────────────────────────────────────
|
|
4719
|
+
# US-ISO-003: `roll test` — runs the project's test suite through the
|
|
4720
|
+
# isolation dispatcher. The configured `test_isolation.type` determines
|
|
4721
|
+
# where the tests execute (host shell vs Tart VM). When type=tart and
|
|
4722
|
+
# the VM fails to start, the failure surfaces non-zero — no silent
|
|
4723
|
+
# fallback to host, since that would lie about where the tests ran.
|
|
4724
|
+
|
|
4725
|
+
# Print where the test suite will execute. Format is machine-readable
|
|
4726
|
+
# (one token, optionally with a colon-separated detail) so scripts can
|
|
4727
|
+
# parse it: `host`, `tart:<ip>`, `tart:stopped`, `tart:not-installed`, …
|
|
4728
|
+
_cmd_test_where() {
|
|
4729
|
+
local type; type=$(_isolation_get_type)
|
|
4730
|
+
case "$type" in
|
|
4731
|
+
none)
|
|
4732
|
+
echo "host"
|
|
4733
|
+
;;
|
|
4734
|
+
tart)
|
|
4735
|
+
local st; st=$(_isolation_tart_status)
|
|
4736
|
+
case "$st" in
|
|
4737
|
+
ready|running)
|
|
4738
|
+
local ip
|
|
4739
|
+
if ip=$(_isolation_tart_ip 2>/dev/null); then
|
|
4740
|
+
echo "tart:${ip}"
|
|
4741
|
+
else
|
|
4742
|
+
echo "tart:${st}"
|
|
4743
|
+
fi
|
|
4744
|
+
;;
|
|
4745
|
+
*)
|
|
4746
|
+
echo "tart:${st}"
|
|
4747
|
+
;;
|
|
4748
|
+
esac
|
|
4749
|
+
;;
|
|
4750
|
+
*)
|
|
4751
|
+
echo "unknown:${type}"
|
|
4752
|
+
;;
|
|
4753
|
+
esac
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
cmd_test() {
|
|
4757
|
+
# US-ISO-005: `--help` / `-h` anywhere in pre-`--` args shows help and
|
|
4758
|
+
# exits 0, so `roll test --reset --help` is a help lookup, not a reset.
|
|
4759
|
+
# Args appearing after `--` are forwarded verbatim and not intercepted.
|
|
4760
|
+
local _a
|
|
4761
|
+
for _a in "$@"; do
|
|
4762
|
+
case "$_a" in
|
|
4763
|
+
--) break ;;
|
|
4764
|
+
--help|-h) set -- --help; break ;;
|
|
4765
|
+
esac
|
|
4766
|
+
done
|
|
4767
|
+
case "${1:-}" in
|
|
4768
|
+
--help|-h)
|
|
4769
|
+
cat <<'EOF'
|
|
4770
|
+
Usage: roll test [--where | --reset] [--] [<extra-args>...]
|
|
4771
|
+
|
|
4772
|
+
Runs the project's test suite through the isolation adapter chosen in
|
|
4773
|
+
.roll/local.yaml:
|
|
4774
|
+
|
|
4775
|
+
test_isolation:
|
|
4776
|
+
type: none (default) Direct host execution — same shell as `npm test`.
|
|
4777
|
+
type: tart Inside the Apple-Silicon `roll-dev-test` Tart VM,
|
|
4778
|
+
so tests can't reach the host's launchd / shared
|
|
4779
|
+
roll state. Tart isn't auto-installed; run
|
|
4780
|
+
`brew install cirruslabs/cli/tart` first.
|
|
4781
|
+
|
|
4782
|
+
Flags:
|
|
4783
|
+
--where Print where tests will run, then exit (e.g. `host`,
|
|
4784
|
+
`tart:192.168.64.5`, `tart:stopped`).
|
|
4785
|
+
--reset Rebuild the isolation environment to a clean baseline.
|
|
4786
|
+
type=tart: stop → delete → clone → provision (~90s).
|
|
4787
|
+
type=none: prints a note and exits 0 (host is stateless).
|
|
4788
|
+
Holds a lockfile under .roll/.iso-reset.lock; concurrent
|
|
4789
|
+
`roll test` invocations fast-fail with a clear error.
|
|
4790
|
+
--help, -h Show this help.
|
|
4791
|
+
|
|
4792
|
+
Examples:
|
|
4793
|
+
roll test Run the suite in whatever the config says.
|
|
4794
|
+
roll test -- --tier=fast Forward arguments to npm test.
|
|
4795
|
+
roll test --where Don't run; just report routing.
|
|
4796
|
+
roll test --reset Rebuild the VM (or host no-op).
|
|
4797
|
+
|
|
4798
|
+
When type=tart and the VM can't be reached, the command exits non-zero
|
|
4799
|
+
rather than silently falling back to host execution.
|
|
4800
|
+
EOF
|
|
4801
|
+
return 0
|
|
4802
|
+
;;
|
|
4803
|
+
--where)
|
|
4804
|
+
_cmd_test_where
|
|
4805
|
+
return 0
|
|
4806
|
+
;;
|
|
4807
|
+
--reset)
|
|
4808
|
+
# Refuse if another reset is in progress — fast-fail beats blocking
|
|
4809
|
+
# on a half-rebuilt VM (US-ISO-004 AC).
|
|
4810
|
+
if _isolation_reset_lock_held; then
|
|
4811
|
+
err "roll test --reset: another reset is already in progress"
|
|
4812
|
+
echo " lock: $(_isolation_reset_lock_path) (delete manually if stale)" >&2
|
|
4813
|
+
return 1
|
|
4814
|
+
fi
|
|
4815
|
+
_isolation_reset_acquire_lock || {
|
|
4816
|
+
err "roll test --reset: failed to acquire reset lock"
|
|
4817
|
+
return 1
|
|
4818
|
+
}
|
|
4819
|
+
# Make sure the lock comes off no matter how dispatch exits.
|
|
4820
|
+
trap '_isolation_reset_release_lock' RETURN
|
|
4821
|
+
_isolation_dispatch reset
|
|
4822
|
+
return $?
|
|
4823
|
+
;;
|
|
4824
|
+
--)
|
|
4825
|
+
shift
|
|
4826
|
+
;;
|
|
4827
|
+
esac
|
|
4828
|
+
|
|
4829
|
+
# Test-execution path. If a reset is in progress, bail rather than racing
|
|
4830
|
+
# into a half-rebuilt VM — user can `roll test --where` to inspect state.
|
|
4831
|
+
if _isolation_reset_lock_held; then
|
|
4832
|
+
err "roll test: a reset is in progress (lock: $(_isolation_reset_lock_path))"
|
|
4833
|
+
echo " re-run once the reset completes, or delete the lockfile if stale" >&2
|
|
4834
|
+
return 1
|
|
4835
|
+
fi
|
|
4836
|
+
|
|
4837
|
+
# Pass remaining args through to npm test inside the configured adapter.
|
|
4838
|
+
_isolation_dispatch exec npm test "$@"
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4420
4841
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
4421
4842
|
# LOOP — autonomous BACKLOG executor management
|
|
4422
4843
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -6768,11 +7189,33 @@ _loop_attach() {
|
|
|
6768
7189
|
exec tmux attach -t "$session"
|
|
6769
7190
|
}
|
|
6770
7191
|
|
|
7192
|
+
# FIX-125: detect whether we are running inside a loop cycle. Cycle context
|
|
7193
|
+
# is signalled by env vars exported by the cycle runner (ROLL_LOOP_AGENT,
|
|
7194
|
+
# bin/roll:5736) or by the outer cycle script (ROLL_CYCLE_LOG_RAW,
|
|
7195
|
+
# bin/roll:6044). Used by callers that touch canonical ${HOME}/Library/LaunchAgents
|
|
7196
|
+
# directly (_loop_gc, cmd_offboard) to refuse host-loop mutations from inside
|
|
7197
|
+
# a cycle. Read-only ops are unaffected.
|
|
7198
|
+
_loop_in_cycle() {
|
|
7199
|
+
[[ -n "${ROLL_LOOP_AGENT:-}" || -n "${ROLL_CYCLE_LOG_RAW:-}" ]]
|
|
7200
|
+
}
|
|
7201
|
+
|
|
6771
7202
|
# US-LOOP-021: garbage-collect orphan slugs, tmp debris, and expired backups.
|
|
6772
7203
|
# Usage: _loop_gc [--dry-run] [--keep-days N]
|
|
6773
7204
|
# Keeps backups/migrated files within N days (default 30).
|
|
6774
7205
|
# Retention order: ROLL_LOOP_GC_RETENTION_DAYS env > .roll/local.yaml loop_gc.retention_days > 30.
|
|
6775
7206
|
_loop_gc() {
|
|
7207
|
+
# FIX-125: refuse from inside a loop cycle. Phase 1 below scans/mutates
|
|
7208
|
+
# ${HOME}/Library/LaunchAgents directly (bin/roll:6814,6847) — running it
|
|
7209
|
+
# from a cycle would let one project's tick remove another project's plist
|
|
7210
|
+
# under the host's launchd domain. Read-only ops (status, runs) are
|
|
7211
|
+
# unaffected; only the GC mutator is gated.
|
|
7212
|
+
if _loop_in_cycle; then
|
|
7213
|
+
echo "roll loop gc: refusing — cycle-context tripwire (FIX-125)" >&2
|
|
7214
|
+
echo " This command scans ~/Library/LaunchAgents directly. Running it" >&2
|
|
7215
|
+
echo " from inside a loop cycle is a known host-state corruption path." >&2
|
|
7216
|
+
return 1
|
|
7217
|
+
fi
|
|
7218
|
+
|
|
6776
7219
|
local dry_run=false
|
|
6777
7220
|
local keep_days=30
|
|
6778
7221
|
|
|
@@ -9854,6 +10297,7 @@ main() {
|
|
|
9854
10297
|
doctor) cmd_doctor "$@" ;;
|
|
9855
10298
|
review-pr) cmd_review_pr "$@" ;;
|
|
9856
10299
|
slides) cmd_slides "$@" ;;
|
|
10300
|
+
test) cmd_test "$@" ;;
|
|
9857
10301
|
prices) cmd_prices "$@" ;;
|
|
9858
10302
|
changelog) cmd_changelog "$@" ;;
|
|
9859
10303
|
version|--version|-v) echo "roll v${VERSION}" ;;
|
package/package.json
CHANGED
|
@@ -121,7 +121,7 @@ Document structure (two-layer separation):
|
|
|
121
121
|
3. **FIX / IDEA detail files use ID-prefixed filenames**: `.roll/features/<epic>/FIX-097.md`, not `.roll/features/<epic>/some-descriptive-slug.md`. Reason: a single FIX is one card, not a long-lived feature; the ID is the most stable handle, descriptive slugs date quickly and break links. US can keep feature-slug naming (US lives inside a multi-Story feature file). Quick lookup: `ls .roll/features/<epic>/FIX-*.md` finds all bugs in that area without grepping content.
|
|
122
122
|
4. .roll/backlog.md only contains index rows (one row per US), **do not write** AC / Files / Notes
|
|
123
123
|
5. Domain model files go in `.roll/domain/` — create on first greenfield design, update incrementally
|
|
124
|
-
6. **Do not** write to `~/.kimi
|
|
124
|
+
6. **Do not** write to `~/.kimi/`, `~/.kimi-code/`, or any global config directory
|
|
125
125
|
|
|
126
126
|
**File path resolution order:**
|
|
127
127
|
1. Determine Feature ownership (based on the requirement domain: compiler / ingest / qa / ...)
|