Skip to content

fix(session-flow): derive retro HANDOFF_DIR from resolved memory_dir#469

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/420-retro-resolve-memory-dir
Jul 19, 2026
Merged

fix(session-flow): derive retro HANDOFF_DIR from resolved memory_dir#469
kyle-sexton merged 2 commits into
mainfrom
fix/420-retro-resolve-memory-dir

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Summary

The retro Phase 1.1 multi-session snippet (plugins/session-flow/skills/retro/context/session.md) hard-coded HANDOFF_DIR=.work/handoffs directly beneath an instruction to resolve it from the concern file's memory_dir. A copy-as-is execution encoded the bare default and bypassed the memory_dir seam — missing the handoff chain in any repo that relocates its memory tier via .claude/topic-docs.yaml.

The snippet now derives HANDOFF_DIR from the resolved memory_dir: it reads the .claude/topic-docs.yaml concern file's memory_dir key and falls back to .work, matching the topic-docs.yaml parse shape already used elsewhere in the fleet. Comment prose trimmed to describe what the snippet actually performs, pointing to the handoff skill for the full resolution order.

Version bumped 0.10.1 -> 0.10.2 with a matching CHANGELOG entry.

Closes #420

Related

The retro Phase 1.1 multi-session snippet hard-coded HANDOFF_DIR to the
bare default .work/handoffs directly under an instruction to resolve it
from memory_dir. A copy-as-is run bypassed the memory_dir seam, missing
the handoff chain in any repo that relocates its memory tier.

Derive HANDOFF_DIR from the resolved memory_dir instead: read the
.claude/topic-docs.yaml concern file's memory_dir key, fall back to
.work. Mirrors the topic-docs.yaml parse shape already used elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 30s —— View job


PR Review

  • Gather context and read changed files
  • Review session.md — core logic change
  • Review plugin.json — version bump
  • Review CHANGELOG.md — entry quality
  • Post findings

Summary

The fix is correct and well-scoped. The hard-coded HANDOFF_DIR=.work/handoffs was a genuine footgun — any repo using a non-default memory_dir in .claude/topic-docs.yaml would silently find no handoff chain, making multi-session retro aggregation fall back to single-session silently. The new bash snippet resolves the seam properly.


session.md — core logic

The fix itself is sound. The sed + parameter-expansion parsing handles the common YAML scalar cases correctly:

  • ^memory_dir: is anchored to line start, so nested/indented YAML keys are correctly ignored.
  • head -1 takes the first match only.
  • %%#* strips inline comments.
  • Trailing-slash normalisation prevents //handoffs double-slash.
  • Quote stripping handles both "path" and 'path' quoting styles.

