Merge PR: Phase 1 Slice 2 — profiles (persona/caps/scope inheritance) + deps/DAG (blocked, cycle detection)
Reviewed against §5/§6 single-host scope; selftest 46/46 (34 regression + 12 new); profile resolution precedence, persona injection, warn-only scope, deps soft/cycle verified.
This commit is contained in:
commit
d43cab8afe
@ -109,10 +109,10 @@ are otherwise **no-ops until a later phase** (they do not yet affect execution).
|
|||||||
| `prefers-engine` | **active** | _(none)_ | optional order hint for `engine-class` resolution, e.g. `[claude, devin]` |
|
| `prefers-engine` | **active** | _(none)_ | optional order hint for `engine-class` resolution, e.g. `[claude, devin]` |
|
||||||
| `capabilities` | **active** | _(none)_ | hard host requirements, e.g. `[os:any, node>=20, has:git]`. If the host can't satisfy them the job is sent to `failed/` with `result=capability_mismatch` **and the agent is never launched** (grammar below) |
|
| `capabilities` | **active** | _(none)_ | hard host requirements, e.g. `[os:any, node>=20, has:git]`. If the host can't satisfy them the job is sent to `failed/` with `result=capability_mismatch` **and the agent is never launched** (grammar below) |
|
||||||
| `idempotency-key` | **active** | _(none)_ | dedupe on `add` (semantics below) |
|
| `idempotency-key` | **active** | _(none)_ | dedupe on `add` (semantics below) |
|
||||||
| `profile` | RESERVED | _(none)_ | role/persona + caps (profiles land in a later slice) |
|
| `profile` | **active** | _(none)_ | inherit persona + verify/caps/engine-class/prefers-engine/allowed-scope/review-policy from `profiles/<name>.md` (job fields override — see **Profiles**) |
|
||||||
| `prefers` | RESERVED | _(none)_ | soft routing/affinity hints (e.g. `[factory:mac-2]`) |
|
| `prefers` | RESERVED | _(none)_ | soft routing/affinity hints (e.g. `[factory:mac-2]`) |
|
||||||
| `budget` | RESERVED | _(none)_ | `{ usd, tokens, wall }` ceilings (`wall` enforcement is a later slice) |
|
| `budget` | RESERVED | _(none)_ | `{ usd, tokens, wall }` ceilings (`wall` enforcement is a later slice) |
|
||||||
| `deps` / `deps-mode` | RESERVED | _(none)_ | DAG dependencies (single-host blocking is a later slice) |
|
| `deps` / `deps-mode` | **active** | _(none)_ | block until each referenced `idempotency-key` is in `shipped/` (or `testing/` when `deps-mode: soft`). Submit-time cycle detection (see **Profiles & deps**) |
|
||||||
| `retry` | **active** | _(none)_ | `{ max: N, backoff: 5m, on: [timeout, verify_failed, crash] }` — requeue failures with backoff up to `max`, then `retries_exhausted` (see **Resilience**) |
|
| `retry` | **active** | _(none)_ | `{ max: N, backoff: 5m, on: [timeout, verify_failed, crash] }` — requeue failures with backoff up to `max`, then `retries_exhausted` (see **Resilience**) |
|
||||||
| `review-policy` | RESERVED | _(none)_ | `auto\|manual\|reviewers:[…]` |
|
| `review-policy` | RESERVED | _(none)_ | `auto\|manual\|reviewers:[…]` |
|
||||||
| `artifacts` | RESERVED | _(none)_ | extra outputs to capture (coverage, screenshots) |
|
| `artifacts` | RESERVED | _(none)_ | extra outputs to capture (coverage, screenshots) |
|
||||||
@ -232,6 +232,42 @@ queue/
|
|||||||
(transient: requeued for another attempt), `recovered` (transient: an orphan was
|
(transient: requeued for another attempt), `recovered` (transient: an orphan was
|
||||||
reclaimed to `inbox/`).
|
reclaimed to `inbox/`).
|
||||||
|
|
||||||
|
## Profiles & deps
|
||||||
|
|
||||||
|
### Profiles (roadmap §6)
|
||||||
|
|
||||||
|
A **profile** is a reusable role preset in `profiles/<name>.md`. A job opts in with
|
||||||
|
`profile: <name>` and inherits any of these fields it does **not** set itself:
|
||||||
|
`verify` (from the profile's `default-verify`), `capabilities`, `engine-class`,
|
||||||
|
`prefers-engine`, `allowed-scope`, `review-policy`. The profile's `persona` block is
|
||||||
|
**prepended** to the body sent to the engine (the job `.md` on disk is unchanged;
|
||||||
|
secrets are never logged). Resolution runs **before** the capability gate and engine
|
||||||
|
resolution, so inherited caps / engine-class take effect.
|
||||||
|
|
||||||
|
**Precedence:** `job field > profile field > built-in default`. Set `AGENT_QUEUE_PROFILES`
|
||||||
|
to point at a different catalog directory (defaults to `./profiles`).
|
||||||
|
|
||||||
|
Starter catalog: `developer`, `backend-engineer`, `frontend-engineer`, `ux-designer`,
|
||||||
|
`ui-designer`, `qa`, `reviewer`, `docs-writer`, and a reserved `planner`. Each presets
|
||||||
|
`name`, `persona`, `capabilities`, `default-verify`, `engine-class`, `prefers-engine`,
|
||||||
|
`allowed-scope`, and `review-policy`.
|
||||||
|
|
||||||
|
**allowed-scope (warn-only this phase).** After a run on a git `cwd`, changed paths
|
||||||
|
outside the profile/job `allowed-scope` globs (`dir/**` matches the whole subtree) are
|
||||||
|
logged as a `WARNING` and recorded as `scope_warning=` in the meta — **non-blocking**
|
||||||
|
(the job is not failed). `path_in_scope` is exposed as a unit-testable function.
|
||||||
|
|
||||||
|
### deps / DAG, single host (roadmap §5)
|
||||||
|
|
||||||
|
`deps: [keyA, keyB]` references other jobs by their author-controlled
|
||||||
|
`idempotency-key`. A dep is **satisfied** when a job with that key is in `shipped/`
|
||||||
|
(default), or in `shipped/` **or** `testing/` when the dependent job sets
|
||||||
|
`deps-mode: soft`. A job with unmet deps is **blocked**: it is skipped in inbox
|
||||||
|
selection (never launched, never failed) and surfaced in `status` as
|
||||||
|
`blocked (waiting on: <keys>)`, then re-evaluated every loop until its deps are met.
|
||||||
|
`add` performs **submit-time cycle detection** over the inbox + active-stage dep graph
|
||||||
|
and rejects (nonzero exit) a job that would create a cycle. Cross-machine deps are P2.
|
||||||
|
|
||||||
## Resilience (crash recovery & work preservation)
|
## Resilience (crash recovery & work preservation)
|
||||||
|
|
||||||
Single-host implementations of the durability model (roadmap §25):
|
Single-host implementations of the durability model (roadmap §25):
|
||||||
|
|||||||
@ -29,6 +29,8 @@ set -uo pipefail
|
|||||||
# ── Resolve paths ───────────────────────────────────────────────────
|
# ── Resolve paths ───────────────────────────────────────────────────
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
QUEUE_ROOT="${AGENT_QUEUE_ROOT:-$SCRIPT_DIR/queue}"
|
QUEUE_ROOT="${AGENT_QUEUE_ROOT:-$SCRIPT_DIR/queue}"
|
||||||
|
# Profile catalog dir (persona + capability presets). Override for tests.
|
||||||
|
PROFILES_DIR="${AGENT_QUEUE_PROFILES:-$SCRIPT_DIR/profiles}"
|
||||||
INBOX="$QUEUE_ROOT/inbox"
|
INBOX="$QUEUE_ROOT/inbox"
|
||||||
BUILDING="$QUEUE_ROOT/building"
|
BUILDING="$QUEUE_ROOT/building"
|
||||||
REVIEW="$QUEUE_ROOT/review"
|
REVIEW="$QUEUE_ROOT/review"
|
||||||
@ -120,6 +122,51 @@ lock_key_for() {
|
|||||||
# _keyhash <key> -> stable filename-safe token for a lock key
|
# _keyhash <key> -> stable filename-safe token for a lock key
|
||||||
_keyhash() { printf '%s' "$1" | cksum | awk '{print $1}'; }
|
_keyhash() { printf '%s' "$1" | cksum | awk '{print $1}'; }
|
||||||
|
|
||||||
|
# ── Profiles (§6): persona + capability/engine/scope presets ─────────
|
||||||
|
#
|
||||||
|
# profile_get <profile-name> <profile-key> [default] -> a single-line value from
|
||||||
|
# profiles/<name>.md, else the default.
|
||||||
|
profile_get() {
|
||||||
|
local pf="$PROFILES_DIR/$1.md"
|
||||||
|
[[ -f "$pf" ]] || { printf '%s' "${3:-}"; return; }
|
||||||
|
fm_get "$pf" "$2" "${3:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# profile_persona <profile-name> -> the multi-line `persona: |` block (2-space
|
||||||
|
# indent stripped), or empty. Used to prepend a persona overlay to the job body.
|
||||||
|
profile_persona() {
|
||||||
|
local pf="$PROFILES_DIR/$1.md"
|
||||||
|
[[ -f "$pf" ]] || return 0
|
||||||
|
awk '
|
||||||
|
NR==1 && $0!="---" { exit }
|
||||||
|
NR==1 { infm=1; next }
|
||||||
|
infm && $0=="---" { exit }
|
||||||
|
!infm { next }
|
||||||
|
inpersona {
|
||||||
|
if ($0 ~ /^[A-Za-z0-9_-]+[ \t]*:/) { inpersona=0 }
|
||||||
|
else { line=$0; sub(/^ /,"",line); print line; next }
|
||||||
|
}
|
||||||
|
$0 ~ /^persona[ \t]*:[ \t]*\|[ \t]*$/ { inpersona=1; next }
|
||||||
|
' "$pf"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fm_eff <file> <job-key> [default] [profile-key] -> effective value with
|
||||||
|
# precedence job > profile > built-in default (§6 resolution). The profile is the
|
||||||
|
# job's `profile:` frontmatter; `profile-key` defaults to `job-key` (e.g. `verify`
|
||||||
|
# inherits the profile's `default-verify`). Inheritable: verify, capabilities,
|
||||||
|
# engine-class, prefers-engine, allowed-scope, review-policy.
|
||||||
|
fm_eff() {
|
||||||
|
local file=$1 jkey=$2 def=${3:-} pkey=${4:-$2} v prof pv
|
||||||
|
v=$(fm_get "$file" "$jkey" "")
|
||||||
|
if [[ -n "$v" ]]; then printf '%s' "$v"; return; fi
|
||||||
|
prof=$(fm_get "$file" profile "")
|
||||||
|
if [[ -n "$prof" ]]; then
|
||||||
|
pv=$(profile_get "$prof" "$pkey" "")
|
||||||
|
[[ -n "$pv" ]] && { printf '%s' "$pv"; return; }
|
||||||
|
fi
|
||||||
|
printf '%s' "$def"
|
||||||
|
}
|
||||||
|
|
||||||
# _mtime <file> -> file modification time in epoch seconds (BSD or GNU stat); empty if missing
|
# _mtime <file> -> file modification time in epoch seconds (BSD or GNU stat); empty if missing
|
||||||
_mtime() {
|
_mtime() {
|
||||||
[[ -e "$1" ]] || { echo ""; return; }
|
[[ -e "$1" ]] || { echo ""; return; }
|
||||||
@ -356,10 +403,10 @@ resolve_engine() {
|
|||||||
local f=$1 eng cls prefers
|
local f=$1 eng cls prefers
|
||||||
eng=$(fm_get "$f" engine "")
|
eng=$(fm_get "$f" engine "")
|
||||||
if [[ -n "$eng" ]]; then printf '%s' "$eng"; return 0; fi
|
if [[ -n "$eng" ]]; then printf '%s' "$eng"; return 0; fi
|
||||||
cls=$(fm_get "$f" engine-class "")
|
cls=$(fm_eff "$f" engine-class "") # inherit engine-class from the job's profile
|
||||||
if [[ -z "$cls" ]]; then printf '%s' "$DEFAULT_ENGINE"; return 0; fi
|
if [[ -z "$cls" ]]; then printf '%s' "$DEFAULT_ENGINE"; return 0; fi
|
||||||
local class_engines; class_engines=$(engine_class_engines "$cls")
|
local class_engines; class_engines=$(engine_class_engines "$cls")
|
||||||
prefers=$(fm_get "$f" prefers-engine "")
|
prefers=$(fm_eff "$f" prefers-engine "")
|
||||||
local ordered=() seen=" " p c
|
local ordered=() seen=" " p c
|
||||||
if [[ -n "$prefers" ]]; then
|
if [[ -n "$prefers" ]]; then
|
||||||
while IFS= read -r p; do
|
while IFS= read -r p; do
|
||||||
@ -377,6 +424,134 @@ resolve_engine() {
|
|||||||
printf '%s' ""
|
printf '%s' ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── deps / DAG, single host (§5) ─────────────────────────────────────
|
||||||
|
# deps reference other jobs by their (author-controlled) `idempotency-key`.
|
||||||
|
#
|
||||||
|
# _key_in_dir <key> <dir> -> 0 if some .md in <dir> has idempotency-key == key.
|
||||||
|
_key_in_dir() {
|
||||||
|
local key=$1 d=$2 ef
|
||||||
|
for ef in "$d"/*.md; do
|
||||||
|
[[ -e "$ef" ]] || continue
|
||||||
|
[[ "$(fm_get "$ef" idempotency-key "")" == "$key" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# dep_satisfied <key> <mode> -> 0 when the dep is met: a job with <key> is in
|
||||||
|
# shipped/ (default), or shipped/ OR testing/ when mode is `soft`.
|
||||||
|
dep_satisfied() {
|
||||||
|
local key=$1 mode=$2
|
||||||
|
_key_in_dir "$key" "$SHIPPED" && return 0
|
||||||
|
[[ "$mode" == soft ]] && _key_in_dir "$key" "$TESTING" && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# deps_unmet <file> -> space-separated list of this job's UNMET dep keys (empty if
|
||||||
|
# none / no deps). `deps-mode` (hard|soft) is job-level.
|
||||||
|
deps_unmet() {
|
||||||
|
local f=$1 keys mode k unmet=""
|
||||||
|
keys=$(parse_list "$(fm_get "$f" deps "")" | tr '\n' ' ')
|
||||||
|
[[ -n "${keys// /}" ]] || { printf ''; return 0; }
|
||||||
|
mode=$(fm_get "$f" deps-mode "hard")
|
||||||
|
for k in $keys; do
|
||||||
|
[[ -n "$k" ]] || continue
|
||||||
|
dep_satisfied "$k" "$mode" || unmet+="$k "
|
||||||
|
done
|
||||||
|
printf '%s' "${unmet% }"
|
||||||
|
}
|
||||||
|
|
||||||
|
# _deps_of_key <key> -> dep keys (space-separated) of the job carrying <key>,
|
||||||
|
# scanned across inbox + active stages.
|
||||||
|
_deps_of_key() {
|
||||||
|
local key=$1 d ef
|
||||||
|
for d in "$INBOX" "$BUILDING" "$REVIEW" "$TESTING" "$SHIPPED"; do
|
||||||
|
for ef in "$d"/*.md; do
|
||||||
|
[[ -e "$ef" ]] || continue
|
||||||
|
[[ "$(fm_get "$ef" idempotency-key "")" == "$key" ]] || continue
|
||||||
|
parse_list "$(fm_get "$ef" deps "")" | tr '\n' ' '
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# deps_would_cycle <new-key> <new-deps-space> -> 0 if adding a job with <new-key>
|
||||||
|
# depending on <new-deps> would create a cycle (BFS over existing key->deps edges
|
||||||
|
# back to new-key; also catches self-dependency).
|
||||||
|
deps_would_cycle() {
|
||||||
|
local newkey=$1 newdeps=$2 visited=" " frontier next k d kd
|
||||||
|
[[ -n "$newkey" ]] || return 1
|
||||||
|
_in_list "$newkey" "$newdeps" && return 0
|
||||||
|
frontier="$newdeps"
|
||||||
|
while [[ -n "${frontier// /}" ]]; do
|
||||||
|
next=""
|
||||||
|
for k in $frontier; do
|
||||||
|
[[ -n "$k" ]] || continue
|
||||||
|
[[ "$k" == "$newkey" ]] && return 0
|
||||||
|
case "$visited" in *" $k "*) continue;; esac
|
||||||
|
visited+="$k "
|
||||||
|
kd=$(_deps_of_key "$k")
|
||||||
|
for d in $kd; do next+="$d "; done
|
||||||
|
done
|
||||||
|
frontier="$next"
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# _drain_pending -> 0 if some inbox job can still make progress on its own: it is
|
||||||
|
# runnable now, or it is waiting on a retry/recovery backoff (which elapses with
|
||||||
|
# time). A job blocked ONLY by unmet deps is NOT pending while the loop is idle
|
||||||
|
# (no running job can satisfy its deps), so `--once` can drain past it.
|
||||||
|
_drain_pending() {
|
||||||
|
local cand cj ne now; now=$(date +%s)
|
||||||
|
for cand in "$INBOX"/*.md; do
|
||||||
|
[[ -e "$cand" ]] || continue
|
||||||
|
cj=$(basename "$cand"); cj=${cj%.md}
|
||||||
|
ne=$(grep '^next_eligible=' "$STATE/$cj.meta" 2>/dev/null | tail -1 | cut -d= -f2)
|
||||||
|
if [[ "$ne" =~ ^[0-9]+$ ]] && [[ "$ne" -gt "$now" ]]; then return 0; fi
|
||||||
|
[[ -n "$(deps_unmet "$cand")" ]] && continue
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── allowed-scope guardrail (§6/§12) — WARN-ONLY this phase ───────────
|
||||||
|
# path_in_scope <path> <globs-space> -> 0 if <path> matches any allowed-scope glob
|
||||||
|
# (`dir/**` matches the whole subtree; `*` matches across `/`). Pure + testable.
|
||||||
|
path_in_scope() {
|
||||||
|
local path=$1 globs=$2 g pat
|
||||||
|
for g in $globs; do
|
||||||
|
[[ -n "$g" ]] || continue
|
||||||
|
pat=${g//\*\*/\*}
|
||||||
|
# shellcheck disable=SC2053
|
||||||
|
[[ "$path" == $pat ]] && return 0
|
||||||
|
[[ "$path" == "$g"/* ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# scope_check <cwd> <base> <scope> <logf> <metaf> -> log a WARNING (non-blocking)
|
||||||
|
# for changed paths outside allowed-scope. Records scope_warning= in the meta.
|
||||||
|
scope_check() {
|
||||||
|
local cwd=$1 base=$2 scope=$3 logf=$4 metaf=$5 changed globs p out=""
|
||||||
|
_is_git_repo "$cwd" || return 0
|
||||||
|
if [[ -n "$base" ]]; then
|
||||||
|
changed=$(git -C "$cwd" diff --name-only "$base" HEAD 2>/dev/null)
|
||||||
|
else
|
||||||
|
changed=$(git -C "$cwd" diff --name-only HEAD 2>/dev/null)
|
||||||
|
fi
|
||||||
|
[[ -n "$changed" ]] || return 0
|
||||||
|
globs=$(parse_list "$scope" | tr '\n' ' ')
|
||||||
|
[[ -n "${globs// /}" ]] || return 0
|
||||||
|
while IFS= read -r p; do
|
||||||
|
[[ -n "$p" ]] || continue
|
||||||
|
path_in_scope "$p" "$globs" || out+="$p "
|
||||||
|
done <<< "$changed"
|
||||||
|
if [[ -n "$out" ]]; then
|
||||||
|
echo "WARNING: allowed-scope violation (warn-only) — changed outside [$globs]: ${out% }" >> "$logf"
|
||||||
|
echo "scope_warning=${out% }" >> "$metaf"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ── Engine driver: builds argv into AGENT_CMD[]; sets AGENT_STDIN if the ──
|
# ── Engine driver: builds argv into AGENT_CMD[]; sets AGENT_STDIN if the ──
|
||||||
# prompt should be fed on stdin (claude/codex) rather than a flag. $pf is the
|
# prompt should be fed on stdin (claude/codex) rather than a flag. $pf is the
|
||||||
# frontmatter-STRIPPED body file, so a body starting with '--' is never
|
# frontmatter-STRIPPED body file, so a body starting with '--' is never
|
||||||
@ -424,8 +599,9 @@ run_worker() {
|
|||||||
# The worker only ever APPENDS (ended/exit/result) to avoid a truncation race.
|
# The worker only ever APPENDS (ended/exit/result) to avoid a truncation race.
|
||||||
|
|
||||||
# ── Capability gate (§5/§8 single-host): if the job declares `capabilities`
|
# ── Capability gate (§5/§8 single-host): if the job declares `capabilities`
|
||||||
# this host does not satisfy, route to failed/ WITHOUT launching the agent. ──
|
# (own or inherited from its profile) this host does not satisfy, route to
|
||||||
local req_caps; req_caps=$(parse_list "$(fm_get "$doing_file" capabilities "")" | tr '\n' ' ')
|
# failed/ WITHOUT launching the agent. ──
|
||||||
|
local req_caps; req_caps=$(parse_list "$(fm_eff "$doing_file" capabilities "")" | tr '\n' ' ')
|
||||||
if [[ -n "${req_caps// /}" ]]; then
|
if [[ -n "${req_caps// /}" ]]; then
|
||||||
local avail; avail=$(detect_capabilities)
|
local avail; avail=$(detect_capabilities)
|
||||||
if ! caps_match "$req_caps" "$avail"; then
|
if ! caps_match "$req_caps" "$avail"; then
|
||||||
@ -474,6 +650,16 @@ run_worker() {
|
|||||||
# Strip our frontmatter so the agent only sees the task body.
|
# Strip our frontmatter so the agent only sees the task body.
|
||||||
local bodyf="$STATE/$job.body.md"
|
local bodyf="$STATE/$job.body.md"
|
||||||
strip_frontmatter "$doing_file" > "$bodyf"
|
strip_frontmatter "$doing_file" > "$bodyf"
|
||||||
|
# ── Persona injection (§6): prepend the profile's persona to the body fed to
|
||||||
|
# the engine (job body unchanged on disk). Secrets are never logged. ──
|
||||||
|
local prof; prof=$(fm_get "$doing_file" profile "")
|
||||||
|
if [[ -n "$prof" ]]; then
|
||||||
|
local persona; persona=$(profile_persona "$prof")
|
||||||
|
if [[ -n "$persona" ]]; then
|
||||||
|
{ printf '%s\n\n' "$persona"; cat "$bodyf"; } > "$bodyf.tmp" && mv "$bodyf.tmp" "$bodyf"
|
||||||
|
echo "profile: injected persona overlay from '$prof'" >> "$logf"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
build_agent_cmd "$engine" "$bodyf" "$yolo"
|
build_agent_cmd "$engine" "$bodyf" "$yolo"
|
||||||
|
|
||||||
# ── WIP checkpoint setup (§25.2): on a git cwd, create/checkout aq/wip/<job>
|
# ── WIP checkpoint setup (§25.2): on a git cwd, create/checkout aq/wip/<job>
|
||||||
@ -547,6 +733,11 @@ run_worker() {
|
|||||||
_numstat_into_meta "$cwd" "$WIP_BASE" "$metaf"
|
_numstat_into_meta "$cwd" "$WIP_BASE" "$metaf"
|
||||||
parse_usage "$engine" "$logf" >> "$metaf"
|
parse_usage "$engine" "$logf" >> "$metaf"
|
||||||
|
|
||||||
|
# ── allowed-scope guardrail (§6) — WARN-ONLY: flag out-of-scope changes but
|
||||||
|
# never block the job this phase. Scope may be inherited from the profile. ──
|
||||||
|
local scope; scope=$(fm_eff "$doing_file" allowed-scope "" allowed-scope)
|
||||||
|
[[ -n "$scope" ]] && scope_check "$cwd" "$WIP_BASE" "$scope" "$logf" "$metaf"
|
||||||
|
|
||||||
if $timed_out; then
|
if $timed_out; then
|
||||||
echo "TIMED OUT after ${tmo}s (rc=$rc): $(date)" >> "$logf"
|
echo "TIMED OUT after ${tmo}s (rc=$rc): $(date)" >> "$logf"
|
||||||
_finish_failure "$job" "$doing_file" "$metaf" "$logf" "timeout" "$rc" "$started"
|
_finish_failure "$job" "$doing_file" "$metaf" "$logf" "timeout" "$rc" "$started"
|
||||||
@ -558,7 +749,8 @@ run_worker() {
|
|||||||
local review_file="$REVIEW/$job.md"
|
local review_file="$REVIEW/$job.md"
|
||||||
echo "exit=$rc" >> "$metaf"
|
echo "exit=$rc" >> "$metaf"
|
||||||
echo "completed OK (rc=0): landed in review — $(date)" >> "$logf"
|
echo "completed OK (rc=0): landed in review — $(date)" >> "$logf"
|
||||||
local verify; verify=$(fm_get "$review_file" verify "$DEFAULT_VERIFY")
|
# verify is job-level, else inherited from the profile's default-verify.
|
||||||
|
local verify; verify=$(fm_eff "$review_file" verify "$DEFAULT_VERIFY" default-verify)
|
||||||
if [[ -z "$verify" ]]; then
|
if [[ -z "$verify" ]]; then
|
||||||
_meta_end "$metaf" "review" "$started"
|
_meta_end "$metaf" "review" "$started"
|
||||||
echo "no verify command — parked in review for manual promote: $(date)" >> "$logf"
|
echo "no verify command — parked in review for manual promote: $(date)" >> "$logf"
|
||||||
@ -896,6 +1088,13 @@ cmd_add() {
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── dep cycle detection (§5): reject a submit that would create a cycle in the
|
||||||
|
# idempotency-key dependency graph (inbox + active stages). ──
|
||||||
|
local newdeps; newdeps=$(parse_list "$(fm_get "$file" deps "")" | tr '\n' ' ')
|
||||||
|
if [[ -n "${newdeps// /}" ]] && deps_would_cycle "$idem" "$newdeps"; then
|
||||||
|
die "dependency cycle detected: job (key '${idem:-<none>}') with deps [${newdeps% }] would create a cycle — refusing."
|
||||||
|
fi
|
||||||
|
|
||||||
local base; base=$(basename "$file")
|
local base; base=$(basename "$file")
|
||||||
local stamp; stamp=$(date +%Y%m%d-%H%M%S)
|
local stamp; stamp=$(date +%Y%m%d-%H%M%S)
|
||||||
local dest="$INBOX/${stamp}__${base}"
|
local dest="$INBOX/${stamp}__${base}"
|
||||||
@ -963,6 +1162,8 @@ cmd_run() {
|
|||||||
cand_job=$(basename "$cand"); cand_job=${cand_job%.md}
|
cand_job=$(basename "$cand"); cand_job=${cand_job%.md}
|
||||||
cand_ne=$(grep '^next_eligible=' "$STATE/$cand_job.meta" 2>/dev/null | tail -1 | cut -d= -f2)
|
cand_ne=$(grep '^next_eligible=' "$STATE/$cand_job.meta" 2>/dev/null | tail -1 | cut -d= -f2)
|
||||||
if [[ "$cand_ne" =~ ^[0-9]+$ ]] && [[ "$cand_ne" -gt "$now_s" ]]; then continue; fi
|
if [[ "$cand_ne" =~ ^[0-9]+$ ]] && [[ "$cand_ne" -gt "$now_s" ]]; then continue; fi
|
||||||
|
# skip jobs whose deps (§5 DAG) are unmet — blocked, re-evaluated next loop
|
||||||
|
if [[ -n "$(deps_unmet "$cand")" ]]; then continue; fi
|
||||||
next="$cand"; break
|
next="$cand"; break
|
||||||
done < <(inbox_sorted)
|
done < <(inbox_sorted)
|
||||||
[[ -z "$next" ]] && break
|
[[ -z "$next" ]] && break
|
||||||
@ -997,16 +1198,17 @@ cmd_run() {
|
|||||||
echo "attempts=$w_attempts"
|
echo "attempts=$w_attempts"
|
||||||
echo "priority=$(fm_get "$doing_file" priority medium)"
|
echo "priority=$(fm_get "$doing_file" priority medium)"
|
||||||
echo "profile=$(fm_get "$doing_file" profile "")"
|
echo "profile=$(fm_get "$doing_file" profile "")"
|
||||||
echo "engine_class=$(fm_get "$doing_file" engine-class "")"
|
echo "engine_class=$(fm_eff "$doing_file" engine-class "")"
|
||||||
echo "capabilities=$(fm_get "$doing_file" capabilities "")"
|
echo "capabilities=$(fm_eff "$doing_file" capabilities "")"
|
||||||
echo "prefers=$(fm_get "$doing_file" prefers "")"
|
echo "prefers=$(fm_get "$doing_file" prefers "")"
|
||||||
echo "prefers_engine=$(fm_get "$doing_file" prefers-engine "")"
|
echo "prefers_engine=$(fm_eff "$doing_file" prefers-engine "")"
|
||||||
|
echo "allowed_scope=$(fm_eff "$doing_file" allowed-scope "" allowed-scope)"
|
||||||
echo "budget=$(fm_get "$doing_file" budget "")"
|
echo "budget=$(fm_get "$doing_file" budget "")"
|
||||||
echo "deps=$(fm_get "$doing_file" deps "")"
|
echo "deps=$(fm_get "$doing_file" deps "")"
|
||||||
echo "deps_mode=$(fm_get "$doing_file" deps-mode "")"
|
echo "deps_mode=$(fm_get "$doing_file" deps-mode "")"
|
||||||
echo "idempotency_key=$(fm_get "$doing_file" idempotency-key "")"
|
echo "idempotency_key=$(fm_get "$doing_file" idempotency-key "")"
|
||||||
echo "retry=$(fm_get "$doing_file" retry "")"
|
echo "retry=$(fm_get "$doing_file" retry "")"
|
||||||
echo "review_policy=$(fm_get "$doing_file" review-policy "")"
|
echo "review_policy=$(fm_eff "$doing_file" review-policy "" review-policy)"
|
||||||
echo "artifacts=$(fm_get "$doing_file" artifacts "")"
|
echo "artifacts=$(fm_get "$doing_file" artifacts "")"
|
||||||
echo "tracker_item=$(fm_get "$doing_file" tracker-item "")"
|
echo "tracker_item=$(fm_get "$doing_file" tracker-item "")"
|
||||||
} > "$STATE/$job.meta"
|
} > "$STATE/$job.meta"
|
||||||
@ -1018,8 +1220,11 @@ cmd_run() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
if $once; then
|
if $once; then
|
||||||
[[ "$(active_workers)" -eq 0 && -z "$(ls -1 "$INBOX"/*.md 2>/dev/null)" ]] && {
|
# drain when no worker is running and nothing in inbox can still progress on
|
||||||
log "drain complete — inbox empty, no workers running"; rm -f "$STATE/daemon.pid"; exit 0; }
|
# its own (backoff jobs still count as pending; dep-blocked jobs do not).
|
||||||
|
if [[ "$(active_workers)" -eq 0 ]] && ! _drain_pending; then
|
||||||
|
log "drain complete — no runnable work, no workers running"; rm -f "$STATE/daemon.pid"; exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
sleep "$POLL_SECONDS"
|
sleep "$POLL_SECONDS"
|
||||||
done
|
done
|
||||||
@ -1072,6 +1277,18 @@ cmd_status() {
|
|||||||
printf ' %s%s%s\n' "$C_DIM" "$(_insights_line "$f")" "$C_RESET"
|
printf ' %s%s%s\n' "$C_DIM" "$(_insights_line "$f")" "$C_RESET"
|
||||||
done
|
done
|
||||||
$printed || printf ' %sno workers running%s\n' "$C_DIM" "$C_RESET"
|
$printed || printf ' %sno workers running%s\n' "$C_DIM" "$C_RESET"
|
||||||
|
|
||||||
|
# blocked jobs (unmet deps, §5) — waiting in inbox/, re-evaluated each loop
|
||||||
|
local bf bj unmet bprinted=false
|
||||||
|
for bf in "$INBOX"/*.md; do
|
||||||
|
[[ -e "$bf" ]] || continue
|
||||||
|
unmet=$(deps_unmet "$bf")
|
||||||
|
[[ -n "$unmet" ]] || continue
|
||||||
|
if ! $bprinted; then echo; printf ' %sBLOCKED%s\n' "$C_BOLD" "$C_RESET"; bprinted=true; fi
|
||||||
|
bj=$(basename "$bf"); bj=${bj%.md}
|
||||||
|
printf ' %s%-26s%s %sblocked (waiting on: %s)%s\n' \
|
||||||
|
"$C_BOLD" "$bj" "$C_RESET" "$C_YEL" "$unmet" "$C_RESET"
|
||||||
|
done
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1328,10 +1545,18 @@ ${C_BOLD}TASK FRONTMATTER${C_RESET} (top of each .md)
|
|||||||
capabilities: [os:any, node>=20, has:git] # hard host requirements; unmet -> failed (capability_mismatch)
|
capabilities: [os:any, node>=20, has:git] # hard host requirements; unmet -> failed (capability_mismatch)
|
||||||
idempotency-key: my-task-1 # re-adding same key+body = no-op; same key+different body = reject/supersede
|
idempotency-key: my-task-1 # re-adding same key+body = no-op; same key+different body = reject/supersede
|
||||||
retry: { max: 2, backoff: 5m, on: [timeout, verify_failed, crash] } # requeue on these classes up to max, then retries_exhausted
|
retry: { max: 2, backoff: 5m, on: [timeout, verify_failed, crash] } # requeue on these classes up to max, then retries_exhausted
|
||||||
|
profile: backend-engineer # inherit persona + verify/caps/engine-class/scope/review-policy (job fields override)
|
||||||
|
deps: [other-key] # block until each idempotency-key is shipped/ (or testing/ if deps-mode: soft)
|
||||||
|
deps-mode: soft # soft = a dep also counts as satisfied while in testing/
|
||||||
# --- reserved (parsed + shown in status, but no-op until a later phase) ---
|
# --- reserved (parsed + shown in status, but no-op until a later phase) ---
|
||||||
profile: prefers: budget: deps: deps-mode: review-policy: artifacts: tracker-item:
|
prefers: budget: review-policy: artifacts: tracker-item:
|
||||||
---
|
---
|
||||||
|
|
||||||
|
${C_BOLD}PROFILES${C_RESET} profiles/<name>.md presets persona + capabilities + default-verify + engine-class +
|
||||||
|
prefers-engine + allowed-scope + review-policy. A job's own fields always override.
|
||||||
|
Catalog: developer, backend-engineer, frontend-engineer, ux-designer, ui-designer,
|
||||||
|
qa, reviewer, docs-writer, planner(reserved).
|
||||||
|
|
||||||
${C_BOLD}RESILIENCE${C_RESET} crash-safe: orphaned building/ jobs (dead worker) are recovered to inbox/ on
|
${C_BOLD}RESILIENCE${C_RESET} crash-safe: orphaned building/ jobs (dead worker) are recovered to inbox/ on
|
||||||
'run' startup; git-repo cwd work is checkpointed to branch aq/wip/<job> on every
|
'run' startup; git-repo cwd work is checkpointed to branch aq/wip/<job> on every
|
||||||
exit (resumed on retry); 'retry' requeues failures with backoff. See 'insights'.
|
exit (resumed on retry); 'retry' requeues failures with backoff. See 'insights'.
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
| Phase | Theme | Status | % | Gate |
|
| Phase | Theme | Status | % | Gate |
|
||||||
| ----- | ----- | ------ | - | ---- |
|
| ----- | ----- | ------ | - | ---- |
|
||||||
| **0** | Baseline (today) | ✅ shipped | 100% | `selftest.sh` green |
|
| **0** | Baseline (today) | ✅ shipped | 100% | `selftest.sh` green |
|
||||||
| **1** | Manifest + profiles + capabilities + tracker adapter (single host) | ◐ in progress | 55% | adapter e2e + selftest |
|
| **1** | Manifest + profiles + capabilities + tracker adapter (single host) | ◐ in progress | 80% | adapter e2e + selftest |
|
||||||
| **2** | Coordinator as platform-service module + Cosmos + multi-factory leasing | ☐ not started | 0% | fleet e2e + module tests |
|
| **2** | Coordinator as platform-service module + Cosmos + multi-factory leasing | ☐ not started | 0% | fleet e2e + module tests |
|
||||||
| **3** | Fleet control plane in tracker-web + DAG deps + budgets + scoring router | ☐ not started | 0% | web e2e + router tests |
|
| **3** | Fleet control plane in tracker-web + DAG deps + budgets + scoring router | ☐ not started | 0% | web e2e + router tests |
|
||||||
| **4** | Message bus + autoscaling + cross-OS capability marketplace | ☐ not started | 0% | load/chaos suite |
|
| **4** | Message bus + autoscaling + cross-OS capability marketplace | ☐ not started | 0% | load/chaos suite |
|
||||||
@ -142,7 +142,7 @@ tracker-item: ITEM-789 # link back to the originating tracker task
|
|||||||
- [x] **Capability grammar** defined: tokens are `key` (presence, e.g. `has:xcode`), `key:value` (e.g. `os:mac`, `engine:devin`), or `key<op>version` with `op ∈ {>=,>,=,<=,<}` (e.g. `node>=20`). `os:any` is a wildcard that matches every factory. A job matches a factory iff every required token is satisfied by the factory descriptor. *(P1-S1: `caps_match`/`detect_capabilities` in `agent-queue.sh`.)*
|
- [x] **Capability grammar** defined: tokens are `key` (presence, e.g. `has:xcode`), `key:value` (e.g. `os:mac`, `engine:devin`), or `key<op>version` with `op ∈ {>=,>,=,<=,<}` (e.g. `node>=20`). `os:any` is a wildcard that matches every factory. A job matches a factory iff every required token is satisfied by the factory descriptor. *(P1-S1: `caps_match`/`detect_capabilities` in `agent-queue.sh`.)*
|
||||||
- [x] **`engine-class` taxonomy** defined as an enum (`agentic-coder`, `chat-coder`, `review-only`) with a documented engine→class map (`devin,claude,codex → agentic-coder`; `copilot → chat-coder`). If `engine` is set it wins; else the scheduler picks any free engine in the class honoring `prefers-engine`. *(P1-S1: `resolve_engine`; `review-only` mapping reserved.)*
|
- [x] **`engine-class` taxonomy** defined as an enum (`agentic-coder`, `chat-coder`, `review-only`) with a documented engine→class map (`devin,claude,codex → agentic-coder`; `copilot → chat-coder`). If `engine` is set it wins; else the scheduler picks any free engine in the class honoring `prefers-engine`. *(P1-S1: `resolve_engine`; `review-only` mapping reserved.)*
|
||||||
- [x] **`idempotency-key` semantics:** `key + content-hash` identical ⇒ no-op (returns existing job). Same `key`, **different** content ⇒ **rejected with 409** unless the prior job is still `queued`/`blocked` (then it is superseded). A re-`run`/`retry` of an existing job is **not** a new submit and never trips dedupe. *(P1-S1: add-time dedupe; bash maps "409" → clear error, `queued` → still in `inbox/` ⇒ superseded.)*
|
- [x] **`idempotency-key` semantics:** `key + content-hash` identical ⇒ no-op (returns existing job). Same `key`, **different** content ⇒ **rejected with 409** unless the prior job is still `queued`/`blocked` (then it is superseded). A re-`run`/`retry` of an existing job is **not** a new submit and never trips dedupe. *(P1-S1: add-time dedupe; bash maps "409" → clear error, `queued` → still in `inbox/` ⇒ superseded.)*
|
||||||
- [ ] **`deps` semantics:** a dep is satisfied when it reaches `shipped` (default) or `testing` if `deps-mode: soft`. Submit-time **cycle detection** rejects cyclic graphs; unmet deps put the job in `blocked` (not `queued`). Cross-factory deps require the coordinator (P2); single-host deps work in P1.
|
- [x] **`deps` semantics:** a dep is satisfied when it reaches `shipped` (default) or `testing` if `deps-mode: soft`. Submit-time **cycle detection** rejects cyclic graphs; unmet deps put the job in `blocked` (not `queued`). Cross-factory deps require the coordinator (P2); single-host deps work in P1. *(P1-S2: `deps_unmet` skip-with-reason in selection + `status` surfacing; `deps_would_cycle` on `add`. Cross-machine deps remain P2.)*
|
||||||
- **Acceptance:** a manifest fixture suite parses/validates; invalid manifests fail with precise errors; capability-grammar + dep-cycle + idempotency-conflict cases covered.
|
- **Acceptance:** a manifest fixture suite parses/validates; invalid manifests fail with precise errors; capability-grammar + dep-cycle + idempotency-conflict cases covered.
|
||||||
- **Verify gate:** schema unit tests (≥ 1 per field incl. defaults + 5 invalid cases + grammar/cycle/409 cases).
|
- **Verify gate:** schema unit tests (≥ 1 per field incl. defaults + 5 invalid cases + grammar/cycle/409 cases).
|
||||||
|
|
||||||
@ -167,11 +167,11 @@ review-policy: manual
|
|||||||
---
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Author starter catalog: `developer`, `backend-engineer`, `frontend-engineer`, `ux-designer`, `ui-designer`, `qa`, `reviewer`, `docs-writer`.
|
- [x] Author starter catalog: `developer`, `backend-engineer`, `frontend-engineer`, `ux-designer`, `ui-designer`, `qa`, `reviewer`, `docs-writer`. *(P1-S2: `profiles/*.md` + a reserved `planner`.)*
|
||||||
- [ ] Persona overlay is **prepended** to the job body before the agent runs; secrets are never written to logs or the event stream (redaction at the source).
|
- [x] Persona overlay is **prepended** to the job body before the agent runs; secrets are never written to logs or the event stream (redaction at the source). *(P1-S2: `profile_persona` prepended to the stripped body file.)*
|
||||||
- [ ] Profile supplies default `verify`, `capabilities`, `engine-class`, `allowed-scope` when the job omits them.
|
- [x] Profile supplies default `verify`, `capabilities`, `engine-class`, `allowed-scope` when the job omits them. *(P1-S2: `fm_eff` — also `prefers-engine` + `review-policy`; job fields always override.)*
|
||||||
- [ ] Profile versioning: changing a profile doesn't mutate in-flight jobs (snapshot at assign time).
|
- [ ] Profile versioning: changing a profile doesn't mutate in-flight jobs (snapshot at assign time). *(P2 — needs Cosmos snapshot at assign time.)*
|
||||||
- [ ] `allowed-scope` enforced as a guardrail (warn in P1, enforce/deny in P2 via pre-flight diff check).
|
- [x] `allowed-scope` enforced as a guardrail (warn in P1, enforce/deny in P2 via pre-flight diff check). *(P1-S2: `scope_check` post-run WARN-only + `scope_warning=` in meta; `path_in_scope` unit-testable.)*
|
||||||
- **Acceptance:** a job with `profile: backend-engineer` and no `verify` inherits the profile's verify + persona.
|
- **Acceptance:** a job with `profile: backend-engineer` and no `verify` inherits the profile's verify + persona.
|
||||||
- **Verify gate:** profile-resolution unit tests; persona-injection golden test.
|
- **Verify gate:** profile-resolution unit tests; persona-injection golden test.
|
||||||
|
|
||||||
@ -342,18 +342,20 @@ Each phase: **Goal → checklist → Exit criteria**. Don't start a phase until
|
|||||||
|
|
||||||
> **Slice progress — P1-S1:** manifest parsing (all §5 fields, defaulted + backward-compatible), `priority` ordering, capability detection+match gate, `engine-class` resolution, and `idempotency-key` dedupe are **done** on the bash runner.
|
> **Slice progress — P1-S1:** manifest parsing (all §5 fields, defaulted + backward-compatible), `priority` ordering, capability detection+match gate, `engine-class` resolution, and `idempotency-key` dedupe are **done** on the bash runner.
|
||||||
>
|
>
|
||||||
> **Slice progress — P1-S3 (resilience & insights, single host):** crash recovery (`recover_orphans` + `aq recover`), git WIP checkpoint/resume (`aq/wip/<job>`), functional `retry` policy (backoff + `retries_exhausted`), and execution insights (`parse_usage`, per-run metrics in meta, `aq insights`, `status`/`dash` insights) are **done** — see §11/§25/§26. Profiles, `deps` DAG, `budget.wall`, `allowed-scope`, and the tracker adapter remain **for later slices**.
|
> **Slice progress — P1-S3 (resilience & insights, single host):** crash recovery (`recover_orphans` + `aq recover`), git WIP checkpoint/resume (`aq/wip/<job>`), functional `retry` policy (backoff + `retries_exhausted`), and execution insights (`parse_usage`, per-run metrics in meta, `aq insights`, `status`/`dash` insights) are **done** — see §11/§25/§26.
|
||||||
|
>
|
||||||
|
> **Slice progress — P1-S2 (profiles + deps/DAG, single host):** the `profiles/` catalog + resolution (`fm_eff` inheritance with job>profile>default precedence, persona injection), the warn-only `allowed-scope` guardrail (`scope_check`/`path_in_scope`), and single-host `deps` (block-with-reason in selection, `status` surfacing, submit-time cycle detection) are **done** — see §5/§6. The tracker adapter and `budget.wall` remain **for later slices**.
|
||||||
|
|
||||||
- [x] Extend `agent-queue.sh` frontmatter parsing for all new manifest fields (§5), defaulted + backward-compatible. *(P1-S1)*
|
- [x] Extend `agent-queue.sh` frontmatter parsing for all new manifest fields (§5), defaulted + backward-compatible. *(P1-S1)*
|
||||||
- [ ] Add `profiles/` directory + profile resolution (persona injection, default verify/caps/scope) (§6).
|
- [x] Add `profiles/` directory + profile resolution (persona injection, default verify/caps/scope) (§6). *(P1-S2)*
|
||||||
- [x] Local capability detection + a job/factory capability match check before launch (§8 subset). *(P1-S1: `detect_capabilities` + `caps_match`; mismatch ⇒ `failed/` `result=capability_mismatch`, agent never launched.)*
|
- [x] Local capability detection + a job/factory capability match check before launch (§8 subset). *(P1-S1: `detect_capabilities` + `caps_match`; mismatch ⇒ `failed/` `result=capability_mismatch`, agent never launched.)*
|
||||||
- [x] `priority` ordering in the inbox pick (replace pure FIFO with priority-then-age). *(P1-S1: `inbox_sorted`; per-lock serialization preserved.)*
|
- [x] `priority` ordering in the inbox pick (replace pure FIFO with priority-then-age). *(P1-S1: `inbox_sorted`; per-lock serialization preserved.)*
|
||||||
- [ ] `deps` (DAG) blocking on a single host; `idempotency-key` dedupe on `add`. *(P1-S1: `idempotency-key` dedupe DONE; `deps` DAG blocking still pending.)*
|
- [x] `deps` (DAG) blocking on a single host; `idempotency-key` dedupe on `add`. *(P1-S1 idempotency dedupe + P1-S2 `deps` blocking/cycle detection.)*
|
||||||
- [ ] `retry` with backoff into `failed`/requeue; `budget.wall` enforced (extends `timeout`). *(P1-S3: `retry` with backoff + `retries_exhausted` DONE; `budget.wall` still pending.)*
|
- [ ] `retry` with backoff into `failed`/requeue; `budget.wall` enforced (extends `timeout`). *(P1-S3: `retry` with backoff + `retries_exhausted` DONE; `budget.wall` still pending.)*
|
||||||
- [ ] `allowed-scope` guardrail (warn-only this phase) + post-run diff report.
|
- [x] `allowed-scope` guardrail (warn-only this phase) + post-run diff report. *(P1-S2: `scope_check` WARN-only + `scope_warning=`.)*
|
||||||
- [ ] **Tracker adapter** `aq from-tracker <ITEM>` + `aq to-tracker` event poster (§10 P1).
|
- [ ] **Tracker adapter** `aq from-tracker <ITEM>` + `aq to-tracker` event poster (§10 P1).
|
||||||
- [ ] Dashboard shows profile + priority + capability tags + tracker-item link. *(P1-S1: `status` shows priority/profile/caps/tracker-item; Node `dash` surfacing pending.)*
|
- [ ] Dashboard shows profile + priority + capability tags + tracker-item link. *(P1-S1: `status` shows priority/profile/caps/tracker-item; Node `dash` surfacing pending.)*
|
||||||
- [ ] Update `selftest.sh` with: manifest parse fixtures, profile resolution, priority order, dep-block, idempotency, adapter round-trip (mock). *(P1-S1: added backward-compat, priority, capability-mismatch, engine-class, idempotency cases; profile/dep-block/adapter pending.)*
|
- [ ] Update `selftest.sh` with: manifest parse fixtures, profile resolution, priority order, dep-block, idempotency, adapter round-trip (mock). *(P1-S1 manifest/priority/idempotency + P1-S2 profile resolution/persona/scope/dep-block/cycle + P1-S3 resilience/insights; tracker adapter round-trip still pending.)*
|
||||||
- [x] Update README + this doc's progress table. *(P1-S1)*
|
- [x] Update README + this doc's progress table. *(P1-S1)*
|
||||||
- **Exit criteria:** all boxes ✅; `selftest.sh` green; a tracker task → executed → tracker `done` with SHA comment, fully on one host; no regression to Phase-0 `.md` files.
|
- **Exit criteria:** all boxes ✅; `selftest.sh` green; a tracker task → executed → tracker `done` with SHA comment, fully on one host; no regression to Phase-0 `.md` files.
|
||||||
|
|
||||||
|
|||||||
19
agent-queue/profiles/backend-engineer.md
Normal file
19
agent-queue/profiles/backend-engineer.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: backend-engineer
|
||||||
|
persona: |
|
||||||
|
You are a senior backend engineer. Favor minimal, well-tested changes. Respect
|
||||||
|
service boundaries, validate inputs, handle errors explicitly, and never log
|
||||||
|
secrets. Prefer existing libraries and patterns over new dependencies. Keep
|
||||||
|
migrations and API changes backward-compatible unless the task says otherwise.
|
||||||
|
capabilities: [os:any, node>=20, has:pnpm]
|
||||||
|
default-verify: pnpm -s typecheck && pnpm -s test
|
||||||
|
engine-class: agentic-coder
|
||||||
|
prefers-engine: [devin, claude]
|
||||||
|
allowed-scope: ["backend/**", "services/**", "packages/**"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# backend-engineer
|
||||||
|
|
||||||
|
Server-side work. Inherits a typecheck+test verify gate and a scope limited to
|
||||||
|
backend/service/package code.
|
||||||
20
agent-queue/profiles/developer.md
Normal file
20
agent-queue/profiles/developer.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: developer
|
||||||
|
persona: |
|
||||||
|
You are a pragmatic senior software engineer. Make the smallest correct change
|
||||||
|
that satisfies the task. Match the surrounding code style and existing patterns,
|
||||||
|
keep diffs focused, and never commit secrets. Add or update tests when you change
|
||||||
|
behavior, and explain non-obvious decisions briefly in the commit message.
|
||||||
|
capabilities: [os:any, has:git]
|
||||||
|
default-verify:
|
||||||
|
engine-class: agentic-coder
|
||||||
|
prefers-engine: [devin, claude, codex]
|
||||||
|
allowed-scope: ["**"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# developer
|
||||||
|
|
||||||
|
General-purpose engineering profile. No default verify (parks in review for a
|
||||||
|
human gate) and an unrestricted scope — pick a more specific profile when you
|
||||||
|
want a tighter blast radius or an automatic QA gate.
|
||||||
18
agent-queue/profiles/docs-writer.md
Normal file
18
agent-queue/profiles/docs-writer.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: docs-writer
|
||||||
|
persona: |
|
||||||
|
You are a technical writer. Produce clear, accurate documentation that matches
|
||||||
|
the repository's existing voice and structure. Update READMEs, guides, and
|
||||||
|
references; keep examples runnable and links valid. Do not change source code
|
||||||
|
beyond doc comments. Never include secrets in examples.
|
||||||
|
capabilities: [os:any]
|
||||||
|
default-verify:
|
||||||
|
engine-class: agentic-coder
|
||||||
|
prefers-engine: [claude, devin]
|
||||||
|
allowed-scope: ["docs/**", "**/*.md", "**/*.mdx"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# docs-writer
|
||||||
|
|
||||||
|
Documentation profile. Scoped to docs + markdown; parks in review for a human read.
|
||||||
18
agent-queue/profiles/frontend-engineer.md
Normal file
18
agent-queue/profiles/frontend-engineer.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: frontend-engineer
|
||||||
|
persona: |
|
||||||
|
You are a senior frontend engineer. Build accessible, responsive UI that matches
|
||||||
|
the existing component library and design tokens. Keep state management simple,
|
||||||
|
avoid unnecessary dependencies, and ensure type-safety. Verify the build and
|
||||||
|
tests pass before finishing; never hardcode secrets or API keys.
|
||||||
|
capabilities: [os:any, node>=20, has:pnpm]
|
||||||
|
default-verify: pnpm -s typecheck && pnpm -s build
|
||||||
|
engine-class: agentic-coder
|
||||||
|
prefers-engine: [claude, devin]
|
||||||
|
allowed-scope: ["dashboards/**", "apps/**", "packages/ui/**", "src/**"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# frontend-engineer
|
||||||
|
|
||||||
|
Client/UI work. Inherits a typecheck+build gate and a UI-oriented scope.
|
||||||
19
agent-queue/profiles/planner.md
Normal file
19
agent-queue/profiles/planner.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: planner
|
||||||
|
persona: |
|
||||||
|
You are a planning agent. Break an objective into a dependency-ordered set of
|
||||||
|
small, well-scoped tasks, each mappable to a job .md (with a profile, scope, and
|
||||||
|
verify). Output the plan as markdown; do not implement the tasks yourself.
|
||||||
|
capabilities: [os:any]
|
||||||
|
default-verify:
|
||||||
|
engine-class: agentic-coder
|
||||||
|
prefers-engine: [claude]
|
||||||
|
allowed-scope: ["docs/**", "**/*.md"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# planner (reserved)
|
||||||
|
|
||||||
|
Reserved for a future planning/decomposition flow that emits child jobs with
|
||||||
|
`deps:` wiring. Usable today as a docs-scoped persona; automatic job emission is
|
||||||
|
a later slice.
|
||||||
18
agent-queue/profiles/qa.md
Normal file
18
agent-queue/profiles/qa.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: qa
|
||||||
|
persona: |
|
||||||
|
You are a QA engineer. Write and strengthen tests; reproduce bugs with a failing
|
||||||
|
test first, then confirm the fix. Cover edge cases, error paths, and regressions.
|
||||||
|
Do not weaken or delete existing tests to make a suite pass — fix the cause.
|
||||||
|
Keep tests deterministic and fast.
|
||||||
|
capabilities: [os:any, node>=20, has:pnpm]
|
||||||
|
default-verify: pnpm -s test
|
||||||
|
engine-class: agentic-coder
|
||||||
|
prefers-engine: [codex, claude]
|
||||||
|
allowed-scope: ["**/*.test.*", "**/*.spec.*", "test/**", "tests/**", "e2e/**"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# qa
|
||||||
|
|
||||||
|
Test-focused profile. Inherits a `pnpm -s test` gate and a test-files scope.
|
||||||
19
agent-queue/profiles/reviewer.md
Normal file
19
agent-queue/profiles/reviewer.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: reviewer
|
||||||
|
persona: |
|
||||||
|
You are a code reviewer. Do NOT modify code. Read the diff/changes and produce a
|
||||||
|
concise review: correctness, security, tests, readability, and scope adherence.
|
||||||
|
Flag risky or out-of-scope changes and supply-chain concerns (edits to shared
|
||||||
|
packages). Output findings as markdown with severity labels.
|
||||||
|
capabilities: [os:any, has:git]
|
||||||
|
default-verify:
|
||||||
|
engine-class: review-only
|
||||||
|
prefers-engine: [claude]
|
||||||
|
allowed-scope: ["docs/**", "**/*.md"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# reviewer
|
||||||
|
|
||||||
|
Read-only review profile. `engine-class: review-only` has no concrete runner
|
||||||
|
mapping yet (reserved) — use an explicit `engine:` until a review engine lands.
|
||||||
19
agent-queue/profiles/ui-designer.md
Normal file
19
agent-queue/profiles/ui-designer.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: ui-designer
|
||||||
|
persona: |
|
||||||
|
You are a UI/visual designer. Focus on visual hierarchy, spacing, color, and
|
||||||
|
typography using the existing design tokens and component library. Keep changes
|
||||||
|
consistent with the design system, ensure sufficient contrast, and respect
|
||||||
|
light/dark themes. Prefer token references over hardcoded values.
|
||||||
|
capabilities: [os:any, node>=20]
|
||||||
|
default-verify:
|
||||||
|
engine-class: agentic-coder
|
||||||
|
prefers-engine: [claude, devin]
|
||||||
|
allowed-scope: ["packages/ui/**", "packages/design-tokens/**", "**/*.css", "design/**"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# ui-designer
|
||||||
|
|
||||||
|
Visual/design-system work scoped to UI + tokens + styles. Parks in review for a
|
||||||
|
human visual check.
|
||||||
19
agent-queue/profiles/ux-designer.md
Normal file
19
agent-queue/profiles/ux-designer.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: ux-designer
|
||||||
|
persona: |
|
||||||
|
You are a UX designer. Focus on user flows, information architecture, and
|
||||||
|
interaction states (empty, loading, error, success). Produce wireframes,
|
||||||
|
flow descriptions, and copy as markdown/specs. Justify decisions with usability
|
||||||
|
heuristics and accessibility (WCAG) considerations. Do not change production code.
|
||||||
|
capabilities: [os:any]
|
||||||
|
default-verify:
|
||||||
|
engine-class: agentic-coder
|
||||||
|
prefers-engine: [claude]
|
||||||
|
allowed-scope: ["docs/**", "design/**", "**/*.md"]
|
||||||
|
review-policy: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
# ux-designer
|
||||||
|
|
||||||
|
Flows, IA, and interaction specs. Documentation-scoped; parks in review for human
|
||||||
|
sign-off (no automatic verify gate).
|
||||||
@ -420,4 +420,169 @@ else
|
|||||||
printf '%s\n' "$out" >&2; fail "insights aggregate rollup missing/incorrect"
|
printf '%s\n' "$out" >&2; fail "insights aggregate rollup missing/incorrect"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# Phase 1 — Slice 2 cases (profiles + deps/DAG, single host).
|
||||||
|
# Uses a temp profile catalog (AGENT_QUEUE_PROFILES) + temp git repos.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
profdir="$tmp/profiles"; mkdir -p "$profdir"
|
||||||
|
printf '%s\n' '---' 'name: vfail' 'persona: |' ' PERSONA-VFAIL' 'default-verify: false' '---' > "$profdir/vfail.md"
|
||||||
|
printf '%s\n' '---' 'name: vpass' 'default-verify: true' '---' > "$profdir/vpass.md"
|
||||||
|
printf '%s\n' '---' 'name: capreq' 'capabilities: [has:definitely-not-installed]' '---' > "$profdir/capreq.md"
|
||||||
|
printf '%s\n' '---' 'name: personap' 'persona: |' ' PERSONA-MARKER-XYZ' ' second persona line' 'default-verify: true' '---' > "$profdir/personap.md"
|
||||||
|
printf '%s\n' '---' 'name: scoped' 'allowed-scope: [backend/**]' '---' > "$profdir/scoped.md"
|
||||||
|
export AGENT_QUEUE_PROFILES="$profdir"
|
||||||
|
funcs="$tmp/aq-funcs.sh"; sed '/^main "\$@"/d' "$AQ" > "$funcs"
|
||||||
|
|
||||||
|
# 19. profile inherits default-verify: vfail (verify=false) → failed/verify_failed;
|
||||||
|
# vpass (verify=true) → testing/.
|
||||||
|
export AGENT_QUEUE_ROOT="$tmp/queue-pverify"
|
||||||
|
"$AQ" init >/dev/null
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'profile: vfail' '---' '' '# pv-fail' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/pvfail.md"
|
||||||
|
DEVIN_BIN="$stub" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
if ls "$AGENT_QUEUE_ROOT"/failed/pvfail.md >/dev/null 2>&1 \
|
||||||
|
&& [ "$(metaval "$AGENT_QUEUE_ROOT/.state/pvfail.meta" result)" = "verify_failed" ]; then
|
||||||
|
pass "profile inherit: default-verify=false → failed/ (verify_failed)"
|
||||||
|
else
|
||||||
|
fail "profile verify=false did not route to failed (result=$(metaval "$AGENT_QUEUE_ROOT/.state/pvfail.meta" result))"
|
||||||
|
fi
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'profile: vpass' '---' '' '# pv-pass' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/pvpass.md"
|
||||||
|
DEVIN_BIN="$stub" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
if ls "$AGENT_QUEUE_ROOT"/testing/pvpass.md >/dev/null 2>&1; then
|
||||||
|
pass "profile inherit: default-verify=true → testing/"
|
||||||
|
else
|
||||||
|
fail "profile verify=true did not reach testing/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 19b. job-level verify overrides the profile (precedence job > profile).
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'profile: vfail' 'verify: true' '---' '' '# override' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/pvoverride.md"
|
||||||
|
DEVIN_BIN="$stub" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
ls "$AGENT_QUEUE_ROOT"/testing/pvoverride.md >/dev/null 2>&1 \
|
||||||
|
&& pass "profile precedence: job verify overrides profile default-verify" \
|
||||||
|
|| fail "job-level verify did not override profile"
|
||||||
|
|
||||||
|
# 20. persona injection (golden): the body fed to the engine begins with the
|
||||||
|
# profile persona. A stub copies its --prompt-file to a sentinel.
|
||||||
|
export AGENT_QUEUE_ROOT="$tmp/queue-persona"
|
||||||
|
sentinel="$tmp/persona-body.txt"; rm -f "$sentinel"
|
||||||
|
copystub="$tmp/copy-engine"
|
||||||
|
cat > "$copystub" <<STUBEOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
pf=""
|
||||||
|
while [ \$# -gt 0 ]; do case "\$1" in --prompt-file) pf="\$2"; shift 2;; *) shift;; esac; done
|
||||||
|
[ -n "\$pf" ] && cp "\$pf" "$sentinel"
|
||||||
|
exit 0
|
||||||
|
STUBEOF
|
||||||
|
chmod +x "$copystub"
|
||||||
|
"$AQ" init >/dev/null
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'profile: personap' '---' '' 'TASK-BODY-LINE' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/personajob.md"
|
||||||
|
DEVIN_BIN="$copystub" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
if [ "$(head -1 "$sentinel" 2>/dev/null)" = "PERSONA-MARKER-XYZ" ] \
|
||||||
|
&& grep -q 'TASK-BODY-LINE' "$sentinel" 2>/dev/null; then
|
||||||
|
pass "persona injection: engine body begins with profile persona, task preserved"
|
||||||
|
else
|
||||||
|
echo "body head: $(head -3 "$sentinel" 2>/dev/null)" >&2
|
||||||
|
fail "persona was not prepended to the engine body"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 21. profile capability inheritance: a job omitting capabilities inherits the
|
||||||
|
# profile's → unmet → failed/ capability_mismatch, agent never launched.
|
||||||
|
export AGENT_QUEUE_ROOT="$tmp/queue-pcaps"
|
||||||
|
launchflag="$tmp/pcaps-launched"; rm -f "$launchflag"
|
||||||
|
launchstub3="$tmp/cap-launch3"
|
||||||
|
printf '#!/usr/bin/env bash\ntouch %q\nexit 0\n' "$launchflag" > "$launchstub3"; chmod +x "$launchstub3"
|
||||||
|
"$AQ" init >/dev/null
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'profile: capreq' '---' '' '# pcaps' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/pcapsjob.md"
|
||||||
|
DEVIN_BIN="$launchstub3" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
if ls "$AGENT_QUEUE_ROOT"/failed/pcapsjob.md >/dev/null 2>&1 \
|
||||||
|
&& [ "$(metaval "$AGENT_QUEUE_ROOT/.state/pcapsjob.meta" result)" = "capability_mismatch" ] \
|
||||||
|
&& [ ! -e "$launchflag" ]; then
|
||||||
|
pass "profile caps inheritance: unmet inherited capability → capability_mismatch (no launch)"
|
||||||
|
else
|
||||||
|
fail "profile caps inheritance failed (result=$(metaval "$AGENT_QUEUE_ROOT/.state/pcapsjob.meta" result) launched=$([ -e "$launchflag" ] && echo yes || echo no))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 22. allowed-scope warn-only: an out-of-scope change logs a WARNING and the job
|
||||||
|
# still succeeds; plus a direct path_in_scope unit check.
|
||||||
|
export AGENT_QUEUE_ROOT="$tmp/queue-scope"
|
||||||
|
repos="$tmp/repo-scope"; mkrepo "$repos"
|
||||||
|
scopestub="$tmp/scope-engine"
|
||||||
|
printf '#!/usr/bin/env bash\nmkdir -p frontend && echo changed > frontend/out.txt\nexit 0\n' > "$scopestub"
|
||||||
|
chmod +x "$scopestub"
|
||||||
|
"$AQ" init >/dev/null
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $repos" 'yolo: true' 'profile: scoped' '---' '' '# scope task' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/scopejob.md"
|
||||||
|
DEVIN_BIN="$scopestub" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
if grep -q 'allowed-scope violation' "$AGENT_QUEUE_ROOT/logs/scopejob.log" 2>/dev/null \
|
||||||
|
&& ls "$AGENT_QUEUE_ROOT"/review/scopejob.md >/dev/null 2>&1; then
|
||||||
|
pass "allowed-scope: out-of-scope change WARNS (warn-only) and job still succeeds"
|
||||||
|
else
|
||||||
|
fail "allowed-scope warn-only did not warn / job did not succeed"
|
||||||
|
fi
|
||||||
|
if bash -c 'set -uo pipefail; source "'"$funcs"'"; path_in_scope "backend/a/b.ts" "backend/**" && ! path_in_scope "frontend/x.ts" "backend/**"'; then
|
||||||
|
pass "allowed-scope: path_in_scope matches subtree, rejects outside (unit)"
|
||||||
|
else
|
||||||
|
fail "path_in_scope unit logic wrong"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 23. deps block→run: B deps:[keyA] stays blocked until A is shipped/, then runs.
|
||||||
|
export AGENT_QUEUE_ROOT="$tmp/queue-deps"
|
||||||
|
"$AQ" init >/dev/null
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'idempotency-key: keyA' '---' '' '# A' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/jobA.md"
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'idempotency-key: keyB' 'deps: [keyA]' '---' '' '# B' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/jobB.md"
|
||||||
|
DEVIN_BIN="$stub" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
if ls "$AGENT_QUEUE_ROOT"/inbox/jobB.md >/dev/null 2>&1 && ls "$AGENT_QUEUE_ROOT"/review/jobA.md >/dev/null 2>&1; then
|
||||||
|
pass "deps: B stays blocked in inbox while A is unshipped"
|
||||||
|
else
|
||||||
|
fail "deps: B should be blocked while A unshipped (A=$(ls "$AGENT_QUEUE_ROOT"/review 2>/dev/null) B-in-inbox=$(ls "$AGENT_QUEUE_ROOT"/inbox 2>/dev/null))"
|
||||||
|
fi
|
||||||
|
# status surfaces the blocked job
|
||||||
|
"$AQ" status 2>/dev/null | grep -q 'blocked (waiting on: keyA)' \
|
||||||
|
&& pass "deps: status surfaces 'blocked (waiting on: keyA)'" \
|
||||||
|
|| fail "deps: status did not surface blocked job"
|
||||||
|
# ship A (review -> testing -> shipped), then B becomes runnable
|
||||||
|
"$AQ" promote jobA >/dev/null 2>&1 # review -> testing
|
||||||
|
"$AQ" promote jobA >/dev/null 2>&1 # testing -> shipped
|
||||||
|
DEVIN_BIN="$stub" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
if ls "$AGENT_QUEUE_ROOT"/review/jobB.md >/dev/null 2>&1; then
|
||||||
|
pass "deps: once A is shipped, B unblocks and completes"
|
||||||
|
else
|
||||||
|
fail "deps: B did not run after A shipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 24. deps-mode soft: dep satisfied when the dependency is in testing/.
|
||||||
|
export AGENT_QUEUE_ROOT="$tmp/queue-depsoft"
|
||||||
|
"$AQ" init >/dev/null
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'idempotency-key: keyA' 'verify: true' '---' '' '# A-soft' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/sjobA.md"
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'idempotency-key: keyC' 'deps: [keyA]' 'deps-mode: soft' '---' '' '# C-soft' \
|
||||||
|
> "$AGENT_QUEUE_ROOT/inbox/sjobC.md"
|
||||||
|
DEVIN_BIN="$stub" "$AQ" run --once >/dev/null 2>&1
|
||||||
|
if ls "$AGENT_QUEUE_ROOT"/testing/sjobA.md >/dev/null 2>&1 && ls "$AGENT_QUEUE_ROOT"/review/sjobC.md >/dev/null 2>&1; then
|
||||||
|
pass "deps-mode soft: dep satisfied while dependency is in testing/"
|
||||||
|
else
|
||||||
|
fail "deps-mode soft did not unblock from testing/ (A=$(ls "$AGENT_QUEUE_ROOT"/testing 2>/dev/null) C=$(ls "$AGENT_QUEUE_ROOT"/review 2>/dev/null))"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 25. cycle detection: adding A deps:[keyB] while B deps:[keyA] exists is rejected.
|
||||||
|
export AGENT_QUEUE_ROOT="$tmp/queue-cycle"
|
||||||
|
"$AQ" init >/dev/null
|
||||||
|
cycB="$tmp/cyc-b.md"
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'idempotency-key: keyB' 'deps: [keyA]' '---' '' '# cyc B' > "$cycB"
|
||||||
|
"$AQ" add "$cycB" >/dev/null 2>&1 # B added (blocked on keyA — allowed)
|
||||||
|
cycA="$tmp/cyc-a.md"
|
||||||
|
printf '%s\n' '---' 'engine: devin' "cwd: $work" 'yolo: true' 'idempotency-key: keyA' 'deps: [keyB]' '---' '' '# cyc A' > "$cycA"
|
||||||
|
if DEVIN_BIN="$stub" "$AQ" add "$cycA" >/dev/null 2>&1; then
|
||||||
|
fail "cycle detection: adding A deps:[keyB] while B deps:[keyA] should be rejected"
|
||||||
|
else
|
||||||
|
pass "cycle detection: dependency cycle on add is rejected"
|
||||||
|
fi
|
||||||
|
unset AGENT_QUEUE_PROFILES
|
||||||
|
|
||||||
echo "self-test PASS"
|
echo "self-test PASS"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user