Skip to content

feat(config): add skillSearchPaths for read-only awareness of shared skill libraries - #138

Merged
kunchenguid merged 6 commits into
kunchenguid:mainfrom
BartekObudzinski:fm/backpass-skill-search-paths
Sep 21, 2026
Merged

kunchenguid merged 6 commits into
kunchenguid:mainfrom
BartekObudzinski:fm/backpass-skill-search-paths

Conversation

@BartekObudzinski

@BartekObudzinski BartekObudzinski commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Intent

Work upstream backpass issue #32 (#32): make backpass aware of skill libraries that live outside the repository, read-only, so it stops proposing duplicate skills.

The reported failure: a repo whose canonical skill library is a shared tree living outside the repo - a machine-wide shared skills directory, or ~/.claude/skills - ran a backward pass. Several of its AGENTS.md lines already pointed at a skill that does exist, in that shared tree, and every harness on that machine loads skills from there, so the pointers were resolving fine. backpass looked only inside the repo-local configured skills directory, concluded the pointer dangled, and proposed creating that skill again inside the repo. Applying that edit would fork one skill into two copies that drift - the exact rot the tool exists to prevent. The same blind spot skews the budget math for the other extraction edits: extraction is the right call, but the right target is sometimes "point at the existing shared skill" (zero new content) rather than "create a repo-local one".

The ask is a configuration key listing additional directories to consider, alongside the existing skillsDir, for example: {"skillsDir": ".agents/skills", "skillSearchPaths": ["/.hermes/skills-shared", "/.claude/skills"]}.

Semantics are read-only awareness. Skills found under the extra paths count as existing for: resolving skill references made from AGENTS.md - such a reference is not dangling; the existing "failed trigger leads to a description edit, not duplicate content" rule; dedup - an extraction whose content substantially matches an existing shared skill becomes a pointer edit instead of new content.

Writes still go only to the repo's own configured skills directory. backpass must never write into a shared library it does not own.

What Changed

  • Added skillSearchPaths config key (src/config.js) that expands ~, validates entries, and folds them into the existing skillsDirs awareness list so skill references, failed-trigger checks, and dedup treat matching external skills as existing rather than dangling.
  • Enforced read-only handling of search-path skills across src/workspace.js, src/synthesize.js, src/scope.js, and src/target.js: staging withholds them from writes in every scope (including user-scope allowExternal), a search-path root that overlaps or contains the repo/skillsDir is rejected at config validation, and a search-path skill still rides the synthesis fingerprint so a direct write to one aborts the run instead of escaping detection.
  • Updated AGENTS.md and README.md to document the new key and its read-only semantics, and added/extended tests in test/config.test.js, test/skills.test.js, test/synthesize.test.js, and test/workspace.test.js covering validation, staging refusal, and the fingerprint/write-refusal path.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

Risk Assessment

✅ Low: The fix round is a small, well-contained correction that resolves both round-1 findings correctly (ancestor/equal search-path roots are now rejected at config validation and defensively dropped in canonicalizeSearchPathRoots; expandUserPath is a clean re-export with identical behavior), verified with real-behavior tests, and does not touch the write/apply boundary or expand scope.

Testing

Built real sandbox git repo under fake $HOME and drove actual backpass CLI (node bin/backpass.js) live: reproduced reported regression (skillSearchPaths: ["~"] with repo under $HOME) and confirmed config validation now fails with clear error; confirmed repo's own skill stays writable --target with search paths configured; confirmed external shared skill counted read-only in status, both scopes; confirmed --target against skillSearchPaths skill in user scope refused by name (READ_ONLY_SEARCH_PATH). One scenario, a harness writing directly into a skillSearchPaths skill during full synthesis run caught by repo fingerprint, was never driven against the live product: only the mocked-acpx unit test (test/synthesize.test.js) ran, no real/fake AI harness end-to-end pass. That scenario is marked untested rather than pass.

  • Live validation: ✅ go - 5 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Config rejects a skillSearchPaths root that equals/ancestors the repo root (the reported "~" under $HOME regression) ✅ pass live CLI artifact 1: UserError "must not be the repository root, or an ancestor of it", exit 1
