action: close

reason: All sprint sub-issues #437–#454 are closed and merged. The blast-radius approval tiers vision goal is complete.


reason: Resolved by merged sub-issues: #502 (load-project.sh derives container repo paths) and #503 (entrypoint.sh workaround removed). Both PRs merged.


reason: Obsolete: issue assumed #379 (while-true loop) was not yet deployed, but #379 was merged and closed on 2026-04-08. The DISINTO_CONTAINER guard in lib/env.sh is now correct — compose env vars flow through the while-true loop process tree, so .env should NOT be sourced inside containers.


## Problem

`has_responses_to_process` is only set to `true` inside the `open_arch_prs >= 3` gate in `architect/architect-run.sh` (line 521). When fewer than 3 architect PRs are open, ACCEPT/REJECT responses on existing PRs are never processed — the response-processing block at line 688 defaults to `false` and is skipped entirely.

This means that if a user ACCEPTs or REJECTs a pitch while the open PR count is below 3, the architect agent will never handle the response.

## Fix

Move the ACCEPT/REJECT response scan outside the `open_arch_prs >= 3` gate. Set `has_responses_to_process=true` unconditionally whenever any open architect PRs have ACCEPT/REJECT comments — not only when the 3-PR cap is hit.

## Affected files

- `architect/architect-run.sh` (lines 521–541: the `>= 3` PR gate and response scan block; line 688: the response-processing block)

## Acceptance criteria

- [ ] ACCEPT/REJECT comments on architect PRs are processed regardless of how many PRs are open
- [ ] The `>= 3` early-exit guard still works for the "no responses, too many PRs" case (skip pitching)
- [ ] A test with 1 open architect PR that has an ACCEPT comment results in the architect processing it
- [ ] Existing architect formula step ordering is preserved

---
*Flagged by AI reviewer in PR #496*


## Problem

In `architect/architect-run.sh` line 203, the `has_open_subissues` function compares `.number` (a JSON integer) against `$vid` (a bash string via `--arg`). In jq, `42 != "42"` evaluates to `true` (different types are never equal), so the self-exclusion filter never fires. The self-exclusion logic is silently broken — though low-risk in practice since vision issues don't contain 'Decomposed from #N' in their own bodies.

## Fix

Cast the string to a number in jq:

```jq
select(.number != ($vid | tonumber))
```

## Affected files

- `architect/architect-run.sh` (line 203: `select(.number != $vid)` in the `has_open_subissues` function)

## Acceptance criteria

- [ ] `select(.number != ($vid | tonumber))` replaces `select(.number != $vid)` at line 203
- [ ] ShellCheck passes (the jq expression is in a heredoc/string, not inline shell)
- [ ] `has_open_subissues` correctly excludes the vision issue itself when checking for open sub-issues

---
*Flagged by AI reviewer in PR #496*


## Bug description

When dev-bot picks a backlog issue and launches dev-agent.sh, a second dev-poll instance (dev-qwen) can race ahead and mark the issue as stale/blocked before dev-agent.sh finishes claiming it.

## Reproduction

Observed on issues #443 and #445 (2026-04-08):

**#443 timeline:**
- `20:39:03` — dev-bot removes `backlog`, adds `in-progress` (via dev-poll backlog pickup)
- `20:39:04` — dev-qwen removes `in-progress`, adds `blocked` with reason `no_assignee_no_open_pr_no_lock`
- `20:40:11` — dev-bot pushes commit (dev-agent was actually working the whole time)
- `20:44:02` — PR merged, issue closed

**#445 timeline:**
- `20:54:03` — dev-bot adds `in-progress`
- `20:54:06` — dev-qwen marks `blocked` (3 seconds later)
- `20:55:13` — dev-bot pushes commit
- `21:09:03` — PR merged, issue closed

In both cases, the work completed successfully despite being labeled blocked.

## Root cause

`issue_claim()` in `lib/issue-lifecycle.sh` performs three sequential API calls:
1. PATCH assignee
2. POST in-progress label
3. DELETE backlog label

Meanwhile, dev-poll on another agent (dev-qwen) runs its orphan scan, sees the issue labeled `in-progress` but with no assignee set yet (assign PATCH hasn't landed or was read stale), no open PR, and no lock file. It concludes the issue is stale and relabels to `blocked`.

The race window is ~1-3 seconds between in-progress being set and the assignee being visible to other pollers.

## Affected files

- `lib/issue-lifecycle.sh` — `issue_claim()` function: reorder API calls to set assignee before adding in-progress label
- `dev/dev-poll.sh` — orphan/stale detection: add grace period (e.g. skip issues whose in-progress label was added < 30s ago via label event timestamp)

## Acceptance criteria

- [ ] `issue_claim()` sets assignee before adding `in-progress` label, OR writes lock file before any label changes
- [ ] Stale detection in dev-poll skips issues where in-progress was added less than 30 seconds ago
- [ ] No spurious `blocked` labels appear on freshly claimed issues in integration test scenario
- [ ] Existing `issue_claim()` behavior is preserved for the happy path (single dev-poll instance)