Skip to content

Copilot CLI: /plannotator-last silently annotates a stale transcript instead of the live session #1149

Description

@mararn1618

Environment

  • plannotator 0.24.2 via install.sh (the code paths referenced below are unchanged on current main, a54b46b)
  • GitHub Copilot CLI 1.0.75
  • macOS 26.5.2

Reproduce on a fresh machine

  1. Full install: curl -fsSL https://plannotator.ai/install.sh | bash. Among other things this writes the shared agent skills to ~/.agents/skills/, including plannotator-last.
  2. In Copilot CLI: /plugin marketplace add backnotprop/plannotator, then /plugin install plannotator-copilot@plannotator, restart.
  3. Open Copilot CLI in a repo that has older transcripts under ~/.claude/projects/ for the same cwd (the normal case on a machine where plannotator's other integrations were used before).
  4. Ask Copilot anything, then run /plannotator-last.

What happens

The annotation UI opens with a message from an old transcript under ~/.claude/projects/<encoded-cwd>/, not the message Copilot just produced. No error, no warning; a message from an unrelated session of a different tool is presented as the thing to annotate. If no transcript exists for the cwd, the command fails to find a session instead. Either way the live Copilot session is never read.

Expected: /plannotator-last annotates the newest assistant message of the live Copilot session (~/.copilot/session-state/<uuid>/events.jsonl), or fails loudly.

Root cause chain

  1. The installer writes an agent-agnostic plannotator-last skill to ~/.agents/skills/ (from apps/skills/core/plannotator-last/SKILL.md) whose body runs plannotator last. Copilot CLI auto-discovers ~/.agents/skills/, and that skill wins the name /plannotator-last over the plugin command shipped with plannotator-copilot, which would run the correct plannotator copilot-last (apps/copilot/commands/plannotator-last.md). The plugin path is never taken.
  2. plannotator last / annotate-last (apps/hook/server/index.ts, handler at line 1113) branches --stdin, then CODEX_THREAD_ID, then droid, then falls through to the default transcript reader for ~/.claude/projects/. There is no Copilot branch.
  3. The origin detection that should catch this checks process.env.COPILOT_CLI (index.ts:326), and the copilot-session.ts header states "The COPILOT_CLI=1 environment variable is set in Copilot CLI sessions". Copilot CLI sets no such variable, so the check can never fire and the fallthrough in step 2 always wins.

Evidence

Session events from the run where I hit this (~/.copilot/session-state/<uuid>/events.jsonl, fields trimmed, repo path shortened):

2026-07-27T14:14:01.586Z  skill.invoked         {"name": "plannotator-last", "path": "/Users/work1618/.agents/skills/plannotator-last/SKILL.md"}
2026-07-27T14:14:27.061Z  tool.execution_start  {"command": "cd <repo> && plannotator last"}

The dead env check, verified against the Copilot CLI 1.0.75 binary (compiled bundle, readable via strings):

$ strings "$(readlink -f "$(which copilot)")" | grep -o "COPILOT_CLI[A-Z_]*" | sort -u
COPILOT_CLI_BINARY_VERSION
COPILOT_CLI_DIST_DIR

It sets COPILOT_CLI_BINARY_VERSION for itself and reads COPILOT_CLI_DIST_DIR; nothing ever sets COPILOT_CLI.

What a live session does provide is a deterministic marker, session-state/<uuid>/inuse.<pid>.lock, where the pid is the live copilot process:

$ ls ~/.copilot/session-state/01e0a97a-8d76-4dac-bc23-291e59fda972/
checkpoints  files  inuse.40706.lock  research  workspace.yaml
$ ps -o pid=,comm= -p 40706
40706 copilot

Alternatives considered

An installer flag to skip ~/.agents/skills/ would not fix this: default installs stay broken, dropping the shared directory also removes the skills Codex reads from there (forcing a choice between Codex and Copilot on the same machine), and a hand-typed plannotator last under Copilot would still silently show the wrong content. Since the name collision cannot be removed, the fix that holds is teaching plannotator last to recognize the Copilot session it runs in.

I have this working end to end via the inuse locks; PR follows.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions