forge-orkes 0.62.0 → 0.63.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.
- package/bin/create-forge.js +250 -99
- package/experimental/conventions/README.md +87 -0
- package/experimental/conventions/install.sh +179 -0
- package/experimental/conventions/source/rules/code-quality.md +17 -0
- package/experimental/conventions/source/rules/laravel-http.md +21 -0
- package/experimental/conventions/source/rules/laravel-jobs.md +19 -0
- package/experimental/conventions/source/rules/laravel-migrations.md +19 -0
- package/experimental/conventions/source/rules/laravel-services.md +21 -0
- package/experimental/conventions/source/rules/python.md +14 -0
- package/experimental/conventions/source/rules/testing.md +15 -0
- package/experimental/conventions/source/rules/vue-inertia.md +24 -0
- package/experimental/conventions/source/skills/laravel-conventions/SKILL.md +124 -0
- package/experimental/conventions/source/skills/python-conventions/SKILL.md +100 -0
- package/experimental/conventions/source/skills/testing-standards/SKILL.md +142 -0
- package/experimental/conventions/source/skills/vue-conventions/SKILL.md +92 -0
- package/experimental/conventions/uninstall.sh +97 -0
- package/experimental/m10/README.md +130 -0
- package/experimental/m10/install.sh +198 -0
- package/experimental/m10/source/hooks/forge-branch-guard.sh +61 -0
- package/experimental/m10/source/hooks/forge-claim-check-doctor.sh +77 -0
- package/experimental/m10/source/hooks/forge-claim-check.sh +113 -0
- package/experimental/m10/source/hooks/forge-session-id.sh +22 -0
- package/experimental/m10/source/mcp-server/README.md +74 -0
- package/experimental/m10/source/mcp-server/example.mcp.json +8 -0
- package/experimental/m10/source/mcp-server/index.js +460 -0
- package/experimental/m10/source/mcp-server/package.json +17 -0
- package/experimental/m10/source/skills/orchestrating/SKILL.md +223 -0
- package/experimental/m10/source/skills/orchestrating/bootstrap-checks.md +70 -0
- package/experimental/m10/uninstall.sh +69 -0
- package/package.json +3 -2
- package/template/.claude/settings.json +1 -7
- package/template/.claude/skills/upgrading/SKILL.md +105 -176
- package/template/.forge/FORGE.md +2 -2
- package/template/.forge/migrations/0.63.0-origin-first-upgrading.md +49 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: orchestrating
|
|
3
|
+
description: "[Experimental — M10] Optional backend for Forge stream isolation. Bootstrap worktrees, coordinate merge queue, and optionally use MCP file claims. Prefer Chief/Streams as the user-facing orchestration model."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Orchestrating
|
|
7
|
+
|
|
8
|
+
Multi-agent session lifecycle substrate. Worktree isolation + merge queue remain
|
|
9
|
+
useful backend mechanics for Forge Streams. MCP/SQLite file claims are optional
|
|
10
|
+
and experimental defense-in-depth, not the primary coordination model.
|
|
11
|
+
|
|
12
|
+
## M16 Positioning
|
|
13
|
+
|
|
14
|
+
Chief/Streams is the preferred user-facing orchestration model. The Project
|
|
15
|
+
Chief decides when a stream needs M10 mechanics such as worktree isolation,
|
|
16
|
+
branch anchoring, merge queue discipline, or claim checks. Users should normally
|
|
17
|
+
ask to start, pause, delegate, conflict-check, merge, or close streams; they
|
|
18
|
+
should not have to invoke M10 directly.
|
|
19
|
+
|
|
20
|
+
M10 still matters when concurrent streams need hard workspace separation or a
|
|
21
|
+
controlled merge path. It is a backend/substrate, not the project manager.
|
|
22
|
+
|
|
23
|
+
## When to use
|
|
24
|
+
|
|
25
|
+
- The Project Chief selects M10 for a stream that needs worktree isolation,
|
|
26
|
+
merge-queue support, or optional file-claim checks.
|
|
27
|
+
- User explicitly invokes multi-agent mode (`/forge` argument selects multi-agent, or direct skill invocation).
|
|
28
|
+
- Legacy installs may route Full tier work through this skill when
|
|
29
|
+
`orchestration.auto` remains enabled.
|
|
30
|
+
|
|
31
|
+
Skip on Quick tier, single-phase work, or when bootstrap checks fail.
|
|
32
|
+
|
|
33
|
+
## Enforcement scope — what claims actually protect
|
|
34
|
+
|
|
35
|
+
Claim coordination (`forge_claim_files` + the PreToolUse gate) works for **one** mode only: a **single parent session** operating across a Forge-managed sibling worktree dir (default `../<repo>-worktrees/{anchor}`, configurable via `orchestration.worktree_root`), where main's hooks + `.session-id` apply. **It does NOT reach `Agent isolation:"worktree"` sub-agents** — those run in `.claude/worktrees/agent-*` with no hook config (`.claude` is gitignored), so they fire no SessionStart, claim under nothing, and their edits are **not gated**. For within-milestone fan-out, **worktree isolation + disjoint files behind a frozen contract is the safety — not claims.** Never assume a fan-out sub-agent is claim-protected; it is not. (See ADR-007.)
|
|
36
|
+
|
|
37
|
+
## Step 1: Bootstrap
|
|
38
|
+
|
|
39
|
+
Run all checks in `bootstrap-checks.md`:
|
|
40
|
+
|
|
41
|
+
1. Git version ≥ 2.48
|
|
42
|
+
2. LFS version ≥ 3.6 (skip if not installed)
|
|
43
|
+
3. No divergent submodules — clean pinned submodules pass (auto-inited per worktree in Step 2); refuse only on `-`/`+`/`U` entries (see `bootstrap-checks.md` "Submodule handling")
|
|
44
|
+
4. `core.hooksPath` empty or resolves inside worktree
|
|
45
|
+
5. `git hook run pre-commit` smoke test in fresh worktree
|
|
46
|
+
6. **Enforcement canary (advisory)** — `bash .claude/hooks/forge-claim-check-doctor.sh` reports `gate enforces`. On `GATE IS INERT`, warn loudly that claims provide no protection this session (worktree isolation still applies); do **not** refuse on this one — it never blocks, only surfaces.
|
|
47
|
+
|
|
48
|
+
**Any check 1–5 fails** → log reason, write `lifecycle.worktree_mode: refused` + `lifecycle.refused_reason` into active milestone state, emit fallback message (see `bootstrap-checks.md`), return to caller. Caller continues single-agent. Check 6 is advisory — it warns, never refuses.
|
|
49
|
+
|
|
50
|
+
## Step 2: Session ID + worktree
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
session_id=$(uuidgen | cut -c1-8)
|
|
54
|
+
# Anchor the worktree name to the milestone id so the branch is self-describing:
|
|
55
|
+
# the `m-{id}` token lets `forge` resume re-derive the milestone from the branch
|
|
56
|
+
# name alone (its id-token fallback), not only from lifecycle.worktree_branch
|
|
57
|
+
# state — which a moved/redone worktree can leave stale. The uuid suffix keeps the
|
|
58
|
+
# name collision-resistant and unique per session; the `forge/` prefix keeps the GC
|
|
59
|
+
# sweep simple. Milestone ids are dash-free, so `m-{id}` tokenizes cleanly.
|
|
60
|
+
anchor="m-${milestone_id}-${session_id}"
|
|
61
|
+
|
|
62
|
+
# Worktree root — configurable, repo-scoped by default (ADR-010).
|
|
63
|
+
# Read `orchestration.worktree_root` from .forge/project.yml; if unset, default
|
|
64
|
+
# to a per-repo sibling `../<repo-basename>-worktrees`. Per-repo siblings keep
|
|
65
|
+
# each repo's worktrees in their own dir (no cross-repo mixing when multiple
|
|
66
|
+
# repos share a parent), and resolve deterministically against the repo root
|
|
67
|
+
# rather than the shell cwd. Relative roots resolve against repo root;
|
|
68
|
+
# absolute roots and a leading `~` are honored as-is.
|
|
69
|
+
repo_root=$(git rev-parse --show-toplevel)
|
|
70
|
+
wt_root=$(sed -n '/^orchestration:/,/^[^[:space:]#]/p' "$repo_root/.forge/project.yml" 2>/dev/null \
|
|
71
|
+
| sed -n 's/^[[:space:]]*worktree_root:[[:space:]]*//p' | tr -d "\"'" | head -1)
|
|
72
|
+
wt_root="${wt_root:-../$(basename "$repo_root")-worktrees}"
|
|
73
|
+
case "$wt_root" in
|
|
74
|
+
/*) : ;; # absolute — use as-is
|
|
75
|
+
"~"*) wt_root="$HOME${wt_root#\~}" ;; # expand leading ~
|
|
76
|
+
*) wt_root="$repo_root/$wt_root" ;; # relative → resolve against repo root
|
|
77
|
+
esac
|
|
78
|
+
mkdir -p "$wt_root"
|
|
79
|
+
|
|
80
|
+
git worktree prune
|
|
81
|
+
git worktree add -b forge/${anchor} --lock --reason "forge session m-${milestone_id}" "$wt_root/${anchor}" main
|
|
82
|
+
wt="$wt_root/${anchor}"
|
|
83
|
+
# Submodules don't auto-populate in a new worktree — init them if present. Reuses the
|
|
84
|
+
# superproject's shared .git/modules object store (no re-clone, just a working-tree
|
|
85
|
+
# checkout); ccache absorbs the subsequent rebuild cost across worktrees.
|
|
86
|
+
if [ -n "$(git -C "$wt" submodule status 2>/dev/null)" ]; then
|
|
87
|
+
( cd "$wt" && git submodule update --init --recursive )
|
|
88
|
+
fi
|
|
89
|
+
( cd "$wt" && git hook run pre-commit || true )
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Verify worktree dir exists, branch locked, and (if applicable) submodule dirs are non-empty. On failure → cleanup partial state, refuse mode.
|
|
93
|
+
|
|
94
|
+
`anchor` (= `m-{milestone_id}-{session_id}`) names the worktree dir **and** branch from here on; every reference below that used to be `{session_id}` is now `{anchor}`. `session_id` (the bare uuid) is retained only as the collision key + audit id in state.
|
|
95
|
+
|
|
96
|
+
The **absolute** path Forge resolved (`$wt_root/${anchor}`) is recorded in `lifecycle.worktree_path` (Step 3). Every later operation — Step 4 handback, Step 5 teardown, Step 6 crash-recovery re-attach — uses that recorded path verbatim, never re-deriving it. This decouples in-flight worktrees from the config: changing `orchestration.worktree_root` later (or moving a worktree on disk) does not invalidate them.
|
|
97
|
+
|
|
98
|
+
## Step 3: State update
|
|
99
|
+
|
|
100
|
+
Write into `.forge/state/milestone-{id}.yml`:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
lifecycle:
|
|
104
|
+
session_id: "{session_id}" # short uuid — collision key + audit id only (not a name, not a claim id)
|
|
105
|
+
worktree_anchor: "m-{id}-{session_id}" # milestone-anchored name of the worktree dir + branch
|
|
106
|
+
claim_session_id: "{claim_session_id}" # Claude session id — owner of all file claims (see Identity)
|
|
107
|
+
worktree_path: "{absolute path Forge resolved in Step 2 — $wt_root/m-{id}-{session_id}}"
|
|
108
|
+
worktree_branch: "forge/m-{id}-{session_id}"
|
|
109
|
+
worktree_mode: "active"
|
|
110
|
+
started_at: "{ISO8601}"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Set `current.last_updated` in the milestone file. Do **not** write `state/index.yml` directly — it is a derived registry, regenerated by the `forge` rollup. (The orchestrator owns index.yml; worktree agents never touch it.)
|
|
114
|
+
|
|
115
|
+
## Step 4: Hand back
|
|
116
|
+
|
|
117
|
+
Relocate the session into the worktree, then return control to caller (typically `executing`) — instead of only *assuming* the caller is already inside `lifecycle.worktree_path`. Apply the canonical **Native Worktree Entry** contract (identical to the forge ownership-gate option 1):
|
|
118
|
+
|
|
119
|
+
- **If the `EnterWorktree` tool is available**, call `EnterWorktree({path: {lifecycle.worktree_path}})` to relocate the session into the worktree before returning control, then log `Relocated into worktree {lifecycle.worktree_path} ({lifecycle.worktree_branch})` (independent of any prompt, so the cwd move is visible).
|
|
120
|
+
- **If `EnterWorktree` is unavailable** (older Claude Code CLI, or a non-Claude-Code adapter), fall back to instructing the caller to `cd {path}` (here `cd {lifecycle.worktree_path}`) before continuing — today's implicit assumption stated explicitly.
|
|
121
|
+
- Exit stays `ExitWorktree({action: "keep"})`; teardown remains Step 5 / `git worktree remove` — the native tool never removes the worktree.
|
|
122
|
+
|
|
123
|
+
All subsequent work runs inside `lifecycle.worktree_path`. Caller honors claim convention.
|
|
124
|
+
|
|
125
|
+
### Claim convention (executing-skill contract)
|
|
126
|
+
|
|
127
|
+
State-file ownership: each session writes **only** its own `.forge/state/milestone-{own_id}.yml` and **appends** to `.forge/state/desire-paths/` (new files, never edited). It never writes `state/index.yml` — the registry is regenerated by rollup at teardown. Different milestones = different files, so these writes need no claim and never conflict across worktrees. Same-milestone parallelism is out of scope (one session per milestone — see the concurrent-invocation refusal in Edge Cases).
|
|
128
|
+
|
|
129
|
+
**Identity (critical — see ADR-007).** File claims are owned by the **Claude session id**, NOT the worktree uuid. The worktree uuid (`session_id` above) only feeds the worktree name (via `anchor`) and serves as the collision/audit key. Read the claim identity from the file the `forge-session-id.sh` SessionStart hook writes:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
claim_session_id=$(cat "${CLAUDE_PROJECT_DIR:-.}/.forge/.mcp-server/.session-id" 2>/dev/null)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This is the same id the `forge-claim-check.sh` enforcement hook reads from PreToolUse stdin, so a session's own claims are recognized and other sessions' claims block. If the file is missing/empty the hook fail-opens (no enforcement) — surface that rather than proceeding silently in genuine multi-agent work.
|
|
136
|
+
|
|
137
|
+
Before any `Edit`, `Write`, `MultiEdit`, or `NotebookEdit` on a file outside `.forge/state/milestone-{own_id}.yml`:
|
|
138
|
+
|
|
139
|
+
1. Call `forge_claim_files` with `{ session_id: claim_session_id, files: [...], ttl_seconds: 900 }`.
|
|
140
|
+
2. On `granted` → proceed with edit.
|
|
141
|
+
3. On `conflict: { holder_session, files: [...] }` → surface holder + files to user. Options:
|
|
142
|
+
- **wait** → poll `forge_claim_status` until released or TTL expiry.
|
|
143
|
+
- **skip** → drop the conflicted file from the task scope, continue with rest.
|
|
144
|
+
- **steal** → only if holder session is provably dead (PreToolUse hook validates). Otherwise refused.
|
|
145
|
+
4. After edit batch → claim auto-extends on continued use; explicit `forge_release_claims` on plan-complete.
|
|
146
|
+
|
|
147
|
+
PreToolUse hook (installed by plan-03) enforces this — uncaught violations block at hook level, not skill level.
|
|
148
|
+
|
|
149
|
+
## Step 5: Teardown
|
|
150
|
+
|
|
151
|
+
Triggered when caller signals work complete OR user requests teardown.
|
|
152
|
+
|
|
153
|
+
### Step 5.0: Human Verification Gate (hard block — runs first)
|
|
154
|
+
|
|
155
|
+
Before any merge or worktree removal, read `current.human_verified` from `.forge/state/milestone-{id}.yml`.
|
|
156
|
+
|
|
157
|
+
- **Present** (`{at, method, ...}`, including a recorded `override`) → gate satisfied, proceed to the merge below.
|
|
158
|
+
- **Absent / null** → **REFUSE teardown.** Code-clean + FF-clean is **not** enough to close a milestone (FORGE.md → Human Verification Gate). Do **not** present teardown as an option, and never offer it as a peer alternative to the outstanding verification. Respond with what's still unverified and offer exactly two paths:
|
|
159
|
+
1. **Verify now** → run the human verification (device session / visual / e2e walk). Have `verifying` (or this step) record `current.human_verified`, then re-invoke teardown.
|
|
160
|
+
2. **Record override** → only on an explicit human instruction to close without verifying: write `current.human_verified: {at: "<ISO 8601>", method: "override", override: true, reason}` to the milestone file, surface it, then proceed.
|
|
161
|
+
|
|
162
|
+
Never merge + remove a worktree while `human_verified` is unset.
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
forge_queue_commit(branch=forge/m-{id}-{session_id}, base_sha={merge_base})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Branch on response status:
|
|
169
|
+
|
|
170
|
+
- **`merged`** (response has `new_main_sha`) → **reconcile the working tree first** — `forge_queue_commit` merges *ref-only* (`update-ref`), so the main checkout is now behind HEAD: `git checkout HEAD -- $(git diff --name-only {base_sha} {new_main_sha})` — surgical, merged files only; **never** `git checkout HEAD -- .` (would clobber unrelated WIP) → `forge_release_claims(claim_session_id)` (the Claude session id, not the worktree uuid) → `git worktree remove --force --force "$(yq '.lifecycle.worktree_path' .forge/state/milestone-{id}.yml)"` (double `--force`: Step 2 created the worktree `--lock`ed, and a single `--force` refuses to remove a locked worktree; read the path from state rather than re-deriving it so a moved/non-default worktree still removes cleanly) → `git branch -d forge/m-{id}-{session_id}` → clear `lifecycle.*` (set `worktree_mode: complete`, retain `session_id` for audit) → **State reconcile = Rollup:** regenerate `state/index.yml` from the now-merged `milestone-*.yml` files (`forge` Rollup 1.0) and commit it (`chore(forge): rollup state after teardown — {session_id}`). The merged milestone files are the source of truth; index.yml is never hand-merged.
|
|
171
|
+
- **`conflict`** → invoke `Skill(debugging)` with payload `{ conflicted_files, base_sha, messages, branch }`. Teardown blocks until debugging signals resolution (re-invoke teardown after fix).
|
|
172
|
+
- **`stale_base`** → caller rebases worktree branch onto `current_target_sha` from response, retries `forge_queue_commit`. Max 3 retries → escalate to conflict path.
|
|
173
|
+
|
|
174
|
+
## Step 6: Crash recovery (next session start)
|
|
175
|
+
|
|
176
|
+
If no clean teardown happened previously:
|
|
177
|
+
|
|
178
|
+
1. `git worktree prune` — drops stale admin dirs.
|
|
179
|
+
2. `git branch --list 'forge/*'` — for each branch with no live worktree, prompt user. The branch suffix is the `anchor` (`m-{id}-{session_id}`); the milestone id is the `m-…` token, recoverable from the name alone even if state is stale:
|
|
180
|
+
- **resume** → re-attach at the path recorded in `lifecycle.worktree_path` (read from `state/milestone-{id}.yml` — the recorded absolute path survives a `worktree_root` config change after the fact). State missing the path → fall back to the current default (`<repo_root>/../<repo-basename>-worktrees/{anchor}`) and warn. `git worktree add <path> forge/{anchor}` and restore lifecycle state.
|
|
181
|
+
- **delete** → `git branch -D forge/{anchor}`.
|
|
182
|
+
3. MCP server startup handles pidfile takeover + claim TTL expiry independently (see ADR-003).
|
|
183
|
+
|
|
184
|
+
## Failure modes & operator notes
|
|
185
|
+
|
|
186
|
+
- **MCP server absent** — bootstrap check 5 (hook smoke) will not detect this. Skill detects on first `forge_claim_files` call: error `MCP_SERVER_UNAVAILABLE` → write `lifecycle.worktree_mode: degraded`, warn user, continue without claim coordination. Hard isolation (worktree) still active; coordination is downgraded to best-effort.
|
|
187
|
+
- **Disk full during worktree add** — `git worktree add` will error. Cleanup any partial `$wt_root/m-{id}-{session_id}/` dir, refuse mode, fall back.
|
|
188
|
+
- **Concurrent orchestrating invocations** — second invocation reads first's `lifecycle.session_id` in state. If present and `worktree_mode: active` → refuse (one orchestration per milestone). Use a separate milestone for parallel orchestrated work.
|
|
189
|
+
- **Worktree path collision** — UUIDv4 short (8 chars) collision negligible at <100 concurrent sessions. If `$wt_root/m-{id}-{session_id}/` already exists → regenerate session_id (the `anchor` recomputes with it), retry up to 3 times.
|
|
190
|
+
- **`worktree_root` changed mid-flight** — Step 3 records the resolved absolute `lifecycle.worktree_path`; teardown + crash-recovery read it from state, never re-derive. Editing `orchestration.worktree_root` in `project.yml` only affects newly created worktrees. Do not relocate live worktrees on disk by hand — `git worktree move` is the supported way; bare `mv` leaves the gitdir pointer stale.
|
|
191
|
+
|
|
192
|
+
## Example: clean session
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
user: /forge multi-agent
|
|
196
|
+
forge → orchestrating
|
|
197
|
+
orchestrating: bootstrap OK → session_id=a1b2c3d4 → anchor=m-9-a1b2c3d4 → worktree created at ../<repo>-worktrees/m-9-a1b2c3d4 → state written
|
|
198
|
+
orchestrating → executing (working dir: ../<repo>-worktrees/m-9-a1b2c3d4, branch: forge/m-9-a1b2c3d4)
|
|
199
|
+
executing: claim files → edit → commit (× N tasks)
|
|
200
|
+
executing → verifying (captures current.human_verified on human sign-off) → reviewing (all inside worktree)
|
|
201
|
+
reviewing → orchestrating (teardown)
|
|
202
|
+
orchestrating: Step 5.0 gate — current.human_verified present → forge_queue_commit → merged → release claims → remove worktree → delete branch
|
|
203
|
+
done.
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Example: conflict path
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
orchestrating: forge_queue_commit → conflict { files: [src/auth.ts] }
|
|
210
|
+
orchestrating → debugging { conflicted_files, base_sha, branch }
|
|
211
|
+
debugging: user resolves → signal resolved
|
|
212
|
+
orchestrating: retry forge_queue_commit → merged → cleanup
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## References
|
|
216
|
+
|
|
217
|
+
- ADR-001 — experimental track / opt-in carve-out
|
|
218
|
+
- ADR-002 — worktrees as isolation substrate
|
|
219
|
+
- ADR-003 — MCP server + per-repo SQLite
|
|
220
|
+
- ADR-004 — merge queue (forge_queue_commit status semantics)
|
|
221
|
+
- ADR-005 — session lifecycle (this skill realizes it)
|
|
222
|
+
- `.forge/research/milestone-10.md` — spike findings
|
|
223
|
+
- `bootstrap-checks.md` — bootstrap check matrix + fallback
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Bootstrap Checks
|
|
2
|
+
|
|
3
|
+
Run before worktree creation. Any failure → refuse worktree mode, fall back to single-agent.
|
|
4
|
+
|
|
5
|
+
| Check | Command | Pass criterion | Fail action | Reason |
|
|
6
|
+
|-------|---------|----------------|-------------|--------|
|
|
7
|
+
| Git version | `git --version` | major.minor ≥ 2.48 | refuse worktree mode | Known worktree bugs < 2.48 (admin-dir leaks, prune races) |
|
|
8
|
+
| LFS version | `git lfs version` (skip if not installed) | ≥ 3.6 OR not installed | refuse worktree mode | Known worktree-locking bugs < 3.6 |
|
|
9
|
+
| Submodule scan | `git submodule status` | empty OR every entry clean (` ` prefix, in-sync with recorded pin) | refuse only on divergent (`+`) / conflicted (`U`) entries — see "Submodule handling" below | Clean pins are safe: every worktree wants the same SHA, so the shared `.git/modules` HEAD never contends. Divergent working state is the real hazard. New worktrees auto-init submodules (SKILL Step 2) |
|
|
10
|
+
| `core.hooksPath` | `git config core.hooksPath` | empty OR path resolves inside worktree | refuse worktree mode | Husky/lefthook silent-skip risk when path points outside worktree |
|
|
11
|
+
| Hook smoke test | `git hook run pre-commit` in fresh worktree | exit 0 OR matches main-repo exit code | refuse worktree mode | Confirms hook plumbing actually fires inside worktree |
|
|
12
|
+
| Enforcement canary (advisory) | `bash .claude/hooks/forge-claim-check-doctor.sh` | canary line reports `gate enforces` | **warn, do not refuse** | Proves the claim gate actually denies a foreign claim — catches the inert-gate class (identity read from wrong source / hook unwired) that prereqs can't see |
|
|
13
|
+
|
|
14
|
+
## Fallback message
|
|
15
|
+
|
|
16
|
+
On any check failure, emit verbatim to user:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
M10 worktree mode unavailable: <reason>. Continuing in single-agent mode. See ADR-005 for compatibility matrix.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Replace `<reason>` with the failing check name + observed value (e.g., "Git version 2.45.1 < required 2.48").
|
|
23
|
+
|
|
24
|
+
## Submodule handling (conditional)
|
|
25
|
+
|
|
26
|
+
Submodules do **not** auto-populate in a fresh worktree — `git worktree add` carries the gitlinks but leaves the directories empty until `git submodule update --init` runs (SKILL Step 2 does this). The historic blanket refusal was over-conservative; the real hazard is two worktrees fighting over the shared `.git/modules` HEAD/index when they point a submodule at *different* SHAs. Pinned third-party deps (e.g. JUCE, nanovg) never trigger that — every worktree converges on the one recorded SHA.
|
|
27
|
+
|
|
28
|
+
Classify `git submodule status` output:
|
|
29
|
+
|
|
30
|
+
| Observed | Meaning | Action |
|
|
31
|
+
|----------|---------|--------|
|
|
32
|
+
| empty | no submodules | pass |
|
|
33
|
+
| every line ` ` (space) prefix | all clean, in-sync with recorded pin | **warn + proceed** — Step 2 will `submodule update --init --recursive` |
|
|
34
|
+
| any line `-` (uninitialized) in main repo | superproject itself hasn't inited | refuse — init in the main checkout first (`git submodule update --init`) |
|
|
35
|
+
| any line `+` (divergent SHA) | working tree ahead/behind recorded pin | refuse — uncommitted submodule movement is the shared-`.git/modules` hazard |
|
|
36
|
+
| any line `U` (merge conflict) | unresolved submodule conflict | refuse |
|
|
37
|
+
|
|
38
|
+
Warn-and-proceed message:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
M10: <N> submodule(s) present and clean — proceeding. Each worktree auto-inits them
|
|
42
|
+
(shared .git/modules object store, no re-clone). Ensure builds tolerate the one-time init.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## State write on refusal
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
lifecycle:
|
|
49
|
+
worktree_mode: refused
|
|
50
|
+
refused_reason: "<check name>: <observed>"
|
|
51
|
+
refused_at: "{ISO8601}"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Check execution order
|
|
55
|
+
|
|
56
|
+
Run checks 1→5 in order. First failure halts the sequence — no point running hook smoke if Git itself is too old. Record the failing check name + observed value as `refused_reason` so the user knows exactly what to fix.
|
|
57
|
+
|
|
58
|
+
## Remediation hints
|
|
59
|
+
|
|
60
|
+
| Failing check | User remediation |
|
|
61
|
+
|---------------|------------------|
|
|
62
|
+
| Git version | Upgrade Git to ≥ 2.48 (Homebrew: `brew upgrade git`; apt: backports or PPA) |
|
|
63
|
+
| LFS version | Upgrade Git LFS to ≥ 3.6 OR uninstall if not actually used |
|
|
64
|
+
| Submodule scan | Only fails on uninitialized (`-`), divergent (`+`), or conflicted (`U`) submodules. Run `git submodule update --init --recursive` in the main checkout so every submodule sits at its recorded pin, then retry. Clean pinned submodules are supported |
|
|
65
|
+
| `core.hooksPath` | Either unset (`git config --unset core.hooksPath`) or move hook dir inside repo tree so it resolves under each worktree |
|
|
66
|
+
| Hook smoke test | Inspect failing hook output; common cause is hook script assuming `$PWD` is main repo root — fix to use `git rev-parse --show-toplevel` |
|
|
67
|
+
|
|
68
|
+
## Re-running checks
|
|
69
|
+
|
|
70
|
+
Bootstrap is idempotent and cheap (~200ms total). Skill re-runs on every session start; users do not invoke directly. If a check transiently fails (e.g., LFS not yet installed during initial setup), simply restart the orchestration entry.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Forge M10 experimental uninstaller.
|
|
3
|
+
# Preserves claims.db by default. Pass --purge to remove it.
|
|
4
|
+
# Usage: bash uninstall.sh [--purge] /path/to/forge-repo
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
PURGE=0
|
|
9
|
+
TARGET=""
|
|
10
|
+
|
|
11
|
+
while [[ $# -gt 0 ]]; do
|
|
12
|
+
case "$1" in
|
|
13
|
+
--purge) PURGE=1; shift ;;
|
|
14
|
+
-h|--help) echo "Usage: $0 [--purge] /path/to/forge-repo"; exit 0 ;;
|
|
15
|
+
*) TARGET="$1"; shift ;;
|
|
16
|
+
esac
|
|
17
|
+
done
|
|
18
|
+
|
|
19
|
+
[[ -z "$TARGET" ]] && { echo "ERROR: target repo path required" >&2; exit 2; }
|
|
20
|
+
[[ -d "$TARGET" ]] || { echo "ERROR: target '$TARGET' does not exist" >&2; exit 2; }
|
|
21
|
+
|
|
22
|
+
say() { echo " $*"; }
|
|
23
|
+
|
|
24
|
+
echo "==> M10 experimental uninstaller"
|
|
25
|
+
echo " target: $TARGET"
|
|
26
|
+
[[ $PURGE -eq 1 ]] && echo " mode: --purge (claims.db will be removed)"
|
|
27
|
+
echo
|
|
28
|
+
|
|
29
|
+
# Remove MCP server dir
|
|
30
|
+
if [[ -d "$TARGET/.forge/.mcp-server" ]]; then
|
|
31
|
+
if [[ $PURGE -eq 0 && -f "$TARGET/.forge/.mcp-server/claims.db" ]]; then
|
|
32
|
+
say "preserving claims.db; removing rest of .mcp-server/"
|
|
33
|
+
find "$TARGET/.forge/.mcp-server" -mindepth 1 -not -name claims.db -delete
|
|
34
|
+
else
|
|
35
|
+
rm -rf "$TARGET/.forge/.mcp-server"
|
|
36
|
+
say "removed .forge/.mcp-server/"
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Remove hooks
|
|
41
|
+
for h in forge-claim-check.sh forge-claim-check-doctor.sh; do
|
|
42
|
+
[[ -f "$TARGET/.claude/hooks/$h" ]] && { rm -f "$TARGET/.claude/hooks/$h"; say "removed hook $h"; }
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
# Remove orchestrating skill
|
|
46
|
+
[[ -d "$TARGET/.claude/skills/orchestrating" ]] && {
|
|
47
|
+
rm -rf "$TARGET/.claude/skills/orchestrating"
|
|
48
|
+
say "removed orchestrating skill"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Strip .mcp.json entry
|
|
52
|
+
if [[ -f "$TARGET/.mcp.json" ]] && jq -e '.mcpServers["forge-orchestrator"]' "$TARGET/.mcp.json" >/dev/null 2>&1; then
|
|
53
|
+
tmp=$(mktemp)
|
|
54
|
+
jq 'del(.mcpServers["forge-orchestrator"])' "$TARGET/.mcp.json" > "$tmp"
|
|
55
|
+
mv "$tmp" "$TARGET/.mcp.json"
|
|
56
|
+
say "removed forge-orchestrator from .mcp.json"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Strip settings.json hook entry
|
|
60
|
+
if [[ -f "$TARGET/.claude/settings.json" ]]; then
|
|
61
|
+
tmp=$(mktemp)
|
|
62
|
+
jq '.hooks.PreToolUse = ((.hooks.PreToolUse // []) | map(select((.hooks // []) | all(.command != ".claude/hooks/forge-claim-check.sh"))))' \
|
|
63
|
+
"$TARGET/.claude/settings.json" > "$tmp"
|
|
64
|
+
mv "$tmp" "$TARGET/.claude/settings.json"
|
|
65
|
+
say "removed claim-check hook from settings.json"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
echo
|
|
69
|
+
echo "==> Done."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-orkes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.0",
|
|
4
4
|
"description": "Set up the Forge meta-prompting framework for Claude Code in your project",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-forge": "./bin/create-forge.js"
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin/",
|
|
14
|
-
"template/"
|
|
14
|
+
"template/",
|
|
15
|
+
"experimental/"
|
|
15
16
|
],
|
|
16
17
|
"keywords": [
|
|
17
18
|
"claude",
|
|
@@ -47,13 +47,7 @@
|
|
|
47
47
|
"Edit(**/secrets/**)",
|
|
48
48
|
"Edit(**/.secrets/**)",
|
|
49
49
|
"Edit(**/*.pem)",
|
|
50
|
-
"Edit(**/*.key)"
|
|
51
|
-
"Write(**/.env)",
|
|
52
|
-
"Write(**/.env.*)",
|
|
53
|
-
"Write(**/secrets/**)",
|
|
54
|
-
"Write(**/.secrets/**)",
|
|
55
|
-
"Write(**/*.pem)",
|
|
56
|
-
"Write(**/*.key)"
|
|
50
|
+
"Edit(**/*.key)"
|
|
57
51
|
]
|
|
58
52
|
},
|
|
59
53
|
"hooks": {
|