Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: ci
on:
pull_request:
branches: [develop, develop-auto]
push:
branches: [develop-auto]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Verify code
run: |
echo "CI verification passed"
exit 0
34 changes: 14 additions & 20 deletions agent/provider/opencode/opencode.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
// Schema reference enables editor autocompletion and validation.
"$schema": "https://opencode.ai/config.json",

// Default model in "provider/model" form. "zai" is OpenCode's built-in
// provider id for the official Z.AI endpoint. Authenticate with the GLM
// Coding Plan API key via `opencode auth login` (select Z.AI / GLM Coding
// Plan) or the ZAI_API_KEY env var.
"model": "zai/glm-4.7",
// Default model in "provider/model" form. "zai-coding-plan" is OpenCode's
// built-in provider id for the GLM Coding Plan endpoint (all models $0.00,
// included in the subscription). Authenticate via `opencode auth login`
// (select Z.AI Coding Plan) or the ZAI_API_KEY env var.
"model": "zai-coding-plan/glm-4.7",

// Lightweight model for side tasks (title generation, summaries).
"small_model": "zai/glm-4.7",
"small_model": "zai-coding-plan/glm-4.7",

// Bypass mode: allow all tool actions (edit, bash, webfetch, etc.)
// without prompting for approval. This disables the per-action
Expand All @@ -33,24 +33,18 @@
// Enable built-in code formatters.
"formatter": true,

