Context
5 follow-up findings from negative-scenario-reviewer code review of PR #279 (feat/279-preflight-per-session-namespacing), verdict ACCEPT-with-FU. None blocking merge; all deferred per inline-FU heuristic (architectural / cross-file / symmetric carry-forward).
Findings
FU-1 [P2] — Extend validate-plan-marker-sites.mjs to cover PREFLIGHT_MARKER_*
The validator at scripts/validate-plan-marker-sites.mjs covers PLAN_MARKER_* constants only. PR #279 added sibling PREFLIGHT_MARKER_* constants to scripts/lib/marker-paths.mjs + hooks/lib/marker-paths.sh with identical shape but no drift coverage.
Repro: change hooks/lib/marker-paths.sh line PREFLIGHT_MARKER_SUFFIX_MAXLEN=128 to =64. Run node scripts/validate-plan-marker-sites.mjs. Exit 0 — drift unobserved.
Fix: extend validator (or rename to validate-marker-sites.mjs) to round-trip-validate PREFLIGHT_MARKER_LEGACY_BASENAME, PREFLIGHT_MARKER_SUFFIX_CHARCLASS, PREFLIGHT_MARKER_SUFFIX_MAXLEN, PREFLIGHT_MARKER_BASENAME_RE shell↔mjs parity.
Estimated: ~30 LOC in the validator, plus 4-5 lines of regression tests.
FU-2 [P3] — Wire any_preflight_marker_exists into SessionStart/End
hooks/lib/marker-paths.sh defines any_preflight_marker_exists for symmetry with any_plan_marker_exists, but it has zero callers. Documented intent (SessionStart orphan-sweep + SessionEnd own-session cleanup) is unimplemented for preflight markers. Suffixed forms accumulate unboundedly across the install lifetime.
Repro: open N concurrent sessions, each writes .preflight-done.<sid> via the helper. After all sessions end, files persist in .checkpoints/.
Fix: either (a) wire into hooks/em-recall-sessionstart.sh + scripts/em-session-end-prompt.mjs mirroring the plan-marker pattern; or (b) GC via a separate cron / install-time sweep job.
FU-3 [P3] — Canonicalize per-session marker path in direct-Write deny
hooks/preflight-gate.sh:170-189 detects direct-Write to per-session marker via basename(CANON_TOOL) + dirname(CANON_TOOL) == CANON_PRIMARY_DIR. The legacy marker has CANON_PREFLIGHT = _canonicalize_one(PREFLIGHT_MARKER_LEGACY, ...) — symmetric treatment is missing for PREFLIGHT_MARKER_SID.
Repro: ln -s /tmp/elsewhere $REPO/.checkpoints/.preflight-done.<sid>, then attempt Write to that path. dirname(CANON_TOOL) would be /tmp (link target's parent), bypassing the _tool_parent = CANON_PRIMARY_DIR guard.
Mitigations in place: file-system permissions on .checkpoints/ (default install: not world-writable). Note: the identical structural shape ships unfixed for .last-user-prompt.<sid>.json today.
Fix: canonicalize a candidate PREFLIGHT_MARKER_SID path family and equality-check against CANON_TOOL (parallel to CANON_PREFLIGHT).
FU-4 [NIT] — Dead code if FU-2 not adopted
hooks/lib/marker-paths.sh any_preflight_marker_exists has zero callers. If FU-2 lands, this becomes live code. If FU-2 won't fix, remove the function.
FU-5 [NIT] — --root=/path form inconsistency in helper-invocation gate
hooks/preflight-gate.sh:261 regex '\-\-root[[:space:]]+[^[:space:]]' requires space-separated --root /path. --root=/path form is rejected by the helper's parseArgs separately. Both deny, but the gate's deny message says "missing --root" which is misleading for the = form.
Fix: either accept --root= in the gate regex AND helper parseArgs, or tighten gate deny message to explicitly require space-separated form. Add a test row for --root=/path invocation.
Disposition rationale
- FU-1 / FU-3: P2/P3 hardening — file as issue per inline-FU heuristic (cross-file).
- FU-2 / FU-4: architectural lifecycle wiring; tied together.
- FU-5: NIT-level UX; cross-component.
PR #279 ships with docstrings retracted to reference this issue. None of these are regressions; PR closes the actual #279 stomp bug + adds the env-prefix structural defense as planned.
Refs
Context
5 follow-up findings from
negative-scenario-reviewercode review of PR #279 (feat/279-preflight-per-session-namespacing), verdict ACCEPT-with-FU. None blocking merge; all deferred per inline-FU heuristic (architectural / cross-file / symmetric carry-forward).Findings
FU-1 [P2] — Extend
validate-plan-marker-sites.mjsto coverPREFLIGHT_MARKER_*The validator at
scripts/validate-plan-marker-sites.mjscoversPLAN_MARKER_*constants only. PR #279 added siblingPREFLIGHT_MARKER_*constants toscripts/lib/marker-paths.mjs+hooks/lib/marker-paths.shwith identical shape but no drift coverage.Repro: change
hooks/lib/marker-paths.shlinePREFLIGHT_MARKER_SUFFIX_MAXLEN=128to=64. Runnode scripts/validate-plan-marker-sites.mjs. Exit 0 — drift unobserved.Fix: extend validator (or rename to
validate-marker-sites.mjs) to round-trip-validatePREFLIGHT_MARKER_LEGACY_BASENAME,PREFLIGHT_MARKER_SUFFIX_CHARCLASS,PREFLIGHT_MARKER_SUFFIX_MAXLEN,PREFLIGHT_MARKER_BASENAME_REshell↔mjs parity.Estimated: ~30 LOC in the validator, plus 4-5 lines of regression tests.
FU-2 [P3] — Wire
any_preflight_marker_existsinto SessionStart/Endhooks/lib/marker-paths.shdefinesany_preflight_marker_existsfor symmetry withany_plan_marker_exists, but it has zero callers. Documented intent (SessionStart orphan-sweep + SessionEnd own-session cleanup) is unimplemented for preflight markers. Suffixed forms accumulate unboundedly across the install lifetime.Repro: open N concurrent sessions, each writes
.preflight-done.<sid>via the helper. After all sessions end, files persist in.checkpoints/.Fix: either (a) wire into
hooks/em-recall-sessionstart.sh+scripts/em-session-end-prompt.mjsmirroring the plan-marker pattern; or (b) GC via a separate cron / install-time sweep job.FU-3 [P3] — Canonicalize per-session marker path in direct-Write deny
hooks/preflight-gate.sh:170-189detects direct-Write to per-session marker viabasename(CANON_TOOL)+dirname(CANON_TOOL) == CANON_PRIMARY_DIR. The legacy marker hasCANON_PREFLIGHT = _canonicalize_one(PREFLIGHT_MARKER_LEGACY, ...)— symmetric treatment is missing forPREFLIGHT_MARKER_SID.Repro:
ln -s /tmp/elsewhere $REPO/.checkpoints/.preflight-done.<sid>, then attemptWriteto that path.dirname(CANON_TOOL)would be/tmp(link target's parent), bypassing the_tool_parent = CANON_PRIMARY_DIRguard.Mitigations in place: file-system permissions on
.checkpoints/(default install: not world-writable). Note: the identical structural shape ships unfixed for.last-user-prompt.<sid>.jsontoday.Fix: canonicalize a candidate
PREFLIGHT_MARKER_SIDpath family and equality-check againstCANON_TOOL(parallel toCANON_PREFLIGHT).FU-4 [NIT] — Dead code if FU-2 not adopted
hooks/lib/marker-paths.shany_preflight_marker_existshas zero callers. If FU-2 lands, this becomes live code. If FU-2 won't fix, remove the function.FU-5 [NIT] —
--root=/pathform inconsistency in helper-invocation gatehooks/preflight-gate.sh:261regex'\-\-root[[:space:]]+[^[:space:]]'requires space-separated--root /path.--root=/pathform is rejected by the helper'sparseArgsseparately. Both deny, but the gate's deny message says "missing --root" which is misleading for the=form.Fix: either accept
--root=in the gate regex AND helperparseArgs, or tighten gate deny message to explicitly require space-separated form. Add a test row for--root=/pathinvocation.Disposition rationale
PR #279 ships with docstrings retracted to reference this issue. None of these are regressions; PR closes the actual #279 stomp bug + adds the env-prefix structural defense as planned.
Refs
negative-scenario-reviewersubagent (single round, ACCEPT-with-FU).