Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
f2116cf
Review navigation gets a design: entry points, order and links
jbachorik Aug 21, 2026
df1e53c
Review navigation design: grouping is the binding constraint, not order
jbachorik Aug 21, 2026
69b2cfe
Tests are reviewed beside the code they pin, not on their own card
jbachorik Aug 22, 2026
f444721
A hunk belongs to every section that explains it, and is reviewed once
jbachorik Aug 22, 2026
b1fc1ef
Scope names the reviewed-state change, not the id scheme it replaced
jbachorik Aug 22, 2026
acce500
An approval is valid only for the content and base it was given against
jbachorik Aug 22, 2026
3879457
Staleness fires only when the base move could matter, and says what i…
jbachorik Aug 22, 2026
567487f
An agent may add staleness to an approval, never take it away
jbachorik Aug 22, 2026
f9bdc9a
Review navigation gets an implementation plan
jbachorik Aug 22, 2026
fe54635
An approval is pinned to a hunk's content, not its position
jbachorik Aug 22, 2026
80d8ef3
Fix: use import for Locale instead of fully-qualified class name
jbachorik Aug 22, 2026
04cf57b
A verdict is keyed by hunk content and names the base it was given ag…
jbachorik Aug 22, 2026
799445d
Verdicts are stored per hunk digest, and the legacy migration is deleted
jbachorik Aug 22, 2026
47c64c5
review_state reports a verdict under the intent's own id, not its sto…
jbachorik Aug 22, 2026
8ea5a12
review_state degrades gracefully when a scope's diff cannot be produced
jbachorik Aug 22, 2026
5a38b01
A section's decision is derived from its hunks, not stored
jbachorik Aug 22, 2026
36d4fae
A base move marks approvals stale only when it could matter
jbachorik Aug 22, 2026
6bd4de9
Parsing of git diff output handles non-ASCII filenames correctly
jbachorik Aug 22, 2026
9e9f0ca
Progress counts hunks, and a section's state is derived from them
jbachorik Aug 22, 2026
1e0fdfa
Unknown staleness is silent, and a drifted section cannot deadlock Su…
jbachorik Aug 22, 2026
6b80e9c
A section's derivation lives in SectionStates, outside the view
jbachorik Aug 22, 2026
70b8e14
Settle a hunk, a file or a section, and say which one a key will hit
jbachorik Aug 22, 2026
4649864
Settle the real hunk in view, not a stuck focus flag or the anchor
jbachorik Aug 22, 2026
8d4d305
Name the acting unit on the Approve button, not a label the floor hides
jbachorik Aug 22, 2026
de7e947
Read the unit pressed, not what focus becomes, and free the listener …
jbachorik Aug 22, 2026
f1420f2
A stale hunk stops counting one layer up too, not just in the bar
jbachorik Aug 22, 2026
58cd8c1
Fit-check the bar's own height and the stale label, not just its buttons
jbachorik Aug 22, 2026
b573f45
tree-sitter grammars are a packaging decision, not an architectural one
jbachorik Aug 22, 2026
888cff5
A broken grammar class only falls back for its own extensions
jbachorik Aug 22, 2026
7a98e88
A file contributes what it declares and what it uses, however it is p…
jbachorik Aug 22, 2026
95b3128
Narrow the tree-sitter fallback to the calls that can actually fail
jbachorik Aug 22, 2026
60881ed
The change graph resolves a name only when exactly one file declares it
jbachorik Aug 22, 2026
d1a026a
A use mints an edge wherever it sits in the diff window, not only on a
jbachorik Aug 22, 2026
9b08c4f
Kahn and Tarjan, hand-rolled, with a total tie-break
jbachorik Aug 22, 2026
687a2b3
Graphs rejects a dependency on a node outside the given set
jbachorik Aug 22, 2026
21d7cc1
Sections follow the code's structure, not its folders
jbachorik Aug 22, 2026
cd39bb5
Sections name what a card is about, and no two read the same
jbachorik Aug 22, 2026
6737e8c
A multi-line declaration keeps its name in the change graph
jbachorik Aug 22, 2026
ef37062
A qualified name references the type that qualifies it
jbachorik Aug 22, 2026
f2dd3d5
Files that are not code contribute no symbols at all
jbachorik Aug 22, 2026
66d47a9
A name that appears only in documentation is not a reference
jbachorik Aug 22, 2026
e5117e7
The rail renders computed sections when no reviewer has run
jbachorik Aug 22, 2026
15a02e2
A closed view stops a background section graph from touching it
jbachorik Aug 22, 2026
90c083a
A computed section's id is derived from its hunks, not its position
jbachorik Aug 22, 2026
94cacdb
Computed cards carry over the fallback's kind and risk
jbachorik Aug 22, 2026
8bf6515
A cycle rationale names at most three files, and stops pointing outward
jbachorik Aug 22, 2026
269a9b3
A blocking finding survives its intent's id changing under it
jbachorik Aug 22, 2026
e686977
Computed intents are worked out once per refresh, not once per finding
jbachorik Aug 22, 2026
c717717
A diff already graphed is not re-parsed on every scope redisplay
jbachorik Aug 22, 2026
df6093a
The rail can say its grouping is provisional
jbachorik Aug 22, 2026
5291b3b
The reviewer's cursor follows the intent across a grouping swap
jbachorik Aug 22, 2026
d0b25b4
Pin the computed-id, degenerate-fallback and rail-swap behaviour with…
jbachorik Aug 22, 2026
f5e863c
A javadoc reference uses an import, not an inline qualified name
jbachorik Aug 22, 2026
4515ba2
The verdict bar and the write path agree on what blocks approval
jbachorik Aug 22, 2026
0803a3a
The provisional-grouping notice gets its own row
jbachorik Aug 22, 2026
3436326
Pin content-derived ids against an actual reorder, not just reproduci…
jbachorik Aug 22, 2026
5294aa1
A computed id hashes the section's files too, not just its hunks
jbachorik Aug 22, 2026
44daed4
A failed graph build is retried, logged, and stops claiming to refine
jbachorik Aug 22, 2026
99d2b5e
The intents cache survives across refreshes, not just within one
jbachorik Aug 22, 2026
5f0dcec
A computed section's id now genuinely depends on its hunk digest too
jbachorik Aug 22, 2026
98cbbe6
belongsToIntent's javadoc admits the safety relaxation it made
jbachorik Aug 22, 2026
cc7d34c
review_scope can hand the agent the grouping it is being asked to name
jbachorik Aug 22, 2026
18dea22
review_scope charges sections against the budget and degrades gracefully
jbachorik Aug 22, 2026
b91d8e1
Staleness also notices a base move in a file a section only references
jbachorik Aug 22, 2026
9c3f0a1
One bounded git grep recovers the strongest entry-point signal
jbachorik Aug 22, 2026
19a05e1
A real no-matches spawn pins exit code 1 as an empty answer
jbachorik Aug 22, 2026
79fec4f
Where to start, what follows it, and why
jbachorik Aug 22, 2026
20c5648
The entry-point rank drops the term that cannot discriminate
jbachorik Aug 22, 2026
c9a8eed
The change graph answers per hunk as well as per file
jbachorik Aug 22, 2026
c6143dd
Links state something true about one hunk
jbachorik Aug 22, 2026
ba1bfac
The path carries the section order it implies
jbachorik Aug 22, 2026
e3813b1
The rank's authoritative list names the terms the rank actually has
jbachorik Aug 22, 2026
5d54275
p walks the change in reading order
jbachorik Aug 22, 2026
0823a81
PATH rows get their own text fills instead of a Button's default
jbachorik Aug 22, 2026
68cb9d9
A part-settled stale hunk no longer reads as untouched
jbachorik Aug 22, 2026
1ea3c60
a, r and u in PATH mode settle the row on screen, not a section
jbachorik Aug 22, 2026
dd92b6a
Computed intent cards number off the reading path too
jbachorik Aug 22, 2026
12e2df8
PATH mode no longer bypasses the blocking-finding refusal
jbachorik Aug 22, 2026
91f9268
The verdict bar reflects and acts on the selected PATH row
jbachorik Aug 22, 2026
d1eff4c
Pin the verdict bar's PATH-mode render, and askAgentToFix's hand-off
jbachorik Aug 22, 2026
70bf8bc
A hunk says what it has to do with the hunks around it
jbachorik Aug 22, 2026
ffd71c2
revealHunk matches a hunk's real index, not its position among render…
jbachorik Aug 22, 2026
24248f1
A link click widens out of a filter that hides its own target
jbachorik Aug 22, 2026
8fcd5a5
setLinks keeps an open comment composer instead of silently dropping it
jbachorik Aug 22, 2026
1d953a5
Request the scope's graph unconditionally, so links show under a revi…
jbachorik Aug 22, 2026
7dbedb7
Pin the reviewer-grouping link footer fix that mutation testing found…
jbachorik Aug 22, 2026
2048493
Called from 3 places outside the change, and you can see which three
jbachorik Aug 22, 2026
4bf9e7b
Keep the reader on their hunk when the scan re-sorts the path under them
jbachorik Aug 22, 2026
3105001
The bar's "Ask the agent to fix it" can no longer do nothing quietly
jbachorik Aug 22, 2026
b47d5d1
p means start at the beginning, even when the path moved while you we…
jbachorik Aug 23, 2026
6a220c7
A test class owns its own stage size, all thirty-six of them
jbachorik Aug 23, 2026
e745ba5
Measure the bar where it actually is, not where the window is
jbachorik Aug 23, 2026
428da1e
An agent may say which intents its intents are built on
jbachorik Aug 23, 2026
869f51c
A reads that is not an array of ids is broken, not absent
jbachorik Aug 23, 2026
281cdfc
An agent may add staleness to an approval, never take it away
jbachorik Aug 23, 2026
16a1739
A mark with no reason, and a broken affected, are both refused
jbachorik Aug 23, 2026
fae871e
A base move asks the agent which approvals it actually disturbed
jbachorik Aug 24, 2026
b663adb
An ordering says whether drydock measured it or an agent claimed it
jbachorik Aug 24, 2026
f133007
The automatic recheck waits until git has said the move could matter
jbachorik Aug 25, 2026
faac736
Relevance is decided per approval, not per section
jbachorik Aug 25, 2026
19c82c3
The recheck scan drops a guard that only added work
jbachorik Aug 25, 2026
0e21078
The diag script can finally deliver a key to the Review view
jbachorik Aug 25, 2026
1e5cf4a
A claimed ordering is legible, and an agent's staleness says so too
jbachorik Aug 25, 2026
3d03854
An automatic recheck leaves a trace, dispatched or not
jbachorik Aug 25, 2026
8748e45
A diag verb that does not exist says so, instead of impersonating mark
jbachorik Aug 25, 2026
9527abe
A documented diag verb without a case fails the build, not a screensh…
jbachorik Aug 25, 2026
9236b14
Log Scene focus state on the CI-only settle-unit failure
jbachorik Aug 26, 2026
a33ebac
Close the async graph-build race that flaked ReviewSettleActionsTest …
jbachorik Aug 26, 2026
a5643b4
Wait for the actual postcondition after focusDiffColumn's click, not …
jbachorik Aug 26, 2026
930bf58
TEMPORARY: log whether the diff-column click even reaches production …
jbachorik Aug 26, 2026
a26e219
Remove the temporary click diagnostics now that CI is green
jbachorik Aug 26, 2026
d9277bd
Try moveTo+press+release for the diff-column click; restore diagnostics
jbachorik Aug 26, 2026
e834975
Identify exactly which focus owner steals the diff-column click
jbachorik Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@ dependencies {
implementation("org.jetbrains.pty4j:pty4j:0.13.10")
runtimeOnly("org.slf4j:slf4j-jdk14:2.0.13")

// Structural parsing for the Review board's change graph (docs/superpowers/
// specs/2026-08-22-review-navigation-design.md §10). The core artifact
// bundles aarch64/x86_64 macOS, x86_64 Windows and both Linux natives --
// exactly the platforms this app supports -- and extracts the matching one
// to ~/.tree-sitter/tree-sitter-lib/ on first use. A grammar missing from
// the classpath is the lexical path (GrammarRegistry), not an error, so
// this list is a packaging decision and may differ per artifact.
implementation("io.github.bonede:tree-sitter:0.25.3")
implementation("io.github.bonede:tree-sitter-java:0.23.4")
implementation("io.github.bonede:tree-sitter-kotlin:0.3.8.1")
implementation("io.github.bonede:tree-sitter-python:0.23.4")
implementation("io.github.bonede:tree-sitter-javascript:0.23.1")
implementation("io.github.bonede:tree-sitter-typescript:0.23.2")
implementation("io.github.bonede:tree-sitter-go:0.23.3")
implementation("io.github.bonede:tree-sitter-rust:0.23.1")
implementation("io.github.bonede:tree-sitter-c:0.23.2")
implementation("io.github.bonede:tree-sitter-cpp:0.23.4")

testImplementation(platform("org.junit:junit-bom:5.11.4"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
81 changes: 78 additions & 3 deletions app/src/main/java/app/drydock/DrydockApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,14 @@ public CompletableFuture<Void> saveOpenChangedFilesInSkim(boolean value) {
// over an hour.
case "quit" -> diagQuit(primaryStage);
case "shot" -> diagSnapshot(primaryStage, Path.of(arg));
// Documented in this hook's comment since Task 18
// and never implemented until now; it silently hit
// the default branch below, which prints "mark".
case "reviewkey" -> mainWorkspace.diagReviewKey(arg.strip());
// Documented alongside reviewkey and unwired for
// just as long; ReviewDiffColumn.diagOpenComposer
// was already written and had no caller.
case "comment" -> mainWorkspace.diagComment();
// forcebanner:<commits>/<files>, or
// forcebanner:none for a session that never wrote
// a brief, or forcebanner:<c>/<f>/dead for one
Expand Down Expand Up @@ -688,7 +696,19 @@ public CompletableFuture<Void> saveOpenChangedFilesInSkim(boolean value) {
}
case "unwind" -> System.out.println("[diag] explorer unwind -> "
+ mainWorkspace.unwindExplorerOverlay());
default -> System.out.println("[diag] mark " + arg);
// mark is a real verb, not a fallthrough. It used to
// BE the default, which is how an unwired verb --
// reviewkey and comment were both documented from
// Task 18 and never wired -- printed a plausible
// beacon and did nothing. A driver could not tell a
// synchronisation marker from a verb that does not
// exist, so a run that did nothing looked like one
// that worked.
case "mark" -> System.out.println("[diag] mark " + arg);
default -> System.out.println(
"[diag] UNKNOWN explorerScript verb '" + verb + "'"
+ " -- nothing was done. Add a case in"
+ " DrydockApplication or fix the script.");
}
});
}
Expand Down Expand Up @@ -1455,6 +1475,28 @@ private void diagTabStep(Stage stage, RepositorySidebar sidebar, String verb, St
// See the explorerScript driver for why every script has this.
case "quit" -> diagQuit(stage);
case "shot" -> diagSnapshot(stage, Path.of(arg));
// The Review board's out-of-diff fan-in popover, and a
// snapshot OF it: a Popup is its own window, so `shot` above
// photographs the board behind it rather than the popover.
case "fanin" -> System.out.println("[diag] fanin -> "
+ mainWorkspace.diagOpenFanIn());
case "popupshot" -> diagPopupSnapshot(Path.of(arg));
// Opening AND photographing in one FX block, not two script
// steps: the popover sets autoHide, and a diag run's window
// is not the focused one, so it closes itself the moment the
// pulse that opened it ends. "no popup window is showing" is
// what a two-step script actually captures.
case "faninshot" -> {
// Focused FIRST. A Popup with autoHide closes itself the
// instant its owner window loses (or never had) focus,
// and a diag run's window is not the one the user is
// looking at -- so without this the popover is gone
// before the snapshot in the very same pulse.
stage.toFront();
stage.requestFocus();
System.out.println("[diag] fanin -> " + mainWorkspace.diagOpenFanIn());
diagPopupSnapshot(Path.of(arg));
}
// DIAG-ONLY, added for the sidebar row-layout visual pass: the
// row-overlay's hover fade and pickOnBounds=false passthrough
// have no other observable hook (Node.hoverProperty is driven
Expand Down Expand Up @@ -1504,7 +1546,13 @@ private void diagTabStep(Stage stage, RepositorySidebar sidebar, String verb, St
// the code. Two rounds were lost to a plausible-but-wrong
// theory that the picture had already contradicted.
case "fadeinfo" -> diagFadeInfo(sidebar, arg);
default -> System.out.println("[diag] mark " + arg);
// See the explorerScript dispatcher: mark is a verb, and an
// unrecognised one has to say so rather than impersonate it.
case "mark" -> System.out.println("[diag] mark " + arg);
default -> System.out.println(
"[diag] UNKNOWN tabScript verb '" + verb + "'"
+ " -- nothing was done. Add a case in"
+ " DrydockApplication or fix the script.");
}
} catch (RuntimeException e) {
System.out.println("[diag] tab step '" + verb + "' failed: " + e);
Expand Down Expand Up @@ -1660,7 +1708,34 @@ private static void diagOpenNewWorktree(MainWorkspace mainWorkspace, AppShell ap
}

private static void diagSnapshot(Stage stage, Path target) {
WritableImage image = stage.getScene().snapshot(null);
diagSnapshotScene(stage.getScene(), target);
}

/**
* Snapshots the topmost showing {@code Popup} instead of the primary
* stage. A popover is its own window: {@code Stage.getScene().snapshot}
* cannot see one at all, so without this a visual pass over the symbol
* lens or the out-of-diff fan-in popover would photograph the board
* BEHIND them and read as a clean result.
*/
private static void diagPopupSnapshot(Path target) {
javafx.stage.Window popup = javafx.stage.Window.getWindows().stream()
.filter(window -> window instanceof javafx.stage.PopupWindow && window.isShowing())
.reduce((first, second) -> second)
.orElse(null);
if (popup == null || popup.getScene() == null) {
System.out.println("[diag] popupshot: no popup window is showing; windows="
+ javafx.stage.Window.getWindows().stream()
.map(window -> window.getClass().getSimpleName() + "(showing="
+ window.isShowing() + ",focused=" + window.isFocused() + ")")
.toList());
return;
}
diagSnapshotScene(popup.getScene(), target);
}

private static void diagSnapshotScene(javafx.scene.Scene scene, Path target) {
WritableImage image = scene.snapshot(null);
int width = (int) image.getWidth();
int height = (int) image.getHeight();
// The snapshot is a fresh, detached copy that nothing else references
Expand Down
38 changes: 38 additions & 0 deletions app/src/main/java/app/drydock/git/GitStatusService.java
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,44 @@ public Optional<String> headCommitBlocking(Path workingDirectory) {
return sha.isEmpty() ? Optional.empty() : Optional.of(sha);
}

/**
* The commit {@code ref} names in {@code workingDirectory}, or empty when
* it names none -- a branch that does not exist here, a tag that was
* never fetched, or a directory that is not a repository.
*
* <p>Empty rather than throwing, for {@link #headCommitBlocking}'s
* reason: the caller is stamping or comparing metadata, and a base branch
* that cannot be resolved right now is an ordinary state of a fresh
* worktree, not a failure worth costing the caller its operation. What
* the caller must NOT do is fall back to the ref name -- a verdict
* recorded against {@code "main"} and compared against {@code "main"}
* would never read as stale, which is the inert no-op this method
* exists to end.</p>
*
* <p>{@code --end-of-options} precedes the ref because a ref may begin
* with {@code -} and would otherwise be read as a flag. Blocking; never
* call on the FX thread.</p>
*/
public Optional<String> commitForRefBlocking(Path workingDirectory, String ref) {
Optional<Path> git = locator.locate();
if (git.isEmpty() || ref == null || ref.isBlank()) {
return Optional.empty();
}
ProcessResult result = run(List.of(git.get().toString(), "-C", workingDirectory.toString(),
"rev-parse", "--verify", "--end-of-options", ref + "^{commit}"));
if (result.exitCode() != 0) {
// Logged rather than folded silently into the empty result: an
// unresolvable base is what makes every verdict on the scope read
// as stale, and a reader asking why must be able to find out.
LOG.log(Level.WARNING, "git rev-parse --verify " + ref + " failed (exit "
+ result.exitCode() + ") in " + workingDirectory + ": "
+ ProcessRunner.excerpt(result.stderr()));
return Optional.empty();
}
String sha = result.stdout().strip();
return sha.isEmpty() ? Optional.empty() : Optional.of(sha);
}

/** Async form of {@link #headCommitBlocking}, on this service's background executor. */
public CompletableFuture<Optional<String>> headCommit(Path workingDirectory) {
return CompletableFuture.supplyAsync(() -> headCommitBlocking(workingDirectory), executor);
Expand Down
39 changes: 38 additions & 1 deletion app/src/main/java/app/drydock/mcp/McpSessionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import app.drydock.domain.HandoffBrief;
import app.drydock.domain.ManagedSessionId;
import app.drydock.git.UnifiedDiff;
import app.drydock.review.RecheckAssessment;
import app.drydock.review.ReviewAnnotation;
import app.drydock.review.ReviewIntent;
import app.drydock.review.ReviewScope;
Expand Down Expand Up @@ -93,15 +94,51 @@ Optional<ReviewAnnotation> mutateAnnotation(ReviewAnnotation.Key key,
/** Replaces a scope's intent grouping ({@code review_intents}). */
void putIntents(String scopeId, List<ReviewIntent> intents);

/**
* {@code scopeId}'s intents over {@code diff}: the reviewer's grouping
* when {@link #putIntents} supplied one, otherwise the by-file fallback
* -- the same choice {@code SessionReviewView.Host#intents} makes for the
* UI. {@code review_state} joins this against {@link #verdictsOf} to
* report a verdict under the id an agent actually sent to {@code
* review_intents}, rather than whatever internal key a verdict happens
* to be stored under.
*/
List<ReviewIntent> intentsOf(String scopeId, UnifiedDiff diff);

/** Upserts findings on {@code finding.id}, so a re-run keeps existing threads. */
void upsertFindings(List<ReviewAnnotation> findings);

/** Every finding of one scope, whatever its state. */
List<ReviewAnnotation> findingsOf(String scopeId);

/** The verdicts recorded on one scope's intents. */
/** The verdicts recorded on one scope's hunks (spec §9.2). */
List<ReviewVerdict> verdictsOf(String scopeId);

/**
* The commit {@code scope}'s base REF resolves to right now, empty when
* git cannot say.
*
* <p>A commit, never the ref name, for {@link ReviewVerdict#staleAgainst}'s
* reason: a verdict recorded against {@code "main"} and compared against
* {@code "main"} could never be stale. This is the {@code toBase} half of
* a {@link RecheckAssessment}'s key, so it has to be the very same string
* the board will later ask {@code assessedAffected} with, or the recheck
* is stored under a key nobody reads.</p>
*
* <p>Empty rather than {@code SessionReviewView.UNRESOLVED_BASE}: the
* board needs a sentinel that reads as stale on a path it cannot fail,
* whereas {@code review_recheck} can simply refuse -- there is no base
* move to assess when the current base is not a commit.</p>
*/
Optional<String> currentReviewBase(ReviewScope scope);

/**
* Records agent rechecks (spec §9.7). Decoded in full before anything is
* stored, like {@link #upsertFindings}: a batch with one bad entry writes
* nothing rather than half a recheck.
*/
void putAssessments(List<RecheckAssessment> assessments);

/** Whether the human has submitted this scope's review. */
boolean reviewSubmitted(String scopeId);

Expand Down
Loading
Loading