Repo's own skillsDir stays a normal, writable --target when skillSearchPaths is configured ✅ pass live CLI artifact 2: analyze --target own-skill succeeds, no refusal
An external shared skill (outside the repo, via skillSearchPaths) is counted as existing/loaded in status, in project and user scope ✅ pass live CLI artifact 3: status --json reports skills:1 in both scopes
A skillSearchPaths skill is refused as a --target write surface even in user scope (allowExternal=true) ✅ pass live CLI artifact 4: UserError names READ_ONLY_SEARCH_PATH reason, exit 1
A harness that writes directly into a skillSearchPaths skill during a real synthesis run is caught by the repo fingerprint and aborts loudly ⏸️ untested no Prior payload only ran node --test test/synthesize.test.js against a mocked acpx subprocess boundary; no live product/CLI run with a real or fake AI harness was performed for this scenario, so no live…
scope.js expandUserPath / config.js expandHomePath dedupe refactor preserves ~ expansion behavior ✅ pass live CLI artifact 1 and 4 both rely on ~ / relative-path expansion through the now-shared helper and behaved correctly
Evidence: Live CLI: ancestor-of-repo skillSearchPaths ("~" with repo under $HOME) rejected at config load
$ HOME=$WORK/home node bin/backpass.js status --json
error config.skillSearchPaths entry "~" must not be the repository root, or an ancestor of it
name a shared skills directory outside the repository
EXIT:1
Evidence: Live CLI: repo's own skill stays a valid writable --target with skillSearchPaths configured
$ node bin/backpass.js analyze --target own-skill --json
targeting skill own-skill (.agents/skills/own-skill/SKILL.md); the memory file and other skills are read-only this run
· no transcripts associated with this repo
{ "memoryFile": "AGENTS.md", "transcripts": 0, "summary": null }
EXIT:0
Evidence: Live CLI: external shared skill counted read-only in status (project + user scope)
project scope: {"skills": 1, ...} EXIT:0
user scope: {"skills": 1, ...} EXIT:0
Evidence: Live CLI: --target on a skillSearchPaths skill in USER scope (allowExternal=true) is refused, never staged
$ HOME=$WORK/home node bin/backpass.js analyze --scope user --target db-tips --json
error --target db-tips is at /tmp/.../shared-skills/db-tips/SKILL.md, which resolves inside a configured skillSearchPaths root
backpass loads and bills that skill but cannot write it there; edit it where it really lives, or point the link at a copy backpass can write
EXIT:1
Evidence: Targeted test run: fingerprint catches a direct write into a skillSearchPaths skill during synthesis
node --test test/synthesize.test.js
✔ a skill reached only through skillSearchPaths is never staged, and a direct write to it is still caught by the fingerprint (170.4ms)
tests 19, pass 19, fail 0
Evidence: Targeted test run: ancestor-overlap regression + dedupe-helper coverage
node --test test/workspace.test.js test/config.test.js test/skills.test.js
✔ a search-path root that is an ancestor of the repo never makes the repo's own skillsDir read-only (19.3ms)
tests 62, pass 62, fail 0
- Outcome: 🔧 1 issue found → no changes applied ✅ across 2 runs (14m34s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ src/config.js:285 - config.skillSearchPaths validation (isFilesystemRoot, src/config.js:154-160,275-292) only rejects the literal OS filesystem root, not a root that is an ancestor of (or identical to) the repository root or the configured skillsDir. In src/workspace.js confinementReason() (line 314-318), the skillSearchPaths check runs BEFORE the repo-containment check, so if a search path is an ancestor of the repo (e.g. skillSearchPaths: [""] on a repo checked out under $HOME, a very common layout), every skill under the repo's own skillsDir also resolves 'inside' that search-path root and gets marked READ_ONLY_SEARCH_PATH. No error is raised; skill writing is silently disabled repo-wide even though the promise this change adds is specifically to leave the repo's own skillsDir writable. This directly contradicts the invariant the added isFilesystemRoot comment states ('a degenerate value that must be rejected, not enforced'), which only covers the bare '/'/drive-root spelling, not an ancestor-of-repo spelling. Fix: extend validation (or canonicalizeSearchPathRoots) to also reject a configured root that equals or contains the repo root / configured skillsDir.
  • ℹ️ src/config.js:163 - expandHomePath (src/config.js:163-168), added by this change, is byte-for-byte identical logic to the pre-existing expandUserPath in src/scope.js:19-24. scope.js already imports from config.js, so scope.js's expandUserPath could delegate to (or re-export) config.js's expandHomePath instead of duplicating the same 4-line implementation.

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → no changes applied ✅
  • ⚠️ live validation verdict: inconclusive (4 of 6 scenarios were driven live against the product); untested: A direct write into a skillSearchPaths skill during synthesis is caught by the repo fingerprint and aborts the run, skillSearchPaths stays read-only even in user scope (allowExternal=true doesn't leak into search paths)
  • Live validation: ⚠️ inconclusive - 4 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Config rejects filesystem-root skillSearchPaths entry ✅ pass live backpass status in a fresh temp repo with .backpassrc.json skillSearchPaths=["/"] exits 1: config.skillSearchPaths entry "/" must not be the filesystem root
Config rejects a skillSearchPaths root that equals or is an ancestor of the repo root (the ancestor-overlap fix) ✅ pass live backpass status with skillSearchPaths=[".."] and ["."] both exit 1 with must not be the repository root, or an ancestor of it. Confirmed as a genuine regression fix by running the identical repo/c…
Skills reached only via an external skillSearchPaths root count as existing alongside the repo's own skill, without error ✅ pass live backpass status with skillSearchPaths pointing at an external /tmp shared dir containing db-helper/SKILL.md, plus a repo-local .agents/skills/local-only/SKILL.md, reports `overflow: 2 skill(s) in .a…
--target on a shared skillSearchPaths skill is refused before any pipeline work runs; --target on the repo's own skill is accepted ✅ pass live backpass propose --target db-helper exits 1 immediately: `--target db-helper is at .../db-helper/SKILL.md, which resolves inside a configured skillSearchPaths root ... backpass loads and bills that…
A direct write into a skillSearchPaths skill during synthesis is caught by the repo fingerprint and aborts the run ⏸️ untested no Prior payload only ran the fixture-based unit test (test/synthesize.test.js, FAKE_ACPX_SCRIPT standing in for the model) and reported that as a pass without ever driving a live synthesis session again…
skillSearchPaths stays read-only even in user scope (allowExternal=true doesn't leak into search paths) ⏸️ untested no Prior payload only ran a non-mocked unit test (test/workspace.test.js) rather than a live --scope user run against a synthetic $HOME with harness-loaded skills directories, so no live result was est…
  • backpass status with skillSearchPaths=["/"] in a temp repo
  • backpass status with skillSearchPaths=[".."] and ["."] in a temp repo, plus the same repo/config against pre-fix commit 583699e for contrast
  • backpass status with a valid external skillSearchPaths root containing a real SKILL.md, alongside a repo-local skill
  • backpass propose --target db-helper (shared search-path skill) and --target local-only (repo skill) in the temp repo

🔧 No changes applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 5 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Config rejects a skillSearchPaths root that equals/ancestors the repo root (the reported "~" under $HOME regression) ✅ pass live CLI artifact 1: UserError "must not be the repository root, or an ancestor of it", exit 1
Repo's own skillsDir stays a normal, writable --target when skillSearchPaths is configured ✅ pass live CLI artifact 2: analyze --target own-skill succeeds, no refusal
An external shared skill (outside the repo, via skillSearchPaths) is counted as existing/loaded in status, in project and user scope ✅ pass live CLI artifact 3: status --json reports skills:1 in both scopes
A skillSearchPaths skill is refused as a --target write surface even in user scope (allowExternal=true) ✅ pass live CLI artifact 4: UserError names READ_ONLY_SEARCH_PATH reason, exit 1
A harness that writes directly into a skillSearchPaths skill during a real synthesis run is caught by the repo fingerprint and aborts loudly ⏸️ untested no Prior payload only ran node --test test/synthesize.test.js against a mocked acpx subprocess boundary; no live product/CLI run with a real or fake AI harness was performed for this scenario, so no live…
scope.js expandUserPath / config.js expandHomePath dedupe refactor preserves ~ expansion behavior ✅ pass live CLI artifact 1 and 4 both rely on ~ / relative-path expansion through the now-shared helper and behaved correctly
  • node bin/backpass.js status --json (ancestor-of-repo skillSearchPaths rejection, live)
  • node bin/backpass.js analyze --target own-skill --json (repo skill stays writable, live)
  • node bin/backpass.js status --json / --scope user (external shared skill counted read-only, live, both scopes)
  • node bin/backpass.js analyze --scope user --target db-tips --json (search-path skill refused as write target in user scope, live)
  • node --test test/synthesize.test.js (fingerprint catches direct write to a skillSearchPaths skill)
  • node --test test/workspace.test.js test/config.test.js test/skills.test.js (ancestor-overlap + dedupe-helper regressions)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.


Re-trigger note (2026-09-21)

This edit exists only to fire an edited event so that Require no-mistakes body-compliance runs against the current head 48114794…; that workflow fires on opened/edited/reopened and never on synchronize, so no compliance run has ever existed for this head. Requested in the triage comment above. Nothing in the intent, change description, risk assessment, testing table, or pipeline attestation blocks was modified.

@greptile-apps

greptile-apps Bot commented Sep 16, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the previously reported writable-search-path, external-write-detection, documentation, and skillsDir-overlap issues are fully addressed.

Reviews (4) · Last reviewed commit: "no-mistakes(ci): Fixed ci-5: skillSearch..."

Comment thread src/config.js
// awareness list (consulted after `skillsDir` in list order) rather than a second plumbing;
// `~` is expanded here so the loaders that join `repoRoot` never mishandle a home path.
const searchPaths = (validated.skillSearchPaths || []).map((p) => expandHomePath(p));
if (searchPaths.length) validated.skillsDirs = [...(validated.skillsDirs || []), ...searchPaths];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Search paths become writable

Appending skillSearchPaths to skillsDirs removes the distinction between read-only search paths and writable skill roots. In user scope, synthesis allows external roots, so a writable shared-library skill can be staged, measured, and later updated by the apply writer. A backpass --scope user run can therefore modify a search path even though the accepted intent requires these paths to remain read-only and all writes to target only skillsDir. Preserve the read-only provenance of search paths through staging and application.

Context Used: If there is a VISION.md file at the root of the repo, the PR must not conflict / diverge / drift from it. If the PR description has an "Intent" section, respect that as the accepted user intent. - Do make comments if anything in the implementation ... (source)

Knowledge Base Used: Workspace state and configuration

Comment thread src/config.js
// awareness list (consulted after `skillsDir` in list order) rather than a second plumbing;
// `~` is expanded here so the loaders that join `repoRoot` never mishandle a home path.
const searchPaths = (validated.skillSearchPaths || []).map((p) => expandHomePath(p));
if (searchPaths.length) validated.skillsDirs = [...(validated.skillsDirs || []), ...searchPaths];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 External writes escape detection

In project scope, shared skills are withheld from staging, but their absolute source paths are still shown to the synthesis agent for grounding and excluded from the repository fingerprint. If the synthesis harness edits one of those paths directly, the shared library changes without assertRepoUntouched detecting it. This violates the accepted read-only intent and the repository directive that model behavior must be enforced in code rather than trusted to prompt compliance. Include these exposed files in change detection or provide their contents without exposing writable source paths.

Context Used: If there is a VISION.md file at the root of the repo, the PR must not conflict / diverge / drift from it. If the PR description has an "Intent" section, respect that as the accepted user intent. - Do make comments if anything in the implementation ... (source)

Knowledge Base Used:

Comment thread README.md Outdated
BartekObudzinski added a commit to BartekObudzinski/backpass that referenced this pull request Sep 16, 2026
…chenguid#138 (skillSearchPaths read-only awareness): - ci-4/ci-5 (src/config.js:408, "search paths become writable" / "external writes escape detection"): `skillSearchPaths` were merged into `skillsDirs`, which `prepareWorkspace` also uses to decide what's staged/writable. In user scope `allowExternal` disables all confinement, so a configured search-path root was staged and writable there, and even where staging correctly withheld it (project scope), it was excluded from the repo fingerprint like any other withheld skill, so a direct write to it would silently escape detection. Fixed by threading the raw `skillSearchPaths` roots (`searchPathRoots`) into `prepareWorkspace`, `skillStagingRefusal` (used by `--target`), and `measureWorkspace`, adding an unconditional `READ_ONLY_SEARCH_PATH` refusal that applies regardless of `allowExternal`/confinement, and keeping search-path skills in the synthesis fingerprint (unlike ordinary withheld skills) so a direct write to one now aborts the run loudly. Touched: src/workspace.js, src/synthesize.js, src/scope.js, src/target.js. - ci-6 (README.md:737, "trigger tuning is impossible"): corrected the doc claim that a failed trigger tunes a shared skill's description — a search-path skill is read-only in every scope, so it's now documented as "reported as already covered, not tuned." ci-1/ci-2/ci-3 were left untouched per instructions (GitHub holding fork workflow runs for maintainer approval; not a code defect). Added regression tests (test/workspace.test.js, test/synthesize.test.js) covering: a search path staying read-only in user scope unlike an ordinary external skills dir, a newly-created file under a search-path root being reported `stray` rather than a measurable `created` change, and an end-to-end synthesis run rejecting loudly when the harness writes directly to a search-path skill. Verified: lint, format check, typecheck, and the full test suite (676 tests) all pass
…skill libraries

Skills that live outside the repo (a machine-wide shared tree, or ~/.claude/skills)
now count as existing for AGENTS.md reference resolution, the failed-trigger rule, and
extraction dedup, so backpass stops proposing duplicates of skills it does not own.

skillSearchPaths is an array of directory paths (~ expanded) accepted wherever skillsDir
is; loadConfig folds it into the existing skillsDirs awareness list, consulted after
skillsDir in list order. It is read-only: every write still targets skillsDir, and a
skill resolving outside the repo is withheld from the synthesis staging copy.

Refs kunchenguid#32
…chenguid#138 (skillSearchPaths read-only awareness): - ci-4/ci-5 (src/config.js:408, "search paths become writable" / "external writes escape detection"): `skillSearchPaths` were merged into `skillsDirs`, which `prepareWorkspace` also uses to decide what's staged/writable. In user scope `allowExternal` disables all confinement, so a configured search-path root was staged and writable there, and even where staging correctly withheld it (project scope), it was excluded from the repo fingerprint like any other withheld skill, so a direct write to it would silently escape detection. Fixed by threading the raw `skillSearchPaths` roots (`searchPathRoots`) into `prepareWorkspace`, `skillStagingRefusal` (used by `--target`), and `measureWorkspace`, adding an unconditional `READ_ONLY_SEARCH_PATH` refusal that applies regardless of `allowExternal`/confinement, and keeping search-path skills in the synthesis fingerprint (unlike ordinary withheld skills) so a direct write to one now aborts the run loudly. Touched: src/workspace.js, src/synthesize.js, src/scope.js, src/target.js. - ci-6 (README.md:737, "trigger tuning is impossible"): corrected the doc claim that a failed trigger tunes a shared skill's description — a search-path skill is read-only in every scope, so it's now documented as "reported as already covered, not tuned." ci-1/ci-2/ci-3 were left untouched per instructions (GitHub holding fork workflow runs for maintainer approval; not a code defect). Added regression tests (test/workspace.test.js, test/synthesize.test.js) covering: a search path staying read-only in user scope unlike an ordinary external skills dir, a newly-created file under a search-path root being reported `stray` rather than a measurable `created` change, and an end-to-end synthesis run rejecting loudly when the harness writes directly to a search-path skill. Verified: lint, format check, typecheck, and the full test suite (676 tests) all pass
… canonicalize consistently

The read-only guard for skillSearchPaths roots could fail open in three ways:
- the read-only identity set was built with fs.realpathSync over raw configured
  values, so a RELATIVE entry resolved against the process cwd while its real skill
  source resolved against the repo root - they never matched and the refusal never fired;
- the --target refusal path (skillStagingRefusal) did not home-expand a ~-prefixed root,
  so realpath threw and the entry was dropped from the set;
- any root that could not be canonicalized was silently discarded, deleting the promise
  instead of enforcing it.

Fix with one shared canonicalizeSearchPathRoots helper: expand ~, resolve relative
entries against the repo root (never cwd), then follow links; used by staging
(prepareWorkspace) and the --target refusal (skillStagingRefusal), with the same set
feeding the measurement fingerprint. Fail CLOSED - an existing root that cannot be
canonicalized raises a clear UserError naming config.skillSearchPaths rather than being
dropped (a not-yet-existing root keeps its resolved absolute identity, since nothing can
load from or be written to it). Reject degenerate roots (empty string, filesystem root)
at config validation.

Regression tests: relative root resolved against repo root with cwd deliberately
different; ~-prefixed root at the --target refusal site; unresolvable root failing
closed; degenerate roots rejected at config validation. Full suite (680) green.

Refs kunchenguid#32
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate.

Diff reviewed on 27600bd4119974ba0963c8175b8e3b5bb20fb6d3. Fork PR from @BartekObudzinski (helps #32). First-time fork workflows approved after security-clean review: CI 35113768509 and Guard 35113768373 (Guard re-run after an initial cancel). CI (ubuntu + macos) and Guard are green on this HEAD. Mergeable true; merge state UNSTABLE solely from Greptile Review FAILURE — not a gate (repo does not use Greptile). Greptile P1s on the first commit (writable search paths / fingerprint escape) were addressed in 27600bd via unconditional READ_ONLY_SEARCH_PATH staging refusal + fingerprint retention; P2 doc wording was corrected. No workflow-file changes; security clean.

Contract-class: opt-in. Default is skillSearchPaths: []. Unconfigured runs do not fold any extra roots into skillsDirs (if (searchPaths.length) only). Awareness of shared libraries and the read-only write refusals activate only when the user sets paths. Not new-default: no always-on discovery reorder. Auto-merge is allowed for opt-in once gates are green — but not yet.

VISION per-rule

  • Evidence is the only currency: aligns (shared skills stop false dangling/duplicate proposals; evidence quality improves).
  • The human owns the weights: aligns (explicit config; writes still only skillsDir; no silent write-scope widen).
  • Nothing the model says is taken on faith: aligns (search-path roots stay fingerprinted; direct writes fail the run in code).
  • The budget is the constraint: aligns (shared skill descriptions can enter budget awareness when configured).
  • It reads what you already have and owns nothing: aligns (local shared trees; no upload/key/server).
  • Failure is loud and named: aligns (READ_ONLY_SEARCH_PATH / stray reasons; no quieter degraded write path).
  • Scope: aligns (project run still does not write user-level surfaces; search paths read-only in every scope).

Blocker (author, not captain): Require no-mistakes is not green for this HEAD binding. Opened body-compliance succeeded on earlier head 1c98bfb; the later edited run (event 148) failed with Pipeline attestation head_sha does not match the current PR head. Body now attests 27600bd, but no successful no-mistakes run has validated that pairing. Please run git push no-mistakes (or re-trigger opened/edited/reopened) so Require no-mistakes goes green on this HEAD. I will not merge until that check is green. No Firstmate captain-decision hold — contract is opt-in.

@BartekObudzinski
BartekObudzinski force-pushed the fm/backpass-skill-search-paths branch from 27600bd to adc8243 Compare September 16, 2026 16:30
Comment thread src/config.js Outdated
…a root that equals or contains the repo's own skillsDir (e.g. ".agents" covering ".agents/skills"), not just the repo root. Root cause: config.js validate() only checked overlap against repoRoot; canonicalizeSearchPathRoots (the runtime drop-logic) lived in workspace.js and only took repoRoot too, so a skillsDir-containing search path passed config load and later made the repo's own skill files read-only during synthesis. Fix: moved canonicalizeSearchPathRoots into config.js (workspace.js now imports and re-exports it - single shared helper, no circular import since workspace.js already depended on config.js), extended its signature to take an optional skillsDir and drop overlapping roots against it too, and rewired validate() to call it instead of hand-rolling a second comparison. Updated prepareWorkspace/skillStagingRefusal/target.js call sites to pass skillsDir through so the runtime defense-in-depth also covers this case for direct callers. Added regression tests: config.test.js (load-time rejection for both the containing case ".agents"/".agents/skills" and the equal case) and workspace.test.js (canonicalizeSearchPathRoots + prepareWorkspace end-to-end, mirroring the existing repo-ancestor test). Verified: full `pnpm run check` (lint, format:check, typecheck, all 684 tests) passes
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate.

Re-triaged on new HEAD 48114794a86449aa1f5a8831a15edbdded19f3ff (after prior stamp on 27600bd). Fork PR from @BartekObudzinski (helps #32).

Fork workflows approved after security-clean review of tip vs main: CI 35124922668 and Guard 35124922671. Both are green (ubuntu + macos build-and-test; Generated files must not be hand-edited). Greptile Review pass — not a gate. No workflow-file changes; security clean. Mergeable; behind main by 1 (#137 / 7d92241) — not rebasing yet (not auto-merge-ready until no-mistakes is green).

Contract-class: opt-in. Tip still defaults skillSearchPaths: []; fold into skillsDirs only when searchPaths.length is non-zero. Unconfigured runs unchanged. Read-only enforcement remains (unconditional READ_ONLY_SEARCH_PATH, fingerprint retention, reject/drop roots that equal/ancestor repo or skillsDir). Not new-default.

VISION per-rule

  • Evidence is the only currency: aligns (shared skills stop false dangling/duplicate proposals).
  • The human owns the weights: aligns (explicit config; writes still only skillsDir).
  • Nothing the model says is taken on faith: aligns (search-path roots stay fingerprinted; direct writes fail in code).
  • The budget is the constraint: aligns (shared skills enter awareness only when configured).
  • It reads what you already have and owns nothing: aligns (local shared trees; no upload/key/server).
  • Failure is loud and named: aligns (READ_ONLY_SEARCH_PATH / stray; fail-closed canonicalize).
  • Scope: aligns (search paths read-only in every scope, including user/allowExternal).

Blocker (author, not captain): Body attestation already matches this HEAD (48114794…), but no Require no-mistakes / body-compliance run exists for this HEAD (workflow only fires on opened/edited/reopened, not synchronize). Latest fork Require no-mistakes run is still action_required on prior head adc82431 (35124913969) — approving that stale head would not bind the current tip. Please run git push no-mistakes (or otherwise re-trigger an edited body event) so Require no-mistakes starts on 48114794…; I will approve that fork run when it appears. Will not merge until that check is green on this HEAD. No Firstmate captain-decision hold — contract is opt-in.

@BartekObudzinski

Copy link
Copy Markdown
Contributor Author

Re-triggered as requested — the body edit fired an edited event, so Require no-mistakes now has a run on this head: https://github.com/kunchenguid/backpass/actions/runs/35572243013

It is waiting on maintainer approval (action_required), so it needs the approval you offered in your triage note. No code changed: head is still 48114794a86449aa1f5a8831a15edbdded19f3ff, and the four existing checks are still green.

@kunchenguid
kunchenguid merged commit 2bade32 into kunchenguid:main Sep 21, 2026
5 checks passed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is merged. Thank you @BartekObudzinski — really appreciate you taking the time on this.

Approved fork Require no-mistakes 35572243013 on tip 48114794… after your body re-trigger; it went green. CI + Guard were already green. Squash-merged (opt-in skillSearchPaths: [] default; helps #32).

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.

2 participants