Skip to content

chore: sync next-feat with main - #367

Draft
DrumRobot wants to merge 65 commits into
next-featfrom
main
Draft

chore: sync next-feat with main#367
DrumRobot wants to merge 65 commits into
next-featfrom
main

Conversation

@DrumRobot

Copy link
Copy Markdown
Member

What

Brings next-feat up to date with main (30 commits behind, 3 ahead).

Why

next-feat has drifted far enough from main that it can no longer serve as a usable base. Measured on this branch: cherry-picking a 12-commit feat bundle onto it conflicts at the 3rd commit, because the files those commits were authored against are ~30 commits newer than the copies on next-feat.

The repo's own rule forbids pushing to next-feat directly, so the sync has to arrive as a PR. This is that PR — it carries no new work of its own.

Scope

  • Only main's existing commits. No new commits authored for this PR.
  • The 3 commits currently unique to next-feat (Agent Plugins Specification v1.0.0 adoption + conformance linter, and a marketplace.json source-directory check) are preserved by the merge.

Follow-up

Once this merges, the feat half of the accumulated-work backlog gets its own PR on top of the refreshed base. The fix half is already open at #366.

Test plan

  • CI green on the PR
  • After merge, git rev-list --count origin/next-feat..origin/main reports 0

DrumRobot and others added 30 commits August 20, 2026 14:08
…ocation (#340)

Moves block-wip-register-before-execute.sh beside its .py in wip/resources/ and makes it fail open when either python3 or the implementation is absent - the split pair had been hard-blocking every Edit and Write.

Repoints three guards' hangul-patterns.regex lookup at hook-kit/data/, and corrects three cleanup-guard registrations in hooks.json that still pointed at pre-move hook-kit paths (a missing script exits 127, which reads as 'no objection').

Adds a bats case asserting every registered command resolves on disk - those same three paths had already been corrected three times and reverted twice.
…ivergence)

Add a Don't/Do row for the git merge-tree --write-tree stage-numbering
gotcha (1=merge-base, 2=ours, 3=theirs -- easy to invert) and a new section
walking through how to tell base staleness apart from genuine parallel
divergence before assuming a base switch will fix a conflict, including
the git cherry -v straggler check for when a conflicting commit's content
is already partially superseded by a separate merged commit.
…ed deletion

The Completed-item RAG sync + delete procedure conditioned deletion only
on RAG-sync success, with zero awareness of Plane as canonical backlog.
This left the deletion direction of the canonical-medium principle
unguarded -- only the completion-flip direction (run.md Step 0) and the
creation direction (fix-plan/add.md, added after a prior data-loss
recurrence) were covered. Add Step 1.5 to the delete procedure: for any
Plane-indexed item, verify/complete the Plane issue (or register via
intake if missing) before its local text is removed, since RAG-sync
preserves search but not canonical-record completeness.
fix(git-repo): document conflict root-cause diagnosis (staleness vs divergence)
`cwd_match` tokens are written as path fragments ("ghq/github.com/<org>"),
but detect_workspace compared each token against single path components. No
multi-segment token could ever match, so every workspace resolved to
"default" — handing consumers the placeholder endpoints (localhost qdrant,
empty plane host, collection "wiki") instead of the configured ones. The
failure was silent: "default" is a legitimate return value, so nothing
surfaced the mismatch.

Split tokens on "/" and compare segment sequences, which handles bare
components and path fragments alike. Segments are still compared whole, so a
token cannot match a longer component that merely contains it — a regression
test pins that property alongside the fix.

Verified against the live config: the previously-misresolved workspace now
returns its real endpoint and collections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Following the marketplace symlink lands inside a working checkout, which in
this workspace routinely holds several git worktrees. Each carries its own
copy of every skill, so one skill name matches N+1 directories and `head -1`
picks arbitrarily among them - `cleanup` alone matched seven.

The consequence is silent: the hook still emits a valid path, so nothing looks
wrong, but it can name a worktree's in-progress copy instead of the installed
one. Observed in a single session pointing five times at a cleanup/run.md that
was two lines behind the live file.

Prunes .worktrees / worktrees / .git from the search rather than trying to
rank matches - a worktree is an in-progress branch by definition and is never
the installed copy, so there is no case where preferring one is correct.
…-guard

