Skip to content

Fleet: retry-safe task requests and truthful status - #288

Merged
itsHabib merged 12 commits into
mainfrom
feat/fleet-task-coordination
Sep 10, 2026
Merged

Fleet: retry-safe task requests and truthful status#288
itsHabib merged 12 commits into
mainfrom
feat/fleet-task-coordination

Conversation

@itsHabib

@itsHabib itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Summary

A supervisor retrying a queued Fleet dispatch can overwrite its timestamp or brief, while branch activity can be mistaken for accepted work. Add a repo-scoped request identity that retains the original assignment on replay, refuses conflicting payloads, and exposes a non-migrating status view that explicitly separates queued work from observed tool activity.

This is the first implementation increment of the natural-coordination design, not the completed agent-team interface. It does not launch or message workers, infer semantic acceptance, stop processes, transfer leases, or install hooks.

What this adds

  • CLI fleet request and MCP fleet_request record immutable request ID/worker fields in the existing dispatch store. Cross-process locking serializes replay and protects request rows against legacy dispatch/reassign/undispatch mutation.
  • CLI fleet status and MCP fleet_status give read-only local request observations. Queued, activity observed and evidence needing inspection remain distinct; no false Started/Stopped/Done claims.
  • Hook-owned post-tool write-target evidence per branch in the existing session record; generated hooks subscribe to the supported write tools. Old activity, read-only calls and another branch do not count.
  • Real-Git and separate-process tests for replay/conflicts, corrupt evidence, legacy mutation, migration boundaries and JSON-RPC packet integrity.

Request records remain retained for replay safety until correlated lifecycle operations are implemented. Request is effectful and can perform existing lazy migration; status does not. Assignment remains accountability, not an execution reservation or a replacement for the branch guard. Source, current limitations and follow-on adapter proof are documented in cmd/fleet/README.md and WORK.md. Broader Dossier task remains open.

Validation

  • go test -race ./cmd/fleet/...
  • go vet ./... and golangci-lint run ./... (final touched-package lint repeated after changes)
  • Both Claude and Codex variants of cmd/fleet/testdata/run-suite.sh: all scenarios passed
  • Compiled-binary fixture smoke: identical request replay retains one record; both hook adapter event formats yield activity without claiming acceptance
  • Work-contract validator and git diff --check
  • Full-module CI, fuzz and hygiene passed at 9a243af21bd7f72f94e5c5af0e7bad0e4d60b1fd; local full-suite request was cost-guarded, so targeted checks were used
  • Live Claude/Codex delivery, acknowledgment, correlated questions and effect-safe replacement remain follow-on implementation/proof

Review focus: immutable request replay across processes, all legacy writers of dispatch rows, read-only CLI/MCP behavior, and the boundary between activity and acceptance. No merge requested by this implementation.

Final review disposition

All three reviewers completed on code head 9a243af21bd7f72f94e5c5af0e7bad0e4d60b1fd.
The later documentation-only commit records residuals in FOLLOWUPS.md: Windows
branch spelling, request-metadata validation, busy-lock diagnostics and a cosmetic
redundancy. These remain for judgment after the two-fix-round cap; they are not
dismissed or accepted. No fourth panel request, merge or installation.

Co-Authored-By: Codex <noreply@openai.com>
@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@claude review

@itsHabib
itsHabib requested a lite review from Copilot September 8, 2026 04:31
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 4m 17s —— View job


Review — PR #288: Fleet retry-safe task requests and truthful status

Reviewed head: 6eeb149


Summary

The implementation is correct and well-bounded. The design is honest about what it does and doesn't do (no delivery, acceptance, lease transfer, or hook changes). Tests are thorough — the cross-process subprocess test in particular (TestRequestConcurrentProcesses) is the right tool for validating cross-process file locking. No blocking defects found.


Findings

P2 — Dead code: second request_id guard in cmdReassign is unreachable

cmd/fleet/internal/verbs/work.go:196–212

// First loop — refuses immediately if any request-bound row is found for this branch
for _, r := range rows {
    if fleet.S(r, "repo") == rid && fleet.S(r, "change") == branch && fleet.S(r, "request_id") != "" {
        return refuse("fleet: request-bound assignments require a correlated lifecycle action...")
    }
}
n := 0
for _, r := range rows {
    ...
    if fleet.S(r, "request_id") != "" {
        // ← this branch is unreachable: the first loop already returned
        return refuse("fleet reassign: request-bound assignment cannot be changed...")
    }
    ...
}

The first loop scans all rows and returns the moment it finds any request-bound row for this branch, so the second loop's request_id check on line 207 is never reached. The same pattern appears in undispatch. Not a correctness issue, but the second check is dead. Either remove it or collapse into one loop. Fix this →


P2 — fleet.ReadOnly global mutation in CmdStatus is fragile

cmd/fleet/internal/verbs/status.go:16–18

before := fleet.ReadOnly
fleet.ReadOnly = true
defer func() { fleet.ReadOnly = before }()

This works correctly now because the MCP server is documented as serial (one call at a time). But ReadOnly is a package-level global: if a concurrent call to any other verb ran during a status call, it would observe the temporarily elevated true. The inCwd/chdir pattern in mcp.go has the same single-threaded assumption and is established convention here, so this isn't a new problem — just worth noting that both assumptions live or die together.


P3 — Usage string alignment

cmd/fleet/internal/verbs/verbs.go (the new fleet status line)

  fleet status [--json]                         read-only request board; queued is not accepted or running

The description column is indented with extra spaces compared to the surrounding lines. Cosmetic, but inconsistent with the existing format (e.g., fleet request above it aligns the description at column 49).


Design notes (not defects)

Hardcoded "implementation" relationshipcreateRequest (request.go:98) checks and writes dispatchFile(rid, branch, "implementation") unconditionally, which limits requests to one per branch. This matches the "one lead, one active worker" stated design, and the PR correctly labels it a first increment. Just confirming this is intentional.

Nested lock orderfleet.KeyLock("dispatch")fleet.KeyLock("repo:rid:branch") inside createRequest. This is consistent with the existing pattern in CmdDispatch and is correct. The file-based nature of the locks means deadlock requires the same process to acquire them in the reverse order elsewhere, which it does not.

fleet_status MCP tool with empty properties — The checkArguments function correctly handles this: any caller-supplied argument is flagged as unknown. CmdStatus reads only from fleet.State (absolute path), so the missing cwd parameter is correct by design.

fleet_request migration inside the dispatch lock — The MigrateLegacyKeys() call inside CmdRequest's dispatch lock (rather than before it, as other verbs do via the top-level Dispatch path) is intentional and correct. It ensures migration and assignment write are atomic from the lock's perspective.


Test coverage — positive observations

  • TestRequestConcurrentProcesses re-executes the test binary as separate subprocesses, which correctly tests cross-process file locking rather than within-process goroutine scheduling. This is the right test for the stated guarantee.
  • TestStatusReadOnlyAndGapReporting uses a file-snapshot diff to verify status writes nothing. Solid correctness proof.
  • TestRequestReplayPreservesOriginalAfterHeadChanges covers all three replay cases: exact replay, prefix replay, and changed-payload conflict. Good.
  • TestPostWriteEvidenceOnlyObservedTarget correctly isolates write vs read tools and verifies outside-repo targets are not attributed to the branch.

Verdict

