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,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
|