Editing a plan or research file fires two PostToolUse skill-triggers at once,
this skill's and vibe-coding's, and both demand the same undecided-item scan:
find unresolved markers, turn each into a question, write the answers back.

Nothing said they were the same obligation, so honouring one read as skipping
the other. Names them as one axis with two entry points - satisfying either is
enough, and invoking both doubles the skill load per edit for no extra check.
fix(hook-kit): stop topic dispatch from resolving into nested worktrees
…k to v1

The v2 config names roles ("rag", "backlog", "wiki", "checklist") and carries
the vendor in a `kind` field, so swapping a vendor becomes a one-line config
edit. Consumers of this module still read the v1 flat vendor-named keys, so v2
is translated down to them on load rather than pushed onto every caller.

v2 wins when present; v1 stays readable so an unmigrated machine keeps working
unchanged during the migration window.

Two translation details are load-bearing:

- `kind: "none"` means the role is not configured for that workspace, and must
  surface as an empty value. Letting it fall through to DEFAULT_PROFILE would
  make "unconfigured" and "configured, pointing at localhost" indistinguishable
  downstream — the exact ambiguity the role schema exists to remove.
- `workspace_name` is carried explicitly. DEFAULT_PROFILE already holds
  "default" for that key, so get_profile's setdefault cannot correct it; v1
  configs hid this by naming workspace_name in every profile. Without it the
  report claims "default" while serving a matched profile's real endpoints.

Verified against the live config: both workspaces resolve to their own name,
endpoints and collections, and tracker_root now comes from the configured
checklist path instead of a filesystem guess.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…check on receiver presence

Add workspace-config.sh, a single resolver every hook can source to learn
which receivers (checklist/backlog/rag/wiki) the current workspace is wired
to. Roles carry a `kind` discriminator so a vendor swap is a one-line config
change, and `kind: "none"` makes "not configured" a first-class state. It
reads the v2 role-shaped config, falls back to the v1 flat config, and
degrades to all-"none" (never blocks) on missing file, malformed JSON, or no
usable interpreter.

Wire check-session-rag.sh to it: a workspace with no RAG receiver now skips
the store demand instead of blocking Stop — resolving a false block observed
on a session whose workspace had no RAG MCP server connected at all. The
literal override phrases stay for muscle memory; a config-derived
"skip <kind> store" phrase is added so the escape hatch survives a vendor
change.

Cover the shim with test-workspace-config.sh (19 cases: profile match,
role->kind indirection, collection flatten, multi-segment token match,
default fallback, missing-config degrade).
… v2 config

load_user_config() reads CONFIG_FILE_V2 (~/.config/agent-workspace/config.json)
before CONFIG_FILE, but the isolated_workspace fixture only patched CONFIG_FILE.
On a machine that has a real v2 config the fixture was shadowed, the throwaway
workspace resolved to the wrong (default) profile, and
test_resolve_profile_reaches_workspace_config failed with an empty plane_host.

Neutralise CONFIG_FILE_V2 in the fixture so the loader falls through to the
fixture config, restoring per-workspace isolation for the whole test module.
…obe interpreter in RAG guard

