forge-orkes 0.75.0 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/bin/create-forge.js +27 -0
  2. package/experimental/m10/README.md +4 -4
  3. package/experimental/m10/install.sh +10 -9
  4. package/experimental/m10/source/hooks/protect-primary-checkout.sh +149 -0
  5. package/package.json +1 -1
  6. package/template/.claude/hooks/README.md +121 -0
  7. package/template/.claude/hooks/forge-state-rollup.sh +431 -0
  8. package/template/.claude/hooks/protect-primary-checkout.sh +149 -0
  9. package/template/.claude/hooks/tests/README.md +20 -0
  10. package/template/.claude/hooks/tests/forge-state-rollup.test.sh +282 -0
  11. package/template/.claude/hooks/tests/protect-primary-checkout.test.sh +147 -0
  12. package/template/.claude/settings.json +20 -0
  13. package/template/.claude/skills/chief-of-staff/SKILL.md +16 -11
  14. package/template/.claude/skills/forge/SKILL.md +11 -7
  15. package/template/.claude/skills/initializing/SKILL.md +20 -1
  16. package/template/.claude/skills/reviewing/SKILL.md +17 -0
  17. package/template/.claude/skills/upgrading/SKILL.md +22 -2
  18. package/template/.forge/FORGE.md +8 -5
  19. package/template/.forge/bin/new-worktree.sh +61 -0
  20. package/template/.forge/bin/tests/new-worktree.test.sh +70 -0
  21. package/template/.forge/git-hooks/pre-commit +57 -0
  22. package/template/.forge/git-hooks/tests/pre-commit.test.sh +97 -0
  23. package/template/.forge/migrations/0.77.0-state-rollup-executable.md +120 -0
  24. package/template/.forge/migrations/0.78.0-weld-primary-checkout.md +66 -0
  25. package/template/.forge/templates/state/desire-path.yml +3 -0
  26. package/experimental/m10/source/hooks/forge-branch-guard.sh +0 -61
