Skip to content

Commit 7f161c3

Browse files
fix(dso-6x8o): change stale test-gate-status guard from reject to clear-and-rerun (merge worktree-20260321-170257)
2 parents 944fa01 + 741bda1 commit 7f161c3

File tree

16 files changed

+408
-14
lines changed

16 files changed

+408
-14
lines changed

.tickets/.index.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@
714714
"w20-4idh": {
715715
"deps": [],
716716
"priority": 1,
717-
"status": "open",
717+
"status": "closed",
718718
"title": "test-batched.sh state file not isolated by repo/worktree \u2014 causes cross-session interference",
719719
"type": "bug"
720720
},
@@ -805,7 +805,7 @@
805805
"w21-3w8y": {
806806
"deps": [],
807807
"priority": 1,
808-
"status": "open",
808+
"status": "closed",
809809
"title": "Preplanning adds epic's own children as deps, making the epic self-blocked",
810810
"type": "bug"
811811
},

.tickets/dso-2dxt.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
---
22
id: dso-2dxt
3-
status: open
3+
status: in_progress
44
deps: []
55
links: []
66
created: 2026-03-21T20:32:23Z
77
type: task
88
priority: 2
99
assignee: Joe Oakhart
10+
parent: w22-ns6l
1011
---
1112
# bug LINK/UNLINK same-second timestamp ordering causes unlink to be ignored
1213

@@ -19,3 +20,7 @@ assignee: Joe Oakhart
1920
<!-- sync: unsynced -->
2021

2122
Discovered in test-ticket-dependency-e2e.sh (dso-ofdp). When ticket-graph.py writes a LINK event and ticket-link.sh writes an UNLINK event in the same Unix second (int(time.time())), the filename sort order is {timestamp}-{uuid}-LINK.json vs {timestamp}-{uuid}-UNLINK.json. Since UUIDs are random, the UNLINK can sort before the LINK alphabetically, causing the event replay in _is_duplicate_link and _find_direct_blockers to treat the link as still active. Fix: use millisecond timestamps or add a tie-breaker suffix to guarantee UNLINK always sorts after LINK in the same second.
23+
24+
**2026-03-22T00:35:13Z**
25+
26+
Classification: behavioral, Score: 2 (BASIC). Root cause: event filename sort key used only the full basename, so when LINK and UNLINK events share the same Unix-second timestamp but have different random UUID segments, alphabetical UUID sort could place UNLINK before LINK. Fix: changed sort key to (timestamp_only, event_type_order, full_name) in both _is_duplicate_link and _get_link_info — ensures LINK always replays before UNLINK at same second. Added Test 9 to test-ticket-link.sh to reproduce the bug deterministically using crafted filenames with controlled UUID ordering.

.tickets/dso-6x8o.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
id: dso-6x8o
3-
status: open
3+
status: closed
44
deps: []
55
links: []
66
created: 2026-03-22T00:00:28Z

.tickets/dso-gg0v.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
id: dso-gg0v
3+
status: open
4+
deps: []
5+
links: []
6+
created: 2026-03-22T00:33:09Z
7+
type: bug
8+
priority: 2
9+
assignee: Joe Oakhart
10+
tags: [agent-compliance, debug-everything]
11+
---
12+
# debug-everything validation prompts use soft "Do NOT fix" language instead of hard-stop READ-ONLY ENFORCEMENT
13+
14+
## Bug
15+
16+
The following debug-everything validation sub-agent prompts use soft "Do NOT fix" imperative language instead of the hard-stop READ-ONLY ENFORCEMENT section pattern fixed in w20-w7pm:
17+
18+
- plugins/dso/skills/debug-everything/prompts/full-validation.md
19+
- plugins/dso/skills/debug-everything/prompts/post-batch-validation.md
20+
- plugins/dso/skills/debug-everything/prompts/tier-transition-validation.md
21+
- plugins/dso/skills/debug-everything/prompts/critic-review.md
22+
- plugins/dso/skills/debug-everything/prompts/diagnostic-and-cluster.md
23+
24+
## Expected Behavior
25+
26+
Each validation/diagnostic prompt should have a dedicated ## READ-ONLY ENFORCEMENT section that explicitly names the Edit tool, Write tool, and file-modifying Bash commands as PROHIBITED — matching the pattern implemented in w20-w7pm for validate-work prompts.
27+
28+
## Discovered by
29+
30+
Bug w20-w7pm anti-pattern search (CLAUDE.md rule 9).

