modastack 0.2.0__tar.gz
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.
- modastack-0.2.0/.claude/hooks/session-state.sh +52 -0
- modastack-0.2.0/.claude/memory/project_dispatch.md +22 -0
- modastack-0.2.0/.claude/plans/github-app-manifest.md +225 -0
- modastack-0.2.0/.claude/settings.json +7 -0
- modastack-0.2.0/.github/workflows/deploy-event-server.yml +36 -0
- modastack-0.2.0/.github/workflows/publish-pypi.yml +41 -0
- modastack-0.2.0/.gitignore +16 -0
- modastack-0.2.0/.idea/.gitignore +5 -0
- modastack-0.2.0/.idea/encodings.xml +6 -0
- modastack-0.2.0/.modastack.yaml +23 -0
- modastack-0.2.0/CHANGELOG.md +63 -0
- modastack-0.2.0/CLAUDE.md +248 -0
- modastack-0.2.0/PKG-INFO +33 -0
- modastack-0.2.0/README.md +430 -0
- modastack-0.2.0/VERSION +1 -0
- modastack-0.2.0/bootstrap.sh +15 -0
- modastack-0.2.0/dashboard/__init__.py +0 -0
- modastack-0.2.0/dashboard/app.py +219 -0
- modastack-0.2.0/dashboard/data.py +240 -0
- modastack-0.2.0/dashboard/templates/index.html +412 -0
- modastack-0.2.0/deploy/INSTALL.md +592 -0
- modastack-0.2.0/deploy/auto-deploy.sh +106 -0
- modastack-0.2.0/deploy/install.sh +760 -0
- modastack-0.2.0/deploy/modastack.service +15 -0
- modastack-0.2.0/deploy/setup-ec2.sh +175 -0
- modastack-0.2.0/deploy/update-webhooks.sh +88 -0
- modastack-0.2.0/docs/CUSTOM_WORKFLOWS.md +345 -0
- modastack-0.2.0/docs/LINEAR_SETUP.md +91 -0
- modastack-0.2.0/docs/SKILL_PACKS.md +105 -0
- modastack-0.2.0/docs/SLACK_SETUP.md +111 -0
- modastack-0.2.0/docs/WORKFLOWS.md +305 -0
- modastack-0.2.0/docs/specs/85-worktree-relocation.md +263 -0
- modastack-0.2.0/event-server/.editorconfig +12 -0
- modastack-0.2.0/event-server/.gitignore +167 -0
- modastack-0.2.0/event-server/.prettierrc +6 -0
- modastack-0.2.0/event-server/package-lock.json +2891 -0
- modastack-0.2.0/event-server/package.json +18 -0
- modastack-0.2.0/event-server/src/deployment-session.ts +166 -0
- modastack-0.2.0/event-server/src/index.ts +383 -0
- modastack-0.2.0/event-server/test/env.d.ts +3 -0
- modastack-0.2.0/event-server/test/index.spec.ts +72 -0
- modastack-0.2.0/event-server/test/tsconfig.json +8 -0
- modastack-0.2.0/event-server/tsconfig.json +42 -0
- modastack-0.2.0/event-server/vitest.config.mts +11 -0
- modastack-0.2.0/event-server/worker-configuration.d.ts +13720 -0
- modastack-0.2.0/event-server/wrangler.jsonc +28 -0
- modastack-0.2.0/modastack/__init__.py +0 -0
- modastack-0.2.0/modastack/__version__.py +5 -0
- modastack-0.2.0/modastack/board_setup.py +86 -0
- modastack-0.2.0/modastack/browser.py +324 -0
- modastack-0.2.0/modastack/cli.py +2218 -0
- modastack-0.2.0/modastack/config.py +342 -0
- modastack-0.2.0/modastack/doctor.py +70 -0
- modastack-0.2.0/modastack/github_issues.py +166 -0
- modastack-0.2.0/modastack/history.py +370 -0
- modastack-0.2.0/modastack/manager/__init__.py +0 -0
- modastack-0.2.0/modastack/manager/events/__init__.py +6 -0
- modastack-0.2.0/modastack/manager/events/consumer.py +284 -0
- modastack-0.2.0/modastack/manager/events/event_client.py +429 -0
- modastack-0.2.0/modastack/manager/events/slack_responder.py +91 -0
- modastack-0.2.0/modastack/manager/session.py +430 -0
- modastack-0.2.0/modastack/monitors/__init__.py +20 -0
- modastack-0.2.0/modastack/monitors/checks.py +144 -0
- modastack-0.2.0/modastack/monitors/registry.py +183 -0
- modastack-0.2.0/modastack/monitors/scheduler.py +214 -0
- modastack-0.2.0/modastack/monitors/schema.py +109 -0
- modastack-0.2.0/modastack/prompts/__init__.py +14 -0
- modastack-0.2.0/modastack/prompts/agent_base.md +40 -0
- modastack-0.2.0/modastack/prompts/agents/engineer.md +492 -0
- modastack-0.2.0/modastack/prompts/manager_base.md +172 -0
- modastack-0.2.0/modastack/prompts/manager_engineering.md +152 -0
- modastack-0.2.0/modastack/prompts/resolver.py +40 -0
- modastack-0.2.0/modastack/relay.py +83 -0
- modastack-0.2.0/modastack/scanner.py +47 -0
- modastack-0.2.0/modastack/sdk.py +219 -0
- modastack-0.2.0/modastack/session.py +277 -0
- modastack-0.2.0/modastack/setup.py +141 -0
- modastack-0.2.0/modastack/subagent.py +1065 -0
- modastack-0.2.0/modastack/tmux.py +287 -0
- modastack-0.2.0/modastack/workflow/__init__.py +8 -0
- modastack-0.2.0/modastack/workflow/orchestrator.py +571 -0
- modastack-0.2.0/modastack/workflow/schema.py +91 -0
- modastack-0.2.0/modastack/workflow/state.py +187 -0
- modastack-0.2.0/modastack/workflow/triggers.py +89 -0
- modastack-0.2.0/modastack/workflow/variables.py +201 -0
- modastack-0.2.0/monitors/defaults.yaml +16 -0
- modastack-0.2.0/pack.json +60 -0
- modastack-0.2.0/pyproject.toml +55 -0
- modastack-0.2.0/scripts/test_executor_coding.py +175 -0
- modastack-0.2.0/scripts/test_executor_live.py +152 -0
- modastack-0.2.0/specs/agd-9-review-readme.md +84 -0
- modastack-0.2.0/specs/mds-21-stall-detection.md +223 -0
- modastack-0.2.0/specs/mds-22-remote-deployment.md +495 -0
- modastack-0.2.0/specs/mds-24-web-dashboard.md +214 -0
- modastack-0.2.0/specs/mds-25-self-updating.md +240 -0
- modastack-0.2.0/tests/__init__.py +0 -0
- modastack-0.2.0/tests/integration/__init__.py +0 -0
- modastack-0.2.0/tests/integration/test_agent_cli.py +67 -0
- modastack-0.2.0/tests/integration/test_consult.py +116 -0
- modastack-0.2.0/tests/integration/test_consumer_startup.py +110 -0
- modastack-0.2.0/tests/integration/test_event_pipeline.py +220 -0
- modastack-0.2.0/tests/integration/test_full_lifecycle.py +318 -0
- modastack-0.2.0/tests/integration/test_inject.py +162 -0
- modastack-0.2.0/tests/integration/test_orchestrator.py +189 -0
- modastack-0.2.0/tests/integration/test_spawn_background.py +94 -0
- modastack-0.2.0/tests/test_browser.py +241 -0
- modastack-0.2.0/tests/test_cli.py +241 -0
- modastack-0.2.0/tests/test_config.py +153 -0
- modastack-0.2.0/tests/test_consult.py +114 -0
- modastack-0.2.0/tests/test_consumer.py +172 -0
- modastack-0.2.0/tests/test_credentials.py +62 -0
- modastack-0.2.0/tests/test_dashboard.py +221 -0
- modastack-0.2.0/tests/test_event_client.py +250 -0
- modastack-0.2.0/tests/test_github_issues.py +230 -0
- modastack-0.2.0/tests/test_inject.py +158 -0
- modastack-0.2.0/tests/test_inject_capture.py +103 -0
- modastack-0.2.0/tests/test_manager_sdk.py +192 -0
- modastack-0.2.0/tests/test_monitors.py +255 -0
- modastack-0.2.0/tests/test_orchestrator.py +443 -0
- modastack-0.2.0/tests/test_registry.py +128 -0
- modastack-0.2.0/tests/test_relay.py +87 -0
- modastack-0.2.0/tests/test_self_update.py +333 -0
- modastack-0.2.0/tests/test_session.py +97 -0
- modastack-0.2.0/tests/test_session_extended.py +133 -0
- modastack-0.2.0/tests/test_setup.py +97 -0
- modastack-0.2.0/tests/test_slack_reply.py +232 -0
- modastack-0.2.0/tests/test_slack_responder.py +202 -0
- modastack-0.2.0/tests/test_subagent.py +314 -0
- modastack-0.2.0/tests/test_subagent_blocking.py +1210 -0
- modastack-0.2.0/tests/test_triggers.py +217 -0
- modastack-0.2.0/workflows/adhoc.yaml +12 -0
- modastack-0.2.0/workflows/build-failure.yaml +14 -0
- modastack-0.2.0/workflows/examples/content-review.yaml +121 -0
- modastack-0.2.0/workflows/examples/research.yaml +99 -0
- modastack-0.2.0/workflows/issue-lifecycle.yaml +64 -0
- modastack-0.2.0/workflows/pr-feedback.yaml +16 -0
- modastack-0.2.0/workflows/pr-merged.yaml +14 -0
- modastack-0.2.0/workflows/self-update.yaml.disabled +47 -0
- modastack-0.2.0/workflows/stall-recovery.yaml +15 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Write Claude Code hook events to the manager activity log.
|
|
3
|
+
# On Stop: also relay the assistant response to Slack via the relay config.
|
|
4
|
+
python3 -c "
|
|
5
|
+
import sys, json, time, os
|
|
6
|
+
|
|
7
|
+
data = json.load(sys.stdin)
|
|
8
|
+
entry = {
|
|
9
|
+
'event': data['hook_event_name'],
|
|
10
|
+
'ts': time.time(),
|
|
11
|
+
'session_id': data.get('session_id', ''),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Write activity log
|
|
15
|
+
log_dir = os.path.expanduser('~/.modastack/manager')
|
|
16
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
17
|
+
with open(os.path.join(log_dir, 'activity.jsonl'), 'a') as f:
|
|
18
|
+
f.write(json.dumps(entry) + '\n')
|
|
19
|
+
|
|
20
|
+
# On Stop: relay assistant response to Slack only if a Slack message triggered this turn
|
|
21
|
+
if data['hook_event_name'] == 'Stop':
|
|
22
|
+
msg = data.get('last_assistant_message', '')
|
|
23
|
+
marker = os.path.expanduser('~/.modastack/manager/slack_reply_pending')
|
|
24
|
+
if msg and os.path.exists(marker):
|
|
25
|
+
os.unlink(marker)
|
|
26
|
+
try:
|
|
27
|
+
import yaml
|
|
28
|
+
config_path = os.path.expanduser('~/.modastack/config.yaml')
|
|
29
|
+
with open(config_path) as cf:
|
|
30
|
+
config = yaml.safe_load(cf) or {}
|
|
31
|
+
slack = config.get('slack', {})
|
|
32
|
+
token = slack.get('bot_token', '')
|
|
33
|
+
channel = slack.get('dm_channel', '') or 'D0B51JP1N4C'
|
|
34
|
+
if token:
|
|
35
|
+
import urllib.request, re
|
|
36
|
+
text = msg
|
|
37
|
+
# Markdown → Slack mrkdwn
|
|
38
|
+
text = re.sub(r'^#{1,6}\s+(.+)$', r'*\1*', text, flags=re.MULTILINE)
|
|
39
|
+
text = re.sub(r'\*\*(.+?)\*\*', r'*\1*', text)
|
|
40
|
+
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<\2|\1>', text)
|
|
41
|
+
if len(text) > 3000:
|
|
42
|
+
text = text[:3000] + '\n_(truncated)_'
|
|
43
|
+
payload = json.dumps({'channel': channel, 'text': text}).encode()
|
|
44
|
+
req = urllib.request.Request(
|
|
45
|
+
'https://slack.com/api/chat.postMessage',
|
|
46
|
+
data=payload,
|
|
47
|
+
headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'},
|
|
48
|
+
)
|
|
49
|
+
urllib.request.urlopen(req, timeout=5)
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: modastack project state
|
|
3
|
+
description: Current state and decisions for the modastack project — cron-based dispatch loop with Linear as primary interaction channel
|
|
4
|
+
type: project
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Building a cron-based agent dispatch system at ~/dev/modastack.
|
|
8
|
+
|
|
9
|
+
**Why:** User wants to understand agentic development from first principles by building their own orchestration layer, taking design decisions from OpenClaw and Hermes.
|
|
10
|
+
|
|
11
|
+
**Key decisions made:**
|
|
12
|
+
- Linear + Linear comments as the sole interaction channel (Slack deferred to later)
|
|
13
|
+
- Cron every 1 minute (cheap cycle, just HTTP calls)
|
|
14
|
+
- Python + pip/venv
|
|
15
|
+
- Per-repo .modastack.yaml for portability
|
|
16
|
+
- Named credentials in ~/.modastack/credentials.yaml for multi-workspace
|
|
17
|
+
- Skills discovery layer (auto-detects gstack or any skill pack)
|
|
18
|
+
- BLOCKED state: agent posts question as Linear comment, polls for reply each cycle
|
|
19
|
+
- GitHub repo: underminedsk/modastack (public)
|
|
20
|
+
- GitHub account for this project: underminedsk
|
|
21
|
+
|
|
22
|
+
**How to apply:** When working on this project, use the underminedsk GitHub account. Linear is the primary channel — no Slack integration yet. Keep the architecture simple (cron, JSON state file, no daemon).
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Plan: Centralized Event Server + GitHub App
|
|
2
|
+
|
|
3
|
+
Replace the current polling-and-direct-webhook architecture with a centralized
|
|
4
|
+
event server on Cloudflare Workers. One GitHub App receives all webhook events.
|
|
5
|
+
Modastack deployments connect outbound via WebSocket to subscribe to their repos'
|
|
6
|
+
events, with automatic catch-up on missed events after downtime.
|
|
7
|
+
|
|
8
|
+
## Problem
|
|
9
|
+
|
|
10
|
+
Three problems, one solution:
|
|
11
|
+
|
|
12
|
+
1. **Webhook permissions** — moda-bot can't install webhooks because it lacks admin
|
|
13
|
+
access. A centralized GitHub App gets webhook permissions via installation.
|
|
14
|
+
2. **Event loss during downtime** — when modastack restarts or crashes, all events
|
|
15
|
+
during the gap are lost. The central server buffers events and replays on reconnect.
|
|
16
|
+
3. **Inbound port requirement** — current webhook server requires a public IP with
|
|
17
|
+
open ports. Outbound WebSocket connections work behind NAT/firewalls.
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
GitHub ──webhook──┐
|
|
23
|
+
Linear ──webhook──┤
|
|
24
|
+
Slack ──webhook───┤
|
|
25
|
+
▼
|
|
26
|
+
Cloudflare Worker (event-server)
|
|
27
|
+
│
|
|
28
|
+
├── receives webhooks, assigns sequence IDs
|
|
29
|
+
├── stores events in KV (48h buffer)
|
|
30
|
+
└── Durable Object per deployment
|
|
31
|
+
│
|
|
32
|
+
▼ WebSocket
|
|
33
|
+
modastack deployment
|
|
34
|
+
│
|
|
35
|
+
└── "last_seen: 57" → replays 58, 59, 60... → live stream
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Components
|
|
39
|
+
|
|
40
|
+
### 1. Cloudflare Worker: `moda-event-server`
|
|
41
|
+
|
|
42
|
+
A single Worker with three responsibilities:
|
|
43
|
+
|
|
44
|
+
**Webhook ingestion (HTTP routes):**
|
|
45
|
+
- `POST /webhooks/github` — receives GitHub App webhook events
|
|
46
|
+
- `POST /webhooks/linear` — receives Linear webhook events
|
|
47
|
+
- `POST /webhooks/slack` — receives Slack event API payloads
|
|
48
|
+
|
|
49
|
+
Each incoming event gets:
|
|
50
|
+
- A globally monotonic sequence ID (per-deployment namespace)
|
|
51
|
+
- Stored in KV with TTL of 48 hours
|
|
52
|
+
- Forwarded to the Durable Object for each matching deployment
|
|
53
|
+
|
|
54
|
+
**Deployment registration (HTTP routes):**
|
|
55
|
+
- `POST /deployments` — register a new deployment, returns API key
|
|
56
|
+
- `GET /deployments/:id/subscribe` — upgrade to WebSocket
|
|
57
|
+
|
|
58
|
+
**Event routing logic:**
|
|
59
|
+
- GitHub: route by `installation.id` or `repository.full_name`
|
|
60
|
+
- Linear: route by workspace ID or team key
|
|
61
|
+
- Slack: route by workspace/bot token identifier
|
|
62
|
+
|
|
63
|
+
### 2. Durable Object: `DeploymentSession`
|
|
64
|
+
|
|
65
|
+
One DO per registered deployment. Responsibilities:
|
|
66
|
+
|
|
67
|
+
- **Subscription state** — which repos/orgs/Linear teams this deployment cares about
|
|
68
|
+
- **Cursor tracking** — last-acknowledged event ID per deployment
|
|
69
|
+
- **WebSocket management** — holds the live connection to the deployment
|
|
70
|
+
- **Replay on reconnect** — when a deployment reconnects with `last_seen: N`,
|
|
71
|
+
fetch events N+1..latest from KV and send them in order before switching to live
|
|
72
|
+
|
|
73
|
+
**Event delivery contract:**
|
|
74
|
+
- Events delivered in sequence order, no gaps
|
|
75
|
+
- Deployment sends `{"ack": 73}` to advance its cursor
|
|
76
|
+
- If WebSocket is disconnected, events buffer in KV (up to 48h)
|
|
77
|
+
- On reconnect, full replay from cursor position
|
|
78
|
+
|
|
79
|
+
### 3. GitHub App: `Modastack` (centralized)
|
|
80
|
+
|
|
81
|
+
One GitHub App registered under the moda-labs org:
|
|
82
|
+
|
|
83
|
+
**Permissions:**
|
|
84
|
+
- `contents: write` — read/write repo contents (for PRs, branches)
|
|
85
|
+
- `issues: write` — manage issues and labels
|
|
86
|
+
- `pull_requests: write` — create/update PRs, request reviews
|
|
87
|
+
- `checks: read` — read CI status
|
|
88
|
+
- Webhook events: `issues`, `issue_comment`, `pull_request`,
|
|
89
|
+
`pull_request_review`, `check_run`, `workflow_run`
|
|
90
|
+
|
|
91
|
+
**Webhook URL:** `https://moda-events.<domain>/webhooks/github`
|
|
92
|
+
|
|
93
|
+
**Installation flow:**
|
|
94
|
+
1. User runs `modastack setup`
|
|
95
|
+
2. Opens `https://github.com/apps/modastack/installations/new`
|
|
96
|
+
3. User selects org and repos to grant access
|
|
97
|
+
4. GitHub sends `installation` webhook to the event server
|
|
98
|
+
5. Event server auto-registers the deployment's subscription
|
|
99
|
+
|
|
100
|
+
**Token generation stays local.** Each modastack deployment has a copy of the
|
|
101
|
+
app's private key (provisioned during `modastack setup`). It generates its own
|
|
102
|
+
installation tokens via JWT for GitHub API calls (creating PRs, managing issues).
|
|
103
|
+
The central server only handles webhook receipt and forwarding — it never needs
|
|
104
|
+
to call GitHub's API on behalf of deployments.
|
|
105
|
+
|
|
106
|
+
### 4. Modastack client changes
|
|
107
|
+
|
|
108
|
+
Replace the current webhook server + polling architecture in `modastack/manager/events/`
|
|
109
|
+
with an outbound WebSocket client:
|
|
110
|
+
|
|
111
|
+
**New file: `modastack/manager/events/event_client.py`**
|
|
112
|
+
- Connects to `wss://moda-events.<domain>/deployments/:id/subscribe`
|
|
113
|
+
- Authenticates with deployment API key
|
|
114
|
+
- Sends `last_seen` cursor on connect (persisted in `~/.modastack/cursor.json`)
|
|
115
|
+
- Receives events, feeds them into the existing event bus
|
|
116
|
+
- Acks events after successful processing
|
|
117
|
+
- Auto-reconnects with exponential backoff
|
|
118
|
+
|
|
119
|
+
**Modified: `modastack/manager/events/consumer.py`**
|
|
120
|
+
- `run()` starts the WebSocket client instead of (or alongside) pollers
|
|
121
|
+
- Events from the WebSocket feed into the same bus/batching pipeline
|
|
122
|
+
- Existing manager session injection unchanged
|
|
123
|
+
|
|
124
|
+
**Removed (or made optional):**
|
|
125
|
+
- `webhook_server.py` — no longer needed; events come via WebSocket
|
|
126
|
+
- GitHub/Linear pollers in `pollers.py` — replaced by webhooks through the
|
|
127
|
+
central server. Slack Socket Mode can stay as-is or move to central server.
|
|
128
|
+
|
|
129
|
+
**Kept as fallback:**
|
|
130
|
+
- `gh` CLI auth for GitHub API calls (creating PRs, etc.)
|
|
131
|
+
- Local polling mode via `modastack start` (no `--webhooks`) for users who
|
|
132
|
+
don't want the central server
|
|
133
|
+
|
|
134
|
+
## KV Schema
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
events:{deployment_id}:{sequence_id} → {event JSON} TTL: 48h
|
|
138
|
+
cursor:{deployment_id} → {last_acked_id}
|
|
139
|
+
deployments:{api_key} → {deployment config}
|
|
140
|
+
subscriptions:{owner/repo} → [deployment_id, ...]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Setup Flow (updated)
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
modastack setup <repo>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
1. Detect GitHub org from remote URL
|
|
150
|
+
2. Check if Modastack GitHub App is installed on that org
|
|
151
|
+
- If not: open browser to install URL, wait for confirmation
|
|
152
|
+
3. Register deployment with central event server (gets API key)
|
|
153
|
+
4. Save API key + deployment ID to `~/.modastack/config.yaml`
|
|
154
|
+
5. Configure repo subscriptions on the event server
|
|
155
|
+
6. Existing setup steps: generate .modastack.yaml, install skills, hooks
|
|
156
|
+
|
|
157
|
+
## Auth Model
|
|
158
|
+
|
|
159
|
+
| Action | Auth method |
|
|
160
|
+
|---|---|
|
|
161
|
+
| Receive webhook events | Central server → deployment via WebSocket (API key) |
|
|
162
|
+
| Create PRs, manage issues | Deployment generates installation token locally (app private key) |
|
|
163
|
+
| Register deployment | One-time API key from central server |
|
|
164
|
+
| Install GitHub App on org | User clicks install in browser (GitHub handles auth) |
|
|
165
|
+
|
|
166
|
+
## Key Design Decisions
|
|
167
|
+
|
|
168
|
+
- **Central server is dumb relay.** It receives, stores, and forwards. It never
|
|
169
|
+
calls external APIs. All GitHub/Linear API calls happen on the deployment side.
|
|
170
|
+
This keeps the Worker simple and stateless (except for KV/DO).
|
|
171
|
+
- **48-hour event buffer.** Covers weekends, maintenance windows, EC2 crashes.
|
|
172
|
+
After 48h, the deployment falls back to polling-based reconciliation (scan
|
|
173
|
+
Linear/GitHub for current state), which already exists.
|
|
174
|
+
- **Private key distributed to deployments.** Each deployment gets a copy during
|
|
175
|
+
setup. This means deployments can make GitHub API calls independently. The
|
|
176
|
+
alternative (central server proxies all API calls) adds latency and complexity.
|
|
177
|
+
- **Sequence IDs per deployment namespace.** Not global. Each deployment gets its
|
|
178
|
+
own monotonic counter. Simpler, no cross-deployment ordering needed.
|
|
179
|
+
- **`gh` CLI stays as fallback** for GitHub API calls. Existing users don't break.
|
|
180
|
+
|
|
181
|
+
## What This Replaces
|
|
182
|
+
|
|
183
|
+
| Current | New |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `webhook_server.py` (inbound HTTP) | WebSocket client (outbound) |
|
|
186
|
+
| GitHub polling in `pollers.py` | GitHub webhooks via central server |
|
|
187
|
+
| Linear polling in `pollers.py` | Linear webhooks via central server |
|
|
188
|
+
| `moda-bot` user account + PAT | Modastack GitHub App + installation tokens |
|
|
189
|
+
| Events lost on restart | 48h buffer with replay |
|
|
190
|
+
| Public IP required | Works behind NAT |
|
|
191
|
+
|
|
192
|
+
## Implementation Sequence
|
|
193
|
+
|
|
194
|
+
### Phase 1: Central event server (Cloudflare Worker)
|
|
195
|
+
1. Scaffold Worker project with Durable Objects + KV
|
|
196
|
+
2. GitHub webhook ingestion endpoint
|
|
197
|
+
3. Deployment registration + API key management
|
|
198
|
+
4. Durable Object: WebSocket handling, cursor tracking, replay
|
|
199
|
+
5. Deploy to Cloudflare
|
|
200
|
+
|
|
201
|
+
### Phase 2: GitHub App
|
|
202
|
+
1. Register Modastack app under moda-labs org
|
|
203
|
+
2. Configure permissions and webhook URL → central server
|
|
204
|
+
3. Install on moda-labs org
|
|
205
|
+
4. Test: webhook events arrive at central server
|
|
206
|
+
|
|
207
|
+
### Phase 3: Modastack client
|
|
208
|
+
1. New `event_client.py` — WebSocket client with reconnect + replay
|
|
209
|
+
2. Wire into consumer.py event loop
|
|
210
|
+
3. Update `modastack setup` to register with central server
|
|
211
|
+
4. Update `modastack start` to use WebSocket mode by default
|
|
212
|
+
5. Keep polling mode as `modastack start --local` fallback
|
|
213
|
+
|
|
214
|
+
### Phase 4: Linear + Slack integration
|
|
215
|
+
1. Add Linear webhook ingestion to central server
|
|
216
|
+
2. Add Slack event API endpoint (or keep Socket Mode local)
|
|
217
|
+
3. Route Linear events to deployments by team key
|
|
218
|
+
4. Remove polling loops from modastack client
|
|
219
|
+
|
|
220
|
+
## Testing
|
|
221
|
+
|
|
222
|
+
- Unit tests: event serialization, cursor logic, subscription matching
|
|
223
|
+
- Integration test: Worker receives webhook → DO buffers → client replays
|
|
224
|
+
- Miniflare for local Worker testing
|
|
225
|
+
- Manual end-to-end: install app, push to repo, verify event arrives at deployment
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Deploy Event Server
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- "event-server/**"
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
deploy:
|
|
12
|
+
name: Deploy Worker
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
defaults:
|
|
15
|
+
run:
|
|
16
|
+
working-directory: event-server
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v5
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-node@v5
|
|
21
|
+
with:
|
|
22
|
+
node-version: "22"
|
|
23
|
+
cache: "npm"
|
|
24
|
+
cache-dependency-path: event-server/package-lock.json
|
|
25
|
+
|
|
26
|
+
- run: npm ci
|
|
27
|
+
|
|
28
|
+
- run: npx vitest run
|
|
29
|
+
|
|
30
|
+
- name: Deploy to Cloudflare
|
|
31
|
+
uses: cloudflare/wrangler-action@v4
|
|
32
|
+
with:
|
|
33
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
34
|
+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
35
|
+
workingDirectory: event-server
|
|
36
|
+
command: deploy
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: pypi
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
|
|
22
|
+
- name: Install build tools
|
|
23
|
+
run: pip install build
|
|
24
|
+
|
|
25
|
+
- name: Build package
|
|
26
|
+
run: python -m build
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
30
|
+
|
|
31
|
+
- name: Extract version from tag
|
|
32
|
+
id: version
|
|
33
|
+
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
|
34
|
+
|
|
35
|
+
- name: Trigger Homebrew formula update
|
|
36
|
+
uses: peter-evans/repository-dispatch@v3
|
|
37
|
+
with:
|
|
38
|
+
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
|
39
|
+
repository: moda-labs/homebrew-modastack
|
|
40
|
+
event-type: pypi-release
|
|
41
|
+
client-payload: '{"version": "${{ steps.version.outputs.version }}"}'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
task_tracking:
|
|
2
|
+
system: github-issues
|
|
3
|
+
project: ENG
|
|
4
|
+
trigger_labels:
|
|
5
|
+
- agent
|
|
6
|
+
skip_labels:
|
|
7
|
+
- blocked
|
|
8
|
+
- human-only
|
|
9
|
+
complexity:
|
|
10
|
+
trivial: label:typo OR label:docs OR label:config
|
|
11
|
+
medium: default
|
|
12
|
+
heavy: label:feature OR label:refactor OR estimate>3
|
|
13
|
+
agent:
|
|
14
|
+
tool: claude
|
|
15
|
+
skills:
|
|
16
|
+
- review
|
|
17
|
+
- ship
|
|
18
|
+
max_parallel: 2
|
|
19
|
+
verify:
|
|
20
|
+
test_command: pytest
|
|
21
|
+
review_required: true
|
|
22
|
+
auto_merge: true
|
|
23
|
+
credentials: modastack
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Auto-resolve merge conflicts: `monitor/pr.conflict_detected` now triggers the manager to auto-spawn an engineer (instead of just noting it) that follows a new `merge-conflict` skill — merge the base branch, resolve conflicts honoring both sides, verify build/tests, and push. If the conflict needs a human decision, the engineer comments on the PR and exits non-zero, and the manager escalates to the human via Slack (#117)
|
|
7
|
+
|
|
8
|
+
## 0.4.1 — 2026-06-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Engineer lifecycle events: `modastack spawn` and workflow-managed engineers now emit `engineer/session.started`, `engineer/session.completed`, and `engineer/session.failed` to the event bus, so the manager can narrate engineer activity without polling (#103)
|
|
12
|
+
- Events post fire-and-forget over HTTP (`POST /api/event`) on a daemon thread, reusing the same path monitor checks use, so delivery never blocks or breaks an engineer run
|
|
13
|
+
- Manager event formatter now surfaces `phase`, `duration`, `summary`, and `error` fields from lifecycle events
|
|
14
|
+
|
|
15
|
+
## 0.4.0 — 2026-06-01
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Background monitoring system: scheduled polling tasks that fill webhook gaps by detecting conditions and injecting synthetic events into the manager's event stream (#100)
|
|
19
|
+
- Three-tier monitor storage (built-in `monitors/defaults.yaml` → user `~/.modastack/monitors.yaml` → repo `.modastack.yaml`), merged with later tiers overriding by `name` and repo-level `enabled: false` opt-out
|
|
20
|
+
- Built-in default monitors: PR conflict check (15m) and stale-PR check (1h), both working out of the box
|
|
21
|
+
- `modastack monitor add/list/pause/remove` CLI for managing monitors across tiers
|
|
22
|
+
- Native check runners (`pr_conflicts`, `stale_prs`) with per-condition deduplication; description-only monitors fall back to manager interpretation
|
|
23
|
+
|
|
24
|
+
## 0.3.3 — 2026-05-27
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Documentation: composable skills principle, workflow resolution chain (repo > user > default), and event normalization table (GitHub Issues + Linear to task.* format)
|
|
28
|
+
|
|
29
|
+
## 0.3.2.1 — 2026-05-27
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- README phase routing table and handoff example now use the correct `implement_complete` phase name (was `implementation_complete`)
|
|
33
|
+
|
|
34
|
+
## 0.3.2 — 2026-05-26
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Mermaid flowchart diagrams in README: event flow, issue lifecycle, skill composition, and deploy pipeline
|
|
38
|
+
|
|
39
|
+
## 0.3.1 — 2026-05-26
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- CLI help text for `workflow` and `history` subcommands now includes descriptions and usage examples
|
|
43
|
+
|
|
44
|
+
## 0.2.2 — 2026-05-23
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- Stall detection: heartbeat tracking via output hashing detects sessions idle >5 min (nudge) or >10 min (kill)
|
|
48
|
+
- Permission prompt detection: sessions blocked on interactive approval are identified and reported
|
|
49
|
+
- Process liveness checks: dead claude processes inside live tmux sessions emit `worker.process_dead`
|
|
50
|
+
- Auto-routing: manager prompt now routes engineers to the next phase based on handoff state
|
|
51
|
+
|
|
52
|
+
## 0.2.1 — 2026-05-23
|
|
53
|
+
|
|
54
|
+
- Self-updating: version check poller, Slack notification, user-approved update
|
|
55
|
+
- Slack threading fix — conversations inline, only proactive updates threaded
|
|
56
|
+
|
|
57
|
+
## 0.2.0 — 2026-05-20
|
|
58
|
+
|
|
59
|
+
- Event-driven architecture with persistent manager session
|
|
60
|
+
- Linear + GitHub Issues task tracking
|
|
61
|
+
- Slack Socket Mode for real-time events
|
|
62
|
+
- Engineer lifecycle: pickup, spec, implement, prepare-pr, feedback
|
|
63
|
+
- Orphan session detection
|