diff --git a/CHANGELOG.md b/CHANGELOG.md index 492a4adbe..9f2ae2017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ that skill is installed beside it — a human committing outside any harness gets the deterministic checks CI would report, first; a repository's first commit skips it with a note (VST-310). +- orch: `reconcile-work-items` reports tracker state written once and never + re-read — parked containers, stale started items, Done items with unchecked + acceptance boxes; oversee's close-out and audit-issues' preflight run it, so + a skipped close step or a partial-scope `Closes` cannot stay silent (VST-318). - settings templates: every key's comment condensed to one-line intent plus landmines (922 → 545 lines across the root and skill templates, zero value diff --git a/skills/orch/README.md b/skills/orch/README.md index eea63c113..13d8bf278 100644 --- a/skills/orch/README.md +++ b/skills/orch/README.md @@ -45,6 +45,7 @@ Invoke through your AI coding harness (`/orch `, `/skill:orch | `ORCH_MERGE_AUTONOMY` | `auto` merges without asking once every merge gate is green; `ask` presents the merge decision. A `MERGE_READY = false` state never auto-merges | `ask` | | `ORCH_OVERSEER_LANES` | Max concurrent lanes `oversee` keeps in flight | `3` | | `QA_PERF_PATHS` | Space-separated path globs whose modification adds the `needs-perf-test` QA signal in `workflows/review-pr.md` § 5. Empty means the diff scan never raises it | empty | +| `RECONCILE_STALE_HOURS` | Hours before an In Progress / In Review item counts as started-stale in `reconcile-work-items` sweeps | `24` | | Review-gate settings | `REVIEW_GATE_MODE`, `PR_REVIEW_GATE`, `PR_REVIEW_CHECK`, `PR_REVIEW_QUORUM`, `PR_REVIEW_ON_TIMEOUT`, `PR_REVIEW_NUDGE*`, `PR_REVIEW_WAIT_SECS` — [references/gates.md](references/gates.md) | — | | Lane settings | `ORCH_LANE_DIRS`, `ORCH_LANE_ALIASES`, `ORCH_LANE_MAX_PCT`, `ORCH_TMUX_VERIFY_SECS` — `lanes --help`, `open-terminal --help` | — | diff --git a/skills/orch/SKILL.md b/skills/orch/SKILL.md index ff3ad67e8..ba0ccee15 100644 --- a/skills/orch/SKILL.md +++ b/skills/orch/SKILL.md @@ -82,6 +82,7 @@ Route ` [args]` to its workflow and follow [Workflow Execution](#workfl | `spawn-adapter` | Resolve Codex spawn parameters (`spawn`) and the runtime thread budget (`slots`) | | `open-terminal` | Launch-only terminal handoff; model, effort, and permission flags come from `--launch-flags`. `--help` | | `lanes` | Enumerate harness auth lanes, their live usage, and the launches already in flight on each; `pick` prints the launch env prefix for the qualifying lane with the fewest in-flight claims, headroom breaking the tie, exit 3 when none qualifies. `--help` | +| `reconcile-work-items` | Read-only tracker sweep: parked containers (children Done, parent open), stale started items (untouched past `RECONCILE_STALE_HOURS`, PR merged or absent), Done items with unchecked `- [ ]` boxes. Exit 1 on findings, mutates nothing. | | `oversee-watch` | Block until the fleet needs the overseer, then print one `EVENT` line: a new pr-watch attention line, a live `--item`'s PR merged, a lane window gone, a lane whose harness exited under a live window, a lane whose account hit its limit with the harness still up, a lane pane at a question prompt, a lane idle at its prompt after a round, or a heartbeat. `--help` | The three waiters share a bounded env-first GitHub auth ladder and exit `3` on hard auth failure — [references/gates.md](references/gates.md). diff --git a/skills/orch/scripts/reconcile-work-items b/skills/orch/scripts/reconcile-work-items new file mode 100755 index 000000000..ae0e9a08e --- /dev/null +++ b/skills/orch/scripts/reconcile-work-items @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# reconcile-work-items — report tracker state the workflows wrote once and +# never re-read. READ-ONLY: prints findings, mutates nothing; the caller +# decides what to fix. +# +# Three checks over the linear skill's cache (offline; sync first for +# freshness): +# container-parked a parent in a non-terminal state whose non-canceled +# children are all Done — the close that only merge-pr § 5 +# performs never ran +# started-stale an In Progress / In Review item untouched for longer +# than the threshold whose branch-named PR is merged (or +# unverifiable) — the item outlived its work +# done-unchecked a Done item whose description still carries unchecked +# `- [ ]` acceptance criteria — a partial-scope PR's +# `Closes` magic word closed it +# +# Exit codes: 0 nothing to reconcile; 1 findings; 2 config/collection error. +# +# RECONCILE_STALE_HOURS started-stale threshold (default 24) +# RECONCILE_GH_CLI PR-probe command (default gh); absent/failing gh +# degrades that item to "PR state unverified", never +# to silence +set -euo pipefail + +config_error() { + echo "::error::reconcile-work-items: $*" >&2 + exit 2 +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || config_error "not inside a git repository" +# shellcheck source=lib/vstack-env.sh +source "$SCRIPT_DIR/lib/vstack-env.sh" +vstack_load_project_env "$REPO_ROOT" + +STALE_HOURS="${RECONCILE_STALE_HOURS:-24}" +case "$STALE_HOURS" in + "" | *[!0-9]* | 0*[0-9]) config_error "RECONCILE_STALE_HOURS must be a non-negative integer, got '$STALE_HOURS'" ;; +esac +GH_CLI="${RECONCILE_GH_CLI:-gh}" + +command -v jq >/dev/null 2>&1 || config_error "jq is required" + +CACHE="$REPO_ROOT/.cache/linear/issues.json" +[ -f "$CACHE" ] || config_error "no linear cache at $CACHE — run linear.sh sync first" +# Structural validation up front: an array of rows missing the fields the +# scans key on would otherwise be skipped silently and report clean. +jq -e 'type == "array" and all(.[]; + type == "object" + and (.identifier | type == "string") + and (.state | type == "object") + and (.state.name | type == "string") + and (.state.type | type == "string"))' "$CACHE" >/dev/null 2>&1 \ + || config_error "linear cache at $CACHE is not an array of issue rows with identifier and state.name/state.type" + +findings=0 +note() { findings=$((findings + 1)); printf '%s\n' "$1"; } + +# --- container-parked -------------------------------------------------------- +# Non-terminal parents whose non-canceled children (by parent identifier) are +# all completed. Trashed/archived rows never count as children or parents. +TMPD="$(mktemp -d)" || config_error "could not create a scratch directory" +trap 'rm -rf -- "$TMPD"' EXIT + +jq -r ' + map(select((.trashed // false) == false and .archivedAt == null)) as $live + | ($live | map(select(.parent.identifier != null))) as $children + | ($children | group_by(.parent.identifier)) as $broods + | $broods[] + | .[0].parent.identifier as $pid + | (map(select(.state.type != "canceled"))) as $countable + | select(($countable | length) > 0 and ($countable | all(.state.type == "completed"))) + | ($live[] | select(.identifier == $pid)) as $parent + | select($parent.state.type != "completed" and $parent.state.type != "canceled") + # A "(one PR)" root is a single-PR bundle, not a container: its children go + # Done while the root stays open until ITS PR merges, so an open root with + # Done children is that contract working, not a parked container. + | select($parent.title | test("\\(one PR\\)"; "i") | not) + | [$parent.identifier, $parent.title, $parent.state.name] | @tsv +' "$CACHE" >"$TMPD/containers" || config_error "container scan failed" +while IFS=$'\t' read -r pid ptitle pstate; do + [ -n "$pid" ] || continue + note "container-parked: $pid [$pstate] — every non-canceled child is Done; close it or say why it stays open ($ptitle)" +done <"$TMPD/containers" + +# --- started-stale ----------------------------------------------------------- +now_epoch="$(date -u +%s)" || config_error "could not read the clock" +jq -r ' + .[] | select((.trashed // false) == false and .archivedAt == null) + | select(.state.type == "started") + | [.identifier, .title, .state.name, .updatedAt] | @tsv +' "$CACHE" >"$TMPD/started" || config_error "started scan failed" +while IFS=$'\t' read -r iid ititle istate iupdated; do + [ -n "$iid" ] || continue + updated_epoch="$(date -u -d "$iupdated" +%s 2>/dev/null)" || updated_epoch="" + if [ -z "$updated_epoch" ]; then + # BSD date has no -d; parse the ISO stamp with -j -f (fractional part + # and zone dropped — whole-second precision is plenty for an hours + # threshold). + trimmed="${iupdated%%.*}" + trimmed="${trimmed%Z}" + updated_epoch="$(date -j -u -f "%Y-%m-%dT%H:%M:%S" "$trimmed" +%s 2>/dev/null)" || updated_epoch="" + fi + # A timestamp neither form can read must never silently shrink the check. + [ -n "$updated_epoch" ] || config_error "could not parse updatedAt '$iupdated' for $iid — the started-stale check cannot run" + age_hours=$(((now_epoch - updated_epoch) / 3600)) + [ "$age_hours" -ge "$STALE_HOURS" ] || continue + branch="$(printf '%s' "$iid" | tr '[:upper:]' '[:lower:]')" + pr_state="unverified" + if command -v "${GH_CLI%% *}" >/dev/null 2>&1; then + # This repo's branch only — a fork's same-named branch is someone + # else's PR and must not vouch for this item. + # The probes answer for THIS checkout: an inherited GH_REPO / + # GITHUB_REPOSITORY redirect would let another repository's same-named + # branch vouch for this item. + merged="$(env -u GH_REPO -u GITHUB_REPOSITORY $GH_CLI pr list --state merged --head "$branch" --json number,isCrossRepository --jq '[.[] | select(.isCrossRepository | not)] | length' 2>/dev/null)" || merged="" + open="$(env -u GH_REPO -u GITHUB_REPOSITORY $GH_CLI pr list --state open --head "$branch" --json number,isCrossRepository --jq '[.[] | select(.isCrossRepository | not)] | length' 2>/dev/null)" || open="" + if [ -n "$merged" ] && [ -n "$open" ]; then + if [ "$open" -gt 0 ]; then + continue # a live PR is a live item, whatever the clock says + elif [ "$merged" -gt 0 ]; then + pr_state="PR merged" + else + pr_state="no PR" + fi + fi + fi + note "started-stale: $iid [$istate] — untouched ${age_hours}h, $pr_state; finish the lifecycle or restate the scope ($ititle)" +done <"$TMPD/started" + +# --- done-unchecked ---------------------------------------------------------- +jq -r ' + .[] | select((.trashed // false) == false and .archivedAt == null) + | select(.state.type == "completed") + | select((.description // "") | test("- \\[ \\]")) + | [.identifier, .title] | @tsv +' "$CACHE" >"$TMPD/done" || config_error "done scan failed" +while IFS=$'\t' read -r iid ititle; do + [ -n "$iid" ] || continue + note "done-unchecked: $iid [Done] — the description still carries unchecked \`- [ ]\` acceptance criteria; reopen or check them off with evidence ($ititle)" +done <"$TMPD/done" + +if [ "$findings" -gt 0 ]; then + echo "reconcile-work-items: $findings finding(s) — the tracker disagrees with the work" + exit 1 +fi +echo "reconcile-work-items: clean — every item's state matches its work" diff --git a/skills/orch/tests/reconcile-work-items.test.sh b/skills/orch/tests/reconcile-work-items.test.sh new file mode 100755 index 000000000..9417475b8 --- /dev/null +++ b/skills/orch/tests/reconcile-work-items.test.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +# Pins for reconcile-work-items (vstack #1388 / VST-318): the read-only sweep +# reports the three write-without-read-back shapes and stays quiet on their +# healthy twins. Fully offline: fixture cache + stubbed PR probe. +set -euo pipefail + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "$TEST_DIR/.." && pwd)" +RW="$SKILL_DIR/scripts/reconcile-work-items" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +PASS=0 +FAIL=0 +ok() { PASS=$((PASS + 1)); printf ' ok %s\n' "$1"; } +bad() { FAIL=$((FAIL + 1)); printf ' FAIL %s\n %s\n' "$1" "${2:-}"; } + +R="$TMP/repo" +mkdir -p "$R/.cache/linear" +git -C "$R" init -q -b main 2>/dev/null || git -C "$R" init -q + +now="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" +old="$(date -u -d '3 days ago' +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -j -u -v-3d +%Y-%m-%dT%H:%M:%S.000Z)" + +issue() { # ID TITLE STATE_NAME STATE_TYPE UPDATED [PARENT] [DESC] + local parent="null" + [ -n "${6:-}" ] && parent="{\"identifier\":\"$6\"}" + jq -cn --arg id "$1" --arg t "$2" --arg sn "$3" --arg st "$4" --arg up "$5" --argjson p "$parent" --arg d "${7:-}" \ + '{identifier:$id, title:$t, state:{name:$sn,type:$st}, updatedAt:$up, parent:$p, description:$d, trashed:false, archivedAt:null}' +} + +{ + issue "T-1" "parked container" "Todo" "unstarted" "$now" + issue "T-2" "done child a" "Done" "completed" "$now" "T-1" + issue "T-3" "done child b" "Done" "completed" "$now" "T-1" + issue "T-4" "canceled child" "Canceled" "canceled" "$now" "T-1" + issue "T-5" "healthy container" "Todo" "unstarted" "$now" + issue "T-6" "done child" "Done" "completed" "$now" "T-5" + issue "T-7" "pending child" "In Progress" "started" "$now" "T-5" + issue "T-8" "closed container" "Done" "completed" "$now" + issue "T-9" "done child of closed" "Done" "completed" "$now" "T-8" + issue "T-10" "stale started merged" "In Review" "started" "$old" + issue "T-11" "fresh started" "In Progress" "started" "$now" + issue "T-12" "stale started live pr" "In Progress" "started" "$old" + issue "T-13" "done with open boxes" "Done" "completed" "$now" "" "did:\n- [x] one\n- [ ] two" + issue "T-14" "done all checked" "Done" "completed" "$now" "" "did:\n- [x] one\n- [x] two" + issue "T-15" "trashed parked" "Todo" "unstarted" "$now" + issue "T-16" "ship the widget (One PR)" "In Review" "started" "$now" + issue "T-17" "done bundle child" "Done" "completed" "$now" "T-16" +} | jq -s 'map(if .identifier == "T-15" then .trashed = true else . end)' >"$R/.cache/linear/issues.json" + +cat >"$TMP/gh-stub" <<'STUB' +#!/usr/bin/env bash +# args: pr list --state STATE --head BRANCH --json number --jq length +# A leaked repo redirect must never reach the probe. +if [ -n "${GH_REPO:-}" ] || [ -n "${GITHUB_REPOSITORY:-}" ]; then + echo "gh-stub: GH_REPO/GITHUB_REPOSITORY leaked into the probe" >&2 + exit 9 +fi +state=""; head="" +while [ $# -gt 0 ]; do + case "$1" in + --state) state="$2"; shift ;; + --head) head="$2"; shift ;; + esac + shift +done +case "$head:$state" in + t-10:merged) echo 1 ;; + t-12:open) echo 1 ;; + *) echo 0 ;; +esac +STUB +chmod +x "$TMP/gh-stub" + +OUT=""; RC=0 +OUT="$(cd "$R" && GH_REPO=elsewhere/other GITHUB_REPOSITORY=elsewhere/other RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? + +[ "$RC" -eq 1 ] && ok "findings exit 1" || bad "exit code" "rc=$RC out=$OUT" +case "$OUT" in *"container-parked: T-1"*) ok "the parked container is reported" ;; *) bad "parked container" "$OUT" ;; esac +# A "(one PR)" root with Done children is the single-PR bundle contract +# working, never a parked container. +case "$OUT" in *"container-parked: T-16"*) bad "one-PR bundle flagged as parked" "$OUT" ;; *) ok "a (One PR) bundle root is not container-parked (case-insensitive marker)" ;; esac +case "$OUT" in *"container-parked: T-5"*) bad "healthy container reported" "$OUT" ;; *) ok "a container with a pending child stays quiet" ;; esac +case "$OUT" in *"T-8"*) bad "closed container reported" "$OUT" ;; *) ok "a closed container stays quiet" ;; esac +case "$OUT" in *"started-stale: T-10"*"PR merged"*) ok "the stale started item with a merged PR is reported" ;; *) bad "stale merged" "$OUT" ;; esac +case "$OUT" in *"T-11"*) bad "fresh started reported" "$OUT" ;; *) ok "a fresh started item stays quiet" ;; esac +case "$OUT" in *"T-12"*) bad "live-PR started reported" "$OUT" ;; *) ok "a stale item with a live PR stays quiet" ;; esac +case "$OUT" in *"done-unchecked: T-13"*) ok "the Done item with open boxes is reported" ;; *) bad "done unchecked" "$OUT" ;; esac +case "$OUT" in *"T-14"*) bad "all-checked reported" "$OUT" ;; *) ok "a Done item with every box checked stays quiet" ;; esac +case "$OUT" in *"T-15"*) bad "trashed reported" "$OUT" ;; *) ok "a trashed row stays out of every check" ;; esac + +# Clean fixture: only healthy rows -> exit 0 with the clean line. +jq '[.[] | select(.identifier == "T-5" or .identifier == "T-6" or .identifier == "T-7" or .identifier == "T-14" or .identifier == "T-11")]' \ + "$R/.cache/linear/issues.json" >"$R/.cache/linear/issues2.json" +mv "$R/.cache/linear/issues2.json" "$R/.cache/linear/issues.json" +OUT=""; RC=0 +OUT="$(cd "$R" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? +[ "$RC" -eq 0 ] && case "$OUT" in *"clean"*) true ;; *) false ;; esac \ + && ok "a healthy tracker exits 0 with the clean line" || bad "clean run" "rc=$RC out=$OUT" + +# A malformed row inside an array-shaped cache: the scan must die loudly, +# never end early as a clean pass. +printf '[{"identifier":"T-BAD"}, 42]' >"$R/.cache/linear/issues.json" +OUT=""; RC=0 +OUT="$(cd "$R" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? +[ "$RC" -eq 2 ] && ok "a malformed cache row is a loud collection error" || bad "malformed row" "rc=$RC out=$OUT" + +# Object-shaped but incomplete rows must not read as a clean tracker: a row +# without identifier/state carries nothing the scans can inspect. +printf '[{}]' >"$R/.cache/linear/issues.json" +OUT=""; RC=0 +OUT="$(cd "$R" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? +[ "$RC" -eq 2 ] && ok "an empty-object row is a config error, never clean" || bad "empty-object row" "rc=$RC out=$OUT" +printf '[{"identifier":"T-1","state":{"name":"Todo"}}]' >"$R/.cache/linear/issues.json" +OUT=""; RC=0 +OUT="$(cd "$R" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? +[ "$RC" -eq 2 ] && ok "a row missing state.type is a config error" || bad "missing state.type" "rc=$RC out=$OUT" + +# Missing cache: loud config error, never a clean pass. +rm "$R/.cache/linear/issues.json" +OUT=""; RC=0 +OUT="$(cd "$R" && "$RW" 2>&1)" || RC=$? +[ "$RC" -eq 2 ] && ok "a missing cache is a config error, never clean" || bad "missing cache" "rc=$RC out=$OUT" + +# --- settings-file threshold ------------------------------------------------- +# RECONCILE_STALE_HOURS set in the project's vstack.settings.toml (not the +# environment) must reach the sweep: a 2h-old In Progress item is quiet at the +# 24h default and a finding at a 1h threshold. +R2="$TMP/settings-repo" +mkdir -p "$R2/.cache/linear" +git -C "$R2" init -q +TWO_H_AGO="$(date -u -d '2 hours ago' '+%Y-%m-%dT%H:%M:%S.000Z' 2>/dev/null || date -j -u -v-2H '+%Y-%m-%dT%H:%M:%S.000Z')" +cat >"$R2/.cache/linear/issues.json" <&1)" || RC=$? +[ "$RC" -eq 0 ] && ok "default 24h threshold stays quiet at 2h" || bad "default threshold" "rc=$RC out=$OUT" +printf '[env]\nRECONCILE_STALE_HOURS = "1"\n' >"$R2/vstack.settings.toml" +RC=0 +OUT="$(cd "$R2" && RECONCILE_GH_CLI="$TMP/gh-stub" "$RW" 2>&1)" || RC=$? +{ [ "$RC" -eq 1 ] && printf '%s' "$OUT" | grep -q "VST-900"; } && ok "settings-file RECONCILE_STALE_HOURS reaches the sweep" || bad "settings-file threshold" "rc=$RC out=$OUT" + +printf '\n%s passed, %s failed\n' "$PASS" "$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/skills/orch/vstack.settings.toml.example b/skills/orch/vstack.settings.toml.example index c3bb39498..3e07c9677 100644 --- a/skills/orch/vstack.settings.toml.example +++ b/skills/orch/vstack.settings.toml.example @@ -80,3 +80,7 @@ ORCH_TMUX_VERIFY_SECS = "15" # Max concurrent lanes the overseer keeps in flight. ORCH_OVERSEER_LANES = "3" + +# Hours before an In Progress / In Review item counts as started-stale in +# reconcile-work-items sweeps. +# RECONCILE_STALE_HOURS = "24" diff --git a/skills/orch/workflows/oversee.md b/skills/orch/workflows/oversee.md index 7de55944e..3fd35887b 100644 --- a/skills/orch/workflows/oversee.md +++ b/skills/orch/workflows/oversee.md @@ -42,7 +42,7 @@ One blocking command, passed the fleet's start as `--since` (the first lane's `l .agents/skills/orch/scripts/oversee-watch --interval 240 --since [FLEET_SINCE] --item [ISSUE_ID]... [LANE_WINDOW...] ``` -- `merged` → mark the lane done, launch the next unblocked item. +- `merged` → mark the lane done, launch the next unblocked item. When the fleet's LAST item merges and the items live in Linear (the sweep reads the linear skill's cache; a GitHub-item fleet skips it with a note), run `.agents/skills/orch/scripts/reconcile-work-items` before closing out — a parked container or a Done item with open acceptance boxes is exactly what this pass ends with when a close step was skipped; report its findings with the close-out. - `lane-exited` → the window is alive but the harness under it is gone (its pane tail says why); a lane stopped by a harness session limit ("You've hit your session limit · resets HH:MM") is not dead: resume it under another auth lane (`lanes pick`; a Claude session resumed under a different `CLAUDE_CONFIG_DIR` needs its session files copied there), or wait for the shown reset and send the lane a one-line continuation nudge. Any other exit is the `window-gone` rule below. - `usage-limit` → read the pane tail first and confirm the banner is the harness's own, not text the lane printed while working; a lane still working is left alone. Confirmed, the account is spent and every lane on it is about to stall: the event line names the config dir when a live lane claim maps the window to one. Move the item to another account lane (kill the window, then re-run the § 3 launch with `--relaunch --lane auto:[HARNESS]` — bare `auto` needs `--harness`), or wait for the reset the banner names and send the lane a one-line continuation nudge. Launching onto the spent account again is not an option until its reset. - `idle-after-return` → the lane is at its prompt with nothing running: read its pane tail, then send the next instruction, or — when its PR is merged and its worktree is gone — close the lane and launch the next item. diff --git a/skills/project-management/workflows/audit-issues.md b/skills/project-management/workflows/audit-issues.md index f051d82d5..475c23631 100644 --- a/skills/project-management/workflows/audit-issues.md +++ b/skills/project-management/workflows/audit-issues.md @@ -52,8 +52,20 @@ Store as `TRACKER`, plus `[OWNER/REPO]` when `TRACKER=github`. .agents/skills/linear/scripts/linear.sh sync --reconcile .agents/skills/linear/scripts/linear.sh session-status .agents/skills/linear/scripts/linear.sh cache labels list --format=safe +.agents/skills/orch/scripts/reconcile-work-items ``` +Run `reconcile-work-items` only where the orch skill is installed (skip the +line otherwise — this workflow does not require orch). It is read-only: +exit 0 is a clean tracker, exit 1 is findings — carry them into the audit +as facts — and exit 2 is a broken sweep to fix before auditing. An audit +decision taken against a parked container's stale state is the failure +this line exists to prevent. +names tracker rows whose state no longer matches the work: parked containers, +stale started items, Done items with unchecked acceptance boxes. Carry its +findings into the audit as facts — an audit decision taken against a parked +container's stale state is the failure this line exists to prevent. + Keep `project` for fallback target resolution and the issue-label inventory for every create/update preflight. #### 1.2.3 Preflight — GitHub (TRACKER=github)