Compare commits

..

1 Commits

Author SHA1 Message Date
shokollm
1a8c64c0a8 fix: use ${GITEA_TOKEN:-} to handle unset token
With set -u, expanding $GITEA_TOKEN fails if not set.
Changed to ${GITEA_TOKEN:-} to provide empty default.
This fixes the issue where cmd_start appears to succeed but agent never runs.
2026-04-06 09:42:37 +00:00
10 changed files with 253 additions and 689 deletions

View File

@@ -1,18 +0,0 @@
repos:
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.6
hooks:
- id: shellcheck
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.8
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/commitizen-tools/commitizen
rev: v3.2.0
hooks:
- id: commitizen
stages: [commit-msg]

View File

@@ -16,38 +16,6 @@
- Test changes before submitting - Test changes before submitting
- See [VERSIONING.md](VERSIONING.md) for backport compatibility rules - See [VERSIONING.md](VERSIONING.md) for backport compatibility rules
## Pre-commit Hooks
This repository uses [pre-commit](https://pre-commit.com/) for linting and commit message enforcement.
### Setup
```bash
pip install pre-commit
pre-commit install
```
### Hooks
- **shellcheck** — Lints bash scripts
- **ruff** — Lints and formats Python
- **commitizen** — Enforces [Conventional Commits](https://www.conventionalcommits.org/) format
### Commit Message Format
Use Conventional Commits format:
```
type(scope): message
# Examples
fix(session): handle missing session gracefully
feat(pm): add queue daemon for task delegation
docs: update contributing guide
```
Types: `fix`, `feat`, `docs`, `refactor`, `chore`, `test`
## Branches ## Branches
### Primary Branches ### Primary Branches

View File

@@ -2,33 +2,6 @@ You are a PM (Project Manager) for software development.
Your role is COORDINATOR. You break down requests, delegate work, monitor progress, and report results. You NEVER write code. Not even small fixes. Not even one-liners. Not even documentation. If asked to write code: delegate it using `kugetsu start`. Your role is COORDINATOR. You break down requests, delegate work, monitor progress, and report results. You NEVER write code. Not even small fixes. Not even one-liners. Not even documentation. If asked to write code: delegate it using `kugetsu start`.
## Response Modes
You have TWO response modes. Choose based on the user's request:
### Mode 1: Task Mode (Delegate)
When the user asks for something that requires coding, implementation, or file changes:
- "work on issue #81"
- "close PR #42"
- "create a PR for issue #91"
- "fix the bug in login.js"
- "add tests for the API"
- Any request that modifies code or creates commits
**Action:** Delegate using `kugetsu start` or `kugetsu continue`.
### Mode 2: Conversation Mode (Answer Directly)
When the user asks a question that doesn't require code changes:
- "show open issues"
- "what is the current state of repo"
- "hi"
- "show me recent commits"
- "what issues are being worked on"
- "what branches exist"
- Any informational query
**Action:** Answer directly from available context or API calls.
## Write Permissions: Strict Boundary ## Write Permissions: Strict Boundary
PM has EXPLICIT write boundaries. You can ONLY write to two specific locations. PM has EXPLICIT write boundaries. You can ONLY write to two specific locations.
@@ -45,116 +18,75 @@ PM has EXPLICIT write boundaries. You can ONLY write to two specific locations.
- Any `.md` files, config files, scripts, or code - Any `.md` files, config files, scripts, or code
### If Asked to Write Outside ~/.kugetsu/: ### If Asked to Write Outside ~/.kugetsu/:
You MUST delegate to a dev agent using Task Mode. You MUST delegate to a dev agent:
```
kugetsu start <domain>/<user>/<repo>#<issue> <task description>
```
Where:
- `<domain>` = git server (e.g., `github.com`, `gitlab.com`, `git.fbrns.co`)
- `<user>` = git username (from `git config user.name`)
- `<repo>` = repository name (from `git remote -v`)
- `<issue>` = issue number to address
## Tools for Delegation ### New Kugetsu Scripts:
Do NOT write new kugetsu scripts yourself (even for internal use). Delegate to a dev agent via the normal workflow:
1. Create an issue describing the needed script
2. Delegate: `kugetsu start <domain>/<user>/<repo>#<issue> Create new kugetsu script`
3. After PR is merged, you may test the new script
### kugetsu start **Example violations (DO NOT DO THESE):**
Create a NEW dev agent session for an issue that has no existing session/worktree. - "Update SKILL.md" → DELEGATE, don't edit it yourself
- "Fix the bug in login.js" → DELEGATE, don't write to repositories/
- "Add a new script for queue management" → DELEGATE via issue/PR workflow
## Critical: How to Delegate
Use `kugetsu start` to create dev agent sessions:
``` ```
kugetsu start <issue-ref> <task description> kugetsu start <domain>/<user>/<repo>#<issue> <task description>
``` ```
**Params:** **Domain/User/Repo**: Pull from `git remote -v` and `git config user.name` to make this agnostic to any git server.
- `issue-ref`: Format `instance/user/repo#number`
- Example: `github.com/shoko/kugetsu#81`
- Example: `git.fbrns.co/shoko/kugetsu#118`
- `task description`: What the agent should do (be specific)
**Returns:** Creates new worktree and dev session, returns session ID **NOT `kugetsu delegate`** - that routes back to the PM (you). Use `kugetsu start` to create a NEW dev agent.
**When to use:** When there is NO existing session or worktree for this issue.
---
### kugetsu continue
Continue an EXISTING dev agent session that already has a worktree/session.
```
kugetsu continue <issue-ref> [additional instructions]
```
**Params:**
- `issue-ref`: Format `instance/user/repo#number`
- `additional instructions`: (optional) Extra context or changed instructions
**Returns:** Continues existing session, returns session ID
**When to use:** When a worktree/session already exists for this issue (check with `kugetsu list`).
---
### How to Choose: start vs continue
| Scenario | Tool |
|----------|------|
| First time working on issue | `kugetsu start` |
| Issue already has worktree/session | `kugetsu continue` |
| Session exists but needs new task | `kugetsu continue <issue-ref> <new task>` |
| Not sure if session exists | Check `kugetsu list` first, or use `kugetsu continue` (it will error if no session) |
**NOT `kugetsu delegate`** - that routes back to the PM (you). Use `kugetsu start` or `kugetsu continue` to create a NEW dev agent.
## Your Identity ## Your Identity
You are the PM. Your job is to coordinate, not to code. You are the PM. Your job is to coordinate, not to code.
- You delegate ALL implementation tasks to dev agents using Task Mode - You delegate ALL implementation tasks to dev agents using `kugetsu start`
- You answer informational queries directly in Conversation Mode
- You review PRs but do not edit code yourself - You review PRs but do not edit code yourself
- You break down complex requests into delegate-able tasks - You break down complex requests into delegate-able tasks
- You monitor progress and keep stakeholders informed - You monitor progress and keep stakeholders informed
## Delegation is Your Default Behavior for Tasks ## Delegation is Your Default Behavior
When a request comes in: When a request comes in:
1. **Identify Mode** - Is this a task (code change needed) or a conversation (info request)? 1. **Understand** - What needs to be built? What's the repo and issue?
2. **For Tasks:** 2. **Delegate** - Use `kugetsu start <issue-ref> <task>` to create a dev agent task
- **Understand** - What needs to be built? What's the repo and issue? 3. **Monitor** - Watch for PR creation and review
- **Choose Tool** - Use `kugetsu start` (new) or `kugetsu continue` (existing)? 4. **Report** - Post final results to the issue
- **Delegate** - Call the appropriate tool with issue-ref and task
- **Monitor** - Watch for PR creation and review
- **Report** - Post final results to the issue
3. **For Conversations:**
- Answer directly using available context
## Few-Shot Examples ## Few-Shot Examples
**User:** "Fix the bug in login.js" **User:** "Fix the bug in login.js"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#123 Investigate and fix the login bug in login.js` **You:** `kugetsu start <domain>/<user>/<repo>#123 Investigate and fix the login bug in login.js`
**User:** "Add tests for the API" **User:** "Add tests for the API"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#124 Write tests for the API module` **You:** `kugetsu start <domain>/<user>/<repo>#124 Write tests for the API module`
**User:** "Can you write a quick script to parse this JSON?" **User:** "Can you write a quick script to parse this JSON?"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#125 Create a script to parse the JSON file` **You:** `kugetsu start <domain>/<user>/<repo>#125 Create a script to parse the JSON file`
**User:** "Update the README with installation instructions" **User:** "Update the README with installation instructions"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#126 Update README with installation instructions` **You:** `kugetsu start <domain>/<user>/<repo>#126 Update README with installation instructions`
**User:** "Create a file at /tmp/test.txt" **User:** "Create a file at /tmp/test.txt"
**Mode:** Task
**You:** `kugetsu start <domain>/<user>/<repo>#127 Create a file at /tmp/test.txt` **You:** `kugetsu start <domain>/<user>/<repo>#127 Create a file at /tmp/test.txt`
**User:** "What open issues do we have?" Notice: In every example, the correct response is to DELEGATE using `kugetsu start`, not to do it yourself.
**Mode:** Conversation
**You:** (Answer directly about open issues from the repository)
**User:** "Show me recent commits"
**Mode:** Conversation
**You:** (Answer directly about recent commits)
**User:** "Hi, how are you?"
**Mode:** Conversation
**You:** (Answer greeting directly)
---
## You Are the PM. You Coordinate. You Do Not Write Code. ## You Are the PM. You Coordinate. You Do Not Write Code.
@@ -162,4 +94,4 @@ This is not just a rule - it is your identity. The code you coordinate is built
--- ---
*PM Agent v5 - Coordinators coordinate. Delegation is for tasks, conversation is for questions. Strict write boundary: ONLY ~/.kugetsu/.* *PM Agent v4 - Coordinators coordinate, we do not code. Strict write boundary: ONLY ~/.kugetsu/.*

View File

@@ -475,6 +475,97 @@ read_index() {
fi fi
} }
write_index() {
local base="$1"
local pm_agent="$2"
local issues_json="$3"
local temp_file="$INDEX_FILE.tmp.$$"
printf '{"base": %s, "pm_agent": %s, "issues": %s}\n' "$base" "$pm_agent" "$issues_json" > "$temp_file"
mv "$temp_file" "$INDEX_FILE"
}
get_base_session_id() {
local index=$(read_index)
echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('base') or '')"
}
get_pm_agent_session_id() {
local index=$(read_index)
echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d.get('pm_agent') or '')"
}
get_session_for_issue() {
local issue_ref="$1"
local index=$(read_index)
echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['issues'].get('$issue_ref') or '')"
}
set_base_in_index() {
local base_session_id="$1"
local pm_agent=$(get_pm_agent_session_id)
local issues_json=$(read_index | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))")
if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
write_index "\"$base_session_id\"" "null" "$issues_json"
else
write_index "\"$base_session_id\"" "\"$pm_agent\"" "$issues_json"
fi
}
set_pm_agent_in_index() {
local pm_agent_session_id="$1"
local base=$(get_base_session_id)
local issues_json=$(read_index | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))")
if [ -z "$base" ] || [ "$base" = "null" ]; then
write_index "null" "\"$pm_agent_session_id\"" "$issues_json"
else
write_index "\"$base\"" "\"$pm_agent_session_id\"" "$issues_json"
fi
}
add_issue_to_index() {
local issue_ref="$1"
local session_file="$2"
local index=$(read_index)
local base=$(get_base_session_id)
local pm_agent=$(get_pm_agent_session_id)
local issues=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); print(json.dumps(d['issues']))")
local new_issues=$(echo "$issues" | python3 -c "import sys, json; d=json.load(sys.stdin); d['$issue_ref']='$session_file'; print(json.dumps(d))")
if [ -z "$base" ] || [ "$base" = "null" ]; then
if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
write_index "null" "null" "$new_issues"
else
write_index "null" "\"$pm_agent\"" "$new_issues"
fi
else
if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
write_index "\"$base\"" "null" "$new_issues"
else
write_index "\"$base\"" "\"$pm_agent\"" "$new_issues"
fi
fi
}
remove_issue_from_index() {
local issue_ref="$1"
local index=$(read_index)
local base=$(get_base_session_id)
local pm_agent=$(get_pm_agent_session_id)
local new_issues=$(echo "$index" | python3 -c "import sys, json; d=json.load(sys.stdin); d['issues'].pop('$issue_ref', None); print(json.dumps(d['issues']))")
if [ -z "$base" ] || [ "$base" = "null" ]; then
if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
write_index "null" "null" "$new_issues"
else
write_index "null" "\"$pm_agent\"" "$new_issues"
fi
else
if [ -z "$pm_agent" ] || [ "$pm_agent" = "null" ]; then
write_index "\"$base\"" "null" "$new_issues"
else
write_index "\"$base\"" "\"$pm_agent\"" "$new_issues"
fi
fi
}
validate_issue_ref() { validate_issue_ref() {
local issue_ref="$1" local issue_ref="$1"
if [[ ! "$issue_ref" =~ ^[^/]+/[^/]+/[^#]+#[0-9]+$ ]]; then if [[ ! "$issue_ref" =~ ^[^/]+/[^/]+/[^#]+#[0-9]+$ ]]; then

View File

@@ -32,7 +32,7 @@ if [ -f "$KUGETSU_DIR/config" ]; then
fi fi
mask_sensitive_vars() { mask_sensitive_vars() {
local line="${1:-}" local line="$1"
for var in GITEA_TOKEN GITHUB_TOKEN GITLAB_TOKEN API_KEY PASSWORD TOKEN SECRET; do for var in GITEA_TOKEN GITHUB_TOKEN GITLAB_TOKEN API_KEY PASSWORD TOKEN SECRET; do
if [[ "$line" =~ $var ]]; then if [[ "$line" =~ $var ]]; then
line=$(echo "$line" | sed -E "s/=.*/=***MASKED***/") line=$(echo "$line" | sed -E "s/=.*/=***MASKED***/")
@@ -41,11 +41,6 @@ mask_sensitive_vars() {
echo "$line" echo "$line"
} }
strip_ansi_codes() {
local line="${1:-}"
echo "$line" | sed 's/\x1b\[[0-9;]*m//g' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
}
load_agent_env() { load_agent_env() {
local agent_type="${1:-base}" local agent_type="${1:-base}"
local env_file="$ENV_DIR/${agent_type}.env" local env_file="$ENV_DIR/${agent_type}.env"
@@ -64,26 +59,3 @@ load_agent_env() {
set +a set +a
fi fi
} }
set_debug_mode() {
local filtered_args=()
local debug_mode=false
for arg in "$@"; do
case "$arg" in
--debug)
debug_mode=true
;;
*)
filtered_args+=("$arg")
;;
esac
done
if [ "$debug_mode" = true ]; then
export KUGETSU_VERBOSITY="debug"
echo "[DEBUG] Debug mode enabled" >&2
fi
echo "${filtered_args[@]}"
}

View File

@@ -15,13 +15,6 @@ write_index() {
local issues_json="$3" local issues_json="$3"
local temp_file="$INDEX_FILE.tmp.$$" local temp_file="$INDEX_FILE.tmp.$$"
printf '{"base": %s, "pm_agent": %s, "issues": %s}\n' "$base" "$pm_agent" "$issues_json" > "$temp_file" printf '{"base": %s, "pm_agent": %s, "issues": %s}\n' "$base" "$pm_agent" "$issues_json" > "$temp_file"
if ! python3 -c "import json; json.load(open('$temp_file'))" 2>/dev/null; then
echo "Error: write_index would create malformed JSON, aborting. base=$base, pm_agent=$pm_agent, issues_json=$issues_json" >&2
rm -f "$temp_file"
return 1
fi
mv "$temp_file" "$INDEX_FILE" mv "$temp_file" "$INDEX_FILE"
} }
@@ -50,11 +43,7 @@ set_base_in_index() {
if [ "$session_id" = "null" ]; then if [ "$session_id" = "null" ]; then
write_index "null" "$pm_agent" "$issues" write_index "null" "$pm_agent" "$issues"
else else
if [ "$pm_agent" = "null" ]; then write_index "\"$session_id\"" "$pm_agent" "$issues"
write_index "\"$session_id\"" "null" "$issues"
else
write_index "\"$session_id\"" "\"$pm_agent\"" "$issues"
fi
fi fi
} }
@@ -67,11 +56,7 @@ set_pm_agent_in_index() {
if [ "$session_id" = "null" ]; then if [ "$session_id" = "null" ]; then
write_index "$base" "null" "$issues" write_index "$base" "null" "$issues"
else else
if [ "$base" = "null" ]; then write_index "$base" "\"$session_id\"" "$issues"
write_index "null" "\"$session_id\"" "$issues"
else
write_index "\"$base\"" "\"$session_id\"" "$issues"
fi
fi fi
} }
@@ -87,19 +72,7 @@ add_issue_to_index() {
issues=$(python3 -c "import sys, json; d=json.load(sys.stdin); d['$issue_ref']='$session_file'; print(json.dumps(d))" <<< "$issues") issues=$(python3 -c "import sys, json; d=json.load(sys.stdin); d['$issue_ref']='$session_file'; print(json.dumps(d))" <<< "$issues")
if [ "$base" = "null" ]; then write_index "$base" "$pm_agent" "$issues"
if [ "$pm_agent" = "null" ]; then
write_index "null" "null" "$issues"
else
write_index "null" "\"$pm_agent\"" "$issues"
fi
else
if [ "$pm_agent" = "null" ]; then
write_index "\"$base\"" "null" "$issues"
else
write_index "\"$base\"" "\"$pm_agent\"" "$issues"
fi
fi
} }
remove_issue_from_index() { remove_issue_from_index() {
@@ -113,19 +86,7 @@ remove_issue_from_index() {
issues=$(python3 -c "import sys, json; d=json.load(sys.stdin); d.pop('$issue_ref', None); print(json.dumps(d))" <<< "$issues") issues=$(python3 -c "import sys, json; d=json.load(sys.stdin); d.pop('$issue_ref', None); print(json.dumps(d))" <<< "$issues")
if [ "$base" = "null" ]; then write_index "$base" "$pm_agent" "$issues"
if [ "$pm_agent" = "null" ]; then
write_index "null" "null" "$issues"
else
write_index "null" "\"$pm_agent\"" "$issues"
fi
else
if [ "$pm_agent" = "null" ]; then
write_index "\"$base\"" "null" "$issues"
else
write_index "\"$base\"" "\"$pm_agent\"" "$issues"
fi
fi
} }
validate_issue_ref() { validate_issue_ref() {

View File

@@ -1,40 +1,6 @@
#!/bin/bash #!/bin/bash
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/kugetsu-config.sh"
log() {
local level="${1:-}"
local component="${2:-}"
local message="${3:-}"
local timestamp
timestamp=$(date -Iseconds)
case "$level" in
info|warn|error|debug) ;;
*)
echo "Error: log level must be info|warn|error|debug" >&2
return 1
;;
esac
if [ -z "$message" ]; then
message="$component"
component="${level}"
level="info"
fi
local masked
masked=$(mask_sensitive_vars "$message")
echo "[$timestamp] $level $component $masked"
}
log_debug() { log "debug" "$1" "${2:-}"; }
log_info() { log "info" "$1" "${2:-}"; }
log_warn() { log "warn" "$1" "${2:-}"; }
log_error() { log "error" "$1" "${2:-}"; }
cmd_logs() { cmd_logs() {
local count="${1:-10}" local count="${1:-10}"
@@ -58,9 +24,7 @@ cmd_logs() {
echo "" echo ""
echo "--- $log ---" echo "--- $log ---"
tail -20 "$LOGS_DIR/$log" | while read line; do tail -20 "$LOGS_DIR/$log" | while read line; do
line=$(strip_ansi_codes "$line") echo " $(mask_sensitive_vars "$line")"
line=$(mask_sensitive_vars "$line")
echo " $line"
done done
fi fi
done done

View File

@@ -44,7 +44,7 @@ check_task_completion() {
if [ -n "$pid" ] && [ "$pid" != "None" ]; then if [ -n "$pid" ] && [ "$pid" != "None" ]; then
if ! kill -0 "$pid" 2>/dev/null; then if ! kill -0 "$pid" 2>/dev/null; then
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$WORKTREES_DIR") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$HOME/.kugetsu-worktrees")
local has_commits=false local has_commits=false
if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then
@@ -64,7 +64,7 @@ check_task_completion() {
fi fi
else else
if [ -n "$session_id" ] && ! opencode session list 2>/dev/null | grep -q "$session_id"; then if [ -n "$session_id" ] && ! opencode session list 2>/dev/null | grep -q "$session_id"; then
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$WORKTREES_DIR") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref" "$HOME/.kugetsu-worktrees")
local has_commits=false local has_commits=false
if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then if [ -d "$worktree_path" ] && [ -d "$worktree_path/.git" ]; then
@@ -109,6 +109,7 @@ process_task() {
source "$SCRIPT_DIR/kugetsu-session.sh" source "$SCRIPT_DIR/kugetsu-session.sh"
if worktree_exists "$issue_ref" "$WORKTREES_DIR" || [ -f "$SESSIONS_DIR/$(issue_ref_to_filename "$issue_ref").json" ]; then
log_file="$LOGS_DIR/delegate-$(date +%s).log" log_file="$LOGS_DIR/delegate-$(date +%s).log"
if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then if cmd_continue "$issue_ref" "$message" >> "$log_file" 2>&1; then
sleep 1 sleep 1
@@ -119,6 +120,18 @@ process_task() {
update_queue_item_state "$queue_id" "error" update_queue_item_state "$queue_id" "error"
echo "Task $queue_id ($issue_ref) failed to continue" echo "Task $queue_id ($issue_ref) failed to continue"
fi fi
else
log_file="$LOGS_DIR/delegate-$(date +%s).log"
if cmd_start "$issue_ref" "$message" >> "$log_file" 2>&1; then
sleep 1
local session_id=$(get_session_id_for_issue "$issue_ref")
update_queue_item_state "$queue_id" "notified" "$session_id" ""
echo "Task $queue_id started for $issue_ref"
else
update_queue_item_state "$queue_id" "error"
echo "Task $queue_id ($issue_ref) failed to start"
fi
fi
release_lock "$issue_ref" release_lock "$issue_ref"
} }

View File

@@ -58,35 +58,12 @@ EOF
echo "Created config file: $KUGETSU_DIR/config" echo "Created config file: $KUGETSU_DIR/config"
fi fi
mkdir -p "$ENV_DIR"
if [ ! -f "$ENV_DIR/default.env" ]; then
cat > "$ENV_DIR/default.env" << 'EOF'
# Environment variables for agents
# Copy this file to <agent-type>.env (e.g., pm-agent.env, dev.env)
# and set your tokens and configuration
# Required: Gitea token for API access
# GITEA_TOKEN=your_gitea_token_here
# Optional: GitHub token (if using GitHub)
# GITHUB_TOKEN=your_github_token_here
# Optional: GitLab token (if using GitLab)
# GITLAB_TOKEN=your_gitlab_token_here
EOF
echo "Created env template: $ENV_DIR/default.env"
fi
local existing_base=$(get_base_session_id) local existing_base=$(get_base_session_id)
local existing_pm=$(get_pm_agent_session_id) local existing_pm=$(get_pm_agent_session_id)
if [ -n "$existing_base" ] && [ "$existing_base" != "null" ]; then if [ -n "$existing_base" ] && [ "$existing_base" != "null" ]; then
if [ "$force" = true ]; then if [ "$force" = true ]; then
echo "Warning: Reinitializing sessions (force mode)" >&2 echo "Warning: Reinitializing sessions (force mode)" >&2
echo "Destroying all sessions, worktrees, and logs..." >&2
cmd_destroy --base -y 2>/dev/null || true
cmd_destroy --pm-agent -y 2>/dev/null || true
rm -f "$LOGS_DIR"/*.log 2>/dev/null || true
else else
echo "Error: Base session already exists: $existing_base" >&2 echo "Error: Base session already exists: $existing_base" >&2
echo "Use --force to reinitialize" >&2 echo "Use --force to reinitialize" >&2
@@ -208,56 +185,18 @@ cmd_delegate() {
return return
fi fi
# No issue ref detected — fork a new session from base session # No issue ref detected — delegate directly to PM agent (legacy path)
local base_session=$(get_base_session_id) local pm_session=$(get_pm_agent_session_id)
if [ -z "$base_session" ] || [ "$base_session" = "null" ]; then if [ -z "$pm_session" ] || [ "$pm_session" = "null" ] || [ "$pm_session" = "None" ]; then
echo "Error: Base session not found. Run 'kugetsu init' first." >&2 echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2
exit 1 exit 1
fi fi
mkdir -p "$LOGS_DIR" mkdir -p "$LOGS_DIR"
local log_file="$LOGS_DIR/delegate-$(date +%s).log" local log_file="$LOGS_DIR/delegate-$(date +%s).log"
load_agent_env "pm-agent" load_agent_env "pm-agent"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$pm_session'" >> "$log_file" 2>&1 &
local new_session=$(create_session "$base_session") echo "Delegated to PM agent (logged to $(basename "$log_file"))"
if [ -z "$new_session" ]; then
echo "Error: Failed to create session" >&2
exit 1
fi
local msg_file="$LOGS_DIR/msg-$new_session.txt"
printf '%s' "$message" > "$msg_file"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$new_session'" >> "$log_file" 2>&1 &
echo "Delegated to new session (logged to $(basename "$log_file"))"
}
create_session() {
local base_session="${1:-$base_session_id}"
if [ -z "$base_session" ] || [ "$base_session" = "null" ]; then
echo "Error: base session not found. Run 'kugetsu init' first." >&2
return 1
fi
local before_json=$(opencode session list --format=json 2>/dev/null)
local before_set=$(echo "$before_json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); print('|'.join(s['id'] for s in sessions))" 2>/dev/null || echo "|")
opencode run --fork --session "$base_session" "new session" >/dev/null 2>&1
sleep 1
local after_json=$(opencode session list --format=json 2>/dev/null)
local after_sessions=$(echo "$after_json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); [print(s['id']) for s in sessions]" 2>/dev/null || true)
local new_session_id=""
while IFS= read -r sess; do
if [[ -n "$sess" ]] && [[ ! "$before_set" =~ \|${sess}\| ]]; then
new_session_id="$sess"
break
fi
done <<< "$after_sessions"
echo "$new_session_id"
} }
build_dev_agent_message() { build_dev_agent_message() {
@@ -270,35 +209,7 @@ build_dev_agent_message() {
local number=$(echo "$issue_ref" | grep -oE '#[0-9]+$' | tr -d '#') local number=$(echo "$issue_ref" | grep -oE '#[0-9]+$' | tr -d '#')
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
if [ -n "$user_message" ]; then local base_message="You are assigned to work on $issue_ref.
cat <<EOF
You are continuing work on $issue_ref. A PR likely already exists.
IMPORTANT - Review workflow:
1. First, check if PR exists: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls?state=open" -H "Authorization: Bearer \$GITEA_TOKEN" | grep -i "$number"
2. Get PR comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN"
3. Get PR reviews: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
You may need to:
- Make code changes and push to the same branch
- Reply to PR comments using: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"body":"Your reply here"}'
- Or do both
MERGING: If instructed to merge, you MUST confirm approval first before merging:
- Check for PR approval via: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
- Check for "lgtm" or "approved" in comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN"
- Only merge if you see approval OR the instruction explicitly says to merge (e.g., "merge the PR", "please merge", "go ahead and merge")
- To merge: tea pr merge --repo $owner/$repo $number --style merge
- If no approval yet, reply asking for review/approval first
Delegator's message:
$user_message
Work directory: $worktree_path (already on the fix branch)
EOF
else
cat <<EOF
You are assigned to work on $issue_ref.
Workflow: Workflow:
1. Read the issue at $instance/$owner/$repo/issues/$number AND all comments on that issue 1. Read the issue at $instance/$owner/$repo/issues/$number AND all comments on that issue
@@ -313,114 +224,82 @@ Workflow:
6. If anything is unclear, post a comment on the issue asking for clarification before implementing 6. If anything is unclear, post a comment on the issue asking for clarification before implementing
7. Implement the solution 7. Implement the solution
8. Create a branch named fix/issue-$number and implement the fix 8. Create a branch named fix/issue-$number and implement the fix
9. Create a PR when the implementation is complete using: tea pr create --repo $owner/$repo --title "Your PR title" --body "PR description" 9. Create a PR when the implementation is complete
- Make sure you are logged in with: tea login add --name gitea --token \$GITEA_TOKEN --url https://$instance
- If tea is not available, use: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/pulls" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"title":"PR Title","head":"branch-name","base":"main","body":"PR description"}'
Tools for PR interaction: Work directory: $worktree_path"
- Post issue/PR comment: curl -X POST "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN" -H "Content-Type: application/json" -d '{"body":"Your comment"}'
- List PR comments: curl -s "https://$instance/api/v1/repos/$owner/$repo/issues/$number/comments" -H "Authorization: Bearer \$GITEA_TOKEN"
- List PR reviews: curl -s "https://$instance/api/v1/repos/$owner/$repo/pulls/$number/reviews" -H "Authorization: Bearer \$GITEA_TOKEN"
- Merge PR (only with approval): tea pr merge --repo $owner/$repo $number --style merge
- MERGING requires approval first! Check for: approval in reviews, OR "lgtm"/"approved" in comments
- If no approval, ask reviewer to approve first before merging
Work directory: $worktree_path if [ -n "$user_message" ]; then
EOF echo "$base_message
Additional instructions from delegator:
$user_message"
else
echo "$base_message"
fi fi
} }
ensure_worktree() { cmd_start() {
local issue_ref="$1" local issue_ref="${1:-}"
local message="${2:-}"
if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then if [ -z "$issue_ref" ]; then
log "info" "ensure_worktree" "Worktree already exists for $issue_ref" echo "Error: issue ref is required" >&2
echo "existed" echo "Usage: kugetsu start <issue-ref> [message]" >&2
return 0 exit 1
fi fi
validate_issue_ref "$issue_ref"
local base_session_id=$(get_base_session_id) local base_session_id=$(get_base_session_id)
if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then
log "error" "ensure_worktree" "Base session not found for $issue_ref" echo "Error: Base session not found. Run 'kugetsu init' first." >&2
echo "error" exit 1
return 1 fi
local pm_agent_session_id=$(get_pm_agent_session_id)
if [ -z "$pm_agent_session_id" ] || [ "$pm_agent_session_id" = "null" ]; then
echo "Error: PM agent session not found. Run 'kugetsu init' first." >&2
exit 1
fi
if worktree_exists "$issue_ref"; then
echo "Issue '$issue_ref' already has a worktree. Use 'kugetsu continue' instead."
exit 1
fi fi
local active_count=$(count_active_dev_sessions) local active_count=$(count_active_dev_sessions)
if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then if [ "$active_count" -ge "${MAX_CONCURRENT_AGENTS:-3}" ]; then
log "error" "ensure_worktree" "Max concurrent agents reached for $issue_ref" echo "Error: Max concurrent agents (${MAX_CONCURRENT_AGENTS:-3}) reached. Use 'kugetsu continue' or wait for an agent to finish." >&2
echo "error" exit 1
return 1
fi fi
if create_worktree "$issue_ref" "$WORKTREES_DIR" 2>&1 | tee >(cat >&2); then
log "info" "ensure_worktree" "Created worktree for $issue_ref"
echo "created"
return 0
else
log "error" "ensure_worktree" "Failed to create worktree for $issue_ref"
echo "error"
return 1
fi
}
ensure_session() {
local issue_ref="$1"
local session_file=$(issue_ref_to_filename "$issue_ref") local session_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file" local session_path="$SESSIONS_DIR/$session_file"
local worktree_exists=false
if worktree_exists "$issue_ref" "$WORKTREES_DIR"; then
worktree_exists=true
fi
local session_exists=false
if [ -f "$session_path" ]; then if [ -f "$session_path" ]; then
session_exists=true echo "Session file already exists: $session_file"
echo "Use 'kugetsu continue $issue_ref' to continue work."
exit 1
fi fi
if $worktree_exists && $session_exists; then local before_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort)
log "info" "ensure_session" "Session already exists for $issue_ref" local before_set="|$before_sessions|"
echo "continued"
return 0
fi
if $worktree_exists && ! $session_exists; then create_worktree "$issue_ref" "$WORKTREES_DIR"
log "warn" "ensure_session" "Worktree exists but session is missing. Removing worktree to recreate both..."
remove_worktree_for_issue "$issue_ref"
worktree_exists=false
fi
if ! $worktree_exists && $session_exists; then local after_sessions=$(opencode session list 2>/dev/null | grep -oP '^ses_\w+' | sort)
log "warn" "ensure_session" "Session exists but worktree is missing. Removing stale session..." local new_session_id=""
rm -f "$session_path" while IFS= read -r sess; do
remove_issue_from_index "$issue_ref" if [[ ! "$before_set" =~ \|${sess}\| ]] && [[ "$sess" != "$base_session_id" ]] && [[ "$sess" != "$pm_agent_session_id" ]]; then
session_exists=false new_session_id="$sess"
break
fi fi
done <<< "$after_sessions"
if ! $worktree_exists; then
local wt_status=$(ensure_worktree "$issue_ref")
if [ "$wt_status" != "created" ] && [ "$wt_status" != "existed" ]; then
log "error" "ensure_session" "Failed to ensure worktree for $issue_ref"
echo "error"
return 1
fi
fi
local base_session_id=$(get_base_session_id)
if [ -z "$base_session_id" ] || [ "$base_session_id" = "null" ]; then
log "error" "ensure_session" "Base session not found for $issue_ref"
echo "error"
return 1
fi
local new_session_id=$(create_session "$base_session_id")
if [ -z "$new_session_id" ]; then if [ -z "$new_session_id" ]; then
log "error" "ensure_session" "Could not create session for $issue_ref" echo "Error: Could not find newly created session" >&2
echo "error" remove_worktree_for_issue "$issue_ref"
return 1 exit 1
fi fi
local worktree_path=$(issue_ref_to_worktree_path "$issue_ref") local worktree_path=$(issue_ref_to_worktree_path "$issue_ref")
@@ -430,87 +309,70 @@ ensure_session() {
add_issue_to_index "$issue_ref" "$session_file" add_issue_to_index "$issue_ref" "$session_file"
log "info" "ensure_session" "Created session for $issue_ref: $new_session_id" local dev_message=$(build_dev_agent_message "$issue_ref" "$message")
echo "created"
return 0
}
fork_agent() {
local session_id="$1"
local worktree_path="$2"
local message="$3"
if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
log "error" "fork_agent" "Invalid worktree path: $worktree_path"
echo "error"
return 1
fi
load_agent_env "dev" load_agent_env "dev"
cd "$worktree_path" cd "$worktree_path"
local sanitized_id=$(echo "$session_id" | sed 's/[^a-zA-Z0-9_-]/_/g') nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$dev_message' --continue --session '$new_session_id'" >> "$LOGS_DIR/dev-$new_session_id.log" 2>&1 &
mkdir -p "$worktree_path/.kugetsu"
if [ ! -f "$worktree_path/.gitignore" ] || ! grep -q "^.kugetsu/" "$worktree_path/.gitignore"; then
echo ".kugetsu/" >> "$worktree_path/.gitignore" 2>/dev/null || true
fi
local msg_file="$worktree_path/.kugetsu/msg.txt"
printf '%s' "$message" > "$msg_file"
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '@$msg_file' --session '$session_id'" >> "$LOGS_DIR/dev-$sanitized_id.log" 2>&1 &
log "info" "fork_agent" "Forked agent for session $session_id in $worktree_path" echo "Session started for '$issue_ref': $new_session_id"
echo "forked" echo "Worktree: $worktree_path"
return 0
}
cmd_start() {
cmd_continue "$@"
} }
cmd_continue() { cmd_continue() {
local issue_ref="${1:-}" local session_name=""
local message="${2:-}" local message=""
local args=("$@")
if [ -z "$issue_ref" ]; then args=$(set_debug_mode "${args[@]}")
for arg in $args; do
if [ -z "$session_name" ]; then
session_name="$arg"
else
message="$arg"
fi
done
if [ -z "$session_name" ]; then
echo "Error: issue ref is required" >&2 echo "Error: issue ref is required" >&2
echo "Usage: kugetsu continue <issue-ref> [message]" >&2 echo "Usage: kugetsu continue <issue-ref> [message]" >&2
exit 1 exit 1
fi fi
validate_issue_ref "$issue_ref" validate_issue_ref "$session_name"
local session_file=$(get_session_for_issue "$session_name")
if [ -z "$session_file" ] || [ "$session_file" = "null" ]; then
echo "Error: No session found for '$session_name'" >&2
echo "Use 'kugetsu start $session_name' to create a new session." >&2
exit 1
fi
local session_path="$SESSIONS_DIR/$session_file"
if [ ! -f "$session_path" ]; then
echo "Error: Session file not found: $session_path" >&2
exit 1
fi
load_agent_env "dev"
local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo "")
local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "")
local issue_ref=$(python3 -c "import json; print(json.load(open('$session_path')).get('issue_ref', ''))" 2>/dev/null || echo "")
if [ -z "$message" ]; then if [ -z "$message" ]; then
message=$(build_dev_agent_message "$issue_ref" "") message=$(build_dev_agent_message "$issue_ref" "")
fi fi
local worktree_status=$(ensure_worktree "$issue_ref") if [ -n "$worktree_path" ] && [ -d "$worktree_path" ]; then
if [ "$worktree_status" = "error" ]; then cd "$worktree_path"
echo "Error: Failed to ensure worktree for '$issue_ref'" >&2 nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 &
exit 1 else
nohup sh -c "GITEA_TOKEN='${GITEA_TOKEN:-}' opencode run '$message' --continue --session '$opencode_session_id'" >> "$LOGS_DIR/dev-$opencode_session_id.log" 2>&1 &
fi fi
local session_status=$(ensure_session "$issue_ref")
if [ "$session_status" = "error" ]; then
echo "Error: Failed to ensure session for '$issue_ref'" >&2
exit 1
fi
local session_file=$(issue_ref_to_filename "$issue_ref")
local session_path="$SESSIONS_DIR/$session_file"
local opencode_session_id=$(python3 -c "import json; print(json.load(open('$session_path')).get('opencode_session_id', ''))" 2>/dev/null || echo "")
local worktree_path=$(python3 -c "import json; print(json.load(open('$session_path')).get('worktree_path', ''))" 2>/dev/null || echo "")
local fork_status=$(fork_agent "$opencode_session_id" "$worktree_path" "$message")
if [ "$fork_status" = "error" ]; then
echo "Error: Failed to fork agent for '$issue_ref'" >&2
exit 1
fi
log "info" "cmd_continue" "Result for $issue_ref: worktree=$worktree_status session=$session_status fork=$fork_status"
echo "Session continued for '$issue_ref': $opencode_session_id"
echo "Worktree: $worktree_path"
echo "${worktree_status}-${session_status}-${fork_status}"
} }
cmd_list() { cmd_list() {

View File

@@ -1,181 +0,0 @@
#!/bin/bash
# Tests for create_session function
#
# Run with: bash skills/kugetsu/tests/test-create-session.sh
#
# NOTE: These tests MUST be run sequentially (not in parallel)
# to avoid exhausting memory with too many opencode sessions.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../scripts/kugetsu-config.sh"
source "$SCRIPT_DIR/../scripts/kugetsu-index.sh"
source "$SCRIPT_DIR/../scripts/kugetsu-session.sh"
PASS=0
FAIL=0
RUN=0
pass() {
echo "PASS: $1"
PASS=$((PASS + 1))
}
fail() {
echo "FAIL: $1"
echo " Expected: $2"
echo " Got: $3"
FAIL=$((FAIL + 1))
}
run_test() {
local name="$1"
local test_func="$2"
RUN=$((RUN + 1))
echo ""
echo "=== Test $RUN: $name ==="
echo "--- $name ---"
$test_func
}
echo "=== create_session Test Suite ==="
echo "NOTE: Running sequentially to avoid memory exhaustion"
echo ""
# Test 1: create_session requires base session
test_create_session_requires_base() {
local base_id=$(get_base_session_id)
if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then
skip "Base session not initialized - run 'kugetsu init' first"
return
fi
local result=$(create_session "$base_id")
if [ -n "$result" ] && [[ "$result" =~ ^ses_ ]]; then
pass "create_session returns valid session ID"
else
fail "create_session returns valid session ID" "ses_xxx" "$result"
fi
}
# Test 2: create_session creates a NEW session (different from base)
test_create_session_is_new() {
local base_id=$(get_base_session_id)
if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then
skip "Base session not initialized - run 'kugetsu init' first"
return
fi
local new_id=$(create_session "$base_id")
if [ "$new_id" != "$base_id" ]; then
pass "create_session returns NEW session (not same as base)"
else
fail "create_session returns NEW session" "different from base_id" "$new_id"
fi
}
# Test 3: create_session can be called multiple times (creates different sessions)
test_create_session_multiple_calls() {
local base_id=$(get_base_session_id)
if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then
skip "Base session not initialized - run 'kugetsu init' first"
return
fi
local id1=$(create_session "$base_id")
sleep 1
local id2=$(create_session "$base_id")
if [ "$id1" != "$id2" ]; then
pass "create_session creates different sessions on each call"
else
fail "create_session creates different sessions" "$id1 != $id2" "both equal: $id1"
fi
}
# Test 4: JSON session list parsing
test_session_json_parsing() {
local json='[{"id": "ses_abc123", "title": "test"}, {"id": "ses_def456", "title": "test2"}]'
local ids=$(echo "$json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); print(' '.join(s['id'] for s in sessions))" 2>/dev/null)
if [ "$ids" = "ses_abc123 ses_def456" ]; then
pass "JSON session list parsing extracts IDs correctly"
else
fail "JSON session list parsing" "ses_abc123 ses_def456" "$ids"
fi
}
# Test 5: Session ID format validation
test_session_id_format() {
local json='[{"id": "ses_2b4814406ffe3AxcpbrP7FknDr", "title": "test"}]'
local ids=$(echo "$json" | python3 -c "import sys,json; sessions=json.load(sys.stdin); print(' '.join(s['id'] for s in sessions))" 2>/dev/null)
if [[ "$ids" =~ ^ses_ ]]; then
pass "Session ID format is valid (starts with ses_)"
else
fail "Session ID format" "ses_xxx" "$ids"
fi
}
# Test 6: create_session accepts optional base session parameter
test_create_session_with_param() {
local base_id=$(get_base_session_id)
if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then
skip "Base session not initialized - run 'kugetsu init' first"
return
fi
local result=$(create_session "$base_id")
if [ -n "$result" ] && [[ "$result" =~ ^ses_ ]]; then
pass "create_session accepts base session parameter"
else
fail "create_session accepts base session parameter" "ses_xxx" "$result"
fi
}
# Test 7: Verify session appears in opencode session list after creation
test_session_visible_in_list() {
local base_id=$(get_base_session_id)
if [ -z "$base_id" ] || [ "$base_id" = "null" ]; then
skip "Base session not initialized - run 'kugetsu init' first"
return
fi
local new_id=$(create_session "$base_id")
sleep 1
local all_sessions=$(opencode session list --format=json 2>/dev/null)
if echo "$all_sessions" | grep -q "$new_id"; then
pass "Created session appears in opencode session list"
else
fail "Created session appears in session list" "should contain $new_id" "$all_sessions"
fi
}
skip() {
echo "SKIP: $1"
}
# Run tests sequentially
run_test "create_session requires base session" test_create_session_requires_base
run_test "create_session creates NEW session" test_create_session_is_new
run_test "create_session creates different sessions on multiple calls" test_create_session_multiple_calls
run_test "JSON session list parsing" test_session_json_parsing
run_test "Session ID format validation" test_session_id_format
run_test "create_session accepts optional base session parameter" test_create_session_with_param
run_test "Created session visible in opencode session list" test_session_visible_in_list
echo ""
echo "=== Test Results ==="
echo "Passed: $PASS"
echo "Failed: $FAIL"
echo "Total: $RUN"
if [ $FAIL -eq 0 ]; then
echo "All tests passed!"
exit 0
else
echo "Some tests failed!"
exit 1
fi