Skip to content

[FORGE-115] feat(skills,init): standardize worktree location on .forge/worktrees/#161

Merged
firatcand merged 5 commits into
mainfrom
feat/FORGE-115-worktree-location-standardization
May 17, 2026
Merged

[FORGE-115] feat(skills,init): standardize worktree location on .forge/worktrees/#161
firatcand merged 5 commits into
mainfrom
feat/FORGE-115-worktree-location-standardization

Conversation

@firatcand
Copy link
Copy Markdown
Owner

Summary

Closes FORGE-115 / P2.5-T19. Eliminates the accidental split where /pickup-task created worktrees at ../${PROJECT}-worktrees/${TICKET}/ while the orchestrator already used .forge/worktrees/<sanitized-id>/. After this lands, both paths converge on the orchestrator's pre-existing convention (inside-repo, gitignored, project-relative).

  • skills/pickup-task/SKILL.md — worktree path moves to .forge/worktrees/${SANITIZED_ID}/. New bash sanitization guard mirrors src/core/workspace.ts sanitizeIssueId (LC_ALL=C-pinned for deterministic ASCII semantics). Anchor switched from pwd/--show-toplevel to git rev-parse --git-common-dir so invocations from inside an existing worktree don't nest. Hydration cp commands rewritten as find -exec so repo paths containing spaces work correctly.
  • src/cli/init/scaffold.ts — new exported helpers appendLineIfMissing and appendToolingExcludes implement the hybrid scaffolding strategy: .eslintignore / .prettierignore get a one-line append (idempotent under CRLF); tsconfig.json / vitest.config.* get a copy-paste snippet emitted to .forge/init-warnings.md rather than auto-mutation (avoids corrupting JSONC tsconfigs or TS configs with extends/composite/references). New safeReadConfig helper enforces a 1 MiB cap and treats ENOENT/EISDIR/ELOOP/EACCES as "unreadable" so optional probes never crash forge init.
  • Docs sweepdocs/{QUICKSTART,WORKTREES,LIFECYCLE}.md, spec/SPEC.md §Module layout (new "Worktree location convention" sub-section), plans/BACKLOG.md sandbox snippet, and CHANGELOG.md [Unreleased] "Behavior change" entry with adopter migration steps.