No blocking defects. The double-loop redundancy in cmdReassign/undispatch (P2) is the only issue worth fixing before merge — the rest are informational. The stated scope is properly bounded and the tests are honest about what they prove vs. what remains adapter work.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T23:03:44.057030Z 9e0a07a Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Reviewer: claude
Reviewed commit: 6eeb1498e0b58068ba69b61646aed283137e8e00

Copilot AI 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.

🟡 Changes recommended

fleet dispatch is now blocked by any unrelated malformed dispatch row due to global strict validation in replaceableDispatch, which is an avoidable operational regression.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR implements the first increment of “natural coordination” for Fleet by adding a retry-safe, repo-scoped request identity (immutable replay) and a truthful, read-only status view that separates queued work from merely observed tool activity.

Changes:

  • Add fleet request / fleet_request to record immutable request-bound assignments and refuse conflicting replays.
  • Add fleet status / fleet_status to provide a non-migrating, read-only status view with explicit evidence gaps and activity vs acceptance separation.
  • Protect request-bound dispatch rows from legacy mutation paths and record hook-owned post-tool write activity into the existing session record.
File summaries
File Description
WORK.md Work plan/intent and stop conditions for this increment.
friction-log.md Records the operational lesson and the implemented boundary (“assignment ≠ acceptance”).
cmd/fleet/testdata/test.sh Updates MCP tool-count expectations for the new tools.
cmd/fleet/README.md Documents the new request/status interface, limitations, and verification steps.
cmd/fleet/internal/verbs/work.go Adds dispatch-store serialization and guards preventing legacy mutation of request-bound assignments.
cmd/fleet/internal/verbs/verbs.go Wires new CLI entrypoints for request and status.
cmd/fleet/internal/verbs/status.go Implements read-only request-bound status rendering and JSON output.
cmd/fleet/internal/verbs/request.go Implements request ID validation, immutable replay, conflict refusal, and strict dispatch-row reading.
cmd/fleet/internal/verbs/request_test.go Adds concurrency, replay/conflict, mutation protection, and read-only/status gap tests.
cmd/fleet/internal/mcp/task_test.go Verifies MCP status does not initialize/migrate state and preserves parseable JSON-RPC error packets.
cmd/fleet/internal/mcp/mcp.go Adds fleet_request/fleet_status MCP tools and routes status via JSON-only output capture.
cmd/fleet/internal/fleet/task_activity_test.go Tests post-tool write evidence attribution/provenance rules.
cmd/fleet/internal/fleet/hook.go Records last_write evidence on PostToolUse via hook-owned session record updates.
Review details

Suppressed comments (1)

cmd/fleet/internal/verbs/work.go:254

  • The request-bound guard is duplicated: the pre-scan loop already refuses if any request-bound assignment exists for this branch, so the second if request_id != "" inside the unlink loop is unreachable.

Removing the dead check reduces complexity and ensures there’s only one refusal path to keep consistent.

		if fleet.S(r, "request_id") != "" {
			return refuse("fleet undispatch: request-bound assignment is retained for replay safety; inspect `fleet status`")
		}
  • Files reviewed: 13/13 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +488 to +497
func replaceableDispatch(rid, branch, rel string) (fleet.Rec, error) {
if _, err := strictDispatchRows(); err != nil {
return nil, err
}
existing := fleet.ReadJSON(dispatchFile(rid, branch, rel))
if fleet.S(existing, "request_id") != "" {
return nil, refuse("fleet dispatch: this assignment is request-bound; inspect `fleet status` rather than replacing it")
}
return existing, nil
}
Comment thread cmd/fleet/internal/verbs/work.go Outdated
Comment on lines +207 to +209
if fleet.S(r, "request_id") != "" {
return refuse("fleet reassign: request-bound assignment cannot be changed by an unbound reassign; inspect `fleet status`")
}

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6eeb1498e0

ℹ️ 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".

