Skip to content

feat: load a private repository's agent instructions without repository files - #402

Merged
danielhanold merged 17 commits into
mainfrom
feat/load-a-private-repository-s-agent-instructions-without-repos
Oct 6, 2026
Merged

danielhanold merged 17 commits into
mainfrom
feat/load-a-private-repository-s-agent-instructions-without-repos

Conversation

@danielhanold

@danielhanold danielhanold commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

↩ Change 0535 — Load a private repository's agent instructions without repository files

Summary

In a private repository, docket now keeps its parent-facing rules in .git/dckt/AGENTS.md and writes nothing into the working tree. User-level triggers deliver those rules to each harness at session start.

  • The private instructions file. In a private repository, the dispatch block and promoted lessons live in .git/dckt/AGENTS.md. Install's repository phase and repository init --private write only this file. They also retire dispatch surfaces an earlier install left in the working tree, including surfaces an install wrote from a linked worktree.
  • docket instructions (read-only). It prints that file from any worktree of a private repository and prints nothing anywhere else.
    • --section dispatch|lessons prints one part.
    • --hook claude|cursor wraps the output as session-start hook JSON. Hook modes never fail a session.
  • User-level triggers. docket install adds one content-free trigger per harness:
    • Claude Code: two SessionStart hooks in ~/.claude/settings.json, one for the dispatch block and one for the lessons, so each fits Claude Code's per-hook size cap.
    • Cursor: a sessionStart hook in ~/.cursor/hooks.json.
    • OpenCode: the plugin ~/.config/opencode/plugins/dckt-instructions.js.
    • Codex: a dckt:private-instructions pointer block in ~/.codex/AGENTS.md, best effort. It retires any leftover docket:dispatch block in that file.
  • Supporting machinery:
    • Managed blocks can use a neutral dckt: marker namespace.
    • One install transaction can remove one managed block and write another in the same file.
    • A new hook-entries install target kind edits JSON hooks files by byte splicing. It adds only missing entries and changes nothing else in the file. It removes an entry only while the entry is unchanged.
  • Hooks files docket can't edit. A symlink, or a file docket cannot parse, is left untouched. Install reports a hook-file-not-editable warning and still installs everything else.
  • Shared repositories are unchanged.

ADR-0145 records the delivery design. The fresh-session acceptance table (Claude Code, Cursor, OpenCode, Codex) is in the results file.

Review (deep tier) — findings disposition

# Severity Finding Disposition
1 important A scoped install stripped the Codex clause from the shared private file fixed (e38c134)
2 important A symlinked settings.json/hooks.json failed every install fixed (4593b6d): skipped with a warning
3 important The install and uninstall docs did not mention the new triggers fixed (0839691)
4 important An unparseable hooks file failed every install fixed (219c898): skipped with a warning
5 minor Hook commands that a later release changes would never be retired fixed (6d50d0a)
6 minor Retirement leaves an empty untracked AGENTS.md in the private worktree recorded: deleting it safely needs a record field showing docket created the file (see Known issues in the results file)
7 minor Surfaces an earlier install wrote and recorded from a linked worktree were never retired fixed (6d50d0a)
8 minor A JSON-mode instructions failure used Failure and carried no reason code fixed (6d50d0a)

Also fixed: an existing flaky test that the build gate hit, internal/process TestRecoverLeavesUnprovableGroupForInspection (64cb9c7).

Fixture: private repository (repository init --private, agent_harnesses
[claude, codex, cursor, opencode]) plus a linked worktree; triggers installed
by `docket development install --harness <all four>` into a temporary HOME,
XDG_CONFIG_HOME, XDG_DATA_HOME and bin dir. Lesson token PRIVTOKEN-4cee33d3
appended to .git/dckt/AGENTS.md. `git status --porcelain` empty after init.
`dckt instructions --section dispatch`: 7773 characters (< 10000).
Second install run: no trigger changes (only the rebuilt binary updated).

Markers: A = reply contains `run.verdict` and `Obey the resulting`;
B = reply ends with the lesson token. The plan's literal prompt asks for
the sentence starting "After the run returns", which ends before "Obey the
resulting", so it cannot carry marker A in any harness; each harness was
also given "Quote verbatim, in full, the numbered step 2 that starts with
'After the run returns' (every sentence of that step). Then say hello."

| Harness | Version | Mode and flags | Location | Verdict | Evidence |
|---|---|---|---|---|---|
| Claude Code | 2.1.291 | claude -p --no-session-persistence --setting-sources project --strict-mcp-config --settings <tmp>/.claude/settings.json | private repo; feature worktree | PASS | full-step prompt: "...run `run.verdict` ... Obey the resulting `run-*` report line exactly..." then "PRIVTOKEN-4cee33d3" in both locations; literal prompt (2 tries): sentence + token |
| Claude Code | 2.1.291 | same | non-git dir (control) | PASS (control) | "I can't find any sentence starting with..."; no token |
| Cursor | cursor-agent 2026.10.01-e373342 | cursor-agent -p --trust --output-format text; hooks.json copied to <repo>/.cursor/hooks.json | private repo | PASS | "...run `run.verdict` ... Obey the resulting `run-*` report line exactly..." then "PRIVTOKEN-4cee33d3."; without the project hooks file: no token, older wording from elsewhere |
| Cursor hook | fixture binary | dckt instructions --hook cursor | CURSOR_PROJECT_DIR=<feature worktree>; and empty + stdin workspace_roots=[<repo>] | PASS | both print {"additional_context": ...} carrying the token; garbage stdin: no output, exit 0 |
| OpenCode | 1.18.31 | OPENCODE_CONFIG_DIR=<tmp>/opencode opencode run (no permission flag); model deepseek/deepseek-v4-flash-0731, agent build | private repo; feature worktree | PASS | "Step 2: ...run `run.verdict` ... Obey the resulting `run-*` report line exactly..." then "PRIVTOKEN-4cee33d3"; control in non-git dir: no quote, no token |
| Codex | codex-cli 0.154.0 | codex exec --sandbox workspace-write; model gpt-6-astra, approval never; pointer copied to <repo>/AGENTS.md (project-level stand-in) | private repo | PASS (recorded, non-blocking) | ran `dckt instructions` itself; reply carried both markers and the token; user-level ~/.codex/AGENTS.md loading not exercised |
…le-group recover test

A single group-wide SIGKILL is not atomic across the group: the command
could die first while a supervisor thread returned from cmd.Wait and
wrote terminal.json, so Recover classified the run terminal and never
reached the group probe the test exercises. Kill the supervisor alone,
wait until it is reaped, then end the orphaned command, and assert the
setup left no terminal record.
…install

Review finding: a scoped install in a private repository stripped the Codex
clause from the shared private instructions file. With agent_harnesses
[claude, codex], `docket install --harness claude` rewrote .git/dckt/AGENTS.md
with the plain interior while codex stayed a recorded owner, because
PlanPrivate chose the interior from the in-scope harnesses only.

The private file is one block shared by every opted-in harness, so its
interior must follow the full opt-in set. PrivatePlanInput gains OptIns (the
repository's full opt-in set; nil means Harnesses already is), which picks the
interior; the in-scope Harnesses still decide whether the file is planned and
which owners this run contributes. ResolveRepoPhase passes the opt-ins.

Regression tests: a claude-scoped run over [claude, codex] keeps the Codex
clause (integration, through ResolveRepoPhase and the apply), and a PlanPrivate
unit case pins the interior/owner split.
… failing the install

A symlinked ~/.claude/settings.json or ~/.cursor/hooks.json (a dotfiles
manager's link) made inspection report an ownership conflict, and any
conflict failed the whole install. The hook entries are only a trigger,
inert outside a private repository, so that must not block anything.

Inspection now returns a new skip disposition for a non-regular hooks
file. Install and development install leave the path byte-untouched,
plan no step and publish no record for it (a prior record is dropped,
and a harness left with no target is not listed), and every other
target proceeds. Install, development install, and install check report
the skip as a hook-file-not-editable warning naming the path and the
remedy; check no longer counts it as drift.
…g the install

A settings.json or hooks.json docket cannot read as an editable hooks
document (a JSONC comment, a byte-order mark, a null or non-object hooks
value, a non-array event) was a managed-block-invalid conflict, and any
conflict failed the install for every harness.

Inspection now skips it like a non-regular hooks file: the file stays
byte-identical, no step or record is planned for it, every other target
installs, and install and check report it as a hook-file-not-editable
warning whose remedy says docket left the file untouched and did not add
its entries. Uninstall still refuses a recorded hooks file that became
unparseable, since removal of the recorded entries cannot be proven, and
never edits it.
…faces; type instructions read failures

Review findings fixed:

- Stale hook commands: when a prior hook-entries record at the same path
  owns commands the plan no longer carries, inspection marks the target
  for update and the apply cuts those commands' exact entries in the same
  rewrite before adding the new ones. An edited entry is the user's and
  stays.
- Linked-worktree surfaces: a private install run from a linked worktree
  now loads that worktree's own earlier record, retires its surfaces
  (proof-gated against that record, as for the primary), and rewrites the
  record through a new RepoPhase.ExtraRecords commit document.
- docket instructions --json: a failure no longer sets Envelope.Failure;
  it carries a typed reason and message (instructions-read-failed,
  instructions-markers-invalid, instructions-unknown-section).

Not fixed: an empty working-tree AGENTS.md after block retirement — the
ownership record cannot prove docket created the file.
@danielhanold
danielhanold merged commit ed335f0 into main Oct 6, 2026
7 checks passed
@danielhanold
danielhanold deleted the feat/load-a-private-repository-s-agent-instructions-without-repos branch October 6, 2026 23:19
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.

1 participant