One subtlety worth noting: ${VAL//[[:space:]]/} removes all whitespace, not just leading/trailing. For the overwhelmingly common case of ASCII paths this is fine, but a path containing an internal space (e.g. my work dir) would have that space collapsed. This matches how the handoff skill's structure.md snippet comments "resolve it first" without providing YAML-parse code, so it's at least consistent. Raising as an observation rather than a blocker.

Scope gap vs. SKILL.md resolution order. SKILL.md specifies that a CLAUDE.md/.claude/rules/ save-point convention also wins over the default. The new snippet only checks topic-docs.yaml. This is the same gap that existed before (the old code didn't consult either), and the PR description explicitly calls it out as part of a broader sweep tracked in #419. Not a regression introduced here.

Repo-root guard absent. structure.md validates that MEMORY_ROOT doesn't resolve to the repo root before using it. The retro snippet doesn't — but it's read-only (ls + parser invocation), so a bad value yields a harmless empty NEWEST and the parser call fails gracefully. Not a concern at this scope.

Comment quality: improved. Old comment described the resolution intent but not the actual (missing) behaviour; new comment is honest about what the snippet does and explicitly delegates the full order to the handoff skill. ✓


plugin.json

Patch bump 0.10.10.10.2 is the correct semver category for a bug fix with no API/schema change. ✓


CHANGELOG.md

Entry is clear, accurate, and mentions both the incorrect prior behaviour and the failure scenario (repos that relocate the memory tier). The Fixed: heading follows the established changelog style. ✓


Verdict

Approve. No blocking issues. The bash parsing is robust enough for the input space; the one whitespace-in-path edge case is unlikely and consistent with the rest of the fleet. The narrower scope vs. the full SKILL.md resolution order is intentional and tracked separately.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3042a8e7e9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/session-flow/skills/retro/context/session.md Outdated
…arse

The retro Phase 1.1 memory_dir parse stripped every whitespace character
with ${VAL//[[:space:]]/}, collapsing interior spaces in a valid quoted
value: memory_dir: ".agent work" resolved to .agentwork, so the
multi-session retro looked in a nonexistent handoff directory and
silently missed the chain.

Trim only trailing whitespace (${VAL%"${VAL##*[![:space:]]}"}); sed
already removed leading whitespace after the colon, and the closing
quote shields any significant trailing space before quote-stripping.
Interior spaces in quoted and plain scalars are preserved; unquoted YAML
trailing whitespace, inline comments, and trailing slashes still trim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 36s —— View job


PR Review (synchronize — new commit 1621a15)

  • Gather context
  • Read changed files
  • Review session.md — whitespace fix
  • Post findings

What changed

The new commit addresses the Codex P2 finding: memory_dir values containing interior whitespace (e.g. .agent work) were silently collapsed by the previous ${VAL//[[:space:]]/} global substitution. The single-line change:

-  VAL="${VAL%%#*}"; VAL="${VAL//[[:space:]]/}"; VAL="${VAL%/}"
+  VAL="${VAL%%#*}"; VAL="${VAL%"${VAL##*[![:space:]]}"}"; VAL="${VAL%/}"

replaces the global whitespace-removal with the standard bash trailing-whitespace-trim idiom:

Step Expression Effect
1 ${VAL##*[![:space:]]} Remove the longest prefix ending in a non-space; leaves only trailing spaces
2 ${VAL%"<above>"} Strip that trailing-space suffix from VAL

Leading whitespace was already handled by the sed pattern (s/^memory_dir:[[:space:]]*//p). The only missing piece was the trailing trim, which this idiom provides precisely.


Correctness

Edge cases all pass:

  • memory_dir: .agent workVAL=".agent work" → interior space preserved, trailing newline trimmed ✓
  • memory_dir: ".work" → quote-stripped to .work
  • memory_dir: (empty) → VAL=""[[ -n "$VAL" ]] guard keeps MEMORY_DIR=.work
  • memory_dir: (only spaces) → strips to "" → guard fires ✓
  • Nested/indented key ( memory_dir: ...) → ^memory_dir: anchor still ignores it ✓

Scope: No other files changed in this commit. The version bump (0.10.1 → 0.10.2) and CHANGELOG entry are already in place from the prior commit.

Existing observations from the earlier review remain unchanged — the CLAUDE.md/.claude/rules/ save-point resolution gap vs. SKILL.md is tracked in #419 and not a regression here; the repo-root guard absence is still harmless (read-only path, empty NEWEST on bad value).


Verdict

No new issues. The whitespace fix is correct, minimal, and exactly addresses the Codex P2 finding. Ready to merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1621a1598a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/session-flow/skills/retro/context/session.md
Comment thread plugins/session-flow/skills/retro/context/session.md
@kyle-sexton
kyle-sexton merged commit 3c68a0a into main Jul 19, 2026
15 checks passed
@kyle-sexton
kyle-sexton deleted the fix/420-retro-resolve-memory-dir branch July 19, 2026 16:20
kyle-sexton pushed a commit that referenced this pull request Jul 19, 2026
Resolve session-flow version collision and CHANGELOG conflict; address #433 finding 2.

- plugin.json: re-bump 0.10.2 -> 0.10.3 (main already shipped 0.10.2 via #469)
- CHANGELOG: keep main 0.10.2 (retro HANDOFF_DIR fix) and re-home the
  override-boundary entry as 0.10.3, newest-first
- workflow/context/pre-pr.md: add override-boundary note (finding 2) -- the
  pre-PR sequence incl. the simplify pass is fixed plugin identity; commands
  and criteria adapt at each gate, a differing consumer runs its own workflow

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…ia shared helper (#756)

## Summary

The topic-docs `memory_dir` seam was parsed inline at two sites with a
naive `${val%%#*}`-first strip — the same naive-string-op class the
#469/#470 whitespace fixes closed, left as a review-deferred residual.
It truncated a legitimately quoted value containing `#` (`memory_dir:
"a#b"` → `a`) because it stripped a "comment" before resolving
surrounding quotes, and an absent concern file collapsed straight to
`.work` without consulting the contract's rung-2 (a save-point
convention declared in `CLAUDE.md` / `.claude/rules`).

Per the issue's acceptance criteria, both sites now delegate to **one**
shared parser rather than re-forking the duplicated block.

## Fix

- **`lib/parse-concern-value.sh`** (new) — the single seam parser. Order
is quote-resolution **first**, then a quote-aware comment strip
(unquoted ` #…` only; `a#b` / `.work/#topic` preserved), then
whitespace-trim, then trailing-slash-normalize. Takes `<concern-file>
<key> [fallback]`; the `fallback` is the **caller-resolved** rung-2
location (prose is an inference source, not a machine key — the agent
resolves it and passes it in). Emits empty only when nothing resolves,
so the caller owns the documented `.work` default.
- **Both consumers wired through it:** claude-memory
`orphan-rule-check.sh` (real script) and the session-flow retro
`HANDOFF_DIR` snippet (`retro/context/session.md`). The non-interactive
orphan check degrades to `.work` and notes that inferred/interactive
resolution is the calling skill's job, rather than silently collapsing
rung 2.
- **Materialization + drift gate:** copies are synced from `lib/` into
each plugin (installed plugins are cache-isolated, so each must be
self-contained — the `lib/hook-utils.sh` precedent). New
`scripts/sync-parse-concern-value.sh` (`--check` / `--check-bump`) + a
`parse-concern-value-sync` CI job keep the copies byte-identical and
enforce version bumps when the lib changes. The two copies live at
different relpaths, so the generic cross-plugin drift check does not
cluster them — a dedicated gate is required.

## Verification

All run locally (Git Bash):

- `lib/parse-concern-value.test.sh` — **19/19 pass**, incl. regressions:
double/single-quoted `a#b` and `.work/#topic` keep `#`; quoted value
with trailing comment drops the comment but keeps the inner `#`;
absent/empty key returns the supplied fallback; held behavior (unquoted
trailing ` # comment` stripped, whitespace trimmed, interior quoted
space preserved, trailing slash normalized).
- `orphan-rule-check.test.sh` — **14/14 pass**, incl. new Case 9: a ref
inside a quoted `".scratch#dir"` tier is correctly excluded (the old
`%%#*` truncated to `.scratch`, masking the orphan).
- `scripts/sync-parse-concern-value.sh --check` — copies match source.
- `scripts/check-cross-plugin-source-drift.sh --check` — no
unregistered/drifted clusters.
- `shellcheck` + `shfmt -d` — clean on all new/edited scripts.
- `scripts/validate-plugins.sh` — all manifests + catalog valid.

Closes #482

## Related

Realizes the shared-helper shape #474 called for, scoped to #482's two
sites. #474 (wiring the *remaining* consumers to this helper) stays
parked and is unblocked by this PR.

---------

Co-authored-by: Claude Opus <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 21, 2026
…o shared parse-concern-value helper (#847)

## Summary

Final wiring of the ratified bounded-widen scope on #474: fold the last
two verified hand-rolled `memory_dir`/`contract_dir` copies into #482's
already-landed `parse-concern-value.sh` helper (merged via #756).

-
**`plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh`**
— `audit_noise_convention_roots_pattern` hand-rolled the same naive
`${val%%#*}`-first strip #482 fixed at its two hot sites (truncates a
quoted value at an interior `#`), plus a defect unique to this copy:
`${val//[[:space:]]/}` collapsed *all* interior whitespace, and there
was no quote-stripping. Now resolves both keys through the shared
helper, materialized to
`plugins/docs-hygiene/skills/audit-noise/scripts/lib/parse-concern-value.sh`
(byte-identical + mode-identical, `100755`, to the two existing copies)
and registered in `scripts/sync-parse-concern-value.sh`'s `copies` array
— the same upstream-SSOT + sync-materialization pattern as the two prior
sites (installed plugins are cache-isolated and cannot `source` a
sibling's file).
- **`plugins/session-flow/skills/handoff/context/structure.md`** L116 —
replaced the placeholder note (`# the concern file's memory_dir when set
— resolve it first`, no mechanism named) with a pointer to the shared
helper via the retro skill's Phase 1.1 as the worked call form. Doc note
only: the handoff skill has no script of its own to rewire.

Per the operator's ratified bounded-widen decision (2026-07-19T23:26Z)
and the tower's 2026-07-21 decision comment on #474, the broader
tracker-seam-resolution class (#470/#469/#432/#365/#369) stays
explicitly OUT of scope.

Version bumps stack past two independently-merged sibling PRs that
landed while this one was open: docs-hygiene `0.8.0` → `0.8.2` (stacks
past #846's `0.8.1`, an exact version collision resolved by re-numbering
this PR's entry on top and preserving #846's CHANGELOG entry
underneath); session-flow `0.11.0` → `0.12.1` (stacks past a merged
`0.12.0` that added the `orient` skill).

## Related

- #482 / #756 — landed the shared `parse-concern-value.sh` helper and
its first two consumers; this PR extends it to the remaining two per the
ratified bounded-widen scope.
- #846 — merged concurrently with this PR and independently bumped
`docs-hygiene` to `0.8.1`; this PR's version was restacked to `0.8.2` on
top, both CHANGELOG entries preserved.
- #470 / #469 / #432 / #365 / #369 — the broader tracker-seam-resolution
class; explicitly excluded from this PR's scope per the operator's
ratified decision.

## Test plan

- [x] `bash
plugins/docs-hygiene/skills/audit-noise/scripts/detect.test.sh` — 34/34
pass, including a new regression case: a quoted `memory_dir:
".scratch#dir/" # trailing comment` now correctly resolves to
`.scratch#dir` (flags `.scratch#dir/foo/` as a concrete slice) instead
of the old parser's silent truncation.
- [x] Manual before/after demonstration of the two defects the old
parser had:
- Quoted + interior `#` + trailing comment + trailing slash: old
`[.scratch]` (wrong — truncated inside the quotes) vs new
`[.scratch#dir]` (correct).
- Unquoted value with interior whitespace: old `[mydir]` (wrong —
collapsed) vs new `[my dir]` (correct, surrounding whitespace trimmed
only).
- [x] `scripts/sync-parse-concern-value.sh --check` — all 3 plugin
copies match `lib/parse-concern-value.sh`.
- [x] `scripts/check-cross-plugin-source-drift.sh --check` — no
unregistered/drifted clusters (the three copies have distinct
within-plugin relpaths, so the generic drift check doesn't need to
cluster them; the dedicated sync gate already covers them).
- [x] `scripts/check-changelog-parity.sh --check` and `--check-bump
origin/main` — both touched plugins (`docs-hygiene` 0.8.0→0.8.2,
`session-flow` 0.11.0→0.12.1) carry matching CHANGELOG entries.
- [x] `shellcheck` + `shfmt -d` — clean on all new/edited scripts.
- [x] `scripts/validate-plugins.sh` — all manifests + catalog valid.

Closes #474

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

session-flow: retro context snippet assigns literal HANDOFF_DIR=.work/handoffs, bypassing memory_dir resolution if copied as-is

1 participant