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.
Files changed (56) 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/hooks/forge-release-fold.sh +9 -1
  32. package/template/.claude/settings.json +1 -7
  33. package/template/.claude/skills/jarvis/SKILL.md +204 -0
  34. package/template/.claude/skills/upgrading/SKILL.md +105 -176
  35. package/template/.forge/FORGE.md +2 -2
  36. package/template/.forge/checks/forge-board-render.sh +11 -5
  37. package/template/.forge/checks/forge-jarvis-answer.sh +79 -0
  38. package/template/.forge/checks/forge-jarvis-awareness.sh +128 -0
  39. package/template/.forge/checks/forge-jarvis-bridge-clean.sh +130 -0
  40. package/template/.forge/checks/forge-jarvis-dispatch.sh +176 -0
  41. package/template/.forge/checks/forge-jarvis-instruments.sh +146 -0
  42. package/template/.forge/checks/forge-jarvis-notify-logonly.sh +40 -0
  43. package/template/.forge/checks/forge-jarvis-promote.sh +103 -0
  44. package/template/.forge/checks/forge-jarvis-prwatch.sh +92 -0
  45. package/template/.forge/checks/forge-jarvis-relay.sh +215 -0
  46. package/template/.forge/checks/forge-jarvis.sh +160 -0
  47. package/template/.forge/checks/tests/forge-jarvis-answer.test.sh +75 -0
  48. package/template/.forge/checks/tests/forge-jarvis-bridge-clean.test.sh +94 -0
  49. package/template/.forge/checks/tests/forge-jarvis-dispatch.test.sh +162 -0
  50. package/template/.forge/checks/tests/forge-jarvis-instruments.test.sh +64 -0
  51. package/template/.forge/checks/tests/forge-jarvis-promote.test.sh +102 -0
  52. package/template/.forge/checks/tests/forge-jarvis-prwatch.test.sh +66 -0
  53. package/template/.forge/checks/tests/forge-jarvis-relay.test.sh +135 -0
  54. package/template/.forge/checks/tests/forge-jarvis.test.sh +122 -0
  55. package/template/.forge/migrations/0.63.0-origin-first-upgrading.md +49 -0
  56. package/template/.forge/templates/project.yml +11 -0
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: testing-standards
3
+ description: Testing conventions and quality standards across stacks. Use when writing tests, reviewing test quality, setting up test infrastructure, or discussing testing strategy. Covers test structure, mocking patterns, coverage expectations, and common pitfalls.
4
+ ---
5
+
6
+ # Testing Standards
7
+
8
+ ## Core principle
9
+
10
+ Tests validate **behaviour defined in requirements and acceptance criteria**, not the current implementation. A good test would catch a subtly wrong implementation.
11
+
12
+ ## Test structure
13
+
14
+ - **Unit tests** (`tests/unit/`): No network, no database, no file system. Fast. Run on every commit.
15
+ - **Integration tests** (`tests/integration/`): Real dependencies. Slower. Run before merge.
16
+ - **Feature tests** (Laravel, `tests/feature/`): HTTP tests against API endpoints with database.
17
+
18
+ ## Naming
19
+
20
+ Test names read as specifications:
21
+
22
+ ```python
23
+ # Good
24
+ def test_expired_token_returns_401(): ...
25
+ def test_duplicate_spf_record_raises_validation_error(): ...
26
+
27
+ # Bad
28
+ def test_token(): ...
29
+ def test_spf(): ...
30
+ ```
31
+
32
+ ## One behaviour per test
33
+
34
+ Each test validates one behaviour. Multiple asserts are fine if they check aspects of the same single behaviour:
35
+
36
+ ```python
37
+ # Good: one behaviour, multiple aspects
38
+ def test_parsed_mechanism_preserves_qualifier_and_value():
39
+ result = parse("~include:example.com")
40
+ assert result.qualifier == "~"
41
+ assert result.value == "example.com"
42
+ assert result.mechanism_type == MechanismType.INCLUDE
43
+
44
+ # Bad: multiple behaviours
45
+ def test_parser():
46
+ assert parse("ip4:1.2.3.4") == ...
47
+ assert parse("include:x.com") == ...
48
+ assert parse("invalid") raises ...
49
+ ```
50
+
51
+ ## Test independence
52
+
53
+ - Each test is independent. No ordering assumptions. No shared mutable state between tests.
54
+ - Factories over fixtures for entity creation. Override only the fields the test cares about.
55
+ - Group tests by the subject under test, not by the type of assertion.
56
+
57
+ ## Mocking
58
+
59
+ Mock at interface/protocol boundaries, not at implementation level:
60
+
61
+ ```python
62
+ # Good: mock the Protocol
63
+ class FakeDNSResolver:
64
+ async def resolve(self, domain: str) -> list[str]:
65
+ return ["1.2.3.4"]
66
+
67
+ service = MyService(resolver=FakeDNSResolver())
68
+
69
+ # Bad: mock internal implementation
70
+ with patch("mymodule.internal_function"):
71
+ ...
72
+ ```
73
+
74
+ ## Coverage expectations
75
+
76
+ - **80% minimum** on business logic
77
+ - **100%** on critical paths: authentication, authorisation, data validation, financial calculations
78
+ - Don't chase 100% overall. Focus coverage on code that matters.
79
+
80
+ ## Test quality self-check
81
+
82
+ After writing tests, verify:
83
+
84
+ 1. Would these tests catch a subtly wrong implementation?
85
+ 2. Are any tests just re-asserting what the code does (tautological)?
86
+ 3. Are error paths and edge cases covered?
87
+ 4. If the implementation changed but behaviour stayed the same, would tests still pass?
88
+ 5. Are mocks at the right level (boundaries, not internals)?
89
+
90
+ ## Common agent testing mistakes
91
+
92
+ - **Tautological tests**: Testing that the code does what it does, not what it should do
93
+ - **Missing error paths**: Only testing the happy path
94
+ - **Implementation coupling**: Tests break when refactoring even though behaviour is unchanged
95
+ - **Overmocking**: Mocking so much that the test validates nothing
96
+ - **Modifying tests to match broken implementation**: If tests fail, fix the code, not the tests
97
+ - **Missing security tests**: No tests for auth failures, input validation rejection, or (in multi-tenant apps) tenant isolation
98
+
99
+ ## Security testing
100
+
101
+ Security-critical code paths require explicit test coverage:
102
+
103
+ - **Authentication**: test that unauthenticated requests are rejected, expired tokens are rejected, invalid tokens are rejected
104
+ - **Authorisation**: test that users cannot access data they are not entitled to, test that role-based restrictions are enforced
105
+ - **Tenant isolation (multi-tenant apps)**: test that a user from tenant A cannot read tenant B's data
106
+ - **Input validation**: test that malformed input is rejected (oversized payloads, invalid formats, injection attempts)
107
+ - **Fail-closed behaviour**: test that exceptions in auth/validation paths result in denial, not permissive fallthrough
108
+ - **DNS-specific** (when applicable): test that malformed DNS responses are handled safely, circular references are detected, depth limits are enforced
109
+
110
+ ## Language-specific
111
+
112
+ ### Python (pytest)
113
+ - Use `pytest-asyncio` and `@pytest.mark.asyncio` for async tests
114
+ - Use `pytest-mock` / `unittest.mock` for mocking
115
+ - Use `conftest.py` for shared fixtures
116
+ - Use `parametrize` for testing multiple inputs against the same logic
117
+ - `freezegun` or `pytest-freezer` for time. Never `sleep` in a test. Never rely on `datetime.now()` without freezing.
118
+ - `responses` or `httpx.MockTransport` for HTTP. Never a live request in a unit test.
119
+ - Django: `pytest-django` with `@pytest.mark.django_db`. Transactional by default; use `transaction=True` only when the test needs committed data.
120
+
121
+ ### PHP (Pest)
122
+ - Use `it()` syntax for readable test names
123
+ - Use `RefreshDatabase` trait for database tests (not `DatabaseTransactions` unless you know why)
124
+ - Use factories for test data
125
+ - Use `expect()` API for assertions
126
+ - In multi-tenant apps, tenant isolation is explicit: at least one test per tenant-scoped endpoint proves a user from tenant A cannot read tenant B's data.
127
+ - Assert against the response envelope, not the raw array shape. `assertJsonPath` / `assertJsonStructure`, not deep equality on a whole blob.
128
+ - Fake queues, mail, storage, events: `Queue::fake()`, `Mail::fake()`, `Storage::fake()`, `Event::fake()`. Then assert the specific dispatch.
129
+ - Do not hit real external services (DNS, third-party APIs, SMTP). Fake or mock at the boundary.
130
+
131
+ ### Vue (Vitest + Testing Library)
132
+ - Use `render()` from `@testing-library/vue` (or the project's custom render helper in `tests/helpers/`) to mount components. Never use `screen` directly -- destructure queries from the `render()` result.
133
+ - Use `userEvent.setup()` for interactions (click, type, etc.), not native events
134
+ - Prefer semantic queries: `getByRole`, `getByLabelText` over `getByTestId` (except for page-level root elements)
135
+ - Use `within()` to scope queries to a specific element instead of searching the whole component
136
+ - Use `waitFor()` for async assertions, never `setTimeout` or manual delays
137
+ - Use the project's translation helper for text assertions instead of hardcoding strings, where one exists
138
+ - Use resource factories from `tests/factories/` for test data. If no factory exists, create one using `@faker-js/faker` and `fishery`
139
+ - Call `unmount()` at the end of each test
140
+ - Always write a mount-check test before any other tests for a component
141
+ - No snapshot tests for components that change often. Snapshots are for stable output (serializers, formatters), not for UI in flux.
142
+ - Mock at the network boundary (MSW or equivalent), not at the `axios` import.
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: vue-conventions
3
+ description: Vue.js frontend coding standards. Use when writing, reviewing, refactoring, or modifying Vue components, composables, or TypeScript frontend code. Covers Composition API, Inertia state management, TypeScript patterns, and component structure.
4
+ ---
5
+
6
+ # Vue.js Conventions
7
+
8
+ ## Component style
9
+
10
+ - Composition API with `<script setup lang="ts">` exclusively. Never Options API.
11
+ - TypeScript required for all components and composables
12
+ - Section order within SFCs: `<script setup>`, `<template>`, `<style scoped>`
13
+
14
+ ## Naming
15
+
16
+ - Components: PascalCase files and tags (`UserProfile.vue`, `<UserProfile />`)
17
+ - Composables: camelCase with `use` prefix (`useAuth.ts`, `useDomainList.ts`)
18
+ - Props/emits: camelCase in script, kebab-case in template
19
+ - Events: past tense (`@updated`, `@deleted`, not `@update`, `@delete`)
20
+
21
+ ## Script setup order
22
+
23
+ Follow this order within `<script setup>`:
24
+
25
+ 1. Imports
26
+ 2. Props (`defineProps`)
27
+ 3. Emits (`defineEmits`)
28
+ 4. Composables (`useRouter()`, `useAuth()`, etc.)
29
+ 5. Reactive state (`ref`, `reactive`)
30
+ 6. Computed properties
31
+ 7. Methods/functions
32
+ 8. Watchers
33
+ 9. Lifecycle hooks (`onMounted`, etc.)
34
+
35
+ ## State management
36
+
37
+ - Inertia.js handles data fetching and page state via XHR. The backend is the source of truth for application state. Do not introduce a client-side store unless there is a clear reason. (Applies when the project uses Inertia; otherwise follow the project's chosen data layer.)
38
+ - Legacy code may use Vuex -- do not extend Vuex usage. New code should not add new stores.
39
+ - Props/emits for parent-child communication
40
+ - `provide/inject` for deep component trees (sparingly)
41
+ - Never mutate props directly
42
+
43
+ ## TypeScript
44
+
45
+ - Prefer `type` over `interface` for prop definitions and general types (team preference)
46
+ - Type all refs: `const count = ref<number>(0)`
47
+ - Type composable return values explicitly
48
+ - Use `as const` for literal types
49
+ - Use destructured defaults on `defineProps`. Avoid `withDefaults()` unless necessary (e.g., non-primitive defaults like arrays or objects)
50
+
51
+ ```typescript
52
+ // Correct: type + destructured defaults
53
+ type Props = {
54
+ domain: Domain
55
+ isEditing?: boolean
56
+ }
57
+ const { domain, isEditing = false } = defineProps<Props>()
58
+
59
+ // Avoid when unnecessary: withDefaults
60
+ const props = withDefaults(defineProps<Props>(), {
61
+ isEditing: false,
62
+ })
63
+
64
+ // Wrong: untyped or inline
65
+ const props = defineProps(['domain', 'isEditing'])
66
+ ```
67
+
68
+ ## Best practices
69
+
70
+ - Keep components under 300 lines. Extract sub-components beyond that.
71
+ - One component per file
72
+ - Always use `:key` on `v-for` (never index-based keys for dynamic lists)
73
+ - Prefer `computed` over methods for derived data
74
+ - Use `watchEffect` for side effects, `watch` only when you need the old value
75
+
76
+ ## Testing
77
+
78
+ - Vitest for unit tests
79
+ - `@testing-library/vue` for component testing (use the project's `render()` helper if one exists in `tests/helpers/`)
80
+ - `userEvent` from `@testing-library/user-event` for interactions, not native events
81
+ - Semantic queries: `getByRole`, `getByLabelText` over `getByTestId` (except for page-level root elements)
82
+ - `waitFor()` for async assertions, never `setTimeout`
83
+ - Test behaviour from the user's perspective, not internal state
84
+
85
+ ## Common agent mistakes to avoid
86
+
87
+ - Don't use Options API — always `<script setup>`
88
+ - Don't skip TypeScript — all components must be typed
89
+ - Don't use index as `:key` in dynamic lists
90
+ - Don't put business logic in components — extract to composables
91
+ - Don't mutate props
92
+ - Don't skip the standard section order in `<script setup>`
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # Forge conventions experimental uninstaller.
3
+ # Removes only the skills/rules this pack installs. Selective by stack, like install.sh.
4
+ #
5
+ # Usage:
6
+ # bash experimental/conventions/uninstall.sh [TARGET] [stack ...]
7
+ #
8
+ # TARGET path to the Forge project (default: current directory)
9
+ # stack one or more of: laravel python vue testing universal
10
+ # no stacks given = remove all this pack's files
11
+
12
+ set -euo pipefail
13
+
14
+ TARGET=""
15
+ STACKS=()
16
+ ALL_STACKS=(laravel python vue testing universal)
17
+
18
+ is_stack() {
19
+ local s="$1"
20
+ for known in "${ALL_STACKS[@]}"; do
21
+ [[ "$known" == "$s" ]] && return 0
22
+ done
23
+ return 1
24
+ }
25
+
26
+ while [[ $# -gt 0 ]]; do
27
+ case "$1" in
28
+ -h|--help) echo "Usage: $0 [TARGET] [stack ...] (stacks: ${ALL_STACKS[*]})"; exit 0 ;;
29
+ *)
30
+ if is_stack "$1"; then
31
+ STACKS+=("$1")
32
+ elif [[ -z "$TARGET" ]]; then
33
+ TARGET="$1"
34
+ else
35
+ echo "ERROR: unknown argument '$1'" >&2; exit 2
36
+ fi
37
+ shift
38
+ ;;
39
+ esac
40
+ done
41
+
42
+ [[ -z "$TARGET" ]] && TARGET="$(pwd)"
43
+ [[ ${#STACKS[@]} -eq 0 ]] && STACKS=("${ALL_STACKS[@]}")
44
+ [[ -d "$TARGET" ]] || { echo "ERROR: target '$TARGET' does not exist" >&2; exit 2; }
45
+
46
+ say() { echo " $*"; }
47
+
48
+ skills_for() {
49
+ case "$1" in
50
+ laravel) echo "laravel-conventions" ;;
51
+ python) echo "python-conventions" ;;
52
+ vue) echo "vue-conventions" ;;
53
+ testing) echo "testing-standards" ;;
54
+ universal) echo "" ;;
55
+ esac
56
+ }
57
+ rules_for() {
58
+ case "$1" in
59
+ laravel) echo "laravel-http.md laravel-services.md laravel-jobs.md laravel-migrations.md" ;;
60
+ python) echo "python.md" ;;
61
+ vue) echo "vue-inertia.md" ;;
62
+ testing) echo "testing.md" ;;
63
+ universal) echo "code-quality.md testing.md" ;;
64
+ esac
65
+ }
66
+
67
+ echo "==> Forge conventions uninstaller"
68
+ echo " target: $TARGET"
69
+ echo " stacks: ${STACKS[*]}"
70
+ echo
71
+
72
+ # Bash 3.2-compatible: space-delimited strings, no namerefs.
73
+ SKILL_LIST=""
74
+ RULE_LIST=""
75
+ contains_word() { case " $1 " in *" $2 "*) return 0 ;; *) return 1 ;; esac; }
76
+ for stack in "${STACKS[@]}"; do
77
+ for s in $(skills_for "$stack"); do
78
+ contains_word "$SKILL_LIST" "$s" || SKILL_LIST="$SKILL_LIST $s"
79
+ done
80
+ for r in $(rules_for "$stack"); do
81
+ contains_word "$RULE_LIST" "$r" || RULE_LIST="$RULE_LIST $r"
82
+ done
83
+ done
84
+
85
+ removed=0
86
+ for s in $SKILL_LIST; do
87
+ d="$TARGET/.claude/skills/$s"
88
+ [[ -d "$d" ]] && { rm -rf "$d"; say "removed skill '$s'"; removed=$((removed + 1)); }
89
+ done
90
+ for r in $RULE_LIST; do
91
+ f="$TARGET/.claude/rules/$r"
92
+ [[ -f "$f" ]] && { rm -f "$f"; say "removed rule '$r'"; removed=$((removed + 1)); }
93
+ done
94
+
95
+ echo
96
+ echo "==> Done. removed: $removed"
97
+ [[ $removed -eq 0 ]] && say "(nothing matched — already absent)"
@@ -0,0 +1,130 @@
1
+ # Forge M10 — Multi-Agent Orchestration (Experimental)
2
+
3
+ ## Status
4
+
5
+ **Experimental.** Per [ADR-001](../../../../.forge/decisions/ADR-001-m10-experimental-track.md), M10 ships outside the core Forge gate. The default `create-forge` template ships **zero** M10 files. This installer is opt-in.
6
+
7
+ Promotion to core requires constitutional amendment after real-world validation.
8
+
9
+ ## M16 Positioning
10
+
11
+ Chief/Streams is now the preferred user-facing orchestration model. Use the
12
+ Project Chief to show, start, pause, resume, delegate, conflict-check, merge, or
13
+ close concurrent streams. M10 remains useful as an optional backend when a stream
14
+ needs worktree isolation, merge-queue discipline, or optional file-claim checks.
15
+
16
+ In practice: think in streams, not claims. Let the Chief decide whether M10 is
17
+ the right substrate for a stream.
18
+
19
+ ## What it does
20
+
21
+ Adds optional multi-agent backend mechanics to a Forge project:
22
+
23
+ - **Worktree isolation** — each agent session runs in its own `git worktree` under `../<repo-basename>-worktrees/{anchor}/` with a locked branch `forge/{anchor}` (configurable via `orchestration.worktree_root` in `.forge/project.yml`; per-repo by default to avoid cross-repo mixing — see ADR-010).
24
+ - **MCP-coordinated claims** — optional experimental defense-in-depth; agents can call `forge_claim_files` before edits so collisions across concurrent sessions surface before damage.
25
+ - **Auto-merge queue** — clean merges go straight to `main`; conflicts route to the `debugging` skill.
26
+ - **PreToolUse claim-check hook** — defense-in-depth: rejects writes to files claimed by another live session.
27
+
28
+ See ADR-001..005 + `.forge/research/milestone-10.md` for design rationale.
29
+
30
+ ## Prerequisites
31
+
32
+ | Tool | Min version | Why |
33
+ |------|-------------|-----|
34
+ | `bash` | 3.2 | Hook + installer |
35
+ | `jq` | any | JSON merging in installer + hook |
36
+ | `sqlite3` | any | Claim store |
37
+ | `node` | ≥ 24 | MCP server runtime (uses built-in `node:sqlite`) |
38
+ | `git` | ≥ 2.48 | Worktree fixes |
39
+ | `git lfs` | ≥ 3.6 (if installed) | Worktree compatibility |
40
+
41
+ Also: **submodules must be clean pins** — initialized and in-sync with their recorded SHA (`git submodule status` shows a ` ` prefix on every line). Clean pinned submodules (e.g. JUCE, nanovg) are supported: each worktree auto-inits them in `orchestrating` Step 2, reusing the shared `.git/modules` store. Bootstrap refuses only on uninitialized (`-`), divergent (`+`), or conflicted (`U`) submodules — see `source/skills/orchestrating/bootstrap-checks.md` "Submodule handling".
42
+
43
+ ## Install (scripted)
44
+
45
+ ```bash
46
+ bash packages/create-forge/experimental/m10/install.sh /path/to/your/forge-repo
47
+ ```
48
+
49
+ Supports `--dry-run` to preview actions without writing.
50
+
51
+ Idempotent — re-running emits warnings instead of errors.
52
+
53
+ ## Install (manual)
54
+
55
+ If you'd rather see exactly what changes:
56
+
57
+ 1. Copy `source/mcp-server/` → `<target>/.forge/.mcp-server/`
58
+ 2. Copy `source/hooks/forge-claim-check.sh`, `source/hooks/forge-claim-check-doctor.sh`, `source/hooks/forge-session-id.sh`, and `source/hooks/forge-branch-guard.sh` → `<target>/.claude/hooks/` (`chmod +x`)
59
+ 3. Copy `source/skills/orchestrating/` → `<target>/.claude/skills/orchestrating/`
60
+ 4. Merge `forge-orchestrator` entry into `<target>/.mcp.json`:
61
+ ```json
62
+ { "mcpServers": { "forge-orchestrator": { "command": "node", "args": [".forge/.mcp-server/index.js"] } } }
63
+ ```
64
+ 5. Append the PreToolUse hook **and** the SessionStart hook to `<target>/.claude/settings.json`:
65
+ ```json
66
+ "PreToolUse": [
67
+ { "matcher": "Edit|Write|MultiEdit|NotebookEdit",
68
+ "hooks": [{ "type": "command", "command": ".claude/hooks/forge-claim-check.sh" }] }
69
+ ],
70
+ "SessionStart": [
71
+ { "hooks": [{ "type": "command", "command": ".claude/hooks/forge-session-id.sh" }] }
72
+ ]
73
+ ```
74
+ Also add the branch-discipline guardrail under `PreToolUse`:
75
+ ```json
76
+ { "matcher": "Bash",
77
+ "hooks": [{ "type": "command", "command": ".claude/hooks/forge-branch-guard.sh" }] }
78
+ ```
79
+ The SessionStart hook captures the Claude session id (claim identity); without it the enforcement gate fail-opens (ADR-007). The branch-guard protects the shared main HEAD when worktrees are active (ADR-008) — orchestration-agnostic, independent of the claim layer.
80
+ 6. `cd <target>/.forge/.mcp-server && npm install`
81
+
82
+ ## Verify
83
+
84
+ After install:
85
+
86
+ 1. Restart Claude Code in the target repo.
87
+ 2. Approve `forge-orchestrator` when prompted.
88
+ 3. Run `/mcp` — confirm `forge-orchestrator: connected`.
89
+ 4. Run a no-op `Edit` on any file — confirm hook fires (check `.forge/.mcp-server/server.log` or watch for hook-injection warnings).
90
+ 5. Start work normally with `forge` — it now auto-routes (see Usage).
91
+
92
+ Run the doctor anytime: `bash .claude/hooks/forge-claim-check-doctor.sh`.
93
+
94
+ ## Usage
95
+
96
+ For new Forge usage, start with Chief/Streams. Ask the Project Chief to start or
97
+ delegate streams, then let it select M10 when a stream needs isolation or merge
98
+ queue support.
99
+
100
+ Legacy behavior remains available for repos that installed M10 as the primary
101
+ orchestration path: `forge` can route Standard/Full work through `orchestrating`
102
+ (worktree + optional file claims) before `executing` when `orchestration.auto`
103
+ is enabled. You still should not invoke `orchestrating` directly unless you are
104
+ debugging or intentionally using the backend.
105
+
106
+ - **Quick tier never auto-orchestrates** — a worktree per typo is the wrong trade.
107
+ - **Incompatible repo** → `orchestrating` bootstrap refuses, writes `lifecycle.worktree_mode: refused`, and `forge` continues single-agent. Nothing breaks.
108
+ - **Opt out** per project: set `orchestration.auto: false` in `.forge/project.yml`. `forge` then reverts to single-agent unless you invoke `Skill(orchestrating)` explicitly.
109
+ - **Mid-session** work already running single-agent (active `lifecycle.worktree_mode`) is never re-bootstrapped.
110
+
111
+ ## Uninstall
112
+
113
+ ```bash
114
+ bash packages/create-forge/experimental/m10/uninstall.sh /path/to/your/forge-repo
115
+ ```
116
+
117
+ Preserves `claims.db` (user state) by default. Pass `--purge` to remove it.
118
+
119
+ ## Known issues
120
+
121
+ See `.forge/research/milestone-10.md` "Sharp edges" section for the canonical list. Headlines:
122
+
123
+ - **Claude Code #33947** — MCP server stdout buffering on macOS can stall first response. Mitigated by keepalive.
124
+ - **Claude Code #40207** — long-idle MCP servers occasionally drop connection. Server emits 8s keepalive ping as mitigation.
125
+ - **Concurrent SQLite contention** — validated to 2-3 agents; not load-tested beyond.
126
+ - **Worktree on submodule repos** — installer refuses (bootstrap check 3).
127
+
128
+ ## Promotion path
129
+
130
+ Per ADR-001 Consequences: M10 graduates to core when (a) used by ≥3 real projects without rollback, (b) constitutional Article XIII amendment is proposed and accepted, (c) installer is converted to a default `create-forge` template option (still opt-in at scaffold time, but built-in).
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env bash
2
+ # Forge M10 experimental installer — multi-agent orchestration opt-in.
3
+ # Idempotent. Re-running emits warnings, not errors.
4
+ # Usage: bash install.sh [--dry-run] /path/to/forge-repo
5
+
6
+ set -euo pipefail
7
+
8
+ DRY_RUN=0
9
+ TARGET=""
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ SOURCE_DIR="${SCRIPT_DIR}/source"
12
+
13
+ while [[ $# -gt 0 ]]; do
14
+ case "$1" in
15
+ --dry-run) DRY_RUN=1; shift ;;
16
+ -h|--help)
17
+ echo "Usage: $0 [--dry-run] /path/to/forge-repo"
18
+ exit 0
19
+ ;;
20
+ *) TARGET="$1"; shift ;;
21
+ esac
22
+ done
23
+
24
+ [[ -z "$TARGET" ]] && { echo "ERROR: target repo path required" >&2; exit 2; }
25
+ [[ -d "$TARGET" ]] || { echo "ERROR: target '$TARGET' does not exist" >&2; exit 2; }
26
+ [[ -f "$TARGET/.forge/project.yml" ]] || { echo "ERROR: target is not a Forge project (.forge/project.yml missing)" >&2; exit 2; }
27
+
28
+ say() { echo " $*"; }
29
+ do_or_say() {
30
+ if [[ $DRY_RUN -eq 1 ]]; then echo "[dry-run] $*"; else eval "$*"; fi
31
+ }
32
+
33
+ echo "==> M10 experimental installer"
34
+ echo " target: $TARGET"
35
+ [[ $DRY_RUN -eq 1 ]] && echo " mode: dry-run (no changes)"
36
+ echo
37
+
38
+ # Step 1: prerequisite doctor
39
+ echo "==> Prerequisites"
40
+ missing=0
41
+ for tool in bash jq sqlite3 node git; do
42
+ if command -v "$tool" >/dev/null 2>&1; then
43
+ say "✓ $tool: $($tool --version 2>&1 | head -1)"
44
+ else
45
+ say "✗ $tool: MISSING"
46
+ missing=1
47
+ fi
48
+ done
49
+ [[ $missing -eq 1 ]] && { echo "ABORT: install missing tools first" >&2; exit 3; }
50
+
51
+ # Node ≥ 24 required for built-in node:sqlite (no native deps, no prebuild-install)
52
+ node_ver=$(node --version 2>&1 | head -1)
53
+ node_major=$(printf '%s' "$node_ver" | sed -E 's/^v?([0-9]+).*/\1/')
54
+ if ! [[ "$node_major" =~ ^[0-9]+$ ]] || (( node_major < 24 )); then
55
+ echo "ABORT: node ≥ v24 required (found $node_ver). MCP server uses built-in node:sqlite." >&2
56
+ echo " Install via: nvm install 24 && nvm alias default 24" >&2
57
+ exit 3
58
+ fi
59
+ echo
60
+
61
+ # Step 2: copy MCP server
62
+ echo "==> MCP server → .forge/.mcp-server/"
63
+ if [[ -d "$TARGET/.forge/.mcp-server" ]]; then
64
+ say "skip: .forge/.mcp-server/ already exists"
65
+ else
66
+ do_or_say "mkdir -p '$TARGET/.forge/.mcp-server'"
67
+ do_or_say "cp -R '$SOURCE_DIR/mcp-server/.' '$TARGET/.forge/.mcp-server/'"
68
+ say "copied"
69
+ fi
70
+ echo
71
+
72
+ # Step 3: copy hooks
73
+ echo "==> Hooks → .claude/hooks/"
74
+ do_or_say "mkdir -p '$TARGET/.claude/hooks'"
75
+ for h in forge-claim-check.sh forge-claim-check-doctor.sh forge-session-id.sh forge-branch-guard.sh; do
76
+ if [[ -f "$TARGET/.claude/hooks/$h" ]]; then
77
+ say "skip: $h already exists"
78
+ else
79
+ do_or_say "cp '$SOURCE_DIR/hooks/$h' '$TARGET/.claude/hooks/$h'"
80
+ do_or_say "chmod +x '$TARGET/.claude/hooks/$h'"
81
+ say "copied $h"
82
+ fi
83
+ done
84
+ echo
85
+
86
+ # Step 4: copy orchestrating skill
87
+ echo "==> Skill → .claude/skills/orchestrating/"
88
+ if [[ -d "$TARGET/.claude/skills/orchestrating" ]]; then
89
+ say "skip: orchestrating skill already exists"
90
+ else
91
+ do_or_say "mkdir -p '$TARGET/.claude/skills/orchestrating'"
92
+ do_or_say "cp -R '$SOURCE_DIR/skills/orchestrating/.' '$TARGET/.claude/skills/orchestrating/'"
93
+ say "copied"
94
+ fi
95
+ echo
96
+
97
+ # Step 5: merge .mcp.json
98
+ echo "==> .mcp.json registration"
99
+ MCP_FILE="$TARGET/.mcp.json"
100
+ if [[ ! -f "$MCP_FILE" ]]; then
101
+ do_or_say "cp '$SOURCE_DIR/mcp-server/example.mcp.json' '$MCP_FILE'"
102
+ say "created .mcp.json"
103
+ elif jq -e '.mcpServers["forge-orchestrator"]' "$MCP_FILE" >/dev/null 2>&1; then
104
+ say "skip: forge-orchestrator already registered"
105
+ else
106
+ if [[ $DRY_RUN -eq 0 ]]; then
107
+ tmp=$(mktemp)
108
+ jq '.mcpServers["forge-orchestrator"] = {"command":"node","args":[".forge/.mcp-server/index.js"]}' "$MCP_FILE" > "$tmp"
109
+ mv "$tmp" "$MCP_FILE"
110
+ fi
111
+ say "merged forge-orchestrator entry"
112
+ fi
113
+ echo
114
+
115
+ # Step 6: merge .claude/settings.json hook
116
+ echo "==> .claude/settings.json PreToolUse hook"
117
+ SETTINGS="$TARGET/.claude/settings.json"
118
+ HOOK_CMD='.claude/hooks/forge-claim-check.sh'
119
+ if [[ ! -f "$SETTINGS" ]]; then
120
+ if [[ $DRY_RUN -eq 0 ]]; then
121
+ cat > "$SETTINGS" <<'JSON'
122
+ {
123
+ "hooks": {
124
+ "PreToolUse": [
125
+ {
126
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
127
+ "hooks": [{ "type": "command", "command": ".claude/hooks/forge-claim-check.sh" }]
128
+ }
129
+ ]
130
+ }
131
+ }
132
+ JSON
133
+ fi
134
+ say "created settings.json with PreToolUse hook"
135
+ elif jq -e --arg cmd "$HOOK_CMD" '.hooks.PreToolUse[]?.hooks[]? | select(.command == $cmd)' "$SETTINGS" >/dev/null 2>&1; then
136
+ say "skip: forge-claim-check.sh hook already registered"
137
+ else
138
+ if [[ $DRY_RUN -eq 0 ]]; then
139
+ tmp=$(mktemp)
140
+ jq '.hooks.PreToolUse += [{"matcher":"Edit|Write|MultiEdit|NotebookEdit","hooks":[{"type":"command","command":".claude/hooks/forge-claim-check.sh"}]}]' "$SETTINGS" > "$tmp"
141
+ mv "$tmp" "$SETTINGS"
142
+ fi
143
+ say "appended PreToolUse hook entry"
144
+ fi
145
+ echo
146
+
147
+ # Step 6b: register SessionStart hook (captures Claude session_id → claim identity)
148
+ echo "==> .claude/settings.json SessionStart hook"
149
+ SID_CMD='.claude/hooks/forge-session-id.sh'
150
+ if [[ ! -f "$SETTINGS" ]]; then
151
+ say "[dry-run] settings.json not yet created — SessionStart would be added alongside PreToolUse"
152
+ elif jq -e --arg cmd "$SID_CMD" '.hooks.SessionStart[]?.hooks[]? | select(.command == $cmd)' "$SETTINGS" >/dev/null 2>&1; then
153
+ say "skip: forge-session-id.sh hook already registered"
154
+ else
155
+ if [[ $DRY_RUN -eq 0 ]]; then
156
+ tmp=$(mktemp)
157
+ jq '.hooks.SessionStart = ((.hooks.SessionStart // []) + [{"hooks":[{"type":"command","command":".claude/hooks/forge-session-id.sh"}]}])' "$SETTINGS" > "$tmp"
158
+ mv "$tmp" "$SETTINGS"
159
+ fi
160
+ say "appended SessionStart hook entry"
161
+ fi
162
+ echo
163
+
164
+ # Step 6c: register branch-discipline guardrail (PreToolUse on Bash). Orchestration-
165
+ # agnostic — protects the shared main HEAD whenever >1 worktree exists.
166
+ echo "==> .claude/settings.json branch-guard hook"
167
+ BG_CMD='.claude/hooks/forge-branch-guard.sh'
168
+ if [[ ! -f "$SETTINGS" ]]; then
169
+ say "[dry-run] settings.json not yet created — branch-guard would be added alongside the others"
170
+ elif jq -e --arg cmd "$BG_CMD" '.hooks.PreToolUse[]?.hooks[]? | select(.command == $cmd)' "$SETTINGS" >/dev/null 2>&1; then
171
+ say "skip: forge-branch-guard.sh hook already registered"
172
+ else
173
+ if [[ $DRY_RUN -eq 0 ]]; then
174
+ tmp=$(mktemp)
175
+ jq '.hooks.PreToolUse += [{"matcher":"Bash","hooks":[{"type":"command","command":".claude/hooks/forge-branch-guard.sh"}]}]' "$SETTINGS" > "$tmp"
176
+ mv "$tmp" "$SETTINGS"
177
+ fi
178
+ say "appended branch-guard PreToolUse(Bash) hook entry"
179
+ fi
180
+ echo
181
+
182
+ # Step 7: next steps
183
+ cat <<EOF
184
+ ==> Done.
185
+
186
+ Next steps:
187
+ 1. cd '$TARGET/.forge/.mcp-server' && npm install
188
+ 2. Restart Claude Code
189
+ 3. Approve 'forge-orchestrator' MCP server when prompted
190
+ 4. Run /mcp to confirm connection
191
+ 5. Just start work with forge — it now auto-routes Standard/Full work
192
+ through orchestrating (worktree + claims). No separate invocation.
193
+
194
+ Install = consent. Opt out per project with 'orchestration.auto: false'
195
+ in .forge/project.yml, or start one explicitly with Skill(orchestrating).
196
+
197
+ Uninstall: bash $(dirname "${BASH_SOURCE[0]}")/uninstall.sh '$TARGET'
198
+ EOF