v1.91.59.0 fix(autoplan): bind init through symlinked plan directories and superseded failed inits (#3083)

* fix(autoplan): bind init through symlinked plan directories and superseded failed inits

The publication guard compared snapshot init's JSON against the literal argv
spelling, but init reports canonical destinations, so a plan under macOS /tmp
or /var (realpath /private/...) always denied init_mismatch. The guard also
hard-failed on the first errored init, so a corrected re-run never bound.

- bin/gstack-autoplan-snapshot.ts: export canonicalDestination (the inner
  destination() path math); initializePlan output is unchanged.
- autoplan/bin/phase-publication-hook.ts: compare activePlan/restorePath
  against the canonicalized argv; a mistyped tool path is unbindable, not a
  crash; skip errored inits and deny init_failed only when the most recent
  init errored; Write/Edit to the active plan match by canonical path.
- autoplan/bin/guard-reasons.ts: init_mismatch never says re-run; init_failed
  does.
- tests: PAX-3638 Fix A/A2/B/C cases (bun test -t "Fix B" runs one fix); the
  three autoplan-init assertions that failed on macOS now compare canonical
  paths. 9 of the 11 new guard cases fail against the previous hook.
- docs: init_mismatch, init_failed, Symlinked plan directories.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(autoplan): canonicalRequested comment matches realpath behavior

realpathSync resolves a leaf link as well as directory links; only a
missing target or dangling link keeps the literal spelling. Note why
resolving a leaf link cannot bind an aliased active plan, and drop Read
from the callers list (only Write/Edit use it).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.59.0 fix(autoplan): bind init through symlinked plan directories and superseded failed inits

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: Wyatt Barrett <wyatt@Wyatts-MacBook-Pro.local>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-08 15:26:56 +00:00
1 parent 9a1dc81a2b
commit 4e5ffbb857
10 files changed
+232 -33

No files matched your search

+27 -3
View File
@@ -177,8 +177,14 @@ previous report.
<a id="init-mismatch"></a>
### `init_mismatch`
The snapshot init result or its artifacts (restore point, active plan header)
do not match this invocation.
Snapshot init returned paths that do not match the canonical form of its own
command arguments, or its artifacts (restore point, active plan header) do not
match this invocation. The guard canonicalizes the `<active>` and `<restore>`
arguments the way `init` does (see
[Symlinked plan directories](#symlinked-plan-directories)) before comparing, so
a retargeted or forged init result is denied here. Zero results or duplicate
results for one init use also deny as `init_mismatch`. Do not re-run the same
call: run init with the paths the guard expects.
<a id="snapshot"></a>
### `snapshot`
@@ -241,7 +247,10 @@ A new `/autoplan` turn needs its own init (it may answer `reused: true`).
<a id="init-failed"></a>
### `init_failed`
The init step failed. Complete it first.
The most recent snapshot init call failed; re-run the same init command with
the same three paths. An errored init is skipped, so a later successful init
still binds. This denial applies when no init ever succeeded, or when an
errored init follows a good one.
<a id="init-unbindable"></a>
### `init_unbindable`
@@ -335,3 +344,18 @@ write it never changes a decision.
Claude Code and Git Bash can spell one path as `C:\x`, `c:\x`, `C:/x` or `/c/x`.
The guard treats these as the same path. It does not resolve `.` or `..`, so a
path containing them is still rejected.
<a id="symlinked-plan-directories"></a>
## Symlinked plan directories (macOS /tmp, /var)
The guard compares plan paths canonically, not by argv spelling. On macOS
`/tmp` and `/var` are symlinks to `/private/tmp` and `/private/var`, so a plan
under `/tmp` or `/var/folders` is expected to appear as `/private/...` in the
init JSON. That is by design: `init` reports canonical paths (the deepest
existing ancestor is resolved with `realpath` and the missing segments are
re-joined, `canonicalDestination` in `bin/gstack-autoplan-snapshot.ts`), and
the guard canonicalizes the init command's `<active>` and `<restore>` the same
way. Writes and Edits to the active plan are matched by canonical path too, so
editing the plan through an alias spelling is recognized. Symlinked plan
directories are allowed; only a result that differs from the canonical form of
its own arguments is denied (`init_mismatch`).