@@ -312,6 +312,22 @@ Deferred issues triage: Glob `.forge/deferred-issues/*.md` (else legacy `.forge/
312
312
  - **Fix-now** → route to `planning` fix mode before completing milestone
313
313
  - **Dismiss** → set `status: dismissed` + `dismissed_reason` in the issue's file (or the legacy file)
314
314
 
315
+ Close-the-loop triage (advisory — never blocks): catches shipped-but-still-open GitHub issues. Forge produces resolution artifacts (ADRs, desire-paths, CHANGELOG/commits) that cite the issue they resolve, but nobody circles back to close the issue — the open list accumulates already-done work. This step cross-references those artifacts against **open** issues and surfaces likely-resolved ones for the operator to close. **Detect-and-surface only — NEVER auto-close** (closing a GitHub issue is outward-facing → operator confirmation, agent safety rules).
316
+
317
+ 1. **Gate / degrade first.** Resolve `forge.upstream_repo` from `project.yml`. If absent, OR `gh` not on PATH, OR `gh auth status` fails, OR offline → **skip the whole step** (at most one log line: *"close-the-loop: gh unavailable — skipped"*). Never blocks reviewing or completion. (Same posture as `forge/desire-paths-review.md`'s resolve→check-gh→offer and notion-integration's advisory-everywhere.)
318
+ 2. **Collect resolution-intent `#N` refs produced THIS milestone** (milestone-scoped default — cheap, fires when the artifact is fresh):
319
+ - **ADRs** authored/touched this milestone whose `**Context source:**` / `resolves` / `closes` line cites `forge#N` (or the `…/issues/N` URL). **Exclude bare supersession / see-also mentions** — a *"Superseded by ADR-021 ([forge#19](…))"* note references #19, it does not resolve it. Only resolution-intent lines count.
320
+ - **desire-path** files with `resolves_issue: N`.
321
+ - **CHANGELOG / commit** lines landed this milestone with `closes|fixes|resolves #N`.
322
+ Scope by this milestone's git range / this milestone's artifacts — prose-directed grep, no executable.
323
+ 3. **Intersect cited ∩ still-open.** `gh issue view N --json state,title` each; **keep only `state == OPEN`**. An `#N` gh can't resolve (nonexistent / transferred / errors on that call) OR reports CLOSED → **no candidate** (surface only gh-confirmed-open — zero false positives). Skip any `#N` with a `.forge/state/issue-loop/declined/{N}.yml` marker (glob first — mirrors the desire-path `declined/` skip).
324
+ 4. **Surface survivors** (existing triage idiom): *"**Close-the-loop** ({N}): 1. #N '{title}' — looks resolved by {artifact} — close it? [y/N]"*.
325
+ - **y** (or "close") → **do NOT close autonomously.** Print the exact command for the operator to run (or to approve Forge running as an explicit per-action step): `gh issue close N --comment "Resolved by {artifact} ({ref}). Closing via Forge close-the-loop."`
326
+ - **N** (or "still open") → write `.forge/state/issue-loop/declined/{N}.yml` (`{issue: N, declined_at: <ISO date>, reason: "<one line>"}`; create the dir if absent) so it is not re-nagged. The marker is inert once the issue actually closes (gh stops returning it as open).
327
+ 5. **Optional full sweep** (operator-invoked, natural language — no new command): if the operator asks *"run a full close-the-loop sweep"* / *"check for orphaned issues"*, run steps 1–4 over **all** open issues × **all** shipped resolution artifacts repo-wide (not just this milestone), same rules + `declined/` markers. This is the periodic-cleanup path; the milestone-scoped default stays quiet.
328
+
329
+ Zero candidates → silent (the common case). This step is **advisory — it never gates milestone completion** (see Gates).
330
+
315
331
  ## Step 7: Backlog + Route
316
332
 
317
333
  ### Backlog
@@ -371,6 +387,7 @@ Idempotent: a backlog with no terminal items and all-canonical statuses is left
371
387
  - **Security/arch critical** -> soft gate (accept risk)
372
388
  - **Warnings** -> advisory
373
389
  - **Refactoring** -> never block
390
+ - **Close-the-loop** -> advisory, never blocks (detect-and-surface only; never auto-closes)
374
391
 
375
392
  Report = audit trail.
376
393
 
@@ -119,6 +119,25 @@ Rebase now? (each / list-only / skip)
119
119
  - **list-only / skip** → print the commands; the operator runs them at a safe stopping point.
120
120
  - Gitignored carve-outs (`.mcp.json`, experimental hooks) are not carried by a rebase — whenever layer (b) re-synced anything, add: each worktree must re-run `experimental/{pkg}/install.sh`.
121
121
 
122
+ ### e. Primary-checkout weld (`core.hooksPath`)
123
+
124
+ Runs on every upgrade (registry and Dev Mode alike) — the bin syncs the `.forge/git-hooks/` body, but `core.hooksPath` is per-clone git config the bin never touches (issue #21, ADR-031). Wire it with the **detect + warn, never clobber** policy — identical to `initializing` Finalize step 8. Main checkout only (`git rev-parse --git-dir` == `--git-common-dir`; from a worktree, skip — the primary checkout owns the config):
125
+
126
+ ```bash
127
+ want=".forge/git-hooks"
128
+ cur=$(git config --get core.hooksPath 2>/dev/null || true)
129
+ if [ -z "$cur" ] || [ "$cur" = "$want" ]; then
130
+ git config core.hooksPath "$want" # repo-relative → resolves inside every worktree
131
+ echo "[Forge] core.hooksPath → $want (primary-checkout weld active)."
132
+ else
133
+ echo "[Forge] core.hooksPath is already '$cur' (Husky/lefthook?). NOT overwriting — the"
134
+ echo " primary-checkout pre-commit backstop is inactive. Chain .forge/git-hooks/pre-commit"
135
+ echo " into your existing hooks dir to keep the weld, or accept PreToolUse-only coverage."
136
+ fi
137
+ ```
138
+
139
+ Idempotent — re-running an upgrade that already wired it is a silent no-op. **WHY repo-relative + never-clobber:** same reasons as `initializing` step 8 (worktree-resolvable path; a consumer's Husky/lefthook must never be silently disabled).
140
+
122
141
  ## Dev Mode (`/upgrading dev` — explicit only)
123
142
 
124
143
  Local-clone sync for developing Forge itself: test framework changes in a project without publishing to npm. Presence of `.forge/dev-source` NEVER selects this mode — only the explicit `dev` argument does.
@@ -141,7 +160,7 @@ Dev mode syncs from a checkout, so it keeps file-classification semantics — **
141
160
 
142
161
  - **Framework-owned** (`.claude/agents/*.md`, `.claude/skills/*/SKILL.md`, `.forge/FORGE.md`) → overwrite when different. Ownership is per top-level **unit** — a skill DIR under `.claude/skills/`, a flat agent FILE under `.claude/agents/`: a unit the template doesn't ship (user-authored, vendored-parity, or an opt-in experimental skill) is **preserved, never deleted**; only stale files *inside* a still-shipped skill dir are cleaned. Experimental skills additionally get the staleness pass below.
143
162
  - **Template-only** (`.forge/templates/**`, `.forge/migrations/**`, `.forge/adapters/**`, `.forge/gitignore` → `.forge/.gitignore`) → overwrite; `.forge/FORGE.md` lands BEFORE the CLAUDE.md pass.
144
- - **Additive** (`.claude/hooks/**` incl. `tests/`, `.claude/rules/**`, `.forge/checks/**`) → add + overwrite template-shipped files, never delete local extras, `chmod +x` synced `*.sh`.
163
+ - **Additive** (`.claude/hooks/**` incl. `tests/`, `.claude/rules/**`, `.forge/checks/**`, `.forge/git-hooks/**`) → add + overwrite template-shipped files, never delete local extras, `chmod +x` synced `*.sh` (and the extensionless git hooks under `.forge/git-hooks/`).
145
164
  - **Import-managed** `CLAUDE.md` → the bin's `ensureClaudeMdImport()` cases verbatim; user content outside a forge section is never modified.
146
165
  - **Merge-owned** `.claude/settings.json` → `forge.*` keys + additive hook-group wiring only; stamp `forge.version` last.
147
166
  - **Never touch** user-generated files: `.forge/project.yml`, `.forge/state/`, `.forge/constitution.md`, `.forge/context.md`, `.forge/requirements/`, `.forge/roadmap.yml`, `.forge/design-system.md`, `.forge/refactor-backlog.yml`.
@@ -150,11 +169,12 @@ Render the same report shape as the default path, with `Source: {path}` instead
150
169
 
151
170
  ### Post-sync layers
152
171
 
153
- Re-use layers (a)–(d) with dev-derived inputs — this is the one mode that computes them itself, since there is no engine JSON:
172
+ Re-use layers (a)–(e) with dev-derived inputs — this is the one mode that computes them itself, since there is no engine JSON:
154
173
 
155
174
  - **(a) Migrations** — the data-driven Detection loop: select freshly-synced guides `.forge/migrations/{v}-*.md` where `installed < v <= source` (semver on the PRE-stamp installed version), run each guide's `## Detection` bash block from the project root; `MIGRATE` on stdout → the layer-(a) prompt with the text after `MIGRATE —` as the reason; no output → `{v}: no action`; block errors → `{v}: detection error (skipped)`, never abort the loop. Range bookkeeping is implicit: once `forge.version` is stamped, crossed guides fall out of the next run's range — no ledger.
156
175
  - **(b) Experimental staleness** — diff installed non-template skill dirs against `{source}/packages/create-forge/experimental/*/source/skills/{name}/`; stale → the layer-(b) offer with `{source}` as the source path.
157
176
  - **(c) Codex** — presence glob (`.agents/` / `.codex/`) → the layer-(c) offer.
158
177
  - **(d) Worktrees** — `git worktree list --porcelain` filtered to `refs/heads/forge/m-*`, main checkout only (`git rev-parse --git-dir` == `--git-common-dir`; from a worktree, skip and say so) → the layer-(d) offer.
178
+ - **(e) Primary-checkout weld** — wire `core.hooksPath` with the detect+warn+never-clobber policy above (main checkout only). Same behavior in Dev Mode as on the registry path.
159
179
 
160
180
  Same consent gates, same report lines as the registry path.
@@ -59,6 +59,8 @@ Where worktrees live and what they're named — single source of truth, base con
59
59
 
60
60
  **Recorded path is authoritative once set** — `lifecycle.worktree_path` (milestone) / `runtime.worktree` (stream) is read verbatim, never re-derived; changing `worktree_root` affects only new worktrees; move a live worktree only with `git worktree move` + update the record. No recorded path → resolve from the convention. `forge` boot and `chief-of-staff` adoption **validate advisorily**: recorded path outside the resolved root → warn (never block, never move); an explicit `worktree_root` is honored.
61
61
 
62
+ **Primary checkout is welded to the default branch — all tiers** ([ADR-031](../docs/decisions/ADR-031-weld-primary-checkout-to-default-branch.md)). Its HEAD is shared across concurrent sessions; a `checkout -b` there silently redirects others' commits. Enforced (`git-dir == git-common-dir` scoped, never touches worktrees): `.claude/hooks/protect-primary-checkout.sh` (PreToolUse — denies branch create/switch/detach + hard-reset) + `.forge/git-hooks/pre-commit` (off-default-commit backstop; wired per-clone by `initializing`/`upgrading` via repo-relative `core.hooksPath`, detect+warn+never-clobber). Branch work: `.forge/bin/new-worktree.sh <slug>`. Escape: `ALLOW_MAIN_BRANCHING=1`.
63
+
62
64
  **`orchestration.worktree_hydrate`** (optional, `project.yml`; forge#18) lists superproject-relative paths to required-but-gitignored artifacts (e.g. `.mcp.json`) a fresh worktree's tracked-only checkout lacks. Copy is guarded `source-exists && dest-absent` — never overwrites, never `git add`s, so the secret stays untracked in both trees. Applied by `orchestrating` at creation and re-asserted idempotently in `executing`'s Workspace Prerequisites, covering worktrees of any origin. Omit ⇒ no propagation. The per-worktree runtime counterpart of the install-time carve-out propagation below (State Ownership § Stable shared).
63
65
 
64
66
  ## Workflow Tiers
@@ -99,7 +101,7 @@ Auto-detects complexity. Override: "Use Quick/Standard/Full tier."
99
101
  | Break work into tasks with gates | `planning` | Standard, Full |
100
102
  | Build with deviation rules + atomic commits | `executing` | All |
101
103
  | Prove work delivers on goals (+ M9 e2e validation gate; captures the Human Verification Gate sign-off) | `verifying` | Standard, Full |
102
- | Audit health + catalog refactoring (+ M9 e2e audits; hard-blocks completion on the Human Verification Gate) | `reviewing` | Standard, Full |
104
+ | Audit health + catalog refactoring (+ M9 e2e audits; hard-blocks completion on the Human Verification Gate) + close-the-loop (surface resolved-but-open issues) | `reviewing` | Standard, Full |
103
105
  | Small scoped fix | `quick-tasking` | Quick |
104
106
  | Rapid disposable UI mockup behind an alpha flag (no verify/review gate) | `prototyping` | Prototyping |
105
107
  | UI with design system | `designing` | When UI |
@@ -195,18 +197,19 @@ State lives in `.forge/`. One line per artifact; protocol detail lives in the na
195
197
  - `design-system.md` — component mapping table
196
198
  - `requirements/m{N}.yml` — per-milestone structured requirements with `[NEEDS CLARIFICATION]` markers. **FR/DEF/NFR ids are globally unique across all milestone files** — reserve via the ID Reservation Protocol before writing. M9 e2e gate fields (`e2e`, `observable_outcome`, `validated`, hash) lazy-migrate; absent ⇒ `false`.
197
199
  - `roadmap.yml` — phases, milestones, dependencies
198
- - `state/index.yml` — DERIVED milestone registry, **git-ignored render-on-read cache** (0.53.0): regenerated at every boot by Rollup 1.0 (`forge` SKILL Step 1.0) from the milestone files; never committed, never hand-edited, never written by worktree agents; absent on a fresh clone (the project sentinel is `project.yml`). Rows carry the derived `release_state`.
200
+ - `state/index.yml` — DERIVED milestone registry, **git-ignored render-on-read cache** (0.53.0): regenerated at every boot from the milestone files by the **`forge-state-rollup.sh` SessionStart hook** (issue #22; skills only *read* it); never committed, never hand-edited. Rendered in any checkout (gitignored local write), but worktree agents never write *committed* derived-source state; absent on a fresh clone (sentinel is `project.yml`). Rows carry the derived `release_state`.
199
201
  - **Derived `release_state`** ([ADR-024](../docs/decisions/ADR-024-merged-validated-fold.md)) — `{unmerged, merged, validated}` computed at rollup by `.claude/hooks/forge-release-fold.sh` (git-only, offline; the fold binary is the only source — agent prose never re-derives it), never stored. Validation binding via `forge.validation_event`: `in_session_signoff` (default; absent ⇒ this) or `promotion_approval` (CI-signed `promotion/{sha}` tag). Inert until a repo opts in; full spec in ADR-024 + the fold header.
200
202
  - `state/milestone-{id}.yml` — per-milestone cursor (**single source of truth**): position, decisions, blockers. One owner at a time.
201
- - `state/desire-paths/` — append-only framework-usage observations, one file per observation, `scope: project|framework` (absent ⇒ project); occurrence counts derived by globbing. **`forge` boot is the single review owner** — surfaces groups at 3+, persists declines (no re-nag), routes `framework`-scope signals upstream ([ADR-012](../docs/decisions/ADR-012-upstream-desire-path-feedback-channel.md)); `verifying` captures but never surfaces.
203
+ - `state/desire-paths/` — append-only framework-usage observations, one file per observation, `scope: project|framework` (absent ⇒ project); occurrence counts derived by globbing. **`forge` boot is the single review owner** — surfaces groups at 3+, persists declines (no re-nag), routes `framework`-scope signals upstream ([ADR-012](../docs/decisions/ADR-012-upstream-desire-path-feedback-channel.md)); `verifying` captures but never surfaces. An issue-sourced entry SHOULD carry a back-reference (`resolves_issue: N`; ADRs use `**Context source:** forge#N`) — close-the-loop keys off it.
202
204
  - `context.md` — active locked decisions + deferred ideas (≤12 KB); history archives to `context-archive.md`
203
- - `streams/active.yml` — DERIVED Project Chief traffic map, git-ignored render-on-read cache — see Stream Rollup below ([ADR-015](../docs/decisions/ADR-015-derived-stream-registry.md))
205
+ - `streams/active.yml` — DERIVED Project Chief traffic map, git-ignored render-on-read cache, rendered by the same `forge-state-rollup.sh` hook — see Stream Rollup below ([ADR-015](../docs/decisions/ADR-015-derived-stream-registry.md))
204
206
  - `streams/{stream}.yml` (+ `brief.md`, `packages/{id}.yml`) — per-stream **source of truth**: coordination lifecycle, ownership, shared surfaces, blockers, merge readiness, optional `stream.milestone` link (milestone-backed streams omit workflow phase — the rollup reads it from the milestone)
205
207
  - `research/milestone-{id}.md` — dated immutable findings snapshot
206
208
  - `phases/milestone-{id}/{phase}-{name}/plan-{NN}.md` — task plans with must_haves frontmatter (`{id}`=milestone, `{phase}`=phase# verbatim, `{name}`=kebab, `{NN}`=seq)
207
209
  - `refactor-backlog.yml` — refactoring catalog worked via quick-tasking; status vocab `pending | in_progress | done | dismissed | deferred`; terminal items auto-archive to `refactor-backlog-archive.yml` on the next reviewing/quick-tasking write
208
210
  - `releases.yml` — append-only version-reservation registry (Version Reservation Protocol)
209
211
  - `reservations.yml` — append-only id-reservation audit trail + cross-machine floor (ID Reservation Protocol); `forge-reserve compact` (R104) moves dropped rows verbatim to `reservations-archive.yml`, retaining each kind's max + a 14-day tail
212
+ - `state/issue-loop/declined/{N}.yml` — append-only close-the-loop decline markers; `reviewing` writes one when the operator declines a surfaced resolved-but-open issue (no re-nag). Inert once the issue closes.
210
213
  - `archive/milestone-{id}/` — archived milestone artifacts (archive-delete)
211
214
  - `prototypes/{slug}.md` — resumable disposable-mockup state (`exploring | parked | graduated | discarded`); terminal → `prototypes/archive/`. Single-owner, one driver. See `Skill(prototyping)`.
212
215
 
@@ -216,7 +219,7 @@ State lives in `.forge/`. One line per artifact; protocol detail lives in the na
216
219
 
217
220
  ### Stream Rollup
218
221
 
219
- `active.yml` is a pure projection — regenerated, never hand-edited — by a deterministic, idempotent **join**: per-stream files contribute coordination state; active milestones contribute `phase` (any active milestone with no stream file gets a derived `implicit` row — coverage is computed, not maintained); `merge_queue` derives from `merge.readiness: ready`. A **step-0 completed-stream auto-close sweep** runs first: a milestone-backed stream whose milestone is `complete`, not yet `closed`, with no live worktree is auto-closed a done-and-merged milestone can never leave a zombie "ready to merge" row. Run by the main/orchestrator session at `forge` boot and `chief-of-staff` Show/Sync; worktrees never write it; milestone Rollup 1.0 runs **first** (the join reads the fresh `index.yml`). **Numbered procedure: the ADR-015 addendum.** Because each field has exactly one source and coverage is computed, two-registry drift cannot occur — supersedes the old advisory Registry Drift Check.
222
+ `active.yml` is a pure projection by a deterministic, idempotent **join**: per-stream files contribute coordination state; active milestones contribute `phase` (any active milestone with no stream file gets a derived `implicit` row — coverage is computed, not maintained); `merge_queue` derives from `merge.readiness: ready`. A **step-0 completed-stream auto-close sweep** runs first: a milestone-backed stream whose milestone is `complete`, not yet `closed`, with no live worktree is auto-closed (no zombie "ready to merge" rows). Rendered by the same `forge-state-rollup.sh` hook (above) at boot and at `chief-of-staff` Show/Sync / mid-session milestone CRUD; it runs Rollup 1.0 **first** (the join reads the fresh `index.yml`), and confines the committed-source sweep to the main checkout. **Numbered procedure: the ADR-015 addendum.** Each field has exactly one source and coverage is computed, so two-registry drift cannot occur.
220
223
 
221
224
  ### State Commit Protocol
222
225
 
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ # One-command worktree for any commit-producing task (issue #21, ADR-031).
3
+ #
4
+ # bash .forge/bin/new-worktree.sh <slug> [base-ref]
5
+ #
6
+ # Creates {worktree_root}/{slug} on branch forge/{slug} off origin/main (or [base-ref]).
7
+ # The primary checkout's HEAD never moves — that is the whole point of the weld: branch
8
+ # work lives in worktrees, each with its own independent HEAD, so concurrent sessions
9
+ # never clobber each other's commits.
10
+ #
11
+ # worktree_root follows the FORGE.md Worktree Convention: `orchestration.worktree_root`
12
+ # from .forge/project.yml, default `../<repo-basename>-worktrees/`. Relative paths resolve
13
+ # against the repo root; absolute / ~ honored verbatim.
14
+
15
+ set -euo pipefail
16
+
17
+ SLUG=${1:?usage: new-worktree.sh <slug> [base-ref]}
18
+ BASE=${2:-origin/main}
19
+
20
+ REPO_ROOT=$(git rev-parse --show-toplevel)
21
+ REPO_BASE=$(basename "$REPO_ROOT")
22
+
23
+ # Resolve worktree_root from project.yml (light grep — no yaml dependency). The value
24
+ # lives under an `orchestration:` block as `worktree_root: <path>`.
25
+ WT_ROOT_CFG=$(grep -E '^[[:space:]]*worktree_root:' "$REPO_ROOT/.forge/project.yml" 2>/dev/null \
26
+ | head -1 | sed -E 's/^[[:space:]]*worktree_root:[[:space:]]*//; s/^["'\'']//; s/["'\'']$//; s/[[:space:]]*(#.*)?$//' || true)
27
+
28
+ if [ -z "$WT_ROOT_CFG" ]; then
29
+ WORKTREE_ROOT="$(dirname "$REPO_ROOT")/${REPO_BASE}-worktrees"
30
+ else
31
+ case "$WT_ROOT_CFG" in
32
+ /*) WORKTREE_ROOT="$WT_ROOT_CFG" ;; # absolute — verbatim
33
+ "~"*) WORKTREE_ROOT="${WT_ROOT_CFG/#\~/$HOME}" ;; # ~ — expand
34
+ *) WORKTREE_ROOT="$REPO_ROOT/$WT_ROOT_CFG" ;; # relative — against repo root
35
+ esac
36
+ fi
37
+
38
+ # Slashes in the slug become dashes in the directory name; the branch keeps the slash.
39
+ DIR_NAME=$(printf '%s' "$SLUG" | tr '/' '-')
40
+ WT_PATH="$WORKTREE_ROOT/$DIR_NAME"
41
+ BRANCH="forge/$SLUG"
42
+
43
+ if [ -e "$WT_PATH" ]; then
44
+ echo "✋ $WT_PATH already exists. Pick another slug or remove it first (git worktree remove)." >&2
45
+ exit 1
46
+ fi
47
+
48
+ mkdir -p "$WORKTREE_ROOT"
49
+ # Best-effort refresh of origin/main so the worktree starts from current main; offline-safe.
50
+ git fetch origin main --quiet 2>/dev/null || true
51
+ git worktree add -b "$BRANCH" "$WT_PATH" "$BASE"
52
+
53
+ echo ""
54
+ echo "✅ Worktree ready"
55
+ echo " path: $WT_PATH"
56
+ echo " branch: $BRANCH (from $BASE)"
57
+ echo ""
58
+ echo " cd \"$WT_PATH\""
59
+ echo ""
60
+ echo "Reminders: run your install step after entering (worktrees don't share deps);"
61
+ echo "the primary checkout stayed on its default branch — that is intentional."
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # Scratch-repo tests for .forge/bin/new-worktree.sh (issue #21, ADR-031).
3
+ #
4
+ # Builds a throwaway repo with a committed HEAD, runs the helper (base ref = HEAD so no
5
+ # network is needed), and asserts the worktree dir + branch it creates. Self-cleans. Run:
6
+ # ./.forge/bin/tests/new-worktree.test.sh
7
+ # Prints `ALL PASS` and exits 0 iff every case passes; non-zero on any failure.
8
+
9
+ set -uo pipefail
10
+
11
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
+ HELPER="$SCRIPT_DIR/../new-worktree.sh"
13
+ [ -x "$HELPER" ] || { printf 'FATAL: helper not executable: %s\n' "$HELPER" >&2; exit 1; }
14
+
15
+ ROOT="$(mktemp -d)"
16
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
17
+
18
+ PASSED=0
19
+ FAILED=0
20
+ pass() { PASSED=$((PASSED + 1)); }
21
+ fail() { FAILED=$((FAILED + 1)); printf ' FAIL: %s\n' "$1" >&2; }
22
+
23
+ # A repo named so its default worktree_root is predictable: <base>-worktrees next to it.
24
+ new_repo() {
25
+ local d="$ROOT/$1"
26
+ mkdir -p "$d"
27
+ git -C "$d" init -q
28
+ git -C "$d" symbolic-ref HEAD refs/heads/main
29
+ git -C "$d" config user.email t@example.com
30
+ git -C "$d" config user.name tester
31
+ mkdir -p "$d/.forge"
32
+ : > "$d/seed"
33
+ git -C "$d" add -A && git -C "$d" commit -qm init
34
+ printf '%s\n' "$d"
35
+ }
36
+
37
+ # ── Case 1: default worktree_root, simple slug ───────────────────────────────────
38
+ R="$(new_repo proj)"
39
+ ( cd "$R" && "$HELPER" myslug HEAD ) >/dev/null 2>&1 || true
40
+ WT="$ROOT/proj-worktrees/myslug"
41
+ if [ -d "$WT" ]; then pass; else fail "case1: worktree dir not created at $WT"; fi
42
+ br=$(git -C "$WT" branch --show-current 2>/dev/null || true)
43
+ if [ "$br" = "forge/myslug" ]; then pass; else fail "case1: branch is '$br', want forge/myslug"; fi
44
+
45
+ # ── Case 2: re-running the same slug errors (dir exists) ─────────────────────────
46
+ if ( cd "$R" && "$HELPER" myslug HEAD ) >/dev/null 2>&1; then
47
+ fail "case2: duplicate slug should have errored (exit non-zero)"
48
+ else
49
+ pass
50
+ fi
51
+
52
+ # ── Case 3: slug with a slash → dashed dir, slashed branch ───────────────────────
53
+ R2="$(new_repo proj2)"
54
+ ( cd "$R2" && "$HELPER" feat/thing HEAD ) >/dev/null 2>&1 || true
55
+ WT2="$ROOT/proj2-worktrees/feat-thing"
56
+ if [ -d "$WT2" ]; then pass; else fail "case3: dashed dir not created at $WT2"; fi
57
+ br2=$(git -C "$WT2" branch --show-current 2>/dev/null || true)
58
+ if [ "$br2" = "forge/feat/thing" ]; then pass; else fail "case3: branch is '$br2', want forge/feat/thing"; fi
59
+
60
+ # ── Case 4: configured relative worktree_root honored ────────────────────────────
61
+ R3="$(new_repo proj3)"
62
+ printf 'orchestration:\n worktree_root: .wt\n' > "$R3/.forge/project.yml"
63
+ git -C "$R3" add -A && git -C "$R3" commit -qm cfg
64
+ ( cd "$R3" && "$HELPER" wtslug HEAD ) >/dev/null 2>&1 || true
65
+ WT3="$R3/.wt/wtslug"
66
+ if [ -d "$WT3" ]; then pass; else fail "case4: configured worktree_root .wt not honored (want $WT3)"; fi
67
+
68
+ printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
69
+ if [ "$FAILED" -eq 0 ]; then printf 'ALL PASS\n'; exit 0; fi
70
+ exit 1
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # Backstop for the primary-checkout branch weld (issue #21, ADR-031).
3
+ #
4
+ # Refuses commits made in the PRIMARY checkout while HEAD is not the default branch
5
+ # (or is detached). The Claude PreToolUse hook (.claude/hooks/protect-primary-checkout.sh)
6
+ # blocks branch create/switch at the source; this git hook catches anything that slips
7
+ # past it — humans, non-agent tools, cd-inside-compound-command races — at the moment
8
+ # damage would occur: a commit landing on a branch some other concurrent session owns.
9
+ #
10
+ # Linked worktrees are unaffected (git-dir != git-common-dir there — their own HEAD is
11
+ # the whole point). Commits ON the default branch in the primary checkout remain allowed
12
+ # by design — Forge's own state-sync commits land there.
13
+ #
14
+ # Default branch is resolved dynamically (main/master + init.defaultBranch), never
15
+ # hardcoded. Fails OPEN on any git-probe error (a hook must not wedge commits on its own
16
+ # malfunction). Exit 0 = allow the commit; exit 1 = abort it.
17
+ #
18
+ # Activate once per clone (done by initializing / upgrading):
19
+ # git config core.hooksPath .forge/git-hooks
20
+ # Escape hatch (deliberate, human use): ALLOW_MAIN_BRANCHING=1 git commit ...
21
+
22
+ set -uo pipefail
23
+
24
+ [ "${ALLOW_MAIN_BRANCHING:-0}" = "1" ] && exit 0
25
+
26
+ GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
27
+ COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
28
+ GIT_DIR_ABS=$(cd "$GIT_DIR" 2>/dev/null && pwd) || exit 0
29
+ COMMON_DIR_ABS=$(cd "$COMMON_DIR" 2>/dev/null && pwd) || exit 0
30
+
31
+ # Linked worktree → its own HEAD; branch commits are the whole point. Allow.
32
+ [ "$GIT_DIR_ABS" != "$COMMON_DIR_ABS" ] && exit 0
33
+
34
+ # Default-branch set (dynamic — mirrors protect-primary-checkout.sh).
35
+ DEFAULT_BRANCHES="main master"
36
+ GIT_DEFAULT=$(git config --get init.defaultBranch 2>/dev/null || true)
37
+ [ -n "$GIT_DEFAULT" ] && DEFAULT_BRANCHES="$DEFAULT_BRANCHES $GIT_DEFAULT"
38
+
39
+ BRANCH=$(git branch --show-current 2>/dev/null || true)
40
+
41
+ if [ -z "$BRANCH" ]; then
42
+ echo "✋ Commit blocked: detached HEAD in the primary checkout." >&2
43
+ echo " Return to the default branch (git checkout main) or move this work to a worktree:" >&2
44
+ echo " bash .forge/bin/new-worktree.sh <slug>" >&2
45
+ echo " Human escape hatch: ALLOW_MAIN_BRANCHING=1 git commit ..." >&2
46
+ exit 1
47
+ fi
48
+
49
+ for b in $DEFAULT_BRANCHES; do
50
+ [ "$BRANCH" = "$b" ] && exit 0 # on the default branch → allowed (state-sync commits land here)
51
+ done
52
+
53
+ echo "✋ Commit blocked: the primary checkout must stay on the default branch (you are on '$BRANCH')." >&2
54
+ echo " Another concurrent session may own this branch — committing here is how work gets clobbered." >&2
55
+ echo " Branch work belongs in a worktree: bash .forge/bin/new-worktree.sh <slug>" >&2
56
+ echo " Human escape hatch: ALLOW_MAIN_BRANCHING=1 git commit ..." >&2
57
+ exit 1
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # Scratch-repo scenario tests for .forge/git-hooks/pre-commit (issue #21 backstop).
3
+ #
4
+ # Each scenario builds a throwaway repo (and, where needed, a linked worktree), then
5
+ # invokes the pre-commit hook directly from the target dir and asserts its exit code
6
+ # (0 = commit allowed, 1 = commit aborted). No real `git commit` is needed — invoking
7
+ # the hook is deterministic and fast. Self-cleans on exit. Run:
8
+ # ./.forge/git-hooks/tests/pre-commit.test.sh
9
+ # Prints `ALL PASS` and exits 0 iff every scenario passes; non-zero on any failure.
10
+
11
+ set -uo pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ HOOK="$SCRIPT_DIR/../pre-commit"
15
+ [ -x "$HOOK" ] || { printf 'FATAL: pre-commit not executable: %s\n' "$HOOK" >&2; exit 1; }
16
+
17
+ ROOT="$(mktemp -d)"
18
+ trap 'rm -rf "$ROOT"' EXIT INT TERM
19
+
20
+ PASSED=0
21
+ FAILED=0
22
+
23
+ new_repo() {
24
+ local d="$ROOT/$1" br="${2:-main}"
25
+ mkdir -p "$d"
26
+ git -C "$d" init -q
27
+ git -C "$d" symbolic-ref HEAD "refs/heads/$br"
28
+ [ "$br" != "main" ] && git -C "$d" config init.defaultBranch "$br"
29
+ git -C "$d" config user.email t@example.com
30
+ git -C "$d" config user.name tester
31
+ : > "$d/seed"
32
+ git -C "$d" add -A && git -C "$d" commit -qm init
33
+ printf '%s\n' "$d"
34
+ }
35
+
36
+ # run_hook <dir> [env-assignment] — run the pre-commit FROM <dir>, return its exit code.
37
+ run_hook() {
38
+ local dir="$1" envset="${2:-}"
39
+ if [ -n "$envset" ]; then
40
+ ( cd "$dir" && env "$envset" "$HOOK" >/dev/null 2>&1 )
41
+ else
42
+ ( cd "$dir" && "$HOOK" >/dev/null 2>&1 )
43
+ fi
44
+ return $?
45
+ }
46
+
47
+ # assert <desc> <expected-exit> <dir> [env]
48
+ assert() {
49
+ local desc="$1" want="$2" dir="$3" envset="${4:-}"
50
+ local got
51
+ run_hook "$dir" "$envset"; got=$?
52
+ if [ "$got" -eq "$want" ]; then
53
+ PASSED=$((PASSED + 1))
54
+ else
55
+ FAILED=$((FAILED + 1))
56
+ printf ' FAIL: %s (got exit %s, want %s)\n' "$desc" "$got" "$want" >&2
57
+ fi
58
+ }
59
+
60
+ ALLOW=0
61
+ BLOCK=1
62
+
63
+ # ── Scenario 1: primary checkout on the default branch → commit allowed ──────────
64
+ P="$(new_repo primary)"
65
+ assert "primary on main: commit allowed" "$ALLOW" "$P"
66
+
67
+ # ── Scenario 2: primary checkout on a feature branch → commit blocked ────────────
68
+ git -C "$P" checkout -q -b feature
69
+ assert "primary on feature branch: commit blocked" "$BLOCK" "$P"
70
+
71
+ # ── Scenario 5 (escape hatch, on the feature branch): allowed ───────────────────
72
+ assert "escape hatch on feature branch: commit allowed" "$ALLOW" "$P" "ALLOW_MAIN_BRANCHING=1"
73
+ git -C "$P" checkout -q main # back home for later worktree add
74
+
75
+ # ── Scenario 3: detached HEAD in the primary checkout → blocked ─────────────────
76
+ D="$(new_repo detached)"
77
+ git -C "$D" checkout -q --detach
78
+ assert "primary detached HEAD: commit blocked" "$BLOCK" "$D"
79
+
80
+ # ── Scenario 4: linked worktree on a feature branch → allowed ────────────────────
81
+ WT="$ROOT/primary-wt"
82
+ git -C "$P" worktree add -q -b wtbranch "$WT" >/dev/null 2>&1
83
+ assert "linked worktree on feature branch: commit allowed" "$ALLOW" "$WT"
84
+
85
+ # ── Bonus: master-default repo on master → allowed; on a branch → blocked ────────
86
+ M="$(new_repo master-primary master)"
87
+ assert "master-default on master: commit allowed" "$ALLOW" "$M"
88
+ git -C "$M" checkout -q -b feat
89
+ assert "master-default on feature branch: commit blocked" "$BLOCK" "$M"
90
+
91
+ # ── Summary ──────────────────────────────────────────────────────────────────────
92
+ printf '\n%d passed, %d failed\n' "$PASSED" "$FAILED"
93
+ if [ "$FAILED" -eq 0 ]; then
94
+ printf 'ALL PASS (incl. escape hatch)\n'
95
+ exit 0
96
+ fi
97
+ exit 1
@@ -0,0 +1,120 @@
1
+ # Migration Guide: State rollup made mechanical (Forge 0.77.0)
2
+
3
+ Applies to projects upgrading to 0.77.0 or later. State Rollup 1.0 and the
4
+ Stream Rollup (ADR-015 addendum) — previously advisory skill prose that an
5
+ agent had to remember to run at boot — are now an executable,
6
+ `.claude/hooks/forge-state-rollup.sh`, invoked mechanically by a `SessionStart`
7
+ hook. A boot session can no longer skip the render and fall back to reading a
8
+ stale `.forge/state/index.yml` / `.forge/streams/active.yml` cache, or
9
+ hand-reconstruct the registries from memory — the exact failure mode filed as
10
+ issue #22.
11
+
12
+ `/upgrading` ships the new hook file (`.claude/hooks/forge-state-rollup.sh`)
13
+ like any other framework file. It does **not** wire the `SessionStart`
14
+ registration, because `.claude/settings.json` is a **user-owned file** —
15
+ upgrades never overwrite it (FORGE.md → State Ownership, "Stable shared"
16
+ class notes settings.json is gitignored-carve-out territory, propagated only
17
+ via explicit installer steps, never blanket overwrite). Without this
18
+ migration's one manual step, the hook ships but is never invoked: the
19
+ executable exists, nothing mechanically triggers it.
20
+
21
+ Projects that ran `initializing` fresh on 0.77.0+ (or already carry the
22
+ `SessionStart` block from an earlier partial adoption) need nothing further.
23
+
24
+ ## Prerequisites
25
+
26
+ 1. On the new version's framework files (`npx forge-orkes upgrade`, or the
27
+ `upgrading` skill) — so `.claude/hooks/forge-state-rollup.sh` and
28
+ `.claude/hooks/forge-release-fold.sh` are present and executable.
29
+ 2. Working tree clean or changes committed — this migration edits
30
+ `.claude/settings.json`, a tracked file.
31
+
32
+ ## Detection
33
+
34
+ Keyed on **the executable existing (or the version already at/above 0.77.0)
35
+ while the SessionStart wiring is absent** — the shipped-but-unwired gap. Uses
36
+ `jq` to inspect `.claude/settings.json`'s `hooks.SessionStart` array for an
37
+ entry referencing `forge-state-rollup`; degrades to a plain-text grep if `jq`
38
+ is not installed. Silent in a non-Forge / non-git tree, silent once the hook
39
+ is wired:
40
+
41
+ ```bash
42
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
43
+ [ -x .claude/hooks/forge-state-rollup.sh ] || exit 0
44
+
45
+ settings=".claude/settings.json"
46
+ [ -f "$settings" ] || {
47
+ echo "MIGRATE — forge-state-rollup.sh is present but $settings does not exist; add it with the SessionStart hook below"
48
+ exit 0
49
+ }
50
+
51
+ if command -v jq >/dev/null 2>&1; then
52
+ wired="$(jq -r '
53
+ (.hooks.SessionStart // []) as $entries
54
+ | [$entries[].hooks[]?.command // "" | select(contains("forge-state-rollup"))]
55
+ | length > 0
56
+ ' "$settings" 2>/dev/null)"
57
+ if [ "$wired" != "true" ]; then
58
+ echo "MIGRATE — $settings has no SessionStart hook referencing forge-state-rollup.sh; the executable shipped but nothing mechanically triggers it"
59
+ fi
60
+ else
61
+ if ! grep -q "forge-state-rollup" "$settings" 2>/dev/null; then
62
+ echo "MIGRATE — $settings has no reference to forge-state-rollup.sh (jq unavailable, ran plain-text check); add the SessionStart hook below"
63
+ fi
64
+ fi
65
+ ```
66
+
67
+ ## Migration steps
68
+
69
+ ### 1. Add the SessionStart hook block
70
+
71
+ Open `.claude/settings.json` and add (or merge into) a `hooks.SessionStart`
72
+ array entry:
73
+
74
+ ```json
75
+ {
76
+ "hooks": {
77
+ "SessionStart": [
78
+ {
79
+ "matcher": "startup|resume|clear",
80
+ "hooks": [
81
+ {
82
+ "type": "command",
83
+ "command": "[ -x \"$CLAUDE_PROJECT_DIR/.claude/hooks/forge-state-rollup.sh\" ] && \"$CLAUDE_PROJECT_DIR/.claude/hooks/forge-state-rollup.sh\" || true"
84
+ }
85
+ ]
86
+ }
87
+ ]
88
+ }
89
+ }
90
+ ```
91
+
92
+ If `hooks.SessionStart` already has other entries (e.g. from a project-local
93
+ customization), append this object to the existing array rather than
94
+ replacing it. If `hooks` exists but has no `SessionStart` key yet, add the key
95
+ alongside the existing `PreToolUse`/`PostToolUse` blocks.
96
+
97
+ The `[ -x ... ] && ... || true` guard means the hook is a no-op — never a
98
+ boot failure — on any checkout where the script is absent or not executable
99
+ (SessionStart hooks are non-blocking; `forge-state-rollup.sh` itself also
100
+ always exits 0).
101
+
102
+ ### 2. Commit the settings change
103
+
104
+ ```bash
105
+ git add .claude/settings.json
106
+ git commit -m "chore(forge): wire forge-state-rollup SessionStart hook (0.77.0)"
107
+ ```
108
+
109
+ ## Validation
110
+
111
+ - Start a new session, or run `/clear` — the boot summary line should include
112
+ `[forge-state-rollup] rendered index.yml (…) …` (plus an `active.yml` clause
113
+ if `.forge/streams/` exists), confirming the hook actually ran.
114
+ - `jq '.hooks.SessionStart' .claude/settings.json` prints a non-null array
115
+ containing the `forge-state-rollup` command, not `null`.
116
+ - Re-running the Detection block above now prints nothing (exits silently) —
117
+ the gap is closed.
118
+ - `.forge/state/index.yml` and `.forge/streams/active.yml` (if applicable)
119
+ exist on disk and are fresh — their mtimes update on every boot, since the
120
+ hook re-renders unconditionally.
@@ -0,0 +1,66 @@
1
+ # Migration Guide: Weld the primary checkout to the default branch (Forge 0.78.0)
2
+
3
+ Applies to projects installed before 0.78.0. This milestone (issue #21, ADR-031) welds the primary checkout to its default branch across ALL tiers: a PreToolUse guard denies branch create/switch there, a tracked git `pre-commit` backstop refuses off-default commits there, and a `new-worktree` helper teaches the right path. The `/upgrading` run that installed this guide already synced the hook bodies (`.claude/hooks/protect-primary-checkout.sh`, `.forge/git-hooks/pre-commit`, `.forge/bin/new-worktree.sh`) and merged the settings.json wiring; the one remaining per-clone step is the `core.hooksPath` wiring, which `/upgrading` applies with a detect+warn+never-clobber policy.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. Nothing to hand-edit — the upgrade sync + the `/upgrading` `core.hooksPath` layer ARE the migration.
8
+ 2. If you use Husky / lefthook / a pre-commit framework (i.e. `core.hooksPath` is already set), the wiring will NOT overwrite it — you'll get a warning and can chain `.forge/git-hooks/pre-commit` into your existing hooks dir, or accept PreToolUse-only coverage.
9
+
10
+ ## Detection
11
+
12
+ Prints `MIGRATE` when the primary-checkout weld is not fully wired: the PreToolUse guard is absent from `settings.json`, OR the tracked pre-commit backstop is missing, OR `core.hooksPath` is not pointing at `.forge/git-hooks`. Silent + exit 0 when fully wired.
13
+
14
+ ```bash
15
+ migrate=""
16
+ # (1) PreToolUse guard wired in settings.json?
17
+ if [ -f .claude/settings.json ]; then
18
+ if ! grep -q 'protect-primary-checkout' .claude/settings.json 2>/dev/null; then
19
+ migrate="guard not wired in settings.json"
20
+ fi
21
+ fi
22
+ # (2) tracked pre-commit backstop present?
23
+ if [ ! -f .forge/git-hooks/pre-commit ]; then
24
+ migrate="${migrate:+$migrate; }pre-commit backstop missing (.forge/git-hooks/pre-commit)"
25
+ fi
26
+ # (3) core.hooksPath wired to our dir? (advisory — /upgrading applies this)
27
+ cur=$(git config --get core.hooksPath 2>/dev/null || true)
28
+ if [ "$cur" != ".forge/git-hooks" ]; then
29
+ migrate="${migrate:+$migrate; }core.hooksPath not '.forge/git-hooks' (currently '${cur:-unset}')"
30
+ fi
31
+ if [ -n "$migrate" ]; then
32
+ echo "MIGRATE — primary-checkout weld incomplete: $migrate"
33
+ fi
34
+ ```
35
+
36
+ ## Migration steps
37
+
38
+ ### 1. Re-run the upgrade / initialize
39
+
40
+ - `/upgrading` on 0.78.0+ framework files syncs the hooks, merges the settings.json guard group, and applies the `core.hooksPath` wiring layer (detect+warn+never-clobber). The run that installed this guide already did the file sync; the `core.hooksPath` layer is what completes the weld.
41
+ - On a brand-new project, `initializing` Finalize wires `core.hooksPath` as part of setup.
42
+
43
+ ### 2. If `core.hooksPath` was already set (Husky/lefthook)
44
+
45
+ The wiring will not overwrite it. To keep the pre-commit weld active, chain the Forge hook into your existing hooks directory — e.g. add to your `pre-commit`:
46
+
47
+ ```bash
48
+ # run the Forge primary-checkout backstop, if present
49
+ [ -x .forge/git-hooks/pre-commit ] && .forge/git-hooks/pre-commit || true
50
+ ```
51
+
52
+ Or accept PreToolUse-only coverage: Claude sessions are still guarded by `protect-primary-checkout.sh`; only the human/CI backstop is inactive.
53
+
54
+ ### 3. Verify
55
+
56
+ From the primary checkout on the default branch:
57
+
58
+ ```bash
59
+ # guard denies branch create (exit 2) — run it directly against a payload:
60
+ printf '{"tool_input":{"command":"git checkout -b probe"},"cwd":"'"$(git rev-parse --show-toplevel)"'"}' \
61
+ | .claude/hooks/protect-primary-checkout.sh; echo "exit=$? (expect 2 in the PRIMARY checkout)"
62
+ # pre-commit allows on the default branch:
63
+ .forge/git-hooks/pre-commit; echo "exit=$? (expect 0 on the default branch)"
64
+ ```
65
+
66
+ All branch work now goes through `bash .forge/bin/new-worktree.sh <slug>`. Escape hatch for deliberate human branching: `ALLOW_MAIN_BRANCHING=1`.
@@ -7,6 +7,9 @@ type: "" # recurring_friction | agent_struggle | us
7
7
  date: "" # ISO 8601 date the observation was made, e.g. "2026-07-23"
8
8
  milestone: "" # milestone id, or a short label for non-milestone work (e.g. "quick-task (...)")
9
9
  scope: project # project | framework — absent ⇒ project (this repo only; framework ⇒ upstream-eligible per ADR-012)
10
+ # resolves_issue: 26 # OPTIONAL — GitHub issue number this observation's fix resolves. The
11
+ # # close-the-loop check (reviewing) keys off it to surface the issue as
12
+ # # likely-closable once the fix ships. Omit when the observation resolves no issue.
10
13
  suggestion_target: "" # the file/skill this observation would change if acted on
11
14
  note: >
12
15
  What was observed — concrete enough that a future review can judge recurrence