Scope reality check (post-#160)

PR #160 already shipped the .forge/worktree-task.json marker contract, skills/_shared/worktree-guard.md, and the worktree-guard preflight on all task-scoped skills. The guard is path-agnostic — no changes were needed here. This PR is the remaining path-move + init scaffolding work.

Acceptance criteria (all met)

  • /pickup-task creates worktrees at .forge/worktrees/<sanitized-id>/
  • forge init writes tooling-exclude entries per hybrid strategy
  • BACKLOG.md sandbox-config snippet updated to reference the new path
  • spec/SPEC.md §Module layout documents .forge/worktrees/ as canonical
  • CHANGELOG entry under "Behavior change" notes the path migration
  • No ../*-worktrees/ references remain in src/, skills/, templates/, docs/, spec/ (sole hit: the CHANGELOG migration note describing what changed)
  • forge doctor — explicitly out of scope (doesn't exist yet; T08/FORGE-99)

Test plan

  • npm run typecheck — clean
  • npm test890 pass / 0 fail / 11 pre-existing skips. 14 new tests: 4× appendLineIfMissing (including CRLF-idempotency + 1 MiB size-cap regressions), 6× appendToolingExcludes (eslint/prettier append + tsconfig/vitest warn-only), 2× scaffoldProject end-to-end + sidecar merge
  • npm run build — clean ESM + CJS
  • node dist/bin/forge.cjs --version0.2.2
  • npm run test:pack — 0 forbidden paths in 65-file tarball
  • gitleaks detect — 0 secrets
  • grep -rn '\.\./.*-worktrees' src/ skills/ templates/ docs/ spec/ — 0 hits
  • Manual smoke (post-merge): run /pickup-task in a fresh adopter sandbox, confirm worktree lands at .forge/worktrees/<id>/
  • Manual smoke (post-merge): run forge init in a fixture with .eslintignore + tsconfig.json present, confirm hybrid scaffolding produces correct written / skipped / warned classification

Reviews

Three review passes ran against this diff:

  1. code-reviewer agent — flagged 1 BLOCK (CRLF false-negative in appendLineIfMissing) + 3 IMPROVEMENT (TOCTOU, comment false-positive, missing regression test). BLOCK fixed in 4e491ff.
  2. security-auditor agent (CRITICAL.md path touched) — 0 CRITICAL, 1 HIGH (TOCTOU per existing learning toctou-between-stat-and-read-leaks-raw-fs-errors.md), 1 MEDIUM (unbounded readFileSync), 2 LOW. HIGH+MEDIUM+LOW#1 fixed in 4e491ff.
  3. Codex 2nd-pass (/codex per ETHOS principle [P1-T02] Define settings.yaml zod schema #6 — required for CRITICAL paths) — BLOCK: None (confidence 8). 3 IMPROVEMENT (broaden safeReadConfig to non-ENOENT errors; size-cap TOCTOU; tsconfig comment false-positive) + 2 NIT (classic-Mac \r, BOM, locale gap on bash sanitizer). Broaden-safeReadConfig + LC_ALL=C fixed in c52d2c8. Others explicitly deferred with rationale in commit body and inline code comments.

Decisions surfaced via AskUserQuestion (per ETHOS Confusion Protocol)

  • Q1 (plan-time): forge init scaffolding aggressiveness → Hybrid (append for flat-ignore, warn-only for code configs). Reason captured as Decision: line in commit 21ad78c.
  • Q2 (review-time): Fix scope after review findings → fix BLOCK + HIGH + MEDIUM + LOW#1. Reason captured in commit 4e491ff.

🤖 Generated with Claude Code

firatcand added 5 commits May 17, 2026 20:34
…, anchor on git-common-dir

- Move worktree location from `../${PROJECT}-worktrees/${TICKET}/` to
  `.forge/worktrees/${SANITIZED_ID}/` (inside repo, gitignored, project-relative).
  Converges /pickup-task with the orchestrator path which has been here all along.
- Anchor on `git rev-parse --git-common-dir` instead of pwd/--show-toplevel so
  invocations from inside an existing worktree don't nest the new one.
- Bash sanitization guard mirrors src/core/workspace.ts sanitizeIssueId rules
  (length≤64, [A-Za-z0-9._-] only, no leading -, no /, no \\, no ..) and exits 1
  on invalid input — skill produces a filesystem path, must not silently degrade.
- Hydration cp commands now read from REPO_ROOT, not pwd, so calls from a sibling
  worktree don't silently no-op (was a latent footgun pre-this-change too).
- Update Next: output (cd .forge/worktrees/...) and "✓ Worktree created" example.

Part of FORGE-115 / P2.5-T19.
…ybrid)

`forge init` now writes tooling-exclude entries so eslint/prettier/tsc/vitest
don't recurse into worktrees that live inside the repo at `.forge/worktrees/`.

Hybrid strategy:
- Flat-ignore files (.eslintignore, .prettierignore): append `.forge/worktrees/`
  via new exported helper `appendLineIfMissing(path, line)`. Idempotent —
  records under `written` first run, `skipped` thereafter.
- Code configs (tsconfig.json, vitest.config.{ts,js,mjs,cjs}): WARN-ONLY.
  Emit a copy-paste snippet to `.forge/init-warnings.md` rather than risk
  corrupting the user's config (tsconfig.json may have // comments and
  reject JSON.parse; vitest.config.* is TypeScript with no safe AST edit).
- All four targets detect-and-skip: silent no-op if the file doesn't exist.

Warnings sidecar reshape: `buildWarningsBody` now takes both `unverified[]`
and `toolingWarnings[]`, emits sections only for non-empty input. Existing
unverified-only callers still produce a single-section file (regex tests
matching `primary_host` etc. still pass).

13 new tests: appendLineIfMissing (4), appendToolingExcludes (6), end-to-end
scaffoldProject with all four tooling targets (1), sidecar merge (1), plus
existing tests updated for the new buildWarningsBody signature.

Decision: F2 (init scaffold aggressiveness) → hybrid (eslint/prettier append,
tsconfig/vitest warn-only) per user answer to plan AskUserQuestion.

Part of FORGE-115 / P2.5-T19.
Aligns user-facing docs + SPEC + CHANGELOG + BACKLOG with the new canonical
worktree location landed in this branch's earlier commits.

- docs/WORKTREES.md: directory-layout diagram now shows .forge/worktrees/
  inside the project (not sibling); "convention" paragraph rewritten;
  command examples (worktree add/remove, cleanup) use the new path.
- docs/QUICKSTART.md: daily-flow "cd" line updated.
- docs/LIFECYCLE.md: /pickup-task "Outputs" description updated.
- spec/SPEC.md §Module layout: comment on workspace.ts now states the
  canonical path; new "Worktree location convention" sub-section after
  the layout codeblock documents single-source-of-truth + forge init's
  hybrid tooling-exclude behavior.
- plans/BACKLOG.md: sandbox-grant snippet (ergonomic enhancements item)
  references the new path so the future implementation matches reality.
- CHANGELOG.md: new "Behavior change" entry under [Unreleased] with
  migration steps for v0.3.x adopters who already have sibling worktrees.

Grep-purge verified: zero `../*-worktrees/` references remain across
src/, skills/, templates/, docs/, spec/ — the only hit is the CHANGELOG
migration note itself, which has to mention the old path to describe
what changed.

Part of FORGE-115 / P2.5-T19.
…e-safe glob

Addresses code-reviewer (1 BLOCK + improvements) and security-auditor
(1 HIGH + 1 MEDIUM + 1 LOW) on the FORGE-115 diff.

scaffold.ts:
- New `safeReadConfig(filePath)` helper: stat-then-read inside try/catch so
  a concurrent delete between syscalls returns null instead of leaking
  ENOENT (the bug class documented in docs/learnings/2026-Q2/
  toctou-between-stat-and-read-leaks-raw-fs-errors.md). Enforces a
  MAX_CONFIG_BYTES = 1 MiB cap so a symlink to /dev/zero or pathological
  config can't stall init via unbounded readFileSync.
- `appendLineIfMissing` now routes through safeReadConfig (TOCTOU-safe) and
  CRLF-normalizes before split('\n').includes(line) so Windows-checked-out
  or autocrlf=true files don't double-append on re-run.
- `appendToolingExcludes` tsconfig.json + vitest.config.* reads go through
  safeReadConfig (same TOCTOU + size-cap protection). False-positive caveat
  on marker-substring detection in // comments documented in the function
  body, not silently lurking.

skills/pickup-task/SKILL.md:
- Hydration cp commands switched from `cp "${REPO_ROOT}"/spec/*.md ...`
  (glob expands after quoted variable → re-splits on whitespace if the
  user's repo path contains a space, e.g. macOS "/Users/foo bar/...") to
  `find "${REPO_ROOT}/spec" -maxdepth 1 -name '*.md' -exec cp {} ... \;`
  which handles space-containing paths correctly.

Tests:
- New "CRLF idempotency" test asserts `\r\n`-separated files don't
  double-append (regression for the BLOCK).
- New "size cap" test asserts files over 1 MiB are treated as missing
  (regression for the MEDIUM).

890 pass / 0 fail (was 888). typecheck clean. build clean. pack clean.

Part of FORGE-115 / P2.5-T19.
…le-pin bash sanitizer

Addresses two of three Codex IMPROVEMENT findings on the FORGE-115 diff.

scaffold.ts safeReadConfig:
- Treat EACCES, EISDIR, ELOOP as "unreadable" alongside ENOENT. Previously a
  directory at `cwd/.eslintignore`, a symlink loop, or a permission-denied
  read would surface a raw fs error and abort `forge init`. These files are
  optional probes — the right behavior is "no entry scaffolded for it", not
  "crash the whole flow". Codex caught the EISDIR path explicitly.
- Inline `isFsError` / `isUnreadable` helpers keep both syscall catches DRY.
- Docblock now enumerates every error class we tolerate and explicitly notes
  the remaining size-cap TOCTOU as deferred (not exploitable in the local-init
  threat model; tightening would require openSync+fstatSync+bounded-read).

skills/pickup-task/SKILL.md sanitizer:
- Wrap the [[ ... ]] tests in an LC_ALL=C scope (save/restore around the block)
  so the [A-Za-z0-9._-] character class has deterministic ASCII semantics in
  exotic locales. `[[` is a bash builtin so the VAR=val inline-prefix form
  doesn't apply — explicit save/restore is the correct pattern.

Codex findings explicitly deferred (rationale in code comments):
- Size-cap TOCTOU window — confidence 7, "not merge-blocking for a local
  init helper" (per Codex). Local-only threat model; complexity not warranted.
- tsconfig comment false-positive — confidence 8. Plan explicitly rejected
  a JSONC parser. Worst case = no warning shown for path the user already
  has; never corrupts state. Documented in code comment.
- Classic-Mac \r-only line endings — confidence 9 NIT. File format extinct.
- BOM-prefixed first-line edge — confidence 9 NIT. Vanishingly rare for
  .eslintignore / .prettierignore; safe-fail.

Codex BLOCK verdict: None (confidence 8).
890 pass / 0 fail. typecheck clean. build clean.

Part of FORGE-115 / P2.5-T19.
@firatcand firatcand merged commit 8fa2226 into main May 17, 2026
10 checks passed
@firatcand firatcand deleted the feat/FORGE-115-worktree-location-standardization branch May 17, 2026 18:13
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