Skip to content

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

Open
itsHabib wants to merge 4 commits into
mainfrom
feat/fleet-task-coordination
Open

Fleet: retry-safe task requests and truthful status#288
itsHabib wants to merge 4 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-08T05:08:25.496018Z 9a243af 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 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.

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