.tickets/w20-4idh.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
id: w20-4idh
3-
status: open
3+
status: closed
44
deps: []
55
links: []
66
created: 2026-03-21T16:04:19Z
@@ -33,3 +33,12 @@ Search for similar patterns of non-isolated /tmp file usage across the codebase.
3333
- tests/scripts/test-batched-state-integrity.sh (update tests)
3434
- Potentially other scripts found during audit
3535

36+
37+
## Notes
38+
39+
<!-- note-id: u0s49rck -->
40+
<!-- timestamp: 2026-03-22T01:04:02Z -->
41+
<!-- origin: agent -->
42+
<!-- sync: unsynced -->
43+
44+
CLOSE REASON: Fixed: derive default state file path using SHA256 hash of git root (12 hex chars) — df51136. 49/49 tests passing.

.tickets/w21-3w8y.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
id: w21-3w8y
3-
status: open
3+
status: closed
44
deps: []
55
links: []
66
created: 2026-03-20T18:57:23Z
@@ -35,3 +35,12 @@ Children are added to `deps`, causing the epic to self-block. `sprint-list-epics
3535

3636
- `plugins/dso/skills/preplanning/` — likely where `tk dep` is called to wire children as blockers
3737
- `plugins/dso/scripts/sprint-list-epics.sh` — could also filter out self-child deps when computing blocked status
38+
39+
## Notes
40+
41+
<!-- note-id: s4dz9l0o -->
42+
<!-- timestamp: 2026-03-22T01:05:02Z -->
43+
<!-- origin: agent -->
44+
<!-- sync: unsynced -->
45+
46+
CLOSE REASON: Fixed: sprint-list-epics.sh now filters child deps from blocked computation via external_deps; tk index stores parent field in _tk_build_full_index and _update_ticket_index; preplanning/SKILL.md guardrail added against tk dep epic-id story-id. Committed in df51136.

plugins/dso/hooks/record-test-status.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,22 @@ fi
204204
# alter the untracked file list and produce a different hash.
205205
DIFF_HASH=$("$HOOK_DIR/compute-diff-hash.sh")
206206

207+
# --- Guard: clear stale status when code changed since last recorded test run ---
208+
# If an existing 'passed' status was recorded for a DIFFERENT hash, clear it so
209+
# the test loop below re-runs tests against the current code (dso-6x8o).
210+
_EXISTING_STATUS_FILE="$ARTIFACTS_DIR/test-gate-status"
211+
if [[ -f "$_EXISTING_STATUS_FILE" ]]; then
212+
_EXISTING_STATUS=$(head -1 "$_EXISTING_STATUS_FILE" 2>/dev/null || echo "")
213+
_EXISTING_HASH=$(grep '^diff_hash=' "$_EXISTING_STATUS_FILE" 2>/dev/null | head -1 | cut -d= -f2 || echo "")
214+
if [[ "$_EXISTING_STATUS" == "passed" ]] && [[ -n "$_EXISTING_HASH" ]] && [[ "$_EXISTING_HASH" != "$DIFF_HASH" ]]; then
215+
echo "WARNING: stale test-gate-status cleared — re-running tests for current hash." >&2
216+
echo " Previously passed hash: ${_EXISTING_HASH:0:12}..." >&2
217+
echo " Current diff hash: ${DIFF_HASH:0:12}..." >&2
218+
rm -f "$_EXISTING_STATUS_FILE"
219+
fi
220+
fi
221+
222+
207223
# --- Run associated tests ---
208224
STATUS="passed"
209225
HAD_TIMEOUT=false

plugins/dso/scripts/sprint-list-epics.sh

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,15 @@ for fname in files:
107107
title = line[2:].strip()
108108
break
109109
110+
parent = get_field('parent')
111+
110112
entry = {'title': title, 'status': status, 'type': type_}
111113
if priority is not None:
112114
entry['priority'] = priority
113115
if deps:
114116
entry['deps'] = deps
117+
if parent:
118+
entry['parent'] = parent
115119
idx[ticket_id] = entry
116120
117121
# Write atomically
@@ -215,8 +219,9 @@ try:
215219
except Exception:
216220
child_counts = {}
217221
218-
# Build lookup for dep status resolution
222+
# Build lookup for dep status and parent resolution
219223
dep_status = {tid: entry.get('status', 'open') for tid, entry in index.items()}
224+
dep_parent = {tid: entry.get('parent', '') for tid, entry in index.items()}
220225
221226
in_progress = []
222227
open_unblocked = []
@@ -230,8 +235,11 @@ for tid, entry in index.items():
230235
continue
231236
232237
deps = entry.get('deps', [])
233-
# An epic is blocked if it has at least one dep whose status is not 'closed'
234-
is_blocked = any(dep_status.get(dep, 'open') != 'closed' for dep in deps)
238+
# An epic is blocked only by external deps — exclude deps that are its own children.
239+
# Preplanning may mistakenly add child story IDs to the epic's deps field (bug w21-3w8y).
240+
# Children are identified by having parent == this epic's ID.
241+
external_deps = [dep for dep in deps if dep_parent.get(dep, '') != tid]
242+
is_blocked = any(dep_status.get(dep, 'open') != 'closed' for dep in external_deps)
235243
236244
priority = entry.get('priority', 4)
237245
if priority is None:

plugins/dso/scripts/test-batched.sh

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,27 @@ trap '_signal_handler URG' SIGURG
121121

122122
# ── Constants ─────────────────────────────────────────────────────────────────
123123
DEFAULT_TIMEOUT=50
124-
DEFAULT_STATE_FILE="${TEST_BATCHED_STATE_FILE:-/tmp/test-batched-state.json}"
125124
DEFAULT_STATE_TTL=14400 # 4 hours in seconds
126125

126+
# Derive a repo/worktree-isolated default state file path.
127+
# Uses a hash of the git root directory so each repo and worktree gets its own
128+
# state file, preventing cross-session interference on the same machine.
129+
# Falls back to /tmp/test-batched-state.json when git is unavailable.
130+
_derive_default_state_file() {
131+
local git_root
132+
git_root=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "/tmp/test-batched-state.json"; return; }
133+
local hash
134+
hash=$(echo -n "$git_root" | sha256sum 2>/dev/null | awk '{print $1}' || \
135+
echo -n "$git_root" | python3 -c "import sys,hashlib; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest())" 2>/dev/null)
136+
if [ -n "$hash" ]; then
137+
echo "/tmp/test-batched-state-${hash:0:12}.json"
138+
else
139+
echo "/tmp/test-batched-state.json"
140+
fi
141+
}
142+
143+
DEFAULT_STATE_FILE="${TEST_BATCHED_STATE_FILE:-$(_derive_default_state_file)}"
144+
127145
# ── Argument parsing ──────────────────────────────────────────────────────────
128146
TIMEOUT=$DEFAULT_TIMEOUT
129147
STATE_FILE="$DEFAULT_STATE_FILE"

plugins/dso/scripts/ticket-link.sh

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,13 @@ for f in sorted(p.glob('*-UNLINK.json')):
8585
all_events.append(('UNLINK', f))
8686
8787
# Re-sort by filename (basename) so LINK and UNLINK interleave in timestamp order.
88-
all_events.sort(key=lambda x: x[1].name)
88+
# Sort key: (timestamp, event_type_order, full_name)
89+
# - timestamp (first filename segment) preserves chronological order
90+
# - event_type_order (LINK=0, UNLINK=1) guarantees LINK processes before UNLINK
91+
# when two events share the same second-level timestamp (different random UUIDs)
92+
# - full name as final tiebreaker for stable ordering within same type+timestamp
93+
_event_order = {'LINK': 0, 'UNLINK': 1}
94+
all_events.sort(key=lambda x: (x[1].name.split('-')[0], _event_order.get(x[0], 99), x[1].name))
8995
9096
# Replay events to build net-active link set: maps uuid -> (target_id, relation)
9197
active_links: dict[str, tuple[str, str]] = {}
@@ -207,7 +213,13 @@ for f in sorted(p.glob('*-UNLINK.json')):
207213
all_events.append(('UNLINK', f))
208214
209215
# Re-sort by filename (basename) so LINK and UNLINK interleave in timestamp order.
210-
all_events.sort(key=lambda x: x[1].name)
216+
# Sort key: (timestamp, event_type_order, full_name)
217+
# - timestamp (first filename segment) preserves chronological order
218+
# - event_type_order (LINK=0, UNLINK=1) guarantees LINK processes before UNLINK
219+
# when two events share the same second-level timestamp (different random UUIDs)
220+
# - full name as final tiebreaker for stable ordering within same type+timestamp
221+
_event_order = {'LINK': 0, 'UNLINK': 1}
222+
all_events.sort(key=lambda x: (x[1].name.split('-')[0], _event_order.get(x[0], 99), x[1].name))
211223
212224
# Replay events to build net-active link set: maps uuid -> (target_id, relation)
213225
active_links: dict[str, tuple[str, str]] = {}

0 commit comments

Comments
 (0)