Review follow-up (PR #345 — CodeRabbit + internal review).

1. v2 was detected per-profile via a `roles` key. A v2 profile that defines
   only `match` and relies on top-level `defaults` carries no `roles`, so the
   whole config was read as v1 and `v1_to_roles` overwrote its configured
   checklist (and, in workspace_profile.py, every default receiver). Detect v2
   by the top-level `version == 2` (keeping the roles heuristic as a fallback
   for version-less configs) and translate every v2 profile, no-roles included.
   Applied symmetrically to workspace_profile.py and workspace-config.sh.

2. check-session-rag.sh ran its transcript scan with a bare `python3`, skipping
   the Windows-safe interpreter probe the shim already uses. On the MS Store
   stub (exit 49) the scan yielded empty metrics and the guard silently no-oped.
   Reuse the probe; no usable interpreter -> skip (fail-safe), as before.

3. Regression tests: a no-roles v2 profile keeps its top-level default receivers
   (bash T20-T22, python T16-T17). Both fail on the old detection.
Three hook commands in hooks.json still pointed at skills/hook-kit/resources/
for scripts that were relocated to skills/cleanup/resources/ during the
PR #330 migration, causing them to fail with exit 127 (ghost registration):
block-cleanup-without-rag.sh, block-cleanup-without-claudify.sh, and
block-cleanup-option-below-context-gate.sh. This is hooks.json's first
commit to git (previously untracked at HEAD on main). Full 66-entry audit
recorded in .agents/docs/generated/research-claude-hooks-audit.md.
…session-end RAG receiver gate

Phase 1: v2 role-shaped config + workspace-config.sh resolver + check-session-rag.sh receiver-presence gate. Review findings 1·2·3 fixed (13042e6); 4·5 deferred (Minor polish).
…e and inject WAF-safe User-Agent

Cloudflare 403s the default Python-urllib User-Agent on plane.es6.kr;
adopt the browser-like UA already proven by plane_create_comment.py in
plane_create_issue.py (REST path) and plane_client.py (all API calls).

The embedded Django-shell f-string moves into build_k3s_py_script(),
keeping json.dumps parameter injection and doubled braces (the fix that
previously landed only in this copy), correcting a mismatched </h{level}>
closing tag to </h{indent}>, resolving the kubectl namespace dynamically
(profile k3s_namespace -> plane-ce, replacing the stale -n plane
hardcode), and skipping the fallback gracefully when kubectl is absent.
…acklog copy and inject User-Agent into plane_sync

The fix-plan copy still carried the K3s fallback f-string defect
(ValueError: Invalid format specifier on single braces) that the
plane-backlog copy had already fixed — reconcile makes the two copies
byte-identical again, adopting the repaired template plus the shared
UA/namespace changes. plane_sync.py gains the same WAF-safe User-Agent
on all Plane REST calls.
Covers the two defect classes end to end: outbound User-Agent capture
for plane_create_issue (both copies), plane_sync and plane_client;
K3s fallback template build+parse with hostile quotes/braces; namespace
resolution (profile override -> plane-ce default); graceful skip without
kubectl; and a byte-identity guard against dual-copy drift.
fix: repair plane script defects — WAF-safe User-Agent + K3s fallback template
A refactor commit is a behaviour-preserving restructure (e.g. the
claude-session -> session skill rename) and drives no release bump, so
it needs no same-skill feat/fix rider context. Accept it as a primary
tag on both next-feat and next-fix instead of rejecting it as an
unsupported tag.
The hooks.json registration for this script landed in an earlier commit
of this PR, but the script itself was never committed — the hooks.json
registration-integrity test rightly fails on a reference that would
exit 127 at runtime. Adds the authored script (PreToolUse gate denying
squash-merge recommendations for multi-commit es6kr/skills PRs).
fix(hooks): correct ghost path for block-wip-register-before-execute.sh
…eue, not to done

An audit_status: approved_by_* transition means the audit STAGE is
complete but the ITEM is not — implementation remains. The relocation
HARD STOP now targets the tracker's implementation-queue section (e.g.
TODO) instead of a priority/execution section, forbids flipping the
item to [x] or harvesting it to Completed at audit time, and
completion-criteria carries a cross-note exempting audit-stage items
from the generic deliverable-exists -> done rule.
…up decision

A below-threshold reading taken earlier in a long tool-call chain is
stale-LOW — a full /fix flow can grow usage tens of points past it.
ask-gates gains the stale-LOW direction (re-measure immediately before
deciding and before composing the ask); suggestion-patterns states the
per-model live thresholds (Fable/Mythos 55, Opus 50, others 45 as the
generic fallback) and makes the cleanup option REQUIRED as the
Recommended #1 when a fresh reading is at/above the threshold.
…next call

Step 4's mandatory Skill(next) call now re-measures live context usage
first — if the fresh reading is at/above the session model's threshold
(or a CLEANUP-GATE directive fired), the turn-final ask leads with a
Recommended cleanup option citing the live percentage instead of
composing next-action candidates on a stale-LOW number.
…when findings were fixed (#348)

Step 8's finding-first ordering already sequences finding-handling before the
merge ask, but it stopped at "fix -> reflected in branch". When the fix lands
this session, the Step 7 AI Review Summary still lists those findings as
open/Valid, so recommending merge on top of it is self-contradictory — the
reader sees unresolved findings while the option says merge, and the
merge-attestation URL points at a pre-fix record. The PATCH-after-fix rule
existed only in post.md's "Single Summary preservation guard" (a duplicate-
comment guard), invisible at the Step 8 decision point.

Add a HARD STOP gate to next.md Step 8: after a finding-handling answer that
applies fixes, PATCH the existing Summary to mark them Resolved (+ fixing SHA)
per post.md's procedure BEFORE composing the merge ask. Don't/Do table +
self-check included.
…#350)

Pruning worktrees was necessary but not sufficient. Several marketplaces can
each ship a skill of the same name (consolidate, github-flow, code-quality),
and every one of those is a legitimate install — no prune rule separates them.
`head -1` then picked by directory-walk order, which answered
Skill("es6kr:consolidate") with dgs-plugins/.claude/skills/consolidate/post.md
and Skill("es6kr:github-flow") with dgs-plugins/skills/github-flow/merge.md,
even though both calls named their plugin.

The prefix was already in hand and being discarded. A plugin name is declared
by exactly one marketplace manifest, so resolving the prefix back to that
marketplace narrows the search to the right tree. Without a prefix there is
nothing to narrow by and the search stays global — but then, when more than
one candidate survives, the reminder now lists them instead of presenting an
arbitrary pick as the answer. A wrong path here is silent precisely because it
still resolves to a real SKILL.md.

Verified against the base (which already carries the worktree prune), so the
delta below is this change alone:
  es6kr:consolidate  dgs-plugins/.claude/... -> es6kr-skills/skills/...
  es6kr:github-flow  dgs-plugins/skills/...  -> es6kr-skills/skills/...
  es6kr:cleanup / :next / :wip  unchanged (already correct)
Fail-open boundaries unchanged: unknown skill, non-Skill tool, and missing jq
all still exit 0. Prefix-less calls still resolve, now with the ambiguity note.
Uses head/grep rather than mapfile so it keeps working where `env bash` is 3.2.
…#351)

A workflow_run trigger's `branches:` filter matches the HEAD branch of the run
that fired, not the base of its PR. A PR into next-feat/next-fix runs its
checks on the feature branch (fix/…, feat/…), so filtering on
[next-feat, next-fix] never matched the PRs this workflow exists to serve. It
matched only promotion PRs (head next-*, base main) — the ones it should leave
alone. All four historical runs were that misfire: PR_NUMBER 336, 346, 347.

The payload does carry the base (pull_requests[0].base.ref, verified against
live runs), so drop the filter and gate on base in the job.

Also drop `Test` from the trigger list: it fires on `pull_request:
branches: [main]`, so it never runs for a PR into next-feat/next-fix, and
waiting on it meant waiting for something that could not arrive.

The token limit is left as-is but no longer fails the run. GITHUB_TOKEN cannot
perform markPullRequestReadyForReview even with pull-requests: write, and a red
X on this workflow reads as "the checks failed", which is not what happened.
It now warns and explains instead.
DrumRobot and others added 30 commits August 21, 2026 14:26
fix(fix-plan,plane-backlog): add fix_plan P0-P3 <-> Plane native priority mapping
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ry POST

The AI Review Summary is the audit trail a merge is read against, but a
consolidate run can write specifics it never verified: a `commit <sha>` that
does not exist, or "Copilot: N findings" inflated above the reviewer's actual
comment count. A reader (or a future session) then approves a merge on a
fabricated basis.

Add a PreToolUse guard that, before any consolidate-provenance comment POST
(receiving-code-review / requesting-code-review / consolidate:verified marker),
verifies both mechanically:
  1. every cited `commit <sha>` resolves (local git cat-file first, else gh api
     commits; only a definite 404/422 marks it fabricated)
  2. any "Copilot: N findings" claim reconciles with the PR's actual Copilot
     review-comment count (gh api pulls/<N>/comments)
Both fail open on ambiguity (no gh / offline / unreadable body / a SHA on an
unfetched branch), with ALLOW_SUMMARY_FABRICATED_CLAIMS=1 as the per-command
override. Registered in hooks.json beside block-noncompliant-review-comment.

Document the same discipline as a HARD STOP in consolidate/post.md, and add
tests/test_consolidate_sha_guard.bats (9 cases, offline via a gh mock).
…fter cleanup