func postWriteEvidence(ev Event, sid string) Rec {
tool := S(ev, "tool_name")
cmd := S(M(ev, "tool_input"), "command")
if !IsWrite(tool, cmd) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Subscribe write tools to post-tool activity

For Codex installations generated by fleet role, cmd/fleet/internal/verbs/role.go:140 registers PostToolUse only for ^Bash$. Consequently this new evidence path is never invoked for ordinary Edit/Write/apply_patch activity, so a Codex worker can modify the assigned branch while fleet status remains Queued. Broaden the installed post-tool matcher to include the write tools handled here.

Useful? React with 👍 / 👎.

Comment thread cmd/fleet/internal/verbs/request.go Outdated
if !requestID.MatchString(id) || worker == "" || lead == "" || strings.TrimSpace(brief) == "" {
return refuse("fleet request: requires --id (1-96 letters/digits/._-), --worker, --for and --brief")
}
rid, branch, head, err := resolveDispatchTarget("request", change)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replay stored requests before resolving mutable branch state

When an identical request is retried after its branch has been deleted or its local/remote ref is otherwise unavailable, resolveDispatchTarget refuses before the existing request_id is inspected. Since request rows are intentionally retained and the README promises same-repo/same-ID replay, a caller recovering from a lost response can no longer retrieve the recorded assignment once repository state changes; look up the stored ID first and apply branch/head preconditions only to new requests.

Useful? React with 👍 / 👎.

Comment thread cmd/fleet/internal/verbs/work.go Outdated
Comment on lines +243 to +244
if fleet.S(r, "repo") == rid && fleet.S(r, "change") == branch && fleet.S(r, "request_id") != "" {
return refuse("fleet: request-bound assignments require a correlated lifecycle action; inspect `fleet status`")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow targeted removal of unrelated relationship rows

If a request-bound implementation row exists and a legacy verify row is subsequently added for the same branch, fleet undispatch <branch> --as verify is refused because this preliminary scan rejects any request row on the branch without considering rel. This leaves the unrelated legacy row impossible to retire through its targeted command; limit this guard to request rows selected by the requested relationship.

Useful? React with 👍 / 👎.

@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

Fix round 1 is pushed at 363fd07 (initial review head: 6eeb149).

Consolidated the completed Codex, Claude and Copilot findings before editing:

  • Generated Codex pre/post hooks now subscribe to file-write tools. Claude role bindings supplement the global Bash hook with local file-write observations. Configuration/rebind regression tests cover this integration boundary.
  • Identical full-ID retries survive deleted branches and removed session records. Prefixes still require unique resolution; conflicting payloads still refuse.
  • Relationship-selective legacy undispatch preserves request-bound siblings. Removed duplicate mutation guards while keeping preflight protection.
  • Legacy dispatch validates its target rather than failing because an unrelated row is damaged.
  • Local inspection additionally fixed status for exempt revoke recipients and added activity age to plain output.

Validation: Fleet race tests, focused lint/vet, both harness regression suites, work-contract validation and diff checks pass. Initial-head full-module CI passed; this head awaits CI and panel re-review. MCP still runs verbs serially; scoped ReadOnly is documented, not advertised as concurrency-safe.

Scope remains the first assignment/observation increment: no delivery, semantic acceptance, actual stop/replacement or live installation. The current request interface is implementation-only; caller-selected relationships remain follow-on work before a general-purpose adapter.

@itsHabib
itsHabib requested a lite review from Copilot September 8, 2026 04:48
@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@claude review

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 5m 53s —— View job


Re-review — PR #288: Fleet retry-safe task requests and truthful status

Reviewed fix round 1 at 363fd07 (initial review head: 6eeb149).

Tasks:

  • Load tools
  • Diff since origin/main and read changed files
  • Verify previous findings are addressed
  • Identify any new issues
  • Post findings

Verification of prior findings

All findings from the initial review and the Codex/Copilot round are addressed.

Claude P2 (dead code — duplicate request_id guard): Confirmed resolved. The second mutation loop in both cmdReassign and undispatch no longer contains a redundant request_id check. The pre-scan loop handles the guard cleanly; the second loop is now a pure mutation pass. (work.go:202–214, work.go:244–)

Codex P1 (PostToolUse only subscribed to ^Bash$): Confirmed resolved. role.go:140 now subscribes PostToolUse to ^(Bash|Edit|Write|NotebookEdit|apply_patch)$ for Codex, and claudeWriteHooks adds a separate Claude-only ^(Edit|Write|NotebookEdit)$ group to .claude/settings.local.json. Both are covered by TestGeneratedCodexHooksSubscribeToWrites and TestClaudeWriteHooksPreserveOtherHandlersOnRebind. The rebind-idempotency test (len(groups) != 2 check) correctly validates that calling claudeWriteHooks twice doesn't duplicate handlers.

Codex P2 (replay fails after branch deleted): Confirmed resolved. CmdRequest now scans retained rows for a matching request_id before calling resolveDispatchTarget. An identical-ID replay returns early without ever reaching branch resolution. TestRequestReplayAfterBranchAndSessionRemoval covers this: deletes the branch and session record, then replays, expecting success and byte-identical row.

Codex P2 (relationship-selective undispatch): Confirmed resolved. The pre-scan in undispatch now filters by relationship: (rel == "" || fleet.S(r, "relationship") == rel). TestUndispatchLegacySiblingPreservesRequest places an implementation request-bound row and a bare verify row, then undispatches --as verify, expecting the verify row to be removed and the request row to survive intact. Test passes.

Copilot P1 (global dispatch DoS from unrelated malformed row): Confirmed resolved. replaceableDispatch (work.go:478–495) now reads only the specific target file via os.Lstat + fleet.ReadJSON; it never calls strictDispatchRows. TestLegacyDispatchIgnoresUnrelatedDamage confirms: an unrelated broken JSON file in the dispatch dir does not block replaceableDispatch on an unrelated slot, but the target's own corruption does refuse correctly.


New findings

P2 — apply_patch subscribed to Codex PostToolUse but produces no evidence

cmd/fleet/internal/verbs/role.go:140, cmd/fleet/internal/fleet/hook.go:462, cmd/fleet/internal/fleet/policy.go:51–56

Codex PostToolUse now subscribes to apply_patch, which fires fleet hook codex. Inside onPostTool, postWriteEvidence calls IsWrite("apply_patch", ""):

// policy.go:51–56 — IsWrite
func IsWrite(tool, cmd string) bool {
    if FileTools[tool] {   // Edit, Write, MultiEdit, NotebookEdit — not apply_patch
        return true
    }
    return tool == "Bash" && (gitWriteRe.MatchString(cmd) || ghWriteRe.MatchString(cmd))
}

apply_patch is not in FileTools, so IsWrite returns false, postWriteEvidence returns empty, and the hook fires without recording any evidence. A Codex worker's apply_patch writes remain invisible to fleet status.

A full fix also requires preToolTarget to extract the patched file path from the patch content (currently it falls back to cwd for non-Bash, non-file-path inputs), so this can't be closed by just adding apply_patch to FileTools. The PR scope table says live delivery and adapter proof are follow-on, so this is within bounds — but worth adding to WORK.md or README.md to document the observable gap: Codex workers using only apply_patch will always show Queued, even after writing. Fix this →


Design notes (not defects)

fleet_request excluded from top-level MigrateLegacyKeys() (mcp.go:308–312) — correct. CmdRequest calls migration inside the dispatch lock for atomicity (migration + assignment are a single critical section from the lock's perspective). The exclusion prevents a redundant outside-lock call; double migration would be idempotent but philosophically wrong for the stated atomicity guarantee.

strictDispatchRows still used in cmdReassign/undispatch — intentional and correct. Both verbs scan all rows to find a branch's full set; strict validation is appropriate there. Only dispatch (the highest-frequency verb, which targets a specific file) was a candidate for narrowing, and that's what replaceableDispatch does.

statusMigrationPending() called twice in CmdStatus — the pre-read and post-read calls are a belt-and-suspenders fence for concurrent writers. Not a bug; the comment in the function correctly documents serial execution as the assumption.

claudeWriteHooks excludes apply_patch — correct by design. apply_patch is a Codex-only tool; Claude sessions never emit it and the Claude hook should not subscribe to it.


Verdict

No blocking defects. The apply_patch evidence gap (P2) is the only new issue worth tracking — it's a follow-on gap from the original Codex P1 fix, not a regression. All previously identified P1 and P2 findings are verifiably addressed with targeted tests. The review bar for this round is met.

Copilot AI 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.

🟡 Changes recommended

Hook subscription and legacy mutation paths have correctness/usability issues (missed write tools and overly strict dispatch-store scanning) that can lead to incomplete activity observation or blocked maintenance actions.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

cmd/fleet/internal/verbs/work.go:239

  • undispatch also uses strictDispatchRows(), so any unrelated corrupted dispatch row prevents retiring an otherwise valid legacy row. Since the protection needed here is specifically “don’t delete a request-bound implementation assignment”, this can be narrowed to validate that one row while keeping legacy cleanup usable.
	rows, err := strictDispatchRows()
	if err != nil {
		return err
	}
	for _, r := range rows {
  • Files reviewed: 15/15 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread cmd/fleet/internal/verbs/role.go Outdated
Comment thread cmd/fleet/internal/verbs/work.go Outdated
Comment on lines 193 to 202
rows, err := strictDispatchRows()
if err != nil {
return err
}
for _, r := range rows {
if fleet.S(r, "repo") == rid && fleet.S(r, "change") == branch && fleet.S(r, "request_id") != "" {
return refuse("fleet: request-bound assignments require a correlated lifecycle action; inspect `fleet status`")
}
}
n := 0

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 363fd07a4c

ℹ️ 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".

Comment thread cmd/fleet/internal/verbs/role.go Outdated
data["hooks"] = hooks
}
specs := [][2]string{{"SessionStart", ""}, {"UserPromptSubmit", ""}, {"PreToolUse", "^(Bash|Edit|Write)$"}, {"PostToolUse", "^Bash$"}, {"Stop", ""}, {"SessionEnd", ""}}
specs := [][2]string{{"SessionStart", ""}, {"UserPromptSubmit", ""}, {"PreToolUse", "^(Bash|Edit|Write|NotebookEdit|apply_patch)$"}, {"PostToolUse", "^(Bash|Edit|Write|NotebookEdit|apply_patch)$"}, {"Stop", ""}, {"SessionEnd", ""}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add apply_patch to the write classifier

When Codex uses apply_patch, this matcher now invokes both hooks, but IsWrite still recognizes only Edit, Write, MultiEdit, and NotebookEdit as file tools. Consequently PreToolUse skips the lease check and PostToolUse records no activity, allowing a patch on a branch held by another session while the assignment remains Queued. Fresh evidence after the earlier subscription finding is that the subscription was added without updating the classifier; align FileTools with this matcher.

Useful? React with 👍 / 👎.

Comment thread cmd/fleet/internal/verbs/status.go Outdated
Comment on lines +66 to +68
case fleet.S(fleet.M(rec, "last_write"), "key") == key && fleet.F(fleet.M(rec, "last_write"), "at") >= fleet.F(d, "at"):
row["activity_at"] = fleet.M(rec, "last_write")["at"]
taskState(row, "Activity observed", "Acceptance and completion remain unconfirmed", "Read the worker's result and current checks")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve observed activity per assigned branch

When one live worker has requests on branches A and B, a qualifying write on A initially makes A Activity observed, but the next write on B replaces the session's single last_write; this predicate then reports A as Queued and suggests delivering its brief again even though A's post-dispatch activity was already observed. Retain the latest write per branch/request rather than consulting only the worker's globally latest write.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Reviewer: claude
Reviewed commit: 363fd07a4cc0d6e69b9a44bc2672ba88e6a5b93b

@itsHabib
itsHabib requested a lite review from Copilot September 8, 2026 05:00
@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

Final allowed fix round: 9a243af21bd7f72f94e5c5af0e7bad0e4d60b1fd.

Consolidated all three completed reviews of 363fd07:

  • Added MultiEdit to generated write subscriptions.
  • Reassign/undispatch now validate selected branch/relationship rows, including malformed target filename candidates, without failing on unrelated damage. Regression covers both successful maintenance and target-damage refusal.
  • Retain hook-owned observations per branch in the existing session record, merged under the session lock. A write on branch B no longer makes previously observed branch A appear queued. Existing last_write readers remain compatible.
  • The reported apply_patch classifier bypass is not reproducible through the configured adapter: main.runHook routes codex to codex.Run, whose mappedEvents converts each patch path to an Edit before fleet.Run. Kept that existing single parser. New TestPatchAdapterEnforcesLeaseAndRecordsActivity proves refusal against a live foreign holder, unchanged foreign lease, permitted holder activity and retained evidence through the actual adapter entrypoint.

Fleet race tests, focused lint/vet, both full harness regression suites, diff checks and work-contract validation pass. CI on this head is pending. This is the final requested panel cycle under the repository's initial-plus-two-fix-round cap; any residual findings will be reported explicitly rather than starting another loop. No merge or installation performed.

@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

@claude review

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 4m 19s —— View job


Re-review — PR #288: Fleet retry-safe task requests and truthful status

Reviewed final fix round at 9a243af (previous head: 363fd07).

Tasks:

  • Load tools
  • Diff since 363fd07 and read changed files
  • Verify previous findings are addressed
  • Identify any new issues
  • Post findings

Verification of prior findings

All round-2 findings are addressed.

Claude P2 (apply_patch classifier bypass — evidence never recorded): Resolved by proof, not by adding apply_patch to FileTools. codex.Run calls mappedEvents, which converts each apply_patch path to an Edit event before any fleet.Run call — so IsWrite always sees Edit, never apply_patch. The fix correctly keeps the classifier narrow and lets the adapter boundary do the translation. TestPatchAdapterEnforcesLeaseAndRecordsActivity (codex/task_activity_test.go:12–47) proves the path end-to-end: foreign-holder refusal at PreToolUse, permitted-holder Code 0, and last_writes[key] populated after PostToolUse — all through the actual adapter entry point rather than a mock.

Copilot P1 / Codex round-2 (MultiEdit missing from write subscriptions): Confirmed resolved. codexHooks (role.go:140) now includes MultiEdit in both PreToolUse and PostToolUse matchers; claudeWriteHooks (role.go:172) likewise. TestGeneratedCodexHooksSubscribeToWrites and TestClaudeWriteHooksPreserveOtherHandlersOnRebind both updated to assert MultiEdit matches.

Codex P2 / Copilot round-2 (global strict dispatch validation blocked unrelated maintenance): Confirmed resolved. cmdReassign and undispatch now call scopedDispatchRows(rid, branch, rel) (work.go:500–530) instead of strictDispatchRows. The new function reads only files that are candidates by filename prefix (or exact path when rel is specified) and refuses the moment a selected target is unreadable or has conflicting identity, while skipping all unrelated files entirely. TestLegacyMaintenanceIgnoresUnrelatedDamage (request_test.go:412–440) covers both arms: unrelated broken file is silently skipped (reassign succeeds), then a broken target file is refused (reassign and undispatch both fail).

Codex P2 round-2 (writes on another branch erased prior observation): Confirmed resolved. touchSessionLocked (session.go:65–74) now merges the incoming last_write into rec["last_writes"][key] before the for k, v := range fields loop, preserving all prior branch entries under the session lock. status.go:57–60 reads last_writes[key] first, falling back to last_write for sessions that predate this change. TestActivitySurvivesWritesOnAnotherBranch (request_test.go:442–458) proves: a PostToolUse write on branch "task" followed by a write on branch "other" leaves the "task" evidence intact — requestStatus still returns "Activity observed".


New findings

Observation — Redundant identity conditions in undispatch request_id guard

cmd/fleet/internal/verbs/work.go:239–243

rows, err := scopedDispatchRows(rid, branch, rel)
...
for _, r := range rows {
    if fleet.S(r, "repo") == rid && fleet.S(r, "change") == branch && (rel == "" || fleet.S(r, "relationship") == rel) && fleet.S(r, "request_id") != "" {

scopedDispatchRows guarantees every returned row matches (rid, branch, rel) — those conditions are always true at this point. The effective guard is fleet.S(r, "request_id") != "". The full conditions are a harmless remnant from when the loop scanned all rows via strictDispatchRows. Not a defect; the same pattern exists in cmdReassign:197–200 (where it was intentional since that loop still has its own filter). Cosmetic only — not worth a fix round.


Design notes (not defects)

scopedDispatchRows filename prefix matching for rel="" — uses fleet.Safe(rid+"__"+branch+"__") as a fast candidate filter, with the decoded-identity check (selected) as the authoritative gate. A row that is a filename candidate but whose decoded identity doesn't match is refused rather than silently skipped. This is correctly strict: a file at a branch-prefix path with wrong content is an indication of store corruption, not an unrelated row.

last_write merge happens before the field-copy loop — the merge into rec["last_writes"] at session.go:67–73 runs before for k, v := range fields, which then writes the backward-compat rec["last_write"]. Since fields never contains "last_writes" (callers only set "last_write"), the loop does not overwrite the map that was just populated. The ordering is load-bearing and correct.

TestPatchAdapterEnforcesLeaseAndRecordsActivity is in package codex — correctly exercises codex.Run (the actual adapter entry point) rather than fleet.Run directly. The test is placed where the claim lives: the adapter's lease check and evidence path, not the classifier.


Verdict

No blocking defects. All P1 and P2 findings from both prior rounds are verifiably addressed with targeted tests. The one new observation (redundant conditions in the undispatch guard) is cosmetic and within the stated scope of residuals for the judge. The review bar for this final allowed round is met.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Reviewer: claude
Reviewed commit: 9a243af21bd7f72f94e5c5af0e7bad0e4d60b1fd

Copilot AI 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.

🟡 Changes recommended

New dispatch-store locking paths can surface fleet.ErrKeyBusy as a generic error instead of a user-actionable refusal, which is a user-visible behavior regression that should be corrected before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

cmd/fleet/internal/verbs/request.go:72

  • If the dispatch-store lock times out, fleet.KeyLock("dispatch", ...) returns fleet.ErrKeyBusy. Right now that bubbles up as a generic error (exit code 4) even though ErrKeyBusy is meant to be a refusal with next-action guidance. Handle fleet.ErrKeyBusy explicitly so a busy store reads as an expected concurrency refusal, not an internal error.

cmd/fleet/internal/verbs/work.go:183

  • fleet.KeyLock("dispatch", ...) can return fleet.ErrKeyBusy, but this wrapper currently bubbles it up as a generic error (exit code 4) instead of a refusal. Since this is a user-visible concurrency condition, map it to a refuse(...) message (similar to other verbs like fleet take).
func CmdReassign(change, forRole string) error {
	return fleet.KeyLock("dispatch", func() error { return cmdReassign(change, forRole) })
}

cmd/fleet/internal/verbs/work.go:225

  • fleet.KeyLock("dispatch", ...) can return fleet.ErrKeyBusy, but cmdUndispatch currently propagates it as a generic error (exit code 4) instead of a refusal. Translate ErrKeyBusy into a refuse(...) so callers get a consistent user-actionable refusal.
func cmdUndispatch(change, rel string) error {
	return fleet.KeyLock("dispatch", func() error { return undispatch(change, rel) })
}
  • Files reviewed: 17/17 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines 99 to +103
func CmdDispatch(change, rel, forRole, due, slot, brief, by, replyTo string, take bool) error {
return fleet.KeyLock("dispatch", func() error {
return cmdDispatch(change, rel, forRole, due, slot, brief, by, replyTo, take)
})
}

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a243af21b

ℹ️ 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".

Comment thread cmd/fleet/internal/verbs/request.go Outdated
Comment on lines +34 to +35
wanted := fleet.Rec{"request_id": id, "repo": rid, "change": branch,
"worker": worker, "for": lead, "brief": strings.TrimSpace(brief), "relationship": "implementation"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Canonicalize branch names before keying requests

On Windows, Git can resolve a differently-cased spelling such as Task to the existing branch task (the existing fleet.BranchSpelling helper handles this case), but this record preserves the caller's spelling. The request then checks leases and status under repo:<id>:Task, while hooks derive repo:<id>:task from HEAD, so it can miss an existing holder and never match the worker's activity. Store the branch's canonical ref spelling before constructing the request record and key.

Useful? React with 👍 / 👎.

Comment on lines +160 to +162
row := fleet.ReadJSON(filepath.Join(dispatchDir(), entry.Name()))
if row == nil || fleet.S(row, "repo") == "" || fleet.S(row, "change") == "" || fleet.S(row, "relationship") == "" {
return nil, fmt.Errorf("assignment evidence unreadable: %s", entry.Name())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject incomplete request rows before deriving activity

A valid JSON row carrying request_id passes this check even when request-specific evidence such as worker or numeric at is missing or malformed. In particular, requestStatus reads a missing/invalid at as zero, so any old matching last_write from a live worker is reported as Activity observed and the packet remains complete: true despite the damaged assignment evidence. Validate the required request fields and types before returning the row.

Useful? React with 👍 / 👎.

@itsHabib

itsHabib commented Sep 8, 2026

Copy link
Copy Markdown
Owner Author

Final panel consolidated on code head 9a243af21bd7f72f94e5c5af0e7bad0e4d60b1fd; documentation-only follow-up records residuals in FOLLOWUPS.md and WORK.md. No fourth panel request.

Claude: no blocking defects, one cosmetic redundancy. Copilot: busy dispatch lock surfaces generic error instead of refusal. Codex: Windows branch spelling needs canonicalization; request-specific fields/types need validation to prevent damaged timestamp evidence from admitting old activity. These findings are recorded with impact and proposed deferral rationale, not dismissed or accepted by me.

Code-head CI (full check, fuzz, hygiene) and both local harness suites/race/lint checks passed. No merge, gate judgment, installation or live worker control performed. This first increment is built and reviewed but retains the recorded P2s for judgment. Broader natural delivery/acceptance/questions/stop/replacement work remains in progress.

@itsHabib

itsHabib commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Landing refresh at 799313e430fb4859b07cfa1d61a3c448ab57bab8 includes merged #292 from main and preserves both sides of the append-only friction log conflict. The merged watcher code and request activity code coexist in session.go; the merge resolved automatically, while the only manual conflict was friction-log.md. Existing FOLLOWUPS.md retains Windows branch spelling, request-field validation and busy-store diagnostics as proposed P2 deferrals, not accepted findings.

Fresh local Fleet race tests pass. Full remote CI is running. Final independent code-head review was 9a243af21bd7f72f94e5c5af0e7bad0e4d60b1fd; no fresh final-head panel completion is claimed. The existing capped review rounds are preserved; no new panel request is being made. This comment records evidence and proposed dispositions for the governed judgment only. No live installation or custody change.

@itsHabib

itsHabib commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Current integration head 8dc96bd incorporates merged #289/#291/#292. Combined Fleet race suite passes. Role/work source files merged automatically; the manual FOLLOWUPS conflict was additive and both PRs' residuals are preserved. Prior watcher refresh had identical feature diff hunks to reviewed9a243af after excluding containing-file index hashes.

No fourth panel round. Final independent reviews cover9a243af, not this current head; exact-head freshness and the recorded Windows branch-case/status timestamp/lock-diagnostic residuals require an explicit governed judgment. This is evidence/disposition bookkeeping, not acceptance or merge authority. Final CI is running; no live installation or session/custody changes.

@itsHabib

itsHabib commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Final integration head 977cf74 includes all four landed Fleet/Org fixes (#289/#291/#292/#293). The only last source conflict was the help-string additions; both request/status and inspect-hooks are preserved. Dispatch retains all read-only entry points before migration.

Documented validation now passes on the combined tree: Fleet race suite, vet, zero lint issues, Claude reference suite (364 scenarios), Codex reference suite (367 scenarios), and full CI plus native Mac/Windows seats. The reference suite initially failed only because it required the old "already running" watcher diagnostic. Commit977cf74 strengthens that assertion to rc1 plus "watcher lock unavailable" and absence of the old claim; no runtime policy changed.

Three original review cycles remain capped. Existing FOLLOWUPS.md still records the Windows branch-spelling, invalid request-field timestamp and busy-store diagnostic residuals for an explicit judgment. No independent final-head panel completion is claimed, and fixture tests do not prove live harness wiring. No live hook or ownership changes.

@itsHabib

itsHabib commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Governed landing decision: BLOCKED, Gate run run_76305f9bfe24ed5f, exact head 977cf747ee26a077a194ac8f215e2a1799997f27. The same-run Codex judge did not accept two proposed residual deferrals:

  1. CmdRequest retains caller branch spelling rather than canonical Git branch identity, so Windows case-insensitive ref lookup can disagree with ownership/activity keys. A fix must preserve replay after branch deletion.
  2. strictDispatchRows does not validate request-specific worker/timestamp evidence; malformed rows can still support complete:true. Validate the required fields/types before replay or status derivation.

The expanded write subscriptions are verified addressed. Busy-store exit classification and redundant conditions are nonblocking. Old-head reviews are not represented as current-head completion. Full CI, native seats checks, Fleet race/vet/lint, and 364 Claude / 367 Codex fixtures passed, but those do not remove the two correctness defects.

No merge, override, grant mint or automatic review-loop extension. The two original repair rounds are exhausted under AGENTS.md; landing pauses for an explicitly bounded repair decision. The existing grant tier/cycle budget is not the issue.

…ws never read complete

Operator-authorized repair of the two blockers from the same-run judgment
at 977cf74. (1) CmdRequest keyed ownership and activity by the caller's
spelling; on a case-insensitive filesystem two spellings keyed two
assignments for one branch. canonicalBranch resolves the spelling from
git's own ref list (exact, else unique case-insensitive match). (2)
strictDispatchRows accepted a request-bound row with no worker, lead or
timestamp, so damaged evidence could still report complete:true; such a
row is now an error for status and refuses new requests. One test each.
Suite 364/364, race tests, lint 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@itsHabib

itsHabib commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review
@claude review at this head — operator-authorized repair of the two blockers from the last judgment: canonical branch spelling in CmdRequest, and strict validation of request-bound rows.

@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 5m 21s —— View job


Re-review — PR #288: Fleet retry-safe task requests and truthful status

Reviewed operator-authorized repair at 376f3b9 (prior final judgment head: 977cf74).

Tasks:

  • Load tools
  • Diff since prior judgment head (977cf74) and read changed files
  • Verify blocker 1: canonical branch spelling in CmdRequest
  • Verify blocker 2: strict validation of request-bound rows
  • Identify any new issues
  • Post findings

Scope of this commit

One commit since the judgment head: 376f3b9 fix(fleet): request keys by git's branch spelling; damaged request rows never read complete.
Two files changed: cmd/fleet/internal/verbs/request.go (+36 lines), cmd/fleet/internal/verbs/request_test.go (+48 lines). Both blockers addressed in one commit. No other files modified.


Verification of blocker 1 — canonical branch spelling

cmd/fleet/internal/verbs/request.go:37, 181–203

canonicalBranch(cwd(), branch) is called immediately after the rid check and before constructing wanted, so the stored change field always uses git's spelling when the branch exists:

branch = canonicalBranch(cwd(), branch)
wanted := fleet.Rec{"request_id": id, "repo": rid, "change": branch, ...}

The implementation runs git for-each-ref --format=%(refname:short) refs/heads and:

  • Returns candidate unchanged on exact match (fast path).
  • Returns git's spelling on a unique case-insensitive match.
  • Returns caller's spelling on ambiguity (two refs differing only by case — git itself is ambiguous) or on git failure.
  • Returns caller's spelling when no branch exists yet (new branch keeps its name).

This is the correct precedence. The call happens before the fleet.KeyLock("dispatch", ...) critical section, keeping subprocess latency out of the lock window.

TestRequestKeysByGitSpellingNotCallerSpelling (request_test.go:460)

Creates a repo with branch "Nav-Fix", calls CmdRequest("nav-fix", "case-1", ...), then verifies:

  1. The dispatch file exists at Nav-Fix/implementation (git's spelling, not the caller's).
  2. The stored change field is "Nav-Fix".
  3. A second call with "NAV-FIX" and ID "case-2" is refused — canonicalized to "Nav-Fix", finds an existing assignment, refuses as a duplicate.

This covers the primary concern: hooks and request records now key on the same spelling while the branch exists.

Design note — cross-case replay after branch deletion (not a new defect)

TestRequestReplayAfterBranchAndSessionRemoval (from round 1) replays with identical caller spelling "task" after the task branch is deleted. This continues to work: canonicalBranch("task") falls back to "task", sameRequest compares stored "task" == wanted "task", replay succeeds. ✓

The fix does not cover cross-case replay after deletion (e.g., stored as "Nav-Fix", replayed with "nav-fix" after the branch is deleted — fallback gives "nav-fix", sameRequest fails). This was called out in FOLLOWUPS.md under "Windows branch spelling" and is a known residual within the stated scope of this increment: the fix ensures records and hooks agree while the branch exists, which is the condition under which assignments are live and hooks fire.


Verification of blocker 2 — strict validation of request-bound rows

cmd/fleet/internal/verbs/request.go:168–172

Added to strictDispatchRows, inside the per-row loop, immediately after the base field check:

if fleet.S(row, "request_id") != "" && (fleet.S(row, "worker") == "" || fleet.S(row, "for") == "" || fleet.F(row, "at") <= 0) {
    return nil, fmt.Errorf("assignment evidence damaged: %s", entry.Name())
}

fleet.F returns 0 for missing, null, or non-numeric values; fleet.Now() always produces a positive Unix timestamp, so <= 0 correctly catches all damaged at cases. The check is scoped to request-bound rows (request_id != "") so legacy dispatch rows without these fields are unaffected.

When strictDispatchRows returns an error, CmdStatus adds it to gaps, writes the JSON packet with complete: false before returning, then returns a non-nil error — the packet is written to Out before the error return. CmdRequest propagates the error directly, blocking new assignments beside damaged evidence.

TestStatusRefusesDamagedRequestEvidence (request_test.go:479)

Creates a valid request, then for each of {"worker", "at", "for"}:

  • Writes a version of the original row with that field removed.
  • Calls fleet status --json, captures output, unmarshals, asserts err != nil && packet["complete"] == false.
  • Calls CmdRequest("task", "two", ...) and asserts it fails.

Each iteration overwrites the previous broken file with a freshly derived broken record from the original, so iterations are independent. All three damage cases are proved. ✓


No new blocking defects

The two fixes are focused and minimal. Both address exactly the stated blockers, introduce no new logic paths beyond the repair, and are covered by targeted tests. The redundant identity conditions in the undispatch guard (cosmetic, noted in the final prior round) remain and are unchanged — still within the recorded residuals for judgment.


itsHabib added a commit that referenced this pull request Sep 9, 2026
Kept verbatim under its own heading so #288 resolves the same-file add by
taking this union.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 376f3b9a4d

ℹ️ 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".

Comment thread cmd/fleet/internal/verbs/request.go Outdated
// Ownership and activity are keyed by the branch as git spells it, never by the
// caller's spelling: on a case-insensitive filesystem two spellings would
// otherwise key two assignments for one branch.
branch = canonicalBranch(cwd(), branch)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve deleted-branch retries after canonicalization

When the initial request uses a case variant (for example nav-fix for local branch Nav-Fix), this stores Nav-Fix; after that branch is deleted, the identical retry canonicalizes back to the caller's nav-fix, so validateReplay rejects it as different work. This breaks the documented full-ID retry behavior after branch deletion. Fresh evidence beyond the earlier replay finding is that the new canonicalization still derives replay identity from mutable refs before consulting the retained row; replay comparison should preserve or use the stored branch spelling.

Useful? React with 👍 / 👎.

Comment thread cmd/fleet/internal/verbs/request.go Outdated
}
// A request-bound row carries who it went to and when; without either it is
// damaged evidence, and damaged evidence must not read as a complete status.
if fleet.S(row, "request_id") != "" && (fleet.S(row, "worker") == "" || fleet.S(row, "for") == "" || fleet.F(row, "at") <= 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.

P2 Badge Reject non-string request IDs in stored rows

When valid JSON contains a non-string request_id (or an invalid string outside the accepted ID grammar), this guard does not validate it: fleet.S returns empty for a non-string value, bypassing the request-specific checks, and CmdStatus subsequently omits the row while returning complete: true. Fresh evidence beyond the prior damaged-row finding is that the new guard uses the successfully decoded string itself to decide whether the row is request-bound; inspect field presence/type and validate the stored ID before that decision.

Useful? React with 👍 / 👎.

…ng; non-string request_id is damaged evidence

Codex at 376f3b9. sameRequest compares the change case-insensitively,
since a retry after the branch is gone can only offer the caller's
spelling of a branch the store keyed by git's. strictDispatchRows treats
a present request_id that is not a string in the ID grammar as damaged
rather than silently non-request. One test each. Its P1 (Codex post-tool
matcher only ^Bash$) is stale at this head: role.go already registers the
write tools.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@itsHabib

itsHabib commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review
@claude review at this head: replay case-fold and non-string request_id fixes; your P1 on the Codex post-tool matcher is already addressed in role.go (matcher covers Edit|Write|MultiEdit|NotebookEdit|apply_patch).

@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 3m 27s —— View job


Re-review — PR #288: Fleet retry-safe task requests and truthful status

Reviewed operator-authorized follow-up at bd4ea8d (prior judgment head: 376f3b9).

Tasks:


Scope of this commit

One commit since the judgment head: bd4ea8d fix(fleet): replay survives branch deletion under the caller's spelling; non-string request_id is damaged evidence.
Two files changed: cmd/fleet/internal/verbs/request.go (+10 lines, −4 lines), cmd/fleet/internal/verbs/request_test.go (+32 lines). Both Codex P2s addressed in one commit.


Verification of Codex P2 #1 — replay survives branch deletion after canonicalization

cmd/fleet/internal/verbs/request.go:101–111

sameRequest removes "change" from the exact-match loop and instead compares it case-insensitively at the end:

func sameRequest(a, b fleet.Rec) bool {
    for _, key := range []string{"request_id", "repo", "worker", "for", "brief", "relationship"} {
        if fleet.S(a, key) != fleet.S(b, key) {
            return false
        }
    }
    // The stored change is git's spelling; a retry after the branch is gone can
    // only offer the caller's, so the same branch spelled differently by case is
    // the same work, as it would be on the filesystem that keyed it.
    return strings.EqualFold(fleet.S(a, "change"), fleet.S(b, "change"))
}

The fix is correct. The scenario is:

  1. Initial request: CmdRequest("nav-fix", ...)canonicalBranch("nav-fix")"Nav-Fix" (git's spelling). Stored change = "Nav-Fix".
  2. Branch deleted. Retry: CmdRequest("nav-fix", ...)canonicalBranch("nav-fix") → fallback "nav-fix" (branch gone). wanted["change"] = "nav-fix".
  3. Scan finds row with request_id = "case-3". Calls validateReplay. sameRequest checks EqualFold("Nav-Fix", "nav-fix") → true. Replay accepted. ✓

The fix doesn't weaken conflict detection: a retry with a genuinely different branch (different case-insensitive name) returns false. An attempt to reassign the same request_id to a different branch is still blocked because EqualFold("Nav-Fix", "task")false.

TestRequestReplaySurvivesBranchDeletionUnderCallerSpelling (request_test.go:508)

Creates branch Nav-Fix, dispatches with "nav-fix" (stores "Nav-Fix" via canonical), deletes Nav-Fix, then replays with "nav-fix" and expects success. Clean, minimal, and addresses precisely the documented gap. ✓

Design note — cross-case replay correctness on case-sensitive filesystems. The fix is narrow and correct: it allows case-insensitive match on a field that git itself is case-insensitive about (on any filesystem where the branch could exist). On a case-sensitive Linux filesystem with two distinct branches Nav-Fix and nav-fix, the ambiguity fallback in canonicalBranch returns the caller's spelling unchanged, so both would key under their own exact spelling — and a cross-case replay attempt (stored "Nav-Fix", retry "nav-fix") after both are deleted would succeed via EqualFold. The FOLLOWUPS.md residual ("Windows branch spelling") acknowledges that full cross-case post-deletion correctness remains a follow-on; the fix closes the common case.


Verification of Codex P2 #2 — non-string request_id is damaged evidence

cmd/fleet/internal/verbs/request.go:173–178

if id, present := row["request_id"]; present {
    ids, ok := id.(string)
    if !ok || !requestID.MatchString(ids) || fleet.S(row, "worker") == "" || fleet.S(row, "for") == "" || fleet.F(row, "at") <= 0 {
        return nil, fmt.Errorf("assignment evidence damaged: %s", entry.Name())
    }
}

The previous guard used fleet.S(row, "request_id") != "", which returns "" for any non-string JSON value (number, bool, null, array, object). A row with request_id: 7.0 would have fleet.S(...) == "" → condition false → row admitted as non-request-bound → CmdStatus would report complete: true with no gap. The fix directly inspects the raw any value: if the key is present but not a string, !ok → damaged. ✓

The !requestID.MatchString(ids) arm extends this to syntactically invalid string IDs (e.g., empty string, illegal chars), closing the related gap where fleet.S returns "" on a missing value and a stored "" request_id would have passed the old != "" check.

TestStatusRefusesNonStringRequestID (request_test.go:520)

Creates a valid request (for branch "task"), reads the dispatch file, sets request_id = 7.0, writes back. Calls status --json, unmarshals output. Asserts err != nil && packet["complete"] == false. The CmdStatus path is correct: strictDispatchRows appends the error to gaps, packet is written with complete: false before the final refuse(...), so both assertions hold. ✓


Verification of Codex P1 (post-tool matcher) — already resolved

cmd/fleet/internal/verbs/role.go:140

specs := [][2]string{..., {"PreToolUse", "^(Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch)$"}, {"PostToolUse", "^(Bash|Edit|Write|MultiEdit|NotebookEdit|apply_patch)$"}, ...}

Codex PostToolUse matcher covers apply_patch (and MultiEdit). Claude's claudeWriteHooks at line 172 uses ^(Edit|Write|MultiEdit|NotebookEdit)$ — correctly excluding apply_patch since Claude sessions never emit it. Confirmed resolved from prior round as stated. ✓


No new blocking defects

The two commits since the judgment head (376f3b9, bd4ea8d) address exactly the two stated Codex P2s with minimal, targeted changes. No new logic paths are introduced. The remaining recorded FOLLOWUPS.md residuals (Windows cross-case post-deletion replay, request-field metadata validation now extended by the type-check fix, busy-lock diagnostics, and the cosmetic redundant conditions in undispatch) are unchanged and remain for the judgment record.


@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Provided git ref bd4ea8dc6ed1171733ba0d01257e8793d727f07f does not exist
ℹ️ 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".

itsHabib added a commit that referenced this pull request Sep 9, 2026
Review P1: it documented request/status verbs that do not exist on main,
plus two transient lines. The section returns with #288's own README.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…solves, the caller's when it does not

Judge counterexample at bd4ea8d: sameRequest folded case, so an ID
recorded for Nav-Fix replayed against a distinct nav-fix on a
case-sensitive filesystem. The row now also stores the caller's spelling
(requested). canonicalBranch reports whether the name resolved to a ref;
a replay compares change exactly when it did, and the caller's original
spelling exactly when it did not (the deleted-branch retry). Regression
covers the counterexample and both retry cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@itsHabib

itsHabib commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review
@claude review at this head: replay identity is now exact (git spelling when the ref resolves, caller spelling when it does not); the case-fold counterexample from the judge is covered by TestReplayNeverMatchesADistinctRefByCase.

@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown

Claude finished @itsHabib's task in 2m 57s —— View job


Re-review — PR #288: Fleet retry-safe task requests and truthful status

Reviewed latest commit at HEAD 9e0a07a (since prior reviewed head bd4ea8d).

Tasks:

  • Load tools
  • Diff since bd4ea8d and read changed files
  • Verify claim: replay identity is exact (git spelling when ref resolves, caller's when not)
  • Verify claim: TestReplayNeverMatchesADistinctRefByCase covers the case-fold counterexample
  • Identify any new issues
  • Post findings

Scope of this commit

One commit since the prior reviewed head: 9e0a07a fix(fleet): replay identity is exact — git's spelling when the ref resolves, the caller's when it does not.
Two files changed: cmd/fleet/internal/verbs/request.go (+23/−16), cmd/fleet/internal/verbs/request_test.go (+20).


Verification of the judge's blocker — exact replay identity

cmd/fleet/internal/verbs/request.go:37–39, 108–118, 194–216

The fix introduces a found bool return from canonicalBranch and threads it through validateReplaysameRequest. The invariant held by sameRequest(a, b, resolved bool) is:

  • resolved = true (branch name resolved to an existing git ref): compare change fields with exact string equality. Two refs differing only by case are distinct on a case-sensitive filesystem and will not match.
  • resolved = false (branch deleted or git failure): compare requested fields (the caller's original pre-canonicalization spelling) with exact string equality. An identical retry after branch deletion succeeds because the caller provides the same spelling both times; a different spelling fails.

This is correct and matches what the judge required. The old strings.EqualFold path is fully replaced — sameRequest now has no case-insensitive comparison of any kind.

canonicalBranch ambiguity case — when two case-variant refs both exist (e.g., Nav-Fix and nav-fix), the caller with an exact match hits the name == cand early return with true, so the ambiguity only fires for a caller whose spelling matches neither exactly. In that case, the function now returns (cand, true) rather than the previous (cand, false). With resolved=true, the stored change is the caller's exact spelling and replay requires the exact same spelling. Correct: an ambiguous spelling creates an unambiguous key.

The requested field is written on every new request row as the caller's pre-canonicalization spelling. This field is what makes deleted-branch replay work: after deletion, canonicalBranch falls back to the caller's spelling with found=false, and sameRequest compares both records' requested fields exactly.


Verification of TestReplayNeverMatchesADistinctRefByCase

cmd/fleet/internal/verbs/request_test.go:540–558

The test exercises sameRequest directly — appropriate because the case-sensitivity concern is a cross-platform guarantee that the filesystem shouldn't decide.

Three cases:

  1. resolved=true, stored change=Nav-Fix vs wanted change=nav-fixfalse. The judge's counterexample: an ID for Nav-Fix cannot replay against the distinct ref nav-fix. ✓
  2. resolved=false, stored {change: "Nav-Fix", requested: "nav-fix"} vs wanted {change: "nav-fix", requested: "nav-fix"}true. Deleted-branch retry with the original caller spelling succeeds. ✓
  3. resolved=false, same stored row vs wanted with requested=Nav-Fixfalse. A differently-spelled retry after deletion is rejected. ✓

TestRequestReplaySurvivesBranchDeletionUnderCallerSpelling (from bd4ea8d) additionally exercises the full CmdRequest path: creates Nav-Fix, calls with "nav-fix" (stores change=Nav-Fix, requested=nav-fix), deletes the branch, replays with "nav-fix" — expects success. This test runs through canonicalBranch and the dispatch store, not just the unit function. Both tests pass. ✓


One observation (not a blocker)

Legacy rows without "requested" fail deleted-branch replay.

Rows created by commits before 9e0a07a (including 376f3b9 and bd4ea8d) don't have a "requested" field. On the resolved=false path, fleet.S(legacyRow, "requested") returns "", which won't match any non-empty caller spelling, so an identical deleted-branch retry would be refused as a conflict for those rows. This affects only records created in the window between 376f3b9 (which introduced canonicalBranch returning string) and 9e0a07a (which added requested), where the branch has since been deleted. Given the first-increment scope and the narrow window, this is within the recorded FOLLOWUPS.md residuals, not a new defect.


Verdict

No blocking defects. The fix closes the judge's counterexample exactly: replay identity is strict equality when the ref resolves (preventing cross-case reuse of an ID between distinct branches) and strict caller-spelling equality when it does not (preserving the deleted-branch retry guarantee). The test exercises all three discriminating cases on sameRequest directly, making the guarantee platform-independent. The recorded residuals (Windows cross-case post-deletion replay, the legacy-requested observation above) remain for the judgment record.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e0a07aa60

ℹ️ 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 name == cand {
return cand, true
}
if name != "" && strings.EqualFold(name, cand) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict case folding to case-insensitive filesystems

On a case-sensitive filesystem, if only Nav-Fix exists and the caller requests nav-fix, Git does not resolve that spelling, but this unconditional EqualFold match rewrites it to Nav-Fix; resolveDispatchTarget then succeeds and records an assignment for the wrong branch instead of rejecting the unresolved caller input. Only apply case-insensitive canonicalization where Git/filesystem semantics actually make the spellings equivalent (the existing fleet.BranchSpelling helper already makes that platform distinction).

Useful? React with 👍 / 👎.

Comment on lines +180 to +183
if id, present := row["request_id"]; present {
ids, ok := id.(string)
if !ok || !requestID.MatchString(ids) || fleet.S(row, "worker") == "" || fleet.S(row, "for") == "" || fleet.F(row, "at") <= 0 {
return nil, fmt.Errorf("assignment evidence damaged: %s", entry.Name())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate the remaining request identity fields

Fresh evidence after the earlier damaged-row finding is that this new guard still accepts request rows whose required brief or requested field is missing or non-string. Such a row makes fleet status report complete: true with an empty assignment brief, and deleting requested also makes the documented identical replay fail after branch deletion because sameRequest compares the missing value against the caller spelling; validate these immutable request fields before admitting the row.

Useful? React with 👍 / 👎.

@itsHabib
itsHabib merged commit 8e1d480 into main Sep 10, 2026
10 checks passed
itsHabib added a commit that referenced this pull request Sep 10, 2026
…ask-coordination increment

#288's section kept under its own heading now that request/status are on
main; its two transient lines (a personal path 'during this build', 'This
PR') dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
itsHabib added a commit that referenced this pull request Sep 10, 2026
* docs(fleet): README for cmd/fleet

The one cmd/ directory without one. What the binary is, the four faces, the
store, install/shadow/switch, ownership rows, the verb groups, testing and
what is deliberately absent. Facts checked against main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(fleet): fold #288's task-coordination section into the README

Kept verbatim under its own heading so #288 resolves the same-file add by
taking this union.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(fleet): say what it is for — leads, seats, location as identity

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(fleet): drop the task-coordination section until #288 is on main

Review P1: it documented request/status verbs that do not exist on main,
plus two transient lines. The section returns with #288's own README.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(fleet): handoff is a control verb; the watcher writes its own board files

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(fleet): repair the sentence the transient-line drop cut

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(fleet): last pull-request phrasing out of the README

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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