feat(copilot-cli): track subagent tasks - #2341
Merged
Merged
Conversation
Entire-Checkpoint: 01M2458H6H6F8Y4GVHCT2DFCY2
Entire-Checkpoint: 01M245A95XMM1PPDRYFXG2BSSQ
…-tracking # Conflicts: # e2e/testutil/repo.go
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Two moderate findings remain, along with one documentation nit.
Pull request overview
Adds Copilot CLI subagent lifecycle tracking and durable task records, including compatibility handling for older Copilot versions.
Changes:
- Correlates subagents and attributes child file changes.
- Preserves transcript-unavailable task metadata through checkpoint condensation.
- Adds tests, E2E coverage, hook support, documentation, and fixture updates.
File summaries
| File | Review summary |
|---|---|
redact/betterleaks_env_test.go |
Quotes module replacement paths. |
e2e/testutil/repo.go |
Adds Copilot repository fixtures. |
e2e/testutil/assertions.go |
Adds durable task-record assertions. |
e2e/tests/subagent_commit_flow_test.go |
Verifies Copilot task persistence. |
docs/architecture/agent-guide.md |
Documents Copilot subagent hooks. |
cmd/entire/cli/strategy/manual_commit_git.go |
Propagates task completion metadata. |
cmd/entire/cli/strategy/manual_commit_condensation.go |
Materializes unavailable transcripts. Moderate (1 vote): Validate record.AgentID before appending reason-only payloads. |
cmd/entire/cli/strategy/manual_commit_condensation_test.go |
Tests condensation safety. |
cmd/entire/cli/session/state.go |
Persists transcript availability state. |
cmd/entire/cli/session/state_test.go |
Tests state serialization. |
cmd/entire/cli/lifecycle.go |
Supports completion-only task records. Moderate (1 vote): Use state.IsEnded() to prevent task creation after parent finalization. |
cmd/entire/cli/lifecycle_test.go |
Tests completion and session-ending behavior. |
cmd/entire/cli/agent/event.go |
Adds shared completion and transcript flags. |
cmd/entire/cli/agent/copilotcli/types.go |
Defines Copilot hook configuration fields. |
cmd/entire/cli/agent/copilotcli/transcript.go |
Correlates child events and extracts files and tokens. |
cmd/entire/cli/agent/copilotcli/transcript_test.go |
Tests transcript parsing and correlation. |
cmd/entire/cli/agent/copilotcli/lifecycle.go |
Handles Copilot subagent hooks. |
cmd/entire/cli/agent/copilotcli/lifecycle_test.go |
Tests correlation and fail-closed behavior. |
cmd/entire/cli/agent/copilotcli/hooks.go |
Installs and validates subagent hooks. |
cmd/entire/cli/agent/copilotcli/hooks_test.go |
Tests hook installation and drift detection. |
cmd/entire/cli/agent/copilotcli/compat.go |
Parses Copilot identities and event formats. |
cmd/entire/cli/agent/copilotcli/AGENT.md |
Documents Copilot integration. Nit (1 vote): Refer to session.shutdown.modelMetrics instead of agentMetrics. |
Review details
Suppressed comments (3)
cmd/entire/cli/agent/copilotcli/AGENT.md:230
- The documented shutdown field is inconsistent with the implementation and fixtures: Copilot data is parsed from
session.shutdown.modelMetricsintranscript.go, while this saysagentMetrics. Please use the actual field name so maintainers do not look for or implement support for a nonexistent schema field.
`session.shutdown.agentMetrics` is authoritative but is appended only after
cmd/entire/cli/lifecycle.go:1394
- Use the canonical
state.IsEnded()predicate here instead of checking onlyPhaseEnded.EndedAtis the other half of the finalized-state invariant and can be present whilePhaseis still active (for legacy or partially written state); with that shape, a completion-only Copilot stop can create a task record after the parent has already been finalized, bypassing the zombie guard.
if event.CompletionWithoutLaunch && state.Phase == session.PhaseEnded {
cmd/entire/cli/strategy/manual_commit_condensation.go:374
- This early
TranscriptUnavailablebranch bypasses theAgentIDvalidation below, so a malformed or untrusted CopilotagentIdcan produce a payload that reacheswriteTaskRecordEntriesand makes condensation fail withinvalid task payloadinstead of skipping the poisoned record as the surrounding contract requires. Validaterecord.AgentIDbefore appending this reason-only payload (or validate it at correlation time).
if record.TranscriptUnavailable {
payload.TranscriptUnavailableReason = taskTranscriptReasonUnresolvable
payloads = append(payloads, payload)
continue
- Files reviewed: 22/22 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Entire-Checkpoint: 01M247HJFJKCH4E77D025HYWEG
Entire-Checkpoint: 01M248VBDZNV4HGRDANKJRT61R
Entire-Checkpoint: 01M26CWTZVWQ9B880TRM84FTV5
Soph
approved these changes
Sep 10, 2026
timothybrush
pushed a commit
to timothybrush/cli-2
that referenced
this pull request
Sep 15, 2026
filterToUncommittedFiles decided "already committed" by comparing the working tree's raw bytes against the HEAD blob. Under core.autocrlf — the Git for Windows default, and what both testutil.InitRepo and e2e/testutil/repo.go set — the working tree holds CRLF while the blob holds LF, so every committed text file compared unequal and the filter never dropped anything. That matters because the filter is what stops an agent's mid-turn commit from being checkpointed twice. With it broken, turn-end saw a file that PostCommit had already condensed, missed the "no changes, skip" gate, and minted a fresh shadow branch on the *new* HEAD seconds after PostCommit deleted the old one. Nothing condenses that branch away — the session ends and no further commit arrives — so it outlives the session. On the nightly install smoke that surfaced as copilot-cli failing TestMultiSessionSequential on windows-latest with "shadow branches should be cleaned up within 10s after commit", every night since 09-11. The condition was latent from the start and only became reachable when copilot-cli's turn-end file list stopped being empty. The last green nightly ran the same broken filter, so its list was genuinely empty rather than correctly filtered; the only extraction change in that window is entireio#2341's restrictedProperties.filePaths fallback. That fallback follows Copilot moving the field out of properties, so both halves are needed to explain the flip — and neither is at fault: reporting a path the agent later committed is correct at that layer, and narrowing it to what is still uncommitted is this function's job. The three other Windows agents produce no turn-end list at all and so never reached the comparison. Compare through gitrepo.HashWorktreeFiles (git hash-object) instead, which applies Git's path-specific clean filters. This also fixes .gitattributes eol/text rules and clean filters such as Git LFS, which the byte comparison got wrong for the same reason. Symlinks stay on the raw path: hash-object follows the link and hashes the target's content, while a Git symlink blob stores the target path. Anything Git cannot hash falls back to the previous comparison, so the function still fails open — toward keeping a file rather than dropping one. Both call sites benefit: turn-end and the subagent task path, whose own comment already described this exact failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M2JJHKDTXB4SAFGYRC6FWRWJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://entire.io/gh/entireio/cli/trails/1282
Summary
Compatibility and behavior
Copilot CLI 1.0.82 exposes the child UUID and per-file tool telemetry, allowing exact child-file attribution. Copilot CLI 1.0.63 omits the ID from subagentStop, so Entire correlates only one unambiguous unfinished subagent.started event with the same agent name. Ambiguous matches fail closed. Copilot does not expose a child transcript path, so the task record records why that transcript is unavailable.
Validation
Rollout
No migration is required. Existing Copilot sessions without subagent events retain their current behavior.
Note
Medium Risk
Touches session lifecycle, task-record completion, and checkpoint condensation—areas where mis-correlation could drop tasks or attribute wrong files, though ambiguous Copilot cases are designed to fail closed.
Overview
Adds Copilot CLI subagent task capture for modern releases (verified against 1.0.82), including a ninth managed hook (
subagentStart, observed but not correlated) and asubagentStoppath that joins the parentevents.jsonlon childagentId/subagent.started.toolCallIdto emit aFinalSubagentEndwith child-scoped modified files (includingrestrictedProperties.filePaths). Stops without identity or unsafe transcript paths fail closed instead of creating shared empty-key task records; 1.0.63 can fall back by agent name only when a single unfinished start matches.Hook install/drift:
CheckHookConfigtreats repos missingsubagentStartas outdated so upgrades pick up the new hook.Shared lifecycle: New event flags
CompletionWithoutLaunchandSubagentTranscriptUnavailablelet completion-only Copilot stops create task records while the parent session is still active, trust adapter-supplied files without scanning a child transcript, and skip genericagent-<id>.jsonlprobing at condensation. ChildagentStophooks that point at the parent transcript are mapped toSessionEndto tear down transient child session state.Tests & E2E: Unit coverage for correlation, concurrency, and condensation; Copilot-specific subagent commit E2E asserts a durable
tasks/.../task.json. Docs andAGENT.mdupdated for the native subagent contract.Reviewed by Cursor Bugbot for commit 392897c. Configure here.