Skip to content

feat(copilot-cli): track subagent tasks - #2341

Merged
peyton-alt merged 6 commits into
mainfrom
feat/copilot-subagent-tracking
Sep 10, 2026
Merged

peyton-alt merged 6 commits into
mainfrom
feat/copilot-subagent-tracking

Conversation

@peyton-alt

@peyton-alt peyton-alt commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1282

Summary

  • handle Copilot CLI subagent lifecycle hooks and persist completed task records into checkpoints
  • correlate modern agent IDs directly and support Copilot CLI 1.0.63 through a fail-closed unique unfinished-name fallback
  • preserve task records through manual-commit condensation and verify them in the real-agent E2E flow
  • quote the Betterleaks test fixture replacement path so the full suite works from checkout paths containing spaces

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

  • mise run check
  • paid real-agent E2E: TestSubagentCommitFlow with copilot-cli
  • wrapper-free copilot -p smoke using a named subagent, followed by a normal Git commit; verified the checkpoint task record and shadow-branch cleanup

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 a subagentStop path that joins the parent events.jsonl on child agentId / subagent.started.toolCallId to emit a Final SubagentEnd with child-scoped modified files (including restrictedProperties.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: CheckHookConfig treats repos missing subagentStart as outdated so upgrades pick up the new hook.

Shared lifecycle: New event flags CompletionWithoutLaunch and SubagentTranscriptUnavailable let 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 generic agent-<id>.jsonl probing at condensation. Child agentStop hooks that point at the parent transcript are mapped to SessionEnd to 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 and AGENT.md updated for the native subagent contract.

Reviewed by Cursor Bugbot for commit 392897c. Configure here.

Entire-Checkpoint: 01M2458H6H6F8Y4GVHCT2DFCY2
Entire-Checkpoint: 01M245A95XMM1PPDRYFXG2BSSQ
Copilot AI lite review requested due to automatic review settings September 9, 2026 22:55
@peyton-alt
peyton-alt requested a review from a team as a code owner September 9, 2026 22:55
…-tracking

# Conflicts:
#	e2e/testutil/repo.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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.modelMetrics in transcript.go, while this says agentMetrics. 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 only PhaseEnded. EndedAt is the other half of the finalized-state invariant and can be present while Phase is 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 TranscriptUnavailable branch bypasses the AgentID validation below, so a malformed or untrusted Copilot agentId can produce a payload that reaches writeTaskRecordEntries and makes condensation fail with invalid task payload instead of skipping the poisoned record as the surrounding contract requires. Validate record.AgentID before 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
@peyton-alt
peyton-alt merged commit 5f09e04 into main Sep 10, 2026
13 checks passed
@peyton-alt
peyton-alt deleted the feat/copilot-subagent-tracking branch September 10, 2026 20:29
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants