Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8639d82
Add intent settle-window config
KristjanPikhof Jun 22, 2026
d8af41c
Add intent settle-window plumbing
KristjanPikhof Jun 22, 2026
fdbf2de
Add settle window intent reporting
KristjanPikhof Jun 22, 2026
d369aff
Expose settle window in intent reports
KristjanPikhof Jun 22, 2026
8c857fc
Disable coalescing by default
KristjanPikhof Jun 22, 2026
00633d2
Update replay test for same-path captures
KristjanPikhof Jun 22, 2026
5e9ade8
Add same-file planner visibility test
KristjanPikhof Jun 22, 2026
8e945ad
Add intent settle-window gating
KristjanPikhof Jun 22, 2026
ecfe6ab
Add settle-window observability
KristjanPikhof Jun 22, 2026
e3ac4f3
Add settle-window details to intent observability
KristjanPikhof Jun 22, 2026
2243d0b
Report intent settle waits in status
KristjanPikhof Jun 22, 2026
4111464
Report intent settle waits consistently
KristjanPikhof Jun 22, 2026
9117dcd
Handle intent settle-window waits
KristjanPikhof Jun 22, 2026
b550e71
Adjust intent replay planner defaults
KristjanPikhof Jun 22, 2026
59a5050
Tighten settle-window replay gating
KristjanPikhof Jun 22, 2026
6019dbe
Bypass settle window for full pending batches
KristjanPikhof Jun 22, 2026
cf50945
Add commit group support to intent planning
KristjanPikhof Jun 22, 2026
839c4d2
Add commit group support to intent planning
KristjanPikhof Jun 22, 2026
33a7067
Add commit group support to intent planning
KristjanPikhof Jun 22, 2026
6bf7910
Update intentTraceCommitGroups
KristjanPikhof Jun 22, 2026
eb3be62
Update TestValidateIntentPlanAcceptsCommitGroups
KristjanPikhof Jun 22, 2026
201bc50
Update TestReplay_IntentStrategyPublishesPartitio…
KristjanPikhof Jun 22, 2026
b0162df
Update captureCommitOID
KristjanPikhof Jun 22, 2026
0dd3d32
Update plugin_subprocess.go
KristjanPikhof Jun 22, 2026
5d8c852
Update replay.go
KristjanPikhof Jun 22, 2026
66b747a
Update intent_planner.go
KristjanPikhof Jun 22, 2026
be38b14
Add planner window observability schema
KristjanPikhof Jun 22, 2026
73cb412
Add planner window observability records
KristjanPikhof Jun 22, 2026
6a4ab56
Add planner window metadata persistence
KristjanPikhof Jun 22, 2026
834ba5e
Record planner windows during replay
KristjanPikhof Jun 22, 2026
b8b5aae
Expose planner windows in CLI events
KristjanPikhof Jun 22, 2026
4733356
Show last planner window in CLI
KristjanPikhof Jun 22, 2026
2553054
Align CLI observability structs
KristjanPikhof Jun 22, 2026
7e82773
Rename intent windows files
KristjanPikhof Jun 22, 2026
8ecec49
Rename intent window files
KristjanPikhof Jun 22, 2026
724a268
Expose planner window in CLI output
KristjanPikhof Jun 22, 2026
373b0a1
Verify planner window partitioning
KristjanPikhof Jun 22, 2026
c6ba17b
Update intent atomicity test bodies
KristjanPikhof Jun 22, 2026
d6b7923
Tune path quiescence test settings
KristjanPikhof Jun 22, 2026
5bb3bf3
Document intent settle window and grouped planning
KristjanPikhof Jun 22, 2026
d986d6d
Reject interleaved commit groups
KristjanPikhof Jun 22, 2026
e9860d7
Document commit_groups planner guidance
KristjanPikhof Jun 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

## Unreleased

### Added

- Intent mode now has a short `ACD_INTENT_SETTLE_WINDOW` burst delay after the
pending-count gate, so rapid related edits can reach one planner-visible
window before AI grouping runs.
- `acd events --json`, `acd status --json`, and `acd diagnose --json` now expose
privacy-safe planner-window summaries showing offered, selected, deferred,
forced, hidden, and grouped seqs without requiring prompt-trace parsing.

### Changed

- Intent planning can partition one visible window into multiple ordered commit
groups, letting unrelated close-together changes and independent same-file
edits publish as separate atomic commits.
- The intent planner prompt now explicitly asks for ordered `commit_groups`
when one visible window contains multiple independent commit intents.
- Consecutive same-path captures are planner-visible by default. Set
`ACD_INTENT_PATH_COALESCE=1` to restore the legacy folding behavior.

## v2026-06-21

### Changed
Expand Down
18 changes: 10 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ make tidy # go mod tidy
Pre-PR gate:

```bash
cleanenv() { env -u ACD_INTENT_MIN_PENDING -u ACD_INTENT_MAX_PENDING_AGE -u ACD_INTENT_WINDOW -u ACD_INTENT_RECENT_COMMITS -u ACD_INTENT_DEFER_LIMIT ACD_COMMIT_STRATEGY=event "$@"; }
cleanenv() { env -u ACD_INTENT_MIN_PENDING -u ACD_INTENT_MAX_PENDING_AGE -u ACD_INTENT_SETTLE_WINDOW -u ACD_INTENT_WINDOW -u ACD_INTENT_RECENT_COMMITS -u ACD_INTENT_DEFER_LIMIT ACD_COMMIT_STRATEGY=event "$@"; }
cleanenv make lint
cleanenv make test
cleanenv go test ./test/integration/... -tags=integration -race -count=1 -timeout 5m
Expand Down Expand Up @@ -49,7 +49,7 @@ ACD_VERSION=v2026-MM-DD sh scripts/install.sh
| `cmd/acd/main.go` | CLI entrypoint |
| `internal/cli` | Cobra commands: setup, hooks, status, diagnose, doctor, fix, commit-all, start cache |
| `internal/daemon` | run loop, capture/replay, intent, branch tokens, bootstrap/shadow, fsnotify, refcount, live-index repair, trace |
| `internal/state` | SQLite v7: events/ops, decisions, planner_state, shadow/meta/clients/flush/safe-ignore/sensitive |
| `internal/state` | SQLite v11: events/ops, decisions, planner_state, planner windows, shadow/meta/clients/flush/safe-ignore/sensitive |
| `internal/git` | bounded refs/tree/diff/blob/scratch-index/history/ignore helpers |
| `internal/ai` | deterministic/openai-compat/subprocess providers, commit message prompts, intent planner |
| `internal/adapter` | harness detection and markers; do not restore TODO stubs |
Expand Down Expand Up @@ -104,7 +104,7 @@ Line 3+: bullet list for why/context

- Repo DB: `<gitDir>/acd/state.db`; central registry/stats use XDG state/share.
- Start cache: `<gitDir>/acd/start-cache-<sha256(session_id)[:16]>.json`, schema v2. `acd stop` removes matching/all caches. Atomic tmp+rename prevents corruption.
- `SchemaVersion = 10`: v5 `decision_records`; v6 `decision_records.event_seq`; v7 `planner_state`; v8-v9 rewrite-plan state; v10 `rewrite_plans.commit_format`.
- `SchemaVersion = 11`: v5 `decision_records`; v6 `decision_records.event_seq`; v7 `planner_state`; v8-v9 rewrite-plan state; v10 `rewrite_plans.commit_format`; v11 `intent_planner_windows`.
- `shadow_paths` key `(branch_ref, branch_generation, path)`; read-heavy paths use `state.DB.ReadSQL()`.
- Shadow bootstrap: 5000-row chunks; marker `shadow.bootstrapped:<branch_ref>:<generation>` only after all chunks commit; clean partial rows on failure. Empty active shadow with marker means delete marker and re-bootstrap.
- Reseed prunes old generations via `ACD_SHADOW_RETENTION_GENERATIONS` default `1`.
Expand All @@ -124,13 +124,15 @@ Line 3+: bullet list for why/context

- Default `ACD_COMMIT_STRATEGY=event`: one capture per commit; must not call planner.
- `ACD_COMMIT_STRATEGY=intent`: AI planner selects one capture or a non-empty subset; capture durability unchanged.
- Intent defaults: `ACD_INTENT_WINDOW=10`, `ACD_INTENT_MIN_PENDING=10`, `ACD_INTENT_MAX_PENDING_AGE=5m`, `ACD_INTENT_RECENT_COMMITS=5`, `ACD_INTENT_DEFER_LIMIT=1`, `ACD_INTENT_PATH_COALESCE=1`.
- Intent defaults: `ACD_INTENT_WINDOW=10`, `ACD_INTENT_MIN_PENDING=10`, `ACD_INTENT_SETTLE_WINDOW=10s`, `ACD_INTENT_MAX_PENDING_AGE=5m`, `ACD_INTENT_RECENT_COMMITS=5`, `ACD_INTENT_DEFER_LIMIT=1`; `ACD_INTENT_PATH_COALESCE` is off by default.
- Planner must classify every offered seq as selected or deferred. Invalid/missing/unsafe output records `intent_planner_error` and falls back to deterministic one-item.
- Planner may return ordered `commit_groups` to partition one visible window into multiple atomic commits. Replay publishes valid groups sequentially.
- `deferred_reasons[i].seq` must appear in `deferred_seqs`. Providers normalize spurious deferred reasons before validation and warn with dropped seqs.
- Deferred stays pending in `planner_state`; at `defer_count >= ACD_INTENT_DEFER_LIMIT`, oldest overdue is forced one-item.
- Same-path coalesce folds consecutive captures touching one shared path into one planner entry. Boundaries: different path, multi-path, rename, delete, or `(branch_ref, branch_generation, base_head)` change. On success every original seq is marked published with the same `commit_oid`; `acd events --json grouped_seqs` shows the run.
- Same-path coalesce is legacy opt-in with truthy `ACD_INTENT_PATH_COALESCE`. By default, consecutive same-path captures remain separate planner-visible seqs. If coalescing is enabled, boundaries are different path, multi-path, rename, delete, or `(branch_ref, branch_generation, base_head)` change. On success every original seq is marked published with the same `commit_oid`; `acd events --json grouped_seqs` and planner-window `hidden_seqs` show the run.
- Planner windows persist in `intent_planner_windows`/`intent_planner_window_events`: offered seqs, visible original seqs, hidden/coalesced seqs, selected groups, deferred seqs/reasons, forced/fallback metadata, provider/model/source/format, and per-event flags. They are privacy-safe summaries, not raw diffs.
- Planner rejects log: `<gitDir>/acd/planner-rejects.jsonl`, 5 MiB + `.1`, best effort. Fields include `ts`, `provider`, `offered_seqs`, `code`, `message`, raw response size, sha256, and parsed-plan summary. `raw_response` is redacted by default; verbatim only with truthy `ACD_INTENT_REJECTS_RAW`, which logs a startup warning.
- `acd status --json` and `acd diagnose --json` expose `intent_strategy`, including recent planner/singleton rates over fixed denominator 100. `diagnose` hints when `planner_error_rate_recent > 0.05`.
- `acd status --json` and `acd diagnose --json` expose `intent_strategy`, including settle fields, `last_planner_window`, and recent planner/singleton rates over fixed denominator 100. `diagnose` hints when `planner_error_rate_recent > 0.05`.
- Per-pass scratch index `<gitDir>/acd/replay-*.index` is seeded from `cctx.BaseHead`; reads via `git.LsFilesIndex(...)`.
- CAS targets literal `HEAD` via `git.UpdateRef`; named refs use `--no-deref`.
- `DefaultReplayLimit = 64`; query `Limit+1`, trim, set `ReplaySummary.HasMore`. `DefaultReplayPerEventTimeout = 60s`; timeout/cancel marks event `failed` and stops batch.
Expand All @@ -143,7 +145,7 @@ Line 3+: bullet list for why/context

- `processBranchTokenChange` runs before capture and after flush drain; do not collapse. Post-flush recheck handles git surgery outside `wakeCh`.
- Branch settle `100ms`; flush drain `DefaultFlushLimit = 256`.
- Daemon stamps `commit.strategy`, `intent.{window,min_pending,max_pending_age,recent_commits,defer_limit,diff_egress}`; CLI reads meta before env.
- Daemon stamps `commit.strategy`, `intent.{window,min_pending,settle_window,max_pending_age,recent_commits,defer_limit,diff_egress}`; CLI reads meta before env.
- Startup sweeps `acknowledged` flush requests older than `OrphanFlushAckThreshold = 5m` to `failed`.
- fsnotify dispatch must not block: runtime creates use `rewalkCh`/`rewalkWorker`; diagnostics `diagCh`; tail clamps `MaxDebounceTail = 500ms`; ENOSPC -> `errBudgetExceeded`.
- Daemon log: `paths.Roots.RepoLogPath(repoHash)` (`~/.local/state/acd/<repo-hash>/daemon.log`) with rotation/compression. Hook log: `${XDG_STATE_HOME:-$HOME/.local/state}/acd/<harness>-hook.log`.
Expand Down Expand Up @@ -227,5 +229,5 @@ LOG="${XDG_STATE_HOME:-$HOME/.local/state}/acd/<harness>-hook.log"
| Shadow/rewind | `ACD_SHADOW_RETENTION_GENERATIONS=1`; `ACD_REWIND_GRACE_SECONDS=60` (`0` disables); `ACD_KEEP_DEAD_BRANCH_BARRIERS` disables auto-prune |
| Capture | `ACD_SENSITIVE_GLOBS`; `ACD_SAFE_IGNORE`; `ACD_SAFE_IGNORE_EXTRA`; `ACD_MAX_PENDING_EVENTS`; `ACD_PATH_QUIESCENCE_SECONDS=0` (off; restart to apply; capture remains durable, planner offer waits for quiet path) |
| AI | `ACD_AI_PROVIDER=deterministic|openai-compat|subprocess:<name>`; `ACD_AI_BASE_URL`; `ACD_AI_API_KEY`; `ACD_AI_MODEL`; `ACD_AI_TIMEOUT=30s`; `ACD_AI_CA_FILE`; `ACD_AI_DIFF_EGRESS`; `ACD_COMMIT_FORMAT=imperative|conventional`; `ACD_INTENT_REJECTS_RAW` |
| Strategy | `ACD_COMMIT_STRATEGY=event|intent`; `ACD_INTENT_WINDOW=10`; `ACD_INTENT_MIN_PENDING=10`; `ACD_INTENT_MAX_PENDING_AGE=5m`; `ACD_INTENT_RECENT_COMMITS=5`; `ACD_INTENT_DEFER_LIMIT=1`; `ACD_INTENT_PATH_COALESCE=1`; `ACD_RECENT_COMMIT_AFFINITY_SECONDS=0`; planner cap `ai.IntentStageDiffCap=16000` |
| Strategy | `ACD_COMMIT_STRATEGY=event|intent`; `ACD_INTENT_WINDOW=10`; `ACD_INTENT_MIN_PENDING=10`; `ACD_INTENT_SETTLE_WINDOW=10s`; `ACD_INTENT_MAX_PENDING_AGE=5m`; `ACD_INTENT_RECENT_COMMITS=5`; `ACD_INTENT_DEFER_LIMIT=1`; `ACD_INTENT_PATH_COALESCE` off by default; `ACD_RECENT_COMMIT_AFFINITY_SECONDS=0`; planner cap `ai.IntentStageDiffCap=16000` |
| Watcher/client | `ACD_FSNOTIFY_ENABLED`; `ACD_DISABLE_FSNOTIFY`; `ACD_MAX_INOTIFY_WATCHES`; `ACD_CLIENT_TTL_SECONDS` |
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,23 @@ export ACD_AI_DIFF_EGRESS=1
export ACD_COMMIT_STRATEGY=intent
export ACD_INTENT_WINDOW=10
export ACD_INTENT_MIN_PENDING=4
export ACD_INTENT_SETTLE_WINDOW=10s
export ACD_INTENT_MAX_PENDING_AGE=5m
export ACD_INTENT_DEFER_LIMIT=1
~~~

`ACD_AI_DIFF_EGRESS=1` lets the planner see redacted captured diffs. Leave it
unset when the endpoint should receive metadata only.

Intent mode waits briefly after the pending-count gate before planning. This
settle window lets a burst of related edits reach one planner-visible window,
while `acd flush --logical` still drains the current visible batch from an
active harness session.

When one visible window contains separate intents, the planner prompt asks for
ordered `commit_groups` so replay can publish atomic commits instead of forcing
the whole window into one commit.

Message format:

| Format | Example subject | Notes |
Expand Down Expand Up @@ -317,6 +327,7 @@ git reset --hard <backup-ref-or-sha>
| `ACD_AI_DIFF_EGRESS` | off | Truthy sends redacted captured diffs to providers that ask for diffs. |
| `ACD_INTENT_WINDOW` | `10` | Max captures offered to one planner pass. |
| `ACD_INTENT_MIN_PENDING` | `10` | Preferred count before planning. Lower it for sparse repos. |
| `ACD_INTENT_SETTLE_WINDOW` | `10s` | Burst settle delay after the count gate. `0` disables it. |
| `ACD_INTENT_MAX_PENDING_AGE` | `5m` | Age escape hatch for sparse queues. |
| `ACD_INTENT_DEFER_LIMIT` | `1` | Deferrals before ACD forces a one-capture window. |
| `ACD_INTENT_RETRY_ON_INVALID` | `2` | Max correction retries after invalid planner output. |
Expand Down
47 changes: 45 additions & 2 deletions docs/ai-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ private repo unless the endpoint or plugin is trusted.
| `ACD_COMMIT_FORMAT` | `imperative` | `imperative` keeps the current subject rules; `conventional` opts into scope-less Conventional Commit subjects. |
| `ACD_INTENT_WINDOW` | `10` | Max captures offered to one planner pass. |
| `ACD_INTENT_MIN_PENDING` | `10` | Preferred pending count before planning. |
| `ACD_INTENT_SETTLE_WINDOW` | `10s` | Burst settle delay after the count gate. `0` disables it. |
| `ACD_INTENT_MAX_PENDING_AGE` | `5m` | Age trigger for sparse queues. |
| `ACD_INTENT_RECENT_COMMITS` | `5` | Recent commits sent as compact context. |
| `ACD_INTENT_DEFER_LIMIT` | `1` | Deferrals before forced one-capture planning. |
| `ACD_INTENT_PATH_COALESCE` | `1` | Folds consecutive same-path captures into one planner offer. |
| `ACD_INTENT_PATH_COALESCE` | off | Truthy restores legacy folding of consecutive same-path captures into one planner offer. |
| `ACD_INTENT_RETRY_ON_INVALID` | `2` | Max correction retries after typed planner validation errors. `0` or false-like values disable retries. |
| `ACD_INTENT_REJECTS_RAW` | off | Truthy stores raw rejected planner responses. Sensitive. |
| `ACD_PATH_QUIESCENCE_SECONDS` | `0` | Waits for paths to go quiet before planner offer. Capture still persists. |
Expand Down Expand Up @@ -168,7 +169,7 @@ Commit-message response:

Intent planner requests set `request_type` to `intent_plan`, include
`commit_format`, and include `planner_request.offered_captures`. The response
must classify every offered seq:
must classify every offered seq. A single-group response still works:

~~~json
{
Expand All @@ -185,12 +186,54 @@ must classify every offered seq:
}
~~~

For windows that contain several independent intents, return ordered
`commit_groups`:

~~~json
{
"version": 1,
"selected_seqs": [101, 103],
"deferred_seqs": [102],
"subject": "Add auth retry handling",
"body": "- Keep transient failures available for retry",
"grouping_reason": "Auth retry handling and retry docs are separate commits",
"commit_groups": [
{
"selected_seqs": [101],
"subject": "Add auth retry handling",
"body": "- Keep transient failures available for retry",
"grouping_reason": "Auth behavior change"
},
{
"selected_seqs": [103],
"subject": "Document retry configuration",
"body": "",
"grouping_reason": "Documentation-only update"
}
],
"deferred_reasons": [
{"seq": 102, "reason": "Separate billing cleanup"}
],
"error": ""
}
~~~

The top-level `selected_seqs`, `subject`, `body`, and `grouping_reason` remain
required for legacy compatibility. When `commit_groups` is present,
`selected_seqs` must be the union of all group selections; the top-level
message can mirror the first group or summarize the selected window.

ACD's built-in prompt tells the planner to use `commit_groups` for independent
intents inside one visible window. Custom subprocess planners should follow the
same contract instead of returning one broad selected group for unrelated work.

Rules:

| Rule | Why it exists |
|---|---|
| `selected_seqs` must be non-empty | Replay must make progress. |
| Every offered seq must be selected or deferred | The planner cannot ignore work. |
| `commit_groups`, when present, must be ordered and non-overlapping | Replay publishes groups sequentially and must preserve chronology. |
| `deferred_reasons` may mention only deferred seqs | Reasons stay aligned with the plan. |
| `subject` must match `commit_format` | Wrong-format output gets rejected, corrected, or falls back deterministically. |
| Non-empty `error` is a soft error | ACD keeps the plugin alive and falls back for that request. |
Expand Down
8 changes: 7 additions & 1 deletion docs/capture-replay.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,20 +139,26 @@ rewrite diffs are capped at 16000 bytes.
| Strategy | Replay behavior |
|---|---|
| `event` | FIFO replay. One capture can become one commit. |
| `intent` | A bounded pending window goes to the planner. Selected seqs replay as one commit; deferred seqs stay pending. |
| `intent` | A bounded pending window goes to the planner. Selected groups publish sequentially; deferred seqs stay pending. |

Intent waits for one of these:

| Trigger | Meaning |
|---|---|
| `ACD_INTENT_MIN_PENDING` | Enough visible pending captures exist. |
| `ACD_INTENT_SETTLE_WINDOW` | After the count gate, wait briefly for bursty edits to stop arriving. |
| `ACD_INTENT_MAX_PENDING_AGE` | Oldest visible capture reached the age escape hatch. |
| `acd flush --logical --session-id <active-session>` | A registered harness session asks to drain the visible batch now. |
| Forced aging | A repeatedly deferred capture reached `ACD_INTENT_DEFER_LIMIT`. |

Plain `acd wake` nudges capture and replay. It does not bypass intent batch
gates.

Planner-window records are stored separately from raw prompt traces. They show
which seqs were offered, selected, deferred, forced, or hidden by legacy
same-path coalescing. Prompt traces remain the opt-in source for exact provider
requests and may contain source text.

## One-shot capture with `commit-all`

`acd commit-all` is for a dirty worktree when the daemon was off.
Expand Down
Loading
Loading