The cleanup-adjacent silent-skip in Step 0.66 is a single, one-time use
right after /cleanup. Clarify that every subsequent Stop-hook re-fire
(including during a polling wait for external/user action) falls back
to the minimal single-item confirmation Exception -- a bare status
report with no AskUserQuestion call is never correct there, regardless
of how many times the hook has already fired.
…cation debug logging

- Update check-completed-bloat.js with ^-\s regex anchor to ignore sub-bullets
- Add debug trace logging and trailing question mark check in check-ask-bypass-keywords.sh
- Update hooks.json cleanup guard paths
- Add block-squash-recommend-multi-commit.sh guard script
hooks.json invokes block-manual-handoff-web-task.sh and
block-taskoutput-long-block.sh by direct path (not `bash <path>`), so
Claude Code exec's them relying on the shebang + executable bit. Both
were committed as mode 100644, causing permission-denied failures at
hook time. Also ignore stray *.debug.log scratch output under
hook-kit/resources/.
…guard

- Require matching clickable PR URL for each distinct PR number referenced in AskUserQuestion payloads

- Update conversation-id.md documentation to reflect PR-URL gate validation
Reuse-first was written as an unconditional obligation, but what reuse buys
back is a dependency install / build cache — not the worktree directory. In a
repo with no dependency manifest a replacement worktree is a plain checkout,
so following the rule there produces the wrong answer: it recommends renaming
a branch whose name encodes planned intent in order to save a second.

Add a cost gate to the decision tree — manifest/lockfile presence classifies
the repo as heavy or lightweight — and scope the reuse-first obligation, the
AskUserQuestion ordering mandate, and the inventory requirement to heavy
repos. In a lightweight repo new-create is the recommended option, and a
finished worktree may be offered for removal at any completion stage (push,
PR open, or merge) rather than held as a reuse candidate.

