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.
Files changed (34) hide show
  1. package/bin/create-forge.js +250 -99
  2. package/experimental/conventions/README.md +87 -0
  3. package/experimental/conventions/install.sh +179 -0
  4. package/experimental/conventions/source/rules/code-quality.md +17 -0
  5. package/experimental/conventions/source/rules/laravel-http.md +21 -0
  6. package/experimental/conventions/source/rules/laravel-jobs.md +19 -0
  7. package/experimental/conventions/source/rules/laravel-migrations.md +19 -0
  8. package/experimental/conventions/source/rules/laravel-services.md +21 -0
  9. package/experimental/conventions/source/rules/python.md +14 -0
  10. package/experimental/conventions/source/rules/testing.md +15 -0
  11. package/experimental/conventions/source/rules/vue-inertia.md +24 -0
  12. package/experimental/conventions/source/skills/laravel-conventions/SKILL.md +124 -0
  13. package/experimental/conventions/source/skills/python-conventions/SKILL.md +100 -0
  14. package/experimental/conventions/source/skills/testing-standards/SKILL.md +142 -0
  15. package/experimental/conventions/source/skills/vue-conventions/SKILL.md +92 -0
  16. package/experimental/conventions/uninstall.sh +97 -0
  17. package/experimental/m10/README.md +130 -0
  18. package/experimental/m10/install.sh +198 -0
  19. package/experimental/m10/source/hooks/forge-branch-guard.sh +61 -0
  20. package/experimental/m10/source/hooks/forge-claim-check-doctor.sh +77 -0
  21. package/experimental/m10/source/hooks/forge-claim-check.sh +113 -0
  22. package/experimental/m10/source/hooks/forge-session-id.sh +22 -0
  23. package/experimental/m10/source/mcp-server/README.md +74 -0
  24. package/experimental/m10/source/mcp-server/example.mcp.json +8 -0
  25. package/experimental/m10/source/mcp-server/index.js +460 -0
  26. package/experimental/m10/source/mcp-server/package.json +17 -0
  27. package/experimental/m10/source/skills/orchestrating/SKILL.md +223 -0
  28. package/experimental/m10/source/skills/orchestrating/bootstrap-checks.md +70 -0
  29. package/experimental/m10/uninstall.sh +69 -0
  30. package/package.json +3 -2
  31. package/template/.claude/settings.json +1 -7
  32. package/template/.claude/skills/upgrading/SKILL.md +105 -176
  33. package/template/.forge/FORGE.md +2 -2
  34. package/template/.forge/migrations/0.63.0-origin-first-upgrading.md +49 -0
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bash
2
+ # Forge PreToolUse(Bash) guardrail — protect the shared main checkout's HEAD when
3
+ # parallel git worktrees are active.
4
+ #
5
+ # Blocks the "moved the shared HEAD under other sessions" catastrophe (canvaz, burned
6
+ # twice 2026-06-05): switching branches or hard-resetting in the MAIN checkout while
7
+ # sibling worktrees are branched off it. Orchestration-agnostic — depends only on git
8
+ # worktree state, so it holds whether coordination is M10, Agent Teams, or manual
9
+ # worktrees.
10
+ #
11
+ # Scope is deliberately narrow to avoid false positives:
12
+ # - fires ONLY when >1 worktree exists (single-worktree/solo work is never touched)
13
+ # - fires ONLY in the main checkout (linked worktrees own their own HEAD)
14
+ # - skips any command containing `cd` (can't reliably tell which tree it targets)
15
+ # - allows safe forms: file restore (`git checkout -- <path>`), branch create (`-b`)
16
+ #
17
+ # exit 0 = allow; exit 2 = deny. Override by running the command yourself outside Claude.
18
+
19
+ set -uo pipefail
20
+
21
+ PAYLOAD=$(cat)
22
+ CMD=$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.command // empty' 2>/dev/null) || exit 0
23
+ [ -z "$CMD" ] && exit 0
24
+
25
+ # Only git commands are interesting; and bail on `cd` (target tree is ambiguous).
26
+ case "$CMD" in
27
+ *git*) ;;
28
+ *) exit 0 ;;
29
+ esac
30
+ case "$CMD" in
31
+ *"cd "*) exit 0 ;;
32
+ esac
33
+
34
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
35
+
36
+ # Single worktree → no shared-HEAD hazard.
37
+ wt_count=$(git worktree list 2>/dev/null | wc -l | tr -d ' ')
38
+ [ "${wt_count:-1}" -le 1 ] && exit 0
39
+
40
+ # Main checkout iff --git-dir == --git-common-dir (linked worktrees differ).
41
+ [ "$(git rev-parse --git-dir 2>/dev/null)" = "$(git rev-parse --git-common-dir 2>/dev/null)" ] || exit 0
42
+
43
+ deny() { echo "[forge-branch-guard] $*" >&2; exit 2; }
44
+
45
+ # Branch switch (checkout/switch to a ref), excluding file-restore (`--`) and create (`-b`).
46
+ switches_branch=0
47
+ if printf '%s' "$CMD" | grep -qE '\bgit[[:space:]]+(checkout|switch)\b'; then
48
+ if ! printf '%s' "$CMD" | grep -qE '([[:space:]]--([[:space:]]|$))|(\bgit[[:space:]]+(checkout|switch)[[:space:]]+-[bB]\b)'; then
49
+ switches_branch=1
50
+ fi
51
+ fi
52
+
53
+ # Hard/keep/merge reset rewrites HEAD + working tree.
54
+ hard_reset=0
55
+ printf '%s' "$CMD" | grep -qE '\bgit[[:space:]]+reset[[:space:]]+(--hard|--keep|--merge)\b' && hard_reset=1
56
+
57
+ if [ "$switches_branch" = "1" ] || [ "$hard_reset" = "1" ]; then
58
+ deny "Refusing to move the shared main HEAD — $wt_count worktrees are branched off it (the 'burned twice' failure). Switch/reset inside a worktree, or stop the other sessions first. Override: run the command yourself outside Claude."
59
+ fi
60
+
61
+ exit 0
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # Forge claim-check hook prerequisites probe. Informational only — always exit 0.
3
+ # Run manually or via install procedure to confirm bash/jq/sqlite3/timeout availability.
4
+
5
+ set -uo pipefail
6
+
7
+ check() {
8
+ local name=$1 cmd=$2 version_flag=${3:---version}
9
+ if command -v "$cmd" >/dev/null 2>&1; then
10
+ local ver
11
+ ver=$("$cmd" "$version_flag" 2>&1 | head -1)
12
+ printf ' ✓ %-10s %s\n' "$name" "$ver"
13
+ else
14
+ printf ' ✗ %-10s MISSING\n' "$name"
15
+ fi
16
+ }
17
+
18
+ echo "Forge claim-check hook — prerequisites"
19
+ echo
20
+
21
+ check "bash" "bash" "--version"
22
+ check "jq" "jq" "--version"
23
+ check "sqlite3" "sqlite3" "--version"
24
+
25
+ # node ≥ 24 required for built-in node:sqlite used by MCP server
26
+ if command -v node >/dev/null 2>&1; then
27
+ node_ver=$(node --version 2>&1 | head -1)
28
+ node_major=$(printf '%s' "$node_ver" | sed -E 's/^v?([0-9]+).*/\1/')
29
+ if [[ "$node_major" =~ ^[0-9]+$ ]] && (( node_major >= 24 )); then
30
+ printf ' ✓ %-10s %s\n' "node" "$node_ver"
31
+ else
32
+ printf ' ✗ %-10s %s (need ≥ v24 — MCP server uses built-in node:sqlite)\n' "node" "$node_ver"
33
+ fi
34
+ else
35
+ printf ' ✗ %-10s MISSING (need ≥ v24)\n' "node"
36
+ fi
37
+
38
+ if command -v timeout >/dev/null 2>&1; then
39
+ printf ' ✓ %-10s %s\n' "timeout" "$(timeout --version 2>&1 | head -1)"
40
+ elif command -v gtimeout >/dev/null 2>&1; then
41
+ printf ' ✓ %-10s (gtimeout) %s\n' "timeout" "$(gtimeout --version 2>&1 | head -1)"
42
+ else
43
+ printf ' ✗ %-10s MISSING (optional — install coreutils for bounded queries)\n' "timeout"
44
+ fi
45
+
46
+ echo
47
+ echo "DB lookup path: ${FORGE_CLAIMS_DB:-${CLAUDE_PROJECT_DIR:-$PWD}/.forge/.mcp-server/claims.db}"
48
+ echo "Session id: ${CLAUDE_SESSION_ID:-<unset — resolved from PreToolUse stdin at edit time>}"
49
+
50
+ # --- Live enforcement canary ---
51
+ # Prove the gate actually DENIES a foreign-claimed edit, against a throwaway DB (never
52
+ # touches real claims.db). Catches the inert-gate class (identity read from the wrong
53
+ # source, hook not wired) that a prereq check can't see — the bug that hid for days.
54
+ echo
55
+ echo "Enforcement canary:"
56
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
57
+ CHK="$HOOK_DIR/forge-claim-check.sh"
58
+ if [ ! -f "$CHK" ]; then
59
+ printf ' ✗ %s\n' "forge-claim-check.sh not found next to doctor — gate not installed"
60
+ elif ! command -v sqlite3 >/dev/null 2>&1; then
61
+ printf ' ✗ %s\n' "sqlite3 missing — cannot run canary"
62
+ else
63
+ cdb=$(mktemp); cfile="${TMPDIR:-/tmp}/forge-canary-$$"
64
+ sqlite3 "$cdb" "CREATE TABLE claims(file_path TEXT, session_id TEXT NOT NULL, claimed_at INTEGER, expires_at INTEGER, reason TEXT);"
65
+ sqlite3 "$cdb" "INSERT INTO claims VALUES('$cfile','canary-foreign', strftime('%s','now'), strftime('%s','now')+60, 'canary');"
66
+ printf '%s' '{"session_id":"canary-self","tool_input":{"file_path":"'"$cfile"'"}}' \
67
+ | env -u CLAUDE_SESSION_ID FORGE_CLAIMS_DB="$cdb" CLAUDE_PROJECT_DIR="${TMPDIR:-/tmp}" bash "$CHK" >/dev/null 2>&1
68
+ rc=$?
69
+ rm -f "$cdb"
70
+ if [ "$rc" -eq 2 ]; then
71
+ printf ' ✓ %s\n' "foreign-claimed edit DENIED (exit 2) — gate enforces"
72
+ else
73
+ printf ' ✗ %s\n' "foreign-claimed edit ALLOWED (exit $rc) — GATE IS INERT; edits are NOT protected"
74
+ fi
75
+ fi
76
+
77
+ exit 0
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # Forge PreToolUse hook — cross-session file-claim collision detector.
3
+ #
4
+ # Contract:
5
+ # stdin: Claude Code PreToolUse JSON payload
6
+ # exit 0 = allow (no claim, own claim, no DB, no session context, unknown schema)
7
+ # exit 2 = deny (cross-session claim active, or any internal error — fail-closed)
8
+ #
9
+ # Defense-in-depth: ERR trap converts ANY unexpected failure into an exit-2 deny.
10
+ # Never exit 1 — Claude Code treats exit 1 as soft warning; we want hard block on error.
11
+
12
+ set -euo pipefail
13
+
14
+ deny() {
15
+ echo "[forge-hook] $*" >&2
16
+ exit 2
17
+ }
18
+
19
+ trap 'deny "internal error at line $LINENO — denying for safety (fail-closed)"' ERR
20
+
21
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
22
+ DB="${FORGE_CLAIMS_DB:-$PROJECT_DIR/.forge/.mcp-server/claims.db}"
23
+ SESSION_ID="${CLAUDE_SESSION_ID:-}" # env override (tests); real id resolved from stdin payload below
24
+
25
+ # Detect timeout wrapper (macOS lacks GNU `timeout` unless coreutils installed → `gtimeout`).
26
+ if command -v timeout >/dev/null 2>&1; then
27
+ TIMEOUT_CMD=(timeout 4)
28
+ elif command -v gtimeout >/dev/null 2>&1; then
29
+ TIMEOUT_CMD=(gtimeout 4)
30
+ else
31
+ TIMEOUT_CMD=() # no wrapper — sqlite call runs unbounded; busy_timeout in DB still applies
32
+ fi
33
+
34
+ PAYLOAD=$(cat)
35
+
36
+ # Claude Code delivers the session id in the PreToolUse stdin payload (.session_id), NOT in
37
+ # the environment — so a bare CLAUDE_SESSION_ID is always empty and the gate would fail-open.
38
+ # Resolve it from stdin when no env override is set. This is the SAME identity the
39
+ # orchestrating skill claims under (captured by the forge-session-id.sh SessionStart hook),
40
+ # so a session's own claims are recognized (line ~88) and other sessions' claims block.
41
+ if [ -z "$SESSION_ID" ]; then
42
+ SESSION_ID=$(printf '%s' "$PAYLOAD" | jq -r '.session_id // empty')
43
+ fi
44
+
45
+ # Normalize across known path fields:
46
+ # Edit / Write → tool_input.file_path
47
+ # NotebookEdit → tool_input.notebook_path (validated in spike — see milestone-10-validation.md §2)
48
+ # MultiEdit → tool_input.file_path (single file, multiple edits; per docs)
49
+ # Future / unknown tools → tool_input.path (defensive)
50
+ # jq emits each matching path on its own line. Empty output = no path field present.
51
+ FILES=$(printf '%s' "$PAYLOAD" | jq -r '
52
+ [ .tool_input.file_path?
53
+ , .tool_input.notebook_path?
54
+ , .tool_input.path?
55
+ , ( .tool_input.edits? // [] | .[]?.file_path? )
56
+ ] | map(select(. != null and . != "")) | unique | .[]
57
+ ')
58
+
59
+ if [ -z "$FILES" ]; then
60
+ # No recognized path field — unknown schema. Allow (do not deny on schema drift).
61
+ exit 0
62
+ fi
63
+
64
+ # No DB = MCP server has never run in this repo (fresh-repo case). Fail-open only here.
65
+ if [ ! -f "$DB" ]; then
66
+ exit 0
67
+ fi
68
+
69
+ if [ -z "$SESSION_ID" ]; then
70
+ # No resolvable identity. Distinguish genuine solo work from a broken gate: if active
71
+ # claims exist, multi-agent coordination is clearly in use and editing blind is unsafe
72
+ # → fail-CLOSED (loud). No active claims → solo work → allow. (Silent fail-open here is
73
+ # exactly how the gate was inert for days — never repeat it when claims are live.)
74
+ active=$("${TIMEOUT_CMD[@]+"${TIMEOUT_CMD[@]}"}" sqlite3 -batch "$DB" \
75
+ "SELECT COUNT(*) FROM claims WHERE expires_at > strftime('%s','now');" 2>/dev/null || echo 0)
76
+ if [ "${active:-0}" -gt 0 ]; then
77
+ deny "no session id (env or stdin .session_id) but $active active claim(s) exist — refusing to edit blind (fail-closed). The SessionStart hook may not have run, or this context (e.g. an Agent isolation:\"worktree\" sub-agent) has no hook config. Worktree isolation still applies; claim coordination does not."
78
+ fi
79
+ echo "[forge-hook] no session id, no active claims — allowing (single-agent mode)" >&2
80
+ exit 0
81
+ fi
82
+
83
+ # Resolve each path to absolute (matches what MCP server stores via path.resolve).
84
+ abspath() {
85
+ case "$1" in
86
+ /*) printf '%s' "$1" ;;
87
+ *) printf '%s/%s' "$PROJECT_DIR" "$1" ;;
88
+ esac
89
+ }
90
+
91
+ while IFS= read -r raw; do
92
+ [ -z "$raw" ] && continue
93
+ file=$(abspath "$raw")
94
+
95
+ # Parameterized query via .param — avoids quoting injection on path strings.
96
+ result=$("${TIMEOUT_CMD[@]+"${TIMEOUT_CMD[@]}"}" sqlite3 -batch "$DB" \
97
+ ".param set :fp '$file'" \
98
+ "SELECT session_id || '|' || expires_at FROM claims WHERE file_path = :fp AND expires_at > strftime('%s','now') LIMIT 1;")
99
+
100
+ [ -z "$result" ] && continue
101
+
102
+ owner="${result%%|*}"
103
+ expires_at="${result##*|}"
104
+
105
+ if [ "$owner" = "$SESSION_ID" ]; then
106
+ continue # own claim
107
+ fi
108
+
109
+ expires_human=$(date -r "$expires_at" "+%Y-%m-%d %H:%M:%S %Z" 2>/dev/null || echo "epoch:$expires_at")
110
+ deny "Edit denied: $file claimed by session $owner until $expires_human. Call forge_release_claims in that session, or wait."
111
+ done <<< "$FILES"
112
+
113
+ exit 0
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # Forge SessionStart hook — persist this session's Claude session_id.
3
+ #
4
+ # Why: file claims are owned by an identity that BOTH the claimer and the enforcer must
5
+ # agree on. The PreToolUse enforcement hook (forge-claim-check.sh) reads the Claude
6
+ # session_id from its stdin payload, but the orchestrating skill (plain Claude, no stdin)
7
+ # can't read its own session_id directly — and Claude Code does NOT export it to the env.
8
+ # So we capture it here, once per session, into a file the skill reads at claim time.
9
+ # Net: claim owner == enforcement id == the Claude session_id. (See ADR-007.)
10
+ #
11
+ # Contract: exit 0 always — this hook must never block session start.
12
+
13
+ set -euo pipefail
14
+
15
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
16
+
17
+ sid=$(cat | jq -r '.session_id // empty' 2>/dev/null || true)
18
+ [ -z "$sid" ] && exit 0
19
+
20
+ mkdir -p "$PROJECT_DIR/.forge/.mcp-server"
21
+ printf '%s' "$sid" > "$PROJECT_DIR/.forge/.mcp-server/.session-id"
22
+ exit 0
@@ -0,0 +1,74 @@
1
+ # forge-orchestrator (M10 experimental MCP server)
2
+
3
+ Per-repo, single-process MCP server exposing file claims + commit queue for multi-agent Forge sessions.
4
+
5
+ This server is **experimental** per `.forge/decisions/ADR-001-m10-experimental-track.md`. It is NOT installed by default. Opt-in only.
6
+
7
+ ## Prerequisites
8
+
9
+ - **Node ≥ 20** — already required by Claude Code; no extra runtime install.
10
+ - **git** — repo must be a git work tree (commit queue depends on `git merge-tree`, `git update-ref`).
11
+ - **Repo dependencies** — run `npm install` once inside `.forge/.mcp-server/` to pull `@modelcontextprotocol/sdk` + `better-sqlite3`.
12
+
13
+ ## Startup
14
+
15
+ **Do not start manually.** Claude Code spawns the server based on `.mcp.json` registration. See `example.mcp.json` for the registration block.
16
+
17
+ On startup the server:
18
+ 1. Detects + adopts any stale `server.pid` (#33947 mitigation — SIGTERM, 5s grace, SIGKILL fallback).
19
+ 2. Opens `claims.db` with WAL + `busy_timeout=5000`. Creates schema if missing (`PRAGMA user_version=1`).
20
+ 3. Begins 8s stderr keepalive emissions (#40207 mitigation).
21
+ 4. Connects to stdio MCP transport.
22
+
23
+ To self-test without MCP wiring: `npm run self-test` — exercises claim + release + list round-trip and exits.
24
+
25
+ ## Shutdown
26
+
27
+ SIGTERM-driven. Claude Code sends SIGTERM when the session ends.
28
+
29
+ The trap:
30
+ - Closes the SQLite handle.
31
+ - Removes `server.pid` if it still matches our PID.
32
+ - Exits 0.
33
+
34
+ Sessions release their own claims via `forge_release_claims`. The server itself does NOT bulk-purge claims on shutdown — multiple sessions may share one server process, and a crash should not drop unrelated sessions' claims (expired rows are cleared lazily on next access).
35
+
36
+ There is no `/stop` admin tool. If you need to force shutdown, kill the PID from `server.pid`.
37
+
38
+ ## Known Bugs (Mitigated)
39
+
40
+ - **Claude Code #33947 — pidfile orphan.** Crashed servers can leave a `server.pid` pointing to a dead process. Mitigation: startup `adoptPidfile()` SIGTERMs any live owner, falls through to SIGKILL after 5s, then writes own PID.
41
+ - **Claude Code #40207 — premature SIGTERM on idle.** Claude Code may SIGTERM MCP servers it considers idle. Mitigation: 8-second stderr keepalive (`KEEPALIVE_MS`). Validated against >10min idle session in plan-01 (`.forge/research/milestone-10-validation.md`).
42
+
43
+ ## Recovery
44
+
45
+ If `/mcp` shows `forge-orchestrator` as failed/stuck:
46
+
47
+ 1. Inspect stderr via Claude Code's MCP log panel.
48
+ 2. If a stale `server.pid` is suspected — confirm process is actually dead (`ps -p $(cat .forge/.mcp-server/server.pid)`), then delete the file.
49
+ 3. Restart via `/mcp reconnect forge-orchestrator` (or restart the Claude Code session).
50
+ 4. If `claims.db` is corrupted — delete `claims.db`, `claims.db-wal`, `claims.db-shm`. Server recreates schema on next start. **All active claims are lost.**
51
+
52
+ ## Logging
53
+
54
+ Stderr only. Claude Code captures and surfaces MCP stderr in its log panel. There is no file log — keep it that way to avoid log-rotation responsibilities and disk-leak surprises.
55
+
56
+ Three stderr line patterns:
57
+ - `[forge-orchestrator] started pid=... db=...` — boot
58
+ - `[forge-orchestrator] keepalive <iso8601>` — every 8s
59
+ - `[forge-orchestrator] stale pidfile pid=... — sending SIGTERM` — orphan-detect path
60
+
61
+ ## Contracts
62
+
63
+ - Tool schemas + semantics: `.forge/phases/m10-01-orchestration/contracts/mcp-tools.md`
64
+ - Data model + retention: `.forge/phases/m10-01-orchestration/data-model.md`
65
+ - Architecture rationale: `.forge/decisions/ADR-003-awareness-mcp-sqlite.md`
66
+ - Lifecycle rationale: `.forge/decisions/ADR-005-session-lifecycle.md`
67
+
68
+ ## Environment Variables
69
+
70
+ | Var | Default | Purpose |
71
+ |-----|---------|---------|
72
+ | `FORGE_REPO_ROOT` | `process.cwd()` | Root for `git` invocations in `forge_queue_commit`. |
73
+ | `FORGE_DB` | `<server-dir>/claims.db` | Override DB path (testing only). |
74
+ | `FORGE_PIDFILE` | `<server-dir>/server.pid` | Override pidfile path (testing only). |
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "forge-orchestrator": {
4
+ "command": "node",
5
+ "args": [".forge/.mcp-server/index.js"]
6
+ }
7
+ }
8
+ }