Skip to content

[worktree-20260606-091709] test(delete-e2e): align two stale assertions with current behavior#696

Merged
JoeOakhartNava merged 9 commits into
mainfrom
staged-46b5387451f3-1780771548
Jun 6, 2026
Merged

[worktree-20260606-091709] test(delete-e2e): align two stale assertions with current behavior#696
JoeOakhartNava merged 9 commits into
mainfrom
staged-46b5387451f3-1780771548

Conversation

@joeoakhart

@joeoakhart joeoakhart commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Commits

  • fix(ticket-link): honor --dry-run in canonical ticket_link dispatcher
  • docs(single-agent-integrate): gate local code review on dso.workflow=local
  • fix(pre-commit): raise isolation-check budget 60s -> 120s for large test files
  • fix(ticket-graph): reject non-canonical link relations in add_dependency
  • fix(ticket-link): unlink succeeds after compaction bakes LINK into SNAPSHOT
  • test(delete-e2e): align two stale assertions with current behavior
  • chore: bump version to v1.17.142

Auto-generated by merge-to-main-pr.sh from git log --no-merges origin/main..HEAD.

Summary by CodeRabbit

  • New Features

    • Added workflow gating to integration protocol based on configuration.
    • Implemented canonical relation validation for dependencies.
  • Bug Fixes

    • Fixed dry-run support for ticket linking operations.
    • Enhanced duplicate detection and link retrieval using snapshot fallback.
    • Resolved unlink operations post-compaction.
  • Chores

    • Increased pre-commit isolation check timeout to 120 seconds.
    • Bumped plugin version to 1.17.142.
  • Tests

    • Added comprehensive tests for dry-run, relation validation, and post-compaction scenarios.

Test added 7 commits June 6, 2026 11:57
ticket_link() in ticket-lib-api.sh discarded a --dry-run argument and always
wrote the LINK event via ticket-graph.py --link, emitting no preview. Parse the
position-independent --dry-run flag, strip it from positional args, and when set
delegate to ticket-link.sh's preview path (no write, [DRY RUN] output). The
non-dry-run path is unchanged. Adds a RED test exercising the canonical
dispatcher dry-run path (asserts exit 0, [DRY RUN] preview, zero LINK events).

DSO-Bug: 3796-ccd3-863f-4d63
…local

Clarify that Step 5 (code-reviewer dispatch) and Step 6 (record-review) run ONLY
when dso.workflow=local. Under dso.workflow=ci-pr they are skipped (no-op): CI
enforces the review gate on the session->main PR (cumulative llm-review), and local
enforcement is already deferred. Mirrors REVIEW-WORKFLOW.md's enforcement.strategy=ci
skip path (the legacy alias that dso.workflow=ci-pr superseded; see CLAUDE.md
rule:no-bypass-review).
…est files