Also correct the count-limit matrix rationale, which cited worktree creation
mechanics rather than the dependency cost that actually motivates reuse, and
split the matrix by repo weight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ports scripts/verify-hooks-json.py + its test suite from es6kr/claude-plugins
(PR #23, merged: es6kr/claude-plugins#23) — that repo
hit the exact class this repo independently hit earlier this session
(hooks.json#L43: a hook relocation commit updated the script's new location
but left the old registration path in place, so the guard stayed listed while
silently enforcing nothing). Neither failure mode has any other signal — a
ghost hook dies with exit 127, which the harness can't distinguish from "ran,
raised no objection".

- scripts/verify-hooks-json.py: detects (1) duplicate (event, matcher,
  script-basename) registrations and (2) ${CLAUDE_PLUGIN_ROOT}-relative paths
  with no file behind them. Handles quoted/unquoted, braced/unbraced, and
  interpreter-prefixed command shapes.
- tests/test_verify_hooks_json.py: 13 tests (ported + translated from Korean —
  this repo is PUBLIC/English-only), covering both failure classes, path
  extraction edge cases (spaces, shell metacharacters), and the duplicate+ghost
  co-occurrence case.
- .github/workflows/test.yml: new `hooks-json-lint` job.

Verified locally against the real hooks/hooks.json (66 registrations, 0
skipped, 0 errors) — confirms this session's earlier ghost-path fix
(commit 7724739) held.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… Step 0

The Step 0 recency check unconditionally re-asked the scope question on
every invocation whenever a recent-completion marker existed, even when
the user had already answered the same ask earlier the same day. Adds a
"Recency-ask answer reuse" HARD STOP: reuse a same-day answer's pattern
instead of re-asking; treat an explicit role-flagged re-invocation after
a completed same-role run as the scope answer itself (skip duplicated
steps, report, proceed to the role's remaining actionable work); re-ask
only on plausibly-moved external state or a conflicting explicit scope
argument.
check-session-rag.sh counted only MCP tool calls (mcp__*__*-store/-find)
while its DENY guidance also named only the MCP medium — a session whose
MCP binding was absent got told the store was impossible even though the
vendor script route (qdrant-import.py / qdrant-search.py) works without
MCP and edit-guard.sh already accepts it. Adds script_store_re /
script_find_re Bash-command counting (mirroring edit-guard's vendor_pat)
and extends the DENY text to name the script route per the tool-priority
rule (skill script -> CLI -> HTTP -> MCP).
fix: accumulate 16 patch-level bug fixes and guard enhancements across skills
…tent titles (#361)

rewind-session.py had two nitpicks surfaced while reviewing the --claude-code
addition:

- main()'s antigravity branch called rewind_antigravity_db() then sys.exit(0)
  unconditionally, discarding its True/False return, so a failed rewind (e.g.
  a missing DB) reported success. Capture the return and exit 0/1, matching the
  --claude-code branch's own success propagation.
- list_claude_sessions() read a user message's content as a string; when it is
  a content-block list the slice raised AttributeError (caught, leaving "(No
  Title)"). Join the blocks' text so array-content sessions get a title.

Verified: antigravity rewind against a missing DB now exits 1; --list-sessions
claude-code extracts titles from both string and array content.
…e, not hardcoded main (#362)

Step 6's Branch guard asserted "the es6kr/skills repo flow is feat/fix branch ->
PR -> main", baking one workspace's release convention into a shared skill. The
PR base is a per-workspace customization: works-config v0.2.0 models it as the
`staging` role (kind "branch" with next_fix/next_feat/main, or "none").

Rewrite the guard to resolve the base from that role instead of a fixed branch:
two-tier staging routes fix/* -> next_fix and feat/* -> next_feat (promoted to
the release branch separately); no staging layer routes feat/fix to the default
integration branch. Reference the schema concept only -- the WSCFG_STAGING_*
resolver export is a later works-config phase, so no shell binding is asserted.
- .githooks/pre-push: skip delete refs with `continue` (not `exit 0`) so a mixed delete + 'local' push still reaches the local-branch guard; heavy CI is skipped only for a delete-only push (HAS_REAL_PUSH).
- skills/fix/scripts/detect-agent-env.sh: header now lists every emitted value (antigravity-agent / antigravity-ide / vscode were undocumented).
- tests/test_structure.bats: duplicate-basename scan also globs *.js (hooks.json registers .js hooks).
- consolidate block-summary-fabricated-claims.sh + post.md: Copilot count uses a case-insensitive contains-match test("copilot";"i") — robust across the inline `Copilot` login and the `copilot-pull-request-reviewer[bot]` review author.
Replaces the hardcoded personal fix_plan path and internal Plane
workspace URL with the shared workspace_profile abstraction
(plane_host / plane_token_env / tracker_root), deriving the workspace
slug from the first Plane issue URL in fix_plan.md (--workspace-slug
overrides). Imports the P0-P3 priority mapping from the sibling
plane_sync.py instead of keeping a third independent copy
(plane-backlog mapping policy), and merges repeated [IDENT-seq] index
lines incrementally so a later line no longer drops fields already
collected (priority/date/is_done).

PR #363 review findings: Copilot inline 1 = CodeRabbit M19, M21, M22.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
INDEX_LINE_RE only matched the Unicode arrow, while
test_matches_ascii_arrow_delimiter (added on this branch's base) asserts
the documented ASCII "->" form also parses -- the test failed against
the shipped regex but was invisible to CI, whose pytest job collects
tests/ only. Adds the (?:UNICODE|ASCII) alternation; the scripts-dir
suite now passes by direct execution.

PR #363 review finding: CodeRabbit M20.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The checker iterated only the outer entries of the installed nested
schema ({event: [{matcher, hooks: [{type, command}]}]}), found no
command key there, and silently skipped every registered hook; it also
resolved the first token of interpreter-prefixed commands, so
"python3 /path/hook.sh" existence-checked the interpreter instead of
the script. Adds iter_hook_commands (both flat and nested schemas) and
resolve_script_operand (shlex tokenization, skipping interpreters,
env-assignment prefixes and flags), skips unresolvable ${VAR} paths,
and adds a CI-visible fixture test under tests/.

PR #363 review finding: CodeRabbit M2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "Decide foreground vs background BEFORE spawning" HARD STOP block
(guidance + Don't/Do table + self-check + footnote) was committed twice
verbatim, back to back. Keeps a single copy so future edits cannot
drift between two identical-looking blocks.

PR #363 review finding: CodeRabbit M17.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: resolve PR #363 audit Pending findings (plane_bulk_update profile, hook checker schema, regex, dedup)
fix: promote next-fix batch (consolidate fabrication guard, session rewind, config-driven PR base)
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