forge-orkes 0.62.0 → 0.64.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/hooks/forge-release-fold.sh +9 -1
- package/template/.claude/settings.json +1 -7
- package/template/.claude/skills/jarvis/SKILL.md +204 -0
- package/template/.claude/skills/upgrading/SKILL.md +105 -176
- package/template/.forge/FORGE.md +2 -2
- package/template/.forge/checks/forge-board-render.sh +11 -5
- package/template/.forge/checks/forge-jarvis-answer.sh +79 -0
- package/template/.forge/checks/forge-jarvis-awareness.sh +128 -0
- package/template/.forge/checks/forge-jarvis-bridge-clean.sh +130 -0
- package/template/.forge/checks/forge-jarvis-dispatch.sh +176 -0
- package/template/.forge/checks/forge-jarvis-instruments.sh +146 -0
- package/template/.forge/checks/forge-jarvis-notify-logonly.sh +40 -0
- package/template/.forge/checks/forge-jarvis-promote.sh +103 -0
- package/template/.forge/checks/forge-jarvis-prwatch.sh +92 -0
- package/template/.forge/checks/forge-jarvis-relay.sh +215 -0
- package/template/.forge/checks/forge-jarvis.sh +160 -0
- package/template/.forge/checks/tests/forge-jarvis-answer.test.sh +75 -0
- package/template/.forge/checks/tests/forge-jarvis-bridge-clean.test.sh +94 -0
- package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +162 -0
- package/template/.forge/checks/tests/forge-jarvis-instruments.test.sh +64 -0
- package/template/.forge/checks/tests/forge-jarvis-promote.test.sh +102 -0
- package/template/.forge/checks/tests/forge-jarvis-prwatch.test.sh +66 -0
- package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +135 -0
- package/template/.forge/checks/tests/forge-jarvis.test.sh +122 -0
- package/template/.forge/migrations/0.63.0-origin-first-upgrading.md +49 -0
- package/template/.forge/templates/project.yml +11 -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). |
|