feat(issue-tracker): surface topology — contested seams as first-class board objects - #54
Conversation
…ne-entry re-match (M2)
…stop, consolidation watch (M4)
… execplan progress (M5)
…gate, lifecycle dedupe, rename matching All 8 findings adopted: - per-surface mkdir locks (DAEMON_HOME/surface-locks/) held from the occupancy check through the spawn; the sweep's relates RMWs take the same lock — one mechanism closes both the cross-process double-dispatch window and the relates-vs-dispatch TOCTOU [P1 x2] - occupancy re-checked UNDER the lock (fresh snapshot helper), not exported from the pre-lock ticket read - surfaces_registry() best-effort-fetches the default branch first — a remotely merged registry entry reaches long-lived clones [P1] - consolidation dedupe covers the ticket's whole open lifecycle: arch states before decompose, epic-with-children after [P1] - no registry → surface labels ignored by dispatch too (T_SURFACES gated) — leftover labels cannot queue work forever [P2] - consolidation label rides the register call via --surface (atomic with create; no window without the dedupe key) [P2] - PR files read via REST pulls/N/files with previous_filename — renames out of a surface still label [P2] - relates edges checked and repaired per SIDE — one-sided edges from a crashed tick converge [P2] - plus the #52 finding-5 code twin: queue-depth members count parks (only deferred/terminal/spike/epic/architect are out)
…elate locking, paginated diffs - the surface lock now survives until board-bind writes the ticket field (a spawn-time meta is unbound — the registry arm cannot see it, so the spawn-time release reopened the cross-process window) [P1] - register-time relate RMWs take the same per-surface locks and re-check liveness under them (issue-event dispatch can bind a worker to the new ticket within seconds); contention defers the edge to the sweep [P1] - PR file reads use --paginate --slurp with page flattening — a multi-page diff was concatenated JSON arrays that json.loads rejected, silently skipping the PR's labels (sweep + lint) [P1] - the --surface no-registry notices go to stderr, keeping the '<number> <url>' first-line stdout contract [P2] - lock contention under SURFACE_OVERRIDE=1 (or the architect lane) logs the unlocked bypass explicitly [P2]
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d03db92ae7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if [ -n "${T_SURFACES:-}" ] && [ "$role" != "SPIKE" ]; then | ||
| if _surf_lock "$T_SURFACES"; then | ||
| surf_locked="$T_SURFACES" | ||
| occ="$(_surface_occupant "$n" "$T_SURFACES" "${DISPATCHED_SURFACES:-}")" |
There was a problem hiding this comment.
Fail closed when the occupancy lookup errors
When the under-lock snapshot fails because of a transient GitHub/API error, this command's exit status is unchecked. In sweep mode dispatch_one is called behind ||, so errexit is disabled and occ is treated as empty, allowing a worker to spawn without proving the surface is free; in triggered mode the script exits while leaving the surface lock behind until its stale timeout. Handle the nonzero status explicitly, release the lock, and leave the ticket queued.
Useful? React with 👍 / 👎.
| if not os.environ.get("SURFACES_REF") and "/" in ref: | ||
| remote, _, branch = ref.partition("/") | ||
| try: | ||
| subprocess.run(["git", "fetch", "--quiet", remote, branch], | ||
| capture_output=True, timeout=30, check=False) |
There was a problem hiding this comment.
Avoid fetching the default branch for every ticket
In a registry-enabled repo this unconditional fetch runs once per Python process, while the cache below is only process-local. A dispatch sweep starts fresh Python processes for each ticket's _ticket_exports and again for each _surface_occupant, so a board with N eligible surface tickets can perform roughly 2N fetches; a slow or unreachable remote can consume the full 30-second timeout each time and stall a nominal five-minute sweep for many minutes. Refresh once per outer tick/process group, or gate refreshes with a shared TTL.
Useful? React with 👍 / 👎.
| if c == "*": | ||
| if pat[i:i + 2] == "**": | ||
| out.append(".*") | ||
| i += 2 |
There was a problem hiding this comment.
Let globstar cover zero nested directories
The documented ** path glob fails to match zero directory levels because the generated .* is surrounded by both literal slashes. For example, src/**/*.py matches src/pkg/a.py but not src/a.py, so a valid surface pattern silently misses files directly under the named directory and those tickets remain unlabeled and unserialized. Treat **/ as matching zero or more complete path segments.
Useful? React with 👍 / 👎.
| # architect lane (the resolver) are out. | ||
| ARCH_STATES = ("ready-for-architect", "in-design") | ||
| OUT_STATES = B.TERMINAL + ARCH_STATES + ("deferred",) | ||
| for s in sorted({x for n in tickets.values() for x in n["surfaces"]}): |
There was a problem hiding this comment.
Ignore orphaned surfaces in the consolidation pass
This loop considers every surface:* label present on tickets rather than restricting itself to names in reg. If three open tickets carry an invented label, or a registry entry is deleted before its carriers are cleared, the sweep emits CONSOLIDATE for that orphan; board-register --surface cannot apply the nonexistent surface label to the new architect ticket, so the structural dedupe never becomes visible and another consolidation issue is created on every tick. Filter queue-depth processing to registered surface names.
Useful? React with 👍 / 👎.
…fetch stamp, lane-scoped spike exemption - SURFACE pass (relates + queue-depth) acts only on names present in the registry: an orphaned label reaching CONSOLIDATE registered with a --surface hint that matched nothing — an unlabeled consolidation the structural dedupe never saw, duplicated every tick [P1] - surfaces_registry() fetch behind a cross-process stamp (SURFACES_FETCH_TTL, default 300s): a dispatch sweep runs a fresh python per ticket, so N labeled tickets meant N fetches per tick [P2] - spike occupancy exemption is lane-scoped: a spike-category ticket in ready-for-architect/in-design routes ARCHITECT (state outranks category) and its design run occupies like any architect's [P2]
…pwire text + surfaces cross-ref
Implements the approved spec
docs/doperpowers/specs/2026-08-10-surface-topology-design.md(v2). ExecPlan with full history:docs/doperpowers/execplans/2026-08-10-surface-topology.md.What
A consumer repo can declare contested code seams in
.doperpowers/surfaces.md(read from the default branch, freshly fetched). The board then:surface:<name>at three moments — registration (identifiers +--surfacehints, label rides the create call), lane-entry transition (re-match of the current body), and the sweep's new SURFACE pass (open linked PR diffs via RESTpulls/N/files, renames included; add-only);board-bind(occupancy = board states ∪ bound registry metas ∪ in-tick claims; architect occupies but is never blocked; spikes neither; epics excluded;SURFACE_OVERRIDE=1bypasses loudly);ready-for-architect) when ≥3 members pile onto one surface (parks count,deferreddoesn't) — structural dedupe via the label across the ticket's whole open lifecycle;surface:*= FAIL, FIX names the newboard-surface.shverb), registry names, declaration drift, and per-surface queue depth.Opt-in inertness is a hard requirement: no
surfaces.md→ zero behavior change (leftover labels included), guarded by tests.Verification
tests/issue-tracker/test-board-surface.sh— new, 38 assertions (spec acceptance 1–9)tests/implementing/test-implement-dispatch.sh— +14 surface assertionstests/issue-tracker/test-board-scripts.sh,test-board-sweep.sh,tests/claude-code/board-api/test-register-transition.sh— pass unchangedscripts/lint-shell.shclean on the changed set--slurppagination, registry fetch, lifecycle dedupe, registry gating, rename matching, per-side relates repair, stderr notices, contention logging — commits96e98d10,6e484dda)Out of scope (follow-ups)
surfaces.mdseed (3 audited clusters) + one-shot migration (spec acceptance 10)