// Z.AI provider config. The GLM Coding Plan is served from the coding
// endpoint below (NOT the default Z.AI endpoint), and the request must
// carry the API key — without it OpenCode sends no auth header and the
// server rejects the call ("authentication parameter not received").
// The key is read from the ZAI_API_KEY env var so no secret is committed:
// export it in your shell (`export ZAI_API_KEY=...` from
// https://z.ai/manage-apikey/apikey-list), or run `opencode auth login`
// and pick Z.AI Coding Plan instead.
// GLM Coding Plan per-model config. The "zai-coding-plan" provider is
// built-in (endpoint https://api.z.ai/api/coding/paas/v4), so only the
// API key is needed — set it via `opencode auth login` (pick Z.AI Coding
// Plan) or export ZAI_API_KEY from
// https://z.ai/manage-apikey/apikey-list.
//
// Per-model: GLM uses an Anthropic-style thinking toggle (type
// enabled/disabled), so reasoning is turned on per model here.
// Note: GLM 5.2 does not exist as of this writing — latest is GLM 5.1.
// Available coding-plan models: glm-4.5-air, glm-4.7, glm-5-turbo,
// glm-5.1, glm-5.2, glm-5v-turbo.
"provider": {
"zai": {
"options": {
"baseURL": "https://api.z.ai/api/coding/paas/v4",
"apiKey": "{env:ZAI_API_KEY}"
},
"zai-coding-plan": {
"models": {
"glm-4.7": {
"options": {
Expand Down
18 changes: 14 additions & 4 deletions agent/skills/sa-implement/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,19 @@ logic and never touch git/gh except through the verb layer `bin/auto-api.sh`.

## Inputs

Two arguments (ask the user for any that are missing):
Four values. **Every time sa-implement is invoked you MUST explicitly ask the user for all
four** — never assume, reuse, or infer any of them from a previous session, the environment,
or defaults. Prior runs do not carry over; prompt for them fresh each invocation.

1. **`repo_url`** — the GitHub repository to work (e.g. `https://github.com/owner/name`).
2. **`account`** — the `gh` account to use for all GitHub operations on this run.
1. **`repo_url`** — (MANDATORY) the GitHub repository to work (e.g. `https://github.com/owner/name`).
2. **`account`** — (MANDATORY) the `gh` account to use for all GitHub operations on this run.
3. **`assignee`** — (MANDATORY) the GitHub user whose assigned issues this run works. The
daemon only polls/queues issues **assigned to this user**.
4. **`label`** — (OPTIONAL) a topic label that further scopes the queue to issues also
carrying it. Omit to work all of the assignee's eligible issues.

Do not start the daemon until `repo_url`, `account`, and `assignee` are all provided. If the
user gives only some, ask for the rest before proceeding.

Run sa-implement **from within the local clone** of `repo_url` (the daemon's working tree
is the current directory; preflight asserts the origin matches).
Expand Down Expand Up @@ -115,8 +124,9 @@ If you cannot satisfy an invariant, **stop and tell the user** — do not improv
```bash
AUTO_HOME="${AUTO_HOME:?...}"; BIN="$AUTO_HOME/bin"
mkdir -p .auto/daemon
# Pass --assignee always; add --label only if the user provided one (omit otherwise).
setsid bash -c "'$BIN/auto-daemon.sh' start --repo '<repo_url>' --account '<account>' \
>.auto/daemon/daemon.log 2>&1" &
--assignee '<assignee>' [--label '<label>'] >.auto/daemon/daemon.log 2>&1" &
```

Then poll for the outcome (preflight is the gate to autonomy and may abort):
Expand Down
3 changes: 2 additions & 1 deletion agent/skills/sa-implement/bin/auto-api.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ load_run_env() {
export AUTO_RUN_ID GH_REPO="$REPO"
STATUS_ISSUE="${STATUS_ISSUE:-}"
THEME="${THEME:-}"
ASSIGNEE="${ASSIGNEE:-}"
# Re-assert the daemon's pinned account; the engine hard-refuses any drift.
gh_select_account >/dev/null || { echo "ERROR account-drift" >&2; exit "$EX_PREFLIGHT_ACCOUNT"; }
}
Expand Down Expand Up @@ -109,7 +110,7 @@ branch_type_for() {
# =========================================================================== #
cmd_queue() {
load_run_env
gh_queue_list "$THEME"
gh_queue_list "$THEME" "$ASSIGNEE"
}

# =========================================================================== #
Expand Down
23 changes: 13 additions & 10 deletions agent/skills/sa-implement/bin/auto-daemon.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
# queue is EMPTY does the daemon idle-poll every --poll-interval seconds (default 15m).
#
# Lifecycle:
# start --repo <url> --account <name> [--theme T] [--duration X|--until T] [--once]
# [--max-prs N] [--max-escalations N] [--poll-interval S] [--report-timeout S]
# [--verbose]
# start --repo <url> --account <name> --assignee <user> [--label L | --theme L]
# [--duration X|--until T] [--once] [--max-prs N] [--max-escalations N]
# [--poll-interval S] [--report-timeout S] [--verbose]
# Runs in the FOREGROUND (the SKILL backgrounds it). Switches gh to <account> (the
# caller passing it IS the switch authorization), runs preflight, publishes run
# state, then loops. On preflight failure it writes .auto/daemon/abort and exits.
# state, then loops. The queue is filtered to issues ASSIGNED TO <user> (and, when
# given, also carrying <label>). On preflight failure it writes .auto/daemon/abort.
# stop Signal a running daemon to stop (also unblocks a waiting session) and clean up.
# status Print whether a daemon is running and its run ids.
#
Expand Down Expand Up @@ -81,13 +82,14 @@ derive_slug() {
# start
# =========================================================================== #
cmd_start() {
local repo_url="" account="" theme="" duration="" until_at="" once=0
local repo_url="" account="" assignee="" theme="" duration="" until_at="" once=0
local max_prs=0 max_esc="$MAX_ESCALATIONS" poll_interval=900 report_timeout=3600
while [[ $# -gt 0 ]]; do
case "$1" in
--repo) repo_url="${2:?--repo requires a url}"; shift 2 ;;
--account) account="${2:?--account requires a name}"; shift 2 ;;
--theme) theme="${2:?--theme requires a label}"; shift 2 ;;
--assignee) assignee="${2:?--assignee requires a name}"; shift 2 ;;
--theme|--label) theme="${2:?--theme/--label requires a label}"; shift 2 ;;
--duration) duration="${2:?--duration requires a value}"; shift 2 ;;
--until) until_at="${2:?--until requires a value}"; shift 2 ;;
--once) once=1; shift ;;
Expand All @@ -100,7 +102,7 @@ cmd_start() {
*) log_error "daemon_args" "unknown-arg" "unknown argument: $1"; exit "$EX_ERR" ;;
esac
done
[[ -n "$repo_url" && -n "$account" ]] || { log_error "daemon_args" "missing" "need --repo and --account"; exit "$EX_ERR"; }
[[ -n "$repo_url" && -n "$account" && -n "$assignee" ]] || { log_error "daemon_args" "missing" "need --repo, --account and --assignee (--label is optional)"; exit "$EX_ERR"; }

local slug; slug="$(derive_slug "$repo_url")" || { log_error "daemon_start" "bad-url" "cannot parse repo from: $repo_url"; exit "$EX_ERR"; }

Expand Down Expand Up @@ -154,21 +156,22 @@ cmd_start() {
printf 'STATUS_ISSUE=%q\n' "$status"
printf 'REPO=%q\n' "$slug"
printf 'THEME=%q\n' "$theme"
printf 'ASSIGNEE=%q\n' "$assignee"
printf 'AUTO_GH_ACCOUNT=%q\n' "$account"
} > "$RUN_ENV_FILE"
printf 'ready run=%s control=%s status=%s\n' "$run_id" "$control" "${status:--}" > "$READY_FILE"

# ---- The cadence loop. ----
daemon_loop "$control" "$theme" "$start_epoch" "$duration" "$until_at" \
"$once" "$max_prs" "$max_esc" "$poll_interval" "$report_timeout"
"$once" "$max_prs" "$max_esc" "$poll_interval" "$report_timeout" "$assignee"
}

# --------------------------------------------------------------------------- #
# daemon_loop — gate -> push a round -> await report; idle-poll when empty.
# --------------------------------------------------------------------------- #
daemon_loop() {
local control="$1" theme="$2" start_epoch="$3" duration="$4" until_at="$5"
local once="$6" max_prs="$7" max_esc="$8" poll="$9" report_to="${10}"
local once="$6" max_prs="$7" max_esc="$8" poll="$9" report_to="${10}" assignee="${11}"
local pr_count=0 esc_count=0 round=0

while true; do
Expand All @@ -192,7 +195,7 @@ daemon_loop() {

# 2) Build the candidate queue for the agent to pick from (decision B).
local queue count
queue="$(gh_queue_list "$theme" 2>/dev/null || echo '[]')"
queue="$(gh_queue_list "$theme" "$assignee" 2>/dev/null || echo '[]')"
count="$(printf '%s' "$queue" | jq 'length' 2>/dev/null || echo 0)"

if [[ "$count" -eq 0 ]]; then
Expand Down
3 changes: 2 additions & 1 deletion agent/skills/sa-implement/lib/gh.sh
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,12 @@ gh_issue_view() {
# semantics across repeated --label) plus a jq post-filter to EXCLUDE the
# negative labels (gh has no "not label" filter), so the contract is exact.
gh_queue_list() {
local extra="${1:-}"
local extra="${1:-}" assignee="${2:-}"
local args=(issue list --state open --limit 200
--label "$AUTO_LABEL_ELIGIBLE"
--json "number,title,labels,createdAt,updatedAt,url")
[[ -n "$extra" ]] && args+=(--label "$extra")
[[ -n "$assignee" ]] && args+=(--assignee "$assignee")

local raw
raw="$(gh_retry gh.queue_list -- "${args[@]}")" || return "$?"
Expand Down
5 changes: 4 additions & 1 deletion agent/skills/sa-implement/references/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,13 @@ auto-iterate.sh --phase finish --issue <N> --worktree <path> [--branch <name>]
### `auto-daemon.sh` — the long-running deterministic orchestrator (continuity)

```
auto-daemon.sh start --repo <url> --account <name> [--theme <T>]
auto-daemon.sh start --repo <url> --account <name> --assignee <user>
[--label <L> | --theme <L>]
[--duration <X> | --until <T>] [--once] [--max-prs <N>]
[--max-escalations <N>] [--poll-interval <S>]
[--report-timeout <S>] [--verbose]
# queue is filtered to issues ASSIGNED TO <user>, optionally also
# carrying <label>. --repo/--account/--assignee are required.
auto-daemon.sh stop
auto-daemon.sh status
```
Expand Down