check-test-isolation.sh scans the full content of each staged tests/**.py file; on
large files (e.g. test_ticket_graph.py, ~2528 lines) the rule loop takes ~63s,
exceeding the 60s pre-commit budget and killing legitimate commits with exit 124
even though the check itself passes (exit 0). Raise the isolation-check wrapper
timeout to 120s in .pre-commit-config.yaml and the shipped example for headroom on
large files under load.

DSO-Bug: 3a36-470f-fe35-4dd6
add_dependency() in ticket_graph/_links.py wrote any relation string verbatim
(e.g. 'blocked_by'), bypassing the canonical grammar the legacy ticket-link.sh path
enforced. Add a CANONICAL_RELATIONS guard that raises ValueError for unknown
relations before any disk write; ticket-graph.py --link maps ValueError to exit 1.
Adds RED tests in test_ticket_graph.py (uses monkeypatch.setenv for isolation) and
test-ticket-link.sh.

DSO-Bug: 61b8-bb44-fb04-402c
…APSHOT

After ticket-compact.sh bakes LINK events into a SNAPSHOT's compiled_state.deps[]
and deletes the *-LINK.json files, `ticket unlink` failed with "no LINK event
found" because _get_link_info/_is_duplicate_link (ticket-link.sh) and
_is_active_link (ticket_graph/_links.py) only consulted *-LINK.json/*-UNLINK.json.
Add a SNAPSHOT fallback that scans compiled_state.deps[] for the link_uuid (minus
any post-compaction UNLINK cancellations). RED test added: link -> compact
(--threshold=1) -> unlink must exit 0, write UNLINK, and drop the dep from show.

DSO-Bug: f5a8-d74d-7593-4313
test-ticket-delete-e2e.sh had 4 failures from two stale tests (production code is
correct; the test predated two intentional changes):

- E2E-1: closures now require a verdict hash (closure gate added in a6e8925). The
  test closed story/epic without one. Added a _close_with_verdict helper computing a
  real HMAC via compute-verdict-hash.sh; fixes the cascading S1/epic/full-lifecycle
  assertions.
- E2E-6: imported _outbound_handlers from the bridge/ module deleted in a3a3928
  (epic 3a03 edge->level-triggered reconciler cutover) and asserted a removed
  delete_issue route. Rewritten as test_reconciler_routes_deleted_to_done, asserting
  the reconciler outbound_differ maps deleted->Done (and never emits a delete action).

UPDATE testing-mode: no skip/xfail, no production-code changes. PASSED:21 FAILED:0.

DSO-Bug: e459-b5ef-9c02-403b
DSO-Story: worktree-20260606-091709
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR enhances ticket linking with canonical relation validation and snapshot-based recovery, adds dry-run support to the ticket link wrapper, refines integration workflow gating, and expands E2E test coverage with verdict hash enforcement and reconciler routing validation.

Changes

Ticket Link System: Validation, Snapshot Fallback, and Dry-Run

Layer / File(s) Summary
Canonical relation validation contract
plugins/dso/scripts/ticket_graph/_links.py
Introduces CANONICAL_RELATIONS frozenset and validates relation parameter in add_dependency() before any file writes, raising ValueError for invalid relations. Updates _is_active_link docstring to document SNAPSHOT fallback behavior.
Snapshot fallback for link idempotency
plugins/dso/scripts/ticket-link.sh
Implements SNAPSHOT fallback in _is_duplicate_link and _get_link_info to recover link state when compacted. Both functions now track cancelled_uuids from UNLINK events and scan *-SNAPSHOT.json deps for matches when no active LINK/UNLINK files exist, excluding entries with cancelled UUIDs.
Dry-run flag support in ticket link wrapper
plugins/dso/scripts/ticket-lib-api.sh
Parses --dry-run flag early in ticket_link(), removes it from positional arguments, and when set delegates to canonical ticket-link.sh link ... --dry-run with proper exit code handling.
Integration and unit tests for ticket link system
tests/scripts/test-ticket-link.sh, tests/scripts/test_ticket_graph.py
Validates dry-run produces preview without writing events, rejects non-canonical relations with error, confirms unlink succeeds after snapshot compaction, and comprehensively tests all canonical relations accepted and all non-canonical relations rejected.

Configuration and Version Updates

Layer / File(s) Summary
Pre-commit isolation-check timeout increase
.pre-commit-config.yaml, plugins/dso/docs/examples/pre-commit-config.example.yaml
Increases isolation-check hook timeout from 60 to 120 seconds in both active config and documentation example.
Plugin manifest version bump
plugins/dso/.claude-plugin/plugin.json
Increments plugin version from 1.17.141 to 1.17.142.

Workflow Gate for Integration Modes

Layer / File(s) Summary
Workflow mode gate in integration prompt
plugins/dso/skills/shared/prompts/single-agent-integrate.md
Adds dso.workflow conditional at Step 5 to skip local code review and recording (Steps 5–6) when dso.workflow=ci-pr, proceeding directly to test status recording (Step 7). Annotates both steps as running only when dso.workflow=local (default).

E2E Test Refactoring for Verdict Hash and Reconciler Routing

Layer / File(s) Summary
Verdict hash helper for E2E tests
tests/acceptance/test-ticket-delete-e2e.sh
Introduces _close_with_verdict() helper that computes verdict hash via compute-verdict-hash.sh and closes ticket with --verdict-hash flag. Updates Test 1 child and parent ticket closures to use this helper instead of direct transition ... closed.
Reconciler routing test and field mapping validation
tests/acceptance/test-ticket-delete-e2e.sh
Replaces bridge-outbound routing test with new reconciler-outbound-differ test. Dynamically constructs Python script that verifies deleted status maps to Jira status Done, validates mutation generation excludes deleted tickets by default, and confirms exactly one update mutation (not delete) is emitted when deletion is not excluded.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

force-merged-fp-recovery

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title refers only to test alignment in delete-e2e, but the PR encompasses broad changes across multiple systems including ticket-link, ticket-graph, pre-commit configuration, and documentation updates. Revise the title to reflect the primary changes across the changeset, such as: 'feat: solidify ticket link/graph, test isolation budget, and workflow gating' or choose the most impactful change as the title.
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch staged-46b5387451f3-1780771548

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@joeoakhart joeoakhart enabled auto-merge (rebase) June 6, 2026 18:58
…rom ci-pr prose

test-legacy-cleanup-grep.sh forbids legacy config-key references in
plugins/dso/skills/. The ci-pr review-gating note referenced the literal
`enforcement.strategy=ci` (legacy alias) in explanatory prose. Replace with a
generic "CI-enforcement skip path" pointer to REVIEW-WORKFLOW.md + rule:no-bypass-review.
The canonical dso.workflow gate logic is unchanged.
…tegrate

fix: drop legacy enforcement.strategy token from single-agent-integrate.md (unblocks #696)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
plugins/dso/docs/examples/pre-commit-config.example.yaml (1)

55-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix stale timeout comment to match configured value.

The inline comment says timeout: 30s, but the hook is now configured to 120s (name and entry). Please align the comment to avoid operator confusion.

Suggested patch
-      # Test isolation check (timeout: 30s) — staged-only scan with file-type
+      # Test isolation check (timeout: 120s) — staged-only scan with file-type
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/dso/docs/examples/pre-commit-config.example.yaml` around lines 55 -
60, The inline comment above the isolation-check hook is stale (mentions
"timeout: 30s") and should be updated to match the configured 120s timeout; edit
the comment to read "Test isolation check (timeout: 120s)" (the hook is
identified by id: isolation-check and references name: "Test Isolation Check
(120s timeout)" and entry: ./scripts/pre-commit-wrapper.sh isolation-check 120
...), ensuring comments and configured values are consistent.
plugins/dso/scripts/ticket-lib-api.sh (1)

2115-2158: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the optional relation when --dry-run is present.

After stripping --dry-run, this wrapper still requires three positional args. That breaks ticket link <id1> <id2> --dry-run, even though ticket-link.sh accepts [<relation>] and defaults to relates_to.

Suggested fix
-        if [ $# -lt 3 ]; then
+        if [ $# -lt 2 ]; then
             echo "Usage: ticket link <id1> <id2> <relation>" >&2
             return 1
         fi
@@
-        local src_id="$1" tgt_id="$2" relation="$3"
+        local src_id="$1" tgt_id="$2" relation="${3:-relates_to}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/dso/scripts/ticket-lib-api.sh` around lines 2115 - 2158, The wrapper
currently requires three positional args after stripping --dry-run, causing
"ticket link <id1> <id2> --dry-run" to fail; change the arg-count check to
require at least 2 (ids) instead of 3, initialize relation from "$3" but allow
it to be empty (e.g. relation="${3:-}"), and when invoking ticket-link.sh in the
--dry-run branch pass the relation argument only if non-empty so the underlying
ticket-link.sh can apply its default (relates_to); update the usage check and
the dry-run invocation that uses TICKETS_TRACKER_DIR and bash
"$_TICKETLIB_DIR/ticket-link.sh" to reflect this conditional inclusion while
keeping resolution logic for src_id and tgt_id unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/dso/scripts/ticket-link.sh`:
- Around line 130-143: The loops that scan '*-SNAPSHOT.json' must use the newest
readable snapshot first and stop after processing that one so you don't
resurrect older state; in both _is_duplicate_link (the loop using snap_path,
snap, compiled, dep_target, dep_uuid, dep_relation) and the similar loop in
_get_link_info, iterate the glob results newest-first (e.g. sort in reverse or
by mtime descending), attempt to open each file until you successfully read one,
then process only that snapshot's compiled.deps and break (don’t continue
scanning older files after a successful read); keep the existing JSON/OSError
continue behavior for unreadable files so you fall back to the next-most-recent
file.

In `@plugins/dso/skills/shared/prompts/single-agent-integrate.md`:
- Line 181: The assignment to DSO_WORKFLOW uses a relative path to run
.claude/scripts/dso read-config.sh which can fail if the current working
directory is not the repo root; update the command to reference the repo root
variable (ORCHESTRATOR_ROOT) when invoking the script so it becomes something
like invoking "${ORCHESTRATOR_ROOT}/.claude/scripts/dso read-config.sh" to
produce DSO_WORKFLOW while preserving the existing fallback behavior
(DSO_WORKFLOW=${DSO_WORKFLOW:-local}); adjust only the path used to run
read-config.sh in the DSO_WORKFLOW assignment.

In `@tests/acceptance/test-ticket-delete-e2e.sh`:
- Line 359: The mktemp invocation that creates py_script uses an inconsistent
quote boundary: move the ".py" suffix inside the quoted template string passed
to mktemp so the template is consistently quoted (update the mktemp call that
assigns py_script in tests/acceptance/test-ticket-delete-e2e.sh to include ".py"
inside the "${TMPDIR:-/tmp}/reconciler-routing-test-XXXXXX" string).

In `@tests/scripts/test-ticket-link.sh`:
- Around line 1319-1347: The test currently only covers the three-arg dry-run
form ("ticket link <id1> <id2> relates_to --dry-run"); add a second test
invocation that exercises the two-ID documented form ("ticket link <id1> <id2>
--dry-run") using the same setup (id1/id2 from TICKET_SCRIPT) and the same
assertions: capture exit code and stdout_out from cd "$repo" && bash
"$TICKET_SCRIPT" link "$id1" "$id2" --dry-run, assert exit_code is 0, assert
stdout_out contains "[DRY RUN]", and assert
_count_link_events("$tracker_dir","$id1") remains "0" (no LINK event written).
Ensure this new invocation uses the same local variables (id1, id2, stdout_out,
exit_code) and descriptive assertion messages to mirror the existing canonical
dry-run checks so the wrapper bug is exercised.

---

Outside diff comments:
In `@plugins/dso/docs/examples/pre-commit-config.example.yaml`:
- Around line 55-60: The inline comment above the isolation-check hook is stale
(mentions "timeout: 30s") and should be updated to match the configured 120s
timeout; edit the comment to read "Test isolation check (timeout: 120s)" (the
hook is identified by id: isolation-check and references name: "Test Isolation
Check (120s timeout)" and entry: ./scripts/pre-commit-wrapper.sh isolation-check
120 ...), ensuring comments and configured values are consistent.

In `@plugins/dso/scripts/ticket-lib-api.sh`:
- Around line 2115-2158: The wrapper currently requires three positional args
after stripping --dry-run, causing "ticket link <id1> <id2> --dry-run" to fail;
change the arg-count check to require at least 2 (ids) instead of 3, initialize
relation from "$3" but allow it to be empty (e.g. relation="${3:-}"), and when
invoking ticket-link.sh in the --dry-run branch pass the relation argument only
if non-empty so the underlying ticket-link.sh can apply its default
(relates_to); update the usage check and the dry-run invocation that uses
TICKETS_TRACKER_DIR and bash "$_TICKETLIB_DIR/ticket-link.sh" to reflect this
conditional inclusion while keeping resolution logic for src_id and tgt_id
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 600e9097-d006-4acb-a7ee-9fd26d2271f5

📥 Commits

Reviewing files that changed from the base of the PR and between 15dd8f0 and 4db7c79.

📒 Files selected for processing (10)
  • .pre-commit-config.yaml
  • plugins/dso/.claude-plugin/plugin.json
  • plugins/dso/docs/examples/pre-commit-config.example.yaml
  • plugins/dso/scripts/ticket-lib-api.sh
  • plugins/dso/scripts/ticket-link.sh
  • plugins/dso/scripts/ticket_graph/_links.py
  • plugins/dso/skills/shared/prompts/single-agent-integrate.md
  • tests/acceptance/test-ticket-delete-e2e.sh
  • tests/scripts/test-ticket-link.sh
  • tests/scripts/test_ticket_graph.py

Comment on lines +88 to +105
for snap_path in sorted(_glob.glob(os.path.join(ticket_dir, "*-SNAPSHOT.json"))):
try:
with open(snap_path, encoding="utf-8") as fh:
snap = json.load(fh)
except (OSError, json.JSONDecodeError):
continue
compiled = snap.get("data", {}).get("compiled_state", {})
for dep in compiled.get("deps", []):
dep_target = dep.get("target_id", "")
dep_uuid = dep.get("link_uuid", "")
dep_relation = dep.get("relation", "")
if (
dep_target == target_id
and dep_relation == relation
and dep_uuid
and dep_uuid not in cancelled_uuids
):
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat only the newest readable SNAPSHOT as authoritative.

This fallback walks all *-SNAPSHOT.json files in ascending order and returns on the first matching dep. If a later compaction writes a newer snapshot that no longer contains this dependency, an older snapshot will still make _is_active_link() return True once the intervening UNLINK has been compacted away.

Suggested fix
-    for snap_path in sorted(_glob.glob(os.path.join(ticket_dir, "*-SNAPSHOT.json"))):
+    for snap_path in sorted(
+        _glob.glob(os.path.join(ticket_dir, "*-SNAPSHOT.json")),
+        reverse=True,
+    ):
         try:
             with open(snap_path, encoding="utf-8") as fh:
                 snap = json.load(fh)
         except (OSError, json.JSONDecodeError):
             continue
         compiled = snap.get("data", {}).get("compiled_state", {})
         for dep in compiled.get("deps", []):
             dep_target = dep.get("target_id", "")
             dep_uuid = dep.get("link_uuid", "")
             dep_relation = dep.get("relation", "")
             if (
                 dep_target == target_id
                 and dep_relation == relation
                 and dep_uuid
                 and dep_uuid not in cancelled_uuids
             ):
                 return True
+        break

Comment on lines +130 to +143
for snap_path in sorted(p.glob('*-SNAPSHOT.json')):
try:
with open(snap_path, encoding='utf-8') as fh:
snap = json.load(fh)
except (json.JSONDecodeError, OSError):
continue
compiled = snap.get('data', {}).get('compiled_state', {})
for dep in compiled.get('deps', []):
dep_target = dep.get('target_id', '')
dep_uuid = dep.get('link_uuid', '')
dep_relation = dep.get('relation', '')
if dep_target == target_id and dep_relation == relation and dep_uuid and dep_uuid not in cancelled_uuids:
print('DUPLICATE')
sys.exit(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the latest readable SNAPSHOT in both fallback paths.

Both embedded Python loops keep scanning older *-SNAPSHOT.json files until they find a match. After a second compaction, that can resurrect a dependency that was removed by a newer snapshot, so _is_duplicate_link() reports a phantom duplicate and _get_link_info() returns a stale link_uuid.

Suggested fix pattern for both loops
-for snap_path in sorted(p.glob('*-SNAPSHOT.json')):
+for snap_path in sorted(p.glob('*-SNAPSHOT.json'), reverse=True):
     try:
         with open(snap_path, encoding='utf-8') as fh:
             snap = json.load(fh)
     except (json.JSONDecodeError, OSError):
         continue
     compiled = snap.get('data', {}).get('compiled_state', {})
     for dep in compiled.get('deps', []):
         ...
             ...
+    break

Also applies to: 314-327

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/dso/scripts/ticket-link.sh` around lines 130 - 143, The loops that
scan '*-SNAPSHOT.json' must use the newest readable snapshot first and stop
after processing that one so you don't resurrect older state; in both
_is_duplicate_link (the loop using snap_path, snap, compiled, dep_target,
dep_uuid, dep_relation) and the similar loop in _get_link_info, iterate the glob
results newest-first (e.g. sort in reverse or by mtime descending), attempt to
open each file until you successfully read one, then process only that
snapshot's compiled.deps and break (don’t continue scanning older files after a
successful read); keep the existing JSON/OSError continue behavior for
unreadable files so you fall back to the next-most-recent file.

Read the canonical workflow knob before dispatching:

```bash
DSO_WORKFLOW=$(.claude/scripts/dso read-config.sh dso.workflow 2>/dev/null); DSO_WORKFLOW=${DSO_WORKFLOW:-local}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Anchor read-config.sh to ORCHESTRATOR_ROOT to avoid CWD-dependent failures.

Line 181 uses .claude/scripts/dso ... as a relative path, which can fail when the orchestrator is not in repo root at that moment. Use the same absolute-root pattern used elsewhere in this protocol.

Suggested patch
-DSO_WORKFLOW=$(.claude/scripts/dso read-config.sh dso.workflow 2>/dev/null); DSO_WORKFLOW=${DSO_WORKFLOW:-local}
+DSO_WORKFLOW=$("$ORCHESTRATOR_ROOT/.claude/scripts/dso" read-config.sh dso.workflow 2>/dev/null); DSO_WORKFLOW=${DSO_WORKFLOW:-local}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DSO_WORKFLOW=$(.claude/scripts/dso read-config.sh dso.workflow 2>/dev/null); DSO_WORKFLOW=${DSO_WORKFLOW:-local}
DSO_WORKFLOW=$("$(git rev-parse --show-toplevel)/.claude/scripts/dso" read-config.sh dso.workflow 2>/dev/null); DSO_WORKFLOW=${DSO_WORKFLOW:-local}
Suggested change
DSO_WORKFLOW=$(.claude/scripts/dso read-config.sh dso.workflow 2>/dev/null); DSO_WORKFLOW=${DSO_WORKFLOW:-local}
DSO_WORKFLOW=$("$REPO_ROOT/.claude/scripts/dso" read-config.sh dso.workflow 2>/dev/null); DSO_WORKFLOW=${DSO_WORKFLOW:-local}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/dso/skills/shared/prompts/single-agent-integrate.md` at line 181, The
assignment to DSO_WORKFLOW uses a relative path to run .claude/scripts/dso
read-config.sh which can fail if the current working directory is not the repo
root; update the command to reference the repo root variable (ORCHESTRATOR_ROOT)
when invoking the script so it becomes something like invoking
"${ORCHESTRATOR_ROOT}/.claude/scripts/dso read-config.sh" to produce
DSO_WORKFLOW while preserving the existing fallback behavior
(DSO_WORKFLOW=${DSO_WORKFLOW:-local}); adjust only the path used to run
read-config.sh in the DSO_WORKFLOW assignment.

# Write the test script to a temp file (avoids heredoc-in-subshell issues)
local py_script exit_code py_output
py_script=$(mktemp "${TMPDIR:-/tmp}/bridge-routing-test-XXXXXX".py)
py_script=$(mktemp "${TMPDIR:-/tmp}/reconciler-routing-test-XXXXXX".py)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Minor: inconsistent quoting in mktemp template.

The .py suffix is outside the quotes, which works but is inconsistent. Recommend including it inside for clarity.

-    py_script=$(mktemp "${TMPDIR:-/tmp}/reconciler-routing-test-XXXXXX".py)
+    py_script=$(mktemp "${TMPDIR:-/tmp}/reconciler-routing-test-XXXXXX.py")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
py_script=$(mktemp "${TMPDIR:-/tmp}/reconciler-routing-test-XXXXXX".py)
py_script=$(mktemp "${TMPDIR:-/tmp}/reconciler-routing-test-XXXXXX.py")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/acceptance/test-ticket-delete-e2e.sh` at line 359, The mktemp
invocation that creates py_script uses an inconsistent quote boundary: move the
".py" suffix inside the quoted template string passed to mktemp so the template
is consistently quoted (update the mktemp call that assigns py_script in
tests/acceptance/test-ticket-delete-e2e.sh to include ".py" inside the
"${TMPDIR:-/tmp}/reconciler-routing-test-XXXXXX" string).

Comment on lines +1319 to +1347
local id1 id2
id1=$(cd "$repo" && bash "$TICKET_SCRIPT" create task "DryRun canonical source" 2>/dev/null | grep -o '[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}' | head -1)
id2=$(cd "$repo" && bash "$TICKET_SCRIPT" create task "DryRun canonical target" 2>/dev/null | grep -o '[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}-[0-9a-f]\{4\}' | head -1)

if [ -z "$id1" ] || [ -z "$id2" ]; then
assert_eq "canonical dry-run: tickets created" "non-empty" "empty"
assert_pass_if_clean "test_canonical_dispatcher_dry_run_no_link_event"
return
fi

# Run via the CANONICAL dispatcher ($TICKET_SCRIPT, NOT $TICKET_LINK_SCRIPT)
local exit_code=0
local stdout_out
stdout_out=$(cd "$repo" && bash "$TICKET_SCRIPT" link "$id1" "$id2" relates_to --dry-run 2>/dev/null) || exit_code=$?

# Assert: exits 0
assert_eq "canonical dry-run: exits 0" "0" "$exit_code"

# Assert: output contains [DRY RUN] preview
if [[ "$stdout_out" =~ \[DRY\ RUN\] ]]; then
assert_eq "canonical dry-run: output contains [DRY RUN] preview" "has-dry-run" "has-dry-run"
else
assert_eq "canonical dry-run: output contains [DRY RUN] preview" "has-dry-run" "missing: $stdout_out"
fi

# Assert: NO LINK event written to disk (this is the failing assertion before the fix)
local link_count
link_count=$(_count_link_events "$tracker_dir" "$id1")
assert_eq "canonical dry-run: no LINK event written for id1" "0" "$link_count"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the two-ID --dry-run case here as well.

This only covers ticket link <id1> <id2> relates_to --dry-run. The wrapper bug shows up on the documented two-ID form, ticket link <id1> <id2> --dry-run, because --dry-run is stripped before the arg-count check.

As per coding guidelines, "all code changes require corresponding tests that fail before (RED) and pass after (GREEN)" and "Tests must exercise observable behavior of new code paths."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/scripts/test-ticket-link.sh` around lines 1319 - 1347, The test
currently only covers the three-arg dry-run form ("ticket link <id1> <id2>
relates_to --dry-run"); add a second test invocation that exercises the two-ID
documented form ("ticket link <id1> <id2> --dry-run") using the same setup
(id1/id2 from TICKET_SCRIPT) and the same assertions: capture exit code and
stdout_out from cd "$repo" && bash "$TICKET_SCRIPT" link "$id1" "$id2"
--dry-run, assert exit_code is 0, assert stdout_out contains "[DRY RUN]", and
assert _count_link_events("$tracker_dir","$id1") remains "0" (no LINK event
written). Ensure this new invocation uses the same local variables (id1, id2,
stdout_out, exit_code) and descriptive assertion messages to mirror the existing
canonical dry-run checks so the wrapper bug is exercised.

Source: Coding guidelines

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO-Review-Cycle: 1 pr_number=696 commit_sha=b878e0bb041490ef1d9172292efc80d125eb7742 findings_hash=d2d1f3d0c303e3a5 tuples=[["plugins/dso/scripts/ticket-lib-api.sh", "", "correctness"], ["plugins/dso/scripts/ticket-link.sh", "", "correctness"], ["plugins/dso/scripts/ticket-link.sh", "", "correctness"], ["plugins/dso/scripts/ticket-link.sh", "", "design"], ["plugins/dso/scripts/ticket_graph/_links.py", "", "correctness"], ["plugins/dso/scripts/ticket_graph/_links.py", "", "correctness"], ["plugins/dso/skills/shared/prompts/single-agent-integrate.md", "", "correctness"], ["tests/acceptance/test-ticket-delete-e2e.sh", "", "correctness"], ["tests/acceptance/test-ticket-delete-e2e.sh", "", "maintainability"], ["tests/acceptance/test-ticket-delete-e2e.sh", "", "verification"], ["tests/scripts/test-ticket-link.sh", "", "verification"], ["tests/scripts/test-ticket-link.sh", "", "verification"]]

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 1/11

[critical] plugins/dso/scripts/ticket-link.sh:116 plugins/dso/scripts/ticket-link.sh:302 (correctness)

SNAPSHOT fallback logic in check-unlink-exists.sh is missing entirely. Sonnet A identified that ticket-link.sh implements the SNAPSHOT fallback for duplicate detection (lines 116-143) and for getting link info (lines 302-327), but the bash check-unlink-exists.sh script does not implement this fallback. When ticket-compact.sh bakes LINK events into SNAPSHOT files and deletes the original *-LINK.json files, check-unlink-exists.sh will fail to locate the link_uuid because it only globs *-LINK.json. This causes 'ticket unlink' to fail after compaction, breaking a critical user workflow. The test test_ticket_unlink_after_compaction_succeeds (added at line 1413) is designed to catch this exact defect but will fail if check-unlink-exists.sh is not updated in parallel with the ticket-link.sh SNAPSHOT fallback implementation.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 2/11

[critical] plugins/dso/scripts/ticket-link.sh:125 plugins/dso/scripts/ticket_graph/_links.py:81 (correctness)

In ticket-link.sh line 125 (duplicate-detection branch within the 'duplicate-exists' check function), the condition checks only `dep_target == target_id` without verifying `dep_relation == relation`. This allows a link with a different relation type to a matching target to be incorrectly flagged as a duplicate. When a SNAPSHOT contains a 'depends_on' link to target B, attempting to create a 'duplicates' link to the same target B will be rejected as a duplicate even though the relations differ. The Python implementation in _links.py (line 81-82) correctly includes both checks: 'dep_target == target_id and dep_relation == relation'. This asymmetry between bash and Python paths creates a critical logic error that can block legitimate link creation.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 3/11

[critical] tests/acceptance/test-ticket-delete-e2e.sh:348 tests/acceptance/test-ticket-delete-e2e.sh:350 tests/acceptance/test-ticket-delete-e2e.sh:352 (correctness)

In test-ticket-delete-e2e.sh, the Python script embedded in test_reconciler_routes_deleted_to_done (starting at line 341) has sys.path.insert() at line 350 appearing AFTER the import statements at lines 352-354. Python executes imports as they are encountered in the source code; the sys.path modification at line 350 happens before line 352 in execution order, BUT the import statement parsing occurs at module load time before any statements execute. When the Python interpreter parses the script, it encounters 'from dso_reconciler.outbound_differ import ...' at line 352. At that moment, the sys.path has NOT yet been modified (because the modification at line 350 is an executable statement, not a compile-time directive). The import will fail with ModuleNotFoundError because sys.path does not yet include the plugins/dso/scripts directory. The test will exit with an error before any assertions execute.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 4/11

[important] plugins/dso/scripts/ticket-lib-api.sh:2151 plugins/dso/scripts/ticket-lib-api.sh:2157 (correctness)

Sonnet A identified a logic error in ticket-lib-api.sh at lines 2115-2127 where the --dry-run flag parsing rebuilds the argument list. While the parsing is correct in this context, the implementation is fragile: if the function signature changes or new flags are added later, the position-dependent indexing of $1, $2, $3 could break. More critically, the condition at line 2151 checks 'if [ "$dry_run" = "1" ]' but does not validate that src_id and tgt_id are non-empty before passing them to the delegated ticket-link.sh call at line 2157. If either src_id or tgt_id is empty (e.g., user calls 'ticket link --dry-run B depends_on'), the --dry-run delegation will pass empty arguments to ticket-link.sh, producing confusing output or errors instead of failing fast with a clear usage message.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 5/11

[important] plugins/dso/scripts/ticket_graph/_links.py:68 plugins/dso/scripts/ticket_graph/_links.py:76 (correctness)

In _links.py, the SNAPSHOT fallback at lines 70-82 does not validate that a SNAPSHOT file's content is well-formed before trusting its deps[] data. The try/except at line 68 catches json.JSONDecodeError and OSError, but if the SNAPSHOT file is valid JSON with a missing or malformed 'compiled_state' key (which would not raise an exception), the fallback will silently proceed with an empty deps list, potentially masking data corruption. Additionally, the fallback does NOT validate link_uuid format (UUID format check) before comparing it against cancelled_uuids. A SNAPSHOT containing a non-UUID string as link_uuid could shadow a legitimately unlinked dependency, causing false 'link exists' returns.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 6/11

[important] tests/acceptance/test-ticket-delete-e2e.sh:352 tests/acceptance/test-ticket-delete-e2e.sh:360 (verification)

Test test_reconciler_routes_deleted_to_done imports functions from dso_reconciler.outbound_differ without first verifying those functions exist or have the expected signatures. If compute_outbound_mutations or _map_local_to_jira_fields are renamed, removed, or have different parameter signatures, the test will fail at import time with ModuleNotFoundError or ImportError, not at assertion time. The _FakeBindingStore mock is defined inline without type-checking against BindingStoreProtocol—if the protocol has changed (additional required methods), the mock will not satisfy the contract and test assertions become meaningless. A test fixture or type stub that validates the mock against the protocol would catch this.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 7/11

[important] tests/scripts/test-ticket-link.sh:1470 tests/scripts/test-ticket-link.sh:1484 (verification)

Test test_ticket_unlink_after_compaction_succeeds checks that an UNLINK event is written to disk and that the target ID no longer appears in 'ticket show' output, but does not verify the actual link graph state was updated. A grep-based check on loose string output is insufficient—if the unlink logic reads SNAPSHOT correctly but fails to update active_links or cancel the uuid, the UNLINK event is still written (test line 481 passes), but the link remains active in the graph. The test only verifies file artifacts and text output, not the functional outcome. A stricter assertion (e.g., parsing structured link info via 'ticket show --json' or a dedicated query function) would catch partial unlink failures.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 8/11

[important] tests/scripts/test-ticket-link.sh:1308 tests/scripts/test-ticket-link.sh:1355 tests/scripts/test-ticket-link.sh:1401 (verification)

Three new bash tests (test_canonical_dispatcher_dry_run_no_link_event at line 1308, test_ticket_link_rejects_non_canonical_relation at line 1355, test_ticket_unlink_after_compaction_succeeds at line 1401) are added to test-ticket-link.sh without explicit .test-index entries. If these tests are marked RED (blocking) in .test-index, the pre-commit gate will block all commits until someone passes them. If they are marked GREEN (or absent from .test-index), the gate machinery does not know to flag a regression if a future change breaks them. The diff does not show any .test-index changes, creating ambiguity about test gate configuration. If these tests are currently RED and designed to document the fix for bugs 3796-ccd3, 61b8, and f5a8, they must be marked in .test-index; otherwise, the pre-commit gate is misconfigured.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 9/11

[important] plugins/dso/scripts/ticket-link.sh:116 plugins/dso/scripts/ticket_graph/_links.py:45 (design)

The diff adds SNAPSHOT fallback logic to both the bash (ticket-link.sh) and Python (ticket_graph/_links.py) paths, implementing the same compaction-recovery algorithm in two languages. This creates a dual-implementation maintenance burden: any future change to the fallback logic (e.g., a new compaction format, a bug fix) must be made in both bash and Python in lockstep. The Python _is_active_link function at line 45 is a good abstraction point—the bash check-unlink-exists.sh and ticket-link.sh code should ideally delegate to this Python function instead of reimplementing the same logic. A comment noting this duplication and suggesting consolidation would help future maintainers.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 10/11

[important] plugins/dso/scripts/ticket_graph/_links.py:265 plugins/dso/scripts/ticket_graph/_links.py:270 (correctness)

In _links.py, the add_dependency function now validates relation against CANONICAL_RELATIONS at line 268, raising ValueError for invalid relations. However, the error message (lines 270-273) references 'bug 3796-ccd3-863f-4d63' which is a dry-run bug fix reference, not the relation-grammar bug (61b8). The comment should reference 'bug 61b8-bb44-fb04-402c' (relation grammar validation) instead. More critically, the ValueError message could be confusing to users—it should explicitly state 'invalid relation' rather than implying the relation format was wrong.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

DSO llm-review — finding 11/11

[important] plugins/dso/skills/shared/prompts/single-agent-integrate.md:175 plugins/dso/skills/shared/prompts/single-agent-integrate.md:207 (correctness)

In single-agent-integrate.md (skill documentation), Step 5 now includes a workflow gate that skips the local code reviewer when dso.workflow=ci-pr. The documentation states 'Do NOT compute REVIEWER_HASH or call record-review.sh; proceed directly to Step 7', but Step 6 (Record review) is still listed in the file. A maintainer following the sequential steps would see 'Step 5 → Step 6 → Step 7' and might not realize Step 6 is conditionally skipped. The numbering should be clarified: either renumber Step 6 as 'Step 6 (conditional)' or move the conditional skip logic into Step 6's header to avoid confusion.

Posted by dso_ci_review.runner; resolve this comment when addressed.

@gitar-bot

gitar-bot Bot commented Jun 6, 2026

Copy link
Copy Markdown
CI failed: The CI run failed due to a combination of blocking code quality findings flagged by the LLM review agent and transient network connectivity issues preventing container image pulls.

Overview

This build failure is composed of two distinct issues: the automated LLM review detected 11 actionable code quality findings, and a separate infrastructure failure occurred due to network timeouts when pulling required Docker images.

Failures

LLM Review Blocking Findings (confidence: high)

  • Type: test
  • Affected jobs: 79905834340
  • Related to change: yes
  • Root cause: The PR introduced code that triggered 3 critical and 8 important findings during the automated review, causing the llm-review job to exit with code 1.
  • Suggested fix: Review the 11 PR comments generated by the review agent. Specifically check plugins/dso/scripts/ticket-link.sh and address the identified logic, safety, or style issues.

Infrastructure Network Timeout (confidence: high)

  • Type: infrastructure
  • Affected jobs: [Not specified, likely a parallel job in the same run]
  • Related to change: no
  • Root cause: Transient network connectivity issues between the CI runner and Docker Hub resulted in a "context deadline exceeded" error when pulling alpine:3.19.
  • Suggested fix: This is a non-deterministic infrastructure issue. Re-trigger the failed CI job to resolve it.

Summary

  • Change-related failures: 1 (LLM review flagged 11 blocking findings in the new code).
  • Infrastructure/flaky failures: 1 (Docker image pull timeout).
  • Recommended action: First, address the 11 code quality findings reported in the PR comments to clear the blocking review status. Second, re-run the CI to clear the transient infrastructure failure once the review issues are resolved.
Code Review ✅ Approved

Aligns stale E2E assertions, fixes ticket-link dry-run handling, and rejects non-canonical link relations. Increases isolation-check budget and gates local code review, with no outstanding issues identified.

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@JoeOakhartNava JoeOakhartNava disabled auto-merge June 6, 2026 23:37
@JoeOakhartNava JoeOakhartNava merged commit 0e636a3 into main Jun 6, 2026
32 of 34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants